在今年5月中,Netflix终于开源了它的支持异步调用模式的Zuul网关2.0版本,真可谓千呼万唤始出来。从Netflix的官方博文[附录1]中,我们获得的信息也比较令人振奋:
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的阻塞状态,没有做实质性工作。
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]中也是有点含糊其词,甚至自身都有一些疑问的。
比较明确的是,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讲》,7月份马上上线第三模块《微服务网关Zuul架构和实践》,其中会讲解Zuul1如何使用AsyncServlet优化连接数,欢迎大家关注。
对于Zuul2,我的建议是持谨慎观望的态度,可以在测试环境小规模实验验证,但是暂不上到生产环境。
结论
同步异步各有利弊,同步多线程编程模型简单,但会有线程开销和阻塞问题,异步非阻塞模式线程少并发高,但是编程模型变得复杂。
架构师做技术选型需要严谨务实,具备批判性思维(Critical Thinking),即使是对于一线大公司推出的开源产品,也要批判性看待,不可盲目追新。
个人建议生产环境继续使用Zuul1,同步阻塞模式的一些不足,可以使用熔断组件Hystrix和AsyncServlet等技术进行优化。波波和极客时间合作的课程《微服务架构和实践160讲》,7月份马上上线第三模块《微服务网关Zuul架构和实践》,其中会讲解对Zuul1的这些优化技术,欢迎大家关注。
附录
Open Sourcing Zuul 2 https://medium.com/netflix-techblog/open-sourcing-zuul-2-82ea476cb2b3
Hystrix https://github.com/netflix/hystrix
Zuul's Journey to Non-Blocking https://github.com/strangeloop/StrangeLoop2017/blob/master/slides/ArthurGonigberg-ZuulsJourneyToNonBlocking.pdf
Zuul2:Netflix's Journey to Asynchronous,Non-blocking Systems https://medium.com/netflix-techblog/zuul-2-the-netflix-journey-to-asynchronous-non-blocking-systems-45947377fb5c
- END -
** 往期推荐:**
- 死磕Java系列:
-
深入分析ThreadLocal
-
深入分析synchronized的实现原理
-
深入分析volatile的实现原理
-
Java内存模型之happens-before
-
Java内存模型之重排序
-
Java内存模型之分析volatile
-
Java内存模型之总结
-
J.U.C之AQS简介
J.U.C之AQS:CLH同步队列
-
J.U.C之AQS同步状态的获取与释放
-
J.U.C之AQS阻塞和唤醒线程
-
J.U.C之重入锁:ReentrantLock
-
J.U.C之读写锁:ReentrantReadWriteLock
-
J.U.C之Condition
-
J.U.C之并发工具类:CyclicBarrier
-
J.U.C之并发工具类:Semaphore
-
J.U.C之并发工具类:CountDownLatch
……
- Spring系列:
-
Spring Cloud Zuul中使用Swagger汇总API接口文档
-
Spring Cloud Config Server迁移节点或容器化带来的问题
-
Spring Cloud Config对特殊字符加密的处理
-
Spring Boot使用@Async实现异步调用:使用Future以及定义超时
-
Spring Cloud构建微服务架构:分布式配置中心(加密解密)
-
Spring Boot快速开发利器:Spring Boot CLI