gRPCのKeepalive設定について調べてみた
gRPCを使い始めると、クライアントとサーバー間の接続を安定して維持する Keepalive 機能を目にすることがあります。「そもそもHTTP/1.1ではKeepaliveなんて意識しなかったのに、なぜgRPCではわざわざ設定する必要があるの?」と思ったことはありませんか?
本記事では、Keepalive設定がなぜ必要なのか、その役割、そして設定方法や検証手順について、できるだけ噛み砕いて解説します。
1. なぜgRPCにはKeepalive設定が必要なのか?
gRPCとHTTP/1.1の違い
gRPCは通信プロトコルとしてHTTP/2を採用していますが、HTTP/2ではクライアントとサーバーが長時間の接続を維持しながら多重化通信を行うのが特徴です。一方、HTTP/1.1は基本的にリクエストごとに接続を作成し、リクエストが完了すると接続を切断する動作(あるいは短時間のみ接続を保持するKeep-Alive)が主流でした。
このように、gRPCでは長期間の接続が前提であるため、以下のような新しい課題が生じます。
-
接続の健全性確認が必要: 長時間アイドル状態になると、ネットワーク障害や中間機器(ファイアウォールやロードバランサー)による切断が起きやすくなります。
-
リアルタイム性の確保: クライアントが接続の不具合を即座に検知し、再接続やエラー処理を行う必要があります。
-
ストリームベース通信の特性: gRPCでは単一の接続で複数のRPCを処理するため、接続の維持そのものがサービス全体の信頼性に直結します。
これらの理由から、gRPCではKeepalive設定が導入されています。この設定により、定期的な「PINGフレーム」の送受信を通じて接続を維持し、問題が発生した場合には速やかに対処できるようになります。
2. Keepalive設定の主要項目
gRPCでは、クライアントとサーバーの両方でKeepalive設定を行うことができます。それぞれの役割と代表的な設定項目を見ていきましょう。
keepalive.ClientParameters
)
クライアント側 (-
Time:
クライアントがサーバーに対してPINGフレームを送信する間隔を指定します。これを設定することで、定期的に接続が健全か確認できます(デフォルトでは無効)。 -
Timeout:
クライアントがPINGフレームを送信後、サーバーからの応答を待つ時間を指定します。この時間内に応答がない場合、接続は切断されます。 -
PermitWithoutStream:
アクティブなRPCストリームがない場合でもPINGを送信するかどうかを指定します。ストリームがなくても接続を維持したい場合に使用します。
keepalive.ServerParameters
)
サーバー側 (-
Time:
サーバーがクライアントに対してPINGフレームを送信する間隔を指定します。定期的に接続を確認する際に使用します。 -
Timeout:
サーバーがPINGフレームを送信後、クライアントからの応答を待つ時間を指定します。この時間内に応答がない場合、接続は切断されます。
keepalive.EnforcementPolicy
)
サーバー側 (-
MinTime:
クライアントから受け入れるPINGフレームの最小間隔を指定します。この値より短い間隔でPINGを送信するクライアントは切断される可能性があります(負荷防止のため)。 -
PermitWithoutStream:
アクティブなRPCストリームがない場合でもPINGを受け入れるかどうかを指定します。
3. gRPC の Keepalive の挙動を確かめる
不安定なネットワーク環境を再現するために
gRPCのKeepalive設定を検証する際、ネットワーク障害や接続遅延などの「不安定な環境」をシミュレーションすることが重要です。しかし、通常のネットワーク環境では、こうした状況を簡単に再現することが難しい場合があります。
そこで役立つのが toxiproxy
です。toxiproxy
は、ネットワークの障害や遅延をシミュレーションできる軽量プロキシツールで、接続断やパケットロス、遅延など、さまざまな障害状況を簡単に再現できます。
なぜ toxiproxy が有効なのか?
-
柔軟なシミュレーション
- 一時的な接続断、特定の遅延、パケットロスなど、ネットワークの異常を細かくコントロールできます。
- これにより、Keepaliveの再接続やエラーハンドリングが正しく機能するか確認できます。
-
簡単な導入
- インストールと設定が簡単で、REST API や CLI を使って障害をリアルタイムで操作可能です。
-
gRPCとの相性が良い
- gRPCは通常、長期間接続を維持するため、toxiproxyのようなプロキシを挟むことで、障害発生時の動作を詳細にテストできます。
toxiproxy の公式情報
toxiproxy
の公式リポジトリは以下のURLから確認できます。
Shopify/toxiproxy - GitHub
ここからダウンロードや使い方の詳細なドキュメントを確認できます。
toxiproxy を用いた検証環境を作成
1. toxiproxy のインストール
macOSの場合
以下のコマンドで toxiproxy
をインストールします。
$ brew install toxiproxy
2. toxiproxy サーバーの起動
以下のコマンドで toxiproxy
のサーバーを起動します。
$ toxiproxy-server
デフォルトでは、localhost:8474
で REST API サーバーが起動します。
3. toxiproxy CLI を使ってプロキシを作成
toxiproxy を使って、gRPC サーバー(例: localhost:50051
)へのプロキシを作成します。
$ toxiproxy-cli create --listen localhost:50052 --upstream localhost:50051 grpc_proxy
Created new proxy grpc_proxy
これにより、gRPC クライアントは localhost:50052
を経由してサーバーに接続するようになります。
4. gRPC サーバーコード
以下は、Keepaliveの設定を含んだサーバーコードの例です。
package main
import (
"context"
"log"
"net"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
pb "example"
)
type server struct {
pb.UnimplementedYourServiceServer
}
func (s *server) YourRPCMethod(ctx context.Context, in *pb.YourRequest) (*pb.YourResponse, error) {
log.Printf("Received: %v", in.Name)
return &pb.YourResponse{Message: "Hello " + in.Name}, nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
// Keepalive設定
grpcServer := grpc.NewServer(
grpc.KeepaliveParams(keepalive.ServerParameters{
// 検証のため短めに
Time: 1 * time.Second, // サーバーからPINGを送信する間隔
Timeout: 1 * time.Second, // PING応答の待機時間
}),
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 5 * time.Second, // クライアントPINGの最小間隔
PermitWithoutStream: true, // ストリームがなくてもPINGを許可
}),
)
pb.RegisterYourServiceServer(grpcServer, &server{})
log.Println("Server is running on port 50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve: %v", err)
}
}
5. gRPC クライアントコード
以下は、Keepaliveの設定を含んだクライアントコードの例です。
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/keepalive"
pb "example"
)
func main() {
conn, err := grpc.Dial(
"localhost:50052", // toxiproxy 経由で接続
grpc.WithInsecure(),
grpc.WithKeepaliveParams(keepalive.ClientParameters{
Time: 1 * time.Second, // 1秒ごとにPINGフレームを送信
Timeout: 1 * time.Second, // 1秒間応答がない場合に接続を切断
PermitWithoutStream: true, // ストリームがなくてもPINGを送信
}),
)
if err != nil {
log.Fatalf("Failed to connect: %v", err)
}
defer conn.Close()
client := pb.NewYourServiceClient(conn)
for {
// アプリケーションのタイムアウトは十分に長く 30秒 に設定
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
response, err := client.YourRPCMethod(ctx, &pb.YourRequest{Name: "World"})
if err != nil {
log.Printf("RPC failed: %v", err)
} else {
log.Printf("Response from server: %s", response.Message)
}
time.Sleep(1 * time.Second)
}
}
6. toxiproxy の 基本的な機能
toxiproxy を使うと以下のようなことができます。
接続を一時的に遮断する
以下のコマンドで、gRPC サーバーとの通信を一時的に遮断します。
$ toxiproxy-cli toggle grpc_proxy
-
toggle
コマンドを実行するたびに、接続の有効/無効が切り替わります。 - 接続が無効化されている間、クライアントは Keepalive の再試行を行います。
今の設定を確認する
以下のコマンドで、toxiproxy の設定を確認できます。
$ toxiproxy-cli inspect grpc_proxy
Name: grpc_proxy Listen: 127.0.0.1:50052 Upstream: localhost:50051
======================================================================
Proxy has no toxics enabled.
Hint: add a toxic with `toxiproxy-cli toxic add`
遅延を追加する
以下のコマンドで、通信に遅延を追加します(例: 1000ms)。
$ toxiproxy-cli toxic add -t latency -a latency=1000 grpc_proxy
これにより、クライアントとサーバー間の通信に 1 秒の遅延が追加されます。
$ toxiproxy-cli inspect grpc_proxy
Name: grpc_proxy Listen: 127.0.0.1:50052 Upstream: localhost:50051
======================================================================
Upstream toxics:
Proxy has no Upstream toxics enabled.
Downstream toxics:
latency_downstream: type=latency stream=downstream toxicity=1.00 attributes=[ jitter=0 latency=1000 ]
Hint: add a toxic with `toxiproxy-cli toxic add`
削除したい場合は 以下を実行すると削除できます
$ toxiproxy-cli toxic remove -n latency_downstream grpc_proxy
7. toxic を使った検証
7-1. 接続断のシミュレーション(toxiproxy-cli toggle grpc_proxy
)
Keepalive設定値(クライアント側):
keepalive.ClientParameters{
Time: 5 * time.Second, // 10秒ごとにPINGフレームを送信
Timeout: 2 * time.Second, // 5秒間応答がない場合に接続を切断
PermitWithoutStream: true, // ストリームがなくてもPINGを送信
}
手順:
-
toxiproxy-cli toggle grpc_proxy
を実行して接続を無効化。 - クライアントがKeepaliveのPINGを送信し、応答がないことを検知。
クライアントの標準出力:
Response from server: Hello World
RPC failed: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing: dial tcp [::1]:50052: connect: connection refused"
-
解説:
- クライアントは10秒ごとにPINGを送信しますが、
toxiproxy
によって接続が遮断されるため応答が得られません。 - 5秒以内に応答がないため、gRPCのトランスポート層で接続が閉じられ、エラー
rpc error: code = Unavailable
が発生します。
- クライアントは10秒ごとにPINGを送信しますが、
7-2. 遅延のシミュレーション(toxiproxy-cli toxic add grpc_proxy -t latency -a latency=3000
)
手順:
-
toxiproxy-cli toxic add -t latency -a latency=3000 grpc_proxy
を実行して3秒の遅延を追加。 - クライアントがPINGを送信するも、遅延によりタイムアウトが発生。
クライアントの標準出力:
Response from server: Hello World
RPC failed: rpc error: code = Unavailable desc = error reading from server: EOF
- 解説:
-
最初のレスポンス成功 (
Response from server: Hello World
)
クライアントは最初のリクエストを正常にサーバーに送り、サーバーからのレスポンスを受け取っています。この段階では、toxiproxy
による遅延の影響を受けていないため、通信が成功しています。 -
その後の切断 (
RPC failed: rpc error: code = Unavailable desc = error reading from server: EOF
)
クライアントはKeepalive.ClientParameters
の設定に基づき、1秒ごとにPINGフレームを送信します。しかし、toxiproxy
による3秒の遅延が発生しているため、サーバーからのPING応答がタイムアウト (Timeout: 1 * time.Second
) に間に合いません。加えて、サーバー側の
MinTime: 5 * time.Second
設定により、クライアントが1秒ごとにPINGを送信することが「違反」と見なされ、サーバーがクライアントを切断します。その結果、クライアントがサーバーから接続終了の通知 (EOF
) を受け取り、エラーとしてログに出力されます。
これらの要因が組み合わさり、最初は正常なレスポンスが出力され、その後にエラーが発生するログが記録されています。
7-3. Keepaliveを緩和した場合の結果
Keepalive設定値(サーバ側):
keepalive.ServerParameters{
Time: 10 * time.Second, // サーバーからPINGを送信する間隔
Timeout: 10 * time.Second, // PING応答の待機時間
}
Keepalive設定値(クライアント側):
keepalive.ClientParameters{
Time: 10 * time.Second, // 20秒ごとにPINGフレームを送信
Timeout: 10 * time.Second, // 15秒間応答がない場合に接続を切断
PermitWithoutStream: true, // ストリームがなくてもPINGを送信
}
手順:
- サーバのKeepalive の設定を上記の通りに伸ばして再起動します。
- 緩やかなKeepalive設定のため、切断が発生するまでの時間が長くなります。
クライアントの標準出力:
RPC succeeded: Response from server: Hello World
-
解説:
- Timeoutが15秒に設定されているため、遅延やパケットロスが一時的であれば、PING応答が間に合う場合があります。
- 接続が維持され、エラーが発生しないケースもあります。
7-検証から得られる知見
- Keepaliveの設定値(
Time
やTimeout
)は、ネットワークの特性に合わせて調整する必要があります。 - 遅延やパケットロスが頻発する環境では、Timeoutを長めに設定することで接続の安定性を確保できます。
- 一方で、迅速な障害検知が求められる場合には、Timeoutを短めに設定し、早期に再接続を試みる動作が有効です。
以上のように、toxiproxy
を活用することで、gRPCのKeepalive設定値が接続の安定性やエラーハンドリングにどのような影響を与えるかを詳細に検証できます。適切な設定値を選ぶことで、システムの信頼性を高めることができます。
5. まとめ
gRPCのKeepalive設定は、HTTP/2ベースの長期間接続の特性を最大限活かしつつ、信頼性を高めるために欠かせない機能です。適切に設定することで、ネットワーク障害や中間機器の影響を最小限に抑え、サービス全体の安定性を向上させることができます。
本記事のコード例や検証手順を参考に、あなたのプロジェクトに最適な設定を探してみてください。
参考文献
以下の資料を参考になります。
-
gRPC Keepalive に関するオプション - Qiita
gRPCのKeepalive機能に関する詳細なオプション設定や、その役割について解説されています。 -
gRPCのkeepaliveで気をつけること - Carpe Diem
gRPCにおけるKeepaliveの役割や設定時の注意点について、具体的な例を交えて説明されています。 -
gRPCのkeep alive動作検証 - tsuchinaga - Scrapbox
gRPCのKeepalive機能の動作検証を行い、その結果や考察がまとめられています。 -
gRPCのkeepaliveで気をつけること その2 - Carpe Diem
gRPCのKeepalive設定に関する追加の注意点やベストプラクティスが紹介されています。
Discussion