美团张志桐:美团 HTTP 服务治理实践

2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,美团基础架构部技术专家张志桐在活动上作了《美团 HTTP 服务治理实践》的分享。html

OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推进 OpenResty 开源项目的发展。活动将陆续在深圳、北京、武汉、上海、成都、广州、杭州等城市巡回举办。前端

首先作下自我介绍,我叫张志桐,毕业于哈尔滨工业大学,2015 年加入美团,目前在美团主要负责 Oceanus 七层负载均衡网关、Mtrace 分布式链路跟踪系统以及 KMS 密钥管理服务等。mysql

美团是 Nginx 的老用户,从创业初期就使用 Nginx,直到 2013 年迁到了阿里的 Tengine,再到今年三四月份,全站服务迁到了 OpenResty 上。从 Tengine 迁到 OpenResty 最根本的缘由是升级困难,随着 Nginx 的版本迭代愈来愈快,致使 Tengine 很难合到官方 Nginx 最新版本上,可是使用 OpenResty 能够平滑地升级整个 Nginx 的社区版本。sql

Oceanus 美团七层负载均衡网关

Oceanus,单词的含义是海神。它是整个美团接入层的七层负载均衡网关,天天有千亿级别的调用量,部署了几千个服务站点,近万个注册应用服务。Oceanus 最核心的功能是提供 HTTP 服务治理功能,主要包括服务的注册与发现,健康检查,以及彻底的可视化管理,同时也提供了像 Session 复用、动态 HTTPS、监控、日志、WAF、反爬虫、限流等网关功能。数据库

这里补充一个限流方面的小问题,目前美团是经过全局 Redis Cluster 来实现的,也简单的作了一些优化,实现了彻底基于 OpenResty 的 Redis Cluster,由于官方的 OpenResty 版本只支持单实例的 Redis 调用。同时咱们不是每次请求都会去作 Redis Incr 的操做,每次会设置一个阈值,设置越大,本机加的代价就越小,由于不须要远程调用了,但出现的偏差也会对应增大。基本的思路就是本地加一个步长,按期的把步长同步到 Redis Cluster 上来实现集群限流的功能。后端

上图是当前 Oceanus 的系统架构,底层的引擎核心是基于 OpenResty 的。在每一个 OpenResty 节点上会部署了一个 Agent 的进程,主要是为了作逻辑的解耦,咱们不但愿整个 Nginx 或者是 OpenResty 上有太重的逻辑和请求无关,因而把不少的逻辑都下沉到 Agent 上,实现与 OpenResty 的解耦,好比用 MNS 拉取服务列表,再经过 Agent 灌入到 OpenResty。站点管理,落地文件配置,统一由前端管理平台 Tethys 进行管理,以后会实时落地到 mysql 里,Agent 经过 mysql 的同步,再落地到本地到 Server block 文件,经过 reload 方式实现站点的从新加载。右边是 Oceanus 体系以外的模块,第一个是 MNS,是公司内部统一的命名服务。另外一个 Scanner,主要负责的是健康检查。缓存

Nginx 配置反向代理

如上图配置 Nginx 反向代理会遇到几个问题:服务器

  • 写死的服务地址,IP 不能变,每次变动须要改文件。
  • 每次变化须要 reload。
  • 文件化的配置容易出问题。

咱们怎么解决这三个问题?第一个动态的服务注册,第二个是不须要 reload 动态配置生效,第三个文件化配置变成一个结构化管理。架构

服务注册

服务注册目前是基于美团内部的 MNS 统一命名服务,上图是整个服务注册的前端界面。它后端仍是依托如 ETCD、ZK 服务注册的基础组件,主要用于缓存服务的信息,实现批量拉取、注册服务功能,能够根据 Nginx 集群选择拉取与这一类集群相关的全部站点信息,同时经过推拉结合的方式保证数据实时和准确。并按期的把全部数据都拉到本地,依靠 ZK 的 watcher 方式来保证数据的实时到达。并发

健康检查

