API⽹关性能⽐较:
NGINXvs.ZUULvs.SpringCloudGatewayvs。。。
前⼏天拜读了 OpsGenie 公司(⼀家致⼒于 Dev & Ops 的公司)的资深⼯程师 Turgay Çelik 博⼠写的⼀篇⽂章(链接在⽂末),⽂中介绍了他们最初也是采⽤ Nginx 作为单体应⽤的⽹关,后来接触到微服务架构后开始逐渐采⽤了其他组件。
我对于所做的⼯作或者感兴趣的技术,喜欢刨根问底,所以当读⼀篇⽂章时发现没有看到我想要看到的设计思想,我就会四处搜集资料,此外这篇⽂章涉及了我正在捣⿎的 Spring Cloud,所以我就决定写⼀篇⽂章,争取能从设计思路上解释为什么会有这样的性能差异。
技术介绍
⽂中针对 Nginx、ZUUL、Spring Cloud、Linkerd 等技术进⾏了对⽐(其实还有 Envoy 和 UnderTow 也是属于可选的 API ⽹关,本⽂不予涉及),那我就分别进⾏介绍,当然,⾸先得介绍 API ⽹关。
API ⽹关
API ⽹关出现的原因是微服务架构的出现,不同的微服务⼀般会有不同的⽹络地址,⽽外部客户端可能
需要调⽤多个服务的接⼝才能完成⼀个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
1. 客户端会多次请求不同的微服务,增加了客户端的复杂性。
2. 存在跨域请求,在⼀定场景下处理相对复杂。
3. 认证复杂,每个服务都需要独⽴认证。
4. 难以重构,随着项⽬的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成⼀个或者将⼀个服务拆分成多个。如果客户端
直接与微服务通信,那么重构将会很难实施。
5. 某些微服务可能使⽤了防⽕墙 / 浏览器不友好的协议,直接访问会有⼀定的困难。
以上这些问题可以借助 API ⽹关解决。API ⽹关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API ⽹关这⼀层。也就是说,API 的实现⽅⾯更多的考虑业务逻辑,⽽安全、性能、监控可以交由 API ⽹关来做,这样既提⾼业务灵活性⼜不缺安全性,典型的架构图如图所⽰:
使⽤ API ⽹关后的优点如下:
易于监控。可以在⽹关收集监控数据并将其推送到外部系统进⾏分析。
易于认证。可以在⽹关上进⾏认证,然后再将请求转发到后端的微服务,⽽⽆须在每个微服务中进⾏认证。
减少了客户端与各个微服务之间的交互次数。
NGINX 服务
Nginx 由内核和模块组成,内核的设计⾮常微⼩和简洁,完成的⼯作也⾮常简单,仅仅通过查配置⽂件与客户端请求进⾏ URL 匹配,⽤于启动不同的模块去完成相应的⼯作。
下⾯这张图反应的是 HTTP 请求的常规处理流程:
Nginx 的模块直接被编译进 Nginx,因此属于静态编译⽅式。启动 Nginx 后,Nginx 的模块被⾃动加载,不像 Apache,⾸先将模块编译为⼀个 so ⽂件,然后在配置⽂件中指定是否进⾏加载。在解析配置⽂件时,Nginx 的每个模块都有可能去处理某个请求,但是同⼀个处理请求只能由⼀个模块来完成。nginx和网关怎么配合使用
Nginx 在启动后,会有⼀个 Master 进程和多个 Worker 进程,Master 进程和 Worker 进程之间是通过
进程间通信进⾏交互的,如图所⽰。Worker ⼯作进程的阻塞点是在像 select()、epoll_wait() 等这样的 I/O 多路复⽤函数调⽤处,以等待发⽣数据可读 / 写事件。Nginx 采⽤了异步⾮阻塞的⽅式来处理请求,也就是说,Nginx 是可以同时处理成千上万个请求的。⼀个 Worker 进程可以同时处理的请求数只受限于内存⼤⼩,⽽且在架构设计上,不同的 Worker 进程之间处理并发请求时⼏乎没有同步锁的限制,Worker 进程通常不会进⼊睡眠状态,因此,当 Nginx 上的进程数与 CPU 核⼼数相等时(最好每⼀个 Worker 进程都绑定特定的 CPU 核⼼),进程间切换的代价是最⼩的。
Netflix 的 Zuul
Zuul 是 Netflix 开源的微服务⽹关组件,它可以和 Eureka、Ribbon、Hystrix 等组件配合使⽤。Zuul 的核⼼是⼀系列的过滤器,这些过滤器可以完成以下功能:
⾝份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
审查与监控:与边缘位置追踪有意义的数据和统计结果,从⽽带来精确的⽣产视图。
动态路由:动态地将请求路由到不同的后端集。
压⼒测试:逐渐增加指向集的流量,以了解性能。
负载分配:为每⼀种负载类型分配对应容量,并弃⽤超出限定值的请求。
静态响应处理:在边缘位置直接建⽴部分响应,从⽽避免其转发到内部集。
多区域弹性:跨越 AWS Region 进⾏请求路由,旨在实现 ELB(Elastic Load Balancing,弹性负载均衡)使⽤的多样化,以及让系统的边缘更贴近系统的使⽤者。
上⾯提及的这些特性是 Nigix 所没有的,这是因为 Netflix 公司创造 Zuul 是为了解决云端的诸多问题(特别是帮助 AWS 解决跨 Region 情况下的这些特性实现),⽽不仅仅是做⼀个类似于 Nigix 的反向代理,当然,我们可以仅使⽤反向代理功能,这⾥不多做描述。
Zuul1 是基于 Servlet 框架构建,如图所⽰,采⽤的是阻塞和多线程⽅式,即⼀个线程处理⼀次连接请求,这种⽅式在内部延迟严重、设备故障较多情况下会引起存活的连接增多和线程增加的情况发⽣。
Zuul2 的巨⼤区别是它运⾏在异步和⽆阻塞框架上,每个 CPU 核⼀个线程,处理所有的请求和响应,请求和响应的⽣命周期是通过事件和回调来处理的,这种⽅式减少了线程数量,因此开销较⼩。⼜由于数据被存储在同⼀个 CPU ⾥,可以复⽤ CPU 级别的缓存,前⾯提及的延迟和重试风暴问题也通过队列存储连接数和事件数⽅式减轻了很多(较线程切换来说轻量级很多,⾃然消耗较⼩)。这⼀变化⼀定会⼤⼤提升性能,我们在后⾯的测试环节看看结果。
我们今天谈的是 API ⽹关性能,这⼀点也涉及到⾼可⽤,简单介绍 Zuul 的⾼可⽤特性,⾼可⽤是⾮常
关键的,因为外部请求到后端微服务的流量都会经过 Zuul,所以在⽣产环境中⼀般都需要部署⾼可⽤的 Zuul 来避免单点故障。⼀般我们有两种部署⽅案:
1. Zuul 客户端注册到 Eureka Server
这种情况是⽐较简单的情况,只需要将多个 Zuul 节点注册到 Eureka Server 上,就可以实现 Zuul 的⾼可⽤。事实上,这种情况下的⾼可⽤和其他服务做⾼可⽤的⽅案没有什么区别。我们来看下⾯这张图,当 Zuul 客户端注册到 Eureka Server 上时,只需要部署多个 Zuul 节点就可以实现⾼可⽤。Zuul 客户端会⾃动从 Eureka Server 查询 Zuul Server 列表,然后使⽤负载均衡组件(例如 Ribbon)请求 Zuul 集。
2. Zuul 客户端不能注册到 Eureka Server
假如说我们的客户端是⼿机端 APP,那么不可能通过⽅案 1 的⽅式注册到 Eureka Server 上。这种情况下,我们可以通过额外的负载均衡器来实现 Zuul 的⾼可⽤,例如 Nginx、HAProxy、F5 等。
如图所⽰,Zuul 客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中⼀个 Zuul 节点,这样就可以实现 Zuul 的⾼可⽤。
Spring Cloud
虽然 Spring Cloud 带有“Cloud”,但是它并不是针对云计算的解决⽅案,⽽是在 Spring Boot 基础上构建的,⽤于快速构建分布式系统的通⽤模式的⼯具集。
使⽤ Spring Cloud 开发的应⽤程序⾮常适合在 Docker 或者 PaaS 上部署,所以⼜叫云原⽣应⽤。云原⽣可以简单理解为⾯向云环境的软件架构。
既然是⼯具集,那么它⼀定包含很多⼯具,我们来看下⾯这张图:
这⾥由于仅涉及到 API ⽹关的对⽐,因此我不逐⼀介绍其他⼯具了。
Spring Cloud 对 Zuul 进⾏了整合,但从 Zuul 来看,没有⼤变化,但是 Spring Cloud 整个框架经过了组件的集成,提供的功能远多于Netflix Zuul,可能对⽐时会出现差异。
Service Mesh 之 Linkerd
我想 Turgay Celik 博⼠把 Linkerd 作为对⽐对象之⼀,可能是因为 Linkerd 为云原⽣应⽤提供弹性的 Service Mesh,⽽ Service Mesh 能够提供轻量级⾼性能⽹络代理,并且也提供微服务框架⽀撑。
从介绍来看,linkerd 是我们⾯向微服务的开源 RPC 代理,它直接⽴⾜于 Finagle(Twitter 的内部核⼼库,负责管理不同服务间之通信流程。事实上,Twitter 公司的每⼀项在线服务都⽴⾜于 Finagle 构建
⽽成,⽽且其⽀持着每秒发⽣的成百上千万条 RPC 调⽤)构建⽽成,设计⽬标在于帮助⽤户简化微服务架构下的运维,它是专⽤于处理时间敏感的服务到服务的通信基础设施层。
和 Spring Cloud 类似,Linkerd 也提供了负载均衡、熔断机器、服务发现、动态请求路由、重试和离线、TLS、HTTP ⽹关集成、透明代理、gRPC、分布式跟踪、运维等诸多功能,功能是相当全了,为微服务框架的技术选型⼜增加了⼀个。由于没有接触过 Linkerd,所以暂时⽆法从架构层⾯进⾏分析,后续会补充这⽅⾯的内容,⾃⼰来做⼀次技术选型。
性能测试结果
Turgay Çelik 博⼠的那篇⽂章⾥使⽤了 Apache 的 HTTP 服务器性能评估⼯具 AB 作为测试⼯具。注意,由于他是基于亚马逊(AWS)公有云的进⾏的测试,可能和你实际物理机上的测试结果有出⼊。
实验中启动了客户端和服务端两台机器,分别安装多个待测试服务,客户端通过⼏种⽅式分别访问,尝试获取资源。测试⽅案如下图所⽰:Turgay Çelik 博⼠的这次测试选择了三个环境,分别是:
1. 单 CPU 核,1GB 内存:⽤于⽐较 Nginx 反向代理和 Zuul(去除第⼀次运⾏后的平均结果);
2. 双 CPU 核,8GB 内存:⽤于⽐较 Nginx 反向代理和 Zuul(去除第⼀次运⾏后的平均结果);
3. 8 个核 CPU,32GB 内存:⽤于⽐较 Nginx 反向代理、Zuul(去除第⼀次运⾏后的平均结果)、Spring Cloud Zuul、Linkerd。测试过程均采⽤ 200 个并⾏线程发送总共 1 万次请求,命令模板如下所⽰:
注意:由于 Turgay Çelik 博⼠的测试过程中是基于 Zuul 1 进⾏的测试,所以性能上较差,不能真实反映当前 Zuul 版本的性能状况,后续⽂章我会⾃⼰做实验并发布结果。
从上⾯的结果来看,单核环境下,Zuul 的性能最差(950.57 次 /s),直接访问⽅式性能最好(6519.68 次 /s),采⽤ Nginx 反向代理⽅式较直接访问⽅式损失 26% 的性能(4888.24 次 /s)。在双核环境下,Nginx 的性能较 Zuul 性能强接近 3 倍(分别是 6187.14次 /s 和 2099.93 次 /s)。在较强的测试环境下(8 核),直接访问、Nginx、Zuul 差距不⼤,但是 Spring Cloud Zuul 可能由于内部整体消耗,导致每秒的请求数只有 873.14。
最终结论
从产品思维来看,API ⽹关负责服务请求路由、组合及协议转换。客户端的所有请求都⾸先经过 API ⽹关,然后由它将请求路由到合适的微服务。API ⽹关经常会通过调⽤多个微服务并合并结果来处理⼀个请求,它可以在 Web 协议(如 HTTP 与 WebSocket)与内部使⽤的⾮ Web 友好协议之间转换,所以说作⽤还是很⼤的,因此技术⽅案选型对于整个系统来说也有⼀定重要性。
从我所理解的这四款组件的设计原理来看,Zuul1 的设计模式和 Nigix 较像,每次 I/O 操作都是从⼯作线程中选择⼀个执⾏,请求线程被阻塞直到⼯作线程完成,但是差别是 Nginx ⽤ C++ 实现,Zuul ⽤ Java 实现,⽽ JVM 本⾝有第⼀次加载较慢的情况。Zuul2 的性能肯定会较 Zuul1 有较⼤的提升,此外,Zuul 的第⼀次测试性能较差,但是从第⼆次开始就好了很多,可能是由于 JIT(Just In Time)优化造成的吧。⽽对于 Linkerd,它本⾝是对于资源⽐较敏感的⼀种⽹关设计,所以在通⽤环境下拿它和其他⽹关实现相⽐较,可能会出现不准确的结果。
英⽂原⽂链接如下:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。