2013 年底,我关闭当时的创业项目,无所事事之时,打电话向快的 CEO Dexter 请教,当时快的和大黄蜂刚刚合并,他建议我可以先和大黄蜂 CEO 李祖闽(Joe)聊聊。
和 Joe 第一次见面是在虹桥火车站的一家肯德基里碰头,当时我看不太懂打车这个项目。那次碰面,我们聊的却不是出租车,聊的全是专车。两年多前的那时候,做这一块的不多,真正意识到这是个大机遇的且投入资源的创业公司比较少。Joe 跟我讲了许多他对未来专车的构想,一下子就挑起我非常大的兴趣,简单说就是 High 了:)。
Joe 也跟我讲了现在公司在技术上碰到的一些瓶颈,限制了业务的开展,比较痛苦,我们原定半个小时的碰面,结果一聊聊了 4 个多小时后,两人意犹未尽,又一起打车回市区聊了一路,下车的时候我和他约好第二天就去大黄蜂报到。
故事就这样展开了。。。
定时炸弹
2013 年 12 月 26 日,刚入职的第一天,我看到了打车大战满地销烟的惨状,快的、滴滴、大黄蜂等多家打车公司刚刚在上海打过几波大战,当时大黄蜂打车在上海市场出租车市场的占有率可以排进前三。快的大黄蜂合并后,虽然明确大黄蜂未来的战略方向转做专车业务,同时也上线了第一版本的专车系统,并将部分出租车请求转发给快的打车。但出于种种因素考虑,还是把一大部分的工作重心依然留在出租车上面,并没有完全放弃出租车业务。
我初进大黄蜂,又恰逢临近春节,专车系统刚刚上线不久,我们想要在上海做一次“春节 50/100 元专车接送机场(虹桥、浦东两个机场)”的活动,但原来的产品及部分开发人员出现了一些波动,正在陆续办理离职手续,留下的开发人员状态也不太稳定,好在专车系统的即将离职的同学还比较靠谱,被我拉着讲了一个周末的数据库表结构及代码,于是我就是在边看代码边熟悉系统的基础上,进行业务开发,技术总监写代码不算什么,其实后面重构上线前期我还做过一整周的测试。
2014 年春节很快过去,快的和滴滴在全国掀起一波补贴大战,大黄蜂的出租车业务在上海市场协同快的也侧面参与了一些,但大部分精力已经开始转向专车业务,我对原来的系统也已经比较了解,当时的系统情况大致是这样:
当时出租车主要开了两个城市,上海和广州,各部署一套系统,数据库、缓存、Web 服务、API、定时任务都在两地部署,由中心库负责定时同步主要的数据,比如用户信息等,但因为两地市场开拓的时候,运营策略不同,比如各项优惠、加急令、司机补贴等政策在两地各有不同,于是就维护了两套 PHP 代码,大致逻辑相似,细节又各有不同。
而专车系统是后来开发,以 Java 为主要开发语言,基础数据(用户、优惠券、订单等)依然依赖于出租车数据库,但主要数据库和代码部署在上海,因为专车只开了上海市场,所以暂时没有什么影响。这样的系统架构限制了当时的业务发展,也给未来留下了许多定时炸弹,下面我们会展开讨论。
但在这里需要申明一点:看到这样的系统结构,确实存在许多不合理性,单纯一张九十几个字段订单表就能干掉一大票观众,但我要为其辩护,在当时打车大战初期,业务的发展是野蛮式的,每天各种补贴政策、活动的调整,开发人手严重不足,技术完全是被业务拖着跑,相信经历过创业这个阶段的人都会有深刻感触。
很显然,这样的系统架构已经不能满足当前的业务需求,越往后拖,隐患越大,定时炸弹越多,我评估下来,主要存在这么几类大的问题:
1、多地保存数据,多地多份代码不可维护。
- 多地逻辑代码冗余:代码无法维护和管理,添加一个业务逻辑要同时改两份码,或者只改一份,但下一次增加新业务逻辑时,因为两个地区的代码逻辑差异越来越大,需要非常小心,坑越挖越深;
- 数据同步问题:上海的乘客到广州不能下单、优惠券不见了,反之亦然,上海的乘客出差去外地后,也不能下上海的机场、车站的预约订单,甚至不能登陆;
- 并发冲突:两套数据的维护,一不小心就出现数据不一致的问题,无法合并,也留下被攻击的隐患;
2、无法快速开城,每开一个城市都要部署一套存储和代码,准备硬件、进机房、部署、调试,效率太低,还要做好数据的同步,及当地的新政策的业务开发,开一个城市需要一个月左右的开发周期。
3、数据库表结构和代码许多地方都是堆出来的,不可维护,比如一张订单表有九十几个字段;PS: 你没有看错,就是九十几个字段。
4、手机端通过轮询接口调用 API,为了快速获取成单、通知、补贴政策调整等信息。但毫无疑问,这个对于手机的资源消耗(流量、电量)很大,也增加服务器的开销。
5、公共服务和业务代码耦合太紧,比如优惠券、支付等模块;
6、专车司机公里数计算不准确,出租车司机带有打表器,专车司机主要靠手机 GPS,手机会有信号不准,接受不到 GPS(跳点、断点、隧道、高架)等情况;
7、系统压力大,活动一上来,慢查层出不穷,各个状况出现;
8、API 接口没有加密机制,容易被刷,用户身份容易被窃取;
9、无法适应专车灵活多变的业务场景,每做一个活动,都需要 2,3 周的开发;
还有许多细节,比如轮询接口直接访问数据库、GPS 坐标系混乱、司乘价格绑定、重复上报 GPS、电子围栏不可维护、支付、定位、保险、第三方接口接入等,已不能满足业务发展,这里就不再一一列举。在这样的前提下,内部反复多轮沟通后,我们启动了重构计划。
和时间赛跑的重构马拉松
2014 年 4 月 9 日,我们正式启动了重构,重构目标就是在解决这些问题的基础上,完成整个系统的第一阶段的服务化工作,大致目标包括有:
- 设计一套灵活的专车系统,以应对专车复杂的业务场景;
- 整个系统,包括存储及服务集中部署,便于管理维护;
- 需要支持出租车、专车两项业务的迅速开城;
- 手机端和服务器的实时消息(包括接单、抢单、成单、通知、计费等)推送采用长连接;
- 接口定义一套的新的数据协议,请求加密,防 token 窃取,请求防篡改;
- 采用多级缓存方案,数据库、Redis、MongoDB、分布式本地缓存;
- 引入分布式日志系统(重构后期来不及加入,只做了一部分,但后期为此吃足苦头,快的的兄弟们给了我们很大的帮助);
- 公共服务及业务服务的抽象及服务化;
- 南、北向接口分离,南向接口主要对接外部供应商,北向接口仅指出租车部分,主要面向第三方渠道、比如携程、去哪儿等。
- 数据库分库、分表、读写分离;
- 优化寻找周边司机,道路距离计算等算法;
- 上阿里云;
第一阶段的服务化设计后的架构图,大致如下:
上图中大部分的设计都实现了,并取得了不错的效果,但也埋了坑。其中订单服务在重构中期因为出租车业务的停止,为了贪图调用方便,又把它合到专车核心服务,后续在订单量爆发的时候,为此吃足苦头。出租车司机 API 也因为方向问题停止维护。
在当时的环境下,重构这样的一个系统主要有这么几大挑战:
1、 时间不够:项目计划梳理需求和旧有系统逻辑 2 周,开发 & 测试 10 周,上线 2 周,预计在 7 月底左右上线。
2、 开发资源严重不足:前线的还在继续作战,原来系统还需要有人留下做维护性开发,真正能抽出来做后端系统重构的开发人员只有 5 个(包括我),4 个 Java 开发,1 个 PHP 开发,其中有 3 个都是新招的开发人员。APP 开发也只有 4 个人参加重构,同时负责 iOS、Android。
3、 对公共模块业务不熟悉:因为专车系统是搭建在原出租车系统之上,所以大部分公共模块都依赖出租车业务,而原出租车业务的开发人员流动比较大,代码也过了好几手之后,留下来的人员对其业务细节,遗留代码并不了解,这将对后续的数据迁移留下很大隐患;
4、业务复杂:如何设计一个灵活多变的专车系统,以适应快节奏的运营策略。专车因为其特性决定了它的业务复杂多变,随便举几个例子:
- 策略 1:希望做接送机场订单一口价的活动,春节前,终点为虹桥 / 浦东机场,起点为全上海任意地区的,可享受 50/100 元一口价,春节后,起 / 终点倒置,为回程的乘客服务;
- 策略 2:当某种车型不够用时,只有下机场单或者特定的好用户时可以看到;
- 策略 3:高峰期时,高级车型可以向下接单,但司机收入不变,给抢单积极的司机更好的订单;
- 希望在上海内环以内做一口价活动,20 元一口价,随便打车,但司机价格会根据活动情况分等级调整;
- 策略 4:好的预约订单给抢单积极的司机,好的即时订单给近的司机;
- 策略 5:多加一个专车产品,带有婴儿椅的车子,女性乘客可以打到;
- 策略 6:打一个专车,如果时间超过 2 个小时,希望变成包车服务,但价格要有给乘客优惠;
- 策略 7:我想让某个起点的乘客享受费用折扣,甚至免单,但平台按正常价格给司机计费;
- 策略 8:每开一个城市,我想对机场、火车站的订单实行一口价策略;
- 策略 9:在某些城市,有一部分司机是合作司机,一部分加盟司机,两者计费规则完全不同,需要满足;
基本上,这样的策略每个月都有,全国几十个城市,每个城市可能还不同,如果需要通过开发来满足这样的需求,那么几百人的开发团队都不够用,运营时间上也等不了。所以,如何设计一个高可用,灵活多变的系统来适应业务的需求,这是一个非常大的挑战。
5、技术挑战:剩下还有就是一些技术上的挑战,比如快速搭建一套分布式服务框架,如何快速找到周边的司机、如何通过道路拟合比较准确的计算道路距离、长连接的 QoS 如何保证、司乘两端心跳上报异常如何处理、如何防止心跳风暴、规则引擎的性能优化、发单 / 抢单处理队列的守护机制等。
如何解决这几个难题呢?
先说开发资源吧,开发资源属于硬伤,创业公司前期招人,招到好的人比较难。业务等不了,有句话说的好“有条件上,没有条件创造条件也要上”,这就是当时的情况,人就这么多了,边做边招吧,后面两个多月我们陆陆续续招了六七个开发人员,但新招聘的大部分经验还比较浅,但也基本上顶过去了。
再说到公共模块,当我入职一周左右的时候我就知道系统必须大规模重构,但当时因为种种因素提重构必然会给业务、团队造成比较大的影响,也会影响刚刚合并的团队信心,所以前期更多是花精力在整合团队和拖着专车系统往前走。
公共模块的代码都是在出租车业务系统,向原来的开发人员了解细节时,被告知许多历史遗留问题已不可追遡,只知道不要轻易去动,一动就崩。于是,重构计划开始后,我挑了一个对原出租车业务比较了解的哥们,拉着他两个人慢慢翻 PHP 代码,翻数据库表,通读一遍,才开始抽象公共模块及进行数据库设计。
其次,说到专车系统灵活多变的设计,我首先把专车业务的预估、选择车型、发单、抢单、撮合、做单、计费、通知等流程环节做了抽象,并固化,把可变的部分剥离。
-
比如预估算价、显示车型、计费时将司机和乘客完全分开,实际上在业务抽象的角度来看,专车的乘客和司机发生的交易对象都不是对方,而是平台,这样就平台在运营过程中就具备非常灵活主导权;
-
比如消息通知模块做了抽象,把不同阶段消息体抽象成模板,模板中带有变量,举个最简单的例子:如参加某个活动下单的用户和普通下单用户,在下单或计费时收到的短信内容不同,但实际只是短信模板不同,中间的价格用变量替换,有的甚至不发短信;
-
比如某些特定场景,第一轮发单不希望发给某类司机,要第二、三轮才发给他,撮合成单时也根据不同维度的指标进行排序定义;
-
比如将优惠券可用在不同业务场景定义成模板,再模板上增加 Scope,Scope 又反作用到产品上面;
-
比如,对专车运营中最经常发生变化的预估、可见车型、发单、抢单、撮合逻辑,通过将乘客和司机的特征指标抽象,这个乘客、司机、订单三个纬度的特征指标抽象很关键,在后期和大数据结合,也可以将乘客司机的画像指标引入,再利用规则引擎来配置每个环节的变化,见下图:
在将以上所有这些灵活配置的内容之上,又定义了大小产品的概念,把这些灵活多变的配置参数抽象成一个个产品,每个产品维护这些配置的参数或规则,从而达到运营策略和运行系统的隔离,实现了一套灵活的专车业务系统。
这些产品对应给乘客看到的其实是一个个车型,但看起来同样是经济型的车,在后台根据不同的条件已经被抽象配置成不同的定义规则,如机场订单的经济型和非机场的经济型就完全是不同的业务规则,无论从预估的价格、发单、抢单、撮合、通知等都完全不同。
这样的设计在重构上线后,业务系统大框架在两年时间基本没有动过,最多是增加为一些新的指标或者函数进行开发,但不会影响主流程,业务系统的架构非常稳定。
市场部门或是业务部门要上线一个新的运营策略,比如一号专车在 2015 年和 Uber 打战的时候,上线一号快车,在研发部门实际只用了两天就已经配置测试完毕,为了配合运营的工作,才拖了一周才上线。
限于篇幅因素,技术细节这里就不再多说,给大家看一个最小片段的机场单的产品命中规则
( 单 v 城市 ==1 ) && ( 函 v 电子围栏 (单 v 目的地, 上海浦东机场) ) && ( 单 v 渠道 ==0 || 单 v 渠道 ==1 || 单 v 渠道 ==201 ) && ( 单 v 类型 ==2 )&&( 单 v 入口 == 0 )&& !((单 v 用车时长 >=1)&&(客 v 版本号 >=4))&&(单 v 回调码 ==0)
题外话:在一次偶然的机会,和行业内的各家专车系统对标时,发现一号专车这种系统设计和 Uber 的设计思路非常类似,所以 Uber 才可以让各城市经理在不同城市开通不同车型做各种各样的活动,Uber 系统应该是经历了数年演化,而一号系统重构头尾只花了 2 个多月,实际上一号的指标维度非常多,有些甚至用自定义函数来组合使用一个指标,因为国内的运营策略变化实在太快太多了。殊途同归吧。
最后再说到时间、时间…. 时间啊!上面提的许多技术问题,如果从时间的维度上考虑,根本就不是问题,什么 GPS 距离计算算法不准、去噪及道路拟合算法不够优、流控还没有做、长连接服务质量不好、SQL 慢查优化,给我们时间,统统都不是问题。重构计划是 4 月 9 号开始, 我们在阅读旧有代码、梳理业务、设计新的数据库结构、搭建团队、搭 API 接口层框架、搭分布式服务架构(Thrift + ZK + DHF SRV Framework + Chukwa[实际没用起来])的过程中,很快就到了 5 月 1 号。
劳动节后,第一波炸弹来袭,听说其他几家打车应用也在做专车,天下武功,唯快不破,我们必须最先上线,于是计划调整改到 7.15 上线,下了死命令,好吧,干吧,需求不能变,时间变了! 临近 5 月中旬,新的需求来了,要求我们直接和快的打车端对接,至少要在重构上线后两周内也上,没有办法,在打车领域竞争的激烈程度超出所有的想象,业务发展的速度都是按天算的。
于是我们把上线时间又提前到 6.15,并计划 6.30 上线快的 APP 端的一号专车,时间,还是时间,需要提前梳理对接流程、接口定义及开发,人呢?时间呢?好吧,继续干吧!没有抱怨,也不谈梦想、更没有什么改造世界的想法,仅仅是一份信任、一个承诺,整个重构团队日夜加班,从 4 月 9 号计划启动,到 6 月 18 号上线,连续 70 天没有休息一天,前后端的重构团队都是全公司来得最早走得最晚的人,终于还是让系统上了线。PS:苦过笑过,留下的都是许多欢乐回忆,当值得书写留念。
上线前一天
上线前一天,虽然已经预演过两次,但大家心里都没有底,因为专车业务量刚刚起来,系统如果真的出问题,将会给对手们一个绝佳的机会。内部在讨论的时候,问的最多的一个问题是如果系统挂了,我们有什么预案,做了两套,一套是切回老系统,但会出现新旧数据不一致的问题,后续补数据非常麻烦;再一套是需要运营的兄弟们支持,系统只记录下单信息,然后出后台报表,由运营同学手工派单,再打电话通知乘客和司机接送,回到原始社会,这就是现实。
通宵不眠,迁移当天增量数据、服务上线、回归验证、强制升级,深更半夜,运营同事们也开着车在路上晃荡着帮忙路测,悦耳的新订单铃声,成单播报不停响起,终于熬过去了。
系统上线:快乐并痛着
系统刚上线,基本的业务流程虽然没有出什么大问题,但随着订单量的迅猛增长,迅速开城,业务变化,各种状况层出不穷:
- 长连接 QoS 没有做好,订单撮合成功,但司机或乘客却没有收到推送通知;
- 司机行驶距离根据 GPS+WIFI+ 基站三种制式的 GPS 坐标值,去噪算法做得不够好,计算出来的距离差异巨大;
- 定位不准,司机有意或无意没有开 GPS、网络,导致发单距离太远;
- 订单量上来,索引不够优化,慢查 SQL 层出不穷,数据库 CPU 百分百;
- 当时上的是阿里云,阿里云的数据库专家在监控应用的时候,曾经给过一份数据库的诊断报告,非常客观的将我们数据库的问题类比成某宝 08、09 年的状态,惭愧;
- 代码没有 Review,小分支的逻辑测试不到位,出现死循环,拖垮系统;
- 当日订单超过 10W 的时候,司机做单时上报的心跳对后台存储(MongoDB、数据库)压力过大;
- 因时间问题,日志系统没有上线,在跨服务的应用中,无法实时监控各服务运行异常并及时告警,为此付出很大代价;
- Redis 在日订单过百万后,也扛不住压力了;
一方面前方捷报不断,另一方面后方销烟弥漫,快乐并痛着,不停的发布新版本,修复 BUG,优化系统,出现过好几次系统大规模宕机,大部分都是因为各种因素(不仅限于慢查)导致数据库不堪重负引起连锁反应,在订单过百万之后更是连续一周的时间,在每天的晚下班高峰期系统负载扛不住,当然后来在前后端团队通力合作下,都顺利优化顶过去了,大致回想一下,挑一些简单的列一下:
- 优化道路距离优化算法;PS:高德地图的兄弟给了许多帮助;
- 所有道路距离计算只取 GPS,基站及 WIFI 获取的点不做为道路计算使用,但听单可以使用 WIFI 的 GPS 点;
- 优化寻找周边司机算法;
- 提升长连接的通讯服务质量,在移动环境下网络不稳,需要双向确认及心跳包客户端打包机制;
- 服务继续拆分;
- 数据库分库、分表、一主多从、读写分离;
- 多级缓存,优化缓存的使用,数据库只做存储,如 Redis Sharding 分 Memory 和 Persistence 两类等;
- 设计了两层的限流熔断方案、服务降级策略等;
- 旧的 SQL 全量 Review,新 SQL 必须 DBA 确认后方可上线;
- APP 端也优化了访问策略;
- 加上日志监控系统,监控系统整体运行状况;Ps:感谢快的兄弟们的支持;
- 增加服务器,这一点我们做得不错,使用了不到一百台的 4 核 *2.8G CPU,16G 内存的服务节点,就挺过了数百万单的业务,且每个服务结点的资源消耗都在个位数;
还有很多细节已经回忆不起来,欢迎大家后续多交流。
一号专车的重构其实是重写,在业务不停往前奔跑的过程中,重写是下策,风险很高,要慎而又慎,好在当时的订单量还不大。大部分时候,系统重构应该尽量要在充分了解业务的基础上,采用分而治之,分阶段进步的方式来,开着飞机换引擎还好,但我见过开着飞机换飞机的重构计划,着实为对方捏一把冷汗,不知道最后结果如何了?当然,如果决定要动手,那还是越早做越好了。
后序
以上行文仅做为系统重构的技术回顾,重构过程艰难而痛苦,它不仅限于技术层面的难度。此后的过程中系统挺过了一轮又一轮浪峰的冲击,也逐渐趋于稳定,基本上算是完成了组织交给我的任务。
2015 年底,Joe 开始了他的新项目——四叶草车险,开始了另外一次从 0 到 1 的挑战之旅,他再次邀请了我,我于 2015 年 12 月 31 日离开滴滴快的,加入四叶草车险平台参与互联网保险的创业项目。
这一次挑战更大,保险业在中国是一个万亿级的市场。据了解,国内的比较大的传统保险公司,一家公司往往都有三、四百个系统在支撑业务运转。保险未来的变化极其复杂多样,我们才刚刚开始,未来还要迎接诸多挑战,在这个过程中,我们需要更多的人才加入,来和我们一起体验新的重构之痛(le)。说句老套的话,我们可以提供业界有竞争力的薪资以及和团队一起战斗成长的经验。
作者介绍
陈美珍(Frank),微信号 zhaocaimaolin,12 年的软件研发以及技术管理经验。擅长互联网的高并发、高可用的分布式系统架构设计,组建并带领团队完成项目的订单从零到数百万量级的突破。对大中型复杂系统的需求分析、抽象、架构设计、拆分、服务化设计及整合也比较擅长,有多年证券、电信等传统业务系统实战经验。
感谢郭蕾对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论