成立四年,估值已超260亿美元,公司指数级发展、业务爆炸式增长,在此背景下,滴滴出行业务系统的架构升级是怎样进行的?
本文根据滴滴出行平台产品中心技术总监——杜欢在2016ArchSummit全球架构师(深圳)峰会上的演讲整理而成。
杜欢
,
滴滴平台产品中心技术总监
。2015 年加入滴滴,负责公司公共业务、客户端/前端架构和新业务孵化,致力于用技术手段解决业务痛点和提升研发效率,曾作为技术负责人主导公司技术架构升级以支撑公司业务快速迭代的需求。在加入滴滴前有长达五年的创业经历,具有丰富的团队管理经验,熟悉移动互联网应用的整个技术栈。
今天给大家主要介绍的是去年滴滴内部做的一次重大架构升级,滴滴快速发展的过程中,系统的迭代速度和其他方面的设计遇到了很多困难,这次升级就是为了解决这些困难。
去年我们做了一次非常大的重构。上面图中是今天要讲的大纲,我会从问题本身出发,回顾一下整个过程,包括如何发现问题、分析问题和解决方案。最后,我也会提出一些想法,如何规避重蹈这样的覆辙。
挑战在哪里?
首先,我们看一下挑战在哪里。滴滴在出行领域是非常独特的公司,它的独特不在于业务模式多复杂,而在于它的发展非常快。滴滴的成立时间是2012年的6月,到现在为止才经过了四年的时间。
滴滴的成长速度十分惊人,到今天它的估值已经超过260亿美元,融资轮次非常多。如果不是因为竞争非常恶劣,滴滴也不会一直用融资的方法为自己开路。在这样的压力之下,滴滴所有的动作可能都会走形,所有的想法可能因为现在一些短期利益不得不进行一些权衡。
同时,公司的业务也在爆炸式地增长。如果滴滴只做一个业务,原本可以做得非常深入。滴滴从2014年开始加入了专车业务,2015年业务数量增加到七条,2016年已经超过十条。业务急速发展之中大家会思考,到底怎么做才能使这些还不稳定或者还没有想清楚的业务很好地迭代起来。
想到最简单的方法是,
如果新业务跟某个旧业务非常类似但又不完全一样,我们就把旧业务的旧代码复制并修改,这样新业务就做出来了
。之前,这种情况经常发生,就造成了很大的问题。
在2015年上半年,滴滴整个系统已经积累了很多问题,分布在乘客App、服务端、Web App之中。特别值得一提的是,服务端的问题并不是性能,而是在于巨大的耦合导致数据紊乱和迭代速度越来越慢。
滴滴的独特性迫使我们独立思考这些问题,所有的解法都要针对滴滴现状,而不是看哪个大公司是怎么做的,然后直接复制过来。
现状是什么?
在解决问题之前,我们需要了解现状是怎样的。如图所示,在2015年下半年,滴滴的系统架构分为四层。最顶层是用户应用,每一个用户应用就是一个端,也就是用户所能看到的入口。然后是接入层,这是非常传统的结构,我们用了Nginx,还专门做了TCP接入层。
在业务层,Web是非常大的集群,有非常大的代码量,我们只对业务做了分割,有策略引擎、司机调度。在数据层,有KV集群、MySQL集群、任务队列、特征存储。这是任何一个初创公司应该有的架构,我们对这个架构并没有做特殊的策划,仅仅在这个技术体系里面把业务逻辑实现出来。
上面这张图可能会比较有趣。右边这个红色的球,代表的是重构之前App依赖的关系。当时我很想梳理一下App在模块之间是如何进行依赖的,然后我就写了一个脚本运行了一下,得到的结果让我很惊讶。我用蓝色的线表示正常的依赖,就是模块A依赖于模块B,A是B的上一层,B不会反过来依赖A,用红色的线表示异常的依赖,即A依赖B、B通过各种手段反过来依赖A,最后发现基本上都是红色的。
做任何模块的拆分,发现不得不面临这样的问题:把任何一个模块取出来就等于把所有模块都取出来,实际上没有做拆分。所以,关键是需要解耦模块结果。这是iOS的情况。安卓的情况更糟糕。
对于Web App来讲,最大的问题在于耦合性。以前滴滴只有出租车这个业务,最开始的Web App只有出租车,后来专车上线了,就在出租车里面加了专车入口,只是业务名不同界面会有小区别,后来加入了快车、代驾,都跟出租车差不多,没遇到太大问题。
再后来有了顺风车,顺风车跟其他功能不一样,整体界面是预约型的,有乘客和车主两种模式。如果在老首页里面开发顺风车成本太大了,需要和出租车业务线的人一起开发业务模块,如果未来做迭代,这种开发模式将非常痛苦。老首页的模块也没有做拆分,代码散落各地,只是通过打包工具拼接在一起,没有做模块化,所以整体情况也比较糟糕。
相比端,API稍好一点的是,API至少在业务维度上是分开的,出租车与专车、快车是分开的两个系统,放在两个仓库里面。不过API也有一个很大的问题,业务代码没有做服务化拆分,没有model 封装,业务所有的API和后台MIS都在一个仓库里,这对系统来说是非常大的一个隐患。
该如何入手?
现状看上去很糟糕,要仔细思考才能入手。最基本的思路是把所有事情分类,就像整理自己家里一样,无论多乱,我们要做的事情就是将东西分门别类放好。因此,最关键的是要了解到底哪些东西应该放在一起,我们用颜色来比喻模块或者代码的归属,核心问题就变成这些模块到底是什么颜色。
我们的思路是,先从前面,也就是
从用户入口进行拆分,要先保证所有的模块是足够内聚的,由统一的团队负责。
比如,出租车业务线可以完全控制自己的代码,能够写自己的客户端,也能够写自己的Web App,最终只是通过一些工程构建手段将多个业务整合起来变成一个完整的端。
做到这一点之后,所有的业务迭代问题就迎刃而解了,因为业务间已经没有依赖和耦合了。这一步完成之后做的就是重新梳理业务,让业务根据自己模型特点进行一些重构。
最开始的时候,我们考虑的是怎么做代码治理和模块下沉。代码治理本质上就是把各种模块进行染色、再把它们归类的过程。代码治理最难的事情在于消除错综复杂的依赖。到底怎么做才对呢?
-
首先,一定要把不同模块的代码放在不同仓库里面,使得模块能够物理上隔离。特别是Java、Obj-C这些静态编译的语言,一旦把代码仓库隔离就完全没有办法直接对其他模块产生依赖,至少绝对不会再出现循环依赖。
-
再者,就看如何把循环依赖通过一些间接层隔离开,比如通过抽象接口隔离开,一点一点把代码拆到不同仓库。
-
最后,有了这样一个简单的拆分之后,就需要考虑怎么让模块能独立的开发、测试、上线。独立的流程一旦独立起来,就意味着拆分基本上成功了。
模块下沉与代码治理息息相关。
如果只是要求把所有代码拆分,而没有合适的拆分方法,这件事情是无法推进下去的。对于程序员来说,他们内心总有一种冲动想做有意思的事情,比如封装一个很有意思的模块给更多程序员用。大家并非不想做封装,只是如果封装并共享出来的代价太大,就会影响大家的热情。
模块下沉是一种机制,一方面我们应该鼓励,另一方面还应该让大家发现这是一件不得不做的事情。
如果仅仅对内公开模块列表让大家自由选择,达不到模块下沉的目的。因为人都很懒,不想思考太多,只想尽快把事情完成,大家往往倾向于复制粘贴,也不愿意额外花时间做下沉。
怎么办呢?我们会给所有业务提供一个统一的SDK,里面包含所有能用的组件,大家必须使用它进行开发。如果业务模块稳定了并且比较通用,我们有工具和相应的简单机制把业务模块下沉下来,变成SDK的一部分,长期下去SDK会越来越大,只要SDK里做好分类和规划,上层就会越来越轻,我们可以真正专注于业务逻辑开发。
除了上面这些,最核心的一点在于,
一定要把所有业务都做到“无状态”和“异步化”。
“无状态”这个概念在服务端比较容易理解。
一般我们倾向于把各种业务做到无状态,这样容易做水平扩展。在客户端也是一个道理,也要考虑横向扩展性。一个简单的框架往往提供一些最基础的控件,比如按纽、列表,这些都不会耦合任何业务逻辑,所以很容易使用。
但是当业务做起来,大家习惯将一些状态放到业务控件里面,这在一定程度上方便了,但是一旦需要将业务进行重构或者进行模块化下沉的时候,就造成了非常大的困难。例如,一个模块如果大量通过全局变量或单例跟上下游耦合,那么这个模块就很难复用和重构,这些全局变量或单例就是状态。
所以,我们在客户端也提出使用“无状态”的方式,把存储的信息都放到外面。后面我会提到到底应该怎么样去做。
“异步化”也是解耦的方式。
服务端的RPC类似于函数调用,如果参数变了,实现和调用的双方都要做改变,这很不透明,也不能够渐进式上线。我们用订阅/发布的模式对 RPC进行解耦,要求所有接口都要异步返回。
在客户端也是这样,比如做数据的缓存,想优化网络,我们不能够期待这个函数是一个同步函数,一定用回调的方式接受所有参数。所以做设计的时候,只要是有可能发生网络请求或者访问磁盘,在客户端也尽量异步请求数据。
刚刚讲的都是相对比较抽象的内容,接下来会说一下滴滴的业务形态本身。
滴滴是一个出行的平台,涵盖的是整个出行领域所有的出行需求。大家出行到底想要什么?就是到达自己想去的地方。实际上,我们的模型可以做得非常抽象和简单。比如,我想要打快车去机场,我就是一个需求方,我的需求会发到很多服务者那里去,服务者会根据特征进行一些匹配。
最基本的特征是服务能力,如果服务者能够开快车并通过了能力验证,这个需求就有可能发给他。如果开出租车的也有能力开快车,但是他还没有在平台上验证这个能力,就只能开出租车。一个人可以验证很多服务,白天可以开快车,晚上可以做代驾,做不同的事。
服务和需求的匹配是通过计价模型和匹配策略来实现的。发送需求的时候需要选择计价模型和车的类型。快车和专车服务过程大同小异,但是价格差别很明显,专车价格会贵很多。通过匹配策略可以实现各种需求的匹配。
例如,选择了拼车,这个需求会尽量匹配已经有拼友和顺路的车。如果选择专车,可以要求这辆车在指定时间来接人,这时候匹配策略会优化倾向这种方式。
滴滴所有的业务基本上都是以这种模式运转的,所有功能都是核心主干或者旁路,只要把业务模型抽象出来,基本上就能够满足大部分的业务了。
基于这样的想法,我们就思考如何设计真正高度抽象的工具。简单起见,我们把滴滴出行的过程抽象成一个框架(见上图),这并不是完整的框架。有颜色的地方表示出租车、快车、专车、代驾共同的流程,只要组合各种流程就可以实现整个业务形态的能力。在这个框架里可以定制所有业务形态的车标、提示语、匹配的模型、计价模型等功能。
当时梳理这个抽象的时候,我们感觉非常兴奋,因为这意味着在这个基础之上就可以简易扩展出滴滴未来的业务形态。只要滴滴还是在做需求和服务的匹配,基本上就离不开这样一种套路。
客户端怎么拆?
然后我们开始落实到具体该怎么拆的问题。
首先就是客户端,最重要的是需要将业务拆出来。
以前所有业务放在同一个仓库里,如果不小心提交了一段错误代码就会带来灾难性的后果,所有业务工作可能都会受到影响。以前编译速度也很糟糕,大家可以想象,每次下载代码都会有几个头文件发生改变,由于循环依赖的缘故几乎所有文件都要重编,二三十分钟后才能重新调试,这个过程让人极度崩溃。
对于iOS,我们用cocoapods把业务拆到不同的pod里面;对于安卓,我们把业务拆分打包并用Maven管理起来。我们拆分方法如下图所示,其中虚线框部分展示的是公共框架,最开始没有很细致分割,只是把它放在一个独立仓库里,保证依赖关系充分清楚,后面就可以随时把代码独立出来,使其变成单独的模块。