前几天, 发现一台阿里云服务器上的 web 服务不可用. 远程 SSH 登录不上, 尝试几次登录上去之后, 执行命令都显示
-bash: fork: Cannot allocate memory
一看以为是内存泄漏导致溢出. 因为执行不了任何命令, 只能通过控制台重启服务器恢复服务.
初步排查
服务恢复后, 查看系统日志, Linux 系统日志路径 / var/log/messages, 可通过 journalctl 命令查看, 如
journalctl --since="2019-06-12 06:00:00" --until="2019-06-12 10:00:00"
可查看 since 之后, until 之前时间段的日志. 除了发现 crond[14954]: (CRON) CAN'T FORK (do_command): Cannot allocate memory 这个错误日志, 未见其它异常 (下面的 sshd[10764]: error: fork: Cannot allocate memory 应是 SSH 登录执行命名失败的日志)
通过阿里云 - 云监控 - 主机监控查看内存使用率指标, 这段时间内, 内存使用率一直在 40% 以下, 基本可排除内存溢出的可能.
通过搜索查阅到进程数超过操作系统限制可能导致 bash: fork: Cannot allocate memory 的报错 (参考: ).
通过 ps -eLf|wc -l 查看当前进程线程数 (ps -ef 只打印进程, ps -eLf 会打印所有的线程), 只有 1000 多个, 故障时刻系统到底运行了多少线程已无从得知, 只能持续跟进监测.
问题定位
几天后, 再次通过 ps -eLf|wc -l 查看, 发现线程数已达 16000 多个. 直接执行 ps -eLf 可看到大量 tomcat 进程所产生的线程, 猜测是不是线程死锁导致大量线程未完成一直 hung 在那里.
执行 jstack 进程号 > ~/jstack.txt 命令将进程所运行线程情况打印出来分析, 发现大量的 WAITING 状态的线程, 如下
- "pool-19-thread-1" #254 prio=5 os_prio=0 tid=0x00007f0b700a6000 nid=0x29a9 waiting on condition [0x00007f0b274df000]
- java.lang.Thread.State: WAITING (parking)
- at sun.misc.Unsafe.park(Native Method)
- - parking to wait for <0x00000006ce3d8790> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
- at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
- at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
- at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
- at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074)
- at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
- at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
- at java.lang.Thread.run(Thread.java:748)
根据上述内容可看出线程在等一个条件, 并且是在执行 LinkedBlockingQueue.take 方法的时候, 查看该方法的 java doc, 当队列为空时, 该方法将会一直等待直到有元素可用.
- /**
- * Retrieves and removes the head of this queue, waiting if necessary
- * until an element becomes available.
- *
- * @return the head of this queue
- * @throws InterruptedException if interrupted while waiting
- */
- E take() throws InterruptedException;
询问同事在哪里用到了 LinkedBlockingQueue, 同事回忆起不久前用线程池实现往阿里云 OSS 服务通过追加的方式上传文件功能, 查看代码后发现问题 -- 线程池没有关闭. 为了使文件片段保存不存在错乱, 每次保存文件时, 都 new 了一个线程池对象,
ThreadPoolExecutor saveImgThreadPool = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
但处理完后, 没有关闭这个线程池对象, 这样线程池仍会通过 take 方法去取等待队列中是否还有未完成的线程任务, 等待队列为空时将会一直等待, 这样就导致大量的线程 hung 在这里了 (基本是只要方法被调一次, 就会产生一个 hung 住的线程).
延伸
线程状态为 "waiting for monitor entry":
意味着它 在等待进入一个临界区 , 所以它在 "Entry Set" 队列中等待. 此时线程状态一般都是 Blocked:
java.lang.Thread.State: BLOCKED (on object monitor)
线程状态为 "waiting on condition":
说明它在等待另一个条件的发生, 来把自己唤醒, 或者干脆它是调用了 sleep(N). 此时线程状态大致为以下几种:
java.lang.Thread.State: WAITING (parking): 一直等那个条件发生 (本文案例即为此种场景);java.lang.Thread.State: TIMED_WAITING (parking 或 sleeping): 定时的, 那个条件不到来, 也将定时唤醒自己.
如果大量线程在 "waiting for monitor entry": 可能是一个全局锁阻塞住了大量线程. 如果短时间内打印的 thread dump 文件反映, 随着时间流逝, waiting for monitor entry 的线程越来越多, 没有减少的趋势, 可能意味着某些线程在临界区里呆的时间太长了, 以至于越来越多新线程迟迟无法进入临界区.
如果大量线程在 "waiting on condition": 可能是它们又跑去获取第三方资源, 尤其是第三方网络资源, 迟迟获取不到 Response, 导致大量线程进入等待状态. 所以如果你发现有大量的线程都处在 Wait on condition, 从线程堆栈看, 正等待网络读写, 这可能是一个网络瓶颈的征兆, 因为网络阻塞导致线程无法执行. 也可能是如本文所提到的, 由于程序编写不当所致.
我的个人博客地址: http://blog.jboost.cn/
我的头条空间:
我的 GitHub 地址: https://github.com/ronwxy
我的微信公众号: jboost-ksxy
--------------------------------------
来源: https://www.cnblogs.com/spec-dog/p/11032779.html