Go+gRPC-WebのアプリケーションをGKE+Envoyで負荷分散する
概要
今回はGo+gRPC-Webで実装したバックエンドサーバをGKE+Envoyで負荷分散していきます。
gRPCとEnvoyを使用した記事は多く見かけたのですが、gRPC-Webを用いた記事は見つからなかったので、実際に構築してみました。基本的には従来のgRPCを使った場合と一緒です。
因みに、詳しくは後述しますが、gRPC-Webはclient→envoy間はhttp/1.1なので、「その間にALB挟めば普通に負荷分散できるんじゃね?🤔」 …と思い一応検証してみましたが、問題なく動作致しました。当たり前か…。
ただ、後学の為にも勉強をしておいて損はないはずです!
僕のスペックとしてはKubernetes含め、インフラに関しては殆ど初心者ですので、誤りや不備に対するご意見・ご指摘大歓迎です!
アーキテクチャ図
今回説明しないこと
- KubernetesやEnvoy等の基礎知識
- gRPC-Webを使用したクライアントの作成
- Goを使用したgRPCサーバの作成
- kubectlやgcloudなどのCLIツールのインストール
この辺の準備は各自でよろしくお願いします🙇♂️
前提知識
GKEのL7LBはgRPCに対応していません。(AWSのALB等も同様です)
また、gRPCの通信は永続化される(HTTP/2)のでスケール時に負荷分散されないという問題もあります。
そこで、Envoyが登場します。Envoyはクライアント→Envoy、Envoy→バックエンドサーバ間の両方ともHTTP/2とgRPCをサポートします。
ただ、gRPC-Web自体はクライアント→バックエンド間はHTTP/1.1になりますので、間にプロキシを挟む必要があります。(EnvoyやNginx等で可能です)
なぜプロキシを挟む必要があるのか
ここら辺に関しては以下のサイトで分かりやすくご説明されています。
長いこと Web では HTTP/1.1 が使われてきました。なので Web サーバが HTTP/1.1 で通信できることは期待できます(というかそれが Web サーバの定義か)が、 HTTP/2 で通信できるかどうかは不明です。ブラウザは HTTP/2 で通信できればそうしますが、できない場合は HTTP/1.1 を勝手に使います。
また通信プロトコルはレイヤーアーキテクチャなので、上位に位置する Web アプリケーションが下位で使われているのが HTTP/2 なのか、あるいは HTTP/1.1 なのかを意識するべきではありません。つまり HTTP/1.1 と HTTP/2 を透過的に扱えなければならない以上、片方にしか存在しない機能を操作する API が提供されることはない、そして gRPC ではその領域に属する機能を使っているわけなのです。
なるほど…🤔
ただ、公式によると、今後プロキシをなくして言語レベルでgRPC-Webに対応する可能性もあるようなので、可能なら是非実現して欲しいですね!
環境構築
それでは実装していきましょう!
流れはざっくりこんな感じです!
- EnvoyをgRPCサーバのSidecarコンテナとしてデプロイ
- ServiceをHeadlessにする
- Envoyをロードバランサーとしてデプロイ
EnvoyをgRPCサーバのSidecarコンテナとしてデプロイ
EnvoyをgRPCサーバのSidecarプロキシとしてデプロイします。
Sidecarは下記の様に spec.template.containers
に続けて書いていけば実現できます。
~~ 略 ~~
containers:
- name: golang # golang
resources:
requests:
cpu: 50m
memory: 10Mi
limits:
cpu: "1"
memory: 20Mi
image: gRPCサーバのイメージ
imagePullPolicy: IfNotPresent
ports:
- containerPort: 9000
name: golang-service
- name: envoy # envoy sidecar
resources:
requests:
cpu: 50m
memory: 10Mi
limits:
cpu: "1"
memory: 20Mi
image: envoyproxy/envoy:v1.12.2
imagePullPolicy: IfNotPresent
volumeMounts:
- name: sidecar-service-config
mountPath: /etc/envoy
command:
- "/usr/local/bin/envoy"
args:
- "--config-path /etc/envoy/sidecar-service.yaml"
ports:
- containerPort: 8080
name: envoy-sidecar
- containerPort: 9901
name: envoy-admin
Envoyの設定ファイルです。
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: golang_service
max_grpc_timeout: 0s
cors:
allow_origin:
- "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: authorization,deadline,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: golang_service
connect_timeout: 0.25s
type: static
http2_protocol_options: {}
upstream_connection_options:
tcp_keepalive:
keepalive_time: 300
lb_policy: round_robin
# win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below
hosts: [{ socket_address: { address: 127.0.0.1, port_value: 9000 } }]
今回はSidecarとしてデプロイするので、clusters.hosts.address
は 127.0.0.1
で大丈夫です。後はConfigMapを作成するなり、イメージに直接組み込むなりして下さい🙆♂️
ServiceをHeadlessにする
下記の様に ClusterIP: None
とするとHeadlessになります。
Headless Serviceを使うと背後にあるPodに直接アクセスできるレコードがクラスタ内部DNSに作成されます。EnvoyはこのDNSレコードからPodのIPを取得し、Envoy側の構成にしたがって負荷分散を行います。
apiVersion: v1
kind: Service
metadata:
name: user-service
spec:
clusterIP: None
ports:
# Actually, no port is needed.
# but set it because of the following bug.
# https://github.com/kubernetes/kubernetes/issues/55158
- name: headless
port: 12345
protocol: TCP
targetPort: 12345
selector:
app: user-service
これでバックエンドサーバ側の設定は終了です!
Envoyをロードバランサーとしてデプロイ
Envoyの設定ファイルを作ります。
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 80 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: user
max_grpc_timeout: 0s
cors:
allow_origin_string_match:
- safe_regex:
google_re2: {}
regex: \*
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: authorization,deadline,keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
access_log:
- name: envoy.file_access_log
config:
path: /dev/stdout
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: user
connect_timeout: 0.25s
type: strict_dns
http2_protocol_options: {}
lb_policy: round_robin
health_checks:
- timeout: 5s
interval: 10s
unhealthy_threshold: 2
healthy_threshold: 2
tcp_health_check: {}
hosts: [{ socket_address: { address: user-service, port_value: 8080 } }]
clusters.hosts
にはEnvoyが負荷分散する為にも、各PodのIPアドレスを知っている必要があるのですが、ここで先程作成したHeadless Serviceを使用します。今回は user-service
となっていますね。これで設定ファイルの記述は終了です!
因みにEnvoyロードバランサーのServiceはこんな感じです。(Deploymentは省略します。)
apiVersion: v1
kind: Service
metadata:
name: lb-service
spec:
type: LoadBalancer
selector:
app: envoy-lb
ports:
- name: proxy
protocol: TCP
port: 80
targetPort: 80
- name: admin
protocol: TCP
port: 9901
targetPort: 9901
そして、最後に下記の様な感じでクラスタを作成し、kubectl apply
すると環境構築は完了です!
$ gcloud container clusters create --num-nodes=3 hoge-cluster \
--zone asia-northeast1-a \
--machine-type g1-small \
--enable-autoscaling --min-nodes=3 --max-nodes=6
あとは kubectl get svc
等で、エンドポイントを調べ、実際にリクエストを送ってみれば負荷分散が完了しているはずです!
終わりに
僕自体はよくAWSを利用するので、元々はEKSを使いたかったのですが、クラスターひとつに付き固定で0.1$/hかかってしまうので、実際に運用するとなると貧乏学生の僕にはちょっと厳しいですね…。その辺GKEはお値段も比較的お安いので、流石のk8sの生みの親Googleって感じですね😊
参考文献
この記事は以下の情報を参考にして執筆しました。
Author And Source
この問題について(Go+gRPC-WebのアプリケーションをGKE+Envoyで負荷分散する), 我々は、より多くの情報をここで見つけました https://qiita.com/hirokikondo86/items/65e7dd55d189cd2ca181著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .