在上次在运维老哥友好的和我沟通之后,还消停没几天,今天又来找(问候)我了……

运维:这个服务也是你们的吧,你看这个 JAVA 进程,内存占用都快 3 个 G 了,这机器才 4G,你们堆才配置 2G,都要告警了!这次是真的内存泄露了吧,不是我无知了吧!

又来搞事情……这大哥是对我有意见吗?有了上次的经验,这回更自信了。还是按照惯例,先怼回去

我:“不可能,我们服务非常稳定,不会有这种问题!”

运维:你这哪来的自信和勇气?梁静茹给的吗?你先回去查查再装

看来大哥这回是有备而来啊,难道真是出问题了?有点慌了……

不过还是不能怂,先敷衍下运维老哥,找个借口回去先偷摸查查监控看看

我:行,我待会看看,我先去跟人开个会啊……

分析监控数据

这个服务的堆内存配置的是 2G,历史内存也确实达到过 2G,虽然现在 used 才几百兆……看起来也没啥问题

再加上一些 VM 自己的开销,一共占用 2 个多 G……好像也说的过去

然后我又找到了运维大哥,(友好的)沟通一下……

我:和上次一样啊,没什么区别,上次都解释过那个内存管理的机制了,你咋还说我们有问题?

运维: 你看你们这个服务,配置的是 CMS+ParNew 垃圾回收器吧,上次是你说的这个回收器组合下会释放内存给操作系统吧?那怎么还占用 2G,释放到哪去了?

我:虽然上回测试结果是会释放,但还有一些其他的说法,说是空闲时会增量释放,而且释放成本这么高,不释放又怎么样?

运维:你这话不是打自己脸么?上回说能释放,现在没释放你也说正常,你是不是觉得我傻?

运维大哥好像看出了我是在狡辩……

不释放也正常啊,释放成本这么高,释放后还得重新申请,重新组织内存结构balabalabala……

这话说的我自己都没底气……毕竟上次才测试过 CMS+ParNew 确实会释放,只是时间问题

运维:你继续狡辩,这服务的内存照这个趋势,估计要不要明天就得 OOM,然后系统再给你来个 OOM Killer 的绝杀,你可就开心了!

我:没问题的,这个内存正常,自己的服务,我还能不了解嘛

此时我已经有点不安了,大哥说的有道理啊,万一 OOM Killer了,可不完蛋了!

我:我晚点有空再仔细看看,应该没啥问题,你先忙你的,放心!

上服务器查实时指标

打发走了运维老哥之后,我仔细思考了一会。这内存的数据好像确实不太正常,这个服务属于那种后端应用,虽然业务复杂,但只是给业务大佬们用。不过这个内存占用确实有点说不过去啊,到底内存跑哪去了……

还是数据说话吧,上主机上找找看有没有什么线索

  1. 内存 - ok
  2. CPU 利用率 - ok
  3. CPU 负载 - ok

也没什么异常,CPU 利用率和负载啥的都很低……到底问题出在哪?

这么一点点的看指标太费劲了,还是拿 Arthas 看吧,JVM 相关的指标它基本都显示,比较全:

[arthas@20727]$ dashboardMemory                    used    total    max     usage    GC
heap                      498M     1963M    1963M   25.1%    gc.ps_scavenge.count          3
ps_eden_space             98M      512M     512M    19.5%    gc.ps_scavenge.time(ms)       44
ps_survivor_space         0K      87040K   87040K  0.00%    gc.ps_marksweep.count         1
ps_old_gen                39M     1365M    1365M   2.88%    gc.ps_marksweep.time(ms)      87
nonheap                   32M     180M      -1      17.7%
code_cache                10M      50M      240M    20%
metaspace                 20M     128M      -1      15.6%
compressed_class_space    2M      2M       1024M   0.25%

看起来JVM 级别的内存也没啥问题,再看看线程呢:

[arthas@20727]$ threadThreads Total: 9831, NEW: 0, RUNNABLE: 8, BLOCKED: 0, WAITING: 9789, TIMED_WAITING: 2, TERMINATED: 0, Internal threads
: 17

wc,这什么玩意!快 1w 个线程!还基本上都是 WAITING!

赶紧看看这些 WAITING 的线程都是些啥:

ead --state WAITINGID   NAME                          GROUP          PRIORITY  STATE     %CPU      DELTA_TIM TIME      INTERRUPT DAEMON
# 此处省略 9000+ 行……
9822 pool-9813-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9823 pool-9814-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9824 pool-9815-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9825 pool-9816-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9826 pool-9817-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9827 pool-9818-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9828 pool-9819-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9829 pool-9820-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9830 pool-9821-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9831 pool-9822-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9832 pool-9823-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9833 pool-9824-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9834 pool-9825-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9835 pool-9826-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9836 pool-9827-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9837 pool-9828-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9838 pool-9829-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9839 pool-9830-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false
9840 pool-9831-thread-1            main           5         WAITING   0.0       0.000     0:0.000   false     false

看到这个线程名,我也大概明白了,一定是哪个小天才在代码里下毒。

