GoからDocker上のElasticsearchに接続する際にネットワークのSniffingでハマった
問題
Docker で Elasticsearch を起動します。
services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.4.0 environment: - discovery.type=single-node ports: - 9200:9200 - 9300:9300
以下の Elasticsearch クライアントライブラリを利用し、http://127.0.0.1:9200
へ接続しに行きます。
c, err := elastic.NewClient() // デフォルトで127.0.0.1:9200へ接続 if err != nil { log.Println(err) }
no active connection found: no Elasticsearch node available
とエラーとなり接続できません。
ちなみに以下では接続できます。
res, err := http.Get("http://127.0.0.1:9200/?pretty=true")
解決策
以下のようにelastic.SetSniff(false)
を引数に与えれば、 Docker 上の Elasticsearch に接続できます。
c, err := elastic.NewClient(elastic.SetSniff(false))
1 時間以上これで時間を溶かしてしまいました。
原因
デフォルトではクライアントがクラスタの全ノードを自動的に見つけにいきそれらのノードにリクエストを投げる(Sniffing)が、それらのノードは外部からは直接アクセスできないため接続に失敗します。
ここで取得されるノードとは具体的には以下のリクエストで取得されます。
この例では、172.18.0.3:9200
にアクセスしにいくことになりますが、127.0.0.1:9200
を通さないのでエラーになります。
$ curl http://localhost:9200/_nodes/http\?pretty\=true
"nodes" : { "bzn5IyC2T3GJj_MGhdwyFQ" : { "transport_address" : "172.18.0.3:9300", "host" : "172.18.0.3", "ip" : "172.18.0.3", "build_type" : "docker", "http" : { "bound_address" : [ "0.0.0.0:9200" ], "publish_address" : "172.18.0.3:9200", "max_content_length_in_bytes" : 104857600 }
実際のコード
NewClient
メソッド内の条件分岐です。
有効の場合、sniff
メソッドが呼ばれます。
if c.snifferEnabled { // Sniff the cluster initially if err := c.sniff(ctx, c.snifferTimeoutStartup); err != nil { return nil, err } } else { // Do not sniff the cluster initially. Use the provided URLs instead. for _, url := range c.urls { c.conns = append(c.conns, newConn(url, url)) } }
sniff
メソッド内ではノード情報を取得するsniffNode
メソッドがありここで Docker 外からは通信できないPublishAddress
を取得することで今回の問題が発生しました。
func (c *Client) sniffNode(ctx context.Context, url string) []*conn { var nodes []*conn // Call the Nodes Info API at /_nodes/http req, err := NewRequest("GET", url+"/_nodes/http") // このリクエストでノード情報を取得する、ここで Docker 外部からアクセスできないURLを取得してしまう if err != nil { return nodes } // 略 res, err := c.c.Do((*http.Request)(req).WithContext(ctx)) if err != nil { return nodes } defer res.Body.Close() var info NodesInfoResponse if err := json.NewDecoder(res.Body).Decode(&info); err == nil { if len(info.Nodes) > 0 { for nodeID, node := range info.Nodes { if c.snifferCallback(node) { if node.HTTP != nil && len(node.HTTP.PublishAddress) > 0 { url := c.extractHostname(c.scheme, node.HTTP.PublishAddress) // ここでノード情報を取得している if url != "" { nodes = append(nodes, newConn(nodeID, url)) } } } } } } return nodes }
クライアントはolivere/elastic
かelastic/go-elasticsearch
か
スター数が最多であり、実際にelastic/go-elasticsearch
より扱いやすかったのでこちらを採用しました。
ビルダーパターンでクエリを作成できるのがシンプルで分かりやすいです。
例えば、res, err := c.Search().Index(targetIndex).Query(multiMatchQuery).Size(1).Do(context.Background())
のようにクエリを書けます。