Opik 后端模块(opik-backend)开发指南:模块结构、构建测试与 traces 拓扑感知迁移实战
【免费下载链接】comet-llmDebug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards.项目地址: https://gitcode.com/GitHub_Trending/co/comet-llm
apps/opik-backend是 Opik 全栈仓库中的 Java 后端服务模块,负责 LLM 可观测性的核心数据面:trace/span 摄取、数据集与实验、评估与告警等业务能力。本文以该模块的开发者指南(apps/opik-backend/AGENTS.md)为主体,系统讲解其目录组织、构建与测试命令、分层编码规范、测试约定与提交流程,并深度展开其中最具工程分量的部分——tracesClickHouse 表在切流窗口内的拓扑感知 DDL 迁移模式,辅以源码与测试佐证,帮助你快速上手并为该模块提交高质量变更。
模块定位与文档继承关系
apps/opik-backend/AGENTS.md是一份模块专属的工程指南,它遵循 monorepo 的文档分层约定:共享的工作流、PR 与安全策略统一收口在仓库根目录的 AGENTS.md,模块级指南只保留与本模块强相关的增量信息。因此任何贡献者都应先读根级指南,再结合本文件落地到 Java 后端的上下文。
仓库内与之协同的模块包括:
apps/opik-frontend:React/TypeScript 前端apps/opik-documentation:文档站点与生成的 API 文档sdks/*:Python、TypeScript、opik_optimizer 等 SDKtests_end_to_end、tests_load:跨栈 E2E 与性能测试套件deployment、scripts:部署编排与开发工具脚本
项目结构与模块组织
后端代码按“源码—测试—资源—迁移”四大区域组织:
- 源码:
apps/opik-backend/src/main/java - 测试:
apps/opik-backend/src/test/java - 资源:
apps/opik-backend/src/main/resources - 数据迁移与运行时脚本:
apps/opik-backend/data-migrations、apps/opik-backend/scripts,以及根目录的scripts/
从源码结构看,业务代码根包为com.comet.opik,内部按职责划分:
api:资源层的数据模型与请求/响应对象(Dataset、Trace、Alert、AnnotationQueue、PromptVersion等)domain:领域服务与 DAO(如TraceDAO)infrastructure:基础设施(配置、Guice 模块、DB 接入等)utils:通用工具
资源目录apps/opik-backend/src/main/resources下包含liquibase/(db-app-analytics与db-app-state两套 changelog)、llm-models-default.yaml、model_prices_and_context_window.json(生成产物,勿直接编辑)等关键配置。
一个特别值得注意的约束出现在文档的显眼位置:任何修改tracesClickHouse 表的迁移,都必须先阅读 docs/traces-schema-ddl.md 并遵循拓扑感知 DDL 模式。原因是traces正处于向分区化继任表的迁移过程中,一条迁移必须同时正确作用于切流前后两种物理布局,而两种布局下犯错的方式在迁移执行时都不会报错。这一点在“traces 拓扑感知迁移”章节详述。
构建、测试与开发命令
模块指南给出了完整的环境验证、本地开发与构建命令链,可在仓库根目录执行:
| 命令 | 用途 |
|---|---|
./opik.sh --build | 以 Docker 启动完整技术栈(全量验证环境) |
./opik.sh --verify/./opik.sh --stop | 健康检查 / 关闭本地服务 |
scripts/dev-runner.sh --be-only-restart | 本地 Java 后端进程模式,含热更新工作流(停止→构建→启动基础设施与后端进程) |
scripts/dev-runner.sh --be-only-start | 不重新构建,快速启动后端进程 |
scripts/dev-runner.sh --build-be | 仅重建后端依赖与产物 |
scripts/dev-runner.sh --lint-be | 运行后端 lint/格式检查 |
scripts/dev-runner.sh --migrate | 执行数据库迁移(MySQL + ClickHouse) |
cd apps/opik-backend && mvn test | 单元/集成测试(含 Testcontainers 支撑的套件) |
cd apps/opik-backend && mvn spotless:apply | 应用 Java 格式化 |
其中dev-runner.sh的各开关在 scripts/dev-runner.sh 中均有对应分支实现,例如--migrate会在本地执行 MySQL 与 ClickHouse 两套 Liquibase 迁移,--be-only-restart承担日常“改代码→重启→看效果”的循环。根级 AGENTS.md 还补充了全栈默认启动./opik.sh、前端构建npm run build、Python SDK 与 E2E 测试等命令,跨栈改动时应按需使用。
编码风格与分层架构
后端遵循清晰的分层设计:resource → service → DAO,并配套以下规范:
- 依赖注入:使用构造器注入(
@Inject),复用现有 Guice 模块,保持层边界不被破坏。业务对象(如TraceDAO)通过@Inject构造器接收配置与连接池等依赖。 - 格式化:Java 风格遵循 Spotless 默认规则(配置见 spotless.xml),只格式化改动过的文件,避免全量重排,以保持 diff 最小、评审成本最低。
- 命名:方法与变量使用 camelCase,类使用 PascalCase。
- 不可变优先:偏好不可变集合(
List.of、Set.of、Map.of),结构化日志中的值用引号包裹,便于检索与排障。
这些约定直接服务于可评审性:构造器 DI 让依赖关系显式化,不可变集合减少共享状态带来的并发隐患,一致的命名降低跨文件理解成本。
测试指南
后端测试框架为JUnit + Mockito + Testcontainers,运行于 Maven 测试生命周期(mvn test):
- 命名规范:测试类置于
src/test/java,文件名以*Test.java结尾;集成边界类使用描述性名称(如TracesSchemaParityPreCutoverTest)。 - 行为变更必须配套测试:改动行为前先补充/调整测试,再提 PR。
- 跨栈改动验证:本地启动后端并保证 MySQL、ClickHouse、Redis 等必需服务可用(根级指南同样提示,本地自托管测试需预先配置这些依赖)。
- 基础设施类测试大量依赖 Testcontainers 拉起真实数据库实例,例如切流相关测试会在容器内执行真实 Liquibase changelog 并断言 schema 一致性。
traces 拓扑感知迁移:模块中最关键的工程约束
这是模块指南中唯一指向专项文档的技术点,也是后端维护中风险最高的区域,值得重点展开。
切流窗口内的两种物理拓扑
traces的物理层正在迁移到一个分区化、面向分片的继任表。在全部安装完成切流之前,同一条迁移文件必须对两种不同的物理布局都正确,而犯错的方式是静默的——迁移时不报任何错误,随后表现为读坏数据或丢数据。两种布局如下:
| 表 | 切流前(新安装、多数自托管) | 切流后(SaaS,及完成迁移的自托管) |
|---|---|---|
traces | 存活的ReplicatedReplacingMergeTree | Distributed包装表——不存数据 |
traces_local | 不存在 | 真正存数据的分片MergeTree |
traces_local_v2 | 切流将提升的空继任表(“影子表”) | 被切流重命名移除 |
traces_pre_cutover_backup | 不存在 | 冻结保留的切流前数据,用于观测期回滚 |
切流由运维 runbook 执行(见>--changeset opik:000123_add_foo_to_traces_pre_cutover --comment: Pre-cutover branch — traces is the live MergeTree and traces_local_v2 is the shadow; apply to both --preconditions onFail:MARK_RAN onError:HALT --precondition-sql-check expectedResult:0 SELECT count() FROM system.tables WHERE database = '${ANALYTICS_DB_DATABASE_NAME}' AND name = 'traces_local' ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.traces ON CLUSTER '{cluster}' ADD COLUMN IF NOT EXISTS foo String DEFAULT ''; ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.traces_local_v2 ON CLUSTER '{cluster}' ADD COLUMN IF NOT EXISTS foo String DEFAULT ''; --changeset opik:000123_add_foo_to_traces_post_cutover --comment: Post-cutover branch — traces is the Distributed wrapper over traces_local --preconditions onFail:MARK_RAN onError:HALT --precondition-sql-check expectedResult:1 SELECT count() FROM system.tables WHERE database = '${ANALYTICS_DB_DATABASE_NAME}' AND name = 'traces_local' ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.traces_local ON CLUSTER '{cluster}' ADD COLUMN IF NOT EXISTS foo String DEFAULT ''; ALTER TABLE ${ANALYTICS_DB_DATABASE_NAME}.traces ON CLUSTER '{cluster}' ADD COLUMN IF NOT EXISTS foo String DEFAULT '';
四个承重的细节:
- 用
sqlCheck查system.tables,而非tableExists:守卫必须读取运行时拓扑,Liquibase 自身的记账无法告诉你运维是否执行了切流。 onFail:MARK_RAN:被跳过的分支记录为“已应用”而不执行,后续启动不会在错误拓扑上重试。(liquibase-clickhouse0.7.2 支持该语义,门禁断言它,升级若破坏会先在 CI 失败而非生产环境。)onError:HALT:前置条件本身无法求值就停止,不要猜测拓扑。- 每条语句都带
ON CLUSTER '{cluster}':缺失时 DDL 只到达 Liquibase 连接的节点,其余副本被记作已应用却缺少变更。且守卫基于本地system.tables读取,非集群 DDL 会让节点之间对拓扑的判定分叉。 - 处处
IF [NOT] EXISTS:保证重跑、部分应用、或从任一侧到达的安装都幂等。
两种典型场景与结构性变更禁区
- 场景 1——新增字段:读面向变更。两个分支、每个分支改两张表;若为保留列还需加入回填列清单。
- 场景 2——新增索引:仅存储层。切流前改两张表;切流后只改分片——不要对无数据的包装表建索引。
罕见的结构性变更:ORDER BY、PRIMARY KEY、PARTITION BY在MergeTree上不可变,根本无法ALTER,改动只能重建表并拷贝数据。文档明确建议不要在混合集群窗口内尝试结构性变更——它应跟随继任表定义落地(如000114中按周分区键的做法),而非窗口内的ALTER。若确有必要,那是一场设计讨论,而不是迁移。不变式对结构性变更依然成立,门禁仍会对比排序键与主键。
已知局限与冻结规则
守卫有一个已知局限:Liquibase 在它持有的单条 JDBC 连接上、针对该服务器自己的system.tables求值sqlCheck,然后才提交ALTER ... ON CLUSTER,即分支是从单一主机的拓扑视角选出的。若副本短暂偏斜(切流中途或副本追赶中),某主机可能选定一个分支并把互补 changeset 记成MARK_RAN,令其他主机在账本声称已应用的情况下永久缺失该变更。实践中有三件事约束风险(切流EXCHANGE本身是集群级的、exchange_and_wrap.sh在推进前等待复制稳定、冻结规则把 schema DDL 挡在偏斜最可能发生的窗口外),但没有一件能消除它。候选加固方案是用clusterAllReplicas而非本地system.tables求值前置条件并在部分回答时失败——尚未定案。在此之前,不要对未确认稳定的集群发布 trace schema DDL。
冻结规则:安装处于EXCHANGE与观测期结束之间(traces_pre_cutover_backup仍保留、回滚仍可能的窗口)时,不要发布任何 trace schema DDL。回滚会把冻结的切流前表重新提为traces,观测期内只作用于继任表的 DDL 会被该回滚丢失,而其 changeset 仍记作已应用——账本声称存在一个实际不存在的列,后续迁移也不会补上。请在切流开始前或观测期结束后落地 trace schema 变更。
CI 如何把关:五个门禁与常见失败
| 门禁 | 断言内容 |
|---|---|
TracesSchemaParityPreCutoverTest | 以新安装的方式应用真实 changelog,断言三方一致:traces≅traces_local_v2影子表 ≅ 回填列清单 |
TracesSchemaParityPostCutoverTest | 在000114处停止 changelog,拼接 runbook 的EXCHANGE+ 包装,恢复后让你的迁移运行在切流后拓扑上,再断言包装表恰好暴露分片的所有列 |
TracesMigrationPreconditionLintTest | 免容器的快速检查:严格在000114之后新增的traces变更迁移,必须在变更 changeset 自身携带守卫,并且两个互补分支都要有 |
TraceMutationRoutingArchTest/TraceMutationSqlRoutingTest | 运行时 DAO 变更必须通过TraceDAOImpl#tracesMutationTable()解析目标表,任何 SQL 都不得直接命名traces/traces_local |
每个门禁还携带注入偏差的负向测试,保证没有任何断言会悄悄停止生效。常见失败信息对照:
- “read-facing column parity”——只改了一张表;补上缺失的
ALTER。 - “cutover backfill parity”——新增保留列却没加入回填列清单。
- “wrapper column parity”/列不可读——切流后分支只改了分片没改包装表。
- “skip-index parity”——索引只加在
traces没加影子表。 - lint 失败——新迁移改动
traces却完全没带前置条件守卫,从上面的模式模板起步即可。
CI 不检查什么:以上全部只比较 schema(名字、类型及建立其上的选择/表达式定义),不移动任何数据行,因此无法证明转换无损。把列加进BASELINE_TYPE_DIFFERENCES可使其豁免类型一致性,但此后没有任何东西再验证切流转换是否保值——值保真由TracesLocalV2CutoverTest及 QA 全量预演负责。因此白名单条目是一项决策而非形式:它断言你已核查转换安全或丢失是有意的,请在条目 reason 里写明是哪一种。
运行时的迁移路由佐证
从源码看,DAO 层对两种拓扑的适配同样遵循“单一决策点”原则。TraceDAO.java 中,tracesDistributedWrapEnabled()通过DatabaseAnalyticsDataModelConfig读取包装是否启用:启用时traces是拒绝变更(code 36/48)的Distributed表,因此所有 trace变更(DELETE/ALTER/OPTIMIZE)必须指向traces_local分片;未启用时traces仍是MergeTree,删除可直接进行。读与插入始终经traces路由,两种拓扑下都正确。
tracesMutationTable()是唯一决定变更目标表名的地方:tracesDistributedWrapEnabled() ? TRACES_LOCAL_TABLE : TRACES_TABLE。历史上该路由是每个变更模板里重复的两分支<if(distributed_wrap)>traces_local<else>traces<endif>条件,导致新变更的正确性取决于是否记得复制分支,而错误写法在视觉上与正确写法无异。收敛为单一名字后,变更模板变为拓扑无关的DELETE FROM <traces_mutation_table>,只剩一行需要审计;TraceMutationRoutingArchTest强制两条半边——任何其他代码单元不得读取包装开关,任何变更 SQL 不得拼出任一表名。Liquibase 迁移则按种类拆分:DELETE/MATERIALIZE COLUMN/ADD INDEX/MODIFY TTL只面向traces_local(Distributed包装表拒绝它们),而ADD/DROP/MODIFY COLUMN必须同时面向traces_local与traces——包装表以元数据方式接受它们,跳过会让读侧看不到该列(code 47)。
Append-only 原则与未决事项
已发布的迁移永不编辑——既不为修 bug,也不为给切流前的老迁移补前置条件。所有修改都是新增一条追加迁移。无守卫地改动traces的迁移(000091、000113等)早于切流,对运行过它们的安装是正确的;lint 从000114起才生效,正是出于这个原因(迁移目录当前已有 129 个 changelog 文件,000114即重建traces_local_v2时间戳精度的那次)。
一个被推迟的开放决策:新安装与开源安装最终是否收敛到切流后拓扑?目前新安装从切流前起步并停留,直到运维运行 runbook,因此每条受守卫迁移的切流前分支无限期承重,混合集群永不闭合。替代方案是让新安装直接创建终态(traces_local+ 包装表),代价是绿地方案与迁移路径不同。尚未定案——在此之前,请假设两种拓扑都是永久的,为每条 trace 迁移都写好两个分支。
Agent 贡献工作流与提交规范
作为 monorepo 的一部分,后端模块遵循根级指南中共享的 Agent 工作流:提交前先阅读 CONTRIBUTING.md,运行本模块的格式与测试命令,再请求评审;推荐 draft PR 优先、多组件并行时使用 worktree。
提交与 PR 层面,后端专属约定是:适用时在 commit/PR 标题中使用[OPIK-####] [BE]前缀(根级规范为[OPIK-1234] [COMPONENT] feat|fix|refactor|docs: short summary语义风格,组件前缀含[FE]、[SDK]、[DOCS]、[NA]、[INFRA]等);PR 描述应包含变更摘要、已运行的测试覆盖与关联 issue(Resolves #...),并遵循 PR 模板的 Details、checklist、Issues、Testing、Documentation 各节。
安全与配置提示
除根级安全策略(密钥与 API key 不得入库,使用本地.env或 shell 变量;SDK 示例对本地部署使用opik configure --use_local)外,后端专属检查是:主要后端变更后运行迁移并验证/healthcheck。跨栈本地验证还需保证 MySQL、ClickHouse、Redis 可用,与测试指南中的要求一致。
小结
apps/opik-backend的工程实践可归纳为三条主线:清晰的 resource → service → DAO 分层与构造器 DI、以 JUnit/Testcontainers 为底座的测试纪律、以及对tracesschema 变更的严格拓扑感知迁移纪律。其中最后一条是当前仓库中最值得注意的运维约束——它把“静默失败”的隐患转化为由sqlCheck守卫、ON CLUSTER落地、MARK_RAN记账、五个 CI 门禁背书的可执行模式,并在 DAO 层以单一路由决策点配合架构测试双向收口。新贡献者按此文档与 docs/traces-schema-ddl.md 操作,即可在混合拓扑集群中长期安全地演进 trace 数据面。
【免费下载链接】comet-llmDebug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards.项目地址: https://gitcode.com/GitHub_Trending/co/comet-llm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考