Knative Serving in Production

# 概要

[Knative Serving](https://github.com/knative/serving) は、ステートレスなアプリケーションを対象に、HTTP リクエスト駆動で自動スケールする仕組みを提供します。Kubernetes (K8s) と Ingress ([Istio](https://istio.io/) or [Gloo](https://docs.solo.io/gloo/latest/), [Ambassader](https://www.getambassador.io/)) を程よく抽象化し、開発者が複雑な知識を必要とせずに、アプリケーションを外部に公開することが出来ます。Knative Serving のオートスケールの仕組みは、[Zero Scale Abstraction in Knative Serving](https://speakerdeck.com/toversus/zero-scale-abstraction-in-knative-serving) にまとめていますが、一部情報が古くなっているので、注意して下さい。

本記事では、Knative Serving on EKS を本番導入するにあたって、注意するべき点をまとめます。

# Why Knative?

本題に入る前に Knative を採用した理由を整理します。

- K8s のネットワークを含めた実用的な抽象化
    - アプリケーションのデプロイだけなら、K8s リソースの深い知識は不要
        - [Resource Requests/Limits](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) や [Readiness/Liveness Probe](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)、[Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) の知識は必要
    - サービスの公開に必要な Ingress リソースを裏で生成してくれる
    - アプリケーションを Immutable な Revision (環境) に閉じ込めてくれる
        - 切り戻しはトラフィックの向き先を変えるだけで良い
    - Revision GC
        - トラフィックの向いていていない Revision は、指定した時間経過した後に自動的に削除される
    - Revision にタグ付けすることで、テスト用のエンドポイントが楽に生成できる
- スケールの制御
    - HTTP リクエストの同時処理数や RPS をベースにアプリケーションをスケールできる
    - アプリケーションの同時処理数を制限することができる
        - 同時に 1 つのリクエストしか処理しないとか
- YAML 地獄からの脱却
    - YAML の記述量が大幅に減り、Helm や kustomize をゴリゴリ使う必要がなくなる
- ゼロスケール出来ることの意義
    - ノード障害でアプリケーションが全滅しても、リクエストキュー (Activator) が生きていればリクエストを取りこぼさない可能性がある
    - HPA に比べてスケールが早いため、突発的なリクエストの増加に対応できる可能性がある
- Portability の向上
    - K8s 上なら基本的にどこでもデプロイ可能
- Knative の拡張
    - ライブラリ ([knative/pkg](https://github.com/knative/pkg)) やテンプレート ([knative/sample-controller](https://github.com/knative/sample-controller))を使って、Knative (K8s) を拡張するコントローラや Webhook を作成できる
        - [Binding Pattern](https://docs.google.com/document/d/1t5WVrj2KQZ2u5s0LvIUtfHnSonBv5Vcv8Gl2k5NXrCQ) ([@knative-user](https://groups.google.com/forum/#!forum/knative-users) に参加する必要あり) に従うことで、Mutating Webhook が簡単に作れる
        - 例えば、Google CloudSQL Proxy をサイドカーとして追加してくれる Webhook の例が [mattmoor/bindings](https://github.com/mattmoor/bindings) にある
        - Binding Pattern で提供しているライブラリは、Knative に限らず K8s でも利用できる

ステートレスなアプリケーションのコンテナ化を進める上で、どのプラットフォームを利用するかは難しい選択です。チームの規模や温度感による部分もあるので、正解はないと思います。AWS の場合、ECS を利用しているプロダクトが多いと思います。今回、ECS を利用しなかった理由として、ECS にも学習コストがあること、ECS だとネットワークレイヤーを自分で設定する必要があること、K8s の拡張性に魅力を感じたことがあげられます。

学習コストについては、[EKSは本当にECSより難しいのか？](https://dev.classmethod.jp/cloud/aws/ecs-and-eks/) にまとまっているので、省略します。ネットワークレイヤーに関しては、サービスが内部通信に gRPC を採用していることもあり、ALB だけではリクエストが偏ってしまいます。アプリケーション側に負荷分散のロジックを組み込むのもありですが、開発側に押し付けるよりは、[Envoy](https://www.envoyproxy.io/) などのプロキシに任せる方が無難だと判断しました。ECS で、Envoy をサイドカーで構築するとなると Envoy を自前で設定する必要があります。[App Mesh](https://aws.amazon.com/jp/app-mesh/) を使うのもありですが、結局 Istio でいうところの Virtual Service などを定義する必要があるようです。ネットワークレイヤーも含めて抽象化したプロダクトって意外とないんですよね。拡張性については、K8s コントローラや Webhook を自作することができます。今のところ、SSM ParameterStore から機密情報をフェッチして K8s Secret として登録してくれる [toVersus/aws-ssm-operator](https://github.com/toVersus/aws-ssm-operator) を簡単に作ったくらいですが、先述の Binding Pattern を使って、ConfigMap のライフサイクル管理やタイムゾーンなどの環境変数やアノテーションを Mutating Webhook で挟み込めるようにしたいです。他にもイメージを全ノードにプリフェッチしてくれる奴とか...。

良い点ばかりを挙げていますが、当然 Knative Serving が銀の弾丸な訳ではありません。Knative Serving を使う場合、ネットワークレイヤーが完全に抽象化されてしまうので、内部実装を深く理解する人間が必要です。今は、私がその役目を担っていますが、徐々に Knative を運用できる人を増やさないとかなり辛いです。Knative Serving を採用するかは正直悩みましたし、今でも悩んでいます。ただ、K8s を利用してみたいと開発側から話を持ちかけてくれたこともあって、これまでの (開発と運用を完全に分業してきた) 組織文化を変えるきっかけになればと思い進めています。K8s の知識に加え、ネットワークレイヤーも含めて開発側に押し付けるのは酷なので、段階的に K8s に慣れていけるレールを敷いてあげるのがインフラの役目と考えました。

# Target Workload

現状、Knative Service は処理の短いワークロードに適しています。

- API/Web サーバ
- Serverless Function (別途 Runtime が必要)

HTTP リクエストをトリガーにバッチ処理を実行したくても、数十分から数時間かかる処理には適しません。最近、機械学習モデルのデプロイ先として Knative を利用するケースを見ます。[kfserving](https://github.com/kubeflow/kfserving) といったプロジェクトがあるくらいです。ただ、Knative Service のスケールダウン時に停止する Pod は、その Pod がリクエストを処理中かどうかに関わらずランダムに決定されます。これは、スケールダウン時に Deployment のレプリカ数を -1 しているだけだからです。どの Pod を削除するかは、ReplicaSet コントローラの判断に委ねられ、選ばれた Pod が無慈悲に [Termination Grace Period](https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods) 経過後に停止されます。リクエストを処理し終わった後に、安全に停止させたいなら、[Termination Grace Period](https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods) を数十分に設定する必要があり、これは現実的ではないです。(Terminating の Pod が大量に存在することになり、ノードのリソースを食い潰すか、IP アドレスが枯渇するかになると思います。)

余談ですが、Knative では、Termination Grace Period を[クラスターレベル](https://github.com/knative/serving/blob/release-0.11/config/config-autoscaler.yaml#L130-L132)でしか設定することができません。Deployment 経由で Pod に指定できるフィールドは[マスク](https://github.com/knative/serving/blob/release-0.11/pkg/apis/serving/fieldmask.go#L156)されています。

私の担当サービスでは、ネイティブアプリのバックエンドの API サーバと Web スクレイピングのジョブを Knative Service 上にデプロイする予定です。長時間掛かるバッチ処理は対象にしていません。

現在、[Selective Pod Destruction](https://github.com/knative/serving/issues/6133) という機能の設計が進められています。この機能が実装されると、Knative で取得している HTTP リクエストのメトリクスをベースに削除する Pod を選択するようになります。すなわち、ReplicaSet コントローラに任せるのはやめるということです。

余談ですが、Knative v1 以降は、K8s の中でサーバーレスの特性に合わない部分を Knative のコンポーネントで置き換えようとする流れがあります。上記の例もそうですが、[Node Local Scheduling](https://docs.google.com/document/d/1lFown9jjBSOEBieSG3kGA8-1_tAgdDxQpeo-jtgXPCE/edit#heading=h.xw9hhjanpv18) ([@knative-user](https://groups.google.com/forum/#!forum/knative-users) に参加する必要あり) という Proposal があります。Autoscaler がスケジューリングを担当し、DaemonSet で立てた Activator が実際にスケールさせる形になるはずです。今の Activator の役割がリクエストバッファとリバースプロキシだけなので、Activator という名前がやっと意味のあるものになります。[古いデザインドキュメント](https://docs.google.com/document/d/1ErAFL7dpC0exdGuLrrrWeY2hxjTgmk2Io8-r0_gkAA8/edit)を更新するところから動くはずなので、まだ先になりそうです。

# Ingress

Knative Serving は、現状 4 つの Ingress に対応しています。 [@toshi0607](https://twitter.com/toshi0607) さんが、それぞれの Ingress の違いについて、次回の技術書店で執筆するらしいので、期待しましょう。この記事では、簡単に触れます。

- [Istio](https://istio.io/)
    - 複雑な Service Mesh を構築し、ネットワークの可視化やセキュリティポリシーの指定など様々な機能を利用できます。
    - Knative が発表された当初から対応しており、多くの E2E テストが Istio ベースで実行されています。Istio への対応は基本的に Knative のコアチームが担当しています。
- [Gloo](https://docs.solo.io/gloo/latest/)
    - > [Glooは2019年2月，Kubernetes Knativeサービスのために，Istioに代わるものとしてローンチされた。Solo.ioの開発によるGlooは，Knativeアプリケーション用のKubernetesイングレスコントローラ(Ingress Controller)として実装されたAPIゲートウェイである。](https://www.infoq.com/jp/news/2019/05/gloo-gateway-knative/)
    - 今は、Knative に関係なく軽量な API Gateway としての地位を確立しているように思います。
    - 先日 [v1.0.0](https://github.com/solo-io/gloo/releases/tag/v1.0.0) がリリースされました。
- [Ambassador](https://www.getambassador.io/)
    - Yet Another Envoy based lightweight API Gateway
- [Kourier](https://github.com/3scale/kourier)
    - Red Hat 主導で開発が進められている Knative のための Ingress で、Istio Ingress の置き換えを目的としています。
    - Knative がサポートする機能を提供できるミニマムなリファレンス実装を目指しています。
    - まだまだ開発中のため、本番利用は推奨されていません。

Ingress の選択も難しい問題です。Knative のコアチームが実装を担当していることもあり、Istio が最も安定した Ingress です。Knative Serving v0.11.0 で ClusterIngress と呼ばれる Knative 内部のリソースが Deprecate されました。v0.8.0 からの長い deprecate のプロセスでしたが、この変更に対する Ambassador の対応が遅れたため、Knative Serving v0.11.0 で Ambassador が[動作しない](https://github.com/datawire/ambassador/issues/1871)状態となっています。Gloo も v0.11.0 に対する [E2E テスト](https://testgrid.knative.dev/serving#gloo-0.17.1&exclude-non-failed-tests=20)が壊れています。Gloo のリリースが落ち着いた段階で、サポート対象の Gloo のバージョンを上げつつ、対応する予定のようです。

では、Istio が良いかと言うと、そうとも言えない状況です。Service Mesh は、少人数のチームにとって機能過多で運用の負荷が重くなるだけだからです。私も、Knative の情報は追っているものの、Istio の情報までは追えていないので、Service Mesh によるネットワークの抽象化で問題が発生した時に切り分けられる自信がありませんでした。

いろいろと迷走した結果、Istio の Service Mesh の機能は有効にせず、Istio IngressGateway と Pilot (Envoy の動的な設定) だけを利用することにしました。Knative では、"Istio Lean" と呼ばれている方式です。Service Mesh と併せて E2E テストのカバー率が高く、flaky なテストの少ない安定した方式となります。以前の[発表](https://speakerdeck.com/toversus/zero-scale-abstraction-in-knative-serving)では、Istio with Service Mesh を使うしかないと言っていましたが、[Prefer Pod IPs to ClusterIPs when possible #5820](https://github.com/knative/serving/pull/5820) がマージされ、他にもいくつか Activator によるロードバランサの機能が改善されたため、ゼロスケール時や急激なトラフィックの増加で Activator (中央集権的なキュー) にリクエストが送られても、gRPC (HTTP/2) の負荷分散が機能するようになってきました。mTLS やネットワークポリシーなどの Service Mesh の機能を、最初から使おうとは思っていないので、運用の負荷やチームの規模を天秤に掛けて最終的に判断しました。

ただ、注意点として、Istio Lean の場合は、Istio IngressGateway を istio-ingressgateway と cluster-localgateway の 2 台立てる必要があります。Knative では、`serving.knative.dev/visibility: cluster-local` ラベルを付与することで、そのサービスをクラスター内からしかアクセスできないようにできます。プライベートなサービスは、全て cluster-localgateway に紐付けられます。

Istio with Service Mesh を利用する場合は、sidecar-injector をデプロイし、[config-istio](https://github.com/knative/serving/blob/release-0.11/config/config-istio.yaml#L51-L69) を修正することで、cluster-localgateway がなくとも、Pod 間の通信が可能とのことです。ただ、[knative/serving#6488](https://github.com/knative/serving/issues/6488#issuecomment-573059668) にあるように、Knative が内部で生成する Virtual Service のホストで指定されたドメインが重複しているため、エラーとなってしまうバグがあるようです。注意するようにして下さい。

余談ですが、トラフィック分割に関して、ヘッダーベースで振り分ける [Proposal](https://docs.google.com/document/d/12t_3NE4EqvW_l0hfVlQcAGKkwkAM56tTn2wN_JtHbSQ) ([@knative-user](https://groups.google.com/forum/#!forum/knative-users) に参加する必要あり) が出ており、そろそろ議論が始まる予定です。

Istio のインストールは、Knative の開発で使っている [スクリプト](https://github.com/knative/serving/blob/release-0.11/third_party/istio-1.4.0/download-istio.sh) を流用させてもらって、公式の Helm Chart から生成しています。今後、Istio 公式提供の Helm Chart が廃止になるようで、istioctl で生成するように変更する必要があります。

# Cloud Load Balancer

クラスター外部から各ノード上の Istio IngressGateway にリクエストを振り分ける L4 の[パススルーロードバランサー](https://blog.envoyproxy.io/introduction-to-modern-network-load-balancing-and-proxying-a57f6ff80236)として、Network Load Balancer (NLB) を利用することにしました。K8s v1.14 の時点で、アルファの機能であるため、採用するか迷いましたが、EKS のアップグレードをクラスターレベルの Blue/Green デプロイで行うために、~~[Global Accelerator](https://aws.amazon.com/jp/global-accelerator/) を利用したいので~~ ([2019/12/20 追記] Global Accelerator の配下に NLB を置く場合は、[ソース IP を保持できない](https://docs.aws.amazon.com/global-accelerator/latest/dg/preserve-client-ip-address.how-to-enable-preservation.html)ことを知り、Route53 のルーティングポリシーを使うことにしたので、NLB は必須ではなくなりました。)強行しました。[K8s v1.15 でベータになる](https://kubernetes.io/docs/concepts/services-networking/service/#aws-nlb-support)ので、早くリリースされることを祈っています。

また、NLB で [ソース IP を保持](https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip)するために、[.spec.externalTrafficPolicy](https://kubernetes.io/docs/concepts/services-networking/service/#aws-nlb-support) を `Local` に設定しています。EKS v1.14 時点では、[kubernetes/kubernetes#61486](https://github.com/kubernetes/kubernetes/issues/61486#issuecomment-542314543) を参考に kube-proxy にパッチを適用しています。これにより、NLB からノードに対するヘルスチェックが通るようになります。外部からのリクエストは NLB で負荷分散されてノードに割り振られ、そのノード上の Istio IngressGateway にルーティングされるようになります。

> By setting .spec.externalTrafficPolicy to Local, the client IP addresses is propagated to the end Pods, but this could result in uneven distribution of traffic.

上記に書かれているように、このままでは、Istio IngressGateway へのリクエストが偏ってしまう可能性があります。そのため、Istio IngressGateway を DaemonSet として起動します。Istio が公式提供している Helm Chart には、DaemonSet でデプロイするオプションがないので、以下のようなパッチを適用しています。

```text
--- istio-copy.yaml	2019-11-17 20:31:57.000000000 +0900
+++ istio.yaml	2019-11-17 20:32:18.000000000 +0900
@@ -1838,7 +1838,7 @@ spec:
                 - "s390x"
 ---
 apiVersion: apps/v1
-kind: Deployment
+kind: DaemonSet
 metadata:
   name: istio-ingressgateway
   namespace: istio-system
@@ -1853,9 +1853,8 @@ spec:
     matchLabels:
       app: istio-ingressgateway
       istio: ingressgateway
-  strategy:
+  updateStrategy:
     rollingUpdate:
-      maxSurge: 100%
       maxUnavailable: 25%
   template:
     metadata:
```

`externalTrafficPolicy: Local` の場合、Cloud Load Balancer から Service のバックエンドに対するリクエストが最悪セカンドホップしてしまうことを防ぐことができます。

# TLS Termination

NLB は、DSR ロードバランサーではないらしい (レスポンスは必ず NLB を経由してクライアントに返る) し、mTLS を利用する予定がないので、NLB で TLS 終端しちゃっても良いかなと思っていました。ただ、v1.14 では、Service type LoadBalancer の annotation で、NLB に TLS 証明書を紐付けることができません。ですので、現在 TLS 終端は後段の Istio IngressGateway (L7) で行っています。こちらの機能も [v1.15 で利用できるようになる](https://github.com/kubernetes/kubernetes/issues/73297)ので、待ち遠しいです。

# Access Logging

Istio IngressGateway (Front Envoy) と Local ClusterGateway (Internal Envoy) のアクセスログはそれぞれ [global.proxy.accessLogFormat](https://istio.io/docs/reference/config/installation-options/#global-options) で指定可能です。Knative のコンポーネントである、queue-proxy と Activator が出力するアクセスログは、[config-observability](https://github.com/knative/serving/blob/release-0.11/config/config-observability.yaml#L52-L80) で変更可能です。Knative Service 名や Pod 名、ネームスペースなどをアクセスログとして吐け、デバッグがし易くなるのでオススメです。

```
  logging.request-log-template: '{"httpRequest": {"requestMethod": "{{.Request.Method}}", "requestUrl": "{{js .Request.RequestURI}}", "requestSize": "{{.Request.ContentLength}}", "status": "{{.Response.Code}}", "responseSize": "{{.Response.Size}}", "userAgent": "{{js .Request.UserAgent}}", "remoteIp": "{{js .Request.RemoteAddr}}", "serverIp": "{{.Revision.PodIP}}", "referer": "{{js .Request.Referer}}", "latency": "{{.Response.Latency}}", "protocol": "{{.Request.Proto}}"}, "traceId": "{{index .Request.Header "X-B3-Traceid"}}", "revision": "{{.Revision.Name}}", "namespace": "{{.Revision.Namespace}}", "podname": "{{.Revision.PodName}}", "podip": "{{.Revision.PodIP}}"}'
```

# Domain Template

パブリックもしくはプライベートなエンドポイントのドメイン名を [config-domain](https://github.com/knative/serving/blob/release-0.11/config/config-domain.yaml#L40-L59) で変更することができます。セレクターベースで利用するドメインを変更することができるため、複数のドメインを一つの Istio IngressGateway に紐付けることができます。

サブドメインの生成テンプレートも [config-network](https://github.com/knative/serving/blob/release-0.11/config/config-network.yaml#L102-L119) で変更しておきましょう。デフォルトだと、`"{{.Name}}.{{.Namespace}}.{{.Domain}}"` という形で、サブドメインが割り振られます。ネームスペースが間に入っているのは、ネームスペース間でサービス名が衝突しないようにするためです。私の担当サービスでは、外部公開するエンドポイントが少ないため、このテンプレートを `"{{.Name}}.{{.Domain}}"` に変更しています。

# HPA-based Autoscaler

Knative では、デフォルトで HTTP リクエストベースでスケールしますが、通常の HPA を使ったリソース使用率でのスケールもサポートしています。HPA ベースを選択する場合は、現状ゼロスケールには対応していません。[対応予定](https://github.com/knative/serving/issues/3064)ではいます。

KPA (HTTP リクエストベース) のオートスケーラーと HPA (CPU/メモリ使用率ベース) のオートスケーラーは、別々のコントローラーとして動作しています。HPA ベースの利用がない場合は、コントローラー自体を停止してしまいましょう。Knative Serving が提供するマニフェストでは、デフォルトで有効になっているので、kustomize でレプリカ数をゼロに変更しています。

```yaml
- op: replace
  path: /spec/replicas
  value: 0
```

# Metrics

Knative のコンポーネントは、貴重なメトリクスを Prometheus 形式で公開してくれています。Activator と Autoscaler、queue-proxy のメトリクスを最低限押さえておけば良いと思います。

私の担当サービスでは、Datadog を利用しているので、kustomize で Autoscaler の Deployment に以下のパッチを適用しています。

```yaml
- op: add
  path: /spec/template/metadata/annotations/ad.datadoghq.com~1autoscaler.check_names
  value: '["openmetrics"]'
- op: add
  path: /spec/template/metadata/annotations/ad.datadoghq.com~1autoscaler.init_configs
  value: '[{}]'
- op: add
  path: /spec/template/metadata/annotations/ad.datadoghq.com~1autoscaler.instances
  value: '[{"prometheus_url": "http://%%host%%:9090/metrics","namespace": "knative-serving","metrics": ["*"], "send_distribution_buckets": true}]'
```

Activator の Deployment も同様です。

```yaml
- op: add
  path: /spec/template/metadata/annotations/ad.datadoghq.com~1activator.check_names
  value: '["openmetrics"]'
- op: add
  path: /spec/template/metadata/annotations/ad.datadoghq.com~1activator.init_configs
  value: '[{}]'
- op: add
  path: /spec/template/metadata/annotations/ad.datadoghq.com~1activator.instances
  value: '[{"prometheus_url": "http://%%host%%:9090/metrics","namespace": "knative-serving","metrics": ["*"], "send_distribution_buckets": true}]'
```

queue-proxy の場合は、Knative Service に以下の annotation を付与しています。1 つ目が queue-proxy の、2 つ目がアプリケーションで公開している Prometheus 形式のメトリクスを収集するためのものです。

```yaml
spec:
  template:
    metadata:
      annotations:
        ad.datadoghq.com/queue-proxy.check_names: '["openmetrics"]'
        ad.datadoghq.com/queue-proxy.init_configs: '[{}]'
        ad.datadoghq.com/queue-proxy.instances: '[{"prometheus_url": "http://%%host%%:9090/metrics","namespace": "default","metrics": ["*"]}]'
        ad.datadoghq.com/user-container.check_names: '["openmetrics"]'
        ad.datadoghq.com/user-container.init_configs: '[{}]'
        ad.datadoghq.com/user-container.instances: '[{"prometheus_url": "http://%%host%%:9091/metrics","namespace": "default","metrics": ["*"], "send_distribution_buckets": true}]'
```

# Readiness Probe

Knative Serving では、可能な限り早く Pod が Ready 状態になるように、ユーザーが定義した Probe をいじっています。これは、K8s の Readiness Probe のパラメータが[秒単位でしか指定できない](https://github.com/kubernetes/kubernetes/issues/76951)からです。ユーザーが定義した TCP および HTTPGet Probe は、ユーザーのコンテナ定義から削除され、サイドカーとしてデプロイされる queue-proxy の Readiness Probe にコピーされます。queue-proxy の Readiness Probe は、次のように指定されています。

```yaml
    readinessProbe:
      exec:
        command:
        - /ko-app/queue
        - -probe-period
        - "0"
      failureThreshold: 3
      periodSeconds: 1
      successThreshold: 1
      timeoutSeconds: 10
```

queue-proxy は、ヘルスチェック用の[シングルバイナリとして動作する](https://github.com/knative/serving/blob/release-0.11/cmd/queue/main.go#L295-L303)ようになっています。


```go
	// If this is set, we run as a standalone binary to probe the queue-proxy.
	if *readinessProbeTimeout >= 0 {
		if err := probeQueueHealthPath(env.QueueServingPort, *readinessProbeTimeout); err != nil {
			// used instead of the logger to produce a concise event message
			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}
		os.Exit(0)
	}
```

この probeQueueHealthPath の中で、`readinessProbe.timeoutSeconds` の間、より細かい粒度のポーリング間隔 (25 ms) で、ヘルスチェックをループさせています。

では、readinessProbe として Exec Probe を指定した場合は、どうなるのでしょうか？gRPC サービスのヘルスチェックは、[grpc-ecosystem/grpc-health-probe](https://github.com/grpc-ecosystem/grpc-health-probe) をイメージ内に同梱して Exec Probe で実行するのが一般的ですよね。Exec Probe の場合、ユーザー定義のコンテナから Exec Probe は削除されず、queue-proxy のヘルスチェックはコンテナのポートに対する TCP Probe に置き換えられます。そのため、Pod が Ready 状態になるのは、Pod 内の全てのコンテナのステータスが Ready になってからなので、この細かい粒度での Readiness Probe は余り意味をなさなくなります。

つい最近まで、Knative Service で Exec Probe を指定した時にエラーになっていたので、[knative/serving#5712](https://github.com/knative/serving/pull/5712) のパッチを適用する必要がありましたが、ついにマージされたので v0.12.0 から不要になる予定です。

# Graceful Shutdown

Knative Serving は、Mutating Webhook を使って PodSpec を色々といじっています。その最たる例が、Lifecycle Hook です。Knative Serving は、ユーザーコンテナに以下の Lifecycle Hook を忍ばせています。

```yaml
    lifecycle:
      preStop:
        httpGet:
          path: /wait-for-drain
          port: 8022
          scheme: HTTP
```

8022 番ポートは、queue-proxy のポートです。queue-proxy が提供する `/wait-for-drain` のエンドポイントは、queue-proxy が停止するまでレスポンスを返さないようにします。これにより、queue-proxy が停止した後に、SIGTERM がユーザーコンテナに届きます。preStop フックを自身で定義する必要はありません。queue-proxy 自体は、SIGTERM を受けると 20 秒スリープしてから、Golang 標準の Shutdown で[安全に停止](https://github.com/knative/serving/blob/release-0.11/cmd/queue/main.go#L388-L407)します。ですので、何も考えずに アプリケーション側で Graceful Shutdown のみ実装してあげれば良いです。

# Private Image Registry

Knative Serving には、Revision という概念があります。Revision は、アプリケーションのコードと設定を封じ込める不変の環境です。

![](https://github.com/knative/serving/raw/master/docs/spec/images/object_model.png)

これを実現するため、[tag to digest](https://knative.dev/docs/serving/tag-resolution/) という機能がデフォルトで有効になっています。イメージタグが不変ではないため、Revision 作成時にイメージタグからイメージダイジェストに[変換](https://github.com/knative/serving/blob/release-0.11/pkg/reconciler/revision/resolve.go#L88-L89)して、イメージを固定します。この tag to digest の機能は、Knative コントローラで実行されるため、Knative コントローラは、Private Registry に対する読み取り権限が必要です。Knative Serving は、Image Registry からタグをダイジェストに変換する際に、[google/go-containerregistry](https://github.com/google/go-containerregistry) のライブラリを利用しています。このライブラリは、k8s.io/kubernetes の [credentialprovider](https://github.com/kubernetes/kubernetes/tree/master/pkg/credentialprovider) に依存しており、バージョンが古いため、ECR で正しく動作しません。 (google/go-containerregistry で [Please stop importing k8s.io/kuberneretes](https://github.com/google/go-containerregistry/issues/496) が上がっているので、そもそも外部で使われる前提のライブラリではないようです。[Credential provider extraction](https://github.com/kubernetes/enhancements/pull/1406) という KEP もあるので、最終的にこちらに寄せるのかなと思っています。)

ですので、ECR を利用する場合は、[gist](https://gist.github.com/toVersus/6a76477e6eb342499ea7242e5ed65a72) にあるパッチを当てる必要があります。このパッチのおかげで、ワーカーノードの EC2 インスタンスに紐付いたポリシーを使って、tag to digest の変換が行われるようになります。以前は、このパッチが upstream に組み込まれていたのですが、v0.10.0 からこのパッチが適用されなくなりました。[knative/serving#4549](https://github.com/knative/serving/issues/4549) で、パッチが適用されなくなった問題の状況がトラックされています。

# Burst Capacity

Knative Serving におけるアプリケーション (Pod) のスケールについては、公式ドキュメントの [Configuring autoscaling](https://knative.dev/docs/serving/configuring-autoscaling/) と [Autoscale Sample App - Go](https://knative.dev/docs/serving/samples/autoscale-go/) にまとまっているので、省略します。

Knative Serving [v0.8.0](https://github.com/knative/serving/releases/tag/v0.8.0) から [Burst Capacity](https://docs.google.com/document/d/1KK32OG-QzOXFrFcD9O7L2udMuzpyYdhDN9PoGdw3y_s) ([@knative-user](https://groups.google.com/forum/#!forum/knative-users) に参加する必要あり)  と呼ばれる機能が入りました。Burst Capacity は、急激なリクエスト数の増加に対応するために実装された機能で、ある閾値を越えると Activator (中央集権的なリクエストバッファ) がリクエストパスに入ってくれます。これにより、突発的なリクエストの増加時に Pod がスケールし終わるまで、Activator でリクエストをバッファすることができます。ベストエフォートの実装なので、特定の条件で上手くいかないことがあります。

Burst Capacity は、クラスターレベルもしくは Pod 単位で設定することができます。クラスターレベルの場合、config-autoscaler の [target-burst-capacity](https://github.com/knative/serving/blob/release-0.11/config/config-autoscaler.yaml#L71-L83) で設定し、Pod 単位の場合は、`autoscaling.knative.dev/targetBurstCapacity` を PodSpec のアノテーションで定義します。v0.11.0 時点で、クラスター全体でのデフォルト値が `200` に設定されています。

Burst Capacity の機能では、Excess Burst Capacity というメトリクスが重要です。このメトリクスは以下の式で計算されます。

```
[Excess Burst Capacity] = [Total Capacity] - [Current # of Request In Flight] - [Target Burst Capacity ]
```

Total Capacity は、Pod が同時処理できるリクエスト数 ([containerConcurrency](https://knative.dev/docs/serving/configuring-autoscaling/#containerconcurrency) か [container-concurrency-target-default](https://knative.dev/docs/serving/configuring-autoscaling/#target) の値) と Ready 状態な Pod の数で計算されます。Current # of Request In Flight は、Revision が現在処理中のリクエスト数です。Excess Burst Capacity が負の値の場合に、Activator がリクエストパスに入ります。(ServerlessService が Proxy モードにスイッチします。)

Activator を常にリクエストパスに置きたくない場合は、サービスが定常的に受けるベースのリクエスト数と突発的に受けるリクエスト数のメトリクスを事前に確かめ、`autoscaling.knative.dev/targetBurstCapacity` の設定値を調整するようにして下さい。

# Wrapped Up

- Knative Serving を利用するワークロードと利用しないワークロードを判断する
- Ingress の選択は安定性も考慮して、慎重に行う
- Cloud Load Balancer の特性に注意して、L4/L7 のロードバランサーを適切に選択する
- アクセスログやメトリクスは正しく収集して役立てる
- Knative Serving の Readiness Probe の仕組みは少し変わっているので注意が必要
- ECR を使いたい場合は、Knative Serving にパッチを提供する必要がある
- Burst Capacity が意外と厄介な機能なので、理解してから利用する

Knative Serving の[公式ドキュメント](https://knative.dev/docs/serving/)は、GA リリースに向けて絶賛改修中です。よりコンパクトにまとまった非公式の [Tips](https://knative.tips/) もあります。