kubernetes環境でcgroupOOMを検出してpodやコンテナと繋げる

内部で子プロセスが作られるコンテナ向けの話

# 環境
kubernetes: eks 1.21
node: amazonlinux2 kernel 5.4

# 前提など
kuberetes環境での Out of memory （以下OOM）は大きく2種類に分かれています

- cgroupOOM
    - podのresourcesに設定したlimitにより引き起こされたOOM
    - コンテナのmemoryがlimitに達すると再起動されるが、その際にOOMが発生している
- systemOOM
    - cgroupOOMでない、kernelがまずいと判断して引き起こすOOM

今回の環境において
systemOOMはkubernetesのイベントに記録されます
cgroupOOMはイベントに記録されませんが、該当コンテナのstatusに再起動がカウントされ、reasonにOOMKilledと入ります
![スクリーンショット 2021-12-22 23.25.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122084/24177386-5fae-90a1-017d-f9335fff7fcb.png)
（kube-state-metricsを使えばprometheusのメトリクスとして扱えます）

# 不便なこと
cgroupOOMにおいて、コンテナが再起動されるのはメインプロセス（コンテナ内のpid:1）がkillされた時のみです
基本的にはこれで事足りるのですが、php-fpmやpythonのceleryなど、メインプロセスから子プロセスが発生して動作するようなものでは足りない場合があります

子プロセスがメモリを使ってOOMkilledされると、メインプロセスが新しく子プロセスを作って終了、となる場合です
実際にphp-fpmやceleryのコンテナを扱っているのですが、メインプロセスがkillされてコンテナが再起動する時もあれば、上記のように子プロセスだけがkillされてコンテナは作り直されないこともあります

そして、この子プロセスだけがkillされる状態は、何もしないと発生を知ることができません
メトリクスを見てメモリの動きが不自然だとか、nodeのsyslogを見るなどで知ることができます

ので、これらを検出して知らせてもらう仕組みを考えます

# 案1. node-probrem-detector

https://github.com/kubernetes/node-problem-detector

syslogやstatsなどを監視して異常を検出することができます
検出したら、イベントとして通知したり、prometheusのメトリクスで出したりできます

お手軽ですが、これでは足りない部分があります
OOMを検出した際に、OOMが起きたことやプロセス名は見てすぐにわかるのですが、たとえばphp-fpmのプロセスが落ちた場合、php-fpmのプロセスが落ちたことしかわからず、どのコンテナのphp-fpmプロセスが落ちたのかは、（メトリクスを見ればなんとなくわかりますが、）syslogを見てコンテナIDを見て、そのIDのコンテナは…と照合していく作業が必要になります
これらが気にならないなら設定も簡単なのでこれでよいかなと思います

# 案2. lokiのrecording rules

https://grafana.com/docs/loki/latest/rules/

上記に比べると設定は多少複雑になりますが、色々と自動で繋げるやり方です
lokiの他に、kube-state-metricsとprometheusが必要になりますが、kubernetesモニタリングとしては基本セットなので許してください
recording rulesが使えるlokiのバージョンは2.3以降です

流れとしては

- promtailでnodeのsyslogをtargetに追加して保存する
- recording rulesでoomのログをメトリクス化してprometheusに保存
- kube-state-metricsのメトリクス `kube_pod_container_info`　とjoinしてoomkilledのメトリクスとコンテナを紐付ける
- あとはそれを元に通知するなりなんなり

といった流れです
細かく見ていきます

### syslogをtargetに追加

journalの場合はpromtailのconfigにこんな感じのを追加で取得できます

```yaml:promtail-config:yaml
    - job_name: journal
      journal:
        path: /var/log/journal
        max_age: 12h
        labels:
          job: systemd-journal
      relabel_configs:
        - source_labels: ['__journal__systemd_unit']
          target_label: 'unit'
        - source_labels: ['__journal__hostname']
          target_label: 'hostname'
```

journalでない場合はわかりません :rolling_eyes: 

### lokiのrecording rulesを設定

recording rulesを有効化する設定は終わっているものとして
（これに関しては別の記事で書くかも）

このようなruleを設定します

```yaml:rules.yaml
groups:
  - name: oomkilling
    rules:
      - record: oom_killed_container_count_info
        expr: sum by(uid,cid)(count_over_time({job="systemd-journal"} |~ "^oom-kill:" | regexp ".+task_memcg=/kubepods(/burstable)?/pod(?P<uid>[0-9,a-f].+)/(?P<cid>[0-9,a-f].+),task.+" [1m]))
```

これで、OOMが発生すると、`oom_killed_container_count_info`という名前で、uid（=podのid）とcid（=コンテナのid）をlabelにしたOOMKillerがどのプロセスをkillしたのかのカウンター（厳密にはゲージ）メトリクスが作成されます
（今回結局uidは使わなかったんですけど）

### kube-state-metricsのメトリクスと繋げる

以下のようなクエリでkube-state-metricsが出すコンテナ情報のメトリクスとjoinします

```prometheus:PromQL例
avg by(pod,container)(
  label_replace(kube_pod_container_info,"cid","$2","container_id","(docker|containerd)://(.+)")
  * on(cid) group_left
  oom_killed_container_count_info
)
```

`kube_pod_container_info`のcontainer_idは`docker://`や`containerd://`が頭に付いてるので、label_replaceで取り除く必要があります

これで、どのpodのどのコンテナでOOMKilledが起きたかがすぐにわかるメトリクスが完成しました

　![スクリーンショット 2021-12-23 0.33.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122084/eec92e94-f957-4c7a-73c9-60e68610bbd0.png)

# 更に問題

この方法の結構な問題は、syslogをgrepとparseしているところで、
なにが問題かというと、OOMが発生した時のログ出力って意外とちょいちょい変更が入るんですよね
なので突然検出できなくなる可能性は大いにあります:innocent:

# 根本解決は

cgroupOOMも情報出してくれ、というkubernetesのissueがあります
https://github.com/kubernetes/kubernetes/issues/100483