背景

在Nginx集群有一定的规模时,比较让人头疼的问题有2个,一是如何在不reload nginx的情况下,动态更新后端rs,减少nginx reload的性能损耗,也能更好的对接到内部的部署平台;二是如何标准化的修改nginx配置并下发,统一对配置进行管理。

目标

目标一:可以在不Reload Nginx的情况下,动态更新upstream后端rs的信息

目标二:可以通过Jenkins对Nginx的配置进行更新、下发、删除文件、证书更新和回滚操作

功能预览

修改配置并上传git

nginx更新前duan包需要重启吗 nginx动态更新配置_运维

配置下发

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_02

规划说明

整体架构

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_03

简要流程:

1.Nginx配置管理

  • 管理员通过jenkins配置的job修改nginx配置并以tag方式提交到gitlab
  • 管理员通过jenkins的配置下发job,下发指定tag的配置到选定的nginx集群主机上

2.Nginx动态负载均衡,使用开源upsync模块配置

  • 管理员通过调用etcd的接口维护(增加、删除、修改)指定upstream下的rs
  • Nginx集群会动态的更新指定upstream下的rs,进行负载调度

环境规划

这里规划的系统配置和角色都是实验环境,生产环境按照实际情况拆分,升配。

IP地址

主机名

系统配置

服务角色

10.0.1.222

ops-node-01

CentOS 7.9.2009 X86_64 2c*2g

Nginx-Pord-L7集群

10.0.1.223

ops-node-02

CentOS 7.9.2009 X86_64 2c*2g

Nginx-Pord-L7集群

10.0.1.224

ops-node-03

CentOS 7.9.2009 X86_64 2c*2g

Nginx-Pord-L4集群

10.0.1.225

ops-node-04

CentOS 7.9.2009 X86_64 4c*8g

etcd/gitlab/jenkins/ansible和业务应用

数据存储目录:

  • ETCD集群12379端口实例数据存储目录:/root/docker/etcd-vloume-12379
  • ETCD集群22379端口实例数据存储目录:/root/docker/etcd-vloume-22379
  • ETCD集群32379端口实例数据存储目录:/root/docker/etcd-vloume-32379
  • Gitlab数据存储目录:/root/docker/gitlab-volume
  • Jenkins数据存储目录:/root/docker/jenkins-volume
  • Nginx数据存储目录:/root/docker/nginx-volume

注意:为了便于管理,建议将Jenkins、Gitlab、Etcd独立出来,只做运维内部使用,和线上区别开。

Gitlab规划

Gitlab用于存储Nginx的配置。规划一个Nginx分组,在Nginx分组中维护两个仓库,分别是Nginx-Prod-L4(四层负载均衡配置)和Nginx-Prod-L7(七层负载均衡配置)

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_04

实际生产中会有很多的配置仓库,建议按照环境进行区分。这里的仓库命名规范:工具名称-环境-用途

Nginx规划

Nginx用于反向代理,使用新浪开源的upsync模块,结合etcd进行动态负载均衡。规划两套集群,分别是Nginx-Prod-L4(四层负载均衡集群)和Nginx-Prod-L7(七层负载均衡集群),规范标准化配置。

Nginx-Prod-L7配置规划如下:

# 存放证书的目录
├── certs
│   ├── zhushiyang.net.key
│   └── zhushiyang.net.pem
# 存放upstream从etcd拉取rs的信息,进行持久化的目录
├── dump_upstreams
│   ├── blog_cluster_upsync.conf
│   └── web_cluster_upsync.conf
├── fastcgi.conf
├── fastcgi.conf.default
├── fastcgi_params
├── fastcgi_params.default
├── koi-utf
├── koi-win
├── mime.types
├── mime.types.default
# nginx主配置文件,已经优化
├── nginx.conf
├── nginx.conf.default
├── proxy.conf
├── scgi_params
├── scgi_params.default
# 存放upstream配置的目录
├── upstreams
│   ├── blog_cluster_upsync.conf
│   └── web_cluster_upsync.conf
├── uwsgi_params
├── uwsgi_params.default
# 存放虚拟主机配置的目录
├── vhosts
│   ├── blog.zhushiyang.net.conf
│   └── www.zhushiyang.net.conf
└── win-utf

Nginx-Prod-L4配置规划如下:

├── fastcgi.conf
├── fastcgi.conf.default
├── fastcgi_params
├── fastcgi_params.default
├── koi-utf
├── koi-win
├── mime.types
├── mime.types.default
# nginx主配置文件,已经优化
├── nginx.conf
├── nginx.conf.default
├── proxy.conf
├── scgi_params
├── scgi_params.default
# 存放upstream配置的目录
├── streams
│   └── etcd.zhushiyang.net.conf
├── uwsgi_params
├── uwsgi_params.default
└── win-utf
  • 开启四层代理:--with-stream

Jenkins规划

Jenkins只用作Nginx集群的配置更新和下发。规划两个目录,分别是Nginx-Prod-L4(四层负载均衡Folder)和Nginx-Prod-L7(七层负载均衡Folder)。Folder命名规范:工具名称-环境-用途

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_05

 Nginx-Prod-L7的Job布局:

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_06

  • configure_deploy:下发配置文件
  • nginx.conf:更新nginx.conf文件到gitlab
  • proxy.conf:更新proxy.conf文件到gitlab
  • upstreams:维护upstream,每个upstream是一个job,命名格式是upstream名称,不需要带.conf后缀,会把配置推送到gitlab
  • vhosts:维护虚拟主机,每个虚拟主机是一个job,命名格式是域名,不需要带.conf后缀,会把配置推送到gitlab

Nginx-Prod-L4的Job布局:

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_07

  • configure_deploy:下发配置文件
  • nginx.conf:更新nginx.conf文件到gitlab
  • streams:维护stream,每个stream是一个job,命名格式是域名,不需要带.conf后缀,会把配置推送到gitlab

Etcd规划

Etcd集群用于存储Nginx的upstream下的RS信息。在一台机器上启动三个不同端口的etcd容器,组成etcd集群(生产中要分成3台机器组建集群),使用Nginx-Prod-L4代理Etcd集群。管理员调用etcd接口动态配置Nginx-Prod-L7集群的upstream。

Ansible规划

Ansible用于下发配置、检验和重启Nginx操作。不需要太复杂,需要在jenkins容器中进行安装。主机的资产文件从宿主机进行挂载。

环境搭建

注意:所有的应用均使用docker搭建。

注意:所有的应用均使用docker搭建。

注意:所有的应用均使用docker搭建。

系统初始化

按照下面的步骤正常初始化系统即可,生产环境要按照组件类别进行系统调优。

# 1.关闭防火墙
systemctl stop firewalld
systemctl disable firewalld

# 2.关闭selinux
setenforce 0
sed -i 's#SELINUX=enforcing#SELINUX=disabled#g' /etc/selinux/config

# 3.关闭postfix
systemctl stop postfix.service
systemctl disable postfix.service

# 4.下载epel源并安装基础依赖包
wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
yum install -y wget vim lsof unzip net-tools ntpdate bash-completion lrzsz jq git mlocate tree

# 5.设置时间同步
/usr/sbin/ntpdate time1.aliyun.com
echo '*/1 * * * * /usr/sbin/ntpdate time1.aliyun.com > /dev/null 2>&1' > /var/spool/cron/root
chmod 600 /var/spool/cron/root
systemctl restart crond
# 6.设置hosts
cat >> /etc/hosts <<EOF
10.0.1.222 ops-node-01
10.0.1.223 ops-node-02
10.0.1.224 ops-node-03
10.0.1.225 ops-node-04
EOF

# 7.设置UTF-8
echo "LANG=\"en_US.UTF-8\"">/etc/locale.conf
source  /etc/locale.conf

# 8.设置ulimit
cat > /etc/security/limits.conf << EOF
* soft nofile 1024000
* hard nofile 1024000
* soft nproc  1024000
* hard nproc  1024000
EOF

cat > /etc/security/limits.d/20-nproc.conf << EOF
*          soft    nproc     409600
root       soft    nproc     unlimited
EOF

# 9.设置终端超时,别名和PS变量
vim /etc/profile
export TMOUT=0
alias grep='grep --color=auto'
alias ll='ls -l --color=auto --time-style=long-iso'
PS1='\[\e[0;33m\]\u\[\e[0m\]@\[\e[0;32m\]\h\[\e[0m\]:\[\e[0;34m\]\w\[\e[0m\]\$ '

