ねらい
Djangoで認証を行い、gRPC‑WebでServer‑Streaming、UIは届いた瞬間に描画することで「待ち時間ゼロ」を体感させる。HTTPポーリングより圧倒的に速く、実装もシンプル。
実務で効くコツ
- サーバーはServer‑Streamingで1レコードずつ即送信。
- chunkは小さめ(数KB〜十数KB)。
- **Djangoは短命JWT(5分)**を発行し、
Authorization: Bearer
ヘッダでgRPCに中継。 - EnvoyはHTTP/2 upstream必須。
grpc_web
フィルタ+CORS許可(content-type, x-grpc-web, x-user-agent, authorization, grpc-timeout
)。
- タイムアウトに注意:
stream_idle_timeout
を十分長く / 必要なら無効化。- LBのアイドルタイムアウトも合わせる。
- 再送より鮮度重視:
- クライアント側は描画を優先し、欠落は次chunkで補正。
- UIは追記型に設計。
つまずきポイント
- CORSで
grpc-timeout
やx-grpc-web
を許可せず、ブラウザで失敗。 - EnvoyのupstreamがHTTP/1.1のまま →
RST_STREAM
多発。 - 巨大メッセージを一括送信 → 詰まる。小さく刻むべし。
監視・運用の着眼点
- p95首バイト時間:サーバーが最初のchunkを出すまで。
- gRPCステータス分布:
UNAVAILABLE
/DEADLINE_EXCEEDED
増加はNW/タイムアウト疑い。 - Envoy指標:
upstream_rq_pending_overflow
、cluster.<name>.*
、http2.rx_messaging_error
。 - ブラウザ側:再接続回数・メモリ使用量(長時間ストリームでリーク検知)。
かんたんコード断片
Django(JWT発行)
from datetime import datetime, timedelta, timezone
import jwt
def issue_token(request):
now = datetime.now(timezone.utc)
payload = {"sub": str(request.user.id),
"exp": int((now + timedelta(minutes=5)).timestamp())}
return JsonResponse({"token": jwt.encode(payload, SECRET, "HS256")})
Python gRPC(Server‑Streaming)
class InferenceServicer(MyServiceServicer):
async def StreamInfer(self, request, context):
for chunk in run_model_iter(request.prompt):
yield InferChunk(delta=chunk) # 小さく刻んで即yield
Envoy(要点)
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
common_http_protocol_options:
idle_timeout: 0s # 長いストリーム向け
http2_protocol_options:
max_concurrent_streams: 1000
cors:
allow_headers: "content-type,x-grpc-web,x-user-agent,authorization,grpc-timeout"
フロント(例:Connect-Web)
for await (const m of client.streamInfer({prompt}, {headers:{Authorization:`Bearer ${jwt}`}})) {
render(m.delta) // 届いたそばから描画
}
まとめ
- Djangoで短命JWT
- EnvoyでgRPC‑Web
- Server‑Streamingで小さく速く
タイムアウトとCORS/HTTP2設定、この2つを外さなければ体感は一気に改善する。