背景

我前面写了几篇文章,讲c3p0数据库连接池发生了连接泄露,但是随机出现,难以确定根因,最终呢,为了快速解决问题,我是先写了个shell脚本,脚本主要是检测服务的接口访问日志,看看过去的30s内是不是接口几乎都超时了,如果是的话,咱们就重启服务。然后把这个shell加入到了crontab里,每30s调度一次。

crontab如下(cron最小调度时间为分钟,所以搞了两条,以支持30s执行一次):

* * * * * /bin/sh /xxx/xxx/check_service_block_gbk.sh
* * * * * sleep 30; /xxx/xxx/check_service_block_gbk.sh

脚本的内容大概如下,:

计算过去n秒内异常请求的次数
...
如果异常请求超过某个阈值,重启服务
if [ "${error_req_count_sum}" -gt ${THRESHOLD_FOR_ERROR_REQ_IN_LAST_N_SECONDS} ];then
log "trigger restart, start restart"
"${SERVICE_ROOT_PATH}"/run.sh stopall
"${SERVICE_ROOT_PATH}"/run.sh startall
log "finish restart,finish current check"
else
log "no need restart,finish current check"
fi

这个脚本上线有半个多月了,这期间问题一直没复现,结果,昨天下午,运维同事和我说,用户反馈app出问题了,也就是问题复现了,但是,我那个脚本吧,好像有问题,现象就是,异常进程是结束了,但是,却没有启动起来,导致这期间服务完全宕了,最后还是他手动启动起来的。

现场排查

由于事情是事后他和我说的,我过去后也就是一起看了下日志,发现服务的访问日志确实是中断了2分钟(就是服务被脚本干掉后,没启动起来的真空期)。而shell脚本呢,在如下几行中:

function log()
{
now=$(date "+%Y-%m-%d %H:%M:%S")
echo -e "$now : $*"
echo "$now : $*" >> "${SERVICE_ROOT_PATH}"/check_service_block_gbk.log
} log "trigger restart, start restart"
"${SERVICE_ROOT_PATH}"/run.sh stopall
"${SERVICE_ROOT_PATH}"/run.sh startall
log "finish restart,finish current check"

当时脚本写得不算很完备,只有log的那两行有日志,真正执行停止服务和启动服务的run.sh的执行,日志都打到console去了,日志文件里完全没有:

"trigger restart, start restart"
"finish restart,finish current check"

所以啊,毕竟还是不够专业,应该把脚本的标准输出重定向到日志文件的。

另外,根据脚本和现象,确定是执行了/run.sh stopall的,毕竟服务都没了嘛。

我们这个服务的框架很老,但也还比较稳定,框架自带的脚本就是上面看到的那个run.sh,这个run.sh呢,执行run.sh startall,会启动3个服务。

  • 一个是服务本身
  • 再一个是一个定时重启脚本,脚本里是一个死循环,每次循环就是检测是否到指定时间了,如果到了,就重启服务;没到,就sleep 1s;
  • 另一个也是一个watchdog脚本,脚本里是一个死循环,每次循环就是检测服务还在不在,不在了就把服务拉起来

这几个服务一启动后,就会在当前目录下,生成几个pid文件:

  • 服务本身,不生成pid文件,停止时就靠服务名去ps -ef|grep来查找
  • 定时重启脚本,生成pid到reset.pid这个文件
  • watchdog脚本,生成pid到watchdog.pid这个文件

而停止的时候run.sh stopall,就会根据上面的几种方法去找到对应的pid来kill。然后这几个服务的框架是支持kill -9这种暴力杀服务的,所以,几个服务被stop了,日志里是一点信息没有;然后呢,既然你说服务没启动起来,难道启动日志也没有嘛?问题是,还真的没有,空空如也,正常的话,自然是有的。但是我们可能就是执行run.sh startall时,报错可能都写到标准输出了,自然就没记录到日志里。