source /etc/profile

# 10.按照环境规划设置主机名
# 在10.0.1.222上执行
hostnamectl set-hostname ops-node-01
# 在10.0.1.223上执行
hostnamectl set-hostname ops-node-02
# 在10.0.1.224上执行
hostnamectl set-hostname ops-node-03
# 在10.0.1.225上执行
hostnamectl set-hostname ops-node-04

安装docker

在所有机器上安装docker,配置阿里云镜像加速,并加入开机启动。没有限制docker版本,也没有自定义太多的docker配置,可以按照实际情况定制。

yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sed -i 's+download.docker.com+mirrors.aliyun.com/docker-ce+' /etc/yum.repos.d/docker-ce.repo
yum install -y docker-ce
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
systemctl enable --now docker
# 配置镜像加速
cat > /etc/docker/daemon.json <<EOF
{
        "registry-mirrors":["https://q2gr04ke.mirror.aliyuncs.com"]
}
EOF

systemctl restart docker

安装etcd

在ops-node-04,10.0.1.225节点上安装etcd集群(使用的是不同端口的etcd实例模拟的集群)。

下面的启动命令在官方文档(Run etcd clusters inside containers | etcd)中有示例,做了一些变量和参数的调整(参数对集群进行了一些优化),如果你对etcd不太了解,可以看一下官方文档(etcd),或者看一下《云原生分布式存储基石 etcd深入解析》这本书。

# 初始化环境变量,在每个节点上都要执行(这里是一台服务器模拟集群)
# 设置三个ETCD的节点名称(这里是一台服务器模拟集群)
ETCD_NAME_1=ops-etcd-01
ETCD_NAME_2=ops-etcd-02
ETCD_NAME_3=ops-etcd-03
# 设置三个ETCD的节点IP地址(这里是一台服务器模拟集群)
ETCD_NODE_IP_1=10.0.1.225
ETCD_NODE_IP_2=10.0.1.225
ETCD_NODE_IP_3=10.0.1.225
# 设置ETCD的节点唯一ID
ETCD_CLUSTER_TOKEN=etcd-cluster
# 设置ETCD初始化集群状态
ETCD_CLUSTER_STATE=new
# 设置ETCD集群CLUSTER的地址
ETCD_CLUSTER=${ETCD_NAME_1}=http://${ETCD_NODE_IP_1}:12380,${ETCD_NAME_2}=http://${ETCD_NODE_IP_2}:22380,${ETCD_NAME_3}=http://${ETCD_NODE_IP_3}:32380
# 设置ETCD镜像仓库
ETCD_REGISTRY=quay.io/coreos/etcd
# 设置ETCD镜像版本
ETCD_VERSION=v3.4.20

# 初始化第一个节点
THIS_NAME=${ETCD_NAME_1}
THIS_IP=${ETCD_NODE_IP_1}
# 设置ETCD数据存储目录
ETCD_DATA_DIR=/root/docker/etcd-vloume-12379/

docker run -d --restart=always --network=host --hostname ${THIS_NAME} --name ${THIS_NAME} \
-v ${ETCD_DATA_DIR}:/etcd-data \
-v /etc/localtime:/etc/localtime:ro \
${ETCD_REGISTRY}:${ETCD_VERSION} \
/usr/local/bin/etcd \
  --name ${THIS_NAME} \
  --data-dir=/etcd-data \
  --initial-advertise-peer-urls http://${THIS_IP}:12380 \
  --listen-peer-urls http://0.0.0.0:12380 \
  --advertise-client-urls http://${THIS_IP}:12379 \
  --listen-client-urls http://0.0.0.0:12379 \
  --initial-cluster ${ETCD_CLUSTER} \
  --initial-cluster-state ${ETCD_CLUSTER_STATE} \
  --initial-cluster-token ${ETCD_CLUSTER_TOKEN} \
  --heartbeat-interval=100 \
  --election-timeout=500 \
  --auto-compaction-mode=periodic \
  --auto-compaction-retention=1 \
  --quota-backend-bytes=8589934592 \
  --snapshot-count=5000 \
  --enable-v2


# 初始化第二个节点
THIS_NAME=${ETCD_NAME_2}
THIS_IP=${ETCD_NODE_IP_2}
# 设置ETCD数据存储目录
ETCD_DATA_DIR=/root/docker/etcd-vloume-22379/

docker run -d --restart=always --network=host --hostname ${THIS_NAME} --name ${THIS_NAME} \
-v ${ETCD_DATA_DIR}:/etcd-data \
-v /etc/localtime:/etc/localtime:ro \
${ETCD_REGISTRY}:${ETCD_VERSION} \
/usr/local/bin/etcd \
  --name ${THIS_NAME} \
  --data-dir=/etcd-data \
  --initial-advertise-peer-urls http://${THIS_IP}:22380 \
  --listen-peer-urls http://0.0.0.0:22380 \
  --advertise-client-urls http://${THIS_IP}:22379 \
  --listen-client-urls http://0.0.0.0:22379 \
  --initial-cluster ${ETCD_CLUSTER} \
  --initial-cluster-state ${ETCD_CLUSTER_STATE} \
  --initial-cluster-token ${ETCD_CLUSTER_TOKEN} \
  --heartbeat-interval=100 \
  --election-timeout=500 \
  --auto-compaction-mode=periodic \
  --auto-compaction-retention=1 \
  --quota-backend-bytes=8589934592 \
  --snapshot-count=5000 \
  --enable-v2
  

# 初始化第三个节点
THIS_NAME=${ETCD_NAME_3}
THIS_IP=${ETCD_NODE_IP_3}
# 设置ETCD数据存储目录
ETCD_DATA_DIR=/root/docker/etcd-vloume-32379/

docker run -d --restart=always --network=host --hostname ${THIS_NAME} --name ${THIS_NAME} \
-v ${ETCD_DATA_DIR}:/etcd-data \
-v /etc/localtime:/etc/localtime:ro \
${ETCD_REGISTRY}:${ETCD_VERSION} \
/usr/local/bin/etcd \
  --name ${THIS_NAME} \
  --data-dir=/etcd-data \
  --initial-advertise-peer-urls http://${THIS_IP}:32380 \
  --listen-peer-urls http://0.0.0.0:32380 \
  --advertise-client-urls http://${THIS_IP}:32379 \
  --listen-client-urls http://0.0.0.0:32379 \
  --initial-cluster ${ETCD_CLUSTER} \
  --initial-cluster-state ${ETCD_CLUSTER_STATE} \
  --initial-cluster-token ${ETCD_CLUSTER_TOKEN} \
  --heartbeat-interval=100 \
  --election-timeout=500 \
  --auto-compaction-mode=periodic \
  --auto-compaction-retention=1 \
  --quota-backend-bytes=8589934592 \
  --snapshot-count=5000 \
  --enable-v2

安装etcd客户端,主要为了使用etcdctl客户端工具

yum install -y etcd

检查etcd集群状态

ENDPOINTS=http://10.0.1.225:12379,http://10.0.1.225:22379,http://10.0.1.225:32379
ETCDCTL_API=3 etcdctl --endpoints=${ENDPOINTS} -w table endpoint status 
# 集群状态正常
+-------------------------+------------------+---------+---------+-----------+-----------+------------+
|        ENDPOINT         |        ID        | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
+-------------------------+------------------+---------+---------+-----------+-----------+------------+
| http://10.0.1.225:12379 | f0507c477f30cfa3 |  3.4.20 |   20 kB |      true |         2 |          9 |
| http://10.0.1.225:22379 | 8fb5ef3dcf4f436c |  3.4.20 |   20 kB |     false |         2 |          9 |
| http://10.0.1.225:32379 | c025315846602833 |  3.4.20 |   20 kB |     false |         2 |          9 |
+-------------------------+------------------+---------+---------+-----------+-----------+------------+

安装gitlab

在ops-node-04,10.0.1.225节点上安装gitlab。在安装gitlab时遇到了几个小问题:

  • 安装后用户头像无法显示,影响体验,有强迫症的人很是抓狂(替换显示头像的url)
  • 安装后需要查看gitlab的初始密码,略麻烦(生产环境还是留给系统自动初始化比较好)(采用预配置参数解决)
  • 创建项目后,项目的url并不是域名或者ip地址,而是默认的主机名,导致无法克隆项目(采用预配置参数解决)
  • 默认启动了监控的相关组件,测试的时候也基本用不到,还占用内存(采用预配置参数解决)。

