Skip to content

feat(cache): harden Redis resilience and multilevel caching#57

Merged
ULookup merged 48 commits into
3.0-devfrom
codex/cache-resilience-completion
Jul 17, 2026
Merged

feat(cache): harden Redis resilience and multilevel caching#57
ULookup merged 48 commits into
3.0-devfrom
codex/cache-resilience-completion

Conversation

@ULookup

@ULookup ULookup commented Jul 16, 2026

Copy link
Copy Markdown
Owner

背景

本 PR 基于 3.0-dev,集中解决 Redis/缓存链路在高并发、故障和热点访问下的稳定性问题,并补齐可执行的 Go 回归与 CI 门禁。

Closes #35
Closes #37
Closes #38
Closes #45
Addresses #48 的缓存与限流范围(不关闭 #48)。

做了什么

1. Redis 熔断、恢复与可观测性

  • 为每个 RedisClient 增加 Closed / Open / HalfOpen 状态机;健康热路径仅使用原子操作。
  • 使用 generation permit 隔离旧请求完成事件,避免旧 epoch 成功回调错误关闭新熔断周期。
  • 仅连接、I/O 和连接池等待类错误计入熔断;Redis 业务 ReplyError 不触发熔断。
  • 增加 open/rejected/recovered/call-failure 指标和低基数状态转换日志。

2. UserInfo L1/L2/singleflight 缓存

  • 建立进程内 L1、Redis L2、Identity RPC 的 cache-aside 链路。
  • Redis L2 使用 64 个虚拟 bucket;批量读按 hash slot 分组 MGET,批量写使用同槽 Lua CAS。
  • 同一 UID 并发 miss 由 singleflight 合并,避免热点穿透和回源风暴。
  • 仅明确 user-not-found 写短负缓存;依赖异常不污染 sentinel。
  • generation fencing 与 Profile 更新前后失效,阻止旧 inflight 结果回填新版本缓存。

3. Redis 故障下的限流与真相源边界

  • 普通限流在 Redis 异常时退化为 64 shard、有界容量的本地 token bucket;容量耗尽时 fail-closed。
  • SeqGen、UnackedPush 等真相源不做不安全降级,失败会显式返回并触发 MQ requeue。
  • Unacked 状态在客户端投递前持久化;OnlineRoute 严格读取,Redis 故障不再伪装为离线。
  • DeviceSet、OnlineRoute、UnackedPush 使用同槽 key 与 Lua 原子更新;TTL、退避和 jitter 已补齐。

4. Go 测试框架中的缓存回归

沿用项目当前 Go 分层测试框架,没有新增 C++/CTest 旁路:

  • FN-CA-01..05:缓存、singleflight、失效、TTL 与限流。
  • RL-05:Redis 停止/恢复、breaker 指标、本地限流、SeqGen/Unacked fail-closed 与 MQ requeue。
  • PF-09:cold/L2/L1、200 并发冷 key、RPC reduction、p95、吞吐与 allocs/req。
  • 新增真实 Push RPC + WebSocket ACK 回归:同一 user_seq 重复写入时 ZSET 只保留一个身份,HASH 保存最新 payload,ACK 后两个索引都清理。
  • UserInfo 更新回归覆盖 L2 删除和最新值重新填充,并覆盖最大 TTL 抖动窗口。

5. 可执行的 Compose service artifact CI 闭环

此前 reliability/perf/func gate 在 clean runner 上没有九个服务二进制,Compose 实际不可执行。本 PR 现在:

  • 使用固定 Ubuntu 24.04 digest 和锁定的第三方源码 revision 构建 builder。
  • service-artifacts 一次构建九个服务并上传同一 artifact;bvtfuncreliabilityperf-cache 全部下载并恢复该产物。
  • func 仍需等待 BVT 通过;develop/3.0-dev 的非 main push 保留 BVT 门禁。
  • 只让 loader/glibc/libstdc++ 等严格 core ABI 来自同 digest runtime;其余 /usr/local/usr/lib 的完整 ldd 闭包都放入应用私有 /im/depends,不覆盖系统 /lib
  • manifest 校验额外文件、哈希和可执行位;缺库、host fallback、symlink escape、打包 core ABI、同 basename 不同内容均 fail-closed。
  • 九个 runtime Dockerfile 与 builder 使用同一 digest;Media 改用 CMake jsoncpp_lib imported target,不再绑定旧 /usr/local/lib/libjsoncpp.so.19

6. seq_id 水位线语义

  • UpdateReadAck 统一使用会话内单调递增 seq_id,通过数据库 GREATEST 原子推进 last_ack_seq,不再用全局 message_id 回查后换算。
  • Push delivery ACK 明确区分三个标识:message_id 标识投递项,user_seq 只清理设备 Unacked,seq_id 推进会话送达水位。
  • 非消息类 Push 的 seq_id=0 仍可正常清理 Unacked,但不会误推进会话水位。
  • 未读数由 Conversation.MarkRead(last_read_seq) 管理;delivery ACK 不再与已读水位混淆。

并发与可用性取舍

  • 没有引入额外数据库、协调服务或复杂一致性平台。
  • 健康热路径避免全局锁;热点 miss 合并;Redis Cluster 操作保持同槽原子性。
  • 可重建缓存允许降级,限流采用有界本地保护,真相源失败则明确 fail-closed。
  • Profile 数据库更新不会因缓存失效失败而回滚,通过指标暴露短暂不一致。

