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個以上の結果を返した場合、それ以降のリゾルバには問い合わせをしません。
👉 おおむね予想通りの挙動をしている
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コンソールで試してみる
mutexを内部に持っているので、マルチスレッドプログラミングを意識した作りになってそう。
コードを読む
以下知りたい
- 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が発生しているという認識は正しいか? (->ここでタスク切り替えできそう)
-
DNS full service サーバーのアドレスをどうやって取得しているのか? (
実際に読んだ
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
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
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
それらのサーバーにはtcp, udpそれぞれのport 53を叩きに行っているという予測は正しいか?
正しくない。
まず、udp接続のための udp_requester を作る。
これに成功した場合はudpプロトコルを、失敗した場合にはtcpプロトコルを使用しているようだ。
このリクエストの際には新しいスレッドが切られているのか?
新しいスレッドは作られていなさそう
このリクエストの際にIO blockingが発生しているという認識は正しいか? (->ここでタスク切り替えできそう)
正しい。
IO.select(@socks, nil, nil, timeout)でIO待ちしている。ref
Socket#send時
ブロックする。
Socket#sendはブロックする。
non blocking send の参考記事
non blocking sendに関しては以下の記事が詳しい。
https://stackoverflow.com/a/19400029
返事待ち時
ブロックする。
IO.select(@socks, nil, nil, timeout) でIO待ちしている。ref
(ただし、これは IO#read を読んでいるわけではないので、そのスレッドでFiberSchedulerが定義されても、モンキーパッチされない)