kindでelasticsearchを動かす

Kubernetes IN Docker(kind)を使えば、helmとかで簡単にk8sにelasticsearchを動かせるかなと思い、試してみる。

elasticsearchのhelmチャートはhelm/stableとelasticの2種類あるが、現時点ではelasticの方は動かなかった…。elasticの方にはexamplesにはkindの例があるのだが、これもダメなので、がんばって調べないと動かせなそうなので、一旦、これは見送って、素直にhelm/stableを試す。

kindやhelmなどの必要なものは入れてある前提で、まずは、kindでk8sのクラスタを作成する。

$ kind create cluster 
 Creating cluster "kind" …
  ✓ Ensuring node image (kindest/node:v1.15.3) 🖼
  ✓ Preparing nodes 📦 
  ✓ Creating kubeadm config 📜 
  ✓ Starting control-plane 🕹️ 
  ✓ Installing CNI 🔌 
  ✓ Installing StorageClass 💾 
 Cluster creation complete. You can now use the cluster with:

 export KUBECONFIG="$(kind get kubeconfig-path --name="kind")"
 kubectl cluster-info

という感じでクラスタが簡単にできる。kindでいろいろな設定でクラスタを作ることもできるらしい。何も指定しなければ、kindという名前のクラスタができる。

$ export KUBECONFIG="$(kind get kubeconfig-path --name="kind")"
$ kubectl cluster-info
 Kubernetes master is running at https://127.0.0.1:36777
 KubeDNS is running at https://127.0.0.1:36777/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

 To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

KUBECONIGを設定すれば、kubectlコマンドが利用できる。あとはhelmを使うので、tillerに権限を付けて初期化しておく。

$ kubectl -n kube-system create serviceaccount tiller
 serviceaccount/tiller created
$ kubectl create clusterrolebinding tiller --clusterrole cluster-admin --serviceaccount=kube-system:tiller
 clusterrolebinding.rbac.authorization.k8s.io/tiller created
$ helm init --service-account=tiller --wait
 $HELM_HOME has been configured at /home/shinsuke/.helm.

 Tiller (the Helm server-side component) has been installed into your Kubernetes Cluster.

 Please note: by default, Tiller is deployed with an insecure 'allow unauthenticated users' policy.
 To prevent this, run helm init with the --tiller-tls-verify flag.
 For more information on securing your installation see: https://docs.helm.sh/using_helm/#securing-your-helm-installation

これ以降は普通にhelmでインストールすれば良い。

$ helm install --name my-release stable/elasticsearch
...
 From outside the cluster, run these commands in the same shell:

 export POD_NAME=$(kubectl get pods --namespace default -l "app=elasticsearch,component=client,release=my-release" -o jsonpath="{.items[0].metadata.name}")
 echo "Visit http://127.0.0.1:9200 to use Elasticsearch"
 kubectl port-forward --namespace default $POD_NAME 9200:9200 

という感じで、上記にあるようにkubectl port-forwardを実行すれば、localhost:9200でアクセスして確認することができる。

k8sのクラスタを破棄するには

$ kind delete cluster

とすれば削除できる。

Can’t merge because of conflicts: [Cannot update includes setting for [_source]]

$ curl -XPUT -H "Content-Type:application/json" localhost:9201/test_put_mapping2/_mapping -d '{"_source":{"includes":["aaa"]}}'

みたいな感じで、PUT Mappingでmappings/_sourceを変更すると、

{"error":{"root_cause":[{"type":"illegal_argument_exception","reason":"Can't merge because of conflicts: [Cannot update includes setting for [_source]]"}],"type":"illegal_argument_exception","reason":"Can't merge because of conflicts: [Cannot update includes setting for [_source]]"},"status":400}

という感じで怒られるけど、ここによると_sourceは変更できないから。Create Indexでやる必要がある。

ElasticsearchのログをGKEでStackdriverで取り込む

