前提

前段时间顺利地把整个服务集群和中间件全部从UCloud迁移到阿里云,笔者担任了架构和半个运维的角色。这里详细记录一下通过NginxConsulUpsync实现动态负载均衡和服务平滑发布的核心知识点和操作步骤,整个体系已经在生产环境中平稳运行。编写本文使用的虚拟机系统为CentOS7.x,虚拟机的内网IP192.168.56.200

动态负载均衡的基本原理

一般会通过upstream配置Nginx的反向代理池:

  1. http {
  2. upstream upstream_server{
  3. server 127.0.0.1:8081;
  4. server 127.0.0.1:8082;
  5. }
  6. server {
  7. listen 80;
  8. server_name localhost;
  9. location / {
  10. proxy_pass http://upstream_server;
  11. }
  12. }
  13. }

现在假如8081端口的服务实例挂了需要剔除,那么需要修改upstream为:

  1. upstream upstream_server{
  2. # 添加down标记该端口的服务实例不参与负载
  3. server 127.0.0.1:8081 down;
  4. server 127.0.0.1:8082;
  5. }

并且通过nginx -s reload重新加载配置,该upstream配置才会生效。我们知道,服务发布时候重启过程中是处于不可用状态,正确的服务发布过程应该是:

  • 把该服务从对应的upstream剔除,一般是置为down,告知Nginx服务upstream配置变更,需要通过nginx -s reload进行重载。
  • 服务构建、部署和重启。
  • 通过探活脚本感知服务对应的端口能够访问,把该服务从对应的upstream中拉起,一般是把down去掉,告知Nginx服务upstream配置变更,需要通过nginx -s reload进行重载。

上面的步骤一则涉及到upstream配置,二则需要Nginx重新加载配置(nginx -s reload),显得比较笨重,在高负载的情况下重新启动Nginx并重新加载配置会进一步增加系统的负载并可能暂时降低性能。

所以,可以考虑使用分布式缓存把upstream配置存放在缓存服务中,然后Nginx直接从这个缓存服务中读取upstream的配置,这样如果有upstream的配置变更就可以直接修改缓存服务中对应的属性,而Nginx服务也不需要reload。在实战中,这里提到的缓存服务就选用了ConsulNginx读取缓存中的配置属性选用了新浪微博提供的NginxC语言模块nginx-upsync-module。示意图大致如下:

Consul安装和集群搭建

ConsulHashicorp公司的一个使用Golang开发的开源项目,它是一个用于服务发现和配置的工具,具备分布式和高度可用特性,并且具有极高的可伸缩性。Consul主要提供下面的功能:

  • 服务发现。
  • 运行状况检查。
  • 服务分块/服务网格(Service Segmentation/Service Mesh)。
  • 密钥/值存储。
  • 多数据中心。

下面是安装过程:

  1. mkdir /data/consul
  2. cd /data/consul
  3. wget https://releases.hashicorp.com/consul/1.7.3/consul_1.7.3_linux_amd64.zip
  4. # 注意解压后只有一个consul执行文件
  5. unzip consul_1.7.3_linux_amd64.zip

解压完成后,使用命令nohup /data/consul/consul agent -server -data-dir=/tmp/consul -bootstrap -ui -advertise=192.168.56.200 -client=192.168.56.200 > /dev/null 2>&1 &即可后台启动单机的Consul服务。启动Consul实例后,访问http://192.168.56.200:8500/即可打开其后台管理UI

下面基于单台虚拟机搭建一个伪集群,关于集群的一些配置属性的含义和命令参数的解释暂时不进行展开

  1. # 创建集群数据目录
  2. mkdir /data/consul/node1 /data/consul/node2 /data/consul/node3
  3. # 创建集群日志目录
  4. mkdir /data/consul/node1/logs /data/consul/node2/logs /data/consul/node3/logs

