如何用Go打造亿级实时分布式出行平台|文末福利

高超 · · 14352 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

前言

Grab是东南亚最大的出行平台,业务覆盖东南亚7个国家39个城市,APP下载量高达3600000次。随着业务量的持续增长,为了解决系统性能瓶颈,打造一套高可用的系统架构,Grab 从最开始的 rails、nodejs 到完全转向 Go。除了全部的后台服务,每天支撑亿万级数据处理的流式数据系统也是基于 Go 打造的。本文来自 Grab 高级工程师高超在 Gopher China 2017大会上的精彩分享,以下是对他演讲内容的整理。






Grab是一家东南亚出行平台,致力于解决东南亚人民的出行问题。Grab 的主要市场是东南亚,我们也没有进军中国市场的打算,我们和滴滴是战略合作伙伴的关系。Grab 成立于2011年,目前在东南亚7个国家都有运营,覆盖了39 个城市。目前有710000位司机,3600000次 APP下载。我们还是一个非常年轻的创业公司,目前在在新加坡、北京、西雅图、越南和印尼都有研发中心。我本人目前是在西雅图研发中心工作,是 Grab 大数据研发团队的 Tech Lead。


Grab 从前的技术栈


图 1


我们创业之初跟其他创业公司一样,都是采用一些比较成熟的手段进行第一版本开发,刚开始的时候用 rails、nodejs,从第一天开始我们所有的服务都运行在亚马逊云上,我们还用 Travis CI 和 MySQL。

 

Grab 现在的技术栈

 

图 2


随着业务量持续增长,以前那一套框架渐渐承载不了我们的业务规模,于是我们做了一个很大的转型。现在的技术栈是这样的,如图2。我们依然在亚马逊云上运行我们的服务,在大数据和机器学习领域也做了很多方面的开发。其中最醒目的也是跟大会最相关的,就是最右边的 Go。现在Grab后台服务都是用 Go 打造的。

 

为什么会转向Go?

 

首先是因为当时我们的服务有非常大的性能压力,经过了一段比较黑暗的时期。当时的服务一天会坏好几次,于是我们就思考往哪个方向转型能实现比较高性能的整体架构,最后就选择了Go。原因有几点,首先Go的语言规范非常简洁,上手很轻松,我们现有的程序员很快就能转向Go的开发,学习的曲线不是很陡峭,同时Go的生产效率也非常高。Go拥有非常完善的工具链,它有自己的测试框架,这些都能够帮助你写出很规范的代码。Go还拥有非常方便的部署流程,相比之前需要部署一大堆东西,Go打包好以后部署非常方便。同时,Go的性能也非常好,从实际的经验来看,用Go以后弹性云机器数量骤减90%,响应延迟平均能降低80%。

 

Grab用Go做什么?

 

我们用Go写了很多东西,像我刚才提到的全部后台服务目前都是用Go来打造的。现在大概是有50多个微服务,这个数量还在不停的上升。我们现在工程师Gophers数量有300个,今年的目标是扩招到800个。我们用Go打造的流式数据系统,大家如果对打车应用服务比较熟悉的话,会知道它会有很多实时信息需要处理,这些信息是支撑你做调度和决策的东西,流式的数据系统就在做这件事情,这也是完全用Go写的。还有 API Gateway、RPC & RESTful framework等。同时我们还自己写了ORM,我们为什么自己写ORM,大家知道做后台服务一个很通常的瓶颈,就是数据系统,我们想通过自己打造的ORM来约束数据库访问的模式,禁止JOIN和Transaction的使用,并且在服务与数据库中间加了一层数据访问层。这样服务的业务逻辑就不需要关心具体数据库技术的选择和转型,这样的一个架构在我们迁移到微服务框架的过程中给了我们很大的自由度。


我们还用 Go 做了一个 CI system,还有一个非常重要的机器学习平台。机器学习现在是非常火的一个话题,在我们的业务领域里面也是非常重要的话题,大家可以想象,每天有成千上万的乘客想要打车,你怎么样合理分配距离他比较近的司机,要看司机与乘客之间的距离、司机乘客评分等,这些都是机器可以帮我们解决的问题。接下来我们正在做的事情就是Serverless  platform,类似于亚马逊的 Lambda你上传一个函数,整个服务的机器运维,运行环境,部署你都不用关心,你关心的只是自己的业务逻辑,可惜 Lambda 只支撑几门语言你关心的只是自己的业务逻辑,它只支撑几门语言,我们自己想到就是做一套自己的系统,专门为Go打造。这个平台可以帮助我们更加快速的迭代产品的功能,让开发者更有效率。

 

Grab的Go实践

 

