罗辑思惟首席架构师:Go微服务改造实践

转自:http://www.infoq.com/cn/news/2018/05/luojisiweiphp

方圆

曾前后在 Cisco,新浪微博从事基础架构研发工做。十多年一直专一于后端技术的研发,在消息通讯,分布式存储等方向有着丰富的经验。我的技术兴趣普遍,主要专一 Go/Java/Python 等编程语言的发展,尤为是在云计算等前沿领域的应用。python

 

1、改造的背景

获得最先的APP就是一个单体的PHP的应用,就是图中最大的黄色块,中间蓝色块表明不一样模块。下面的黄色部分表明passport 和支付系统,这个是在作获得以前就存在的系统,由于公司早期有微信里的电商业务。程序员

image

后来发现有一些业务逻辑并不须要从获得走,还有一些数据格式转换的工做也不须要跟业务彻底耦合,因此加了一层PHP的网关就是下图看到的V3那部分。可是这样作也有一些问题,PHP后端是FPM,一旦后端的接口响应较慢,就须要启动大量FPM保证并发访问,从而致使操做系统负载较高,从这一点上来讲,使用PHP作这部分工做并不合适。redis

image

屋漏偏逢连夜雨

案例一:8/31大故障:2017年8月31日的时候,老板作活动,致使流量超过预期不少,系统挂了两个小时。

案例二:罗老师要跨年

每一年罗老师都要跨年演讲,第一年是在优酷,有200多万人的在线观看,第二年是同时和优酷等视频网站再加上深圳卫视一块儿合做直播,2016年深圳卫视的收视率是地方第一。2017年的老板当时想要送东西,送东西的这个场景比较恐怖,二维码一放出来,就会有大量用户同时请求。算法

最恐怖的事情是,老板要送的东西8月31日的时候尚未,要在后面2个月期间把东西开发出来。一方面业务迭代不能停,一方面须要扛过跨年,因此就须要咱们对业务系统进行改造。spring

改造目标

  • 高性能:首先是性能要高,若是你单台机器跑几十QPS,那么堆机器也很难知足要求。

  • 服务化:服务化实际上在故障以前就已经开始了,而且因为咱们不一样的业务团队已经在负责不一样的业务,实际上也是须要服务化继续作下去。

  • 资源拆分隔离:随着服务化过程,就须要对资源进行拆分,须要每一个服务提供相应的接口,服务之间不能直接访问其余服务的数据库或者缓存。

  • 高可用:当时定的目标是99.9的可用性。

Go的好处不少,最重要的仍是对PHP程序员来讲,上手更容易,并且性能好不少数据库

 

2、改造的过程

首先有一个系统架构图

image

对于系统改造来讲,首先须要知道,系统须要改为什么样子。所以咱们须要一个架构的蓝图。上面就是咱们的架构蓝图。首先须要的是一个统一对外的API GATEWAY,图中最上层的黄色部分。 中间淡紫色的部分是对外的业务服务。浅绿色部分是基础资源服务,好比音频文稿信息,加密服务。下面红色部分是支付和passport等公用服务,最右侧是一些通用的框架和中间件。最下层是一些基础设施。编程

咱们的框架跟基础设施的完善和系统重构是交织进行的,不是说一开始就有一个彻底没问题的设计,随着业务的改造,会有不少新的功能加进来。json

框架和基础设施完善

我不讲应用系统怎么拆分,由于每一个公司业务系统都不同,我讲一下咱们在框架和中间件这部分事情。后端

API gateway

API gateway是咱们和陈皓(著名的左耳朵耗子)团队合做研发的。他们团队对于咱们成功跨年帮助很大,在此先感谢一下。

目的

  • 限流

    API gateway主要的目的就是限流,改造过程中,咱们线上有400多个接口,常常加新功能。咱们能够保证新接口的性能,可是总有在改造过程当中疏忽的老接口,经过API gateway限流能够保证在流量大的时候,老接口也有部分用户可用。

  • 升级API

    大部分的API升级都是跟客户端解决的,可是咱们不太强制用户升级,致使线上老接口存在很长时间,咱们须要在API gateway这一层作一些把新接口数据格式转成老接口数据格式的工做。

  • 鉴权

    在拆分服务以后,须要统一对接口进行鉴权和访问控制,业界的作法一般都是在网关这一层来作,咱们也不例外。

接下来看一下API gateway的架构

image

API gateway由一个write节点和多个read节点,节点之间经过gossip协议通讯。每一个节点最上层有一个CLI的命令行,能够用来调用Gateway的API。下层的HTTPServer等都是一个plugin,由多个plugin组成不一样的pipeline来处理不一样的请求。在后面我会介绍这个的设计。每一个节点都有一个统计模块来作一些统计信息,这个统计信息主要是接口平均响应时间,QPS等。修改配置以后,write节点会把配置信息同步到read节点上,而且经过model模块持久化到本地磁盘上。