Nginx 主动健康检查有一些开源模块,但这些主动的健康检查会遇到一些问题。假设有一个站点 xxx.meituan.com,配在 upstream 里作健康检查,每一个 proxy 的服务器的每一个 worker 都会按期向后端服务发起健康检查。假如每秒检查一次,整个 Nginx 集群数量是 100,每一个单机实例上部署了 32 个 worker,健康检查的请求 QPS 就是 100×32,而实际服务器天天的 QPS 不到 10,加上健康检查机制就变成 3000 多了。因此咱们摒弃了在内部主动去作健康检查的方式,选择了 Scanner 去作周期性健康检查。此外, Scanner 支持自定义心跳,能够检查端口是否通畅、HTTP 的 url 是否准确,而且支持快慢线程的隔离。

动态 upstream

美团实现动态 upstream 用的是业内比较成熟的方式:Tengine 提供的 dyups 模块。它提供一个 dyups API,经过这个 API 添加、删除、建立服务节点,以后经过一个 worker 处理这一次修改请求,把请求放到了一个共享内存的队列中,各个 worker 会从这个队列把此次变动拉取出来在本地生效,而后落到本地的内存中,实现整个步骤。其中,第一次调用时是须要加锁,而后同步内存中尚未被消费的数据,同步完以后才会更新操做,保证了数据的串性。

dyups 存在的一些问题:

1.持久化

最大的问题是内存生效,由于它走的是本地 worker 进程内部的内存,因此下一次 reload 时,整个服务列表会丢失。咱们的解决方案是经过本地 Agent 来托管这个节点的更新和文件落地。当 Agent 按期感知到服务列表变化时,首先把本地生成的 upstream 文件更新,以后再去调用 dyups API,把这一次变动的节点实时同步到内存中,实现了服务节点不只落地到本地文件作持久化存储,同时还灌入到了 Nginx worker 内存中来保证服务的实施。

其中须要注意的是 reload 调用 dyups API 并发的问题。假如出现一种特殊的场景,Agent 感知到服务节点变化时,还没来得及落地 upstream 文件,这时候 Nginx 出现了一次 reload,更新的仍是旧的 upstream 文件。此时 dyups API 调用过来,通知须要更新服务节点,更新服务节点以后会把更新的信息放到共享内存中,相似于一个接收器,每个 worker 拿到更新以后才会把消息删除掉。这里可能出现一个问题,当 reload 的时候,出现了六个 worker 进程,有可能这一次更新被旧的 worker 进程拿掉了,致使新的 worker 没有更新,进而致使了新的 worker 里有部分是更新成功,有部分是更新不成功的。

咱们目前是把 Nginx 全部的 reload、start、stop 包括一些灌入的节点都统一交给 Agent 进行处理,保障了 reload 和 dyups API 调用的串行化。

2.流量倾斜

每台机器同一时刻更新节点,初始序列是同样的,致使流量倾斜。好比线上有 100 个服务节点,每 25 个节点一个机房,当灌入节点时顺序是一致的。从最开始选节点,第一个选的节点都是同样的,致使一次请求筛选的节点都是请求列表里的第一个,因此同一时刻全部的流量都到了同一台后端机器上。

咱们的解决方案是在 Nginx 内部加权轮训时的初始化节点,作了内部的 random,来保证每一个 worker 选的第一个节点都是随机化的节点,而不是根据原来的动态 upstream 加权轮训的方式保证的稳定的序列去选节点。

Nginx 结构化配置管理

如上图,建立站点能够直接在 Oceanus 平台上配置,提交后至关于创建了一个 Nginx 的 server 配置。同时支持导入功能,Nginx server 的配置文件能够实时导入,落到集群的机器上。

匹配规则

建完站点以后,能够直接配置映射规则,左侧是的 location,右侧对应的 pool 在美团内部是 appkey,每一个服务都有一个名字。以后会经过一些校验规则来验证配置的规则从 location 到 appkey 是否合法,或者是否超出预期。 当 location 配置规则很是复杂,中间出现一些正则时,做为一名业务 RD 在平台上配置规则时是很容易出问题,由于你不知道配置的规则是否正确,是否真的把原来想引流的流量导到了 appkey 上,仍是把错误地把不应导入这个服务的请求导到了 appkey 上。所以须要作不少的前置校验,目前美团内部使用的校验规则是模拟生成已有路径下的正则匹配的 url,用于测试哪些流量到了新部署的 appkey上作校验。这种校验也是有必定的不足,好比配置了不少正则匹配的方式,咱们模拟出来的 url 其实不足以覆盖全部的正则 url ,会致使校验不许确。目前咱们的规划是获取到全部的后端服务,好比 Java 的服务,后面会有 Controller,Controller 上有指定业务的 url,咱们能够针对业务的 url 去离线的日志里筛选出来它们历史上每一个路径下匹配真实的 url,用真实的 url 作一次回放,看是否匹配到了应该匹配的服务上去。

指令配置与流量统计

咱们也支持全部的 Nginx 上的指令配置,包括设置 Header、设置超时、rewrite、自定义指令等,或者咱们封装好的一些指令。 同时也支持一些服务的性能统计,好比说 QPS,HTTPS QPS,以及服务内部的 4XX、5XX。

负载均衡方案迭代历程

精细化分流

精细化分流项目的背景是美团在线上的一些需求,好比在线上但愿实现对某一个地域的用户作灰度的新功能特性更新,或者按百分比引流线上的流量,以及对固定流量的特征,选择让它落到固定后端的服务器上,保证这一部分的用户和其余的用户的物理隔离。

举个例子,上图右边是三台服务器都是服务 A,把其中两台服务器做为一个分组 group-G,Agent 获取到这个服务信息后,会把它实时落地到 upstream 文件里。若是是 group-G ,能够落到Upstream AGRG 的 upstream 文件中;若是是 upstream A,就和普通的服务同样落地好,3 个 server 同时落到一个服务上。此时前端有用户 ID 的请求进来,须要选择一种分流的策略,好比但愿用户的 ID 的 mod100 若是等于 1 的请求,路由到灰度的分组 groupG 上,经过这种策略的计算,把 1001 用户请求路由到 upstream A-GR-G 服务上,而后剩下的其余的用户都经过策略的筛选,路由到服务 A 上 。 

精细化分流具体实现的逻辑,首先在一个 worker 进程嵌入 timer,它会按期拉取策略配置,同时 DB 配置结构化写入共享内存的双 buffer,worker数据请时候,会从共享内存中读取策略进行匹配。策略匹配的粒度是 Host+Location+appkey,策略分为公共策略和私有策略,公共策略是整个全网都须要采用的一个策略,私有策略是能够针对本身的服务作一些定制化。 

当请求来临的时候,获取请求的上下文,经过 Host+Location 来查找它须要使用的策略集合,若是是匹配公共策略就直接生效,若是是私有策略就会按 appkey 查找策略。以上图为例,请求来了以后,获取到请求的上下文,以后经过请求上下文里的 Host+Location 去找相应的策略集合,而后可能找到了左下角的策略集合。

分流转发的过程是在 rewrite 阶段触发的,请求进入到 rewrite 阶段之后会解析策略数据,实时获取请求来源中的参数,经过参数和表达式渲染成表达式串:

if (ngx.var.xxx % 1000 = 1)    ups = ups + target_group;复制代码

经过执行这段命令,看是否命中分流策略,若是命中则改写路由的 ups 到指定的 ups group,不然不对 upstream 作修改。

泳道

微服务框架下服务个数多、调用链路较长,其中一个服务出问题会影响到整条链路。举个的例子,QA 提测每每须要该条链路上的多个服务配套测试,甚至是同时测试一个服务的多个演进版本,测试的科学性是不完善的,为了解决线下 QA 实现稳定的并发测试,咱们提出了泳道的概念。

如上图,有两个 QA。第一个 QA 能够创建属于本身的泳道 1,第二个 QA 能够创建属于本身的泳道 2。QA 1 测试的功能在 B、C、D 服务上,它只须要创建一个有关于此次测试特性的 B、C、D 的服务,就能够复用原来的骨干链路。好比骨干链路的请求经过泳道的域名进来,首先会路由到骨干链的 A 服务上,以后他会直接把此次请求转发给泳道 1 上的 B、C、D 服务,以后 D 服务由于没有部署和他不相干的服务,因此它又会回到骨干链路的 E 服务和 F 服务。

QA2 测试的功能主要是集中在 A 和 B 服务,它只须要单独部署一个 A 和 B 服务相关于本次测试特性服务就能够了。当请求进来,在泳道 2 上 A、B 服务流经结束,就会回到主干链路 C、D、E 和 F 服务上,从而实现并发测试的效果,同时保证了骨干链路的稳定,由于这个过程当中骨干链路是一直没有动的,惟一动的是要测试的那部分的内容。