接下来跟大家分享一下我们用Go两年半积累的一些经验。重要的有这么几点:首先怎么样管理你的代码,第二就是讲讲在微服务领域现在比较火的话题Distributed Tracing,然后是Go的测试,怎么样管理你的代码质量,最后一个是我们在Go里面写过的Bugs和遇到的问题。

代码管理

我们的代码管理可能和很多公司的方法不太一样,我们所有后端服务的Go代码都放在一个Repository 里面。这样做有一些好处,首先你有一致的版本,都可以引用公有库,公有库升级之后服务A、B拿到不同的版本,当服务B遇到的问题找维护者,说你怎么没升级,他会回答你不知道有升级这回事儿。如果全部放在一个Repository里面,就不会存在这个问题,因为所有版本都是可见的。另外一个是极致的代码分享和复用,别的服务写的代码你都可以知道。大家可能都有这样的经验,在不同团队里面程序员喜欢造轮子,一个公司里面时间格式化的函数可能有十几个版本,你不知道别人已经有了,所以自己又写了一个。但是如果全都在一个Repo里面是可见的。


另外一个好处是依赖管理非常简单,如果有公有库在不同的Repo里面你要更新它,需要部署不同的依赖管理。在一个代码组织方式下,你可以随时打包进公有库。同时代码的更改可以做到原子化,比方说我们一开始写Go的时候,没有什么经验,很多最佳的实践也不熟悉,写了一段时间之后发现一个模式非常好用,我们想把很多公有库函数加上这个代码,我们先要改公有库,再改服务A的代码,用户代码也要改,服务B的代码改完之后要改B的代码,这样无论更改还是什么都会造成很大的问题,但是我们代码都放在一个Repo里面。这样的方式,就可以支撑很大规模的重构,代码库的更新。同时团队之间也可以更好的合作,因为沟通的障碍可以被消除。还有更灵活的团队界限,大家可能都经历过一个服务移动到另外一个服务,如果你放在一个Repo里面就是一个文件夹的更改。代码透明度自然而然就最大化了,假如说你调度团队下面有十几个服务,这十个服务都放在调度团队文件夹里面,代码的归属权就会非常清晰。


这就是我们管理代码的方式。这个方式有这么多好处,当然也有它的弊端。它的弊端其中之一就是所有市面上的CI系统没有为这样的代码组织方式做优化,所以你在用这些系统的时候,效率会非常低。这也是为什么我们花时间打造了自己的CI,总而言之这样一个方式有自己的好处,但你要有足够强大的工具链支撑才能让好处凸显出来。

Distributed Tracing

图 3


图 3 是今年2月份我们系统之间服务间的关系,可以看到杂乱无章。在一个创业公司架构的演变中,从单体应用到大规模微服务架构的时候,都会产生出这么一个很杂乱的图来。之前在单体应用里面,不同的功能就是函数,函数变成了服务,每个服务加入越来越多的依赖。在你遇到问题的时候,如何快速发现及诊断服务存在的问题,这就是Distributed Tracing可以帮助我们的。有如下几个场景:一个请求耗时三秒才能完成,如何诊断何处耗时最多;如何定位Single Point of Failure;如何定位并发现循环依赖关系;如何定位Fan IN,Fan Out。这都是Distributed Tracing可以帮助我们解决的问题。


它的实现原理是:当一个请求进入你系统时,在API Gateway生成一个全局唯一的traceID,并将其注入请求的Header里,在该请求的每个耗时节点生成一个spanID,以traceID+spanID为索引计时,并记录其他元数据,将tracing信息顺着请求的流动一直传下去,当请求结束的时候,以 traceID 为key来聚合所有诊断信息。

图 4

图 4是一个基本的 tracing,大家可以看到,从一个客户端发出的请求,它第一个事情可能就是做一个 auth,然后请求用户的账单信息,然后再读取其他资源。耗费节点三个,然后再请求流出系统的时候我们做一个聚合,这就是基本的工作流程。

图 5

图5是我们系统里面的真实图,这是一个请求司机评分,在他进入之后会调用另外一个后端服务,那个服务会调用司机的评分,他的数据存在 mysql,耗时两毫秒。有了这么一个图,你想诊断你的性能信息或者系统的瓶颈,就可以帮你很快的缩小范围诊断。而且这个力度,是你可以自己控制的,我们这边只做到这几个,当中可能还有一些计算当天时间之类的函数,你都可以加上去。同时,你还可以根据服务间的协议做一些分析,比如服务A和B之间的协议,服务B必须在100毫秒之内返回请求,服务B实际花了120毫秒,根据这个图就可以到服务B的维护者里面,为什么这个慢,可以做到密度的监控。

图 6

在 Go 里面,相关的信息传递是通过 Context 来做的。这里建议大家多用善用 Context,如果公司刚开始接触 Go 还没有用很多 Context 的时候,抓紧时间,如果有一定代码量的时候再想加入 Context,会花出非常大的努力。Context 可以提供很有用的辅助函数,具体的我有列在 PPT 里,大家也可以到 Go 的官网学习 Context Pattern https://blog.golang.org/context

这里还介绍一下 Open Tracing,它不是一个框架而是一个库,只要做成这个库都可以以同样的方式实现聚合。你不需要做任何事情,你只需要开启 Open Tracing 开关,这个东西非常好,掌握标准就等于掌握全世界。比较理想的情况下,我们用的数据库这些都可以实现,这样我们每一个服务每一个环节都可以做到非常透明的监控。

Go Testing

我们在Grab最常写的测试包括两种,第一种是单元测试,另外一个是端到端的测试。

先讲单元测试,图 7的代码大家应该都不陌生,表格驱动的测试。最基本的是你定义一串测试场景,然后定义你要测试函数的输入和输出和有没有期待错误,在边就可以用一个循环进行不同场景的测试,扩展性非常好,当你想要测试另外一个场景的时候,你只需要在场景定义的 struct 里面添加就可以了。我们做单元测试的时候,用的 testify 这个包,它可以提供很方便的函数,也是内部的规范。

图 7


讲到单元测试就引出另外一个话题,我们做单元测试的时候,刚开始写Go年少无知引出很多其他不好的习惯,做出不好的实践。其中之一如图 7 这个东西,数据库是一个全局的单例。我测试的时候不想写数据库,首先CI数据库本身不稳定,我不知道是数据库出错还是我程序出错。同样全局的单例,我们也写了很多把函数定义成变量,在另外一个地方测试的时候直接替换函数的定量,你的代码结构会变得非常难看,这是一个比较不好的实践。

图8

经过我们的不断努力学习,我们现在有了一些稍微好的实践,如图 8。在接口里面约束数据库有两种行为,把数据库作为一个依赖,注入到Server里面。这样的好处是,在每个测试的时候,我可以实际化不同的Server,这样就可以互相之间不影响,代码写起来就会轻松很多,整个架构就会清晰简洁很多。

端到端测试由于时间不够展开讲,简单提一下。我们有不同的集群,有一个Staging集群,还有一个Production集群,端到端测试发生在Staging集群上。端到端测试有一点很重要,你一定要有真实的环境,你数据库要是真实的,所有东西都要真实。我们有两种不同的端到端测试的方法,第一是Postman,另外就是用Go自己的测试框架写端到端的测试。

代码质量管理

我们讲了很多测试的东西,测试是为了发现代码里面的问题。为什么会有这些问题,归根结底就是代码写的不好,所有这些议题中最重要的一个,怎么样去管理你的代码质量。我们的观点是Code Review非常重要,但是它的重要性经常会被忽视。我相信没有一家公司会把Code Review 列为KPI。当你同事代码写的不好你怎么提醒他,你看到一段比较乱的代码,我会想这个说的太重会不会影响同事关系,说的不清晰又会影响质量。所以Code Review的重要性,经常会被小事忽略。

图 9


解决这个问题就是要用好的工具提高Code Review的效率,减少Code Review带来的人与人之间的摩擦。我们用的工具主要是这三个,Phabricator、Jenkins和Slackbot。Phabricator是facebook开源的Code Review工具。这三个工具具体如何协作起来工作呢?首先当一个代码写好,我第一件事情想要部署这个代码,我们是工程师我们想要创造价值,可是代码的质量是不是能够部署下去,也是一个问题。工具就是为了保障每个代码至少是能看的,怎么做呢?当我们的工程师写好代码,他会有一个命令,它首先会跑Linter,你的代码基本条件有没有保证边缘条件有没有检查到。我们之前是用GoLniter,我们现在也在研究自己的。Linter检查什么东西呢?比如说你有一个错误,它就会说这个错误不能忽略,要明确的解决这个错误。你的代码虽然可能是可读的,但全是漏洞,我们还要跑一遍测试,在本地跑一遍代码更改所有包测试,你的代码会不会引进其他的漏洞,全跑一遍。跑这个测试的过程中,会有测试覆盖率的报告。假如说你的测试覆盖率没有达到我们想要的水准,你的代码审查是不会被提交的。因为测试覆盖率是我们审核代码质量一个很重要的指标,所以说我们通过这样的方式,来尽可能保证看到的代码是有一定的质量。

图 10


