FastAPI + gRPC 双协议服务:Intelligence Layer 的 Python 微服务架构
1. [双协议架构的设计动机](#1-双协议架构的设计动机)
Coomia发布于 2025年11月24日11 分钟阅读
分享本文Twitter / X
“系列:S8 技术组件深潜 · 第 15 篇 | 难度:高级 | 阅读时间:20 分钟
FastAPI + gRPC 双协议服务:Intelligence Layer 的 Python 微服务架构
#TL;DR
- coomia-dip 的 Intelligence Layer(Reasoning & Decision Layer + Agent Runtime Layer)使用 Python 3.x 构建,通过 FastAPI 暴露外部 REST API,通过 gRPC 与内部服务通信
- 本文深入分析双协议架构的设计动机、FastAPI 的 async/await 模型、gRPC Python Server 的线程模型、Protobuf 序列化优化、以及两种协议的统一错误处理
- 涵盖 Pydantic v2 模型与 Protobuf Message 的自动转换、健康检查、中间件链、以及生产环境的部署模式
#目录
- 双协议架构的设计动机
- FastAPI 异步模型
- gRPC Python Server
- Pydantic v2 与 Protobuf 转换
- 统一错误处理
- 中间件与拦截器
- 健康检查与服务发现
- 并发模型与性能
- 部署模式
- 测试策略
- Key Takeaways
#1. 双协议架构的设计动机
#1.1 为什么需要两种协议?
Code
外部客户端(浏览器/SDK) 内部服务(Control Layer + Data Layer)
│ │
│ REST/JSON(易于调试、通用) │ gRPC/Protobuf(高性能、类型安全)
│ │
▼ ▼
┌─────────────────────────────────────────────┐
│ Intelligence Layer (Python) │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ FastAPI │ │ gRPC Server │ │
│ │ Port 8000 │ │ Port 50051 │ │
│ │ REST/JSON │ │ Protobuf │ │
│ └──────┬───────┘ └──────┬───────────┘ │
│ │ │ │
│ └────────┬───────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ Service Layer │ │
│ │ (共享业务逻辑) │ │
│ └─────────────────┘ │
└──────────────────────────────────────────────┘
#1.2 协议选择矩阵
| 调用方 | 协议 | 理由 |
|---|---|---|
| 前端/浏览器 | REST | 浏览器原生支持 |
| Python SDK | REST | 简单易用 |
| Control Layer (Java) | gRPC | 类型安全、高性能 |
| Data Layer (Java) | gRPC | 流式传输、低延迟 |
| Agent Runtime (Python) | gRPC | 内部调用、Protobuf 共享 |
#2. FastAPI 异步模型
#2.1 ASGI 架构
Python
# coomia-dip Intelligence Layer 入口
from fastapi import FastAPI
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时初始化
await init_grpc_clients()
await init_reasoning_engine()
yield
# 关闭时清理
await shutdown_grpc_clients()
app = FastAPI(
title="coomia-dip Intelligence Layer",
version="1.0.0",
lifespan=lifespan,
)
#2.2 路由定义
Python
from fastapi import APIRouter, Depends, Query, Path
from pydantic import BaseModel, Field
router = APIRouter(prefix="/api/v1/reasoning", tags=["reasoning"])
class ReasoningRequest(BaseModel):
"""推理请求"""
world_id: str = Field(..., description="World ID")
query: str = Field(..., description="推理查询")
context: dict[str, Any] = Field(default_factory=dict)
max_depth: int = Field(default=5, ge=1, le=20)
class ReasoningResponse(BaseModel):
"""推理响应"""
result: dict[str, Any]
confidence: float = Field(ge=0.0, le=1.0)
reasoning_chain: list[str]
execution_time_ms: float
@router.post("/execute", response_model=ReasoningResponse)
async def execute_reasoning(
request: ReasoningRequest,
world_context: WorldContext = Depends(get_world_context),
) -> ReasoningResponse:
result = await reasoning_service.execute(
world_id=request.world_id,
query=request.query,
context=request.context,
max_depth=request.max_depth,
)
return ReasoningResponse(
result=result.data,
confidence=result.confidence,
reasoning_chain=result.chain,
execution_time_ms=result.duration_ms,
)
#2.3 依赖注入
Python
from fastapi import Depends, Header, HTTPException
async def get_world_context(
x_world_id: str = Header(..., alias="X-World-Id"),
x_user_id: str | None = Header(None, alias="X-User-Id"),
) -> WorldContext:
"""从请求头提取 World Context"""
if not x_world_id:
raise HTTPException(status_code=400, detail="X-World-Id header required")
return WorldContext(world_id=x_world_id, user_id=x_user_id)
async def get_reasoning_service(
world_ctx: WorldContext = Depends(get_world_context),
) -> ReasoningService:
"""获取绑定了 World Context 的推理服务"""
return ReasoningService(world_context=world_ctx)
#3. gRPC Python Server
#3.1 Protobuf 定义
PROTOBUF
// reasoning_service.proto
syntax = "proto3";
package onto.intelligence.v1;
service ReasoningService {
rpc ExecuteReasoning (ReasoningRequest) returns (ReasoningResponse);
rpc StreamReasoning (ReasoningRequest) returns (stream ReasoningStep);
rpc BatchReasoning (stream ReasoningRequest) returns (stream ReasoningResponse);
}
message ReasoningRequest {
string world_id = 1;
string query = 2;
map<string, string> context = 3;
int32 max_depth = 4;
}
message ReasoningResponse {
map<string, string> result = 1;
double confidence = 2;
repeated string reasoning_chain = 3;
double execution_time_ms = 4;
}
message ReasoningStep {
int32 step_number = 1;
string description = 2;
string status = 3;
map<string, string> intermediate_result = 4;
}
#3.2 gRPC Server 实现
Python
import grpc
from concurrent import futures
from onto.intelligence.v1 import reasoning_service_pb2_grpc as reasoning_grpc
from onto.intelligence.v1 import reasoning_service_pb2 as reasoning_pb
class ReasoningGrpcServicer(reasoning_grpc.ReasoningServiceServicer):
"""gRPC 推理服务实现"""
def __init__(self, reasoning_service: ReasoningService):
self.service = reasoning_service
async def ExecuteReasoning(
self,
request: reasoning_pb.ReasoningRequest,
context: grpc.aio.ServicerContext,
) -> reasoning_pb.ReasoningResponse:
# 从 gRPC metadata 获取 World Context
metadata = dict(context.invocation_metadata())
world_id = metadata.get("x-world-id", request.world_id)
try:
result = await self.service.execute(
world_id=world_id,
query=request.query,
context=dict(request.context),
max_depth=request.max_depth,
)
return reasoning_pb.ReasoningResponse(
result=result.data,
confidence=result.confidence,
reasoning_chain=result.chain,
execution_time_ms=result.duration_ms,
)
except ValidationError as e:
await context.abort(grpc.StatusCode.INVALID_ARGUMENT, str(e))
except PermissionError as e:
await context.abort(grpc.StatusCode.PERMISSION_DENIED, str(e))
async def StreamReasoning(
self,
request: reasoning_pb.ReasoningRequest,
context: grpc.aio.ServicerContext,
):
"""服务端流式推理 — 逐步返回推理过程"""
async for step in self.service.stream_execute(
world_id=request.world_id,
query=request.query,
):
yield reasoning_pb.ReasoningStep(
step_number=step.number,
description=step.description,
status=step.status,
intermediate_result=step.data,
)
#3.3 双服务器启动
Python
import asyncio
import uvicorn
async def start_grpc_server() -> grpc.aio.Server:
"""启动 gRPC Server"""
server = grpc.aio.server(
futures.ThreadPoolExecutor(max_workers=10),
options=[
("grpc.max_send_message_length", 50 * 1024 * 1024),
("grpc.max_receive_message_length", 50 * 1024 * 1024),
("grpc.keepalive_time_ms", 30000),
("grpc.keepalive_timeout_ms", 5000),
],
)
reasoning_grpc.add_ReasoningServiceServicer_to_server(
ReasoningGrpcServicer(reasoning_service), server
)
server.add_insecure_port("[::]:50051")
await server.start()
return server
async def main():
# 并行启动 FastAPI 和 gRPC
grpc_server = await start_grpc_server()
config = uvicorn.Config(
app="main:app",
host="0.0.0.0",
port=8000,
workers=1,
loop="asyncio",
)
uvicorn_server = uvicorn.Server(config)
await asyncio.gather(
uvicorn_server.serve(),
grpc_server.wait_for_termination(),
)
if __name__ == "__main__":
asyncio.run(main())
#4. Pydantic v2 与 Protobuf 转换
#4.1 自动转换层
Python
from pydantic import BaseModel
from google.protobuf.message import Message
from typing import TypeVar, Type
T = TypeVar("T", bound=BaseModel)
P = TypeVar("P", bound=Message)
class ProtoConverter:
"""Pydantic v2 与 Protobuf 之间的自动转换"""
@staticmethod
def to_pydantic(proto_msg: P, pydantic_class: Type[T]) -> T:
"""Protobuf Message → Pydantic Model"""
from google.protobuf.json_format import MessageToDict
data = MessageToDict(proto_msg, preserving_proto_field_name=True)
return pydantic_class.model_validate(data)
@staticmethod
def to_proto(pydantic_obj: T, proto_class: Type[P]) -> P:
"""Pydantic Model → Protobuf Message"""
from google.protobuf.json_format import ParseDict
data = pydantic_obj.model_dump(mode="json")
return ParseDict(data, proto_class())
# 使用示例
pydantic_req = ProtoConverter.to_pydantic(grpc_request, ReasoningRequest)
proto_resp = ProtoConverter.to_proto(pydantic_response, reasoning_pb.ReasoningResponse)
#4.2 共享模型定义
Python
from pydantic import BaseModel, Field, ConfigDict
class OntologyInstanceModel(BaseModel):
"""同时服务 REST 和 gRPC 的共享模型"""
model_config = ConfigDict(from_attributes=True)
id: str
object_type: str = Field(alias="objectType")
object_id: str = Field(alias="objectId")
world_id: str = Field(alias="worldId")
properties: dict[str, Any] = Field(default_factory=dict)
version: int = 0
created_at: datetime | None = None
updated_at: datetime | None = None
#5. 统一错误处理
#5.1 错误码映射
Python
from enum import IntEnum
import grpc
from fastapi import HTTPException
class ErrorCode(IntEnum):
VALIDATION_ERROR = 1001
NOT_FOUND = 1002
PERMISSION_DENIED = 1003
CONFLICT = 1004
INTERNAL_ERROR = 1005
TIMEOUT = 1006
# REST HTTP 状态码映射
ERROR_TO_HTTP = {
ErrorCode.VALIDATION_ERROR: 400,
ErrorCode.NOT_FOUND: 404,
ErrorCode.PERMISSION_DENIED: 403,
ErrorCode.CONFLICT: 409,
ErrorCode.INTERNAL_ERROR: 500,
ErrorCode.TIMEOUT: 504,
}
# gRPC 状态码映射
ERROR_TO_GRPC = {
ErrorCode.VALIDATION_ERROR: grpc.StatusCode.INVALID_ARGUMENT,
ErrorCode.NOT_FOUND: grpc.StatusCode.NOT_FOUND,
ErrorCode.PERMISSION_DENIED: grpc.StatusCode.PERMISSION_DENIED,
ErrorCode.CONFLICT: grpc.StatusCode.ALREADY_EXISTS,
ErrorCode.INTERNAL_ERROR: grpc.StatusCode.INTERNAL,
ErrorCode.TIMEOUT: grpc.StatusCode.DEADLINE_EXCEEDED,
}
#5.2 统一异常类
Python
class CoomiaDipError(Exception):
"""coomia-dip 统一异常基类"""
def __init__(self, code: ErrorCode, message: str, details: dict | None = None):
self.code = code
self.message = message
self.details = details or {}
super().__init__(message)
def to_http_exception(self) -> HTTPException:
return HTTPException(
status_code=ERROR_TO_HTTP[self.code],
detail={"code": self.code, "message": self.message, "details": self.details},
)
async def abort_grpc(self, context: grpc.aio.ServicerContext) -> None:
await context.abort(
ERROR_TO_GRPC[self.code],
f"[{self.code}] {self.message}",
)
#6. 中间件与拦截器
#6.1 FastAPI 中间件
Python
from starlette.middleware.base import BaseHTTPMiddleware
class RequestTracingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
request_id = request.headers.get("X-Request-Id", str(uuid4()))
start_time = time.monotonic()
# 注入 request context
token = request_context.set({"request_id": request_id})
try:
response = await call_next(request)
duration = time.monotonic() - start_time
response.headers["X-Request-Id"] = request_id
response.headers["X-Response-Time"] = f"{duration:.3f}s"
logger.info(
"request completed",
request_id=request_id,
method=request.method,
path=request.url.path,
status=response.status_code,
duration_ms=duration * 1000,
)
return response
finally:
request_context.reset(token)
app.add_middleware(RequestTracingMiddleware)
#6.2 gRPC 拦截器
Python
class TracingInterceptor(grpc.aio.ServerInterceptor):
async def intercept_service(self, continuation, handler_call_details):
request_id = None
for key, value in handler_call_details.invocation_metadata:
if key == "x-request-id":
request_id = value
break
if not request_id:
request_id = str(uuid4())
start_time = time.monotonic()
method = handler_call_details.method
try:
handler = await continuation(handler_call_details)
duration = time.monotonic() - start_time
logger.info(
"gRPC call completed",
request_id=request_id,
method=method,
duration_ms=duration * 1000,
)
return handler
except Exception as e:
duration = time.monotonic() - start_time
logger.error(
"gRPC call failed",
request_id=request_id,
method=method,
error=str(e),
duration_ms=duration * 1000,
)
raise
# 注册拦截器
server = grpc.aio.server(interceptors=[TracingInterceptor()])
#7. 健康检查与服务发现
#7.1 双协议健康检查
Python
# FastAPI 健康检查
@app.get("/health")
async def health_check():
checks = {
"grpc_server": await check_grpc_server(),
"database": await check_database(),
"redis": await check_redis(),
}
all_healthy = all(checks.values())
return JSONResponse(
status_code=200 if all_healthy else 503,
content={"status": "healthy" if all_healthy else "unhealthy", "checks": checks},
)
# gRPC 健康检查(标准协议)
from grpc_health.v1 import health_pb2_grpc, health_pb2
class HealthServicer(health_pb2_grpc.HealthServicer):
async def Check(self, request, context):
service = request.service
if service == "" or service == "onto.intelligence.v1.ReasoningService":
return health_pb2.HealthCheckResponse(
status=health_pb2.HealthCheckResponse.SERVING
)
return health_pb2.HealthCheckResponse(
status=health_pb2.HealthCheckResponse.NOT_SERVING
)
#8. 并发模型与性能
#8.1 asyncio + ThreadPoolExecutor
Python
import asyncio
from concurrent.futures import ThreadPoolExecutor
# CPU 密集型任务使用线程池
executor = ThreadPoolExecutor(max_workers=4)
async def cpu_intensive_reasoning(query: str) -> dict:
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
executor,
_sync_reasoning, # 同步的 CPU 密集型函数
query,
)
return result
def _sync_reasoning(query: str) -> dict:
# OR-Tools 求解等 CPU 密集型操作
return solver.solve(query)
#8.2 性能基准
Code
测试环境:4 核 8GB
工具:wrk2, ghz
REST (FastAPI):
简单查询: 12,000 RPS, P99 = 5ms
推理请求: 2,000 RPS, P99 = 45ms
gRPC:
简单查询: 18,000 RPS, P99 = 2ms
推理请求: 3,500 RPS, P99 = 30ms
流式传输: 50,000 msg/s
gRPC 比 REST 快 50-75%(序列化 + 传输)
#9. 部署模式
#9.1 Docker 部署
Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir .
COPY . .
EXPOSE 8000 50051
CMD ["python", "-m", "coomia-dip.intelligence.main"]
#9.2 Kubernetes 配置
YAML
apiVersion: apps/v1
kind: Deployment
metadata:
name: intelligence-Layer
spec:
replicas: 3
template:
spec:
containers:
- name: intelligence
image: coomia-dip/intelligence-Layer:latest
ports:
- containerPort: 8000
name: http
- containerPort: 50051
name: grpc
resources:
requests:
cpu: "2"
memory: "4Gi"
limits:
cpu: "4"
memory: "8Gi"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
readinessProbe:
grpc:
port: 50051
initialDelaySeconds: 5
#10. 测试策略
#10.1 REST API 测试
Python
from fastapi.testclient import TestClient
def test_execute_reasoning():
client = TestClient(app)
response = client.post(
"/api/v1/reasoning/execute",
json={"world_id": "test", "query": "optimize route", "max_depth": 3},
headers={"X-World-Id": "test"},
)
assert response.status_code == 200
data = response.json()
assert "result" in data
assert 0 <= data["confidence"] <= 1
#10.2 gRPC 测试
Python
import grpc
import pytest
@pytest.fixture
async def grpc_channel():
async with grpc.aio.insecure_channel("localhost:50051") as channel:
yield channel
@pytest.mark.asyncio
async def test_grpc_reasoning(grpc_channel):
stub = reasoning_grpc.ReasoningServiceStub(grpc_channel)
response = await stub.ExecuteReasoning(
reasoning_pb.ReasoningRequest(
world_id="test",
query="optimize route",
max_depth=3,
),
metadata=[("x-world-id", "test")],
)
assert response.confidence > 0
assert len(response.reasoning_chain) > 0
#11. Key Takeaways
| 主题 | 关键结论 |
|---|---|
| 双协议 | REST 对外(浏览器/SDK),gRPC 对内(微服务间) |
| FastAPI | async/await + Pydantic v2 = 高性能 REST |
| gRPC | grpc.aio 异步服务器,流式传输支持 |
| 模型转换 | Pydantic ↔ Protobuf 自动转换层 |
| 错误处理 | 统一错误码映射到 HTTP 和 gRPC 状态码 |
| 并发 | asyncio 处理 I/O,ThreadPoolExecutor 处理 CPU |
| 性能 | gRPC 比 REST 快 50-75% |
| 部署 | 单进程双端口,K8s 分别配置探针 |
“下一篇预告:S8-16 将深入 Apache Arrow Flight SQL,探讨 coomia-dip 如何实现高性能柱状数据传输。