2019 年 5 月 11 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙武汉站,斗鱼资深工程师张壮壮在活动上作了《 斗鱼 API 网关演进之路 》的分享。html
OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推进 OpenResty 开源项目的发展。活动已前后在深圳、北京、武汉举办,后续还将陆续在上海、广州、杭州等城市巡回举办。git
张壮壮,斗鱼数据平台部资深工程师,负责打点、API 网关及后端服务架构建设。github
如下是分享全文:算法
你们下午好,先简单作下自我介绍,我是来自斗鱼的张壮壮,曾就任于拉勾网和滴滴出行,2017 年 3 月加入斗鱼,主要负责 API 网关和数据采集等工做。数据库
今天给你们带来斗鱼 API 网关的一些细节,在分享以前,先感谢刚才的邵海杨老师,由于咱们的 API 网关基于 Slardar 二次开发的,刚才海杨老师已经详细介绍了 Slardar 的基础原理,因此这块内容我再也不重复介绍,直接介绍斗鱼在此基础上作的更细节的工做。后端
今天我主要从三个方面来分享:跨域
为何是 API 网关?缓存
为何是 API 网关?这要从微服务化遇到的两个问题提及,第一,怎样保证服务的无宕机更新部署;第二,怎样保证服务的自动扩容及故障恢复。这两个问题,又拍云已经有了解决方案:只须要在服务之上作服务路由,让路由支持服务的无宕机更新部署,保证服务的扩容及故障恢复,咱们也是按照这个思路来实现的。bash
可是只有这个不能解决全部问题,好比服务的性能监控、系统的资源调度等问题,还须要其余基础设施来支撑。因此 Docker 和 Kubernetes 进入了咱们的视野。因为本次活动主题是 OpenResty,对容器技术选型就不作展开了。服务上容器,在更新和迁移中 IP 和 Port 是变化的、不肯定的,这是必需要解决的问题。网络
服务路由要支持服务注册、服务发现和负载均衡。通过多方调研,咱们发现又拍云开源的动态负载均衡组件 Slardar 很是适合业务场景,主要解决了容器环境服务 IP 和 Port 均变化的问题。
服务发现有不少开源的工具能够用,好比 Consul、etcd 和 Apache Zookeeper。因为咱们选型是 K8S,因此服务发现选择 etcd。
负载均衡,基本上就是三个:LVS、HAProxy 和 Nginx。
接下来咱们简单了解下 Slardar,它由四个部分组成:
Slardar 在启动过程当中先拉取服务配置,拉取完配置就能够对外进行服务了。若是咱们的服务由于扩容或者异常宕机又起了一个新的实例,此时 IP和 Port 都会变化,须要把服务 IP 和 Port 等信息注册到 Slardar 和 Consul。逻辑清晰,结构简单,这是选择 Slardar 的缘由,不过想要真正应用,仅仅有动态负载均衡远远不够,还须要解决如下的问题:
咱们拿到了 Slardar 进行了大刀阔斧地调整:
斗鱼 API 网关的架构&功能
下面介绍斗鱼的 API 网关的部署架构,以及内部功能细节。
抽象来看,服务接入 API 网关的架构很是清晰,和原生 Nginx 架构同样,全部流量必须通过 API 网关后,才能访问到真实的后端。经服务端处理,并将响应返回给 API 网关以后,再交给客户端。这是单机房的部署架构。实际上使用 Nginx 做为入口网关,API 网关做为内网网关,Nginx 负责处理复杂的 location 逻辑、SSL 认证等,API 网关负责抽象后台服务间通用功能。
这是多机房部署架构,上游使用 CDN 来作流量分配,方便机房故障时进行一键流量切换。
这张图很好的展现了斗鱼 API 网关生态体系。
上图左侧是 API 网关的内部功能,绿色部分是已实现的功能,包括限流、OA 认证、请求限制、AB 测试、灰度测试、流量复制、蓝绿发布、API 开放平台等。灰色部分是即将实现的功能,蓝色部分是 Slardar 原生的功能,固然咱们也作了大量的优化工做,好比:路由算法支持动态的权重更新。
上图右侧 API 网关的支撑服务,其中网关管理 MIS 系统是可视化管理后台。日志聚合提供了接入服务的性能图表,好比状态码 4XX,5XX 统计,以及请求不一样水位线耗时分布,俗称 P90,P99。天眼是 Java agent,负责实现服务对接下面的注册中心、实现服务优雅停机。
上图下侧是 API 网关代理的服务,如搜索、推荐、风控、流量分发等等。
以上整个罗列了咱们已经实现且在线使用的重要功能。由于时间关系,后面我会挑选其中三个功能详细介绍实现原理。
**OA 认证:**为了解决后端服务各自对接 OA 认证繁琐,比较典型的是内部使用的开源系统,如Kibana、zabbix、Dubbo Admin 等,这些开源组件咱们不可能投入人力去二次开发,可是须要接 OA,还须要进行一些 ACL 权限控制。
**QPS 限流:**用的是很是简单的计数器模式,是单机版的,限流是为了保证斗鱼的核心服务,好比视频拉流,还有刚才提到的推荐、搜索免受洪峰攻击。
**服务兜底:**这个功能想必你们很是了解,它能保证上游的数据服务永不消失,它与 QPS 限流实际上是结合在一块儿的,触发限流的请求会直接返回兜底数据,这能够保证在极端状况下,咱们的后端服务不会被瞬时流量打垮,同时保证友好的用户体验。典型的场景就是 S 级主播的首秀,例如 3 月份PDD 的首播就给咱们带来了很是大的流量冲击,经过 QPS 限流保证了咱们的核心服务不受影响。
流量复制:在不影响用户正常请求的前提下,将原始请求复制一份或者多份,供开发人员在线对服务进行功能测试、性能测试和压力测试。功能上支持自身复制及跨域名复制,流量的放大和缩小。
AB测试、灰度测试和蓝绿发布,能够归为一类,均是维护多套 upstream 列表,经过某种策略,将不一样的请求代理到不一样 upstream 。
**签名认证:**对外暴露的接口,须要一套签名算法,避免服务直接裸露在外,因此这里面作了一个功能抽象。
咱们保证服务高可用其实就是要处理两个场景:第一个场景是它的更新部署;第二个场景就是运行期间故障。
咱们从服务更新部署和服务下线来看考虑第一个场景。
想要避免服务更新部署致使请求异常,其实知足两个条件便可:
第一个是保证服务注册到注册中心的实例,必定是能够对外服务的状态,即某些服务必定要在启动完成后才能加入到中心。另外一个场景是一些服务须要热加载,启动完了后不能当即对外进行服务,这时服务有一个预热的过程,必定是预热完了后才能注册到注册中心。
第二个是下线时经过 trap 命令,在接收到程序结束(terminate)和程序终止(interrupt)时执行 shutdown 函数,这里 shutdown 函数会先通知注册中心下线改实例,而后 hang 住 5-10 秒,等待下线事件更新到网关。
这里特别说明一下服务更新流程,由于 API 网关是基于 Nginx 的,因此控制单个 worker 进程全量从注册中心拉取 upstream 配置信息,并写入 lua_shared_dict,其余的 worker 定时同步lua_shared_dict 配置信息到本地缓存。为了反向代理的高性能,网关是从本地缓存获取 upstream 信息,而不是从 lua_shared_dict 。
接下来咱们看运行期故障怎么处理。
咱们是这样作的:心跳探活+状态回溯,咱们知道只有心跳探活是不能彻底保证服务的高可用的,因而加上了“状态回溯”。
另外说一句:“状态回溯”是我本身想的,不知道用的对不对,即一次请求过来,若是反向代理失败,就将该服务标记为不可用。
加上以前启动和中止的操做,就保证了在任何状况下,正常的发布或者某台实例异常故障不会出现服务不可用的瞬间。这里就解决了以前提到的保证服务的无宕机更新部署和保证服务的自动扩容和故障恢复这两个问题。
AB 测试咱们只是作了一个通用的功能。咱们的 AB 测试是基于 Nginx rewrite 命令,主要使用场景有:
目前网关对外提供 AB Test 功能是面向 HTTP 服务的,其实现原理是:后台服务提供一个默认接口,同时提供多种须要在线验证的其余接口,请求到达API网关,根据命中规则,重定向至对应的接口。其中,源 URI 做为对外暴露的接口,保持固定不变。
AB 测试规则支持白名单、尾数、轮询策略——一致性哈希等等。固然,业界仍是另一种实现方式,好比马蜂窝根据策略访问不一样的 upstream 列表。
请求到达网关以后,会反向代理给后端,若是是错误状态码,好比 500、50二、503,咱们直接拿到兜底数据,返回给客户端就能够了。因为 OpenResty 的限制,没法在 header_filter_by_lua* 和 body_filter_by_lua 中使用发起非阻塞的 HTTP 请求或者其余依赖 TCP 的协议(如 Redis)去查询兜底数据(缘由请点击github.com/openresty/l…),有过 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。
示例中请求 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 服务上,提供通用、可抽象的服务治理功能。
2018 年我参加了杭州的 OpenResty 大会,当时受到了很大的启发。
如今 API 网关已经全面应用到斗鱼整个后端架构,咱们对其作了一些规划。首先网关是分集群的,因为请求量很是大,磁盘的性能不高,反而会反向影响 API 网关吞吐性能。所以咱们作了不少优化,好比上 SSD,精简日志等,这些功能都已经上线了,日志精简完后大概是原来体量的 2/3,效果很是明显,固然 SSD 才是“银弹”。
咱们后期的一个想法是将网关日志直接写入 MQ,后续用专门的服务消费 MQ,把日志落地到本地磁盘。另一个想法是从 MQ 里面直接消费到 ES 里面,作一些其余的分析。
而后,就是将请求分同步和异步两条线路。客户端请求到达 API 网关,网关会通过如限流,服务兜底,AB测试,灰度测试等功能,这些功能都是在同步的逻辑线路中。显然,同步逻辑功能越多,势必会影响客户端请求 HTTP 的时延。因此咱们在 API 网关这一块,将请求日志放到 MQ,由一些模块直接消费这个 MQ,好比通过风控、流量控制、限流分析,产生一些 API 网关可使用的配置,写入到 DB 里。网关经过拉取 DB 的配置,对后续的请求作一些限制。咱们如今的同步链路已经比较完善,下一步重点是异步链路,并且我相信异步链路应该是整个 API 网关体系中更加庞大的生态链。
演讲视频及PPT下载: 斗鱼张壮壮:斗鱼 API 网关演进之路