1. 问题描述
线上某应用出问题,查看日志
这一组服务器是2台,每台都有。配置为64G,使用7G,空余内存非常多
2. 问题排查
环境变化:程序迁移到新机器,新机器是CentOS 7,程序运行账号由原来的root改为work。硬件配置由32G升级为64G。
首先切换到work账号,然后运行一个测试程序就是建立线程,发现只能跑2900多个,我的笔记本还能跑2000多个呢,这显然不对。然后在java –Xms2g 来运行,结果一样,这就说明不是JVM内存不够问题。肯定是哪里有系统限制。
测试程序代码如下:
编译代码,它会在当前目录生成一个类文件,名称就是你定义的第一个类
java 类名 即可
java –Xms2g 类名 指定内存大小
无论你是否设置2g内存,运行线程数量都上不去,这就说明不是JVM内存不过,因为线程消耗的是系统内存,那么系统内存充裕但是依然创建失败,所以一定是其他方面有问题。因为任何系统可以运行的线程数量都有有限的。在JAVA语言里,你创建一个线程,会在JVM内存创建一个内存对象的同时创建一个操作系统线程,而这个系统线程的内存不是使用JVM内存的,而是使用系统中剩下的内存建立的。也就是你给JVM内存越多,那么你能创建的线程数就越少,也就是越容易发生这个异常。
(MaxProcessMemory – JVMMemory – ResverdOsMemory)/ ThreadStackSizk = 线程数量
MaxProcessMemory:一个进程最多使用的内存大小
JVMMemory:JVM的Heap大小,也就是堆,因为你也只能设置堆的大小,这个的值是堆的最小值+PermGen的大小
ResverdOsMemory:操作系统保留的内存,也就是内核使用的,一般为120M。
ThreadStackSizk:线程栈的大小,单位为字节byte 所以上面的公式要统一换成字节来计算。系统有64G内存,就算不适用公式计算也应该知道内存是绝对够用的。那到底是什么问题?
怀疑到了系统限制,通过使用ulimit –u来查看最大进程数量是16384,这显然也很大,因为通过pstree –p | wc –l 进行统计,当前系统才几千个线程,离16384还很远。但是我是在root账号下运行的ulimt –u,突然想到不同账号会不会有不同限制,果断切换到work账号,运行ulimit –u发现结果是4096,而这时再运行pstree –p | wc –l 统计结果为3800多个。也就是说work账号可用线程数量不太多了。这也就是为什么在work账号下运行测试程序即使你更改了JVM参数结果还是一样的。这时候就基本定位到问题,然后修改系统参数
然后再次切换到work账号运行程序,发现可以跑到1.5万,线程明显上升。这时候再运行ulimt –u发现值和root账号的结果是一样的。
需要注意在/etc/secriyt/limits.cof中也可以设置这个,但是如果你在limits.d下面的配置文件也设置了同样的名称只是值不同,那么它会取2个值中最小的。
重点:虽然对work和root设置了unlimited,但是也不意味着可以无限增长,因为这个最大可用量是系统决定的,它的具体值受到物理内存页个数等限制。
解决上面问题的通常办法是:
- 使用64位系统,这样意味着可以使用更多物理内存
- 减少给JVM分配的内存,这样就有更多可用物理内存给线程,不过现在服务器级别都是64G内存起,这个JVM内存也可以不设置或者给它最多2G大小,这个取决于具体你物理机跑多少个JVM实例所决定。
- 减少单个线程的栈大小(-Xss 参数设置)
如果上面的办法不行,你就需要考虑系统限制了
说明:当某一账号所能运行的线程跑满了之后,你就无法运行任何系统命令了,它会重试几次然后最终失败,这是系统线程跑满的一个最明显信号。
3. 关于系统参数说明
查看当前系统运行的总线程数量
top -H
# 也可以通过下面的命令查看
pstree -p | wc -l
3.1 PID_MAX
系统允许的最大PID值,也就是最多有多少个进程或者线程(因为一个线程也占用一个PID)。因为0-299是系统使用,所以最大值减去300就是用户可用的数量。这个值由系统自动生成的。最大可以是32767,但是其实新系统都没有这个限制,这个32767主要是为了和老版本的Linux和Unix兼容,如果你不考虑先后兼容问题,可以设置的更大,不过太大也没有什么意义。
这个值由2个数决定,其中一个是PIDS_PER_CPU_DEFAULT值为1024,另外一个是和当前服务器CPU个数有关。
这个PID是进程号,但是Linux中线程也占用一个号。如下图:
所以如果这个可用进程号范围太小也会出现无法创建线程的情况,不过一般不会因为这个原因。如何修改呢?
# 临时修改 echo NUMBER > /proc/sys/kernel/pid_max # 永久修改 修改该文件/etc/sysctl.conf,增加如下内容 kernel.pid_max=NUMBER
3.2 thread-max
系统允许的最大线程数量
这个值最主要受到物理内存限制,这个值是这么算出来的:
# mempages 是物理内存大小 # THREAD_SIZE 就是栈大小,通过ulimit -s可以看到,默认是8M # PAGE_SIZE 内存页大小 通过 getconf PAGESIZE 查看 max_threads = mempages / (8 * THREAD_SIZE / PAGE_SIZE)
3.3. Max user processes
最大用户进数量,也就是某个用户最多可以运行多少个进程或者线程
# CentOS 7是下面的文件 /etc/security/limits/d/20-nproc.conf # CentOS 6是下面的文件 /etc/security/limits.d/90-nproc.conf
不同用户可以运行的线程数量也不同,就算你的内存很大,可用PID很多,系统允许的线程数量也非常多,但是你运行程序的账号如果被设置了很小的值也是不能超过这个值的。这个值可以计算:
这2个数应该很相近,但是我这里差别很大,是因为在/etc/security/limit.conf中设置了限制为16384,所以即便你在limit.d的20-nproc.conf针对某个用户设置了最大值也不生效,这两个文件如果设置了相同的内容则以最小值为准。看下图:
注意:ulimit -u NUMBER这个设置仅对当前shell有效,当你再打开一个shell时其值依然是之前的,以及你退出当前shell再次进入则又会恢复到之前的设置。
该文件的格式是:
USER TYPE RESOURCE NUMBER
USER:具体用户或者*表示所有用户
TYPE:soft表示当前系统生效的值、hard表示所能设置的最大值,soft不能大于hard的值,用“-”表示同时对soft和hard设置值
RESOURCE | 说明 |
core | 限定内核文件的大小 |
data | 最大数据大小 |
fsize | 最大文件大小 |
memlock | 最大锁空间大小 |
nofile | 单个进程最大打开文件数量 |
rss | 最大持久设置 |
statck | 最大栈大小 |
cpu | 以分钟为单位的CPU时间 |
nproc | 最大进程(含线程)数量 |
as | 地址空间限制 |
maxlogins | 某用户裕兴登录的最大条目 |
3.4 stack size
系统线程栈大小
这个是系统创建一个线程所消耗的内存大小,8M=8192kb。理论上最大线程数量应该小于物理内存除以8M(毕竟代码段、数据段也要占用内存以及管理线程的线程等)。如果你要想增大线程数量可以把栈大小调整小一点。
3.5 总结
一个JVM可以创建多少线程,首先由JVM设置决定(-Xms,-Xmx,-Xss),另外受到外部因素影响,就是系统设置(最大PID、最大线程、栈内存大小、最重要的还是物理内存由多少)、其二就是用户设置(用户可以运行多少个进程或线程),综合上述因素的最小值就是一个JVM可以创建多少线程。那么如何快速知道当前系统允许的最大进程或线程数量呢?
PID_MAX和thread_max这两个一般不用关心,很少有人去修改这个东西,而且就算改也会改大。另外要注意你当前的登录账户或者说运行程序的账户,这个就要去/etc/security/limit.conf和/etc/seurity/limits.d/20-nproc.conf里查看指定用户的最大进程数量也就是ulimit可以看到的,两个文件取最小值。如果最大进程数量不是问题,配置文件也没有问题,那你就要看看 /proc/sys/vm/max_map_count 这个值,如果也很大但是线程数量还是上不去你就要考虑是不是内存太小导致的。
4. 排查过程中使用到的命令
# 找到最消耗资源的线程 top –Hp `pgrep –u work java` # 查看最消耗资源的线程在干什么 jstack -l PID > /tmp/a.txt # 查看进程中线程的情况 top –H PID # 查看该进程里面有多少线程 top –H pid | wc –l # 查看系统目前运行的线程或进程总数 pstree –p | wc –l