基于 OpenResty 的动态服务路由方案

2019 年 5 月 11 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙武汉站,又拍云首席布道师在活动上作了《 基于 OpenResty 的动态服务路由方案 》的分享。html

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

邵海杨,又拍云首席布道师,运维总监,资深系统运维架构师,多年 CDN 行业架构设计、运维开发、团队管理相关经验,精通 Linux 系统及嵌入式系统,互联网高性能架构设计、CDN 加速、KVM 虚拟化及 OpenStack 云平台的研究,目前专一于容器及虚拟化技术在又拍云的私有云实践。git

 

如下是分享全文:github

 

今天和你们介绍一个基于 ngx_lua 的动态服务路由解决方案,它是整个容器化过程当中的组件,容器化在服务路由上有很大的挑战,又拍云经过本身的方案来实现了,而且已经稳定运行了三年左右。目前这个方案已经开源,若是你们后续也碰到同样的问题,能够直接使用这个方案。算法

服务 zero down-time 更新

在更新服务时,如何能作到让服务不断掉呢?又拍云作服务更新的时候,是不容许有失败的,若是由于咱们的更新失败致使请求失败,即便请求很是少,口碑上也会很差,并且若是形成了事故,是要赔钱的。这也是咱们作动态服务路由的重要缘由。数据库

服务路由主要包括如下几个部分:后端

  • 服务注册是指服务提供者在起来时,去服务发现注册,以代表提它提供的服务、端口、IP是多少,服务名是什么等;
  • 服务发现是集中管理服务的地方,记录了有哪些服务,它们在哪些地方;
  • 负载均衡,因为有不少一样的容器提供了一样的服务,须要考虑怎么在这些容器里作负载均衡。

服务发现有不少方案,可是它们的应用场景和语言都不太同样。Zookeeper 是一个比较老牌的开源项目,相对比较成熟,但对资源的要求比较高,是咱们最先使用的一个方案,包括咱们如今的 kafka、消息队列都是依赖 Zookeeper;etcd 和 Consul 是后起之秀,K8S 是依赖 etcd 的,etcd 在容器编排里面是依赖的;又拍云在服务注册和发现环节用了 Consul ,它是一站式的技术站,部署、可视化、维护等环节都比较方便,它不但支持 KV 存储,还有原生的服务监控、多数据中心、DNS 功能等。缓存

负载均衡也有不少方案, LVS 有一个优点是在作完前面两层后,若是性能很差能够再加一个 LVS,由于它在四层,更加底层,不会破坏原来的网络结构,可是它的扩展很是难。HA_PROXY 和 Nginx 各有千秋,HA_PROXY 对 HTTP 头部解析消耗的 CPU 更少,若是作纯转发,如 WAF 可使用 HA_PROXY,HA_PROXY 大概占 CPU 10% 左右 ,而 Nginx 作纯头部转发基本上是占 CPU 20%-25%,可是 Nginx 可扩展性更强,Nginx 能够作 TCP、UDP、HTTP 三种协议的转发和负载均衡,可是 HA_PROXY 只支持 TCP、HTTP。 HA_PROXY 最大的变化是它已经用 lua 重构,后续的发展也会与 lua 紧密结合,这至关因而又多了一种能力,它们也在拥抱 K8S 的生态圈。咱们的方案是选择了 Nginx ,由于它专一于作 HTTP ,扩展性好,支持 TCP。网络

如上图,咱们把 Nginx 和 Consul 放在一张图里。为了突出服务,这里把一些跟服务不太相关的都省略掉了。咱们基于 Mesos、Docker、 Marathon 作了服务管理。其中有一个特殊的服务是 Registrator,它会经过 Docker API 在每一个物理机上起一个容器,经过 Docker API,把容器的状态定时的汇报给 Consul。上面的 Nginx 作负载均衡,由于咱们的服务目前都是基于 Nginx 直接到容器里面。架构

Consul 里的服务如何更新到 Nginx

