Elasticsearchのmemory locking requested for elasticsearch process but memory is not lockedから学んだCapabilityの話

Elasticsearchを起動したらこんなエラーが発生しました。昨日一日くらいずっとこれに頭を悩ましてましたね...。

[2018-02-20T03:39:32,327][WARN ][o.e.b.JNANatives         ] Unable to lock JVM Memory: error=12, reason=Cannot allocate memory
[2018-02-20T03:39:32,329][WARN ][o.e.b.JNANatives         ] This can result in part of the JVM beingswapped out.
[2018-02-20T03:39:32,329][WARN ][o.e.b.JNANatives         ] Increase RLIMIT_MEMLOCK, soft limit: 65536, hard limit: 65536
[2018-02-20T03:39:32,329][WARN ][o.e.b.JNANatives         ] These can be adjusted by modifying /etc/security/limits.conf, for example:
        # allow user 'elasticsearch' mlockall
        elasticsearch soft memlock unlimited
        elasticsearch hard memlock unlimited
[2018-02-20T03:39:32,329][WARN ][o.e.b.JNANatives         ] If you are logged in interactively, you will have to re-login for the new limits to take effect.
(中略)
[2018-02-20T03:39:35,963][INFO ][o.e.b.BootstrapChecks    ] [es-master-7c799755c-zftvl] bound or publishing to a non-loopback or non-link-local address, enforcing bootstrap checks
ERROR: [1] bootstrap checks failed
[1]: memory locking requested for elasticsearch process but memory is not locked
[2018-02-20T03:39:35,968][INFO ][o.e.n.Node               ] [es-master-7c799755c-zftvl] stopping ...
[2018-02-20T03:39:36,020][INFO ][o.e.n.Node               ] [es-master-7c799755c-zftvl] stopped
[2018-02-20T03:39:36,020][INFO ][o.e.n.Node               ] [es-master-7c799755c-zftvl] closing ...
[2018-02-20T03:39:36,029][INFO ][o.e.n.Node               ] [es-master-7c799755c-zftvl] closed
[2018-02-20T03:39:36,030][INFO ][o.e.x.m.j.p.NativeController] Native controller process has stopped- no new native processes can be started

問題

Elasticsearch起動時にメモリのロックがされない。ちなみにElasticsearchはDockerのコンテナで動かしています(ここ重要)

環境

  • Kubernetes 1.8.3
  • Docker 17.03
  • docker.elastic.co/elasticsearch/elasticsearch:5.4.3のイメージを使用
  • ホストはUbuntu 16.04

引き金

Elasticsearchの設定ファイルにbootstrap.memory_lock: trueを書いたため。これはElasticsearchのプロセスアドレス空間をRAMにロックし、スワップを禁止するオプション。

解決策

メモリロックの上限を解放する。また、そのためにコンテナにCapabilityを与える。

1. コンテナへのCapabilityの付与

メモリロックの上限を解放するにはunlimit -l unlimitedで設定可能です。ただ、コンテナ内でこのコマンドを実行するにはCapabilityを与える必要があります。今回はk8sを使っていますのでk8sのpodの設定ファイルに下記の内容を記述します。

capabilities:
  add:
    - IPC_LOCK
    - SYS_RESOURCE

SYS_RESOURCEはメモリロックの上限を変更できる権限、IPC_LOCKはメモリロックを行うための権限となっています。

2. Dockerfileの記述 

bootstrap.memory_lock: trueを記述したelasticsearch.ymlと、Elastic社公式のDockerイメージを使用するとして、このようなDockerfileを書きます。

FROM docker.elastic.co/elasticsearch/elasticsearch:5.4.3
COPY elasticsearch.yml /usr/share/elasticsearch/config/elasticsearch.yml
COPY run.sh /usr/share/elasticsearch/run.sh
CMD ["./run.sh"]
USER root

3行目, 4行目で出てきてるrun.shulimitを実行し、elasticsearchのプロセスを開始させるためだけのシェルスクリプトです(次でもうちょっと詳しく説明します)。

Elasticsearchの公式イメージではプロセスを開始するユーザーがelasticsearchという一般ユーザーになっていますが、このユーザーはulimit -l unlimitedが実行できません。それでDockerfileの5行目の設定を行い、ユーザーをrootに変更しました。

3. run.sh

run.shの内容はこのようになっています。

#!/bin/bash
ulimit -l unlimited
su -c 'bin/es-docker' elasticsearch 

また、bin/es-dockerは間接的にbin/elasticsearchを実行するのですが、bin/elasticsearchの実行はrootユーザーで行うと下記のようなエラーが発生してしまいます

[2018-02-20T10:10:38,862][WARN ][o.e.b.ElasticsearchUncaughtExceptionHandler] [] uncaught exception in thread [main]
org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root
        at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:127) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:114) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:67) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:122) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.cli.Command.main(Command.java:88) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:91) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:84) ~[elasticsearch-5.4.3.jar:5.4.3]
Caused by: java.lang.RuntimeException: can not run elasticsearch as root
        at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:106) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:194) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:350) ~[elasticsearch-5.4.3.jar:5.4.3]
        at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:123) ~[elasticsearch-5.4.3.jar:5.4.3]
        ... 6 more

