SonnetDB 的目标是让物联网 / 工业 / 边缘场景下,一台 SonnetDB 替掉一组开源组件的组合(PostgreSQL + Redis + InfluxDB + MinIO + NATS + Meilisearch + Qdrant + ClickHouse)。本路线图规定如何对这一目标做"可证伪"的对齐验证。
SndbConnection / SndbDocumentClient / SndbMqClient / SndbObjectStorageClient / EF Core provider / HTTP API。竞品也走它们各自的官方 .NET 客户端(Npgsql / MongoDB.Driver / StackExchange.Redis / InfluxDB.Client / Minio / NATS.Client.Core / Meilisearch.Net / Qdrant.Client / ClickHouse.Client);MongoDB Driver 只连接参考 MongoDB 容器。| 对齐维度 | 含义 | 判定方式 |
|---|---|---|
| 能力对齐 (Capability) | 同一个使用场景,两边 API 都能完成 | 同一份 Scenario.yaml 在 BACKEND=sonnetdb 与 BACKEND=composite 都能跑通,且结果在阈值内一致 |
| 可靠性对齐 (Reliability) | 异常注入下两边的恢复语义一致 | 同一份 kill -9 / disk-full / oom / power-loss 注入剧本,重启后两边 committed_count / lost_count 都在合同范围内 |
| 算法准确度对齐 (Accuracy) | 同一组数据,两边算出的统计量与排序在容差内一致 | 数值类容差 ≤ 1e-9 相对误差;分位数 ≤ 0.5%;BM25 / KNN top-K 重合率 ≥ 95% |
不对齐:吞吐绝对值、延迟绝对值、内存占用、进程数。这些写报告,不做 gating。
每个支柱选 1 个主竞品 +(可选)1 个对照品。SonnetDB 走自有连接器,竞品走官方 .NET 客户端。
| 支柱 | 主竞品 | 对照品 | SonnetDB 入口 |
|---|---|---|---|
| 关系型 (Relational) | PostgreSQL 16 | — | SonnetDB.EntityFrameworkCore + SonnetDB.Data |
| 时序 (TSDB) | InfluxDB 2.7 | VictoriaMetrics 1.106 | SonnetDB.Data + Bulk Ingest LP/JSON/Bulk |
| KV / 缓存 | Redis 7 | — | SonnetDB.Caching.EasyCaching / SonnetDB.Caching.Distributed + KvKeyspace |
| 对象桶 (Object) | MinIO | — | SndbObjectStorageClient |
| 消息 (MQ) | NATS JetStream 2.10 | Mosquitto 2.0(功能对照) | SndbMqClient |
| 全文 (FT) | Meilisearch 1.10 | — | SonnetDB 内置全文引擎 + DocumentFullTextIndexStore |
| 向量 (Vector) | Qdrant 1.11 | — | SonnetDB 内置向量引擎 + VectorIndexAdapter |
| 分析 (Analytics) | ClickHouse 24.8 | — | SQL GROUP BY + window functions |
| 文档 (Document,M25) | MongoDB 8.0 | — | SndbDocumentClient / Document API;只验证语义,不做协议兼容 |
tests/SonnetDB.Parity/
├── docker-compose.parity.yml # 13 个服务 + harness
├── docker-compose.parity.override.yml # 用户本地覆盖
├── .env # 端口 / 凭证 / 数据集大小
├── adapters/
│ ├── IDataPlane.cs # 能力契约 + Capability 标志位
│ ├── Scenarios/IScenario.cs # 单场景接口
│ ├── SonnetDb/
│ ├── Postgres/ # Npgsql
│ ├── Redis/ # StackExchange.Redis
│ ├── Influx/ # InfluxDB.Client
│ ├── VictoriaMetrics/ # Prometheus remote_write + PromQL
│ ├── Minio/ # AWS SDK pointed at MinIO
│ ├── Nats/ # NATS.Client.Core
│ ├── Mosquitto/ # MQTTnet
│ ├── Meilisearch/ # Meilisearch.NET
│ ├── Qdrant/ # Qdrant.Client
│ ├── ClickHouse/ # ClickHouse.Client
│ └── Composite/ # 路由器:每支柱选最优竞品
├── scenarios/
│ ├── tsdb/ # ingest_1m, groupby_time, derivative_accuracy ...
│ ├── relational/ # tpcc_lite, fk_cascade, isolation_read_committed
│ ├── kv/ # set_get_scan, ttl_accuracy, incr_concurrency
│ ├── object/ # putget_1gb, multipart_5gb, range_read
│ ├── mq/ # publish_consume_ack, replay_after_restart
│ ├── fulltext/ # bm25_ranking, cjk_tokenize, incremental_index
│ ├── vector/ # ann_recall, filtered_search, upsert_during_query
│ ├── analytics/ # window_avg_accuracy, percentile_p95
│ ├── reliability/ # crash_kill9, disk_full, oom_protection, power_loss
│ └── accuracy/ # 跨支柱算法精度专项
├── runner/
│ ├── ParityRunner.cs # xUnit + Verify.Xunit
│ ├── BackendSelector.cs # 读 BACKEND env
│ ├── ScenarioLoader.cs # 反序列化 yaml
│ ├── Reporting/JsonReporter.cs
│ ├── Reporting/MarkdownReporter.cs
│ └── ResultDiffer.cs # 计算两边差异 + 容差判定
└── reports/ # gitignore
├── sonnetdb/<run-id>/
├── composite/<run-id>/
└── diff/<run-id>.md
public interface IDataPlane : IAsyncDisposable
{
Capability Capabilities { get; }
IRelationalOps Relational { get; }
ITimeSeriesOps Ts { get; }
IKvOps Kv { get; }
IObjectOps Objects { get; }
IMqOps Mq { get; }
IFulltextOps Fulltext { get; }
IVectorOps Vector { get; }
IAnalyticsOps Analytics { get; }
}
[Flags]
public enum Capability : long
{
None = 0,
Relational = 1L << 0,
TimeSeries = 1L << 1,
Kv = 1L << 2,
Object = 1L << 3,
Mq = 1L << 4,
Fulltext = 1L << 5,
Vector = 1L << 6,
Analytics = 1L << 7,
// 细粒度能力标志(每个场景声明依赖)
KvIncr = 1L << 16,
KvCas = 1L << 17,
KvRangeScan = 1L << 18,
ObjectMultipart = 1L << 19,
MqConsumerGroup = 1L << 20,
MqReplayFromOffset= 1L << 21,
SqlSubquery = 1L << 22,
SqlWindowFunction = 1L << 23,
SqlForeignKey = 1L << 24,
AccuracyPercentile= 1L << 25,
HnswFiltered = 1L << 26,
}
每个 IScenario 声明 RequiredCapabilities,runner 看到 backend 不支持时标 SKIPPED 不算 fail,但写入报告 gap_reason 字段——这就把"我们没有的功能"变成结构化数据,而不是红色 CI。
# scenarios/kv/ttl_accuracy.yaml
name: kv_ttl_accuracy
pillar: kv
required_capabilities: [Kv]
description: TTL 到期回收准确度,对照 Redis EXPIRE 行为
parameters:
key_count: 10000
ttl_seconds: 5
tolerance_seconds: 0.5
steps:
- kv.set_with_ttl(key, value, ttl) # 10000 个键
- sleep(ttl + tolerance)
- kv.get(key) # 期望全部 null
assertions:
- expired_count == key_count
- leaked_count == 0
- early_expired_count <= 1 # 容忍 0.01% 时钟误差
| 类型 | 阈值 | 来源 |
|---|---|---|
| 标量数值(sum/avg/min/max) | 相对误差 ≤ 1e-9(IEEE 754 双精度) | 直接比较 |
| 分位数(p50/p95/p99) | 绝对误差 ≤ 0.5% × 数据集 spread | t-digest 误差合同 |
| HyperLogLog distinct_count | 相对误差 ≤ 2% | HLL 标准误差 |
| 时间窗口聚合 | 标量阈值 + 桶边界一致 | 桶起止时间必须完全相等 |
| 排序结果(KNN top-K) | top-K 集合 Jaccard ≥ 0.95 | 允许少量浮点抖动导致顺序差异 |
| 全文 BM25 排序 | top-10 重合率 ≥ 0.8(同义分词差异容忍) | 不强求排序完全一致 |
| 计数(消息 / 行 / 对象) | 必须精确相等 | 任何差异都是 bug |
| 可靠性 commit/lost | lost_count <= advertised_loss_window |
由各 backend 各自声明 RPO |
任何 SonnetDB 比竞品差超过 100× 的延迟必须升 P1 issue,单项不阻塞 CI。比竞品好的不写在 gating 里,写在 README 营销表里。
详见 tests/SonnetDB.Parity/docker-compose.parity.yml(M20 #127 落地)。要点:
parity-net,避免与本机已有 service 冲突。light(仅 sonnetdb + pg + redis + minio + nats + harness,~1.5GB RAM)、full(全 12 个服务,~6-8GB RAM)。docker compose down -v 一键清空。harness-reports volume。.github/workflows/parity.yml,每日凌晨 02:00 UTC + manual dispatch。{light, full} × {ubuntu-latest}(macOS / Windows runner 暂不跑,containers 不友好)。tests/SonnetDB.Parity/reports/<run-id>.md 并贴 PR 评论,不阻塞 merge。summary.json / summary.md;gateFailures 使用稳定 gap_reason 区分 restore_failed、build_failed、stack_start_failed、parity_test_failed、reliability_test_failed 和 parity_report_missing。| 风险 | 处理 |
|---|---|
| Docker 在 Windows / macOS CI 不稳 | 仅 ubuntu-latest 跑 parity,开发者本地用 Docker Desktop |
| 竞品镜像版本漂移 | docker-compose.parity.yml pin 到具体 tag(不用 :latest),手动升级 |
| 竞品官方客户端 NuGet 版本冲突 | adapters 各自 csproj,不互相引用,不进 main solution SonnetDB.slnx |
| 测试时长爆炸 | 默认数据集小(10k 行 / 100MB 对象),long-soak 用 [Trait("Profile","longsoak")] 仅 nightly 跑 |
| 第三方客户端 IL trim 不安全 | parity 项目设 <IsAotCompatible>false</IsAotCompatible>,不污染主仓 AOT 流水线 |
| Mosquitto / NATS .NET 客户端依赖 native | 全在 docker 内运行,本机不安装 |
详见 ROADMAP.md Milestone 20,PR 范围 #127 ~ #136。
简略路径:
里程碑跑下来会顺带把以下事情做掉(不另立 PR,作为各 PR 的副产品):
INCR / DECR / CompareAndSet / EXPIRE / PERSIST / TTL(#130 强需求)RecordTypeTombstone 段滚动(#132 强需求)+ FlushOnPublish=true 默认值(不延迟到下一里程碑)ListObjectsV2 ContinuationToken 分页(#131 强需求)tests/SonnetDB.CrashTests/ 真子进程 + Process.Kill(true) 注杀(#135)docker compose --profile full up 在干净 ubuntu-latest 5 分钟内健康。tests/SonnetDB.Parity/reports/sample-run.md 产出可读样例报告(含 24+ 场景表格)。public sealed class KvTtlAccuracyScenario : IScenario
{
public string Name => "kv_ttl_accuracy";
public Capability Required => Capability.Kv;
public async Task<ScenarioResult> RunAsync(IDataPlane plane, ScenarioContext ctx)
{
const int keyCount = 10_000;
var ttl = TimeSpan.FromSeconds(5);
// 阶段 1:批量 SET TTL
for (int i = 0; i < keyCount; i++)
await plane.Kv.SetAsync($"k{i}", BitConverter.GetBytes(i), ttl);
// 阶段 2:等过期 + 容差
await Task.Delay(ttl + TimeSpan.FromMilliseconds(500), ctx.Cancellation);
// 阶段 3:验证全部回收
int leaked = 0, earlyExpired = 0;
for (int i = 0; i < keyCount; i++)
if ((await plane.Kv.GetAsync($"k{i}")) is not null) leaked++;
return new ScenarioResult
{
Pass = leaked == 0 && earlyExpired <= keyCount * 0.0001,
Metrics = { ["leaked"] = leaked, ["early_expired"] = earlyExpired },
};
}
}
同一份代码跑 SonnetDB 与 Redis:
SonnetDbKvAdapter 调用 KvKeyspace.Put(key, value, expiresAtUtc)。RedisKvAdapter 调用 IDatabase.StringSetAsync(key, value, ttl)。输出报告:
| Scenario | SonnetDB | Redis | Diff |
|---|---|---|---|
| kv_ttl_accuracy | leaked=0 early=0 | leaked=0 early=2 | ✅ within tolerance |