当时看到的东西,差不多就是这些。

本地复现

有的人会说,感觉这脚本没测试,直接就上线了,我可以这么说,测试,肯定是测了的,本地运行shell,都能把服务重启起来;但是,把脚本放到crontab里面后,倒是没有测试过这个分支。

这两个脚本是在同一目录下:

[root@xxxx]# ll check_service_block_gbk.sh run.sh
-rwxr-xr-x 1 root root 3498 Aug 1 08:50 check_service_block_gbk.sh
-rwxr-xr-x 1 root root 11425 Aug 1 10:51 run.sh

我当时测试的时候,在本地测是通过如下命令,一点问题没有:

./check_service_block_gbk.sh

结果这次上线又出了这个,我就想着在本地复现下,弄到cron里去触发,没想到,还真的和线上一样,服务被stop了,但是并没有重启起来。

我先是crontab -e加了日志,把脚本的标准输出重定向到文件/root/cron.log:

* * * * * /bin/sh -x /foo/bar/check_service_block_gbk.sh >> /root/cron.log 2>&1

然后触发了一次后,去查看shell执行日志/root/cron.log,发现,在执行startall时,nohup启动服务的地方,看着有点怪:

+ nohup /foo/bar/TBAServer

这个TBAServer是个二进制,就是我们的后台服务。我当时,为啥感觉有点怪呢,因为run.sh中,nohup那一行如下:

nohup $SERVER_PATH >> ${SERVER_DIR}/stdout.txt 2>&1 &

我们脚本里,后面还有>> xxx/stdout.txt 2>&1 &这些,为啥在shell -x的执行log里没有呢?

当时以为找到了问题,结果后来,我正常在shell中如下执行:

sh -x check_service_block_gbk.sh

发现执行日志也是一样的,看起来不是这个的问题。

然后左查右查,搞了好久,反正昨晚没弄出来,然后早上上班的时候,在互联网上关键字找了下,好像也没有类似的问题,只看到说,在cron执行的话,环境变量和在shell中执行不一样,不过我还没来得及测试环境变量这块,就有了新发现。

换机测试,柳暗花明

由于昨晚那台本地拿来复现的机器,白天用的人较多,为了不影响别人,我就换了台机器,没想到,换到新机器后测试时,在shell的执行日志中看到了关键日志:

+ nohup /foo/bar/TBAServer
...
启动路径不是进程所在路径,系统无法正常运行

看到这个,我大概就知道是啥原因了,为啥这个关键日志,在昨晚的机器没有呢,是因为二进制文件的版本不同,我今天这台机器上的二进制,版本更新。

看到这个错误,我大概猜测是进程的current working directory的问题,于是我修改了下run.sh,打印pwd。

echo cwd: "$PWD"  ---增加的一行
nohup $SERVER_PATH >> ${SERVER_DIR}/stdout.txt 2>&1 &

然后,分别测试在shell下正常运行和通过crontab运行:

  • crontab时,

    cwd: /root

  • shell正常运行时:

    cwd: /foo/bar

虽然已基本确认问题,我还是进一步检验了一下,为啥二进制文件里会报那个错误,我用IDA对那个二进制反编译了一下,(只能看懂一点,非常勉强)。

问题根源

里面有如下代码:

  if ( !IsStartFromPFPath() )
{
OUT(byte_4AB1C0);
exit(1);
}

这里调用了一个函数,大意是是否从xxx路径启动,不是的话,就会输出一个信息,然后exit。

这个输出的信息,我找了下,确实就是日志里那句。

接下来,我们进函数一览,现在看c++已经非常吃力了,只能看个大概:

这里面调用了另外两个函数,一个是获取进程的cwd(当前工作目录),一个是获取进程文件的路径,然后做比较,看看是否一致。

这个获取进程文件路径的函数,如上,我们结合猜测,应该就是获取二进制文件TBAServer的位置。

