あらためてKnative入門!(Knative Servingやや発展編)

こんにちは、Red Hatでソリューションアーキテクトをしている石川です。 前回の記事からやや間が空いてしまいましたが今回もKnativeについて改めて入門していきたいと思います。

f:id:jpishikawa:20211112205542p:plain

前回の記事はこちら
rheb.hatenablog.com

本編に入る前に、Knative関連の最近のトピックをご紹介させて下さい。 この度なんとKnativeがCNCFのincubatingプロジェクトに移管されることが発表されました! knative.dev

現在はまだ申請中のステータスとのことなので正式にCNCF配下となるのは少し時間がかかると思いますが、これからKnativeがますます盛り上がっていくことが期待されますね。

TLDR;

  • Knative ServingではKPA(Knative Pod Autoscaler)という仕組みでスケーリングを実施。HPAと挙動が違うので注意。
  • Knative/Serviceの設定で複数のRevisionにトラフィックをルーティングできる。
  • Knativeのルーティングを実現するためKourier(中身はenvoy)が使われている。

Knative Servingの特徴

前回の記事ではKnative Servingの基本的な内容に触れましたが、今回はやや発展編としてKnative Servingの特徴についていくつか焦点を当て見ていきたいと思います。 具体的には、
* Knativeのオートスケールの仕組み
* 複数のRevisionへのトラフィックルーティング
* アプリケーションPodにトラフィックが到達するまでの仕組み

という3つのトピックについてご紹介しようと思います。 それでは早速見ていきましょう。

Knativeのオートスケールの仕組み

Knativeはアクセスがない時はPodを0までスケールインし、アクセスが増えるとPodをスケールアウトするという特徴を持っています。 このオートスケールの仕組みはKPA(Knative Pod Autoscaler)というカスタムリソースにより実現されています。 Kubernetesを普段から使われている方であれば、Podのスケーリングの仕組みとしてHPA(Horizontal Pod Autoscaler)を思い浮かべるかと思いますが、Knativeではデフォルトの設定としてHPAではなくKPAを使用しています。

KPAを使うことで多くの場合で適切にスケールを行い、必要な処理を実行することができますが、スケーリングの挙動について注意すべきこともあります。 それは何をトリガーにスケールを実施するか、という点です。

HPAの場合、一般的にはPodのCPUやメモリの使用量に閾値を設定しておき、その値を超えた場合にPodがスケールアウトする挙動となります。またCPU、メモリ以外にもPrometheus等で取得したカスタムメトリクスを利用するパターンもあります。

一方でKPAはデフォルトでPodへの実行中のリクエスト数(concurrency、in-flight requestsととも)の閾値が設定され、その値を超えた場合にスケールするような挙動となります。

concurrencyを使うのではなくrps(requests per second=1秒ごとのリクエスト数)をスケールのトリガーにすることもできますが、どちらもリクエスト数に基づく設定となるため、実行したいサーバーレスのアプリケーションによってはKPAでは上手くスケールできないこともあります。そのため、アプリケーションの性質に応じてKPAとHPAと使い分けることが重要となります。

f:id:jpishikawa:20211214210704p:plain

試しにKPAでのオートスケールを実践してみましょう。 以下のコマンドはheyというツールを使い並列リクエストを実行する例です。

# hey -z 30s -c 50 \
"https://blog-django-py-knative-serving.apps.mycluster.gzqc.p1.openshiftapps.com/" \
&& oc get pods -l app.kubernetes.io/name=blog-django-py

ここでheyコマンドの-c 50はリクエストを実行するworkerの数を、-z 30sでリクエストの実行時間を指定しています。 コマンド実行の結果は以下となります。

Summary:
  Total:    30.0245 secs
  Slowest:  6.9209 secs
  Fastest:  0.0048 secs
  Average:  0.0580 secs
  Requests/sec: 861.0630
  
  Total data:   51654294 bytes
  Size/request: 1998 bytes
  
[中略]

NAME                                               READY   STATUS    RESTARTS   AGE
blog-django-py-00003-deployment-65ccd7dcf7-4vbz8   2/2     Running   0          28s
blog-django-py-00003-deployment-65ccd7dcf7-6bhj4   2/2     Running   0          30s
blog-django-py-00003-deployment-65ccd7dcf7-h6vpd   2/2     Running   0          26s
blog-django-py-00003-deployment-65ccd7dcf7-jv26l   2/2     Running   0          24s
blog-django-py-00003-deployment-65ccd7dcf7-rzgn5   2/2     Running   0          28s
blog-django-py-00003-deployment-65ccd7dcf7-shvwb   2/2     Running   0          28s
blog-django-py-00003-deployment-65ccd7dcf7-v4sf8   2/2     Running   0          28s
blog-django-py-00003-deployment-65ccd7dcf7-wd5n7   2/2     Running   0          28s

