Nginx + UpSync + Consul 实现 Dynamic Upstream

语言: CN / TW / HK

传统做法

通常我们先会配置一个 upstream 地址池,包含后端的多台应用服务器,然后通过 proxy_pass 将流量分发给 upstream 中的成员。

http {
    
    upstream upstream_server{
        server 192.168.1.134:81;
        server 192.168.1.134:82;
    }

    server {
        listen       80;
        server_name localhost;

        location / {
            proxy_pass http://upstream_server;
        }
    }
}

假如现在由于应用服务器压力比较大,要新增一台服务器,那么需要修改 upstream 为:

upstream upstream_server{
    server 192.168.1.134:81;
    server 192.168.1.134:82;
    #新增的服务器
    server 192.168.1.134:83;
}

修改完成之后,需要通过 nginx -s reload 命令重新加载配置,才能使配置生效。虽然 Nginx 可以做到平滑地重载配置,但是每次应用服务器增加或删除时都要改动 Nginx 显得并不是那么智能。如果有大量的 Nginx 需要管理,每次都需要手动操作将会极大地增加运维的负担。

Dynamic Upstream

基于传统做法的弊端,我们引入了注册中心保存应用服务信息,Nginx 通过动态获取注册中心中的服务信息,更新 upstream 配置,无需人为干预和重启。实际生产应用中我们可以将 CMDB 和 注册中心整合,管理人员只需要在 CMDB 上维护应用服务信息即可。

Nginx 第三方模块 nginx-upsync-module 支持通过注册中心动态发现 upstream 信息。目前 nginx-upsync-module 模块支持 Consul 和 Etcd 作为 注册中心。

另外开源版本的 Nginx 默认只支持被动的健康检查,只有当客户端访问时,才会发起对后端节点的探测。假设本次请求中, Nginx 转发的后端节点正好出现了异常,Nginx 会将请求再转交给另一个 upstream 中的节点处理,所以不会影响到这次请求的正常进行,但是会影响效率,因为多了一次转发。并且自带模块无法做到预警。因此我们还使用了第三方模块 nginx_upstream_check_module 用于健康检查,该模块不仅支持主动的健康检查还提供了 WebUI 用于查看健康检查状态。

本示例 github 地址:https://github.com/cr7258/nginx-lab/dynamic-upstream

目前还有其他产品支持动态配置,不仅仅是 upstream,还包括了其他方面的配置。例如 Nginx 的商业版本 Nginx Plus,和云原生结合得比较好的 Envoy、Kong、Traefik 等等。大家有兴趣可以自行了解。

注册中心 Consul 搭建

我们在一台虚拟机上启动 3 个 Consul 服务,组成一个伪集群。

下载安装包。

wget https://releases.hashicorp.com/consul/1.9.3/consul_1.9.3_linux_amd64.zip
unzip consul_1.9.3_linux_amd64.zip
mv consul /usr/local/bin/

创建相关目录:

mkdir /data/consul && cd $_
mkdir -pv /data/consul/node{1..3}

创建 3 个 Consul 节点使用的配置文件:

node1

vim /data/consul/node1/consul_config1.json

{
  "datacenter": "dev",
  "data_dir": "/data/consul/node1",
  "log_file": "/data/consul/node1/consul.log",
  "log_level": "INFO",
  "server": true,
  "node_name": "node1",
  "ui": true,
  "bind_addr": "192.168.1.134",
  "client_addr": "192.168.1.134",
  "advertise_addr": "192.168.1.134",
  "bootstrap_expect": 3,
  "ports":{
    "http": 8510,
    "dns": 8610,
    "server": 8310,
    "serf_lan": 8311,
    "serf_wan": 8312
    }
}

node2

vim /data/consul/node2/consul_config2.json

{
  "datacenter": "dev",
  "data_dir": "/data/consul/node2",
  "log_file": "/data/consul/node2/consul.log",
  "log_level": "INFO",
  "server": true,
  "node_name": "node2",
  "ui": true,
  "bind_addr": "192.168.1.134",
  "client_addr": "192.168.1.134",
  "advertise_addr": "192.168.1.134",
  "bootstrap_expect": 3,
  "ports":{
    "http": 8520,
    "dns": 8620,
    "server": 8320,
    "serf_lan": 8321,
    "serf_wan": 8322
    }
}

node3

vim /data/consul/node3/consul_config3.json

{
  "datacenter": "dev",
  "data_dir": "/data/consul/node3",
  "log_file": "/data/consul/node3/consul.log",
  "log_level": "INFO",
  "server": true,
  "node_name": "node3",
  "ui": true,
  "bind_addr": "192.168.1.134",
  "client_addr": "192.168.1.134",
  "advertise_addr": "192.168.1.134",
  "bootstrap_expect": 3,
  "ports":{
    "http": 8530,
    "dns": 8630,
    "server": 8330,
    "serf_lan": 8331,
    "serf_wan": 8332
    }
}

