基于Go语言的滴滴DevOps重塑之路 - 滴滴技术

研发效率和系统稳定性是研发团队永远无法绕开的话题,前者决定业务迭代效率,而后者决定交付质量。多年来,滴滴在保障稳定性的前提下不断探索更高效的技术手段,积累了大量实践经验。近期由网约车研发效率与稳定性负责人魏静武在Gopher China上分享了滴滴在相关领域的实践。

以下为演讲实录。

很多技术同学或许都有过类似经历:处理故障的过程中除了要承受巨大压力之外,还会承受来自业务方新需求上线的压力。似乎稳定性和研发效率很难调和,我们一直在探索如何在保障稳定性的前提下不断提升我们的交付效率,既然是技术上的问题,还得从技术上找解法,DevOps就是我们的突破口,因为它几乎包含了整个研发交付流程。

当然在保证稳定性的前提下,提高效率是一个很庞大的课题,整个行业甚至整个社会都在寻求最优解。今天我的分享只是抛砖引玉,希望我们的实践,能给大家带来一些启发。

DevOps ——新的挑战

从下图来看,滴滴与业界其他公司在DevOps面临的挑战几乎是类似的,无非是围绕 DevOps 上层的业务挑战和下层的技术架构挑战。

我们分开说,从业务挑战来看,滴滴有专车、快车、豪华车、拼车等多个产品。随着业务发展,产品会越来越精细,比如拼车里有极速拼车、特价拼车等,同时滴滴还开放了运力,让第三方网约车平台可以接入滴滴运力。这些复杂的业务细节,投射到技术上就是复杂的微服务架构。

从架构挑战来讲,滴滴早已经全面上云,并且实现了业务的同城双活和异地多活。虽然这些技术升级有明显收益,但不可否认,它也进一步加剧了技术架构的复杂度,原有的微服务要部署到多个机房,当微服务数量只有几十、几百个的时候,可能不会有太大问题,一旦微服务数量达到几千甚至上万的时候,任何一个原本我们认为很小的问题,都会被无限放大。具体来说,分为以下三方面问题:

开发方面

随着业务的发展,微服务越做越小,功能越来越单一。相对于单体服务来说,微服务只是业务逻辑少了,但会引入很多新的功能,比如服务发现、RPC调用、链路追踪等等,“微服务虽小,五脏俱全",重复工作繁重。

测试方面

对于小体量的微服务架构,靠个人或者一个团队就可以快速把测试环境搭建起来,那大家可以设想一下,如果有几千甚至上万个微服务时,靠个人或者一个团队很难构建出一套稳定的测试环境,更别说人手一套。其次,在微服务架构下如何进行回归测试,也就是说这次变更到底影响了什么,没有人能说清楚,即使能说清楚,那能不能把所有的场景都准备好,进行回归测试呢?——很难。

运维方面

随着微服务数量变多,为了避免引起线上事故,我们需要建设更多的监控和报警,比如针对业务指标、技术指标等不同维度的监控报警。但建设多了之后就又引起另外两个问题:一个是报警太多,误报也多;另一个是报警策略可能要随时调整,因为业务在不断变化,持续保障报警的有效性需要投入巨大的人力成本。还有就是定位难,任何一个环节出问题都可能会导致整个系统的崩溃,如何快速根因定位并止损,也是运维阶段面临的巨大挑战。

接下来,我会从不同阶段面临的挑战出发,聊聊滴滴的应对之策。

开发 – 云原生脚手架

首先是上面提到的开发阶段繁重的重复性工作,比如服务发现、链路追踪、RPC调用等等。但还有其他的挑战,滴滴在最初技术选型的时候,针对业务开发选的是弱类型语言——PHP。PHP 最大的优势是快,它帮助滴滴快速实现了产品落地,抢占了市场先机,但随着业务复杂程度的提高,或者说微服务的交互越来越多,弱类型语言也暴露出来一些问题。一方面由于数据是弱类型,导致我们在跟一些强类型语言,比如 C++、Java 等进行交互时,经常会导致强类型语言崩溃;另一方面是性能问题,大家知道,在出行领域是有比较明显的谷峰流量特征的,比如早晚高峰、节假日高峰等,尤其是像五一、十一这种长假前一天的晚高峰,流量会很高,这时弱类型语言的性能问题会消耗大量机器资源,虽然滴滴通过弹性伸缩降低很大一部分成本,但高峰期的消耗依然很高。

