背 景
很多用户在使用Jenkins做项目发布的时候都没有结合监控系统一块去做,如果同时还没有消息通知系统的话发布是否成功无法知晓。出现发布事故的概率大大的增加。
痛 点
运维人员一般会被上级要求每次发布要确认是否发布成功。服务器数量多的话会要求分批发布,如何验证发布成功成为很多运维人员的挑战,手动验证方式又不够及时。
收 益
其实Jenkins除了发布也能做接口的健康检查,这样每次发布完循环多次去调用接口做对应到各个应用的健康检查并细化到每个主机的应用端口。哪个主机哪个端口出问题就及时报警。用户可以迅速的知道哪个应用对应的哪个主机出了问题。如果要做出现故障后重启或回滚也会方便很多。保证了业务上线的稳定性。
配 置
Description
所有服务健康检测
设置每两小时运行一次H */2 * * *
原因是防止进程僵死和jenkins内存泄漏
构建 build
shell执行
OLD_BUILD_ID=$BUILD_ID
#因为脚本monitor-all-services.sh是会调用另外一个脚本monitor-service-status.sh跑的,如果monitor-all-services.sh循环跑完了,那么monitor-service-status.sh也就不跑了,只有把build_id改成一个不存在的值让服务kill不了,这样就可以继续后台跑
BUILD_ID=dontKillMe
${JENKINS_SHELL_PATH}/ops/monitor-all-services.sh
#改回原来的BUILD_ID值
BUILD_ID=$OLD_BUILD_ID
构建后操作 Post-build Actions
jenkins URL 默认自动输入
钉钉access token 使用钉钉机器人的token
在构建失败时通知 勾选
钉钉通知器配置
$ cat monitor-all-services.sh
#! /bin/bash
#定义jenkins_shell路径
if [[ -z ${JENKINS_SHELL_PATH} ]];then
JENKINS_SHELL_PATH="/opt/app/jenkins/scripts/"
fi
#杀掉旧的还在执行的监控脚本
kill $(ps aux|grep monitor-se|awk '{print $2}')
sleep 1
#映射文件路径
mapping_path=${JENKINS_SHELL_PATH}/mapping/
#定义当前脚本执行的路径
basepath=$(cd `dirname $0`; pwd)
cd ${basepath}
cd ${mapping_path}
#用ls命令来显示当前目录下的文件并进行for循环遍历
mapping_files=$(ls)
for mapping_file in ${mapping_files}
do
#定义应用名称
app_name=${mapping_file}
#使用文件内写的应用名称
app_real_name=$(cat ${mapping_file} | awk '{print $1}')
#定义部署类型
app_deploy_type=$(cat ${mapping_file} | awk '{print $2}')
#定义要检查的uri
check_uri=$(cat ${mapping_file} | awk '{print $3}')
ps aux | grep "monitor-service-status.sh ${app_name} " | grep -v 'grep'
#如果ps返回不等于0说明没有这个监控,那么执行如下操作
if [[ $? -ne 0 ]];then
#如果检查的uri不为空
if [[ ! -z ${check_uri} ]];then
echo ${app_name} 开启检测
date=`date +%Y%m%d-%H`
#nohup ${basepath}/monitor-service-status.sh ${app_name} ${app_real_name} ${app_deploy_type} ${check_uri} >> "/tmp/monitor-"${app_name}-${date} 2>&1 &
#后台执行监控程序,日期参数${date}已经不用
nohup ${basepath}/monitor-service-status.sh ${app_name} ${app_real_name} ${app_deploy_type} ${check_uri} >> /dev/null 2>&1 &
fi
fi
done
$ cat monitor-service-status.sh
#! /bin/bash
function getConsul() {
#取余数
mod=$(($j % 10))
#如果余数等于1
if [[ ${mod} == 1 ]];then
k=1
#循环9次
for (( i=1;i<10;i++))
do
sleep 1
#获取consul所有的节点信息,因为服务器的ip和端口信息是存在consul上的,python -m json.tool显示json格式,jq -r '.[] | .Key'选取Key这列的值,grep -v '\/d-\|\/t-\|\/b-' 去掉dev/test/beta环境,公司当前d-开头的代表开发环境,t-开头的代表测试环境,b-开头的代表预发环境
app_info=$(curl --connect-timeout ${timeout} -m ${timeout} -s -X GET "${consul_url}?recurse" | python -m json.tool | jq -r '.[] | .Key' | grep ${app_name}/ | grep upstream |grep -v '\/d-\|\/t-\|\/b-')
# 如果拿到信息keys信息从app_info变量里拿
if [[ $? -eq 0 ]];then
keys=$(echo "${app_info}")
#grep -P是把匹配到的标红,o是把匹配到的取出来
app_hosts=$(echo ${keys} | grep -Po [0-9.]+:[0-9]+)
echo $app_hosts > ${temp_consul_path}${app_name}
i=10
else
#k值+1
k=$(($k+1))
i=1
fi
# 如果k=10,就到文件里去取,consul有时会挂掉,挂掉就拿不到了
if [[ ${k} -eq 10 ]];then
app_hosts=$(cat ${temp_consul_path}${app_name})
i=10
fi
done
else
#如果余数不等于1也是到文件里去取
app_hosts=$(cat ${temp_consul_path}${app_name})
fi
}
#查看url状态
function checkUrlStatus() {
http_code=$(curl --connect-timeout ${timeout} -m ${timeout} -o /dev/null -s -w %{http_code} ${check_url})
if [[ ${http_code} != 200 ]];then
sleep 2
echo 'second check'
http_code=$(curl --connect-timeout ${timeout} -m ${timeout} -o /dev/null -s -w %{http_code} ${check_url})
fi
}
#查看uri状态
function checkUriStatus() {
for app_host in ${app_hosts}
do
#获取ip和ssh端口
app_host_ip=$(echo ${app_host} | awk -F ':' '{print $1}')
ssh_port=8888
if [[ ${app_host_ip} =~ "192.168." ]];then
ssh_port=22
fi
#定义check_url
check_url="http://${app_host}${check_uri}"
checkUrlStatus
#如果检查url不为200
if [[ ${http_code} != "200" ]];then
if [[ ${app_deploy_type} == "jetty" || ${app_deploy_type} == "springboot" || ${app_deploy_type} == "go" || ${app_deploy_type} == "tomcat" ]];then
desc=${check_url}" http_code is "${http_code}
# 获取主机名,这是自己做的api接口通过ip可以获取主机名,此处不做详细介绍
hostname=$(curl -s http://jenkins.xxx.com:7000/getInstanceName/${app_host_ip})
# 如果主机名不是华为云的服务器那么直接登录服务器拿主机名,公司主机名定义规则为云厂商-地域-部门-应用组-使用语言-编号,例如huawei-sh-crm-sp-java-001
if [[ ${hostname} != "huawei-" ]];then
hostname=$(ssh -p ${ssh_port} -n ${remote_user}@${app_host_ip} "hostname")
fi
# 拿部门信息
dpt=$(echo ${hostname} | awk -F '-' '{print $3}')
# 拿环境信息,环境信息是放在用户家目录下.bashrc的app_env变量里的
app_env=$(ssh -p ${ssh_port} -n ${remote_user}@${app_host_ip} "cat ~/.bashrc | grep app_env | grep -v \#" | awk -F '=' '{print $2}' | grep -Po [a-z-]+)
# 获取最终的环境信息,get-env.sh脚本在下面
env=$(bash ${basepath}/get-env.sh ${app_env})
# prometheus中自定义value设置1是错误,0是正常
value=1
#把监控信息push到prometheus上,注意此处的PUSHGATEWAY_BASE大写变量是配置在jenkins的全局变量中的,例如值为192.168.0.200
echo 'ops_auto_restart_application{dpt="'${dpt}'",application="'${app_real_name}'",deploy_type="'${app_deploy_type}'",hostname="'${hostname}'",env="'${env}'",desc="'${desc}'"} '"${value}"'' | curl --data-binary @- http://${PUSHGATEWAY_BASE}:9091/metrics/job/"monitor-"${app_name}"-"${app_host_ip}
# 如果出问题就重启服务
source ~/.bash_profile && ssh -p ${ssh_port} -n ${remote_user}@${app_host_ip} "/opt/bin/${app_deploy_type} restart ${app_real_name}"
# 重启好服务后继续检查url状态
checkUrlStatus
fi
fi
#定义一个秒数变量
second=$(date "+%S")
# 如果状态3小于404,那么值为0
if [[ ${http_code} -lt "404" ]];then
value=0
echo `date`
#把正常信息推送到prometheus
sleep 15 && echo 'ops_auto_restart_application '"${value}"'' | curl --data-binary @- http://${PUSHGATEWAY_BASE}:9091/metrics/job/"monitor-"${app_name}"-"${app_host_ip}
fi
done
}
#定义consul临时目录
temp_consul_path="/tmp/consul/"
if [[ ! -d ${temp_consul_path} ]];then
mkdir ${temp_consul_path}
fi
# 超时时间
timeout=5
#app_name用变量传进去,没传就退出
app_name=$1
if [[ -z ${app_name} ]];then
echo no app_name
exit 1
fi
app_real_name=$2
app_group=$(echo ${app_real_name} | awk -F '/' '{print $1}')
app_deploy_type=$3
check_uri=$4
remote_user="dev"
#jenkins_shell路径定义
if [[ -z ${JENKINS_SHELL_PATH} ]];then
JENKINS_SHELL_PATH="/opt/app/jenkins/scripts/"
fi
#consul地址
consul_url="http://consul.xxx.com:8500/v1/kv/upstreams/"
#映射的文件路径
mapping_path=${JENKINS_SHELL_PATH}/mapping/
#监控检查的日志路径
health_log_path="/tmp/health/"
health_log_file=${health_log_path}${app_name}
mkdir -p ${health_log_path}
basepath=$(cd `dirname $0`; pwd)
# 设定备份代码的路径
if [[ -z ${LOCAL_BACKUP_PATH} ]];then
LOCAL_BACKUP_PATH="/opt/code-backup"
fi
# 设置锁定目录
lock_dir=${LOCAL_BACKUP_PATH}/lock
mkdir -p ${lock_dir}
cd ${basepath}
j=0
while true
do
{
#如果存在锁定目录则说明在发布不检测
if_exist=$(ls ${lock_dir} | grep ${app_name}"-" | grep "beta\|online")
if [[ ${if_exist} ]];then
echo `date` "发布代码中,不检测!"
sleep 10
else
#否则把j+1
j=$(($j+1))
#获取consul信息
getConsul
#查看uri
checkUriStatus
sleep 1
fi
}
done
#用于打印环境信息
$ cat get-env.sh
#!/bin/bash
app_env=$1
if [[ ${app_env} = "d-" ]];then
echo "dev"
elif [[ ${app_env} = "t-" ]];then
echo "test"
elif [[ ${app_env} = "b-" ]];then
echo "beta"
elif [[ ${app_env} = "" ]];then
echo "online"
elif [[ ${app_env} = "hd-" ]];then
echo "online"
elif [[ ${app_env} = "hb-" ]];then
echo "online"
else
echo ${app_env}
fi
在/opt/app/jenkins/scripts/mapping下示例:
$ cat crm-lmsweb
crm-lmsweb tomcat /health/HealthCheck/health
依次是组名/项目名 部署类型 健康检查接口
自动添加mapping文件流程
在function-common.sh中写映射函数
# 如果环境是线上的,并且部署类型是如下的,输出仓库名称,不属类型,要检查的uri到映射对象文件,${mapping_file}在common.sh,${CHECK_URI}参数通过jenkins页面设置文本参数配置获得,APP_DEPLOY_TYPE变量也是jenkins 发布项目job上配置的部署方式参数
function mappingInfo() {
if [[ ${app_env} == "online" ]];then
if [[ ${APP_DEPLOY_TYPE} == "python" || ${APP_DEPLOY_TYPE} == "springboot" || ${APP_DEPLOY_TYPE} == "jetty" || ${APP_DEPLOY_TYPE} == "go" || ${APP_DEPLOY_TYPE} == "tomcat" ]];then
echo ${job_name} ${APP_DEPLOY_TYPE} ${CHECK_URI} > ${mapping_file} # 映射关系表,监控使用
fi
fi
}
common.sh中调用,任何文件调用了common.sh就会调用执行它
#使用jenkins自带的环境变量${JOB_NAME获取信息}
job_name=$(echo ${JOB_NAME} | awk -F '/' '{print $1"/"$2}')
# lms-dev/S_cwsp/master => lms-dev/S_cwsp
remote_hosts=${job_name//\//-} # ansible hosts中的组名
mapping_file=${JENKINS_SHELL_PATH}/mapping/${remote_hosts}
source ${JENKINS_SHELL_PATH}/function-common.sh
mappingInfo # mapping信息