Skip to content
berkaroadPublic

About

事件存储服务,用于存储基于eventsourcing的领域事件。支持节点高可用+数据多副本。

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

175 Commits

Folders and files

Repository files navigation

PushupES

面向 CQRS 框架的领域事件流存储服务:只追加(append-only)、高可用、高性能。 Go 实现,单二进制、无外部依赖(不需要 ZooKeeper/etcd),内置 React 管理控制台。

分层原则:Raft 只复制元数据(槽位分配表、成员、节点地址),事件数据走 leader→follower 专用拉取协议(ISR + 高水位语义),两条链路互不影响。

客户端 ──gRPC(EventService)──▶ 槽 leader ──append-only WAL──▶ 本地落盘
                                  │
                     follower 长轮询拉取(PeerService.MFetch,与 Raft 同端口)
                                  │
Raft(自研简化 Raft + 自研分段 WAL,仅元数据)──▶ 槽位分配表 / epoch / 集群成员

集群拓扑

以 3 个控制节点 + 2 个数据节点 为例(medium 副本策略:每槽 2 份, 两个数据节点互为对方的副本):

客户端(CQRS 框架)
  │ gRPC EventService(client 面 85xx):读写都直连槽 leader
  │ 收 MOVED(1003)/ASK(1004) → 刷新本地 slot→node 路由缓存后重试
  ▼
控制面 · 3 个控制节点(role=voter)—— Raft 法定数 2/3
  ctrl-1 ◀──Raft 日志复制──▶ ctrl-2 ◀──Raft 日志复制──▶ ctrl-3
  只复制元数据:槽位分配表 slot→{leader, replicas[], epoch}、集群成员、副本策略
  voter 不承载事件数据;controller 在控制面上:每 1s 探活,
  某节点连续 3 次失败 → 从其 ISR 副本重选该节点名下所有槽的 leader(epoch+1)
  │ WatchLayout:把槽位分配表分发到各数据节点(peer 面 83xx)
  │ 探活 / 新节点报名(peer 面 83xx)
  ▼
数据面 · 2 个数据节点(role=learner)—— 承载槽副本:append-only WAL + 派生索引
  data-1:leader 槽 0–2047 ┃ 副本 槽 2048–4095
  data-2:leader 槽 2048–4095 ┃ 副本 槽 0–2047
  data-1 ◀──MFetch 长轮询拉取(peer 面 83xx,与 Raft 同端口族)──▶ data-2
  follower 按 leader 分组批量拉增量,应答是裸 WAL 字节、原样落盘,
  批量回报 LEO,leader 推进 HW = ISR 内最小 LEO;
  写入等到高水位覆盖该条记录才回 success(单节点宕机不丢已确认写入)

三条链路各走各的面:共识与数据复制都走 peer 面,事件读写只走 client 面, 运维与观察(含管理控制台)走 admin 面(80xx)。

功能特性

数据模型与写入协议

  • 事件记录 EventRecord:aggregate_id + version(聚合内连续版本号,从 1 严格 +1 递增)+ unix_time + command_id(幂等键)+ events[](type + 原始 bytes body,端到端裸传,无 JSON/base64 包装层)。
  • 幂等:command_id 命中槽内索引 → 返回 exists + 已存储记录本体。
  • 乐观锁:版本必须 +1,冲突返回 fail + 错误 ID 1001 + current_version, 客户端可据此校准后重试。
  • 两套编号各司其职:version 是业务版本坐标,seq(槽内单调序列号)是 复制/迁移的物理偏移坐标。

