地震是由不可抗力导致的,而事故与之不同,任何大的生产事故在发生之前都有迹可循,而且事故的发生并不是偶然的,我们应该善于从现象中总结规律,找到发现、止损和避免的方法

海恩法则

每一起严重事故的背后,必然有29次轻微事故和300起未遂先兆及1000起事故隐患。

  • 事故的发生是量的积累的结果。
  • 再好的技术、再完美的规章,在实际操作层面也无法取代人自身的素质和责任心。

       根据海恩法则,一起重大事故发生后,我们在处理事故和解决问题的同时,还要及时对同类问题的"事故征兆"和"事故苗头"进行排查处理,以防止类似问题重复发生,把问题解决在萌芽状态,这完全可以作为互联网企业线上应急的指导思想。在线上应急的过程中,不但要定位和解决问题,还要发现问题的根源,并找到发生事故之前的各种征兆,对征兆进行排查和分析,并做响应的报警处理。

墨菲定律

       如果有两种或两种以上方式去做某件事情,而选择其中一种方式将导致灾难,则必定有人会做出这种选择。

  • 任何事情都没有表面看起来那么简单。
  • 所有事情的发展都会比你预计的时间长。
  • 会出错的事情总会出错。
  • 如果你担心某种情况发生,那么它更有可能发生。

       墨菲定律实际上是个心理学效应,如果你担心某种情况会发生,那么它更有可能发生,久而久之就一定会发生。这警示我们,在互联网公司里,对环境发生的任何怪异现象和问题都不要轻易忽视,对于其背后的原因一定要彻查。 同样,海恩法则也强调任何严重事故的背后都是多次小问题的积累,积累到一定的量级后会导致质变,严重的问题就会浮出水面。

       对于任何现象都要秉承着"为什么发生?发生了怎么应对?怎么恢复?怎么避免?"的原则,对问题要彻查,不能因为问题的现象不明显而忽略

6.2. 线上应急的目标、原则和方法

6.2.1. 应急目标

       在生产环境发生故障时快速恢复服务,避免或减少故障造成的损失,避免或减少故障对客户的影响,

6.2.2 应急原则

  • 恢复系统,快速止损,而不是彻底解决问题。
  • 有明显的资金损失时,升级问题快速止损。
  • 不影响用户体验的前提下,保留部分现场和数据。

6.2.3 线上应急的额方法和流程

有条不紊地进行,遇事胆大心细,该做决策的时候要毫不犹豫,该升级的时候要果断。

线上应急一般分为6个阶段:发现问题、定位问题、解决问题、消除影响、回顾问题、避免措施。

在应急过程中要记住,应急只有一个总体目标:尽快恢复问题,消除影响

定位问题
  • 问题系统最近是否进行了上线?
  • 依赖的基础平台和资源是否进行了上线或者升级?
  • 依赖的系统最近是否进行了上线?
  • 运营是否在系统里面做过运营变更?
  • 网络是否有波动?
  • 最近的业务量是否增加?
  • 服务的使用方是否有促销活动?
回顾问题
  • 类似的问题还有哪些没有想到?
  • 做了哪些事情,这个事故就不会发生?
  • 做了哪些事情,这个事故即使发生了,也不会产生损失?
  • 做了哪些事情,这个事故即使发生了,也不会造成这么大的损失?

6.3 技术攻关的方法论

       技术攻关的目标是解决问题,因此首先要从问题发生的环境和背景入手,首先考虑下面几个问题。

  • 最近是否有变更、升级和上线?(回滚)
  • 之前是否遇到过类似的情况?(使用历史经验)
  • 是否有相关领域的专家?例如:安全、性能、数据库、大数据和业务等领域的专家(开启专家模式)【最小化复现→找到原因→提出解决方案→验证解决方案→线上实施】。

       对于任何问题,我们必须收集发生这些问题的现象,考虑如下问题。

  • When:什么时候出的问题?
  • What:什么出了问题?
  • Who:谁在什么时间里发现了问题?问题影响了谁?
  • Where:哪里出现了问题?哪里又没出现问题?
  • Why:为什么出现了问题?

6.5高效的服务化治理脚本

6.5.1 show-busiest-java-threads

       此命令是用来查找java进程内CPU利用率最高的线程,一般适用于服务器负载较高的场景,并需要快速定位负载过高的成因。

命令格式:

./show-busiest-java-threads -p 进程号 -c 显示条数
./show-busiest-java-threads -h

脚本源码:

#!/bin/bash

PROG=`basename $0`
usage(){
	cat <<EOF
Usage: ${PROG} [OPTION]...
Example: ${PROG} -c 10
Options:
	-p, --pid   java process
	-c, --count thread count to show,default is 5
	-h, --help display this help and exit
EOF
    exit $1
}

# getopt:用于解析命令行参数。
# -n选项用于指定程序名称,这里使用变量$PROG的值作为程序名称
# -a选项告诉getopt允许使用短选项后面跟一个冒号的方式,即使长选项后面也跟了冒号
# -o选项用于指定短选项及其参数格式。这里的选项有:
#	c: 接受一个参数。
#	p: 接受一个参数。
#	h: 不接受参数(帮助选项)
# -l count:,pid:,help:
# -l选项用于指定长选项及其参数格式。这里的选项有:
#	`count:` 接受一个参数。
#	`pid:` 接受一个参数。
#	`help` 不接受参数(帮助选项)
# `--`:双连字符--表示之后的所有参数都应该被视为命令行参数,而不是getopt选项
# `$@`:引用所有传入脚本的命令行参数。