/data/consul/node1目录添加consul_conf.json文件,内容如下:

  1. {
  2. "datacenter": "es8-dc",
  3. "data_dir": "/data/consul/node1",
  4. "log_file": "/data/consul/node1/consul.log",
  5. "log_level": "INFO",
  6. "server": true,
  7. "node_name": "node1",
  8. "ui": true,
  9. "bind_addr": "192.168.56.200",
  10. "client_addr": "192.168.56.200",
  11. "advertise_addr": "192.168.56.200",
  12. "bootstrap_expect": 3,
  13. "ports":{
  14. "http": 8510,
  15. "dns": 8610,
  16. "server": 8310,
  17. "serf_lan": 8311,
  18. "serf_wan": 8312
  19. }
  20. }

/data/consul/node2目录添加consul_conf.json文件,内容如下:

  1. {
  2. "datacenter": "es8-dc",
  3. "data_dir": "/data/consul/node2",
  4. "log_file": "/data/consul/node2/consul.log",
  5. "log_level": "INFO",
  6. "server": true,
  7. "node_name": "node2",
  8. "ui": true,
  9. "bind_addr": "192.168.56.200",
  10. "client_addr": "192.168.56.200",
  11. "advertise_addr": "192.168.56.200",
  12. "bootstrap_expect": 3,
  13. "ports":{
  14. "http": 8520,
  15. "dns": 8620,
  16. "server": 8320,
  17. "serf_lan": 8321,
  18. "serf_wan": 8322
  19. }
  20. }

/data/consul/node3目录添加consul_conf.json文件,内容如下:

  1. {
  2. "datacenter": "es8-dc",
  3. "data_dir": "/data/consul/node3",
  4. "log_file": "/data/consul/node3/consul.log",
  5. "log_level": "INFO",
  6. "server": true,
  7. "node_name": "node3",
  8. "ui": true,
  9. "bind_addr": "192.168.56.200",
  10. "client_addr": "192.168.56.200",
  11. "advertise_addr": "192.168.56.200",
  12. "bootstrap_expect": 3,
  13. "ports":{
  14. "http": 8530,
  15. "dns": 8630,
  16. "server": 8330,
  17. "serf_lan": 8331,
  18. "serf_wan": 8332
  19. }
  20. }

新建一个集群启动脚本:

  1. cd /data/consul
  2. touch service.sh
  3. # /data/consul/service.sh内容如下:
  4. nohup /data/consul/consul agent -config-file=/data/consul/node1/consul_conf.json > /dev/null 2>&1 &
  5. sleep 10
  6. nohup /data/consul/consul agent -config-file=/data/consul/node2/consul_conf.json -retry-join=192.168.56.200:8311 > /dev/null 2>&1 &
  7. sleep 10
  8. nohup /data/consul/consul agent -config-file=/data/consul/node3/consul_conf.json -retry-join=192.168.56.200:8311 > /dev/null 2>&1 &

如果集群启动成功,观察节点1中的日志如下:

通过节点1的HTTP端点访问后台管理页面如下(可见当前的节点1被标记了一颗红色的星星,说明当前节点1是Leader节点):

至此,Consul单机伪集群搭建完成(其实分布式集群的搭建大同小异,注意集群节点所在的机器需要开放使用到的端口的访问权限),由于Consul使用Raft作为共识算法,该算法是强领导者模型,也就是只有Leader节点可以进行写操作,因此接下来的操作都需要使用节点1的HTTP端点,就是192.168.56.200:8510

重点笔记:如果Consul集群重启或者重新选举,Leader节点有可能发生更变,外部使用的时候建议把Leader节点的HTTP端点抽离到可动态更新的配置项中或者动态获取Leader节点的IP和端口。

Nginx编译安装

直接从官网下载二级制的安装包并且解压:

  1. mkdir /data/nginx
  2. cd /data/nginx
  3. wget http://nginx.org/download/nginx-1.18.0.tar.gz
  4. tar -zxvf nginx-1.18.0.tar.gz

