头图.png
作者|一啸
来源|尔达 Erda 公众号

任何大型工程项目的研发都会涉及到两个非常共通的难题:

  • 第一个是稳定性问题,越大的项目越难做稳定,“魔鬼在细节里”;
  • 第二个是工程研发效率。

本文我们先聊聊第二个问题,后面再谈谈 Erda 的稳定性建设。具体谈论如何打造大型的工程研发效率之前,先回顾一下我之前在阿里的 8 年研发经历,希望借此形成一个有带入感的对比。

我在阿里的经历

DataX

我刚毕业加入淘宝后,第一次真正接触的研发工作就是参与 DataX 的开发。

datax 的工作原理就是全量将某个数据库或存储中的数据读出,然后再全量写入到另一个数据库或存储中,总结起来,就是为了将数据从一个地方传输到另一个地方。当时在淘宝的核心场景是将 MySQL 的表数据传输到 hdfs 中进行 mapreduce 的大数据计算,除了 hadoop 计算场景外,还有将 MySQL 数据导入 Oracle RAC 集群进行分析的场景。

datax 的设计其实很简洁:1 个核心框架 + N 个数据库或存储插件。当时整个研发团队也就不过 4、5 个人,我主要负责 Oracle 数据库的插件开发,很多时候都是在写插件,因为插件的运行本身也就是单机程序,所以整个研发工作基本都是自己一个人完成。除了最后联调的时候,过程中也不需要过多的复杂协作,研发的效率是非常高效的。

Logserver

后来,我开始接触 logserver 的开发,logserver 就是接收从淘宝每一个页面发送过来的埋点请求,生成下游能够消费的日志数据。

除了上下游的需求团队外,这个服务的开发只有我一个人全权负责,我的主要工作就是将 logserver 从 apache httpd 的架构迁移到 Nginx 架构上,核心工作就是在 Nginx 上开发一个模块。logserver 当时在很长时间内都是整个淘宝 qps 量最大的服务,没有之一(今天就不清楚了)。

现在回想一下,大都是当初一个人在慢慢倒腾这个 Nginx 模块的场景。logserver 和 datax 一样,都是单机版程序,同样不需要太多的协作开发。

dbsync

接着,我又投向 dbsync 的开发。dbsync 和 datax 要做的事情本质上差不多,就是想从全量同步数据升级到实时的增量同步。当时主要做的是从 MySQL 实时同步到 HDFS 上,印象中只有 3 个人参与开发,而我负责的工作就是写一个 C 程序从 MySQL 中实时地将 binlog 日志订阅出来。

Timetunnel

写到这里突然发现:在淘宝的前几年,我一直在做数据传输相关的中间件。

TT 本质也是用来传输数据的,并且它本身也是一个消息中间件,类似 kafka。整个 TT 的开发团队大概 5、6 人的样子,我的主要工作就是基于 hbase 来实现消息的存储引擎。

这也是我在阿里唯一一个用 java 开发的项目,其他大部分都是写 C 和 Go。

tengine & cdn

后续我转岗去了 tengine 团队,这里有趣度超高。团队大概有十多人,基本都是各做各的事情(模块) ,同时也乐在其中。整个团队颇受外界猎头喜欢,甚至一度流传“要挖就挖 tengine 团队”。

最后,整个 tengine 团队一起去做 cdn 了。在 cdn ,我被安排带着 3、4 个人去优化 dns 服务器和调度系统,大概情况就是老牌的 bind 性能不行了,于是完全重写一个 dns 服务器。其实,写一个 dns 服务器也不难,大多时候都是一个人在看 RFC ,把标准撸到丝毫不差就完事。最终,我重写的 dns 服务器性能大幅度超越 bind ,成功上线节省了很多机器。后续准备再用 dpdk 干一版的,还没来得及干,阴差阳错就跑去做容器了。

容器

在我去容器团队前,只有一个小伙伴天天和 docker 做斗争,我去之后变成了两个人,他也不再是孤军奋战了。

然后,我们两个人把 docker 里里外外魔改了一通,将其称之为 alidocker。改到最后,老板们觉得始终还是 docker ,索性就决定做一个自己的容器引擎,也就是现在的 pouch。就这样,我也算是成了 pouch 的第一个作者,可惜做了第一个版本就跑路了,现在的 pouch 也挺遗憾的。

补一句题外话(小声吐槽):容器团队选择做 Swarm 而不是 K8s 这件事情也挺遗憾的。

总结一下

