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

[インデックス 17108] ファイルの概要

このコミットは、Go言語の標準ライブラリnetパッケージ内のドメイン名検証ロジックisDomainName関数の改善に関するものです。具体的には、不要な文字列操作を排除し、ドメイン名の検証ルールをより明示的にすることで、パフォーマンスを大幅に向上させ、メモリ割り当てを削減しています。また、ドメイン名検証のテストケースが拡充され、より堅牢な検証が保証されています。

コミット

commit 654f35865fbbb595593e245887e58ba50d213f9c
Author: Volker Dobler <dr.volker.dobler@gmail.com>
Date:   Thu Aug 8 16:33:57 2013 -0700

    net: avoid string operation and make valid domain names explicit

    Having a trailing dot in the string doesn't really simplify
    the checking loop in isDomainName. Avoid this unnecessary allocation.
    Also make the valid domain names more explicit by adding some more
    test cases.

    benchmark            old ns/op    new ns/op    delta
    BenchmarkDNSNames       2420.0        983.0  -59.38%

    benchmark           old allocs   new allocs    delta
    BenchmarkDNSNames           12            0  -100.00%

    benchmark            old bytes    new bytes    delta
    BenchmarkDNSNames          336            0  -100.00%

    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12662043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/654f35865fbbb595593e245887e58ba50d213f9c

元コミット内容

このコミットの目的は、netパッケージのisDomainName関数における不要な文字列操作を排除し、有効なドメイン名の定義をより明確にすることです。具体的には、isDomainName関数内で文字列の末尾にドットを追加する処理が、検証ループを簡素化する目的で存在していましたが、これが不要なメモリ割り当てを引き起こしていました。このコミットではその割り当てを回避し、さらにテストケースを追加することで、ドメイン名検証の正確性を高めています。

ベンチマーク結果は以下の通りです。

  • BenchmarkDNSNames (ns/op): 2420.0 ns/op から 983.0 ns/op へと 59.38% の高速化。
  • BenchmarkDNSNames (allocs): 12 回の割り当てから 0 回へと 100% の削減。
  • BenchmarkDNSNames (bytes): 336 バイトの割り当てから 0 バイトへと 100% の削減。

これらの結果は、変更がパフォーマンスとメモリ効率に劇的な改善をもたらしたことを示しています。

変更の背景

netパッケージのisDomainName関数は、与えられた文字列が有効なDNSドメイン名であるかを検証するために使用されます。従来のisDomainName関数には、検証ロジックを簡素化するために、入力文字列の末尾にドットがない場合に強制的にドットを追加するという処理が含まれていました。

この「末尾にドットを追加する」という操作は、Go言語において新しい文字列を生成し、元の文字列の内容をコピーする必要があるため、ヒープメモリの割り当て(アロケーション)とそれに伴うガベージコレクションのオーバーヘッドを発生させていました。特に、isDomainNameのような頻繁に呼び出される可能性のある関数において、このような不要なアロケーションはパフォーマンスのボトルネックとなり得ます。

このコミットの背景には、このようなパフォーマンス上の非効率性を解消し、より高速でメモリ効率の良いドメイン名検証を実現するという目的がありました。また、ドメイン名に関するRFC(Request For Comments)で定義されている厳密なルール(例:ラベルの最大長、ハイフンの使用制限など)をより正確に反映させる必要性も背景にあります。

前提知識の解説

DNSドメイン名とRFC

DNS(Domain Name System)ドメイン名は、インターネット上のリソースを一意に識別するための階層的な名前付けシステムです。ドメイン名の構造と有効性に関するルールは、主にIETF(Internet Engineering Task Force)によって発行されるRFC(Request For Comments)文書で定義されています。

主要な関連RFCは以下の通りです。

  • RFC 1034 (Domain Names - Concepts and Facilities): ドメイン名の基本的な概念、構造、および命名規則について定義しています。
  • RFC 1035 (Domain Names - Implementation and Specification): DNSプロトコルの詳細と、ドメイン名の表現方法について定義しています。
  • RFC 2181 (Clarifications to the DNS Specification): RFC 1034とRFC 1035の曖昧な点を明確にし、特にドメイン名ラベルの最大長(63オクテット)や、ドメイン名全体の最大長(255オクテット)について言及しています。また、ラベルがハイフンで始まったり終わったりしてはならないというルールも含まれます。

