🤖

🤖

:gijutsu_burogu:

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へ接続しに行きます。

github.com

   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)が、それらのノードは外部からは直接アクセスできないため接続に失敗します。

f:id:kotaroooo0:20200915002437p:plain
127.0.0.1:9200以外からElasticsearchにアクセスできない

ここで取得されるノードとは具体的には以下のリクエストで取得されます。 この例では、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())のようにクエリを書けます。

参考

Sniffing · olivere/elastic Wiki · GitHub

Docker · olivere/elastic Wiki · GitHub