image

请求通过了两段pipeline,第一段pipeline基于请求的url。能够在不一样的pipeline上面组合不一样的plugin。假设一个接口不须要限流,只须要在接口的配置里头不加limiter plugin就能够了。第二段pipeline基于后端的Server配置,作一些负载均衡的工做。

接下来看整个API gateway启动的流程和调度方面

image

启动是比较简单的,去加载plugin,而后再去加载相应的配置文件,根据配置文件把plugin和pipeline作对应。右上角的这个调度器分为静态调度和动态调度。静态调度是假设分配5个go routine来作处理,始终都有5个go routine来处理对应的请求。动态调度器是根据请求繁忙程度,在一个go routine最大值和最小值之间变化。

image

API gateway鉴权方面比较简单,客户端调用登陆接口,passport会把token和userid,传到API gateway,API gateway再把相应的token传到这个APP端。客户端下次请求就拿token请求,若是token验证不过,就返回客户端。若是验证经过再调用后端不一样的服务获取结果,最后返回结果给客户端。

最后再强调一下API gateway如何进行

image

咱们在API gateway里面引入两种限流的策略

  • 滑动窗口限流

    为何会根据滑动窗口限流呢?由于线上接口太多,咱们也不知道究竟是限100好200好仍是限10000好,除非每个都进行压测。用滑动窗口来统计一个时间窗口以内,响应时间,成功和失败的数量,根绝这个统计数据对下一个时间窗口是否要进行限流作判断。

  • QPS的限流

    为何还会留一个QPS的限流呢?由于要作活动,滑动窗口是一个时间窗口,作活动的时候,客户拿起手机扫二维码,流量瞬间就进来了,滑动窗口在这种状况下很难起到做用。

服务框架

目的

  • 简化应用开发

  • 服务注册发现

  • 方便配置管理

服务框架的经常使用架构

image

第一种方式是作成一个库,把相关功能编译进服务自己。这里有两个问题,第一个是咱们兼容好几种语言,开发量比较大。还有一个是一旦客户端跟随服务调用方发布到生产环境中,后续若是要对客户库进行升级,势必要求服务调用方修改代码并从新发布,因此该方案的升级推广有不小的阻力。在业界来讲,spring cloud,dubbo,motan都是用这样的机制。

image

还有一种方案是把Lord Balancing的功能拿出来作成一个agent,跟consumer单独跑,每次consumer请求的时候是经过agent拿到Service Provder的地址,而后再调用Service Provder。

  • 好处是简化了服务调用方,不须要为不一样语言开发客户库,LB的升级不须要服务调用方改代码。

  • 缺点也很明显,部署比较复杂;还有可用性检测会更麻烦一点,这个agent也可能会挂。若是agent挂掉,整个服务也要摘下来。

百度内部的BNS和Airbnb的SmartStack服务发现框架也是这种作法。因为咱们内部语言较多,所以选择了第二种作法。

image

在Consul集群中,每一个提供服务的节点上都要部署和运行Consul的agent,全部运行Consul agent节点的集合构成Consul Cluster。Consul agent有两种运行模式:

  • Server

  • Client

这里的Server和Client只是Consul集群层面的区分,与搭建在Cluster之上 的应用服务无关。以Server模式运行的Consul agent节点用于维护Consul集群的状态,官方建议每一个Consul Cluster至少有3个或以上的运行在Server mode的Agent,Client节点不限。

Client和Server的角色在DDNS是没有严格区分的,请求服务时该服务就是Client,提供服务时候就是Server。

NNDS提供出来的是一个SDK能够很容易的集成和扩展为一个独立的服务而且集成更多的功能。采用agent方式,将在每个服务器部署安装获得的agent,支持使用HTTP和grpc进行请求。

image

服务完成启动并能够能够对外提供服务以后,请求agent的接口v1/service/register将其注册的进入DDNS;

  • 注册成功则其余客户端能够经过DDNS发现接口获取到该APP节点信息;

  • 若是注册失败,APP会重复尝试从新注册,重试三次失败则报警;

image

假设服务A须要请求服务B,服务名称为bbb,直接请求本机的agent接口v1/service/getservice,获取到bbb的服务节点信息。

对于agent而言,若是服务bbb是第一次被请求,则会请求Consul集群,获取到服务bbb的数据以后进行本地从cache并对服务bbb的节点进行watch监控,并定时更新本地的service信息;

若是获取失败,给出缘由,若是是系统错误则报警;

这是服务框架基本的接口

image

这个就是客户端调用的封装,能够同时支持HTTP和JRTC,在这个以后咱们还作了RBAC的权限控制,咱们但愿能调哪些服务都是能够作权限控制的。

