表題の通りです.
ほら,ローカルでのテスト用途でそういうコンテナが欲しくなることが月2くらいであるじゃないですか……あるんですよ……本チャンの環境で使うともちろん即死なので使ってはいけません.
このようにすると test
ユーザーでパスワード無しでsshログインできるコンテナを作ることができます.便利ですね.
sedで気合を入れまくっている部分を,あらかじめ書き換えておいた sshd_config
との COPY
に置き替えても良いでしょう.
現場からは以上です.
そうは言っても障害は起きるものです.で,障害が起きて,終息したあとの振り返りとして社内向けにoutage report (障害報告書的な?) のようなものを書くと思うのですが,本記事ではそのときに気をつけていることについて書きたいと思います。
そもそもですが,outage reportを書く目的としては以下のような物があるのかなと思っています。
以下は個人的に (というか所属している組織的に) 書いている内容なので千差万別だとは思いますが,このような感じのことを書いていますというご紹介です.カッコ内のアルファベットは目的の部分に書いたアルファベットと対応しています (Dについては横断的に有効だと思うので省略しています).
こんなところでしょうか.気づいたら追記するかもわかりません.
outage reportを書く時ってあまり気持ちがアガるものではないし,障害明けで疲れてたりピリピリしていたり,えてして「なんで俺が書かなきゃならんのだ……」みたいな罰当番的な感じにもなりがちだと思うんですが,実際はそうじゃないですよという雰囲気と仕組みを作ることが重要な気がしています.
例えばoutage reportは関連する人が複数人で書くようにするだとか,あるいはoutage reportを書いた人が評価されるというようにしていくのが良いのではないかと思っている次第です.非常に重要なドキュメントだと思うので.
所属している組織ではscrapboxを使っているので自然と複数人でoutage reportを書く感じになっており,ここらへんは上手くワークしているという感じがしているところです.
これは気をつけている事というよりも抱えている課題なんですが,
というのがあって,前者はchat opsに全振りしているような組織だと簡単にできそうで良いな〜と思っているところです (ただそれだけの理由でchat opsに全振りする理由にはならないな……という感じでもある).なんらかのソリューションが欲しい……
またoutage reportを効果的に社内に通知する方法についてもいい方法を探していて,今の所朝会的なところでoutage reportへのポインタを示すくらいしかできていない状況で,もうちょっとなんかいい方法はないかと探っているという段階です.
なんらか良い方法をご存知の方がいたら教えてもらえると嬉しいです.あと他のみなさんがどういう感じでoutage reportを書いているのかも気になる.
表題のような問題があり,その調査したという記録です.なお,結論を一言で言うと--init
を使え,ということになります.
そもそもDockerコンテナを起動すると,CMD
あるいはENTRYPOINT
に指定されたコマンドがコンテナ内でPID 1として起動します.これが何を意味するかと言うと,「CMD
あるいはENTRYPOINT
に指定されたコマンド」はそのコマンド自体の責務をまっとうするのと同時に,initプロセスとしての振る舞いも行わなければならないということになります (id:hayajo_77さんにこの辺を詳しく教えてもらいました,ありがとうございます).
つまりPID 1で動いているプロセスは「SIGCHLDをトラップすることで孤児プロセスを適切に回収し,waitpidをかける」という処理も適切に行う必要があります.
さて,puppeteerを使ってChromeブラウザを起動するとどうなるでしょうか *1.以下に検証で利用したコード片を記します.
なお検証環境は
$ uname -a Linux ip-198-18-0-91.ap-northeast-1.compute.internal 4.14.88-88.76.amzn2.x86_64 #1 SMP Mon Jan 7 18:43:26 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux $ cat /etc/system-release Amazon Linux release 2 (Karoo) $ docker --version Docker version 18.06.1-ce, build e68fc7a215d7133c34aa18e3b72b4a21fd0c6136
となります.
index.js
:
const puppeteer = require('puppeteer'); new Promise((resolve) => { resolve(puppeteer.launch({ args: [ '--no-sandbox', '--disable-setuid-sandbox' ] })); }).then((browser) => { console.log('launched'); setTimeout(() => { browser.close(); console.log('closed'); setTimeout(() => { console.log('finished'); }, 10000); }, 10000); });
Dockerfile
:
FROM node:10-jessie WORKDIR /app ADD . /app/ RUN apt-get update && apt-get install -y libx11-dev libx11-xcb-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxi-dev libxtst-dev libnss3-dev libcups2-dev libxss-dev libxrandr-dev libasound2-dev libatk1.0-dev libatk-bridge2.0-dev libgtk-3-dev RUN npm install ENTRYPOINT ["node", "index.js"]
そしてこのDockerコンテナを起動すると以下のようなプロセスツリーとなります (一部の長大なコマンドについては省略しています).
root 12029 0.1 7.1 792184 72480 ? Ssl Feb28 3:50 /usr/bin/dockerd --default-ulimit nofile=1024:4096 root 12040 0.1 1.8 644872 18304 ? Ssl Feb28 3:54 \_ docker-containerd --config /var/run/docker/containerd/containerd.toml root 29692 0.0 0.3 8792 3888 ? Sl 06:15 0:00 \_ docker-containerd-shim -namespace moby root 29728 4.0 3.7 595548 37676 pts/0 Ssl+ 06:15 0:00 \_ node index.js root 29789 1.2 6.4 535180 65444 ? Ssl 06:15 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome root 29791 0.4 4.2 374876 42716 ? S 06:15 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=zygote --no-sandbox --headless --headless root 29813 0.4 6.2 607496 63388 ? Sl 06:15 0:00 | \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=renderer --no-sandbox root 29810 0.6 5.4 432496 55320 ? Sl 06:15 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=gpu-process
index.js
がpuppeteer.launch()
を実行することによってChromeのプロセスがspawnされ,さらにそのChromeプロセスがChromeのプロセスをspawnしていることが分かります.つまり「子プロセスであるChrome」と「孫プロセスであるChrome」が存在しているという状況になります.
そしてその10秒後にbrowser.close()
によってブラウザをクローズすると……
root 12029 0.1 7.1 792184 72480 ? Ssl Feb28 3:50 /usr/bin/dockerd --default-ulimit nofile=1024:4096 root 12040 0.1 1.8 644872 18304 ? Ssl Feb28 3:54 \_ docker-containerd --config /var/run/docker/containerd/containerd.toml root 29692 0.0 0.3 8792 3888 ? Sl 06:15 0:00 \_ docker-containerd-shim -namespace moby root 29728 4.0 3.7 595548 37676 pts/0 Ssl+ 06:15 0:00 \_ node index.js root 29791 0.0 0.0 0 0 ? Z 06:15 0:00 \_ [chrome] <defunct> root 29813 0.2 0.0 0 0 ? Z 06:15 0:00 \_ [chrome] <defunct> root 29810 0.1 0.0 0 0 ? Z 06:15 0:00 \_ [chrome] <defunct>
孫プロセスであるChromeがゾンビプロセスになってしまっていますね.
これはbrowser.close()
でkillしたプロセスは「子プロセスであるChrome」のプロセスで,その配下にいた「孫のChromeプロセス群」はPID 1のプロセス (つまりこの場合のnode index.js
) に回収されてしまうものの,node index.js
はその孫プロセスたちについてwaitpid(2)
を発行して看取るということをしないため,このようにゾンビプロセスとしてコンテナ内に溜まっていくということになります.
puppeteerとしては,孫プロセスはinitに回収されることを期待しているという感じですね.非コンテナ環境であれば期待通りに動く仕組みと言えます.
で,どうすべきかと言うと向かうべき道は2つくらいあると思っていまして,
というものが考えられると思います.
前者はまさに文面通りPID 1となるプロセスの中にSIGCHLDをトラップしてwaitpid(2)を発行するようなロジックを入れるというものです.良識的なプログラミング言語をご利用であればわりかしシンプルに実現可能でしょう.これによってChromeのゾンビプロセスが爆発するというのを避けられると思います.
後者はコンテナ内にinitを導入する,つまりPID 1のプロセスをinitにして,その配下に任意のコマンドをぶら下げるという方法です.
Docker 1.13以降のバージョンをお使いであれば *2,docker run
に対して--init
オプションを付与することでPID 1にinitを指定することができるので,これによりinitのメカニズムをコンテナ内に持ち込むことが可能となります: https://docs.docker.com/engine/reference/run/#specify-an-init-process
root 12029 0.1 5.0 792184 51268 ? Ssl Feb28 4:03 /usr/bin/dockerd --default-ulimit nofile=1024:4096 root 12040 0.1 1.7 644872 17516 ? Ssl Feb28 3:58 \_ docker-containerd --config /var/run/docker/containerd/containerd.toml root 27900 0.0 0.3 8792 3888 ? Sl 07:08 0:00 \_ docker-containerd-shim -namespace moby root 27945 0.5 0.0 956 4 pts/0 Ss 07:08 0:00 \_ /dev/init -- node index.js root 27982 8.5 3.7 595548 37708 pts/0 Sl+ 07:08 0:00 \_ node index.js root 27999 3.0 6.5 535212 65616 ? Ssl 07:08 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome root 28001 0.5 4.1 374876 42320 ? S 07:08 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=zygote --no-sandbox --headless --headless root 28022 1.5 6.1 607496 62492 ? Sl 07:08 0:00 | \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=renderer --no-sandbox root 28020 1.5 5.3 432496 54448 ? Sl 07:08 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=gpu-process
このようにinitが差し込まれるので,孫プロセスが孤児になった際はこのinitが回収して看取ってくれることになります.
あるいは様々な理由により *3 --init
オプションを使えない場合はYelp/dumb-initやkrallin/tiniを手で差し込むという方法も可能です.例えば今回の例のDockerfileが以下のようになります:
FROM node:10-jessie WORKDIR /app ADD . /app/ RUN apt-get update && apt-get install -y libx11-dev libx11-xcb-dev libxcomposite-dev libxcursor-dev libxdamage-dev libxi-dev libxtst-dev libnss3-dev libcups2-dev libxss-dev libxrandr-dev libasound2-dev libatk1.0-dev libatk-bridge2.0-dev libgtk-3-dev RUN wget https://github.com/Yelp/dumb-init/releases/download/v1.2.2/dumb-init_1.2.2_amd64.deb RUN dpkg -i dumb-init_*.deb RUN npm install ENTRYPOINT ["/usr/bin/dumb-init", "--"] CMD ["node", "index.js"]
これを起動すると以下のようなプロセスツリーになります:
root 12029 0.1 7.1 792184 72232 ? Ssl Feb28 4:09 /usr/bin/dockerd --default-ulimit nofile=1024:4096 root 12040 0.1 1.9 644872 20024 ? Ssl Feb28 3:59 \_ docker-containerd --config /var/run/docker/containerd/containerd.toml root 11466 0.0 0.3 7384 3892 ? Sl 07:19 0:00 \_ docker-containerd-shim -namespace moby root 11502 0.2 0.0 212 4 ? Ss 07:19 0:00 \_ /usr/bin/dumb-init -- node index.js root 11547 2.2 3.7 595548 37840 pts/0 Ssl+ 07:19 0:00 \_ node index.js root 11564 1.0 6.4 535180 65576 ? Ssl 07:19 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome root 11566 0.1 4.2 374876 42672 ? S 07:19 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=zygote --no-sandbox --headless --headless root 11587 0.2 6.1 607496 62228 ? Sl 07:19 0:00 | \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=renderer --no-sandbox root 11585 0.3 5.3 432528 54468 ? Sl 07:19 0:00 \_ /app/node_modules/puppeteer/.local-chromium/linux-624492/chrome-linux/chrome --type=gpu-process
dumb-init
がPID 1として差し込まれており,--init
によってinitを差し込んだときと同じような効果が得られます.
というわけで色々と調査した結果でした.今回はたまたまPuppeteerで発生した事例でしたが,他のケースでも有効かと思います.initの偉大さを再認識させられますね!!
背景としてはこれがgrafanaのスクリーンショットを取るためpuppeteerを使っててchromeを立ち上げちゃうのよね〜,コンテナ環境で素直に使うとゾンビが増える https://t.co/FvqizKQh3T
— moznion (@moznion) 2019年3月2日
本来は1コンテナ1プロセスみたいな感じで扱うべきだと思っているし,コンテナ内でプロセスをガンガンspawnするのは良くないと思っている,少なくとも自分が設計するアプリケーションでは1コンテナ1プロセスという感じ (あるいはそれに近しい感じ) にすると思うのですが,生きていると色々ありますね.
puppeteerのtroubleshooting.mdに書いてたことに気づいた.
*1:ちなみにpuppeteerは内部でchild_process.spawnを利用してプロセスをspawnしている
*2:Add init process for zombie fighting and signal handling by crosbymichael · Pull Request #26061 · moby/moby · GitHub
*3:たとえばElastic BeanstalkのDocker Single Container runtime environmentを使っているとか
出ます,出るのです.
ソラコムプラットフォームの概観やその設計思想,実際のユースケースに応じた参考アーキテクチャなどが満載されている楽しい本です!!
ソラコムをすでにご利用の方にもそうでない方にもお役立ちな情報が記されていると確信しております.
いやー久々に商業誌に文章を書きました (WEB+DB PRESS Vol.81以来なので5年ぶりくらいでしょうか?).とは言っても本当にほんの一部なのですが……
手に取ってもらえると嬉しいです,よろしくお願いします!!
遅くなりましたが,表題のとおりです.YAPCが東京に凱旋してきたので参加してきました.
じつはこのような形で事前に関わっていたりもします:
んで,LTをやってきました.資料は以下です.
AWS LambdaでPerlを動かすというテーマのLTで,これはかつてブログにも書いた内容だったのですが,ここらで一丁LTにでもしてみるか〜となり,なったという感じです.
ベストLT賞は、 @moznion さんの「Perl meets AWS Lambda」です。おめでとうございます!ベストLTスポンサーのコルシス様より賞品の贈呈です!#yapcjapan pic.twitter.com/ndjhT7kZga
— yapcjapan (@yapcjapan) January 26, 2019
ありがたいことにベストLTをいただきました.大感謝です.
さて今回のYAPCはなんやかやPerlの話題が多く,まだまだホットではあるな〜とは思うものの,charsbarさんの発表にあったように「年々CPAN Authorの数が減少している」など少々寂しい話題もあり,いろいろ考えさせられるものがある回でした.とはいえtokuhiromさんのキーノートにもあったように,なんやかやperlは便利なので今後も使っていく気はしており,まあここらへんはやっていくことになるのであろう……
次回の開催地・会場はまだ未定とのことでしたが,楽しみに待ちたいと思います.今回も運営の皆さんお疲れ様でした,ありがとうございました.
public static class Something { private Map<String, String> prop; }
をJacksonでserializeすると
{ "prop": { "foo": "bar" } }
と,トップレベルにprop
のようなpropertyが出てくるので微妙……となるシチュエーションがまれによくあります.
で,どうすると良いかというと @JsonUnwrapped
を使うという方法がまず考えられると思うんですが,これは問題があって期待通りに動かない.かれこれ6年くらいチケットがオープンになっています: @JsonUnwrapped not supported for Map-valued properties · Issue #171 · FasterXML/jackson-databind · GitHub
public static class Something { @JsonUnwrapped private Map<String, String> prop; }
つまりこれは動かない.というわけでどうするかと言うと,チケット中にも示されているように@JsonAnyGetter
を使うという方法があります.
public static class Something { private Map<String, String> prop; // workaround: https://github.com/FasterXML/jackson-databind/issues/171#issuecomment-117794241 @JsonAnyGetter public Map<String, String> getProp() { return prop; } }
このようにすると,
{ "foo": "bar" }
というふうにトップレベルのpropertyが省略された,MapのKey-Valueがそのままserializeされることとなります.よかったよかった.
gowrtr (go writer
と発音します) というgoのコード生成支援ライブラリ (ジェネレータ群) を書きました.
Synopsisに書いたように,
package main import ( "fmt" "github.com/moznion/gowrtr/generator" ) func main() { generator := generator.NewRoot( generator.NewComment(" THIS CODE WAS AUTO GENERATED"), generator.NewPackage("main"), generator.NewNewline(), ).AddStatements( generator.NewFunc( nil, generator.NewFuncSignature("main"), ).AddStatements( generator.NewRawStatement(`fmt.Println("hello, world!")`), ), ). EnableGofmt("-s"). EnableGoimports() generated, err := generator.Generate(0) if err != nil { panic(err) } fmt.Println(generated) }
のようなコード (ジェネレータ) を書いてやると
// THIS CODE WAS AUTO GENERATED package main import "fmt" func main() { fmt.Println("hello, world!") }
というようなコードが生成されるというようなライブラリです.
この例だと生成結果があまりにシンプルなので,逆に記述量が増えてアレな感じになっていますが,ある程度生成結果が大きくなるようなものだと便利に使えるはず……です (後述のImmutabilityの話題もご覧ください).詳細につきましてはGoDocをご覧ください (Exampleも示してあります).
特徴としては
gofmt
, goimports
) を適用することができるというものが挙げられます.
前者は生成結果にgofmt
をかけることによって「生成コードのフォーマットが統一される」というメリットと「Syntaxチェックができる」というメリットを享受できることに加え,goimports
を適用することによって「コード生成のためのジェネレータを書いているときに『何をimportするか』を考えなくても (記述しなくても) 良くなる」というメリットを得ることができます.
後者については各ジェネレータの各メソッドが暗黙的に内部状態を変更しないので,利用者はジェネレータのスナップショットを任意のタイミングで取得することができます.これによりジェネレータの再利用が可能になると同時に,そこからジェネレータを派生させていくようなコードが書きやすくなるというメリットがあります.
さて,Javaにはsquare/javapoetというライブラリがあって,これはJavaコードの生成支援を行うライブラリなのですが,gowrtrはこのライブラリに着想を得て作られました.
まだできたてのライブラリなのですが,一部の自前環境に適用したところうまくワークしているのである程度動くのではないでしょうかというステータスです.が,もしかしたら足りない機能や記法のサポートがあるかもしれません.ご意見・ご要望をお待ちしております.
ぜひご利用くださいませ.