2019 年 5 月 11 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙武汉站,斗鱼资深工程师张壮壮在活动上做了《 斗鱼 API 网关演进之路 》的分享。

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

1.jpg

张壮壮,斗鱼数据平台部资深工程师,负责打点、API 网关及后端服务架构建设。


以下是分享全文:


大家下午好,先简单做下自我介绍,我是来自斗鱼的张壮壮,曾就职于拉勾网和滴滴出行,2017 年 3 月加入斗鱼,主要负责 API 网关和数据采集等工作。

今天给大家带来斗鱼 API 网关的一些细节,在分享之前,先感谢刚才的邵海杨老师,因为我们的 API 网关基于 Slardar 二次开发的,刚才海杨老师已经详细介绍了 Slardar 的基础原理,所以这块内容我不再重复介绍,直接介绍斗鱼在此基础上做的更细节的工作。

今天我主要从三个方面来分享:

  • 斗鱼使用 API 网关的背景

  • 网关的架构&功能

  • 斗鱼 API 网关远期规划


为什么是 API 网关?

为什么是 API 网关?这要从微服务化遇到的两个问题说起,第一,怎样保证服务的无宕机更新部署;第二,怎样保证服务的自动扩容及故障恢复。这两个问题,又拍云已经有了解决方案:只需要在服务之上做服务路由,让路由支持服务的无宕机更新部署,保证服务的扩容及故障恢复,我们也是按照这个思路来实现的。

但是只有这个不能解决所有问题,比如服务的性能监控、系统的资源调度等问题,还需要其他基础设施来支撑。所以 Docker 和 Kubernetes 进入了我们的视野。由于本次活动主题是 OpenResty,对容器技术选型就不做展开了。服务上容器,在更新和迁移中 IP 和 Port 是变化的、不确定的,这是必须要解决的问题。

服务路由

服务路由要支持服务注册、服务发现和负载均衡。经过多方调研,我们发现又拍云开源的动态负载均衡组件 Slardar 非常适合业务场景,主要解决了容器环境服务 IP 和 Port 均变化的问题。

  • 服务注册是服务需要主动上报服务相关信息,重要的是 IP 和端口;

  • 服务发现是把服务注册的信息集中起来,最好持久化

  • 为了避免单点故障,服务会启动多个实例,因此还需要做负载均衡。

服务发现有很多开源的工具可以用,比如 Consul、etcd 和 Apache Zookeeper。由于我们选型是 K8S,所以服务发现选择 etcd。

负载均衡,基本上就是三个:LVS、HAProxy 和 Nginx。

2.jpg

接下来我们简单了解下 Slardar,它由四个部分组成:

  • 第一部分是官方的 Nginx,没有任何改动;

  • 第二部分是 Nginx Lua 模块,核心是 Lua 版本的负载均衡算法+ balance_by_lua

  • 第三部分是 lua-resty-checkups,它是把 Nginx upstream 模块常用的功能单独抽出来,用 lua 重新实现了一遍;

  • 第四部分是 luacocket,用于加载配置信息

Slardar 在启动过程中先拉取服务配置,拉取完配置就可以对外进行服务了。如果我们的服务因为扩容或者异常宕机又起了一个新的实例,此时 IP和 Port 都会变化,需要把服务 IP 和 Port 等信息注册到 Slardar 和 Consul。逻辑清晰,结构简单,这是选择 Slardar 的原因,不过想要真正应用,仅仅有动态负载均衡远远不够,还需要解决以下的问题:

  • 服务如何在启动后自动上报信息到 Consul

  • Slardar 如何解决自身单点问题

  • 怎样应对 Consul 集群故障或者网络故障

  • 没有可视化管理;

  • 灰度测试、AB 测试、流量复制等等功能的实现(考虑未来使用场景)

3.jpg

我们拿到了 Slardar 进行了大刀阔斧地调整:

  1. 取消 Consul,实现注册中心,并持久化配置到数据库;

  2. 开发 Java agent,实现 Java 服务自动上报;

  3. 对接 Kubernetes API,实现非 Java 服务自动上报;

  4. 提供可视化管理后台;

  5. 配置定时落盘,当网络故障时作为托底;

  6. 定时全量拉取配置,增加集群(机房)概念,可一键切换流量;

  7. 支持集群部署,应对单点问题;

  8. upstrem 列表 key 由 Host 改为 Host+URI(前缀);

  9. 引入插件模式,新增了很多功能,如灰度测试、AB测试、流量复制、服务限流等。


斗鱼 API 网关的架构&功能

下面介绍斗鱼的 API 网关的部署架构,以及内部功能细节。       

4.jpg

