基于Nginx dyups模块的动态上下线

基于以上这些情况, 我计划实现一个工具,这个工具首先解决站点上下线和动态扩容问题,也就是说在不需要重启nginx的情况下,并且在保证请求不丢失的情况下来更新站点。 同时带有部分服务治理功能。 

123.png 

服务上线

  1. 在一个新服务上线的时候,一般会提前申请几台机器, 运维会在nginx上新增server,并新增server对应的upstream ,正常情况下upstream应该配置是后端服务器的IP,但是这里不配置(如果允许,甚至这一步都可以省略)。 

  2. 服务部署好并启动,在启动的时候,向注册中心注册自身的服务信息,包括IP和端口。 

  3. 注册中心收到请求后,会对服务进行健康检测,确保提供的服务没有问题,则将服务状态标示为预上线状态。 

  4. 在后台管理中心,就可以将预上线的服务设置为上线,服务管理中心会调用nginx的上线接口,将服务IP新增或者更新到upstream中,服务就可以提供访问。

服务更新

假如我们现在有一个服务需要更新,则执行以下步骤:

  1. 在后台管理中心,将一个服务设为下线,此时服务中心会调用nginx的下线接口,将指定服务器的IP设置为下线。 

  2. 在等待1分钟后,确保没有新连接连过来,则可以开始更新服务站点。

  3. 更新完毕后,再手动设为上线,此时服务中心会调用nginx的上线接口,将指定服务器的IP设置为上线。当然对于成熟的服务,这些都可以自动化,有些公司会有一些自动化发布工具, 与自动化发布工具集成,可以一键下线,更新并上线。

服务运行期间

在服务运行过程中,会有一个健康检测的服务对所有提供服务的站点进行健康检测,一旦检测到有问题,就执行下线逻辑。 直到问题被解决,最后执行上线流程。 

动态加减机器

在服务运行过程中,可能因为某些原因,服务请求飙高(前提是这些请求都是合法的),超过了当前集群的承载能力,当系统检测到这些情况后,可以动态扩充机器,比如现在流行的docker,在启动容器的时候,同时启动应用,应用在启动的时候,将自身信息注册给注册中心,注册中心再将这些信息同步到nginx,应用就可以提供访问,整体上就可以实现弹性计算。 

为什么不实现服务动态发现?

   这里可以看到图中已经有一个服务注册中心。 既然有了服务注册中心了, 那可以让业务站点连接服务注册中心来获取真实的服务IP,然后绕过nginx来连接服务,这里之所以没有这样做,是因为:

  1. 实现服务动态发现,这个需要和RPC框架配合,而且需要做服务的软负载,失败重连,限流等,整个项目设计就上升了一个复杂度, 考虑到有些项目还未使用RPC,并且不想对原有的项目有过多的侵入, 所以这里不做实现。 但是并不意味没有这些功能,服务的负载, 失败重连, 限流,其实这些功能在nginx中同样也有,可以直接使用,所以没有必要重新再开发。 

  2. 实现服务动态发现,获取到真实的服务IP,然后直连,这些一般是在流量特别大,nginx上出现短板的时候使用,但实际情况,一般很少会耗尽nginx的性能,即使有,也可以通过ngxin水平扩展来实现,所以这里依然使用nginx作为负载均衡。

