服务器 频道

携程定制化路由代理网关实现

  本文主要介绍携程软负载产品,在业务增长及路由需求日渐复杂的背景下,如何从传统Nginx反向代理逐渐发展成支持多元化路由的网关产品。以及其中利用的开源框架OpenResty的主要功能,和我们在路由转发场景的落地实践及探索。

  一、背景

  携程软负载产品(SLB)基于Nginx实现,为携程几乎所有HTTP请求流量提供负载均衡服务,目前每天处理千亿次请求。最开始时,SLB的主要职责是管理业务的HTTP路由和实现反向代理,取代更传统的硬件负载为业务集群提供应用层负载均衡。

  近几年,随着业务增长,以及多机房容灾、混合云部署等背景,逐渐衍生出了多机房容灾,跨区域流量调度等定制化需求,一个请求需要基于不同条件在机房间转发。传统的反向代理功能已经无法满足业务需求,如何在现有Nginx技术栈上支持越来越多的定制化路由需求,管理路由配置,并快速迭代功能成为了我们面临的问题。

  二、流量链路

  SLB需要处理携程内网和外网所有的HTTP流量,SLB为业务应用提供HTTP层负载均衡能力,管理维护所有域名访问入口和业务应用集群的关联关系。在早期,公司整体的流量模式比较单一,SLB的职责也较为单一,主要作为HTTP等协议的反向代理。举一个最简单的Nginx样例配置,我们将foo.bar.com/hello的请求转发到后端一个三台机器组成的固定集群上,这也是早期SLB大量配置的模式,将HTTP请求转发到固定的某组机器。  

  后来随着公司业务的增长以及对可用性更高的要求,有了混合云部署、异地容灾等场景后,公司业务的流量链路也更加复杂,业务也提出了很多定制化场景。

  2.1 多机房容灾

  携程各业务线需要在私有云,公有云的多个机房部署,日常情况由各个机房部署的机器共同分担处理业务流量。如果有机房大面积故障,需要从路由摘除整个故障机房,使外部流量导向正常机房。另外当流量高峰时,支持将高峰流量向容量相对更多的公有云机房泄洪。容灾是业务高可用的重要保障,需要SLB在全局、应用和服务层面,有机房间流量动态分配切换的能力。  

  2.2 多元化需求

  不同业务和场景往往关心请求中不同的维度和数据,比如有些场景往往希望同一个用户的请求链路能够保持一致,实现set化的场景,而另一些场景可能更关心如何分流给不同的后端应用和不同的机房。功能上除了流量调度,也有请求标记,请求响应数据采集等需求。我们希望能有一个通用的方案来实现这些定制化的需求。

  上述复杂场景和需求,显然是无法通过朴素的反向代理模式和Nginx静态配置实现的,这些需求推动SLB向集成API网关功能的产品前进。

  三、面临的问题

  在SLB着手实现和落地这些需求的过程中,我们碰到了一些困难和痛点,总结下来有这几方面。

  3.1 路由能力

  Nginx原生API提供的路由和请求处理能力都比较有限,像一些比较复杂的条件路由,即需要根据运行时的情况动态选择路由目标或对请求做一些处理,Nginx很难以优雅的方式支持。

  3.2 动态更新

  Nginx的配置更新需要一次reload操作,reload操作过程中master进程会用新配置fork出新的worker进程,这是一个非常耗资源的操作。Nginx和客户端服务端的连接都需要重建,进程创建和加载配置本身也需要消耗资源,并且对于一些高频请求的业务,reload会导致有请求失败的情况。而在实际场景里,配置变更又是一个高频操作,业务的一次灰度切流就需要好几次有损变更,我们需要寻求一种没有reload的变更生效方式。

  dyups也是一种常见的动态更新方式,我们也通过这种方式来动态更新upstream。但实践过程中我们发现,大量dyups实际也会阻塞Nginx进程,我们需要更优雅的方式。

  3.3 集群管理

  SLB在部署上,需要涵盖并对齐公司所有网络环境和机房,线上目前有上百套集群,我们的一些版本迭代需要消耗非常大的人力去做发布、版本对齐和兼容等工作。实际部署上,不同集群的职责也有差异,我们希望这些功能切面的升级更新可以独立升级、动态生效。另外,每次路由逻辑的更新,路由的切换,需要能以灰度的方式进行,避免带来灾难性后果。

  3.4 Nginx Lua

  某种程度上,为了进一步拓展SLB的路由功能,我们引入了OpenResty的Nginx Lua模块来尝试解决这个问题。OpenResty开源的Nginx Lua模块为Nginx嵌入了LuaJIT,提供了丰富的指令和API,包括但不限于:

  1)读写请求(header, url, body),实现一些请求标记,rewrite等功能

  2)请求转发,实现自定义的路由逻辑,不再局限于固定的upstream和server

  3)共享内存,Nginx进程间共享数据

  4)网络编程,实现一些旁路请求

  我们通过Lua脚本在Nginx处理请求的各种阶段,执行自定义的脚本,实现不同的功能。涵盖了nginx初始化、请求处理、请求转发、响应和日志等非常完整的请求和响应流程,并且在很多公司都有实际应用。  

  在SLB早期方案中我们使用Nginx Lua实现一些相对固定的逻辑如集群间流量灰度。业务可以配置某个路径的请求在新老集群间的流量比例,或指定某个集群路由,举个例子:

  Nginx Lua一定程度上解决了传统反向代理路由能力的问题,但在动态更新上仍然存在问题:变更仍然需要reload生效,一次业务灰度从0到100需要经历多次reload。

  四、解决方案

  4.1 核心:逻辑和数据

  一个较为理想的方案来解决传统路由动态更新所需的reload和它带来的开销问题:在Nginx中将数据和逻辑进行隔离。比如我们可以直接在Nginx配置中引用一个Lua文件,由Lua文件提供一个固定的方法入口,读取内存中的数据进行计算和转发。通过这种方式,流量调度逻辑是相对固定的,动态和高频变更的部分在数据模型,这样我们就能避免reload,业务就可以进行高频操作和修改。

  内存数据可以是基于不同路由场景自定义的数据结构,来描述期望的配置、关联关系、路由目标等。比如上述的集群间流量灰度,如果要实现流量根据不同比例转发到两个集群,这个结构可以类似于:

  对于每个流量调度场景,我们只要定义流量调度逻辑和相应的数据模型,就能比较方便的实现我们需要的功能。

  4.2 整体架构  

  方案整体上分为三个职责不同的模块:

  1)API模块:作为控制面集群的核心组件,API模块基于Java开发,它负责管理Lua文件和数据模型的生命周期。这包括对Lua文件和数据模型的创建、更新、删除和持久化等操作,支持灰度下发。此外,API模块还负责集群配置的管理,包括节点的注册和发现、路由配置等。

  2)Agent模块:Agent模块是连接控制面和数据面的桥梁,承担着数据的加工和传递的任务。它负责将控制面下发的Lua文件和数据模型持久化到本地存储,以便在数据面进行实时的路由计算和流量处理。Agent模块还负责与管理层的API模块进行通信,下发最新的Lua脚本文件和数据模型的更新,并将其应用到本地存储中,以确保数据面能够及时获取最新的路由规则。

  3)Nginx & Lua模块:作为数据面集群的核心组件,Nginx & Lua模块承担着实际的请求处理和路由逻辑的执行。Nginx Lua提供了灵活的编程能力,使得我们可以根据业务需求定制化地处理请求和路由流量。Lua脚本可以访问实时更新的数据模型,根据其中的信息进行动态的路由计算,并将请求转发到预期的目标。这种动态路由的能力使得系统能够根据实时的业务需求和环境变化来灵活地调整流量分配。

  4.3 数据模型生命周期

  在方案实践中比较重要且复杂的地方是路由逻辑依赖的数据模型如何更新和生效。我们采用类似于Nginx配置文件的工作和生效方式,把数据模型存储在磁盘,而不同之处在于我们旁路将数据模型从磁盘读取到内存,运行时从内存读取模型,这样就不再依赖reload生效。

  具体来说,Nginx是一个多进程的应用,会存在多个worker进程处理请求,进程间天然存在内存隔离。在worker的生命周期里,在每个worker初始化时先加载数据到内存,运行时动态同步更新内存数据。为了把更新推送到每个worker进程,我们基于Nginx Lua提供的共享内存功能,在共享内存中缓存每个数据模型的最新版本,而worker进程通过一个timer来不断轮询共享内存,发现版本有更新后从磁盘读取数据模型,加载到内存,对于每个场景可以在Lua中自定义具体的数据加载逻辑。 

  相比于worker直接或旁路轮询数据接口,共享内存最小化了请求和解析数据对运行时的影响。另外Agent把每个数据模型都持久化到磁盘解决了进程重启时内存数据丢失的问题,worker进程可以在初始化时主动从磁盘重新读取数据。这样即使我们的Nginx由于一些意外重启或者退出(发布,OOM,机器重启等),至少还能以磁盘上持久化的数据继续工作,并且不依赖其他组件(API、Agent)的存活。

  4.4 实践和落地情况

  在实践中我们注意到,Lua或者说数据面应用的处理逻辑应尽可能简单,但对于运维和开发同学来说,一般都是希望有全局视角的数据来掌控全局。

  所以在数据结构设计上,我们一般会进行拆分:API和运维人员及其他系统的交互通常采用全局数据,会有多个层级。而在和Agent中间层交互时主动加工数据模型,去掉运行时不需要的部分,如其他集群的配置,被关闭的配置等;到了真正处理请求的数据面,往往我们只希望从一个扁平化的数据模型以类似key-value的形式读取。比方说一个应用在多数据中心的流量分配,在全局视角来看类似于:

  避免了运行时Lua做过多的数据解析逻辑,是较为合理的职责分配方式。

  目前我们通过该方案实现了应用和服务粒度的流量分配,使得业务应用能够在多个数据中心提供服务。同时,该方案还提供了全局及应用粒度的故障降级功能,以增强业务的可用性,经过多次机房故障演练验证,切换可以在秒级生效。除了实现复杂的请求路由功能之外,我们通过Lua也落地了请求响应数据采集,不同渠道流量标记等旁路功能。

  五、结语

  流量链路随着公司技术和业务的拓展,走向多元、动态和定制化的发展方向,不断对负载均衡和网关产品提出新的场景和需求。定制化路由解决方案解决了传统反向代理在路由能力和动态更新上的问题,降低了大规模集群部署下的研发和迭代成本,帮助SLB产品从传统的反向代理中间件向综合性的API网关逐渐靠拢。方案对不同场景有良好的扩展性和可靠性,未来也会有更多的流量调度模式通过统一方案快速落地交付。

  在产品技术栈上,不同的编程语言和技术各有所长,我们希望能使不同技术达成优势互补,像Lua的动态,Nginx的性能。需要我们持续在这些技术上不断学习和投入,加深我们对产品技术栈的理解,扬长避短。

  在未来,如何更好的利用现有技术栈,方便Java开发的同学能快速上手Lua开发,如何优化Lua责任链和异常处理,优化Lua的内存管理,还有进一步努力和探索的空间。

0