我在阿里的 8 年,一直做系统级的基础软件,这些软件有着共同的特点:

  • 很基础
  • 没有业务需求
  • 迭代速度较慢
  • 缺少大型团队的协作开发
  • 代码级复杂,但服务架构简单
  • 行业内非常地通用且标准
  • 对稳定性性能要求很高
  • 很容易拿出来吹牛逼 (是不是值得吹,又是另外一个话题了~)

总结而言:我在阿里 8 年待过的所有团队,好像没有一个用过项目协作工具、持续集成 CI/CD 、全链路调用追踪等研发效能相关的工具。这可能就是基础软件的研发现状:团队小、单兵作战、强调个人能力;甚至,线上 debug、 fix bug 这种事情都是做过的。

有幸,今天我还能继续从事基础软件的研发工作。不同的是团队规模更大了,近百人的团队共同开发一个基础软件,一起往前快速迭代是一件非常具有工程挑战的事情。如果今天继续采用我过去在阿里这种粗放、散养式的研发方式肯定是行不通的。

Erda 工程实践

接下来聊一聊:离开阿里后,我现在所处的 Erda 团队是如何实践大规模工程研发的?

里程碑

undefined
(里程碑管理)

里程碑是宏观层面比较重要的一个工具,我们主要靠它来设定方向、框定产品大图。如果没有里程碑设计的话,我们很容易突然迷失方向,陷入各种日常需求和问题中。

我们一般会把里程碑做得很大,或者说设计得很远。比如,未来两年一个比较粗的时间点要完成的事情都放入里程碑管理。

我们 Erda 设计的里程碑,大概就是这个样子:

undefined

里程碑的设计和管理毕竟太 high level ,有点类似顶层设计,所以它并不能成为日常研发工作内容的安排。这有点类似于 OKR,但没有 OKR 那么复杂,我们只是将未来的产品大图打散到一个比较期望的时间节奏上去,然后让日常的工作内容尽可能沿着这条时间线往前推进。

里程碑管理好的话,其实能起到承上启下的作用:向上,能够给公司层面跨团队合作形成很好的同步和信任感;向下,能指导方向,拿到研发结果,不至于一年到头碌碌无为。

需求管理

undefined

前面我们提到了里程碑工具,要想支撑里程碑的完成,还得靠日常的具体需求和问题,这里先一起聊聊需求。实际观察下来,很多开发同学其实不懂什么是真正的需求,也不懂如何接需求。

下面举个例子。

需求方:Erda 能不能支持从 Excel 中导入数据?
(初一听,这确实是个需求,就是要支持从 Excel 这种很常见的文件里导入数据。稍微思考多一点的同学,就可以继续追问。)
开发:为什么要从 Excel 导入数据,具体的场景是什么?
需求方:Excel 表格上填写这些数据很方,我习惯先在表格上把这些工作做好,再导入进 Erda。

追问到这里,其实已经能够发现。需求方的真正的痛点是 Erda 这个功能不如 Excel 方便好用,导致用户绕了一个弯路。所以,我们真正要做的需求是优先解决这个不好用的痛点。(至于有些人就是要这个功能,那是我们另外要讨论的事情了。)

最常见的需求就是,能不能加个 XX 功能。(这里大家可以细品一下。)

综上,我们要求产品和开发同学对于任何需求都要不断思考,多追问几个为什么,拿到用户最原始的需求。始终要记住,用户给你的极有可能是他认为的方案,而不是原始需求。

需求管理对了,后面的开发才会少走弯路,需求是起点、是根本。

迭代队列

undefined

一个商业化的大规模工程团队,一定有 PD 这个角色。PD 是做什么的呢?除了上面提到的需求管理外,PD 最基本的工作就是设计产品逻辑,这里就不展开描述了。

在 Erda 团队里,PD 最需要核心设计的是迭代队列,注意:这里提到的是队列,这个队列里需要长期装满 3 个迭代,其中的 1 个迭代里存放着最优先要解决的需求和问题。为什么这里是 3 个迭代的队列,而不是 1 个迭代呢?可以思考一下,我们在系统架构中,引入消息队列中间件的作用。PD 和开发之间完全可以通过这种方式来解耦的,开发只需要从队列中取设计好的产品任务解决问题就好,PD 只需要不断地从需求池里取内容经过设计后再合理排入到迭代队列中即可,两边的角色都可以实现自我驱动,不需要严格同步工作,所以这里的核心是为了异步工作。

undefined
(迭代管理)

那么,这个队列为什么是迭代队列,而不直接设计成需求队列,开发直接取需求而不是取迭代呢?

