44

云平台的容器线程数量限制机制

 4 years ago
source link: https://www.tuicool.com/articles/EVrm6je
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

58云计算平台(以下简称云平台)是TEG-架构线基于Kubernetes + Docker的私有云(Kubernetes简写为K8S),旨在为集团内部提供一套业务实例管理平台。58云平台具有简单,轻量的特点,能够高效利用物理资源,更快的部署和统一规范的标准化运行环境,通过云平台,实现服务标准化,上线流程规范化,资源利用合理化。Docker对于容器的CPU,内存做了限制,云平台对于容器的网络进行了限制,但是缺少对于线程数量的限制,本文针对如何限制容器线程数量进行讨论。 如果你想和更多Kubernetes技术专家交流,可以加我微信liyingjiese,备注『加群』。群里每周都有全球各大公司的最佳实践以及行业最新动态

1、问题描述

目前云平台对容器的CPU,内存以及网络都做了限制,但是缺少对于线程数量的限制,这可能会引入一些问题。比如某些容器上的服务,大量的创建线程,会耗光系统资源,导致系统无法新建线程,CPU负载过高等问题。

通过跟进社区的releasenotes,发现Kubernetes 1.10 + Docker 1.11之后开始支持限制容器创建的进程数,社区采用的方式是使用pids子系统限制创建的进程数量,但是没有实现实时感知的机制, 而Kubernetes 1.7 + Docker 1.10没有该功能的支持。如果部署在容器上的应用,代码编写不规范,大量的创建线程,可能会导致系统的pid(Linux上的线程可以认为是使用进程模拟的)耗尽,进而导致宿主机无法创建子进程的问题,使得整个宿主机处于一个很危险的状态。

针对上述问题,我们计划设计一个通用的方案,能够对Kubernetes 1.10 + Docker 1.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中,引入了一个新的CGroup子系统pids,通过这个子系统,可以实现对某个CGroup中进程和线程的总数进行限制。如下图所示:

VF3iyq6.png!web

其中,pids.max控制该组中最多可以拥有的进程数(也可以用来限制线程数)。pids.current存储了当前CGroup的进程(线程)总数。cgroup.procs是需要限制的进程pid列表。pid.events记录CGroup触发进程上限的次数。

Facebook在2016年向内核提交了一个patch(135b8b),实现了pid.events的功能。当CGroup尝试fork新进程的时候,会调用pids_can_fork判断是否可以fork,如果不能,会通过cgroup_file_notify触发pid.events的事件。

内核推荐的宿主机机进程数上限为:CPU核数*1024,云平台的容器也将使用这一内核推荐值,即随着核数的增加,允许的最大线程上限也随之增加。考虑到部分云实例的CPU核数较少,如按照上述方式配置,部分服务会受影响,容器核数不足6核的,按照6核计算。

通过cgroup pids子系统可以有效的限制容器启动的线程数量,但是当容器创建的线程触达上限的时候,会使得容器处于不可预知的状态,我们需要通过inotify实时检测pids.events文件的变化,感知容器线程数量是否触达上限。

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、总结

总体来说,上述方案很好的解决了限制容器启动线程数量的问题,并能够提供实时感知容器触达线程上限的机制。从本次方案设计的过程中,我们发现平台还存在一些潜在的问题。我们需要改进监控和报警系统,尽早发现潜在的问题并解决。我们也会持续关注社区演进,同时会将我们比较好的功能提交到社区。

原文链接: https://mp.weixin.qq.com/s/f43hbIoxxNQy3qJvoL8kLg


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK