背景介绍
A企业产品经过商业广告的精准运营和加速迭代,逐渐适应了市场的需求,成为了消费者出差旅行中必不可少的伙伴。每天的业务高峰和低谷都维持在比较稳定的频率中,但在业务高峰期,经常有研发反馈自己的业务,部分应用的接口有异常现象出现,影响用户体验和收益指标。运维经过排查后,最终发现是Nginx集群下的某些业务实例已经无法访问,却还在负载中接收请求,从而导致异常。
解决方案
通过上面的背景介绍可知,在业务高峰期出现的问题有两个,一是应用实例异常,运维无法第一时间知晓,二是应用实例异常后,无法自动下掉负载。由于所有的业务都通过Nginx集群做流量转发,为解决上述问题,采取如下解决方案:
- 问题一的解决方案:运维开发应用实例健康检查脚本,一旦实例异常将发送消息到飞书告警群。
- 问题二的解决方案:为Nginx添加nginx_upstream_check_module健康检查模块,为业务实例添加健康检查,实例异常后,Nginx会主动踢掉异常实例。
注意:问题一的解决方案依赖于问题二的健康检查功能。
环境搭建
条件:
- 熟悉Nginx的基本配置
- 飞书机器人API使用:开发文档 - 飞书开放平台
基本说明:
- 操作系统:CentOS Linux 7 (Core)
- 业务集群:OpenResty+健康检查模块
- 飞书:飞书告警群,添加群机器人
业务场景:
现有业务app01和app02两个业务,分别使用app01.ops.com和app02.ops.com域名,Nginx虚拟主机配置如下:
app01: app01.ops.com.conf域名配置
# app01服务器组
upstream app01-servers {
server 127.0.0.1:9090;
server 127.0.0.1:9091;
# 每隔3秒进行一次健康检查,重试2次,连续5次失败,超时1秒,http请求
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
# 完成1次请求后即关闭连接
check_keepalive_requests 1;
# 以HEAD方式最小化请求
check_http_send "HEAD / HTTP/1.0\r\n\r\n";
# 返回http 2xx或3xx表示正常
check_http_expect_alive http_2xx http_3xx;
}
server {
listen 80;
server_name app01.ops.com;
location / {
proxy_pass http://app01-servers;
}
}
app02: app02.ops.com.conf域名配置
# app01服务器组
upstream app02-servers {
server 127.0.0.1:9092;
server 127.0.0.1:9093;
# 每隔3秒进行一次健康检查,重试2次,连续5次失败,超时1秒,http请求
check interval=3000 rise=2 fall=5 timeout=1000 type=http;
# 完成1次请求后即关闭连接
check_keepalive_requests 1;
# 以HEAD方式最小化请求
check_http_send "HEAD / HTTP/1.0\r\n\r\n";
# 返回http 2xx或3xx表示正常
check_http_expect_alive http_2xx http_3xx;
}
server {
listen 80;
server_name app02.ops.com;
location / {
proxy_pass http://app02-servers;
}
}
架构图如下:
搭建Nginx集群
注意:这里使用定制的OpenResty镜像(该镜像已经编译好了健康检查模块),且以一个OpenResty实例模拟整个Nginx集群。
镜像地址:registry.cn-hangzhou.aliyuncs.com/op-public/openresty:1.17.8.2-alpine-conf-upsync
准备配置文件:📎conf.tar.gz.doc,下载后去掉.doc后缀,上传到服务器任意目录下。配置文件说明
root@ops-mgr-backup:~# tree docker/nginx-volume/conf/
docker/nginx-volume/conf/
├── certs
├── fastcgi.conf
├── fastcgi.conf.default
├── fastcgi_params
├── fastcgi_params.default
├── koi-utf
├── koi-win
├── mime.types
├── mime.types.default
# 主配置文件进行过优化,同时添加了/stub_status和/check_status检查接口
├── nginx.conf
├── nginx.conf.default
# 反向代理配置文件,已经进行过优化
├── proxy.conf
├── scgi_params
├── scgi_params.default
├── streams
# 两个业务的upstreams配置文件
├── upstreams
│ ├── app01-servers.conf
│ └── app02-servers.conf
├── uwsgi_params
├── uwsgi_params.default
# 两个业务的域名配置文件
├── vhosts
│ ├── app01.ops.com.conf
│ └── app02.ops.com.conf
└── win-utf
创建集群:
mkdir -p /root/docker/nginx-volume/
tar -xf conf.tar.gz -C /root/docker/nginx-volume/
docker run -d --restart=always --hostname ops-service-openresty --name ops-service-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 registry.cn-hangzhou.aliyuncs.com/op-public/openresty:1.17.8.2-alpine-conf-upsync
验证集群是否成功,访问nginx状态检查接口http://IP地址/stub_status和Nginx健康检查接口http://IP地址/check_status?format=csv,返回如下内容说明集群正常
root@ops-mgr-backup:~# curl http://10.0.1.66/stub_status
Active connections: 2
server accepts handled requests
8 8 12
Reading: 0 Writing: 1 Waiting: 1
root@ops-mgr-backup:~# curl http://10.0.1.66/check_status?format=csv
0,app01-servers,127.0.0.1:9090,down,0,47,http,0
1,app01-servers,127.0.0.1:9091,down,0,47,http,0
2,app02-servers,127.0.0.1:9092,down,0,47,http,0
3,app02-servers,127.0.0.1:9093,down,0,47,http,0
也可以使用浏览器访问上面两个健康检查接口
创建业务实例
注意:所有的业务实例,均使用nginx镜像模拟
app01实例组:
# 创建配置文件
mkdir -p /root/docker/app-servers/app01-servers-909{0..1}
echo "app01-servers-9090" > /root/docker/app-servers/app01-servers-9090/index.html
echo "app01-servers-9091" > /root/docker/app-servers/app01-servers-9091/index.html
# 创建app01实例组
docker run -d --restart=always --hostname app01-servers-9090 \
--name app01-servers-9090 \
-p 9090:80 \
-v /root/docker/app-servers/app01-servers-9090/index.html:/usr/share/nginx/html/index.html nginx
docker run -d --restart=always --hostname app01-servers-9091 \
--name app01-servers-9091 \
-p 9091:80 \
-v /root/docker/app-servers/app01-servers-9091/index.html:/usr/share/nginx/html/index.html nginx
# 验证启动情况,返回实例信息表示正常
root@ops-mgr-backup:~# curl http://127.0.0.1:9090/
app01-servers-9090
root@ops-mgr-backup:~# curl http://127.0.0.1:9091/
app01-servers-9091
app02实例组
# 创建配置文件
mkdir -p /root/docker/app-servers/app02-servers-909{2..3}
echo "app02-servers-9092" > /root/docker/app-servers/app02-servers-9092/index.html
echo "app02-servers-9093" > /root/docker/app-servers/app02-servers-9093/index.html
# 创建app02实例组
docker run -d --restart=always --hostname app02-servers-9092 \
--name app02-servers-9092 \
-p 9092:80 \
-v /root/docker/app-servers/app02-servers-9092/index.html:/usr/share/nginx/html/index.html nginx
docker run -d --restart=always --hostname app02-servers-9093 \
--name app02-servers-9093 \
-p 9093:80 \
-v /root/docker/app-servers/app02-servers-9093/index.html:/usr/share/nginx/html/index.html nginx
# 验证启动情况,返回实例信息表示正常
root@ops-mgr-backup:~# curl http://127.0.0.1:9092/
app02-servers-9092
root@ops-mgr-backup:~# curl http://127.0.0.1:9093/
app02-servers-9093
此时访问http://IP地址/check_status,出现如下界面说明环境搭建成功,因为健康检查通过,所以页面变为白色
主机绑Hosts
在你的电脑上为app01.ops.com和app02.ops.com两个域名绑定hosts,假设你的服务器地址是10.0.1.66,在C:\Windows\System32\drivers\etc\hosts文件中追加如下内容
# IP地址根据实际情况替换
10.0.1.66 app01.ops.com app02.ops.com
站点访问验证
在浏览器中访问http://app01.ops.com/和http://app02.ops.com/,会出现端口轮训的响应,说明整个集群工作正常。
访问http://app01.ops.com/轮训结果
访问http://app02.ops.com/轮训结果
飞书群机器人
飞书注册:略,飞书——先进企业协作与管理平台,一站式无缝办公协作,团队上下对齐目标,全面激活组织和个人。先进团队,先用飞书。
创建群组
添加群机器人,名称叫做Ops小助手,记录下最终生成的webhook地址
记录webhook地址:https://open.feishu.cn/open-apis/bot/v2/hook/xxxx
脚本内容
脚本名称:nginx_rs_check.sh
#!/bin/bash
#######################################
# 脚本名称: nginx_rs_check.sh
# 脚本版本: v1.0
# 功能描述: 检测Nginx后端RS实例状态
# 参数说明: sh nginx_rs_check.sh
# 核心逻辑: 调用Nginx的/check_status接口
# 将异常的RS取出来进行报警
# 依赖工具: jq - [Linux处理json工具]
# 脚本作者: shiyang.zhu
# 联系邮箱: zhushiyang@ops.com
# 创建时间: 2023-03-09
# 定时任务: 每分钟执行一次脚本
#######################################
#######################################
# 全局变量:
# SCRIPT_DIR 脚本所在目录
# SCRIPT_NAME 脚本名称
# LOG_DIR 日志目录
# LOG_FILE 日志文件
# CHECK_STATUS Nginx健康检查接口
# DOWN_RS_FILENAME 异常rs信息
# UPSTREAM_NAME upstream名称
# FEISHU_URL 飞书群机器URL,需要进行替换
# APP_MANAGER 业务负责人
# TIMENOW 当前时间
#######################################
SCRIPT_DIR=$(dirname $(readlink -f "$0"))
SCRIPT_NAME="$0"
LOG_DIR=/var/log/shell/${SCRIPT_NAME}
LOG_FILE=${LOG_DIR}/$(date +%Y-%m-%d).log
CHECK_STATUS='http://127.0.0.1/check_status?format=csv'
DOWN_RS_FILENAME="upstream_rs_down.ngx"
UPSTREAM_NAME="upstream.ngx"
FEISHU_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxx"
APP_MANAGER="张三"
[ ! -d ${LOG_DIR} ] && mkdir -p ${LOG_DIR}
# 错误日志函数
function err_log() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: [ERROR] $@" |tee -a ${LOG_FILE}
exit 1
}
# 正常日志函数
function log(){
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: [INFO] $@" |tee -a ${LOG_FILE}
}
#######################################
# 发送消息到飞书告警群
# 局部变量:
# MSG_DOMAIN_NAME: 业务域名
# MSG_UPSTREAM: upstream名称
# MSG_UPSTREAM_RS: 异常RS列表
# SEND_RESULT_CODE: API返回结果0表示正常
#######################################
function sendFeishuMsg(){
local MSG_DOMAIN_NAME="$1"
local MSG_UPSTREAM="$2"
local MSG_UPSTREAM_RS="$3"
local SEND_RESULT_CODE=$(curl -s -X 'POST' -H 'Content-Type: application/json' -d \
"{
\"msg_type\": \"interactive\",
\"card\": {
\"elements\": [
{
\"tag\": \"div\",
\"text\": {
\"content\": \"**业务域名:** ${MSG_DOMAIN_NAME}\n**上游名称:** ${MSG_UPSTREAM}\n**实例列表:** ${MSG_UPSTREAM_RS}\n**业务研发:** ${APP_MANAGER}\n**报警时间:** ${TIMENOW}\",
\"tag\": \"lark_md\"
}
}
],
\"header\": {
\"template\": \"red\",
\"title\": {
\"content\": \"Nginx-Rs状态告警\",
\"tag\": \"plain_text\"
}
}
}
}" ${FEISHU_URL}|jq -r '.code')
if [ "${SEND_RESULT_CODE}"x = "0"x ];then
log "飞书消息发送成功"
else
err_log "飞书消息发送失败[code:${SEND_RESULT_CODE}]"
fi
}
#######################################
# 依赖命令检查函数
# 局部变量:
# CMDS: 依赖命令列表,使用空格分隔
#######################################
function check_cmd(){
local CMDS="jq"
for CMD in ${CMDS};do
local CHECK_RESULT=$(rpm -qa ${CMD}|wc -l)
if [[ ${CHECK_RESULT} -eq 0 ]];then
err_log "${CMD}命令不存在,请手动进行安装: yum install -y ${CMD}!"
fi
done
}
check_cmd
# 将健康检查页面中,Status为down的结果取出来存放到${DOWN_RS_FILENAME}文件中
curl -s ${CHECK_STATUS}|grep down > ${DOWN_RS_FILENAME}
# ${DOWN_RS_FILENAME}文件行数
FILE_LINE=$(wc -l ${DOWN_RS_FILENAME}|awk '{print $1}')
# 判断${DOWN_RS_FILENAME}文件行数,如果为0则退出执行
# 否则将${DOWN_RS_FILENAME}文件中的upstream取出来
if [ "${FILE_LINE}" = "0" ];then
err_log "所有的实例Status都是UP,无需报警"
else
# 取出${DOWN_RS_FILENAME}文件中的upstream并去重
cat ${DOWN_RS_FILENAME}|awk -F ',' '{print $2}'|sort|uniq > ${UPSTREAM_NAME}
fi
# 遍历信息并告警
while read upstream
do
# 获取当前时间
TIMENOW=$(date "+%Y-%m-%d %H:%M:%S")
# upstream名称
UPSTREAM="${upstream}"
# upstream对应的域名,生产中要从CMDB中拉取真实域名,而不是在这里进行判断
if [[ "${UPSTREAM}"x = "app01-servers"x ]];then
DOMAIN_NAME="app01.ops.com"
else
DOMAIN_NAME="app02.ops.com"
fi
# RS的数量,多个实例异常时,需要判断换行输出
UPSTREAM_RS_NUM=$(grep "${UPSTREAM}" ${DOWN_RS_FILENAME}|wc -l)
# 获取upstream down的IP和端口,处理成:1.1.1.1:80\n1.1.1.2:80格式
UPSTREAM_RS_TMP=$(grep "${UPSTREAM}" ${DOWN_RS_FILENAME}|awk -F ',' '{print $3}'|xargs|tr ' ' '-' > rs.tmp.ngx)
sed -i 's#-#\\n#g' rs.tmp.ngx
UPSTREAM_RS=$(cat rs.tmp.ngx)
# 发送告警消息
if [ "${UPSTREAM_RS_NUM}"x = "1"x ];then
sendFeishuMsg "${DOMAIN_NAME}" "${UPSTREAM}" "${UPSTREAM_RS}"
else
# 多个实例异常时,添加一个\n
sendFeishuMsg "${DOMAIN_NAME}" "${UPSTREAM}" "\n${UPSTREAM_RS}"
fi
sleep 2
done < ${UPSTREAM_NAME}
# 清理临时文件
rm -f *.ngx
配置定时任务:
echo '*/1 * * * * /bin/sh /root/nginx_rs_check.sh >/dev/null 2>&1' > /var/spool/cron/root
预期结果
当业务的RS停止时,会报送报警消息到飞书报警群
# 停止RS实例
docker stop app01-servers-9091 app02-servers-9092 app02-servers-9093
访问/check_status接口,业务RS已经异常
过一分钟飞书会收到告警消息