parallel で k8s の複数 pod にコマンドを「並列」で流して結果を保存する

f:id:kyagi:20180216023000p:plain

k8s の複数 pod を横断してかつ「並列」でコマンドを実行する必要がある場合、どのような手段があるだろうか。例えばパフォーマンス測定のために jstat を複数 pod で実行したい場合、以下のように kubectl に pod を次々と「直列」で渡していっては、各 pod でコマンドの実行時間のズレが生じてしまう。何よりも jstat のように標準出力(stdout)を取られてしまうコマンドを kubectl に渡した場合、ひとつひとつ Ctrl+C していかなければならず、目的を果たすことができない。

複数 pod に「直列」でコマンドを実行する例

  1. pod リストを取得する
    $ kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}" | xargs -n 1 echo
    myapp-78ccc84d99-2cr24
    myapp-78ccc84d99-2pmp7
    myapp-78ccc84d99-59f4g
    myapp-78ccc84d99-5bwkc
    myapp-78ccc84d99-5d8wc
    myapp-78ccc84d99-5fzsd
    myapp-78ccc84d99-67qkr
  2. 取得した pod リストを kubectl に渡して date コマンドを実行するものの、最初の pod と最後の pod で実行時間のズレが生じてしまう。ターミナルを pod 数分開いて「せーの」で実施すれば実行時間のズレはある程度是正できるものの、数十から数百 pod ある環境では現実的ではない。
    $ while read p; do kubectl exec -it $p -c myapp -- date; done < <(kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}" | xargs -n 1 echo)

複数 pod に「並列」でコマンドを実行する例

  1. pod リストを取得する。parallel に渡すのに適したフォーマットにする。
    $ kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}"
    myapp-78ccc84d99-2cr24 myapp-78ccc84d99-2pmp7 myapp-78ccc84d99-59f4g myapp-78ccc84d99-5bwkc myapp-78ccc84d99-5d8wc myapp-78ccc84d99-5fzsd myapp-78ccc84d99-67qkr myapp-78ccc84d99-6glcz myapp-78ccc84d99-6htmf myapp-78ccc84d99-7dchn
  2. parallel を使って並列でコマンドを流す。jstat は標準出力(stdout)に連続して出力しようとするので、ここでは標準出力を out ディレクトリ配下に吐き出すように指定する。
    $ parallel --results out kubectl exec -it {} -c myapp -- /usr/bin/jstat -gcutil -t 1 1000 ::: $(kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}")
  3. 適当に時間が経過したところで Ctrl+C で jstat を中止する。out ディレクトリ配下を確認すると pod 名のディレクトリが自動作成され、jstat の標準出力が吐き出されていることが確認できる。
    $ tree out/
    out/
    └── 1
        ├── myapp-78ccc84d99-2cr24
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-2pmp7
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-59f4g
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-5bwkc
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-5d8wc
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-5fzsd
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-67qkr
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-6glcz
        │   ├── stderr
        │   └── stdout
        └── myapp-78ccc84d99-6htmf
            ├── stderr
            └── stdout
    $ cat out/1/myapp-78ccc84d99-*/stdout
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339412.0  36.71   0.00  33.87  85.15  97.85  96.46 112186 6009.403   232   25.555 6034.958
           339413.1  36.71   0.00  62.38  85.15  97.85  96.46 112186 6009.403   232   25.555 6034.958
           339414.1  36.71   0.00  89.70  85.15  97.85  96.46 112186 6009.403   232   25.555 6034.958
           339415.1   0.00  37.42  24.00  85.20  97.85  96.46 112187 6009.455   232   25.555 6035.011
           339416.1   0.00  37.42  61.76  85.20  97.85  96.46 112187 6009.455   232   25.555 6035.011
           339417.1   0.00  37.42  89.42  85.20  97.85  96.46 112187 6009.455   232   25.555 6035.011
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339551.1   0.00  39.96  68.38  46.40  97.99  95.55 107987 6136.845   238   49.173 6186.018
           339552.1   0.00  39.96  93.15  46.40  97.99  95.55 107987 6136.845   238   49.173 6186.018
           339553.1  44.37   0.00  32.62  46.43  97.99  95.55 107988 6136.903   238   49.173 6186.075
           339554.1  44.37   0.00  88.96  46.43  97.99  95.55 107988 6136.903   238   49.173 6186.075
           339555.1   0.00  41.82  17.52  46.49  97.99  95.55 107989 6136.963   238   49.173 6186.135
           339556.1   0.00  41.82  64.14  46.49  97.99  95.55 107989 6136.963   238   49.173 6186.135
           339557.1  39.57   0.00   4.16  46.59  97.99  95.55 107990 6137.027   238   49.173 6186.200
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339522.0   0.00  35.20  92.33  45.48  97.82  96.47 112235 6360.675   236   25.843 6386.519
           339523.0  37.73   0.00  34.98  45.53  97.82  96.47 112236 6360.728   236   25.843 6386.572
           339524.0  37.73   0.00  79.99  45.53  97.82  96.47 112236 6360.728   236   25.843 6386.572
           339525.0   0.00  36.40  20.69  45.58  97.82  96.47 112237 6360.781   236   25.843 6386.624
           339526.0   0.00  36.40  64.99  45.58  97.82  96.47 112237 6360.781   236   25.843 6386.624
           339527.0  35.51   0.00  10.36  45.64  97.82  96.47 112238 6360.835   236   25.843 6386.678
           339528.0  35.51   0.00  55.57  45.64  97.82  96.47 112238 6360.835   236   25.843 6386.678
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339454.0  38.18   0.00  38.61  44.10  97.78  95.53 114078 6133.794   236   23.938 6157.731
           339455.1  38.18   0.00  82.87  44.10  97.78  95.53 114078 6133.794   236   23.938 6157.731
           339456.1   0.00  41.97  17.91  44.16  97.78  95.53 114079 6133.854   236   23.938 6157.792
           339457.1   0.00  41.97  51.55  44.16  97.78  95.53 114079 6133.854   236   23.938 6157.792
           339458.1   0.00  41.97  82.24  44.16  97.78  95.53 114079 6133.854   236   23.938 6157.792
    (... snip ...)

kubectl と parallel の組み合わせは相性がよいので組み合わせでいろいろなことができそう。(๑•̀ㅂ•́)و✧

※なお、複数 pod を横断してログを tail するには stern という素晴らしいツールがある。

tail -f /dev/null で調査用コンテナを起動してそのままずっと保持しておく

f:id:kyagi:20180216163134p:plain

Docker コンテナを ENTRYPOINT や CMD の指定なしにちょっとだけ立ち上げたいことがある。例えば Docker イメージで提供されている OS の環境で少しだけ作業をしたい場合など。そんな時は大抵 -d でバックグラウンド指定して sleep コマンドで適当に長い秒数を指定してコンテナを起動した後に docker exec -it /bin/bash してその OS に入っているパッケージやそのバージョンを調査していた。

OS の調査用コンテナを起動して 1 時間後にコンテナを終了させる

$ docker run -d ubuntu sh -c 'sleep 3600' # 1 時間後にコンテナは終了する
5ec839f1cdc7ec657583a4fa992d556946d3b7775e9991175da68653020511ed

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
5ec839f1cdc7        ubuntu              "sh -c 'sleep 3600'"   7 seconds ago       Up 7 seconds                            eloquent_liskov

$ docker exec -it 5ec839f1cdc7 /bin/bash

root@5ec839f1cdc7:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

root@5ec839f1cdc7:/# exit
exit

これだと毎回コンテナを起動し直すはめになるので、調査に複数日かかったりコンテナ状態を保持しておきたい場合は無限ループを与えていた。

OS の調査用コンテナを起動してそのままずっと保持しておく(無限ループ編)

$ docker run -d ubuntu sh -c 'while :; do sleep 3600; done' # 数字はなんでもよい
69f6b10563b922e53139ed5acb5ceb9526daebc857f4a6cc031833f50e1ca04d

ただ、無限ループを与えるよりも tail -f /dev/null を使った方が簡単だいうことを知った。

OS の調査用コンテナを起動してそのままずっと保持しておく(tail -f /dev/null 編)

$ docker run -d ubuntu sh -c 'tail -f /dev/null'
1a258e96395f01d47ec976082ec2d99c5ee5b70949cef02a3108df2255240ab0


$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
1a258e96395f        ubuntu              "sh -c 'tail -f /d..."   3 minutes ago       Up 3 minutes                            unruffled_wilson
69f6b10563b9        ubuntu              "sh -c 'while :; d..."   4 minutes ago       Up 4 minutes                            gracious_wescoff
5ec839f1cdc7        ubuntu              "sh -c 'sleep 3600'"     5 minutes ago       Up 5 minutes                            eloquent_liskov

$ docker exec -it 1a258e96395f /bin/bash
root@1a258e96395f:/#


root@1a258e96395f:/# apt-get update
(... snip ...)

root@1a258e96395f:/# apt-get install dateutils
(... snip ...)

root@1a258e96395f:/# dateutils.ddiff 2018-04-01 2018-12-31 # 今年が終わるまで後何日?
274