而,我们在cron执行时,cwd为/root,而TBAServer的位置为/foo/bar/TBAServer,这两个路径,明显不一致,所以,最终报了那个错误,导致启动失败。

为了进一步确认以上猜想,我准备使用strace命令,来看看启动这个TBAServer时,到底进行了哪些系统调用?

我改了run.sh,如下:

echo cwd: "$PWD"
nohup strace -q -f -s 200 $SERVER_PATH 2>&1 & >> ${SERVER_DIR}/stdout.txt

strace命令的强大毋庸置疑,可以跟踪系统调用,很多难题都能迎刃而解。下图也确认了我们的代码分析的结论。

问题解决

知道是cwd的问题了,那就解决吧,解决的办法就是要将工作目录切换到正确的目录,我采用的方法是直接在crontab这里修改。

* * * * * cd /foo/bar; /bin/sh -x /foo/bar/check_service_block_gbk.sh

有一个小问题是,这里我就没再将执行日志重定向了,也就是去掉了下面这部分。因为我发现服务运行时的日志也会打印进去,到时候日志就打了两份了。暂时还没去想怎么解决。

>> /root/cron.log 2>&1

下午的时候,到运维同事那边试了试,运行很平稳,检测到异常就可以自动重启了,终于可以了了这个事了。

参考文章

关于cron环境变量问题的文章:

https://mp.weixin.qq.com/s/Qafz20Mum51yY7OjfRpywA

https://mp.weixin.qq.com/s/9dWlgS4YslC6MnXbF2E8rw

线上问题排查--进程重启失败,最后发现是忘了cd的更多相关文章

  1. 线上服务Java进程假死快速排查、分析

    引用 https://zhuanlan.zhihu.com/p/529350757 最近我们有一台服务器上的Java进程总是在运行个两三天后就无法响应请求了,具体现象如下: 请求业务返回状态码502, ...

  2. Java线上问题排查神器Arthas实战分析

    概述 背景 是不是在实际开发工作当中经常碰到自己写的代码在开发.测试环境行云流水稳得一笔,可一到线上就经常不是缺这个就是少那个反正就是一顿报错抽风似的,线上调试代码又很麻烦,让人头疼得抓狂:而且deb ...

  3. Java架构师线上问题排查,这些命令程序员一定用得到!

    Java架构师线上问题排查,这些命令程序员一定用得到! 线上问题排查,以下场景,你遇到过吗? 一.了解机器连接数情况 问题:1.2.3.4的sshd的监听端口是22,如何统计1.2.3.4的sshd服 ...

  4. Arthas常用功能及一次线上问题排查

    一.Arthas简介 Arthas是Alibaba开源的Java诊断工具,功能很强大,它是通过Agent方式来连接运行的Java进程.主要通过交互式来完成功能. https://arthas.aliy ...

  5. Java线上问题排查思路及Linux常用问题分析命令学习

    前言 之前线上有过一两次OOM的问题,但是每次定位问题都有点手足无措的感觉,刚好利用星期天,以测试环境为模版来学习一下Linux常用的几个排查问题的命令. 也可以帮助自己在以后的工作中快速的排查线上问 ...

  6. 线上问题排查神器 Arthas

    线上问题排查神器 Arthas 之前介绍过 BTrace,线上问题排查神器 BTrace 的使用,也说它是线上问题排查神器.都是神器,但今天这个也很厉害,是不是更厉害不好说,但是使用起来非常简单.如果 ...

  7. JVM 线上故障排查基本操作--CPU飙高

    JVM 线上故障排查基本操作 CPU 飚高 线上 CPU 飚高问题大家应该都遇到过,那么如何定位问题呢? 思路:首先找到 CPU 飚高的那个 Java 进程,因为你的服务器会有多个 JVM 进程.然后 ...

  8. JVM 线上故障排查

    JVM 线上故障排查 Linux 1.1 CPU 1.2 内存 1.3 存储 1.4 网络 一.CPU 飚高 寻找原因 二.内存问题排查 三.一般排查问题的方法 四.应用场景举例 4.1 怎么查看某个 ...

  9. BTrace:线上问题排查工具

    BTrace简介 GitHub地址:BTrace 下载地址:v1.3.11.3 官方使用教程:Btrace使用教程 使用场景 BTrace 是一个事后工具,所谓事后工具就是在服务已经上线了,但是发现存 ...

  10. 记一次线上bug排查-quartz线程调度相关

    记一次线上bug排查,与各位共同探讨. 概述:使用quartz做的定时任务,正式生产环境有个任务延迟了1小时之久才触发.在这一小时里各种排查找不出问题,直到延迟时间结束了,该任务才珊珊触发.原因主要就 ...