这里讲一下这个项目的关键点:

  1. 服务的注册和健康检测这个没有技术难点,这里不做解释。

  2. 关于操作nginx上下线,这里的确是一个难点,因为nginx本身并没有提供这些上下线API,需要openresty并配合一些第三方扩展来实现。 这里主要用到了两个扩展模块:ngx_http_dyups_module  lua-upstream-nginx-module

    ngx_http_dyups_module(https://github.com/yzprofile/ngx_http_dyups_module)提供了粗粒度的upstream管理方法,可以对整个upstream进行新增,删除。 

    lua-upstream-nginx-module(https://github.com/openresty/lua-upstream-nginx-module) ,则提供了细粒度的管理方式,可以对某一个服务IP进行管理,其中提供的set_peer_down方法,可以对upstream中的某个ip进行上下线。

  3. 也可以使用ngx_dynamic_upstream(https://github.com/cubicdaiya/ngx_dynamic_upstream)

这些插件有一个共同点,那就是在不需要重启nginx的基础上, 动态修改nginx的配置。 

方案一:模块ngx_dynamic_upstream,api+python/shell方式(直接操作upstream里的server)

因为api接口操作不是太直观,可以在django上以web方式增删查改upstream

Directives

dynamic_upstream

Syntax dynamic_upstream
Default -
Context location

Now ngx_dynamic_upstream supports dynamic upstream under only http context.

Quick Start
upstream backends {
    server 127.0.0.1:6001;
    server 127.0.0.1:6002;
    server 127.0.0.1:6003;
    
    zone zone_for_backends 1m;
}

server {
    listen 6000;

    location /dynamic {
	allow 127.0.0.1;
	deny all;
        dynamic_upstream;
    }

    location / {
	proxy_pass http://backends;
    }
}HTTP APIs

You can operate upstreams dynamically with HTTP APIs.

list

$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends"
server 127.0.0.1:6001;
server 127.0.0.1:6002;
server 127.0.0.1:6003;
$

verbose

$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&verbose="
server 127.0.0.1:6001 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6002 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6003 weight=1 max_fails=1 fail_timeout=10;
$

update_parameters

$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&server=127.0.0.1:6003&weight=10&max_fails=5&fail_timeout=5"
server 127.0.0.1:6001 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6002 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6003 weight=10 max_fails=5 fail_timeout=5;
$

The supported parameters are blow.

  • weight

  • max_fails

  • fail_timeout

down

$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&server=127.0.0.1:6003&down="
server 127.0.0.1:6001 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6002 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6003 weight=1 max_fails=1 fail_timeout=10 down;
$

up

$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&server=127.0.0.1:6003&up="
server 127.0.0.1:6001 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6002 weight=1 max_fails=1 fail_timeout=10;
server 127.0.0.1:6003 weight=1 max_fails=1 fail_timeout=10;
$

add

$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&add=&server=127.0.0.1:6004"
server 127.0.0.1:6001;
server 127.0.0.1:6002;
server 127.0.0.1:6003;
server 127.0.0.1:6004;
$

remove

$ curl "http://127.0.0.1:6000/dynamic?upstream=zone_for_backends&remove=&server=127.0.0.1:6003"
server 127.0.0.1:6001;
server 127.0.0.1:6002;
server 127.0.0.1:6004;
$

API调用脚本:

# cat nginx-upstream.sh
#------------------------------------------------------
#!/bin/bash

nginx_list="
172.16.1.1
172.16.1.2
172.16.1.3
"

cd /data/script/nginx

upstream="$1"

if [ -n "$upstream" ]; then
    upstream=${upstream//./_}
    for ip in ${nginx_list}; do
        if [ -n "$2" -a -n "$ip" ]; then
            action=${@: -1}
            if [ "$action" != "up" -a "$action" != "down" ]; then
                echo "action error!"
            else
                echo "### 【NGINX】 $ip"
                server=${@: 2:$#-2}
                for s in $server; do
                    curl -sq "http://${ip}/dynamic?upstream=${upstream}&server=${s}&${action}=" | egrep "server.+${s}.+;$" >/dev/null || echo "Action: $action Upstream: $upstream Server: $s  Failed"
                done
                curl -sq "http://${ip}/dynamic?upstream=${upstream}&verbose="
                echo
            fi
        else
            echo "### 【NGINX】 $ip"
            curl -sq "http://${ip}/dynamic?upstream=${upstream}&verbose="
            echo
        fi
    done
else
    echo "$0 <domain> <server1> <server2> <server3> <up/down>"
fi

通过脚本调用接口:

# 禁用
nginx-upstream test.qsh.com 172.16.10.100:6001 down
# 启用
nginx-upstream test.qsh.com 172.16.10.100:6001 up

方案二:lua(lua_code_cache)热装载+upstream ,直接操作upstream

简说热装载(热加载) :lua代码产生变更,利用lua_code_cache on ,实现自动加载,不用reload nginx。 

  • 亮点: 通过lua处理,不用重启nginx,利用api进行增删查改 

  • 不足:操作还是基于命令方式,后期还得加上web操作

GET

/detail get all upstreams and their servers
/list get the list of upstreams
/upstream/name find the upstream by it's name

POST

/upstream/name update one upstream
body commands;
body server ip:port;

DELETE

/upstream/name delete one upstream
Call the interface, when you get the return code is HTTP_INTERNAL_SERVER_ERROR 500, you need to reload nginx to make the Nginx work at a good state.
If you got HTTP_CONFLICT 409, you need resend the same commands again latter.
The /list and /detail interface will return HTTP_NO_CONTENT 204 when there is no upstream.
Other code means you should modify your commands and call the interface again.
ATTENEION: You also need a third-party to generate the new config and dump it to Nginx'conf directory.
Sample
» curl -H "host: dyhost" 127.0.0.1:8080
<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.3.13</center>
</body>
</html>

» curl -d "server 127.0.0.1:8089;server 127.0.0.1:8088;" 127.0.0.1:8081/upstream/dyhost
success

» curl -H "host: dyhost" 127.0.0.1:8080
8089

» curl -H "host: dyhost" 127.0.0.1:8080
8088

» curl 127.0.0.1:8081/detail
host1
server 127.0.0.1:8088

host2
server 127.0.0.1:8089

dyhost
server 127.0.0.1:8089
server 127.0.0.1:8088

» curl -i -X DELETE 127.0.0.1:8081/upstream/dyhost
success

» curl 127.0.0.1:8081/detail
host1
server 127.0.0.1:8088

host2
server 127.0.0.1:8089

API

extern ngx_flag_t ngx_http_dyups_api_enable;
ngx_int_t ngx_dyups_update_upstream(ngx_str_t *name, ngx_buf_t *buf,
    ngx_str_t *rv);
ngx_int_t ngx_dyups_delete_upstream(ngx_str_t *name, ngx_str_t *rv);

extern ngx_dyups_add_upstream_filter_pt ngx_dyups_add_upstream_top_filter;
extern ngx_dyups_del_upstream_filter_pt ngx_dyups_del_upstream_top_filter;

NOTICE: you should add the directive dyups_interface into your config file to active this feature

content_by_lua '
    local dyups = require "ngx.dyups"

    local status, rv = dyups.update("test", [[server 127.0.0.1:8088;]]);
    ngx.print(status, rv)
    if status ~= ngx.HTTP_OK then
        ngx.print(status, rv)
        return
    end
    ngx.print("update success")

    status, rv = dyups.delete("test")
    if status ~= ngx.HTTP_OK then
        ngx.print(status, rv)
        return
    end
    ngx.print("delete success")
';

方案三:模块fly操作upstream;web方式增删查改upsterem里的server

# 无drop重启nginx

kill -HUP `cat /data/nginx/logs/nginx.pid` 

#!/bin/sh 
BASE_DIR='/usr/local/' 

${BASE_DIR}nginx/sbin/nginx -t -c ${BASE_DIR}nginx/conf/nginx.conf >& ${BASE_DIR}nginx/logs/nginx.start 
info=`cat ${BASE_DIR}nginx/logs/nginx.start` 

if [ `echo $info | grep -c "syntax is ok" ` -eq 1 ]; then 
    if [ `ps aux|grep "nginx"|grep -c "master"` == 1 ]; then 
        kill -HUP `cat ${BASE_DIR}nginx/logs/nginx.pid` 
        echo "ok" 
    else 
        killall -9 nginx 
        sleep 1 
        ${BASE_DIR}nginx/sbin/nginx 
    fi 
else 
    echo "######## error: ########" 
    cat ${BASE_DIR}nginx/logs/nginx.start 
fi