Zeals TECH BLOG

チャットボットでネットにおもてなし革命を起こす、チャットコマース『Zeals』を開発する株式会社Zealsの技術やエンジニア文化について発信します。現在本ブログは更新されておりません。新ブログ: https://medium.com/zeals-tech-blog

GKE で OOMKilled されたコンテナを検知

こんにちは、分析基盤を担当しながらインフラも面倒見ている鍵本です。

本日は Google Kubernetes Engine(GKE) 環境において OOMKilled されたコンテナを検知する方法についてお話します。

はじめに

弊社サービス Zeals は定期バッチを含む全サービスがGKE上で起動されています。

tech.zeals.co.jp

Rails サーバには puma_worker_killer を導入して一定以上のメモリ使用量になったら再起動していますが、それ以外のアプリケーションについては何もケアしておりません。特に定期バッチでは、その時に使用するデータ量に応じてメモリ使用量が変化しますので、場合によってはメモリを使いすぎて異常終了することもあります。この場合、当該コンテナはホストサーバ(ノード)から OOMKilled されます。この時、原因となる問題を取り除いて再実行したいところですが、そもそも OOMKilled されたということを検知する仕組みがないので、それも簡単には叶いません。

既存の方法を試した結果

OOMKilled を検知する方法は本当にないのでしょうか?

Google 先生に聞いてみたところ、OOMKilled を検知してくれそうなアプリケーションには以下があります。

すべて試したのですが、結論としては検知できませんでした。以下、そのように判断した理由です。

kubernetes-event-exporter

どのノードで OOMKilled が発生したかはわかりますが、どのコンテナが対象なのかがわかりません。

kubernetes-oom-event-generator

初めて発生した OOMKilled は検知できませんが、二回目以降のイベントを検知することができます。つまり常駐型のアプリケーションに対して監視したい場合には利用可能ですが、cronjob リソースのように実行が一度きりのものについては役に立ちません。

kubernetes-oomkill-exporter

OOMKilled されてないコンテナに対しても OOMKilled されたというログメッセージが出続けます。これはコンテナの exit コードが 137 というだけで OOMKilled と判定されてログ出力されているのではないかと推察されます。具体的にどういう状態かについてはわからないのですが、おそらくメモリ使用量がオーバーコミットした時に起きているのではないかと考えております。これは後述の検知アプリケーションを自作したことでその結論に至りました。

なお、これを利用する場合には Alpine イメージではなく Debian 等のイメージを使う必要があります。以下のようなエラーが発生します。

standard_init_linux.go:211: exec user process caused "no such file or directory"

自作してみた

目的に適うものが存在しないので kubernetes-oomkill-exporter を参考にして作ってみました。使用した言語は go です。

処理の流れは以下の通りです。 1. ノードの /var/run/docker.sock をマウントしたコンテナを DaemonSet として起動 2. docker API でコンテナリストを取得 3. exit コードが 137 のコンテナを抽出 4. 上で抽出したコンテナIDからコンテナの情報を docker API で取得 5. State.OOMKilled が true ならば標準出力にログメッセージを出力する 6. 2〜5 を定期的に繰り返す

docs.docker.com

具体的にコードを示しながらいくつか説明していきましょう。

コンテナリストの取得

コンテナリストを取得するには、ContainerList 関数(API はこちら)を使用します。我々はすでに終了している cronjob についても情報が欲しいので、All オプションを true にして全て取得します。

import (
    docker_types "docker.io/go-docker/api/types"
)

func (dm *DockerMonitor) monitor() error {
    ctx := context.Background()
    containers, err := dm.client.ContainerList(ctx, docker_types.ContainerListOptions{All: true})
    ...
}

dm.client は以下のように生成しています。

import (
    docker_client "docker.io/go-docker"
)

    client, err := docker_client.NewEnvClient()

コンテナ情報の取得

コンテナ情報を取得するには ContainerInspect 関数(APIはこちら)を使用します。 

    for _, c := range containers {
        containerInspect, err := dm.client.ContainerInspect(ctx, c.ID)
    }

ログ出力

containerInspect.State.OOMKilled が true ならば OOMKilled されたコンテナと判断できますので、その旨ログ出力をします。ただし containerInspect.State.FinishedAt を見て古いもの(現在の時刻との差が実行間隔以上離れてる場合)については出力をしないようにします。

func setAlertMessage(containerInspect docker_types.ContainerJSON) (alertMessage, error) {
    var message alertMessage

    now := time.Now()
    finishedAt, _ := time.Parse(time.RFC3339Nano, containerInspect.State.FinishedAt)
    if now.Sub(finishedAt) > MONITOR_INTERVAL {
            return message, fmt.Errorf("Error message is too old, thus skipped.")
    }

    if !containerInspect.State.OOMKilled {
            return message, fmt.Errorf("Exit code is 137, but the container, %s, was not OOMKilled.", containerInspect.Config.Labels["io.kubernetes.pod.name"])
    }
    ...[message を作成]...

    return message, nil
}

まとめ

GKE 環境で OOMKilled されたコンテナを検出する仕組みが存在しないようだったので、go言語で自作してみました。これで Cloud Logging にログが出力されるようになったので、たとえば Cloud Log Router + Cloud Pub/Sub + Cloud Functions を活用することで Slack に通知させることができそうです。

参考文献