2019年6月16日日曜日

k8s 上に ElasticSearch7.1 クラスタを構築してみた

概要

前回 docker-compose で構築しました
今回は k8s 上に構築するのにチャレンジしました
StatefulSet + Headless Service で構築しています
シングル構成から複数台構成まで順を追って説明します

環境

  • macOS 10.14.5
  • minikube 1.1.0
  • ElasticSearch 7.1

とりあえず run (Deployment)

まずは kubctl run でシングル構成で立ち上げます

  • kubectl run elasticsearch --image=docker.elastic.co/elasticsearch/elasticsearch:7.1.1 --env="discovery.type=single-node" --port=9200

kubectl run は Deployment を使っています
YAML 定義を確認するには -o yaml --dry-run オプションを付与します

作成されたリソースを確認するには Deployment, ReplicaSet, Pod を確認します

  • kubectl get deploy,rs,po
NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/elasticsearch   1/1     1            1           12s

NAME                                             DESIRED   CURRENT   READY   AGE
replicaset.extensions/elasticsearch-775d564d6c   1         1         1       12s

NAME                                 READY   STATUS    RESTARTS   AGE
pod/elasticsearch-775d564d6c-h4hxs   1/1     Running   0          12s

動作確認は exec を使って直接 Pod の localhost にアクセスしてみましょう

  • kubectl exec elasticsearch-775d564d6c-h4hxs curl localhost:9200/_cat/health
  • kubectl exec elasticsearch-775d564d6c-h4hxs curl localhost:9200/_cat/nodes

リソースの削除は Deployment を削除すれば OK です

  • kubectl delete deploy elasticsearch

シングル構成 (Pod)

次に YAML ファイルを定義してシングル構成を構築してみます
先程 run で Deployment 経由の構築はしたので今回は Pod リソースを使った YAML ファイルを定義してみます
※先程の run に --restart=Never を付与すれば Pod のみになりますが勉強のため自分で YAML ファイルを作成しています

  • vim es_master_pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: es-pod
  labels:
    app: es
spec:
  containers:
  - name: es
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    env:
    - name: discovery.type
      value: single-node
    ports:
    - containerPort: 9200
      name: api
    - containerPort: 9300
      name: gossip
  restartPolicy: Never

ポイントは envdiscovery.type を渡している点です
これを apply しましょう

  • kubectl apply -f es_master_pod.yml

作成されるリソースは Pod になります

  • kubectl get po
NAME     READY   STATUS    RESTARTS   AGE
es-pod   1/1     Running   0          4s

動作確認します

  • kubectl exec es-pod curl localhost:9200/_cat/health

リソースの削除は Pod を削除すれば OK です

  • kubectl delete po es-pod

シングル構成 (StatefulSet)

次にシングル構成を StatefulSet を使って構築します
今回は 3 台構成のクラスタを StatefulSet を使って構築します
そのための準備としてまずはシングル構成を構築します

  • vim kubectl apply -f es_master_sts1.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: es-cluster
spec:
  selector:
    matchLabels:
      app: es
  serviceName: "es-cluster"
  replicas: 1
  template:
    metadata:
      labels:
        app: es
    spec:
      containers:
      - name: es
        image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
        env:
        - name: discovery.type
          value: single-node
        ports:
        - containerPort: 9200
          name: api
        - containerPort: 9300
          name: gossip
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

PVC (PersistentVolumeClaim) を追加しています
template.spec.containers は先程の Pod の定義とほぼ同じです
selector.matchLabelstemplate.metadata.labels は同じになるようにします
これで起動してみましょう

  • kubectl apply -f es_master_sts1.yml

作成されるリソースは StatefulSet, Pod, PVC になります

  • kubectl get sts,po,pvc
NAME                          READY   AGE
statefulset.apps/es-cluster   1/1     49s

NAME               READY   STATUS    RESTARTS   AGE
pod/es-cluster-0   1/1     Running   0          48s

NAME                                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data-es-cluster-0   Bound    pvc-ae9aa9e8-8e3a-11e9-8372-080027d5c49e   1Gi        RWO            standard       48s

動作確認しましょう

  • kubectl exec es-cluster-0 curl localhost:9200/_cat/health