在前面的图里,Nginx 到容器、服务注册到配置文件都没有问题,可是从 Consul 到 Nginx 会出现问题,由于 Consul 有全部的信息,可是这些信息如何通知给 Nginx 呢?一个新的服务起来,或者是一个服务挂掉,这些信息 Consul 知道后怎么让 Nginx 把这些有问题的服务删掉,再把一些新写的服务加进去,这就是咱们要解决的问题。

这里的问题就是 Consul 里的服务如何更新到 Nginx,若是解决了这个问题,Nginx +Consul+Registrator 的模式就圆满了。目前也有不少方案能够来解决这个问题:

一、方案一:Consul_template

监听 Consul 里的 key,触发执行一个脚本,利用这个特性的服务,服务发生变更,会根据预先配置好的模板从新生成配置,这个就是最后要执行的一个脚本。

上图是一个例子,有模板生成 upstream.conf,中间都是未来要被渲染的一些变量,若是 K/v 发生变更,模板化生成一份真实的配置文件,而后再执行一个本地的命令,Nginx -s reload,从新生成配置文件,Reload 一下,这样新的服务就生效了。

固然 Reload 也会有一些缺点:

  • 第一,若是频繁 Reload 会有性能损耗;
  • 第二,旧进程长时间处于 shutting down 状态,若是链接里有长链接,旧的进程会一直处于中间进程,这个时间是不定的,你不知道到底何时Reload真正完成;
  • 第三,进程内缓存失效,咱们会把数据库的一些信息,一些代码所有缓存进本地,这样缓存就所有失效了;
  • 最重要的一点是与设计初衷不符,它设计的初衷是方便运维不影响当前的请求,就至关于拿 Docker 作虚拟机用同样走歪了,走歪了以后极可能会碰到不少奇怪的坑,因此当时没有用这个方案。

二、方案二:内部 NDS 方案

DNS 的方案也是比较经常使用的,好比把以前是一个 IP 地址的 Server,如今改为一个域名,只要把它解析掉一批 IP 就行了,这个听起来已经很完美了,并且 Consul 自己支持DNS,咱们也不用维护另外的 DNS 了,只要把这个 ID 换成域名就行了。

可是咱们感受使用 DNS 方案还不如作 Reload,缘由是

  • 第一,多了一层 DNS 解析时间,增长了额外的处理时间;
  • 第二,DNS 缓存,这是最主要的缘由,由于缓存的存在没办法当即把一台有问题的机器切掉,若是须要缓解这个问题,就要把缓存设得短一点,但这样解析次数就多了。
  • 第三,端口号会改变,物理机通常会配置同一个端口,在 Docker 里也能够这么作,但对于一些对网络不是很敏感的应用,好比一些强 CPU 的应用,咱们会直接把容器的网络用桥接的方式链接起来,而这时候端口是随机分配的,可能每一个容器分配的都不同,因此不可行。

咱们想要的是经过 HTTP 接口,动态修改 Nginx 的上游服务列表,咱们找到了现成的方案,叫 ngx_http_dyups_module。

三、方案三:ngx_http_dyups_module

ngx_http_dyups_module 能够经过 GET 接口查询当前的一些信息;POST 能够更新上游;也能经过 Delete 删除上游。

上图是一个例子,这个例子有三个请求:

  • 第一个,给 8080 这个服务端口发了请求以后,发现后面根本就没有任何的上游服务,因此它就 502 了;
  • 第二个,经过一个 Curl 的请求把两个服务地址给加进来;
  • 第三个,从新访问,第三条指令跟第一条指令是如出一辙,由于第二条已经把服务加进来了,因此这是一个正常的输出。

在这个过程里没有任何 Reload 的操做,也没有改配置,它就完成了一个功能。

这个模块写得很是好,可是咱们用了一段时间后把它下掉了,主要缘由不是由于它很差,而是咱们结合了一些自身的状况,发现了一些问题:

  • 第一,致使依赖 Nginx 自己的负载均衡算法。若是咱们内部用 Ngx_lua 写得比较多,用了这个模块以后,会致使咱们很是依赖 C 模块,也就是自身的一些负载均衡算法,咱们有本身特有的需求,好比“本机优先”,优先访问本机的服务,这样听起来比较奇怪的负载均衡,若是要作这些事情,咱们就要改 C 代码;
  • 第二,二次开发效率低,C 的开发效率远不及 Lua;
  • 第三,纯 lua 的方案没法使用,咱们作这样一个方案并非有一个项目能用就好了,而最好是其余项目均可以用。


