1
/
5

Elasticsearch on Kubernetes の本番運用方法

Photo by Nathan Dumlao on Unsplash

Elasticsearch はオープンソースの分散型検索分析エンジンです。いろんな形式のデータを保持することができ、複数のフィールドを指定した検索クエリが簡単に書けたり、文字数の多いデータでも高速に検索ができるといった特徴があります。これらの利点を活かして Wantedly でも検索機能や推薦機能の裏側で利用しています。

この記事では Wantedly の本番環境で運用している Elasticsearch について構成と運用についてのノウハウを紹介します。

背景

Wantedly では Elasticsearch を Kubernetes 上で運用しています。以前は EC2 上で AutoScaling Group を利用して直接クラスタを構成し、内製ツール (https://github.com/dtan4/esnctl) によって管理していました。しかし、マイクロサービス化の推進によって EC2 上で運用されていたアプリケーションが Kubernetes に移行され、 EC2 等の AWS の機能に頼った運用よりも Kubernetes を利用した方が運用コストが低くなるという経緯があり、Elasticsearch についても Kubernetes 上での運用に移行しました。

Kubernetes 上で Elasticsearch を運用する方法としては Elastic 社が提供している Elastic Cloud on Kubernetes (ECK) がありますが、ECK では古いバージョンの Elasticsearch が利用できないという制約があります。Wantedly では古いバージョンのクラスタも運用しているため、StatefulSet という機能を用いて Kubernetes 上に Elasticsearch クラスタを構築しています。

コード管理

Wantedly では用途ごとに Elasticsearch クラスタを分割しており、複数のクラスタを運用しています。それぞれのクラスタごとに固有の設定が存在しますが、同じ設定を再利用している部分がほとんどです。また、各クラスタのメンテナンスはインフラチームがまとめて責任を持っており、クラスタ間の差分を明確化し管理しやすくするために単一リポジトリで設定情報をコード管理しています。

各 Elasticsearch クラスタの環境差異を管理するために kustomize を利用してクラスタを構成するために必要な manifest file を生成します。kustomize は Kubernetes で扱う YAML を生成するツールで、ベースとなる manifest にパッチを加えて最終成果物の manifest を作ることができます。Wantedly ではベースの構成に3回パッチを当てて 各 Elasticsearch クラスタを作るための manifest を生成しています。

  1. ベースの構成
  2. Elasticsearch のバージョンごとのパッチ
  3. Elasticsearch クラスタ (= namespace) ごとのパッチ
  4. Kubernetes クラスタ (本番環境, 検証環境, 開発環境) ごとのパッチ

次の YAML は v7.8.1 用のパッチの例です。使用する docker image の tag は Elasticsearch のバージョンにより異なるため、それぞれのバージョンでパッチを当てています。

resources:
- ./../base
images:
- name: wantedly/elasticsearch
  newTag: 7.8

また、 kustomize はパッチを当てる単位でディレクトリを分ける必要があるため、以下のように構成しています。

.
├── base
│   ├── kustomization.yaml
│   ├── pdb.yaml
│   ├── service.yaml
│   └── statefulset.yaml
├── v5.2
│   ├── Dockerfile
│   └── kubernetes
│       └── kustomization.yaml
├── v6.8
│   ├── Dockerfile
│   └── kubernetes
│       └── kustomization.yaml
├── v7.8
│   ├── Dockerfile
│   └── kubernetes
│       └── kustomization.yaml
└── namespaces
    ├── cluster1
    │       ├── base
    │       │   └── kustomization.yaml
    │       └── production
    │           └── kustomization.yaml
    └── cluster2
    │       ├── 
   ︙     ︙

クラスタ構成の方法

Elasticsearch のクラスタ構成には Zen Discovery を利用しています。Zen Discovery は Elasticsearch に組み込まれているノード検索の仕組みで、ファイルの内容やDNSのAレコードからホストの一覧を取得し、この一覧に合わせるようにノードの追加や削除を行います。Elasticsearch 7.x 以降は Zen Discovery が廃止されましたが、同等の機能は discovery.seed_hosts で提供されています。

Kubernetes ではクラスタ構成に利用するホストの一覧は Headless Service という機能によって提供します。通常のKubernetes の Service は ClusterIP を持ち、ClusterIP 宛に送信されたパケットを iptables や IPIPトンネルなどにより pod の IP アドレスに振り分けます。Service の名前解決をしようとすると ClusterIP が返却されますが、これは Elasticsearch クラスタを構成するノードの一覧ではなくノードにネットワークトラフィックを流すための代表アドレスとなるためうまくクラスタを構築できません。ここで ClusterIP に “None” を指定することで Headless Service として動作させることで、Service の代表アドレスとなる ClusterIP を持たなくなり、名前解決をすると関連する pod の IPアドレス一覧が返されるようになります。

apiVersion: v1
kind: Service
metadata:
  name: nodes
spec:
  ports:
  - port: 9200
    protocol: TCP
    targetPort: 9200
    name: http
  - port: 9300
    protocol: TCP
    targetPort: 9300
    name: cluster
  selector:
    app: elasticsearch
  type: ClusterIP
  publishNotReadyAddresses: true
  clusterIP: None

ノード入れ替え時のデータ欠損回避

Elasticsearch では index に対してデータを格納します。index には実際にデータを分散して保持するために shard が複数存在し、各 shard はクラスタを構成する各ノードに配置されます。

ここで注意したい点として、shard が配置されているノードがクラスタから外れると最悪の場合データ欠損が起きてしまうことが挙げられます。実際の運用では replica shard を利用することが多いためノード1台がクラスタから外れただけで問題が起きることは少ないですが、同時に複数台のノードがクラスタから外れるとデータ欠損に繋がります。特に、Kubernetes で運用する場合は rolling update 等によって Elasticsearch クラスタのノードとなる pod が頻繁に削除されるため、データ欠損の対策が必要になります。

Wantedly ではデータ欠損を起こさずノードを入れ替えるために Cluster settings update API を利用して shard の退避を行っています。

Cluster settings update API による shard 退避の例
https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-update-settings.html

PUT /_cluster/settings
{
  "transient": {
    "cluster.routing.allocation.exclude._name": "node-1"
  }
}

この API を実行することで指定した node を shard のアサイン対象から除外し、除外されたノードからは shard が退避されていきます。退避が終わったノードには shard がアサインされていないためクラスタから安全に切り離すことができます。

Wantedly ではこの操作を pod の preStop と postStart に指定することで、データ欠損を起こさずに安全にノードの入れ替えができる構成になっています。preStop は pod の終了タイミングに処理を挟む機能で、指定した処理が終わってから pod の停止が実行されるため graceful shutdown に利用されます。preStop には Cluster settings update API を実行して shard の退避が終わるまで待つスクリプトを指定しています。

ここで、除外設定はクラスタからノードが外れた場合も残り続けるため注意が必要です。Wantedly で運用している Elasticsearch の構成上、ノードの入れ替え時に新しく起動するノードは元のノード名を再利用するため、起動時に除外設定が適用されて shard が配置されません。このため、postStart に除外設定からノードを消す処理を入れて新しく起動したノードにも shard が配置できるようにしています。

shard の退避は curl で POST リクエストを実行することで実現はできますが、結果をパースして完了を待つ処理を書くとコードが長くなり YAML の中に入れると可読性が下がってしまうため、 esnode という shell script を作って preStop と postStart に指定しています。

︙
lifecycle:
  postStart:
    exec:
      command: ["esnode", "join"]
  preStop:
    exec:
      command: ["esnode", "leave"]
︙

まとめ

Wantedly で運用している Elasticsearch の構成について、特に Kubernetes 上で本番運用するための工夫について紹介しました。Elasticsearch はマネージドサービスとして運用されている場合も多く、自前で運用するための情報があまり出回っていないため、同じように困っている人に届けばいいなと思っています。

今回の記事には書いていませんが、Kubernetes 上で Elasticsearch を運用することによってよかったことや、逆に困ったこともあるので、興味があればぜひ話を聞きに来てください。

Wantedly, Inc.では一緒に働く仲間を募集しています
23 いいね!
23 いいね!
同じタグの記事
今週のランキング