Java进程内线程数量限制的相关学习

发布时间 2023-12-11 13:39:30作者: 济南小老虎

Java进程内线程数量限制的相关学习


背景

还是之前出现 cannot create native thread 的问题的后续
周末在家学习了下如何在容器外抓取dump.
也验证了下能否开启超过宿主机 nofile 配置的进程数量. 

想着总结一下学习到的东西, 不枉周六不午休, 周天晚上还开会到11点多. 

关于线程

Linux 里面进程是资源分配的最小单元
      现成是字段调度的最小单元. 

实际上Linux并没有实现严格意义上的进程与线程的关系.
他实际上是一个LWP 轻量级进程的 概念来实现的线程. 

java启动之后 会不停地创建一些线程来干具体的工作. 
有一些java自身使用, 有一些事具体业务使用的. 
这里想着从系统层和java层进行一些简单的学习. 

Java的线程信息

Java 启动之后其实会产生很多现成
1. Java进程号对应的线程.  这个应该是一个守护或者是单独的现成
2. 会有CPU个数的GC线程.存在. 这个与GC的算法有关系.
3. 会有一些compile的进程, 用于进行解释和编译. 而且还分为C1和C2的编译器.
4. 会有连接redis的线程, 连接数据库的线程, 以及一些logback,记录日志等的线程. 
5. 计划任务的线程, 如果有的话.
6. 还有一个核心线程 比如 http 开头 tomcat 或者是 jetty的线程池

一般情况下: 这个http类似的核心线程池的数量一般是比较重要的内容. 

关于http线程池的配置

很早之前研究过这个线程池
当时的想法是线程池的数量跟能够承载的并发数正相关. 

跟CPU有关系, 跟网络有关系, 跟数据库有关系. 

CPU多了肯定能够支撑更多的线程数量. 
但是不完全有CPU数量来决定. 
理论上应该是
CPU数量/平均每个线程一次请求需要的时间
这个线程处理的时间.  是on CPU的时间.  要排除 网络,数据库,的请求时间. 
因为线程的主动和被动的的切换是很快的 是微秒级, 每秒钟进行万级别的上下切换. 

所以理论上一个CPU可以干很多事情, 这个可以类比 单线程的redis的请求处理过程. 

但是理论上不要将CPU消耗的太多
建议至少留有 40%的资源用于 网络,io,系统监控, 调度等资源
也就是建议是 
0.6*CPU数量/(平均每个线程一次请求需要的时间+调度损耗+切换损耗等)
保证机器不要太高的CPU导致问题. 

关于线程数量的限制

Linux下线程数量的限制参数很多
通过国内最好国际最差的百度搜索引擎可以查到如下内容: 

stack_size
max_user_processes
sys.vm.max_map_count
sys.kernel.threads-max
sys.kernel.pid_max

分别解释为

stack_size

stack_size 
是一个进程/线程重建之后建立的栈区域大小
如果值太大, 那么系统支持的栈数量就会很小, 
如果值太小, 则很容易出现栈溢出的问题,导致功能不正常.
理论上机器上面能够用来存储栈的内存大小 除以 stack_size 就是可以创建进程的数量了
这个值是一个硬件限制的值, 还可能会受到其他参数的影响. 

max_user_processes

max_user_processes

可以通过 ulimit -a 或者是
ulimit -u 进行查看 可以看到一个用户级别的能够打开多少个线程信息

这个值可以你在 /etc/security/limits.conf 里面进行限制. 
需要注意一个配置文件的优先级:
专有的比全部的级别要高.
/etc/security/limits.d/
的优先级高于
/etc/security/limits.conf

sys.vm.max_map_count

max_map_count
会限制一个进程可以拥有的VMA(虚拟内存区域)的数量
这个参数也会间接影响进程能够创建的线程数量. 

经常遇到的问题是:
报错“max virtual memory areas vm.max_map_count [65530] is too low, 
increase to at least [262144]”

