Oceanus:美团HTTP流量定制化路由的实践

标签: oceanus 美团 http | 发表时间:2018-09-07 03:33 | 作者:美团点评技术团队
出处:https://tech.meituan.com/

背景简述

Oceanus是美团基础架构部研发的统一HTTP服务治理框架,基于Nginx和ngx_lua扩展,主要提供服务注册与发现、动态负载均衡、可视化管理、定制化路由、安全反扒、session ID复用、熔断降级、一键截流和性能统计等功能。本文主要讲述Oceanus如何通过策略抽象、查询、渲染和分组动态更新,实现HTTP请求的定制化路由。
随着公司业务的高速发展,路由场景也越来越复杂。比如:

  • 团购秒杀要灵活控制压测流量,实现线上服务单节点、各机房、各地域等多维度的压测。
  • 外卖业务要做流量隔离,把北方地域的流量转发到分组a,南方地域的流量转发到分组b。
  • 酒旅业务要对App新版本进行灰度,让千分之一的用户试用新版本,其他用户访问老版本。
  • QA部门要通过请求的自定义参数指定转发分组,构建稳定且高可用的测试环境。

由于公司早期的业务场景相对比较简单,所以均通过Nginx if指令支持。比如某业务要把来源IP为10.4.242.16的请求转发到后端节点10.4.232.110,其它请求转发到后端节点10.4.232.111和10.4.232.112,就可以进行如下配置:

  upstream backend_aaa {
    server 10.4.232.110:8080 weight=10;
}
upstream backend_bbb {
    server 10.4.232.111:8080 weight=10;
    server 10.4.232.112:8080 weight=10;
}
location /abc {
    if($remote_ip = "10.4.242.16") {
        proxy_pass http://backend_aaa; #路由到backend_aaa集群
    }
    proxy_pass http://backend_bbb; #路由到backend_bbb集群
}

上述方式虽然不需要额外开发,性能方面也接近原生的Nginx框架,但是使用场景比较受限,因为if指令仅支持比较简单的condition类型,官方描述如下:
NginxIf

如果该业务要把IP段10.4.242.16/34的请求转发到10.4.232.110时,if指令勉强还可以支持。但对于上述的复杂业务场景,if指令均无法支持。除此之外,这种方式还存在以下两点不足:

  • 规则调整不支持动态化:如果要把客户端10.4.242.16调整为10.4.242.17,需要对Nginx进行reload,而reload操作会使Nginx的并发能力下降,业务高峰时甚至会导致请求504或502。
  • 指令坑太多:if指令和set、rewrite指令等一起使用时,很多时候会出现不符合预期的行为,严重时甚至会导致段错误,最好的方法就是避免使用。

为了解决上述问题,Oceanus开始探索如何实现HTTP流量的定制化路由。

业界调研

通过初步调研,发现业界有一套开源的ABTestingGateway(以下简称AB)框架:

ABTestingGateway
由上图所示,AB框架使用Redis存储策略数据,key是Host字段,value是策略对象,包括策略类型、匹配区间和要分发的Upstream。策略的增删改查可以通过基于Nginx搭建的Web服务的API实现,运行时根据请求的Host字段从lua-shared-dict或Redis获取关联的策略,根据策略类型(iprange/uidrange/uidsuffix/uidappoint)选择对应的Lua脚本从请求中获取相关参数(IP、UID)查询是否匹配策略,若匹配,就修改请求的Upstream上下文完成分流的目的。
相比if指令的方式,AB框架有下面两个优点:

  • 策略调整动态生效:已有策略类型中的策略变更均可以通过HTTP API进行动态管理。
  • 分流策略丰富:支持IP段、UID段等策略,也可以通过新增策略类型对策略库进行扩展。

由于AB框架只支持4种策略类型,对于业务要根据请求Cookie、自定义header控制转发的情况,均需要开发新的策略类型和发布上线。另外,策略类型和业务场景紧密相关,导致AB系统的扩展性极差,很难快速支持新业务的路由需求。

无论是Nginx if指令,还是AB框架,要么需要reload重新加载才能生效,要么无法支持某些业务场景下的分流需求,所以都很难作为解决公司级分流框架的有效手段。针对它们所存在的不足,Oceanus开发了一套应用级、高可扩展的动态分流框架,不仅动态支持各种业务场景的分流需求,而且保证了请求转发的性能,下文将阐述我们如何解决分流机制的几个核心问题。

Oceanus定制化路由的核心设计&实现