確かにこれは楽だ。(´・ω・`)

fluentd プラグイン sampling-filter と numeric-monitor を組み合わせて nginx のアクセスログから標本を抽出してパフォーマンス測定を実施する

ログの一部を抽出して計算するには sampling-filter + numeric-monitor の組み合わせがぴったり!

大規模データ収集のログコレクタとして fluentd を使用している場合、全部のログの中から一部を抽出してデータ分析を行いたい場合がないだろうか。例えば、サービスのレスポンスタイムが遅くなったり(早くなったり)していないかを測定することができれば、異常検知に役立つ。つまり、ログ(母集団)をデータベースまで運ぶ間にちょっとつまみ喰い(標本)するといった具合だ。平均や特定のパーセンタイル値を出しておけばパフォーマンス監視としては十分だと思う。こういった要求を満たすのに便利な fluentd プラグインの組み合わせが fluent-plugin-sampling-filterfluent-plugin-numeric-monitor だ。

nginx のアクセスログの一部を抽出してパフォーマンス測定を行う場合

ここでは具体例として nginx のアクセスログを考えてみる。例えば以下のアクセスログから vhost ごとの reqtime をサンプリングしてパフォーマンスの指標として vhost ごとの平均と 99 パーセンタイルを出してみる。

access.log

time:15/Mar/2018:01:00:00 +0900  host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.001   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.001   vhost:www.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.002   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.002   vhost:www.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.003   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:02 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.004   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:03 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.005   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:05 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.006   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:06 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.007   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:08 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.008   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:08 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.009   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:08 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.010   vhost:blog.ayakumo.net

fluent プラグインの組み合わせによる実装

以下 4 つのプラグインを組み合わせることによりパフォーマンス測定を実装する。rewrite-tag-filter は昔の rewriteruleN 記法ではなく新しい <rule> ... </rule> 記法を使用することでマッチした部分をタグとして利用できるのが素晴らしい。

sample_td_agent.conf

<source>
  @type forward
  port 88888
</source>

<source>
  @type prometheus
</source>

<match nginx.access>
  @type rewrite_tag_filter
  <rule>
    key vhost
    pattern ^(.*)\.ayakumo\.net$
    tag nginx.access.$1
  </rule>
</match>

<match nginx.access.*>
  @type sampling_filter
  interval 100 # 1/100 sampling
  sample_unit all
  add_prefix sampled
</match>

<match sampled.nginx.access.*>
  @type forest
  subtype numeric_monitor
  <template>
    tag reqtime.${tag_parts[3]}
    unit minute
    aggregate all
    monitor_key reqtime
    percentiles 50,99
  </template>
</match>

<match reqtime.*>
  @type forest
  subtype copy
  <template>
    <store>
      @type prometheus
      <metric>
        name reqtime_percentile_50_${tag_parts[1]}
        type gauge
        desc The 50 percentile of reqtime per minute.
        key  percentile_50
      </metric>
      <metric>
        name reqtime_percentile_99_${tag_parts[1]}
        type gauge
        desc The 99 percentile of reqtime per minute.
        key  percentile_99
      </metric>
    </store>
    <store>
      @type stdout
    </store>
  </template>
</match>

<match **>
  @type null
</match>

nginx のアクセスログを擬似的に発行するために fluent-post を以下のように使用する。reqtime のところは $RANDOM を使う代わりに shuf -i 1-100 -n 1 でもよいかもしれない。

fluent-post.sh

for sld in www blog; do
  for i in `seq 1 100`; do
    r=$(($RANDOM % 100))
    /opt/td-agent/embedded/bin/fluent-post -p 88888 -t nginx.access -v time=time:15/Mar/2018:01:00:01 -v host=10.0.0.123 -v forwardedfor=12.123.123.123 -v req="GET /    index.html HTTP/1.1" -v status=200 -v referer="-" -v ua="-" -v reqtime=0.${r} -v vhost=${sld}.ayakumo.net
  done
done

こうすると @type stdout で td-agent.log に出力している numeric-monitor の基本機能により avg や num は自動的に計算されていることがわかる。www と blog の 2 つの sld(Second Level Domain) に対して 100 回(seq 1 100) 投げているが、サンプリング粒度を 1/100 にしているので(interval 100) 標本のサイズはそれぞれ 10 個づつ("num": 10) となる。

td-agent.log

2018-03-15 01:02:32 +0900 [info]: out_forest plants new output: numeric_monitor for tag 'sampled.nginx.access.www'
2018-03-15 01:02:32 +0900 reqtime.blog: {"num":10,"min":0.1,"max":0.98,"avg":0.587,"sum":5.87,"percentile_50":0.6,"percentile_99":0.94}
2018-03-15 01:02:32 +0900 [info]: out_forest plants new output: copy for tag 'reqtime.www'
2018-03-15 01:02:32 +0900 reqtime.www: {"num":10,"min":0.14,"max":0.79,"avg":0.433,"sum":4.33,"percentile_50":0.36,"percentile_99":0.7}

prometheus のメトリクス名としているパーセンタイル 50, 99 も取得できる。

$ curl -s http://localhost:24231/metrics/ | grep -v '^#'
reqtime_percentile_50_blog 0.6
reqtime_percentile_99_blog 0.94
reqtime_percentile_50_www 0.36
reqtime_percentile_99_www 0.7

fluentd のプラグインは便利なものがたくさんそろっているので、組み合わせ次第でいろいろなことができて本当に助かっている。(^_^)

新世代 grep の使い方 ripgrep(rg) と silver-searcher(ag)

はやいは正義

ripgrep が現在進行形で大正義らしい。名前からなんとなく「闇を切り裂く grep」みたいなイメージが浮かぶ。こう、アスファルトタイヤを切り裂きながら暗闇走り抜ける素敵なサムシング、を感じる... のはおぢさんだけ、か。 github.com

なぜ、rip という言葉が頭に浮かんだのかは覚えていない。何か "速い" とか "テキストを切り裂く" みたいなイメージが浮かんだんだ。rip が "Rest in Peace"、(魂よ)安らかに眠れ (つまり ripgrep によって grep の役目を終わらせるってこと) の略語たりうるってことに、その時はまだ気づいていなかった。

I don't remember how it popped into my head, but "rip" came up as something that meant "fast," as in, "to rip through your text." The fact that RIP is also an initialism for "Rest in Peace" (as in, "ripgrep kills grep") never really dawned on me. (FAQ.md)

インストールもはやくするためにワンライナを書いておく。バージョン 0.8.0 で鋭意開発中なので今年中にあと何回か叩くことになる気がする。

$ curl -sfSL https://github.com/BurntSushi/ripgrep/releases/download/0.8.0/ripgrep-0.8.0-x86_64-unknown-linux-musl.tar.gz | sudo tar xzf - -C /usr/local/bin/ --wildcards */rg --strip=1

所感

ag を踏襲しつつ速度をさらにはやめた感じ。新世代 grep 共通で ag も rg も結果表示が以下の形式になるのは同じ。ただこれはターミナルの表示上だけで行指向な本質はそのまま持っている。パイプで繋げるとマッチ行だけになる。デフォルトだとドット(.) ではじまるファイルは検索対象外となるが --hidden を使えば含めてくれる。

ファイル名
マッチ行

ag/rg 使い方逆引き一覧

やりたいこと ag (v2.1.0) rg (v0.8.0)
インストール(mac) $ brew install the_silver_searcher $ brew install ripgrep
インストール(ubuntu) $ sudo apt-get install silversearcher-ag 上記参照
サポートしているファイルタイプ一覧を出す $ ag --list-file-types $ rg --type-list
yml のファイルのリストを出す $ ag --yaml -l . $ rg -t yaml --files
yml のファイルで Deployment が含まれるファイルのリストを出す $ ag --yaml Deployment -l $ rg -t yaml Deployment -l
scala のファイルで case class を検索する $ ag --scala '^case class' $ rg -t scala '^case class'
scala のファイルで getOrElse を使っている(良い)箇所を検索する $ ag --scala -w getOrElse もしくは $ ag --scala '\bgetOrElse\b' $ rg -t scala -w getOrElse もしくは $ rg -t scala '\bgetOrElse\b'
scala のファイルで get を使っている(悪い)箇所を検索する $ ag --scala -w get $ rg -t scala -w get
scala のファイルで get を使っている箇所が全ファイル中どれくらいなのかの統計情報を出す $ ag --scala -w get --stats --stats-only なし
scala のファイルで get.wild を使っているワイルドな箇所を検索する(パイプつなぎのファイル名つき/行番号つき) $ ag --scala --filename --numbers -w get | ag wild $ rg -t scala --with-filename --line-number -w get | rg wild
色付き表示のまま less につなげる $ ag --scala get --pager 'less -R' $ rg -p -t scala get | less -R

ripgrep には統計の機能がないようだが、そこまで使わないし、作者がやる気になればすぐ作れるだろう。速度は rg のほうが早く感じたが両者とも (grep に感じる) ストレスを感じさせない速度だったので(てきとう)使いたいほうを使えばいいと思う。

複数行マッチをサポートする ag とサポートしない rg

例えば Kubernetes の yaml ファイルで以下のようなファイルがあるとする。ag は複数行マッチに対応しており --multiline オプションがデフォルトで有効になっているため ag --yaml 'name.*\n.*spec.*\n.*replicas' . でマッチできる。一方 rg は実装の複雑さとパフォーマンスへの影響から複数行マッチには対応していないし、今後もおそらく対応する予定がない。rg の作者の Andrew Gallant はこの機能差分は ag と rg の違いを示すいい例だとコメントしている。(^_^;

(... snip ...)
  name: user-ranking-app
spec:
  replicas: 10

github.com

検索結果からさらに検索したい時はパイプ/プロセス置換

例えば linux カーネルソースから cgroup_selvictim の両方を含んでいる行を検索したい時(例はてきとう)。パイプのほうがプロセス置換よりも若干速い。フィルタする時は最初の検索にファイル名表示 -H と行番号表示 -n をつけておくほうが便利。

$ time rg -nH -t c cgroup_sel | rg victim
mm/memcontrol.c:1285:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
mm/memcontrol.c:1305:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
mm/vmscan.c:3124:   nid = mem_cgroup_select_victim_node(memcg);
include/linux/memcontrol.h:428:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg);

real  0m42.889s
user  0m1.455s
sys   0m4.252s
$ time rg victim <(rg -nH -t c cgroup_sel)
1:mm/memcontrol.c:1285:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
2:mm/memcontrol.c:1305:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
3:mm/vmscan.c:3124: nid = mem_cgroup_select_victim_node(memcg);
4:include/linux/memcontrol.h:428:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg);

real  0m48.226s
user  0m1.958s
sys   0m6.691s

percol 連携に都合のいいオプション

alias s='ag --all-types --hidden --noheading --nonumbers --nofilename . ~/memo/*| percol'

grep は滅びぬ、なんでも蘇るさ

新世代 grep は速度や表示の点で使いやすいけれども、それでも grep が滅びることはない。速度だけを見れば完敗だけれど。grep の問題は歴史的なしがらみだと思う。長年使われてきたせいで、さまざまなスクリプトが依存しており、仕様を変えると世界中に様々な影響が出てきてしまう。高速化もできないことはないが、後方互換性を維持する慎重な開発方針にならざるを得ない、といったところだろうか。あれ、人間も同じようなことが起きるような... |ω・)

d.hatena.ne.jp

Docker, Kubernetes 学習とツールとコンピュータサイエンス

f:id:kyagi:20180216023000p:plain

Docker, Kubernetes 学習の進めかた

Udemy の Learning Docker and Kubernetes by Lab を完了した。実際に手を動かしながらだったので 1 週間ほどかかってしまった。内容はかなりよかった。Docker の基礎から Linux カーネルとの関連(Network Namespaces, cgroups)、docker-compose, Docker Swarm, そして Kubernetes の紹介をそれぞれ理論→実践の順番で進めていく形式だったのでよく理解できた。

Kubernetes については 57 アイテムのうち 9 つでアーキテクチャと minikube を使った簡単な実例だけだったので、別に Learn DevOps: The Complete Kubernetes Course で学習を進めている。本家の https://kubernetes.io/ にも katacoda などを使用して理解を促すコンテンツもあるが、本家はどちらかというとリファレンス的な読み方を前提とした構成になっているように感じられる。基礎から手を動かして進めるのであれば Udemy のコースのほうがよいと思った。

Play with DockerPlay with Kubernetes など制限時間はあるが、ブラウザで体験できるプレイグラウンドなサービスもあるので、これらを組織の研修やプレゼンに活かす方法を考えている(特に直接 Docker や Kubernetes に触れることがないアプリケーション開発チームのエンジニアに対して有効な手段となるのではないかと唸っている)

個人的に印象深かったツールは Docker Swarm Visualizer だ。どのノードでどのコンテナが動いているかを可視化できる。これは比較的小規模なクラスタを前提として Docker Swarm の特徴をよく活かしたツールだと思う。Kubernetes にはないし(そもそも Kubernetes Dashboard や kubectl はそういう設計ではない)、同様のことをやろうとするならば kubectl describe pod だろうか。

github.com

Kubernetes 学習環境の比較

お手軽さ 環境 ツール コメント
最もお手軽 ブラウザ Play with Kubernetes お手軽だが最初は無理。自分で yaml がかけるようになったら研修コースやプレゼンで使えそう
お手軽 Mac/Win/Linux minikube クラスタは一つのノードのみの構成となるが kubectl でのリソース作成の練習にはよい
お手軽 AWS kops 簡単だが Route 53 や IAM の設定があるので AWS に慣れている必要がある(慣れていれば常識的なことをやるだけ)
??? AWS Amazon EKS プレビュー中なのでまだ使用できない
お手軽(だと思う) GCP GCP Kubernetes Engine 少しだけ触ったことがあるが、個人的に GCP ではなく AWS に集中する方針なので ???

Kubernetes は GCP で触るのが王道なのかもだけど、個人的な選択と集中の方針として AWS に集中投資していくので評価は ? としている。

コンピュータサイエンスの理論を学ぶことの大切さ

Swarm mode cluster architecture で紹介されていたものだが、これらの技術やアルゴリズムについては概要だけでも知っておくと、エンジニア人生がより楽しくなると思う。Kubernetes の原型である Borg system の論文は(細かいところはともかくとして)図も豊富でわかりやすいし、基本的な考え方がよくわかるので個人的にはとても気に入っている。

Large-scale cluster management at Google with Borg – Google AI

https://static.googleusercontent.com/media/research.google.com/ja//pubs/archive/43438.pdf

grpc.io

developers.google.com

http://thesecretlivesofdata.com/raft/

Gossip protocol - Wikipedia

Paxos (computer science) - Wikipedia

いわゆる、コンピュータサイエンスを専攻した方であれば、これらはすでに体系的に学んでいるものなのかもしれない。私のような雑草エンジニアはその時その時で自分の知識のスキマを見つけて埋めていくしかない。だが、それもまた楽しい。ԅ( ˘ω˘ԅ)

xargs が Linux と Mac 両方できちんと動くためには -i ではなく -I オプションを使う

LinuxMac で xargs の挙動が違うのは以前から認識していたが、詳細までは調べていなかった。今回、プライベートの git レポジトリをコミットして Mac で xargs を実行した際に illegail option -- i と怒られてしまったので、これを機会に xargs が LinuxMac 両方できちんと動作するように修正した。

Linux では動く、Mac では動かない

# da = dot apply
alias da="ls -A ~/git/$repo/t/dot/ | xargs -i cp {} $HOME && pushd $HOME && source $HOME/.bash_profile && popd"

Mac 版の xargs (BSD) では -i オプションがサポートされていないのが問題だった。

$ da
xargs: illegal option -- i
usage: xargs [-0opt] [-E eofstr] [-I replstr [-R replacements]] [-J replstr]
             [-L number] [-n number [-x]] [-P maxprocs] [-s size]
             [utility [argument ...]]

xargs 調査

Linux 版の xargs の man を読むと、そもそも -i オプションはすでに deprecated となっており -I オプションを使うことが推奨されている。単純に -i を -I に変えればいいのかと思ったら、少し事情は異なっていて -I オプションは -i と違ってデフォルトの replace-str に {} を設定してくれないので、明示的に {} を指定する必要がある。

$ man xargs
(.. snip ...)

       -I replace-str
              Replace  occurrences  of replace-str in the initial-arguments with names read from standard input.  Also, unquoted blanks do not terminate input items;
              instead the separator is the newline character.  Implies -x and -L 1.

       -i[replace-str], --replace[=replace-str]
              This option is a synonym for -Ireplace-str if replace-str is specified.  If the replace-str argument is missing, the effect is the same as -I{}.   This
              option is deprecated; use -I instead.

(.. snip ...)

上記を踏まえて以下のように修正した。(๑•̀ㅂ•́)و✧

Linux でも Mac でも動く(replace-str に伝統的(?) に {} を使用)

$ git diff -U0 HEAD~2 HEAD
(... snip ...)
-alias da="ls -A ~/git/$repo/t/dot/ | xargs -i cp {} $HOME && pushd $HOME && source $HOME/.bash_profile && popd"
+alias da="ls -A ~/git/$repo/t/dot/ | xargs -I {} cp {} $HOME && pushd $HOME && source $HOME/.bash_profile && popd"
(.. snip ...)

Linux でも Mac でも動く(replace-str に独自のトークンを使ってみた例)

$ git diff -U0 HEAD~2 HEAD~1
(.. snip ...)
-alias da="ls -A ~/git/$repo/t/dot/ | xargs -i cp {} $HOME && pushd $HOME && source $HOME/.bash_profile && popd"
+alias da="ls -A ~/git/$repo/t/dot/ | xargs -I _file_ cp _file_ $HOME && pushd $HOME && source $HOME/.bash_profile && popd"
(... snip ...)

Docker イメージの LABEL 情報は Label-Schema が標準になる

f:id:kyagi:20180216163134p:plain

Docker イメージのメタ情報(メンテナ、ライセンス種別、ビルド日時、Github の URL など) は LABEL に集約しようという動きが広まってきている。そのために「どういった情報を載せるべきか」という議論がコミュニティベースで展開された結果、標準として Label-Schema という規格で定まってきているようだ。

label-schema.org

Docker 社もこの動きには賛同している。現在は 1.0.0-rc.1 (Release Candidate) だが、近いうちに正式に標準となりそうだ。

Docker Inc. express a preference that container labels should be namespaced. Label Schema is a community project to provide a shared namespace for use by multiple tools, specifically org.label-schema.

個人的にもイメージの透明性を高めるために当然の動きだと思う。正直 Dockerhub でよさそうなイメージを見つけても Dockerfile が公開されていない(≒ Github 連携していない) イメージは怖くて使えない。

Dockerfile の中で ARG で指定した変数は docker build 時に --build-arg var=value とオプションで渡すことができる。動的な情報のビルド日時や VCS_REF(=git のコミット値(SHA)) が対象となるようだ。

$ docker build --build-arg BUILD_DATE=`date -u +"%Y-%m-%dT%H:%M:%SZ"` .
(... snip ...)
Successfully built d187459cd07e

$ docker image inspect d187459cd07e | jq '.[].ContainerConfig.Labels' | jq '.["org.label-schema.build-date"]'
"2018-02-05T16:39:17Z"

$ date
Tue Feb  6 01:57:38 JST 2018

Github と Dockerhub を連携して Automated Build を設定している場合は hooks/build というスクリプトを置くことで同様に ARG で指定した変数を渡すことができる。サンプルを公開してくださっている方がいるので、こちらを参考に GitHub - kyagi/rod: REPLs On Docker を修正した。

github.com

Automated Build されたイメージを pull して LABEL 情報を確認すると vcs-ref がきちんと入っていることを確認できた。

$ docker image inspect kyagi/rod:latest | jq '.[].ContainerConfig.Labels' | jq -r '.["org.label-schema.vcs-ref"]'
02714fd

git rev-parse --short HEAD
commit 02714fd

ただ、問題がふたつ。ひとつめは、LABEL に BUILD_DATE など動的な値を渡すとキャッシュが効かなくなるようで、ビルドに時間がかかる。--no-cache を指定した時と同じ動きをしているように見える。ふたつめは org.label-schema.abc とキー名にドットが入っているせいで jq の処理に手間がかかる。どうしたものかなあ。(´・_・`)