解压后的所有源文件在/data/nginx/nginx-1.18.0目录下,编译之前需要安装pcre-develzlib-devel依赖:

  1. yum -y install pcre-devel
  2. yum install -y zlib-devel

编译命令如下:

  1. cd /data/nginx/nginx-1.18.0
  2. ./configure --prefix=/data/nginx

如果./configure执行过程不出现问题,那么结果如下:

接着执行make

  1. cd /data/nginx/nginx-1.18.0
  2. make

如果make执行过程不出现问题,那么结果如下:

最后,如果是首次安装,可以执行make install进行安装(实际上只是拷贝编译好的文件到--prefix指定的路径下):

  1. cd /data/nginx/nginx-1.18.0
  2. make install

make install执行完毕后,/data/nginx目录下新增了数个文件夹:

其中,Nginx启动程序在sbin目录下,logs是其日志目录,conf是其配置文件所在的目录。尝试启动一下Nginx

  1. /data/nginx/sbin/nginx

然后访问虚拟机的80端口,从而验证Nginx已经正常启动:

通过nginx-upsync-module和nginx_upstream_check_module模块进行编译

上面做了一个Nginx极简的编译过程,实际上,在做动态负载均衡的时候需要添加nginx-upsync-modulenginx_upstream_check_module两个模块,两个模块必须提前下载源码,并且在编译Nginx过程中需要指定两个模块的物理路径:

  1. mkdir /data/nginx/modules
  2. cd /data/nginx/modules
  3. # 这里是Github的资源,不能用wget下载,具体是:
  4. nginx-upsync-module需要下载release里面的最新版本:v2.1.2
  5. nginx_upstream_check_module需要下载整个项目的源码,主要用到靠近当前版本的补丁,使用patch命令进行补丁升级

下载完成后分别(解压)放在/data/nginx/modules目录下:

  1. ll /data/nginx/modules
  2. drwxr-xr-x. 6 root root 4096 Nov 3 2019 nginx_upstream_check_module-master
  3. drwxrwxr-x. 5 root root 93 Dec 18 00:56 nginx-upsync-module-2.1.2

编译前,还要先安装一些前置依赖组件:

  1. yum -y install libpcre3 libpcre3-dev ruby zlib1g-dev patch

接下来开始编译安装Nginx

  1. cd /data/nginx/nginx-1.18.0
  2. patch -p1 < /data/nginx/modules/nginx_upstream_check_module-master/check_1.16.1+.patch
  3. ./configure --prefix=/data/nginx --add-module=/data/nginx/modules/nginx_upstream_check_module-master --add-module=/data/nginx/modules/nginx-upsync-module-2.1.2
  4. make
  5. make install

上面的编译和安装过程无论怎么调整,都会出现部分依赖缺失导致make异常,估计是这两个模块并不支持太高版本的Nginx。(生产上用了一个版本比较低的OpenResty,这里想复原一下使用相对新版本Nginx的踩坑过程)于是尝试降级进行编译,下面是参考多个Issue后得到的相对比较新的可用版本组合:

  1. # 提前把/data/nginx下除了之前下载过的modules目录外的所有文件删除
  2. cd /data/nginx
  3. wget http://nginx.org/download/nginx-1.14.2.tar.gz
  4. tar -zxvf nginx-1.14.2.tar.gz

开始编译安装:

  1. cd /data/nginx/nginx-1.14.2
  2. patch -p1 < /data/nginx/modules/nginx_upstream_check_module-master/check_1.12.1+.patch
  3. ./configure --prefix=/data/nginx --add-module=/data/nginx/modules/nginx_upstream_check_module-master --add-module=/data/nginx/modules/nginx-upsync-module-2.1.2
  4. make && make install

安装完成后通过/data/nginx/sbin/nginx命令启动即可。

启用动态负载均和健康检查