基于此,滴滴在开发阶段做了三个“统一”:

统一 Go 技术栈

至于为什么统一到 Go,除了上面提到的问题外,还有一个更重要的原因,就是整个技术团队的转型成本,滴滴是比较早应用Go语言的公司,在2016我还未加入滴滴之前,滴滴内部就已经有很多团队在使用 Go 语言了,所以基于这些原因,我们最终决策统一Go技术栈。

但说实话,选定 Go 技术栈,其实大家只是达成了一个思想上的统一,而在具体操作上,我们不可能一刀切地要求大家全部迁移到Go,或者要求大家暂停业务开发来完成迁移,业务方也不答应。

统一框架

我们通过框架的方式把所有非业务逻辑全部封装起来,以帮助业务在迁移或者新建服务过程中成本足够低。同时为了兼容原有服务,满足业务的一些历史“债务”需求,我们选择用 Thrift IDL为中心,进行扩展(见下图)。

Thrift是一个提供可扩展,跨语言的RPC框架,IDL 是它的接口描述语言。虽然 Thrift 有自己的协议,但通过我们在 Thrift IDL的扩充,使得它可以在兼容Thrift原生协议的情况下支持了 HTTP 和 gRPC 协议。这样既可以解决原来已经使用Thrift 服务的兼容问题,也能顺利完成 HTTP 服务或者 gRPC 服务的迁移工作。

同时基于扩展后的IDL ,对上可以生成不同于语言的 SDK、文档以及 Mock 服务;对下可以生成一个支持多协议的 Server,在这个 Server 里,除了一些基础能力之外,还包括一些中间件封装,可以说我们几乎封装了滴滴所有中间件SDK,比如 MQ、RDS、KV 等,开箱即用。

统一数据

我个人认为这对滴滴来讲,是更为重要且难的事情。因为人多了,服务多了,数据统一的难度会指数级上升。在框架去统一数据不但可以提升服务治理能力,同时也降低了大量重复开发工作。

这三件事做完之后,业务的迁移成本就相对低了很多。尤其是新服务的开发,业务研发工程师只需要专注业务逻辑即可,底层的复杂性都被屏蔽了。

测试 – 流量回放与测试环境

上面我们提到了框架迁移的问题,统一了技术栈,准备好了框架,但对于迁移老服务来说,更大的挑战在于如何保障迁移过来的服务足够稳定,能实现平滑迁移。

对于滴滴来说,一个微服务,下游依赖服务可能有几百个,如果靠传统单测或者自动化测试,很难覆盖所有场景。大家不妨细想一下,不管是小型的微服务架构,还是大型的微服务架构,对于它们来说,真正的测试成本大头在哪?通常情况下,不是在新功能的测试上,而是在那些已经积累了很长时间的回归测试上(大家可以回忆下自己的经历,是不是每次故障大多是因为影响了老功能,反而新功能没啥问题)。在实际的测试过程中,由于各种错综复杂的原因,我们很难做到每次上线就把所有的场景都覆盖一遍,因为不管从人力上还是依靠传统技术都很难实现。

在测试阶段的另外一个挑战也是我们刚刚提到过的,就是如何搭建测试环境。当涉及到成千上万的微服务时,要怎样构建测试环境?怎样保证每个人都有一套稳定测试环境而且互不影响?

我了解到,业界关于测试环境构建分为两派,一派以契约测试为代表,他们认为就不应该构建微服务的测试环境,应该通过契约测试的方式把下游全部 Mock 掉来进行测试。但是,试想一下,如果我们下游有几百个依赖,每次请求会带来几十上百次的下游次的下游调用,要如何 Mock 呢,就算Mock了,又怎么持续保障Mock有效呢,这不是靠堆人可以解决的。

另一派叫做 Test in Production,即在线上进行测试。他们认为既然线下搭建测试环境成本太高,那在线上环境通过一些测试账号做逻辑隔离不就行了。这种方式对于滴滴来说并不能被接受,因为虽然做了逻辑隔离,但它并不能完全保障对线上无污染,尤其是线上数据的污染。