预配置参数规则:只要是gitlab.rb文件中的配置都可以追加到容器的GITLAB_OMNIBUS_CONFIG变量中。

处理了上面几个问题后,启动的命令如下

# 为了降低内存占用,关闭了所有监控组件(建议测试的时候关闭); 
# 设置root密码为root1234
# 替换gravatar源,解决用户头像无法显示问题
docker run -d --restart=always --hostname ops-gitlab --name ops-gitlab \
-e GITLAB_OMNIBUS_CONFIG="external_url 'http://10.0.1.225/'; \
     gitlab_rails['gitlab_shell_ssh_port'] = 22; \
     grafana['enable'] = false; \
     prometheus['enable'] = false; \
     node_exporter['enable'] = false; \
     redis_exporter['enable'] = false; \
     postgres_exporter['enable'] = false; \
     gitlab_exporter['enable'] = false; \
     alertmanager['enable'] = false; \
     gitlab_rails['gravatar_plain_url'] = 'http://sdn.geekzu.org/avatar/%{hash}?s=%{size}&d=identicon';\
     gitlab_rails['gravatar_ssl_url'] = 'https://sdn.geekzu.org/avatar/%{hash}?s=%{size}&d=identicon'" \
-e GITLAB_ROOT_PASSWORD="root1234" \
-p 443:443 -p 80:80 -p 2222:22 \
-v /root/docker/gitlab-volume/config:/etc/gitlab \
-v /root/docker/gitlab-volume/logs:/var/log/gitlab \
-v /root/docker/gitlab-volume/data:/var/opt/gitlab \
-v /etc/localtime:/etc/localtime:ro gitlab/gitlab-ee:latest

账号:root,密码:root1234

访问http://10.0.1.225,输入账号密码,登录后进行中文设置:点击【右上角头像】-【Preferences】-【Localization】-【Chinese, Simplified - 简体中文 (95% translated)】-保存

安装jenkins

在ops-node-04,10.0.1.225节点上安装jenkins。容器启动的jenkins一般在填写初始化密码后,安装插件环节会报【离线】的错误,大部分的解决方案都是修改updates/default.json中www.google.com地址以此来跳过健康检查,或者将hudson.model.UpdateCenter.xml中的域名替换http,在或者将域名替换为国内三方源,但是尝试过所有的方案后,发现该问题并没有得到解决,这是为什么呢?

jenkins使用updates/default.json文件中的www.google.com检测主机能否联网,如果检测正常,就会进入到安装插件页面,反之就会报离线错误。而且在修改配置文件重启jenkins后,jenkins还会从指定的url重新拉取updates/default.json文件导致修改无效。所以,在容器中给www.google.com设置hosts可以解决该问题,给www.google.com解析的IP地址,发送http请求时必须可以返回2xx或者3xx,因为安全性要求,内网的主机不一定都能联网,也可以使用私有地址。

# 配置Ansible资产文件
mkdir -p /root/ansible
cat > /root/ansible/hosts <<EOF
[nginx_prod_l7]
10.0.1.222
10.0.1.223
[nginx_prod_l4]
10.0.1.224
EOF

# 获取www.baidu.com的IP地址,用来给www.google.com设置hosts
ALIVE_IP=$(ping -c 1 www.baidu.com|grep PING|awk -F '[()]' '{print $2}')
docker run -d -u root --add-host www.google.com:${ALIVE_IP} --restart=always --hostname ops-jenkins --name ops-jenkins \
-e  JAVA_OPTS="-Xms1024m -Xmx2048m -XX:MaxNewSize=512m" \
-p 8080:8080 -p 50000:50000 \
-v /root/docker/jenkins-volume:/var/jenkins_home \
-v /root/ansible/hosts:/root/hosts \
-v /etc/localtime:/etc/localtime:ro jenkins/jenkins:2.346.3-alpine-jdk8

# 安装基础工具
docker exec -it ops-jenkins /bin/bash
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories && apk add --no-cache  rsync git ansible

# 生成公私钥
docker exec ops-jenkins ssh-keygen -t rsa
# 配置SSH免密,因为Ansible在容器中,所以进入到容器中执行,Ansible执行需要
docker exec -it /bin/bash
# 执行如下命令,输入yes回车,输入root密码
ssh-copy-id root@10.0.1.222
ssh-copy-id root@10.0.1.223
ssh-copy-id root@10.0.1.224

查看jenkins密码

docker exec ops-jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# 密码如下
f52367de922d43a79850b82973447b23

访问http://10.0.1.225:8080,输入jenkins密码

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_08

接下来安装推荐的插件,如果有安装失败的插件,重试即可。安装完成后配置jenkins管理员密码(账号:admin,密码:admin123)

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_09

进入【系统管理】-【插件管理】-【可选插件】,安装以下使用到的插件(如果没有搜索到说明已经安装了):

  • 活跃选择参数插件:Active Choices
  • 修改构建名称和描述插件:Build Name and Description Setter
  • 构建的用户变量插件:build user vars
  • 扩展选择参数插件:Extended Choice Parameter
  • Git参数插件:Git Parameter
  • GitLab插件:GitLab
  • 汉化插件:Localization: Chinese (Simplified)
  • 持久化参数插件:Persistent Parameter
  • Job修改历史插件:Job Configuration History
  • 构建后操作,根据构建结果执行shell或批处理插件:Post build task

对接Gitlab

进入【系统管理】-【系统配置】-【Git plugin】,配置如下图所示

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_10

  • Global Config user.name Value:root
  • Global Config user.email Value:root@zhushiyang.net

添加ops-gitlab凭据,

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_11

  • 范围:全局 (Jenkins, nodes, items, all child items, etc)
  • 用户名:root(gitlab root用户)
  • 密码:root1234(gitlab root用户密码)
  • ID:ops-gitlab
  • 描述:ops-gitlab

至此Jenkins安装基础环境完成。

安装openresty

在ops-node-01、ops-node-02和ops-node-03上分别构建镜像。使用openresty官方的Dockerfile构建镜像为模板进行简单调整,添加alpine的国内源,编译upsync模块和check_module模块,然后使用构建的镜像启动。

先在ops-node-01上执行

mkdir /root/openresty && cd /root/openresty
# 如果git clone失败,直接到github上下载
git clone https://github.com/msaf1980/nginx_upstream_check_module
git clone https://github.com/weibocom/nginx-upsync-module

# 手动下载上传到/root/openresty目录下解压
unzip nginx_upstream_check_module-master.zip
unzip nginx-upsync-module-master.zip
mv nginx_upstream_check_module-master nginx_upstream_check_module
mv nginx-upsync-module-master nginx-upsync-module

Dockerfile文件的第50-51行添加了nginx模块,85行替换了安装源,145行打check_module补丁。创建Dockerfile,执行vim Dockerfile写入下面的内容

# Dockerfile - alpine
# https://github.com/openresty/docker-openresty

ARG RESTY_IMAGE_BASE="alpine"
ARG RESTY_IMAGE_TAG="3.12"

FROM ${RESTY_IMAGE_BASE}:${RESTY_IMAGE_TAG}

LABEL maintainer="Evan Wies <evan@neomantra.net>"

# Docker Build Arguments
ARG RESTY_IMAGE_BASE="alpine"
ARG RESTY_IMAGE_TAG="3.12"
ARG RESTY_VERSION="1.17.8.2"
ARG RESTY_OPENSSL_VERSION="1.1.1g"
ARG RESTY_OPENSSL_PATCH_VERSION="1.1.1f"
ARG RESTY_OPENSSL_URL_BASE="https://www.openssl.org/source"
ARG RESTY_PCRE_VERSION="8.44"
ARG RESTY_J="1"
ARG RESTY_CONFIG_OPTIONS="\
     --with-compat \
     --with-file-aio \
     --with-http_addition_module \
     --with-http_auth_request_module \
     --with-http_dav_module \
     --with-http_flv_module \
     --with-http_geoip_module=dynamic \
     --with-http_gunzip_module \
     --with-http_gzip_static_module \
     --with-http_image_filter_module=dynamic \
     --with-http_mp4_module \
     --with-http_random_index_module \
     --with-http_realip_module \
     --with-http_secure_link_module \
     --with-http_slice_module \
     --with-http_ssl_module \
     --with-http_stub_status_module \
     --with-http_sub_module \
     --with-http_v2_module \
     --with-http_xslt_module=dynamic \
     --with-ipv6 \
     --with-mail \
     --with-mail_ssl_module \
     --with-md5-asm \
     --with-pcre-jit \
     --with-sha1-asm \
     --with-stream \
     --with-stream_ssl_module \
     --with-threads \
     --add-module=/root/nginx_upstream_check_module \
     --add-module=/root/nginx-upsync-module \
"
ARG RESTY_CONFIG_OPTIONS_MORE=""
ARG RESTY_LUAJIT_OPTIONS="--with-luajit-xcflags='-DLUAJIT_NUMMODE=2 -DLUAJIT_ENABLE_LUA52COMPAT'"