存储

  • 固定 4096 个哈希槽(mix64(FNV-1a(aggregate_id)) % 4096)。槽数不可配置, 4096 = 2^12 按「最多 1024 个数据节点、每节点至少 4 槽」估算;数据节点数取 偶数,2 的幂次节点数(2~1024)都整除 4096、每节点槽主数完全相同,其余偶数 按环布局摊到相差不超过 1(推导见 DESIGN.md §7.2)。
  • 每槽一条 append-only WAL,默认按 256MiB 分段(可配 64MiB~2GiB,须为 64MiB 的整数倍)。段文件名即 baseSeq,整段可独立滚动、拷贝、删除,因此槽级 热迁移走复制协议按 seq 追平(见「槽位热迁移」)。
  • 每段附带追加式的派生索引文件(.idx/.agx/.spx/.cidx/.aidx), 支撑命令幂等查重(布隆过滤器 + 磁盘二分)与聚合按版本直读(靠段内序数与 字节偏移,一次 pread 直达)。索引全部是派生数据:缺失、损坏或滞后时一律 退化为全量帧头扫描,正确性不依赖索引。
  • 刷盘策略可配(默认每 1000 条或每 5s 组提交 fsync);写入确认等的是 ISR 高水位,不是本地 fsync。
  • 崩溃恢复:启动时并行打开各槽,一次窗口遍历同时完成「找到尾部完整记录的 边界(丢弃撕裂的尾巴)+ 重建索引」,只解析帧头、不物化事件体。

高可用与复制

  • 控制面:自研简化 Raft(internal/raft,日志与 term/vote 存于自研分段 WAL internal/raft/wal.go)复制槽位分配表 (slot → {leader, replicas[], epoch, state})与成员;成员集合静态(由 -peers 启动时写入);路由表快照为二进制编码(几 KB 级)。
  • 数据面:follower 把要跟随的槽按 leader 分组,每个 leader 维持一条常驻的 MFetch 长轮询会话(gRPC/HTTP2 多路复用,一次请求合并多个槽,payload 是 裸 WAL 字节;有数据立即应答,唤醒后合并窗口内的全部增量一次带走)。 follower 收到帧后原样落盘(不重新编解码),批量回报 LEO,leader 据此推进 高水位 HW = ISR 内最小 LEO。
  • 写入成功的条件:leader 必须等到 ISR 高水位覆盖该条记录(所有同步副本都 已把它写进自己的 WAL)才回 success——回 success 的写入不会因单节点宕机 而丢失。副本全部掉出 ISR 的极端情况下,高水位跟随 leader 推进以保证可用性 (承诺只对同步副本成立);等待超过 10s 则返回失败,记录此时已在 leader 的 WAL 中,客户端用同一 command_id 重试命中 exists 即可确认。
  • 故障切换:controller 每 1s 探活,某节点连续 3 次失败即判定失联 → 从其 ISR 副本中为该节点名下所有槽重选 leader(epoch+1,经 Raft 提交),客户端收 MOVED 重定向;原 leader 恢复后自动回切(election_mode=leader)。
  • 读语义只暴露 seq ≤ HW 的记录(防脏读回滚);非本地槽的读由服务端就地 答 1003(MOVED)+ 槽 + leader 的 client 地址,客户端库跟随重定向重试 ——与写同一套机制,服务端从不中继读流量。

槽位热迁移(副本构建三阶段,不停写)

入场 → 追平 → 提交,对一切布局变更同一条链路(因子提升、节点加入/重加入、 leader 回切、分叉副本修复):

  1. 入场(Raft 命令,便宜):目标加入该槽副本集,不动 leader、不搬数据、不冻结写入;
  2. 追平(只走复制 fetch):目标按 seq 自拉,每槽每轮 ≤4MiB、整轮 ≤32MiB。 追平中的席位不计入水位 min、也不被写入等待,所以写入不受影响;环席位 (节点加入/重加入、成员离开后环重新分摊)先于因子补的席位拿到轮预算,ack 关键尾巴两者都不让路;
  3. 提交(有界、串行,每轮一次):置 migrating_out(读请求 ASK 改道)→ 写栅栏(冻结源端写入、只推冻结尾巴,O(尾巴))→ OpLeaderMove 切主 → 回收多余席位(best effort)。

提交前置判据:两端目录摘要一致且目标不落后。同一 LEO 但摘要不同(结构性短缺) ⇒ 丢弃该副本让它从空重新拉,不原地修(fetch 永远修不好这种副本)。 前源节点在自己失去该槽成员资格后按 -drop-after(默认 30s)本地删除副本。

  • 迁移到副本集以外的目标:一次 API 调用内自动完成「加入副本集 → 追平 → 移主 → 回收前源」三步。
  • 客户端路由:本地缓存 slot → node,收到 MOVED(1003)/ASK(1004) 即刷新; 幂等规则保证转发期间的写入不产生重复。

