Traffic Director でプロキシレス サービスメッシュを試してみる

Kazuki Uchima
google-cloud-jp
Published in
27 min readDec 20, 2020

この記事は Google Cloud Japan Customer Engineer Advent Calendar 2020 の 20 日目の記事です。

皆さん、こんにちは。Google Cloud の内間(@kuchima)です。本日も前日の Kozzy さん(Kozzy Hasebe)に引き続きみんな大好きサービスメッシュネタです。

この記事では、Google Cloud が提供している Traffic Director というサービスを利用したプロキシレス サービスメッシュ(以降、プロキシレス gRPC と記載します) の概要について説明していきたいと思います。

サービスメッシュとは?という話についてはこちらの記事では触れないので、Kozzy さんの記事「Anthos Service Mesh を導入、移行、そして使いこなしてみよう」をご参照ください。

Traffic Director 概要

まず、Traffic Director の概要について説明します。

Traffic Director はサービスメッシュのコントロールプレーン機能を提供してくれるサービスです。Traffic Director を利用することで仮想マシンやコンテナベースのアプリケーションで簡単にスケーラブルなサービスメッシュを構成することができます。

Traffic Director の特徴は以下の通りです:

  • Google Cloud が管理するコントロールプレーン — 99.99% の SLA あり
  • オープンな xDS API による制御
  • データプレーンとして Envoy (サービスプロキシ) や gRPC をサポート
  • グローバルな負荷分散
  • ヘルスチェックの集中管理
  • トラフィックベースのオートスケーリング
  • 柔軟なトラフィックコントロール — Canary, Blue / Green 他
  • 仮想マシンやコンテナベースのアプリケーションに対応

Traffic Director はデータプレーンとして、Envoy プロキシや gRPC をサポートしており、xDS API v2 を利用しこれらデータプレーンを制御します。

xDS API とは、Envoy や gRPC クライアントの動的な設定変更を可能にする機構です。エンドポイントの設定を提供する Endpoint Discovery Service (EDS) や、ルーティングの設定を提供する Route Discovery Service (RDS) などがあり、 これらを総称して xDS API と呼びます。

Traffic Director は xDS API を使用してデータプレーンの設定を動的に制御します。実際のデータトラフィックは Envoy や gRPC クライアントといったデータプレーンで制御しており、Envoy 等プロキシを利用している場合、アプリケーションが送ったトラフィックはプロキシによってインターセプトされ、適切なバックエンド サービスに送信される動きとなります。

Traffic Director による負荷分散は複数のコンポーネントから構成されています。「転送ルール」というコンポーネントでサービスが待ち受けるホスト名や IP アドレス、ポート番号等を定義し、「URL マップ」で実際のルーティング設定を定義、「バックエンド」で実際にサービスをホストするインスタンスの定義をします。この辺りはHTTP(S) Load Balancing といったサービスと同様ですね。各コンポーネント構成のもう少し細かい部分については後ほど説明します。

プロキシレス gRPC サービス 概要

Traffic Director では、前述したようにプロキシを利用したサービスメッシュだけではなく、gRPC アプリケーションのサービスメッシュをプロキシを利用せず構成することができます。

gRPC は HTTP/2 上に実装されたオープンソースの RPC フレームワークです。様々な言語やプラットフォームでサポートされており、ストリームの多重化等によるハイパフォーマンスな通信を実現することが可能です。

gRPC では v1.30 より Go, Java, C++, Python など多くの言語にて xDS API がサポートされました。これにより、Envoy のようなプロキシを介さずに xDS を利用したサービスディスカバリやロードバランシングといったサービスメッシュ機能を利用することができるようになっています。

ただし、まだ Envoy ベースのサービスメッシュと比べて利用可能な機能が限られていますのでご注意ください。2020/12 現在の制約等については、記事の後半でご紹介します。

プロキシレス gRPC の主なユースケースとして以下が考えられます:

  • プロキシ分のリソースを削減したい

プロキシ分のリソースを削減することができるので、サービスメッシュが導入された環境内のリソースを効率的に利用することができます。特に規模が大きくなればなるほど、プロキシの数も増えてくるのでプロキシレス gRPC のメリットは大きくなってきます。

  • プロキシによるレイテンシを回避したい