验证

本轮完成的代码层验证:

  • go test ./pkg/client/... ./pkg/contracts/... -count=1
  • go test ./... -count=1
  • func / reliability / perf 三套 tagged go vet
  • PF-09 helper race test
  • artifact packager/validator/core-ABI 脚本语法与端到端 Go contract
  • workflow YAML 解析、docker compose config --quiet
  • git diff --check
  • 多轮独立代码审查;最终无 Critical / Important 遗留

按当前要求,本地没有实际执行九服务原生编译或 Linux full-stack 性能门禁;这些由 PR CI 的 service-artifacts、RL-05 和 PF-09 gate 执行,正文不伪报本地吞吐结果。

Review 重点

请重点关注:

  • breaker generation/state 的并发正确性;
  • UserInfo generation fencing 与失效顺序;
  • Unacked persist-before-delivery 和 ACK 原子清理;
  • artifact 的 core ABI allowlist、non-core 私有闭包和 fail-closed validator;
  • message_id / user_seq / seq_id 三类标识的职责边界。

Comment thread transmite/source/transmite_server.h Outdated
// L1 is deliberately short-lived and remains available even when the
// Redis generation read failed. This lets singleflight followers share
// a successful Identity result without allowing an unfenced L2 write.
if (_local_user_cache) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] generation CAS 失败后仍会把旧值写入 L1。 竞态场景:reader 捕获 generation G 并从 Identity 读到旧资料;UpdateProfile 随后推进 generation 并提交新资料;set_if_generation(..., G) 会正确返回 false,但这里仍无条件把旧 bytes 放入 L1,导致 Redis 健康时也会继续返回约 36–54 秒的旧资料。请保留 CAS 结果:generation 已知时仅在 CAS 成功后写 L1;只有 generation 无法读取时才按注释所述允许短期 L1 fallback(或重新读取 generation 确认未变化)。

Comment thread common/dao/data_redis.hpp
local ht = key_type(KEYS[2])
if zt ~= 'none' and zt ~= 'zset' then return redis.error_reply('unacked key wrong type') end
if ht ~= 'none' and ht ~= 'hash' then return redis.error_reply('unacked index wrong type') end
redis.call('ZADD', KEYS[1], score, ARGV[2])

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 同一 user_seq 使用不同 payload 重试时,这个写入不是幂等的。 ZSET member 是 seq:payload,所以 payload 改变会新增 member;随后 HSET 会覆盖 seq -> payload。ACK 只根据 HASH 中的最新 payload 删除最新 member,旧 member 会成为无法 ACK 的孤儿,并持续被 peek_due 重发直到 TTL 到期。请在 Lua 中先读取旧 payload 并 ZREM seq:old_payload,再写新值;或者让 ZSET member 只使用稳定的 seq、payload 单独保存在 HASH。

Comment thread tests/perf/cache_test.go
// executes a fixed ten-second steady workload over 20 prewarmed senders. This
// prevents Go's adaptive benchmark calibration from changing the cache state.
func BenchmarkPF09_UserInfoCache(b *testing.B) {
if os.Getenv("PF09_RUN_FULLSTACK") != "1" {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] PF-09(以及 RL-05)目前并未形成 CI gate。 workflow 的 perf job 调用的是 make test-perf,它不会设置 PF09_RUN_FULLSTACK=1,因此这里会直接 skip;test-perf-cache-gate 没有被任何 workflow 调用。类似地,新增的 test-reliability/RL-05 也没有 CI job 调用。请把两个 gate 接入适当的 workflow,并为 PF-09 配置其要求的 Transmite 高限流参数,否则 PR 描述中的吞吐、RPC reduction 和故障恢复门槛不会被自动验证。

Comment thread .github/workflows/ci.yml
- name: Install Go protobuf generator
run: go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11
- name: Start full stack
run: docker compose up -d --build

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 新增 gate job 在没有服务二进制的情况下启动 Compose,因此当前不可执行。 reliability/perf-cache job 都只 checkout、安装 Go/protoc,然后直接运行 docker compose up -d --build;仓库的各服务 Dockerfile 会 COPY ./build/*_server,但这些 job 没有编译或下载该目录。当前 reliability run 已在 COPY ./build/conversation_server(以及其他服务)处失败,PF-09 的定时 job 也会走同一路径。请在 job 内构建服务,或从一个可成功完成的构建 job 上传/下载 artifacts 后再启动 Compose。

Comment thread common/test/CMakeLists.txt Outdated
set_property(TARGET test_redis_mutex_contract PROPERTY CXX_STANDARD_REQUIRED ON)
INSTALL(TARGETS test_redis_mutex_contract RUNTIME DESTINATION bin)

add_executable(test_unacked_pending_ledger test_unacked_pending_ledger.cc)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 新增的两个 C++ 回归测试没有被 CI 实际执行。 这里仅创建了 test_unacked_pending_ledger executable,没有 add_test(),workflow 也没有运行该二进制;而 test_user_info_generation_fence.cc 完全没有加入任何 CMake target。因此前者最多只会被编译,后者甚至不会编译,两项修复都缺少自动回归验证。请为两者创建 target、注册 CTest(或显式执行),并在 CI 中运行。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant