K8s代码贡献浙江大学排名第六

全球企业Kubernetes开源社区代码贡献排名:浙江大学SEL实验室排名第六。请看来自Stackalytics|Kubernetes community contribution官方网站截图。 1   从全球企业的贡献排名来看,排名状况基本变化不大,仍然是Google独占鳌头。令人欣喜的是,浙江大学SEL实验室排名又上升了一位,代码贡献量近24万行,总排名超越CoreOS达到世界第六位。这无疑是振奋人心的事情,这意味着越来越多的年轻学术人员投入Kubernetes开源社区建设,这将近一步提高中国在容器云社区的地位。   Kubernetes是当前容器集群编排3大开源方案中社区活跃度最高的。虽然现在正值入冬,但仍然挡不住 Kubernetes掀起全球关注的热潮。Kubernetes体现了Google对海量容器集群管理的经验和精华,这让Kubernetes在容器云计算框架横行天下的年代游刃有余,已经逐步成为了容器云框架中的佼佼者。从Kubernetes开放、自由、持续发展生态圈兼容的态度能看出,Kubernetes正变得越来越自信。   下图为Kubernetes模块代码贡献分析图: 2

从图中可知:Kubernetes运维和生态圈组件模块代码贡献直追核心模块kubernetes,排名2-4位的模块分别是Kubernetes生态圈、操作和界面。Kubernetes使用者越来越关心容器云框架落地而非仅仅将其作为空中楼阁远观而止。  Kubernetes在国内发展已经超过了3年的时间,但是遗憾的一点,尽管我们拥有世界上最多的开发人员,但是我们常常对社区没有话语权,国内的用户的需求无法对社区上游形 成影响,导致很多本地化定制的需求无法真正的在社区版本代码得到体现。所以如何让中国的声音出现在社区,是我们所有容器云计算人需要思考的问题。更希望越来越多的国内传统IT巨头能够意识到这个问题,投身于开源的事业中。

《Docker容器与容器云》第2版推荐

自Docker容器与容器云第1版出版以来,销量达到10000多本,得到了广大技术人员的认可,并且翻译成繁体,进入台湾市场。本书对Docker和Kubernetes的源码解析深入细致,是国内Docker界的良心之作。

经过作者们多年的实践经验积累及近一年的精心准备,浙江大学SEL实验室出版的《Docker容器与容器云》第2版,终于与我们见面了。

本书根据Docker 1.10版和Kubernetes 1.2版对第1版进行了全面更新,从实践者的角度出发,以Docker和Kubernetes为重点,沿着“基本用法介绍”到“核心原理解读”到“高级实践技巧”的思路,一本书讲透当前主流的容器和容器云技术,有助于读者在实际场景中利用Docker容器和容器云解决问题并启发新的思考。全书包括两部分,第一部分深入解读Docker容器技术,包括Docker架构与设计、核心源码解读和高级实践技巧;第二部分归纳和比较了三类基于Docker的主流容器云项目,包括专注Docker容器编排与部署的容器云、专注应用支撑的容器云以及一切皆容器的Kubernetes,进而详细解读了Kubernetes核心源码的设计与实现,最后介绍了几种典型场景下的Kubernetes最佳实践。

自本书第1版出版以来,容器生态圈已经发生了翻天覆地的变化。新的开源项目层出不穷,各个开源项目都在快速迭代演进。Docker已经从本书第1版里的1.6.2发展为当前的1.10。Kubernetes也从本书第1版里的0.16发展到了现在的1.2,并且在1.0.1版本时宣布其已经正式进入可投入生产环境(production ready)的状态。 

第3章是本书第一部分的重点。Docker 1.10版相对于本书第1版中的1.6.2版,主要的更新包括如下几个方面:

1、Docker在架构方面不断将自身解耦,逐步发展成容器运行时(runtime)、镜像构建(builder)、镜像分发(distribution)、网络(networking)、数据卷(volume)等独立的功能组件,提供daemon来管理,并通过Engine暴露一组标准的API来操作这些组件(详见本书3.2节);

2、将网络和数据卷提升为“一等公民”,提供了独立子命令进行操作,网络和数据卷具备独立的生命周期,不再依赖容器的生命周期(详见本书3.7节、3.8节);

3、网络实现方面,Docker将网络相关的实现解耦为独立的组件libnetwork,抽象出一个通用的容器网络模型(CNM),功能上也终于原生支持了跨主机通信(详见本书3.8节);

4、在扩展性方面,在1.7.0版本后就开始支持网络、volume和存储驱动(仍处于实验阶段)的插件化,开发者可以通过实现Docker提供的插件标准来定制自己的插件(详见本书3.6节、3.7节、3.8节);

5、在Docker安全方面,Docker支持了user namespace和seccomp来提高容器运行时的安全,在全新的镜像分发组件中引入可信赖的分发和基于内容存储的机制,从而提高镜像的安全性(详见本书3.5节、3.6节、3.9节)。

需要特别指出的一点是,随着容器如火如荼的发展,为了推动容器生态的健康发展,促进生态系统内各组织间的协同合作,容器的标准化也显得越来越重要。Linux基金会于2015年6月成立OCI(Open Container Initiative)组织,并针对容器格式和运行时制定了一个开放的工业化标准,即OCI标准。Docker公司率先贡献出满足OCI标准的容器运行时runC,HyperHQ公司也开源了自己的OCI容器运行时runV,相信业界会有越来越多的公司加入这个标准化浪潮中。Docker公司虽然没有在Docker 1.10版本中直接使用runC作为容器的运行时,但是已经将“修改Docker engine来直接调用runC的二进制文件为Docker提供容器引擎”写入到了1.10版本的roadmap中。本书在3.4.3节中对runC的构建和使用进行了介绍。   第8章是本书第二部分的重点。由于Kubernetes的代码始终处于积极更新之中,自本书第1版截稿以来,Kubernetes又相继发布了0.17、0.18、0.19、0.20、0.21、1.0、1.1与1.2等几个版本。主要的更新包括如下几个方面:

1、大大丰富了支撑的应用运行场景。从全面重构的long-running service的replicaSet,到呼声渐高的支持batch job的Job、可类比为守护进程的DaemonSet、负责进行应用更新的Deployment、具备自动扩展能力的HPA(Horizontal Pod Autoscaler),乃至于有状态服务的petSet,都已经或者即将涵盖在Kubernetes的支撑场景中(详见本书8.2节)。

2、加强各个组件的功能扩展或者性能调优。apiserver和controller manager为应对全新的resource和API有显著的扩展;scheduler也在丰富调度策略和多调度器协同调度上有积极的动作;kubelet在性能上也有长足的进步,使得目前单个节点上支持的pod从原来的30个增长到了110个,集群工作节点的规模也从100个跃升为1000个;多为人诟病的kube-proxy如今也鸟枪换炮,默认升级为iptables模式,在吞吐量上也更为乐观;在可以预期的未来,rescheduler将成为Kubernetes家庭中的新成员,使得重调度成为可能(详见本书8.3节);

3、兼容更多的容器后端模型、网络及存储方案。从Docker到rkt,Kubernetes展示了对容器后端开放姿态,同时它还准备以C/S模式实现对其他容器的支撑。在网络方面,Kubernetes引入了网络插件,其中最为瞩目的当属CNI;存储上的解决方案更是层出不穷,flocker、Cinder、CephFS不一而足,还增加了许多特殊用途的volume,如secret、configmap等(详见本书8.4节、8.5节);

4、增加了OpenID、Keystone等认证机制、Webhook等授权机制,以及更为丰富的多维资源管理机制admissioncontroller(详见本书8.6节);

5、另外,作为Kubernetes社区的积极参与者,我们还专门增加了8.8节,讨论当前社区正在酝酿中的一些新特性,如Ubernetes、petSet、rescheduler。我们还讨论了Kubernetes性能优化,以及Kubernetes与OCI的关系等话题。 

除了全面更新这两个重点章节之外,我们还在第1章中更新了Docker近期的“大事记”并重新整理了容器生态圈,加入了许多重要的容器云技术开源项目,以及OCI、CNCF等国际标准化组织;在第2章中,我们将Docker命令行工具的基础用法更新到了Docker 1.10版;在第4章中完善了对时下火热的“容器化思维”和“微服务”的讨论;在第6章中更新了对Docker“三剑客”——Compose、Swarm和Machine的讨论;在附录中以Docker 1.10版为标准更新了附录A的Docker安装指南,以Kubernetes 1.2为标准,更新了附录F中Kubernetes的安装指南。

如果你是初级程序员,本书可以帮助你熟悉Docker与kubernetes的基本使用;如果你正在IT领域进行高级进阶修炼,那本书也可以与你一起探索Docker与kubernetes的工作原理。无论是架构师、开发者、运维人员,还是对Docker比较好奇的读者,本书都是一本不可多得的带你从入门向高级进阶的精品书,值得大家选择!

docker2

 

最后,摘录一些读者的推荐如下——

 —— 许式伟,七牛云存储CEO : “虽然在此之前已经有了由Docker团队出的第一本Docker书,但是这是国内第一本深入解读Docker与Kubernetes原理的原创图书,这一点意义重大。本书比较完整地介绍了Docker与Kubernetes的工作原理和生态,非常有借鉴意义。” 

——肖德时,数人科技CTO: “Docker容器技术已经在国内如火如荼地流行起来,浙江大学SEL实验室目前是国内掌握Docker技术最熟练的技术团队之一,他们在国内Docker技术界一直产生着重要影响。这次他们把Docker的实战经验汇编成书,可以帮助更多的Docker爱好者学习到一手的实战经验。”

——程显峰,火币网CTO : “本书非常细致地讲解了Docker技术的来龙去脉和技术细节,更为难得是还加入了Docker生态当中的其他技术。Docker这项技术本身就是将多种思想和技术融合的产物,从生态的视角去解读技术的来龙去脉将极大地促进读者对云计算和容器技术的重新思考。”

—— 刘俊,百度运维部高级架构师,两次百度最高奖获得者 : “本书宏观上描绘了容器和容器云技术发展的浪潮和生态系统,微观上以Docker和Kubernetes为典型进行了深度分析。无论是Docker技术爱好者,还是系统架构师、云端开发者、系统管理和运维人员,都能在本书中找到适合自己阅读的要点。浙江大学SEL实验室云计算团队是一支非常优秀的云计算研究团队,很多85后、90后人才活跃在顶级社区前沿,感谢他们能将多年的知识和智慧积累分享出来!”

——郝林,微影时代架构师,《Go并发编程实战》作者 : “本书是浙江大学SEL实验室云计算团队多年深耕Docker及背后的容器技术的结晶。最大的特点就是深入,并且有各种实用案例和细致讲解。另外,这本书在怎样真正地把Docker及周边产品落地以构建灵活多变的云平台方面也进行了生动的阐释。”   ——网友 Monster-Z: “自本书第一版出版以来,Docker社区又经过了如火如荼的发展,特别是网络部分的实现已经发生了翻天覆地的变化,而本书在第一版的基础之上及时地对网络部分的内容进行了更新。本书对于docker网络最新内容深入透彻的分析,让我们这些急于想要对Docker网络部分实现原理有所了解的开发人员如醍醐灌顶。相信对Docker技术感兴趣的读者在读完本书之后会与我有相同的感受。”

——网友 XiaoYu: “之前对于浙江大学的SEL实验室早有耳闻,也深知他们在云计算领域有着深厚的积淀。这次对他们著作的第二版进行了深入的研读,可以看得出,在保持了第一版架构的同时,对各个章节的内容又进行了大量的扩充,紧紧跟随着Docker以及k8s社区的发展步伐。相信不论是对于初涉容器领域的新人,还是希望深入理解Docker容器实现原理的开发人员,这都是一本不可多得的好书。” 

——网友 Mongo: “在学习了本书的第一版之后,就一直期待着本书第二版的出版。于是当得到第二版出版的消息之后,就迫不及待入手了一本。捧读之后,发现在内容上与第一版有了较大的变化,这应该与Docker本身快速的发展有关,当然也从侧面反应了容器技术在时下的热度。相信对于像我这样Docker技术的一线使用者来说,本书固有的深度以及第二版的时效性都将让我们对Docker的使用和理解都更上一层楼。”

——网友 YaoDD: “之前在学习k8s的过程中,一直苦于没有深入到代码层面的书籍可供参考。国内出版的大多数图书更多的是对官方文档的”翻译”,而并没有进一步深入的解读。直到发现了浙江大学SEL实验室出版的《Docker容器与容器云》一书,其中对k8s的各个组件做到了深入源码级的原理分析,让人直呼过瘾。所以在该书第二版出版的第一时间就购买并阅读了,发现在保持第一版分析深度的同时,又加入了许多社区最新的研发成果。如果你想对k8s有着更加深入的理解,那么本书将是你的不二选择。”    ——网友 DockerLover: “作为一名Kubernetes企业级应用的一线实践者,早就对浙大的SEL实验室慕名已久。该实验室的研究人员为Kubernetes社区共享了大量的代码,数量上堪比华为等巨头企业,尤其是其中的张磊博士,更是Kubernetes的member,技术实力可见一斑。因此本书不论是深度广度,还是对于学习容器技术的指导意义都是毋庸置疑的。” 

——网友 GaryRong: “自Docker出现以来,一股容器热潮已经席卷了整个互联网,如火如荼,方兴未艾。而浙大SEL实验室在云计算和PaaS领域深耕多年,作为docker技术在国内第一批的学习者和实践者,积累了丰富的经验。而出版的《Docker 容器与容器云》从第一版到第二版,对低到底层容器技术的实现,高到整个容器生态圈的发展都有着独到而深刻的理解。相信本书一定不会让你失望。 ” 

《Docker容器与容器云》推荐

《Docker容器与容器云》是Kubernetes社区核心开发者张磊博士及其技术团队近年来PaaS方面积累,全书不仅描述了容器与容器云技术,还融入了实验室四年来对云计算各个层面的理解。该书是国内第一本对Docker原理进行深度解析的书籍,也是第一本结合PaaS对各类容器云进行深度剖析,并着重深入分析Kubernetes原理的书籍。

该书从2014年12月开始写作到2015年9月正式出版发行,期间数易其稿,从最初的对源码进行逐字逐句的分析,转变为带着问题去思考,最后再回到源码去理解问题背后的本质,书的每一章每一节都是实验室智慧与汗水的结晶。

出版发行以来,我们几乎没有对本书进行特别的宣传,凭着少数几次活动以及朋友圈里朋友们的口口相传,逐渐被广大云计算爱好者们发现,并受到了业内外读者的一致好评。从九月初至今,不到两个月的时间,第一次印刷的三千五百册就已接近全部售罄,准备进行第二次印刷。我们也会尽可能多的根据读者的反馈,对第一次印刷中存在的问题进行勘误。

我们自己没有太多权利评判书本本身的好坏,但是我们看到了业内外读者的评价,让我们感受到了深深的肯定,非常感谢大家的支持!同时也向更多朋友推荐我们这本《Docker容器与容器云》,这绝对是您在容器技术方面最值得一读的书!

Docker 容器与容器云-