Tectonic CoreOS を使って AWS 上に Kubernetes クラスタを構築する

CoreOS 社が提供している Kubernetes にセキュリティ、監視、マルチクラウド統合、そしてカッコイイ管理インターフェイスを備えた統合環境 Tectonic を使用してみた。

同社の推しは「Kubernetes の管理コストをさげる」「クラウドベンダーにロックインされない」だが、逆に柔軟が設計/設定ができる Kubernetes の自由度を下げ、同社にロックインされるとも受け取れる。試していないが Openshift も似たような統合環境のようだ。

coreos.com

課金体系

kubernetes クラスタのノードが 10 台までは無料、それ以上は応相談。

インストール

インストールの前に CoreOS の URL からアカウントを作成してライセンス情報と設定ファイルを入手する必要がある。CoreOS 社が提供する圧縮ファイルの中のインストーラを起動し、ブラウザ上で各種情報(AWS の場合、認証情報や Manage, Worker, Etcd それぞれのノード台数とインスタンスタイプなど)を入力して進めていく。内部では Terraform が使われている。インスタンスタイプはデフォルトで t2.medium なのでそこまでお金はかからない。

ハマりどころ

  • デフォルトでは Manage : Worker : Etcd = 3 : 3 : 3 で kubernetes クラスタを構築しようとするが、個人の AWS だと EIP の上限はだいたい 5 になっているのでそこでエラーになる。
  • 使用するドメインTTL を短くする必要がある。retry = 90, ttl = 89 にして成功した。

DNS caching

An issue arises when a domain's Address record (A record) resolution is attempted before Route 53 publishes the cluster's A record and the NXDOMAIN response is cached in the NCACHE (RFC2308). This negative response may be cached for up to the number of seconds set in the domain's SOA record's TTL. Resolution fails until the negative caching TTL expires. These TTLs are typically large enough to disrupt the installation. The current workaround is to ensure your TTLs are set to a low interval, or to wait for them to expire, then proceed with the installation.

AWS Troubleshooting | Tectonic Installer on AWS | CoreOS

変更前 SOA) ns-123.awsdns-45.com. awsdns-hostmaster.amazon.com.  1 7200 900 1209600 86400
変更後 SOA) ns-123.awsdns-45.com. awsdns-hostmaster.amazon.com.  1 7200 90 1209600 89
  • 上記エラーの場合 Destroy & Start Over でやり直すことができるのだが、IAM ロールが削除されていないようで Already Exists なエラーになるので削除する。

感想

tectonic-system というネームスペースが追加されて、そこで Tectonic 固有の監視やコンソールの Pods が動いているようだ。Prometheus, Grafana も同時にインストールされる。ノードが 10 台まで無料(EC2 や AWS リソースはもちろん自分で払う) なので、開発環境にはよいかもしれない。個人的には kops のほうがコマンド一発で使いやすい。

  • Tectonic Console 1 f:id:kyagi:20180206022634p:plain

  • Tectonic Console 2 f:id:kyagi:20180206022645p:plain

  • Tectonic Console 3 f:id:kyagi:20180206022731p:plain

  • Prometheus f:id:kyagi:20180206022743p:plain

  • Grafana f:id:kyagi:20180206022748p:plain