bin/elasticsearchの実行はelasticsearchというユーザーでなくてはなりません。そんなわけでsuコマンドでユーザーを指定してプロセスを開始しています。

あとはDockerfileをビルドしてコンテナを起動すればおっけーです。

お勉強メモ(Capabilityの話)

ここからはほぼ自分用のメモです

Linux Capability

Linuxにはどんな権限でも持ってる特権ユーザーと非特権ユーザーが存在します。Capabilityは特権ユーザーが持ってる権限を細かく分けたものであり、これを非特権ユーザーに付与することで、非特権ユーザーの動かしたプロセスでもメモリのロックとかシステムの再起動とか、色々な操作ができるようになっちゃいます。

もちろんCapabilityの付与はroot権限じゃないと行えません。

メモリロックの上限の設定(ハードリミットとソフトリミット)

ソフトリミットとは、そのリソースに対してカーネルが課す制限値のことです。 ハードリミットとは、ソフトリミットの上限値のことです 。*1

ソフトリミットを変更するのは非特権ユーザーでも可能なんですが、ハードリミットは特権ユーザーでないとできません。

さて、bootstrap.memory_lock: trueを有効にするためにはメモリロックの上限の設定が必要です。これはソフトリミットだけでなくハードリミットの上限の変更も必要になっています。これらはulimit -lで設定可能です。

また、/etc/security/limits.confというファイルに記述しても設定できるみたいです。この記事の一番上で見せたエラーメッセージの一部分は「limits.confにこんな感じのことを記述してみてね」というものですね。

[2018-02-20T03:39:32,329][WARN ][o.e.b.JNANatives         ] These can be adjusted by modifying /etc/security/limits.conf, for example:
        # allow user 'elasticsearch' mlockall
        elasticsearch soft memlock unlimited
        elasticsearch hard memlock unlimited

ホストでElasticsearchを動かす時は

  • root権限でulimitを実行
  • /etc/security/limits.confに記述して再ログイン

この2通りのやり方でリミットの変更を反映させることができます。ただ、コンテナ内でこの2通りのやり方をそのまま行うことはできません。

コンテナ内のメモリロックの上限の設定

ホストのrootユーザーには当然メモリのハードリミットを変更できる権限がありますが、コンテナ内のrootユーザーにはこの権限がありません(Dockerではコンテナにこの権限を与えません)。そんなわけで、コンテナ内のrootユーザーがulimit -l unlimitedを実行すると下記のようなエラーが発生します。

ulimit: max locked memory: cannot modify limit: Operation not permitted

/etc/seurity/limits.confに記述する方法も無理。

コンテナにCapabilityを与える

デフォルトでDockerコンテナにどの程度のCapabilityが与えられているのかはよくわからないんですけど、Elasticsearchの設定を有効化するためにはCAP_IPC_LOCKCAP_SYS_RESOURCEを与える必要があります。

Capability Key Capability Description
CAP_IPC_LOCK リソース制限の上書き
CAP_SYS_RESOURCE Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2))


Dockerで設定する場合はdocker runのオプション--cap-addで行うことができます。Capability KeyのプレフィックスのCAPは省略してください。

$ docker run --cap-add=IPC_LOCK --cap-add=SYS_RESOURCE <image>

k8sの設定ファイルの場合は上にも示しましたがこのように記述します。

securityContext:
  capabilities:
    add:
      - IPC_LOCK
      - SYS_RESOURCE

おまけ

docker runに--ulimitオプションっていうのもあるみたいですね。ここらへんのissueに書かれている方法でも今回の問題は解決できそうです。

github.com

こっちはdocker-compose.ymlでulimitする場合

github.com

まとめ

しょーもない図ですけどこういう関係があるという話でした

f:id:etogen:20180220182812p:plain

参考

Capabilityとかulimit関連

Elasticsearchの今回の問題に関連する記事

追記

コンテナに与えられているCapabilitiesの確認方法を見つけた

github.com

libcapパッケージをインストールしてcapshを実行すると確認できるみたいです。

$ docker container run --rm -it alpine sh -c 'apk add -U libcap; capsh --print'
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.9/community/x86_64/APKINDEX.tar.gz
(1/1) Installing libcap (2.26-r0)
Executing busybox-1.29.3-r10.trigger
OK: 6 MiB in 15 packages
Current: = cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap+eip
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
Ambient set =
Securebits: 00/0x0/1'b0
 secure-noroot: no (unlocked)
 secure-no-suid-fixup: no (unlocked)
 secure-keep-caps: no (unlocked)
 secure-no-ambient-raise: no (unlocked)
uid=0(root)
gid=0(root)
groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
$

Currentの行が現在有効なCapabilitiesっぽい。案の定CAP_IPC_LOCKとCAP_SYS_RESOURCEは有効ではないですね。

また、docker run--privilegedのオプションをつけることで、コンテナに全てのCapabilitiesを設定することができるようです。

The --privileged flag gives all capabilities to the container, and it also lifts all the limitations enforced by the device cgroup controller. In other words, the container can then do almost everything that the host can do. This flag exists to allow special use-cases, like running Docker within Docker.

docker run | Docker Documentation

よくネットのDockerの入門記事で「--privilegedをつけてコンテナ作っちゃだめだよ」みたいなのを見かけたんですが、何故なのかはわかってなかったのでスッキリしました。