[インデックス 19358] ファイルの概要
このコミットは、Go言語のnet
パッケージにおいて、Unix系システム上の/etc/resolv.conf
ファイルへの変更を動的に検出し、DNS設定を再読み込みする機能を追加するものです。これにより、システム管理者がresolv.conf
を更新した際に、Goアプリケーションが再起動なしに新しいDNS設定を即座に反映できるようになります。
コミット
commit bf1d400d1c75985354f52f7969ba15fb228aacb2
Author: Guillaume J. Charmes <guillaume@charmes.net>
Date: Wed May 14 17:11:00 2014 -0700
net: detect changes to /etc/resolv.conf.
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/bf1d400d1c75985354f52f7969ba15fb228aacb2
元コミット内容
net: detect changes to /etc/resolv.conf.
Implement the changes as suggested by rsc.
Fixes #6670.
LGTM=josharian, iant
R=golang-codereviews, iant, josharian, mikioh.mikioh, alex, gobot
CC=golang-codereviews, rsc
https://golang.org/cl/83690045
変更の背景
Go言語のnet
パッケージは、DNSルックアップのために/etc/resolv.conf
ファイルからDNSサーバーの設定を読み込みます。しかし、このファイルは通常、アプリケーションの起動時に一度だけ読み込まれ、その後ファイルの内容が変更されても、Goアプリケーションは古い設定を使用し続けるという問題がありました。これは、特に長時間稼働するサーバーアプリケーションにおいて、DNSサーバーの変更やネットワーク環境の変動に対応できないという運用上の課題を引き起こしていました。
この問題はGoのIssue #6670として報告されており、コミットメッセージにあるTODO(rsc)
コメント(Check periodically whether /etc/resolv.conf has changed.
)も、この機能の必要性を示唆していました。このコミットは、この長年の課題を解決し、Goアプリケーションが/etc/resolv.conf
の変更に動的に対応できるようにすることを目的としています。これにより、システム管理者がDNS設定を更新した際に、Goアプリケーションを再起動することなく、新しい設定が即座に反映されるようになります。
前提知識の解説
/etc/resolv.conf
/etc/resolv.conf
は、Unix系オペレーティングシステムにおいて、DNS (Domain Name System) リゾルバの設定を記述するファイルです。このファイルには、DNSクエリの解決に使用されるネームサーバーのIPアドレス(nameserver
エントリ)、ドメイン検索リスト(search
エントリ)、およびその他のリゾルバオプション(options
エントリ)が含まれます。Goのnet
パッケージは、DNSルックアップを行う際にこのファイルを読み込み、設定されたネームサーバーにクエリを送信します。
DNS (Domain Name System)
DNSは、インターネット上のドメイン名(例: www.example.com
)をIPアドレス(例: 192.0.2.1
)に変換(名前解決)するための分散型データベースシステムです。ユーザーがドメイン名を入力すると、OSのリゾルバが/etc/resolv.conf
に設定されたDNSサーバーに問い合わせを行い、対応するIPアドレスを取得します。
Go net
パッケージ
Go標準ライブラリのnet
パッケージは、ネットワークI/Oのプリミティブを提供します。これには、TCP/UDP接続、IPアドレスの操作、そしてDNSルックアップ機能が含まれます。このコミットが修正を加えているのは、net
パッケージ内のDNSルックアップに関連する部分です。
sync.Once
sync.Once
は、Goの標準ライブラリsync
パッケージに含まれる型で、特定の関数が一度だけ実行されることを保証するために使用されます。複数のGoroutineから同時に呼び出されても、その関数は一度しか実行されません。このコミットでは、DNS設定の初期読み込みを一度だけ行うために使用されていました。
sync.RWMutex
sync.RWMutex
は、Goの標準ライブラリsync
パッケージに含まれる読み書きロック(Reader-Writer Lock)です。複数のGoroutineが同時に読み取りアクセスを行うことを許可しますが、書き込みアクセスは排他的に行われます。これにより、共有データへの同時アクセスを安全に制御し、データ競合を防ぎます。このコミットでは、DNS設定構造体cfg
へのアクセスを保護するために使用されています。
time.Sleep
time.Sleep
は、指定された期間だけ現在のGoroutineの実行を一時停止する関数です。このコミットでは、バックグラウンドで/etc/resolv.conf
の変更を定期的にチェックするために、Goroutine内で一定時間待機するために使用されています。
os.Stat
os.Stat
は、指定されたファイルまたはディレクトリのファイル情報(FileInfo
インターフェース)を返す関数です。この情報には、ファイルのサイズ、パーミッション、最終更新時刻(ModTime
)などが含まれます。このコミットでは、/etc/resolv.conf
の最終更新時刻を取得し、ファイルが変更されたかどうかを検出するために使用されています。
io.WriteString
, ioutil.TempDir
, reflect.DeepEqual
(テスト関連)
io.WriteString
:io
パッケージの関数で、指定されたライターに文字列を書き込みます。テストコードで一時ファイルにresolv.conf
の内容を書き込むために使用されます。ioutil.TempDir
:io/ioutil
パッケージの関数で、一時ディレクトリを作成します。テストで一時的なresolv.conf
ファイルを配置する場所として使用されます。reflect.DeepEqual
:reflect
パッケージの関数で、2つの値が「深く」等しいかどうかを比較します。テストコードで、読み込まれたDNSサーバーリストが期待される値と一致するかどうかを検証するために使用されます。
Goroutineとチャネル
- Goroutine: Goの軽量な並行処理単位です。このコミットでは、バックグラウンドで
/etc/resolv.conf
の変更を監視する独立したGoroutineが起動されます。 - チャネル: Goroutine間で値を送受信するための通信メカニズムです。このコミットでは、
cfg.ch
チャネルが、resolv.conf
の再読み込みをトリガーするために使用されます。
技術的詳細
このコミットの主要な変更点は、/etc/resolv.conf
の変更を検出し、DNS設定を動的に再読み込みするメカニズムを導入したことです。
-
新しい
cfg
構造体: 以前はグローバル変数として*dnsConfig
とdnserr
が直接定義されていましたが、これらが新しいcfg
構造体の中にまとめられました。var cfg struct { ch chan struct{} mu sync.RWMutex // protects dnsConfig and dnserr dnsConfig *dnsConfig dnserr error }
ch
:resolv.conf
の再読み込みをトリガーするためのチャネルです。バッファサイズ1のチャネルとして初期化されます。mu
:dnsConfig
とdnserr
へのアクセスを保護するためのsync.RWMutex
です。これにより、複数のGoroutineからの安全な読み書きが可能になります。dnsConfig
: 現在のDNS設定を保持する*dnsConfig
ポインタです。dnserr
: DNS設定の読み込み中に発生したエラーを保持します。
-
loadConfig
関数の変更とバックグラウンドGoroutine:- 以前の
loadConfig
は一度だけ設定を読み込むシンプルな関数でした。 - 新しい
loadConfig
関数は、指定されたresolvConfPath
、reloadTime
、および終了シグナルを受け取るquit
チャネルを引数に取ります。 - この関数は、まず初期設定を読み込み、そのファイルの最終更新時刻(
mtime
)を記録します。 - その後、無限ループを持つ新しいGoroutineを起動します。このGoroutineは
reloadTime
ごとにスリープし、resolvConfPath
のmtime
をos.Stat
でチェックします。 mtime
が以前記録したものと異なる場合、ファイルが変更されたと判断し、dnsReadConfig
を再度呼び出して新しい設定を読み込みます。- 新しい設定が正常に読み込まれた場合(エラーがなく、サーバーリストが空でない場合)、
cfg.mu.Lock()
で書き込みロックを取得し、cfg.dnsConfig
とcfg.dnserr
を更新します。これにより、他のGoroutineが古い設定を読み取っている間に設定が変更されるのを防ぎます。 - 設定の読み込みに失敗した場合でも、以前の有効な設定を保持し続けます。
cfg.ch
チャネルへの送信は、テストケースなどで即座の再読み込みをトリガーするために使用されます。
- 以前の
-
DNSルックアップ関数の変更:
lookup
,goLookupHost
,goLookupIP
,goLookupCNAME
といったDNSルックアップを行う関数は、設定にアクセスする前にonceLoadConfig.Do(loadDefaultConfig)
を呼び出し、初期設定が読み込まれていることを保証します。- これらの関数は、設定を使用する際に
cfg.mu.RLock()
で読み取りロックを取得し、cfg.dnsConfig
とcfg.dnserr
にアクセスします。これにより、設定が更新されている最中でも安全に読み取りを行うことができます。処理が完了するとdefer cfg.mu.RUnlock()
でロックを解放します。
-
テストの追加:
src/pkg/net/dnsclient_unix_test.go
に、resolvConfTest
というヘルパー構造体と、TestReloadResolvConfFail
、TestReloadResolvConfChange
という新しいテストケースが追加されました。resolvConfTest
は、一時ディレクトリと一時的なresolv.conf
ファイルを作成し、その内容を動的に変更できるようにします。また、バックグラウンドのloadConfig
Goroutineを制御するためのチャネルも提供します。- これらのテストは、
resolv.conf
ファイルが存在しない場合、内容が不正な場合、内容が変更された場合など、様々なシナリオでDNS設定の再読み込みが正しく機能するかどうかを検証します。特に、mtime
の変更を検出して設定が更新されること、および不正な設定が読み込まれても以前の有効な設定が保持されることを確認します。
この変更により、Goアプリケーションは/etc/resolv.conf
の変更に柔軟に対応できるようになり、運用性が向上しました。
コアとなるコードの変更箇所
このコミットによって変更された主要なファイルは以下の2つです。
-
src/pkg/net/dnsclient_unix.go
:var cfg *dnsConfig
とvar dnserr error
が、新しい構造体cfg
に置き換えられました。この構造体は、ch
(チャネル)、mu
(sync.RWMutex
)、dnsConfig
、dnserr
を含みます。loadConfig()
関数がloadDefaultConfig()
にリネームされ、新しいloadConfig()
関数が追加されました。新しいloadConfig()
は、/etc/resolv.conf
の変更を監視し、設定を動的に再読み込みするバックグラウンドGoroutineを起動します。lookup
、goLookupHost
、goLookupIP
、goLookupCNAME
などのDNSルックアップ関連関数が、新しいcfg
構造体とcfg.mu
(sync.RWMutex
) を使用するように変更されました。これにより、設定へのアクセスがスレッドセーフになり、動的に更新された設定を反映できるようになりました。- 古い
TODO(rsc)
コメントが削除されました。
-
src/pkg/net/dnsclient_unix_test.go
:resolvConfTest
というテストヘルパー構造体が追加されました。これは、テスト中に一時的なresolv.conf
ファイルを作成・操作し、loadConfig
Goroutineを制御するために使用されます。TestReloadResolvConfFail
とTestReloadResolvConfChange
という2つの新しいテスト関数が追加されました。これらのテストは、/etc/resolv.conf
の変更が正しく検出され、DNS設定が再読み込みされることを検証します。
コアとなるコードの解説
src/pkg/net/dnsclient_unix.go
// 変更前
// var cfg *dnsConfig
// var dnserr error
// 変更後
var cfg struct {
ch chan struct{}
mu sync.RWMutex // protects dnsConfig and dnserr
dnsConfig *dnsConfig
dnserr error
}
var onceLoadConfig sync.Once
// Assume dns config file is /etc/resolv.conf here
// 変更前
// func loadConfig() { cfg, dnserr = dnsReadConfig("/etc/resolv.conf") }
// 変更後
func loadDefaultConfig() {
loadConfig("/etc/resolv.conf", 5*time.Second, nil)
}
func loadConfig(resolvConfPath string, reloadTime time.Duration, quit <-chan chan struct{}) {
var mtime time.Time
cfg.ch = make(chan struct{}, 1) // バッファサイズ1のチャネル
if fi, err := os.Stat(resolvConfPath); err != nil {
cfg.dnserr = err
} else {
mtime = fi.ModTime()
cfg.dnsConfig, cfg.dnserr = dnsReadConfig(resolvConfPath)
}
go func() { // バックグラウンドGoroutineの起動
for {
time.Sleep(reloadTime) // 定期的なチェック間隔
select {
case qresp := <-quit: // テストからの終了シグナル
qresp <- struct{}{}
return
case <-cfg.ch: // 即時再読み込みトリガー
}
fi, err := os.Stat(resolvConfPath)
if err != nil {
continue // エラーの場合は前回の設定を維持
}
m := fi.ModTime()
if m.Equal(mtime) {
continue // mtimeが同じなら再読み込みしない
}
mtime = m // mtimeを更新
ncfg, err := dnsReadConfig(resolvConfPath)
if err != nil || len(ncfg.servers) == 0 {
continue // 新しい設定が不正な場合も前回の設定を維持
}
cfg.mu.Lock() // 書き込みロック
cfg.dnsConfig = ncfg
cfg.dnserr = nil
cfg.mu.Unlock() // ロック解除
}
}()
}
// lookup関数などの変更例 (抜粋)
func lookup(name string, qtype uint16) (cname string, addrs []dnsRR, err error) {
onceLoadConfig.Do(loadDefaultConfig) // 初期設定の読み込みを一度だけ保証
select {
case cfg.ch <- struct{}{}: // 再読み込みを試みる (バッファされているためブロックしない)
default:
}
cfg.mu.RLock() // 読み取りロック
defer cfg.mu.RUnlock() // 関数終了時にロックを解放
if cfg.dnserr != nil || cfg.dnsConfig == nil {
err = cfg.dnserr
return
}
// ... cfg.dnsConfig を使用した処理 ...
}
このコードは、cfg
構造体によってDNS設定とその状態を一元管理し、sync.RWMutex
によって安全な並行アクセスを保証します。loadConfig
関数内で起動されるGoroutineは、time.Sleep
とos.Stat
を使って/etc/resolv.conf
の変更を定期的に監視し、変更があればdnsReadConfig
で再読み込みを行います。lookup
などの関数は、読み取りロックを取得してからcfg.dnsConfig
にアクセスすることで、常に最新かつ整合性のある設定を使用できるようになります。
src/pkg/net/dnsclient_unix_test.go
type resolvConfTest struct {
*testing.T
dir string
path string
started bool
quitc chan chan struct{}
}
func newResolvConfTest(t *testing.T) *resolvConfTest {
dir, err := ioutil.TempDir("", "resolvConfTest")
if err != nil {
t.Fatalf("could not create temp dir: %v", err)
}
// Disable the default loadConfig to control it in tests
onceLoadConfig.Do(func() {})
r := &resolvConfTest{
T: t,
dir: dir,
path: path.Join(dir, "resolv.conf"),
quitc: make(chan chan struct{}),
}
return r
}
func (r *resolvConfTest) Start() {
loadConfig(r.path, 100*time.Millisecond, r.quitc)
r.started = true
}
func (r *resolvConfTest) SetConf(s string) {
// Make sure the file mtime will be different
time.Sleep(time.Second)
f, err := os.OpenFile(r.path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
// ... エラーハンドリングと書き込み ...
io.WriteString(f, s)
f.Close()
if r.started {
// cfg.ch にシグナルを送り、再読み込みをトリガーし、完了を待つ
cfg.ch <- struct{}{}
cfg.ch <- struct{}{}
cfg.ch <- struct{}{}
}
}
func (r *resolvConfTest) WantServers(want []string) {
cfg.mu.RLock()
defer cfg.mu.RUnlock()
if got := cfg.dnsConfig.servers; !reflect.DeepEqual(got, want) {
r.Fatalf("Unexpected dns server loaded, got %v want %v", got, want)
}
}
func (r *resolvConfTest) Close() {
resp := make(chan struct{})
r.quitc <- resp
<-resp
os.RemoveAll(r.dir)
}
func TestReloadResolvConfFail(t *testing.T) {
// ... テストロジック ...
r := newResolvConfTest(t)
defer r.Close()
r.Start() // resolv.conf がまだ存在しない状態で開始
// goLookupIP が失敗することを確認
r.SetConf("nameserver 8.8.8.8") // 有効な設定を書き込む
// goLookupIP が成功することを確認
r.SetConf("") // 不正な設定を書き込む
// goLookupIP が引き続き成功することを確認 (以前の有効な設定が保持されているため)
}
func TestReloadResolvConfChange(t *testing.T) {
// ... テストロジック ...
r := newResolvConfTest(t)
defer r.Close()
r.SetConf("nameserver 8.8.8.8")
r.Start()
r.WantServers([]string{"[8.8.8.8]"}) // 8.8.8.8 が読み込まれたことを確認
r.SetConf("") // 不正な設定
// goLookupIP が引き続き成功することを確認
r.SetConf("nameserver 8.8.4.4") // 新しい有効な設定
r.WantServers([]string{"[8.8.4.4]"}) // 8.8.4.4 が読み込まれたことを確認
}
テストコードは、resolvConfTest
ヘルパーを使って、実際のファイルシステム操作をシミュレートし、resolv.conf
の変更がGoのDNSリゾルバにどのように影響するかを検証します。SetConf
関数は、time.Sleep
を使ってファイルのmtime
が確実に変更されるようにし、cfg.ch
チャネルを使ってバックグラウンドの再読み込みGoroutineを明示的にトリガーします。これにより、テストの信頼性と再現性が向上しています。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/bf1d400d1c75985354f52f7969ba15fb228aacb2
- Gerrit Change-ID: https://golang.org/cl/83690045
- Go Issue #6670: このコミットが修正した問題。Gerritの議論で言及されています。
参考にした情報源リンク
- Gerrit Change-ID 83690045 の議論: このコミットの背景、実装の詳細、テストに関する議論など、多くの情報が提供されています。 https://golang.org/cl/83690045