那滴滴是如何应对上述两个挑战的呢?我们先来第一个问题,针对这个问题的解法,滴滴采用了比较创新的手段——流量录制和回放,不同于业界的流量录制和回放,我们实现了每一次请求的上下文全部录制。什么意思呢?比如打车的时候用户会进行价格预估,预估的流量会请求到后端服务,这个时候我们除了会把这次预估接口的请求流量和返回流量录制下来,还会把涉及调用下游的 RPC、MySQL、Redis 等outbound流量也都录制下来,并把这些流量绑定在一起,这就是刚刚所说的请求的上下文,我们叫一次Session,同时能保证并发请求之间的流量不会串。

具体怎么实现呢?关注滴滴开源的同学可能会知道,我们开源过两个项目,分别针对PHP和 Go 语言的方案,对于Go语言是通过更改 Go 源码的方式来解决录制的问题。但现在滴滴内部升级了一种新的方案——内核录制,无需修改源码。

如下图所示,在我们的新方案中,针对 Go 和 C++ 服务,我们采用 Cilium 提供的 eBPF-Go 的库,通过内核将所有相关系统调用 Hook 起来,比如 Accept、Recv、Send 等系统调用。

在这个基础上,还需要通过 Uprobe 的方式去 Hook 一下用户态的调用,因为我们是绑定在进程上进行录制的,而不是通过网络层面进行录制的,所以我们就可以把inbound和outbound流量全部录制下来并进行绑定,同时也忽略了进程外的噪音流量。

对于 PHP,我们实现的逻辑是采用 CGO +LD_PRELOAD,其实是在更上一层(libc 这一层) Hook 了系统调用。其实,这种方式更好用一些,但 Go 语言默认不依赖libc ,所以我们针对不同语言采用了两种方案来进行录制,并在线上进行不间断的实时录制。

录制完以后,还需要进行流量过滤,也就是降采样或者异常流量过滤,最后把所录制的流量落到 ElasticSearch中,这个过程其实就是在构建测试Case,且不需要人工干预。有了这些构建好的 Case,用户只需要在 ElasticSearch 中检索自己需要的场景,或者直接选取最新的一批流量来进行批量回放。

检索方面,其实就是对ElasticSearch进行搜索,比如接口请求和返回有什么参数,下游请求 MySQL 发了什么字段,甚至一些异常场景也能够搜索出来。为了能够让大家能检索更多的场景,我们针对不同的协议做了文本化处理,比如Thrift协议、MySQL协议等,都能在ES中检索出来。

将流量查询出来后,接下来就需要进行回放。做回放,除了将所有的网络请求 Mock 掉之外,最重要的一点,还需要将时间、本地缓存、配置等回溯回去,即回到录制的时间点。为什么这么做,大家可以想一下,我们在进行回放时,业务逻辑里是不是有很多是根据时间戳处理的,比如缓存时间等,如果不把时间回溯回去,它的逻辑跟线上可能就完全不一样了。

最后就是进行线上和线下流量 Diff,比如 Diff 线上的 Response 与测试的 Response 是不是一致,发送下游的 MySQL、Thrift 、HTTP等请求流量是否跟线上一致,如果不一致,就是有 Diff,当然这里会有噪音,我们也做了很多降噪处理。最终,基于流量的Diff来判断回放的成功与否。

我们做过验证,我们用大概一万条录制的流量进行回放测试,与线上的服务做代码覆盖率的对比,最终发现,大概只有 2% 到 3%的差距,也就是说几乎可以覆盖所有场景。

基于此,我们可以在不需要人工干预的情况下,就可以自动化的构建高覆盖率测试场景,然后大批量的进行回放,从而达到高覆盖率的回归测试。像我们前面提到的迁移框架、迁移Go技术栈,都依赖于这种方式。

以上就是我们解决回归测试挑战的解法。

这套技术方案已经在滴滴的线上流水线跑了,上游的模块已经全部接入进来。每次上线或者提交代码时,会批量的全跑一遍。但是我们不会跑一万条,我们会对每个模块进行流量筛选,大概会从一万条中筛选出几千条。当然有些模块的流量,只筛选出几百条,因为大部分流量是重复的,这样上线的流水线其实只需要跑几百或者几千个 Case 就可以了,大概 5 分钟就能够回放完成。

接下来说测试阶段的第二个挑战,也就是如何在微服务架构下构建测试环境,滴滴其实也走过一段弯路,想靠个人或者一个团队就构建起整个测试环境。刚开始,我们把所有的服务都塞到一个镜像里,在最初服务少的时候还能运转,大家用得也比较顺畅,毕竟每个人只需要申请自己的镜像就可以了。