动态负载均衡 Slardar 特性

基于以上这些缘由,咱们开始造本身的轮子。

这个轮子有四个部分:

  • 第一个部分,是最基础的 Nginx,咱们但愿用一些原生的指令和重试的策略;
  • 第二部分,是 lua 的模块;
  • 第三部分,是 lua_resty_checkups,这是咱们 lua 版的管理模块,实现了动态的upstream 管理,这个模块实现了大概 30% 的功能,并且还有一些主动的健康检查功能,它的代码量大概是 1500 行左右,若是是 C 模块估计至少有 1 万行;
  • 第四部分,是 luasocket,千万不能在 Nginx 在处理请求的时候用。

一、lua-resty-checkups

简单介绍下 lua_resty_checkups 这个模板,它有几个功能:

  • 第一,是动态 upstream 管理,基于共享内存实现 worker 间同步;
  • 第二,是被动健康检查,这个是 Nginx 自身的一个特性;
  • 第三,是主动健康检查,这个模块会主动给后端发心跳包,能够定时,15 秒发一次,检查后端的服务是否是存活。咱们还能够有一些个性化的检查,好比 heratbeat 定时给上游发送心跳包检测服务是否存活;
  • 第四,是负载均衡算法,本地优先可节约内网流量等。

二、服务区分

以 Host 区分服务:好比上图两个 curl 往同一个地址去发,这二者之间是不同的。

三、请求流程

简单介绍下请求的流程,它能够分为三个部分,最上面是接收请求,会加载一个 worker 代码,worker 代码执行完根据 host 找对应的列表,而后把这个请求代理给服务端。

四、动态 upstream 更新

这个跟 dyups 的 C 模块同样,也是经过 HTTP 接口来动态更新 upstream 列表,加完后能够在管理页面看到刚加进去的两个服务,这里会有 server 地址、一些健康检查的消息、状态变动的时间,以及它失败的次数,下图是一次主动健康检查的一个记录。

为何会有主动健康检查呢?你们平时用的就是一些被动的健康检查,也就是请求发出去以后失败了才知道失败了,主动的检查是发心跳包,在请求以前就能够知道服务是否是出问题了。

五、动态 lua 加载

动态 lua 加载在作游戏的时候会常常用到。一开始程序里面跑了一些 lua 的代码,给后端的程序作参数转化和作兼容,好比有一个小调整不乐意去改,就拿前面的路由去作,首先能够对请求作改写,由于我能够拿到整个请求,它的请求体能够作任意的事情。

此外,咱们还能够跟一些权限控制结合,作一些简单的参数检查。据咱们的统计,咱们至少有 10% 是重复请求,若是这些重复请求都去执行就是无谓的消耗,咱们会返 304,表示结果跟以前的同样,能够直接用以前的结果。在返 304 的同时,若是咱们须要后端的服务去判断,会把整个请求收下来,而后再日后面发,至关于内网带宽要增长一些,这样其实已经节省了带宽,能够不日后面发了。

这是一个动态负载加载的例子,若是把这段代码推到 Slardar 里面,它会执行,若是进行一个删除操做,它会返 403,便可以当即经过这个代码禁掉这个操做,那还有什么功能呢?你能够想象到的功能均可以作,并且这个过程是动态的,若是代码加载,也能够从状态页里看到它的信息。

动态负载均衡 Slardar 实现

前面介绍都是 Slardar 的特性,接下来简单介绍一下实现过程,一共分为三个部分: 动态 upstream 管理、负载均衡和动态 lua 代码加载。

一、动态 upstream 管理