首先编写一个简易的HTTP服务,因为Java比较重量级,这里选用Golang,代码如下:

  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "net/http"
  6. )
  7. func main() {
  8. var host string
  9. var port int
  10. flag.StringVar(&host, "h", "127.0.0.1", "IP地址")
  11. flag.IntVar(&port, "p", 9000, "端口")
  12. flag.Parse()
  13. address := fmt.Sprintf("%s:%d", host, port)
  14. http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
  15. _, _ = fmt.Fprintln(writer, fmt.Sprintf("%s by %s", "pong", address))
  16. })
  17. http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
  18. _, _ = fmt.Fprintln(writer, fmt.Sprintf("%s by %s", "hello world", address))
  19. })
  20. err := http.ListenAndServe(address, nil)
  21. if nil != err {
  22. panic(err)
  23. }
  24. }

编译:

  1. cd src
  2. set GOARCH=amd64
  3. set GOOS=linux
  4. go build -o ../bin/app app.go

这样子在项目的bin目录下就得到一个Linux下可执行的二级制文件app,分别在端口90009001启动两个服务实例:

  1. # 记得先给app文件的执行权限chmod 773 app
  2. nohup ./app -p 9000 >/dev/null 2>&1 &
  3. nohup ./app -p 9001 >/dev/null 2>&1 &

修改一下Nginx的配置,添加upstream

  1. # /data/nginx/conf/nginx.conf部分片段
  2. http {
  3. include mime.types;
  4. default_type application/octet-stream;
  5. sendfile on;
  6. keepalive_timeout 65;
  7. upstream app {
  8. # 这里是consul的leader节点的HTTP端点
  9. upsync 192.168.56.200:8510/v1/kv/upstreams/app/ upsync_timeout=6m upsync_interval=500ms upsync_type=consul strong_dependency=off;
  10. # consul访问不了的时候的备用配置
  11. upsync_dump_path /data/nginx/app.conf;
  12. # 这里是为了兼容Nginx的语法检查
  13. include /data/nginx/app.conf;
  14. # 下面三个配置是健康检查的配置
  15. check interval=1000 rise=2 fall=2 timeout=3000 type=http default_down=false;
  16. check_http_send "HEAD / HTTP/1.0\r\n\r\n";
  17. check_http_expect_alive http_2xx http_3xx;
  18. }
  19. server {
  20. listen 80;
  21. server_name localhost;
  22. location / {
  23. proxy_pass http://app;
  24. }
  25. # 健康检查 - 查看负载均衡的列表
  26. location /upstream_list {
  27. upstream_show;
  28. }
  29. # 健康检查 - 查看负载均衡的状态
  30. location /upstream_status {
  31. check_status;
  32. access_log off;
  33. }
  34. }
  35. }
  36. # /data/nginx/app.conf
  37. server 127.0.0.1:9000 weight=1 fail_timeout=10 max_fails=3;
  38. server 127.0.0.1:9001 weight=1 fail_timeout=10 max_fails=3;

手动添加两个HTTP服务进去Consul中:

  1. curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000
  2. curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9001

最后重新加载Nginx的配置即可。

动态负载均衡测试

前置工作准备好,现在尝试动态负载均衡,先从Consul下线9000端口的服务实例:

  1. curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":1}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000

可见负载均衡的列表中,9000端口的服务实例已经置为down,此时疯狂请求http://192.168.56.200,只输出hello world by 127.0.0.1:9001,可见9000端口的服务实例已经不再参与负载。重新上线9000端口的服务实例:

  1. curl -X PUT -d '{"weight":1, "max_fails":2, "fail_timeout":10, "down":0}' http://192.168.56.200:8510/v1/kv/upstreams/app/127.0.0.1:9000

再疯狂请求http://192.168.56.200,发现hello world by 127.0.0.1:9000hello world by 127.0.0.1:9001交替输出。到此可以验证动态负载均衡是成功的。此时再测试一下服务健康监测,通过kill -9随机杀掉其中一个服务实例,然后观察/upstream_status端点:

疯狂请求http://192.168.56.200,只输出hello world by 127.0.0.1:9001,可见9000端口的服务实例已经不再参与负载,但是查看Consul9000端口的服务实例的配置,并没有标记为down,可见是nginx_upstream_check_module为我们过滤了异常的节点,让这些节点不再参与负载。

总的来说,这个相对完善的动态负载均衡功能需要nginx_upstream_check_modulenginx-upsync-module共同协作才能完成。

服务平滑发布

服务平滑发布依赖于前面花大量时间分析的动态负载均衡功能。笔者所在的团队比较小,所以选用了阿里云的云效作为产研管理平台,通过里面的流水线功能实现了服务平滑发布,下面是其中一个服务的生产环境部署的流水线:

其实平滑发布和平台的关系不大,整体的步骤大概如下:

步骤比较多,并且涉及到大量的shell脚本,这里不把详细的脚本内容列出,简单列出一下每一步的操作(注意某些步骤之间可以插入合理的sleep n保证前一步执行完毕):

  • 代码扫描、单元测试等等。
  • 代码构建,生成构建后的压缩包。
  • 压缩包上传到服务器X中,解压到对应的目录。
  • Consul发送指令,把当前发布的X_IP:PORT的负载配置更新为down=1
  • stop服务X_IP:PORT
  • start服务X_IP:PORT
  • 检查服务X_IP:PORT的健康状态(可以设定一个时间周期例如120秒内每10秒检查一次),如果启动失败,则直接中断返回,确保还有另一个正常的旧节点参与负载,并且人工介入处理。
  • Consul发送指令,把当前发布的X_IP:PORT的负载配置更新为down=0

上面的流程是通过hard code完成,对于不同的服务器,只需要添加一个发布流程节点并且改动一个IP的占位符即可,不需要对Nginx进行配置重新加载。笔者所在的平台流量不大,目前每个服务部署两个节点就能满足生产需要,试想一下,如果要实现动态扩容,应该怎么构建流水线?

小结

服务平滑发布是CI/CD中比较重要的一个环节,而动态负载均衡则是服务平滑发布的基础。虽然现在很多云平台都提供了十分便捷的持续集成工具,但是在使用这些工具和配置流程的时候,最好能够理解背后的基本原理,这样才能在工具不适用的时候或者出现问题的时时候,迅速地作出判断和响应。

参考资料:

(本文完 c-7-d e-a-20200613 感谢广州某金融科技公司运维大佬昊哥提供的支持)

通过Nginx、Consul、Upsync实现动态负载均衡和服务平滑发布的更多相关文章

  1. 动态负载均衡(Nginx+Consul+UpSync)

    Http动态负载均衡 什么是动态负载均衡 传统的负载均衡,如果Upstream参数发生变化,每次都需要重新加载nginx.conf文件, 因此扩展性不是很高,所以我们可以采用动态负载均衡,实现Upst ...

  2. 【Nginx】基于Consul+Upsync+Nginx实现动态负载均衡

    一.Http动态负载均衡 什么是动态负载均衡 动态负载均衡实现方案 常用服务器注册与发现框架 二.Consul快速入门 Consul环境搭建 三.nginx-upsync-module nginx-u ...

  3. Consul+upsync+Nginx 动态负载均衡

    1,动态负载均衡 传统的负载均衡,如果修改了nginx.conf 的配置,必须需要重启nginx 服务,效率不高.动态负载均衡,就是可配置化,动态化的去配置负载均衡. 2,实现方案 1. Consul ...

  4. Nginx(四) nginx+consul+upasync 在ubnutu18带桌面系统 实现动态负载均衡

    1.1 什么是动态负载均衡 传统的负载均衡,如果Upstream参数发生变化,每次都需要重新加载nginx.conf文件,因此扩展性不是很高,所以我们可以采用动态负载均衡,实现Upstream可配置化 ...

  5. 《nginx 三》实现nginx的动态负载均衡——实战

    Http动态负载均衡 什么是动态负载均衡 传统的负载均衡,如果Upstream参数发生变化,每次都需要重新加载nginx.conf文件, 因此扩展性不是很高,所以我们可以采用动态负载均衡,实现Upst ...

  6. Consul+upsync+Nginx实现动态负载均衡 摘自https://blog.csdn.net/qq_29247945/article/details/80787014

    传统感念:每次修改完nginx配置文件,要重启nginx 动态感念:每次修改完nginx配置信息,不需要重启,nginx实时读取配置信息. Nginx: 反向代理和负载均衡 Consul:是用go编写 ...

  7. 基于Consul+Upsync+Nginx实现动态负载均衡

    基于Consul+Upsync+Nginx实现动态负载均衡 1.Consul环境搭建 下载consul_0.7.5_linux_amd64.zip到/usr/local/src目录 cd /usr/l ...

  8. 动态负载均衡(Nginx+Consul+UpSync)环境搭建

    首先 安装好 Consul upsync 然后: 1.配置安装Nginx 需要做配置,包括分组之类的,创建目录,有些插件是需要存放在这些目录的 groupadd nginx useradd -g ng ...

  9. Nginx 实现动态负载均衡(Nginx-1.10.1 + Consul v0.6.4)

    一直也没有找到合适的类似Socat + Haproxy 的组合能用在Nginx,后来发现了Nginx的几个模块,但是也存在各种不足. 而且Nginx 在大流量的情况下nginx -s reload 是 ...

