查询接口设计:HTTP/gRPC 双协议支持

基于当前实现梳理 Seastar Log Engine 的查询服务,包括 HTTP / gRPC 接口、状态字段、记录查询语义与 Prometheus 暴露方式。

为什么需要独立查询服务

Seastar Log Engine 的写入链路和查询链路是分开的。

写入侧关注:

  1. 路由
  2. 批量
  3. DMA 对齐写入
  4. rotate / archive / checkpoint

查询侧关注:

  1. 当前路由配置是否符合预期
  2. active log / archive 是否能被正确读取
  3. 读路径是否出现损坏 segment、坏 gzip、恢复回退等问题
  4. 运维侧能否通过状态接口和指标快速判断系统健康度

因此项目当前提供了独立 log_engine_query_server

当前实际提供的接口

HTTP

当前 HTTP 端点共有 4 个:

GET /healthz
GET /v1/status
GET /v1/route
GET /v1/records

gRPC

当前 proto 只定义了 3 个 unary RPC:

service QueryService {
  rpc GetStatus(Empty) returns (StatusReply);
  rpc Route(RouteRequest) returns (RouteReply);
  rpc QueryRecords(QueryRecordsRequest) returns (QueryRecordsReply);
}
Rendering diagram...

这里要明确两点:

  1. 当前没有 gRPC streaming 接口。
  2. 当前没有额外的查询缓存、游标分页、服务端索引层或访问控制层实现。

这些都可以是后续演进方向,但不属于当前仓库事实。

查询服务启动方式

典型启动命令如下:

./build/log_engine_query_server \
  --config ./config/engine.conf \
  --routing-strategy consistent_hashing \
  --routing-virtual-nodes 256 \
  --http-address 0.0.0.0 \
  --http-port 18080 \
  --grpc-address 0.0.0.0 \
  --grpc-port 19090 \
  --metrics-address 0.0.0.0 \
  --metrics-port 19181

这里的 Prometheus 指标不是作为 /metrics 挂在主 HTTP 服务上,而是通过单独的 metrics server 暴露在 metrics-port

1. Status 接口

HTTP

curl http://127.0.0.1:18080/v1/status

gRPC

./build/log_engine_query_client --target 127.0.0.1:19090 --method status

当前返回的信息

状态接口会返回几类信息:

  1. 路由配置

    • routing_strategy
    • routing_shards
    • routing_virtual_nodes
    • ring_size
  2. 存储路径

    • log_dir
    • archive_dir
    • shard_file_prefix
  3. Reader 统计

    • segments_read
    • archive_segments_read
    • active_segments_read
    • records_returned
    • corrupted_segments
    • corrupted_lines
    • gzip_read_errors
  4. LogManager 统计

    • rotate_operations
    • checkpoint_write_successes
    • checkpoint_write_failures
    • recovery_fallbacks
    • recovery_fallback_incomplete_checkpoint
    • recovery_fallback_stale_checkpoint
    • gzip_archive_successes
    • gzip_archive_failures
  5. 健康度汇总

    • health
    • health_reason
    • health_reason_basis
    • recovery_fallback_reason
    • health_recent_errors

health 字段的真实含义

当前实现里健康度取值不是 healthy,而是:

  • ok
  • degraded
  • unhealthy

无异常时也不是 no_recent_errors,而是:

  • health = "ok"
  • health_reason = "none"
  • health_reason_basis = "none"

这来自当前 health_monitor 的状态机逻辑。

2. Route 接口

HTTP

curl "http://127.0.0.1:18080/v1/route?key=route-a"

gRPC

./build/log_engine_query_client --target 127.0.0.1:19090 --method route --route-key route-a

返回字段

{
  "route_key": "route-a",
  "shard": 1,
  "hash": 123456789,
  "token": 987654321,
  "used_local_fallback": false
}

字段含义:

  • shard 当前 route key 应该落到的 shard
  • hash route key 的稳定哈希值
  • token 一致性哈希命中的 ring token;hash_modulo 下通常等于 hash
  • used_local_fallback 是否命中了空 key 本地回退路径

一个容易忽略的细节

query_server 的 route 查询使用的是独立 ShardRouter 配置,并不依赖正在运行的 writer 实例。

默认情况下:

  • routing-shards=0 会回落到当前 seastar::smp::count

所以 route 查询是一个“按当前配置推导路由结果”的接口,而不是写入面实时状态探针。

3. Records 接口

HTTP

