“大家好,我是得到API网关的负责人闫勃,我是15年毕业加入的罗辑思维,当时是在我们的生活作风团队,主要是做电商业务和商城异步任务调度系统。后来,经过两年的工作可能是觉得更喜欢研究底层技术吧,所以当网关项目启动的时候就申请调到了网关这面。今天我给大家分享的就是如何从零研发一款高性能的API网关。
我今天分享的内容主要分为两部分,第一部分就是给大家介绍下我们的网关,从起源到实现。第二部分就是分享一些干货——如何实现一个高性能的Golang系统。
1、API网关是什么?
要回答这个问题我们需要先了解下我们得到的架构变迁。
我们公司最早的时候都是PHP实现的单体应用,比如生活作风的H5商城,得到的V3。这张图就是我们得到的早期架构,当时所有的业务逻辑实现全部在V3当中,然后DCAPI封装了与数据库的交互。这就是一个典型的单体应用架构。
微服务无服务架构
然后到17年的时候,随着公司人员越来越多以及微服务的兴起,我们也开始进行服务化的改造。但是在进行服务化的时候首先面临的一个问题就是:当我们把一个单体应用拆成众多微服务之后,每一个服务如何与客户端进行通信?原来客户端只需要和V3进行对接,现在难道要让客户端分别与这么多服务进行对接么?这显然是不可行的。
所以,实际上这时候我们与所有进行微服务落地工作的团队一样面临微服务的一些痛点。那么解决这些痛点的方式,一般业界通用的是引入一个叫做API网关的组件。也就是这样的微服务架构。
微服务架构
现在我们就可以来回答网关是什么?
“API网关一般作为系统与外界联通的入口,在微服务架构中,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。
1.1、网关的核心功能
路由转发
路由重写
访问控制
流量控制
负载均衡
健康检查
服务发现
熔断降级
1.2、得到老网关
EaseGateway架构图
HTTPInput: To enable HTTP endpoint to receive RESTful request for upstream service.
HTTPOutput: Sending the body and headers to a certain endpoint of upstream RESTFul service.
UpstreamOutput: To output request to an upstream pipeline and waits the response.
DownstreamInput: Handles downstream request to running pipeline as input and send the response back.
EaseGateway内部的基本元素是Plugin和Pipeline,它是一个很牛的系统,他的设计很精美,实现了高度的可配置化、积木化,你可以基于他的Plugin实现很多功能,你也可以自行开发自己的Plugin。
虽然这是一个很牛的系统,但是我认为它不是一个好的产品。原因在于:它追求的高度可配置化导致它被设计成了一个高度插件化的系统。 这样带来的结果就是配置复杂,不能像nginx一样开箱即用。
当我们拿到nginx的时候可以通过简单的配置就可以让它跑起来,虽然nginx也有各种模块,但是人家的核心模块是固化的。而在EaseGateway中,没有一点核心模块。它的所有功能都需要使用Plugin和Pipeline进行编排来实现,这也就导致EaseGateway的调度系统反而成了整个系统的核心。所以,我一度认为EaseGateway其实是一个流式调度系统,网关只是基于这个调度系统组装出来的一个功能而已。。。
得到老网关存在的问题:
不支持开箱即用,需要大量二次开发
基于gossip协议实现的集群存在单点问题
调度系统过于复杂消耗了不必要的性能
配置及其复杂且不支持并发写
集群伸缩时不能自动更新单机限流值
不支持多机房部署
1.3、得到自研网关设计目标
经过老网关一年多的折磨和学习,最终我们希望能够取其精华去其糟粕,研发我们自己的网关。我们的设计目标如下:
具备Cloud-Native特性
部署支持无限水平扩展
高性能,高可用
开箱即用,功能易扩展
支持多机房部署
1.4、得到自研网关抽象模型
应用:一组路由的集合,抽象为应用,一般为一个业务或一条业务线
路由:归属于应用,定义了可以通过网关访问的路由规则及限流策略
服务:对应一个真实的后端服务,可以绑定在多个应用的不同路由上
节点:每个节点即一个后端服务节点,支持监控检查及负载均衡
这四个模型的关系如下:
1.5、网关高可用方案
EaseGateway2.0高可用方案
这个方案是EaseGateway为了解决单点写的问题提出的,在EaseGateway1.0当中是使用Gossip协议实现了一个半成品的Consul,也就是缺少了Consul中使用Raft协议选主的逻辑。但是在2.0中他们准备放弃Gossip,直接使用Raft,也就是ETCD的方案。通过在网关集群中内嵌一个ETCD集群来实现网关的配置存储,这样网关集群同时也是一个ETCD集群。这样带来的好处是:整个系统不依赖任何外部存储,不需要再单独维护一个ETCD集群
但我们认为这有点炫技,ETCD集群本身已经足够成熟了,而自行实现一个ETCD集群的稳定性还需要验证。最重要的是:依据Raft协议的原理,集群节点数量越多写性能越低,所以为了避免这个问题影响到无限水平扩展的目标,在设计上还需要把整个集群分为Write和Read两种节点,这就给将来的部署带来很多麻烦。
得到网关高可用方案
最终,我们还是希望把重心放在网关的功能研发上,在高可用方面就直接使用成熟的ETCD集群,这样,ETCD集群只要搭建好,一般情况下都不需要特别关注,只需要关注我们的网关集群就好了。
1.6、自研网关易扩展设计
我们还是希望整个系统能够具备一定的扩展能力,为了避免调度系统过于复杂,所以我们想采用类似Gin框架中间件的形式来实现易扩展。这样每个请求进来之后会穿过所有的中间件,中间件也就可以在这个过程中对其进行操作控制。
Gin框架中间件的巧妙之处主要在于通过Context的Next方法进行自调用实现了一个拦截器。这样,不论在中间件中是否调用了Next都不会影响中间件的执行顺序。如果看代码不好理解,可以看下面这两张图。
中间件内部不调用Next
中间件内部调用Next
1.7、自研网关内部组件
内部组件调用关系
最终,在易扩展、高可用的基础上,我们在系统内部实现了上图这样的内部结构。
APIServer负责提供配置变更的接口
当它收到配置变更请求时将配置数据写入ETCD
由Watcher组件对ETCD进行watch并把最新的数据映射到Model上,同时编译中间件的HandlerChain
代理服务器负责接收需要转发的请求,开始执行HandlerChain
2、Golang高性能系统实战
工具:
Jmeter:用于发起压测流量
runtime/pprof:采集程序(非 Server)的运行数据进行分析
net/http/pprof:采集 HTTP Server 的运行时数据进行分析
go tool pprof bin/server http://10.2.0.2:8088/debug/pprof/profile
go-torch -u http://10.2.0.2:8088/debug/pprof/profile -f slice.svg
环境:
压测机:8C16G
API网关:4C8G
系统:Centos7
2.1、反向代理
Go反向代理服务器
反向代理是网关的基础功能,所以我们首先就对Golang实现的反向代理做了压测,如上图采用Golang官方的net.http包实现。经过多次压测验证,在4C8G的服务器上只能压到19000+,将近两万QPS。这结果令我们很愕然,因为大家都知道Golang的并发处理能力是不弱的,所以我们又单独对Golang实现的HTTPServer进行了压测,实现代码如下图。
GoHTTP服务器
最终压出了88000+的QPS,这结果才令人满意。那么我们就分析,Golang的http.Client性能可能远低于Server。我们都知道Golang社区除了官方的net.http包还有一个fasthttp,那么能不能使用fasthttp来替代反向代理中的Client部分呢。见过反复验证,也是可以的。最终我们压出了这两张火焰图。
nethttp反向代理:19343QPS
fasthttp反向代理:45654QPS
2.2、正则匹配
路由重写正则测试
路由重写正则测试结果
Tips:
在热点代码中避免使用正则,如果无法避免,那么一定要进行预编译。
2.3、字符串拼接
多字符串拼接测试
多字符串拼接测试结果
单字符拼接测试
单字符拼接测试结果
Tips:
单次调用时,操作符+ > strings.Join >= bytes.Buffer > fmt.Sprintf
多次调用时,bytes.Buffer >= strings.Join > 操作符+ > fmt.Sprintf
2.4、频繁创建对象 Vs 复用对象
频繁创建对象
使用sync.Pool复用对象后
可以看到明显的提升,所以在热点代码中如果有创建对象的操作,要尽量进行复用。
2.5、Slice查询 Vs Map查询
Slice和Map查询对比测试
Slice和Map查询对比测试结果
Map查询的时间复杂度为O1,而Slice查询的时间复杂度为On,我们直观上理解Map肯定是比Slice快的。但是实际的情况并不是那么绝对,可以看到上面的例子中,当成员数量为10时我们遍历整个slice的速度都比map的一次查询快。经过研究map底层代码,我们发现这是因为map底层还有hash的操作。
所以,最终经过我们测试,在不考虑key大小的情况下,成员数量小于25时slice的性能要好于map。