Go2 Advent Calendar 2019 8日目の参加記事です。
TL;DR
- SRVレコードで
Dial()
できる koron-go/dialsrv を作った dialsrv.Dialer
はnet.Dialer
をラップする形で互換インタフェースを備えている- HTTP も gRPC も SRV レコード越しで接続できるようになる
- nomad のようなオーケストレーションツールで動かすアプリで使う想定
SRV レコード Dialer
皆さんはDNSをひいてますか? いえこのページを見れているということはDNSをひけていることはわかってます。 ですがそれを普段から意識している人は少ないはずです。 まずはちょっとこのDNSを意識してみましょう。
DNSというのは名前をIPアドレスに変換するためのデータベースです。
ブラウザがこのDNSに www.kaoriya.net
という名前を問い合わせて、
IPアドレスを得ているからこのブログポストを目にできています。
より正確にはDNSの「Aレコード」によりIPアドレスを知ることができています。
あいだに「CNAMEレコード」を挟むこともありますが、
最終的にAレコードによりIPアドレスが決定できることでウェブサーバーに繋がります。
このようにDNSには「レコード」の種類がいくつかあります。
インターネットの多くを支えているTCP/IPでは、 接続先を決定するのにIPアドレスだけでは情報が足りません。 わかってる人はわかってると思いますがポート番号も必要です。 Webサーバーであればこのポート番号にはHTTPであれば80番を、 HTTPSであれば443番を利用することになっています。 こういうサービスごとによく知られたポート番号をWell Known Portといいます。 なのでこのポート番号を意識することは普段はまずありません。 そう開発者を除いては。
本題のSRVレコードに入りましょう。 SRVレコードはDNSのレコードの1つなのですが、 Aレコードのように名前からIPアドレスだけではなくポート番号もあわせて引けます。 たとえば開発用にWebサーバーのようなコンポーネントを動かしたい、 でもWell Known Portでは動かしたくない(動かせない)というシーンはよくあります。 さらに一歩進んでdockerコンテナなどをオーケストレーションツールで管理した場合、 特定コンポーネントのIPアドレスだけでなくポート番号まで動的に変わりうるケースがあります。 そのような場合にコンポーネントのIPアドレスとポート番号を見つけられるようにするのがSRVレコードの役割です。
話をgolangに移しましょう。
golangにおいてTCP/IP接続を担うのは net.Dial()
です。
この net.Dial()
を次のように呼び出すことでTCP/IP接続が確立します。
c, err := net.Dial("tcp", "www.kaoriya.net:http")
net.Dial()
は第2引数を解釈し、前半の www.kaoriya.net
をDNSでAレコードを引いてIPアドレスを解決し、
後半の http
からポート番号がWell Known Portの80番であると決め実際に接続しています。
また接続に伴う様々な情報や手続きをカプセル化したものとして net.Dialer
構造体があります。
この構造体のもつ Dial()
と DialContext()
の2つのメソッドは net.Dial()
とほぼ同じもので、
net/http
など TCP/IP を前提とするパッケージの基本構造として君臨しています。
この Dial()
の中のIPアドレスとポート番号を決める部分、
ここに介入してSRVレコードにより両方を決定するようにしたのが koron-go/dialsrv
です。
いやーコレにたどり着くまで長かったですね。
dialsrv
は *net.Dialer
をラップして *dialsrv.Dialer
を作ります。
この *dialsrv.Dialer
は *net.Dialer
と互換の
Dial()
及び DialContext()
を実装しているのでその代用として使えます。
以下のコードはSRVレコードを利用できる http.Client
の実装例です。
// HTTPTransport is replacement for http.DefaultTransport
var HTTPTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: dialsrv.New(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// HTTPClient is replacement for http.DefaultClient
var HTTPClient = &http.Client{
Transport: HTTPTransport,
}
少々わかりにくいですが *net.Dialer
を dialsrv.New
で *dialsrv.Dialer
としてラップし、
その DialContext()
メソッドを http.Transport
の DialContext
フィールドに指定しています。
そのトランスポートを http.Client
に設定すれば、ハイできあがり。
なおこの HTTPClient
の定義は koron-go/dialsrv
に含まれているので、
以下のようにお手軽にSRVレコードによるHTTP通信ができます。
r, err := dialsrv.HTTPClient.Get("http://srv+myservice+example.com/")
net.Dialer
互換であるため、
TCP/IPを使う通信であれば容易にSRVレコードに対応できます。
以下のコードはちょっとだけやることが多いですが
gRPCサーバーへの接続にSRVレコードを使えるようにする例です。
import (
"context"
"net"
"time"
"github.com/koron-go/dialsrv"
"google.golang.org/grpc"
)
func dial(adr string, to time.Duration) (net.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), to)
defer cancel()
d := dialsrv.New(nil)
return d.DialContext(ctx, `tcp`, adr)
}
// ...(snip)...
c, err := grpc.DialContext(ctx, name, grpc.WithDialer(dial))
// TODO: work with `c *grpc.ClientConn`
この dialsrv にはSRVレコードとして受付可能なフォーマットが2種類あるのですが、
少しだけ特殊な独自記法を採用しています。
独自記法を採用した理由はこの *dialsrv.Dialer
を使ったとしても
普通のAレコードを使ったDNS問い合わせもそのまま対応できるようにしたかったためです。
そのため上記のコードを用いてもサーバー名として www.kaoriya.net
で問い合わせることは依然可能です。
SRV用のフォーマットの1つ目は srv+{service}+{hostname}
です。
このときDNSには _{service}._{network}.{hostanme}
という形での問い合わせが発生します。
{network}
部分には Dial()
の第1引数が入りますので、通常ならば tcp
となります。
2つ目のフォーマットは srv+{hostname}
です。
この場合は {hostname}
のSRVレコードを問い合わせます。
残念ながら現在は複数のSRVレコードが返された場合最初の1つしか利用していません。 後々問題を起こしそうなため対応したいところですが 今後改善したいところですが、まだ必要になっていないため、対応の目処はたっていません。
みなさんもgolangでSRVレコードが必要になったら koron-go/dialsrv を試してみてください。