API 设计哲学:63 个 proto + 59 个 REST 端点的统一治理
在任何分布式平台中,API 不仅仅是"接口"——它是系统的契约层、协作协议和演进边界。一个设计良好的 API 体系可以让团队独立开发、独立部署、独立演进;一个混乱的 API 体系则会让每次变更都变成一场噩梦。
API 设计哲学:63 个 proto + 59 个 REST 端点的统一治理
“系列:S2 架构全景 · 第 8 篇 | 难度:中级 | 阅读时间:18 分钟
#TL;DR
- 智策平台采用 Contract-First 开发模式,所有内部通信通过 63 个 Protobuf 文件定义契约,外部客户端通过 59 个 REST 端点接入,两层之间由 API Gateway 自动转换。
- 63 个 proto 文件按 8 个 Layer 分包组织,每个 Layer 独立维护自己的
.proto文件,通过统一的错误码体系和版本策略实现跨 Layer 一致性。 - Python SDK 从 proto 文件自动生成 38 个 gRPC 客户端,实现了 "改一次 proto,SDK/Server/文档三处同步" 的开发体验。
#引言:API 是分布式系统的神经系统
在任何分布式平台中,API 不仅仅是"接口"——它是系统的契约层、协作协议和演进边界。一个设计良好的 API 体系可以让团队独立开发、独立部署、独立演进;一个混乱的 API 体系则会让每次变更都变成一场噩梦。
智策平台面临的 API 治理挑战尤其严峻:
8 个 Layer × 多种技术栈 × 3 种语言(Java/Python/TypeScript)
= 大量跨边界调用需要精确定义
我们的选择是:Protobuf First, REST Second。所有核心逻辑通过 gRPC 通信,REST 只是外部客户端的"翻译层"。本文将详细讲解这个 API 体系是如何设计、组织和治理的。
#1. Contract-First 开发:proto 文件是唯一真相
#1.1 什么是 Contract-First
传统开发模式是 Code-First:先写代码,再从代码生成 API 文档(如 Swagger)。这种方式看似高效,但有一个致命问题——接口定义散落在代码中,没有单一真相源。
Contract-First 反过来:先写接口定义(proto 文件),再从定义生成代码。
Contract-First 流程:
┌─────────────┐
│ .proto 文件 │ ← 唯一真相源(Single Source of Truth)
└──────┬──────┘
│
┌────┴────┬──────────┬──────────┐
▼ ▼ ▼ ▼
Java Python TypeScript 文档
Server Client Client (自动)
Stub Stub Stub
#1.2 为什么选择 Protobuf 而不是 OpenAPI
| 维度 | Protobuf/gRPC | OpenAPI/REST |
|---|---|---|
| 序列化性能 | 二进制,比 JSON 快 5-10x | JSON 文本 |
| 类型安全 | 强类型,编译时检查 | 运行时验证 |
| 流式传输 | 原生支持双向流 | SSE/WebSocket 需额外处理 |
| 代码生成 | 一等公民,官方支持 | 工具链碎片化 |
| 向后兼容 | 字段编号机制天然支持 | 需手动管理 |
| 浏览器友好 | 不友好,需要 proxy | 原生支持 |
结论很清楚:内部用 gRPC(性能+类型安全),外部用 REST(兼容性+浏览器友好)。
#1.3 proto 文件的开发流程
1. 设计阶段:
├── 在设计文档中定义接口语义
├── 编写 .proto 文件
└── Team Review(检查向后兼容性)
2. 代码生成阶段:
├── protoc 生成 Java/Python stubs
├── grpc-gateway 生成 REST 反向代理
└── 自动更新 SDK 客户端
3. 实现阶段:
├── Server 端实现 Service 接口
├── 编写单元测试
└── 集成测试验证跨 Layer 调用
4. 发布阶段:
├── 版本号递增
├── 更新 CHANGELOG
└── SDK 发布新版本
#2. 63 个 proto 文件的组织结构
#2.1 按 Layer 分包
我们的 proto 文件按照 8 个 Layer 的边界进行组织,每个 Layer 维护自己的 proto 包:
protos/
├── plane_b/ # Control Layer (Control Layer)
│ ├── ontology/
│ │ ├── object_type.proto # ObjectType CRUD
│ │ ├── link_type.proto # LinkType CRUD
│ │ ├── property_type.proto # PropertyType 管理
│ │ ├── action_type.proto # ActionType 定义
│ │ └── ontology_version.proto # Ontology 版本管理
│ ├── world/
│ │ ├── world.proto # World 生命周期
│ │ ├── world_branch.proto # World 分支管理
│ │ └── world_merge.proto # World 合并操作
│ ├── auth/
│ │ ├── authentication.proto # 认证服务
│ │ ├── authorization.proto # 授权检查
│ │ └── rbac.proto # 角色权限管理
│ ├── governance/
│ │ ├── audit.proto # 审计日志
│ │ ├── data_lineage.proto # 数据血缘
│ │ └── compliance.proto # 合规检查
│ └── registry/
│ ├── service_registry.proto # 服务注册
│ └── config_registry.proto # 配置注册
│
├── plane_c/ # Data Layer (Data Layer)
│ ├── storage/
│ │ ├── object_storage.proto # 对象实例存储
│ │ ├── link_storage.proto # 关系存储
│ │ ├── property_storage.proto # 属性存储
│ │ └── time_series.proto # 时序数据
│ ├── query/
│ │ ├── object_query.proto # 对象查询
│ │ ├── aggregation.proto # 聚合计算
│ │ └── search.proto # 全文搜索
│ ├── pipeline/
│ │ ├── sync_task.proto # 数据同步任务
│ │ ├── transform.proto # 数据转换
│ │ └── schedule.proto # 调度管理
│ └── materialization/
│ ├── view.proto # 物化视图
│ └── subscription.proto # 变更订阅
│
├── plane_d/ # Reasoning & Decision (Reasoning & Decision Layer)
│ ├── reasoning/
│ │ ├── rule.proto # 规则定义
│ │ ├── rule_evaluation.proto # 规则评估
│ │ └── constraint.proto # 约束条件
│ ├── decision/
│ │ ├── decision.proto # 决策定义
│ │ ├── decision_tree.proto # 决策树
│ │ └── scoring.proto # 评分模型
│ ├── derived/
│ │ ├── derived_property.proto # 派生属性
│ │ └── dependency_dag.proto # 依赖 DAG
│ └── simulation/
│ ├── scenario.proto # 场景模拟
│ └── what_if.proto # What-If 分析
│
├── plane_e/ # Agent Runtime (Agent Runtime Layer)
│ ├── action/
│ │ ├── action.proto # Action 执行
│ │ ├── action_template.proto # Action 模板
│ │ └── approval.proto # 审批流程
│ ├── workflow/
│ │ ├── workflow.proto # 工作流定义
│ │ └── workflow_execution.proto # 工作流执行
│ └── agent/
│ ├── agent.proto # Agent 定义
│ └── agent_execution.proto # Agent 执行
│
├── plane_h/ # SDK & Developer Experience (SDK & Developer Experience Layer)
│ ├── sdk/
│ │ ├── client_config.proto # SDK 配置
│ │ └── batch_operation.proto # 批量操作
│ └── developer/
│ ├── function.proto # 自定义函数
│ └── webhook.proto # Webhook 管理
│
└── common/ # 跨 Layer 共享类型
├── types.proto # 基础类型
├── pagination.proto # 分页
├── error.proto # 错误码
├── metadata.proto # 元数据
└── health.proto # 健康检查
#2.2 proto 文件的命名与编号规范
每个 proto 文件遵循严格的规范:
// file: protos/plane_b/ontology/object_type.proto
syntax = "proto3";
package onto.plane_b.ontology;
option java_package = "com.onto.control.ontology.grpc";
option java_outer_classname = "ObjectTypeProto";
option java_multiple_files = true;
import "common/types.proto";
import "common/pagination.proto";
import "common/error.proto";
// ObjectType 定义了本体模型中的对象类型
service ObjectTypeService {
// 创建对象类型
rpc CreateObjectType(CreateObjectTypeRequest)
returns (CreateObjectTypeResponse);
// 获取对象类型
rpc GetObjectType(GetObjectTypeRequest)
returns (GetObjectTypeResponse);
// 列出对象类型
rpc ListObjectTypes(ListObjectTypesRequest)
returns (ListObjectTypesResponse);
// 更新对象类型
rpc UpdateObjectType(UpdateObjectTypeRequest)
returns (UpdateObjectTypeResponse);
// 删除对象类型
rpc DeleteObjectType(DeleteObjectTypeRequest)
returns (DeleteObjectTypeResponse);
}
message CreateObjectTypeRequest {
string world_id = 1; // 所属 World
string api_name = 2; // API 名称(驼峰)
string display_name = 3; // 显示名称
string description = 4; // 描述
string icon = 5; // 图标标识
repeated PropertyDefinition properties = 6; // 属性列表
string primary_key_property = 7; // 主键属性名
}
message CreateObjectTypeResponse {
onto.common.ResponseMetadata metadata = 1;
ObjectType object_type = 2;
}
message ObjectType {
string id = 1;
string api_name = 2;
string display_name = 3;
string description = 4;
string icon = 5;
repeated PropertyDefinition properties = 6;
string primary_key_property = 7;
int64 created_at = 8;
int64 updated_at = 9;
string created_by = 10;
ObjectTypeStatus status = 11;
}
enum ObjectTypeStatus {
OBJECT_TYPE_STATUS_UNSPECIFIED = 0;
OBJECT_TYPE_STATUS_ACTIVE = 1;
OBJECT_TYPE_STATUS_DEPRECATED = 2;
OBJECT_TYPE_STATUS_ARCHIVED = 3;
}
#2.3 字段编号策略
我们采用分段编号策略来预留扩展空间:
字段编号分配策略:
1-15 : 高频字段(使用 1 字节 varint,最优性能)
16-99 : 常规字段
100-199 : 扩展字段(预留给未来版本)
200-299 : 内部字段(不对外暴露)
900-999 : 调试/诊断字段
#3. 59 个 REST 端点与 Gateway 模式
#3.1 REST 端点分布
虽然内部通信全部使用 gRPC,但外部客户端(浏览器、第三方系统、移动端)需要 REST API。我们通过 API Gateway 实现 REST 到 gRPC 的自动转换:
59 个 REST 端点按 Layer 分布:
Control Layer (Control):
/api/v1/ontology/object-types (CRUD = 5)
/api/v1/ontology/link-types (CRUD = 5)
/api/v1/ontology/action-types (CRUD = 5)
/api/v1/worlds (CRUD + branch/merge = 8)
/api/v1/auth/* (login/logout/token = 4)
小计: 27 端点
Data Layer (Data):
/api/v1/objects (CRUD + search = 6)
/api/v1/links (CRUD = 4)
/api/v1/queries (execute/save/list = 3)
/api/v1/pipelines (CRUD + run = 5)
小计: 18 端点
Reasoning & Decision Layer (Intelligence):
/api/v1/rules (CRUD + evaluate = 5)
/api/v1/decisions (CRUD + execute = 3)
小计: 8 端点
Agent Runtime Layer (Agent):
/api/v1/actions (execute/list/status = 3)
/api/v1/workflows (CRUD = 3)
小计: 6 端点
合计: 59 端点
#3.2 Gateway 转换架构
┌──────────────────────────────────┐
│ API Gateway │
│ (Spring Cloud Gateway / Envoy) │
│ │
REST Client ─────►│ 1. 认证/鉴权 (JWT 验证) │
(Browser, │ 2. 限流 (Token Bucket) │
Mobile, │ 3. REST→gRPC 转换 │
3rd Party) │ 4. 响应 gRPC→JSON 转换 │
│ 5. 错误码映射 │
└────────┬─────────┬────────┬──────┘
│ │ │
┌────────▼──┐ ┌────▼────┐ ┌─▼──────────┐
│onto-control│ │onto-data│ │onto-intelli│
│ (gRPC) │ │ (gRPC) │ │ (gRPC) │
│ Control Layer │ │ Data Layer │ │ Reasoning & Decision Layer + Agent Runtime Layer │
└───────────┘ └─────────┘ └────────────┘
#3.3 REST→gRPC 映射规则
转换遵循一套确定性的映射规则:
REST 方法 → gRPC 方法映射:
POST /api/v1/object-types → ObjectTypeService.CreateObjectType
GET /api/v1/object-types/{id} → ObjectTypeService.GetObjectType
GET /api/v1/object-types → ObjectTypeService.ListObjectTypes
PUT /api/v1/object-types/{id} → ObjectTypeService.UpdateObjectType
DELETE /api/v1/object-types/{id} → ObjectTypeService.DeleteObjectType
URL 路径参数 → Protobuf 字段映射:
{id} → request.id
?page_size=20 → request.pagination.page_size
?page_token=xxx → request.pagination.page_token
HTTP Header → gRPC Metadata 映射:
Authorization: Bearer <token> → metadata["authorization"]
X-World-Id: <world_id> → metadata["x-world-id"]
X-Request-Id: <uuid> → metadata["x-request-id"]
#3.4 JSON 与 Protobuf 字段名转换
Protobuf (snake_case) → JSON (camelCase)
object_type_id → objectTypeId
display_name → displayName
created_at → createdAt
page_token → pageToken
这个转换由 protobuf-java-util 的 JsonFormat 自动完成,无需手动映射。
#4. API 版本策略
#4.1 版本号设计
版本策略:
URI 版本: /api/v1/... /api/v2/...
Proto 包版本: onto.plane_b.ontology.v1 → onto.plane_b.ontology.v2
当前版本: v1(所有 59 个端点)
规划版本: v2(当 v1 有不兼容变更时启用)
版本生命周期:
v1 发布 → v2 发布 → v1 标记废弃 → 6 个月过渡期 → v1 下线
#4.2 向后兼容规则
我们定义了明确的"兼容性契约":
✅ 向后兼容的变更(不需要升级版本):
- 添加新的 RPC 方法
- 添加新的 message 字段(使用新编号)
- 添加新的 enum 值
- 添加新的 REST 端点
- 放宽验证规则(如从 required 改为 optional)
❌ 不兼容的变更(必须升级版本):
- 删除或重命名 RPC 方法
- 删除或重命名 message 字段
- 修改字段类型或编号
- 修改 RPC 方法的语义
- 收紧验证规则
#4.3 Proto 兼容性检查
我们在 CI/CD 中使用 buf 工具进行自动兼容性检查:
# .gitlab-ci.yml 中的 proto 检查阶段
proto-lint:
stage: validate
script:
- buf lint protos/
- buf breaking protos/ --against .git#branch=main
rules:
- changes:
- protos/**/*.proto
# buf.yaml 配置
version: v1
breaking:
use:
- WIRE_JSON # 检查 wire format 和 JSON 兼容性
- PACKAGE # 检查包级别变更
lint:
use:
- DEFAULT
- COMMENTS # 强制要求注释
except:
- PACKAGE_VERSION_SUFFIX
#5. 认证与限流
#5.1 认证流程
认证架构:
Client Gateway Auth Service (Control Layer)
│ │ │
│ 1. Login │ │
│ POST /auth/login │ │
│ {user, password} │ │
│─────────────────────►│ │
│ │ 2. gRPC Authenticate │
│ │───────────────────────►│
│ │ │ 3. 验证凭证
│ │ 4. JWT + Refresh │ 查询RBAC
│ │◄───────────────────────│
│ 5. {access_token, │ │
│ refresh_token} │ │
│◄─────────────────────│ │
│ │ │
│ 6. API 请求 │ │
│ Authorization: │ │
│ Bearer <jwt> │ │
│─────────────────────►│ │
│ │ 7. JWT 本地验证 │
│ │ (公钥缓存,不调用 │
│ │ Auth Service) │
│ │ │
│ │ 8. 转发 gRPC 请求 │
│ │ (metadata 注入 │
│ │ user_id, roles) │
│ │───────────────────────►│
#5.2 JWT Token 结构
{
"sub": "user-001",
"iss": "onto-platform",
"iat": 1711234567,
"exp": 1711238167,
"roles": ["admin", "data-engineer"],
"worlds": ["world-prod", "world-staging"],
"permissions": [
"ontology:read",
"ontology:write",
"objects:read",
"objects:write",
"actions:execute"
]
}
#5.3 限流设计
限流策略(三层防护):
第一层:全局限流
├── 所有客户端共享:10,000 req/s
└── 超限返回 HTTP 429
第二层:租户限流
├── 每个租户:1,000 req/s
├── 关键 API(写操作):100 req/s
└── 超限返回 HTTP 429 + Retry-After header
第三层:用户限流
├── 每个用户:100 req/s
├── 特殊 API(导出/批量):10 req/s
└── 超限返回 HTTP 429
算法:Token Bucket(令牌桶)
实现:Gateway 内存 + Redis 共享计数
// Gateway 限流配置示例
@Configuration
public class RateLimitConfig {
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest()
.getHeaders()
.getFirst("X-User-Id")
);
}
@Bean
public RateLimiter rateLimiter() {
return new RedisRateLimiter(100, 200); // 100 req/s, burst 200
}
}
#6. Python SDK 自动生成 38 个 gRPC 客户端
#6.1 生成流程
这是我们开发效率的核心机制之一:proto 文件一旦变更,Python SDK 的 gRPC 客户端代码自动重新生成。
Proto 文件变更触发的自动化链:
protos/*.proto
│
▼
protoc + grpc_python_plugin
│
├── *_pb2.py (消息类)
├── *_pb2_grpc.py (Stub 类)
└── *_pb2.pyi (类型提示)
│
▼
SDK 封装层(手写)
│
├── grpc_object_type_client.py (ObjectType 客户端)
├── grpc_world_client.py (World 客户端)
├── grpc_action_client.py (Action 客户端)
└── ...共 38 个客户端
#6.2 SDK 客户端的封装模式
每个 gRPC 客户端遵循统一的封装模式:
"""ObjectType gRPC 客户端封装"""
from typing import Optional, List
import grpc
from ontology_sdk.grpc_client.base_client import BaseGrpcClient
from ontology_sdk.models.object_type import (
ObjectTypeModel,
CreateObjectTypeRequest,
ObjectTypeListResponse,
)
from plane_b.ontology import object_type_pb2, object_type_pb2_grpc
class GrpcObjectTypeClient(BaseGrpcClient):
"""ObjectType gRPC 客户端
提供对 ObjectType 的 CRUD 操作,自动处理:
- gRPC 连接管理(连接池 + 重连)
- 认证 token 注入
- 错误码转换(gRPC Status → SDK Exception)
- Protobuf ↔ Pydantic 模型转换
"""
def __init__(self, channel: grpc.Channel, metadata_provider):
super().__init__(channel, metadata_provider)
self._stub = object_type_pb2_grpc.ObjectTypeServiceStub(channel)
def create(
self,
world_id: str,
api_name: str,
display_name: str,
description: str = "",
properties: Optional[List[dict]] = None,
) -> ObjectTypeModel:
"""创建新的 ObjectType
Args:
world_id: 所属 World ID
api_name: API 名称(驼峰命名)
display_name: 显示名称
description: 描述
properties: 属性定义列表
Returns:
ObjectTypeModel: 创建的 ObjectType
Raises:
AlreadyExistsError: api_name 已存在
InvalidArgumentError: 参数校验失败
"""
request = object_type_pb2.CreateObjectTypeRequest(
world_id=world_id,
api_name=api_name,
display_name=display_name,
description=description,
)
if properties:
for prop in properties:
request.properties.append(
self._to_property_definition(prop)
)
response = self._call_with_retry(
self._stub.CreateObjectType,
request,
)
return ObjectTypeModel.from_proto(response.object_type)
def get(self, object_type_id: str) -> ObjectTypeModel:
"""获取 ObjectType 详情"""
request = object_type_pb2.GetObjectTypeRequest(
id=object_type_id
)
response = self._call_with_retry(
self._stub.GetObjectType, request
)
return ObjectTypeModel.from_proto(response.object_type)
def list(
self,
world_id: str,
page_size: int = 20,
page_token: str = "",
) -> ObjectTypeListResponse:
"""列出 World 下的所有 ObjectType"""
request = object_type_pb2.ListObjectTypesRequest(
world_id=world_id,
pagination=common_pb2.PaginationRequest(
page_size=page_size,
page_token=page_token,
),
)
response = self._call_with_retry(
self._stub.ListObjectTypes, request
)
return ObjectTypeListResponse.from_proto(response)
#6.3 BaseGrpcClient 的通用能力
class BaseGrpcClient:
"""所有 gRPC 客户端的基类
提供通用的:
- 重试机制(指数退避)
- 错误码转换
- 元数据注入
- 日志记录
"""
RETRYABLE_CODES = {
grpc.StatusCode.UNAVAILABLE,
grpc.StatusCode.DEADLINE_EXCEEDED,
grpc.StatusCode.RESOURCE_EXHAUSTED,
}
def _call_with_retry(self, method, request, max_retries=3):
"""带重试的 gRPC 调用"""
last_error = None
for attempt in range(max_retries + 1):
try:
metadata = self._metadata_provider.get_metadata()
return method(request, metadata=metadata)
except grpc.RpcError as e:
last_error = e
if e.code() not in self.RETRYABLE_CODES:
raise self._convert_error(e)
if attempt < max_retries:
delay = (2 ** attempt) * 0.1 # 0.1s, 0.2s, 0.4s
time.sleep(delay)
raise self._convert_error(last_error)
def _convert_error(self, rpc_error: grpc.RpcError):
"""gRPC 错误码 → SDK 异常"""
code = rpc_error.code()
detail = rpc_error.details()
mapping = {
grpc.StatusCode.NOT_FOUND: NotFoundError,
grpc.StatusCode.ALREADY_EXISTS: AlreadyExistsError,
grpc.StatusCode.INVALID_ARGUMENT: InvalidArgumentError,
grpc.StatusCode.PERMISSION_DENIED: PermissionDeniedError,
grpc.StatusCode.UNAUTHENTICATED: UnauthenticatedError,
grpc.StatusCode.RESOURCE_EXHAUSTED: RateLimitError,
grpc.StatusCode.INTERNAL: InternalError,
}
exc_class = mapping.get(code, PlatformError)
return exc_class(detail, grpc_code=code)
#6.4 38 个客户端清单
38 个 gRPC 客户端按 Layer 分组:
Control Layer (Control) — 16 个客户端:
grpc_object_type_client.py
grpc_link_type_client.py
grpc_property_type_client.py
grpc_action_type_client.py
grpc_ontology_version_client.py
grpc_world_client.py
grpc_world_branch_client.py
grpc_world_merge_client.py
grpc_auth_client.py
grpc_rbac_client.py
grpc_audit_client.py
grpc_lineage_client.py
grpc_compliance_client.py
grpc_service_registry_client.py
grpc_config_registry_client.py
grpc_metric_client.py
Data Layer (Data) — 10 个客户端:
grpc_object_storage_client.py
grpc_link_storage_client.py
grpc_property_storage_client.py
grpc_time_series_client.py
grpc_query_client.py
grpc_aggregation_client.py
grpc_search_client.py
grpc_sync_task_client.py
grpc_view_client.py
grpc_subscription_client.py
Reasoning & Decision Layer (Intelligence) — 7 个客户端:
grpc_rule_client.py
grpc_rule_evaluation_client.py
grpc_decision_client.py
grpc_derived_property_client.py
grpc_dependency_dag_client.py
grpc_scenario_client.py
grpc_reasoning_client.py
Agent Runtime Layer (Agent) — 5 个客户端:
grpc_action_client.py
grpc_action_template_client.py
grpc_approval_client.py
grpc_workflow_client.py
grpc_agent_client.py
#7. 统一错误码体系
#7.1 错误码结构
跨所有 Layer 的统一错误码格式:
错误码格式:ONTO-{Layer}-{CATEGORY}-{NUMBER}
Layer:
B = Control Data Layer = Data Reasoning & Decision Layer = Intelligence Agent Runtime Layer = Agent Runtime
H = SDK
CATEGORY:
VAL = 验证错误
AUTH = 认证/授权
RES = 资源错误
SYS = 系统错误
BIZ = 业务错误
示例:
ONTO-B-VAL-001 = Control Layer 参数验证失败
ONTO-C-RES-003 = Data Layer 对象不存在
ONTO-D-BIZ-007 = Intelligence Layer 规则冲突
ONTO-E-SYS-002 = Agent Runtime 工作流超时
#7.2 gRPC Status 与 HTTP Status 映射
gRPC → HTTP 状态码映射:
gRPC Code HTTP Status 含义
─────────────────────────────────────────────────
OK 200 成功
INVALID_ARGUMENT 400 参数错误
UNAUTHENTICATED 401 未认证
PERMISSION_DENIED 403 无权限
NOT_FOUND 404 资源不存在
ALREADY_EXISTS 409 资源已存在
FAILED_PRECONDITION 412 前置条件失败
RESOURCE_EXHAUSTED 429 限流
CANCELLED 499 客户端取消
INTERNAL 500 内部错误
UNAVAILABLE 503 服务不可用
DEADLINE_EXCEEDED 504 超时
#7.3 错误响应格式
{
"error": {
"code": "ONTO-B-RES-003",
"message": "ObjectType 'CustomerOrder' not found in World 'world-prod'",
"grpc_code": "NOT_FOUND",
"http_status": 404,
"details": {
"resource_type": "ObjectType",
"resource_id": "CustomerOrder",
"world_id": "world-prod"
},
"request_id": "req-abc-123",
"timestamp": "2026-03-24T10:30:00Z"
}
}
#7.4 Proto 定义的错误详情
// common/error.proto
syntax = "proto3";
package onto.common;
message ErrorDetail {
string code = 1; // ONTO-X-XXX-NNN
string message = 2; // 人类可读描述
string request_id = 3; // 请求追踪 ID
int64 timestamp = 4; // 错误发生时间
map<string, string> context = 5; // 上下文信息
// 可选:重试建议
RetryInfo retry_info = 6;
}
message RetryInfo {
bool retryable = 1;
int32 retry_after_seconds = 2;
int32 max_retries = 3;
}
#8. 跨 Layer 调用链路追踪
#8.1 请求追踪设计
一次 API 请求的完整调用链:
Client → Gateway → onto-control → onto-data → onto-intelligence
│
▼
Kafka Event
│
▼
onto-data (Consumer)
每一跳都携带:
X-Request-Id: 全局唯一请求 ID
X-Trace-Id: 分布式追踪 ID(OpenTelemetry)
X-Span-Id: 当前 Span ID
X-Parent-Span: 父 Span ID
X-World-Id: 当前 World 上下文
#8.2 gRPC Interceptor 实现
class TracingInterceptor(grpc.UnaryUnaryClientInterceptor):
"""客户端追踪拦截器"""
def intercept_unary_unary(self, continuation,
client_call_details, request):
# 注入追踪上下文到 gRPC metadata
metadata = list(client_call_details.metadata or [])
trace_context = get_current_trace_context()
metadata.extend([
("x-request-id", trace_context.request_id),
("x-trace-id", trace_context.trace_id),
("x-span-id", generate_span_id()),
("x-parent-span", trace_context.span_id),
])
new_details = client_call_details._replace(
metadata=metadata
)
return continuation(new_details, request)
// Java 端的服务端拦截器
@Component
public class TracingServerInterceptor
implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String requestId = headers.get(
Metadata.Key.of("x-request-id",
Metadata.ASCII_STRING_MARSHALLER)
);
String traceId = headers.get(
Metadata.Key.of("x-trace-id",
Metadata.ASCII_STRING_MARSHALLER)
);
// 设置 MDC 上下文用于日志关联
MDC.put("requestId", requestId);
MDC.put("traceId", traceId);
try {
return next.startCall(call, headers);
} finally {
MDC.clear();
}
}
}
#9. 对比分析:Palantir vs 智策平台的 API 设计
| 维度 | Palantir Foundry | 智策平台 |
|---|---|---|
| 外部 API | REST (OSDK) | REST (59 端点) |
| 内部通信 | 未公开(推测 gRPC/自研RPC) | gRPC (63 proto) |
| API 定义 | 部分公开 Swagger | Protobuf Contract-First |
| SDK 生成 | OSDK CLI | protoc + 手动封装 |
| 认证方式 | OAuth 2.0 | JWT + OAuth 2.0 |
| API 版本 | URI 版本 (/v1, /v2) | URI + Proto 包版本 |
| 限流 | 有(未公开细节) | Token Bucket 三层限流 |
| 错误码 | HTTP Status + JSON | 统一编码 ONTO-X-XXX-NNN |
| API 网关 | 私有 | Spring Cloud Gateway |
#10. 实践经验与避坑指南
#10.1 proto 设计的 5 个教训
教训 1:字段命名要从 Day 1 就严格统一
错误:有的叫 object_type_id,有的叫 objectTypeId,有的叫 type_id
修正:全部使用 snake_case,严格遵循 {entity}_{field} 命名
教训 2:不要把 domain model 直接暴露为 proto message
错误:proto message 和数据库表结构一一对应
修正:proto 是 API 契约,可以与内部模型不同
教训 3:enum 的第一个值必须是 UNSPECIFIED
错误:enum Status { ACTIVE = 0; }
修正:enum Status { STATUS_UNSPECIFIED = 0; ACTIVE = 1; }
教训 4:分页从 Day 1 就必须是标准组件
错误:每个 List 方法自己定义 offset/limit
修正:统一 PaginationRequest/PaginationResponse
教训 5:预留字段编号空间
错误:字段编号连续分配 1,2,3,4,5...
修正:按类别分段 1-15(高频), 16-99(常规), 100+(扩展)
#10.2 Gateway 层的 3 个陷阱
陷阱 1:流式 API 的 REST 映射
问题:gRPC 的 ServerStream 无法直接映射为 REST
方案:使用 SSE (Server-Sent Events) 或分页轮询
陷阱 2:大文件上传
问题:gRPC 默认消息大小 4MB
方案:大文件走独立的 REST 端点,使用分块上传
陷阱 3:WebSocket 需求
问题:实时推送场景 REST 不够用
方案:Gateway 提供独立的 WebSocket 端点,
内部转换为 gRPC 双向流
#Key Takeaways
-
Contract-First 是大型分布式平台的必选项:63 个 proto 文件是系统的唯一真相源,从 proto 生成代码而不是反过来,确保了跨语言、跨团队的接口一致性。
-
REST 和 gRPC 不是二选一:内部 gRPC 保证性能和类型安全,外部 REST 保证兼容性和易用性,Gateway 层负责转换——两全其美。
-
统一的错误码和追踪体系是可观测性的基础:ONTO-X-XXX-NNN 编码让任何错误都能快速定位到具体 Layer 和类别,配合 X-Request-Id 实现端到端追踪。
#下一篇预告
S2-09 数据流全景:一条数据从采集到决策的完整旅程 —— 我们将追踪一条数据变更如何穿越整个系统:从外部数据库变更 → Flink CDC 捕获 → Kafka 传输 → 存储层写入 → Ontology Runtime 触发订阅 → 派生属性重算 → 规则评估 → 决策执行 → 审计记录,完整展示智策平台的数据流动全景。
tags: API-Design, Protobuf, gRPC, REST, Gateway, Contract-First, SDK, Error-Handling, Rate-Limiting, coomia-dip