需求队列会存在两个很大的问题:

  • 第一,迭代是用来严格定义一个版本的时间周期,如果没有迭代,版本的时间周期节奏会变得很乱,会进入一种比较随意的状态。
  • 第二,当前实际开发中的需求如果要延期解决的话,这个延期的需求重新排到哪里去?难道重新放到需求队列的末尾吗?放到末尾显然不合适。如果是迭代,就可以严格规定放到下一个迭代或者下下一个迭代。

开发过程

undefined

开发过程中,对我们而言非常重要的一个关键指标就是:我们能够合理接受单个需求的延期,但不能接受整个版本的延期(个别的特殊情况不在讨论中)。

很多人可能对于这个指标的理解仅停留在保持发版节奏上,甚至会觉得这只是一种表面工作,版本出来了,需求没有做完有何用,为什么不延期版本把需求全部一起做完?

其实,这个关键指标完全不是一个管理结果,而是技术结果。很多项目在做架构设计、代码设计的时候,是没有考虑后续小步快跑的,不能小步快跑地添加新功能、解决新需求的话,就会经常性导致部分需求做了一半要延期的时候,根本停不下来,这个版本根本无法临时放弃需求而继续发版;更严重的是,需求和需求之间还是强耦合的,一个需求做不完,其他需求也不能正常发版。这都是实实在在的技术问题、代码问题,而不是管理问题。

需求任务关系
(需求任务管理)

除了关键指标以外,开发过程的管理,主要是基于任务来协同的,每个需求在开发前都已经被拆分成了合理的任务,也就是说一个需求会关联完成这个需求的所有任务,开发同学只需要每天按照优先级完成,并及时更新反馈任务的情况和进度即可。(这里提到了及时反馈,反馈又是一个异步工作的关键机制。)

很多研发主管都喜欢有事没事在钉钉、微信里询问一下具体的工作进度,或者拉个会议对一下进度,所以 “已读 + ding 一下“ 对他们来说是一个很好的功能。我们非常不鼓励这种依赖即时通信工具或会议的方式来沟通、了解开发进度,这种方式非常同步,就和同步调用一样。正确的做法,应该是开发同学每天根据自己的情况及时在项目管理工具对自己的任务进行进度更新和总结反馈,研发主管自己按需关注任务的研发进度和情况,彼此没事不要相互打扰。

CI 流程

在今年年初我们决定将 Erda 开源后,就把整个团队的日常迭代开发全部转移到 GitHub 上执行了,因此 CI 主要是基于 GitHub 的 Action 来做的,会把常规性的检查任务全部放到 CI 中来完成,比如:单元测试、代码质量检查、规范等。这个部分比较常规,没有什么特别的。但 GitHub Action 太慢了,我们后续计要把 GitHub Action 迁移到 Erda 的 CI,这样我们可以实现 CI 并发更高、跑得更快、效率更高。

本地+云端

一个很大的产品就是一个很大的软件工程,这样大的工程要完全放到个人笔记本电脑上进行本地调试开发,是一件难度不小的事情。就像一个中间件、一个框架、一个 webserver 等,都可以很轻松地放到本地电脑上开发调试,但要把整个淘宝装到本地电脑就有点为难了,当然我们也正在向这个方向努力。

在不能完全将整个 Erda 轻装到本地电脑前,我们采用了本地 + 云端联合的方式来进行开发调试。云端本身是一个已经部署好的 Erda 全平台开发环境,包含所有能够正常工作的组件;然后使用 telepresence 打通本地和云端 K8s 之间的网络:

  • 本地可以直接使用 K8s 容器网络内的 DNS,同时支持短域名的 search 机制。
  • 本地启动一个服务,劫持 (intercept)云端 K8s 集群内的 Pod,当 K8s 集群内其他服务访问该 Pod 时,流量会转发到本地。
  • 实现将云端 K8s 集群 Pod 中的环境变量全部自动导出到本地。
  • 实现将云端 K8s 集群 Pod 中挂载的 volume 通过 sshfs 映射到本地的文件系统。

简而言之,本地起一个服务,可以随意访问 K8s 集群内的任意 service;同时,本地的服务也能被 K8s 集群里的其他服务访问到。这样一来,确实可以大幅度提高开发调试效率,开发人员不用在复杂环境上来来回回的折腾。

集成环境

虽然 Erda 给上层的业务应用提供了很强大的持续集成、部署和运维监控等能力,但 Erda 在很长一段时间内却不能通过自己部署自己,也就是不能实现 Erda On Erda,所以那段时间一直没有真正的自动化集成环境,研发质量和效率也不太高。