これらのRFCに基づき、有効なドメイン名には以下のような一般的なルールがあります。

  1. 文字セット: 英数字(a-z, A-Z, 0-9)とハイフン(-)のみが使用できます。
  2. ラベル: ドメイン名はドット(.)で区切られた「ラベル」のシーケンスで構成されます。
  3. ラベルの長さ: 各ラベルは1文字以上63文字以下でなければなりません。
  4. ラベルの開始/終了: ラベルは英数字で始まり、英数字で終わらなければなりません(ハイフンで始まったり終わったりしてはならない)。
  5. ドメイン名全体の長さ: ドメイン名全体の長さは255文字以下でなければなりません(末尾のドットを含む場合)。
  6. ルートラベル: ドメイン名の末尾にドットがある場合、それはルートドメイン(空のラベル)を示します。これはFQDN(Fully Qualified Domain Name)と呼ばれる形式です。

Go言語における文字列操作とメモリ割り当て

Go言語では、文字列は不変(immutable)な値です。これは、一度作成された文字列の内容を変更できないことを意味します。例えば、既存の文字列に文字を追加したり、一部を置換したりする操作は、実際には新しい文字列を生成し、その中に変更後の内容をコピーするという形で実現されます。

この新しい文字列の生成には、ヒープメモリの割り当て(アロケーション)が必要です。アロケーションは、プログラムが実行時に動的にメモリを要求するプロセスです。アロケーションされたメモリは、不要になった時点でガベージコレクタによって解放されます。

頻繁なアロケーションは、以下の理由でパフォーマンスに影響を与えます。

  • アロケーション自体のコスト: メモリを要求し、OSから取得するプロセスには時間がかかります。
  • ガベージコレクションのオーバーヘッド: アロケーションが増えると、ガベージコレクタがより頻繁に、またはより長時間動作する必要があり、これがアプリケーションの実行を一時停止させる(ストップ・ザ・ワールド)原因となることがあります。

したがって、パフォーマンスが重要なコードパスでは、不要な文字列操作やアロケーションを避けることが、Goプログラムの最適化において重要なプラクティスとなります。

技術的詳細

このコミットの技術的詳細は、isDomainName関数の内部ロジックの変更と、それに関連するテストの追加に集約されます。

isDomainName関数の変更点

変更前のisDomainName関数は、入力文字列sの末尾がドットでない場合に、s += "."という操作でドットを追加していました。この操作は、Go言語の文字列が不変であるため、新しい文字列をヒープに割り当て、元の文字列の内容と追加のドットをコピーするという処理を伴いました。これが、ベンチマーク結果に示される「12回の割り当て」と「336バイトのメモリ使用」の主な原因でした。

変更後、このs += "."の行が削除されました。これにより、関数は入力された文字列をそのままの形で処理するようになります。

この変更に伴い、ドメイン名検証のロジックも調整されました。従来のロジックでは、末尾にドットを追加することで、ループの終了条件や、最後のラベルの検証が簡素化されるという側面がありました。ドットの追加を削除したことで、ループの終了後に明示的なチェックが必要になりました。

追加されたチェックは以下の通りです。

if last == '-' || partlen > 63 {
	return false
}

このコードは、forループが終了した後に実行されます。

  • last == '-': これは、ドメイン名の最後の文字がハイフンであった場合にfalseを返すことを意味します。RFCのルールでは、ドメイン名ラベルはハイフンで終わってはならないため、これは正しい検証です。従来のロジックでは、末尾にドットを追加することで、このケースがc == '.'if last == '.' || last == '-'条件で捕捉される可能性がありましたが、ドット追加がなくなったため、明示的なチェックが必要になりました。
  • partlen > 63: これは、現在処理中のラベルの長さ(partlen)が63文字を超えている場合にfalseを返すことを意味します。RFCのルールでは、各ドメイン名ラベルの最大長は63オクテット(文字)と定められています。このチェックは、特に末尾のラベルが63文字を超えるケースを正確に捕捉するために重要です。

これらの変更により、isDomainName関数は、不要なメモリ割り当てなしに、よりRFCに準拠した厳密なドメイン名検証を行うようになりました。

テストケースの拡充

