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

[インデックス 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設定を動的に再読み込みするメカニズムを導入したことです。

  1. 新しいcfg構造体: 以前はグローバル変数として*dnsConfigdnserrが直接定義されていましたが、これらが新しいcfg構造体の中にまとめられました。

    var cfg struct {
        ch        chan struct{}
        mu        sync.RWMutex // protects dnsConfig and dnserr
        dnsConfig *dnsConfig
        dnserr    error
    }
    
    • ch: resolv.confの再読み込みをトリガーするためのチャネルです。バッファサイズ1のチャネルとして初期化されます。
    • mu: dnsConfigdnserrへのアクセスを保護するためのsync.RWMutexです。これにより、複数のGoroutineからの安全な読み書きが可能になります。
    • dnsConfig: 現在のDNS設定を保持する*dnsConfigポインタです。
    • dnserr: DNS設定の読み込み中に発生したエラーを保持します。
  2. loadConfig関数の変更とバックグラウンドGoroutine:

    • 以前のloadConfigは一度だけ設定を読み込むシンプルな関数でした。
    • 新しいloadConfig関数は、指定されたresolvConfPathreloadTime、および終了シグナルを受け取るquitチャネルを引数に取ります。
    • この関数は、まず初期設定を読み込み、そのファイルの最終更新時刻(mtime)を記録します。
    • その後、無限ループを持つ新しいGoroutineを起動します。このGoroutineはreloadTimeごとにスリープし、resolvConfPathmtimeos.Statでチェックします。
    • mtimeが以前記録したものと異なる場合、ファイルが変更されたと判断し、dnsReadConfigを再度呼び出して新しい設定を読み込みます。
    • 新しい設定が正常に読み込まれた場合(エラーがなく、サーバーリストが空でない場合)、cfg.mu.Lock()で書き込みロックを取得し、cfg.dnsConfigcfg.dnserrを更新します。これにより、他のGoroutineが古い設定を読み取っている間に設定が変更されるのを防ぎます。
    • 設定の読み込みに失敗した場合でも、以前の有効な設定を保持し続けます。
    • cfg.chチャネルへの送信は、テストケースなどで即座の再読み込みをトリガーするために使用されます。
  3. DNSルックアップ関数の変更:

    • lookup, goLookupHost, goLookupIP, goLookupCNAMEといったDNSルックアップを行う関数は、設定にアクセスする前にonceLoadConfig.Do(loadDefaultConfig)を呼び出し、初期設定が読み込まれていることを保証します。
    • これらの関数は、設定を使用する際にcfg.mu.RLock()で読み取りロックを取得し、cfg.dnsConfigcfg.dnserrにアクセスします。これにより、設定が更新されている最中でも安全に読み取りを行うことができます。処理が完了するとdefer cfg.mu.RUnlock()でロックを解放します。
  4. テストの追加:

    • src/pkg/net/dnsclient_unix_test.goに、resolvConfTestというヘルパー構造体と、TestReloadResolvConfFailTestReloadResolvConfChangeという新しいテストケースが追加されました。
    • resolvConfTestは、一時ディレクトリと一時的なresolv.confファイルを作成し、その内容を動的に変更できるようにします。また、バックグラウンドのloadConfig Goroutineを制御するためのチャネルも提供します。
    • これらのテストは、resolv.confファイルが存在しない場合、内容が不正な場合、内容が変更された場合など、様々なシナリオでDNS設定の再読み込みが正しく機能するかどうかを検証します。特に、mtimeの変更を検出して設定が更新されること、および不正な設定が読み込まれても以前の有効な設定が保持されることを確認します。

この変更により、Goアプリケーションは/etc/resolv.confの変更に柔軟に対応できるようになり、運用性が向上しました。

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

このコミットによって変更された主要なファイルは以下の2つです。

  1. src/pkg/net/dnsclient_unix.go:

    • var cfg *dnsConfigvar dnserr error が、新しい構造体cfgに置き換えられました。この構造体は、ch (チャネル)、mu (sync.RWMutex)、dnsConfigdnserrを含みます。
    • loadConfig() 関数が loadDefaultConfig() にリネームされ、新しい loadConfig() 関数が追加されました。新しい loadConfig() は、/etc/resolv.confの変更を監視し、設定を動的に再読み込みするバックグラウンドGoroutineを起動します。
    • lookupgoLookupHostgoLookupIPgoLookupCNAMEなどのDNSルックアップ関連関数が、新しいcfg構造体とcfg.mu (sync.RWMutex) を使用するように変更されました。これにより、設定へのアクセスがスレッドセーフになり、動的に更新された設定を反映できるようになりました。
    • 古いTODO(rsc)コメントが削除されました。
  2. src/pkg/net/dnsclient_unix_test.go:

    • resolvConfTestというテストヘルパー構造体が追加されました。これは、テスト中に一時的なresolv.confファイルを作成・操作し、loadConfig Goroutineを制御するために使用されます。
    • TestReloadResolvConfFailTestReloadResolvConfChangeという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.Sleepos.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を明示的にトリガーします。これにより、テストの信頼性と再現性が向上しています。

関連リンク

参考にした情報源リンク

  • Gerrit Change-ID 83690045 の議論: このコミットの背景、実装の詳細、テストに関する議論など、多くの情報が提供されています。 https://golang.org/cl/83690045