从线程名称来看,肯定是线程池里的线程嘛,而且是默认的线程名生成规则。线程池里的线程都是通过 ThreadFactory 来创建的,而默认的 ThreadFactory 生成规则就是这样:

DefaultThreadFactory() {SecurityManager s = System.getSecurityManager();group = (s != null) ? s.getThreadGroup() :Thread.currentThread().getThreadGroup();// 前缀,每一个新的 ThreadFactory 都有一个新的前缀// 每一个线程池,都只有一个 ThreadFactorynamePrefix = "pool-" +poolNumber.getAndIncrement() +"-thread-";
}public Thread newThread(Runnable r) {// 每个线程都会使用 factory的前缀,然后加上自增的线程数Thread t = new Thread(group, r,namePrefix + threadNumber.getAndIncrement(),0);if (t.isDaemon())t.setDaemon(false);if (t.getPriority() != Thread.NORM_PRIORITY)t.setPriority(Thread.NORM_PRIORITY);return t;
}

所以这个问题,肯定是哪个小天才,在代码里每次都新建线程池,然后还不 shutdown 导致的!随便找个线程,看看它的 stack:

"pool-1-thread-1" Id=10 WAITING on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@6ba7592at sun.misc.Unsafe.park(Native Method)-  waiting on java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject@6ba7592at 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)

实锤了,就是每次 new 线程池不 shutdown……现在只需要找到创建线程池的地方,再翻翻提交记录,就可以知道是哪个小天才了……

但代码这么多,想找到这个新建线程池的地方还是有点麻烦的,搜代码可不太方便。这里还是用 Arthas 来看看,stack 一看就知道

不过如果这里直接 stack ThreadPoolExecutor#execute 方法的话,干扰信息可能太多了,毕竟调用 execute 的地方太多,不好抓到重点

所以还是 stack 线程池初始化的方法比较好:

[arthas@20727]$ stack java.util.concurrent.ThreadPoolExecutor <init>Affect(class count: 0 , method count: 0) cost in 4 ms, listenerId: 5
No class or method is affected, try:
1\. Execute `sm CLASS_NAME METHOD_NAME` to make sure the method you are tracing actually exists (it might be in your parent class).
2\. Execute `options unsafe true`, if you want to enhance the classes under the `java.*` package.
3\. Execute `reset CLASS_NAME` and try again, your method body might be too large.
4\. Check arthas log: /home/jiangxin/logs/arthas/arthas.log
5\. Visit https://github.com/alibaba/arthas/issues/47 for more details.

这……是不支持吗?

Arthas 的增强策略是比较保守的,部分系统级别的类它不做增强,java.* 包下的类默认是不增强的,需要手动开启才行:

[arthas@20727]$ options unsafe trueNAME    BEFORE-VALUE  AFTER-VALUE
-----------------------------------unsafe  false         true

再次执行 stack 之后,可以用了。过了两分钟之后,终于有一次调用:

ts=2021-06-12 12:04:03;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@4e0e2f2a@java.util.concurrent.ThreadPoolExecutor.<init>()at java.util.concurrent.Executors.newFixedThreadPool(Executors.java:89)at XXXService.sendSms(XXXService.java:782)//...

终于找到了这个骚操作代码……它是这么写的:

ExecutorService executorService = Executors.newFixedThreadPool(8);executorService.submit(smsTask);//...

和我猜测的一样,果然是每次 new,然后还不 shutdown。

这也能和上面的情况对上了,多出的内存占用是因为这小一万个线程……而且这些线程池没有任务需要执行的话,线程肯定是 WAITING 状态,也不会占用 CPU 的利用率,负载有不会有影响。不仔细还真看不出问题 ,还是得结合各种指标来看,综合分析。

解决这个问题倒简单,让写这个屎代码的人去改了,然后拉出去祭天。