ARG RESTY_ADD_PACKAGE_BUILDDEPS=""
ARG RESTY_ADD_PACKAGE_RUNDEPS=""
ARG RESTY_EVAL_PRE_CONFIGURE=""
ARG RESTY_EVAL_POST_MAKE=""

# These are not intended to be user-specified
ARG _RESTY_CONFIG_DEPS="--with-pcre \
    --with-cc-opt='-DNGX_LUA_ABORT_AT_PANIC -I/usr/local/openresty/pcre/include -I/usr/local/openresty/openssl/include' \
    --with-ld-opt='-L/usr/local/openresty/pcre/lib -L/usr/local/openresty/openssl/lib -Wl,-rpath,/usr/local/openresty/pcre/lib:/usr/local/openresty/openssl/lib' \
"

LABEL resty_image_base="${RESTY_IMAGE_BASE}"
LABEL resty_image_tag="${RESTY_IMAGE_TAG}"
LABEL resty_version="${RESTY_VERSION}"
LABEL resty_openssl_version="${RESTY_OPENSSL_VERSION}"
LABEL resty_openssl_patch_version="${RESTY_OPENSSL_PATCH_VERSION}"
LABEL resty_openssl_url_base="${RESTY_OPENSSL_URL_BASE}"
LABEL resty_pcre_version="${RESTY_PCRE_VERSION}"
LABEL resty_config_options="${RESTY_CONFIG_OPTIONS}"
LABEL resty_config_options_more="${RESTY_CONFIG_OPTIONS_MORE}"
LABEL resty_config_deps="${_RESTY_CONFIG_DEPS}"
LABEL resty_add_package_builddeps="${RESTY_ADD_PACKAGE_BUILDDEPS}"
LABEL resty_add_package_rundeps="${RESTY_ADD_PACKAGE_RUNDEPS}"
LABEL resty_eval_pre_configure="${RESTY_EVAL_PRE_CONFIGURE}"
LABEL resty_eval_post_make="${RESTY_EVAL_POST_MAKE}"

COPY ./nginx_upstream_check_module/ /root/nginx_upstream_check_module
COPY ./nginx-upsync-module/ /root/nginx-upsync-module

RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories \
    && apk add --no-cache --virtual .build-deps \
        build-base \
        coreutils \
        curl \
        gd-dev \
        geoip-dev \
        libxslt-dev \
        linux-headers \
        make \
        perl-dev \
        readline-dev \
        zlib-dev \
        ${RESTY_ADD_PACKAGE_BUILDDEPS} \
    && apk add --no-cache \
        gd \
        geoip \
        libgcc \
        libxslt \
        zlib \
        ${RESTY_ADD_PACKAGE_RUNDEPS} \
    && cd /tmp \
    && if [ -n "${RESTY_EVAL_PRE_CONFIGURE}" ]; then eval $(echo ${RESTY_EVAL_PRE_CONFIGURE}); fi \
    && cd /tmp \
    && curl -fSL "${RESTY_OPENSSL_URL_BASE}/openssl-${RESTY_OPENSSL_VERSION}.tar.gz" -o openssl-${RESTY_OPENSSL_VERSION}.tar.gz \
    && tar xzf openssl-${RESTY_OPENSSL_VERSION}.tar.gz \
    && cd openssl-${RESTY_OPENSSL_VERSION} \
    && if [ $(echo ${RESTY_OPENSSL_VERSION} | cut -c 1-5) = "1.1.1" ] ; then \
        echo 'patching OpenSSL 1.1.1 for OpenResty' \
        && curl -s https://raw.githubusercontent.com/openresty/openresty/master/patches/openssl-${RESTY_OPENSSL_PATCH_VERSION}-sess_set_get_cb_yield.patch | patch -p1 ; \
        fi \
    && if [ $(echo ${RESTY_OPENSSL_VERSION} | cut -c 1-5) = "1.1.0" ] ; then \
        echo 'patching OpenSSL 1.1.0 for OpenResty' \
        && curl -s https://raw.githubusercontent.com/openresty/openresty/ed328977028c3ec3033bc25873ee360056e247cd/patches/openssl-1.1.0j-parallel_build_fix.patch | patch -p1 \
        && curl -s https://raw.githubusercontent.com/openresty/openresty/master/patches/openssl-${RESTY_OPENSSL_PATCH_VERSION}-sess_set_get_cb_yield.patch | patch -p1 ; \
        fi \
    && ./config \
        no-threads shared zlib -g \
        enable-ssl3 enable-ssl3-method \
        --prefix=/usr/local/openresty/openssl \
        --libdir=lib \
        -Wl,-rpath,/usr/local/openresty/openssl/lib \
    && make -j${RESTY_J} \
    && make -j${RESTY_J} install_sw \
    && cd /tmp \
    && curl -fSL https://nchc.dl.sourceforge.net/project/pcre/pcre/8.44/pcre-${RESTY_PCRE_VERSION}.tar.gz -o pcre-${RESTY_PCRE_VERSION}.tar.gz \
    && tar xzf pcre-${RESTY_PCRE_VERSION}.tar.gz \
    && cd /tmp/pcre-${RESTY_PCRE_VERSION} \
    && ./configure \
        --prefix=/usr/local/openresty/pcre \
        --disable-cpp \
        --enable-jit \
        --enable-utf \
        --enable-unicode-properties \
    && make -j${RESTY_J} \
    && make -j${RESTY_J} install \
    && cd /tmp \
    && curl -fSL https://openresty.org/download/openresty-${RESTY_VERSION}.tar.gz -o openresty-${RESTY_VERSION}.tar.gz \
    && tar xzf openresty-${RESTY_VERSION}.tar.gz \
    && cd /tmp/openresty-${RESTY_VERSION}/bundle/nginx-1.17.8 \ 
    && patch -p1 < /root/nginx_upstream_check_module/check_1.16.1+.patch \
    && cd /tmp/openresty-${RESTY_VERSION} \
    && eval ./configure -j${RESTY_J} ${_RESTY_CONFIG_DEPS} ${RESTY_CONFIG_OPTIONS} ${RESTY_CONFIG_OPTIONS_MORE} ${RESTY_LUAJIT_OPTIONS} \
    && make -j${RESTY_J} \
    && make -j${RESTY_J} install \
    && cd /tmp \
    && if [ -n "${RESTY_EVAL_POST_MAKE}" ]; then eval $(echo ${RESTY_EVAL_POST_MAKE}); fi \
    && rm -rf \
        openssl-${RESTY_OPENSSL_VERSION}.tar.gz openssl-${RESTY_OPENSSL_VERSION} \
        pcre-${RESTY_PCRE_VERSION}.tar.gz pcre-${RESTY_PCRE_VERSION} \
        openresty-${RESTY_VERSION}.tar.gz openresty-${RESTY_VERSION} \
    && apk del .build-deps \
    && mkdir -p /var/run/openresty \
    && ln -sf /dev/stdout /usr/local/openresty/nginx/logs/access.log \
    && ln -sf /dev/stderr /usr/local/openresty/nginx/logs/error.log \
    && rm -rf /root/ngx_healthcheck_module

# Add additional binaries into PATH for convenience
ENV PATH=$PATH:/usr/local/openresty/luajit/bin:/usr/local/openresty/nginx/sbin:/usr/local/openresty/bin

# Copy nginx configuration files

CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]

# Use SIGQUIT instead of default SIGTERM to cleanly drain requests
# See https://github.com/openresty/docker-openresty/blob/master/README.md#tips--pitfalls
STOPSIGNAL SIGQUIT