最后,摘录一些读者的推荐如下:

  • “虽然在此之前已经有了由Docker团队出的第一本Docker书,但是这是国内第一本深入解读Docker与Kubernetes原理的原创图书,这一点意义重大。本书比较完整地介绍了Docker与Kubernetes的工作原理和生态,非常有借鉴意义。” ——许式伟,七牛云存储CEO

  • “Docker容器技术已经在国内如火如荼地流行起来,浙江大学SEL实验室目前是国内掌握Docker技术最熟练的技术团队之一,他们在国内Docker技术界一直产生着重要影响。这次他们把Docker的实战经验汇编成书,可以帮助更多的Docker爱好者学习到一手的实战经验。”——肖德时,数人科技CTO

  • “本书非常细致地讲解了Docker技术的来龙去脉和技术细节,更为难得是还加入了Docker生态当中的其他技术。Docker这项技术本身就是将多种思想和技术融合的产物,从生态的视角去解读技术的来龙去脉将极大地促进读者对云计算和容器技术的重新思考。”——程显峰,OneAPM首席运营官

  • “本书宏观上描绘了容器和容器云技术发展的浪潮和生态系统,微观上以Docker和Kubernetes为典型进行了深度分析。无论是Docker技术爱好者,还是系统架构师、云端开发者、系统管理和运维人员,都能在本书中找到适合自己阅读的要点。浙江大学SEL实验室云计算团队是一支非常优秀的云计算研究团队,很多85后、90后人才活跃在顶级社区前沿,感谢他们能将多年的知识和智慧积累分享出来!”——刘俊,百度运维部高级架构师,百度最高奖获得者

  • “本书是浙江大学SEL实验室云计算团队多年深耕Docker及背后的容器技术的结晶。最大的特点就是深入,并且有各种实用案例和细致讲解。另外,这本书在怎样真正地把Docker及周边产品落地以构建灵活多变的云平台方面也进行了生动的阐释。”——郝林,微影时代架构师,《Go并发编程实战》作者

  • “Docker颠覆了容器技术,也将容器技术带到了新的高度。InfoQ从2014年初就开始密切关注容器技术,见证并切身参与了容器技术的发展。作为我们的优秀作者,浙江大学SEL实验室在InfoQ撰写了很多与Docker、Kubernetes相关的技术文章,得到了广大读者的肯定。希望这本书能推动容器技术在中国的落地。”——郭蕾,InfoQ主编

  • “浙江大学SEL实验室属于国内较早接触并研究开源PaaS技术的团队之一,从传统PaaS的开源代表CloudFoundry、OpenShift,到新一代基于Docker的PaaS平台如DEIS、Flynn等,他们均有深入的研究和实践经验。更为难得的是,他们不仅参与开源贡献,而且笔耕不辍,通过博客、论坛等方式积极分享有深度、有内涵的技术文章,并广泛参与国内PaaS届各种技术交流会议。华为PaaS团队也在与之交流中汲取了不少营养。此次,他们将近年来对Docker容器和Kubernetes、DEIS、Flynn等PaaS开源平台的研究成果结集成册,内容详尽且深入浅出。我相信,无论是入门者还是老手,都能够从中获益。”——刘赫伟,华为中央软件院

  • “容器技术在大型互联网企业中已广泛应用,而Docker是容器技术中的杰出代表。本书不仅介绍了Docker基础知识,而且进行了代码级的深入分析,并通过对Kubernetes等技术的讲解延伸至集群操作系统以及对Docker生态领域的思考,同时结合了大量实践,内容丰富,值得拥有。”——王炜煜,百度运维部高级架构师,JPaaS项目负责人

  • “Docker作为操作系统层面轻量级的虚拟化技术,凭借简易的使用、快速的部署以及灵活敏捷的集成支持等优势,奠定了Docker如今在PaaS领域的江湖地位。 浙江大学SEL实验室在云计算和 PaaS领域耕耘多年, 积累了丰富的经验。本书既有对Docker源代码层面的深度解读,也有实战经验的分享,希望这本书能够帮助Docker开发者在技术上更上一层楼。”——李三红,蚂蚁金服基础技术部JVM Architect

  • 本书覆盖面非常广,从docker的使用、核心原理、高级实践再到编排工具以及著名的集群调度工具kubernetes均有涉及,很好的把握了技术人员的痛点。而且架构原理方面均是用docker1.7和部分1.6作为依据,非常有时效性。《Docker源码分析》的作者也是出于该团队,容器云方面功底很深厚啊。 ——网友Crazykev

  • 比较专业的书,书中的内容远远高于这本书的物理容量,希望好好研究Docker和Linux某些虚拟化机制的同学,可以以这本书入门去不断扩充自己的知识。不过这边毕竟是一Docker为主线的书,主要还是为了深入帮助大家理解Docker,以及Docker生态圈内的应用和服务。一句话:国内不可多得的一本Docker相关知识的书,此书讲解深入,排版合理,内容紧凑,值得好好看。 ——网友RHMAN

  • 其实之前不太了解SEL实验室的背景是什么,不过从此书的内容来看,团队的研究能力和文字能力都相当不错。好久没有读过让人“爽快”的技术书籍了。在Docker应用中各领域的知识要点都能提纲挈领的进行讲解,而且对于生产环境的挑战也有深刻的理解,算得上是既有广度又有深度的佳作。 ——网友jerryshang

Docker背后的标准化容器执行引擎——runC

文 孙健波

随着容器技术发展的愈发火热,Linux基金会于2015年6月成立OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。该组织一成立便得到了包括谷歌、微软、亚马逊、华为等一系列云计算厂商的支持。而runC就是Docker贡献出来的,按照该开放容器格式标准(OCF, Open Container Format)制定的一种具体实现。

1. 容器格式标准是什么?

制定容器格式标准的宗旨概括来说就是不受上层结构的绑定,如特定的客户端、编排栈等,同时也不受特定的供应商或项目的绑定,即不限于某种特定操作系统、硬件、CPU架构、公有云等。

该标准目前由libcontainerappc的项目负责人(maintainer)进行维护和制定,其规范文档就作为一个项目在Github上维护,地址为https://github.com/opencontainers/specs

1.1 容器标准化宗旨

标准化容器的宗旨具体分为如下五条。

  • 操作标准化:容器的标准化操作包括使用标准容器感觉创建、启动、停止容器,使用标准文件系统工具复制和创建容器快照,使用标准化网络工具进行下载和上传。

  • 内容无关:内容无关指不管针对的具体容器内容是什么,容器标准操作执行后都能产生同样的效果。如容器可以用同样的方式上传、启动,不管是php应用还是mysql数据库服务。

  • 基础设施无关:无论是个人的笔记本电脑还是AWS S3,亦或是Openstack,或者其他基础设施,都应该对支持容器的各项操作。

  • 为自动化量身定制:制定容器统一标准,是的操作内容无关化、平台无关化的根本目的之一,就是为了可以使容器操作全平台自动化。

  • 工业级交付:制定容器标准一大目标,就是使软件分发可以达到工业级交付成为现实。

1.2 容器标准包(bundle)和配置

一个标准的容器包具体应该至少包含三块部分:

  • config.json: 基本配置文件,包括与宿主机独立的和应用相关的特定信息,如安全权限、环境变量和参数等。具体如下:
    • 容器格式版本
    • rootfs路径及是否只读
    • 各类文件挂载点及相应容器内挂载目录(此配置信息必须与runtime.json配置中保持一致)
    • 初始进程配置信息,包括是否绑定终端、运行可执行文件的工作目录、环境变量配置、可执行文件及执行参数、uid、gid以及额外需要加入的gid、hostname、低层操作系统及cpu架构信息。
  • runtime.json: 运行时配置文件,包含运行时与主机相关的信息,如内存限制、本地设备访问权限、挂载点等。除了上述配置信息以外,运行时配置文件还提供了“钩子(hooks)”的特性,这样可以在容器运行前和停止后各执行一些自定义脚本。hooks的配置包含执行脚本路径、参数、环境变量等。
  • rootfs/:根文件系统目录,包含了容器执行所需的必要环境依赖,如/bin/var/lib/dev/usr等目录及相应文件。rootfs目录必须与包含配置信息的config.json文件同时存在容器目录最顶层。

1.3 容器运行时和生命周期

容器标准格式也要求容器把自身运行时的状态持久化到磁盘中,这样便于外部的其他工具对此信息使用和演绎。该运行时状态以JSON格式编码存储。推荐把运行时状态的json文件存储在临时文件系统中以便系统重启后会自动移除。

基于Linux内核的操作系统,该信息应该统一地存储在/run/opencontainer/containers目录,该目录结构下以容器ID命名的文件夹(/run/opencontainer/containers/<containerID>/state.json)中存放容器的状态信息并实时更新。有了这样默认的容器状态信息存储位置以后,外部的应用程序就可以在系统上简便地找到所有运行着的容器了。

state.json文件中包含的具体信息需要有:

  • 版本信息:存放OCI标准的具体版本号。
  • 容器ID:通常是一个哈希值,也可以是一个易读的字符串。在state.json文件中加入容器ID是为了便于之前提到的运行时hooks只需载入state.json就可以定位到容器,然后检测state.json,发现文件不见了就认为容器关停,再执行相应预定义的脚本操作。
  • PID:容器中运行的首个进程在宿主机上的进程号。
  • 容器文件目录:存放容器rootfs及相应配置的目录。外部程序只需读取state.json就可以定位到宿主机上的容器文件目录。

标准的容器生命周期应该包含三个基本过程。

  • 容器创建:创建包括文件系统、namespaces、cgroups、用户权限在内的各项内容。
  • 容器进程的启动:运行容器进程,进程的可执行文件定义在的config.json中,args项。
  • 容器暂停:容器实际上作为进程可以被外部程序关停(kill),然后容器标准规范应该包含对容器暂停信号的捕获,并做相应资源回收的处理,避免孤儿进程的出现。

1.4 基于开放容器格式(OCF)标准的具体实现

从上述几点中总结来看,开放容器规范的格式要求非常宽松,它并不限定具体的实现技术也不限定相应框架,目前已经有基于OCF的具体实现,相信不久后会有越来越多的项目出现。

  • 容器运行时opencontainers/runc,即本文所讲的runc项目,是后来者的参照标准。
  • 虚拟机运行时hyperhq/runv,基于Hypervisor技术的开放容器规范实现。
  • 测试huawei-openlab/oct基于开放容器规范的测试框架。

2. runC工作原理与实现方式

runC的前身实际上是Docker的libcontainer项目,笔者曾经写过一篇文章《Docker背后的容器管理——Libcontainer深度解析》专门对libcontainer进行源码分析和解读,感兴趣的读者可以先阅读一下,目前runC也是对libcontainer包的调用,libcontainer包变化并不大。所以此文将不再花费太多笔墨分析其源码,我们将着重讲解其中的变化。

2.1 runC从libcontainer的变迁

从本质上来说,容器是提供一个与宿主机系统共享内核但与系统中的其他进程资源相隔离的执行环境。Docker通过调用libcontainer包对namespaces、cgroups、capabilities以及文件系统的管理和分配来“隔离”出一个上述执行环境。同样的,runC也是对libcontainer包进行调用,去除了Docker包含的诸如镜像、Volume等高级特性,以最朴素简洁的方式达到符合OCF标准的容器管理实现。

总体而言,从libcontainer项目转变为runC项目至今,其功能和特性并没有太多变化,具体有如下几点。

  1. 把原先的nsinit移除,放到外面,命令名称改为runc,同样使用cli.go实现,一目了然。
  2. 按照开放容器标准把原先所有信息混在一起的一个配置文件拆分成config.jsonruntime.json两个。
  3. 增加了按照开放容器标准设定的容器运行前和停止后执行的hook脚本功能。
  4. 相比原先的nsinit时期的指令,增加了runc kill命令,用于发送一个SIG_KILL信号给指定容器ID的init进程。

总体而言,runC希望包含的特征有:

  • 支持所有的Linux namespaces,包括user namespaces。目前user namespaces尚未包含。
  • 支持Linux系统上原有的所有安全相关的功能,包括Selinux、 Apparmor、seccomp、cgroups、capability drop、pivot_root、 uid/gid dropping等等。目前已完成上述功能的支持。
  • 支持容器热迁移,通过CRIU技术实现。目前功能已经实现,但是使用起来还会产生问题。
  • 支持Windows 10 平台上的容器运行,由微软的工程师开发中。目前只支持Linux平台。
  • 支持Arm、Power、Sparc硬件架构,将由Arm、Intel、Qualcomm、IBM及整个硬件制造商生态圈提供支持。
  • 计划支持尖端的硬件功能,如DPDK、sr-iov、tpm、secure enclave等等。
  • 生产环境下的高性能适配优化,由Google工程师基于他们在生产环境下的容器部署经验而贡献。
  • 作为一个正式真实而全面具体的标准存在!

2.2 runC是如何启动容器的?

从开放容器标准中我们已经定义了关于容器的两份配置文件和一个依赖包,runc就是通过这些来启动一个容器的。首先我们按照官方的步骤来操作一下。

runc运行时需要有rootfs,最简单的就是你本地已经安装好了Docker,通过

docker pull busybox

下载一个基本的镜像,然后通过

docker export $(docker create busybox) > busybox.tar

导出容器镜像的rootfs文件压缩包,命名为busybox.tar。然后解压缩为rootfs目录。

mkdir rootfs
tar -C rootfs -xf busybox.tar

这时我们就有了OCF标准的rootfs目录,需要说明的是,我们使用Docker只是为了获取rootfs目录的方便,runc的运行本身不依赖Docker。

接下来你还需要config.jsonruntime.json,使用

runc spec 可以生成一份标准的config.jsonruntime.json配置文件,当然你也可以按照格式自己编写。

如果你还没有安装runc,那就需要按照如下步骤安装一下,目前runc暂时只支持Linux平台。

`\

create a ‘github.com/opencontainers’ in your GOPATH/src

cd github.com/opencontainers git clone https://github.com/opencontainers/runc cd runc make sudo make install `\

最后执行

runc start

你就启动了一个容器了。

可以看到,我们对容器的所有定义,均包含在两份配置文件中,一份简略的config.json配置文件类似如下,已用省略号省去部分信息,完整的可以查看官方github

{
"version": "0.1.0",
"platform": {
"os": "linux",
"arch": "amd64"
},
"process": {
"terminal": true,
"user": {
"uid": 0,
"gid": 0,
"additionalGids": null
},
"args": [
"sh"
],
"env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM=xterm"
],
"cwd": ""
},
"root": {
"path": "rootfs",
"readonly": true
},
"hostname": "shell",
"mounts": [
{
"name": "proc",
"path": "/proc"
},
……
{
"name": "cgroup",
"path": "/sys/fs/cgroup"
}
],
"linux": {
"capabilities": [
"CAP_AUDIT_WRITE",
"CAP_KILL",
"CAP_NET_BIND_SERVICE"
]
}
}
各部分表示的意思在1.2节中已经讲解,针对具体的内容我们可以看到,版本是0.10,该配置文件针对的是AMD64架构下的Linux系统,启动容器后执行的命令就是sh,配置的环境变量有两个,是PATHTERM,启动后user的uid和gid都为0,表示进入后是root用户。cwd项为空表示工作目录为当前目录。capabilities能力方面则使用白名单的形式,从配置上可以看到只允许三个能力,功能分别为允许写入审计日志、允许发送信号、允许绑定socket到网络端口。

一份简略的runtime.json配置则如下,同样用省略号省去了部分内容:

{
"mounts": {
"proc": {
"type": "proc",
"source": "proc",
"options": null
},
……
"cgroup": {
"type": "cgroup",
"source": "cgroup",
"options": [
"nosuid",
"noexec",
"nodev",
"relatime",
"ro"
]
}
},
"hooks": {
"prestart": null,
"poststop": null
},
"linux": {
"uidMappings": null,
"gidMappings": null,
"rlimits": [
{
"type": "RLIMIT_NOFILE",
"hard": 1024,
"soft": 1024
}
],
"sysctl": null,
"resources": {
"disableOOMKiller": false,
"memory": {
"limit": 0,
"reservation": 0,
"swap": 0,
"kernel": 0,
"swappiness": -1
},
"cpu": {
"shares": 0,
"quota": 0,
"period": 0,
"realtimeRuntime": 0,
"realtimePeriod": 0,
"cpus": "",
"mems": ""
},
"pids": {
"limit": 0
},
"blockIO": {
"blkioWeight": 0,
"blkioWeightDevice": "",
"blkioThrottleReadBpsDevice": "",
"blkioThrottleWriteBpsDevice": "",
"blkioThrottleReadIopsDevice": "",
"blkioThrottleWriteIopsDevice": ""
},
"hugepageLimits": null,
"network": {
"classId": "",
"priorities": null
}
},
"cgroupsPath": "",
"namespaces": [
{
"type": "pid",
"path": ""
},
{
"type": "network",
"path": ""
},
{
"type": "ipc",
"path": ""
},
{
"type": "uts",
"path": ""
},
{
"type": "mount",
"path": ""
}
],
"devices": [
{
"path": "/dev/null",
"type": 99,
"major": 1,
"minor": 3,
"permissions": "rwm",
"fileMode": 438,
"uid": 0,
"gid": 0
},
……
{
"path": "/dev/urandom",
"type": 99,
"major": 1,
"minor": 9,
"permissions": "rwm",
"fileMode": 438,
"uid": 0,
"gid": 0
}
],
"apparmorProfile": "",
"selinuxProcessLabel": "",
"seccomp": {
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": []
},
"rootfsPropagation": ""
}
}