src/pkg/net/dnsname_test.goファイルでは、isDomainName関数の正確性を検証するためのテストケースが拡充されました。

  • 重複テストケースの削除: {"_xmpp-server._tcp.google.com", true}という重複したテストケースが一つ削除されました。

  • 新しいテストケースの追加:

    • {"a.b-.com", false}: ラベルがハイフンで終わる無効なケース。
    • {"a.b.com-", false}: ドメイン名全体の最後のラベルがハイフンで終わる無効なケース。
    • {"a.b..", false}: 空のラベル(..)を含む無効なケース。
    • {"b.com.", true}: 末尾にドットを持つ有効なFQDNのケース。 これらのテストケースは、isDomainName関数の新しい検証ロジック(特にlast == '-'のチェックや、ドット追加の削除による影響)が正しく機能することを確認します。
  • BenchmarkDNSNames関数の追加: このコミットで最も重要なテスト関連の変更は、BenchmarkDNSNamesという新しいベンチマーク関数の追加です。この関数は、isDomainNameのパフォーマンスを測定するために設計されました。

    func BenchmarkDNSNames(b *testing.B) {
    	benchmarks := append(tests, []testCase{
    		{strings.Repeat("a", 63), true},
    		{strings.Repeat("a", 64), false},
    	}...)
    	for n := 0; n < b.N; n++ {
    		for _, tc := range benchmarks {
    			if isDomainName(tc.name) != tc.result {
    				b.Errorf("isDomainName(%q) = %v; want %v", tc.name, !tc.result, tc.result)
    			}
    		}
    	}
    }
    

    このベンチマークは、既存のtestsに加えて、特にラベル長が63文字(有効)と64文字(無効)のケースを追加しています。これは、partlen > 63という新しい検証ロジックがパフォーマンスに与える影響を正確に測定するために重要です。strings.Repeatを使用して、これらの長い文字列を効率的に生成しています。

これらのテストの追加と拡充により、isDomainName関数の変更が意図した通りに機能し、パフォーマンスが向上し、かつドメイン名検証の正確性が維持されていることが保証されます。

コアとなるコードの変更箇所

src/pkg/net/dnsclient.go

