![adca3f79ea364172af7b74e4ac33cec9.gif](https://i-blog.csdnimg.cn/blog_migrate/63a9f4baeb6b6feac034983a82ad061a.gif)
1、问题描述
目前云平台对容器的CPU,内存以及网络都做了限制,但是缺少对于线程数量的限制,这可能会引入一些问题。比如某些容器上的服务,大量的创建线程,会耗光系统资源,导致系统无法新建线程,CPU负载过高等问题。
通过跟进社区的releasenotes,发现K8S1.10+docker1.11之后开始支持限制容器创建的进程数, 社区采用的方式是使用pids子系统限制创建的进程数量,但是没有实现实时感知的机制, 而K8S 1.7+docker1.10没有该功能的支持。如果部署在容器上的应用,代码编写不规范,大量的创建线程,可能会导致系统的pid(Linux上的线程可以认为是使用进程模拟的)耗尽,进而导致宿主机无法创建子进程的问题,使得整个宿主机处于一个很危险的状态。
针对上述问题,我们计划设计一个通用的方案,能够对K8S1.10+docker1.11之前的版本提供限制容器线程数量的功能,同时提供实时感知容器线程数触达上线的机制。
2、问题复现
import time import os from multiprocessing import Process s = [] def func(i): print 'hello', i time.sleep(1000) def main(): for i in range(50000): p = Process(target=func, args=(i,)) s.append(p) for p in s: p.start() time.sleep(2000) main() |
基于1中的调研,我们找了两台机器做实验,上面设置的kernel.pid_max = 40960,即系统允许创建的最大进程数是40960,我们又写了一个python脚本不断去创建进程,看宿主机是会出现1中描述的问题。
通过运行上面的脚本,发现当进程创建到27000的时候,执行docker命令就会很卡,当进程数达到kernel.pid_max定义上限的时候,已经耗尽系统的pid资源,同时报出报出fork:retry: Resource temporarily unavailable的错误。
3、解决方案
针对2中潜在的问题,我们讨论了该问题的可行方案,最终确定如下:
1)调大kernel.pid_max,允许系统创建更多的进程。
2)限制单个容器创建的线程数量。
3.1 kernel.pid_max参数
kernel.pid_max是内核允许系统创建的最大进程数,这个值在64位机上最大可设置为 4194304(4M)。目前从4.18的代码来看,调大后的影响是内核在维护PID时要多用一些内存,分配PID时要花更多一点时间,这些与进程本身的消耗相比,可以忽略。 内核的推荐值是cpu数*1024,也就是说内核认为一个CPU最多可以应对1024个进程,如果一台机器有40个CPU,那 40*1024 = 40960。但根据当前云平台宿主机的实际情况,内核推荐的设置已经不适用于58云平台宿主机。所以从稳定性的角度,结合58云平台的业务,建议将宿主机的kernel.pid_max设置成1048576 (1M)。 kernel.pid_max仅仅是调大了系统允许创建的进程/线程数,这并没有从根本上解决问题,单个容器依然可能创建很多的进程/线程,我们需要通过cgroup的pids子系统限制每个容器启动的最大进程/线程数。 3.2 解决方案设计 经过调研,我们采用cgroup pids子系统 + inotify的方式,限制容器启动的进程/线程数量。3.2.1 cgroup pids子系统
CGroup 是 Control Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组 (process groups) 所使用的物理资源 (如cpu, memory, I/O 等等) 的机制,它是容器限制资源的基础。 CGroup 是将任意进程进行分组化管理的 Linux 内核功能。CGroup 本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。这些具体的资源管理功能称为 CGroup 子系统或控制器。 在Linux Kernel 4.3中,引入了一个新的cgroups子系统pids,通过这个子系统,可以实现对某个CGroup中进程和线程的总数进行限制。如下图所示:
3.2.2 inotify
inotify是Linux中用于监控文件系统变化的一个框架,不同于前一个框架dnotify, inotify可以实现基于inode的文件监控。 也就是说监控对象不再局限于目录,也包含了文件。 不仅如此,在事件的通知方面,inotify摈弃了dnotify的信号方式,采用在文件系统的处理函数中放置hook函数的方式实现。 inotify 提供一个简单的 API,使用最小的文件描述符,并且允许细粒度监控。 与inotify 的通信是通过系统调用实现。 主要的API如下: inotify_init 用于创建一个 inotify 实例的系统调用,并返回一个指向该实例的文件描述符。 inotify_add_watch 增加对文件或者目录的监控,并指定需要监控哪些事件。 标志用于控制是否将事件添加到已有的监控中,是否只有路径代表一个目录才进行监控,是否要追踪符号链接,是否进行一次性监控,当首次事件出现后就停止监控。 inotify_rm_watch 从监控列表中移出监控项目。 read 读取包含一个或者多个事件信息的缓存。 close 关闭文件描述符,并且移除所有在该描述符上的所有监控。 当关于某实例的所有文件描述符都关闭时,资源和下层对象都将释放,以供内核再次使用。 由3.2.1中pids子系统的机制可知,我们可以通过inotify_add_watch和read监控pid.events文件的变化,可以感知CGroup的进程是否触达pid.max定义的上限,并进行后续的处理。4、总结
总体来说,上述方案很好的解决了限制容器启动线程数量的问题,并能够提供实时感知容器触达线程上限的机制。从本次方案设计的过程中,我们发现平台还存在一些潜在的问题。我们需要改进监控和报警系统,尽早发现潜在的问题并解决。我们也会持续关注社区演进,同时会将我们比较好的功能提交到社区。