Netflix宣布了通用API网关Zuul的架构转型。Zuul原本采用同步阻塞架构,转型后叫作Zuul 2,采用异步非阻塞架构。Zuul 2和Zuul 1在架构方面的主要区别在于,Zuul 2运行在异步非阻塞的框架上,比如Netty。Zuul 1依赖多线程来支持吞吐量的增长,而Zuul 2使用的Netty框架依赖事件循环和回调函数。
InfoQ对负责这次转型的Netflix项目经理Mikey Cohen进行了采访。
InfoQ:你把Zuul 2的转型描述成了一个旅程,而且它看起来像是一个长途旅程,你能说一下这个旅程的动机和概况,以及为什么需要这么长时间吗?
Mikey Cohen:我们希望构建一个可以为持久连接设备横向扩展的系统,这就是转向Zuul 2的主要动机。8300多万用户,每个用户持有多个设备,在系统横向扩展方面,我们面临着一个巨大挑战。基于持久连接,我们可以实现推送,还可以支持从用户设备到云控制系统之间的双向通信。我们还可以使用推送通知取代从用户设备发出的轮询请求。轮询请求在用户设备访问微服务方面扮演了重要角色。使用推送可以降低通信成本,还能提升用户体验。除此之外,基于持久连接和推送机制可以开发出很多用户体验更好、更易于调试的功能特性。最后,我们会支持更多协议,比如websocket和HTTP/2。
实际上,这确实是一个旅程。如果我们在构建Zuul 2时脱离实际,那么它就不会是一个值得纪念的旅程。这次转型的复杂性是巨大的。运行在Zuul上的云网关是Netflix云服务的主要入口。我们支持超过1000种设备,这些设备在功能配置、使用限制方面也是千差万别。Zuul需要支持这些设备的各种古怪行为,并把它们规范化。例如,有些设备关心消息头的顺序,有些对消息头有大小限制,有些不支持块编码,而且它们的一些特性在Tomcat和Netty里的处理方式不一样。这个项目的一个巨大挑战是找出这些差异,并做到让它们兼容。大部分这类问题只有在真实环境里才会暴露出来。要想在不影响生产环境用户的情况下检测出这类问题,需要承担巨大的风险,同时对减小风险影响范围的工程技术来说也是一个巨大考验。一个失效就会让我们8300多万用户中的一部分人或所有人无法消费Netflix的内容。
更深入一点看我们的架构,网关的所有特性和平台基础架构需要运行在异步环境里。Netflix的所有平台基础架构,包括Zuul过滤器,都是基于同一个假设而构建的,那就是它们会被运行在阻塞式的环境里。跟请求相关的线程变量在我们的支持包和平台代码里无处不在。使用阻塞式I/O是这个平台的第二大特点。这些设计概念在异步非阻塞的架构里无法正常工作。识别出这些陷阱,并构建出基于异步非阻塞架构的可行方案是很耗费时间的,有时候会变得很困难。
作为Netflix网关的构成角色之一,尝试理解我们所看到的一切也是这个旅程的一部分。简单地说,这个角色主要负责路由、观察和规范化那些进入Netflix的请求。我们使用迂回的方式构建Zuul 2,我们先移除Zuul 1的大部分业务逻辑,再把新的逻辑填充进原先的系统。除了让Zuul 2在网关里的角色更加明确之外,也简化了阻塞式代码的迁移工作,因为我们移除了大量代码。
创建Zuul过滤器和过滤解析异步化也一直是这次转型的另一个重大挑战。正如博文里所讲的那样,在Zuul 2之前我们就做了这方面的工作,所以我们可以在Zuul 1和Zuul 2上运行之前创建的Zuul过滤器(异步代码可以在同步环境里运行,反之则不行)。这样我们就可以基于这些过滤器继续开发网关的其它功能特性。不过在迭代过程中还是会对Zuul过滤器接口进行修改,并把这些过滤器串联起来。我们使用RxJava来串联Zuul过滤器。通过对100多个Zuul过滤器进行串联,我们发现这真是一个重注细节、耗费时间的任务。
最后,我们使用几个迭代来构建可以让Zuul 2运行起来的框架。我们从使用早期版本的Netty转到使用RxNetty项目。在构建Zuul 2的过程中,我们发现有些地方仍需要Netty的核心功能,而RxNetty却把它们移除了,所以在这些地方我们仍然使用Netty。一路走来,伴随着学习和纠错,我们构建了一个越来越健壮的产品。
InfoQ:你在博客里提到,除了后端延迟、错误重试,多线程系统能够应对大部分场景。是否还有其它原因促使你转向新的架构?
Cohen:我们一度相信在CPU使用效率和弹性方面会得到大幅改进。在部分集群(不是全部)里,我们确实看到了效率方面的改进(10-25%),不过这似乎不值一提。未能准确估算网关CPU工作量,也未能准确判断那些CPU比异步NIO密集的场景,这些大概就是造成我们错误预期的原因。我相信,使用Zuul 2开源版本(在它发布之后)的人将要看到的性能方面的大幅度提升,这在很大程度上是因为我们在Netflix Zuul 2网关上所做的大部分工作都是跟Netflix整体架构相关的。很多人认为10-25%这样的性能提升已经是一个很大的收获。不过如果把所耗费的时间、资源,还有运营方面面临的挑战以及异步NIO系统引入的调式复杂性都考虑在内,那么在性能方面获得的提升跟所付出的工作量无法划上等号。这么说来,其它方面的成效,比如连接管理和推送通知,以及弹性的提升,对我们来说就更重要了。
我们期待在弹性方面有更大的提升。就像我在博客里提到的那样,我坚信它会变成现实,但天下没有免费的午餐。我们积极地采取各种措施来获得更多的弹性提升,比如减少实例化对象,记录异常信息,改变节流机制,重用连接,改进负载均衡算法等。
InfoQ:Zuul过滤器是Zuul的关键组成部分。这次转型的切入点就是这些过滤器,对吗?你能否描述一下过滤器的重构过程,同时为那些使用类似方式重构大型系统的开发者们提供一些建议吗?
Cohen:把网关的业务逻辑部分异步化是有远见的想法,从长远来看,它会为我们节省很多时间。我们没必要为了保持一致性而维护两套过滤器(Zuul 1一套,Zuul 2一套)。在构建Zuul 2之前,我们准备了超过6个月的时间,这着实是一个巨大的前期投入。我想很多团队会认为这是本末倒置,但我们有理由相信这样做对我们有很大好处。首先,过滤器的开发可以在同一个代码库上进行,这样可以保证业务逻辑的一致性。既然我们知道业务逻辑要保持一致,那么就可以把不一致的部分移除掉。最后,我们可以借助这样的一个非常有用的工具,从系统和运营的角度来比较Zuul 1和Zuul 2。
InfoQ:你有没有一些实战经验可以分享给开发者和架构师们,帮助他们完成从同步阻塞到异步非阻塞的架构转型?另外,RxNetty是一个什么样的框架,它在这次转型过程中起到多大的作用?
Cohen:在转向异步系统的过程中,我们打了很多场仗。它们相互交织在一起,有着相同的主题和模式。在一开始,我们发现了资源泄露的问题:ByteBuf、信号量、文件描述符等。这类罕见的极端情况在我们的系统里却大量出现。通常需要几天时间的调试才能找到问题的根源。产生问题的根源多种多样,不过大部分都跟错误事件丢失有关。在打仗过程中,我们构建了一些工具,并尝试把它们贡献出来。我们向Netty贡献了一个可插拔的资源泄露探测器,有了这个插件,就可以使用监控工具检测资源泄露。我们还总结了一些模式,并把它们分享出来,遵循这些模式可以帮助我们更快地解决问题。
我们打的这些仗不是相互孤立的。我们构建的测试框架止步于此,因为很多问题只有在生产环境里才会出现。生产环境有大规模的用户和真实的系统,会暴露出大量问题。变更会带来巨大风险,一旦网关崩溃,用户就无法使用系统。转型越快意味着经历得越多,不过对用户的影响也越大。相反,以保守的速度转型意味着经历是循序渐进的,对用户的影响也较小。所以,对Zuul 2来说,在高风险的快速转型和保守的转型之间,有着微妙的平衡关系。
InfoQ:你说自己有意避开测试基准。既然异步和非阻塞都跟性能相关,那么你能告诉我们一些整体性能改进的细节吗?
Cohen:关于Zuul 1和Zuul 2(阻塞和非阻塞)之间的性能比较问题,我想我们在连接扩展性能上取得了大幅度的改进,不过吞吐量的提升受到CPU密集型任务的限制。这些任务主要包括收集度量指标、记录日志、分析数据、加密和压缩。不过在把Zuul 2开源以后,我相信在移除了这些任务的实现版本上可以看到吞吐量的重大提升。
InfoQ:你能具体说一下Zuul 2的开源计划以及Zuul的路线图吗?
Cohen:我们一直在致力于Zuul 2的开源工作,并计划在今年年底发布。我们还计划增加一些新的功能,比如对websocket和HTTP/2的支持。从路线图来看,我们即将对外开放部分过滤器、路由工具和设计理念。把Netflix特定的逻辑梳理出去是这个工作的主要内容。既然我们开发了websocket和推送功能,我们也想把相关的经验和基础架构也开源出来。
Zuul 2文档还在准备当中。Zuul wiki上有Zuul的相关信息,它还告诉我们如何开始使用Zuul。
12月年底技术嘉年华,阿里云研究员褚霸、Go基金会主席谢孟军、饿了么框架工具部研发总监兰建刚、腾讯毫秒服务引擎技术负责人杨宇将带我们去看架构从0到1,从1到100的进化之路,更多精彩点击阅读原文链接了解。