Nginx健康检查告警

背景介绍

A企业产品经过商业广告的精准运营和加速迭代,逐渐适应了市场的需求,成为了消费者出差旅行中必不可少的伙伴。每天的业务高峰和低谷都维持在比较稳定的频率中,但在业务高峰期,经常有研发反馈自己的业务,部分应用的接口有异常现象出现,影响用户体验和收益指标。运维经过排查后,最终发现是Nginx集群下的某些业务实例已经无法访问,却还在负载中接收请求,从而导致异常。

解决方案

通过上面的背景介绍可知,在业务高峰期出现的问题有两个,一是应用实例异常,运维无法第一时间知晓,二是应用实例异常后,无法自动下掉负载。由于所有的业务都通过Nginx集群做流量转发,为解决上述问题,采取如下解决方案:

  • 问题一的解决方案:运维开发应用实例健康检查脚本,一旦实例异常将发送消息到飞书告警群。
  • 问题二的解决方案:为Nginx添加nginx_upstream_check_module健康检查模块,为业务实例添加健康检查,实例异常后,Nginx会主动踢掉异常实例。

注意:问题一的解决方案依赖于问题二的健康检查功能。

环境搭建

条件:

基本说明:

  • 操作系统:CentOS Linux 7 (Core)
  • 业务集群:OpenResty+健康检查模块
  • 飞书:飞书告警群,添加群机器人

健康检查模块:GitHub - msaf1980/nginx_upstream_check_module: For http protocol. Health checks upstreams for nginx,support dynamic health check interface for adding servers, transplat from tengine based on upstream_check_module

业务场景:

现有业务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已经异常

过一分钟飞书会收到告警消息

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值