最近项目升级,核心代码重构,接口地址从 api 切换成 newapi,虽然上线前做了全面测试,但从运维角度来讲,万一升级之后出现异常,要能够快速切换回来,于是把接口通过 Nginx 做了一层反向代理,默认先代理到 newapi,也可以人为切换到 api,这就涉及到动态 upstream 问题了。
在 Nginx 的商业版本(Nginx Plus)中提供了 key‑value 存储功能可以实现零停机动态更新 upstream,可惜我们没钱买。不过业界已经有非常成熟的开源解决方案,例如:
- 微博的 nginx-upsync-module
- 淘宝的 ngx_http_dyups_module
- 又拍云的 slardar
这些模块很厉害,都能实现无需 reload 即可更新 upstream,就是使用起来麻烦一些,有些要自己手动编译 Nginx,有些用到了 Lua 脚本,对于一名前端工程师来讲,这些内容略显深奥,后来发现下面这种利用 Consul 和 Consul template 动态更新 Nginx 配置文件,自动 reload 的方案,实现和部署起来最简单,在此记录一下其安装、使用和部署过程。
安装
首先安装非常简单,去官方地址下载压缩包,解压后就一个文件 consul,然后将这个文件放到环境变量 PATH 中就能直接用了。在 Mac 中可以用 brew 安装:
$ brew install consul
==> Downloading https://homebrew.bintray.com/bottles/consul-1.8.4.catalina.bottle.tar.gz
To have launchd start consul now and restart at login:
brew services start consul
Or, if you don't want/need a background service you can just run:
consul agent -dev -bind 127.0.0.1
==> Summary
🍺 /usr/local/Cellar/consul/1.8.4: 8 files, 103MB
安装完成之后,在命令行输入:
$ consul
Usage: consul [--version] [--help] <command> [<args>]
Available commands are:
acl Interact with Consul's ACLs
agent Runs a Consul agent
catalog Interact with the catalog
config Interact with Consul's Centralized Configurations
connect Interact with Consul Connect
debug Records a debugging archive for operators
event Fire a new event
exec Executes a command on Consul nodes
force-leave Forces a member of the cluster to enter the "left" state
info Provides debugging information for operators.
intention Interact with Connect service intentions
join Tell Consul agent to join cluster
keygen Generates a new encryption key
keyring Manages gossip layer encryption keys
kv Interact with the key-value store
leave Gracefully leaves the Consul cluster and shuts down
lock Execute a command holding a lock
login Login to Consul using an auth method
logout Destroy a Consul token created with login
maint Controls node or service maintenance mode
members Lists the members of a Consul cluster
monitor Stream logs from a Consul agent
operator Provides cluster-level tools for Consul operators
reload Triggers the agent to reload configuration files
rtt Estimates network round trip time between nodes
services Interact with services
snapshot Saves, restores and inspects snapshots of Consul server state
tls Builtin helpers for creating CAs and certificates
validate Validate config files/directories
version Prints the Consul version
watch Watch for changes in Consul
可以发现 Consul 提供了很多 cli 命令,如启动代理、键值存储、加入集群等。
启动
下面的命令启动了一个开发模式下的单节点 Consul 代理:
$ consul agent -dev
consul agent -dev
==> Starting Consul agent...
Version: '1.8.4'
Node ID: '3b99c8c0-2bf9-cfb9-fa6b-7b6ac47ed6bf'
Node name: 'lx-4.local'
Datacenter: 'dc1' (Segment: '<all>')
Server: true (Bootstrap: false)
Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600)
Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302)
Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false
==> Log data will now stream in as it occurs:
注意:-dev 不能用于生产环境,因为不持久化任何状态,如果想要进行持久化,需要这样启动:
$ consul agent -server -bootstrap-expect=1 -data-dir=/tmp/consul -bind=192.168.31.134
==> Starting Consul agent...
Version: '1.8.4'
Node ID: 'a55e6040-cdeb-42b5-c12f-48e0e85c18b6'
Node name: 'promote.cache-dns.local'
Datacenter: 'dc1' (Segment: '<all>')
Server: true (Bootstrap: true)
Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: -1, DNS: 8600)
Cluster Addr: 192.168.31.134 (LAN: 8301, WAN: 8302)
Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false
注意:如果没加 -server 选项的话,则默认启动的是 client,后续操作会失败:
$ consul kv put abc 123
Error! Failed writing data: Unexpected response code: 500 (No known Consul servers)
注意:如果没加 -bootstrap-expect=1 的话,表明集群没有 leader,后续操作也会失败:
$ consul kv put abc 123
Error! Failed writing data: Unexpected response code: 500 (No cluster leader)
注意:如果没加 -bind 的话,可能会报错:
==> Multiple private IPv4 addresses found. Please configure one with 'bind' and/or 'advertise'.
因为 Consul 默认将绑定到本地计算机上的所有地址,并将第一个可用的私有 IPv4 地址通告给群集的其余部分。如果有多个私有 IPv4 地址可用,Consul 将在启动时退出并显示上面的错误。
如果指定 data-dir ,数据就会被持久化到 /tmp/consul 目录下了,即使 server 重启了,也能获取到值:
$ consul kv put abc 123
Success! Data written to: abc
# 重启 server 之后依然能够获得数据
$ consul kv get abc
123
还有一些其他启动参数都可以在启动时候加上,例如:
$ consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul -node=keliq -bind=192.168.31.134 -config-dir /etc/consul.d -ui
可以使用 consul agent --help 查看所有配置项,这里列举部分常用的配置:
-server:以 server 模式启动代理-data-dir: 配置 consul 数据存储路径-bootstrap-expect:期望的 server 节点数目,consul 一直等到指定sever数目的时候才会引导整个集群-bind:绑定的 IP 地址,默认是 0.0.0.0-node:节点在集群中的名称,在一个集群中必须是唯一的,默认是该节点的主机名-ui: 开启 web 管理界面-config-dir:配置文件目录,所有以.json结尾的文件都会被加载,可以是服务或 Consul 自身的配置-client:提供 HTTP、DNS、RPC 等服务,默认是127.0.0.1,不对外提供服务,如果需要则改成 0.0.0.0-join:加入一个已经启动的 agent 的 ip 地址,可以指定多个-retry-join:允许你在第一次失败后进行尝试-retry-interval:两次 join 之间的时间间隔,默认是30s-retry-max:尝试重复 join 的次数,默认是0,即无限次尝试-log-level:日志信息级别,默认是info,还可以选择:trace、debug、info、warn、err。
除了可以在命令行启动之外,还可以把参数写入配置文件中,然后通过 -config-dir 指定配置文件目录进行启动。如果以这种方式启动的话,该目录下所有的 .json 或 .hcl 后缀文件会按照字母序进行加载,然后合并到一起。注意:-config-dir 参数可以多次使用,表示指定多个配置文件目录。
$ consul agent -config-dir $PWD/config
一个示例配置文件如下:
{
"bind_addr": "192.168.31.134",
"datacenter": "keliq",
"data_dir": "/tmp/consul",
"encrypt": "txrd7Y61t8Mjim14tNaYB7cNpr8nvcAWwNifaSG3qY8=",
"log_level": "INFO",
"enable_debug": true,
"node_name": "ConsulServerKeliq",
"server": true,
"ui": true,
"bootstrap_expect": 1,
"leave_on_terminate": false,
"skip_leave_on_interrupt": true,
"rejoin_after_leave": true
}
上面启动的 Consul Agent 运行在 server 模式,由于只有单节点,所以自己就是 leader,因为单节点在故障的时候会丢失数据,所以不建议单server节点部署,官方推荐使用 3 或 5 个 server 节点。多节点的时候,就需要用 consul join 命令来组建集群,该命令在任意一个节点执行即可:
$ consul join <Node A Address> <Node B Address> <Node C Address>
Successfully joined cluster by contacting 3 nodes.
server 节点启动之后,client 节点就可以开始加入了。一个 client 是一个非常轻量级的进程,用于注册服务、运行健康检查和转发对 server 的查询,必须在集群中的每个主机上运行。client 节点的加入可以通过 join 任何一个已存在的节点来完成,所有的节点都是通过 gossip 协议来实现发现功能的。所以一旦有节点接入到任意的集群成员,新节点都会自动找到 server 节点,并向 server 节点注册自己。
命令
Consul 提供了丰富的命令,例如可以用下面的命令查看 Consul 集群的成员:
$ consul members
Node Address Status Type Build Protocol DC Segment
lx-4.local 127.0.0.1:8301 alive server 1.8.4 2 dc1 <all>
除了 shell 命令之外,Consul 也提供了 HTTP 的 API 操作接口,例如下面的请求可以查看所有节点:
$ curl localhost:8500/v1/catalog/nodes
[
{
"ID": "2ee111ce-3599-7651-6686-9103a4e5975b",
"Node": "lx-4.local",
"Address": "127.0.0.1",
"Datacenter": "dc1",
"TaggedAddresses": {
"lan": "127.0.0.1",
"lan_ipv4": "127.0.0.1",
"wan": "127.0.0.1",
"wan_ipv4": "127.0.0.1"
},
"Meta": {
"consul-network-segment": ""
},
"CreateIndex": 7,
"ModifyIndex": 7
}
]
Consul 中比较实用的功能之一就是 key/value 存储,可以用下面的命令获取 Key 的值:
$ consul kv get nginx/upstream/device
Error! No key exists at: nginx/upstream/device
表示数据不存在,我们尝试写入数据:
$ consul kv put nginx/upstream/device/localhost:8080 'weight=1 max_fails=1 fail_timeout=20s;'
Success! Data written to: nginx/upstream/device/localhost:8080
再次读取数据:
$ consul kv get nginx/upstream/device/localhost:8080
weight=1 max_fails=1 fail_timeout=20s;
注意:读取 nginx/upstream/device 是不存在的,因为 Key 是 nginx/upstream/device/localhost:8080,这是两个不同的 key,给它们设置值不会覆盖另一个::
$ consul kv put nginx/upstream/device localhost:8080
Success! Data written to: nginx/upstream/device
但是可以通过 -recurse 选项可以递归查询所有子键/值:
$ consul kv get -recurse nginx/upstream/device
nginx/upstream/device:localhost:8080
nginx/upstream/device/localhost:8080:weight=1 max_fails=1 fail_timeout=20s;
还可以递归删除:
$ consul kv delete -recurse nginx/upstream/device
Success! Deleted keys with prefix: nginx/upstream/device
除了 key/value 之外,Consul 还有一个强大的功能就是:可以通过 watch 对数据进行监控,一旦发生变化,就能触发执行指定处理程序。例如下面的语句表示只要 hello 这个键发生变化,就执行 echo 'hello change' 命令:
$ consul watch -type key -key hello echo 'hello change'
watch 的类型有
- key:监听键值对变化
- keyprefix:监听指定前缀键值对变化
- services:监听服务列表
- nodes:监听节点列表
- service:监听服务实例
- checks:监听健康检查
- event:监听用户事件
Consul 还提供了一个 Web 管理界面,只需在启动的时候指定 -ui 参数即可通过地址 http://localhost:8500 访问,下图是在 Web 界面中添加键值对:
Web 管理界面功能比较全,例如服务注册、键值对、账号权限管理等操作都可以在管理界面中完成。
Consul template
Consul template 是一个可以读取 Consul 内容并动态生成配置文件的轻量级工具,不仅可以实时根据模板生成配置文件,还能在生成之后执行各种命令,是一款非常强大的自动化工具。Consul template 的安装也非常简单,下载之后直接放到 path 目录下即可:
$ wget https://releases.hashicorp.com/consul-template/0.25.1/consul-template_0.25.1_linux_amd64.tgz
$ tar xzf consul-template_0.25.1_linux_amd64.tgz
$ cp consul-template /usr/local/bin
$ ls /usr/local/bin
consul-template docker-compose hexo pm2 pm2-dev pm2-docker pm2-runtime
$ consul-template -v
consul-template v0.25.1 (171d54d)
在 Mac 下也可以使用 brew 安装:
$ brew install consul-template
查看选项:
$ consul-template -h
根据模板生成配置文件:
$ consul-template \
-consul-addr demo.consul.io \ # consul 服务的地址
-template "/tmp/template.ctmpl:/tmp/result" # 模板和生成文件的路径
上述命令就是读取渲染模板文件 /tmp/template.ctmpl,用 Consul 中的值填入模板中,然后把生成的配置文件保存到 /tmp/result。其中 template.ctmlp 模板文件的格式大致如下:
upstream device_upstream {
{{range ls "upstreams/device" -}}
{{" "}}server {{.Key}} {{.Value}}
{{end -}}
}
其中 {{ 和 }} 代表左右分隔符,中间的部分是从 Consul 中读取的变量。不过分隔符是可以配置的:
left_delimiter = "{{"
right_delimiter = "}}"
还可以添加命令,当配置文件更新完毕之后会自动执行该命令:
$ consul-template \
-consul-addr demo.consul.io \
-template "/tmp/template.ctmpl:/tmp/result:service nginx restart"
还支持同时渲染多个模板、执行多个命令:
$ consul-template \
-consul-addr my.consul.internal:6124 \
-template "/tmp/nginx.ctmpl:/var/nginx/nginx.conf:service nginx restart" \
-template "/tmp/redis.ctmpl:/var/redis/redis.conf:service redis restart" \
-template "/tmp/haproxy.ctmpl:/var/haproxy/haproxy.conf"
这些参数可以通过配置文件的形式提供给 Consul template:
$ consul-template -config /root/config.hcl
其中 config.hcl 文件格式如下:
consul {
address = "10.154.156.24:8500"
}
template {
source = "ctmpl/nginx.conf.ctmpl"
destination = "conf/test-consul-template.conf"
command = "echo done"
}
到这里,一个动态 upstream 的方案就完成了,总结如下:
Nginx 的配置文件通过 Consul template 动态生成的,如果 Consul 中的值发生变化,便会更新本地的 Nginx 的配置文件,然后发起
nginx -s reload命令。
进阶内容
上面仅仅介绍了 Consul 的基础用法,在实践过程中还涉及到一些 Docker 部署、ACL 权限、数据备份的问题,细节就不多赘述了,仅仅记录如下:
容器化
Consul 官方也提供了 Docker 容器,用起来很方便:
$ docker pull consul # 下载镜像
$ docker run --name consul -p 8500:8500 -itd consul # 启动容器
注意端口号要映射到宿主机上,否则外部访问不了。
账号与权限
默认情况下,Consul 是不会开启账号和权限(ACL)的,要开启的话需要在控制台执行下面的命令:
$ consul acl bootstrap
Failed ACL bootstrapping: Unexpected response code: 401 (ACL support disabled)
不过前提是,Consul 启动的时候,需要在配置文件里面添加下面的配置,否则会报上面的错误:
{
"acl": {
"enabled": true,
"default_policy": "deny",
"down_policy": "extend-cache"
}
}
然后再执行就能看到管理员的账号和密码了:
$ consul acl bootstrap
AccessorID: 6e15f2d3-7213-2f80-c8af-986e9733830b
SecretID: 1b352e5a-1ff1-ffa9-a56c-00efd7472cd6
Description: Bootstrap Token (Global Management)
Local: false
Create Time: 2020-10-23 14:34:13.590304 +0800 CST
Policies:
00000000-0000-0000-0000-000000000001 - global-management
上面的 AccessorID 相当于账号,SecretID 相当于密码,保存好 SecretID,后续的命令都要带上这个 token:
$ consul members -token 1b352e5a-1ff1-ffa9-a56c-00efd7472cd6
$ curl --location --request GET 'http://localhost:8500/v1/catalog/nodes' \
--header 'X-Consul-Token: 1b352e5a-1ff1-ffa9-a56c-00efd7472cd6'
如果你觉得麻烦,可以设置环境变量, Consul 都会自动带上:
$ export CONSUL_HTTP_TOKEN=1b352e5a-1ff1-ffa9-a56c-00efd7472cd6
下面的命令可以查看所有 token:
$ consul acl token list
AccessorID: 6e15f2d3-7213-2f80-c8af-986e9733830b
Description: Bootstrap Token (Global Management)
Local: false
Create Time: 2020-10-23 14:34:13.590304 +0800 CST
Legacy: false
Policies:
00000000-0000-0000-0000-000000000001 - global-management
AccessorID: 00000000-0000-0000-0000-000000000002
Description: Anonymous Token
Local: false
Create Time: 2020-10-23 14:34:06.170368 +0800 CST
Legacy: false
002 账号是由 Consul 自动创建的,但它没有任何权限,表示匿名账号。第一个 bootstrap 令牌,则是一个有无限权限的管理 token,因为它拥有 global-management 策略,查看策略列表:
$ consul acl policy list
global-management:
ID: 00000000-0000-0000-0000-000000000001
Description: Builtin Policy that grants unlimited access
Datacenters:
如果我们希望不用 token 就能在多数环境下看到所有节点。我们可以配置一个 Consul 策略,该策略可以在不用 token 的时候以匿名 token 的身份去访问节点列表。
# 新建查看所有节点的权限策略
$ consul acl policy create -name 'list-all-nodes' -rules 'node_prefix "" { policy = "read" }'
ID: 72574f4a-1c7b-8b67-44c4-1331da37b5e4
Name: list-all-nodes
Description:
Datacenters:
Rules:
node_prefix "" { policy = "read" }
然后更新匿名令牌的访问权限:
$ consul acl token update -id 00000000-0000-0000-0000-000000000002 -policy-name list-all-nodes -description "Anonymous Token - Can List Nodes"
AccessorID: 00000000-0000-0000-0000-000000000002
SecretID: anonymous
Description: Anonymous Token - Can List Nodes
Local: false
Create Time: 2020-10-23 14:34:06.170368 +0800 CST
Policies:
72574f4a-1c7b-8b67-44c4-1331da37b5e4 - list-all-nodes
后面再访问节点就不需要令牌了。同理,还可以为其增加其他的权限:
# 新建读取 consul 服务的策略
$ consul acl policy create -name 'service-consul-read' \
-rules 'service "consul" { policy = "read" }'
# 更新匿名令牌
$ consul acl token update -id 00000000-0000-0000-0000-000000000002 \
--merge-policies \
-description "Anonymous Token - Can List Nodes" \
-policy-name service-consul-read
创建一个只能读取指定键或键前缀的策略:
# 指定只能读取 nginx 这个 key
$ consul acl policy create -name "kv-nginx-read" \
-description "read kv" \
-rules 'key "nginx" { policy = "read" }'
ID: 75ead007-71bd-92b1-ea23-621546aee435
Name: kv-nginx-read
Description: read kv
Datacenters:
Rules:
key "nginx" { policy = "read" }
# 可以读取所有以 nginx 开头的 key
$ consul acl policy create -name "kv-nginx-prefix-read" \
-description "read kv with nginx prefix" \
-rules 'key_prefix "nginx" { policy = "read" }'
ID: 97b8f62d-2f87-0ea8-61d4-7927dc7d0eac
Name: kv-nginx-prefix-read
Description: read kv with nginx prefix
Datacenters:
Rules:
key_prefix "nginx" { policy = "read" }
# 给匿名令牌访问 nginx kv 的权限
$ consul acl token update -id 00000000-0000-0000-0000-000000000002 \
--merge-policies \
-description "Anonymous Token - Can read nginx kv" \
-policy-name kv-nginx-read
# 或者可以重新创建一个账号,给这个账号读取 nginx key 的权限
$ consul acl token create -description "Nginx Token" -policy-name "kv-nginx-read"
AccessorID: 532ae236-81a6-18fe-fce1-c5817482bb34
SecretID: 9f671c12-175f-2782-a7e0-2733e2bf94d4
Description: Nginx Token
Local: false
Create Time: 2020-10-23 15:50:07.458503 +0800 CST
Policies:
75ead007-71bd-92b1-ea23-621546aee435 - kv-nginx-read
# 账号创建之后,也能更新权限
$ consul acl token update -id 532ae236-81a6-18fe-fce1-c5817482bb34 \
--merge-policies \
-description "Anonymous Token - Can List Prefix Nginx" \
-policy-name kv-nginx-prefix-read
AccessorID: 532ae236-81a6-18fe-fce1-c5817482bb34
SecretID: 9f671c12-175f-2782-a7e0-2733e2bf94d4
Description: Anonymous Token - Can List Prefix Nginx
Local: false
Create Time: 2020-10-23 15:50:07.458503 +0800 CST
Policies:
75ead007-71bd-92b1-ea23-621546aee435 - kv-nginx-read
97b8f62d-2f87-0ea8-61d4-7927dc7d0eac - kv-nginx-prefix-read
数据备份
虽然 Consul 会自动帮我们持久化数据,但是有时候也需要手动创建快照或者把数据保存起来,可以用 export 和 snapshot 命令,例如导出所有的键值到本地:
$ consul kv export '' > consul.d/consul_kv.json
后面可以导入到 Consul 中:
$ consul kv import consul_kv.json
生成快照:
$ consul snapshot save consul_state.snap
还原快照:
$ consul snapshot restore consul_state.snap