可以看到基本的几项配置分别为挂载点信息、启动前与停止后hooks脚本、然后就是针对Linux的特性支持的诸如用户uid/gid绑定,rlimit配置、namespace设置、cgroups资源限额、设备权限配置、apparmor配置项目录、selinux标记以及seccomp配置。其中,namespacescgroups笔者均有写文章详细介绍过。

再下面的工作便都由libcontainer完成了,大家可以阅读这个系列前一篇文章《Docker背后的容器管理——Libcontainer深度解析》或者购买书籍《Docker容器与容器云》,里面均有详细介绍。

简单来讲,有了配置文件以后,runC就开始借助libcontainer处理以下事情:

  • 创建libcontainer构建容器需要使用的进程,称为Process;
  • 设置容器的输出管道,这里使用的就是daemon提供的pipes;
  • 使用名为Factory的工厂类,通过factory.Create(<容器ID>, <填充好的容器模板container>)创建一个逻辑上的容器,称为Container;
  • 执行Container.Start(Process)启动物理的容器;
  • runC等待Process的所有工作都完成。

可以看到,具体的执行者是libcontainer,它是对容器的一层抽象,它定义了Process和Container来对应Linux中“进程”与“容器”的关系。一旦上述物理的容器创建成功,其他调用者就可以通过ID获取这个容器,接着使用Container.Stats得到容器的资源使用信息,或者执行Container.Destory来销毁这个容器。

综上,runC实际上是把配置交给libcontainer,然后由libcontainer完成容器的启动,而libcontainer中最主要的内容是Process、Container以及Factory这3个逻辑实体的实现原理。runC或者其他调用者只要依次执行“使用Factory创建逻辑容器Container”、“用Process启动逻辑容器Container”即可。

3. 总结

本文从OCI组织的成立开始讲起,描述了开放容器格式的标准及其宗旨,这其实就是runC的由来。继而针对具体的runC特性及其启动进行了详细介绍。笔者在后续的文章中还将针对runC中诸如CRIU热迁移、selinux、apparmor及seccomp配置等特性进行具体的介绍。可以看到OCI的成立表明了社区及各大厂商对容器技术的肯定以及加快容器技术发展进步的强烈决心,相信在不久的将来,符合OCI标准的开放容器项目会越来越多,容器技术将更加欣欣向荣地不断前进。

4. 作者简介

孙健波,浙江大学SEL实验室硕士研究生,《Docker容器与容器云》主要作者之一,目前在云平台团队从事科研和开发工作。浙大团队对PaaS、Docker、大数据和主流开源云计算技术有深入的研究和二次开发经验,团队现将部分技术文章贡献出来,希望能对读者有所帮助。

kubernetes apiserver源码分析——api请求的认证过程

文 王哲

解决什么问题

笔者之前希望全面分析一下k8apiserver的源码,后来发现这样并不十分有效,其一没有针对性,其二由于代码本身比较复杂,涉及到的功能较多,面面俱到也不太现实。

于是我们就回到最初的需求,到底需要解决什么问题,第一个问题就是,apiserver启动的时候,使用secure模式,参数应该如何设置,相关的机制又是怎样?

这一部分的issue很多,如果不从源码来分析的话,就只能黑盒化的去尝试各种参数搭配,费时费力,也不确定是否正确,之前就是这样,这几个issue可以供参考:

https://github.com/GoogleCloudPlatform/kubernetes/issues/10159#issuecomment-113955582

https://github.com/GoogleCloudPlatform/kubernetes/issues/11000

Continue reading

kubernetes node components – kubelet

kubelet作为k8s集群node上的重要组件,一直饱受关注。下面请随笔者一起walk through the code.

文 何思玫

1. Brief introduction of kubelet

管中窥豹,可见一斑。我们首先从k8s的官方文档中对kubelet的描述中一探究竟。

The kubelet is the primary “node agent” that runs on each node. The kubelet works in terms of a PodSpec. A PodSpec is a YAML or JSON object that describes a pod. The kubelet takes a set of PodSpecs that are provided through various echanisms (primarily through the apiserver) and ensures that the containers described in those PodSpecs are running and healthy. Other than from an PodSpec from the apiserver, there are three ways that a container manifest can be provided to the Kubelet.

File: Path passed as a flag on the command line. This file is rechecked every 20 seconds (configurable with a flag).

HTTP endpoint: HTTP endpoint passed as a parameter on the command line. This endpoint is checked every 20 seconds (also configurable with a flag).

HTTP server: The kubelet can also listen for HTTP and respond to a simple API (underspec’d currently) to submit a new manifest.

从上述描述中我们可以看到,kubelet是在集群中每个node上都必须存在的组件,负责维护管理pod。

它的工作主要基于四种source—— apiserver的PodSpec,File,HTTP endpoint及HTTP server——来更新pod。其中,File通过--file-check-frequency参数传入, HTTP通过--http-check-frequency参数。

2. Walk through the src code

文章分析的代码版本>v1.0.1, HEAD 6129d3d4。

kubelet相关代码的主入口在cmd/kublet下,调用方法的实现可能会在pkg/kubelet下。文中为了便利读者,也会就具体函数再进行标明。 下面我们就根据代码逻辑继续一次源码解读的DFS。

kubelet.go#main函数的主要流程包括如下三个步骤:

  • 创建一个KubeletServer实例
  • 根据命令行参数加载flag
  • 启动该kubeletServer。

如下图1所示。

kubelet-go-main

图1 kubelet.go#main

注意到,Run函数负责执行具体的kubelet的构建过程,在正常情况下是永远不会exit的。下面我们就来对该函数进行进一步的分析。

2.1 server.go#Run

如上图所示,KubeletServer.Run函数主要包括以下三个主要流程:

  • 配置并构建KubeletConfig实例。
  • 调用RunKubelet(&KubeletConfig,nil)函数真正地运行kubelet,该函数接受一个KubeletConfig类型的实例作为参数,包含了kubelet的配置信息。
  • 根据KubeletServer.HealthzPort的配置(即命令行参数--healthz-port),为/healthz url注册一个默认的handler(http.DefaultServeMux),启动health server。

可见,Run函数的核心步骤在于RunKubelet函数。

2.2 server.go#RunKubelet

我们首先通过略显复杂的图2来了解RunKubelet函数。它的使用场景非常明确,在node上创建并运行kubelet。 Runkubelet 图2 RunKubelet函数可以被大致划分为三大步骤。

Step1: 配置KubeletConfig信息,包括Hostname,NodeName,Recorder等。KubeletConfig这个结构包含了运行kubelet需要的所有参数,在可见的将来可能会和KubeletServer结构进行merge,不再单独存在。

  • 处理Hostname/NodeName信息
  • 创建了一个EventBroadcaster,创建一条包含kubelet及其NodeName配置的api.EventSource,将其注册到该EventBroadcaster中,用以记录kubelet产生的event并发送给apiserver。
  • 配置capabilities:系--allow-privileged--HostNetworkSources参数传入的值。
  • 配置使用docker拉取镜像时的credentialprovider的路径。
  • 配置OSInterface信息

Step2: createAndInitKubelet,顾名思义,创建并初始化kubelet。

builder在这里定义为createAndInitKubelet。

注意到这里返回了三个变量值,其中k为KubeletBootstrap(kubelet的interface),podCfg是config.PodConfig,最后一个是error变量。 podCfg变量是通过pc = makePodSourceConfig(kc)返回的,其中kc为传入的kubeletConfig变量。k则由NewMainKubelet函数返回。

Step3: 通过startKubelet启动kubelet,传入参数包括上一个函数的返回值及kcfg。

下面我们将就Step2和Step3进行更为详细的阐述。

2.2.1 server.go#createAndInitKubelet

这节内容主要包括两部分,首先来看makePodSourceConfig函数。

Section 1: makePodSourceConfig

该函数的返回值是一个config.PodConfig变量,用于将不同source的pod configuration合并到一个结构中,结构如下所示。

下面,我们具体来看makePodSourceConfig完成了什么工作。源码如下:

  • 使用NewPodConfig函数创建一个PodConfig变量cfg,用到了config.PodConfigNotificationSnapshotAndUpdateskc.RecorderPodConfigNotificationSnapshotAndUpdates的更新规则是,在增删pod的时候发送SET消息,其它变更发送UPDATE消息。其中,cfg.Updates是一个缓存大小为50的channel,收发数据类型为kubelet.PodUpdate(即对于某个pod的某种更新操作)。
  • 向podConfig变量cfg加入config source,包括 file/hhtp(url)/apiserver source。如同在开篇综述中所说,File类型和HTTP类型的pod configuration的变更,是通过定时查询对应source完成的。
  • 此处以更为常见的apiserver为例,我们使用NewSourceApiserver监听apiserver中podSpec的变更。该函数接收三个参数,第一个参数为用于向apiserver发起请求的Client,第二个参数为NodeName,第三个参数是由cfg.Channel返回的一个只读channel,类型是chan <- interface{}

也许读者会好奇,这个Channel函数具体做了哪些工作呢? 它会创建并返回一个interface{}类型的channel,并且启动go routine持续监听该Channel,并且将监听到的update进行merge。

merge是在NewPodConfig阶段决定的,即此前newPodStorage(updates, mode, recorder)返回的podStorage的Merge函数。该Merge函数定义在pkg/kubelet/config/config.go中,在该过程中会将监听的channel中更新的信息更新到cfg.updates中。限于篇幅,此处只展示PodConfigNotificationSnapshotAndUpdates对应的case,其它仅以”…”代替。

也就是说,cfg.Channel返回的是一个未知具体类型的被持续监听的channel(且该channel在merge之后会影响到原cfg),那么写channel的工作由谁完成呢?我们相信这是由config source(此处即apiserver)来完成的,也会在NewSourceApiserver函数分析中进行阐释。

  • 通过NewListWatchFromClient函数构造了一个ListWatch
  • 调用newSourceApiserverFromLW来watch并pull(拉取)apiserver的更新。

    // newSourceApiserverFromLW holds creates a config source that watches and pulls from the apiserver. func newSourceApiserverFromLW(lw cache.ListerWatcher, updates chan<- interface{}) { send := func(objs []interface{}) { var pods []api.Pod for _, o := range objs { pods = append(pods, o.(api.Pod)) } updates <- kubelet.PodUpdate{pods, kubelet.SET, kubelet.ApiserverSource} } cache.NewReflector(lw, &api.Pod{}, cache.NewUndeltaStore(send, cache.MetaNamespaceKeyFunc), 0).Run() }

可以看到,newSourceApiserverFromLW的执行核心只有一行代码,将创建出来的Reflector Run起来。

NewReflector函数创建一个Reflector对象,用于保证传入的第三个参数Store里的内容是update to date的。第三个参数以send函数为参数构建一个UndeltaStore,其PushFunc为send函数。 UndeltaStore listens to incremental updates and sends complete state on every change.也就是说,UndeltaStore不仅接受从Reflector来的update,并且还会使用UndeltaStore.PushFunc来进行change的deliver。这里就是写入到Cfg.Updates里,即完成了update的写操作。

Note:之前在集群运行过程中遇到过一个error,(可能在将来会upgrage成warning),即watcher超时,导致kubelet报错,位于reflector.go line#225。更新的内容来自listWatch,即apiserver。这里相当于是写updates。

Section 2: NewMainKubelet