Istio 等を利用すると各アプリケーションの間にプロキシが入ってきてしまうので、ある程度のレイテンシが発生してしまいます(詳細は以下のページを参照してください)。このようなレイテンシを回避したいというような、パフォーマンスを重視するケースではプロキシ分のオーバーヘッドを削減できるプロキシレス gRPC は良い選択となるかもしれません。

  • サイドカー プロキシをデプロイできない環境

クライアントアプリケーションやサーバーアプリケーションでプロキシを導入できないような環境でも、サービスメッシュの機能を利用することができるようになりました。3rd Party アプリケーションなど、サイドカー プロキシがデプロイできない / サポートされない構成においてもサービスメッシュの機能を利用することができます。

プロキシレス gRPC アーキテクチャ

ここからはプロキシレス gRPC がどのような仕組みで実現されているか簡単に説明をしていきます。

gRPC クライアントで xDS を利用する場合、クライアント側でライブラリ(Go の例: https://godoc.org/google.golang.org/grpc/xds) をインポートし、xDS スキーマでの名前解決ができるよう構成する必要があります。

プロキシレス gRPC では、名前解決のスキーマとして DNS ではなく xDS を構成します。その際、ターゲット URI は xds:///hostname:port のような形式となります(Port を指定しない場合はデフォルトで 80 番ポートが利用されます)。

gRPC クライアントは、後述する bootstrap ファイルから Traffic Director の情報を取得後、Traffic Director に対して LDS (Listener discovery service) をリクエストしターゲット URI の名前解決を行います。
Traffic Director は一致するポートが構成されている転送ルールを検索し、hostname:port に一致するホストルールに対応する URL マップを見つけます。そして URL マップに関連付けられたバックエンドの情報(IP アドレス:ポート番号) を gRPC クライアントに返却します。gRPC は返却されたバックエンドサービスから高い優先度のインスタンスを選択し通信を開始します。

全体的な構成や大まかな処理の流れは以下のようになっています (EDS、RDS 等各 Discovery 処理の詳細は省略しています)。ここから先は、プロキシレス gRPC を利用する際の Traffic Director の各コンポーネントの構成方法について説明していきます。

bootstrap

gRPC アプリケーションは Traffic Director に接続してターゲット サービスの構成情報を取得する必要があるのですが、この接続先の情報は以下のような bootstrap ファイルから参照します。 bootstrap ファイルの中で xDS Server として Traffic Director を指定することにより、gRPC クライアントが Traffic Director と接続することが可能になります。

この bootstrap ファイルの中で定義されているTRAFFICDIRECTOR_GCP_PROJECT_NUMBER TRAFFICDIRECTOR_NETWORK_NAME という値や zone 情報はメタデータサーバからの自動取得もしくは、オプションを利用した手動構成が可能です。

{
"xds_servers": [
{
"server_uri": "trafficdirector.googleapis.com:443",
"channel_creds": [
{
"type": "google_default"
}
]
}
],
"node": {
"id": "b7f9c818-fb46-43ca-8662-d3bdbcf7ec18~10.0.0.1",
"metadata": {
"TRAFFICDIRECTOR_GCP_PROJECT_NUMBER": "123456789012",
"TRAFFICDIRECTOR_NETWORK_NAME": "default"
},
"locality": {
"zone": "us-central1-a"
}
}
}

サービスアカウント

bootstrap ファイルで定義された Traffic Director API にアクセスする際、xDS クライアントはサービスアカウントを利用してアクセスします。GCE 環境であれば、仮想マシンに割り当てられたサービスアカウントを利用し、GKE 環境であれば Node のサービスアカウント、もしくは Workload Identity が有効になっている場合は xDS クライアントの Pod にバインドされている Google Cloud サービス アカウントを利用しアクセスします。

xDS クライアントが利用するサービスアカウントに必要な権限については、以下公式ドキュメントをご参照ください。

転送ルール

転送ルールでは、構成するサービスで受け付ける仮想 IP アドレスとポート、また利用するターゲットプロキシを設定します。以下、構成する際の注意点です:

  • プロキシレス gRPC サービスでは、転送ルールの負荷分散スキームを INTERNAL_SELF_MANAGED にする必要があります
  • 転送ルールは複数所有できますが、各転送ルールの IP:port は一意でなければならず、ポートはホストルールで指定されたポートと一致する必要があります
  • プロキシレス gRPC では DNS ではなく xDS 形式で名前解決を行うため接続先に関する IP アドレス情報は特に指定する必要がありません。そのため、転送ルールの IP アドレスは 0.0.0.0 で設定します
  • ターゲット URI のポートと一致するポートが複数の転送ルールにある場合、Traffic Director は、これらの転送ルールによって参照されている URL マップのホストルールで、一致する hostname[:port] を探し接続先情報を取得します

ターゲットプロキシ

ターゲットプロキシでは、利用プロトコルやルーティングで参照する URL マップを設定します。

プロキシレス gRPC サービスを構成する場合は、ターゲット gRPC プロキシを設定します。また設定する際、validateForProxylessTRUE にすることで構成チェックが行われ、プロキシレス gRPC と互換性のない構成になることを防ぐことができます。

URL マップ

URL マップでは、プロキシレス gRPC クライアントがトラフィックの送信に使用するルーティング ルールを定義します。プロキシレス gRPC ではターゲット URI を指定する際、hostname:port の形式で指定します。

バックエンドサービス

バックエンドサービスでは、バックエンドサービスが利用するプロトコルやヘルスチェックを定義します。プロキシレス gRPC では、プロトコルとして GRPC を設定します。また、負荷分散スキームは INTERNAL_SELF_MANAGED を設定します。

バックエンド

バックエンドでは、サービスをホストしているインスタンスを指定します。GCE 環境では、バックエンドサービスとして、マネージド インスタンス グループ(MIG)と非マネージドインスタンスグループをサポートしており、GKE 環境では、ネットワーク エンドポイント グループ(NEG)をサポートしています。

プロキシレス gRPC を試してみる

ここから実際に簡単なアプリケーションでプロキシレス gRPC を試してみます。前提として、Traffic Director API の有効化など、プロキシレス gRPC サービスを使用した Traffic Director の設定の準備 に記載されている要件はクリアしているものとします。

今回は以下のような構成で環境を作っていきます。 リクエストを投げると “Hello XXX, from <POD_NAME>” と返してくれるシンプルな gRPC サーバと gRPC クライアントを GKE 上で Pod としてデプロイし、プロキシレス gRPC の動作や設定の確認を行います。

1. GKE クラスタの準備

以下のコマンドを実行し、us-central1-a zone 内に grpc-td-cluster という GKE クラスタを作成します。NEG を構成するため、VPC ネイティブクラスタとして作成します。(クラスタ作成まで数分かかります)

gcloud services enable container.googleapis.comgcloud container clusters create grpc-td-cluster \
--zone us-central1-a \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--tags=allow-health-checks \
--enable-ip-alias

GKE クラスタの認証情報を取得します。

gcloud container clusters get-credentials grpc-td-cluster \
--zone us-central1-a

2. gRPC アプリケーションのデプロイ

以下コマンドを実行し、gRPC のサンプルアプリケーション用のマニフェストを作成します。 Service 定義のマニフェスト内で annotation に cloud.google.com/neg: ‘{“exposed_ports”:{“8080”:{}}}’ を追加し NEG (Network Endpoint Group)としてデプロイする点にご注意ください。

cat << EOF > grpc-td-helloworld.yaml
apiVersion: v1
kind: Service
metadata:
name: helloworld
annotations:
cloud.google.com/neg: '{"exposed_ports":{"8080":{}}}'
spec:
ports:
- port: 8080
name: helloworld
protocol: TCP
targetPort: 50051
selector:
run: app1
version: v1
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: app1
version: v1
name: app1
spec:
replicas: 2
selector:
matchLabels:
run: app1
version: v1
template:
metadata:
labels:
run: app1
version: v1
spec:
containers:
- image: grpc/java-example-hostname:1.27.0
name: app1
ports:
- protocol: TCP
containerPort: 50051
EOF

マニフェスト作成後、kubectl apply で GKE クラスタ上にデプロイします。

kubectl apply -f grpc-td-helloworld.yaml

NEG が正常に作成されていることを確認し、後ほどの設定のために NEG 名を記録しておきます。

# List the NEGs
gcloud compute network-endpoint-groups list

# Save NEG name for future configuration
NEG_NAME=$(gcloud compute network-endpoint-groups list | grep helloworld | awk '{print $1}')

# Verify the variable (optional)
echo ${NEG_NAME}

# Optionally examine the NEG
gcloud compute network-endpoint-groups describe ${NEG_NAME} \
--zone us-central1-a

# Optionally examine the endpoint(s) contained
gcloud compute network-endpoint-groups list-network-endpoints \
${NEG_NAME} --zone us-central1-a

3. Traffic Director の構成

まず、バックエンドサービスの定義と、バックエンドに対するヘルスチェック定義を作成します。

まず、ヘルスチェックを作成します。

gcloud compute health-checks create grpc grpc-gke-helloworld-hc \
--use-serving-port

ヘルスチェック用のファイアウォールルールを作成します。Traffic Director のヘルスチェック プローブの IP アドレスレンジである、35.191.0.0/16 と 130.211.0.0/22 を許可します。

gcloud compute firewall-rules create grpc-gke-allow-health-checks \
--network default --action allow --direction INGRESS \
--source-ranges 35.191.0.0/16,130.211.0.0/22 \
--target-tags allow-health-checks \
--rules tcp:50051

バックエンドサービスを作成し、先ほど作成した NEG をバックエンドとして追加します。プロトコルとして GRPC を指定し、load-balancing-schemeとして INTERNAL_SELF_MANAGED を指定します。

gcloud compute backend-services create grpc-gke-helloworld-service \
--global \
--load-balancing-scheme=INTERNAL_SELF_MANAGED \
--protocol=GRPC \
--health-checks grpc-gke-helloworld-hc
gcloud compute backend-services add-backend grpc-gke-helloworld-service \
--global \
--network-endpoint-group ${NEG_NAME} \
--network-endpoint-group-zone us-central1-a \
--balancing-mode RATE \
--max-rate-per-endpoint 5

バックエンドの設定が終わったら、ルーティングルールマップを構成します。まず、URL マップの作成します。

gcloud compute url-maps create grpc-gke-url-map \
--default-service grpc-gke-helloworld-service

作成した URL マップにパスマッチ定義を追加します。ここでは、helloworld-gke:8000 というホスト名に一致するリクエストを 先ほど定義した grpc-gke-helloworld-service というバックエンドサービスに飛ばすよう設定します。

gcloud compute url-maps add-path-matcher grpc-gke-url-map \
--default-service grpc-gke-helloworld-service \
--path-matcher-name grpc-gke-path-matcher \
--new-hosts helloworld-gke:8000

gRPC ターゲットプロキシを作成し、URL マップと紐付けます。

gcloud compute target-grpc-proxies create grpc-gke-proxy \
--url-map grpc-gke-url-map \
--validate-for-proxyless

転送ルールを作成し、先ほど作成したターゲットプロキシと紐付けます。Portは 8000 番で待ち受け、IP アドレスとして 0.0.0.0 を指定します。バックエンドサービスと同様に load-balancing-scheme には INTERNAL_SELF_MANAGED を指定します。

gcloud compute forwarding-rules create grpc-gke-forwarding-rule \
--global \
--load-balancing-scheme=INTERNAL_SELF_MANAGED \
--address=0.0.0.0 \
--target-grpc-proxy=grpc-gke-proxy \
--ports 8000 \
--network default

4. 動作確認

ここまでで、Traffic Director を利用してプロキシレス gRPC アプリケーションに対してアクセスができるようになりました。

ちゃんと接続ができるか、gRPC クライアントをデプロイして試してみましょう。以下コマンドを実行し、GKE クラスタ上に gRPC クライアントをデプロイします。

cat << EOF  | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
run: client
name: sleeper
spec:
selector:
matchLabels:
run: client
template:
metadata:
labels:
run: client
spec:
containers:
- image: openjdk:8-jdk
imagePullPolicy: IfNotPresent
name: sleeper
command:
- sleep
- 365d
env:
- name: GRPC_XDS_BOOTSTRAP
value: "/tmp/grpc-xds/td-grpc-bootstrap.json"
resources:
limits:
cpu: "2"
memory: 2000Mi
requests:
cpu: 300m
memory: 1500Mi
volumeMounts:
- name: grpc-td-conf
mountPath: /tmp/grpc-xds/
initContainers:
- args:
- --output
- "/tmp/bootstrap/td-grpc-bootstrap.json"
image: gcr.io/trafficdirector-prod/td-grpc-bootstrap:0.9.0
imagePullPolicy: IfNotPresent
name: grpc-td-init
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 10m
memory: 100Mi
volumeMounts:
- name: grpc-td-conf
mountPath: /tmp/bootstrap/
volumes:
- name: grpc-td-conf
emptyDir:
medium: Memory
EOF

マニフェストを見てもらうとわかるように、通常の gRPC クライアントに加えて bootstrap ファイルを生成する initContainer 定義が追加されています。これにより、bootstrap ファイルを利用して gRPC クライアントが接続先の Traffic Director の情報を知ることができるようになります。

上記 Pod がデプロイできたら、以下コマンドを実行し Pod 内 Shell に入ります。

kubectl exec -it $(kubectl get pods -o custom-columns=:.metadata.name \
--selector=run=client) -- /bin/bash

Java ベースの gRPC クライアントで動作を確認してみます。まず、Java の gRPC v1.30 をダウンロードしクライアントアプリケーションをビルドします。

curl -L https://github.com/grpc/grpc-java/archive/v1.30.0.tar.gz | tar -xz
cd grpc-java-1.30.0/examples/example-xds
../gradlew --no-daemon installDist

ビルドが終わったら、以下コマンドを実行し gRPC サーバへ接続ができることを確認します。ターゲット URI は xds:/// 形式で入力します。

./build/install/example-xds/bin/xds-hello-world-client "world" \
xds:///helloworld-gke:8000

以下のようにレスポンスが返ってきたら成功です。

Dec 20, 2020 9:16:50 AM io.grpc.examples.helloworld.HelloWorldClient greet
INFO: Will try to greet world ...
Dec 20, 2020 9:16:54 AM io.grpc.examples.helloworld.HelloWorldClient greet
INFO: Greeting: Hello world, from app1-5d5844b6f6-x2thw

ここまでで、gRPC サービスが Traffic Director が提供する xDS を利用して名前解決し、gRPC サーバと接続できることまで確認ができました。

プロキシレス gRPC の制約 (2020.12 現在)

本記事の前半でお伝えした通り、プロキシレス gRPC は Envoy ベースのサービスメッシュと比べてまだいろいろと制約があります。

まず、ルーティングやトラフィック管理機能について、プロキシレス gRPC でも基本的なホスト名やパスベースのルーティングはサポートしていますが、タイムアウトやリトライの設定などまだサポートできていない機能もいくつかあります。詳細については以下対応表をご参照ください。

負荷分散ポリシーも現在はラウンドロビンのみをサポートしています。

また、プロキシレス gRPC サービスを利用可能な言語は現在 C++、Java、Go、Python、PHP、Ruby、C# となっています(今後増えていく予定ありです)。利用可能な gRPC バージョンも上記の通り各機能や利用言語によって異なりますので、ご利用になる際は事前にご確認をお願いします。

その他、細かな制限事項、注意点については公式ドキュメントをご参照ください。

まとめ

ここまでで、Traffic Director が提供する、プロキシレス gRPC の特徴や構成について説明をしてきました。

まだいろいろと縛りはありますが、個人的にはこれからどんどん開発が進み機能拡充していくと、サービスメッシュの構成方法のメジャーな選択肢の1つとなりそうだなあとワクワクしています。

もしご興味があればぜひお試しください!

明日 21 日目は Shinya (Yanagihara Shinya)さんによる「Spring Cloud GCP を使ったアプリケーション開発入門」です!Spring Cloud GCPを使った Java アプリケーションのモダナイズについて説明してくれます!ぜひお楽しみに!

--

--

Kazuki Uchima
google-cloud-jp

Application Modernization Specialist at Google Cloud. All views and opinions are my own.