同时多泳道并存能够保证多服务和多版本的并行测试,并作错误的隔离,极大的提升了的服务上线的流程。

泳道的实现基于精细化分流就很简单了。例如给服务 A 一个标签,它属于泳道 S,用一样的原理能够把它落地成 upstream A-SL-S,同时把泳道 IP 放到 upstream 里面,此时 A 服务上里没有泳道的机器。美团内部通常使用经过服务镜像的方式作服务的测试,经过 Docker 直接建立泳道的链路,自动化生成一个泳道的域名,经过测试域名访问就会直接把请求转发到泳道域名上。实现方案就是经过 Lua 泳道模块判断 Host 的命名规则和 Header 里是否有泳道,从而判断是否须要转发到后端的 upstream 节点上。

单元化

随着公司规模的不断扩大,咱们实现了第三套的负载均衡方案——单元化。首先先介绍一些问题,你的服务是否真的作到了水平的扩展?你的服务是否真的作到了物理隔离?

举个例子,如上图,一条业务线上有两套集群,服务 A 和服务 B,同时下面有数据库,数据库作了分库分表,而且服务也是分布式服务,它究竟是不是一个水平扩展的服务呢?

服务集群 A 和 B 的服务节点都有 N 个,当在服务集群 B 加一个节点时,全部服务集群 A 的节点都会与服务集群 B 中新加的节点创建一条链接,作长链接的链接池。长链接的资源实际上是不可水平扩展的,由于每加一台机器,承受的长链接的数量都是 N。同理这个问题最严重的是在 DB 上,DB 的主库通常都是单点的,即便分了库,全部的写请求都会放到主库上,它的长链接实际上是受限的,你如何怎么保证它的长链接一直在一个可控的范围内呢?

另外一个问题是任意节点有异常均可能影响全部的用户,服务集群 B 的 N 节点出现问题,此时服务集群 A 里的全部请求,都有可能转发给 B 集群的 N 服务节点,也就是说任意一个用户的请求均可能会受到影响。因此看似你作的整个的分布式的系统能作到水平扩展,但其实不是这样。 

为了解决上面的问题,咱们提出了单位化的操做。按用户的流量特征把全部的请求都框到一个服务单元内,一般服务单元都是按地域划分的。此时每一个单元内的服务是互相分布式调用的,可是跨单元的服务之间是没有关系的。原来服务集群 A 里的服务节点对服务集群 B 里的每个节点都创建链接,变成了只针对本身服务单元内的服务作长链接,这样链接数量就降到原来的 N 分之一。同时用户的流量会在某个单元内作闭环,实现了彻底的隔离。固然现实中单元化还有一些前提,好比说 DB 的数据分布,若是 DB 不能按单元划分,那单位化仍是实现不了。

Oceanus 网关层实现单元化的路由,复用了报文转换的功能模块,支持根据某个Header或者Get参数来修改、删除、新加 Header 或者 Get 参数。 

如上图的例子,假如从 App 端上来的请求,会带有地域特征,北京的用户可能带的 Location ID 是 0100一、0100二、01003。当它上来之后,咱们有一个 Map 映射表,它跟前面的精细化分流不太同样,而是经过路由表作路由筛选的,前面的多是基于表达式的。假如 01001 的Location 的路由表,它对应 Set ID 是 SET1,那么就直接在 01001 的用户请求里加一个 header,这个 header 的名称就是 SET1,这样就实现了报文的转换,也就是北京的用户在网关层都会新加一个 SET1 标识。以后就能够复用前面的精细化分流的方案,当遇到 SET1 的请求就转发到 SET1 的分组,从而实现了前端的单位化的路由方案。

将来规划

Oceanus 将来主要在配置动态化上作进一步优化,尤为是 location 动态化,由于经过文件配置 location 的方式,每次 reload 的操做,对线上的集群仍是有损的。同时但愿作到插件的管理动态化,它的热部署与升级,以及自动化运维。美团线上近千台机器,作自动化运维是很解放人效的操做,如何去快速搭建一个集群以及迁移各个集群的站点,是一个比较关键的任务。

演讲视频及PPT下载:

美团 HTTP 服务治理实践

相关文章
相关标签/搜索