构建镜像,生产环境需要把镜像上传到阿里云或者内部的镜像仓库中,一次构建即可。现在的环境中可以在ops-node-01节点导出镜像并拷贝到ops-node-02和ops-node-03两个节点。

docker build -t openresty:1.17.8.2-alpine-upsync_check .
# 导出镜像拷贝到ops-node-02和ops-node-03两个节点
docker save -o openresty.tar openresty:1.17.8.2-alpine-upsync_check
scp openresty.tar.gz root@10.0.1.223:/root
scp openresty.tar.gz root@10.0.1.224:/root
# 在ops-node-02和ops-node-03两个节点执行
docker load -i /root/openresty.tar

现在ops-node-01、ops-node-02和ops-node-03三个节点都生成了openresty:1.17.8.2-alpine-upsync_check镜像。

下载示例配置文件到三个节点的/root/docker/nginx-volume/conf目录下,示例文件地址:https://github.com/zhushiyang/nginx_config.git,说明见Nginx规划部分。这里的配置只是初始化的,接下来会通过Jenkins统一进行管理。

# ops-node-01、ops-node-02和ops-node-03三个节点都执行
mkdir -p /root/docker/nginx-volume/conf
cd /root/docker/nginx-volume/conf
git clone https://github.com/zhushiyang/nginx_config.git
mv nginx_config/* ./
rm -rf nginx_config
# 文件web_cluster.conf不是本次的重点,删除掉
rm -f upstreams/web_cluster.conf

在ops-node-01、ops-node-02和ops-node-03三个节点启动openresty

docker run -d --restart=always --hostname ops-openresty --name ops-openresty --network=host \
-v /root/docker/nginx-volume/conf:/usr/local/openresty/nginx/conf \
-v /root/docker/nginx-volume/logs:/var/log/nginx \
-v /etc/localtime:/etc/localtime:ro openresty:1.17.8.2-alpine-upsync_check

启动nginx容器后,查看容器日志汇报下面的错误,可暂时先忽略,是因为etcd的地址不对导致的

2022/08/26 15:48:25 [error] 10#10: recv() failed (111: Connection refused)
2022/08/26 15:48:25 [error] 10#10: upsync_recv: recv error with upstream: "web_cluster_upsync"
2022/08/26 15:48:25 [error] 7#7: recv() failed (111: Connection refused)
2022/08/26 15:48:25 [error] 7#7: upsync_recv: recv error with upstream: "web_cluster_upsync"
2022/08/26 15:48:25 [error] 8#8: recv() failed (111: Connection refused)
2022/08/26 15:48:25 [error] 8#8: upsync_recv: recv error with upstream: "web_cluster_upsync"
2022/08/26 15:48:25 [error] 9#9: recv() failed (111: Connection refused)
2022/08/26 15:48:25 [error] 9#9: upsync_recv: recv error with upstream: "web_cluster_upsync"
2022/08/26 15:48:25 [error] 8#8: send() failed (111: Connection refused)
2022/08/26 15:48:26 [error] 7#7: recv() failed (111: Connection refused)
2022/08/26 15:48:26 [error] 7#7: upsync_recv: recv error with upstream: "web_cluster_upsync"
2022/08/26 15:48:26 [error] 10#10: recv() failed (111: Connection refused)
2022/08/26 15:48:26 [error] 10#10: upsync_recv: recv error with upstream: "web_cluster_upsync"

启动验证,curl访问后,返回nginx的状态监控数据说明是正常的

curl http://10.0.1.222/stub_status
curl http://10.0.1.223/stub_status
curl http://10.0.1.224/stub_status

环境配置

配置gitlab

创建Nginx分组

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_12

在Nginx分组下创建Nginx-Prod-L4仓库(不要勾选【使用自述文件初始化仓库】)

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_13

在Nginx分组下创建Nginx-Prod-L7仓库(不要勾选【使用自述文件初始化仓库】)

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_14

最后的仓库布局如下

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_15

将Nginx的示例文件上传到两个仓库中,因为示例配置已经在ops-node-01的/root/docker/nginx-volume/conf目录下存在,所以在ops-node-01上执行即可

mkdir /root/nginx/ && cd /root/nginx/
# 如果你修改的gitlab的密码这里将root1234替换即可
git clone http://root:root1234@10.0.1.225/nginx/nginx-prod-l7.git
git clone http://root:root1234@10.0.1.225/nginx/nginx-prod-l4.git

cp -rf /root/docker/nginx-volume/conf/* nginx-prod-l4
cp -rf /root/docker/nginx-volume/conf/* nginx-prod-l7

# 设置gitlab的邮箱和用户
git config --global user.email "root@zhushiyang.net"
git config --global user.name "root"

cd /root/nginx/nginx-prod-l4
git add --all
git commit -m '初始化Nginx配置'
git push -u origin master

cd /root/nginx/nginx-prod-l7
git add --all
git commit -m '初始化Nginx配置'
git push -u origin master

配置jenkins

注意:jenkins的所有的job配置文件全部已经打包好,下载文件后放到任意位置并加压到/root/docker/jenkins-volume/jobs目录下,然后重启jenkins即可,但是强烈建议手动配置一遍,增加下印象,在ops-node-04上执行。

git clone https://github.com/zhushiyang/nginx-prod-l7.git
cp -rf nginx-prod-l7 /root/docker/jenkins-volume/jobs/
docker restart jenkins

下面是手动配置的过程,建议按照自定义配置走一遍,以此来熟悉每个配置的作用。

创建Folder,名称是Nginx-Prod-L7

下面是手动配置的过程,建议按照自定义配置走一遍,以此来熟悉每个配置的作用。

创建Folder,名称是Nginx-Prod-L7

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_16

创建Folder,名称是Nginx-Prod-L4

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_17

Jenkins布局如下

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_18

进入到Nginx-Prod-L7目录下,创建如下格式的job:

  • configure_deploy:下发配置文件(job类型【自由风格的软件项目】)
  • nginx.conf:更新nginx.conf文件到gitlab(job类型【自由风格的软件项目】)
  • proxy.conf:更新proxy.conf文件到gitlab(job类型【自由风格的软件项目】)
  • upstreams:维护upstream,每个upstream是一个job,命名格式是upstream名称,不需要带.conf后缀,会把配置推送到gitlab(job类型【文件夹】)
  • vhosts:维护虚拟主机,每个虚拟主机是一个job,命名格式是域名,不需要带.conf后缀,会把配置推送到gitlab(job类型【文件夹】)

job是按照nginx的配置文件来划分的,原则是经常修改的放到jenkins上进行管理,不修改的无需配置。

Nginx-Prod-L7-nginx.conf配置

任务名称:nginx.conf

任务类型:构建自由风格的软件项目

描述:Nginx主配置文件

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_19

丢弃旧的构建:这里构建历史保留10个,可以按照实际情况修改,多点,少点都没关系。

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_20

参数化构建:试过文本参数,不过不理想,在编辑配置提交后,每次在输入框里显示都是默认配置,而我希望的是在修改配置后,可以在上一次的基础上继续进行修改。jenkins没有从workspace中读取文件显示在文本框里面的插件,后来发现【Persistent Text Parameter】插件可以满足要求,原理是从上一次构建记录中读取对应的参数到当前构建中。比如,我设置的APP_NAME参数的默认值是1,上一次我把APP_NAME的值改为了2,那么在新的构建中,APP_NAME的值就是2

参数类型:Persistent Text Parameter

Name:NGINX_PROCESS_EVENT_CONFIG

Default Value:

# nginx work进程用户
user   nobody;
# 根据cpu的核数自动生成work进程树
worker_processes   auto;
# 启用多核CPU;
worker_cpu_affinity   auto;
# work进程打开最大文件数
worker_rlimit_nofile   65536;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
#error_log  "pipe:rollback logs/error_log interval=1d baknum=7 maxsize=2G";


pid   /var/run/nginx.pid;


# event context
events {
  # 默认开启,解决惊群问题
  accept_mutex   on;
  # 使用epoll
  use   epoll;
  # 每个进程最大连接数(最大连接=连接数x进程数)
  worker_connections   20480;
}

Description:Nginx进程和事件模型配置

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_21

在添加一个Persistent Text Parameter

Name:NGINX_HTTP_CONFIG

Default Value:

# 七层负载均衡
http {
   # 隐藏nginx版本号
   server_tokens off; 
   # 文件扩展名与文件类型映射表
   include   mime.types;
   log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';

   #access_log  logs/access.log  main;
   #access_log  "pipe:rollback logs/access_log interval=1d baknum=7 maxsize=2G"  main;
   
   # 调整hash表,解决绑定域名过多问题
   server_names_hash_max_size  4096;
   server_names_hash_bucket_size   1024;
   variables_hash_max_size 4096;
   variables_hash_bucket_size 2048;
   # 默认文件类型
   default_type   text/html;
   # 设置编码格式utf-8
   charset   utf-8;
   # 当请求头部中有下划线时,认为请求有效
   underscores_in_headers   on;

   # 防止网络阻塞
   tcp_nopush   on;
   # 输出缓冲区
   tcp_nodelay   on;
   # 输出拆包大小
   output_buffers   4 32k;
   postpone_output   1460;
   # 缓冲区代理缓冲用户端请求的最大字节数
   client_header_buffer_size   128k;
   large_client_header_buffers   4 256k;
   # 客户端发送header超时
   client_body_timeout   30;
   # 发送到客户端超时
   send_timeout   30;
   # 注意:如果图片显示不正常把这个改成off。
   sendfile   on;
   # keepalive_timeout  60;
   keepalive_timeout   60;

   # 开启gzip压缩输出
   gzip   on;
   # 最小压缩文件大小
   gzip_min_length   1k;
   # 处理请求压缩的缓冲区数量和大小
   gzip_buffers   4 16k;
   # 设置健康检查共享内存
   check_shm_size 20M;  
   # 压缩类型,默认就已经包含textml,所以下面就不用再写了,写上去也不会有问题,但是会有一个warn
   gzip_types   text/plain application/x-javascript text/css application/xml;
   # 压缩比率,1代表压缩比最小处理速度最快,9压缩比最大但处理速度最慢(传输快但比较消耗cpu)
   gzip_comp_level   9;
   # 压缩通过代理的所有文件
   gzip_proxied   any;
   # vary header支持
   gzip_vary   on;
  
   include   proxy.conf;
   # server配置
   server {  
       listen 80 backlog=65535;
       # listen 8080 backlog=65535;
       # listen 443 ssl backlog=65535;
       # ssl_certificate ./ssl/xxx/xxx.crt;
       # ssl_certificate_key ./ssl/xxx/xxx.key;
       root   /dev/null;
       server_name   _;
       # nginx状态信息
       location = /stub_status {
           stub_status;
        }
       # upstream健康检查信息
       location = /check_status {
           check_status;
           access_log off;
           error_log off;
           # allow SOME.IP.ADD.RESS;
           # deny all;
       }
       # upsync show
       location /upstream_list {
           upstream_show;
       }
   }
   # 虚拟主机配置目录
   include vhosts/*.conf;
   # upstreams配置目录
   include upstreams/*.conf; 
}

Description:Nginx的http配置

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_22

源码管理:Git

Repository URL:http://10.0.1.225/nginx/nginx-prod-l7.git

Credentials:选择ops-gitlab

高级里面的Name:nginx-prod-l7(因为配置修改后,需要把修改推送到master分支,同时创建tag,jenkins的Git publish插件就是通过Git插件中的Name进行关联的)

Branches to build:*/master

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_23

