readnessProve, livenessProveを設定していない場合の挙動
readnessProve, livenessProveを設定していない場合、両者ともstateのデフォルト値がSuccessになる。つまりチェックされない。
https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/
livenessProbe: Indicates whether the Container is running. If the liveness probe fails, the kubelet kills the Container, and the Container is subjected to its restart policy. If a Container does not provide a liveness probe, the default state is Success.
readinessProbe: Indicates whether the Container is ready to service requests. If the readiness probe fails, the endpoints controller removes the Pod’s IP address from the endpoints of all Services that match the Pod. The default state of readiness before the initial delay is Failure. If a Container does not provide a readiness probe, the default state is Success.
という記述があった。 何らかデフォルトのヘルスチェックが入っているのかと思ったがそうではなかったのでメモしとく
Goで任意のJSONオブジェクトを文字列のままUnmershalする
{ "id": 1 "payload": { "a":1 } }, { "id": 2 "payload": { "b":2 } },
こういうJSONをGoで扱うときに、payload要素を、{"a": 1}, {"b": 2 } のように文字列そのままで取得したいときの方法です。
DBやBigQueryにjsonのまま保存したいみたいな用途です。
jsonstringという構造体を定義して、
type jsonstring struct { Body string } func (js jsonstring) MarshalJSON() ([]byte, error) { return []byte(js.Body), nil } func (js *jsonstring) UnmarshalJSON(data []byte) error { *js = jsonstring{Body: string(data)} return nil } func (js jsonstring) String() string { return js.Body }
それをUnmarshalする構造体の型に指定する。
type Item struct { Id string `json:"text"` Payload jsonstring `json:"payload"` }
するとUnmarshalできます
Istioを使うとLBでセットしたx-forwarded-protoが上書きされる
Istioを利用すると、LBでセットしたx-forwarded-protpがhttpに上書きされてしまうようです。
解決策、というかworkaroundがIssue内にあり、それを適用して回避しました。 istioのingressgatewayに来たrequestのx-forwarded-protpをhttpsに差し替えるという対応です。
httpも受け付けていたりすると、この対応では不都合が発生する可能性もあるのでこれが使えるかはケースバイケースです。
apiVersion: networking.istio.io/v1alpha3 kind: EnvoyFilter metadata: name: my-app-filter spec: workloadLabels: istio: ingressgateway filters: - filterName: envoy.lua filterType: HTTP filterConfig: inlineCode: | function envoy_on_request(request_handle) request_handle:headers():replace("x-forwarded-proto", "https") end function envoy_on_response(response_handle) end
KubernetesのJobを定義するときに覚えておくと便利なコマンド
特定のエンドポイントが疎通したことを確認してから処理を行う
特定のエンドポイントが200返すことを確認してから処理を行うことができます。istio-proxyなどSidecarコンテナの起動を待てます。
e.g :
istio-proxyの起動を待ってから処理開始。完了したらファイル配置
- name: job-name image: alpine command: ["/bin/sh","-c"] args: ["while ! wget http://127.0.0.1:15020/healthz/ready -O -; do echo 'waiting istio-proxy...' && sleep 1; done; /startjob && touch /tmp/proxy-killer/ready"] volumeMounts: - name: proxy-killer mountPath: /tmp/proxy-killer
特定のファイルの存在を待ってから処理を行う
volume経由で実行完了を通知して後処理を行うことができます。
e.g :
ファイルの存在を確認してからpkill
- name: proxy-killer image: alpine command: ["/bin/sh", "-c"] args: ["while [ ! -f /tmp/proxy-killer/ready ]; do echo 'waiting ready to kill proxy...' && sleep 1; done; pkill -f /usr/local/bin/pilot-agent"] volumeMounts: - name: proxy-killer mountPath: /tmp/proxy-killer
KubernetesにNodeJSアプリをデプロイするときにやったこと
KubernetesにNodeJSアプリをデプロイするときに行なったことのまとめ。
問題
NodeJSで作ったアプリをk8s上にデプロイしていたとき、下記のような問題が発生していました。アプリケーションは、TypeScript, express、next.jsを利用しています。
- Pod終了時にときどきエラーになったり、正常終了しなかったりする
- コンテナサイズが大きすぎる(1GB越・・!)
一つずつ解決していきます
Podの終了
以前記事にしたのですが、k8s上のアプリケーションは、SIGTERMで正常終了する必要があります。なので、まずはSIGTERMで正常終了させるように進めていきます。 KubernetesでPodが終了するときのフロー - koukiblog
起動コマンドの変更
問題が発生しているアプリの起動コマンドは"npm start" でした。npm start でnodeを起動すると、SIGTERMを受け取るプロセスはnodeではなくnpmです。アプリ側でSIGTERMをハンドリングしても意味がないので、まずはシグナルをNodeJSのプロセスで受け取れるようにします。
DockerfileのCMDを npm start から nodeコマンドに切り替えればよいだけなのですが、NodeJSにはpid 1では正しく動作しないという制約( https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#handling-kernel-signals ) があり、initプロセスを挟む必要があります。
dockerコマンドの場合、 --init オプションを利用することでinitプロセスを挟むことができます。
docker run -it --init node
上記相当のことをk8sで行うには、tiniを利用します。
FROM node:11.14.0-alpine # Setup tini ENV TINI_VERSION v0.18.0 ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini RUN chmod +x /tini ENTRYPOINT ["/tini", "--"] CMD ["node", "/server.js"]
とすればokです。上記の例では、alpineを利用しているので、tiniではなく、tini-staticを利用しています。
シグナルハンドラの追加
ここまでで、SIGTERMシグナルが受け取れるようになったので、次はSIGTERMシグナルでGraceful shutdownできるようにアプリケーションに修正を加えます。
expressの場合、terminus( https://github.com/godaddy/terminus ) というnpmモジュールを利用するのがよさそうでした。シグナルハンドラを自前で書かなくてよいのに加え、httpサーバの安全な停止、コールバック関数の提供を行なってくれます。
terminusのREADMEにも記載がありますが、下記のようにterminusでラップするだけで安全に停止させることができます。ちなみに、5秒待っているのは、SIGTERMシグナルが送信された後でもリクエストが流れてくる可能性があるからです。(どちらかというとリクエスト送信側にリトライを仕込むのが筋がよさそうではあります)
const http = require('http'); const express = require('express'); const app = express(); app.get('/', (req, res) => { res.send('ok'); }); const server = http.createServer(app); function beforeShutdown () { // given your readiness probes run every 5 second // may be worth using a bigger number so you won't // run into any race conditions return new Promise(resolve => { setTimeout(resolve, 5000) }) } createTerminus(server, { beforeShutdown }) server.listen(PORT || 3000);
なお、terminus内部では、stopable( https://github.com/hunterloftis/stoppable )というモジュールを利用してNodeJSを停止しています。
コンテナサイズの削減
TypeScript, next.jsを利用すると、事前にビルドが必要になり、ビルドに必要なnpmモジュールのサイズは非常に大きくなりがちです。なので、マルチステージビルドを利用し、ビルド用のコンテナでビルドした成果物を空の実行用コンテナにコピーするという方法が非常に有効です。 yarnを使ってモジュールの管理をしている場合、下記のようになります。
FROM node:11.14.0-alpine as builder # Add source, and Install all dependencies RUN yarn install && yarn cache clean # buid output /dist dir RUN yarn build FROM node:11.14.0-alpine as runner COPY --from=builder /dist /dist RUN yarn install --production && yarn cache clean ENTRYPOINT ["/tini", "--"] CMD ["node", "/server.js"]
package.jsonのdevDependeciesにビルドに必要なパッケージ群、dependenciesにサーバ起動に必要なパッケージ群を記載しておくことで、実行用のコンテナから不要なパッケージを除くことができます。
なお、yarn install したあとに yarn cache cleanしているのは、yarnのcacheを全て削除するためです。 yarn は一度インストールしたnpmモジュールをキャッシュする仕組みがあるため、キャッシュ削除しないと膨大なサイズになってしまいます。( clearしないまま使っていたとき、yarnのキャッシュディレクトリのサイズが400MBほどになっていました。)
ここまで実施することで、1GBのコンテナを250MB程度に削減することができました。
istio-proxyが有効なJobを正常終了させる
以前の記事で、現在は有効な手段がないという記事を書きましたが、数ヶ月たって状況が変わっていたのでアップデート
Sidecarを利用しているとk8sのjobが終了しない - koukiblog
workaround
Podの起動コマンドの最後にistio-proxyを停止させるコマンドを追加するという手段が有効です。
具体的には、
- Pod specに "shareProcessNamespace: true"を追加
- コンテナのコマンドに"pkill -f /usr/local/bin/pilot-agent"を追加
の2つです。
manifestsは下記のようになります。
containers: - name: job_container image: job_image command: ["/bin/sh","-c"] args: ["/container_start && pkill -f /usr/local/bin/pilot-agent"] shareProcessNamespace: true
Istio 1.0.x では、pkillしてもistio-proxyが正常終了せず、Jobが完了しなかったのですが、1.1.xから正常終了するように変わっていました。
このIssue内に記載があります。
Better support for sidecar containers in batch jobs · Issue #6324 · istio/istio · GitHub
kubernetes本体の対応
kubernetes本体で、この問題を根本的に解決する対応が進んでいます。まだ開発中ですが、そう遠くない将来使えるようになりそうです。
Sidecar Containers · Issue #753 · kubernetes/enhancements · GitHub
manifests内でcontainerがsidecarかどうかを宣言することができて、sidecarの場合は他のPodが終了するときに一緒に終了してくれるようです。
今後
当面はSidecar毎に都度回避策を考えていくしかない状態ですが、Sidecar Containersが実装されれば状況はかなりよくなりそうです。
Istioのタイムアウト・リトライのデフォルト値
Istioのタイムアウト・リトライのデフォルト値が気になって調べた。
結論
タイムアウトは15sec。リトライは1回。5xx、タイムアウトなど一般的にリトライしていい状況であればリトライする。
それぞれHTTPHeaderで挙動を上書き可能
Istio
Istioにはタイムアウト、リトライを制御する仕組みがあり、それはx-envoy-upstream-rq-timeout-ms、x-envoy-max-retriesという2つのHTTPヘッダーでデフォルト値を上書きできると書いてある。
リトライ
envoyのx-envoy-max-retriesのデフォルト値を調べる https://www.envoyproxy.io/docs/envoy/latest/configuration/http_filters/router_filter#x-envoy-max-retries
Envoy will default to retrying one time unless explicitly specified
とあるので1回
タイムアウト
x-envoy-upstream-rq-timeout-msを調べる
Setting this header on egress requests will cause Envoy to override the route configuration.
route configuration を上書きするために利用すると書いてあるので、route configurationを調べる。
Specifies the upstream timeout for the route. If not specified, the default is 15s. This spans between the point at which the entire downstream request (i.e. end-of-stream) has been processed and when the upstream response has been completely processed.
デフォルトは15秒
いつリトライするのか
envoyはx-envoy-retry-onというヘッダーでどのステータスコードのときにリトライするのかを設定することができる。 https://www.envoyproxy.io/docs/envoy/latest/configuration/http_filters/router_filter#config-http-filters-router-x-envoy-retry-on
Istioのドキュメントにはなかったが、検索してみたところIssueを発見した。 github.com 1.0時点では、ハードコードされていたが、x-retry-onヘッダーを受け付けるようになったらしい。
ちなみに、1.1時点のデフォルト値は
policy := route.RetryPolicy{ NumRetries: &types.UInt32Value{Value: 2}, RetryOn: "connect-failure,refused-stream,unavailable,cancelled,resource-exhausted,retriable-status-codes", RetriableStatusCodes: []uint32{http.StatusServiceUnavailable},
https://github.com/istio/istio/blob/28f2fbbbb0bb4910130b3362cc18c780e1fac87b/pilot/pkg/networking/core/v1alpha3/route/retry/retry.go#L33 となっています。5xx, タイムアウトなどリトライしてもよさそうな状況では基本的にはリトライする、と認識しておけばよさそうです。
connect-failureなどのそれぞれの定義はこちらにあります。 https://www.envoyproxy.io/docs/envoy/latest/configuration/http_filters/router_filter#x-envoy-retry-on
Istioの理解深めるには、envoy理解するのが効率よさそうだと思いつつ出来てないのでenvoy勉強しないなーと思いました。