実行結果の後半部分からアプリケーションのPodがスケーリングし複数実行されているのが確認できました。

Knativeのスケーリングに関する設定は、Revision単位ではKnative/Serviceのspec.template.metadata.annotationsに記載を行うことで実施可能です。 全てのKnative/Serviceに設定を反映させる場合は、Namespaceknative-servingにあるConfigMapconfig-autoscalerに設定を追記することで反映されます。
詳細についてはKnativeの公式ドキュメントをご参照下さい。

是非色々と試行錯誤をして最適な設定を見つけてみて下さい。

複数のRevisionへのトラフィックルーティング

前回の基礎編でデプロイされるアプリケーションのバージョンはRevisionという単位で管理されるということをお伝えしました。 Knative ServingではKnative/Serviceの設定で各Revisionにどの程度トラフィックを割り当てるのか決めることができます。これを利用することでサーバーレスアプリケーションにおいてもカナリアリリースなどの高度なリリース手法を実現することができます。

f:id:jpishikawa:20211214210739p:plain

具体的に設定の例を見てみましょう。 以下ではKnative/Serviceのspec.traffic以下に各Revisionの情報とそれぞれのトラフィックの割合を定義しています。 ここではカナリアリリースを想定し、最新のRevisionに全体のトラフィックの5%、一つ前のRevisionに95%が振り分けられるようにしています。

spec:
  traffic:
    - latestRevision: true
      percent: 5
      tag: latest
    - revisionName: "blog-django-py-00002"
      percent: 95
      tag: current

このyamlを適用すると、OpenShiftコンソールでは以下のような形でトポロジーを見ることができます。

f:id:jpishikawa:20211214210804p:plain

意図した通りにアクセス先が振り分けられていることを確認できました。
この時、それぞれのRevisionのPodは、エンドポイントへのアクセスではなくPodへのアクセスがあって初めて起動するような形となります。

アプリケーションPodにトラフィックが到達するまでの仕組み

これまでお伝えしてきた通り、Knative Servingではサーバーレスアプリケーションのエンドポイントにアクセスした時の動きとして、Podの数が0であればスケールしたり、Revisionごとにトラフィックを割り振ったりなど、様々な機能を提供しています。

こうした機能を提供するため、どのようなオブジェクトが関連しているか詳細を以下の図で表しました。 f:id:jpishikawa:20211214210825p:plain

やや複雑な絵になりますが、ポイントとして以下を押さえれば大丈夫です。

1) 外部向けエンドポイント公開や、Revision間のトラフィックルーティングはNamespaceknative-serving-ingress内のオブジェクトで実施されている
2) クラスタ内部向けのエンドポイントはアプリケーションのNamespace(上記の場合app-ns)で公開されている
3) サーバーレスのアプリケーションPodはアプリケーションのNamespaceで実行される
4) アプリケーションPodの数が0の場合、Namespaceknative-servingactivator/autoscalerを介してPodをスケールする

外部/内部向けに公開されたエンドポイントにアクセスすると、まずはNamespaceknative-serving-ingress3scale-kourier-ingressという名前のPodにアクセスすることとなります。

これはKourierというKnativeでトラフィックルーティングを行うコンポーネントで、実態としてはIstioでも利用されているenvoyです。
ルーティングの設定は同じNamespaceにあるnet-kourier-controlerというPodに管理されており、このPodはControl planeとしてアプリケーションのNamspaceにあるKnative/Ingressというカスタムリソースを見てenvoyに動的に設定を反映させています。

このKnative/Ingressは、
Knative/Service -> Knative/Route -> Knative/Ingress
といった親子関係を持っており、大元のKnative/Serviceが作成されるとその情報を元に自動で作成されるカスタムリソースです。

エンドポイントからKourier Podに到達したトラフィックはアプリケーションNamspaceのDefault/Serviceを経由し、Namspaceknative-servingにあるactivatorというPodにアクセスします。このactivatorはその名の通り、アプリケーションPodを起動するトリガーとなる役割を持っており、autoscalerを経由してアプリケーションPodをスケールさせます。

アプリケーションPodが起動すると同じNamspaceにあるEndpontsオブジェクトが更新され、Default/Serviceを介してPodへのアクセスが可能となります。

まとめ

Knative Serving発展編いかがだったでしょうか。前回に比べるとやや深掘りした内容となりましたが、あくまでもKnative/Serviceカスタムリソースさえ作成してしまえばサーバーレスのアプリケーションを実現できてしまうというのがKnativeの良いところです。
次回はKnativeのもう一つのコンポーネントであるKnative Eventingについてご紹介し、イベント駆動でのサーバーレスアプリ実行方法をご紹介したいと思います。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。