feat(cache): harden Redis resilience and multilevel caching#57
Conversation
| // 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) { |
There was a problem hiding this comment.
[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 确认未变化)。
| 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]) |
There was a problem hiding this comment.
[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。
| // 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" { |
There was a problem hiding this comment.
[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 和故障恢复门槛不会被自动验证。
| - 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 |
There was a problem hiding this comment.
[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。
| 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) |
There was a problem hiding this comment.
[P2] 新增的两个 C++ 回归测试没有被 CI 实际执行。 这里仅创建了 test_unacked_pending_ledger executable,没有 add_test(),workflow 也没有运行该二进制;而 test_user_info_generation_fence.cc 完全没有加入任何 CMake target。因此前者最多只会被编译,后者甚至不会编译,两项修复都缺少自动回归验证。请为两者创建 target、注册 CTest(或显式执行),并在 CI 中运行。
背景
本 PR 基于
3.0-dev,集中解决 Redis/缓存链路在高并发、故障和热点访问下的稳定性问题,并补齐可执行的 Go 回归与 CI 门禁。Closes #35
Closes #37
Closes #38
Closes #45
Addresses #48 的缓存与限流范围(不关闭 #48)。
做了什么
1. Redis 熔断、恢复与可观测性
RedisClient增加 Closed / Open / HalfOpen 状态机;健康热路径仅使用原子操作。2. UserInfo L1/L2/singleflight 缓存
3. Redis 故障下的限流与真相源边界
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。user_seq重复写入时 ZSET 只保留一个身份,HASH 保存最新 payload,ACK 后两个索引都清理。5. 可执行的 Compose service artifact CI 闭环
此前 reliability/perf/func gate 在 clean runner 上没有九个服务二进制,Compose 实际不可执行。本 PR 现在:
service-artifacts一次构建九个服务并上传同一 artifact;bvt、func、reliability、perf-cache全部下载并恢复该产物。func仍需等待 BVT 通过;develop/3.0-dev 的非 main push 保留 BVT 门禁。/usr/local或/usr/lib的完整ldd闭包都放入应用私有/im/depends,不覆盖系统/lib。jsoncpp_libimported target,不再绑定旧/usr/local/lib/libjsoncpp.so.19。6.
seq_id水位线语义UpdateReadAck统一使用会话内单调递增seq_id,通过数据库GREATEST原子推进last_ack_seq,不再用全局message_id回查后换算。message_id标识投递项,user_seq只清理设备 Unacked,seq_id推进会话送达水位。seq_id=0仍可正常清理 Unacked,但不会误推进会话水位。Conversation.MarkRead(last_read_seq)管理;delivery ACK 不再与已读水位混淆。并发与可用性取舍
验证
本轮完成的代码层验证:
go test ./pkg/client/... ./pkg/contracts/... -count=1go test ./... -count=1go vetdocker compose config --quietgit diff --check按当前要求,本地没有实际执行九服务原生编译或 Linux full-stack 性能门禁;这些由 PR CI 的
service-artifacts、RL-05 和 PF-09 gate 执行,正文不伪报本地吞吐结果。Review 重点
请重点关注:
message_id/user_seq/seq_id三类标识的职责边界。