curl "http://127.0.0.1:18080/v1/records?limit=10"

gRPC

./build/log_engine_query_client --target 127.0.0.1:19090 --method records --limit 10 --include-archive true

支持的查询参数

参数类型必填说明
shardint指定 shard
seq_fromuint64起始 sequence
seq_touint64结束 sequence
time_fromstring起始时间字符串
time_tostring结束时间字符串
limitint默认 100
include_archivebool默认 true

这里要特别强调两点:

  1. limit 在当前实现里不是必填,默认值为 100
  2. time_from/time_to 当前只是对记录里的 timestamp 字符串做比较,不是 RFC3339 解析器。

时间过滤的真实语义

项目当前记录时间格式来自 writer:

YYYY-MM-DD HH:MM:SS.ffffff

例如:

2026-05-15 10:30:00.123456

因此在实际使用中,time_from/time_to 最安全的写法应与记录格式一致,例如:

curl "http://127.0.0.1:18080/v1/records?time_from=2026-05-15 10:00:00.000000&time_to=2026-05-15 11:00:00.000000&limit=100"

如果直接传 RFC3339 形式的 2026-05-15T10:00:00Z,当前实现并不会做时间解析或时区转换。

返回字段

每条记录返回:

  • crc
  • timestamp
  • shard
  • has_sequence
  • sequence
  • level
  • payload
  • raw_line

其中:

  • level 当前输出为 INFO / WARN / ERROR
  • 若该记录没有结构化 sequence,has_sequence=false
  • raw_line 是原始日志行,适合做问题排查

4. 读路径的容错语义

当前查询链路不是“遇到坏数据直接失败”,而是尽量保守读取。

损坏处理策略

  1. 普通 segment 中出现坏行

    • 截断到第一条坏行之前
    • 当前 segment 记一次损坏
    • 继续读后续 segment
  2. gzip archive 读错

    • 跳过整个 gzip segment
    • 计入 gzip_read_errors
    • 继续读后续 segment
  3. 文件在读取过程中消失

    • 视为并发 cleanup
    • warn,但不当作损坏

这个语义和当前 log_reader.cc 的实现一致,目的是避免“一段坏 archive 把整条查询链路拖死”。

5. Proto 结构

当前 proto 里最需要注意的是 StatusReply 字段已经比较完整:

message StatusReply {
  string routing_strategy = 1;
  uint32 routing_shards = 2;
  uint64 routing_virtual_nodes = 3;
  uint64 ring_size = 4;
  string log_dir = 5;
  string archive_dir = 6;
  string shard_file_prefix = 7;
  uint64 reader_segments_read = 8;
  uint64 reader_archive_segments_read = 9;
  uint64 reader_active_segments_read = 10;
  uint64 reader_records_returned = 11;
  uint64 reader_corrupted_segments = 12;
  uint64 reader_corrupted_lines = 13;
  uint64 reader_gzip_read_errors = 14;
  string health = 15;
  ...
}

换句话说,当前 gRPC status 已经不是一个只有路由配置的轻量接口,而是和 HTTP /v1/status 对齐的综合状态接口。

6. Prometheus 指标暴露

查询服务启动时会注册:

  • log_engine_reader_*
  • log_engine_health_*

如果同时还有 writer 进程暴露 writer metrics,则整体观测可拼出:

  1. 写入吞吐和背压情况
  2. 查询返回量与读路径损坏情况
  3. 最近 5 分钟健康窗口内的异常分布

典型查看方式:

curl http://127.0.0.1:19181/metrics

7. 当前接口边界

为了避免把“设计想法”误写成“现状”,这里把尚未落地的能力明确列出来:

  • 没有 gRPC streaming query
  • 没有内建分页 cursor
  • 没有查询结果缓存
  • 没有索引加速层
  • 没有内建 IP 白名单 / ACL
  • 没有查询审计日志
  • 没有统一的 query rate limit

这些都值得做,但不属于当前代码。

总结

当前 query_server 的定位很明确:

  1. 暴露 route / records / status 三类核心能力
  2. 保持 HTTP 与 gRPC 的结果结构一致
  3. 用保守读语义处理坏 segment / 坏 gzip
  4. 通过独立 metrics 端口提供 Reader 与 Health 指标

如果把它描述成一句话:

它不是一个“重型日志检索系统”,而是当前单机日志引擎的运维排障与一致性观测接口。


下一篇:《故障注入与容错设计:系统健壮性验证》