节点级流控(令牌桶)

  • 流控是节点级旋钮:POST /admin/flow-control {"tokens":N,"period":"1s"} 打到哪个节点就只对该节点生效,且预算也在节点层——该节点作为 leader 的所有槽 共享一个令牌桶,任何 leader 槽的一次事件流写入从桶里取一个令牌;控制与设置的 粒度一致。副本写入走复制落地路径,不消耗令牌。
  • 令牌模型是「取走即登记归还时间点」:一次取令牌记下取走时刻 + period 为它的 归还点,每次取之前把所有已到期的时间点一次性归还(最小堆扫堆头)。桶空即写入 报 fail/1006(记录不落 WAL、不等高水位),稳态写入上限 = tokens / period。 业务失败(1001 版本冲突 / 1002 参数非法)与 WAL 写错误立即退还令牌;exists (幂等重试)与高水位超时(1005)不退——按写入请求计费,重放不会免费穿透流控。
  • 配置不进集群共识、不落盘,进程重启回到不限流;DELETE /admin/flow-control 或 tokens<=0 / period<=0 都是不限流。它是运维应急面,与副本策略(集群级、 持久化)分属两层。机制细节(换代语义、批内逐条计费、栅栏顺序)见 DESIGN.md §6.2。
  • 客户端库把 1006 吸收为回压:pkg/client 的 BatchAppend 从不把 1006 交给 调用方——被拒的记录进入回退环,持有该节点的闸门(每节点一个),每 FlowBackoff(默认 100ms)只重发被拒子集,直到一轮应答干净才放闸;闸门关闭 期间,发往该节点的后续写请求全部在闸门外排队,其他节点与读路径不受影响。等待 发生在尝试预算之外(限流是等待,不是重试),重发安全靠 command_id 幂等兜底。 观测面 Client.FlowPauses() 数被拒的重发轮次。
  • 控制台在集群页的节点卡片上呈现:橙色「流控」tag = 该节点配置了流控;检测到 该节点拒绝写入时 tag 持续闪烁 3 秒(每轮新检测都续期,末次触发 3s 后熄灭)。 卡片上的「流控」按钮打开设置 modal,直发该节点。

内存与容量设计

内存开销主体由记录条数驱动。实测常数:命令布隆过滤器 1.25 B/条、未封段 seq 8 B/条、段稀疏索引 16 B/每 64 KiB 数据。配合「封段后把 seq 移出内存」与 派生索引文件,31 GiB / 3250 万条数据的节点活堆约 110 MiB(其中稀疏索引约 8 MiB,按实测常数推算),据此可规划 1 TiB/节点、16 GiB 内存的部署(选型判据 见 DESIGN.md §7.1/§7.2)。

管理控制台(frontend/)

React 18 + antd 5 + Vite,提供集群页(含运行中添加 Raft 节点、节点卡片的流控 tag 与设置入口、每节点底部「写入速度(数量/大小)」双轴折线图——左轴 msg/s、 右轴 KiB/s,2s 采样、最近 2 分钟,数据与集群写入速度卡同一次轮询)与槽位管理:槽位表(状态/副本/总字节、 迁移弹窗、待清理副本置灰)、槽详情抽屉(HW/LastSeq/ISR + 写入速率折线图, 2s 轮询)、事件流列表(只读内存索引,不触发 WAL 扫描)。支持 light/dark 主题。

部署与启动

构建

要求 Go 1.26+:

CGO_ENABLED=0 go build -o bin/pushupes ./cmd/pushupes   # 纯 Go 静态链接,不依赖 glibc
go build ./... && go vet ./... && go test ./... -count=1   # 全量门禁

前端(可选,构建产物 frontend/dist 供静态托管):

cd frontend && npm install && npm run build

一键起 3 节点集群(推荐)

REPLICAS=5 VOTERS=3 scripts/cluster.sh start   # 一条命令建整簇:3 控制节点 + 2 数据节点
scripts/cluster.sh restart   # 裸跑即可:节点数、每节点角色、端口都读自 $RUN_DIR/cluster.env
scripts/cluster.sh status    # 各节点 Raft 角色、leader 槽数、迁移中槽数
scripts/cluster.sh smoke     # 端到端冒烟:MOVED → v1/v2 写入 → 幂等 exists
                             #           → 版本冲突 fail/1001 → 回读