ホスト名も確認してみましょう
StatefulSet を使っているのでホスト名が固定されているのが確認できると思います

  • kubectl exec es-cluster-0 hostname

削除は StatefulSet と PVC を削除すれば OK です

  • kubectl delete sts es-cluster
  • kubectl delete pvc data-es-cluster-0

3 台クラスタ構成 (StatefulSet + Headless Service)

さてここからが本番です
3 台構成のクラスタを StatefulSet を使って構築します
また Headless Service も使います
Headless Service を使うとホスト名+サブドメイン (サービス名) で他の Pod にアクセスすることができます
クラスタを構成するにあたり IP ではなくホスト名を使いたいので Headless Service を使います

  • vim es_master_svc.yml
apiVersion: v1
kind: Service
metadata:
  name: es-cluster
  labels:
    app: es
spec:
  clusterIP: None
  ports:
  - port: 9200
    name: api
  - port: 9300
    name: gossip
  selector:
    app: es

まずは Headless Service を作成します

  • kubectl apply -f es_master_svc.yml

これで同じ namespace 配下の Pod 間で名前解決ができるようになります
次に StatefulSet を作成します

  • vim kubectl apply -f es_master_sts3.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: es-cluster
spec:
  selector:
    matchLabels:
      app: es
  serviceName: "es-cluster"
  replicas: 3
  template:
    metadata:
      labels:
        app: es
    spec:
      containers:
      - name: es
        image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
        env:
        - name: HOSTNAME_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: node.name
          value: $(HOSTNAME_NAME).es-cluster
        - name: cluster.name
          value: k8s-cluster
        - name: discovery.seed_hosts
          value: es-cluster-0.es-cluster,es-cluster-1.es-cluster,es-cluster-2.es-cluster
        - name: cluster.initial_master_nodes
          value: es-cluster-0.es-cluster,es-cluster-1.es-cluster,es-cluster-2.es-cluster
        - name: ES_JAVA_OPTS
          value: "-Xms512m -Xmx512m"
        ports:
        - containerPort: 9200
          name: api
        - containerPort: 9300
          name: gossip
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

だいぶ長くなりました
が一つずつ見ればそこまで複雑ではありません
serviceName: "es-cluster" は Headless Service の metadata.name と同じにします
3 台構成にするので replicas: 3 にしましょう
一番のポイントは env になります
ElasticSearch のクラスタを構成する場合 node.namediscovery.seed_hosts, cluster.initial_master_nodes そしてマシンの hostname はすべて合わせたほうが無難です
IP でもできるようですが個人的にはハマりやすいのでできれば名前でアクセスできるようにしましょう
今回はそのために Headless Service を使っています

まず HOSTNAME_NAMEfieldPath: metadata.name でホスト名を環境変数に設定しています
今回の 3 台構成であれば es-cluster-0, es-cluster-1, es-cluster-2 がそれぞれ動的に代入されます
そしてそれを使って node.name$(HOSTNAME_NAME).es-cluster にしています
なぜわざわざマシンのホスト名にサブドメインの .es-cluster を付与しているのかというと Headless Service の場合他の Pod にアクセスするにはホスト名だけではなくサブドメインも必要になります
なので discovery.seed_hostscluster.initial_master_nodeses-cluster-0.es-cluster という感じにしなければクラスタに所属させるホストを見つけることができません
でここに指定した値と node.name は同じにする必要があるためわざわざこのような方法にしています
(もしかするともっと簡単な方法があるかもしれませんが)

  • kubectl apply -f es_master_sts3.yml

あとはこれを適用して確認してみます

  • kubectl get sts,po,pvc,svc
NAME                          READY   AGE
statefulset.apps/es-cluster   3/3     45s

NAME               READY   STATUS    RESTARTS   AGE
pod/es-cluster-0   1/1     Running   0          45s
pod/es-cluster-1   1/1     Running   0          40s
pod/es-cluster-2   1/1     Running   0          35s

