API网关是什么?
要回答这个问题我们需要先了解下我们得到的架构变迁。
我们公司最早的时候都是PHP实现的单体应用,比如生活作风的H5商城,得到的V3。这张图就是我们得到的早期架构,当时所有的业务逻辑实现全部在V3当中,然后DCAPI封装了与数据库的交互。这就是一个典型的单体应用架构。
然后到17年的时候,随着公司人员越来越多以及微服务的兴起,我们也开始进行服务化的改造。但是在进行服务化的时候首先面临的一个问题就是:当我们把一个单体应用拆成众多微服务之后,每一个服务如何与客户端进行通信?原来客户端只需要和V3进行对接,现在难道要让客户端分别与这么多服务进行对接么? 这显然是不可行的。
所以,实际上这时候我们与所有进行微服务落地工作的团队一样面临微服务的一些痛点。那么解决这些痛点的方式,一般业界通用的是引入一个叫做API网关的组件。也就是这样的微服务架构。
现在我们就可以来回答网关是什么?
API网关一般作为系统与外界联通的入口,在微服务架构中,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。
网关的核心功能
- 路由转发
- 路由重写
- 访问控制
- 流量控制
- 负载均衡
- 健康检查
- 服务发现
- 熔断降级
网关的设计目标
- 具备Cloud-Native特性
- 部署支持无限水平扩展
- 高性能,高可用
- 开箱即用,功能易扩展
- 支持多机房部署
网关的抽象模型
- 应用:一组路由的集合,抽象为应用,一般为一个业务或一条业务线
- 路由:归属于应用,定义了可以通过网关访问的路由规则及限流策略
- 服务:对应一个真实的后端服务,可以绑定在多个应用的不同路由上
- 节点:每个节点即一个后端服务节点,支持监控检查及负载均衡
这四个模型的关系如下:
网关的高可用方案
我们采用etcd来存储配置,通过ETCD的高可用来保证网关集群的高可用和水平无限扩展。
网关的易扩展设计
我们还是希望整个系统能够具备一定的扩展能力,为了避免调度系统过于复杂,所以我们想采用类似Gin框架中间件的形式来实现易扩展。这样每个请求进来之后会穿过所有的中间件,中间件也就可以在这个过程中对其进行操作控制。
Gin框架中间件的巧妙之处主要在于通过Context的Next方法进行自调用实现了一个拦截器。这样,不论在中间件中是否调用了Next都不会影响中间件的执行顺序。如果看代码不好理解,可以看下面这两张图。
网关的内部组件
最终,在易扩展、高可用的基础上,我们在系统内部实现了上图这样的内部结构。
- APIServer负责提供配置变更的接口
- 当它收到配置变更请求时将配置数据写入ETCD
- 由Watcher组件对ETCD进行watch并把最新的数据映射到Model上,同时编译中间件的HandlerChain
- 代理服务器负责接收需要转发的请求,开始执行HandlerChain
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
反向代理
反向代理是网关的基础功能,所以我们首先就对Golang实现的反向代理做了压测,如上图采用Golang官方的net.http包实现。经过多次压测验证,在4C8G的服务器上只能压到19000+,将近两万QPS。这结果令我们很愕然,因为大家都知道Golang的并发处理能力是不弱的,所以我们又单独对Golang实现的HTTPServer进行了压测,实现代码如下图。
最终压出了88000+的QPS,这结果才令人满意。那么我们就分析,Golang的http.Client性能可能远低于Server。我们都知道Golang社区除了官方的net.http包还有一个fasthttp,那么能不能使用fasthttp来替代反向代理中的Client部分呢。见过反复验证,也是可以的。最终我们压出了这两张火焰图。
正则匹配
Tips: 在热点代码中避免使用正则,如果无法避免,那么一定要进行预编译。
字符串拼接
Tips1:单次调用时,操作符+ > strings.Join >= bytes.Buffer > fmt.Sprintf
Tips2:多次调用时,bytes.Buffer >= strings.Join > 操作符+ > fmt.Sprintf
频繁创建对象 Vs 复用对象
可以看到明显的提升,所以在热点代码中如果有创建对象的操作,要尽量进行复用。
Slice查询 Vs Map查询
Map查询的时间复杂度为O1,而Slice查询的时间复杂度为On,我们直观上理解Map肯定是比Slice快的。但是实际的情况并不是那么绝对,可以看到上面的例子中,当成员数量为10时我们遍历整个slice的速度都比map的一次查询快。经过研究map底层代码,我们发现这是因为map底层还有hash的操作。
所以,最终经过我们测试,在不考虑key大小的情况下,成员数量小于25时slice的性能要好于map。
高性能总结
- 避免反射和锁的使用
- 避免创建过多的对象(避免GC)
- 尽量复用已经创建的对象(sync.Pool)
- 避免进行[]byte和string的转换
- 设置GOGC用内存换CPU时间
- 对于精度不高的时间自己实现时钟
- 数据量较少时用slice替代map