多级缓存

image

client请求到server,server先在缓存里找,找到就返回,没有就数据库找,若是找到就回设到缓存而后返回客户端。这里是一个比较简单的模型。只有一级cache,可是一级cache有可能不够用,好比说压测的时候咱们发现,一个redis在咱们的业务状况下支撑到接口的QPS就是一万左右,QPS高一点怎么办呢?咱们引入多级缓存。

image

越靠近上面的缓存就越小,一级就是服务local cache,若是命中就返回数据,若是没有就去L1查,若是查到就更新local cache,而且返回数据。若是L1级也没有就去

L2级查,若是查到数据就更新L1 cache/local cache,并返回数据

image

咱们上面看到的是针对单条内容自己的缓存,在整个栈上来看,gateway也能够缓存一部分数据,不用请求透穿。这个5的虚线是什么意思呢?由于数据修改后须要更新,在应用层作有时候会有失败,因此读取数据库binlog来补漏,减小数据不一致的状况。

image

我一直以为若是有泛型代码好写不少,没有泛型框架里面就要大量的反射来代替泛型。

image

多级缓存开始加了以后整个性能的对比,最先PHP是一两百,改为Go以后,也不强多少,后面Go和big cache的大概到两千左右的,可是有一些问题,后面会讲当问题。后面基于对象的cache,把对象缓存起来,咱们跑测试的机器是在八核,达到这样的结果还能够接受。

熔断降级

image

接口同时请求内部服务,service七、八、9不同,service5是挂掉的状态,可是对外的服务还在每次调用,咱们须要减小调用,让service5恢复过来。

image

打开的状态下,失败达到必定的阈值就关起来,等熔断的窗口结束,达到一个半开的状态接受一部分的请求。若是失败的阈值很高就回到关闭的状态。这个统计的作法就是咱们以前提到的滑动窗口算法。

image

这里是移植了JAVA hystrix的库,JAVA里面有不少作得很不错的框架和库,值得咱们借鉴。

经验总结

通用基础库很是重要

刚才讲的性能提高部分,QPS 从600提高到12000,咱们只用了一天,主要缘由就在于咱们经过基础库作了大量优化,并且基础库作的提高,全部服务都会受益。

善用工具

• generate + framework提高开发效率

• pprof+trace+go-torch肯定性能问题

好比说咱们大量的用generate + framework,经过generate和模板生成不少代码。查性能的时候,pprof+trace+go-torch能够帮你节省不少工做。Go-torch是作火焰图的,Go新版本已经内置了火焰图的功能。

image

这是根据咱们的表结构生成相应的数据库访问代码,多级缓存是把全部的访问都要抽象成K-V,K-LIST等访问模式,每次这么作的时候手动去写太繁琐,咱们就作了一个工具,你用哪个表,工具就生成好,你只须要把它组装一下。

定位性能问题的时候,火焰图必定要用

image

好比说定位性能问题就要看最长的地方在哪里,着力优化这个热点的code,压测的时候发现,你们600、900的火火焰图这里有问题,优化完成后以下图

image

其余经验总结

例如:

  • 针对热点代码作优化

  • 合理复用对象

  • 尽可能避免反射

  • 合理的序列化和反序列化方式

接下来重点讲几个操做:

GC开销

举例来讲咱们以前有一个服务会从缓存里面拿到不少ID的list,数据是存成json格式[1,2,3]这样,发现json的序列化和反序列化性能开销很是大,基本上会占到50%以上的开销。

滴滴讲他们的json库,能够提高10倍性能,实际上在咱们的场景下提高不了那么多,大概只能提高一倍,固然提高一倍也是很大的提高(由于你只用改一行代码就能提高这么多)。

其次json饭序列化致使的GC的问题也很厉害,最猛的时候可以达到20%CPU,即便是在Go的算法也作得很不错的状况下。

最终解决的办法就是在这里引入PB替代json。PB反序列化性能(在咱们的状况下)确实比json好10倍,而且分配的临时对象少多了,从而也下降了GC开销。

为何要避免反射呢?咱们在本地建了local cache,缓存整个对象就要求你不能在缓存以外修改这个对象,可是实际业务上有这个需求。咱们出现过这样的状况后就用反射来作deep copy。JAVA反射还能够用,缘由是jvm会将反射代码生成JAVA代码,实际上调用的是生成的代码。

可是在Go里面不是,原本Go的性能是和C接近的,大量用了反射以后,性能就跟python接近额。后来咱们就定义一个cloneable的接口,让程序员手动来作这个clone工做。

压力测试

咱们主要用的就是ab和Siege,这两个一般是针对单个系统的压力测试。实际上用户在使用的过程中,调用链上每个地方均可能出现问题。因此在微服务的状况下,单个系统的压力测试,虽然很重要,可是不足以彻底消除咱们系统的全部问题。