我们通过pkg/kubelet/kubelet.go#NewMainKubelet函数构造了一个Kubelet。在该函数中创建各个kubelet需要的组件,部分参见如下列表

  • ContainerGC manages garbage collection of dead containers。负责进行对死掉的container进行垃圾回收。
  • ImageManager manages lifecycle of all images. 负责管理image。
  • DiskSpaceManager manages policy for diskspace management for disks holding docker images and root fs. 负责进行磁盘管理。
  • StatusManager updates pod statuses in apiserver. Writes only when new status has changed.该结构体中的podStatusChannel负责buffer podStatusSyncRequest,是一个缓冲区大小为1000的channel。 从apiserver中更新pod状态。
  • ReadinessManager maintains the readiness information(probe results) of containers over time to allow for implementation of health thresholds.负责收集container的readiness information(readiness info具体指什么并不明晰,是一个bool型,可能代表该container是否准备好?)。
  • RefManager manages the references for the containers. The references are used for reporting events such as creation, failure, etc.
  • VolumeManager manages the volumes for the pods running on the kubelet. Currently it only does book keeping, but it can be expanded to take care of the volumePlugins.
  • OOMWatcher watches cadvisor for system oom’s and records an event for every system oom encountered.
  • 初始化network plugin。这是个alpha feature。
  • ContainerRuntime 可以是DockerManager或者是rkt,默认为docker。
  • ContainerManager manages the containers running on a machine. -(周期性)配置网络参数(iptables rules)。 -(周期性)向master节点发送其它各个node的状态。synchronizes node status to master, registering the kubelet first if necessary.
  • 启动ContainerRuntime。ContainerRuntime是Runtime(interface)的一个实现,定义在pkg/kubelet/container/runtime.go中,包含了SyncPod/GetPodStatus等重要方法。以”docker”为例,它将是一个DockerManager,定义在pkg/kubelet/dockertools/manager.go中,并且实现了Runtime的所有方法。
  • PodManager stores and manages access to the pods. Kubelet discovers pod updates from 3 sources: file, http, and apiserver.三个update pod的来源,均在上例中的makePodSourceConfig 中进行了注册。 特别地,对于非apiserver来源的pod,apiserver是不知道其变更的。为了对其进行更新,我们创建了static pod。A mirror pod has the same pod full name (name and namespace) as its static counterpart (albeit different metadata such as UID, etc). By leveraging the fact that kubelet reports the pod status using the pod full name, the status of the mirror pod always reflects the actual status of the static pod. When a static pod gets deleted, the associated orphaned mirror pod will also be removed. 根据GetPodByFullName的原则,mirror pod会映射原pod的状态。
  • 为containerRuntime设置RuntimeCache。RuntimeCache caches a list of pods.该Cache中pod的状态会通过它的两个方法GetPodsForceUpdateIfOlder来更新。
  • 创建PodWorkersklet.podWorkers = newPodWorkers(runtimeCache, klet.syncPod, recorder) 第二个参数将作为podWorker.syncPodFn,即用以sync the desired stated of pod.实现为‵func (kl *Kubelet) syncPod`(定义在pkg/kubelet/kubelet.go)。
  • 在metrics中注册RuntimeCache。 创建目录,默认根目录在/var/lib/kubelet下,并在根目录下创建pods目录和plugins目录。
  • 初始化所有注册的(即可以访问kubelet的)plugin。
  • 建立记录容器的log目录(/var/log/containers)。

至此,所有相关组件或目录文件都已经被创建完毕,该函数返回一个Kubelet实例。

2.2.2 startKubelet

如果读者还记得我们在2.2节中的下段描述,“Step 3: 通过startKubelet启动kubelet,传入参数包括上一个函数的返回值及kcfg。” 那将是一个莫大的幸事,因为我们终于要从一个非常深的源码栈中跳出进入另一个更深的栈了:) 创建完毕kubelet的,下边无疑要进行的是运行操作。

根据运行类别分为两种,其一为RunOnce(只运行一次),其二为startKubelet(持续运行不退出);两种情况本质的处理逻辑是类似的,下面谨以第二种为例进行讲述。

该函数的核心——启动kubelet——由如下一行代码完成,

podCfg.Updates()返回podCfg.updates,类型为<-chan kubelet.PodUpdate,即前述提及的缓冲区为50的用以存储Pod更新信息的channel,让我们来一览其结构。

Kubelet.Run()定义在pkg/kubelet/kubelet.go中,它完成的工作包括

  • 定义kubelet.logServer
  • 如果kl.resourceContainer不为空(命令行参数--resource-container,”Absolute name of the resource-only container to create and run the Kubelet in (Default: “/kubelet.”) ),则将kubelet移入该container中。
  • 启动imageManager。运行时该imageManager负责持续list image和container,并在其Record中更新。
  • 启动cadvisor。
  • 启动containerManager。
  • 启动oomWatcher。
  • 启动周期性更新uptime。
  • 启动statusManager。
  • syncLoop(updates, kl)。

statusManager.StartsyncLoop具体做了什么工作,可能会引起读者的兴趣。下面我们就具体进行分析。为了让结构显得更为明晰,我们还是新起一个小章节:)

2.2.2.1 statusManager的启动

我们知道statusManager负责监听从apiserver对于pod的更新,更新的读写通过podStatusSyncRequest类型的channelpodStatusChannel

statusManager.Start执行核心工作的代码如下

即持续运行s.syncBatch(),这个函数负责同apiserver交互来获得pod status的更新(located @pkg/kubelet/status_manager.go),在没有更新时会block。这是因为在该函数中会等待s.podStatusChannel的返回值。从另一个角度而言,这是在读channel,写channel的操作应该由apiserver完成。

2.2.2.2 syncLoop

syncLoop这个函数使用到了传入的channel参数,即podCfg.updates,第二个参数为kubelet实例本身。它的函数体异常简单,但是却为我们执行了非常重要的sync pod的核心任务。

syncLoopIteration方法在检查各种配置之后,启动select监听updates(channel)上的数据,一旦有更新的数据,则执行podManager.UpdatePods方法,负责update the internal pods with those provided by the update,实际上就是在podManager的列表里进行相应的更新,并进行相应的标记——把每个pod(以types.UID标记)及其SyncPodType记录好(该函数执行完毕后,所有现有的pod的状态都是”sync”)。

然后,则是最为重要的sync pod

handler.SyncPods方法实际为pkg/kubelet/kubelet.go#SyncPods,进行的工作为SyncPods synchronizes the configured list of pods (desired state) with the host current state。具体包括如下步骤:

  1. 根据传入的现存pod列表将statusManager中的不存在的pod entry删除。
  2. 通过admitPods函数过滤terminated& notfitting & outofdisk pods,返回的pod列表即为desired pod。
  3. 对照desired pod列表,进行sync工作,具体是由Kubelet.podWorkers.UpdatePod来完成的。
  4. 把不存在的pod对应的podWorkers释放掉。这里就体现了维护podWorkers.podUpdates这个field的重要性。
  5. 杀死不需要的pod。
  6. 杀死孤儿volume。
  7. 删除不存在的pod对应的dir
  8. 删除孤儿mirror pod

下面我们来具体分析一下第3条Kubelet.podWorkers.UpdatePod进行的工作。

如果该pod在podWorkers.podUpdates里不存在,则需要进行创建,此时调用podWorkers.managePodLoop。 然后,根据podWorkers.isWorking[pod.UID]的值更新对应podUpdates的值或者传入podWorkers.lastUndeliveredWorkUpdate。这是对于对应pod状态的一个记录。

下面我们具体来看一下创建新pod的过程经历了什么。

从代码中可以看到,核心步骤调用了podWorkers.syncPodFn,这是在创建podWorker时构建的,具体指向了func (kl *Kubelet) syncPod(定义在pkg/kubelet/kubelet.go,完整的传参列表

这个函数在做什么呢? Let’s figure it out.

  • canRunPod:确认是否有足够的权限运行该pod。
  • 创建pod data存储的目录,包括root目录(以pod的UID命名)/volumes/plugins(默认在/var/lib/kubelet/pods下)
  • 为pod创建reference
  • mount volume,并且在volumeManager中设置相应的entry。volume是pod的manifest里边的Spec.Volumes下的信息。
  • 更新statusManager中的pod信息。根据传入参数updateType的不同,a)对于create:kl.statusManager.SetPodStatus(pod, podStatus),在statusManager里为新的pod设置StartTime,并且将新的podStatusSyncRequest传给statusManager.podStatusChannel。b)否则,通过kl.containerRuntime.GetPodStatus(pod)获得pod信息(返回值是api.PodStatus )。但是,在现在这个context中,应该为a).
  • 获得inspect pod的manifest中pod.Spec.ImagePullSecrets的信息。(这个secret是用来pull image的)
  • sync pod(在下文中再进行详细论述)
  • 根据该pod是否为static pod,在podManager中进行了相应的设置。

sync pod的过程,以DockerManager.SyncPod为例。有鉴于函数体比较长,在此就不再贴出源码。其功能非常明晰——目的在于使得running pod能够match对应的desired pod。

  • 计算running pod和desired pod之前的差距,返回的信息里包括StartInfraContainer(是否需要创建infra container),InfraContainerId(infra container ID),ContainersToStart(需要start的container),ContainersToKeep(需要keep的container)。
  • 根据返回的信息决定是否重启infra container((即gcr.io/google_containers/pause:0.8.0),配置network及其它container

至此,Kubelet.syncLoopIteration算是基本完成了。这也就意味着kubelet已经在node上运行起来,充当其pod的维护者的角色了。

3. Summary

总之,从宏观角度上而言,kubelet进行的工作就是从apiserver/file/http中获取pod更新的信息,并且定期进行sync pod。其中有大量的工作都涉及了channel的使用,也希望读者在阅读的过程中能够加以注意。

4S: Services Account, Secret, Security Context and Security in Kubernetes

Service Account, Secrets和Security Contexts作为保证kubernetes集群Security的策略被引入,相关代码一直处于快速变更与迭代中。本文谨从design和初级实践的视角对其进行概略性的分析,以飨读者。

文 何思玫

1. 集群安全(Security in Kubernetes)

众所周知,集群安全的首要关注点无疑是隔离性。进程之间的相互隔离,进程与集群基础设施的严格界限,用户与管理员之前的天然角色区分,都应该被考虑到隔离性的范畴内。 统而言之,集群安全性必须考虑如下几个目标: (1) 保证容器与其运行的宿主机的隔离。 (2) 限制容器对于基础设施及其它容器的影响权限,运行拥有特权模式的容器是不被推荐的行为。 (3) 最小权限原则——对所有组件权限的合理限制。 (4) 通过清晰地划分组件的边界来减少需要加固和加以保护的系统组件数量。 (5) 普通用户和管理员的角色区分,同时允许在必要的时候将管理员权限委派给普通用户。 (6) 允许集群上运行的应用拥有secret data。 涉及安全,authentication和authorization是不能绕过的两个话题。下面我们就先来了解一下k8s在这两个issue所提供的支持。

2. Authentication

k8s目前支持三种认证方式,包括certificates/tokens/http basic auth。 client certificate authentication是双向认证的方式,可以经由easyrsa等证书生成工具生成服务器端并客户端证书。 Token authentication:单向认证方式,为kube-apiserver提供- -token_ auth_file,格式为一个有3columns的csv file:token,user name,user id。此处为使用该认证方法的常见的elasticsearch case。 Basic authentication:传入明文用户名密码作为apiserver的启动参数,不支持在不重启apiserver的前提下进行用户名/密码修改。

更多细节详见官方相关文档

3. Authorization

--authorization-mode,apiserver的参数,用于定义对secure port设置何种authorization policy,包括三种AlwaysAllow/AlwayDeny/ABAC,第一种policy允许所有对apiserver的API request,与之相反,第二种则会block所有的API request,第三种则为Attribute-Based Access Control,即对于不同request attribute,有不同的access control。 下面我们着重讨论ABAC mode。

3.1 Request Attributes

在考虑authorization时,一个request有5种attribute需要考虑: – user – group – readOnly – resource(如只访问API endpoint,如/api/v1/namesapces/default/pods,或者其它杂项endpoint,如/version,此时的resource是空字符串) – namespace

3.2 Policy File Format

对于ABAC mode,还需要specify --authorization-policy-file参数,例子参见此处

除此之外,apiserver还可以通过调用Authorizer(interface)来决定是否允许某个API操作。

4. UserAccount

这个概念适用于所有希望与k8s集群交互的个体(通常可以认为是human),因此用户名通常是human-readable,目前并不作为一个代码中的类型单独出现,一般通过config file来读取和感知。

5. Service Account

Service Account概念的引入是基于这样的使用场景:运行在pod里的进程需要调用k8s API(如scheduler/replication controller/minitor system等)以及非k8s API的其它服务(如image repository/被mount到pod上的NFS volumes中的file等)。我们使用Service Account来为pod提供id。 Service Account和第4节中提及的User account可能会带来一定程度上的混淆,下面我们就先从概念的层面将其sort it out。

  • user account通常是为human设计的,而service account则是为跑在pod里的process。
  • user account是global的,即跨namespace使用;而service account是namespaced的,即仅在belonging的namespace下使用。
  • 创建一个新的user account通常需要较高的特权并且需要经过比较复杂的business process(即对于集群的访问权限的创建),而service account则不然。
  • 不同的auditing consideration。
5.1 Design Overview

Service Account包括如下几个元素:

  • name,用以作为id
  • principal,用以authenticated及authorized
  • security context,定义linux capabilities等与系统相关的参数。
  • secrets
5.2 Use Cases

通常我们在使用kubectlbinary来与集群交互时,我们(User Account)都会通过apiserver的认证(如果该集群设置了认证方式的话),可以被看作某个特定的User Account。类似地,当pod希望与apiserver进行交互时,也需要使用特定的认证机制,即Service Account。

当创建pod且没有经过特殊设置时,它将会被默认地分发到该namespace下default的service account,可以通过kubectl get pods/podname -o yaml的命令行查看spec.serviceAccount的field)。

5.2.1 default service account

每个namespace在创建时都会有一个default的service Account,如下所示

5.2.2 Create your own service Account

用户可以方便地通过yaml文件等形式创建自己的serviceAccount,如

通过`kubectl get < resource > -o yaml获取详细信息

可以看到,token(secret)被自动生成并ref了。如果在之后创建pod的时候ref这个自建的serviceAccount,在spec.serviceAccount中指定即可。

通过如下命令可以删除serviceAccount。

5.2.3Adding Secrets to a service account

注意到,我们可以不使用serviceAccount自建的secrets,而是选用我们自己生成的secrets,这是一种consume secret的方式。 首先创建secret,kubectl create -f secret.yaml

然后创建一个comsume该secret,kubectl create -f serviceaccount.yaml

最后在pod的manifest中指定其serviceaccount,kubectl create -f busybox4.yaml

已了解如何创建一个引用自建secret的serviceaccount,但是如果在exsiting serviceaccount中添加引用,目前官方文档mark it as TODO。笔者认为此用法应该与上例类似,但笔者采用这种方式consume secret时,serviceaccount似乎并不对该secret有维护作用。 Update:此用法确与上例类似,参见issue#12012,笔者后续会考虑对相关doc进行更新。

6. Secrets

secret是用于存放sensitive information的数据结构,可以通过manual或者automatic两种方式创建。 secret的use case主要有两种,或者作为volume mount到pod上,或者用于kubelet在为pod里的container拉取镜像时。 使用serviceAccount自建的secret是一种保证secure的推荐方式,当然可以由用户disable或者overridden,如果用户愿意承担额外的风险的话。

6.1 Secrets类型一览

secret的类型目前有三种,如下所示