scripts/cluster.sh slotcheck # 副本一致性体检:逐槽比较 leader 与各副本的聚合摘要,
                             #   报出「LEO 相同但目录偏短」的发散副本(有则退出码 1)
                             #   可加 -quiet / -json / -list=N 透传给 bin/slotcheck
scripts/cluster.sh logs 1    # 跟踪 node-1 日志
scripts/cluster.sh restart   # 重启(保留数据,验证 WAL/Raft 崩溃恢复)
scripts/cluster.sh stop      # 停止(只杀 pid 文件记录的进程)
scripts/cluster.sh clean     # stop 并删除运行目录(含数据,慎用)

每个节点占用三个端口,按节点序号依次递增(node-i = BASE + i − 1);数据、日志、 pid 分别放在 $RUN_DIR/node-i/(默认在仓库根的 .cluster/)。可用环境变量覆盖: REPLICAS(节点数)、VOTERS(控制面节点数:node-1..VOTERS=voter,其余=learner; 也可用 ROLE_<n>=voter|learner 逐个点名)、HOST、ADMIN_BASE、PEER_BASE、 CLIENT_BASE、SEGMENT_BYTES、RUN_DIR、READY_TIMEOUT、BUILD。start/join 会把生效形态(节点数、每节点角色、端口)写进 $RUN_DIR/cluster.env,之后裸跑 restart/stop/status/smoke 都按它执行,命令行显式传的变量仍优先;运行目录里 只有节点目录而没有 cluster.env(老目录)时脚本按最大 node-N 推断节点数并提示补传 角色。每槽副本数由副本策略分档(PUSHUPES_REPLICA_POLICY=low/medium/high,默认 medium:1/2/每个数据节点一份全量):该值只为新集群种初始值,策略存放在 Raft 复制的 槽表里,之后改档位走 POST /admin/cluster/replica-policy(控制台集群页的「副本策略」 卡片即可操作)。扩节点两种做法等价:REPLICAS=<新总数> cluster.sh start(集群已存在 时,缺的节点自动报名加入)或逐个 cluster.sh join N。

集群配置只由一个节点写下(-peers 里 id 最小的那个,或用 -bootstrap 指定的 那个);其余配置了 -peers 的节点一律以「种子」身份启动、向这些成员报名加入,自己 不写配置。所以「把新节点用更大的 -peers 列表拉起来」就是扩容,它不会和现有集群 各写一份配置而分裂成多个各自能提交的 raft 组。

手工启动单个节点

# 最小部署 = 1 个控制节点(voter)+ 1 个数据节点(learner)。-role 必填,
# 只能是这两个值:voter 投票但不承载槽副本,learner 承载副本但不投票。
# -peers 只列控制面(voter)节点:它既是 voter 的初始配置种子,也是 learner
# 连控制面的投递点(注册与布局分发都走 peer 端口)。
# learner 要先进目录(控制面 POST /admin/cluster/nodes {"role":"learner"})
# 再启动进程;准入与拉起顺序见 scripts/cluster.sh。
./bin/pushupes -node node-1 -role voter \
  -peer 127.0.0.1:8391 -admin 127.0.0.1:8091 -client 127.0.0.1:8591 \
  -data ./node-1 \
  -peers 'node-1=127.0.0.1:8391'

./bin/pushupes -node node-2 -role learner \
  -peer 127.0.0.1:8392 -admin 127.0.0.1:8092 -client 127.0.0.1:8592 \
  -data ./node-2 \
  -peers 'node-1=127.0.0.1:8391'

# 再加一个控制成员:它不在配置里,按 -peers 里的成员报名进 raft(无需 -join
# 也能轮转报名;-join 只用于点名某一个成员)。
./bin/pushupes -node node-3 -role voter \
  -peer 127.0.0.1:8393 -admin 127.0.0.1:8093 -client 127.0.0.1:8593 \
  -data ./node-3 \
  -peers 'node-1=127.0.0.1:8391' \
  -join 127.0.0.1:8391

地址规范:地址统一带 scheme 存储,未写协议时默认补 http://;-peers 也接受裸 host:peerport(此时地址兼作节点 id)。

启动参数