启动 Consul 集群:

nohup consul agent -config-file=/data/consul/node1/consul_config1.json > /dev/null 2>&1 &
nohup consul agent -config-file=/data/consul/node2/consul_config2.json -retry-join=192.168.1.134:8311 > /dev/null 2>&1 &
nohup consul agent -config-file=/data/consul/node3/consul_config3.json -retry-join=192.168.1.134:8311 > /dev/null 2>&1 &

启动之后,便可以通过 http://192.168.1.134:8510 访问,此处 192.168.1.134:8510 是 Leader 角色。

启动后端应用服务

后端服务是用 Nginx 启动的 Web 服务。准备 3 个后端服务,IP 和 Port 分别是:

192.168.1.134:81
192.168.1.134:82
192.168.1.134:83

配置文件内容如下,3 个服务的配置基本一样,只是改了相关的端口和路径:

user root;
events{}
http{
server {
    listen       81 default_server;
    listen       [::]:81 default_server;
    server_name  127.0.0.1;
    root         /root/myapp/nginx/html/81;
    server_tokens off;

    gzip on;
    gzip_buffers 16 8k;
    gzip_comp_level 6;
    gzip_http_version 1.1;
    gzip_min_length 256;
    gzip_proxied any;
    gzip_vary on;
    gzip_types text/xml application/xml application/atom+xml application/rss+xml application/xhtml+xml image/svg+xml text/javascript application/javascript application/x-javascript text/x-json application/json application/x-web-app-manifest+json text/css text/plain text/x-component font/opentype application/x-font-ttf application/vnd.ms-fontobject image/x-icon;
    gzip_disable "msie6";


    location / {
        expires max;  
        open_file_cache max=1000 inactive=20s; 
        open_file_cache_valid 30s; 
        open_file_cache_min_uses 2; 
        open_file_cache_errors on;
    }

    error_page 404 /404.html;
        location = /40x.html {
    }

    error_page 500 502 503 504 /50x.html;
        location = /50x.html {
    }
  }
}

html 文件如下:

[root@nginx-plus1 nginx]# cat html/81/index.html 
<html>
<head>
    <meta charset="utf-8">
    <title>server1</title>
</head>
<body style="background-color:blue;">
    <h1>Server 1 url 1<h1>
</body>
</html>

启动 3 个后端服务,在启动 Nginx 的时候指定配置文件即可:

sbin/nginx -c /root/myapp/nginx/81.conf
sbin/nginx -c /root/myapp/nginx/82.conf
sbin/nginx -c /root/myapp/nginx/83.conf

访问 3 个应用服务:

编译 Nginx

实现 Dynamic Upstream 需要添加 nginx-upsync-modulenginx_upstream_check_module 两个第三方模块,在编译 Nginx 的时候要将这两个模块添加进去。这里准备了一个 Dockerfile,使用 docker build -t 镜像名:标签名 . 就可以构建出一个编译好的 Nginx Docker 镜像。

FROM debian:stretch-slim

RUN useradd  www && \
mkdir -p /logs/nginx/  /webserver/nginx /webserver/nginx/conf/upsync && \
chown -R www:www /logs/nginx/  /webserver/nginx && \
echo 'deb http://mirrors.163.com/debian/ stretch main non-free contrib' > /etc/apt/sources.list && \
echo 'deb http://mirrors.163.com/debian/ stretch-updates main non-free contrib' >> /etc/apt/sources.list && \
echo 'deb-src http://mirrors.163.com/debian/ stretch main non-free contrib' >> /etc/apt/sources.list && \
echo 'deb-src http://mirrors.163.com/debian/ stretch-updates main non-free contrib' >> /etc/apt/sources.list && \
echo 'deb-src http://mirrors.163.com/debian/ stretch-backports main non-free contrib' >> /etc/apt/sources.list && \
echo 'deb-src http://mirrors.163.com/debian-security/ stretch/updates main non-free contrib' >> /etc/apt/sources.list && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
apt-get update && \
apt-get install -y wget vim net-tools unzip libjemalloc-dev && \
apt-get build-dep -y nginx

RUN \
cd /usr/local/src/ && \
wget -c http://nginx.org/download/nginx-1.14.2.tar.gz && \
wget -c https://www.openssl.org/source/old/1.0.2/openssl-1.0.2m.tar.gz && \
wget -c https://github.com/simplresty/ngx_devel_kit/archive/v0.3.1rc1.tar.gz && \
wget -c https://github.com/openresty/lua-nginx-module/archive/v0.10.11.tar.gz && \
wget -c https://github.com/xiaokai-wang/nginx_upstream_check_module/archive/master.zip -O nginx_upstream_check_module.zip && \
wget -c https://github.com/weibocom/nginx-upsync-module/archive/master.zip -O nginx-upsync-module.zip && \
tar zxf ./nginx-1.14.2.tar.gz && rm nginx-1.14.2.tar.gz && \
tar zxf ./openssl-1.0.2m.tar.gz && rm openssl-1.0.2m.tar.gz && \
tar zxf ./v0.3.1rc1.tar.gz && rm v0.3.1rc1.tar.gz && \
tar zxf ./v0.10.11.tar.gz && rm v0.10.11.tar.gz &&  \
unzip ./nginx_upstream_check_module.zip && rm nginx_upstream_check_module.zip && \
unzip ./nginx-upsync-module.zip && rm nginx-upsync-module.zip

