OB 无论是从公司规模还是产品复杂度上来说,肯定算是数一数二的数据库大厂,比绝大多数云厂商里面的数据库团队都专业的多。所以其项目管理的经验,肯定还是有学习的价值。
这里浅浅记录一下 OB 是如何管理整个数据库项目的开发,不过我待得也不久,肯定总结的不全。
前置背景
Dima
Dima 是 OB 内部使用的类 Jira 平台,负责项目追踪管理。
OB flow
OB flow 是 OB 自己开发的一个流程管理软件,你所有的开发需求,都需要通过 ob flow 进行。
比如 QA 在 Dima 上面创建了一个【缺陷】,研发通过 ob flow start dima-id 命令开始开发。此时 Dima 平台上的【缺陷】,会追踪并记录你的整个开发生命周期。
Dima 平台上面的每个任务都有唯一的 dima-id。
当任务开发完成,并且已经合入目标分支,研发执行 ob flow finish 命令,它会删除本地工程,并同时把 Dima 上面的任务给自动 finish 了,自此 Dima 上的这个【缺陷】算是完成了整个开发周期。
管理理念
没有规矩,不成方圆,规则尽可能的通过系统进行约束,靠人为自觉遵守是不现实的。
一切东西都要留痕,在 OB 里面,无论是开发、修 bug、cherry-pick 等一切操作都会在 Dima 上留痕,可追溯。这么说吧,你不在 Dima 上面开个任务,你就没法写代码。
项目管理
制定发版规划
OB 会在每年开始的时候,就规划好今年要发布的版本,比如几号发布 x.x 版本,然后这个版本里面哪些需求是必保需求。
一个版本有提测日期和发版日期,每一个 feature 都需要研发在提测日期之前,发起提测。这些日期基本都是硬性 deadline,如果研发赶不上,就只能自己加班想办法赶上。
Feature 开发
OB 一个 feature 开发的流程如下:
- Leader 或产品在 Dima 上面创建 feature 需求,同时创建一个 feature 分支。
- 研发通过 ob flow start 开始开发,代码写完后找人 review,然后合入 feature 分支。
- Feature 开发完成后,研发开始走提测流程。提测流程里面需要完成如下事情:
- 跑大小 farm 回归,你可以认为这个 farm 里面会有非常非常多的测试 case,跑一轮需要很久。等跑完后,肯定会有一堆 case 挂了,需要研发自己确认是不是本次 feature 引入的。如果和本 feature 无关,研发就不用管。
- 跑性能回归,确保 TP、AP 性能没有回退。
- 编写提测文档,把上面的测试结果,和 feature 的介绍都写到提测文档里面。
- 在提测系统里面,提交提测文档,发起提测。接下来 QA 就会介入测试流程。
- QA 开始测试,测试完成后,QA 会出具测试报告,贴在提测系统里面并同意合入本 feature。
- 研发可以把 feature 分支合入主干分支。但是因为大家都是赶 deadline 的,所以此时主干代码很容易冲突,前脚合了别人的,后脚就和你的 pr 冲突了,这时候你就要自己反复解冲突。
这么做好处是确保了主干的相对稳定性,像开源社区那种,把一个 feature 拆成多个 pr 合入,很容易造成主干的不稳定,这样会导致发版的时候,QA 每一轮回归都会有新问题产生。
虽然 OB 这么搞也不能 100% 保证主干的绝对稳定,因为像 bugfix 这种,还是可以随时合的,所以最后一般还是通过锁分支大法来解决问题。
Feature 的 cherry-pick
当客户有需求时,一些主干上的 feature 会被 cherry-pick 到已发版的分支上,但是你不能直接合入,还是需要重新发起一次提测流程。
Bug 修复
当日常 QA 或者客户发现 bug 的时候,我们会在 Dima 上面创建一个【缺陷】,然后进行修复。在合入代码的时候,ob flow 会问你要不要 cherry-pick 到以前的分支。你选了 cherry-pick 到哪些分支,ob flow 就会在自动在 DIMA 上创建那几个要 cherry-pick 分支的 【缺陷】,这样 DIMA 上也记录了你的 cherry-pick 过程。
三方库引入,都需要进行审批
当你引入一个三方库,比如 paimon-cpp,你需要获得各个组的 Leader 审批。每个 Leader 都会做自己负责领域的检查,比如安全部门会审批 license 是否合规,会不会影响信创,有没有已知漏洞等等。你只有获得所有 Leader 的 approve 之后,才可以引入该三方库。整个审批过程都会被留痕。
值班
当客户遇到问题,都会记录在一个值班平台里面,然后研发会在那个平台里面对问题进行解决。所有问题的解决过程,都会在这个值班平台上面留痕。
这样以后有类似的问题时候,DBA 可以先从值班平台搜一下,看看有没有类似的问题。当然现在有 ai 了,值班平台沉淀的解决方案,结合 RAG,能大大提升 DBA 解决问题的效率。
代码管理
OB 的代码风格是很有特色的,反正我是没见过,算开眼了。当然这不是讽刺,这种代码写法很奇怪,但是你不得不说就是有用,有时候即使我代码有明显的 bug,都不会 core 掉。
OB 在 https://oceanbase.github.io/oceanbase/coding-convention/ 里面说明了一些代码风格规则。
所有函数的返回值都是 int
OB 的函数定义一般都是:
int func() {
int ret = OB_SUCCESS;
return ret;
}
返回值就是错误码,OB 会把错误码的定义都放到 https://github.com/oceanbase/oceanbase/blob/master/deps/oblib/src/lib/ob_errno.h
理论上每个模块都要搞个专属的错误码,不过很多时候大家都会偷懒,直接返回个 OB_ERR_UNEXPECTED 万金油错误码。因为貌似添加新的错误码,需要审批?
函数不能提早 return
int func() {
int ret = OB_SUCCESS;
// do something
// do something
// do something
return ret;
}
你只能在函数的末尾执行 return,这么做是为了当函数长了,如果有人在中间提早 return 了,有可能导致函数末尾的资源释放操作没被执行。
这个我存疑,因为有时候为了刻意在末尾才能 return,反而导致函数的逻辑复杂起来。
int func() {
int ret = OB_SUCCESS;
if (OB_FAIL(do_something())) {
ret = OB_ERR_UNEXPECTED;
}
if (OB_SUCC(ret)) {
if (OB_FAIL(do_next_1())) {
ret = OB_ERR_UNEXPECTED;
}
}
if (OB_SUCC(ret)) {
if (OB_FAIL(do_next_2())) {
ret = OB_ERR_UNEXPECTED;
}
}
return ret;
}
不能使用 break、continue、goto 这种影响代码执行流的东西
int func() {
int ret = OB_SUCCESS;
for (int i = 0; i < 10; i++) {
if (OB_FAIL(do_something(i))) {
// 如果这里执行失败,你不能写 break,立刻结束 for 循环
}
}
}
需要写成
int func() {
int ret = OB_SUCCESS;
for (int i = 0; OB_SUCC(ret) && i < 10; i++) {
if (OB_FAIL(do_something(i))) {
// 在 for 循环上面,通过 OB_SUCC(ret) 进行判断
}
}
}
我同样存疑,感觉没啥必要。
非必要不能使用 std 标准库
OB 需要严格控制内存,std 内部会自己调用 new 分配内存,不易于管控。所有的内存都要通过 Allocator 来获得。Allocator 上面会记录租户的 tenant_id ,这样就可以严格控制每个租户内存的分配。
从 Allocator 里面申请出来内存的时候,也需要进行判空处理,返回空指针就意味着这个租户内存耗尽,要返回内存不足的错误码。
Allocator 也可以记录模块 ID,比如 OB 可以在 Optimizer、Executor 上面使用不同的 Allocator,这样后续分析问题的时候, 可以很容易的发现哪个模块内存用的多,或有内存泄露。
这里详细描述了 OB 的内存管理:https://oceanbase.github.io/oceanbase/zh/memory/
对于 std::vector 这种常见的容器,OB 自己也都手写了,以便于和 OB 的 Allocator 配合使用,具体见 https://oceanbase.github.io/oceanbase/container/。
对于和三方库对接的地方,还是能够使用 std 标准库的,毕竟你不可能去改三方库的源码。
不能使用智能指针
上面说了,非必要不能使用 std 标准库,所以智能指针也用不了。对于内存释放这种操作,OB 一般就是不管,他有一个 ArenaAllocator,你从这个 ArenaAllocator 申请完内存后,你不用管释放,后面在这个 ArenaAllocator 生命周期结束之后,其上面所有挂载的对象,会一次性统一释放。
一次性全部释放还更高效,比你一点一点 free 来的快多了。
可以多打日志
很多系统不推荐你频繁打日志,因为输出过多日志会影响系统的性能。但是在 OB 你不用担心这个,OB 的日志系统已经被优化过了,当日志输出流量很大时,OB 会自动对日志限流,确保系统运行的稳定。
日志多的好处就是,出问题的时候,很容易定位问题,你可以根据整个 WARNING 日志出现的顺序,知道整个调用栈。
int func1() {
int ret = OB_SUCCESS;
ret = OB_ERR_UNEXPECTED;
return ret;
}
int func2() {
int ret = OB_SUCCESS;
if (OB_FAIL(func1())) {
LOG_WARN("func1 failed", K(ret));
}
return ret;
}
int func3() {
int ret = OB_SUCCESS;
if (OB_FAIL(func2())) {
LOG_WARN("func2 failed", K(ret));
}
return ret;
}
上面这个例子中,一旦 func1 执行错误,在每一层调用都会 LOG_WARN 一次,这样你从日志里面就能够看到整个错误调用链。
所有函数的返回值都要被判断
就是你每一个调用的函数,都要判断其返回值是否符合预期,不符合立刻打日志 LOG_WARN ,并修改错误码,一般大家都用 if (OB_FAIL(func())) 这种写法简化代码。
函数的每个入参都要检查
每个函数都要天然假设入参不安全,需要进行检查后才可以继续执行,比如:
int func(Metadata *metadata, int flag) {
int ret = OB_SUCCESS;
if (OB_UNLIKELY(metadata == NULL || flag <= 0)) {
ret = OB_INVALID_ARGUMENT;
} else {
// do something
}
return ret;
}
下面这个例子函数入参是数组,我们要先检查 idx 下标访问是否会越界:
int func(ObIArray<int> data, int idx) {
if (OB_UNLIKELY(idx < 0 || idx >= data.count())) {
ret = OB_INVALID_ARGUMENT;
} else {
// do something
}
return ret;
}
Code owner 机制
每一个 CPP 文件都有 code owner 负责,如果你的代码想要被合入,需要该文件的 codeowner 给你 approve。
一般 code owner 离职了,leader 会让你改 code owner 匹配文件,换个人。
总结
OB 肯定做的不止我说的这些,奈何低 P 没有机会接触整个链路,只能管中窥豹。