Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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クライアント実装には、以下の課題があったと考えられます。

  1. useTCP フラグの冗長な判定: exchange 関数内で、引数として渡される Conn インターフェースの実体が *UDPConn なのか *TCPConn なのかを switch 文で判定し、useTCP というブール値を設定していました。これは、型アサーションを直接利用することでより簡潔に表現できる可能性がありました。
  2. 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 インターフェースが定義されています。このインターフェースは ReadWriteClose などの基本的なメソッドを提供し、具体的な実装(例: *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 型である場合にのみ useTCPtrue に設定し、それ以外の場合は false のままにするという意図を持っています。このロジックは、Goの型アサーションのセカンドリターン値(ok)を利用することで、より簡潔に表現できます。

// 変更後
_, useTCP := c.(*TCPConn)

この変更により、c*TCPConn 型であれば useTCPtrue に、そうでなければ 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 のケースは実質的に useTCPfalse のままにするだけでした。
  • 変更後: 型アサーション c.(*TCPConn) を使用し、そのセカンドリターン値(ok)を直接 useTCP に代入しています。c*TCPConn 型であれば useTCPtrue に、そうでなければ 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 に合わせて、bufbuf[0:n] でスライスしていました。
  • 変更後: buf[:n] と記述することで、先頭からのスライスであることを明示的に 0 を書かずに表現しています。これはGoの慣用的な書き方であり、コードの簡潔性を向上させます。機能的な違いはありません。

これらの変更は、コードの簡潔性、可読性、そして特にTCPフォールバック時のメモリ効率を向上させることに貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式リポジトリ: https://github.com/golang/go
  • Goのコードレビューシステム (Gerrit): https://go.dev/cl/12925043 (コミットメッセージに記載されているCLリンク)
  • DNSのUDPとTCPに関する一般的な情報源 (例: RFC 1035, RFC 5966)
  • Go言語のビット演算子に関する情報源