7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby の Resolv クラスの実装を読む

Last updated at Posted at 2022-03-21

Rubyの非同期タスクスケジューラーの#address_resolve を実装したい。
要は、DNSの名前解決を非同期に行いたい。
このために、ruby/resolv の実装を読んだのでメモる。

Ruby のDNS名前解決について

Ruby標準ライブラリを使用した名前解決の方法は複数ある。

  • Resolv.getaddress "www.ruby-lang.org"
    • アプリケーションレイヤーでDNS full service resolverにリクエストを投げている
    • 並列可能?? (->内部実装読む)
  • Socket.getaddrinfo "google.com", 443
    • 多分libc のgetaddrinfo を呼び出している
    • 並列不可能なはず

前者のResolvを読む。
(これはfull service resolverではなくstub resolverだという認識をしている)

Resolv のドキュメントを読む

公式ドキュメントを読む。

要約
リゾルバを表すクラスです。このクラス自体は実際には名前解決をせず、 Resolv.new で与えられたリゾルバに順に問合せることしかしません。
このクラスのクラスメソッドで名前解決をした場合には、内部で /etc/hosts, DNS の順に問合せます。
順に問合せる過程で、あるリゾルバが1個以上の結果を返した場合、それ以降のリゾルバには問い合わせをしません。

👉 おおむね予想通りの挙動をしている

定義されているメソッドは以下。
image.png

ruby/resolv のREADME

Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can handle multiple DNS requests concurrently without blocking the entire Ruby interpreter.
https://github.com/ruby/resolv

pryコンソールで試してみる

image.png
mutexを内部に持っているので、マルチスレッドプログラミングを意識した作りになってそう。
image.png

コードを読む

以下知りたい

  • Resolv は「multiple DNS requests concurrently without blocking the entire Ruby interpreter」とある が、これはシングルスレッドで並行処理なのか?マルチスレッドを切ってGVLに引っかからないように並列処理しているのか?
  • Resolv::Hosts に関して
    • /etc/hosts を見に行っているという予測は正しいか?
    • そうであれば、毎回ファイルへの読み込みが発生しているのか?一度読んだらメモリ上に保存しているのか?
  • Resolv::DNS に関して
    • DNS full service サーバーのアドレスをどうやって取得しているのか? (/etc/resolv.confとか/etc/nsswitch.confとか?)
    • そうであれば、毎回ファイルへの読み込みが発生しているのか?
    • それらのサーバーにはtcp, udpそれぞれのport 53を叩きに行っているという予測は正しいか?
    • このリクエストの際には新しいスレッドが切られているのか?
    • このリクエストの際にIO blockingが発生しているという認識は正しいか? (->ここでタスク切り替えできそう)

実際に読んだ

Resolv は「multiple DNS requests concurrently without blocking the entire Ruby interpreter」とある が、これはシングルスレッドで並行処理なのか?マルチスレッドを切ってGVLに引っかからないように並列処理しているのか?

マルチスレッドを切っているわけではなさそう。
grep検索して見た限り、Thread::Mutexは 以下のメソッドで使われいてる。

  • Resolv::Hosts#initialize, #lazy_initialize
  • Resolv::DNS#initialize, #lazy_initialize, #close
  • Resolv::DNS::Requester::[Un]ConnectedUDP#initialize, lazy_initialize, close
  • Resolv::DNS::Config#initizliae, lazy_initialize, close

image.png

concurrenlyにリクエストができるというのは、full service serverが複数あった際に、それらを叩きに行くのが並行だ、ということ。複数の名前解決を行いたい際に、それらが並行に解決される、というわけではない。
ref. https://github.com/ruby/resolv/blob/55e42221d4fafb5a73358d5defdde809157049e7/lib/resolv.rb#L679

Resolv::Hosts に関して /etc/hosts を見に行っているという予測は正しいか?

正しい。
デフォルトでこのファイルを読んでいる。別のファイルを指定することもできる。

src. https://github.com/ruby/resolv/blob/55e42221d4fafb5a73358d5defdde809157049e7/lib/resolv.rb#L169-L176

そうであれば、毎回ファイルへの読み込みが発生しているのか?一度読んだらメモリ上に保存しているのか?

一度読んだらメモリ上に保持している。

Resolvインスタンスの初期化ではなく、そのインスタンスで初回のns lookup時に、/etc/hostsを読んで、@addr2name (ハッシュ)を初期化している。

なお、この初期化時には、mutexでロックをしている。
つまり別スレッドから同時にResolvインスタンスを初回使用したとしても、それぞれで同時にファイルを読みに行くことはない。
ref. https://github.com/ruby/resolv/blob/55e42221d4fafb5a73358d5defdde809157049e7/lib/resolv.rb#L187-L213

pryで確認

image.png

(👉 Resolv::DNS::Config でも同様のlazy loadingが行われている(後述))

DNS full service サーバーのアドレスをどうやって取得しているのか? (/etc/resolv.confとか/etc/nsswitch.confとか?)

/etc/resolv.confを読んでいる。
ref. https://github.com/ruby/resolv/blob/55e42221d4fafb5a73358d5defdde809157049e7/lib/resolv.rb#L988-L1001

ただし、/etc/nsswitch.conf は現状読んでいない。これはバグだと認識されている。
ref. https://github.com/ruby/resolv/blob/55e42221d4fafb5a73358d5defdde809157049e7/lib/resolv.rb#L33-L36

そうであれば、毎回ファイルへの読み込みが発生しているのか?

No.
Resolv::DNS::Config#lazy_initialize で初回のみ初期化され、その後は@nameserver_port(hash map)に保持される。
ref. https://github.com/ruby/resolv/blob/55e42221d4fafb5a73358d5defdde809157049e7/lib/resolv.rb#L1003-L1071

これは、Resolv::DNS#lazy_initialize にて、初期化されている。(lazy_initializeが二重にネストされている??)
ref. https://github.com/ruby/resolv/blob/55e42221d4fafb5a73358d5defdde809157049e7/lib/resolv.rb#L350-L358

pryで確認

image.png

👉 Resolv::DNS::Config でも同様のlazy loadingが行われている

それらのサーバーにはtcp, udpそれぞれのport 53を叩きに行っているという予測は正しいか?

正しくない。

まず、udp接続のための udp_requester を作る。
これに成功した場合はudpプロトコルを、失敗した場合にはtcpプロトコルを使用しているようだ。

関連箇所

image.png

image.png

このリクエストの際には新しいスレッドが切られているのか?

新しいスレッドは作られていなさそう

関連箇所

image.png

このリクエストの際にIO blockingが発生しているという認識は正しいか? (->ここでタスク切り替えできそう)

正しい。
IO.select(@socks, nil, nil, timeout)でIO待ちしている。ref

Socket#send時

ブロックする。
Socket#sendはブロックする。

該当箇所

image.png

non blocking send の参考記事

non blocking sendに関しては以下の記事が詳しい。
https://stackoverflow.com/a/19400029

返事待ち時

ブロックする。
IO.select(@socks, nil, nil, timeout) でIO待ちしている。ref
(ただし、これは IO#read を読んでいるわけではないので、そのスレッドでFiberSchedulerが定義されても、モンキーパッチされない)

Requester#requestのコード

image.png

Ref.

7
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?