GKEとかで普通にElasticsearchを使って、コンソールのログをStackdriverで回収してもらえば、Logs Viewerで見ることができると思うけど、ログレベルがINFOかERRORの2択になる。つまり、WARNとかのログはINFOとして出てしまう。これはStackdriverに投げているfluentdがログメッセージのパースができなければ、stdoutならINFOで、stderrならERRORみたいな決め方になっているためで、fluentd-configmap.yamlあたりをみればわかる。

それではどうするか?だけど、fluentd-configmap.yamlには

format /^(?<severity>\w)(?<time>\d{4} [^\s]*)\s+(?<pid>\d+)\s+(?<source>[^ \]]+)\] (?<log>.*)/

という感じでパースするフォーマットが書いてあるのでこれに合わせる必要がある。<time>\d{4}って、何を指しているのだろうとかも思ったけど、MMddだった。

Elasticsearchのでデフォルトのログフォーマットとかはlog4j2.propertiesを見ると

appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] [%node_name]%marker %m%n

という感じになっていると思うけど、この辺のログフォーマットを

appender.console.layout.pattern = %.-1p%d{MMdd HH:mm:ss.SSSSSS} 1 [%node_name] %m%xThrowable{separator(|)}%n

みたいな感じで、patternたちを変更しておく。ログレベルが始めの1文字で表されている。pidはとりあえず適当に1とかにしたけど、きちんと取得してもよいのかも(試してない)。あとは、スタックトレースが1行内に収めないと、Stackdriver上で分解されれるので1行にしておく。

これも普通にぐぐると、エージェントを組み込むとか、GKEのfluentdの設定をカスタマイズするとかがほとんどな気がするので、このログフォーマット問題は結構ハマる気がする…。

_sourceからデータを消しておく

クエリー時に_sourceに含めないという設定もあるけど、インデックス時に_sourceに入れないでおくという方法もある。Fess 13ではこの機能を使ってハイブリットな言語用インデックスで検索する予定ではある。

で、使うためにはElasticsearchのサイトにもあるような感じで

$ curl -X PUT "localhost:9200/fess" -H 'Content-Type: application/json' -d'
{
"mappings": {
"_doc": {
"_source": {
"excludes": [
"content_*",
"title_*"
]
}
}
}
}
'

のようにすれば、title_〜とcontent_〜のプロパティは_sourceに保存されなくなる。

でも、Fessではcontent_lengthは除外したくないので、

$ curl -X PUT "localhost:9200/fess" -H 'Content-Type: application/json' -d'
{
"mappings": {
"_doc": {
"_source": {
"includes": [
"content_length"
],
"excludes": [
"content_*",
"title_*"
]
}
}
}
}
'

としたところ、全部が消えることになり、期待する動きとは違っていた…。つまり、includes > excludesの順に処理するっぽい。というわけで、Fessではexcludesにワイルドカードを使わずに明示して対応した。

cluster.routing.use_adaptive_replica_selection

Elasticsearch 6までは複数のレプリカがある場合、レプリカの使われ方はラウンドロビンで順番に使われます。たとえば、
・シャード1P, シャード1R1 シャード1R2
・シャード2P, シャード2R1 シャード2R2
のように2シャードの3レプリカの場合は、1回目の検索で1P&2P, 2回めの検索で1R1&2R1, 3回目の検索で1R2&2R2みたいな感じで使われることになります(実際には非同期で処理されるのでシャード1と2ではそのときのが選択されて場合によってはPとRはずれる気はするけど)。という感じで、デフォルトはラウンドロビンということ。

Elasticsearch 7からはラウンドロビンでなくなり、それを6でも試すなら、cluster.routing.use_adaptive_replica_selectionをtrueにする。7ではtrueになっている。

curl -X PUT "localhost:9200/_cluster/settings" -H 'Content-Type: application/json' -d'
{
"transient": {
"cluster.routing.use_adaptive_replica_selection": true
}
}
'