[インデックス 17246] ファイルの概要
このコミットは、Go言語の標準ライブラリである net
パッケージ内のDNSクライアントの実装、特に非cgo(純粋なGo)環境におけるDNS問い合わせの処理を簡素化し、TCPフォールバック時のバッファ割り当てを最適化することを目的としています。変更は src/pkg/net/dnsclient_unix.go
ファイルに集中しており、主に exchange
関数内のロジックが修正されています。
コミット
commit 2eb7c6ec8a34c23466e40946d0e9ea1574b0006a
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Thu Aug 15 05:08:08 2013 +0900
net: simplify non-cgo DNS exchange
Also does less buffer allocation in case of TCP fallback.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12925043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2eb7c6ec8a34c23466e40946d0e9ea1574b0006a
元コミット内容
net: simplify non-cgo DNS exchange
Also does less buffer allocation in case of TCP fallback.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/12925043
変更の背景
Go言語の net
パッケージは、ネットワーク通信の基盤を提供し、DNS(Domain Name System)の名前解決機能もその一部として含まれています。DNS問い合わせは通常UDPで行われますが、応答が大きすぎる場合や、応答がない場合にはTCPにフォールバックすることがあります。
このコミット以前の net
パッケージのDNSクライアント実装には、以下の課題があったと考えられます。
useTCP
フラグの冗長な判定:exchange
関数内で、引数として渡されるConn
インターフェースの実体が*UDPConn
なのか*TCPConn
なのかをswitch
文で判定し、useTCP
というブール値を設定していました。これは、型アサーションを直接利用することでより簡潔に表現できる可能性がありました。- TCPフォールバック時のバッファ再割り当ての非効率性: DNSの応答メッセージは、UDPでは512バイトの制限がありますが、TCPではより大きなサイズになることがあります。TCPにフォールバックした場合、最初に読み込んだヘッダ情報からメッセージ全体の長さを判断し、その長さに合わせて再度バッファを割り当て直していました。この再割り当ては、特に大きなDNS応答を頻繁に処理する場合に、メモリ割り当てのオーバーヘッドやガベージコレクションの負荷を増大させる可能性がありました。
これらの課題に対処し、コードの可読性と実行効率を向上させることが、このコミットの背景にある目的です。
前提知識の解説
DNS (Domain Name System)
DNSは、インターネット上のコンピュータやサービスを識別するためのドメイン名とIPアドレスを対応させる分散型データベースシステムです。ユーザーがウェブサイトにアクセスする際、ブラウザはまずDNSを使ってドメイン名(例: www.example.com
)を対応するIPアドレスに変換します。
DNS問い合わせのプロトコル (UDP vs TCP)
- UDP (User Datagram Protocol): DNS問い合わせのほとんどはUDPポート53を使用します。UDPはコネクションレスで高速ですが、信頼性が低く、メッセージサイズに制限(通常512バイト)があります。
- TCP (Transmission Control Protocol): DNS応答が512バイトを超える場合(例: DNSSECの応答やゾーン転送)、またはUDPでの問い合わせが失敗した場合、DNSクライアントはTCPポート53にフォールバックして問い合わせを行います。TCPはコネクション指向で信頼性が高く、より大きなメッセージを扱うことができます。
Go言語の net
パッケージ
Go言語の net
パッケージは、ネットワークI/Oのプリミティブを提供します。これには、TCP/UDPソケットの操作、IPアドレスの解決、DNSルックアップなどが含まれます。
Conn
インターフェース
net
パッケージでは、ネットワーク接続を表すために Conn
インターフェースが定義されています。このインターフェースは Read
、Write
、Close
などの基本的なメソッドを提供し、具体的な実装(例: *UDPConn
や *TCPConn
)はこれらのメソッドをそれぞれのプロトコルに合わせて実装します。
型アサーション (value.(type)
)
Go言語では、インターフェース型の変数が保持している具体的な型を動的に調べるために型アサーションを使用します。value.(type)
は switch
ステートメント内で使用され、インターフェース変数の動的な型に基づいて異なる処理を行うことができます。また、value.(ConcreteType)
の形式で、インターフェース変数が特定の具体的な型であるかどうかをチェックし、その型に変換することもできます。この際、ok
という2番目の戻り値で変換が成功したかどうかが示されます。
技術的詳細
このコミットは、net
パッケージの dnsclient_unix.go
ファイル内の exchange
関数に焦点を当てています。この関数は、DNSクエリを送信し、応答を受信する主要なロジックを含んでいます。
1. useTCP
フラグの判定の簡素化
変更前は、Conn
インターフェースの具体的な型を switch
文で判定し、useTCP
というブール変数を設定していました。
// 変更前
var useTCP bool
switch c.(type) {
case *UDPConn:
useTCP = false
case *TCPConn:
useTCP = true
}
このコードは、c
が *TCPConn
型である場合にのみ useTCP
を true
に設定し、それ以外の場合は false
のままにするという意図を持っています。このロジックは、Goの型アサーションのセカンドリターン値(ok
)を利用することで、より簡潔に表現できます。
// 変更後
_, useTCP := c.(*TCPConn)
この変更により、c
が *TCPConn
型であれば useTCP
は true
に、そうでなければ false
に設定されます。これにより、コードの行数が減り、意図がより明確になります。
2. TCPフォールバック時のバッファ割り当ての最適化
DNSのTCP問い合わせでは、応答メッセージの先頭2バイトがメッセージ全体の長さを表します。変更前は、この2バイトを読み込んだ後、その長さに合わせて新しいバッファを make
で再割り当てしていました。
// 変更前 (TCPの場合のバッファ処理)
// ...
buf = make([]byte, uint16(buf[0])<<8+uint16(buf[1])) // 新しいバッファを割り当て
n, err = io.ReadFull(c, buf) // 新しいバッファに読み込み
// ...
このアプローチの問題点は、buf
が既に存在しているにもかかわらず、メッセージ長が判明した時点で常に新しいバッファを割り当ててしまうことです。もし既存の buf
の容量が十分であれば、再割り当ては不要であり、非効率です。
変更後は、まずメッセージ長 mlen
を計算し、既存の buf
の容量が mlen
よりも小さい場合にのみ新しいバッファを割り当て直すように改善されています。
// 変更後 (TCPの場合のバッファ処理)
// ...
mlen := int(buf[0])<<8 | int(buf[1]) // メッセージ長を計算
if mlen > len(buf) { // 既存のバッファ容量が不足している場合のみ
buf = make([]byte, mlen) // 新しいバッファを割り当て
}
n, err = io.ReadFull(c, buf[:mlen]) // 既存または新しいバッファの適切なスライスに読み込み
// ...
さらに、io.ReadFull(c, buf)
ではなく io.ReadFull(c, buf[:mlen])
を使用することで、読み込むバイト数を明示的に mlen
に制限しています。これにより、バッファのサイズがメッセージ長よりも大きい場合でも、必要なデータだけを正確に読み込むことができます。
3. バッファスライスの簡素化
最後に、読み込みが完了した後のバッファのスライス処理も簡素化されています。
// 変更前
buf = buf[0:n]
// 変更後
buf = buf[:n]
これは機能的には同じですが、0:
を省略することでよりGoらしい簡潔な表現になっています。
これらの変更により、DNSクライアントのコードはより簡潔になり、特にTCPフォールバック時のメモリ割り当ての効率が向上し、ガベージコレクションの負荷が軽減される可能性があります。
コアとなるコードの変更箇所
src/pkg/net/dnsclient_unix.go
ファイルの exchange
関数内で以下の変更が行われました。
--- a/src/pkg/net/dnsclient_unix.go
+++ b/src/pkg/net/dnsclient_unix.go
@@ -26,13 +26,7 @@ import (
// Send a request on the connection and hope for a reply.
// Up to cfg.attempts attempts.
func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error) {
- var useTCP bool
- switch c.(type) {
- case *UDPConn:
- useTCP = false
- case *TCPConn:
- useTCP = true
- }
+ _, useTCP := c.(*TCPConn)
if len(name) >= 256 {
return nil, &DNSError{Err: "name too long", Name: name}
}
@@ -69,8 +63,11 @@ func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error
continue
}
}
- buf = make([]byte, uint16(buf[0])<<8+uint16(buf[1]))
- n, err = io.ReadFull(c, buf)
+ mlen := int(buf[0])<<8 | int(buf[1])
+ if mlen > len(buf) {
+ buf = make([]byte, mlen)
+ }
+ n, err = io.ReadFull(c, buf[:mlen])
} else {
n, err = c.Read(buf)
}
@@ -80,7 +77,7 @@ func exchange(cfg *dnsConfig, c Conn, name string, qtype uint16) (*dnsMsg, error
}
return nil, err
}
- buf = buf[0:n]
+ buf = buf[:n]
in := new(dnsMsg)
if !in.Unpack(buf) || in.id != out.id {
continue
コアとなるコードの解説
1. useTCP
フラグの判定
- var useTCP bool
- switch c.(type) {
- case *UDPConn:
- useTCP = false
- case *TCPConn:
- useTCP = true
- }
+ _, useTCP := c.(*TCPConn)
- 変更前:
c
が*UDPConn
型か*TCPConn
型かをswitch
文で明示的にチェックし、useTCP
変数を設定していました。これは冗長であり、*UDPConn
のケースは実質的にuseTCP
をfalse
のままにするだけでした。 - 変更後: 型アサーション
c.(*TCPConn)
を使用し、そのセカンドリターン値(ok
)を直接useTCP
に代入しています。c
が*TCPConn
型であればuseTCP
はtrue
に、そうでなければfalse
になります。これにより、コードが大幅に簡素化され、意図がより明確になりました。
2. TCPフォールバック時のバッファ割り当て
- buf = make([]byte, uint16(buf[0])<<8+uint16(buf[1]))
- n, err = io.ReadFull(c, buf)
+ mlen := int(buf[0])<<8 | int(buf[1])
+ if mlen > len(buf) {
+ buf = make([]byte, mlen)
+ }
+ n, err = io.ReadFull(c, buf[:mlen])
- 変更前: TCP接続の場合、DNSメッセージの先頭2バイト(
buf[0]
とbuf[1]
)からメッセージ全体の長さ(uint16(buf[0])<<8+uint16(buf[1])
)を計算し、その長さで新しいバイトスライスをmake
で作成し、buf
に再割り当てしていました。その後、io.ReadFull
で残りのメッセージをその新しいバッファに読み込んでいました。この方法は、既存のbuf
の容量が十分であっても常に新しいバッファを割り当てるため、非効率でした。 - 変更後:
mlen := int(buf[0])<<8 | int(buf[1])
: DNSメッセージの長さを計算します。uint16
ではなくint
を使用していますが、これはGoのビット演算子の挙動と型推論によるものです。if mlen > len(buf)
: 既存のbuf
の現在の長さ(len(buf)
)が、読み込むべきメッセージの長さmlen
よりも小さい場合にのみ、新しいバッファをmake([]byte, mlen)
で割り当て直します。これにより、不要なメモリ再割り当てが削減されます。n, err = io.ReadFull(c, buf[:mlen])
:io.ReadFull
の第二引数にbuf[:mlen]
を渡しています。これは、buf
の先頭からmlen
バイト分のスライスを渡すことを意味します。これにより、io.ReadFull
は正確にmlen
バイトを読み込もうとします。既存のバッファが再利用される場合でも、新しいバッファが割り当てられる場合でも、このスライスによって読み込み範囲が適切に制御されます。
3. バッファスライスの簡素化
- buf = buf[0:n]
+ buf = buf[:n]
- 変更前: 読み込んだバイト数
n
に合わせて、buf
をbuf[0:n]
でスライスしていました。 - 変更後:
buf[:n]
と記述することで、先頭からのスライスであることを明示的に0
を書かずに表現しています。これはGoの慣用的な書き方であり、コードの簡潔性を向上させます。機能的な違いはありません。
これらの変更は、コードの簡潔性、可読性、そして特にTCPフォールバック時のメモリ効率を向上させることに貢献しています。
関連リンク
- Go言語の
net
パッケージのドキュメント: https://pkg.go.dev/net - Go言語の型アサーションに関するドキュメント: https://go.dev/tour/methods/15
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Goのコードレビューシステム (Gerrit): https://go.dev/cl/12925043 (コミットメッセージに記載されているCLリンク)
- DNSのUDPとTCPに関する一般的な情報源 (例: RFC 1035, RFC 5966)
- Go言語のビット演算子に関する情報源