flag env 默认 说明
-node PUSHUPES_NODE node-1 节点 id
-role PUSHUPES_ROLE 无(必填) 节点角色,只能是两者之一:voter=控制面(参与选举、不承载槽副本)、learner=数据面(不投票,从控制面接收布局分发)。最小部署 = 1 个 voter + 1 个 learner;缺失或非法值启动即报错。角色由节点自报进复制目录,布局池只认 learner 成员。learner 的槽表由控制面经 WatchLayout 流分发(DESIGN §12.4),它不建共识组、不提交、也不报名成为投票者
-peer PUSHUPES_PEER http://127.0.0.1:8391 peer 面:Raft + PeerService gRPC(全部节点间流量,运维只需配这个端口)
-admin PUSHUPES_ADMIN http://127.0.0.1:8091 admin 面:HTTP 管理 API + pprof
-client PUSHUPES_CLIENT http://127.0.0.1:8591 client 面:gRPC 事件读写唯一入口
-data PUSHUPES_DATA ./data 数据目录
-peers PUSHUPES_PEERS 空 集群种子 id=host:peerport,...
-join PUSHUPES_JOIN 空 启动时向指定成员(host:peerport)报名。可选:不写时,非 -peers 首位的节点也会按配置成员轮转报名,-join 用于指定其中一个
-adopt-interval PUSHUPES_ADOPT_INTERVAL 3s 尚未成为集群成员的节点每隔多久重试一次报名
-bootstrap — false 由本节点写下集群的初始配置。默认由 -peers 里 id 最小的节点写;其余配置了 -peers 的节点以种子身份启动、向这些成员报名加入,自己不写配置
-replica-policy PUSHUPES_REPLICA_POLICY medium 副本策略档位:low=每槽 1 份、medium=2 份(默认)、high=每个数据节点各存一份全量(因子=数据节点数)。只用于新集群的初始值:策略存放在 Raft 复制的槽表里,之后只能经 POST /admin/cluster/replica-policy 修改,重启不会重刷;任何一档的因子按数据节点数(布局池 HostingIDs())钳制(1 个数据节点时都是 1 份)。GET /admin/cluster/status 的 replica_policy / replica_factor 读回当前值;非法值启动即报错
-flush-messages PUSHUPES_FLUSH_MESSAGES 1000 每 N 条 fsync(0 关闭)
-flush-interval PUSHUPES_FLUSH_INTERVAL 5s 每周期 fsync(0 关闭)
-segment-bytes PUSHUPES_SEGMENT_BYTES 256MiB WAL 段滚动大小:64MiB 整数倍,≤2GiB;可写字节数或带单位(256MiB/1GiB),env 同样接受带单位形式
-fetch-settle PUSHUPES_FETCH_SETTLE 200µs fetch 轮被数据唤醒后的合并窗口:一轮覆盖整批写入涉及的多个槽;每次 ack 都要付它一次,换的是轮次与上报次数;0=首个槽有数据就答(轮次更多)
-drop-after PUSHUPES_DROP_AFTER 30s 迁移后前源副本保留期;0=默认,负数启动即报错,不可关闭
-rebalance-interval PUSHUPES_REBALANCE_INTERVAL 2s controller 每隔多久检查一次 leader 布局并执行回切(节点宕机恢复后把槽 leader 迁回环上预期节点);每轮只提交一次(提交严格串行,任一时刻只有一个槽在交接栅栏上),所以收敛时间由这个间隔决定;0=关闭,负数启动即报错
-grpc-max-msg-size PUSHUPES_GRPC_MAX_MSG_SIZE 4MiB client 面 gRPC 消息上限(recv/send 同值);BatchAppend 单批要装进它,可写 16MiB 等带单位形式(env 同样接受);≤0 启动即报错
-batch-slot-parallelism PUSHUPES_BATCH_SLOT_PARALLELISM 100 BatchAppend 一批最多同时执行几个槽(异槽并行、同槽串行;铺满 4096 槽的宽批也不会瞬间压上等量并发 WAL 写者);≤0 启动即报错
-raft-heartbeat-timeout PUSHUPES_RAFT_HEARTBEAT_TIMEOUT 100ms 共识心跳间隔。leader 的心跳没按时到达就会被投票换掉,所以宿主机可能拖住共识事件循环时要与 -raft-election-timeout 一起放大
-raft-election-timeout PUSHUPES_RAFT_ELECTION_TIMEOUT 500ms follower 多久收不到心跳就发起选举。必须 ≥ 2× 心跳,否则一次迟到的心跳就会掀掉 leader(启动即报错);与心跳是一对比例,一起放大而不是只收窄其中一个
-raft-flush-interval PUSHUPES_RAFT_FLUSH_INTERVAL 200µs 共识 WAL 组提交窗口:窗口内到达的追加共用一次 fsync
-raft-segment-bytes PUSHUPES_RAFT_SEGMENT_BYTES 64MiB 共识 WAL 段滚动大小;可写字节数或带单位(64MiB),env 同样接受

客户端接入

事件读写只走 gRPC client 面(proto 契约 proto/pushupes/v1/events.proto):

  • EventService/BatchAppend:事件写入的唯一入口,单次与批量写入都走它—— 一条 AppendRequest 就是单条写,多条就是批量。服务端逐条校验幂等与版本 (见「数据模型与写入协议」),按请求同序返回逐条结果(回显 aggregate_id),槽不在本节点时以响应字段返回重定向:err_id=1003/1004 (MOVED/ASK)+ node(槽 leader 的 client 地址),客户端据此重连。批内 aggregate_id 必须各不相同,重复的那组整组 fail/1002 且不执行。服务端 同槽串行、异槽并行(并发上限 -batch-slot-parallelism,默认 100)。单批 必须装进 client 面 gRPC 消息上限(-grpc-max-msg-size,默认 4MiB)。
  • EventService/ReadStream:按聚合读取事件流(≤HW 语义)。
  • EventService/ReadTails:一次查询多个聚合的最新版本(resume 扫描用)。 服务端逐条应答:本地可服务的槽给版本,其余给该条自己的 1003 + node; pkg/client 按本地路由表分组扇出、逐条修补重发。
  • EventService/ReadVersionByTime:按聚合 + 时间取「时间点及之前」的最大 版本锚点,再用 ReadStream 回放。
  • EventService/ReadByCommand:按 command_id 查询已写入的记录。 以上读接口与写一样走 MOVED 重定向(应答带 err_id/slot/node), 非本地槽不再由服务端代理转发;pkg/client 对调用方透明地跟随。

错误 ID:1001 版本冲突(附 current_version)、1002 参数非法、 1003 MOVED、1004 ASK、1005 本节点非 controller(admin 写命令专用, 响应附 controller 与 controller_admin_addr)、1006 本节点流控拒绝 (见「节点级流控」;经 pkg/client 写入时该码被库吸收为回压,调用方看不到它)、 1007 本地副本来源未证实(owner 围栏拦下该槽:leader 拒写、本地读答空,直到 运维经 /admin/slots/pending/adopt 采纳、或删除槽目录,见 DESIGN §3.1)。世代号 是复制状态里的集群级值(GET /admin/cluster/status 的 topology,每个节点报同一个), 只有真正重建共识状态才会换世代;普通重启、raft 换主都不影响副本来源的证明。

admin 面 HTTP(仅管理):GET /admin/cluster/status、GET /admin/writes、 GET /admin/slots/{slot}/describe、GET /admin/slots/{slot}/streams、 POST /admin/slots/{slot}/migrate、POST /admin/slots/{slot}/remove-replica、 POST /admin/cluster/plan、POST /admin/cluster/replica-policy、 POST/GET/DELETE /admin/flow-control(节点本地:收到请求的节点就是被设置/查询/ 恢复的节点)、GET /admin/slots/pending、POST /admin/slots/pending/adopt (owner 围栏:查看被拦副本并采纳「属于当前世代」,同样节点本地;丢弃走运维 手工删除槽目录,见 DESIGN §3.1)、GET /healthz、/debug/pprof/。 控制器专属写命令(migrate/remove-replica/plan/replica-policy)必须直接发到 Raft leader 的 admin 地址,follower 一律拒绝(425 + 1005),不做转发。

仓库自带工具:

# 冒烟探针(MOVED 跟随 + 幂等 + 版本冲突 + 回读断言)
go run ./cmd/grpccheck -addrs http://127.0.0.1:8591,http://127.0.0.1:8592,http://127.0.0.1:8593

