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
また、ソースコードは以下に置いてあります。
準備
今回はローカルの 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 は name
に san
を付けて (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.
また、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 というツールを見つけたので使用してみます。
また、トラフィック分散の確認のため 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% とトラフィックを指定した割合で分散できていることが以下のように確認できました。
今回はこれで終わりです。これをベースに Istio の機能を諸々確認したいと思います。