由于 Erda 整个工程项目比较庞大,代码从构建到部署更新,再加上所有的自动化测试跑一遍,整个流程需要数小时,耗费时间长。所以,我们的集成环境主要采用每天夜间定时运行自动化集成 + 测试,次日上班就可以看到集成结果。集成出来的错误结果,当天内必须 fix 掉,这是持续保证集成效果的关键。

undefined
(基于流水线的自动化集成)

自动化集成主要针对的是 Master 主干分支,我们没法做到任何一个 PR 触发自动化集成,核心问题还是项目太、开发人员多、自动化用例太多,基于 PR 集成的时间成本接受不了。

可以看出来,我们的日常开发测试和自动化集成两条线路也不是串行的,而是异步关系。我们不断地追求、设计整个工程流程的异步化,只有异步才能高效支撑大型工程的研发。即使你的整个工程流程执行速度非常快,没有时间消耗的成本,同步也会有额外问题产生的;同步的最大问题就是会被异常情况打断、干扰,哪怕是一些可以延迟处理的异常情况,也会干扰你的工作,你必须花时间先解决异常,这种打断和干扰对研发团队的效率影响是致命的。

undefined

API 设计和自动化测试

集成环境能够有效工作的关键是什么呢?很显然,必定是自动化测试了。

自动化测试分为自动化的单元测试、接口测试、性能测试、安全测试、UI 测试等等。日常开发过程中,效率影响最大,最频繁的显然是单元测试、接口测试、UI 测试。

  • 单元测试比较简单,我们放到了 CI 流程中,每一个 PR 就驱动完成了单元测试。
  • 接口测试非常关键,我们需要覆盖那些全链路场景的接口,从最顶层的接口出发完成整个系统的功能自动化测试,所以接口的自动化测试就放到了整个集成环境中完成,因为集成环境有最新、最稳定的代码,以及异步执行等优势。
  • UI 的自动化测试成本相对比较高,我们目前还在尝试阶段,没有完全达到成熟。我一直希望从智能测试的角度,用 AI 算法的思路去解决 UI 的自动化测试。

这里先介绍一下接口测试。接口就是 API,要想编写出好的接口测试用例,必须得先有 API 的设计。API 的设计从哪里来呢?我们当然不希望从开发工程师的口中来,所以自研了 API 管理平台,从 API 的设计到测试进行全覆盖。开发工程师在完成需求前,首先在平台上完成 API 的设计工作;然后将 API 的设计和需求关联上,这样一来,测试工程师就自主从需求里获取到具体的 API 信息,完成接口测试用例的编写。

undefined
(API 设计管理)

除了异步协同外,打通整个工程流程,也是我们至关重要的一环,这也是我们为什么不用 jira,jenkins 等的一个原因。

另外,我们的自动化测试用例是由测试工程师编写、开发工程师一起维护,对测试工程师的 OKR 设定就是任何一个版本的回归 + 新功能测试必须控制在一个非常小的人/天内。今天 Erda 有着近百人的研发团队,而测试只有 5 个人。

undefined
(基于场景的自动化自动化测试流程)

手工测试

有了自动化测试,为什么还需要手工测试?手工测试显得似乎没有那么高级了。

对于没有前端 UI 的系统,自动化掉所有的测试理论上是可行的;一个有着复杂前端 UI 的系统,自动化起来还是会有很多难点,大多数做 UI 自动化的,基本也都是集中在几个核心的流程上,很难覆盖边边角角的全部场景。

在专业测试工程师眼里,手工测试也是很重要的一部分,需要有工具、有方法来支撑的。每一个版本,手工测试用例也是要能够被回归的,因此这些手工用例也要被记录下来。

undefined
(手工测试用例管理)

这部分我们就讲到这里,不再过多讨论。

答疑和问题处理

Erda 作为基础平台性产品,用户会比较多,零零碎碎的答疑和服务支持直接打到产研团队的话,会消耗非常多的研发精力,因此我们建设了专门的 “SRE 团队 + 轮值的产品研发答疑同学” 来面对客户,针对客户和用户的各种问题进行答疑。除了配置专业的服务以外,另外一个更重要的事情就是需要将服务支持过程中的问题记录到 Erda 项目的工单(tickets)中,这里说的Erda 项目工单就是等同于 GitHub 的 Issues,它并不是一个面向客户服务的工单系统。

undefined
(答疑问题管理)