NAME                                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data-es-cluster-0   Bound    pvc-f581cf6c-8e3c-11e9-8372-080027d5c49e   1Gi        RWO            standard       45s
persistentvolumeclaim/data-es-cluster-1   Bound    pvc-f8371d19-8e3c-11e9-8372-080027d5c49e   1Gi        RWO            standard       40s
persistentvolumeclaim/data-es-cluster-2   Bound    pvc-fae468fa-8e3c-11e9-8372-080027d5c49e   1Gi        RWO            standard       36s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
service/es-cluster   ClusterIP   None         <none>        9200/TCP,9300/TCP   17h
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP             17h

クラスタを構築するまでに少し時間がかかるので待ちましょう

  • kubectl exec es-cluster-0 curl -- -s localhost:9200/_cat/health
1560473021 00:43:41 k8s-cluster green 3 3 0 0 0 0 0 0 - 100.0%
  • kubectl exec es-cluster-0 curl localhost:9200/_cat/nodes
172.17.0.7 26 98 36 1.61 2.13 1.32 mdi * es-cluster-1.es-cluster
172.17.0.6 14 98 41 1.61 2.13 1.32 mdi - es-cluster-0.es-cluster
172.17.0.8 16 98 33 1.61 2.13 1.32 mdi - es-cluster-2.es-cluster

こんな感じで取得できるようになれば OK です

minikube を使っている場合

minikube の場合初期リソースが少ないため以下を実施しないとクラスタ構成時に Pod が起動しませんでした

メモリ増設

  • minikube config set memory 4096
  • minikube delete
  • minikube start

max_map_count 増設

  • minikube ssh
  • sudo sysctl -w vm.max_map_count=262144

failover を確認する

master になっている Pod を 1 台削除してみましょう

  • kubectl delete po es-cluster1

StatefulSet なので Pod が削除されても自動で復活します
再作成中はしばらく 2 台構成のクラスタ状態になります
復活したら再度 health と nodes を確認してみましょう

  • kubectl exec es-cluster-0 curl -- -s localhost:9200/_cat/health
1560473297 00:48:17 k8s-cluster green 3 3 0 0 0 0 0 0 - 100.0%
  • kubectl exec es-cluster-0 curl -- -s localhost:9200/_cat/nodes
172.17.0.6 19 98 32 1.05 1.43 1.20 mdi - es-cluster-0.es-cluster
172.17.0.8 23 98 32 1.05 1.43 1.20 mdi * es-cluster-2.es-cluster
172.17.0.7 14 98 26 1.05 1.43 1.20 mdi - es-cluster-1.es-cluster

failover し master が es-cluster-1 から es-cluster-2 になっているのがわかります
また再作成された es-clsuter-1 も自動的にクラスタに再ジョインいしていることがわかります
リソースの削除は以下の通りです

  • kubectl delete sts es-cluster
  • kubectl delete pvc data-es-cluster-0 data-es-cluster-1 data-es-cluster-2
  • kubectl delete svc es-clsuter

トラブルシューティング

今回の 3 台構成にたどり着くまでに何度もトライアンドエラーしました
今回のポイントは Headless Service を使ってホスト名+サブドメインでクラスタ間のアクセスをする点かなと思います
あとは minikube のリソース周りもはまりポイントかなと思います

これらのミスに気づいた方法としては minikube ssh してログインし直接 docker logs で ElasticSearch コンテナのログを確認していました
StatefulSet は Pod の作成に失敗すると自動でコンテナを再作成します
なのでコンテナが削除される前に docker logs -f f93400366819 でログを眺めるのが一番良いと思います
リソースが足りない場合にエラーは大抵これで解決できます

StatefulSet の場合 restartPolicy を Never に設定できません
なので今回のように restartPolicy=Never にできる Pod リソースだけを使って原因調査するのもありかなと思います

最後に

ElasticSearch7.1 のクラスタ構成を k8s 上に構築してみました
記事内では StatefulSet + Headless Service を使って構築しています
調べると他にも CRD (Custom Resource Definition) を使ったり Deployment を使って構築しているサンプルもありました
個人的に試した感じだとクラスタ間はホスト名を使ってアクセスできるようにしたほうが運用も楽になる感じがするのが自分は StatefulSet + Headless Service のリソースを使って構築した感じです

この辺りのリソースやコントローラのチョイスは経験が大きく影響するので自分にあったものを選択するのが良いかなと思います

参考サイト

1 件のコメント:

  1. この記事のおかげでうまくいきました。本当に助かりました。

    返信削除