--- a/src/pkg/net/dnsclient.go
+++ b/src/pkg/net/dnsclient.go
@@ -122,12 +122,9 @@ func isDomainName(s string) bool {
 	if len(s) > 255 {
 		return false
 	}
-	if s[len(s)-1] != '.' { // simplify checking loop: make name end in dot
-		s += "."
-	}
 
 	last := byte('.')
-	ok := false // ok once we've seen a letter
+	ok := false // Ok once we've seen a letter.
 	partlen := 0
 	for i := 0; i < len(s); i++ {
 		c := s[i]
@@ -141,13 +138,13 @@ func isDomainName(s string) bool {
 			// fine
 			partlen++
 		case c == '-':
-			// byte before dash cannot be dot
+			// Byte before dash cannot be dot.
 			if last == '.' {
 				return false
 			}
 			partlen++
 		case c == '.':
-			// byte before dot cannot be dot, dash
+			// Byte before dot cannot be dot, dash.
 			if last == '.' || last == '-' {
 				return false
 			}
@@ -158,6 +155,9 @@ func isDomainName(s string) bool {
 		}
 		last = c
 	}
+	if last == '-' || partlen > 63 {
+		return false
+	}
 
 	return ok
 }

src/pkg/net/dnsname_test.go

--- a/src/pkg/net/dnsname_test.go
+++ b/src/pkg/net/dnsname_test.go
@@ -5,6 +5,7 @@
 package net
 
 import (
+	"strings"
 	"testing"
 )
 
@@ -16,7 +17,6 @@ type testCase struct {
 var tests = []testCase{
 	// RFC2181, section 11.
 	{"_xmpp-server._tcp.google.com", true},
-	{"_xmpp-server._tcp.google.com", true},
 	{"foo.com", true},
 	{"1foo.com", true},
 	{"26.0.0.73.com", true},
@@ -24,6 +24,10 @@ var tests = []testCase{
 	{"fo1o.com", true},
 	{"foo1.com", true},
 	{"a.b..com", false},
+	{"a.b-.com", false},
+	{"a.b.com-", false},
+	{"a.b..", false},
+	{"b.com.", true},
 }
 
 func getTestCases(ch chan<- testCase) {
@@ -63,3 +67,17 @@ func TestDNSNames(t *testing.T) {
 		}
 	}
 }
+
+func BenchmarkDNSNames(b *testing.B) {
+	benchmarks := append(tests, []testCase{
+		{strings.Repeat("a", 63), true},
+		{strings.Repeat("a", 64), false},
+	}...)
+	for n := 0; n < b.N; n++ {
+		for _, tc := range benchmarks {
+			if isDomainName(tc.name) != tc.result {
+				b.Errorf("isDomainName(%q) = %v; want %v", tc.name, !tc.result, tc.result)
+			}
+		}
+	}
+}

コアとなるコードの解説

src/pkg/net/dnsclient.go の変更点

  1. 不要な文字列操作の削除:

    -	if s[len(s)-1] != '.' { // simplify checking loop: make name end in dot
    -		s += "."
    -	}
    

    このコードブロックは、入力文字列sの末尾にドットがない場合に、強制的にドットを追加していました。この操作は、Go言語の文字列が不変であるため、新しい文字列の割り当てとコピーを引き起こし、パフォーマンスのボトルネックとなっていました。この行を削除することで、アロケーションが完全に排除され、ベンチマーク結果に示される大幅なパフォーマンス向上とメモリ使用量の削減が実現されました。

  2. 末尾の文字とラベル長の明示的な検証:

    +	if last == '-' || partlen > 63 {
    +		return false
    
  • }
    これは、`for`ループの直後に追加された新しい検証ロジックです。
    *   `last == '-'`: ループの最後に処理された文字がハイフンであった場合、`false`を返します。これは、DNSドメイン名ラベルがハイフンで終わってはならないというRFCのルール(例: `example-.com`は無効)を厳密に適用するためのものです。従来のドット追加ロジックでは、このケースが間接的に処理される可能性がありましたが、ドット追加がなくなったため、明示的なチェックが必要になりました。
    *   `partlen > 63`: 現在処理中のラベルの長さ(`partlen`)が63文字を超えている場合、`false`を返します。DNSの仕様では、各ラベルの最大長は63オクテットと定められています。このチェックにより、例えば`a`を64回繰り返した文字列のような無効なラベルを正確に検出できるようになりました。
    
    

これらの変更により、isDomainName関数は、より効率的かつRFCに準拠した形でドメイン名を検証するようになりました。

src/pkg/net/dnsname_test.go の変更点

  1. stringsパッケージのインポート:

    +	"strings"
    

    新しいベンチマーク関数BenchmarkDNSNames内でstrings.Repeatを使用するために、stringsパッケージがインポートされました。

  2. 重複テストケースの削除:

    -	{"_xmpp-server._tcp.google.com", true},
    

    既存のテストケースの重複が解消されました。

  3. 新しいテストケースの追加:

    +	{"a.b-.com", false},
    +	{"a.b.com-", false},
    +	{"a.b..", false},
    +	{"b.com.", true},
    

    これらのテストケースは、isDomainName関数の変更によって影響を受ける可能性のあるエッジケースや、RFCに準拠したドメイン名ルールの特定の側面を検証するために追加されました。

    • a.b-.coma.b.com-: ラベルがハイフンで終わる無効なケースをテストします。
    • a.b..: 空のラベル(..)を含む無効なケースをテストします。
    • b.com.: 末尾にドットを持つ有効なFQDN(Fully Qualified Domain Name)をテストします。
  4. BenchmarkDNSNames関数の追加:

    +func BenchmarkDNSNames(b *testing.B) {
    +	benchmarks := append(tests, []testCase{
    +		{strings.Repeat("a", 63), true},
    +		{strings.Repeat("a", 64), false},
    +	}...)
    +	for n := 0; n < b.N; n++ {
    +		for _, tc := range benchmarks {
    +			if isDomainName(tc.name) != tc.result {
    +				b.Errorf("isDomainName(%q) = %v; want %v", tc.name, !tc.result, tc.result)
    +			}
    +		}
    +	}
    +}
    

    このベンチマーク関数は、isDomainName関数のパフォーマンスを定量的に測定するために導入されました。特に、strings.Repeat("a", 63)(有効な最大ラベル長)とstrings.Repeat("a", 64)(無効なラベル長)のケースを追加することで、新しいpartlen > 63の検証ロジックがパフォーマンスに与える影響を正確に評価できるようにしています。b.N回ループを回し、各テストケースに対してisDomainNameを呼び出すことで、ナノ秒単位の実行時間、メモリ割り当て回数、および割り当てバイト数を測定します。これにより、変更がもたらしたパフォーマンス改善が明確に示されました。

これらのコード変更は、Go言語のnetパッケージにおけるドメイン名検証の堅牢性と効率性を大幅に向上させるものです。

関連リンク

参考にした情報源リンク