关于分流机制,我们主要从以下四个方面来讲述:

  • 策略抽象:合理定义策略结构,适用尽可能多的业务场景。
  • 策略的高效查询:接口粒度关联,应用维度管理。
  • 运行时策略渲染:渲染策略模板,判断是否匹配策略,实现动态路由。
  • 分组动态更新:分组数据增删改,均不需要reload。

策略的结构定义

以AB框架为例,只支持iprange、uidrange、uidsuffix、uidappoint四种场景,对策略类型和匹配方式太具体化,导致无法支持更多普适性的业务场景。从分流的本质出发,即根据请求特征完成流量的定制化路由。结合Nginx if指令的几个组成部分:条件判断依赖的变量、条件判断要匹配的value、条件表达式、匹配后要执行的proxy_pass,一个策略必须要包含请求特征描述、定制化路由描述以及两者的关系描述。其中请求特征描述包含特征关键字、关键字的上下文传输方式,定制化路由描述通过Upstream表示,Upstream可以预先设置,也可以动态指定,两者的关系通过泛型表达式表示。那么一个策略就需要包含下面几个属性:

  • name:策略名,没有实际意义,可以根据业务场景进行定义。
  • key:分流时依赖的关键字,比如要根据城市地域进行分发路由时,key就是regionid。
  • passway:关键字在HTTP协议中的传输方式,可以是Parameter、Cookie、header、body中的一种。
  • condition:表达式模板,支持四则运算/取模、关系运算符、逻辑运算符等。
  • group:后端服务集群,即匹配策略后,转发请求的目标节点,一般是策略所属应用集群中的部分节点。
  • category:策略类型,如果为1,表示某个服务的私有策略;如果为2,表示公共策略,主要用于策略数据管理。
  • switch:策略开关,用于控制当前策略是在线还是离线。
  • graylist:灰度列表,用于策略变更的线上灰度校验。

其中switch、graylist字段主要用于策略的上下线操作,这里不做过多讨论。下面重点介绍上面的策略定义是如何表述业务场景的:
DivisionDemo
备注:应用apk1和apk2分别配置2个私有策略,apk3使用公共策略。
如上图所示,无论业务根据请求的哪些特征进行分流,策略结构均可以支持。
以私有策略gray-deploy为例,在Oceanus管理平台进行添加,如下图所示:
AddStrategy
备注:这里省略了策略的非核心字段比如switch、graylist等。

如何实现策略的高效查询?

策略拓扑关系

分流策略分为私有策略和公共策略。私有策略是面向服务的,而且和该服务创建的分组紧密相关。不同服务的私有策略完全独立,可以相同,也可以不同。一个服务可以配置多个私有策略,也可以关联多个Host的Location,Location之间的策略使用完全独立,一个Location可以启用该服务的一个或者多个私有策略。如果通过Host+location_path直接关联策略数据,不同Location关联同一个私有策略时,会存在大量的数据冗余。所以我们通过服务标识(appkey,唯一标识一个应用服务)关联具体的策略数据,Host+location_path只关联当前Location使用的策略名列表,策略之间支持指定顺序。
公共策略与具体服务无关,策略名全局唯一,可以使用策略名关联策略数据即可。综上,策略的拓扑关系描述如下:
StrategyTopology
如上图所示,以应用apk1为例,关联了两个Location接口,分别为/api和/list,总共部署了8个节点,创建了2个分组ups-cq和ups-gray,其中节点10.5.23.6和10.5.24.72属于分组ups-cq,节点10.7.46.32和10.7.72.232属于分组ups-gray。应用配置了两个私有策略stress-testing和gray-deploy,其中策略stress-testing被接口/api启用,匹配策略的流量路由到分组ups-cq,策略gray-deploy被接口/list启用,匹配策略的流量路由到ups-gray。

运行时获取Location path

Nginx在解析Location配置时,通过不同的字段区分不同类型的Location,没有记录配置中的Location path。如果要运行时获取,一般有两种方式:一种是根据相关字段逆向还原path,另一种是为框架新增变量。由于Nginx在处理正则Location时,对于是否忽略大小写的情况,并没有做标记,即解析的过程是不可逆的,所以我们选择了第二种方式。在核心模块的变量数组ngx_http_core_variables中新增了内置变量,记录下原始的Location path,变量属性定义如下:

  {ngx_string("loc_mod"), NULL, ngx_http_variable_loc_mod,
  0, NGX_HTTP_VAR_NOCACHEABLE, 0},
{ngx_string("loc_name"), NULL, ngx_http_variable_loc_name,
  0, NGX_HTTP_VAR_NOCACHEABLE, 0}

loc_mod和loc_name之间用一个空格符连接,格式和Oceanus管理平台保持一致。

异步更新机制

为了保证运行时获取策略数据的高效性,我们通过异步定时拉取,把策略数据全量同步到本地的共享内存中。基于稳定性和灵活性的考虑,我们采用了关系型数据库MySQL存储策略。
更新机制如下图所示:
StrategyUpdate

  1. Oceanus在init_worker阶段随机选择某个worker进程,嵌入timer。
  2. 被选中的worker会异步非阻塞地从MySQL定时拉取策略数据。
  3. timer worker把拉取到的策略数据解析,按照策略的拓扑关系,更新到当前共享内存中的写缓存区,完成更新后,切换读写缓存区,保证最新的策略立即生效。
  4. worker进程在处理请求时,从当前共享内存中的读缓存区获取策略数据。

为了解决timer worker和其它worker在读写策略数据时的竞态关系,我们采用了双buffer机制,实现了业务层策略数据的无锁读写。另外,通过设置timer的时间为0,保证在所有worker处理请求前,策略数据已经在共享内存中完成初始化。

策略查询机制

查询算法如下图所示:
FindStrategy

  1. worker进程从request上下文中获取请求的Host,以及所匹配Location的location_path。
  2. 根据Host+location_path,到共享内存中查询所开启的策略名。
  3. 如果是公共策略,直接根据策略名去查询策略数据。
  4. 如果是私有策略,从request上下文获取Location关联的Upstream,即应用标识appkey,到共享内存读缓存区获取具体的策略数据。

备注:公共策略以"oceanus"开头,区别于私有策略的命名。

运行时策略渲染

查询到请求开启的策略后,Oceanus需要运行时判断是否匹配,以私有策略为例,执行流如下图所示:
RomanceStrategy

  1. 在rewrite phase,Oceanus通过rewrite_by_lua_file嵌入回调,触发请求处理,进入分流框架的主流程。
  2. 通过上面的策略查询机制获取请求的策略,进行解析,获取策略的key和passway。
  3. 根据passway从请求对应的上下文获取key的value。
  4. 用3获取到的value渲染策略的condition,把condition中的占位符替换为value。
  5. 基于Lua VM,通过load计算condition的结果,即true或false。
  6. 从策略中获取condition的value和group数据。
  7. 如果condition为true,就用group覆盖请求的Upstream上下文,否则,不做处理。

分组动态更新

分组列表的动态化是分流框架的重要一环。更新机制如下图所示:
UpdateGroup

  1. 分组数据使用ZooKeeper存储,变更通过watcher机制实现增量同步。
  2. Oceanus也会定时拉取,进行全量同步。
  3. Oceanus把所有变更都通过本地的HTTP调用同步到Nginx内存。
  4. worker处理变更请求前,会先抢锁,读取共享内存中的消息队列,同步其它worker进行的历史变更。
  5. 把这次变更同步到当前worker的Upstream main上下文中,完成当前worker的更新。
  6. 把变更封装成消息,加入到共享内存中的队列。
  7. 其它worker通过timer或者自己处理变更消息前读取消息队列,完成更新。

总结与展望

通过Oceanus分流机制在美团外卖、酒旅、到店餐饮等多个业务线的广泛使用,基础架构部帮助业务同胞解决了多个定制化路由的需求,比如服务set化、链路压测、灰度发布、泳道环境建设等等。目前,Oceanus分流机制只关注了流量转发方向,还不支持更复杂的转发动作,比如根据策略调整请求的Parameter、header、Cookie,也不支持根据请求的URL实现动态路由等,未来我们还将逐一完善这些问题,当然也欢迎大家跟我们一起交流,共同进步。

作者简介

周峰,美团高级工程师,2015年7月加入美团基础架构部,先后负责统一密钥管理服务、智能反爬服务和HTTP负载均衡,目前主要负责HTTP服务治理Oceanus的相关工作,致力于探索和研究服务的自动化、智能化、和高性能等方向。

招聘广告:如果你对大规模分布式环境下的HTTP服务治理、分布式会话链路追踪等系统感兴趣,诚挚欢迎投递简历至:zhangzhitong#meituan.com。

参考文献

  1. ngx_http_rewrite_module: https://nginx.org/en/docs/http/ngx_http_rewrite_module.html
  2. AB框架: https://github.com/CNSRE/ABTestingGateway

相关 [oceanus 美团 http] 推荐:

Oceanus:美团HTTP流量定制化路由的实践

- - 美团点评技术团队
Oceanus是美团基础架构部研发的统一HTTP服务治理框架,基于Nginx和ngx_lua扩展,主要提供服务注册与发现、动态负载均衡、可视化管理、定制化路由、安全反扒、session ID复用、熔断降级、一键截流和性能统计等功能. 本文主要讲述Oceanus如何通过策略抽象、查询、渲染和分组动态更新,实现HTTP请求的定制化路由.

HTTP Headers 入门

- johnny - Time Machine
非常感谢 @ytzong 同学在twitter上推荐这篇文章,原文在此. 本文系统的对HTTP Headers进行了简明易懂的阐述,我仅稍作笔记. 什么是HTTP Headers. HTTP是“Hypertext Transfer Protocol”的所写,整个万维网都在使用这种协议,几乎你在浏览器里看到的大部分内容都是通过http协议来传输的,比如这篇文章.

HTTP基础

- - ITeye博客
HTTP的结构主要包括下面几个要点:. HTTP的版本主要有1.0,1.1 和更高版本.    1.1 及以上版本允许在一个TCP连接上传送多个HTTP协议,1.0能 .    1.1 及以上版本多个请求和响应可以重叠,1.0不能.    1.1 增加了很多的请求头和响应头.     一个请求行,若干小心头,以及实体内容,其中的一些消息头和实体内容是可选的,消息头和实体内容需要空行隔开.

HTTP Header 详解

- - 博客园_Ruby's Louvre
HTTP(HyperTextTransferProtocol)即超文本传输协议,目前网页传输的的通用协议. HTTP协议采用了请求/响应模型,浏览器或其他客户端发出请求,服务器给与响应. 就整个网络资源传输而言,包括message-header和message-body两部分. 首先传递message- header,即 http header消息.

HTTP负载测试

- - 博客 - 伯乐在线
英文原文: ON HTTP LOAD TESTING 来源: oschina. 有很多人在谈论HTTP服务器软件的性能测试,也许是因为现在有太多的服务器选择. 这很好,但是我看到有人很多基本相同的问题,使得测试结果的推论值得怀疑. 在日常工作中花费了很多时间在高性能代理缓存和源站性能测试方面之后,这里有我认为比较重要的一些方面来分享.

HTTP断点续传

- - CSDN博客互联网推荐文章
要实现断点续传的功能,通常都需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段. HTTP1.1协议(RFC2616)中定义了断点续传相关的HTTP头 Range和Content-Range字段,一个最简单的断点续传实现大概如下:.   1.客户端下载一个1024K的文件,已经下载了其中512K.

http-kit 1.2 发布

- - 开源中国社区最新新闻
Http-kit 是主要由Java 和Clojure开发,为Clojure定制的零依赖的Http lib,包括异步的高性能HTTP Server 和 HTTP Client. 在普通的PC上进行性能测试时,http-kit server 每秒能处理数万个请求. 修复处理文件上传时,content-type没能正确处理.

HTTP缓存算法

- - PHP源码阅读,PHP设计模式,PHP学习笔记,项目管理-胖胖的空间
HTTP协议缓存的目标是去除许多情况下对于发送请求的需求和去除许多情况下发送完整请求的需求. 以不发送请求或减少请求传输的数据量来优化整个HTTP架构,此目标的实现可以产生如下好处:. 降低对原始服务器的请求量. 减少了传送距离,降低了因为距离而产生的时延. 缓存基本处理过程包括七个步骤. 接收 – 缓存从网络中读取抵达的请求报文.

HTTP头部详解

- - CSDN博客推荐文章
HTTP(HyperTextTransferProtocol)是超文本传输协议的缩写,它用于传送WWW方式的数据,关于HTTP 协议的详细内容请参考RFC2616. HTTP协议采用了请求/响应模型. 客户端向服务器发送一个请求,请求头包含请求的方法、URI、协议版本、以及包含请求修饰符、客户信息和内容的类似于MIME的消息结构.

spring http invoker 实践

- - CSDN博客互联网推荐文章
搞了半天,终于把一个 Spring http invoker 的小例子运行起来了. UserServices接口:. UserServicesImpl实现类:. System.out.println("这里只是测试了一下. .