gRPCは、Googleによる高性能なRPCフレームワークで、HTTP/2の上で動作し、サービス間通信のデファクトスタンダードとなっています。しかし、gRPCトラフィックをプロキシ経由で流そうとすると、すぐに問題が発生します。ほとんどの従来のプロキシサーバーは、HTTP/2や長寿命のストリーミング接続を処理できません。この記事では、プロキシ経由でgRPCを正しく設定する方法を、アーキテクチャの選択からNginx、Envoy、HAProxyの具体的な設定まで解説します。
なぜgRPCは従来のプロキシでうまく機能しないのか
問題を理解するためには、gRPCの内部構造を理解する必要があります。このプロトコルはHTTP/2をトランスポート層として使用しており、これが従来のHTTP/1.1上のRESTとは根本的に異なる点です。ほとんどの企業向けプロキシ、ファイアウォール、負荷分散装置はHTTP/1.1の時代に設計されており、HTTP/2のマルチプレクスストリームを処理できません。
従来のHTTPプロキシを介してgRPCを流そうとすると、以下の具体的な問題に直面します:
- HTTP/1.1へのダウングレード。 多くのプロキシはプロトコルのバージョンを自動的に下げます。gRPCはHTTP/2を必要とします — それがなければ接続は確立されず、クライアントは
UNAVAILABLEエラーを受け取ります。 - 長寿命の接続の切断。 gRPCはサーバーストリーミングと双方向ストリーミングを積極的に使用します。攻撃的なタイムアウトを持つプロキシ(特にAWS ELB Classicや一部のSquidバージョン)は、60秒以上データを送信しない接続を切断します。
- Content-Typeの問題。 gRPCは
Content-Type: application/grpcヘッダーを使用します。このタイプを知らないプロキシは、リクエストを拒否するか、ボディを不正にバッファリングします。 - トレーラー。 gRPCはHTTP/2トレーラーを使用して呼び出しの完了ステータスを伝えます。HTTP/1.1プロキシはトレーラーをサポートしておらず、ステータス情報は失われます。
- ボディのバッファリング。 一部のプロキシは、クライアントに送信する前にレスポンスの全ボディをバッファリングします。ストリーミングgRPC呼び出しの場合、クライアントはストリームが完了するまでメッセージを受け取れません。
重要な結論:
gRPCをプロキシ経由で動作させるには、エンドツーエンドでHTTP/2をサポートするプロキシまたは特別なトンネリングモードが必要です。従来のHTTPプロキシは、設定を変更しない限り適していません。
HTTP/2トンネリング: それはどのように機能するのか
gRPCトラフィックをプロキシするための2つの根本的に異なるアプローチがあり、それらの違いを理解することが、アーキテクチャに適したソリューションを選択するために重要です。
アプローチ1: HTTP/2エンドツーエンド(推奨)
プロキシはHTTP/2を理解し、クライアントとバックエンドの両方にHTTP/2接続を確立します。個別のストリームを分析し、リクエストレベルでの負荷分散を適用し、ヘッダーを追加し、TLSターミネーションを実行できます。これは最も機能的なオプションであり、Envoy、Nginx(バージョン1.13.10以降)、およびクラウドプロバイダーのgRPC対応負荷分散装置がこのように動作します。
アプローチ2: TCPトンネリング(CONNECT)
プロキシはHTTP/2トラフィックを分析せず、単にCONNECTメソッドを介して透過的なTCPトンネルを作成します。クライアントはトンネルを介してバックエンドと直接TLS接続を確立します。プロキシは暗号化されたバイトストリームのみを確認します。この方法は設定が簡単ですが、gRPCリクエストレベルでの負荷分散やヘッダーの追加機能を失います。
| 特徴 | HTTP/2エンドツーエンド | TCP CONNECTトンネル |
|---|---|---|
| リクエストによる負荷分散 | ✅ はい | ❌ いいえ(TCPのみ) |
| プロキシでのTLSターミネーション | ✅ はい | ❌ いいえ |
| ヘッダーの追加 | ✅ はい | ❌ いいえ |
| 設定の難易度 | 中程度 | 低い |
| ストリーミングのサポート | ✅ 完全 | ✅ 完全 |
| 可観測性(メトリクス) | ✅ 詳細 | ❌ TCPのみ |
NginxをgRPCプロキシとして設定する
Nginxはバージョン1.13.10(2018年2月)以降、gRPCのプロキシをサポートしています。動作にはngx_http_grpc_moduleモジュールが必要で、これは標準ビルドに含まれています。重要な点は、Nginxはクライアント側(フロントエンド)でHTTP/2をサポートしていますが、バックエンド側ではgRPCのためにのみHTTP/2を使用し、通常のHTTPアップストリームはHTTP/1.1で動作することです。
NginxでのgRPCプロキシの基本設定
server {
listen 443 ssl http2;
server_name grpc.example.com;
# TLS証明書
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# 最新のTLSパラメータ
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
# proxy_passの代わりにgrpc_passディレクティブを使用
grpc_pass grpc://grpc_backend;
# 長寿命のストリームのためのタイムアウト
grpc_read_timeout 3600s;
grpc_send_timeout 3600s;
grpc_connect_timeout 5s;
# クライアントの実際のIPを渡す
grpc_set_header X-Real-IP $remote_addr;
grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
upstream grpc_backend {
server backend-1:50051;
server backend-2:50051;
server backend-3:50051;
# HTTP/2接続のためのKeepalive
keepalive 32;
}
いくつかの重要な詳細に注意してください。まず、grpc_passディレクティブが使用されており、proxy_passではないこと — これは異なるモジュールで異なる動作をします。次に、grpc_read_timeoutとgrpc_send_timeoutのタイムアウトが3600秒(1時間)に設定されていること — これはデータを送信せずに長時間動作するサーバーストリームにとって重要です。最後に、keepalive 32は、バックエンドとのHTTP/2接続を再利用することを可能にします。
暗号化されていないgRPCの設定 (grpc://)
server {
listen 80 http2;
server_name grpc-internal.example.com;
location / {
grpc_pass grpc://127.0.0.1:50051;
# gRPCエラーの処理
error_page 502 = /error502grpc;
}
location = /error502grpc {
internal;
default_type application/grpc;
add_header grpc-status 14;
add_header content-length 0;
return 204;
}
}
error502grpcブロックは重要な詳細です: バックエンドが利用できない場合、gRPCクライアントが正しく処理できないHTTP 502の代わりに、正しいgRPCステータスUNAVAILABLE (14)を返します。
Envoy Proxy: マイクロサービスにおけるgRPCの最良の選択
EnvoyはLyftによってマイクロサービスアーキテクチャのために設計されており、gRPCのサポートは非常に深いレベルで実装されています。Protocol Buffersを理解し、gRPCをRESTにトランスコーディングし、各RPCメソッドに対して詳細なメトリクスを収集し、サービスメッシュソリューションの基盤となっています — Istio、AWS App Meshなど。真剣なマイクロサービスアーキテクチャを構築している場合、Envoyは業界標準です。
gRPCのためのEnvoyの基本設定
static_resources:
listeners:
- name: grpc_listener
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: grpc_proxy
codec_type: HTTP2
route_config:
name: grpc_routes
virtual_hosts:
- name: grpc_services
domains: ["*"]
routes:
- match:
prefix: "/com.example.UserService"
route:
cluster: user_service
timeout: 30s
retry_policy:
retry_on: "reset,connect-failure,retriable-status-codes"
num_retries: 3
retriable_status_codes: [14]
- match:
prefix: "/com.example.OrderService"
route:
cluster: order_service
timeout: 60s
http_filters:
- name: envoy.filters.http.grpc_stats
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_stats.v3.FilterConfig
emit_filter_state: true
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: user_service
connect_timeout: 5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
http2_protocol_options: {}
load_assignment:
cluster_name: user_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: user-service
port_value: 50051
この設定の主な利点は、gRPCサービスに基づくルーティング(パスのプレフィックスがpackage.ServiceName形式のサービスの完全な名前に対応)、UNAVAILABLEステータス時の自動リトライ、フィルターgrpc_statsを介したgRPCメトリクスの収集です。
EnvoyにおけるgRPC-Webトランスコーディング
Envoyのキラー機能の1つは、組み込みのgRPC-Webトランスコーダです。ブラウザはgRPCを直接サポートしていません(HTTP/2トレーラーに対するFetch APIの制限のため)、そのためgRPC-Webプロトコルが使用されます。EnvoyはブラウザからのgRPC-Webリクエストを通常のgRPCに自動的に変換できます:
http_filters:
- name: envoy.filters.http.grpc_web
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
- name: envoy.filters.http.cors
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.CorsPolicy
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
HAProxyとgRPC: 負荷分散のための設定
HAProxyはバージョン1.9.2以降、gRPCをサポートしています。TCPまたはHTTP/2レベルで動作し、gRPCトラフィックを負荷分散し、ヘルスチェックを実行できます。HAProxyは、すでにインフラストラクチャで使用している場合、gRPCのサポートを追加するのに良い選択肢です。
global
maxconn 50000
log stdout format raw local0
defaults
log global
timeout connect 5s
timeout client 3600s
timeout server 3600s
frontend grpc_frontend
bind *:443 ssl crt /etc/haproxy/certs/server.pem alpn h2,http/1.1
mode http
option http-use-htx
default_backend grpc_servers
backend grpc_servers
mode http
balance leastconn
option http-use-htx
# gRPCヘルスチェックプロトコルを介したヘルスチェック
option httpchk GET /grpc.health.v1.Health/Check
http-check expect status 200
server grpc1 10.0.0.1:50051 check ssl verify none
server grpc2 10.0.0.2:50051 check ssl verify none
server grpc3 10.0.0.3:50051 check ssl verify none
いくつかの重要な設定に注意してください。alpn h2,http/1.1は、HAProxyがTLS ALPNネゴシエーションを介してHTTP/2およびHTTP/1.1接続を受け入れることを示しています。timeout client 3600sおよびtimeout server 3600sは、長寿命のgRPCストリームにとって重要なパラメータです。leastconnアルゴリズムは、ストリーミング接続が長寿命であり、バックエンドに不均等に負荷をかける可能性があるため、roundrobinよりも好ましいです。
gRPCのためのTLSターミネーションとエンドツーエンドの暗号化
gRPCはデフォルトでTLSの使用を前提としています — これは仕様の一部です。実際のマイクロサービスアーキテクチャでは、TLSターミネーションをどこで行うか、コンポーネント間の暗号化をどのように構成するかという問題が発生します。主に3つのパターンがあります。
パターン1: プロキシでのTLSターミネーション(エッジTLS)
プロキシはクライアントからの暗号化されたトラフィックを受け取り、それを復号化してバックエンドに暗号化されていないチャネル(または別のTLS)で送信します。これは企業ネットワークで最も一般的なアプローチです。バックエンドは設定を簡素化するためにgrpc.Insecure()を使用できます。
パターン2: エンドツーエンドの暗号化(mTLS)
Mutual TLS (mTLS)は、サービスメッシュの標準です。各サービスは独自の証明書を持ち、接続のたびに両方の側が互いの証明書を検証します。これにより、サービスの認証とクラスター内のトラフィックの暗号化が確保されます。これは、IstioがEnvoyサイドカーのプロキシで機能する方法です。
# Go: mTLSを使用したgRPCサーバー
import (
"crypto/tls"
"crypto/x509"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func createMTLSCredentials() credentials.TransportCredentials {
cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
caCert, _ := os.ReadFile("ca.crt")
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
}
return credentials.NewTLS(tlsConfig)
}
// mTLSを使用したサーバーの作成
creds := createMTLSCredentials()
server := grpc.NewServer(grpc.Creds(creds))
パターン3: TLSパススルー
プロキシはTCPレベルで動作し、トラフィックを復号化せず、暗号化されたバイトをバックエンドに単に転送します。これはプロキシの観点から最も簡単なオプションですが、gRPCトラフィックを分析したり、ヘッダーを追加したり、リクエストレベルでの負荷分散を実行する機会を失います。
gRPCストリームのための負荷分散: 特徴と解決策
gRPCの負荷分散は簡単なタスクではなく、その理由は以下の通りです。HTTP/1.1では、各リクエストは個別のTCP接続(またはプールからの接続)であり、負荷分散装置はリクエストをバックエンドに簡単に分散できます。HTTP/2では、1つのTCP接続が多数のストリームをマルチプレクスします — もし負荷分散装置がTCPレベルで動作している場合、同じ接続のすべてのストリームは1つのバックエンドに送られます。
gRPCにとってこれは意味します: クライアントが1つのHTTP/2接続を確立し、その接続を介して100のRPCリクエストを送信した場合、TCP負荷分散ではすべての100リクエストが1つのバックエンドインスタンスによって処理されます。他のバックエンドはアイドル状態になります。解決策は、HTTP/2ストリームレベルでの負荷分散(L7負荷分散)です。
gRPCのための負荷分散アルゴリズム
| アルゴリズム | gRPCに適している | コメント |
|---|---|---|
| ラウンドロビン | ✅ はい | ほぼ同じ処理時間の単一RPCに適しています |
| 最小接続数 | ✅ 最良の選択 | アクティブなストリームを考慮し、負荷を均等に分散します |
| ランダム | ⚠️ 条件付き | リクエストが多い場合に機能し、少ない場合には不均等です |
| IPハッシュ | ❌ 悪い | クライアントを1つのバックエンドに固定し、L7では意味がありません |
| 最初の選択(gRPC組み込み) | ❌ 本番環境には不適切 | すべてのリクエストが最初の利用可能なサーバーに送信されます |
gRPCにおけるクライアント負荷分散
gRPCはクライアント負荷分散をサポートしており、クライアント自身がリクエストを送信するサーバーを決定します。これにより、TCPマルチプレクシングの問題を回避できます。サービスディスカバリーには、複数のAレコードを持つDNSや特別なリゾルバー(Consul、etcd)が使用されます。Goでのクライアント負荷分散の設定例:
import (
"google.golang.org/grpc"
"google.golang.org/grpc/balancer/roundrobin"
)
// DNS経由でラウンドロビン負荷分散を使用するクライアント
conn, err := grpc.Dial(
"dns:///grpc-service.internal:50051",
grpc.WithDefaultServiceConfig(
`{"loadBalancingConfig": [{"round_robin":{}}]}`
),
grpc.WithTransportCredentials(creds),
)
// 最小接続数でのクライアント(gRPC >= 1.58で利用可能)
conn, err := grpc.Dial(
"dns:///grpc-service.internal:50051",
grpc.WithDefaultServiceConfig(
`{"loadBalancingConfig": [{"least_request":{}}]}`
),
grpc.WithTransportCredentials(creds),
)
gRPCのための外部プロキシ: いつ、なぜ必要なのか
内部プロキシコンポーネント(Nginx、Envoy、HAProxy)に加えて、マイクロサービスアーキテクチャでは、特定の地域を通じてトラフィックをルーティングしたり、ネットワークの制約を回避したり、発信接続を隔離したりするために外部プロキシサーバーを使用する必要が生じることがあります。主要なシナリオを考えてみましょう。
シナリオ1: 地理的に分散したマイクロサービス
マイクロサービスが異なる地域に配置されている場合(例えば、一部がヨーロッパ、一部がアメリカにある場合)、gRPC接続がどのIPアドレスを介して確立されるかを制御する必要がある場合、外部プロキシは予測可能なルーティングを整理するのに役立ちます。このようなタスクには、データセンタープロキシが適しています — これにより、安定したIPアドレスと高い接続速度が提供され、gRPCのレイテンシに対する感受性が重要です。
シナリオ2: 発信トラフィックの隔離
一部の企業環境では、すべての発信トラフィックが企業プロキシを通過する必要があります。外部gRPCサービス(例えば、gRPCを使用するGoogle Cloud APIs)にアクセスする必要があるgRPCクライアントにとって、これは困難を引き起こします。解決策は、企業プロキシを介してCONNECTトンネルを設定することです。
GoでのHTTP CONNECTプロキシを介して動作するgRPCクライアントの設定例:
import (
"net"
"net/http"
"golang.org/x/net/proxy"
"google.golang.org/grpc"
)
// gRPCのためのSOCKS5プロキシの使用
proxyDialer, _ := proxy.SOCKS5(
"tcp",
"proxy.example.com:1080",
&proxy.Auth{User: "user", Password: "pass"},
proxy.Direct,
)
conn, err := grpc.Dial(
"grpc-service.example.com:443",
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return proxyDialer.Dial("tcp", addr)
}),
grpc.WithTransportCredentials(creds),
)
シナリオ3: テストと開発
マイクロサービスを開発する際には、異なるネットワーク環境からサービスの動作をテストする必要があることがよくあります — 高いレイテンシでのサービスの応答を確認したり、地理的に依存するロジックをテストしたりします。このようなタスクには、レジデンシャルプロキシが便利で、特定の地域からのリクエストを実際の家庭ユーザーのIPで模倣することができます。
gRPCをプロキシ経由で設定する際の一般的なエラーとその解決方法
gRPCをプロキシ経由で設定する際に開発者が直面する最も一般的な問題とその具体的な解決方法を見ていきましょう。
エラー1: "transport: received the unexpected content-type"
症状:
クライアントはtransport: received the unexpected content-type "text/html; charset=utf-8"エラーを受け取ります。
原因: プロキシがgRPCレスポンスの代わりにエラーページ(例えば、502 Bad Gateway)を返しました。gRPCクライアントはHTMLを処理できず、このエラーを返します。
解決策: バックエンドが利用可能であることを確認してください。プロキシレベルでgRPCエラーの処理を追加します(上記のNginxの例で示したerror502grpcブロックのように)。
エラー2: 接続が60〜120秒で切断される
症状:
ストリーミングgRPC接続が予期せず切断され、UNAVAILABLE: transport is closingエラーが発生します。
原因: プロキシがアイドル接続をタイムアウトで切断します。従来の値: AWS ELBは60秒、Nginxはデフォルトで60秒です。
解決策1: プロキシのタイムアウトを増やします(上記の例で示したように)。
解決策2: gRPCクライアント側でkeepaliveを設定します:
import "google.golang.org/grpc/keepalive"
kaParams := keepalive.ClientParameters{
Time: 30 * time.Second, // 30秒ごとにping
Timeout: 10 * time.Second, // 10秒間応答を待つ
PermitWithoutStream: true, // アクティブなRPCがなくてもpingを送信
}
conn, err := grpc.Dial(
"grpc-service:50051",
grpc.WithKeepaliveParams(kaParams),
grpc.WithTransportCredentials(creds),
)
エラー3: HTTP/2が合意しない(ALPN失敗)
症状:
エラーtransport: failed to dial: context deadline exceededまたはno application protocol
原因: プロキシまたは中間装置がALPN(Application-Layer Protocol Negotiation)をサポートしていないか、h2がサポートされているプロトコルのリストに含まれていません。
解決策: プロキシのTLS設定に明示的にh2プロトコルが指定されていることを確認します: alpn h2,http/1.1(HAProxy)またはlisten 443 ssl http2(Nginx)。
エラー4: ストリーミングがハングする — データが届かない
症状:
サーバーストリームがバックエンドで動作している(ログに表示される)が、クライアントはストリームが完了するまでメッセージを受け取らない。
原因: プロキシがレスポンスをバッファリングし、完了した後にのみクライアントに送信します。これは、proxy_buffering onで設定されたプロキシに典型的な問題です。
Nginxの解決策:
location / {
grpc_pass grpc://backend;
# gRPCストリームのためのバッファリングを無効にする
grpc_buffer_size 0;
# または通常のproxy_passの場合:
proxy_buffering off;
proxy_cache off;
}
gRPCプロキシの診断チェックリスト
✅ チェックリスト: gRPCプロキシの診断
- プロキシはHTTP/2をサポートしている(
curl --http2 -vで確認) - プロキシのTLS設定にALPN h2が含まれている
- クライアントとサーバーのタイムアウトが300秒以上に設定されている
- gRPCエンドポイントのレスポンスバッファリングが無効になっている
- クライアントでgRPC keepaliveが設定されている(Time: 30s, Timeout: 10s)
- バックエンドのエラーがgRPCステータスとして返され、HTTPコードではない
- ヘルスチェックがgRPCヘルスチェックプロトコルを使用している
- 負荷分散がL7(HTTP/2ストリーム)で機能し、L4(TCP)ではない
結論
gRPCをプロキシ経由で設定するには、HTTP/2とHTTP/1.1の重要な違いを理解する必要があります: ストリームのマルチプレクシング、長寿命の接続、HTTP/2トレーラー、ALPN合意。従来のHTTPプロキシは、gRPCに特別な設定なしでは機能しません — HTTP/2をサポートするL7プロキシ(Nginx 1.13.10以上、Envoy、HAProxy 1.9.2以上)またはTCP CONNECTトンネリングが必要です。
本番環境にはEnvoyを推奨します — これはマイクロサービスアーキテクチャのために設計されており、gRPCをネイティブにサポートし、各RPCメソッドに対して詳細なメトリクスを収集し、ほとんどのサービスメッシュソリューションの基盤となっています。Nginxは、すでにAPIゲートウェイとして使用している場合、gRPCサポートを追加するのに良い選択肢です。HAProxyは、パフォーマンスが重要で、すでにその設定に慣れている場合に適しています。
選択したプロキシに関係なく、3つのルールは変わりません: 長寿命のストリームのためにタイムアウトを増やし、レスポンスのバッファリングを無効にし、クライアントでkeepaliveを設定してください。これらの3つの設定は、gRPCをプロキシ経由で使用する際の80%の問題を解決します。
アーキテクチャ内でgRPCサービスが外部ネットワークを介して相互作用する必要がある場合や、特定の地域を通じてトラフィックをルーティングする必要がある場合は、データセンタープロキシに注目してください — これにより、安定したIPアドレス、低レイテンシ、高スループットが提供され、gRPCのバイナリプロトコルとレイテンシに対する感受性が特に重要です。
```