启动时经过 luasocket 从 consul 加载配置文件,服务若是没有任何理由的挂了,挂了以后你刚起来时,你怎么知道刚刚怎么了呢?因此得有一个方式去固化这些东西,而咱们选的是 consul,因此它启动的时候必须从 consul 加载,启动以后要监听管理的端口,接收 upstream 更新指令,还要启动一个定时器,这个定时器作 worker 间的同步,定时从共享内存看一下有没有更新,有更新就能够同步在本身的 worker 里。

这是一个简单的流程图,最开始的时候从 consul 加载,在完成 fork 后到了 worker 进程,也就是刚刚初始化加载的那些 worker 都有了,另一部分启动定时器,一旦有更新就会进入到这个里面。


二、负载均衡

负载均衡咱们主要用到了 balance_by_lua_*,一个请求过来,经过 upstream 的 C 模块把这个请求往这里发,如图是配置文件,刚刚也有一个相似的,就是在这里写了地址。经过 balance_by_lua_* 指令,咱们会把它拦到这个文件里,就能够在这个 lua 文件里用 lua 代码选一个,这就是自身的一个 checkups 的选择的过程。

上图是大概的流程,能够先看下边部分,一开始的时候,checkups.select_peer 是咱们的模块,而后根据这个 host 再到当前的 peer 就跳出去了,这就实现了用 lua 控制。上面部分是要知道它是成功仍是失败的,若是它失败了,要对这个状态进行反馈。

三、动态 lua 加载

这个主要是用到 lua 的三个函数,分别是 loadfile、loadstring 和 setfenv。loadfile 是加载本地的 lua 代码,loadstring 是从 consul 或 HTTP 请求 body 加载代码,setfenv 设置代码的执行环境,经过这三个函数就能够加载,具体的实践细节这里就再也不介绍。

四、动态负载均衡 Slardar 的优点

这就是咱们造的轮子,主要用到 lua-resty-checkups 的模块和 balance_by_lua_* ,它有如下的优点:

  • 纯 lua 实现,不依赖第三方 C 模块,所以二次开发很是高效,减小维护负担;
  • 能够用 Nginx 原生的 proxy_*,由于咱们只在请求的选 peer 的那个阶段作,peer 选完以后,发数据的那个阶段是直接走 Nginx 本身的指令,因此它能够用到 Nginx 原生的 proxy_* 指令;
  • 它适用于几乎任何的 ngx_lua 项目,可同时知足纯 lua 方案与 C 方案。

在微服务架构里,Slardar 能作什么

咱们目前也在把以前的一些服务改形成微服务模式。微服务其实就是源于一个比较大的服务,把它拆分红一些小的服务,它的扩容跟迁移也不同,微服务的扩容能够只扩容其中一部分,扩容多少能够根据需求。

咱们如今正在尝试一个方案,这个方案背景是咱们有作图的需求,作图这个功能有不少,好比说美化、缩略、水印等,若是要对作图的服务进行优化是很是困难的,由于它功能太多了,若是咱们把它拆成微服务就不同了,好比上图虚线上面的是咱们如今的服务,这个是微服务的一个网关,下面是一些小的服务。好比说美化,它的运算比较复杂,耗 CPU 比较多,咱们确定选择一些 CPU 比较好的机器;用 GPU 来作缩略图,这个性能可能提升几十倍;最后是一个中规中矩的作图,那就普通的一些就够了。

还有一些比较偏门的,好比说梯度,可能只要保证服务能够用就好了,经过这个微服务的路由,咱们根据后面的区分把以前的一个服务,以及它的参数拆成三个小的服务,这样经过三个步骤能够完成一个作图的服务。

固然咱们在尝试这个方案其实也有不少的问题,好比一个服务原来用一个程序就能够作了,如今变成了三个,势必内网的带宽要增长了,中间的图片要被导来导去,这个怎么办呢?咱们如今想到的办法就是作一些本地优先的调度策略,即作完以后,本地有一些水印的,那就优先用本地的。

最后套用大师的一句话:Talk is cheap,Show me the code。目前咱们已经将 Sladar 项目开源,项目地址是: 。

 

演讲视频及PPT:

基于 OpenResty 的动态服务路由方案

相关文章
相关标签/搜索