ARGS=`getopt -n "$PROG" -a -o c:p:h -l count:,pid:,help -- "$@"`

# 需要检查getopt的返回值,以确保命令行参数是有效的。如果getopt遇到无效的选项,它会返回非零值。
# [ ... ] 是一个测试命令(test command),用于进行条件判断
# $? 是一个特殊的shell变量,用来存储上一个命令的退出状态码
# -ne 是一个测试操作符,用于比较两个整数是否不相等(not equal)。
# 0 表示成功退出的状态码
# && 是一个逻辑运算符,在这里表示“如果前面的条件成立,则执行后面的命令
# 如果上一个命令的退出状态码不是0(即命令执行失败),则调用usage函数并传递参数1。
[ $? -ne 0 ] && usage 1
# 重新设置脚本的参数列表
# eval命令用于执行一个字符串中的命令序列
# set -- "${ARGS}"是set命令的一个变体,用于设置脚本的参数列表。--之后的部分会被解释为新的命令行参数。
# set -- "${ARGS}"实际上会将ARGS变量中的内容设置为脚本的参数列表。
# eval在这里的作用是执行set -- "${ARGS}"这一命令
eval set -- "${ARGS}"

while true; do
    case "$1" in
	-c|--count)
	   count="$2"
	   shift 2
	   ;;
	-p|--pid)
	   pid="$2"
	   shift 2
	   ;;
	-h|--help)
	    usage
		;;
	--)
        shift
		break
		;;
	# esac 是 case 语句的结束标记	
	esac
done
count=${count:-5}

redEcho(){
	# [ -c /dev/stdout ] 是一个测试表达式,用来检查 /dev/stdout 是否是一个字符设备文件
    [ -c /dev/stdout ] && {
	    # if sdout is console, turn on color output.
		echo -ne "\033[1;31m"
		echo -n "$@"
		echo -e "\033[0m"
	} || echo "$@"
}	

# Check the existence of jstack command
# if ! which jstack &> /dev/null; then: 这一行检查jstack命令是否可用。如果不可用,则条件为真
if ! which jstack &> /dev/null; then
	# -n选项测试一个字符串是否非空
	# -f选项测试一个文件是否存在
	# -x选项测试一个文件是否可执行
	# 将$JAVA_HOME/bin添加到PATH环境变量的开始位置。这样可以确保jstack工具在系统路径中可用
	# || { ... }如果前面的任何一个条件不满足,则执行大括号内的命令
    [ -n "$JAVA_HOME" ] && [ -f "$JAVA_HOME/bin/jstack" ] && [ -x "$JAVA_HOME/bin/jstack" ] && {
	    export PATH = "$JAVA_HOME/bin:$PATH"
	} || {
	    redEcho "Error: jstack not found on PATH and JAVA_HOME!"
		exit 1
	} 
fi

# $$: 当前进程的进程ID
uuid=`date +%s`_${RANDOM}_$$

cleanupWhenExit() {
  rm /tmp/${uuid}_* & > /dev/null
}
trap "cleanupWhenExit" EXIT

printStackOfThread(){
    while read threadLine ; do
	    pid=`echo ${threadLine} | awk '{print $1}'`
		threadId=`echo ${threadLine} | awk '{print $2}'`
		threadId0x=`printf %x ${threadId}`
		user=`echo ${threadLine} | awk '{print $3}'`
		pcpu=`echo ${threadLine} | awk '{print $5}'`
		echo "pid=${pid},threadId=${threadId},threadId0x=${threadId0x},user=${user},pcpu=${pcpu}"

	    jstackFile=/tmp/${uuid}_${pid}
		
		[ ! -f "${jstackFile}" ] && {
		    jstack ${pid} > ${jstackFile} || {
			    redEcho "Fail to jstack java process ${pid}!"
				rm ${jstackFile}
				continue
			
			}
		}
		
		redEcho "The stack of busy(${pcpu}%) thread (${threadId}/0x${threadId0x}) of java process(${pid}) of user(${user}):"
		sed "/nid=0x${threadId0x}/,/^$/p" -n ${jstackFile}
	done
}
# -z 是否为空,-n 是否不为空
[ -z "${pid}" ] && {
# -Leo: 这些选项组合在一起用于定制输出格式:
#	-L: 显示线程信息。
#	-eo: 指定输出格式

# pid,lwp,user,comm,pcpu: 这些字段名指定了要显示的列:
#	pid: 进程ID。
#	lwp: 线程ID。
#	user: 运行进程的用户。
#	comm: 进程的命令名称。
#	pcpu: 进程使用的CPU百分比。
    ps -Leo pid,lwp,user,comm,pcpu --no-headers | awk '$4=="java"{print $0}' | 
	sort -k5 -r -n | head --lines "${count}" | printStackOfThread
} || {
    ps -Leo pid,lwp,user,comm,pcpu --no-headers | awk -v "pid=${pid}" '$1==pid,$4=="java"{print $0}' |
	sort -k5 -r -n | head --lines "${count}" | printStackOfThread
}

		

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部