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

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 のドキュメントを読む

### 公式ドキュメントを読む。
https://docs.ruby-lang.org/ja/3.1/class/Resolv.html

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

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


定義されているメソッドは以下。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/fc4bd48d-7211-6446-6211-d69d1ba7b7aa.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/a5fffd6d-187f-92e9-936c-1bd17d24e38e.png)
mutexを内部に持っているので、マルチスレッドプログラミングを意識した作りになってそう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/12f339d1-a613-9d60-5fce-5fd05cec0d12.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/f7d994e9-1cc0-5f1b-f5ce-8ee3f81f8833.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


<details>
<summary>pryで確認</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/9458a268-978c-7a4b-f3b3-23132d0dff87.png)

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

</details>


> 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


<details>
<summary>pryで確認</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/9458a268-978c-7a4b-f3b3-23132d0dff87.png)

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

</details>


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


正しくない。

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


<details>
<summary>関連箇所</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/dd7f1a0d-4d99-4546-7934-9ad2c604ba89.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/698eba5a-0d3b-4965-9fc8-96a27480d13d.png)

</details>


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


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

<details>
<summary>関連箇所</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/698eba5a-0d3b-4965-9fc8-96a27480d13d.png)

</details>

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

正しい。
IO.select(@socks, nil, nil, timeout)でIO待ちしている。[ref](https://github.com/ruby/resolv/blob/261f04905860725de3920911dafe01eb8afa97f1/lib/resolv.rb#L679)


#### Socket#send時

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

<details>
<summary>該当箇所</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/880e3c95-4794-adf7-1a95-02cc953688cc.png)

</details>



<details>
<summary>non blocking send の参考記事</summary>

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

</details>



#### 返事待ち時

ブロックする。
IO.select(@socks, nil, nil, timeout) でIO待ちしている。[ref](https://github.com/ruby/resolv/blob/261f04905860725de3920911dafe01eb8afa97f1/lib/resolv.rb#L679)
(ただし、これは IO#read を読んでいるわけではないので、そのスレッドでFiberSchedulerが定義されても、モンキーパッチされない)


<details>
<summary>Requester#requestのコード</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534442/29baeccf-0206-70e7-ed71-6706a070aee1.png)


</details>






## Ref.

- https://zwischenzugs.com/2018/06/08/anatomy-of-a-linux-dns-lookup-part-i/
-