活动主持人

个人签名

169篇博客

效率倍增!网易杭研Nginx自动扩缩容实践

活动主持人2020-01-15 15:59

随着网易云对容器化的支持,各个业务正在逐步推进容器化改造,传统人工修改 Nginx 配置并推送线上生效已经不能满足容器化时代业务的变更需求,Nginx 的变更效率亟待提升,基于 Nginx 自动扩缩容快速变更,网易杭研运维进行了持续实践,推进并完成了网易云音乐、传媒等部分入口集群的自动扩缩容改造,提高了变更效率,解放了运维人力。
本文由作者授权发布,未经许可,请勿转载!

作者:秦鹏程,杭州研究院运维工程师


前言

随着网易云容器化业务(系网易轻舟微服务核心组件之一)的兴起以及部门运维提效的战略规划,对 Nginx 自动化变更提出了更高要求,尤其是Nginx 的 upstream 增、删、改变更,传统的人工变更流程相对较长,变更效率低还耗费了一定的运维人力,为了进一步降底人力成本,提高变更效率,杭研运维在Nginx自动扩缩容方面做了一系列实践,本文主要整理记录了实践过程中相关信息,供有兴趣同学参考阅读,阅读本文您将有如下收获:

1.了解杭研自动扩缩容运维体系和架构;
2.了解杭研自动扩缩容接入过程中所进行的探索和优化;
3.了解杭研自动扩缩容接入过程中踩过的坑。

关于方案选型

通过前期调研,目前业务基于Nginx自动扩缩容的开源组件有阿里的 dyups 和 新浪微博的 upsync,其中微博的 upsync 后端可以直接从 Consul 集群中拉取数据,系统只需要调用API向consul里面写数据,实现起来相对简单,对业务改动性不大,后期更加容易推动业务接入自动扩缩容,因此采用微博开源的 nginx-upsync-module 模块,并结合运维的 夸父系统 和 运管系统 进行组合,满足后期自动扩缩容日常运维、变更、配置查询的需求。
注:

1.dyups 链接: https://github.com/yzprofile/ngx_http_dyups_module
2.upsync 链接: https://github.com/weibocom/nginx-upsync-module
3.夸父系统: 杭研运维的工单流程平台,支持自动化对接和流程定制,可以高效完成日常运维相关任务;
4.运管系统: 杭研运维的基础管控平台,涵盖IDC管理、变更管理、网络管理、脚本平台等核心模块,可以辅助支撑各类运维需求的查询和实现;

系统架构与实现流程

图一:系统数据同步变更架构


图一简单展示了自动扩缩容数据变更(增、删、改)以及Nginx集群同步流程,覆盖了数据变更的全流程,基于上述架构图,自动扩缩容的整体变更流程如下:

步骤1: 用户提交变更需求到夸父平台,通过流程工单形式进行需求提交,夸父平台会对数据的合法性(IP/Port 存活)进行校验;
步骤2: 运管AT封装Consul的增删改的API,提供给夸父平台调用,通过运管AT平台将数据写入Consul;
步骤3: Nginx集群定时去 Consul集群拉取配置,保证本地配置和Consul一致,并且能直接更新生效,无需Nginx Reload。
整体实现流程相对比较简单,在实现过程中,为充分考虑系统可用性和业务不同需求,我们对运维系统进行了如下功能定位:

1. 运管AT系统


运管AT系统作为自动扩缩容的后台管理平台,提供不同业务Consul集群的管理和API封装功能,业务将自己Consul Token 存入运管平台,由运管统一集成API 暴露给其他系统调用,并做好权限控制,把变更入口收缩在运管系统上,后期运维系统(夸父)可以直接对接调用,也可以提供API给其他关联系统调用,做到系统间兼容;另外,AT系统还提供 Nginx配置查询、upstream 列表关联、域名与upstraem关联等等一系列的配置查询需求,供后期业务按需查询,为线上变更提供基础条件。

2. 夸父系统


夸父系统作为工单流程平台,可以直接对接运管封装的Consul API,进行配置增、删、改操作,夸父系统重点放在变更审批、参数校验、用户体验等功能上;比如,根据不同业务、不同变更要求提供不同的审批流程,确保变更操作合法,同时,在用户体验上我们也做了一定优化工作,提供模糊匹配功能,用户可以模糊搜索需要变更的 Nginx 域名,并自动能关联出其对应的 upstream 列表,选择 upstream 列表后,能自动关联当前 ip 列表信息,从而提升用户变更体验,减少用户变更成本。

3. Consul集群


杭研运维产品比较多,为了做好业务划分,对于不同业务我们建议都采用独立的 Consul 集群,这样能对数据进行隔离,业务间变更不会相互影响;AT系统开放了Consul集群接入能力,更加方便后续其他业务接入到自动扩缩容运维体系里面。

4. Nginx集群


目前杭研维护的前端入口集群Nginx版本都是支持 nginx-upsync-module 模块,故集群接入自动扩缩容后只需对配置进行少许改造,即可无缝完成切换,整体切换过程业务无感知。

关于标准的制定

线上标准都需要进行规范化统一,以方便系统进行自动化处理,针对自动扩缩容我们制定了如下基本标准:

1. 不同业务通过不通的 Cousul Key 来区分拉取配置,基本规则为: {consulEnv}/nginx/clusters/{consulName}/{upstreamName}, 其中 consulEnv 用于区分 线上(online)、测试(test) 环境,consulName 则为不通业务的consul 集群定义,用于和其他业务分开,upstreamName 则为 upstream 名,即需要变更的 upstream 名;

2. 约定 Nginx upstream 命名和应用所在CDMB的集群名保持强一致性,全局唯一性,命名规范: 小写字母和中横线组合,以小写字母开头和结尾,不允许又其他特殊字符,例如下划线等;针对业务集群过渡期间,为了区分接入了自动扩缩容和非自动扩缩容,统一约定在当前 upstream 后面加上 -upsync 以示区分,例如:music-abc,在进行 upsync 自动扩缩容改造后,其命名为 music-abc-upsync ;

3. 约定 upsync_dump_path 的目录为 /home/srv/nginx/{upstreamName}-upsync.conf 保持一致,方便后续自动扩缩容检查生效功能检查实际配置文件 dump 情况;

4. 建议设置 upsync_timeout=5s upsync_interval=90s,代表consul拉取5s超时时间,每90s定时拉取一次配置,后面会提到Nginx与Consul之间会加上的缓存优化,Consul会快速返回查询结果,并且结合业务实际需求,控制生效时间再90s内,同时减少高频率的upsync配置同步对代理集群造成一定压力。

通过上述相关标准,一个相对标准一致配置如下:

upstream music-abc-upsync {
upsync 192.168.1.29:8500/v1/kv/online/nginx/clusters/music/upstreams/music-abc-upsync/ upsync_timeout=5s upsync_interval=90s upsync_type=consul strong_dependency=off;
upsync_dump_path /home/srv/nginx/music-abc-upsync.conf;
include /home/srv/nginx/music-abc-upsync.conf;
}

关于Consul代理架构的优化

在实际推进过程中,Nginx集群到 Consul 集群之间的配置定时同步这块进行了一系列探索,先来简单聊聊 Github 上原作者给的示例与说明:

upstream test {
upsync 127.0.0.1:8500/v1/kv/upstreams/test/ upsync_timeout=6m upsync_interval=500ms upsync_type=consul strong_dependency=off;
upsync_dump_path /usr/local/nginx/conf/servers/servers_test.conf;
check interval=1000 rise=2 fall=2 timeout=3000 type=http default_down=false;
check_http_send "HEAD / HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
原作者针对upsync 实时更新机制给出的说明:

每个work进程定时的去consul拉取相应upstream的配置,定时的间隔可配;其中 Consul 提供了time_wait机制,利用value的版本号,若 Consul 发现对应upstream的值没有变化,便会hang住这个请求5分钟(默认),在这5min内对此upstream的任何操作,都会立刻返回给Nginx,对相应路由进行更新;对于拉取的间隔可以结合场景的需要进行配置,基本可以实现所要求的实时性。
针对上述配置,原作者设置 upsync_timeout=6m upsync_interval=500ms, 此处 upsync_timeout设置为6m,是要大于 5min 的,并且 upsync_interval设置500ms,间隔非常短,目的就是要做到近乎实时更新,考虑到我们实际业务情况和 upsync 拉取原理,由于我们业务前端入口集群一般均为几十核的服务器,并且一个Nginx集群往往有多台机器,业务规模大的还同时绑定好多个集群,这样以来前端机数量比较大,在单台服务器几十核的情况下,每次定时同步每个 upstream 都至少要发起几十个请求保持 5min( 如果没有数据变化),upstream数量也有一定规模,这势必造成了Nginx前端机和consul集群的负载压力较大,并且持续5min链接有可能在业务高峰期时候给业务稳定带来隐患,同时大量请求也会对 Consul 集群的容量提出挑战,Consul 集群容量是否能满足同步需求,因此给予上述示例值,对我们相关业务是不具有实用性的,需要改变,需要优化。

再谈谈 Consul默认 hang住 5min的情况,这里牵扯到 Consul 的 “阻塞查询” 功能,Consul官网给出的解释如下:

In addition to index, endpoints that support blocking will also honor a wait parameter specifying a maximum duration for the blocking request. This is limited to 10 minutes. If not set, the wait time defaults to 5 minutes. This value can be specified in the form of "10s" or "5m" (i.e., 10 seconds or 5 minutes, respectively). A small random amount of additional wait time is added to the supplied maximum wait time to spread out the wake up time of any concurrent requests. This adds up to wait / 16 additional time to the maximum duration.
简单来说,在请求Consul时候带上 index 参数,会一直保持连接直到数据变化,默认hang时间为 5min,可以确保数据能实时探测到更新;观察 upsync 请求日志也可以发现“GET /v1/kv/online/nginx/clusters/music/upstreams/music-abc-upsync/?recurse&index=18112”,其默认是带了 index 参数,此请求发出后便会hang住,默认5min断开,好了这就整体解释通了。

接下来我们看看如何进行优化,优化主要解决如下两方面问题:

1.每个worker 都发请求去consul集群,怎么减少consul集群压力?
2.默认 hang 5min 的机制怎么解决?

首先咨询了团队大神顾总,并参考其他业务配置经验,可以通过在 Consul集群与Nginx集群之间加了一个 Consul Nginx 代理,代理启用 Nginx cache 缓存配置,并缩短超时时间 upsync_timeout=5s ,由 upsync 强制断开请求连接,一定程度上解决了上述两个问题,参考Cache配置在测试环境搭建了一个demo测试集群,发现整体流程都能走通。

但是在实际测试运行中发现,consul nginx 代理的日志有 502 请求,并且nginx error 日志里面有 “no live upstreams........" 等问题,开始思考为什么会产生 502,最终通过查阅资料发现是 Nginx默认自带的健康检查机制 max_fails/fail_timeout 引起的:

max_fails=number

sets the number of unsuccessful attempts to communicate with the server that should happen in the duration set by the fail_timeout parameter to consider the server unavailable for a duration also set by the fail_timeout parameter. By default, the number of unsuccessful attempts is set to 1. The zero value disables the accounting of attempts. What is considered an unsuccessful attempt is defined by the proxy_next_upstream, fastcgi_next_upstream, uwsgi_next_upstream, scgi_next_upstream, memcached_next_upstream, and grpc_next_upstream directives.

fail_timeout=time

sets the time during which the specified number of unsuccessful attempts to communicate with the server should happen to consider the server unavailable; and the period of time the server will be considered unavailable. By default, the parameter is set to 10 seconds.

502 产生原因初步总结如下:

因consul 阻塞查询机制,默认会hang 5min,upsync 在设置超时时间自动断开后,此时,Nginx会认为此次请求失败(max_fails 加 1),继续尝试访问下一个upstream,因为consul请求量很大,在 fail_timeout(default 10s)时间内,max_fails 达到默认的次数 1次,Nginx将会将此server临时down掉 fail_timeout 时间,在此时间内,请求日志就会出现 502,如果所有后端都被判断失效,就会出现出现 “no live upstreams...” 日志;

嗯,问题定位了,接着是怎么去规避这个问题,比较直观做法是调整 max_fails / fail_timeout 值,适当将 max_fails 调大,fail_timeout 调小,一定程度上可以降低 502 日至产生,但是不能完全解决502 问题,修正后部分配置如下:

upstream nginx-consul {
server 192.168.10.31:8500 max_fails=3 fail_timeout=5s;
server 192.168.10.32:8500 max_fails=3 fail_timeout=5s;
server 192.168.10.33:8500 max_fails=3 fail_timeout=5s;
keepalive 64;
}
仔细看的同学或许会看到“max_fails , The zero value disables the accounting of attempts”,那么我将 max_fails设置为0 ,关闭掉这种检查机制是不是就是能彻底规避问题呢,理论确实是可以的,但是也带来了如下两种隐患:

1. consul nginx 代理里面 error.log 会出现大量 Connection time out 日志,持续刷,有强迫症的我看着很不舒服,虽说并没有很大影响;

2. 因为我们采用了 upsync ---> consul nginx proxy -----> consul cluster 代理架构,如果设为0,那么所有请求都会导致 consul nginx proxy 到 consul cluster 新增连接,而且,而且 nginx默认 proxy _read_timeout 默认值为60s,也就是在 60s 内会持续堆积新连接,从而拖死consul nginx proxy,实际上是真正遇到过,还好监控及时发现了,不然就跪了;基于此原因,又决定在 consul nginx proxy 中新增 Nginx配置,设置 proxy _read_timeout=5s,确保consul nginx proxy 到 consul cluster请求也能在 5s 内超时强制断开,不会产生请求堆积,从而避免拖死 consul nginx proxy。

基于上述分析,最终决定适当调整 max_fails 与 fail_timeout 的值,并设置 proxy _read_timeout=5s,一定程度上就缓解了问题,线上也能正常跑起来,但是有强迫症的我依然对 “Connection time out"看着不舒服,继续思考能不能彻底解决这个问题。

首先思考 upsync 模块能不能新增一个wait_timeout 参数,类似 consul wait 参数功能,请求的时候可以将 consul wait参数带上,我们可以通过 wait_timeout 设置这个时间,那么问题基本都迎刃而解了,而不是当前默认的 5min,我们什么也做不了,但是,这个要投入精力修改 upsync 源码,而且目前线上也就是刷刷错误日志,并没有其他影响,投入精力开发收益不明显,但基于这个疑问,同团队其他同事进行了探讨,顾总表示也思考过这个问题,并提示可以尝试看看在 nginx consul proxy 加一个 rewrite 规则,重写 consul url,过滤掉 index 参数,从而“关闭" Consul 阻塞查询功能,也就能解决超时引发的问题,但是不确定这样设置是否会带来其他隐患;

既然顾总提出一种思路,我觉得这个方法可行,结合前面提到各种优化参数(蹚过的坑),可以搞到测试环境试试效果,好了,先看看我们 consul 代理最终的优化架构:
图二:Consul代理架构图


通过在 Consul proxy 添加如下规则 rewrite ^/(.*)?$ $uri?recurse? break; 后,就能过滤掉 index 参数,直接递归查询 consul cluster,能立即返回最新结果,彻底解决了阻塞查询的超时问题,这个架构率先在测试环境测试验证没问题后,逐步灰度到线上环境,整体运行正常,说明方案可行,逐步推广到其他集群,目前杭研的自动扩缩容 consul proxy 代理均采用此种架构进行配置的自动拉取,线上平稳运行。

关于一个大坑

坑,无处不在,在推进接入过程中,我们也踩到了一个大问题坑,先来说说业务的影响现象:

在一周的某个时间点,会突然出现线上服务器所有设置过健康检查的请求都 502 (没看错,是所有,所有!!!),而且持续时间比较短,一般在分钟级别,部分特殊情况会要强行重启 Nginx 才能解决,出问题时候是超级慌啊,现场定位发现在异常时间点里面,几乎同一秒的时间,所有带有健康检查接口的 upstream的服务器,均全部被 Nginx disable 了,所有才会产生所有请求都是 502 情况;嗯,为什么同一时间所有正常机器都全部被 Nginx disable 呢,最起码我们健康检查还设置了 interval =3s 也不至于在同一时间点全部挂了吧,我始终不敢相信这个神奇的现象,但是它就是发生了,并且每周那那么一两次,搞的业务人心惶惶,不得已将线上灰度的配置先回滚了,问题不再出现;

接着测试环境开始尝试复现问题,但是遗憾没有复现出来,推测是不是和业务请求量有关系,于是在线上流量相对较小集群继续灰度埋点测试,发现问题表现为:

Nginx前端机在某一时间点会同时发送多个健康检查请求,并且前端机会认为健康检查超时,短时间多次超时,超过了 fall 次数,故健康检查将所有后端踢掉,从而业务表现为 502 问题( no live upstreams ),业务 502 异常日志:

2019/07/01 15:01:25 [error] 11687#11687: *3570743056 no live upstreams while connecting to upstream, xxxxxx

Nginx前端机 error.log 表现为:

短时间大量有健康检查的机器都会出现:“check time out with peer: xx " 报错,在达到 fall 次数后,会进行下线操作:“ disable check peer: ”,此处前端机认为后端超时,但是后端健康检查都是正常时间返回的,唯一可以推测,同一时间多次健康检查发送就表示Nginx认为健康检查异常了,所以才会发送多次;但是,正常情况下,一台机器会按照设定 interval 进行健康检查请求发送,理论不会存在前端机同一时间点发送多次健康检查情况。

我也确实抓到了某一特定时间点,Nginx同时发送多条健康检查请求,例如下图三中在 15:01:10 这个时间点,同时发送了4条健康检查请求,并且每次健康检查 $rmote_port不一致,但 upstream 返回的健康检查结果都正常,前端却判断超时了。

图三:Nginx同一时间点发送多次健康检查


综上,统一时间加载upsync模块的Nginx会同事发送多次健康检查,并且会认为健康检查超时,从而后端下线,是造成“no live upstreams ”的直接原因。

问题初步定位了异常原因,思考为什么健康检查没有按照既定的interval发送,推测 nginx-upsync-module 模块和 nginx_upstream_check_module 模块兼容性在某些特定业务场景下存在兼容性问题,从而导致异常,但是 github upsync 原作者却极力推荐与 health check 模块一起组合使用, 且看:

nginx-upsync-module support adding or deleting servers health check, needing nginx_upstream_check_module. Recommending nginx-upsync-module + nginx_upstream_check_module.

所以还是怀疑是不是这些集群不规范的参数导致了异常,于是开始了集群配置规范化,在所有相关参数(backlog , inetrval , timeout)都优化一遍后,问题还是会出现,这个时候就有点没有头绪了,准备尝试看源码,也与团队同事反馈这个问题,顾总在经过一翻源码review之后,发现 upsync 在配置拉取时候确实存在一定概率 crash 与 health check 问题,于是顾总尝试修改代码测试,因新代码稳定性测试需要时间,基于线上服务稳定性考虑,暂未投入线上使用;

问题是基本定位了,但是它是偶发的,肯定是特殊场景出发了这个异常,具体什么场景异常呢,开始陷入深深思考中,对比 github 给的示例,发现 upsync 配置均为 ip:port 端口直接连consul形式,我们加了个 consul proxy,临时加了另外一个优化,consul proxy 为 keepalived 互为主备结构,为了达到流量均衡目的,我在 upsync 采用了 域名形式,例如:

upsync http://music-consul.abc.163.org/v1/kv/online/nginx/clusters/music/upstreams/music-abc-upsync/ upsync_timeout=5s upsync_interval=90s upsync_type=consul strong_dependency=off;

开始怀疑会不会是域名解析过程中内网DNS抖动或者其他问题(Nginx Resolver),然后尝试将域名改成 ip:port 形式进行灰度观察,然后奇迹发生了,问题竟然再也没有出现了,没有出现了, 是的,问题算基本规避解决了。

由于前期这个问题已经耽搁了很多时间,阻碍了各业务接入进度,问题规避后开始积极推动各个产品接入,具体此问题初步判断是upsync在域名解析不稳定情况下会存在配置拉取异常,至此,后续 consul proxy 均采用了 ip:port 代理形式,各个集群均稳定运行。

总结

本文主要就杭研运维在自动扩缩容实践过程中运维系统联动设计、Consul 代理架构优化以及踩过的坑进行记录总结,总体上最终还是规避解决各种问题,确保线上稳定性,有力保证了各个业务的自动扩缩容改造接入进度;每个公司都有不同的业务背景和运维体系,业务规模扩张以及容器化的接入急需提高Nginx扩缩容变更效率,本文相关Nginx自动扩缩容接入经验和实践供参考,欢迎交流学习。