但是随着越来越多的服务加入,环境越来越不稳定了,也很难构建起来,没有人可以搞定,甚至有些人开始去预发环境进行测试,风险非常高。

其实在“如何在微服务架构下构建测试环境”这个问题上,主要有两个难点,也可以说是挑战,一个是如何低成本的构建测试环境,最起码是在线下把整套测试环境搭建起来;而另一个挑战则是如何低成本的保证人手一套测试环境,并且能够互不影响。

通过下图可以看到,滴滴的完整测试环境叫做“线下基准环境”,不得不提,这得益于滴滴在云原生方面的投入,大幅降低了新增机房的成本。我们只需要在线上节点下增加一个机房节点,把基准环境与线上节点绑定到一起,这样做的好处,除了能复用线上能力,比如报警、监控、日志采集等, 还能让业务代码在上线、回滚时都与线上服务保持同步,保证了测试环境的仿真度。

为了实现人手一套测试环境且互不影响,我们基于 Go 语言自研了一套 Sidecar,即在所有的基准环境前都部署一个 Sidecar,通过流量染色的方案按需构建测试环境,这是借鉴了 Service Mesh 和业界一些关于流量染色的思路。

以上图为例,假设我只需要开发 S1 ,那我只需要把 S1 构建好,其他依赖都用基准环境即可。当前,在网约车开发高峰期,我们基本上能保持 1000 套左右的测试环境,能保证每人一套环境,甚至可以一人多套环境。当然,我们还会复用线上云原生能力进行资源超卖、定期释放等能力,极致压缩成本。

运维 – AIOps

在运维方面,我们前面也提到过,主要问题有两个,一个是报警多、但不准、难维护,另一个是定位难、止损慢,而针对这两类问题,除了组织和机制的优化外,在技术层面,我认为最好的解法就是 AIOps。AIOps 的概念是由 Gartner 在 2016 年提出的,核心思想是通过机器学习来分析海量的运维数据,帮助运维提高运营能力。

根据滴滴的业务场景,我们把 AIOps 分为了三个方向:监控告警、根因定位以及故障恢复。

稍微展开讲一下,在监控告警中,最重要的事情:一是时序异常检测,因为大多数的指标都是时序数据,并且不同时间会有不同的数据,不同的指标也有不同的曲线,这是异常检测很重要的一个地方。二是指标拆解、分类,我们需要把一个指标拆解为不同维度的指标。举个例子,我们监控“完单量”,不只是简单监控完单量就够了,而是需要把不同的产品线、不同的城市等维度指标做笛卡尔乘积,衍生出N条曲线,然后进行曲线分类。三是自动建设告警,在指标拆解完成后,需要根据不同的曲线类别配置不同的告警策略,由于曲线很多,此过程不需要自动化建设,同时可以定期的根据业务变化来调整曲线的报警策略。

而根因定位的逻辑,其实就是模拟人的定位过程,大家可以回想一下自己在进行根因定位的时候思路是什么样的。是不是根据报警信息或者用户反馈的信息,然后检索大脑里的知识图谱,找到最有可能的原因,逐个去排除,直到找到根因。

在滴滴的事件中也是类似过程,只是我们把它做成了自动化,我们会通过一些相关性分析、因果推断以及一些人工标注等构建好知识图谱,并将其沉淀下来,就想我们大脑的知识图谱一样。接下来,我们会结合链路追踪、报警信息、事件变更等数据,综合决策出最有可能的根因。

拿一个具体的例子来说,主要分为以下几步:

收集黄金指标

这个数据非常重要,前面我们研发阶段提到的统一数据,其实就是在做这个事情。在整个运维体系里,最重要的就是标准化数据,其实就是服务机器学习的特征工程,这个几乎占了70%以上的成本。

做指标分类

刚才也提到过,时序数据的曲线是不一样的,比如完单量,在一些大城市,完单量曲线可能会随早晚高峰出现明显的波峰波谷,而在一些小城市,完单量的曲线可能没那么明显,这些指标都需要进行分类。

确定报警策略