クラスタ削除は terraform destroy で可能。構築した時のインストーラの情報を削除しないように注意されたし。(´・ω・`) https://coreos.com/tectonic/docs/latest/install/aws/uninstall.html

Scala, Ruby, Go の REPL 環境を Docker コンテナですぐに試せる Rod (REPLs on Docker) を作った

Scala の学習用に Ammonite 環境AWS EC2 上に構築していて、Scala Exercises の例を試せるように紹介されている cats や shapeless のライブラリを追加していた。ただ、個人的に Ruby を使う機会が圧倒的に多いので pry は必須だし、Go もやってみたいと考えているうちに、「REPL 環境をまとめた Docker コンテナを作成してしまえば楽なのでは?」と思って Rod (REPLs on Docker) というツールを作成した。

github.com

Github と Dockerhub を連携させて Automated Build 設定しているので、Github が更新されると自動的に Dockerhub 内部で docker builddocker push してくれる。

https://hub.docker.com/r/kyagi/rod/

簡単な使い方は以下の通り。例えば Scala Exercises の Cats | Semigroup の紹介されているコード例をそのまま打ち込めば試せるようになっている。自分自身が sbt console の使い勝手にあまり満足していないのと、Scala の開発環境構築に手間取ったところがあるので、こういったツールが誰かの役に立ってくれれば嬉しい。(^.^)

docker コンテナをたちあげる。

$ docker run -it kyagi/rod
 ____  _____ ____  _                         ____             _
|  _ \| ____|  _ \| |    ___    ___  _ __   |  _ \  ___   ___| | _____ _ __
| |_) |  _| | |_) | |   / __|  / _ \| '_ \  | | | |/ _ \ / __| |/ / _ \ '__|
|  _ <| |___|  __/| |___\__ \ | (_) | | | | | |_| | (_) | (__|   <  __/ |
|_| \_\_____|_|   |_____|___/  \___/|_| |_| |____/ \___/ \___|_|\_\___|_|
root@e8dcf015f1b5:~# 

Scala の REPL を使いたい時は rod scala で amm が起動する。

root@e8dcf015f1b5:~# rod scala
Loading...
Compiling (synthetic)/ammonite/predef/interpBridge.sc
Compiling (synthetic)/ammonite/predef/replBridge.sc
Compiling (synthetic)/ammonite/predef/DefaultPredef.sc
Compiling /root/.ammonite/predef.sc
Welcome to the Ammonite Repl 1.0.3
(Scala 2.12.4 Java 1.8.0_151)
If you like Ammonite, please support our development at www.patreon.com/lihaoyi
@ import cats.Semigroup
import cats.Semigroup

@ import cats.implicits._
import cats.implicits._

@ Semigroup[Int => Int].combine({ (x: Int) => x + 1 }, { (x: Int) => x * 10 }).apply(6)
res2: Int = 67

@ exit
Bye!
root@e8dcf015f1b5:~#

Ruby の場合は pry が立ち上げる。

root@e8dcf015f1b5:~# rod ruby

[1] pry(main)> ri open
(... snip ...) いっぱいあるけど Tempfile.open が知りたい
[2] pry(main)> ri Tempfile.open
(... snip ...)
[3] pry(main)> ri Array
(... snip ...) Array クラスのドキュメント
[4] pry(main)> ri Array#
(... snip ...) Array クラスのインスタンスメソッド一覧

Go の場合は gore が立ち上げる。

root@e8dcf015f1b5:~# rod go
gore version 0.2.6  :help for help
gore> :help
    :import <package>     import a package
    :print                print current source
    :write [<file>]       write out current source
    :doc <expr or pkg>    show documentation
    :help                 show this help
    :quit                 quit the session
gore> :quit
root@e8dcf015f1b5:~#

いろいろいれたらイメージサイズがふくらんでしまった(1.43GB)。特定の言語の REPL があればいい場合は、Dockerfile を削ることで軽量化できるけど、その逆にこの REPL も入れたい(例えば gore じゃなくて go-pry を使いたいなど) とかもあるかもしれない。

ちなみに .rod-prompt を読み込むことでプロンプトが変わって、Docker コンテナ内の環境にいることがわかりやすくなる(おまけ)。(๑˃̵ᴗ˂̵)و

https://camo.githubusercontent.com/4982b60d784c7eea80e15e3a47de26ab4e4fd01f/687474703a2f2f63646e2d616b2e662e73742d686174656e612e636f6d2f696d616765732f666f746f6c6966652f6b2f6b796167692f32303138303230342f32303138303230343132323935322e706e67

プロンプト新世代を agnoster-bash と docker-prompt に感じた

agnoster-bash

会社の同僚に教えていただいて Mac のプロンプトを変えてみた。今までも環境ごとにプロンプトは変えていたけれど、vt100 で xterm な枠の中でせいぜい背景色と文字書を変更するぐらいだった。大げさかかもしれないけど、ブランチの記号がフォントに含まれるのにプロンプト新世代を感じた。打ったコマンドの終了ステータスが 0 以外だと赤いバツが出るのも心憎い。

GitHub - speedenator/agnoster-bash: Agnoster Theme for Bash

いままで)

f:id:kyagi:20180204111353p:plain

これから)

f:id:kyagi:20180204111407p:plain

コマンドの終了ステータスが 0 以外の場合、赤いバツがでる)

f:id:kyagi:20180204183523p:plain

プロンプト中のブランチマークが ? になってしまって表示できない現象は iTerm2 のフォントを Meslo LG ファミリに変更すれば表示できるようになる。

My theme error, shows question mark symbol · Issue #23 · agnoster/agnoster-zsh-theme · GitHub

個人的にフォントは Menlo 派なので Meslo LG フォントが Menlo の流れを汲んでいるのはありがたい(Menlo と Meslo で名前も似ている理由があるのかな、と探してみたけど、見つからなかった)

GitHub - andreberg/Meslo-Font: Customized version of Apple's Menlo font. Great monospaced font for development work. Should also work with the Windows Console (see Wiki for Windows infos).

日付と時間を表示しておきたいのと、視点はいつも左端に固定しておくのが楽なので、ちょっとだけ修正を加えておく。

Added datetime and newline for my preference by kyagi · Pull Request #1 · kyagi/agnoster-bash · GitHub

docker-prompt

最近では Docker コンテナかそうでないかによってプロンプトを変更するということもあるようだ。環境ごとに違った emoji をプロンプトに出すというのもなかなかいいかもしれない。

How to get a fancier bash prompt PS1 inside a docker container #docker #ps1 #emoji · GitHub

歴史的なプロンプト変更方法

環境ごとにプロンプトを変更するのは昔からよく行われてきた。よくある例だと、開発環境か本番環境によって色を変える *1 など。

昔読んだドキュメントにプロンプトに焦点を当てて解説していたものがあったので探してみた。バージョン管理が RCS ぽいところも懐かしい。ただし、内容はまだ現在でも褪せていない。(๑´ㅂ`๑)

http://linuxdocs.org/HOWTOs/Bash-Prompt-HOWTO/index.html

*1:具体的には ip a show dev eth0 | grep -o 'inet [0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}' | cut -d ' ' -f2 して切り出した IP アドレスを、さらに cut -d'.' -f1-2 でネットワーク部分を抽出して case ごとに PS1 を変える