4. 定义搜索接口契约
在分布式搜索系统中,接口契约是客户端与服务端之间的“法律条文”。本章定义 SearchService 的 gRPC/Protobuf 接口、请求/响应结构体以及全局错误码规范,确保前后端、多语言 SDK 之间的通信行为可预测、可测试、可演进。
设计原则:契约先行,代码后写。先定义 .proto 文件,再生成 Go 代码,避免实现过程中随意修改接口导致上下游断裂。
4.1 SearchService 接口定义
采用 gRPC 作为 RPC 框架,Protobuf 作为 IDL。核心服务只暴露一个 RPC 方法 Search,保持接口简洁,便于后续扩展流式或双向流版本。
// search.proto
syntax = "proto3";
package search.v1;
option go_package = "github.com/example/search/api/v1;searchv1";
service SearchService {
// 单次搜索请求,返回搜索结果及性能元数据
rpc Search(SearchRequest) returns (SearchResponse);
}
为什么只定义一个 RPC? 搜索系统的核心语义就是“查询→返回结果”。将自动补全、推荐、统计等能力拆分为独立服务或独立 RPC,避免单个接口膨胀,降低尾延迟排查难度。
4.2 请求结构体设计
请求结构体需要携带查询文本、分页参数、排序规则以及可选的追踪标识。所有字段均使用小写蛇形命名,符合 Protobuf 风格。
message SearchRequest {
// 查询字符串,必填,最大长度 1024 字符
string query = 1;
// 页码,从 1 开始,默认 1
int32 page = 2;
// 每页条数,默认 20,最大 100
int32 page_size = 3;
// 排序字段,可选:relevance / date / price
string sort_by = 4;
// 排序方向,true 表示降序
bool sort_desc = 5;
// 请求追踪 ID,用于全链路日志关联
string trace_id = 6;
// 超时控制(毫秒),客户端建议值,服务端可忽略
int32 timeout_ms = 7;
}
| 字段 | 类型 | 必填 | 校验规则 |
|---|---|---|---|
| query | string | 是 | 1 ≤ len ≤ 1024 |
| page | int32 | 否 | ≥ 1,默认 1 |
| page_size | int32 | 否 | 1 ≤ value ≤ 100,默认 20 |
| sort_by | string | 否 | 枚举值之一 |
| trace_id | string | 否 | UUID 格式建议 |
4.3 响应结构体设计
响应结构体包含结果列表、分页元数据、性能指标以及错误信息。性能指标字段用于客户端感知尾延迟和服务端排查慢查询。
message SearchResponse {
// 搜索结果条目列表
repeated SearchResult results = 1;
// 分页信息
Pagination pagination = 2;
// 性能元数据
PerformanceMeta performance = 3;
// 业务错误码,0 表示成功
int32 code = 4;
// 人类可读的错误消息
string message = 5;
}
message SearchResult {
string doc_id = 1;
string title = 2;
string snippet = 3;
double score = 4;
map<string, string> fields = 5;
}
message Pagination {
int32 page = 1;
int32 page_size = 2;
int32 total_results = 3;
int32 total_pages = 4;
}
message PerformanceMeta {
// 服务端实际处理耗时(毫秒)
int64 server_cost_ms = 1;
// 命中缓存层数:0=未命中,1=本地缓存,2=分布式缓存
int32 cache_hit_level = 2;
// 参与计算的 shard 数量
int32 shard_count = 3;
}
注意:响应中的
code 和 message 用于业务层错误,不要与 gRPC 状态码混淆。gRPC 状态码只表示传输层成功/失败,业务错误必须通过响应体传递。
4.4 错误码规范
错误码采用三段式数字编码:服务域(2位) + 错误类型(2位) + 具体错误(2位)。例如 010201 表示搜索服务(01)的查询错误(02)中的查询超时(01)。
| 错误码 | 名称 | 说明 | 建议客户端行为 |
|---|---|---|---|
| 000000 | SUCCESS | 请求成功 | 正常展示结果 |
| 010101 | QUERY_EMPTY | 查询字符串为空 | 提示用户输入 |
| 010102 | QUERY_TOO_LONG | 查询超过 1024 字符 | 截断后重试 |
| 010201 | QUERY_TIMEOUT | 服务端处理超时 | 降级展示或重试 |
| 010301 | SHARD_UNAVAILABLE | 部分分片不可用 | 展示部分结果 |
| 019999 | INTERNAL_ERROR | 服务端未知错误 | 展示错误页 |
// Go 错误码常量定义
const (
CodeSuccess = 0
CodeQueryEmpty = 10101
CodeQueryTooLong = 10102
CodeQueryTimeout = 10201
CodeShardUnavailable = 10301
CodeInternalError = 19999
)
4.5 接口契约与尾延迟的关系
明确的接口契约是尾延迟治理的第一道防线:
- 超时字段传递:请求中的
timeout_ms让服务端提前感知客户端容忍度,避免无限制等待。 - 性能元数据回传:响应中的
server_cost_ms和cache_hit_level帮助客户端判断延迟来源。 - 错误码分级:客户端根据错误码决定是否重试、降级或报错,避免盲目重试加剧尾延迟。
4.6 契约演进策略
接口契约一旦发布,必须遵循向后兼容原则。以下策略确保服务端升级不影响已有客户端:
- 字段只能新增,不能删除或修改类型。新增字段使用 optional 或设置合理的默认值。
- 错误码只能追加,不能回收。废弃的错误码保留定义,标记为
deprecated。 - RPC 方法签名不变。如需新语义,定义新的 RPC 方法(如
SearchV2),而不是修改现有方法。
实战建议:在 .proto 文件中使用
reserved 关键字预留字段编号和枚举值,防止未来冲突。每次修改后运行 buf breaking 检查兼容性。