前几天拜读了 OpsGenie 公司(一家致力于 Dev & Ops 的公司)的资深工程师 Turgay Çelik 博士写的一篇文章(连接在文末),文中介绍了他们最初也是采用 Nginx 做为单体应用的网关,后来接触到微服务架构后开始逐渐采用了其余组件。nginx
我对于所作的工做或者感兴趣的技术,喜欢刨根问底,因此当读一篇文章时发现没有看到我想要看到的设计思想,我就会四处搜集资料,此外这篇文章涉及了我正在捣鼓的 Spring Cloud,因此我就决定写一篇文章,争取能从设计思路上解释为何会有这样的性能差别。程序员
技术介绍
文中针对 Nginx、ZUUL、Spring Cloud、Linkerd 等技术进行了对比(其实还有
API 网关
API 网关出现的缘由是微服务架构的出现,不一样的微服务通常会有不一样的网络地址,而外部客户端可能须要调用多个服务的接口才能完成一个业务需求,若是让客户端直接与各个微服务通讯,会有如下的问题:后端
- 客户端会屡次请求不一样的微服务,增长了客户端的复杂性。
- 存在跨域请求,在必定场景下处理相对复杂。
- 认证复杂,每一个服务都须要独立认证。
- 难以重构,随着项目的迭代,可能须要从新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分红多个。若是客户端直接与微服务通讯,那么重构将会很难实施。
- 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有必定的困难。
以上这些问题能够借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,全部的外部请求都会先通过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控能够交由 API 网关来作,这样既提升业务灵活性又不缺安全性,典型的架构图如图所示:设计模式
使用 API 网关后的优势以下:api
- 易于监控。能够在网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。能够在网关上进行认证,而后再将请求转发到后端的微服务,而无须在每一个微服务中进行认证。
- 减小了客户端与各个微服务之间的交互次数。
NGINX 服务
Nginx 由内核和模块组成,内核的设计很是微小和简洁,完成的工做也很是简单,仅仅经过查找配置文件与客户端请求进行 URL 匹配,用于启动不一样的模块去完成相应的工做。跨域
下面这张图反应的是 HTTP 请求的常规处理流程:浏览器
Nginx 的模块直接被编译进 Nginx,所以属于静态编译方式。启动 Nginx 后,Nginx 的模块被自动加载,不像 Apache,首先将模块编译为一个 so 文件,而后在配置文件中指定是否进行加载。在解析配置文件时,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 客户端注册到
这种状况是比较简单的状况,只须要将多个 Zuul 节点注册到
2. Zuul 客户端不能注册到
假如说咱们的客户端是手机端 APP,那么不可能经过方案 1 的方式注册到
如图所示,Zuul 客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个 Zuul 节点,这样就能够实现 Zuul 的高可用。
Spring Cloud
虽然 Spring Cloud 带有“Cloud”,可是它并非针对云计算的解决方案,而是在 Spring Boot 基础上构建的,用于快速构建分布式系统的通用模式的工具集。
使用 Spring Cloud 开发的应用程序很是适合在
既然是工具集,那么它必定包含不少工具,咱们来看下面这张图:
这里因为仅涉及到 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 博士的那篇文章里使用了
实验中启动了客户端和服务端两台机器,分别安装多个待测试服务,客户端经过几种方式分别访问,尝试获取资源。测试方案以下图所示:
Turgay Çelik 博士的此次测试选择了三个环境,分别是:
- 单 CPU 核,1GB 内存:用于比较 Nginx 反向代理和 Zuul(去除第一次运行后的平均结果);
- 双 CPU 核,8GB 内存:用于比较 Nginx 反向代理和 Zuul(去除第一次运行后的平均结果);
- 8 个核 CPU,32GB 内存:用于比较 Nginx 反向代理、Zuul(去除第一次运行后的平均结果)、Spring Cloud Zuul、Linkerd。
测试过程均采用 200 个并行线程发送总共 1 万次请求,命令模板以下所示:
ab -n 10000 -c 200 HTTP://
<server-address>
/<path to resource>
注意:因为 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,它自己是对于资源比较敏感的一种网关设计,因此在通用环境下拿它和其余网关实现相比较,可能会出现不许确的结果。
做者介绍
周明耀,毕业于浙江大学,工学硕士。13 年软件开发领域工做经验,10 年技术管理经验,4 年分布式软件开发经验,提交发明专利 17 项。著有《大话 Java 性能优化》、《深刻理解 JVM&G1 GC》、《技术领导力 程序员如何才能带团队》。微信号 michael_tec,微信公众号“麦克叔叔每晚 10 点说”。
感谢郭蕾对本文的策划和审校。