可是运维大哥那边……已经装出去了,这下脸可是丢完了。上次好不容易挣回来的面子,这次啥都没了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KZVhVNXC-1641284405595)(https://upload-images.jianshu.io/upload_images/26809252-7d8102f59823c41a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

给运维的交代

我:服务确实有点问题,我们发个紧急修复版本,晚上上线就能解决了

运维:你不是说没问题么?自信哪去了

我:这不是没吃早饭,饿的头发昏,脑子不够清醒……没看出来问题,我的锅我的锅

运维:肯定是你们的屎山代码导致的,没事也搞搞 code review ,查查代码,以后少出这种低级问题,不然我一天到晚处理你们这些破事不得烦死了

没想到这运维还喘起来了,给点面子就要上天……不过谁让我理亏呢,只能应下

我:对对对,我们以后一定多搞 code review,加强代码审查,避免这种屎代码再提交上去,影响生产。不给大佬添麻烦

作者:空无
链接:https://juejin.cn/post/6973808359614971918

运维:你们 JAVA 服务怎么又又又又出问题了,内存降不下来。相关推荐

  1. Linux企业化运维--(7)redis服务之redis配置及主从复制、主从自动切换、集群、redis+mysql、gearman实现数据同步

    Linux企业化运维 实验所用系统为Redhat-rhel7.6. 目录 Linux企业化运维 Linux企业化运维--(7)redis服务之redis配置及主从复制.主从自动切换.集群.redis+ ...

  2. 智能运维 VS 传统运维|AIOps服务管理解决方案全面梳理

    云智慧 AIOps 社区是由云智慧发起,针对运维业务场景,提供算法.算力.数据集整体的服务体系及智能运维业务场景的解决方案交流社区.该社区致力于传播 AIOps 技术,旨在与各行业客户.用户.研究者和 ...

  3. CenOS7 运维 - DNS域名解析服务 | 正向解析 | 反向解析 | 主从服务器实时同步 | 分离解析 | 排错思路及方案 | 超详细

    CenOS7 运维 - DNS域名解析服务 一.DNS系统的作用 二.域名的结构 根域 顶级域 二级域 子域 主机 三.DNS服务器类型 ►主域名服务器 ►从域名服务器 ►缓存域名服务器 ►转发域名服 ...

  4. 桌面运维中持续服务改进需要怎么进行?

    桌面运维中持续服务改进是确保企业内部计算机和网络设备持续运行的关键.以下是一些持续服务改进的方法: 1. 定期更新和维护软件和硬件 桌面运维人员应该定期更新和维护软件和硬件设备,确保它们保持最新和最佳 ...

  5. 运维与微服务结合?深度解析微服务框架Tars整体解决方案

    内容导航 什么是Tars? Tars框架源码部署 Tars服务部署管理 Tars配置中心 Tars服务发现 Tars远程日志 Tars状态监控 什么是Tars Tars是一个支持多语言内嵌服务治理功能 ...

  6. Spring Cloud Alibaba 极速运维:微服务与 DevOps

    前面我们反复强调微服务架构是将大的应用打散为多个小服务,这就必然导致打散后形成更多需要独立部署的应用程序,在大型互联网应用中,这些程序可能会达到上千个之多.频繁的测试.打包.发布,无疑会给运维部门带来 ...

  7. 从谷歌CRE谈起,运维如何培养服务意识?

    2016年10月,谷歌云平台博客(Google Cloud Platform Blog)上更新了一篇文章,谷歌宣布了一个新的专业岗位,CRE(Customer Reliability Engineer ...

  8. 工作网络安全运维兼顾JAVA开发

    工作有一段时间了,开始是代码审计(JAVA web.Android应用),工作快两个月,就担任安全运维项目经理,开始的代码审计突然到渗透测试.基线核查.日常漏洞扫描.漏洞复核.给开发讲解解决漏洞问题等 ...

  9. 运维监控工具-pigoss 服务于浙江电子口岸

    文章摘自pigoss 官网  http://www.netistate.com   如需转载,请标明出处! 案例所属行业: 电子政务/电子商务平台 项目实施时间:2009年 1.1    项目背景   ...

  10. 故障申报系统php源码,运维不再专业救火 不会PHP照样找出代码性能问题

    作者: 凉白开 网站:www.ttlsa.com 身处互联网的SA(运维)们总感叹自己职业的苦逼,Why?我来告诉你:APP奔溃.网站打不开.网站502.搜索缓慢.应用卡顿通通找运维,运营.项目经理. ...

最新文章

  1. 0513JS基础:数组内置方法、数学函数、时间函数
  2. linux下进制是如何转换?
  3. 无法在指定计算机上定位,Win10电脑无法打开定位功能时启动GeolocationService服务提示找不到文件怎么办...
  4. python读取excel表格-python xlrd读取excel(表格)详解
  5. 方程组求解matlab实现(朴素高斯求解、LU分解、雅可比迭代方法、高斯-塞德尔方法、连续过松弛(SOR))
  6. 当心啊!仅仅50行Python,就可以在手机端看小姐姐的电脑桌面!
  7. ADO.NET Entity Framework学习笔记(2)建模[转]
  8. P7099-[yLOI2020]灼【数学期望,结论】
  9. 单元测试 问题描述_单元测试技巧:创建描述性测试
  10. mysql按字段同步_MySQL同步(一) 基础知识
  11. 基础算法 —— 递归/递推 —— 汉诺塔问题(Hanoi)
  12. 单元测试 - mock异常
  13. 14年,50%市场份额,一家中国公司何以在丹麦成功?
  14. dd命令打包多个文件_linux下如何tar打包多个并列文件夹,如:将a文件夹 b文件夹 c文件夹,打包成d.tar文件...
  15. AD库转allegro步骤
  16. gem devise
  17. 宾得的宾干微距镜头DA35mm
  18. 关于height:100%的简单理解
  19. PCB设计软件-入门
  20. 不能作为c语言常量的是115L,c语言考试选择题

热门文章

  1. java段子_Java程序员的内涵段子
  2. c语言计算100天后是星期几,用C语言试编写一个程序,输入今天是星期几,计算并输出100天后是星期几....
  3. 基于CNN的人脸 性别、年龄识别
  4. Linux 命令之 locale -- 设置和显示程序运行的语言环境
  5. 关于locale的设定
  6. data[i] is underfined
  7. Android获取u盘容量的方法,android经过usb读取U盘的方法
  8. Android call requires API level 12 的解决方案
  9. torch.Tensor
  10. 切线法(牛顿法)、割线法、抛物线法