修改方式:
sysctl -w vm.max_map_count=262144
永久修改为:
vim /etc/sysctl.conf
vm.max_map_count=262144

sys.kernel.threads-max

该参数大致意思是,系统内核fork()允许创建的最大线程数,
在内核初始化时已经设定了此值,但是即使设定了该值,但是线程结构只能占用可用RAM page的一部分,
约1/8(注意是可用内存,即Available memory page),如果超出此值1/8则threads-max的值会减少

内核初始化时,默认指定最小值为MIN_THREADS = 20,MAX_THREADS的最大边界值是由FUTEX_TID_MASK值而约束,
但是在内核初始化时,kernel.threads-max的值是根据系统实际的物理内存计算出来的

sys.kernel.pid_max

kernel允许当前系统分配的最大PID identify,
如果kernel 在fork时hit到这个值时,
kernel会wrap back到内核定义的minimum PID identify,
意思就是不能分配大于该参数设定的值+1,该参数边界范围是全局的,属于系统全局边界

参数范围

参数名称 范围边界
kernel.pid_max 系统全局限制
kernel.threads-max 系统全局限制
vm.max_map_count 进程级别限制
/etc/security/limits.conf 用户级别限制

关于K8S模式下的限制

PodPidsLimit 参数
Kubernetes 允许你限制 Pod 中运行的进程个数。
你可以在节点级别设置这一限制, 而不是为特定的 Pod 来将其设置为资源限制。
每个节点都可以有不同的 PID 限制设置。 
要设置限制值,你可以设置 kubelet 的命令行参数 --pod-max-pids,或
者在 kubelet 的配置文件中设置 PodPidsLimit

在某些 Linux 安装环境中,操作系统会将 PID 约束设置为一个较低的默认值,例如 32768。
这时可以考虑提升 /proc/sys/kernel/pid_max 的设置值。

如果资料来自官网, 需要注意如果K8S的kubelet 限制了 pid in pod
那么在达到这个limit 时 程序会提示无法 create native thread
这是在产品内部进行 执行命令时 比如 top ls 等
会提示  cannot fork 

这个地方很容易出现坑, 需要云平台进行协助设置. 

官方关于这个参数的解释

你可以配置 kubelet 限制给定 Pod 能够使用的 PID 个数。 
例如,如果你的节点上的宿主操作系统被设置为最多可使用 262144 个 PID, 
同时预期节点上会运行的 Pod 个数不会超过 250,
那么你可以为每个 Pod 设置 1000 个 PID 的预算,避免耗尽该节点上可用 PID 的总量。 

如果管理员系统像 CPU 或内存那样允许对 PID 进行过量分配(Overcommit),
他们也可以这样做, 只是会有一些额外的风险。
不管怎样,任何一个 Pod 都不可以将整个机器的运行状态破坏。 
这类资源限制有助于避免简单的派生炸弹(Fork Bomb)影响到整个集群的运行。

一个不是总结的总结

线程池,连接池都是为了减少资源创建和销毁需要时间开销的管理方式

但既然是一种管理, 他自就会有开销

重点在于. 管理的overhead和节约的overhead 之间的比率. 
如果能够在仅仅使用 1k个CPU的cycle 进行管理的开销下能够减少 1G甚至更高的CPU开销
那么这个管理就非常值得. 

所以线程池的使用 是要很慎重的
如果每次申请了线程不进行复用, 其实没有必要创建线程池. 
必须复用的东西才可以进行池化的处理. 

换句话说, 如果你想更高的承载客户的需求, 可以将线程池的max 设置的比较大
但是这样会导致每个人都会变慢. 

一个合理的方式是在 CPU 出去必然的开销之外, 可以用于业务处理的周期内. 
插空进去最多的线程数量, 就是最优秀的参数配置.

还是那句话
这个配置跟CPU的算力有关系,跟网络,跟IO,跟数据库,跟应用的业务逻辑有关系. 
并没有一个放之四海而皆准的公式来进行计算.