基于 OpenResty 的动态服务路由方案
2019 年 5 月 11 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙武汉站,又拍云首席布道师在活动上做了《 基于 OpenResty 的动态服务路由方案 》的分享。
OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推动 OpenResty 开源项目的发展。活动已先后在深圳、北京、武汉举办,后续还将陆续在上海、广州、杭州等城市巡回举办。
邵海杨,又拍云首席布道师,运维总监,资深系统运维架构师,多年 CDN 行业架构设计、运维开发、团队管理相关经验,精通 Linux 系统及嵌入式系统,互联网高性能架构设计、CDN 加速、KVM 虚拟化及 OpenStack 云平台的研究,目前专注于容器及虚拟化技术在又拍云的私有云实践。
以下是分享全文:
今天和大家介绍一个基于 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 的模式就圆满了。目前也有很多方案可以来解决这个问题:
1、方案一:Consul_template
监听 Consul 里的 key,触发执行一个脚本,利用这个特性的服务,服务发生变动,会根据预先配置好的模板重新生成配置,这个就是最后要执行的一个脚本。
上图是一个例子,有模板生成 upstream.conf,中间都是将来要被渲染的一些变量,如果 K/v 发生变动,模板化生成一份真实的配置文件,然后再执行一个本地的命令,Nginx -s reload,重新生成配置文件,Reload 一下,这样新的服务就生效了。
当然 Reload 也会有一些缺点:
- 第一,如果频繁 Reload 会有性能损耗;
- 第二,旧进程长时间处于 shutting down 状态,如果连接里有长连接,旧的进程会一直处于中间进程,这个时间是不定的,你不知道到底什么时候Reload真正完成;
- 第三,进程内缓存失效,我们会把数据库的一些信息,一些代码全部缓存进本地,这样缓存就全部失效了;
- 最重要的一点是与设计初衷不符,它设计的初衷是方便运维不影响当前的请求,就相当于拿 Docker 做虚拟机用一样走歪了,走歪了之后很可能会碰到很多奇怪的坑,所以当时没有用这个方案。
2、方案二:内部 NDS 方案
DNS 的方案也是比较常用的,比如把之前是一个 IP 地址的 Server,现在改成一个域名,只要把它解析掉一批 IP 就好了,这个听起来已经很完美了,而且 Consul 本身支持DNS,我们也不用维护另外的 DNS 了,只要把这个 ID 换成域名就好了。
但是我们感觉使用 DNS 方案还不如做 Reload,原因是
- 第一,多了一层 DNS 解析时间,增加了额外的处理时间;
- 第二,DNS 缓存,这是最主要的原因,因为缓存的存在没办法立即把一台有问题的机器切掉,如果需要缓解这个问题,就要把缓存设得短一点,但这样解析次数就多了。
- 第三,端口号会改变,物理机一般会配置同一个端口,在 Docker 里也可以这么做,但对于一些对网络不是很敏感的应用,比如一些强 CPU 的应用,我们会直接把容器的网络用桥接的方式连接起来,而这时候端口是随机分配的,可能每个容器分配的都不一样,所以不可行。
我们想要的是通过 HTTP 接口,动态修改 Nginx 的上游服务列表,我们找到了现成的方案,叫 ngx_http_dyups_module。
3、方案三: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 在处理请求的时候用。
1、lua-resty-checkups
简单介绍下 lua_resty_checkups 这个模板,它有几个功能:
- 第一,是动态 upstream 管理,基于共享内存实现 worker 间同步;
- 第二,是被动健康检查,这个是 Nginx 自身的一个特性;
- 第三,是主动健康检查,这个模块会主动给后端发心跳包,可以定时,15 秒发一次,检查后端的服务是不是存活。我们还可以有一些个性化的检查,比如 heratbeat 定时给上游发送心跳包检测服务是否存活;
- 第四,是负载均衡算法,本地优先可节约内网流量等。
2、服务区分
以 Host 区分服务:比如上图两个 curl 往同一个地址去发,这两者之间是不一样的。
3、请求流程
简单介绍下请求的流程,它可以分为三个部分,最上面是接收请求,会加载一个 worker 代码,worker 代码执行完根据 host 找对应的列表,然后把这个请求代理给服务端。
4、动态 upstream 更新
这个跟 dyups 的 C 模块一样,也是通过 HTTP 接口来动态更新 upstream 列表,加完后可以在管理页面看到刚加进去的两个服务,这里会有 server 地址、一些健康检查的消息、状态变更的时间,以及它失败的次数,下图是一次主动健康检查的一个记录。
为什么会有主动健康检查呢?大家平时用的就是一些被动的健康检查,也就是请求发出去之后失败了才知道失败了,主动的检查是发心跳包,在请求之前就可以知道服务是不是出问题了。
5、动态 lua 加载
动态 lua 加载在做游戏的时候会经常用到。一开始程序里面跑了一些 lua 的代码,给后端的程序做参数转化和做兼容,比如有一个小调整不乐意去改,就拿前面的路由去做,首先可以对请求做改写,因为我可以拿到整个请求,它的请求体可以做任意的事情。
此外,我们还可以跟一些权限控制结合,做一些简单的参数检查。据我们的统计,我们至少有 10% 是重复请求,如果这些重复请求都去执行就是无谓的消耗,我们会返 304,表示结果跟之前的一样,可以直接用之前的结果。在返 304 的同时,如果我们需要后端的服务去判断,会把整个请求收下来,然后再往后面发,相当于内网带宽要增加一些,这样其实已经节省了带宽,可以不往后面发了。
这是一个动态负载加载的例子,如果把这段代码推到 Slardar 里面,它会执行,如果进行一个删除操作,它会返 403,即可以立即通过这个代码禁掉这个操作,那还有什么功能呢?你可以想象到的功能都可以做,而且这个过程是动态的,如果代码加载,也可以从状态页里看到它的信息。
动态负载均衡 Slardar 实现
前面介绍都是 Slardar 的特性,接下来简单介绍一下实现过程,一共分为三个部分: 动态 upstream 管理、负载均衡和动态 lua 代码加载。
1、动态 upstream 管理
启动时通过 luasocket 从 consul 加载配置文件,服务如果没有任何理由的挂了,挂了之后你刚起来时,你怎么知道刚刚怎么了呢?所以得有一个方式去固化这些东西,而我们选的是 consul,所以它启动的时候必须从 consul 加载,启动之后要监听管理的端口,接收 upstream 更新指令,还要启动一个定时器,这个定时器做 worker 间的同步,定时从共享内存看一下有没有更新,有更新就可以同步在自己的 worker 里。
这是一个简单的流程图,最开始的时候从 consul 加载,在完成 fork 后到了 worker 进程,也就是刚刚初始化加载的那些 worker 都有了,另外一部分启动定时器,一旦有更新就会进入到这个里面。
2、负载均衡
负载均衡我们主要用到了 balance_by_lua_*,一个请求过来,通过 upstream 的 C 模块把这个请求往这里发,如图是配置文件,刚刚也有一个类似的,就是在这里写了地址。通过 balance_by_lua_* 指令,我们会把它拦到这个文件里,就可以在这个 lua 文件里用 lua 代码选一个,这就是自身的一个 checkups 的选择的过程。
上图是大概的流程,可以先看下边部分,一开始的时候,checkups.select_peer 是我们的模块,然后根据这个 host 再到当前的 peer 就跳出去了,这就实现了用 lua 控制。上面部分是要知道它是成功还是失败的,如果它失败了,要对这个状态进行反馈。
3、动态 lua 加载
这个主要是用到 lua 的三个函数,分别是 loadfile、loadstring 和 setfenv。loadfile 是加载本地的 lua 代码,loadstring 是从 consul 或 HTTP 请求 body 加载代码,setfenv 设置代码的执行环境,通过这三个函数就可以加载,具体的实践细节这里就不再介绍。
4、动态负载均衡 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 项目开源,项目地址是:https://github.com/upyun/slardar 。
演讲视频及PPT:
基于 OpenResty 的动态服务路由方案的更多相关文章
- 基于ngx_lua的动态服务路由方案
基于ngx_lua的动态服务路由方案 http://geek.csdn.net/news/detail/131497
- HelloTalk 基于 OpenResty 的全球化探索之路
2019 年 12 月 14 日,又拍云联合 Apache APISIX 社区举办 API 网关与高性能服务最佳实践丨Open Talk 广州站活动,HelloTalk, Inc. 后台技术负责人李凌 ...
- RPC原来就是Socket——RPC框架到dubbo的服务动态注册,服务路由,负载均衡演化
序:RPC就是使用socket告诉服务端我要调你的哪一个类的哪一个方法然后获得处理的结果.服务注册和路由就是借助第三方存储介质存储服务信息让服务消费者调用.然我们自己动手从0开始写一个rpc功能以及实 ...
- 基于OpenResty与Consul实现服务网格ServiceMesh
一.逻辑架构 1.基于OpenResty开发智能代理: 利用其动态可编程特性,动态化配置nginx服务路由: 2.需要向OpenResty添加weibo开源的upsync服务发现模块: 3.基于con ...
- 几种常见的微服务架构方案简述——ZeroC IceGrid、Spring Cloud、基于消息队列
微服务架构是当前很热门的一个概念,它不是凭空产生的,是技术发展的必然结果.虽然微服务架构没有公认的技术标准和规范草案,但业界已经有一些很有影响力的开源微服务架构平台,架构师可以根据公司的技术实力并结合 ...
- 几种常见的微服务架构方案——ZeroC IceGrid、Spring Cloud、基于消息队列、Docker Swarm
微服务架构是当前很热门的一个概念,它不是凭空产生的,是技术发展的必然结果.虽然微服务架构没有公认的技术标准和规范草案,但业界已经有一些很有影响力的开源微服务架构平台,架构师可以根据公司的技术实力并结合 ...
- TopoLVM: 基于LVM的Kubernetes本地持久化方案,容量感知,动态创建PV,轻松使用本地磁盘
正文 研发测试场景下,一般追求的是一键快速起环境,横向动态复制,一人一套,随起随用,用完即走.作为使用方,其不用关心实际的物理资源是怎样的,环境起在哪里,只要声明自己的使用需求即可.但作为方案构建者以 ...
- 基于geoserver的REST服务完成mysql数据源动态发布
文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1. 背景 在之前的<简析GeoServer服务的内部文件组织以及 ...
- Upsync:微博开源基于Nginx容器动态流量管理方案
Upsync:微博开源基于Nginx容器动态流量管理方案 https://mp.weixin.qq.com/s?__biz=MzAwMDU1MTE1OQ==&mid=404151075& ...
随机推荐
- Android 本地css引用
/** 全局web样式 * 以前看不懂,现在仔细,耐心的看看,全懂了,认真的看一遍都懂了 * * * */ // 链接样式文件,代码块高亮的处理 public final static String ...
- Docker背后的内核知识(一)
Docker背后的内核知识 当谈论Docker时,常常会聊到Docker的实现方式.很多开发者都知道,Docker容器本质上是宿主机上的进程.Docker通过namespace实现了资源隔离.通过cg ...
- js中基础数据类型
变量声明 undefined //未定义只声明 var age; alert(name);function fc(a1,a2,a3) { //alert(a1); //alert(a2); //a ...
- 微服务化的不同阶段 Kubernetes 的不同玩法
欢迎访问网易云社区,了解更多网易技术产品运营经验. 作为容器集群管理技术竞争的大赢家,Kubernetes已经和微服务紧密联系,采用Kubernetes的企业往往都开始了微服务架构的探索.然而不同企业 ...
- 设计模式之第8章-策略模式(Java实现)
设计模式之第8章-策略模式(Java实现) “年前大酬宾了啊,现在理发冲500送300,冲1000送500了.鱼哥赶紧充钱啊,理发这事基本一个月一回,挺实惠的啊.不过话说那个理发店的老板好傻啊,冲10 ...
- xss games20关小游戏附源代码
1. get方式的的值直接输出来了. ?name=<script>alert(1)</script> 2. 同样没有过滤,不过需要闭合前边的双引号和>. "&g ...
- jeakins配置邮件通知,附带解决535报错:authentication failed,如果发现测试邮件可以发出,项目构成无法发出邮件,请开启SSL认证,端口号改为(465),qq邮箱、163邮箱通用
535报错解决方案:调用163邮箱服务器来发送邮件,我们需要开启POP3/SMTP服务,这时163邮件会让我们设置客户端授权码,这个授权码替代上面代码部分的passwd即可成功发送邮件 如果设置的邮箱 ...
- [19/02/23]ToolsShare 工具分享 VPNTethering Android (Root Required)
To be short, VPN Tethering is a quite useful tool when you want to share your private network with s ...
- [DM8168]Linux下控制GPIO控制12864液晶屏(ST7565控制器)
首先加载驱动模块,应用程序通过调用API实现GPIO控制功能. 驱动函数: /* * fileName: st7565_driver.c * just for LCD12864 driver * GP ...
- 树上路径(path)
树上路径(path) 题目描述 在Berland,有n个城堡. 每个城堡恰好属于一个领主.不同的城堡属于不同的领主.在所有领主中有一个是国王,其他的每个领主都直接隶属于另一位领主,并且间接隶属于国王. ...