Knative Servingを試してみた

この記事はCyberAgent Developers Advent Calendar 2018 8日目の記事です

この記事では、最近興味があるknativeの中のServingについて紹介していきます。

knative とは

サーバレスワークロードのビルド、デプロイ、管理するためのKubernetesをベースにしたプラットフォームを提供するフレームワークです cloud.google.com

knativeは、Serving, Build, Eventingの3つのコンポーネントがあります。概要としてはそれぞれこのような認識です。

  • Serving: リクエスト駆動でポッドの起動やポッド数の管理
  • Build: ソースからコンテナを生成するビルドに関するオーケストレーション
  • Eventing: イベント管理と配信

今回は、このServing部分で実際に動作させながらスケールリングやリクエストの振り分けなどをまとまめていきます。 環境としては、docker desktop for mac上のk8s環境で行いました。

インストール

knative servingのセットアップには、kubernetes環境にIstioとKnative Servingの2つをインストールすることでできます。

今回は、docker desktop for macでのkubernetesクラスタを使っていますが、他環境の場合も記載されているので、手持ちのkubernetesで試すことができると思います。

docs/README.md at master · knative/docs · GitHub

istioのインストール

まずistioのインストールから行います。 minikubeの場合を参考にNodePortにしてapplyします。

curl -L https://github.com/knative/serving/releases/download/v0.2.2/istio.yaml \
  | sed 's/LoadBalancer/NodePort/' \
  | kubectl apply --filename -

kubectl label namespace default istio-injection=enabled

関連するのpodがRunningやCompletedになるとインストールは完了です。apply後にはistio-injection=enabled ラベルを設定も行います。 この istio-injection の設定ができていないと動作させられないです。後続のkubectl applyなどはエラーも出ず、この設定漏れにも気づかずになぜ動作しないのかと時間を使ったことがあるので忘れずやりましょう。

それぞれ行うと自分の環境だと次のようになりました。

$ kubectl get pods --namespace istio-system
NAME                                        READY     STATUS      RESTARTS   AGE
istio-citadel-84fb7985bf-tvjf2              1/1       Running     0          1m
istio-cleanup-secrets-4lk9z                 0/1       Completed   0          1m
istio-egressgateway-bd9fb967d-g57cw         1/1       Running     0          1m
istio-galley-655c4f9ccd-vr2dz               1/1       Running     0          1m
istio-ingressgateway-688865c5f7-ppqwl       1/1       Running     0          1m
istio-pilot-6cd69dc444-9mqgp                2/2       Running     0          1m
istio-policy-6b9f4697d-b8n4r                2/2       Running     0          1m
istio-sidecar-injector-8975849b4-2sf9h      1/1       Running     0          1m
istio-statsd-prom-bridge-7f44bb5ddb-wm2pw   1/1       Running     0          1m
istio-telemetry-6b5579595f-vmlfj            2/2       Running     0          1m

$ kubectl get namespace -L istio-injection
NAME           STATUS    AGE       ISTIO-INJECTION
default        Active    3d        enabled
docker         Active    3d        
istio-system   Active    6m        disabled
kube-public    Active    3d        
kube-system    Active    3d     

Knative Servingのインストール

Knative Servingのインストール時にも、先程と同様にNodePortに変更してapplyします。

curl -L https://github.com/knative/serving/releases/download/v0.2.2/release-lite.yaml \
  | sed 's/LoadBalancer/NodePort/' \
  | kubectl apply --filename -

完了時には次のようになりました。

$ kubectl get pods --namespace knative-serving
NAME                          READY     STATUS    RESTARTS   AGE
activator-df78cb6f9-cdjqz     2/2       Running   0          1m
activator-df78cb6f9-gbn7f     2/2       Running   0          1m
activator-df78cb6f9-whhbk     2/2       Running   0          1m
autoscaler-6fccb66768-cnwzk   2/2       Running   0          1m
controller-56cf5965f5-dnfr6   1/1       Running   0          1m
webhook-5dcbf967cd-8bjzs      1/1       Running   0          1m

これで一連のインストールは完了です。

Knative Serving

今回扱うKnative Servingは、次の4つの要素があります。

  1. Service
  2. Route
  3. Configuration
  4. Revision

参考: https://github.com/knative/docs/tree/master/serving

Service

他の3つの要素を含んだ全体を自動的に管理するもの。

更新に対する新しいRevionを作成し、最新のRevisionや特定のRevisionに常にルーティングされるように管理している。

kubectl get kservice で定義されているものを取得できる

Route

どのRevionに何%トラフィックを流すかなどをマッピング

kubectl get route で定義されているものを取得できる

Configuration

実行する構成の定義、変更されると新しいRevisionが作成される

他のものと同様に kubectl get configuration 定義されているものを取得できる

Revision

Configurationの履歴にあたるポイントインタイムスナップショット。 kubectl get revision で取得できる

デプロイ

環境も整ったので、実際にデプロイしていきます。 今回は、単純なjsonを返すサーバアプリで、このアプリのdockerイメージはDocker Hubにある状態です。

これをknative上で動かすためにknative servingのServiceリソースを追加します。

# echo-server-service.yaml

apiVersion: serving.knative.dev/v1alpha1
kind: Service
metadata:
  name: echo-server
  namespace: default
spec:
  runLatest:
    configuration:
      revisionTemplate:
        spec:
          container:
            image: terachanple/echo-server-go:latest
kubectl apply -f echo-server-service.yaml

このapplyが成功すると、

curl -H "Host: echo-server.default.example.com" "http://localhost:32380"

上記のようなcurlコマンドでデプロイしたecho-serverへリクエストできます。

ポートの32380knative-ingressgateway へリクエストするためのもので次のようにして確認できます。

# knative-ingressgateway のNodePort
kubectl get svc knative-ingressgateway --namespace istio-system --output 'jsonpath={.spec.ports[?(@.port==80)].nodePort}'

# HostヘッダのURL
kubectl get services.serving.knative.dev echo-server -o jsonpath='{.status.domain}'

podの動きとしては次のようになっていて、初めてリクエストしたときにpodの初期化生成が行われています。そしてしばらくリクエストをしないとそのpodは削除されています。なのでリクエストがない状態であればpod数が0になります。

$ kubectl get pod --watch
NAME                                           READY     STATUS    RESTARTS   AGE
echo-server-00001-deployment-dbb4b64b9-xsnjt   0/3       Pending   0         0s
echo-server-00001-deployment-dbb4b64b9-xsnjt   0/3       Pending   0         0s
echo-server-00001-deployment-dbb4b64b9-xsnjt   0/3       Init:0/1   0         0s
echo-server-00001-deployment-dbb4b64b9-xsnjt   0/3       PodInitializing   0         3s
echo-server-00001-deployment-dbb4b64b9-xsnjt   2/3       Running   0         5s
echo-server-00001-deployment-dbb4b64b9-xsnjt   3/3       Running   0         6s
echo-server-00001-deployment-dbb4b64b9-xsnjt   3/3       Terminating   0         5m
echo-server-00001-deployment-dbb4b64b9-xsnjt   0/3       Terminating   0         5m
echo-server-00001-deployment-dbb4b64b9-xsnjt   0/3       Terminating   0         6m
echo-server-00001-deployment-dbb4b64b9-xsnjt   0/3       Terminating   0         6m

上のように1podだけでなく、リクエストが増えれば、podの数も増加させてくれます。

https://github.com/knative/docs/blob/master/serving/samples/autoscale-go/README.md#algorithm ここで記載されているように、スケールはpodごとのリクエスト中でまだ完了していない数の平均をもとに算出しており、例としては次のような計算になるようです。

条件
- リクエスト数: 350req/s
- 1リクエストの処理時間: 0.5s/req

式
350[req/s] * 0.5[s/req] = 175
175 / 100 (デフォルト:100) = 1.75
ceil(1.75) = 2 [pods]

ちなみに、podがない状態からの最初のアクセスではpodの作成処理から行われているので時間はかかってしまいます。

# echo-server podが0個の場合
$ curl -H "Host: echo-server.default.example.com" -s -o /dev/null -w  "%{time_starttransfer}\n" "http://localhost:32380"
9.562675

# echo-server podが1個の場合
$ curl -H "Host: echo-server.default.example.com" -s -o /dev/null -w  "%{time_starttransfer}\n" "http://localhost:32380"
0.023103

一連のserviceを削除する場合は次のようにkubectlから削除できます。

kubectl delete -f echo-server-service.yaml

リクエストをrevisionに振り分ける

knative servingではRouteを設定することでバージョンAに何%のトラフィック、バージョンBに何%のトラフィックを流すといったような制御ができます。 なので機能追加などで変更した新しいものへのリクエストは数パーセントから初めて徐々に増やしていくような形をとることができます。

先程で使ったecho-serverを使って試していきます。 Serviceだと自動で最新のものになるようなので、 ConfigurationRoute を追加していきます。 今あるecho-serverに100%リクエストが行く形です。

# echo-server-split.yaml (latestのみ)

apiVersion: serving.knative.dev/v1alpha1
kind: Configuration
metadata:
  name: echo-server
  namespace: default
spec:
  revisionTemplate:
    metadata:
      labels:
        knative.dev/type: container
    spec:
      container:
        image: terachanple/echo-server-go:latest
        imagePullPolicy: Always
---
apiVersion: serving.knative.dev/v1alpha1
kind: Route
metadata:
  name: echo-server
  namespace: default
spec:
  traffic:
  - revisionName: echo-server-00001
    percent: 100
    name: "v1"

上記のyamlをapplyすると先程と同じように

curl -H "Host: echo-server.default.example.com" "http://localhost:32380"

curlコマンドでリクエストできるようになります。

次にイメージに変更を加えたバージョンを指定し、今までのバージョンには90%、新しいバージョンには10%で振り分けるように設定します。(今回のアプリの変更はレスポンスjsonに含まれるmeta:"v1"meta:"v2" へ変更しただけです)

# echo-server-split.yaml (新しくfeature_aを追加)

apiVersion: serving.knative.dev/v1alpha1
kind: Configuration
metadata:
  name: echo-server
  namespace: default
spec:
  revisionTemplate:
    metadata:
      labels:
        knative.dev/type: container
    spec:
      # container:
      #   image: terachanple/echo-server-go:latest
      #   imagePullPolicy: Always

      container:
        image: terachanple/echo-server-go:feature_a
        imagePullPolicy: Always
---
apiVersion: serving.knative.dev/v1alpha1
kind: Route
metadata:
  name: echo-server
  namespace: default
spec:
  traffic:
  - revisionName: echo-server-00001
    percent: 90
    name: "v1"

  - revisionName: echo-server-00002
    percent: 10
    name: "v2"

再度applyし、 先程までと同様にリクエストすると、 "meta":"v2"となっているレスポンスがたまに返ってくるようになります。 リクエストとレスポンスの例としては、次のようなものです。

# リクエストとレスポンスの例
$ curl -H "Host: echo-server.default.example.com" "http://localhost:32380"
{"meta":"v2","msg":""}%

$ curl -H "Host: echo-server.default.example.com" "http://localhost:32380"
{"meta":"v1","msg":""}%

$ curl -H "Host: echo-server.default.example.com" "http://localhost:32380"
{"meta":"v1","msg":""}%

$ curl -H "Host: echo-server.default.example.com" "http://localhost:32380"
{"meta":"v1","msg":""}%

このように同じリクエストでもリクエストごとに2つのものに振り分けられます。この振り分ける割合を traffic.percent の値を調整することで簡単に制御できます。 特定のものにリクエストする場合はtraffic.name を設定し、その設定したnameをリクエスト時のhostの先頭につけることで指定したrevisionへアクセスできます。 今回であれば、今までの方にv1、新しく追加した方に v2 とnameを設定しているので、次のようにして特定のものへアクセスできます。

# name v1へのアクセス
curl -H "Host: v1.echo-server.default.example.com" "http://localhost:32380"

# name v2へのアクセス
curl -H "Host: v2.echo-server.default.example.com" "http://localhost:32380"

# 指定なし (v1, v2は設定したpercentで振り分けられる)
curl -H "Host: echo-server.default.example.com" "http://localhost:32380"

リクエストを振り分けて簡単に適用できるのは、カナリアリリースなどもできるようになるのですごく嬉しいですね。

まとめ

  • knative servingをローカルのKubernetes環境でデプロイやリクエストの振り分けの動作までの方法をまとめました
  • リクエストに応じて自動でスケールし、リクエストが無いとpod数が0にまでなるので、必要分だけが起き上がるサーバレスな感じは体感でよかった
  • pod数が0の状態からのアクセス時には時間がかかってしまうので、apiサーバとして使うときは注意が必要そうに感じました。batchサーバだとすぐに開始しないといけない用途以外にはあまり気にならないようには感じました。
  • リクエストの振り分けは特定の%を設定するだけで簡単に行えるので便利
  • servingだけだとリクエストに応じてだけになるってしまうので今回できなかったですが、BuildとEventingを連携して、特定のイベントでソースがビルドされり、特定のサービスが動くというのも試していきたいと思います

今回使用したyamlやecho-serverのコードは次のリポジトリに置いてあります github.com