构建环境:

Delete workspace before starts:在构建前清理workspace

Add timestamps to the Console Output:构建日志中显示时间戳

Set jenkins user build variables:设置jenkins的用户变量,在构建时可以通过${BUILD_USER}记录当前的构建用户

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_24

构建:执行 shell

脚本的内容比较简单,生产环境可以优化,添加报警功能等内容,简化版本的脚本如下

# 加锁,防止多人同时修改
if [ ! -f /tmp/${JOB_BASE_NAME}.lock ];then
    echo "> ==========> 进行加锁 <=========="
    echo "" > /tmp/${JOB_BASE_NAME}.lock
else
    echo "==========> 不能多人同时修改该文件,请稍后修改 <=========="
    exit 1
fi
echo "==========> 正在生成配置文件 <=========="
echo -e "${NGINX_PROCESS_EVENT_CONFIG}\n${NGINX_HTTP_CONFIG}" > ${JOB_BASE_NAME}
echo "==========> 正在校验文件变化 <=========="
# 时间戳,格式:年月日时分-202208100510
DATE_TIME=$(date +%Y%m%d%H%M)
# git status结果为0,说明有修改,其它结果说明没有修改
CHECK_RELT=$(git status ${JOB_BASE_NAME}|grep ${JOB_BASE_NAME}>/dev/null ;echo $?)
if [  ${CHECK_RELT} = "0" ];then
    git add --all
    git commit -m "Update ${JOB_BASE_NAME} version to ${JOB_BASE_NAME}-${DATE_TIME}-${BUILD_NUMBER} by ${BUILD_USER}"
else
    echo "==========> 文件内容无变化 <=========="
    exit 1
fi

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_25

构建后操作:Git Publisher

勾选:Push Only If Build Succeeds,只有成功时才提交tag

勾选:Merge Results,合并结果

Add Tag:

  • Tag to push:${JOB_BASE_NAME}-${BUILD_USER}-${BUILD_NUMBER},设置tag格式job名称-构建用户-构建顺序,如:nginx.conf-admin-1
  • 勾选:Create new tag,每次构建都创建新的tag,便于配置的更新和回滚
  • Target remote name:nginx-prod-l7,关联指定Name为nginx-prod-l7的配置

Add Branches:

  • Branch to push:master,文件更新后同步更新的分支
  • Target remote name:nginx-prod-l7,关联指定Name为nginx-prod-l7的配置

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_26

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_27

构建后操作:Post build task,主要使用Script,执行删除锁文件

Script内容如下:

echo "==========> 删除锁文件 <=========="
rm -f /tmp/${JOB_BASE_NAME}.lock

运行该job,先按照默认参数执行,默认的参数删除了示例配置中的stream配置

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_28

然后再构建一次,在NGINX_PROCESS_EVENT_CONFIG的输入框的第一行,添加一行注释# 再次构建

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_29

查看Nginx-Prod-L7的git仓库,发现nginx.conf配置已经更新

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_30

生成的tag

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_31

然后再点击一次构建(不要执行,这一次只是为了看下参数的内容,由于我一开始调试了一次,所以我的构建数是3,你们的应该是2),上次的修改记录保存下来了

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_32

第3次构建的日志保存在/root/docker/jenkins-volume/jobs/Nginx-Prod-L7/jobs/nginx.conf/builds/3/build.xml文件中,jenkins的Persistent Text Parameter参数就是读取该文件相同变量的值。

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_33

Nginx-Pord-L7-proxy.conf配置

proxy.conf定义的是全局配置反向代理配置文件,偶尔会有调整。新建jenkins的job,因为proxy.conf和nginx.conf的配置大同小异,所以从nginx.conf进行复制

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_34

要修改的配置如下

描述:修改为Nginx的proxy.conf文件

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_35

参数:删除所有的Persistent Text Parameter,然后再添加Persistent Text Parameter,设置Name为NGINX_PROXY_CONFIG,内容如下

proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header REMOTE_ADDR $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header RealIP $remote_addr;
proxy_set_header Host $host;
proxy_set_header Connection "";
proxy_set_header host-name $hostname;
proxy_pass_header Server;

client_max_body_size    30m;
proxy_connect_timeout   120s;
proxy_send_timeout      120s;
proxy_read_timeout      120s;
proxy_temp_file_write_size 1024m;
proxy_buffer_size               32k;
proxy_buffers                   4 32k;
proxy_busy_buffers_size 64k;

proxy_ignore_client_abort on;

#失败不重试
proxy_next_upstream_tries  1;

Description:修改为Nginx的代理配置

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_36

构建:执行shell修改内容如下

if [ ! -f /tmp/${JOB_BASE_NAME}.lock ];then
    echo "> ==========> 进行加锁 <=========="
    echo "" > /tmp/${JOB_BASE_NAME}.lock
else
    echo "==========> 不能多人同时修改该文件,请稍后修改 <=========="
    exit 1
fi
echo "==========> 正在生成配置文件 <=========="
echo -e "${NGINX_PROXY_CONFIG}" > ${JOB_BASE_NAME}
echo "==========> 正在校验文件变化 <=========="
# 时间戳,格式:年月日时分-202208100510
DATE_TIME=$(date +%Y%m%d%H%M)
# git status结果为0,说明有修改,其它结果说明没有修改
CHECK_RELT=$(git status ${JOB_BASE_NAME}|grep ${JOB_BASE_NAME}>/dev/null ;echo $?)
if [  ${CHECK_RELT} = "0" ];then
    git add --all
    git commit -m "Update ${JOB_BASE_NAME} version to ${JOB_BASE_NAME}-${DATE_TIME}-${BUILD_NUMBER} by ${BUILD_USER}"
else
    echo "==========> 文件内容无变化 <=========="
    exit 1
fi

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_37

其它的配置无需修改了,直接保存即可。点击构建,在输入框的第一行添加内容【测试proxy.conf构建】

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_38

构建成功后,查看Nginx-Pord-L7仓库的proxy.conf文件内容

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_39

生成的tag

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_40

Nginx-Prod-L7-upstreams配置

这里的配置用来管理nginx的upstream,每个去掉.conf后缀的upstream文件名作为job名称。

在Nginx-Prod-L7目录下,创建Job,类型是文件夹,名称是upstreams。构建后,最终会在upstreams目录下生成${JOB_BASE_NAME}.conf文件。

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_41

因为job的配置和其他配置大致相同,可以使用jenkins的job复制功能。jenkins的job无法跨文件夹进行复制,所以可以先把proxy.conf的job移动到upstreams文件夹下,用于复制需要。

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_42

选择移动到upstreams的目录

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_43

移动完成后,切换到upstreams目录,可以看到proxy.conf文件已经移动过来,现在新建job,名称是web_cluster_upsync,复制源是proxy.conf

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_44

要修改的内容如下

描述:修改为Nginx的upstream配置文件

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_45

构建参数:将Persistent Text Parameter的Name修改为NGINX_UPSTREAM_CONFIG,默认值如下(在使用时去掉下面的注释)

# ${JOB_BASE_NAME}对的值就是job的名称,这里是web_cluster_upsync
upstream ${JOB_BASE_NAME} {
    # upsync在首次配置时,必须包含一个server,不然nginx会没有server的错误
    server 127.0.0.1:80 weight=1 max_fails=2 fail_timeout=10;
    # 这里是upsync的具体配置
    # etcd的api:10.0.1.224:2379/v2/keys/upstreams/${JOB_BASE_NAME}
    # 超时时间6分钟:upsync_timeout=6m
    # 同步间隔500ms:upsync_interval=500ms
    # 使用的外部存储是etcd:upsync_type=etcd
    # 每次nginx启动时,是否会重新从etcd拉取数据,这里关闭:strong_dependency=off
    upsync 10.0.1.224:2379/v2/keys/upstreams/${JOB_BASE_NAME} upsync_timeout=6m upsync_interval=500ms upsync_type=etcd strong_dependency=off;
    # 用来持久化upstream的文件路径,当etcd集群异常时,会从本地文件中持久化的内容,继续提供服务
    upsync_dump_path /usr/local/openresty/nginx/conf/dump_upstreams/${JOB_BASE_NAME}.conf;

    # http_check健康检查配置
    check interval=3000 rise=2 fall=5 timeout=1000 type=http;
    check_keepalive_requests 1;
    check_http_send "HEAD / HTTP/1.0\r\n\r\n";
    check_http_expect_alive http_2xx http_3xx;
}

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_46

构建:执行shell的内容如下,内容比较简单,替换了变量,对upstreams目录是否存在进行校验

if [ ! -f /tmp/${JOB_BASE_NAME}.lock ];then
    echo "> ==========> 进行加锁 <=========="
    echo "" > /tmp/${JOB_BASE_NAME}.lock
else
    echo "==========> 不能多人同时修改该文件,请稍后修改 <=========="
    exit 1
fi
echo "==========> 正在生成配置文件 <=========="
[ ! -d ./upstreams ] && mkdir ./upstreams
echo "${NGINX_UPSTREAM_CONFIG}" > ./upstreams/${JOB_BASE_NAME}.conf
echo "==========> 正在校验文件变化 <=========="
# 时间戳,格式:年月日时分-202208100510
DATE_TIME=$(date +%Y%m%d%H%M)
# git status结果为0,说明有修改,其它结果说明没有修改
CHECK_RELT=$(git status ./upstreams/${JOB_BASE_NAME}.conf|grep ${JOB_BASE_NAME}.conf>/dev/null ;echo $?)
if [  ${CHECK_RELT} = "0" ];then
    git add --all
    git commit -m "Update ${JOB_BASE_NAME} version to ${JOB_BASE_NAME}-${DATE_TIME}-${BUILD_NUMBER} by ${BUILD_USER}"
else
    echo "==========> 文件内容无变化 <=========="
    exit 1
fi

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_47

其他内容不变,保存即可。点击构建,在输入框的第一行添加内容【测试upstream构建】

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_48

构建成功后在git仓库Nginx-Prod-L7的upstreams目录下查看生成的web_cluster_upsync.conf配置文件,模板的变量也已经替换为期望的值

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_49

然后,把proxy.conf在移动回去

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_50

Nginx-Prod-L7-vhosts配置

这里的配置用来管理nginx的虚拟主机配置,每个去掉.conf后缀的vhosts文件名作为job名称。

在Nginx-Prod-L7目录下,创建Job,类型是文件夹,名称是vhosts。构建后,最终会在vhosts目录下生成${JOB_BASE_NAME}.conf文件。

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_51

因为job的配置和其他配置大致相同,可以使用jenkins的job复制功能。jenkins的job无法跨文件夹进行复制,所以可以先把proxy.conf的job移动到vhosts文件夹下,用于复制需要。

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_52

移动完成后,切换到vhosts目录,可以看到proxy.conf文件已经移动过来,现在新建job,名称是www.zhushiyang.net,复制源是proxy.conf

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_53

修改的内容如下

描述:修改为Nginx的vhosts文件

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_54

构建参数:将Persistent Text Parameter的Name修改为NGINX_VHOSTS_CONFIG,默认值如下(在使用时去掉下面的注释)

#server {
#    listen 80;
#    server_name ${JOB_BASE_NAME};
#    return 301 https://$server_name$request_uri;
#}

server{
  listen 80 ;
  listen 443 ssl;
  server_name  ${JOB_BASE_NAME};
    
    access_log  /var/log/nginx/${JOB_BASE_NAME}.access.log  main;
  
  ssl_certificate      ./certs/zhushiyang.net.pem;
  ssl_certificate_key  ./certs/zhushiyang.net.key;
  ssl_session_timeout 1d;
  ssl_session_cache shared:MozSSL:10m;
  ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
  ssl_prefer_server_ciphers off;
  ssl_protocols TLSv1.1 TLSv1.2;
  add_header Strict-Transport-Security "max-age=63072000" always;
  
  
  # www主程序
  location / {
    # 支持upsync
    proxy_pass http://web_cluster_upsync;
  }
  
}

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_55

构建:执行shell,修改为如下内容

if [ ! -f /tmp/${JOB_BASE_NAME}.lock ];then
    echo "> ==========> 进行加锁 <=========="
    echo "" > /tmp/${JOB_BASE_NAME}.lock
else
    echo "==========> 不能多人同时修改该文件,请稍后修改 <=========="
    exit 1
fi
echo "==========> 正在生成配置文件 <=========="
[ ! -d ./vhosts ] && mkdir ./upstreams
echo "${NGINX_VHOSTS_CONFIG}" > ./vhosts/${JOB_BASE_NAME}.conf
echo "==========> 正在校验文件变化 <=========="
# 时间戳,格式:年月日时分-202208100510
DATE_TIME=$(date +%Y%m%d%H%M)
# git status结果为0,说明有修改,其它结果说明没有修改
CHECK_RELT=$(git status ./vhosts/${JOB_BASE_NAME}.conf|grep ${JOB_BASE_NAME}.conf>/dev/null ;echo $?)
if [  ${CHECK_RELT} = "0" ];then
    git add --all
    git commit -m "Update ${JOB_BASE_NAME} version to ${JOB_BASE_NAME}-${DATE_TIME}-${BUILD_NUMBER} by ${BUILD_USER}"
else
    echo "==========> 文件内容无变化 <=========="
    exit 1
fi