接下来代码提交了,我们会把我们的代码放到CI里面,CI里面跑的就是更大规模测试,包括简单的集成测试。我们有用一串CI的脚本都是用Go写的。代码被传上去了,收到请求说我要测试,跑单元测试和端到端测试,过了5分钟,CI成功了,代码成功了。小机器人就会发一条消息给你,恭喜你,你的代码测试成功了。Phabricator会清晰显示你代码哪个部分被测试所覆盖,哪个部分没有。比如这个代码块错误检查的地方就没有被测试覆盖,蓝色就被覆盖。这个可以提升效率,首先你写的测试合理合法,测试覆盖过的代码,就不需要花太多精力检查漏洞,测试没有覆盖到的地方,或者提交上来的地方没有覆盖到,可能本身就有问题,这是成功的情况。

图 11

如果你的CIBuild失败了,会发生三件事情:Slackbot就发送消息作者没有通过;Phabricator会自动Reject Diff;Phabricator Code Review工具会显示哪个测试没有通过。大家应该都有经验,莫名其妙有一天代码被改了,在任何更改你的公有库的代码情况下,你希望你能得到消息,知道发生什么事情。当你看这些画面的时候,就证明你这个Diff是不符合标准的,你就要花一些时间把Diff重新测试写一下提交。这些东西都发生在服务器端,我们在做的一件事情,你测试的失败率是会被统计起来,你如果长期样就会收到邮件,告知你最近代码质量非常差,需要改进一下,你还会有一个冻结期,会有一个通知。

另外一个CI跑过程中做的事情是跑代码覆盖率。大家可以看到,这套东西就是我们用Go写的集成在CI里面,红色字体是代码覆盖率没有符合我们标准的,当覆盖率没有达到统一标准的时候,你的Diff也是会被拒绝的。通过这些工具,我们希望把工程师对代码的心态调整好一些,因为工程师程序员最重要的就是代码,你代码都写不好的话,谈架构都是白谈的。我们想通过一系列的东西,提高工程师的编码严谨态度。

我们曾经遇到的和Go相关的问题

图 12

Go里面我们常见的两个问题,一个是Nil Pointer, 另一个是Dns Resolution。首先讲Nil Pointre。右边这个图大家应该很熟悉,大家可以看看这个代码有没有什么问题。我们有一个structA,它自己有一个成员函数Test,我们有一个getA生成的函数,这个函数返回一个nil,我们调用函数获得A的时候执行Test,奇怪的是A到Test这条打印消息能够被看到。当你一个getA返回A指针类型的时候,你可以调用Test的函数。如果大家有一个类似getA有很多分支的函数,它在很边缘的条件下会返回nil的条件下,大家要注意了。有人讲过,让空值有用起来,如果你想撤nil可能你需要加这一行,这个代码在很多库里面有出现过,大家有兴趣的话,可以去看看这个视频(https://www.youtube.com/watch?v=PAAkCSZUG1c&t=6m25s ),是讲述Go空值的运用以及合理处理的问题。这是曾经困扰我们很久的问题。

图 13

我们还有一个问题是DNS Resolution。AWS ELB请求没有被均衡的分布在不同的机器上。这个RFC是定义你服务程序受到DNS发过来IP地址的时候,怎么样做一个选择来返回你的应用,Go一开始用C写的,在C的库里面,如果看到你对外接口里面只有IPv4的时候,它不会对列表进行排序,它会尊重DNS排序选择返回你的应用程序,但是Go的库只实现了子集,这样导致的结果Go每次都会把DNS返回的结果以同样的规则进行排序。可想而知,在同一个子网里面收到的IP地址从列表里面第一个选,在列表中间第一个IP自然而然就会收到更多的请求。我们作为一个Go社区的良好用户,第一时间得到的回复,这个会在1.9里面修复。

图 14


Grab还在用Golang做什么

图 15

讲了这么多,我们现在还在用Go做什么呢?如图我们在做数据的处理,整合数以亿计的乘客,司机以及交通的信息,在做函数式计算以及机器学习平台。





Go 中国粉丝专属福利在本文下提供精彩留言,获赞最多的三位读者即可获得由博文视点提供的《Cloud Native Go : 构建基于Go和React的云原生Web应用与微服务》新书一本!

Asta、郝林、张鑫、费良宏等大家联袂推荐 Go 领域图书,四大优势:

1.引导读者了解云原生理念的产生、应用场景、优势

2.集现今诸多热点技术之大成

3.掌握Go语言助理云开发完美实现的方法

4.流程完整,示例具体详细

集赞截止时间:8月21日14:00


有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

14352 次点击  
加入收藏 微博
被以下专栏收入,发现更多相似内容
1 回复  |  直到 2017-08-18 17:47:20
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传