以全国的数据来说,数据的曲线相对平滑且有规律(早晚高峰,工作日,节假日等),可以选择ETS(指数平滑)策略,通过学习历史数据预测未来数据,然后设置一个置信区间,超过置信区间即报警。但对于小城市无规律的曲线,可能需要兜底的策略。当然业界有很多种异常检测算法,主要分为监督和无监督的算法,滴滴是更倾向于无监督的方式,因为监督学习需要大量标注数据,但线上出现的异常点非常少,数据量不够。

告警屏蔽

当真正发生事故时,可能会出现告警风暴,这时想要短时间内找到哪个是因哪个是果很难,反而会扰乱大家的排查思路。报警屏蔽中大概有两种思路,一种是进行单策略屏蔽,这是一种比较简单的方式。比如我们配置了 CPU告警,某一个节点有 1000 台机器在报“CPU掉底”,那我们可以将这 1000 条报警合并为 1 条;另外报警的时间可以设置为 10 分钟1次或者 30 分钟1次。

第二种思路,主要是依赖于根因定位来判断出哪些报警是可以被屏蔽掉的。比如完单掉了,可能是因预估失败导致的,这时预估可以报警,而完单没有必要报警。

根因定位

定位的逻辑也分为两类,一类是横向定位,或者叫做业务定位。还是以完单量来举例,完单量下降的时候,首先要判断出是司机导致的和乘客乘客导致的,如果是乘客掉了,那是发单掉了,还是预估掉了?是小程序掉了还是主端掉了,如果是小程序掉了,那可能就从小程序出发去排查了。

另一类是纵向定位。如果已经定位出来是预估全国掉了,接下来就需要进行纵向定位,也叫技术定位,分析跟预估相关的服务,比如通过 Trace 链路追踪,Metric 指标,饱和度指标、变更事件等,以定位到报警的根因,大多数事故都跟变更事件有关,所以所有线上变更都会有记录,方便及时查询。

故障恢复

定位到根因后或者定位到可以选择降级预案后,接下来就是要恢复故障。所谓的故障恢复,其实并不是说在每次报警出现时,临时选择降级方案,而是要在报警出现之前做大量的放火和演练,提前将有可能的风险点建设好预案,然后止损阶段只是执行提前建设好的预案。

大家可以看下面这幅图,这是滴滴网约车现在正在使用的定位机器人,我们内部管叫它东海龙王。它基本上已经替代了我们大部分的定位工作,通过东海龙王我们可以几秒内把所有相关信息检索出来,比如根据接口成功率定位到是哪个机房出问题了 ,如果能够定位到某一个具体的机房,那我们就不需要再进行根因定位,可以直接操作切流止损了,等恢复后再来排查根因。

另外,我们会把所有的状态码返回的信息,包括 Trace 都摘取出来,并把其中的一些关键信息提取到东海龙王中,比如上图中的错误码、错误信息等都会有提示,还有 RPC 定位和针对 RPC 的降级预案,CPU、内存、磁盘检查信息等等。最重要的一点还会at负责人,因为在实际的止损过程中,有很多降级还需要人工进行决策。

未来 – 自动化部署

如果对整个 DevOps 进行人工和自动化区分,我们发现在每个环节里,尤其是在大规模的微服务架构下,人工的瓶颈越来越突出,比如在开发阶段,人工主要是制定规范,然后进行 Code Review等,但是每个人的能力、标准甚至同一个人不同时间点的心情都不一样,很难到达一个相对平稳的水平,而通过自动化,我们可以通过静态扫描、编译检查、框架约束等方式,把静态检查的能力提升到一个相对稳定的水平,包括我们刚提到的 AIOps 运维方式,也会将运维能力保持在一个稳定的水平,而我们可以通过不断优化自动化能力,来提高我们的能力下限。

基于这个理论,滴滴在尝试构建一个完全自动化的流水线,比如把从 Code Review 到部署回滚都变成无人值守,完全依靠自动化的保障手段来提前发现问题,并通知负责人,不但可以提升上线效率,也可以把稳定性保障下限提高到一定水平。当然所有这一切都离不开滴滴多年在基础能力上的建设,比如云原生能力的建设、可观测能力的建设、混沌工程的建设等等。

END

作者及部门介绍

本篇文章作者魏静武,来自滴滴网约车出行技术团队,出行技术作为网约车业务研发团队,通过建设终端用户体验平台、C端用户产品生态、B端运力供给生态、出行安全生态、服务治理生态、核心保障体系,打造安全可靠、高效便捷、用户可信赖的出行平台。