项目的工单列表在每周五需要进行 Review,将那些比较简单、能够快速解决的工单问题梳理出来,然后一键复制到迭代队列中。这里有两个很关键的点:

  1. Review 先挑选的是那些简单的、能快速解决的问题。
  2. 放入到迭代队列,而不是需求池。

软件系统的 bug 总是解决一个少一个,越跑越稳定,所以简单的问题要快速解决、0 容忍对待。难的问题需要专项对待、专项解决,不对的时间节点或者资源有限的情况去死磕难题,大概率不是一个好想法。

筛选出来的工单问题千万不要放到需求池中,一定要直接进迭代规划。进了需求池,就不知道什么时间才可以排上号了,明确放入迭代、明确好解决时间,快速收敛问题最重要。

发版经理

我们在一个迭代周期内,会轮值一个“发版经理” 的角色,发版经理要做的事情核心就是协调 + 跟踪。当然,发版经理不是去协调团队内的你我他的事情,也不是去跟踪某个人有没有划水。而是和 PD 一起确定迭代要做的需求内容,参与需求是否延期到下一个迭代的决策,确认新版本周期内几个关键时间节点的产物并做好验收,以及统一负责冻结代码分支等事情。总之,“发版经理”需要为新版本的效率和质量负责。

工具优先给谁用

最后,这里再聊聊工具的推行使用问题。我个人在做整个研发管理过程中,有一个深切体会:工具的推行一般来自于上级,上级推行这个工具的出发点当然是为了效率;但是,你经常会发现很多研发主管在关注工具的时候,一般都是从自己的管理视觉出发,而不是真正从一线员工使用工具的视角出发。

比如,项目管理类工具的第一核心究竟应该是定位给 PM 或研发主管用,还是应该定位成工程师间的协同使用?如果你要将团队打造成更高效的异步协同团队,那么这类管理工具一定是先给一线员工使用的,真正做到让团队内的每个人在工具上协同起来,通过工具平台来连接你我他。

架构设计

这里谈架构设计,不是想聊 Erda 的架构,我们还是聊一下工程效率这件事情:从架构层面如何做一些效率上的保障,让大规模研发团队可以更加从容迭代。

微服务化

早期版本,Erda 也是一个大的单体应用,团队规模就几号人,和我之前在阿里团队经历的差不多,协同起来非常高效,添加功能和解决问题也很快。但随着人数的逐渐增多,过程中出确实现了很多协作问题、效率问题,反正最后就是拆分成了微服务。当然,微服务也有微服务的问题。

讲微服务设计方法论的分享有很多,可以自行搜索参考。

组件化协议

Erda 团队的前后端比例,在最高时能达到 1:7,也就是一个前端要对接多个团队的多个后端,产品开发迭代的瓶颈被前端资源限制住了。

为了解决这个问题,我们思考并探索出了一套组件化协议框架,前端提供组件库和交互定义,专注于丰富组件的功能和改善交互体验,如何拼装组件和提供数据来实现业务功能,就交给后端来做,由于前端可以不关注业务逻辑和对接沟通 API 定义,中间能节省掉许多沟通成本,从而提高了整体的产品开发效率。

db migration

对于 Erda 来说,快速迭代产生的众多版本必须保证能够顺利升级。对我们来说,升级最难的其实是数据库的 migration,针对这样的一个情况,我们自己开发了一个 dbmigration 的管理框架,然后基于第一个产品版本定义好数据库的基线,后续的每个版本都在前一个版本的基础上开发 migration 逻辑放入到框架集中管理。Erda 的升级必须是从发布的版本按顺序一个一进行 migration 升级。

开发一个软件可能比较容易,开发一个能够持续升级的软件相对来讲困难度就比较高了。针对 migration 和升级,我们在测试阶段也会反复验证这件事情。

写在最后

研发管理这件事情本身管理的是技术,而不是人。我所接触到的很多人,还是会觉得研发管理是安排一个 PM 去盯着整个研发过程,甚至拿着“鞭子”去抽那些走得慢的人。这个意识和认知肯定是不对的,只有把管理动作拉回到技术本身这件事情上,才能真正激发团队的热情。

另外,任何人设计的任何一个工程流程和管理,在实际落地执行的时候,都不要去追求完美、100% 的精确,这是一件不现实的事情,有误差才是符合自然规律的,我们要做的事情就是将误差控制在足够小的范围即可。能够接受不完美,是研发 TL 的自我修养。

关于 Erda 如果你有更多想要了解的内容,欢迎添加小助手微信(Erda202106)进入交流群讨论,或者直接点击下方链接了解更多!