抽象来看,服务接入 API 网关的架构非常清晰,和原生 Nginx 架构一样,所有流量必须经过 API 网关后,才能访问到真实的后端。经服务端处理,并将响应返回给 API 网关之后,再交给客户端。这是单机房的部署架构。实际上使用 Nginx 作为入口网关,API 网关作为内网网关,Nginx 负责处理复杂的 location 逻辑、SSL 认证等,API  网关负责抽象后台服务间通用功能。

5.jpg

这是多机房部署架构,上游使用 CDN 来做流量分配,方便机房故障时进行一键流量切换。

6.jpg

这张图很好的展示了斗鱼 API 网关生态体系。

上图左侧是 API 网关的内部功能,绿色部分是已实现的功能,包括限流、OA 认证、请求限制、AB 测试、灰度测试、流量复制、蓝绿发布、API 开放平台等。灰色部分是即将实现的功能,蓝色部分是 Slardar 原生的功能,当然我们也做了大量的优化工作,比如:路由算法支持动态的权重更新。

上图右侧 API 网关的支撑服务,其中网关管理 MIS 系统是可视化管理后台。日志聚合提供了接入服务的性能图表,比如状态码 4XX,5XX 统计,以及请求不同水位线耗时分布,俗称 P90,P99。天眼是 Java agent,负责实现服务对接下面的注册中心、实现服务优雅停机。

上图下侧是 API 网关代理的服务,如搜索、推荐、风控、流量分发等等。 

7.jpg

以上整个罗列了我们已经实现且在线使用的重要功能。因为时间关系,后面我会挑选其中三个功能详细介绍实现原理。

OA 认证为了解决后端服务各自对接 OA 认证繁琐,比较典型的是内部使用的开源系统,如Kibana、zabbix、Dubbo Admin 等,这些开源组件我们不可能投入人力去二次开发,但是需要接 OA,还需要进行一些 ACL 权限控制。

QPS 限流用的是非常简单的计数器模式,是单机版的,限流是为了保证斗鱼的核心服务,比如视频拉流,还有刚才提到的推荐、搜索免受洪峰攻击。

服务兜底这个功能想必大家非常了解,它能保证上游的数据服务永不消失,它与 QPS 限流其实是结合在一起的,触发限流的请求会直接返回兜底数据,这可以保证在极端情况下,我们的后端服务不会被瞬时流量打垮,同时保证友好的用户体验。典型的场景就是 S 级主播的首秀,例如 3 月份PDD 的首播就给我们带来了非常大的流量冲击,通过 QPS 限流保证了我们的核心服务不受影响。

流量复制:在不影响用户正常请求的前提下,将原始请求复制一份或者多份,供开发人员在线对服务进行功能测试、性能测试和压力测试。功能上支持自身复制及跨域名复制,流量的放大和缩小。

AB测试、灰度测试和蓝绿发布,可以归为一类,均是维护多套 upstream 列表,通过某种策略,将不同的请求代理到不同 upstream 。

签名认证:对外暴露的接口,需要一套签名算法,避免服务直接裸露在外,所以这里面做了一个功能抽象。

服务高可用

我们保证服务高可用其实就是要处理两个场景:第一个场景是它的更新部署;第二个场景就是运行期间故障。

我们从服务更新部署和服务下线来看考虑第一个场景。

更新部署

想要避免服务更新部署导致请求异常,其实满足两个条件即可:

  • 第一保证注册到注册中心的服务都是已经可以对外提供服务的

  • 第二下线之前,先从反向代理列表中剔除       

8.jpg

第一个是保证服务注册到注册中心的实例,一定是可以对外服务的状态,即某些服务一定要在启动完成后才能加入到中心。另一个场景是一些服务需要热加载,启动完了后不能立即对外进行服务,这时服务有一个预热的过程,一定是预热完了后才能注册到注册中心。

第二个是下线时通过 trap 命令,在接收到程序结束(terminate)和程序终止(interrupt)时执行 shutdown 函数,这里 shutdown 函数会先通知注册中心下线改实例,然后 hang 住 5-10 秒,等待下线事件更新到网关。

这里特别说明一下服务更新流程,因为 API 网关是基于 Nginx 的,所以控制单个 worker 进程全量从注册中心拉取 upstream 配置信息,并写入 lua_shared_dict,其他的 worker 定时同步lua_shared_dict 配置信息到本地缓存。为了反向代理的高性能,网关是从本地缓存获取 upstream 信息,而不是从 lua_shared_dict 。

运行期

接下来我们看运行期故障怎么处理。

9.jpg

我们是这样做的:心跳探活+状态回溯,我们知道只有心跳探活是不能完全保证服务的高可用的,于是加上了“状态回溯”。

另外说一句:“状态回溯”是我自己想的,不知道用的对不对,即一次请求过来,如果反向代理失败,就将该服务标记为不可用。

加上之前启动和停止的操作,就保证了在任何情况下,正常的发布或者某台实例异常故障不会出现服务不可用的瞬间。这里就解决了之前提到的保证服务的无宕机更新部署和保证服务的自动扩容和故障恢复这两个问题。

AB 测试

AB 测试我们只是做了一个通用的功能。我们的 AB 测试是基于 Nginx  rewrite 命令,主要使用场景有:

  • 需测试的 ABCD…策略(或算法,或模型,或服务)会长期且同时存在于生产环境;

  • 不想同时维护多套代码分支;

  • 客户端无需改造。

目前网关对外提供 AB Test 功能是面向 HTTP 服务的,其实现原理是:后台服务提供一个默认接口,同时提供多种需要在线验证的其他接口,请求到达API网关,根据命中规则,重定向至对应的接口。其中,源 URI 作为对外暴露的接口,保持固定不变。

AB 测试规则支持白名单、尾数、轮询策略——一致性哈希等等。当然,业界还是另外一种实现方式,比如马蜂窝根据策略访问不同的 upstream 列表。

服务兜底      

10.jpg

请求到达网关之后,会反向代理给后端,如果是错误状态码,比如 500、502、503,我们直接拿到兜底数据,返回给客户端就可以了。由于 OpenResty 的限制,无法在 header_filter_by_lua* 和 body_filter_by_lua 中使用发起非阻塞的 HTTP 请求或者其他依赖 TCP 的协议(如 Redis)去查询兜底数据(原因请点击https://github.com/openresty/lua-Nginx-module),有过 OpenResty 或者 Nginx 模块开发经验的同学应该都知道:阻塞 API 的使用将会大大的降低 Nginx 的性能。

业界是怎么处理这个问题的?我见到的有基于 OpenResty 实现的兜底功能,均是使用ngx.location.capture 来主动发起请求给上游服务,而非使用 Nginx 原生的命令 proxy_pass 来实现反向代理。ngx.location.capture 的返回结果包含了响应状态码,如果状态码属于 Error Code,则去查询兜底数据,并返回给 C 端用户。

    --子查询代理完成请求local res = ngx.location.capture('/backend' .. request_uri, {    method = method,    always_forward_body = true})
    if 504 == res.status then return bottom_valueend

    使用 ngx.location.capture 替换 proxy_pass 的问题在于,HTTP 协议的应用场景非常广泛,Nginx 实现反向代理时已经做了大量的适配和场景覆盖,使用 ngx.location.capture 相当于重新 Nginx 的反向代理模块,需要考虑诸如:文件上传下载、静态资源和动态资源、是否传递 Cookie 等等场景。任何一个应用场景的遗漏都将是一个线上 BUG。

    我们要做服务兜底的时候,网关已经上线了一年多,这个时候如果想要重写,无异于火中取栗。比较幸运的是,在查看 Nignx 官方文档的时候,发现原生命令 error_page。       

    11.jpg

    示例中请求 location / 如果上游服务返回 404,则会内部重定向至 @fallback,而 @fallback 接口中可以发起二次 HTTP 请求,例如获取兜底数据,并且是可以使用非阻塞类库的。最后实现的核心代码如下:

      location / {    proxy_pass  http://backend;    error_page 500 502 503 504 =200 @janus_bottom;}location @janus_bottom {    content_by_lua '        local bottom = require "bottom";        --非阻塞获取兜底数据,并返回给client        bottom.bottom();    ';}

      总结一下斗鱼的 API 网关,就是运行在所有的 HTTP 服务上,提供通用、可抽象的服务治理功能。


      斗鱼 API 网关的远期规划

      2018 年我参加了杭州的 OpenResty 大会,当时受到了很大的启发。

      12.jpg

      现在 API 网关已经全面应用到斗鱼整个后端架构,我们对其做了一些规划。首先网关是分集群的,由于请求量非常大,磁盘的性能不高,反而会反向影响 API 网关吞吐性能。因此我们做了很多优化,比如上 SSD,精简日志等,这些功能都已经上线了,日志精简完后大概是原来体量的 2/3,效果非常明显,当然 SSD 才是“银弹”。

      我们后期的一个想法是将网关日志直接写入 MQ,后续用专门的服务消费 MQ,把日志落地到本地磁盘。另外一个想法是从 MQ 里面直接消费到 ES 里面,做一些其他的分析。

      然后,就是将请求分同步和异步两条线路。客户端请求到达 API 网关,网关会经过如限流,服务兜底,AB测试,灰度测试等功能,这些功能都是在同步的逻辑线路中。显然,同步逻辑功能越多,势必会影响客户端请求 HTTP 的时延。所以我们在 API 网关这一块,将请求日志放到 MQ,由一些模块直接消费这个 MQ,比如经过风控、流量控制、限流分析,产生一些 API 网关可以使用的配置,写入到 DB 里。网关通过拉取 DB 的配置,对后续的请求做一些限制。我们现在的同步链路已经比较完善,下一步重点是异步链路,而且我相信异步链路应该是整个 API 网关体系中更加庞大的生态链。