Fluentdサイドカー付きJob Podを削除したい
問題
バッチ処理などを行う時に便利なKubernetesのJob Podに、Fluentdなどのログ収集プログラムを動かすためのサイドカーコンテナを付けることはよくあるパターンだと思う。しかし、このパターンには問題がある。それはメインコンテナの処理が終了してもサイドカーコンテナが生き続けるため、.spec.ttlSecondsAfterFinished
オプションを付けていてもJob Podがずっと残りしてまうことである。
既存の解決策
この「サイドカーがあるとJob Podが消えない問題」の解決策を調べていると大きく分けて2つの解決策があることがわかった。
- メインコンテナの処理が終わったら共有ボリュームに適当なファイルを作成し、サイドカーコンテナではそのァイルが作成されたことを検知してプロセスを終了させるプログラムを動かす
- Pod内のコンテナでPID Namespaceを共有し、メインコンテナの処理が終わったらサイドカーコンテナのプロセスを落とす
(また、コンテナにタイプを割り振ることでメインコンテナが終了したタイミングでサイドカーコンテナを終了させる機能が提案されていたがクローズされていた。)
今回の解決案
ログ収集にFluentdのtailプラグインを使っている場合、posファイルというものを使うとFluendはその情報をもとにログをどこまで読み込んだかを管理するので、ログの欠損や重複読み取りを防ぐことができる。ログが書き出されている間はこのposファイルも更新され続けるので、このファイルの中身を監視して一定期間変更がなければサイドカーコンテナを終了させることで、問題が解決できるのではと考えた。この方法ではサイドカーコンテナだけに手を入れれば良いので、何かしらの理由でメインコンテナには手を入れづらい場合は役に立つかもしれない。
準備
Jobを用意する。メインコンテナではバッチ処理を行なって何かしらのログを書き出している想定なので、3秒おきにログファイルに時間などを書き出すスクリプトを動かす。
apiVersion: batch/v1
kind: Job
metadata:
name: sidecar-killer-sample
spec:
completions: 1
parallelism: 1
backoffLimit: 5
ttlSecondsAfterFinished: 30
template:
spec:
containers:
- name: main
image: busybox:1.28
args: ['/bin/sh', '-c', 'for i in $(seq 20); do echo "$i: $(date)" >> /var/log/1.log && sleep 3; done']
volumeMounts:
- name: varlog
mountPath: /var/log
これをデプロイするとコンテナ内の/var/log/1.log
の以下のようなログが書き出される。
$ cat /var/log/1.log
1: Sat May 21 21:11:39 JST 2022
2: Sat May 21 21:11:42 JST 2022
3: Sat May 21 21:11:45 JST 2022
4: Sat May 21 21:11:48 JST 2022
5: Sat May 21 21:11:51 JST 2022
6: Sat May 21 21:11:54 JST 2022
7: Sat May 21 21:11:57 JST 2022
8: Sat May 21 21:12:00 JST 2022
9: Sat May 21 21:12:03 JST 2022
10: Sat May 21 21:12:06 JST 2022
次にFluentdサイドカーを追加して再度デプロイする。
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentconf
data:
fluent.conf: |
<source>
@type tail
format none
path /var/log/1.log
pos_file /var/log/1.log.pos
tag hoge
read_from_head true
</source>
<match **>
@type stdout
</match>
---
apiVersion: batch/v1
kind: Job
metadata:
name: sidecar-killer-sample
spec:
completions: 1
parallelism: 1
backoffLimit: 5
ttlSecondsAfterFinished: 30
template:
spec:
containers:
- name: main
image: busybox:1.28
args: ['/bin/sh', '-c', 'for i in $(seq 20); do echo "$i: $(date)" >> /var/log/1.log && sleep 3; done']
volumeMounts:
- name: varlog
mountPath: /var/log
- name: fluentd
image: fluent/fluentd:v1.14-1
args: ['/bin/sh', '-c', 'fluentd -c /fluentd/etc/fluent.conf']
securityContext:
runAsUser: 0
volumeMounts:
- name: varlog
mountPath: /var/log
- name: fluentconf
mountPath: /fluentd/etc
restartPolicy: Never
volumes:
- name: varlog
emptyDir: {}
- name: fluentconf
configMap:
name: fluentconf
items:
- key: fluent.conf
path: fluent.conf
Fluentdのコンテナのログを出力して正常に動作していることを確認する。
$ k logs sidecar-killer-sample-xxx fluentd
2022-05-21 15:16:06 +0000 [info]: parsing config file is succeeded path="/fluentd/etc/fluent.conf"
...
2022-05-21 15:16:59.756059200 +0000 hoge: {"message":"19: Sat May 21 15:16:59 UTC 2022"}
2022-05-21 15:17:02.756613500 +0000 hoge: {"message":"20: Sat May 21 15:17:02 UTC 2022"}
この状態ではメインコンテナが終了しても、サイドカーコンテナが生きているのでJobが終了しないことを確認する。
$ k get pods
NAME READY STATUS RESTARTS AGE
sidecar-killer-sample-xxx 1/2 NotReady 0 2m18s
$ k get jobs
NAME COMPLETIONS DURATION AGE
sidecar-killer-sample 0/1 4m29s 4m29s
サイドカーを終了させるプログラムを追加する
Fluentdのposファイルを一定期間監視して、中身に変更がなければFluentdのプロセスを終了させるプログラムをサイドカーコンテナで動かす。FluentdコンテナではRubyが使えるので今回はRubyで簡単なスクリプトを書く。
def main()
prev = nil
puts 'start monitoring pos file'
while true
begin
pos = {}
File.foreach('/var/log/1.log.pos') do |line|
elements = line.split(' ')
pos[elements[0]] = elements[1] + '-' + elements[2]
end
rescue => e
puts 'pos file not found'
sleep 10
next
end
if prev == pos
puts 'terminate the process because pos file was unchanged for a minute'
system('pkill -INT fluentd')
break
end
prev = pos
sleep 60
end
end
main()
このスクリプトをサイドカーコンテナのバックグラウンドで動かす。
apiVersion: v1
kind: ConfigMap
metadata:
name: fluentconf
data:
fluent.conf: |
<source>
@type tail
format none
path /var/log/1.log
pos_file /var/log/1.log.pos
tag hoge
read_from_head true
</source>
<match **>
@type stdout
</match>
terminator.rb: |
def main()
prev = nil
puts 'start monitoring pos file'
while true
begin
pos = {}
File.foreach('/var/log/1.log.pos') do |line|
elements = line.split(' ')
pos[elements[0]] = elements[1] + '-' + elements[2]
end
rescue => e
puts 'pos file not found'
sleep 10
next
end
if prev == pos
puts 'terminate the process pos file because unchanged for a minute'
system('pkill -INT fluentd')
break
end
prev = pos
sleep 60
end
end
main()
---
apiVersion: batch/v1
kind: Job
metadata:
name: sidecar-killer
spec:
completions: 1
parallelism: 1
backoffLimit: 5
ttlSecondsAfterFinished: 30
template:
spec:
containers:
- name: main
image: busybox:1.28
args: ['/bin/sh', '-c', 'for i in $(seq 10); do echo "$i: $(date)" >> /var/log/1.log && sleep 3; done']
volumeMounts:
- name: varlog
mountPath: /var/log
- name: fluentd
image: fluent/fluentd:v1.14-1
args: ['/bin/sh', '-c', 'fluentd -c /fluentd/etc/fluent.conf & ruby fluentd/etc/terminator.rb']
securityContext:
runAsUser: 0
volumeMounts:
- name: varlog
mountPath: /var/log
- name: fluentconf
mountPath: /fluentd/etc
restartPolicy: Never
volumes:
- name: varlog
emptyDir: {}
- name: fluentconf
configMap:
name: fluentconf
items:
- key: fluent.conf
path: fluent.conf
- key: terminator.rb
path: terminator.rb
こうすることでposファイルに一定期間変更がない場合はFluentdのプロセスが終了し、Jobも完了状態となる。
$ k logs sidecar-killer-sample-xxx fluentd
...
2022-05-21 14:19:15.378827400 +0000 hoge: {"message":"10: Sat May 21 14:19:14 UTC 2022"}
start monitoring pos file
pos file not found
terminate the process because pos file was unchanged for a minute
$ k get pod
NAME READY STATUS RESTARTS AGE
sidecar-killer-xxx 0/2 Completed 0 57s
$ k get jobs
NAME COMPLETIONS DURATION AGE
sidecar-killer 1/1 53s 73s
まとめ
この実装だと、メインのコンテナで時間がかかる処理をしている場合も何かしらのログを吐き続けないといけなかったりと色々問題がある。ログに高い信頼性を求められる場合は別の方法を考えた方が良いだろう。このように泥臭い実装をしなくても済むように本家に機能が追加されるのを待ちつつ、もっとイケてる実装ができないか考えたいと思う。
あとがき
Pod内のコンテナでPID Namespaceを共有し、メインコンテナの処理が終わったらサイドカーコンテナのプロセスを落とパターンも時間があれば実験してみたい。