举一个例子,跨年的时候罗老板要送东西,首先要领东西,领东西是一个接口,接下来一般用户会再刷一下已购列表看看在不在,最后再确认一下他领到的东西对不对。所以你须要对整个链路进行压测,不能只压测一下领取接口,这样多是有问题的。假设你已购列表接口比较慢,用户领了之后就再刷一下看一看有没有,没有的状况下,通常用户会持续的刷,致使越慢的接口越容易成为瓶颈。所以须要合理的规划访问路径,对链路上的全部服务进行压测,不能只关注一个服务。

咱们直接买了阿里云PTS的服务,他们作法就是在CDN节点上模拟请求,能够对整个访问路径进行模拟。

正在作什么

分库分表和分布式事务

选择一个数据库跟你公司相关的运维是相关的。分布式事务在我这里比较重要,咱们有不少购买的环节,一旦拆了微服务以后,只要有一个地方错,就须要对整个进行回滚。咱们如今的作法是手动控制,可是随着你后面的业务愈来愈多,不可能全部的都手动控制,这时就须要有一个分布式事务框架,因此咱们如今基于TCC的方式正在作本身的分布式事务框架。

分库分表也是一个硬性的需求,咱们在这里暂时没有上tidb的缘由主要是DBA团队对tidb不熟悉。咱们以前的分库分表也是程序员本身来处理,如今正在作一个框架能同时支持分库和分表,同时支持hash和range两种方式。

API gateway

API gateway上面有不少事情能够作,咱们在熔断和降级作了一些事情。如今一些Service mesh作的不少事情是把不少工做放在内部API gateway上,是作控制的事情,实际上不该该是业务逻辑关心的事情。咱们也在考虑怎么把API gateway和SM作结合。

APM

拆了微服务以后,最大的问题是不方便定位具体问题在哪里。咱们有时候出问题,我叫好几我的看看各自负责的系统对不对,你们人肉看出问题的地方在哪,这是个比较蛋疼的作法。因入APM+tracing以后,就方便咱们来追踪问题在哪里。

容器化

咱们如今的线上环境,仍是在用虚拟机。仿真环境和测试环境已是容器,使用容器有不少好处,我就不一一列举了。这也是咱们下半年要作的重点工做。

缓存服务化

咱们如今有多级缓存的实现,可是多级缓存仍是一个库的形式来实现的。若是把缓存抽出来,使用memcached或者redis的协议,抽出来成为一个独立的服务。后面的业务系统迭代的时候不用关心缓存自己的扩容缩容策略。

我今天分享的内容就到这儿,谢谢你们!

提问环节

Q:经过你的描述,我知道你之前有JAVA方面的经验,咱们Go其实没有一个像JAVA spring cloud这样比较成熟的微服务的开箱即用的解决方案,如今让你从新作服务化转型这个事情,你会怎么选择?

方圆:让一群php程序员学JAVA比较麻烦,学习Go就比较简单。其次如今搞一个微服务框架,其实并无那么难以接受。由于不少开源软件已经提供了对应的功能,因此要造的轮子其实没有那么多。

Q:你的分库分表框架里面有支持水平分库的状况吗?

方圆:支持。

Q:众所周知PHP是世界上最好的语言,大家转成Go,Go的开发效率比PHP(04:49:50)这两种状况。

方圆:我PHP学得不是太好,我本身写Go确定比PHP快不少。咱们团队来讲,Go的开发效率比PHP略低,可是运行性能却好太多。

Q:从新选择一次技术选型,你是上来就选Go,仍是从PHP再演化到Go?

方圆:我确定是上来就选Go,我仍是认为PHP作rest API没有啥优点。


ArchSummit演讲摘要

本次方圆在ArchSummit的分享主要基于罗辑思惟后端平台从其余语言转型至Go的整个实践过程,和你们分享转型的经历。涉及现有Go语言实现的服务框架技术栈选型,其中包括API Gateway,配置中心/服务注册发现,客户端负载均衡,分库分表,任务调度,多级缓存,熔断等多个方面。此外也会涉及到服务监控和容器化相关的工做,以及一些常见的痛点。最后对公司将来技术方向进行阐述。大纲以下:

为何选择Go语言?

  • 选择Go语言的缘由:并发,性能

  • 微服务架构

  • 服务框架

  • API Gateway

  • 配置中心/服务注册发现

  • client side lb

  • 自动分库分表

  • 分布式任务调度

  • 多级缓存

  • 熔断器

  • 服务监控

  • 容器化

遇见的问题:

  • Error处理和缺少泛型

  • CPU使用分析

  • 内存使用分析

  • 公用代码库

将来须要的方向:

  • 分布式事务

  • 分布式追踪

  • Service Mesh

相关文章
相关标签/搜索