Nginx+consul实现集群主机优雅扩缩容
# 1,前置准备
# 1,方案选型
这里选用的方案是:Nginx 结合 upsync 模块儿,结合 consul 配置管理,实现后端 upstream 的动态管理。
- 选用 upsync 有如下特性:
- 优点:
- 可以动态对接 etcd/consul 获取 upstream,无需重启 nginx
- 可以 dump 内存中的 upstream 配置到磁盘,即使 etcd/consul 挂掉仍可正常运行
- 缺点:
- 无 api 接口,只能操作 etcd/consul 来控制 upstream
- 优点:
模块儿添加,可直接使用二丫 (opens new window)下边定制好的 spec 文件打出 rpm 包,便于安装使用:
- name: rpmbuild
desc: 工作中常用的RPM构建spec
avatar: https://avatars2.githubusercontent.com/u/416130?s=460&u=8753e86600e300a9811cdc539aa158deec2e2724&v=4 # 可选
link: https://github.com/eryajf/rpmbuild # 可选
bgColor: "#0074ff" # 可选,默认var(--bodyBg)。颜色值有#号时请添加单引号
textColor: "#fff" # 可选,默认var(--textColor)
2
3
4
5
6
# 2,规范约定
不同业务通过不通的 Cousul Key 来区分拉取配置
基本规则为:
{Env}/nginx/upstreams/{Service}-upsync/{Backend}
Env
:用于区分环境。测试(test),预发(pre),线上(prod)。Service
:指定业务名字,一般与 CMDB 业务名字一致。Backend
:不同后端的地址信息。
约定 Nginx upstream 命名和应用所在 CDMB 的集群名保持强一致性,全局唯一性
命名规范: 小写字母和中横线组合,以小写字母开头和结尾,不允许又其他特殊字符,例如下划线等;针对业务集群过渡期间,为了区分接入了自动扩缩容和非自动扩缩容,统一约定在当前 {Service} 后面加上 -upsync 以示区分,例如:user-api,在进行 upsync 自动扩缩容改造后,其命名为 user-api-upsync。
约定 upsync_dump_path 的目录与上边目录保持一致,以便于对齐两者,简化运维工作。
# 2,Nginx 配置接入
# 1,前置准备
为了便于验证一个入口多个后端的场景,二丫 (opens new window)先通过 Nginx 准备三个后端服务,一顿操作如下:
# 准备基础文件
$ cd /data/test/docker/nginx/
$ mkdir test1 test2 test3
$ echo test1 > test1/index.html
$ echo test2 > test2/index.html
$ echo test3 > test3/index.html
# 启动服务
$ docker run --name test1 -v /data/test/docker/nginx/test1:/usr/share/nginx/html:ro -d -p 9901:80 daocloud.io/library/nginx:1.15.9-alpine-perl
$ docker run --name test2 -v /data/test/docker/nginx/test2:/usr/share/nginx/html:ro -d -p 9902:80 daocloud.io/library/nginx:1.15.9-alpine-perl
$ docker run --name test3 -v /data/test/docker/nginx/test3:/usr/share/nginx/html:ro -d -p 9903:80 daocloud.io/library/nginx:1.15.9-alpine-perl
# 访问验证
$ curl localhost:9901
test1
$ curl localhost:9902
test2
$ curl localhost:9903
test3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
现在有了三个后端,访问之后能够返回对应的标识。
# 2,主要配置
接下来进入 Nginx 的主要配置阶段,将如下内容放到 Nginx 的 http 区域:
upstream eryajf-test-upsync {
upsync 127.0.0.1:8500/v1/kv/prod/nginx/upstreams/eryajf-test-upsync/ upsync_timeout=2m upsync_interval=2s upsync_type=consul strong_dependency=off;
upsync_dump_path /etc/nginx/upsync/eryajf-test-upsync.config;
include /etc/nginx/upsync/eryajf-test-upsync.config;
}
server {
listen 66;
server_name localhost;
location / {
proxy_pass http://eryajf-test-upsync;
proxy_set_header X-Forwarded-For $remote_addr;
}
# 便于观测后端代理列表
location /upstream_list {
upstream_show;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
upsync
:定义从 consul 拉取最新的 upstream 信息并存到本地的操作。127.0.0.1:8500
:建议是在每台 Nginx 部署一个 consul-client,统一请求 127,降低 server 端压力。upsync_timeout
:定义从 consul 拉取配置的超时时间。upsync_interval
:定义从 consul 拉取配置的间隔时间upsync_type
:配置同步接入类型,consul 或者 etcd。strong_dependency
:启动时是否强制依赖配置服务器,如果配置为 on,则拉取失败,nginx 同样会启用失败。
upsync_dump_path
:定义本地持久化目录,如果 consul 服务不可用,Nginx 将会引用此路径。一般这个路径应与 consul 中的 path 一致。此文件需要预先创建,并且内容与后端配置一致。否则将检测失败。include
:如果 consul 服务不可用,将会从上边 dump 的路径读取对应的配置。
# 3,调试验证
# 1,添加节点
此时可通过如下请求添加一个后端:
$ curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://127.0.0.1:8500/v1/kv/prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9901
true
2
返回 true 说明添加成功。
此时可在如下三个地方看到此配置:
upsync_dump_path
$ cat /etc/nginx/upsync/eryajf-test-upsync.config server 10.6.6.66:9901 weight=10 max_fails=3 fail_timeout=10s;
1
2server_upstream_list
$ curl http://10.6.6.66:66/upstream_list Upstream name: eryajf-test-upsync; Backend server count: 1 server 10.6.6.66:9901 weight=10 max_fails=3 fail_timeout=10s;
1
2
3consul_server
$ curl -s http://10.6.6.66:8500/v1/kv/?recurse | jq [ { "LockIndex": 0, "Key": "prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9901", "Flags": 0, "Value": "eyJ3ZWlnaHQiOjEwLCJtYXhfZmFpbHMiOjMsImZhaWxfdGltZW91dCI6MTAsImRvd24iOjB9", "CreateIndex": 14013, "ModifyIndex": 14022 } ]
1
2
3
4
5
6
7
8
9
10
11其中的 Value 是经过 base64 过的,使用如下命令可查看原内容:
$ echo 'eyJ3ZWlnaHQiOjEwLCJtYXhfZmFpbHMiOjMsImZhaWxfdGltZW91dCI6MTAsImRvd24iOjB9' | base64 -d | jq { "weight": 10, "max_fails": 3, "fail_timeout": 10, "down": 0 }
1
2
3
4
5
6
7以及从 web 界面也可以直接看到:
# 2,再加节点
这个时候再将另外两个节点加上:
$ curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://127.0.0.1:8500/v1/kv//prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9902
$ curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://127.0.0.1:8500/v1/kv//prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9903
2
再次查看如上三个信息:
upsync_dump_path
$ cat /etc/nginx/upsync/eryajf-test-upsync.config server 10.6.6.66:9903 weight=1 max_fails=2 fail_timeout=10s; server 10.6.6.66:9902 weight=1 max_fails=2 fail_timeout=10s; server 10.6.6.66:9901 weight=1 max_fails=2 fail_timeout=10s;
1
2
3
4server_upstream_list
$ curl http://10.6.6.66:66/upstream_list Upstream name: eryajf-test-upsync; Backend server count: 3 server 10.6.6.66:9903 weight=1 max_fails=2 fail_timeout=10s; server 10.6.6.66:9902 weight=1 max_fails=2 fail_timeout=10s; server 10.6.6.66:9901 weight=1 max_fails=2 fail_timeout=10s;
1
2
3
4
5consul_server
$ curl -s http://10.6.6.66:8500/v1/kv/?recurse | jq [ { "LockIndex": 0, "Key": "prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9901", "Flags": 0, "Value": "eyJ3ZWlnaHQiOjEsICJtYXhfZmFpbHMiOjIsICJmYWlsX3RpbWVvdXQiOjEwfQ==", "CreateIndex": 25518, "ModifyIndex": 25518 }, { "LockIndex": 0, "Key": "prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9902", "Flags": 0, "Value": "eyJ3ZWlnaHQiOjEsICJtYXhfZmFpbHMiOjIsICJmYWlsX3RpbWVvdXQiOjEwfQ==", "CreateIndex": 25525, "ModifyIndex": 25525 }, { "LockIndex": 0, "Key": "prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9903", "Flags": 0, "Value": "eyJ3ZWlnaHQiOjEsICJtYXhfZmFpbHMiOjIsICJmYWlsX3RpbWVvdXQiOjEwfQ==", "CreateIndex": 25527, "ModifyIndex": 25527 } ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
此时访问三次入口:
$ for i in a b c;do echo $i && curl localhost:66 ;done
a
test3
b
test2
c
test1
2
3
4
5
6
7
# 3,下线节点
下线某个后端,因为 Nginx 不能将权重 weight 置为 0,所以二丫 (opens new window)操作 down 字段来实现下线一个后端:
$ curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":1}' http://127.0.0.1:8500/v1/kv/prod/nginx/upstreams/eryajf-test-upsync/10.6.6.66:9902
然后看到相应的变化:
$ cat /etc/nginx/upsync/eryajf-test-upsync.config
server 10.6.6.66:9903 weight=1 max_fails=2 fail_timeout=10s;
server 10.6.6.66:9902 weight=1 max_fails=2 fail_timeout=10s down;
server 10.6.6.66:9901 weight=1 max_fails=2 fail_timeout=10s;
2
3
4
此时访问三次入口:
$ for i in a b c;do echo $i && curl localhost:66 ;done
a
test3
b
test1
c
test3
2
3
4
5
6
7
可以看到已经没有 test2 的返回了。
# 4,go 客户端
package main
import (
"encoding/json"
"fmt"
consulapi "github.com/hashicorp/consul/api"
)
const (
consulAddress = "10.6.6.14:8500"
)
func InitConsulCli() *consulapi.Client {
config := consulapi.DefaultConfig()
config.Address = consulAddress
client, err := consulapi.NewClient(config)
if err != nil {
msg := fmt.Sprintf("init consul client failed,err: %v\n", err)
panic(msg)
}
return client
}
type NgConsulKey struct {
Env string `json:"env"`
Service string `json:"service"`
Backend string `json:"backend"`
}
type NgConsulValue struct {
Weight int `json:"weight"`
MaxFails int `json:"max_fails"`
FailTimeout int `json:"fail_timeout"`
Down int `json:"down"`
}
func Addkv(k NgConsulKey, v NgConsulValue) (*consulapi.WriteMeta, error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
kv := InitConsulCli().KV()
p := &consulapi.KVPair{Key: fmt.Sprintf("%s/nginx/upstreams/%s/%s", k.Env, k.Service, k.Backend), Value: []byte(b)}
wm, err := kv.Put(p, nil)
if err != nil {
return nil, err
}
return wm, nil
}
func main() {
key := NgConsulKey{
Env: "prod",
Service: "eryajf-api-upsync",
Backend: "10.6.6.66:9902",
}
value := NgConsulValue{
Weight: 10,
MaxFails: 3,
FailTimeout: 10,
Down: 0,
}
_, err := Addkv(key, value)
if err != nil {
fmt.Printf("add kv failed:%v\n", err)
}
fmt.Println("add kv success")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
可以通过调整参数,添加不同的对象,通过调整 weight 调整后端的权重,通过调整 down 字段来决定后端服务上下线。