# 写压测(-nodes 传 client 面 gRPC 地址,路由表经 PrefetchRoutes 解析并按
# -route-refresh 周期刷新,MOVED/ASK 即时修补本地行;槽数取自应答长度;
# -batch N 为每批条数,N=0 即一条 BatchAppend 只带一条记录;-duration 是硬
# 窗口:到点时在飞调用不再排干限流积压,未答记录单记 window-dropped,
# 不计入吞吐与 fail)
go run ./cmd/bench -nodes http://127.0.0.1:8591 -conns 8 -size 1024 -duration 30s
go run ./cmd/bench -nodes http://127.0.0.1:8591,http://127.0.0.1:8592,http://127.0.0.1:8593 -conns 8 -size 1024 -batch 64 -duration 30s

# BatchAppend 冒烟(经 pkg/client 客户端库:success/exists/1001/批内重复拒绝/回读断言)
go run ./cmd/batchsmoke -nodes http://127.0.0.1:8591,http://127.0.0.1:8592,http://127.0.0.1:8593

# 流控回压冒烟(经 pkg/client:给 -admin 节点设 N tokens/period,并发写断言
# 调用方零 1006 零失败——节点拒绝全部被库的回压吸收)
go run ./cmd/flowcheck -seed http://127.0.0.1:8591 -admin 127.0.0.1:8091 -tokens 2 -period 1s

# 单槽灌数据(容量/恢复调试)
go run ./cmd/seed -slot 7 -mib 512

# 副本一致性体检:逐槽比较 leader 与各副本的聚合摘要(读 /admin/slots/N/describe)。
# 报出「LEO 相同但目录偏短」的发散副本 —— 这类副本在环上不可见(leader 回切器只看
# 偏离环的槽),但读不到自己尾部,且会让该槽永远回切不回去。有发散时退出码 1。
go run ./cmd/slotcheck -admins http://127.0.0.1:8091,http://127.0.0.1:8092,http://127.0.0.1:8093

修改 proto 后重新生成:./scripts/genproto.sh(buf 工具链在 proto/ 下随仓库 分发;events.proto 生成到 pkg/grpcapi/,peer.proto 生成到 internal/grpcapi/,勿手改)。

性能数据

硬件环境

  • CPU:4 核 2.4GHz
  • 内存:8 GiB
  • 存储:NVMe SSD,顺序读写约 3 GB/s

以下均为实测(非估算)。测试环境:3 节点、RF=2、Docker 沙箱 4 核 CPU 配额,bench 客户端与服务端共享配额——绝对吞吐受容器配额封顶,不代表宿主机的上限;比较改动收益时以「每条消息 CPU 成本」与同负载 A/B 为准。以下写入数字都是「高水位确认」(success 时每个 ISR 副本均已落盘,见「高可用与复制」)口径下的测量。

写入吞吐

组合矩阵(节点数 × 连接数 × 批大小)实测。参数:1 KiB 事件体、aggs=1000、每组 30 秒;每组冷启动独立集群(单节点 RF=1、3 节点 RF=2),测完销毁数据重测。批量行的延迟按批往返计(整批一个样本),批大小=1 即一条 BatchAppend 携带一条记录(一次 RPC 一条记录)。12 组均 fail=0、exists=0、redirects=0。

节点 连接数 批大小 吞吐量 (msg/s) p50 p99
1 1 1 4711 0.18 ms 1.00 ms
1 1 10 30795 0.27 ms 1.02 ms
1 1 100 70285 1.19 ms 2.98 ms
1 4 1 12288 0.28 ms 1.14 ms
1 4 10 60625 0.52 ms 1.99 ms
1 4 100 101635 2.80 ms 9.20 ms
3 1 1 636 1.62 ms 4.41 ms
3 1 10 2130 4.42 ms 20.34 ms
3 1 100 13730 6.78 ms 12.56 ms
3 4 1 3172 1.09 ms 4.53 ms
3 4 10 9197 4.08 ms 9.05 ms
3 4 100 27645 11.47 ms 22.63 ms

About

事件存储服务,用于存储基于eventsourcing的领域事件。支持节点高可用+数据多副本。

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages