Istio で gRPC サービスの AB テスト基盤っぽいものを作ってみる

機械学習を用いたサービスを開発する際、推論用の API サーバを立ててリクエストを受ける構成が多いと思います。モデルを改良したときには、全てのリクエストを新しいモデルをサービングする API に渡すわけではなく、トラフィックを分散して AB テストを行い、何らかのメトリクスが改善するかを確認することが多々あります。また、ここ数年は gRPC でAPI サーバを実装することも多いと思います。

基盤に Kubernetes を利用している場合、Istio の Traffic Management により指定の割合でトラフィックを分散させて AB テストが可能です。今回は、Istio で簡易的な gRPC サービスの AB テスト基盤を作ってみたのでメモ代わりに記載します。

検証に使用した version 情報は以下です。

  • Kubernetes
    • minikube v1.20.0
  • Istio
    • 1.10.2

また、ソースコードは以下に置いてあります。

github.com

準備

今回はローカルの Kubernetes 環境として Minikube を使用します。

まず、 Minikube を起動します。

$ minikube start

また、後ほど Istio をインストールした際に作られる istio-ingressgateway という LoadBalancer の service のため、以下のコマンドを実行します。(別のターミナルで実行することをおすすめします)

$ minikube tunnel

次に、Istio をインストールします。方法は多くありますが、今回は istioctl を使ってインストールします。 ドキュメントの Download Istio に従ってダウンロードし、 istioctl コマンドが利用できるようにします。 そして、preset の demo profile を使用して Istio をインストールします。(その他の profile については こちら を参照ください)

$ istioctl install --set profile=demo

インストールが終わると、 istio-system という namespce に istio-ingressgateway という service がデプロイされています。これがクラスタ外部からのリクエストの受け口です。

$ kubectl get service -n istio-system
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)                                                                      AGE
istio-egressgateway    ClusterIP      10.96.47.101    <none>          80/TCP,443/TCP                                                               4h51m
istio-ingressgateway   LoadBalancer   10.103.48.202   10.103.48.202   15021:30365/TCP,80:31206/TCP,443:32478/TCP,31400:32443/TCP,15443:32189/TCP   4h51m
istiod                 ClusterIP      10.98.134.165   <none>          15010/TCP,15012/TCP,443/TCP,15014/TCP                                        4h51m

また、アプリケーションをデプロイした際に pod に Envoy がサイドカーとして自動で挿入されるように label を付与します。(今回は default の namespace を使用します)

$ kubectl label namespace default istio-injection=enabled

アプリケーションのインストール

デモ用の gRPC サービスをデプロイします。今回デモ用として作成したのは、name を受け取りその name を利用した message を返すだけのアプリケーションです。 grpcurl によるリクエスト例は以下です。

$ grpcurl -d '{"name": "your name"}' -plaintext localhost:50051 sample.HelloService/SayHello
{
  "message": "Hello your name"
}

以下の manifest を apply します。v1 は 受け取った name をそのまま (Hello your name)、v2 は namesan を付けて (Hello your name san) 返します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-server-v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-server
  template:
    metadata:
      labels:
        app: hello-server
        version: v1
    spec:
      containers:
      - name: hello-server
        image: unpuytw/grpc-hello-server:v1
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 50051
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-server-v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: hello-server
  template:
    metadata:
      labels:
        app: hello-server
        version: v2
    spec:
      containers:
      - name: hello-server
        image: unpuytw/grpc-hello-server:v2
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 50051
---
apiVersion: v1
kind: Service
metadata:
  name: hello-server-service
spec:
  selector:
    app: hello-server
  ports:
  - name: grpc-api
    protocol: TCP
    port: 50051
    targetPort: 50051
  type: ClusterIP

ここで大事なのは、service の port の name を grpc-* にしなければいけないっぽいです。

An ordered list of route rules for HTTP traffic. HTTP routes will be applied to platform service ports named ‘http-’/‘http2-’/‘grpc-*’, gateway ports with protocol HTTP/HTTP2/GRPC/ TLS-terminated-HTTPS and service entry ports using HTTP/HTTP2/GRPC protocols. The first rule matching an incoming request is used.

Istio / Virtual Service

また、pod を確認すると 2 つのコンテナが起動しています。1 つが gRPC サーバのコンテナ、もう 1 つが自動で挿入されるように設定した Envoy のコンテナです。

$ kubectl get pod
NAME                               READY   STATUS    RESTARTS   AGE
hello-server-v1-6f59f85596-j4q89   2/2     Running   0          5h30m
hello-server-v2-6dc6684798-xbfm4   2/2     Running   0          5h30m

Traffic Management 用リソースのデプロイ

Istio の Traffic Management のため、Gateway, Virtual Service, Destination Rule をデプロイします。

以下の manifest を apply します。VirtualService リソースで指定していますが、今回は v1 に 90% 、v2 に 10% のトラフィックを割り当てます。

apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: ab-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
      - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ab-virtualservice
spec:
  hosts:
  - "*"
  gateways:
  - ab-gateway
  http:
    - route:
      - destination:
          host: hello-server-service
          subset: v1
          port:
            number: 50051
        weight: 90
      - destination:
          host: hello-server-service
          subset: v2
          port:
            number: 50051
        weight: 10
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: grpc-api
spec:
  host: hello-server-service
  subsets:
    - name: v1
      labels:
        version: v1
    - name: v2
      labels:
        version: v2

動作確認

まず、クラスタ内から hello-server-service に対してリクエストを送ってみて正常に動作している確認します。そのために、gRPC のクライアントとして grpcurl が使用できるコンテナをデプロイして、そのコンテナからリクエストを送ってみます。使っているイメージは こちら で用意した golang のベースイメージに grpcurl をインストールしただけのものです。

以下のように、正常にリクエストが返ってくることが確認できました。

$ kubectl run grpc-client --rm -it --image=unpuytw/grpc-client sh
root@grpc-client:/go# grpcurl -d '{"name": "your name"}' -plaintext hello-server-service:50051 sample.HelloService/SayHello
{
  "message": "Hello your name"
}

次に、クラスタ外から istio-ingressgateway に対してリクエストを送ってみて、正常に Traffic Management が動作しているかを確認します。

こちらも、以下のように正常にリクエストが返ってくることが確認できました。

$ export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
$ grpcurl -d '{"name": "your name"}' -plaintext $INGRESS_HOST:80 sample.HelloService/SayHello
{
  "message": "Hello your name"
}

Traffic Management の検証用に複数リクエストを送るため、 ghz というツールを見つけたので使用してみます。

github.com

また、トラフィック分散の確認のため kiali を使ってみます。以下でインストールできます。(${ISTIO_HOME} は Istio をダウンロードしたパスです。)

$ kubectl apply -f ${ISTIO_HOME}/samples/addons

以下で起動できます

$ istioctl dashboard kiali

それでは、1000 件のリクエストを送ってみます。以下で可能です。

$ ghz --call sample.HelloService/SayHello \
    -d '{"name": "your name"}' \
    -n 1000 \
    --insecure \
    $INGRESS_HOST:80

kiali のコンソールを確認すると、v1 に約 90%、 v2 に約 10% とトラフィックを指定した割合で分散できていることが以下のように確認できました。

f:id:pompom168:20210702222624p:plain

今回はこれで終わりです。これをベースに Istio の機能を諸々確認したいと思います。

参考