微服务网关 | Zuul1.0和2.0我们该如何选择?

微服务网关 | Zuul1.0和2.0我们该如何选择?

閱讀本文約花費: 12 (分鐘)

文章介绍了Zuul1、Zuul2,同步阻塞模式,异步非阻塞模式,编程模型和优劣,希望对大家的学习有所帮助。

在今年 5 月中,Netflix 终于开源了它的支持异步调用模式的 Zuul 网关 2.0 版本,真可谓千呼万唤始出来。从 Netflix 的官方博文 [附录 1] 中,我们获得的信息也比较令人振奋:

The Cloud Gateway team at Netflix runs and operates more than 80 clusters of Zuul 2, sending traffic to about 100 (and growing) backend service clusters which amounts to more than 1 million requests per second.

Netflix 部署了超过 80+ 的 Zuul2 云网关集群,流量经过 Zuul2 集群被路由到后端超过 100+ 的微服务,且每秒钟经过 Zuul2 集群的请求超过 100 万。

Zuul2 看起来很强大,支持异步高并发 (Zuul1 仅支持同步) 特性看起来很亮眼,那么我们是否就应该抛弃 Zuul1,开始拥抱 Zuul2 呢?作为架构师,我们不能盲目追时髦,技术的选择必须基于实践和理性的分析,基于我之前对 Zuul1 的一线落地实战经验,也基于我近期对 Zuul2 的一些调研,我会在本文中对 Zuul1 和 Zuul2 做一个比较客观的编程模型和优劣分析,同时给出我的个人建议。

Zuul 1.0 编程模型和优劣

Zuul1 设计比较简单,代码不多也比较容易读懂,它本质上就是一个同步 Servlet,采用多线程阻塞模型,如上图所示 [图片来自附录 4]。

同步 Servlet 使用 thread per connection 方式处理请求。简单讲,每来一个请求,Servlet 容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被阻塞,阻塞期间线程资源被占用,不能干其它事情。我们知道 Servlet 容器线程池的大小是有限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成容器无法接受新的请求,Netflix 为此还专门研发了 Hystrix[附录 2] 熔断组件来解决慢服务耗尽资源问题。

注意在上图 Netflix 给出的场景中,它的后台服务调用也是启动另外一个 IO 线程来处理的,但是本质上还是阻塞模式,后台 IO 线程在处理的时候,前台容器线程仍然是阻塞的。

同步阻塞模式有利有弊,分析如下图:

优势

同步阻塞模式的编程模型比较简单,整个请求 ->处理 ->响应的流程 (call flow) 都是在一个线程中处理的,这样开发调试比较方便易于理解,比如出了问题跟踪调试比较方便。另外,线程局部变量 (ThreadLocal) 机制在同步多线程模式下可以工作,有些监控产品,例如 CAT 调用链依赖于 ThreadLocal,在同步多线程模式下,CAT 埋点比较方便,调用链关系的展示也比较直观。

不足

我们知道线程本身需要消耗 CPU 和内存资源,且多线程之间切换是有开销的 (所谓的上下文切换 Context Switch 开销),线程越多,这种上下文切换的开销就越大,同步阻塞模式一般会启动很多的线程,必然引入线程切换开销。另外,同步阻塞模式下,容器线程池的数量一般是固定的,造成对连接数有一定限制,当后台服务慢,容器线程池易被耗尽,一旦耗尽容器会拒绝新的请求,这个时候容器线程其实并不忙,只是被后台服务调用 IO 阻塞,但是干不了其它事情。

总体上,同步阻塞模式比较适用于计算密集型 (CPU bound) 应用场景。对于 IO 密集型场景 (IO bound),同步阻塞模式会白白消耗很多线程资源,它们都在等待 IO 的阻塞状态,没有做实质性工作。

我在极客时间「微服务架构和实践 160 讲」第三模块「微服务网关 Zuul 架构和实践」,对 Zuul 进行了十分详细的讲解,戳此了解详情

Zuul 2.0 编程模型和优劣

Zuul2 的设计相对比较复杂,代码也不太容易读懂,它采用了 Netty 实现异步非阻塞编程模型,如上图所示 [图片来自附录 4]。

一般异步模式的本质都是使用队列 Queue(或称总线 Bus),在上图中,你可以简单理解为前端有一个队列专门负责处理用户请求,后端有个队列专门负责处理后台服务调用,中间有个事件环线程 (Event Loop Thread),它同时监听前后两个队列上的事件,有事件就触发回调函数处理事件。这种模式下需要的线程比较少,基本上每个 CPU 核上只需要一个事件环处理线程,前端的连接数可以很多,连接来了只需要进队列,不需要启动线程,事件环线程由事件触发,没有多线程阻塞问题。

异步非阻塞模式也是有利有弊,分析如下图:

优势

异步非阻塞模式启动的线程很少,基本上一个 CPU core 上只需启一个事件环处理线程,它使用的线程资源就很少,上下文切换 (Context Switch) 开销也少。非阻塞模式可以接受的连接数大大增加,可以简单理解为请求来了只需要进队列,这个队列的容量可以设得很大,只要不超时,队列中的请求都会被依次处理。

不足

异步模式让编程模型变得复杂。一方面 Zuul2 本身的代码要比 Zuul1 复杂很多,Zuul1 的代码比较容易看懂,Zuul2 的代码看起来就比较费劲。另一方面异步模型没有一个明确清晰的请求 ->处理 ->响应执行流程 (call flow),它的流程是通过事件触发的,请求处理的流程随时可能被切换断开,内部实现要通过一些关联 id 机制才能把整个执行流再串联起来,这就给开发调试运维引入了很多复杂性,比如你在 IDE 里头调试异步请求流就非常困难。另外 ThreadLocal 机制在这种异步模式下就不能简单工作,因为只有一个事件环线程,不是每个请求一个线程,也就没有线程局部的概念,所以对于 CAT 这种依赖于 ThreadLocal 才能工作的监控工具,调用链埋点就不好搞 (实际可以工作但需要进行特殊处理)。

总体上,异步非阻塞模式比较适用于 IO 密集型 (IO bound) 场景,这种场景下系统大部分时间在处理 IO,CPU 计算比较轻,少量事件环线程就能处理。

Zuul1 和 Zuul2 的性能比对

Netflix 本身对网关使用异步非阻塞模式这件事情是非常谨慎的,它们进行了严格的性能测试,下面是 Netflix 给出的一些性能数据,来自 Zuul2 网关核心研发成员 Arthur Gonigberg 的 ppt(Zuul’s Journey to Non-Blocking)[附录 3]:

Netflix 给出了一个比较模糊的数据,大致 Zuul2 的性能比 Zuul1 好 20% 左右,这里的性能主要指每节点每秒处理的请求数。为什么说模糊呢?因为这个数据受实际测试环境,流量场景模式等众多因素影响,你很难复现这个测试数据。即便这个 20% 的性能提升是确实的,其实这个性能提升也并不大,和异步引入的复杂性相比,这 20% 的提升是否值得是个问题。Netflix 本身在其博文 [附录 4] 和 ppt[附录 3] 中也是有点含糊其词,甚至自身都有一些疑问的。

While we did not see a significant efficiency benefit in migrating to async and non-blocking, we did achieve the goals of connection scaling.

比较明确的是,Zuul2 在连接数方面表现要好于 Zuul1,也就是说 Zuul2 能接受更多的连接数。

Zuul 2.0 架构和额外特性

上图是 Zuul2 的架构,和 Zuul1 没有本质区别,两点变化:

前端用 Netty Server 代替 Servlet,目的是支持前端异步。后端用 Netty Client 代替 Http Client,目的是支持后端异步。

过滤器换了一下名字,用 Inbound Filters 代替 Pre-routing Filters,用 Endpoint Filter 代替 Routing Filter,用 Outbound Filters 代替 Post-routing Filters。

上图是 Zuul2 的一些功能亮点,我个人认为除了对 HTTP/2 的支持算是一个亮点,其它都是在安全、弹性和运维层面的一些优化,不能算新功能。其中像 Request Passport,Status Categories,Request Attempts 这些所谓的新功能,其实是为了减轻异步带来的复杂性,方便开发人员调试异步请求而专门开发的。

建议

基于上述分析,我的建议是在生产环境中继续使用 Zuul1,原因如下:

Zuul1 同步编程模型简单,门槛低,开发运维方便,容易调试定位问题。Zuul2 门槛高,调试不方便。

Zuul1 监控埋点容易,比如和调用链监控工具 CAT 集成,如果你用 Zuul2 的话,CAT 不好埋点是个问题。

Zuul1 已经开源超过 6 年,稳定成熟,坑已经被踩平。Zuul2 刚开源很新,实际落地案例不多,难说有 bug 需要踩坑。

大部分公司达不到 Netflix 那个量级,Netflix 是要应对每日千亿级流量,它们才挖空心思搞异步,一般公司亿级可能都不到,Zuul1 绰绰有余。

Zuul1 可以集成 Hystrix 熔断组件,可以部分解决后台服务慢阻塞网关线程的问题。

Zuul1 可以使用 Servlet 3.0 规范支持的 AsyncServlet 进行优化,可以实现前端异步,支持更多的连接数,达到和

Zuul2 一样的效果,但是不用引入太多异步复杂性。在《微服务架构实战 160 讲》视频课程中,有我关于讲解 Zuul1 如何使用 AsyncServlet 优化连接数的内容,戳此了解

对于 Zuul2,我的建议是持谨慎观望的态度,可以在测试环境小规模实验验证,但是暂不上到生产环境。

结论

同步异步各有利弊,同步多线程编程模型简单,但会有线程开销和阻塞问题,异步非阻塞模式线程少并发高,但是编程模型变得复杂。

架构师做技术选型需要严谨务实,具备批判性思维 (Critical Thinking),即使是对于一线大公司推出的开源产品,也要批判性看待,不可盲目追新。

个人建议生产环境继续使用 Zuul1,同步阻塞模式的一些不足,可以使用熔断组件 Hystrix 和 AsyncServlet 等技术进行优化。

Rate this post
No tags for this post.

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注