随机推荐

  1. .NET开源分布式锁DistributedLock

    一.线程锁和分布式锁 线程锁通常在单个进程中使用,以防止多个线程同时访问共享资源. 在我们.NET中常见的线程锁有: 自旋锁:当线程尝试获取锁时,它会重复执行一些简单的指令,直到锁可用 互斥锁: Mu ...

  2. 几行代码教你快速创建scrapy项目,非常实用建议收藏!

    import shutil,os修改settings.py def config(scrapy_path,project_name): judge=input("是否自动修改配置?是:yes ...

  3. 工作中,我们经常用到哪些SQL语句呢?

    目录 一.DDL部分(create.drop.alter) 1.1 create 语句上 1.2 drop 语句 1.3 alter 语句 二.DML(数据操纵语言)和DQL(数据查询语言) 2.1 ...

  4. 2022-07-29:一共有n个人,从左到右排列,依次编号0~n-1, h[i]是第i个人的身高, v[i]是第i个人的分数, 要求从左到右选出一个子序列,在这个子序列中的人,从左到右身高是不下降的。

    2022-07-29:一共有n个人,从左到右排列,依次编号0~n-1, h[i]是第i个人的身高, v[i]是第i个人的分数, 要求从左到右选出一个子序列,在这个子序列中的人,从左到右身高是不下降的. ...

  5. 2020-12-25:MQ中,如何保证消息的顺序性?

    福哥答案2020-12-25:[稍微详细的答案:](http://bbs.xiangxueketang.cn/question/875)生产者保证消息入队的顺序.MQ 本身是一种先进先出的数据接口,将 ...

  6. 【故障公告】博客站点一台阿里云负载均衡被DDoS攻击

    13:06 收到阿里云的电话与邮件通知,博客站点的一台阿里云负载均衡因被 DDoS 攻击被关进黑洞(所有访问被屏蔽),部分用户的访问受影响,由此给您带来麻烦,请您谅解. 您的IP:x.x.x.x 实例 ...

  7. 自定义 coding.net 静态网站域名

    点击文章左下角 "阅读原文",预览本文章的示例站点. 在上一篇文章<使用 coding.net 发布你的个人博客>,我们介绍了怎么在 coding.net 部署个人的静 ...

  8. 基于 gulp 的 fancybox 源码压缩

    前不久,处理生信分析的网页版自动化报告时候就使用过 fancybox,今天在优化个人博客,为博文增加图片缩放效果,解决一些滚动条问题时,才从 fancybox 的 Github 源码中接触到 gulp ...

  9. x86 机器指令编码规则

    x86 机器指令编码依次由一下部分组成: 指令前缀(prefix,非必需) 操作码(opcode,必需) 寻址方式 R/M(ModR/M,非必需) 比例因子-变址-基址(SIB,非必需) 地址偏移量( ...

  10. 深入理解注解驱动配置与XML配置的融合与区别

    摘要:本文旨在深入探讨Spring框架的注解驱动配置与XML配置,揭示两者之间的相似性与差异. 本文分享自华为云社区<Spring高手之路2--深入理解注解驱动配置与XML配置的融合与区别> ...