背 景
很多用户在使用Jenkins做项目发布的时候都没有结合监控系统一块去做,如果同时还没有消息通知系统的话发布是否成功无法知晓。出现发布事故的概率大大的增加。
痛 点
运维人员一般会被上级要求每次发布要确认是否发布成功。服务器数量多的话会要求分批发布,如何验证发布成功成为很多运维人员的挑战,手动验证方式又不够及时。
收 益
其实Jenkins除了发布也能做接口的健康检查,这样每次发布完循环多次去调用接口做对应到各个应用的健康检查并细化到每个主机的应用端口。哪个主机哪个端口出问题就及时报警。用户可以迅速的知道哪个应用对应的哪个主机出了问题。如果要做出现故障后重启或回滚也会方便很多。保证了业务上线的稳定性。
jenkins上的job配置
选择构建自由风格的项目
配 置
-
-
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 fidone
$ cat monitor-service-status.sh#! /bin/bashfunction 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="https://${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 https://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 @- https://${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 @- https://${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=$1if [[ -z ${app_name} ]];then echo no app_name exit 1fiapp_real_name=$2app_group=$(echo ${app_real_name} | awk -F '/' '{print $1}')app_deploy_type=$3check_uri=$4remote_user="dev"#jenkins_shell路径定义if [[ -z ${JENKINS_SHELL_PATH} ]];then JENKINS_SHELL_PATH="/opt/app/jenkins/scripts/"fi#consul地址consul_url="https://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}/lockmkdir -p ${lock_dir}cd ${basepath}j=0while truedo{ #如果存在锁定目录则说明在发布不检测 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/bashapp_env=$1if [[ ${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
mapping文件
在/opt/app/jenkins/scripts/mapping下示例:
$ cat crm-lmswebcrm-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_cwspremote_hosts=${job_name////-} # ansible hosts中的组名mapping_file=${JENKINS_SHELL_PATH}/mapping/${remote_hosts}source ${JENKINS_SHELL_PATH}/function-common.shmappingInfo # mapping信息