Opaque类型是默认的用户自建类型,即任意string(实际上也并不是十分任意,data label的key/value必须满足特定的要求:Each key must be a valid DNS_SUBDOMAIN or leading dot followed by valid DNS_SUBDOMAIN. Each value must be a base64 encoded string as described in https://tools.ietf.org/html/rfc4648#section-4)。

第二种类型的例子如下所示

第三种类型一般用于向private registry拉取镜像,在6.3节中再做阐述。

6.2 manually specify a secret

如上述所示,使用kubectl create命令即可创建一个secret。创建完毕之后,可以使用serviceAccount来consume,也可以手动添加到pod的manifest中,如下例所示:

More example here。

6.3 Manually specifying an imagePullSecret

这个方法仅在GKE/GCE或其它cloud-provider场景下推荐使用,创建一个类型为kubernetes.io/dockercfg的secret,并且在pod manifest的imagePullSecrets label下使用。因为缺乏实验平台,在此不作赘述。

总而言之,目前k8s对于secret的support仍处于不十分完善的阶段,secret可能以多种方式暴露给外部用户甚至是attacker。

7. Security context

Security context是用以对容器进行限制,使得不同的运行容器之前能够实现较为明晰的隔离,以及降低其影响宿主机和其它容器的可能性。通俗而言,容器中的security context用于表征在创建及运行容器时,它能够使用及访问的资源参数。 该概念将会被用到如下两处:

  • kubelet用于创建及运行container。
  • 作为serviceAccount的一部分定义在pod中。

这个概念具体如何使用在k8s场景中基本上可以认为在servicAccount中的securityContext label下,目前代码只实现了capabilities和privileged,在此不展开说明。

tutum-agent原理浅析

孙健波

tutum-aget是tutum提供的一个开源代理引擎。当你把tutum-agent安装在你本地机器上以后,你就可以把本地的机器节点添加到云端,让tutum来帮你统一进行管理。你随时随地都可以登陆tutum网站获得如下功能。

  1. 启停Docker容器。
  2. 动态伸缩容器实例数量。
  3. 监控容器状态、容器CPU\Memory\Disk\Bandwidth使用量
  4. 查看容器日志、操作记录
  5. 与tutum其他服务结合的衍生功能(部署应用、绑定服务等)

可见,安装了tutum-agent以后相当于把本地的机器加入到了tutum的数据中心进行统一管理。本文将分析tutum-agent的工作原理。

安装

tutum-agent通过shell进行安装,安装脚本可以在执行官方的安装指令时下载curl -Ls https://get.tutum.co/,也可以在github源码中找到。

这个shell脚本很简单,根据不同linux发行版安装tutum-agent,如果是ubuntu则是执行apt-get install -yq tutum-agent,然后生成配置文件/etc/tutum/agent/tutum-agent.conf,内容如下。

这些配置文件的作用在随后的代码解析中讲解。生成完配置文件后启动该服务。

service tutum-agent restart

至此shell脚本执行完了,但是安装过程并没有结束,在安装tutum-agent时,会因为冲突卸载原先已经安装好的Docker。

启动后的第一件事就是在TutumHost上注册并验证用户的TutumToken和TutumUUID,获得相应的证书文件,然后从TutumHost上下载Docker的二进制文件,目前Tutum官方提供的是1.5版本的Docker。随后启动Docker daemon,之前下载的证书也主要用在这里。

启动完Docker以后,就开始下载Ngrok客户端。Ngrok是一个网络代理,它可以让你在防火墙或者NAT映射的私有网络内的服务,让公网可以直接访问。Ngrok的实现方式是,在公网架设一个Ngrok 服务器,私有网络的Ngrok客户端与公网的Ngrok Server保持tcp长连接,用户通过访问Ngrok Server进行代理,从而访问到Ngrok客户端所在机器的服务。使用Ngrok客户端,只需要配置你想要提供服务的应用程序对外服务的端口即可。把这个端neng l口告诉Ngrok客户端,就相当于把本地私有网络下的服务提供给外部。

至此,所有的服务也就安装完毕了。总结一下,我们实际上安装了tutum-agent、Docker、Ngrok三个应用程序。通过Ngrok把本地的Docker作为服务让公网的其他用户可以使用,tutum-agent在随后会处理包括日志、监控、Docker指令处理等相关内容。

连接

安装完Ngrok以后,实际上就相当于建立了穿越NAT网络的隧道,使得公网所在的程序可以直接控制私有网络的Docker服务。Tutum-agent在启动Docker时使用了用户相关的证书,保证了Docker只接受有证书认证的请求,一定程度上保证了本地服务的安全。

获得了本地Docker服务的控制能力,容器相关的功能的实现自然迎刃而解。Ngrok还会为用户提供命令操作日志以便于审计,所以你可以方便的查看容器的操作记录。

建立了连接后我们还需要做什么?

通过Ngrok,这一切都变得很简单,但是别忘了,Docker daemon的运行状态需要时刻监控。tutum-agent会对Docker daemon的运行状态周期性的检查,一旦daemon意外推出,可以保证快速检测到并重启。

性能

ngrok是一个非常强大的tunnel工具,使用它以后本身带来的代理转发性能损失非常低。

但是实际上通过ngrok搭建起来的服务,都是应用用户先去访问ngrok服务端,然后再有服务端转发给ngrok客户端,最后处理用户请求并返回。所以,真正让人感知到性能损失的可能是你的ngrok服务端搭建的位置较远,而ngrok客户端与应用用户本身网络较近,这样就容易导致较高的应用访问延迟。

延伸

看完了上述内容,可能你会想到,如何构建一个类似Tutum的“Bring your own node”服务,并把它应用到Docker以为的其他项目上。

第一步,搭建自己的ngrok服务,使得用户可以通过ngrok客户端连接并在任意网络访问本地的服务,可参考博客“搭建自己的ngrok服务”

第二步,区分用户。当你的ngrok对外提供服务的时候,会有许多客户端来连接,不同的客户端可能是不同的用户连接的,也有可能是同一个用户的不同应用或不同主机节点。所以你需要在服务端编写自己的判断逻辑,方法很简单。ngrok客户端与服务端建立连接时会生成子域名(或者自定义域名),这个域名一旦建立了连接,就是唯一的,别的用户无法占用,通过这个方法就可以进行最简单的区分。当然,根据你对外提供的服务,你可能还需要通过生成证书来保证服务的安全性。

第三步,通过API进行操控。当ngrok客户端连接成功后,实际上服务端已经可以连接用户私有网络下服务的端口了,通过端口自然可以访问到服务的API,就基本上可以让用户全局操控自己的服务了。

第四步,日志、监控、报警与可视化。简单的日志收集分为两部分,一部分自然是ngrok的操作日志,另一部分则是应用相关的日志,需要通过应用API收取。简单来说,监控也分为两部分,一部分是用户主机资源、服务可用性相关的数据监控,另一部分是业务相关的监控。

基本上,有了这些,一个最基本的服务就完成了。

Docker背后的容器管理——libcontainer深度解析

孙健波

libcontainer 是Docker中用于容器管理的包,它基于Go语言实现,通过管理namespacescgroupscapabilities以及文件系统来进行容器控制。你可以使用libcontainer创建容器,并对容器进行生命周期管理。

容器是一个可管理的执行环境,与主机系统共享内核,可与系统中的其他容器进行隔离。

在2013年Docker刚发布的时候,它是一款基于LXC的开源容器管理引擎。把LXC复杂的容器创建与使用方式简化为Docker自己的一套命令体系。随着Docker的不断发展,它开始有了更为远大的目标,那就是反向定义容器的实现标准,将底层实现都抽象化到libcontainer的接口。这就意味着,底层容器的实现方式变成了一种可变的方案,无论是使用namespace、cgroups技术抑或是使用systemd等其他方案,只要实现了libcontainer定义的一组接口,Docker都可以运行。这也为Docker实现全面的跨平台带来了可能。

1. libcontainer 特性

目前版本的libcontainer,功能实现上涵盖了包括namespaces使用、cgroups管理、Rootfs的配置启动、默认的Linux capability权限集、以及进程运行的环境变量配置。内核版本最低要求为2.6,最好是3.8,这与内核对namespace的支持有关。

目前除user namespace不完全支持以外,其他五个namespace都是默认开启的,通过clone系统调用进行创建。

1.1 建立文件系统

文件系统方面,容器运行需要rootfs。所有容器中要执行的指令,都需要包含在rootfs(在Docker中指令包含在其上叠加的镜像层也可以执行)所有挂载在容器销毁时都会被卸载,因为mount namespace会在容器销毁时一同消失。为了容器可以正常执行命令,以下文件系统必须在容器运行时挂载到rootfs中。

路径 类型 参数 权限及数据
/proc proc MS_NOEXEC,MS_NOSUID,MS_NODEV
/dev tmpfs MS_NOEXEC,MS_STRICTATIME mode=755
/dev/shm shm MS_NOEXEC,MS_NOSUID,MS_NODEV mode=1777,size=65536k
/dev/mqueue mqueue MS_NOEXEC,MS_NOSUID,MS_NODEV
/dev/pts devpts MS_NOEXEC,MS_NOSUID newinstance,ptmxmode=0666,mode=620,gid5
/sys sysfs MS_NOEXEC,MS_NOSUID,MS_NODEV,MS_RDONLY

当容器的文件系统刚挂载完毕时,/dev文件系统会被一系列设备节点所填充,所以rootfs不应该管理/dev文件系统下的设备节点,libcontainer会负责处理并正确启动这些设备。设备及其权限模式如下。

路径 模式 权限
/dev/null 0666 rwm
/dev/zero 0666 rwm
/dev/full 0666 rwm
/dev/tty 0666 rwm
/dev/random 0666 rwm
/dev/urandom 0666 rwm
/dev/fuse 0666 rwm

容器支持伪终端TTY,当用户使用时,就会建立/dev/console设备。其他终端支持设备,如/dev/ptmx则是宿主机的/dev/ptmx 链接。容器中指向宿主机 /dev/null的IO也会被重定向到容器内的 /dev/null设备。当/proc挂载完成后,/dev/中与IO相关的链接也会建立,如下表。

源地址 目的地址
/proc/1/fd /dev/fd
/proc/1/fd/0 /dev/stdin
/proc/1/fd/1 /dev/stdout
/proc/1/fd/2 /dev/stderr

pivot_root 则用于改变进程的根目录,这样可以有效的将进程控制在我们建立的rootfs中。如果rootfs是基于ramfs的(不支持pivot_root),那么会在mount时使用MS_MOVE标志位加上chroot来顶替。

当文件系统创建完毕后,umask权限被重新设置回0022

1.2 资源管理

《Docker背后的内核知识:cgroups资源隔离》一文中已经提到,Docker使用cgroups进行资源管理与限制,包括设备、内存、CPU、输入输出等。

目前除网络外所有内核支持的子系统都被加入到libcontainer的管理中,所以libcontainer使用cgroups原生支持的统计信息作为资源管理的监控展示。

容器中运行的第一个进程init,必须在初始化开始前放置到指定的cgroup目录中,这样就能防止初始化完成后运行的其他用户指令逃逸出cgroups的控制。父子进程的同步则通过管道来完成,在随后的运行时初始化中会进行展开描述。

1.3 可配置的容器安全

容器安全一直是被广泛探讨的话题,使用namespace对进程进行隔离是容器安全的基础,遗憾的是,usernamespace由于设计上的复杂性,还没有被libcontainer完全支持。

libcontainer目前可通过配置capabilitiesSELinuxapparmor 以及seccomp进行一定的安全防范,目前除seccomp以外都有一份默认的配置项提供给用户作为参考。

在本系列的后续文章中,我们将对容器安全进行更深入的探讨,敬请期待。

1.4 运行时与初始化进程

在容器创建过程中,父进程需要与容器的init进程进行同步通信,通信的方式则通过向容器中传入管道来实现。当init启动时,他会等待管道内传入EOF信息,这就给父进程完成初始化,建立uid/gid映射,并把新进程放进新建的cgroup一定的时间。

在libcontainer中运行的应用(进程),应该是事先静态编译完成的。libcontainer在容器中并不提供任何类似Unix init这样的守护进程,用户提供的参数也是通过exec系统调用提供给用户进程。通常情况下容器中也没有长进程存在。

如果容器打开了伪终端,就会通过dup2把console作为容器的输入输出(STDIN, STDOUT, STDERR)对象。

除此之外,以下4个文件也会在容器运行时自动生成。
* /etc/hosts
* /etc/resolv.conf
* /etc/hostname
* /etc/localtime

1.5 在运行着的容器中执行新进程

用户也可以在运行着的容器中执行一条新的指令,就是我们熟悉的docker exec功能。同样,执行指令的二进制文件需要包含在容器的rootfs之内。

通过这种方式运行起来的进程会随容器的状态变化,如容器被暂停,进程也随之暂停,恢复也随之恢复。当容器进程不存在时,进程就会被销毁,重启也不会恢复。

1.6 容器热迁移(Checkpoint & Restore)

目前libcontainer已经集成了CRIU作为容器检查点保存与恢复(通常也称为热迁移)的解决方案,应该在不久之后就会被Docker使用。也就是说,通过libcontainer你已经可以把一个正在运行的进程状态保存到磁盘上,然后在本地或其他机器中重新恢复当前的运行状态。这个功能主要带来如下几个好处。

  • 服务器需要维护(如系统升级、重启等)时,通过热迁移技术把容器转移到别的服务器继续运行,应用服务信息不会丢失。
  • 对于初始化时间极长的应用程序来说,容器热迁移可以加快启动时间,当应用启动完成后就保存它的检查点状态,下次要重启时直接通过检查点启动即可。
  • 在高性能计算的场景中,容器热迁移可以保证运行了许多天的计算结果不会丢失,只要周期性的进行检查点快照保存就可以了。

要使用这个功能,需要保证机器上已经安装了1.5.2或更高版本的criu工具。不同Linux发行版都有criu的安装包,你也可以在CRIU官网上找到从源码安装的方法。我们将会在nsinit的使用中介绍容器热迁移的使用方法。

CRIU(Checkpoint/Restore In Userspace)由OpenVZ项目于2005年发起,因为其涉及的内核系统繁多、代码多达数万行,其复杂性与向后兼容性都阻碍着它进入内核主线,几经周折之后决定在用户空间实现,并在2012年被Linus加并入内核主线,其后得以快速发展。

你可以在CRIU官网查看其原理,简单描述起来可以分为两部分,一是检查点的保存,其中分为3步。

  1. 收集进程与其子进程构成的树,并冻结所有进程。
  2. 收集任务(包括进程和线程)使用的所有资源,并保存。
  3. 清理我们收集资源的相关寄生代码,并与进程分离。

第二部分自然是恢复,分为4步。

  1. 读取快照文件并解析出共享的资源,对多个进程共享的资源优先恢复,其他资源则随后需要时恢复。
  2. 使用fork恢复整个进程树,注意此时并不恢复线程,在第4步恢复。
  3. 恢复所有基础任务(包括进程和线程)资源,除了内存映射、计时器、证书和线程。这一步主要打开文件、准备namespace、创建socket连接等。
  4. 恢复进程运行的上下文环境,恢复剩下的其他资源,继续运行进程。

至此,libcontainer的基本特性已经预览完毕,下面我们将从使用开始,一步步深入libcontainer的原理。

2. nsinit与libcontainer的使用

俗话说,了解一个工具最好的入门方式就是去使用它,nsinit就是一个为了方便不通过Docker就可以直接使用libcontainer而开发的命令行工具。它可以用于启动一个容器或者在已有的容器中执行命令。使用nsinit需要有 rootfs 以及相应的配置文件。

2.1 nsinit的构建

使用nsinit需要rootfs,最简单最常用的是使用Docker busybox,相关配置文件则可以参考sample_configs目录,主要配置的参数及其作用将在配置参数一节中介绍。拷贝一份命名为container.json文件到你rootfs所在目录中,这份文件就包含了你对容器做的特定配置,包括运行环境、网络以及不同的权限。这份配置对容器中的所有进程都会产生效果。

具体的构建步骤在官方的README文档中已经给出,在此为了节省篇幅不再赘述。

最终编译完成后生成nsinit二进制文件,将这个指令加入到系统的环境变量,在busybox目录下执行如下命令,即可使用,需要root权限。

nsinit exec --tty --config container.json /bin/bash

执行完成后会生成一个以容器ID命名的文件夹,上述命令没有指定容器ID,默认名为”nsinit”,在“nsinit”文件夹下会生成一个state.json文件,表示容器的状态,其中的内容与配置参数中的内容类似,展示容器的状态。

2.2 nsinit的使用

目前nsinit定义了9个指令,使用nsinit -h就可以看到,对于每个单独的指令使用--help就能获得更详细的使用参数,如nsinit config --help

nsinit这个命令行工具是通过cli.go实现的,cli.go封装了命令行工具需要做的一些细节,包括参数解析、命令执行函数构建等等,这就使得nsinit本身的代码非常简洁明了。具体的命令功能如下。

  • config:使用内置的默认参数加上执行命令时用户添加的部分参数,生成一份容器可用的标准配置文件。
  • exec:启动容器并执行命令。除了一些共有的参数外,还有如下一些独有的参数。
    • –tty,-t:为容器分配一个终端显示输出内容。
    • –config:使用配置文件,后跟文件路径。
    • –id:指定容器ID,默认为nsinit
    • –user,-u:指定用户,默认为“root”.
    • –cwd:指定当前工作目录。
    • –env:为进程设置环境变量。
  • init:这是一个内置的参数,用户并不能直接使用。这个命令是在容器内部执行,为容器进行namespace初始化,并在完成初始化后执行用户指令。所以在代码中,运行nsinit exec后,传入到容器中运行的实际上是nsinit init,把用户指令作为配置项传入。
  • oom:展示容器的内存超限通知。
  • pause/unpause:暂停/恢复容器中的进程。
  • stats:显示容器中的统计信息,主要包括cgroup和网络。
  • state:展示容器状态,就是读取state.json文件。
  • checkpoint:保存容器的检查点快照并结束容器进程。需要填--image-path参数,后面是检查点保存的快照文件路径。完整的命令示例如下。
    nsinit checkpoint --image-path=/tmp/criu

  • restore:从容器检查点快照恢复容器进程的运行。参数同上。

总结起来,nsinit与Docker execdriver进行的工作基本相同,所以在Docker的源码中并不会涉及到nsinit包的调用,但是nsinit为libcontainer自身的调试和使用带来了极大的便利。

3. 配置参数解析

  • no_pivot_root :这个参数表示用rootfs作为文件系统挂载点,不单独设置pivot_root
  • parent_death_signal: 这个参数表示当容器父进程销毁时发送给容器进程的信号。
  • pivot_dir:在容器root目录中指定一个目录作为容器文件系统挂载点目录。
  • rootfs:容器根目录位置。
  • readonlyfs:设定容器根目录为只读。
  • mounts:设定额外的挂载,填充的信息包括原路径,容器内目的路径,文件系统类型,挂载标识位,挂载的数据大小和权限,最后设定共享挂载还是非共享挂载(独立于mount_label的设定起作用)。
  • devices:设定在容器启动时要创建的设备,填充的信息包括设备类型、容器内设备路径、设备块号(major,minor)、cgroup文件权限、用户编号、用户组编号。
  • mount_label:设定共享挂载还是非共享挂载。
  • hostname:设定主机名。
  • namespaces:设定要加入的namespace,每个不同种类的namespace都可以指定,默认与父进程在同一个namespace中。
  • capabilities:设定在容器内的进程拥有的capabilities权限,所有没加入此配置项的capabilities会被移除,即容器内进程失去该权限。
  • networks:初始化容器的网络配置,包括类型(loopback、veth)、名称、网桥、物理地址、IPV4地址及网关、IPV6地址及网关、Mtu大小、传输缓冲长度txqueuelen、Hairpin Mode设置以及宿主机设备名称。
  • routes:配置路由表。
  • cgroups:配置cgroups资源限制参数,使用的参数不多,主要包括允许的设备列表、内存、交换区用量、CPU用量、块设备访问优先级、应用启停等。
  • apparmor_profile:配置用于SELinux的apparmor文件。
  • process_label:同样用于selinux的配置。
  • rlimits:最大文件打开数量,默认与父进程相同。
  • additional_groups:设定gid,添加同一用户下的其他组。
  • uid_mappings:用于User namespace的uid映射。
  • gid_mappings:用户User namespace的gid映射。
  • readonly_paths:在容器内设定只读部分的文件路径。
  • MaskPaths:配置不使用的设备,通过绑定/dev/null进行路径掩盖。

4. libcontainer实现原理

在Docker中,对容器管理的模块为execdriver,目前Docker支持的容器管理方式有两种,一种就是最初支持的LXC方式,另一种称为native,即使用libcontainer进行容器管理。在孙宏亮的《Docker源码分析系列》中,Docker Deamon启动过程中就会对execdriver进行初始化,会根据驱动的名称选择使用的容器管理方式。

虽然在execdriver中只有LXC和native两种选择,但是native(即libcontainer)通过接口的方式定义了一系列容器管理的操作,包括处理容器的创建(Factory)、容器生命周期管理(Container)、进程生命周期管理(Process)等一系列接口,相信如果Docker的热潮一直像如今这般汹涌,那么不久的将来,Docker必将实现其全平台通用的宏伟蓝图。本节也将从libcontainer的这些抽象对象开始讲解,与你一同解开Docker容器管理之谜。在介绍抽象对象的具体实现过程中会与Docker execdriver联系起来,让你充分了解整个过程。

4.1 Factory 对象

Factory对象为容器创建和初始化工作提供了一组抽象接口,目前已经具体实现的是Linux系统上的Factory对象。Factory抽象对象包含如下四个方法,我们将主要描述这四个方法的工作过程,涉及到具体实现方法则以LinuxFactory为例进行讲解。

  1. Create():通过一个id和一份配置参数创建容器,返回一个运行的进程。容器的id由字母、数字和下划线构成,长度范围为1~1024。容器ID为每个容器独有,不能冲突。创建的最终返回一个Container类,包含这个id、状态目录(在root目录下创建的以id命名的文件夹,存state.json容器状态文件)、容器配置参数、初始化路径和参数,以及管理cgroup的方式(包含直接通过文件操作管理和systemd管理两个选择,默认选cgroup文件系统管理)。
  2. Load():当创建的id已经存在时,即已经Create过,存在id文件目录,就会从id目录下直接读取state.json来载入容器。其中的参数在配置参数部分有详细解释。
  3. Type():返回容器管理的类型,目前可能返回的有libcontainer和lxc,为未来支持更多容器接口做准备。
  4. StartInitialization():容器内初始化函数。
    • 这部分代码是在容器内部执行的,当容器创建时,如果New不加任何参数,默认在容器进程中运行的第一条命令就是nsinit init。在execdriver的初始化中,会向reexec注册初始化器,命名为native,然后在创建libcontainer以后把native作为执行参数传递到容器中执行,这个初始化器创建的libcontainer就是没有参数的。
    • 传入的参数是一个管道文件描述符,为了保证在初始化过程中,父子进程间状态同步和配置信息传递而建立。
    • 不管是纯粹新建的容器还是已经创建的容器执行新的命令,都是从这个入口做初始化。
    • 第一步,通过管道获取配置信息。
    • 第二步,从配置信息中获取环境变量并设置为容器内环境变量。
    • 若是已经存在的容器执行新命令,则只需要配置cgroup、namespace的Capabilities以及AppArmor等信息,最后执行命令。
    • 若是纯粹新建的容器,则还需要初始化网络、路由、namespace、主机名、配置只读路径等等,最后执行命令。

至此,容器就已经创建和初始化完毕了。

4.2 Container 对象

Container对象主要包含了容器配置、控制、状态显示等功能,是对不同平台容器功能的抽象。目前已经具体实现的是Linux平台下的Container对象。每一个Container进程内部都是线程安全的。因为Container有可能被外部的进程销毁,所以每个方法都会对容器是否存在进行检测。

  1. ID():显示Container的ID,在Factor对象中已经说过,ID很重要,具有唯一性。
  2. Status():返回容器内进程是运行状态还是停止状态。通过执行“SIG=0”的KILL命令对进程是否存在进行检测。
  3. State():返回容器的状态,包括容器ID、配置信息、初始进程ID、进程启动时间、cgroup文件路径、namespace路径。通过调用Status()判断进程是否存在。
  4. Config():返回容器的配置信息,可在“配置参数解析”部分查看有哪些方面的配置信息。
  5. Processes():返回cgroup文件cgroup.procs中的值,在Docker背后的内核知识:cgroups资源限制部分的讲解中我们已经提过,cgroup.procs文件会罗列所有在该cgroup中的线程组ID(即若有线程创建了子线程,则子线程的PID不包含在内)。由于容器不断在运行,所以返回的结果并不能保证完全存活,除非容器处于“PAUSED”状态。
  6. Stats():返回容器的统计信息,包括容器的cgroups中的统计以及网卡设备的统计信息。Cgroups中主要统计了cpu、memory和blkio这三个子系统的统计内容,具体了解可以通过阅读“cgroups资源限制”部分对于这三个子系统统计内容的介绍来了解。网卡设备的统计则通过读取系统中,网络网卡文件的统计信息文件/sys/class/net/<EthInterface>/statistics来实现。
  7. Set():设置容器cgroup各子系统的文件路径。因为cgroups的配置是进程运行时也会生效的,所以我们可以通过这个方法在容器运行时改变cgroups文件从而改变资源分配。
  8. Start():构建ParentProcess对象,用于处理启动容器进程的所有初始化工作,并作为父进程与新创建的子进程(容器)进行初始化通信。传入的Process对象可以帮助我们追踪进程的生命周期,Process对象将在后文详细介绍。
    • 启动的过程首先会调用Status()方法的具体实现得知进程是否存活。
    • 创建一个管道(详见Docker初始化通信——管道)为后期父子进程通信做准备。
    • 配置子进程cmd命令模板,配置参数的值就是从factory.Create()传入进来的,包括命令执行的工作目录、命令参数、输入输出、根目录、子进程管道以及KILL信号的值。
    • 根据容器进程是否存在确定是在已有容器中执行命令还是创建新的容器执行命令。若存在,则把配置的命令构建成一个exec.Cmd对象、cgroup路径、父子进程管道及配置保留到ParentProcess对象中;若不存在,则创建容器进程及相应namespace,目前对user namespace有了一定的支持,若配置时加入user namespace,会针对配置项进行映射,默认映射到宿主机的root用户,最后同样构建出相应的配置内容保留到ParentProcess对象中。通过在cmd.Env写入环境变量_libcontainer_INITTYPE来告诉容器进程采用的哪种方式启动。
    • 执行ParentProcess中构建的exec.Cmd内容,即执行ParentProcess.start(),具体的执行过程在Process部分介绍。
    • 最后如果是新建的容器进程,还会执行状态更新函数,把state.json的内容刷新。
  9. Destroy():首先使用cgroup的freezer子系统暂停所有运行的进程,然后给所有进程发送SIGKIL信号(如果没有使用pid namespace就不对进程处理)。最后把cgroup及其子系统卸载,删除cgroup文件夹。
  10. Pause():使用cgroup的freezer子系统暂停所有运行的进程。
  11. Resume():使用cgroup的freezer子系统恢复所有运行的进程。
  12. NotifyOOM():为容器内存使用超界提供只读的通道,通过向cgroup.event_control写入eventfd(用作线程间通信的消息队列)和cgroup.oom_control(用于决定内存使用超限后的处理方式)来实现。
  13. Checkpoint():保存容器进程检查点快照,为容器热迁移做准备。通过使用CRIU的SWRK模式来实现,这种模式是CRIU另外两种模式CLI和RPC的结合体,允许用户需要的时候像使用命令行工具一样运行CRIU,并接受用户远程调用的请求,即传入的热迁移检查点保存请求,传入文件形式以Google的protobuf协议保存。
  14. Restore():恢复检查点快照并运行,完成容器热迁移。同样通过CRIU的SWRK模式实现,恢复的时候可以传入配置文件设置恢复挂载点、网络等配置信息。

至此,Container对象中的所有函数及相关功能都已经介绍完毕,包含了容器生命周期的全部过程。

TIPs: Docker初始化通信——管道

libcontainer创建容器进程时需要做初始化工作,此时就涉及到使用了namespace隔离后的两个进程间的通信。我们把负责创建容器的进程称为父进程,容器进程称为子进程。父进程clone出子进程以后,依旧是共享内存的。但是如何让子进程知道内存中写入了新数据依旧是一个问题,一般有四种方法。

  • 发送信号通知(signal)
  • 对内存轮询访问(poll memory)
  • sockets通信(sockets)
  • 文件和文件描述符(files and file-descriptors)

对于Signal而言,本身包含的信息有限,需要额外记录,namespace带来的上下文变化使其不易理解,并不是最佳选择。显然通过轮询内存的方式来沟通是一个非常低效的做法。另外,因为Docker会加入network namespace,实际上初始时网络栈也是完全隔离的,所以socket方式并不可行。

Docker最终选择的方式就是打开的可读可写文件描述符——管道。

Linux中,通过pipe(int fd[2])系统调用就可以创建管道,参数是一个包含两个整型的数组。调用完成后,在fd[1]端写入的数据,就可以从fd[0]端读取。

调用pipe函数后,创建的子进程会内嵌这个打开的文件描述符,对fd[1]写入数据后可以在fd[0]端读取。通过管道,父子进程之间就可以通信。通信完毕的奥秘就在于EOF信号的传递。大家都知道,当打开的文件描述符都关闭时,才能读到EOF信号,所以libcontainer中父进程先关闭自己这一端的管道,然后等待子进程关闭另一端的管道文件描述符,传来EOF表示子进程已经完成了初始化的过程。

4.3 Process 对象

Process 主要分为两类,一类在源码中就叫Process,用于容器内进程的配置和IO的管理;另一类在源码中叫ParentProcess,负责处理容器启动工作,与Container对象直接进行接触,启动完成后作为Process的一部分,执行等待、发信号、获得pid等管理工作。

ParentProcess对象,主要包含以下六个函数,而根据”需要新建容器”和“在已经存在的容器中执行”的不同方式,具体的实现也有所不同。

  • 已有容器中执行命令

    1. pid(): 启动容器进程后通过管道从容器进程中获得,因为容器已经存在,与Docker Deamon在不同的pid namespace中,从进程所在的namespace获得的进程号才有意义。
    2. start(): 初始化容器中的执行进程。在已有容器中执行命令一般由docker exec调用,在execdriver包中,执行exec时会引入nsenter包,从而调用其中的C语言代码,执行nsexec()函数,该函数会读取配置文件,使用setns()加入到相应的namespace,然后通过clone()在该namespace中生成一个子进程,并把子进程通过管道传递出去,使用setns()以后并没有进入pid namespace,所以还需要通过加上clone()系统调用。
    • 开始执行进程,首先会运行C代码,通过管道获得进程pid,最后等待C代码执行完毕。
    • 通过获得的pid把cmd中的Process替换成新生成的子进程。
    • 把子进程加入cgroup中。
    • 通过管道传配置文件给子进程。
    • 等待初始化完成或出错返回,结束。
  • 新建容器执行命令

    1. pid():启动容器进程后通过exec.Cmd自带的pid()函数即可获得。
    2. start():初始化及执行容器命令。
    • 开始运行进程。
    • 把进程pid加入到cgroup中管理。
    • 初始化容器网络。(本部分内容丰富,将从本系列的后续文章中深入讲解)
    • 通过管道发送配置文件给子进程。
    • 等待初始化完成或出错返回,结束。
  • 实现方式类似的一些函数

    • **terminate() **:发送SIGKILL信号结束进程。
    • **startTime() **:获取进程的启动时间。
    • signal():发送信号给进程。
    • wait():等待程序执行结束,返回结束的程序状态。

Process对象,主要描述了容器内进程的配置以及IO。包括参数Args,环境变量Env,用户User(由于uid、gid映射),工作目录Cwd,标准输入输出及错误输入,控制终端路径consolePath,容器权限Capabilities以及上述提到的ParentProcess对象ops(拥有上面的一些操作函数,可以直接管理进程)。

5. 总结

本文主要介绍了Docker容器管理的方式libcontainer,从libcontainer的使用到源码实现方式。我们深入到容器进程内部,感受到了libcontainer较为全面的设计。总体而言,libcontainer本身主要分为三大块工作内容,一是容器的创建及初始化,二是容器生命周期管理,三则是进程管理,调用方为Docker的execdriver。容器的监控主要通过cgroups的状态统计信息,未来会加入进程追踪等更丰富的功能。另一方面,libcontainer在安全支持方面也为用户尽可能多的提供了支持和选择。遗憾的是,容器安全的配置需要用户对系统安全本身有足够高的理解,user namespace也尚未支持,可见libcontainer依旧有很多工作要完善。但是Docker社区的火热也自然带动了大家对libcontainer的关注,相信在不久的将来,libcontainer就会变得更安全、更易用。

Docker背后的内核知识——cgroups资源限制

摘要

当我们谈论Docker时,我们常常会聊到Docker的实现方式。很多开发者都会知道,Docker的本质实际上是宿主机上的一个进程,通过namespace实现了资源隔离,通过cgroup实现了资源限制,通过UnionFS实现了Copy on Write的文件操作。但是当我们再深入一步的提出,namespace和cgroup实现细节时,知道的人可能就所剩无几了。浙江大学SEL/VLIS实验室孙健波同学在docker基础研究工作中着重对内核的cgroup技术做了细致的分析和梳理,希望能对读者深入理解Docker有所帮助

正文

上一篇中,我们了解了Docker背后使用的资源隔离技术namespace,通过系统调用构建一个相对隔离的shell环境,也可以称之为一个简单的“容器”。本文我们则要开始讲解另一个强大的内核工具——cgroups。他不仅可以限制被namespace隔离起来的资源,还可以为资源设置权重、计算使用量、操控进程启停等等。在介绍完基本概念后,我们将详细讲解Docker中使用到的cgroups内容。希望通过本文,让读者对Docker有更深入的了解。

1. cgroups是什么

cgroups(Control Groups)最初叫Process Container,由Google工程师(Paul Menage和Rohit Seth)于2006年提出,后来因为Container有多重含义容易引起误解,就在2007年更名为Control Groups,并被整合进Linux内核。顾名思义就是把进程放到一个组里面统一加以控制。官方的定义如下{![引自:https://www.kernel.org/doc/Documentation/cgroups/cgroups.txt]}。

cgroups是Linux内核提供的一种机制,这种机制可以根据特定的行为,把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。

通俗的来说,cgroups可以限制、记录、隔离进程组所使用的物理资源(包括:CPU、memory、IO等),为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基石。

对开发者来说,cgroups有如下四个有趣的特点:
* cgroups的API以一个伪文件系统的方式实现,即用户可以通过文件操作实现cgroups的组织管理。
* cgroups的组织管理操作单元可以细粒度到线程级别,用户态代码也可以针对系统分配的资源创建和销毁cgroups,从而实现资源再分配和管理。
* 所有资源管理的功能都以“subsystem(子系统)”的方式实现,接口统一。
* 子进程创建之初与其父进程处于同一个cgroups的控制组。

本质上来说,cgroups是内核附加在程序上的一系列钩子(hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。

2. cgroups的作用

实现cgroups的主要目的是为不同用户层面的资源管理,提供一个统一化的接口。从单个进程的资源控制到操作系统层面的虚拟化。Cgroups提供了以下四大功能{![参照自:http://en.wikipedia.org/wiki/Cgroups]}。

  • 资源限制(Resource Limitation):cgroups可以对进程组使用的资源总额进行限制。如设定应用运行时使用内存的上限,一旦超过这个配额就发出OOM(Out of Memory)。
  • 优先级分配(Prioritization):通过分配的CPU时间片数量及硬盘IO带宽大小,实际上就相当于控制了进程运行的优先级。
  • 资源统计(Accounting): cgroups可以统计系统的资源使用量,如CPU使用时长、内存用量等等,这个功能非常适用于计费。
  • 进程控制(Control):cgroups可以对进程组执行挂起、恢复等操作。

过去有一段时间,内核开发者甚至把namespace也作为一个cgroups的subsystem加入进来,也就是说cgroups曾经甚至还包含了资源隔离的能力。但是资源隔离会给cgroups带来许多问题,如PID在循环出现的时候cgroup却出现了命名冲突、cgroup创建后进入新的namespace导致脱离了控制等等{![详见:https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=a77aea92010acf54ad785047234418d5d68772e2]},所以在2011年就被移除了。

3. 术语表

  • task(任务):cgroups的术语中,task就表示系统的一个进程。
  • cgroup(控制组):cgroups 中的资源控制都以cgroup为单位实现。cgroup表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup。
  • subsystem(子系统):cgroups中的subsystem就是一个资源调度控制器(Resource Controller)。比如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量。
  • hierarchy(层级树):hierarchy由一系列cgroup以一个树状结构排列而成,每个hierarchy通过绑定对应的subsystem进行资源调度。hierarchy中的cgroup节点可以包含零或多个子节点,子节点继承父节点的属性。整个系统可以有多个hierarchy。

4. 组织结构与基本规则

大家在namespace技术的讲解中已经了解到,传统的Unix进程管理,实际上是先启动init进程作为根节点,再由init节点创建子进程作为子节点,而每个子节点由可以创建新的子节点,如此往复,形成一个树状结构。而cgroups也是类似的树状结构,子节点都从父节点继承属性。

它们最大的不同在于,系统中cgroup构成的hierarchy可以允许存在多个。如果进程模型是由init作为根节点构成的一棵树的话,那么cgroups的模型则是由多个hierarchy构成的森林。这样做的目的也很好理解,如果只有一个hierarchy,那么所有的task都要受到绑定其上的subsystem的限制,会给那些不需要这些限制的task造成麻烦。

了解了cgroups的组织结构,我们再来了解cgroup、task、subsystem以及hierarchy四者间的相互关系及其基本规则{![参照自:https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/sec-Relationships_Between_Subsystems_Hierarchies_Control_Groups_and_Tasks.html]}。

  • 规则1: 同一个hierarchy可以附加一个或多个subsystem。如下图1,cpu和memory的subsystem附加到了一个hierarchy。
    pic1
    图1 同一个hierarchy可以附加一个或多个subsystem

  • 规则2: 一个subsystem可以附加到多个hierarchy,当且仅当这些hierarchy只有这唯一一个subsystem。如下图2,小圈中的数字表示subsystem附加的时间顺序,CPU subsystem附加到hierarchy A的同时不能再附加到hierarchy B,因为hierarchy B已经附加了memory subsystem。如果hierarchy B与hierarchy A状态相同,没有附加过memory subsystem,那么CPU subsystem同时附加到两个hierarchy是可以的。
    pic2
    图2 一个已经附加在某个hierarchy上的subsystem不能附加到其他含有别的subsystem的hierarchy上

  • 规则3: 系统每次新建一个hierarchy时,该系统上的所有task默认构成了这个新建的hierarchy的初始化cgroup,这个cgroup也称为root cgroup。对于你创建的每个hierarchy,task只能存在于其中一个cgroup中,即一个task不能存在于同一个hierarchy的不同cgroup中,但是一个task可以存在在不同hierarchy中的多个cgroup中。如果操作时把一个task添加到同一个hierarchy中的另一个cgroup中,则会从第一个cgroup中移除。在下图3中可以看到,httpd进程已经加入到hierarchy A中的/cg1而不能加入同一个hierarchy中的/cg2,但是可以加入hierarchy B中的/cg3。实际上不允许加入同一个hierarchy中的其他cgroup野生为了防止出现矛盾,如CPU subsystem为/cg1分配了30%,而为/cg2分配了50%,此时如果httpd在这两个cgroup中,就会出现矛盾。
    pic3
    图3 一个task不能属于同一个hierarchy的不同cgroup

  • 规则4: 进程(task)在fork自身时创建的子任务(child task)默认与原task在同一个cgroup中,但是child task允许被移动到不同的cgroup中。即fork完成后,父子进程间是完全独立的。如下图4中,小圈中的数字表示task 出现的时间顺序,当httpd刚fork出另一个httpd时,在同一个hierarchy中的同一个cgroup中。但是随后如果PID为4840的httpd需要移动到其他cgroup也是可以的,因为父子任务间已经独立。总结起来就是:初始化时子任务与父任务在同一个cgroup,但是这种关系随后可以改变。
    pic4
    图4 刚fork出的子进程在初始状态与其父进程处于同一个cgroup

5. subsystem简介

subsystem实际上就是cgroups的资源控制系统,每种subsystem独立地控制一种资源,目前Docker使用如下八种subsystem,还有一种net_cls subsystem在内核中已经广泛实现,但是Docker尚未使用。他们的用途分别如下。

  • blkio: 这个subsystem可以为块设备设定输入/输出限制,比如物理驱动设备(包括磁盘、固态硬盘、USB等)。
  • cpu: 这个subsystem使用调度程序控制task对CPU的使用。
  • cpuacct: 这个subsystem自动生成cgroup中task对CPU资源使用情况的报告。
  • cpuset: 这个subsystem可以为cgroup中的task分配独立的CPU(此处针对多处理器系统)和内存。
  • devices 这个subsystem可以开启或关闭cgroup中task对设备的访问。
  • freezer 这个subsystem可以挂起或恢复cgroup中的task。
  • memory 这个subsystem可以设定cgroup中task对内存使用量的限定,并且自动生成这些task对内存资源使用情况的报告。
  • perf_event 这个subsystem使用后使得cgroup中的task可以进行统一的性能测试。{![perf: Linux CPU性能探测器,详见https://perf.wiki.kernel.org/index.php/Main_Page]}
  • *net_cls 这个subsystem Docker没有直接使用,它通过使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(TC:Traffic Controller)识别从具体cgroup中生成的数据包。

6. cgroups实现方式及工作原理简介

(1)cgroups实现结构讲解

cgroups的实现本质上是给系统进程挂上钩子(hooks),当task运行的过程中涉及到某个资源时就会触发钩子上所附带的subsystem进行检测,最终根据资源类别的不同使用对应的技术进行资源限制和优先级分配。那么这些钩子又是怎样附加到进程上的呢?下面我们将对照结构体的图表一步步分析,请放心,描述代码的内容并不多。

cgroup_struct
图5 cgroups相关结构体一览

Linux中管理task进程的数据结构为task_struct(包含所有进程管理的信息),其中与cgroup相关的字段主要有两个,一个是css_set *cgroups,表示指向css_set(包含进程相关的cgroups信息)的指针,一个task只对应一个css_set结构,但是一个css_set可以被多个task使用。另一个字段是list_head cg_list,是一个链表的头指针,这个链表包含了所有的链到同一个css_set的task进程(在图中使用的回环箭头,均表示可以通过该字段找到所有同类结构,获得信息)。

每个css_set结构中都包含了一个指向cgroup_subsys_state(包含进程与一个特定子系统相关的信息)的指针数组。cgroup_subsys_state则指向了cgroup结构(包含一个cgroup的所有信息),通过这种方式间接的把一个进程和cgroup联系了起来,如下图6。

cgroup_task
图6 从task结构开始找到cgroup结构

另一方面,cgroup结构体中有一个list_head css_sets字段,它是一个头指针,指向由cg_cgroup_link(包含cgroup与task之间多对多关系的信息,后文还会再解释)形成的链表。由此获得的每一个cg_cgroup_link都包含了一个指向css_set *cg字段,指向了每一个task的css_setcss_set结构中则包含tasks头指针,指向所有链到此css_set的task进程构成的链表。至此,我们就明白如何查看在同一个cgroup中的task有哪些了,如下图7。

cgroup_cglink
图7 cglink多对多双向查询

细心的读者可能已经发现,css_set中也有指向所有cg_cgroup_link构成链表的头指针,通过这种方式也能定位到所有的cgroup,这种方式与图1中所示的方式得到的结果是相同的。

那么为什么要使用cg_cgroup_link结构体呢?因为task与cgroup之间是多对多的关系。熟悉数据库的读者很容易理解,在数据库中,如果两张表是多对多的关系,那么如果不加入第三张关系表,就必须为一个字段的不同添加许多行记录,导致大量冗余。通过从主表和副表各拿一个主键新建一张关系表,可以提高数据查询的灵活性和效率。

而一个task可能处于不同的cgroup,只要这些cgroup在不同的hierarchy中,并且每个hierarchy挂载的子系统不同;另一方面,一个cgroup中可以有多个task,这是显而易见的,但是这些task因为可能还存在在别的cgroup中,所以它们对应的css_set也不尽相同,所以一个cgroup也可以对应多个·css_set

在系统运行之初,内核的主函数就会对root cgroupscss_set进行初始化,每次task进行fork/exit时,都会附加(attach)/分离(detach)对应的css_set

综上所述,添加cg_cgroup_link主要是出于性能方面的考虑,一是节省了task_struct结构体占用的内存,二是提升了进程fork()/exit()的速度。

cgroup_hashtable
图8 css_set与hashtable关系

当task从一个cgroup中移动到另一个时,它会得到一个新的css_set指针。如果所要加入的cgroup与现有的cgroup子系统相同,那么就重复使用现有的css_set,否则就分配一个新css_set。所有的css_set通过一个哈希表进行存放和查询,如上图8中所示,hlist_node hlist就指向了css_set_table这个hash表。

同时,为了让cgroups便于用户理解和使用,也为了用精简的内核代码为cgroup提供熟悉的权限和命名空间管理,内核开发者们按照Linux 虚拟文件系统转换器(VFS:Virtual Filesystem Switch)的接口实现了一套名为cgroup的文件系统,非常巧妙地用来表示cgroups的hierarchy概念,把各个subsystem的实现都封装到文件系统的各项操作中。有兴趣的读者可以在网上搜索并阅读VFS的相关内容,在此就不赘述了。

定义子系统的结构体是cgroup_subsys,在图9中可以看到,cgroup_subsys中定义了一组函数的接口,让各个子系统自己去实现,类似的思想还被用在了cgroup_subsys_state中,cgroup_subsys_state并没有定义控制信息,只是定义了各个子系统都需要用到的公共信息,由各个子系统各自按需去定义自己的控制信息结构体,最终在自定义的结构体中把cgroup_subsys_state包含进去,然后内核通过container_of(这个宏可以通过一个结构体的成员找到结构体自身)等宏定义来获取对应的结构体。

cgroup_subsys
图9 cgroup子系统结构体

(2)基于cgroups实现结构的用户层体现

了解了cgroups实现的代码结构以后,再来看用户层在使用cgroups时的限制,会更加清晰。

在实际的使用过程中,你需要通过挂载(mount)cgroup文件系统新建一个层级结构,挂载时指定要绑定的子系统,缺省情况下默认绑定系统所有子系统。把cgroup文件系统挂载(mount)上以后,你就可以像操作文件一样对cgroups的hierarchy层级进行浏览和操作管理(包括权限管理、子文件管理等等)。除了cgroup文件系统以外,内核没有为cgroups的访问和操作添加任何系统调用。

如果新建的层级结构要绑定的子系统与目前已经存在的层级结构完全相同,那么新的挂载会重用原来已经存在的那一套(指向相同的css_set)。否则如果要绑定的子系统已经被别的层级绑定,就会返回挂载失败的错误。如果一切顺利,挂载完成后层级就被激活并与相应子系统关联起来,可以开始使用了。

目前无法将一个新的子系统绑定到激活的层级上,或者从一个激活的层级中解除某个子系统的绑定。

当一个顶层的cgroup文件系统被卸载(umount)时,如果其中创建后代cgroup目录,那么就算上层的cgroup被卸载了,层级也是激活状态,其后代cgoup中的配置依旧有效。只有递归式的卸载层级中的所有cgoup,那个层级才会被真正删除。

层级激活后,/proc目录下的每个task PID文件夹下都会新添加一个名为cgroup的文件,列出task所在的层级,对其进行控制的子系统及对应cgroup文件系统的路径。

一个cgroup创建完成,不管绑定了何种子系统,其目录下都会生成以下几个文件,用来描述cgroup的相应信息。同样,把相应信息写入这些配置文件就可以生效,内容如下。

  • tasks:这个文件中罗列了所有在该cgroup中task的PID。该文件并不保证task的PID有序,把一个task的PID写到这个文件中就意味着把这个task加入这个cgroup中。
  • cgroup.procs:这个文件罗列所有在该cgroup中的线程组ID。该文件并不保证线程组ID有序和无重复。写一个线程组ID到这个文件就意味着把这个组中所有的线程加到这个cgroup中。
  • notify_on_release:填0或1,表示是否在cgroup中最后一个task退出时通知运行release agent,默认情况下是0,表示不运行。
  • release_agent:指定release agent执行脚本的文件路径(该文件在最顶层cgroup目录中存在),在这个脚本通常用于自动化umount无用的cgroup。

除了上述几个通用的文件以外,绑定特定子系统的目录下也会有其他的文件进行子系统的参数配置。

在创建的hierarchy中创建文件夹,就类似于fork中一个后代cgroup,后代cgroup中默认继承原有cgroup中的配置属性,但是你可以根据需求对配置参数进行调整。这样就把一个大的cgroup系统分割成一个个嵌套的、可动态变化的“软分区”。

7. cgroups的使用方法简介

(1)安装cgroups工具库

本节主要针对Ubuntu14.04版本系统进行介绍,其他Linux发行版命令略有不同,原理是一样的。不安装cgroups工具库也可以使用cgroups,安装它只是为了更方便的在用户态对cgroups进行管理,同时也方便初学者理解和使用,本节对cgroups的操作和使用都基于这个工具库。

apt-get install cgroup-bin

安装的过程会自动创建/cgroup目录,如果没有自动创建也不用担心,使用 mkdir /cgroup 手动创建即可。在这个目录下你就可以挂载各类子系统。安装完成后,你就可以使用lssubsys(罗列所有的subsystem挂载情况)等命令。

说明:也许你在其他文章中看到的cgroups工具库教程,会在/etc目录下生成一些初始化脚本和配置文件,默认的cgroup配置文件为/etc/cgconfig.conf,但是因为存在使LXC无法运行的bug,所以在新版本中把这个配置移除了,详见:https://bugs.launchpad.net/ubuntu/+source/libcgroup/+bug/1096771。

(2)查询cgroup及子系统挂载状态

在挂载子系统之前,可能你要先检查下目前子系统的挂载状态,如果子系统已经挂载,根据第4节中讲的规则2,你就无法把子系统挂载到新的hierarchy,此时就需要先删除相应hierarchy或卸载对应子系统后再挂载。

  • 查看所有的cgroup:lscgroup
  • 查看所有支持的子系统:lssubsys -a
  • 查看所有子系统挂载的位置: lssubsys –m
  • 查看单个子系统(如memory)挂载位置:lssubsys –m memory

(3)创建hierarchy层级并挂载子系统

在组织结构与规则一节中我们提到了hierarchy层级和subsystem子系统的关系,我们知道使用cgroup的最佳方式是:为想要管理的每个或每组资源创建单独的cgroup层级结构。而创建hierarchy并不神秘,实际上就是做一个标记,通过挂载一个tmpfs{![基于内存的临时文件系统,详见:http://en.wikipedia.org/wiki/Tmpfs]}文件系统,并给一个好的名字就可以了,系统默认挂载的cgroup就会进行如下操作。

mount -t tmpfs cgroups /sys/fs/cgroup

其中-t即指定挂载的文件系统类型,其后的cgroups是会出现在mount展示的结果中用于标识,可以选择一个有用的名字命名,最后的目录则表示文件的挂载点位置。

挂载完成tmpfs后就可以通过mkdir命令创建相应的文件夹。

mkdir /sys/fs/cgroup/cg1

再把子系统挂载到相应层级上,挂载子系统也使用mount命令,语法如下。

mount -t cgroup -o subsystems name /cgroup/name
其​​​中​​​ subsystems 是​​​使​​​用​​​,(逗号)​​​分​​​开​​​的​​​子​​​系​​​统​​​列​​​表,name 是​​​层​​​级​​​名​​​称​​​。具体我们以挂载cpu和memory的子系统为例,命令如下。

mount –t cgroup –o cpu,memory cpu_and_mem /sys/fs/cgroup/cg1

mount命令开始,-t后面跟的是挂载的文件系统类型,即cgroup文件系统。-o后面跟要挂载的子系统种类如cpumemory,用逗号隔开,其后的cpu_and_mem不被cgroup代码的解释,但会出现在/proc/mounts里,可以使用任何有用的标识字符串。最后的参数则表示挂载点的目录位置。

说明:如果挂载时提示mount: agent already mounted or /cgroup busy,则表示子系统已经挂载,需要先卸载原先的挂载点,通过第二条中描述的命令可以定位挂载点。

(4)卸载cgroup

目前cgroup文件系统虽然支持重新挂载,但是官方不建议使用,重新挂载虽然可以改变绑定的子系统和release agent,但是它要求对应的hierarchy是空的并且release_agent会被传统的fsnotify(内核默认的文件系统通知)代替,这就导致重新挂载很难生效,未来重新挂载的功能可能会移除。你可以通过卸载,再挂载的方式处理这样的需求。

卸载cgroup非常简单,你可以通过cgdelete命令,也可以通过rmdir,以刚挂载的cg1为例,命令如下。

rmdir /sys/fs/cgroup/cg1

rmdir执行成功的必要条件是cg1下层没有创建其它cgroup,cg1中没有添加任何task,并且它也没有被别的cgroup所引用。

cgdelete cpu,memory:/
使用cgdelete命令可以递归的删除cgroup及其命令下的后代cgroup,并且如果cgroup中有task,那么task会自动移到上一层没有被删除的cgroup中,如果所有的cgroup都被删除了,那task就不被cgroups控制。但是一旦再次创建一个新的cgroup,所有进程都会被放进新的cgroup中。

(5)设置cgroups参数

设置cgroups参数非常简单,直接对之前创建的cgroup对应文件夹下的文件写入即可,举例如下。

  • 设置task允许使用的cpu为0和1.
    echo 0-1 > /sys/fs/cgroup/cg1/cpuset.cpus

使用cgset命令也可以进行参数设置,对应上述允许使用0和1cpu的命令为:

cgset -r cpuset.cpus=0-1 cpu,memory:/

(6)添加task到cgroup

  • 通过文件操作进行添加
    echo [PID] > /path/to/cgroup/tasks
    上述命令就是把进程ID打印到tasks中,如果tasks文件中已经有进程,需要使用">>"向后添加。

  • 通过cgclassify将进程添加到cgroup
    cgclassify -g subsystems:path_to_cgroup pidlist
    这个命令中,subsystems指的就是子系统(如果使用man命令查看,可能也会使用controllers表示)​​​,如果mount了多个,就是用","隔开的子系统名字作为名称,类似cgset命令。

  • 通过cgexec直接在cgroup中启动并执行进程
    cgexec -g subsystems:path_to_cgroup command arguments
    commandarguments就表示要在cgroup中执行的命令和参数。cgexec常用于执行临时的任务。

(7)权限管理

与文件的权限管理类似,通过chown就可以对cgroup文件系统进行权限管理。

chown uid:gid /path/to/cgroup

uidgid分别表示所属的用户和用户组。

8. subsystem配置参数用法

(1)blkio – BLOCK IO资源控制

  • 限额类
    限额类是主要有两种策略,一种是基于完全公平队列调度(CFQ:Completely Fair Queuing )的按权重分配各个cgroup所能占用总体资源的百分比,好处是当资源空闲时可以充分利用,但只能用于最底层节点cgroup的配置;另一种则是设定资源使用上限,这种限额在各个层次的cgroup都可以配置,但这种限制较为生硬,并且容器之间依然会出现资源的竞争。

    • 按比例分配块设备IO资源
    1. blkio.weight:填写100-1000的一个整数值,作为相对权重比率,作为通用的设备分配比。
    2. blkio.weight_device: 针对特定设备的权重比,写入格式为device_types:node_numbers weight,空格前的参数段指定设备,weight参数与blkio.weight相同并覆盖原有的通用分配比。{![查看一个设备的device_types:node_numbers可以使用:ls -l /dev/DEV,看到的用逗号分隔的两个数字就是。有的文章也称之为major_number:minor_number。]}
    • 控制IO读写速度上限
      1. blkio.throttle.read_bps_device:按每秒读取块设备的数据量设定上限,格式device_types:node_numbers bytes_per_second
      2. blkio.throttle.write_bps_device:按每秒写入块设备的数据量设定上限,格式device_types:node_numbers bytes_per_second
      3. blkio.throttle.read_iops_device:按每秒读操作次数设定上限,格式device_types:node_numbers operations_per_second
      4. blkio.throttle.write_iops_device:按每秒写操作次数设定上限,格式device_types:node_numbers operations_per_second
    • 针对特定操作(read, write, sync, 或async)设定读写速度上限
      1. blkio.throttle.io_serviced:针对特定操作按每秒操作次数设定上限,格式device_types:node_numbers operation operations_per_second
      2. blkio.throttle.io_service_bytes:针对特定操作按每秒数据量设定上限,格式device_types:node_numbers operation bytes_per_second
  • 统计与监控
    以下内容都是只读的状态报告,通过这些统计项更好地统计、监控进程的 io 情况。

    1. blkio.reset_stats:重置统计信息,写入一个int值即可。
    2. blkio.time:统计cgroup对设备的访问时间,按格式device_types:node_numbers milliseconds读取信息即可,以下类似。
    3. blkio.io_serviced:统计cgroup对特定设备的IO操作(包括read、write、sync及async)次数,格式device_types:node_numbers operation number
    4. blkio.sectors:统计cgroup对设备扇区访问次数,格式 device_types:node_numbers sector_count
    5. blkio.io_service_bytes:统计cgroup对特定设备IO操作(包括read、write、sync及async)的数据量,格式device_types:node_numbers operation bytes
    6. blkio.io_queued:统计cgroup的队列中对IO操作(包括read、write、sync及async)的请求次数,格式number operation
    7. blkio.io_service_time:统计cgroup对特定设备的IO操作(包括read、write、sync及async)时间(单位为ns),格式device_types:node_numbers operation time
    8. blkio.io_merged:统计cgroup 将 BIOS 请求合并到IO操作(包括read、write、sync及async)请求的次数,格式number operation
    9. blkio.io_wait_time:统计cgroup在各设​​​备​​​中各类型​​​IO操作(包括read、write、sync及async)在队列中的等待时间​(单位ns),格式device_types:node_numbers operation time
    10. blkio.*_recursive:各类型的统计都有一个递归版本,Docker中使用的都是这个版本。获取的数据与非递归版本是一样的,但是包括cgroup所有层级的监控数据。

(2) cpu – CPU资源控制

CPU资源的控制也有两种策略,一种是完全公平调度 (CFS:Completely Fair Scheduler)策略,提供了限额和按比例分配两种方式进行资源控制;另一种是实时调度(Real-Time Scheduler)策略,针对实时进程按周期分配固定的运行时间。配置时间都以微秒(µs)为单位,文件名中用us表示。

  • CFS调度策略下的配置

    • 设定CPU使用周期使用时间上限
    1. cpu.cfs_period_us:设定周期时间,必须与cfs_quota_us配合使用。
    2. cpu.cfs_quota_us :设定周期内最多可使用的时间。这里的配置指task对单个cpu的使用上限,若cfs_quota_uscfs_period_us的两倍,就表示在两个核上完全使用。数值范围为1000 – 1000,000(微秒)。
    3. cpu.stat:统计信息,包含nr_periods(表示经历了几个cfs_period_us周期)、nr_throttled(表示task被限制的次数)及throttled_time(表示task被限制的总时长)。
    • 按权重比例设定CPU的分配
    1. cpu.shares:设定一个整数(必须大于等于2)表示相对权重,最后除以权重总和算出相对比例,按比例分配CPU时间。(如cgroup A设置100,cgroup B设置300,那么cgroup A中的task运行25%的CPU时间。对于一个4核CPU的系统来说,cgroup A 中的task可以100%占有某一个CPU,这个比例是相对整体的一个值。)
  • RT调度策略下的配置
    实时调度策略与公平调度策略中的按周期分配时间的方法类似,也是在周期内分配一个固定的运行时间。

    1. cpu.rt_period_us :设定周期时间。
    2. cpu.rt_runtime_us:设定周期中的运行时间。

(3) cpuacct – CPU资源报告

这个子系统的配置是cpu子系统的补充,提供CPU资源用量的统计,时间单位都是纳秒。
1. cpuacct.usage:统计cgroup中所有task的cpu使用时长
2. cpuacct.stat:统计cgroup中所有task的用户态和内核态分别使用cpu的时长
3. cpuacct.usage_percpu:统计cgroup中所有task使用每个cpu的时长

(4)cpuset – CPU绑定

为task分配独立CPU资源的子系统,参数较多,这里只选讲两个必须配置的参数,同时Docker中目前也只用到这两个。
1. cpuset.cpus:在这个文件中填写cgroup可使用的CPU编号,如0-2,16代表 0、1、2和16这4个CPU。
2. cpuset.mems:与CPU类似,表示cgroup可使用的memory node,格式同上

(5) device – 限制task对device的使用

  • **设备黑/白名单过滤 **
    1. devices.allow:允许名单,语法type device_types:node_numbers access typetype有三种类型:b(块设备)、c(字符设备)、a(全部设备);access也有三种方式:r(读)、w(写)、m(创建)。
    2. devices.deny:禁止名单,语法格式同上。
  • 统计报告
    1. devices.list:报​​​告​​​为​​​这​​​个​​​ cgroup 中​​​的​task设​​​定​​​访​​​问​​​控​​​制​​​的​​​设​​​备

(6) freezer – 暂停/恢复cgroup中的task

只有一个属性,表示进程的状态,把task放到freezer所在的cgroup,再把state改为FROZEN,就可以暂停进程。不允许在cgroup处于FROZEN状态时加入进程。
* **freezer.state **,包括如下三种状态:
– FROZEN 停止
– FREEZING 正在停止,这个是只读状态,不能写入这个值。
– THAWED 恢复

(7) memory – 内存资源管理

  • 限额类

    1. memory.limit_in_bytes:强制限制最大内存使用量,单位有kmg三种,填-1则代表无限制。
    2. memory.soft_limit_in_bytes:软限制,只有比强制限制设置的值小时才有意义。填写格式同上。当整体内存紧张的情况下,task获取的内存就被限制在软限制额度之内,以保证不会有太多进程因内存挨饿。可以看到,加入了内存的资源限制并不代表没有资源竞争。
    3. memory.memsw.limit_in_bytes:设定最大内存与swap区内存之和的用量限制。填写格式同上。
  • 报警与自动控制

    1. memory.oom_control:改参数填0或1, 0表示开启,当cgroup中的进程使用资源超过界限时立即杀死进程,1表示不启用。默认情况下,包含memory子系统的cgroup都启用。当oom_control不启用时,实际使用内存超过界限时进程会被暂停直到有空闲的内存资源。
  • 统计与监控类

    1. memory.usage_in_bytes:报​​​告​​​该​​​ cgroup中​​​进​​​程​​​使​​​用​​​的​​​当​​​前​​​总​​​内​​​存​​​用​​​量(以字节为单位)
    2. memory.max_usage_in_bytes:报​​​告​​​该​​​ cgroup 中​​​进​​​程​​​使​​​用​​​的​​​最​​​大​​​内​​​存​​​用​​​量
    3. memory.failcnt:报​​​告​​​内​​​存​​​达​​​到​​​在​​​ memory.limit_in_bytes设​​​定​​​的​​​限​​​制​​​值​​​的​​​次​​​数​​​
    4. memory.stat:包含大量的内存统计数据。
    • cache:页​​​缓​​​存​​​,包​​​括​​​ tmpfs(shmem),单位为字节。
    • rss:匿​​​名​​​和​​​ swap 缓​​​存​​​,不​​​包​​​括​​​ tmpfs(shmem),单位为字节。
    • mapped_file:memory-mapped 映​​​射​​​的​​​文​​​件​​​大​​​小​​​,包​​​括​​​ tmpfs(shmem),单​​​位​​​为​​​字​​​节​​​
    • pgpgin:存​​​入​​​内​​​存​​​中​​​的​​​页​​​数​​​
    • pgpgout:从​​​内​​​存​​​中​​​读​​​出​​​的​​​页​​​数
    • swap:swap 用​​​量​​​,单​​​位​​​为​​​字​​​节​​​
    • active_anon:在​​​活​​​跃​​​的​​​最​​​近​​​最​​​少​​​使​​​用​​​(least-recently-used,LRU)列​​​表​​​中​​​的​​​匿​​​名​​​和​​​ swap 缓​​​存​​​,包​​​括​​​ tmpfs(shmem),单​​​位​​​为​​​字​​​节​​​
    • inactive_anon:不​​​活​​​跃​​​的​​​ LRU 列​​​表​​​中​​​的​​​匿​​​名​​​和​​​ swap 缓​​​存​​​,包​​​括​​​ tmpfs(shmem),单​​​位​​​为​​​字​​​节
    • active_file:活​​​跃​​​ LRU 列​​​表​​​中​​​的​​​ file-backed 内​​​存​​​,以​​​字​​​节​​​为​​​单​​​位
    • inactive_file:不​​​活​​​跃​​​ LRU 列​​​表​​​中​​​的​​​ file-backed 内​​​存​​​,以​​​字​​​节​​​为​​​单​​​位
    • unevictable:无​​​法​​​再​​​生​​​的​​​内​​​存​​​,以​​​字​​​节​​​为​​​单​​​位​​​
    • hierarchical_memory_limit:包​​​含​​​ memory cgroup 的​​​层​​​级​​​的​​​内​​​存​​​限​​​制​​​,单​​​位​​​为​​​字​​​节​​​
    • hierarchical_memsw_limit:包​​​含​​​ memory cgroup 的​​​层​​​级​​​的​​​内​​​存​​​加​​​ swap 限​​​制​​​,单​​​位​​​为​​​字​​​节​​​

8. 总结

本文由浅入深的讲解了cgroups的方方面面,从cgroups是什么,到cgroups该怎么用,最后对大量的cgroup子系统配置参数进行了梳理。可以看到,内核对cgroups的支持已经较为完善,但是依旧有许多工作需要完善。如网络方面目前是通过TC(Traffic Controller)来控制,未来需要统一整合;资源限制并没有解决资源竞争,在各自限制之内的进程依旧存在资源竞争,优先级调度方面依旧有很大的改进空间。希望通过本文帮助大家了解cgroups,让更多人参与到社区的贡献中。

9. 作者简介

孙健波,浙江大学SEL实验室硕士研究生,目前在云平台团队从事科研和开发工作。浙大团队对PaaS、Docker、大数据和主流开源云计算技术有深入的研究和二次开发经验,团队现将部分技术文章贡献出来,希望能对读者有所帮助。

参考资料

https://sysadmincasts.com/episodes/14-introduction-to-linux-control-groups-cgroups

https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/Resource_Management_Guide/index.html

http://www.cnblogs.com/lisperl/archive/2013/01/14/2860353.html

https://www.kernel.org/doc/Documentation/cgroups