随机推荐

  1. vue绑定数据之前 会看到源代码

    http://blog.csdn.net/fengjingyu168/article/details/72915468 VUE绑定数据闪现问题 问题描述如下: 1.在HTML中使用Vue为div绑定数 ...

  2. python 串口 透传

    python正常情况通过串口 serial  传输数据的时候,都是以字符串的形式发送的 str = ‘abcd’ ser.write(str.encode())#直接发送str报错,需要发送byte类 ...

  3. 用python selenium 单窗口单IP刷网站流量脚本

    近期有朋友说需要增加自己网站的流量,故用python写了一个简单的脚本,配合IP池或者动态IP都可以刷,当然最爽的是单窗口单IP. 说明 作弊可耻! 环境 python3.8 + selenium + ...

  4. Mysql创建修改删除-表

    创建表之前要链接到库  例如  库名为 student use student; 连接结束可以查看此库中所有表 show tables; 创建表 create table student( id in ...

  5. 使用EditPlus根据指定字符批量换行,快速填充Postman请求参数键值对

    1.当某个.ext格式的文件中的重复格式内容太多时,而又想要根据某个字符进行批量换行时,那么可以使用EditPlus进行批量换行. 在开发过程中就会经常遇到这种问题,比如把Url的请求参数,快速的填写 ...

  6. zookeeper安装部署步骤

    安装步骤 本安装教程,采用zookeeper 3.6.1 装java 下载zk包,分发到各个机器 确定或创建一个zookeeper的数据存放路径,并在该路径下创建一个myid文件,其中设置当前zook ...

  7. 跟着拉大锯大神学Android——网络编程中运行后台服务器端口占用问题

    拉大锯网页地址:https://www.sunofbeach.net/u/1153952789488054272 跟着拉大锯大神学Android,在学到网络编程时,使用了大神搭建的用于学习的后台服务器 ...

  8. CPU缓存学习及C6678缓存使用总结(知识归纳)

    作者注: 1.本篇博客内容是本人在学习cpu缓存原理时进行的学习总结,参考了多处相关资源(书籍,视频,知乎回答等),参考出处标注在内容最后. 2.由于本篇内容的编辑工作在印象笔记完成,输出的PDF文件 ...

  9. Spring Cloud 系列之 Apollo 配置中心(一)

    背景 随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关.参数的配置.服务器的地址等等. 对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境.分集群管理配置,完善的权限.审核机 ...

  10. [06]HTML基础之表单标签

    1. <form>标签 表单容器,指定method属性和action属性是个良好的习惯. <form methor="POST" action="htt ...