RUN \
cd /usr/local/src/nginx-1.14.2 &&\
patch -p1 < /usr/local/src/nginx_upstream_check_module-master/check_1.12.1+.patch &&\
./configure \
--prefix=/webserver/nginx \
--user=www --group=www --with-pcre \
--with-stream \
--with-http_v2_module \
--with-http_ssl_module \
--with-ld-opt=-ljemalloc \
--with-http_realip_module \
--with-http_gzip_static_module \
--with-http_stub_status_module \
--http-log-path=/logs/nginx/access.log \
--error-log-path=/logs/nginx/error.log \
--with-openssl=/usr/local/src/openssl-1.0.2m \
--add-module=/usr/local/src/ngx_devel_kit-0.3.1rc1 \
--add-module=/usr/local/src/lua-nginx-module-0.10.11 \
--add-module=/usr/local/src/nginx_upstream_check_module-master \ 
--add-module=/usr/local/src/nginx-upsync-module-master && \
make && \
make install

另外我也准备了一个已经构建好的镜像:registry.cn-shanghai.aliyuncs.com/public-namespace/nginx-dynamic-upstream:v1.0.0 ,可以直接拿来使用。

准备 Nginx 动态更新的配置文件

配置 nginx.conf 文件:

worker_processes  1;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    upstream app {
        upsync 192.168.1.134:8510/v1/kv/upstreams/app/ upsync_timeout=6m upsync_interval=500ms upsync_type=consul strong_dependency=off;
        upsync_dump_path /webserver/nginx/conf/app.conf; # 当consul故障时候,就可以把此作为备份配置文件
        include /webserver/nginx/conf/app.conf; # 准备一个兼容的nginx测试文件,如果没有第一次启动会起不来
        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;
        }
    server {
        listen       80;
        server_name  localhost;
        location / {
            proxy_pass http://app;
        }
        location /upstream_list {
            upstream_show;
        }
        location /upstream_status {
            check_status;
            access_log off;
        }
    }
}

app.conf 文件里随便写上一个 IP 和 Port 信息,可以是无法访问的服务,因为 Nginx 的 upstream 中必须要有地址才能启动 Nginx。我们后面会通过在 Consul 上注册服务让 Nginx 动态更新 Upstream。

server 0.0.0.0:12345 weight=1 max_fails=2 fail_timeout=10s;

在本地的 Mac 电脑上通过 Docker 启动 Nginx 容器:

docker run -d --name nginx-dynamic-upstream \
-v /Users/chengzhiwei/lab/docker-lab/nginx/dynamic-upstream/nginx.conf:/webserver/nginx/conf/nginx.conf \
-v /Users/chengzhiwei/lab/docker-lab/nginx/dynamic-upstream/app.conf:/webserver/nginx/conf/app.conf \
-p 80:80 -p 443:443 \
registry.cn-shanghai.aliyuncs.com/public-namespace/nginx-dynamic-upstream:v1.0.0 \
/webserver/nginx/sbin/nginx  -g "daemon off;"

通过 curl 命令发送 HTTP 请求往 Consul 中注册两个新的服务。

curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":0}' http://192.168.1.134:8510/v1/kv/upstreams/app/192.168.1.134:81
curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":0}' http://192.168.1.134:8510/v1/kv/upstreams/app/192.168.1.134:82

通过 http://localhost/upstream_list 查看 upstream 主机:

通过 http://localhost/upstream_status 可以看到应用服务的健康检查状态:

访问 http://localhost 可以代理到后端的应用服务:

此时我们停掉 192.168.1.134:81 的服务:

#查看监听 81 端口的进程号
[root@nginx-plus1 nginx]# lsof -i:81
COMMAND   PID USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
nginx   26047 root    6u  IPv4 202389912      0t0  TCP *:81 (LISTEN)
nginx   26047 root    7u  IPv6 202389913      0t0  TCP *:81 (LISTEN)
nginx   26048 root    6u  IPv4 202389912      0t0  TCP *:81 (LISTEN)
nginx   26048 root    7u  IPv6 202389913      0t0  TCP *:81 (LISTEN)
#停止服务
[root@nginx-plus1 nginx]# kill 26047

此时查看健康检查状态,发现 81 端口的服务已经被置为 down 了。

此时再访问 http://localhost 就只能访问到端口为 82 的服务了。

参考链接

欢迎关注