其他内容不变,保存即可。点击构建,在输入框的第一行,添加【# 测试vhosts构建】

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_56

构建成功后,在git仓库Nginx-Prod-L7的vhosts目录下查看www.zhushiyang.net.conf文件内容

nginx更新前duan包需要重启吗 nginx动态更新配置_负载均衡_57

然后,把proxy.conf在移动回去

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_58

Nginx-Prod-L7-configure_deploy配置

前面配的jenkins任务都是本地修改配置后推送到gitlab,并没有将nginx的配置文件分发到nginx的主机上,接下来要配置的job就是实现配置分发功能。

在Nginx-Pord-L7文件夹下新建名称为configure_deploy的自由风格软件项目。

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_59

描述:Nginx配置文件下发

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_60

丢弃旧的构建:设置为10

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_61

参数:Git 参数

名称:NGINX_GIT_TAGS

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_62

参数:Extended Choice Parameter

Name:NGINX_HOSTS

选择Basic Parameter Types

  • Parameter Type:Check Boxes
  • Number of Visible items:5
  • Delimiter:,
  • 点击Choose Source for Value

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_63

Git:

Repository URL:http://10.0.1.225/nginx/nginx-prod-l7.git

Credentials:选择添加的ops-gitlab

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_64

构建环境:

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_65

构建:执行shell,写入的内容如下

echo "==========> 正在下发配置文件 <=========="
ansible -i /root/hosts ${NGINX_HOSTS} -m synchronize -a 'src=./  dest=/root/docker/nginx-volume/conf delete=yes copy_links=yes rsync_opts=--exclude=.git'
CHECK_RELT=$(ansible -i /root/hosts ${NGINX_HOSTS} -m shell -a "docker exec ops-openresty nginx -t" > /dev/null;echo $?)
if [ ${CHECK_RELT} != "0" ];then
    echo "Nginx check configuration failed!"
    exit 1
else
    ansible -i /root/hosts ${NGINX_HOSTS} -m shell -a "docker exec ops-openresty nginx -s reload"
fi

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_66

保存后,点击构建,选择一个tag和要更新nginx配置的主机

nginx更新前duan包需要重启吗 nginx动态更新配置_运维_67

点击开始构建,查看构建日志,出现文件同步且nginx reload成功说明配置下发成功。

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx_68

配置etcd

Etcd集群使用nginx的四层负载对外,在ops-node-03(10.0.1.224)上创建四层负载,监听端口是2379

mkdir /root/docker/nginx-volume/conf/streams
cat > /root/docker/nginx-volume/conf/streams/etcd.zhushiyang.net.conf <<EOF
server {
    listen 2379;
    proxy_pass etcd_cluster;
}
 
upstream etcd_cluster {
    server 10.0.1.225:12379;
    server 10.0.1.225:22379;
    server 10.0.1.225:32379;
}
EOF

docker exec ops-openresty nginx -t
docker exec ops-openresty nginx -s reload

验证通过Nginx代理etcd集群情况,有返回值说明正常

ETCDCTL_API=3 etcdctl --endpoints=http://10.0.1.224:2379 -w table endpoint status
+------------------------+------------------+---------+---------+-----------+-----------+------------+
|        ENDPOINT        |        ID        | VERSION | DB SIZE | IS LEADER | RAFT TERM | RAFT INDEX |
+------------------------+------------------+---------+---------+-----------+-----------+------------+
| http://10.0.1.224:2379 | 8fb5ef3dcf4f436c |  3.4.20 |   20 kB |     false |        23 |         44 |
+------------------------+------------------+---------+---------+-----------+-----------+------------+

向ETCD集群中添加upstream名称是web_cluster_upsync的rs,rs分别是10.0.1.225:9091,10.0.1.225:9092,10.0.1.225:9093

curl -X PUT -d value="{\"weight\":1, \"max_fails\":2, \"fail_timeout\":10}" http://10.0.1.224:2379/v2/keys/upstreams/web_cluster_upsync/10.0.1.225:9091
curl -X PUT -d value="{\"weight\":1, \"max_fails\":2, \"fail_timeout\":10}" http://10.0.1.224:2379/v2/keys/upstreams/web_cluster_upsync/10.0.1.225:9092
curl -X PUT -d value="{\"weight\":1, \"max_fails\":2, \"fail_timeout\":10}" http://10.0.1.224:2379/v2/keys/upstreams/web_cluster_upsync/10.0.1.225:9093

在你的电脑上配置hosts,windows系统的hosts文件是C:\Windows\System32\drivers\etc\hosts,添加如下内容

10.0.1.223 www.zhushiyang.net

在浏览器中访问www.zhushiyang.net,你会发现报502错误

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_69

查看http健康检查页面,http://10.0.1.223/check_status,发现三个rs健康检查没有通过,因为我们还没有在10.0.1.225上面启动这三个rs

nginx更新前duan包需要重启吗 nginx动态更新配置_nginx更新前duan包需要重启吗_70

现在,在ops-node-04(10.0.1.225)上,启动三个nginx,作为域名www.zhushiyang.net的后端应用

docker run --name nginx-rs-9091 -p 9091:80 -d --restart=always nginx
docker run --name nginx-rs-9092 -p 9092:80 -d --restart=always nginx
docker run --name nginx-rs-9093 -p 9093:80 -d --restart=always nginx

# 自定义nginx-rs-9091页面
docker exec -it nginx-rs-9091 /bin/bash
echo "nginx-rs-9091" > /usr/share/nginx/html/index.html

# 自定义nginx-rs-9092页面
docker exec -it nginx-rs-9092 /bin/bash
echo "nginx-rs-9092" > /usr/share/nginx/html/index.html

# 自定义nginx-rs-9093页面
docker exec -it nginx-rs-9093 /bin/bash
echo "nginx-rs-9093" > /usr/share/nginx/html/index.html

再次查看健康检查页面,发现三个rs健康检查都已经通过

nginx更新前duan包需要重启吗 nginx动态更新配置_jenkins_71

在浏览器中访问www.zhushiyang.net,现在是正常的响应了。

然后将10.0.1.225:9091的rs从etcd中删除,在查看健康检查页面并访问www.zhushiyang.net查看负载情况

curl -X DELETE -d value="{\"weight\":1, \"max_fails\":2, \"fail_timeout\":10}" http://10.0.1.224:2379/v2/keys/upstreams/web_cluster_upsync/10.0.1.225:9091

 至此,Nginx动态负载均衡和配置管理基本完成。

注意:Nginx-Pord-L4的示例这里就不在列举了,根据实际情况配置即可。

后期维护

动态负载均衡更新rs流程:在新的rs上启动服务-将rs添加到etcd集群中的指定upstream上

配置管理:

  • 新增upstream:在jenkins的upstreams目录下,复制现有的upstream,如果配置模板无变动,只需修改job名称,然后构建提交到git,在下发配置。
  • 新增虚机主机:在jenkins的vhosts目录下,复制现有的vhosts,修改job名称,修改配置,然后构建提交到git,在下发配置。
  • 更新upstream:找到要更新的upstream的job名称,构建后下发配置。
  • 更新虚拟主机:找到要更新的vhosts的job名称,构建后下发配置。

证书更新:

  • 使用jenkins的文件参数来实现,配置很容易

文件删除:

  • 使用活跃选择参数来实现,显示文件列表,通过参数传递实现文件删除

注意:

  • 生产中,下发nginx配置,不要一把梭,先灰度在生产,且生产要分批进行,并通过劫持和日志观察,判断更新的配置是否生效。
  • jenkins的参数的输入框不确定可以支持多少字节,如果nginx的虚拟主机配置几千行,能否正常生成需要验证。

etcd接口:

# 增加和修改rs
curl -X PUT -d value="{\"weight\":1, \"max_fails\":2, \"fail_timeout\":10}" http://etcd_ip:etcd_port/v2/keys/upstreams/upstream名称/rs_ip:rs_port
# 删除rs
curl -X DELETE -d value="{\"weight\":1, \"max_fails\":2, \"fail_timeout\":10}" http://etcd_ip:etcd_port/v2/keys/upstreams/upstream名称/rs_ip:rs_port

数据备份与监控:

  • Gitlab备份与监控
  • Jenkins备份备份与监控
  • Etcd备份备份与监控

监控必须告警,否则无任何意义,这些不是本次的主要内容,不做更多介绍。