写给工程师的十条精进原则(转自美团点评技术团队)

引言

时间回到8年前,我人生中第一份实习的工作,是在某互联网公司的无线搜索部做一个C++工程师。当时的我可谓意气风发,想要大干一场,结果第一次上线就写了人生中第一个Casestudy。由于对部署环境的不了解,把SVN库里的配置文件错误地发到线上,并且上完线就去吃晚饭了,等吃饭回来发现师傅在焦头烂额地回滚配置。那次故障造成了一个核心服务20分钟不可用,影响了几百万的用户。这仅仅是一个开始,在后来半年的时间里,我几乎把所有职场新人可能犯的错误都犯了个遍。架构师让我调研一个抓取性能提升方案,我闷头搞了两周,也没有得出任何结论;本来安排好的开发计划,由于我临时要回去写论文,搞得经理措手不及;参加项目座谈会,全程“打酱油”……那段时间,自己也很苦恼,几乎每天晚上11点多才走,很累很辛苦,但依然拿不到想要的结果。

8年过去了,自己从一个职场小白逐步成长为一名技术Leader。我发现团队中的很多同学在不停地重复犯着自己当年类似的错误。他们并不是不努力,到底是哪里出了问题?经过一段时间的观察与思考后,我想我找到了答案。那就是:我们大多数同学在工作中缺乏原则的指导。原则,犹如指引行动的“灯塔”,它连接着我们的价值观与行动。不久前,桥水基金创始人雷·达里奥在《原则》一书中所传达的理念,引爆了朋友圈。每个人都应该有自己的原则,当我们需要作出选择时,一定要坚持以原则为中心。但是在现实生活中,我们往往缺少对原则的总结,对于很多人来说这是一门“只可意会不可言传”的玄学,是属于老司机的秘密,其实不然。

“追求卓越”是美团的价值观。作为一名技术人员,我们应该如何践行呢?本文总结了十条精进原则,希望能够给大家带来一些启发,更好地指导我们的行动。

原则一:Owner意识

“Owner意识”主要体现在两个层面:一是认真负责的态度,二是积极主动的精神。

认真负责是工作的底线。首先,要对我们交付的结果负责。项目中每一个设计文档、每一行代码都需要认真完成,要对它的质量负责。如果设计文档逻辑混乱,代码没有注释,测试时发现一堆Bug,影响的不仅仅是RD的工程交付质量,还会对协同工作的RD、QA、PM等产生不好的影响。久而久之,团队的整体交付质量、工作效率也会逐步下降,甚至会导致团队成员之间产生不信任感。其次,我们要对开发的系统负责。系统的架构是否需要改进,接口文档是否完善,日志是否完整,数据库是否需要扩容,缓存空间够不够等等,这些都是需要落地的事情。作为系统Owner,请一定要认真履行。

积极主动是“Owner意识”更高一级的要求。RD每天要面对大量的工作,而且很多并不在计划内,这就需要具备一种积极主动的精神。例如我们每天可能会面对大量的技术咨询,如果客户提出的问题很长时间得不到回应的话,就会带来不好的客户体验。很多同学说忙于自己的工作没有时间处理,有同学觉得这件事不是很重要,也有很多同学是看到了,但是不知道怎么回答,更有甚者,看到了干脆装没看见。这些都是缺乏Owner意识的体现。正确的做法是积极主动地推动问题的解决,如果时间无法排开或者不知道如何解决,可以直接将问题反馈给能解决的同学。积极主动还可以表现在更多方面。比如很多同学会自发地梳理负责服务的现状,根据接口在性能方面暴露的问题提出改进意见并持续推动解决;也有同学在跨团队沟通中主动承担起主R的角色,积极发现问题、暴露问题,推动合作团队的进度,保证项目顺利推进。这些同学无一不是团队的中坚力量。所以,我们在做好自己份内工作的同时,也应该积极主动地投入到“份外”的工作中去。一分耕耘一分收获,不要给自己设限,努力成为一个更加优秀的人。

原则二:时间观念

相信大家都有时间观念,但是真正能执行到位的可能并没有那么多。互联网是一个快速发展的行业,RD的研发效率是一个公司硬实力的重要体现。项目的按期交付是一项很重要的执行能力,在很大程度上决定着领导和同事对自己靠谱程度的评价。大家可能会问:难度几乎相同的项目,为什么有的同学经常Delay,而有的同学每次都能按时上线?一个很重要的原因,就是这些按时交付的同学往往具备如下两个特质:做事有计划,工作分主次。

工作安排要有计划性。通常,RD在设计评审之后就能预估出精确的开发时间,进而再合理地安排开发、联调、测试计划。如果是项目负责人,那么就会涉及协调FE、QA、PM等多个工种的同学共同完成工作。凡事预则立,不预则废。在计划制定过程中,要尽可能把每一项拆细一点(至少到pd粒度)。事实证明,粒度越细,计划就越精准,实际开发时间与计划之间的误差就会越小。此外,务必要规定明确的可检查的产出,并在计划中设置一些关键的时间点进行核对。无数血淋淋的事实告诉我们,很多项目延期都是因为在一些关键交付点上双方存在分歧造成的。例如后台RD的接口文档计划在周五提供,FE认为是周五上午,而RD认为是周五下班前提交,无形中会给排期带来了1pd的误差。所以,我们要做到计划粒度足够细,关键时间点要可检查。

工作安排要分清楚主次。我们每天要面对很多的事情,要学会分辨这些工作的主次。可以尝试使用“艾森豪威尔法则”(四象限法则),把工作按照重要、紧急程度分成四象限。优先做重要紧急的事情;重要不紧急的事情可以暂缓做,但是要持续推进;紧急不重要的事情可以酌情委托给最合适的人做;不重要不紧急的事情可以考虑不做。很多项目无法按期交付的原因,都是因为执行人分不清主次。比如在开发中需要使用到ES,一些不熟悉ES的同学可能想系统性地学习一下这方面的知识,就会一头扎进ES的汪洋中。最后才发现,原本一天就能完成的工作被严重拖后。实际工作中,我们应当避免这种“本末倒置”的工作方式。在本例中,“系统性地学习ES”是一件重要但不紧急的事情。要学会分辨出这些干扰的工作项,保证重要紧急的事情能够按时交付。

原则三:以终为始

“以终为始”(Begin With The End In Mind),是史蒂芬·柯维在《高效能人士的七个习惯》中提到的一个习惯。它是以所有事物都经过两次创造的原则(第一次为心智上的创造,第二次为实际的创造)为基础的。直观的表达就是:先想清楚目标,然后努力实现。

在工作中,很多RD往往只是埋头走路,很少抬头看天。每次季度总结的时候,罗列了很多项目,付出很多努力。但是具体这些项目取得了哪些收益,对业务有哪些提升,却很难说出来。这就说明在工作中并没有遵守“以终为始”这一原则。此外,很多同学在做需求的过程中,对于目标与收益关注不够,系统上线之后,也没有持续地跟进使用效果。这一点在技术优化项目中体现得尤为明显。例如在一个接口性能优化的项目中,经过RD的努力优化,系统TP99缩短了60%,支持QPS提升了2倍。但是系统到底需要优化到什么程度呢?是不是缩短60%,提升2倍就能满足需求呢?在优化之前,很多同学常常忘记设置一个预设的目标(TP99小于多少,支持QPS大于多少)。我们必须清楚,优化一定是有原因的,比如预期某节假日流量会暴增或者某接口超时比例过高,如果不进行优化,系统可能会存在宕机风险。解决特定的问题才是技术优化的最终目的,所以要根据问题设定目标,再进行优化

“以终为始”,这一原则还可以作用于我们的学习中。很多同学看过很多技术文章,但是总是感觉自己仍然一无所知。很重要的一个原因,就是没有带着目标去学习。在这个信息爆炸的时代,如果只是碎片化地接收各个公众号推送的文章,效果几乎可以忽略不计。在学习之前,我们一定要问自己,这次学习的目标是什么?是想把Redis的持久化原理搞清楚,还是把Redis的主从同步机制弄明白,亦或是想学习整个Redis Cluster的架构体系。如果我们能够带着问题与目标,再进行相关的资料搜集与学习,就会事半功倍。这种学习模式的效果会比碎片化阅读好很多。

原则四:闭环思维

你是否遇到过这样的场景:参加了一个设计(或需求)评审,大家兴致勃勃地提了很多合理的意见,等到再次评审的时候,却发现第一次提的很多问题都没有得到改进,很多讨论过的问题需要从头再开始讨论。这种情况就是一种典型的工作不闭环。

之前看过一句话:一个人是否靠谱,就看他能否做到凡事有交代,件件有着落,事事有回音。这就是闭环思维的重要性。它强调的是一种即时反馈闭环,如果别人给我们分配了一个任务,不管完成的结果如何,一定要在规定的时间内给出明确的反馈。例如在跨部门的沟通会议中,虽然各方达成了一致,会议发起者已经将最终的记录周知大家。但是,到这一步其实并没有完成真正的闭环,在落地执行过程中很可能还存在一些潜在的问题。例如,会议纪要是否经各方仔细核对并确认过?会议中明确的To Do进展是什么?完成结果有没有Check的机制?如果这些没有做到的话,就会陷入“沟通-发现问题-再沟通-再发现问题”的恶性循环中。真正的闭环,要求我们对工作中的事情都能够养成良好的思维习惯,沟通要有结论,通知要有反馈,To Do要有验收。

“闭环思维”还要求能够定期主动进行阶段性的反馈。刚参加工作时,我接了一个工期为两个月的项目。整个项目需要独自完成,自己每天按照计划,有条不紊地进行开发。大概过了两周之后,Leader询问项目进度,虽然我已经跟他说没问题。然而,Leader告诉我,因为我每天对着电脑也不说话,让他心里很没底。这时,我才意识到一个很重要的问题,我跟Leader之间存在信息不对称。从那以后,我就时不时得跟他汇报一下进度,哪怕就只有简短的一句话,也可以明显感觉,他对我的信心增加了很多。特别是我做Leader之后,对这种闭环反馈的理解,就更加深刻了。从Leader的角度看,其实只是想知道项目是否在正常推进,是否遇到问题需要他协助解决。

原则五:保持敬畏

“君子之心,常怀敬畏”,保持敬畏之心能够让我们少犯错误。在工作中存在各种各样的规范,例如代码规范、设计规范、上线规范等等。我们必须明白,这些规范的制定一定是基于某些客观原因的,它们都是历史上无数Case积累而来的经验。团队里的每一个成员都应该学习并严格遵守,这一点对于新人尤其重要。

当我们进入到一个新的团队,请先暂时忘掉之前的习惯,要尽快学习团队既有的规范,并且让自己与团队保持一致。以编码风格为例,很多同学往往习惯于自己之前的代码写作风格,在做新公司第一个项目时,也按照自己的习惯进行变量、包的命名等等。结果在代码Review过程中,被提了很多修改意见,不得不返工重写,得不偿失。如果能够保持敬畏之心,提前了解编码规范,这种问题完全可以避免。类似的问题,还包括对上线流程不了解,对回滚操作不熟悉,对SRE线上变更过程不了解等等。除了这些显而易见的规范,还有一些约定俗成的规则。个人建议是:如果有事情拿不准,不妨多问问其他同事,不要凭自己的感觉做事情。

保持敬畏之心并不意味着要“因循守旧”。在我们充分了解这些规范和约定之后,如果觉得存在不妥之处,可以跟全组同学讨论,是否采纳新的建议,然后及时去更新迭代。其实,让规范与约定与时俱进,也是另一种形式的敬畏。

原则六:事不过二

“事不过二”,是我们团队一贯坚持的原则,它可以解读为两层含义。

一层含义是“所有的评审与问题讨论,不要超过两次”。之所以有这样的要求,是因为我们发现,很多RD都把时间花费在一些无休止的评审与问题讨论中,真正投入到实际开发中的时间反而很少。在实际工作场景中,我们经常会遇到一些不是很成熟的需求评审。这些需求文档,要么是背景与目标含糊不清,要么是产品方案描述不够细化,或者存在歧义。RD与PM被迫反复进行讨论,我曾经遇到过一个需求评审,进行了三次还被打回。同样的问题,在设计评审中也屡见不鲜。方案固然需要经过反复的讨论,但是如果迟迟不能达成一致,就会耗费很多RD与PM的宝贵时间,这就与提升研发效率的理念背道而驰。因此,我们团队规定:所有的评审最多两次。通过这种方式,倒逼利益相关方尽可能地做好需求与方案设计。评审会议组织前,尝试与所有相关人员达成一致,询问对方的意见,并进行有针对性的讨论,这样能够大大提升评审会议的效率和质量。如果在第一次评审中不通过,那么就只有一次机会进行复审。一旦两次不通过,就需要进行Casestudy。

“事不过二”原则的另一层含义,是“同样的错误不能犯第二次”。每次故障之后,Casestudy都必须进行深刻的总结复盘,对故障原因进行5Why分析,给出明确可执行的To Do List。每次季度总结会,大家自我反省问题所在,在下个季度必须有所改善,不能再犯类似的错误。孔子云:“不迁怒,不贰过”,在错误中反思与成长,才能让我们成为更优秀的人。

原则七:设计优先

“设计优先”这条原则,相对来说更加具体一些。之所以单列一项,是因为架构设计太重要了。Uncle Bob曾说过:“软件架构的目标,是为了让构建与维护系统的所需人力资源最小化。”

架构设计,并不仅仅关系到系统的质量,还关乎团队的效能问题。很多团队也有明文规定,开发周期在3pd以上的项目必须有设计文档,开发周期在5pd以上的项目必须有设计评审。在具体的执行过程中,由于各种原因,设计往往并不能达到预期的效果。究其原因,有的是因为项目周期紧,来不及设计得足够详细;有的是因为RD主观上认为项目比较简单,设计草草了事。无数事实证明,忽略了前期设计,往往会导致后续开发周期被大幅拉长,给项目带来了很大的Delay风险。而且最可怕的是,不当的设计会给项目带来巨大的后期维护成本,我们不得不腾出时间,专门进行项目的优化与重构。因此,无论什么时候都要记住“设计优先”这一原则。磨刀不误砍柴工,前期良好的设计,会给项目开发以及后期维护带来极大的收益。

“设计优先”这一原则,要求写别人看得懂的设计。我们了解一个系统最直接的途径就是结合设计文档与代码。在实际工作中,很多同学的设计文档让大家看得一头雾水,通篇下来,看不出系统整体的设计思路。其实,设计的过程是一种智力上的创造,我们更希望它能成为个人与集体智慧的结晶。如何才能让我们的设计变得通俗易懂?我个人认为,设计应该尽量使用比较合理的逻辑,进而把设计中的一些点组织起来。比如可以使用从抽象到具体,由总到分的结构来组织材料。在设计过程中,要以需求为出发点,通过合理的抽象把问题简化,讲清楚各个模块之间的关系,再详细分述模块的实现细节。做完设计之后,可以发给比较资深的RD或者PM审阅一下,根据他们的反馈再进行完善。好的设计,一定是逻辑清晰易懂、细节落地可执行的。

原则八:P/PC平衡

“P/PC平衡”原则,即产出与产能平衡原则。伊索寓言中讲述了一个《生金蛋的鹅》的故事。产出好比“金蛋”,产能好比“会下金蛋的鹅”。“重蛋轻鹅”的人,最终可能连产蛋的资产都保不住;“重鹅轻蛋”的人,最终可能会被饿死。产出与产能必须平衡,才能达到真正的高效能。为了让大家更清晰的了解这一原则,本文举两个例子。

从系统的角度看,每一个系统都是通过持续不断地叠加功能来实现其产出,而系统的产能是通过系统架构的可扩展性、稳定性等一系列特性来表征。为了达到产出与产能的平衡,需要在不断支持业务需求的过程中,持续进行技术架构层面的优化。如果一味地做业务需求,经过一定的时间,系统会越来越慢,最终影响业务的稳定性;反之,一个没有任何业务产出的系统,最终会消亡。

再从RD的角度来看这个问题,RD通过做需求来给公司创造价值,实现自己的产出。而RD的产能是指技术能力、软素质、身体健康状况,有这些资本后,我们才能进行持续的产出。在日常工作中,我发现很多RD往往只重视产出。他们也在很努力地做项目,但是每一个项目所使用的方法,还是沿用自己先前一贯的思路。最终,不仅项目做得一般,还会抱怨自己得不到任何成长。这就是P/PC不平衡的体现。如果能在做项目的过程中,通过学习总结持续提升自己的技术能力和软素质,并将其应用于项目实施交付中,相信一定会取得双赢的结果。

“P/PC平衡”原则还适用于很多其他的领域,例如团队、家庭等,我本人也非常推崇这一原则。希望大家也能将其作为自身的一项基本原则,努力寻找到产出与产能的平衡点。

原则九:善于提问

“善于提问”,首先要勤于提问。求知欲源于好奇心,是人类的一种本能。在工作中要养成勤于提问的好习惯,不懂就问,不要因为自己一时懒惰或者碍于情面,就放弃提问的机会。当遇到不同的观点时,也要礼貌地问出来。波克定理告诉我们,只有在争辩中,才可能诞生最好的主意和最好的决定

在设计评审、代码评审这类体现集体智慧的活动中,遇到有问题的地方一定要提出来。我经常看到,很多同学评审全程一言不发,这就是浪费大家的时间。设计评审的目的,是让大家针对方案提出改进意见并达成一致,如果全程“打酱油”,那就失去了评审的意义。我们鼓励大家多提问,把自己内心的疑惑表达出来,然后通过交流的方式得到答案。

“善于提问”,还要懂得如何提问。为什么同样是参加设计评审,有的同学就能提出很好的问题,而有的同学却提不出任何问题?除了知识储备、专业技能、经验等方面的差异外,还有一点很重要:批判性思维。

批判性思维主张通过批判性思考达到理性思维,即对事物本质的认知和掌握。关于如何进行批判性思维,大家可以参考一些经典的图书如《批判性思维》、《学会提问》等。在工作中面临一项决策时,会有各种各样的意见摆在你面前,所以我们必须要学会使用批判性思维来进行分析,每个人的论据是否可靠,论证是否合理,是否有隐含的立场。同样,在阅读一篇技术博客的时候,也要使用批判性的思维,多问几个为什么,作者得出的结论是否合理?论据是否充分?只有这样,才能不断地获取真正的知识。

原则十:空杯心态

“满招损,谦受益”,“空杯心态”是最后一项原则。我觉得这也是一个人能够持续成长的前提。做技术的人,骨子里通常有股傲气,并且会随着资历、成绩的提升而不断增加。初入职场的小白,可能会非常谦虚,但是工作几年之后,专业技能逐步提升,可能还取得了一些小成就,人就会越来越自信。这时候,如果不能始终保持“空杯心态”,这种自信就会逐步演变为自满。自满的人,往往表现为工作中把别人的建议当成是批评,不接受任何反对意见,学习上也缺乏求知的动力,总是拿自己的长处去跟别人的短处做比较。其实每个人多少都会有一些自满,可怕的是不知道甚至不愿承认自满。

保持“空杯心态”这一原则要求我们时刻进行自我检视与反省。在工作中,多去跟不同级别的同学聊一聊,或者做一个360度评估,这有助于我们更加客观地评价自己。在横向对比中,多向那些优秀的同学看齐,学习他人的优点。很多同学在设计评审或者代码review过程中,针对别人提出的问题与建议,往往都采用一种对立的态度。错误地认为别人是在挑刺,是在针对自己。诚然,在某些方面,我们可能确实比其他人想得深入,但是这不代表在所有方面都能考虑周全。对于别人的建议,建议使用“善于提问”原则里提到的批判性思维仔细分析一下,虚心地吸取那些好的建议。

工作学习就像“练级打怪”,技能储备的越多,就越容易走到最后。保持空杯心态,可以让我们发现很多以前注意不到的新能力,我们要做的就是努力学习它,将它们转化为自己能力库的一部分。

总结

以上,是我总结的工作与学习的十条基本原则。其中有的侧重于个人做事情的方法,如“Owner意识”、“时间观念”、“以终为始”、”闭环思维”;有的侧重于团队工作标准规范,如“保持敬畏”、“事不过二”、“设计优先”;有的侧重于团队或个人效能提升,如“P/PC平衡”、“善于提问”、“空杯心态”。这些原则是我多年在工作与学习中,不断总结得来的经验。希望在大家面临选择时,这些原则能够起到一定的帮助和指导作用。

以原则为中心地工作与生活,让自己与团队变得更加强大。

作者介绍

云鹏,2014年加入美团,先后参与了美团酒店供应链体系、分布式调度系统的建设,现在负责美团旅行客户关系管理系统、基础信息服务的建设工作。

原文地址https://tech.meituan.com/10_principles_for_engineers.html

Docker之安全防护与配置

1. 风险的来源

docker的安全性,在很大程度上来自于Linux的本身。目前,我们考虑到的安全性时,主要考虑下面几个方面:

  • Linux内核的命名空间机制提供的容器隔离安全
  • Linux控制组机制对容器资源的控制能力安全
  • Linux内核的能力机制所带来的操作权限安全
  • docker程序(尤其是服务端)本身的抗攻击性
  • 其他安全增强机制(包括APPArmor、SELinux等)对容器安全的影响
  • 通过第三方工具(包括docker bench工具)对docker环境的安全性的评估

2. 风险的分析

2.1 命名空间隔离的安全

当我们在运行docker run的时候,docker会对针对容器创建一个隔离的命名空间,通过这个命名空间,将容器之间的进程和网络进行隔离,这就意味着容器不能独立的访问其他容器的接口或者套接字。

我们知道,所有容器的网络都是通过docker0进行桥接,当然,如果宿主机上面做一些特殊的配置,可以实现 container1->宿主机->container2 网络的交互方式。

那么,命名空间的架构设计本身,是否是足够安全呢?

其实,命名空间出现的历史很长了,从Linux内核2.6.15的版本(大概是2008年)就已经开始引用了命名空间,但是实际上,“命名空间”这个含义要更早,最开始是从2005年开始提出来的,所以设计和实现足够成熟。

当然,和虚拟机相比,命名空间并不是绝对。因为命名空间,实际上是“假隔离”,虚拟机是“真隔离”。运行在容器内的应用,会直接访问宿主机上面的内核和部分文件。所以,归根结底,我们应该保证的是镜像是足够安全的,只有镜像是安全的,才能保证我们能够在Linux运行安全可信的服务。

2.2 控制组资源控制的安全

CGroups有一个重要的作用就是资源审计和资源限制

当我们再运行docker run的时候,docker会通过Linux的相关调用,在后台创建一个控制组,用来控制容器对宿主机的资源消耗,比如控制容器使用内存、CPU等。

控制组有很多重要的作用。比如确保每个容器能够合理的使用共享资源,最重要的是可以通过控制组限制资源的使用,这一点在防止DDoS的时候尤其重要。

对于PaaS、容器云这样的容器服务平台,运行着成千上万个容器的实例,如果一旦某个容器被DDoS攻击,那么就会控制组的作用就显现出来,这样可以防止单个容器抢占过多资源,导致整个服务平台出现雪崩!

2.3 内核能力机制

传统的Unix系统对进程的权限其实只有root权限和非root权限两种粗粒度。

后来,随着Linux内核的升级,开始对权限的粒度越来越灵活,例如,可以给用户分配某个文件的修改权限、可以给某个用户操作某个进程的权限等等。

默认情况下,docker在运行容器的时候,只使用Linux内核的一部分能力,而且,容器的一些能力往往也是由宿主机上面的一些服务进行支持,比如网络的管理等。所以docker其实并不需要获取真正的“root权限”,此外,容器还能禁用一些不必要的权限,比如:

  • 禁止任何文件挂载操作(挂载实际上是宿主机,而不是容器本身);
  • 禁止访问宿主机上面的套接字;
  • 禁止访问一些文件系统的操作,比如创建新设备;
  • 禁止模板加载

所以,及时攻击者入侵到容器内部,在容器内部获取了root权限,也并不是真正的宿主机上面的“root权限”,能进行的破坏也是有限的。

2.4 Docker服务端的防护

使用docker最核心的就是docker服务器了。由于现在启动docker服务器需要使用root权限,所以服务端的权限显得尤其重要。

首先,我们应该确保运行docker的用户是可信的人。由于容器的内部一般都是root权限,如果某个恶意的用户,将宿主机上面的/目录映射到容器内部,那么容器理论上就会有修改根目录下面的权限。因此,在创建容器的时候,我们应该详细检查运行的参数。

尽量将容器映射到非root权限的用户目录下面,这样,可以有效减轻容器和宿主机上面因为权限而导致的安全隐患。

允许docker服务端在非root权限下运行,利用安全可靠的子进程限制特殊权限的操作。比如,这些子进程只能负责文件管理、配置等操作。

2.5 更多安全特性的使用

除了docker能力机制之外,我们可以使用一些安全软件增加docker的安全性。比如APParmor等。

docker默认只启用了能力机制。用户还可以启用更多的方案加强docker安全:

此外,将宿主机的文件挂载到容器内部的时候,可以通过设置一些只读(read-only)权限来避免容器对宿主机文件系统的破坏,特别是一些系统运行状态的目录,包括/proc/sys、/proc/irq、/proc/bus等等。

2.6 使用第三方检测工具

前面说了很多加强docker安全性的方式,但是注意去检查,会比较繁琐。幸亏现在有一些自动化的检测工具,比较出名的就是docker bench和Clair。

2.6.1 docker bench

docker bench其实是一个docker的镜像,仓库地址:https://hub.docker.com/r/docker/docker-bench-security/ 通过运行docker bench,可以对docker的一些配置做自动化安全检测。检测的标准是CIS Docker,检测项包括主机配置、Docker引擎、配置文件权限、镜像管理、容器运行时环境、安全项等6大方面。

docker run -it --net host --pid host --userns host --cap-add audit_control \
    -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
    -v /var/lib:/var/lib \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v /usr/lib/systemd:/usr/lib/systemd \
    -v /etc:/etc --label docker_bench_security \
    docker/docker-bench-security

在输出的结果中,会给出响应的提示信息,然后用户可以根据对应的提示,进行一些配置的更改等操作。一般是避免出现WARN或以上的问题。

3. 总结

docker其实自身携带的一些基本的抵御安全风险的机制,配合APParmor等安全机制,可以让docker容器更加安全。任何技术层面的实现,都需要合理的使用才能等到巩固,特别是生产环境,可能遭遇很多位置的安全风险,所以需要配合完善的监控系统来加强管理。

Docker使用的时候需要注意:

  1. 容器自身携带的隔离,并不是很完善,需要加强对容器的安全审查。
  2. 尽量使用官方的镜像,降低安全风险。
  3. 采用专门的服务器用来管理docker服务,加强对容器的监控机制。
  4. 随着容器的大规模使用,甚至构成容器集群的时候,需要考虑容器网络上必备的安全防护,比如DDoS攻击等。

 

docker之联合文件系统

1. 作用

联合文件系统(UnionFS) 是一种轻量级的高性能分层文件系统,它支持将文件系统中的修改信息当做一次提交,然后层层叠加(有点像git),同时可以将不同的目录挂载到同一个虚拟文件系统下,应用看到的是挂载的最终结果。

Debian/Ubuntu上成熟的AUFS(Another Union File System)就是一种联合文件系统的实现。AUFS支持为每个成员目录设定只读权限(readonly)、读写权限(readwrite)或(whiteout-able)权限,同时AUFS里有一个类似分层的概念,对只读权限的分支可以在逻辑上进行增量地修改(不影响其他只读部分)。

Docker镜像自身就是由多个文件层组成,每一层组成有唯一的编号(层ID)

2. docker存储

联合文件是docker镜像技术的基础。docker镜像就是根据分层技术来进行继承的。

举个例子,用户基于一些基础镜像,来制作另外的一个镜像。这些镜像共享同一个基础镜像层,提高的存储的效率和空间利用率。

假如,我们使用php7做基础镜像,来制作多个不同的镜像,那么这些镜像,就会公用一个基础镜像作为“底层”,这样做,提高了利用率,因为不用每个自定义镜像都要创建php7的“底层”。这也就是,为什么我们再build一个镜像的时候,会把基础镜像pull下来。当我们创建的自定义镜像还要有变动的时候,至于要创建一个新的层就好了。这样,也就不用我们从头开始构建镜像,节省了构建时间。

这也是docker十分轻量级和快速的重要原因!

docker安装自带了查看镜像层的命令:docker history

下面我们来看下基础镜像和自定义镜像层的比较:

localhost:~ feilong$ docker pull php:7.0
7.0: Pulling from library/php
7.0: Pulling from library/php
802b00ed6f79: Pull complete
59f5a5a895f8: Pull complete
6898b2dbcfeb: Pull complete
8e0903aaa47e: Pull complete
b627a118b728: Pull complete
e2e2cb10942b: Pull complete
e63e2fa0c7d4: Pull complete
57c09353077e: Pull complete
Digest: sha256:f0e774402dd485c11c60f52c05989da088c5debb44d1126cc089970e1bfca002
Status: Downloaded newer image for php:7.0
localhost:~ feilong$
localhost:~ feilong$
localhost:~ feilong$
localhost:~ feilong$
localhost:~ feilong$
localhost:~ feilong$ docker history php:7.0
IMAGE CREATED CREATED BY SIZE COMMENT
a6c560acbfc5 9 hours ago /bin/sh -c #(nop) CMD ["php" "-a"] 0B
<missing> 9 hours ago /bin/sh -c #(nop) ENTRYPOINT ["docker-php-e… 0B
<missing> 9 hours ago /bin/sh -c #(nop) COPY multi:2cdcedabcf5a3b9… 6.42kB
<missing> 9 hours ago /bin/sh -c set -eux; savedAptMark="$(apt-m… 79.4MB
<missing> 9 hours ago /bin/sh -c #(nop) COPY file:207c686e3fed4f71… 587B
<missing> 9 hours ago /bin/sh -c set -xe; fetchDeps=' wget ';… 13.3MB
<missing> 9 hours ago /bin/sh -c #(nop) ENV PHP_SHA256=ff6f62afeb… 0B
<missing> 9 hours ago /bin/sh -c #(nop) ENV PHP_URL=https://secur… 0B
<missing> 9 hours ago /bin/sh -c #(nop) ENV PHP_VERSION=7.0.32 0B
<missing> 10 days ago /bin/sh -c #(nop) ENV GPG_KEYS=1A4E8B7277C4… 0B
<missing> 10 days ago /bin/sh -c #(nop) ENV PHP_LDFLAGS=-Wl,-O1 -… 0B
<missing> 10 days ago /bin/sh -c #(nop) ENV PHP_CPPFLAGS=-fstack-… 0B
<missing> 10 days ago /bin/sh -c #(nop) ENV PHP_CFLAGS=-fstack-pr… 0B
<missing> 10 days ago /bin/sh -c mkdir -p $PHP_INI_DIR/conf.d 0B
<missing> 10 days ago /bin/sh -c #(nop) ENV PHP_INI_DIR=/usr/loca… 0B
<missing> 10 days ago /bin/sh -c apt-get update && apt-get install… 209MB
<missing> 10 days ago /bin/sh -c #(nop) ENV PHPIZE_DEPS=autoconf … 0B
<missing> 10 days ago /bin/sh -c set -eux; { echo 'Package: php… 46B
<missing> 10 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 10 days ago /bin/sh -c #(nop) ADD file:e6ca98733431f75e9… 55.3MB

我pull了一个php:7.0的镜像,可以看到,整个过程分为20层,每个层级都会执行对应的命令,然后我们基于php7在做一些自定义的操作:安装mysqli和redis的扩展,构建一个新的镜像:

#Dockerfile
FROM php:7.0
RUN apt-get update \
    && docker-php-ext-install mysqli \
    && curl -L -o ./redis-4.1.0.tgz http://pecl.php.net/get/redis-4.1.0.tgz \
    && tar zxvf redis-4.1.0.tgz \
    && cd redis-4.1.0 \
    && phpize \
    && ./configure \
    && make && make install \
    && echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini \
    && cd .. \
    && rm -rf redis-4.1.0.tgz redis-4.1.0
localhost:feilong_test feilong$ docker build -t feilongtest .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM php:7.0
 ---> a6c560acbfc5
Step 2/2 : RUN apt-get update     && docker-php-ext-install mysqli     && curl -L -o ./redis-4.1.0.tgz http://pecl.php.net/get/redis-4.1.0.tgz     && tar zxvf redis-4.1.0.tgz     && cd redis-4.1.0     && phpize     && ./configure     && make && make install     && echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini     && cd ..     && rm -rf redis-4.1.0.tgz redis-4.1.0
 ---> Running in bd2e3fbbde25
Get:3 http://security.debian.org/debian-security stretch/updates InRelease [94.3 kB]
Get:4 http://security.debian.org/debian-security stretch/updates/main amd64 Packages [414 kB]
Ign:1 http://cdn-fastly.deb.debian.org/debian stretch InRelease
Get:2 http://cdn-fastly.deb.debian.org/debian stretch-updates InRelease [91.0 kB]
Get:5 http://cdn-fastly.deb.debian.org/debian stretch-updates/main amd64 Packages [5148 B]
Get:6 http://cdn-fastly.deb.debian.org/debian stretch Release [118 kB]
Get:7 http://cdn-fastly.deb.debian.org/debian stretch Release.gpg [2434 B]
Get:8 http://cdn-fastly.deb.debian.org/debian stretch/main amd64 Packages [7099 kB]
省略
Installing shared extensions: /usr/local/lib/php/extensions/no-debug-non-zts-20151012/
Removing intermediate container bd2e3fbbde25
 ---> 41b978fc1549
Successfully built 41b978fc1549
Successfully tagged feilongtest:latest

然后我们看下自己构建的镜像层

localhost:feilong_test feilong$ docker history feilongtest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
41b978fc1549        48 seconds ago      /bin/sh -c apt-get update     && docker-php-…   18.3MB
a6c560acbfc5        10 hours ago        /bin/sh -c #(nop)  CMD ["php" "-a"]             0B
<missing>           10 hours ago        /bin/sh -c #(nop)  ENTRYPOINT ["docker-php-e…   0B
<missing>           10 hours ago        /bin/sh -c #(nop) COPY multi:2cdcedabcf5a3b9…   6.42kB
<missing>           10 hours ago        /bin/sh -c set -eux;   savedAptMark="$(apt-m…   79.4MB
<missing>           10 hours ago        /bin/sh -c #(nop) COPY file:207c686e3fed4f71…   587B
<missing>           10 hours ago        /bin/sh -c set -xe;   fetchDeps='   wget  ';…   13.3MB
<missing>           10 hours ago        /bin/sh -c #(nop)  ENV PHP_SHA256=ff6f62afeb…   0B
<missing>           10 hours ago        /bin/sh -c #(nop)  ENV PHP_URL=https://secur…   0B
<missing>           10 hours ago        /bin/sh -c #(nop)  ENV PHP_VERSION=7.0.32       0B
<missing>           10 days ago         /bin/sh -c #(nop)  ENV GPG_KEYS=1A4E8B7277C4…   0B
<missing>           10 days ago         /bin/sh -c #(nop)  ENV PHP_LDFLAGS=-Wl,-O1 -…   0B
<missing>           10 days ago         /bin/sh -c #(nop)  ENV PHP_CPPFLAGS=-fstack-…   0B
<missing>           10 days ago         /bin/sh -c #(nop)  ENV PHP_CFLAGS=-fstack-pr…   0B
<missing>           10 days ago         /bin/sh -c mkdir -p $PHP_INI_DIR/conf.d         0B
<missing>           10 days ago         /bin/sh -c #(nop)  ENV PHP_INI_DIR=/usr/loca…   0B
<missing>           10 days ago         /bin/sh -c apt-get update && apt-get install…   209MB
<missing>           10 days ago         /bin/sh -c #(nop)  ENV PHPIZE_DEPS=autoconf …   0B
<missing>           10 days ago         /bin/sh -c set -eux;  {   echo 'Package: php…   46B
<missing>           10 days ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>           10 days ago         /bin/sh -c #(nop) ADD file:e6ca98733431f75e9…   55.3MB

可以看出,在dockerfile里面,增加了1步操作,分别是按照mysqli和redis扩展,然后在build镜像的时候,在原有的20层的基础上,继续添加了1层。

基础镜像层的层内容都是不可用自改的、只读的。当docker利用镜像创建容器的时候,会在最顶端创建一个可以读写的层给容器。容器内的数据,都会写到这个读写层里面。当所操作的对象位于比较深的层时,需要先复制到最上层的可读写层。当数据对象较大的时候,往往意味着IO性能比较差。因此,一般推荐奖容器修改数据通过volume方式挂载,而不是直接修改镜像内的数据。

Docker的所有存储,都是在docker文件夹下面,以Centos或者Ubuntu为例,默认的路径一般是/var/lib/docker。(我仅仅以Centos为例)

[root@izj6c9b96ia369l2i47yq3z docker]# ll
total 52
drwx------ 2 root root 4096 Sep  6 20:09 builder
drwx------ 4 root root 4096 Sep  6 20:09 buildkit
drwx------ 3 root root 4096 Sep  6 20:09 containerd
drwx------ 2 root root 4096 Sep 17 00:07 containers
drwx------ 3 root root 4096 Sep  6 20:09 image
drwxr-x--- 3 root root 4096 Sep  6 20:09 network
drwx------ 4 root root 4096 Sep 17 00:07 overlay2
drwx------ 4 root root 4096 Sep  6 20:09 plugins
drwx------ 2 root root 4096 Sep  6 20:09 runtimes
drwx------ 2 root root 4096 Sep  6 20:09 swarm
drwx------ 2 root root 4096 Sep 16 23:56 tmp
drwx------ 2 root root 4096 Sep  6 20:09 trust
drwx------ 2 root root 4096 Sep  6 20:55 volumes

docker的镜像层基本上都是在overlay2里面

[root@izj6c9b96ia369l2i47yq3z overlay2]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox latest e1ddd7948a1c 6 weeks ago 1.16MB
[root@izj6c9b96ia369l2i47yq3z docker]# cd overlay2/
[root@izj6c9b96ia369l2i47yq3z overlay2]# ll
total 8
drwx------ 3 root root 4096 Sep 16 23:56 4c819c3673c3416b65c2cf6394818d270363cfd53a0389a5f6c237e1c8ad3ef4
drwxr-xr-x 2 root root 4096 Sep 17 00:07 l

现在,我们只有一个busybox的镜像,该目录下面包括diff文件夹,diff文件夹就是我们创建容器之后,初始化的文件夹

[root@izj6c9b96ia369l2i47yq3z diff]# ll
total 40
drwxr-xr-x 2 root      root      12288 Aug  1 04:20 bin
drwxr-xr-x 2 root      root       4096 Aug  1 04:20 dev
drwxr-xr-x 3 root      root       4096 Aug  1 04:20 etc
drwxr-xr-x 2 nfsnobody nfsnobody  4096 Aug  1 04:20 home
drwx------ 2 root      root       4096 Aug  1 04:20 root
drwxrwxrwt 2 root      root       4096 Sep 17 00:06 tmp
drwxr-xr-x 3 root      root       4096 Aug  1 04:20 usr
drwxr-xr-x 4 root      root       4096 Aug  1 04:20 var
[root@izj6c9b96ia369l2i47yq3z diff]#

为了验证我们说的是否是正确的,我们在tmp的文件夹里面创建一个测试的文件a.txt,然后写入Hello world

[root@izj6c9b96ia369l2i47yq3z diff]# touch  tmp/a.txt
[root@izj6c9b96ia369l2i47yq3z diff]# echo 'Hello world' > tmp/a.txt
[root@izj6c9b96ia369l2i47yq3z diff]# cat tmp/a.txt
Hello world
[root@izj6c9b96ia369l2i47yq3z diff]#

如果分析是正确的,那么创建的容器中,也会存在这个文件

[root@izj6c9b96ia369l2i47yq3z diff]# docker run -it --rm --name test busybox
/ # ll
sh: ll: not found
/ # ls -l
total 36
drwxr-xr-x    2 root     root         12288 Jul 31 20:20 bin
drwxr-xr-x    5 root     root           360 Sep 16 16:19 dev
drwxr-xr-x    1 root     root          4096 Sep 16 16:19 etc
drwxr-xr-x    2 nobody   nogroup       4096 Jul 31 20:20 home
dr-xr-xr-x  131 root     root             0 Sep 16 16:19 proc
drwx------    1 root     root          4096 Sep 16 16:19 root
dr-xr-xr-x   13 root     root             0 Sep 16 16:19 sys
drwxrwxrwt    2 root     root          4096 Sep 16 16:17 tmp
drwxr-xr-x    3 root     root          4096 Jul 31 20:20 usr
drwxr-xr-x    4 root     root          4096 Jul 31 20:20 var
/ # ls -l tmp
total 4
-rw-r--r--    1 root     root            12 Sep 16 16:17 a.txt
/ # cat tmp/a.txt
Hello world
/ #

在创建的容器中,我们果然看到了内容为Hello world的tmp/a.txt文件

在创建容器之后,我们会发现多了两个文件夹

这两个文件夹,是用来存储一些容器的数据,如果容器一旦删除,那么这些数据也会随着一块被清理掉,这就是为什么建议我们把一些重要的数据,挂载到外部的原因!

3. 多种文件系统比较

Docker目前支持多种联合文件系统:AUFS、OverlayFS、btrfs、vfs、zfs和Device Mapper等。

AUFS:最早支持的文件系统,对Debian/Ubuntu支持比较好,虽然没有合并到Linux内核,但是成熟度很高

OverlayFS:类似AUFS,性能更好,上面的例子明显就是OverlayFS,已经合并到内核,将来会取代AUFS

Device Mapper:Redhat和Docker团队一起开发并用于支持RHEL的文件系统,内核支持,性能略慢,成熟度高

4. 总结

docker的镜像层级设计,让docker的性能更高,更加符合软件设计,具有很高的复用性,这个也是docker镜像编译迅速的重要原因。

此外,docker容器默认将数据存储到docker文件夹下,如果容器被删除,那么容器数据也将被删除掉,所以,对于容器的重要数据,我们应该映射到宿主机上面,避免由于容器删除,而导致的数据丢失。

 

另:关于镜像层ID为missing,请参阅论坛:Layer IDs shown as <missing> in history

 

docker之控制组

1. 作用

控制度(CGroups) 其实是Linux内核的一个特性,主要是用来控制共享资源,比如限制内存、CPU的的一些使用等。容器使用的CPU、内存等硬件信息,其实就是使用的宿主机上面的硬件设备,所以合理的分配资源,也是为了避免不同容器之间、容器和宿主机进程之间,产生资源的抢占。

2. 容器控制组

2.1 资源限制

比如我们要限制容器的使用内存,可以在run的时候加上–memory的参数

feilongdeMBP:~ feilong$ docker run -it --rm --name test --memory 10m busybox

然后新打开一个窗口,可以实时查看下容器的内存使用情况

TIP:

使用docker-compose的时候需要注意一下,设置内存限制的参数是mem_limit,但是在docker-compose的3.x版本之后,不支持这个参数,所以在写docker-compose.yaml 的时候,会出现 Unsupported config option for xxxx: ‘mem_limit’ 的错误信息,所以需要指定 version: ‘2’

2.2 优先级

docker run的时候支持使用-c 或者 –cpu-shares 用来指定容器使用CPU的加权值。如果不指定,那么就是使用的是默认值,一般是1024。

-c 或者 –cpu-shares并不能指定容器能够使用多少CPU或者多少GHz,而是一个加权值。有点类似nginx的负载均衡配置。

这个配置在少量容器的时候,并没有太大的实际意义。只有CPU资源比较紧缺的时候,这个配置参数才会展现出来。

比如,一个容器的加权值是100,另一个加权值是50,那么加权值为100的容器,获取CPU时间片的概率就是另一个的2倍

如果只有一个容器,那么CPU时间片肯定都会给这个容器使用。

创建一个容器,安装stress软件,然后开启10个进程,看下CPU占用情况

localhost:marvin feilong$ docker run -itd --name cpu512  --cpu-shares 512 ubuntu
localhost:marvin feilong$ docker exec -it cpu512
# apt update
# apt install stress
# stress -c 10
stress: info: [250] dispatching hogs: 10 cpu, 0 io, 0 vm, 0 hdd

打开一个新窗口,然后登录到这个容器,使用top查看下CPU占用情况

localhost:~ feilong$ docker exec -it cpu512 sh

可以从截图看到cpu大概占用了3.3%左右

新打开另一个窗口,创建新容器,一样的操作,安装stress,然后开10个进程,查看下CPU占用情况

localhost:~ feilong$ docker run -itd --name cpu1024  --cpu-shares 1024 ubuntu
# apt update
# apt install stress
# stress -c 10
stress: info: [241] dispatching hogs: 10 cpu, 0 io, 0 vm, 0 hdd

新开窗口,进入cpu1024容器,然后使用top查看CPU占用情况

可以看出CPU占用情况大概是6.6%左右,基本上是cpu是cpu512的两倍。

2.3 资源审计

资源审计主要是做一些审计操作,用来统计系统实际上把多少资源用到适合的目的上,可以使用cpuacct子系统记录某个进程组使用的CPU时间

2.4 隔离

为组隔离命名空间,这样一个组不会看到另一个组的进程、网络连接和文件系统

2.5 控制

挂起、恢复和重启动等操作

 

docker之命名空间

1. 基本架构

docker目前采用了标准的C/S架构。客户端和服务端既可以运行在一个机器上,又可以通过socket或者restful API来进行通信。

1.1 服务端

docker服务端一般都是在宿主机上,来接受客户端的命令。docker默认使用套接字的方式,但是也是允许使用tcp进行端口的监听,可以使用docker daemon -H IP:PORT的方式进行监听。

1.2 客户端

docker的客户端主要作用是向服务端发送操作的指令。客户端默认也是采用套接字的方式,向本地的docker服务端发送命令。当然,客户端也是可以使用tcp的方式进行发送指令,使用docker -H tcp://IP:PORT,用来指定接收命令的docker服务端。

2. 命名空间

大家在平时使用Linux或者macos的时候,我们并没有拆分多个环境的需求。但是在服务器上面,加入一台服务器运行多个进程,进程之间是相互影响的,比如共享内存,操作相同的文件。我们其实更希望能够将这些进程分离开,这样情况下,如果服务受到攻击,不会影响其他的服务。

docker目前主要有6命名空间的隔离方式

2.1 进程空间隔离

进程在操作系统中是一个很重要的概念,也就是大家认为的正在运行中的程序。

feilongdeMBP:~ feilong$ ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 9:31下午 ?? 0:10.07 /sbin/launchd
0 44 1 0 9:31下午 ?? 0:00.65 /usr/sbin/syslogd
0 45 1 0 9:31下午 ?? 0:01.37 /usr/libexec/UserEventAgent (System)
0 48 1 0 9:31下午 ?? 0:00.25 /System/Library/PrivateFrameworks/Uninstall.framework/Resources/uninstalld
0 49 1 0 9:31下午 ?? 0:02.57 /usr/libexec/kextd
0 50 1 0 9:31下午 ?? 0:02.40 /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/FSEvents.framework/Versions/A/Support/fseventsd
0 52 1 0 9:31下午 ?? 0:00.16 /System/Library/PrivateFrameworks/MediaRemote.framework/Support/mediaremoted
55 55 1 0 9:31下午 ?? 0:00.38 /System/Library/CoreServices/appleeventsd --server
0 56 1 0 9:31下午 ?? 0:00.75 /usr/sbin/systemstats --daemon

可见当前系统运行了很多“程序”。

我们现在新建一个容器,然后进入容器看下,docker容器里面的进程列表

feilongdeMBP:~ feilong$ docker run -it --rm --name test busybox
/ # ps -ef
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    6 root      0:00 ps -ef

对比很明显,容器内部只有很少的几个正在运行的进程。

我们新建一个窗口,然后看下宿主机上面和docker相关的进程

localhost:~ feilong$ ps -ef | grep docker
    0    82     1   0  9:31下午 ??         0:00.02 /Library/PrivilegedHelperTools/com.docker.vmnetd
  501   918   879   0 10:26下午 ??         0:00.14 /Applications/Docker.app/Contents/MacOS/com.docker.supervisor -watchdog fd:0
  501   920   918   0 10:26下午 ??         0:03.32 com.docker.osxfs serve --address fd:3 --connect vms/0/connect --control fd:4 --log-destination asl
  501   921   918   0 10:26下午 ??         0:00.73 com.docker.vpnkit --ethernet fd:3 --port fd:4 --diagnostics fd:5 --pcap fd:6 --vsock-path vms/0/connect --host-names host.docker.internal,docker.for.mac.host.internal,docker.for.mac.localhost --gateway-names gateway.docker.internal,docker.for.mac.gateway.internal,docker.for.mac.http.internal --vm-names docker-for-desktop --listen-backlog 32 --mtu 1500 --allowed-bind-addresses 0.0.0.0 --http /Users/feilong/Library/Group Containers/group.com.docker/http_proxy.json --dhcp /Users/feilong/Library/Group Containers/group.com.docker/dhcp.json --port-max-idle-time 300 --max-connections 2000 --gateway-ip 192.168.65.1 --host-ip 192.168.65.2 --lowest-ip 192.168.65.3 --highest-ip 192.168.65.254 --log-destination asl --udpv4-forwards 123:127.0.0.1:59434 --gc-compact-interval 1800
  501   922   918   0 10:26下午 ??         0:01.17 com.docker.driver.amd64-linux -addr fd:3 -debug
  501   928   922   0 10:26下午 ??         2:40.08 com.docker.hyperkit -A -u -F vms/0/hyperkit.pid -c 2 -m 2048M -s 0:0,hostbridge -s 31,lpc -s 1:0,virtio-vpnkit,path=vpnkit.eth.sock,uuid=246fb3f9-3ad5-4683-837a-33ac39f57f25 -U 5a3669ae-b209-443a-a074-312cd32a258a -s 2:0,ahci-hd,/Users/feilong/Library/Containers/com.docker.docker/Data/vms/0/Docker.raw -s 3,virtio-sock,guest_cid=3,path=vms/0,guest_forwards=2376;1525 -s 4,ahci-cd,/Applications/Docker.app/Contents/Resources/linuxkit/docker-for-mac.iso -s 5,ahci-cd,vms/0/config.iso -s 6,virtio-rnd -s 7,virtio-9p,path=vpnkit.port.sock,tag=port -l com1,autopty=vms/0/tty,asl -f bootrom,/Applications/Docker.app/Contents/Resources/uefi/UEFI.fd,,
  501  2074  1102   0 11:21下午 ??         0:00.50 /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper /Users/feilong/.vscode/extensions/peterjausovec.vscode-docker-0.1.0/node_modules/vscode-languageclient/lib/utils/electronForkStart /Users/feilong/.vscode/extensions/peterjausovec.vscode-docker-0.1.0/node_modules/dockerfile-language-server-nodejs/lib/server.js --node-ipc --node-ipc --clientProcessId=1102
  501  2100  1065   0 11:24下午 ttys001    0:00.12 docker run -it --rm --name test busybox
  501  2086  2083   0 11:21下午 ttys002    0:00.19 docker exec -it 910aa64a312b3a884f4efb059e47ee601bbd3ba3d62f4c92abd4120cff770828 /bin/sh
  501  2090  2087   0 11:21下午 ttys003    0:00.12 docker exec -it 73f8fbcc50651fd4fea9fe0be7fe4066ea78efd7e9b2438fe657a3e7725e7903 /bin/sh
  501  2115  2111   0 11:27下午 ttys004    0:00.00 grep docker

在进程列表中,我们没有看到容器内部运行的进程,说明相对于容器的“外部”,容器“内部”的进程是隔离的。但是我们也可以发现,刚刚创建的名字为test的容器,实质上就是宿主机上面的一个PID为2090的进程。

所以,我们可以理解docker的进程树是这个状态:

2.2 网络空间隔离

容器其实不能完全和宿主机器隔离网络,要不然的话容器就没办法通过外部进行访问,那么也就没有实际的意义。但是容器之间是网络隔离的,这种隔离的方式,就是通过网络命名空间实现的。

docker有四种不同的网络模式:Host、Container、None和bridge

docker默认的是桥接模式。

docker在创建容器的时候, 不仅会给容器创建IP地址,还会在宿主机上面创建一个虚拟网桥docker0,在运行的时候,将容器和该网桥进行相连。

在默认的情况下,创建容器的时候,都会创建一对虚拟网卡,两个虚拟网卡组成数据通道,一个在容器内部,另外一个加入到docker0的网桥中。

打开两个窗口,分别创建redis和redis2容器

[root@izj6c9b96ia369l2i47yq3z feilong]# docker run -it --rm --name redis  -p 6379:6379 redis:latest /bin/bash
root@d89535b59b0b:/data#
[root@izj6c9b96ia369l2i47yq3z feilong]# docker run -it --rm --name redis2 -p 6378:6379 redis:latest /bin/bash
root@7736850135af:/data#

打开第三个窗口,查看网桥的状态

[feilong@izj6c9b96ia369l2i47yq3z ~]$ brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.024219a15f9d       no              veth8331b03
                                                        vethc5f3cb9

docker0 会为每一个容器分配一个新的 IP 地址并将 docker0 的 IP 地址设置为默认的网关。网桥 docker0 通过 iptables 中的配置与宿主机器上的网卡相连,所有符合条件的请求都会通过 iptables 转发到 docker0 并由网桥分发给对应的机器。同时也会在防火墙加上一条新的规则。

[root@izj6c9b96ia369l2i47yq3z feilong]# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination
DOCKER-USER  all  --  anywhere             anywhere
DOCKER-ISOLATION-STAGE-1  all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
DOCKER     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere
ACCEPT     all  --  anywhere             anywhere

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination

Chain DOCKER (1 references)
target     prot opt source               destination
ACCEPT     tcp  --  anywhere             172.17.0.2           tcp dpt:6379
ACCEPT     tcp  --  anywhere             172.17.0.3           tcp dpt:6379
ACCEPT     tcp  --  anywhere             172.17.0.4           tcp dpt:http
......

2.3 挂载点命名空间

docker已经可以通过命名空间将网络和进程进行隔离。挂载命名空间,允许不同的容器,查看到不同的文件结构,这样,每个命名空间的进程所看到的文件目录彼此被隔离。每个容器内的进程只会更改容器内部的文件目录。

2.4 IPC命名空间

容器中的进程交互采用的是Linux中常见的进程间交互方式(Interprocess Communication, IPC),包括信号量、消息队列和内存共享等。IPC命名空间和PID命名空间可以组合使用,同一个IPC命名空间的进程可以彼此可见,允许进行交互,不同空间的进程无法交互。

2.5 UTS 命名空间

UTS(Unix time-sharing system)命名空间允许每个容器拥有一个独立的主机名和域名,从而可以虚拟出一个独立的主机名和网络空间的环境,就可以跟网络上的一台独立主机一样。

默认情况下,docker的主机名是容器的id

2.6 用户命名空间

每个容器内部都有不同的用户组和组id,也就是说可以在容器内部使用特定的内部用户执行程序,而不是宿主机上的用户。每个容器都有root账号,但是和宿主机都不在一个命名空间。通过使用命名空间隔离,来保证容器内部用户无法操作容器外部的操作权限。

3. 总结

6种命名空间让容器之间松耦合,也让容器与宿主机松偶尔。同时,也保证了安全性。容器内部不能操作其他容器内部的东西,docker的这种命名空间隔离的方式,也比较符合Linux的系统设计。

docker之运行golang

众所周知,docker解决了编程的痛点问题——运行环境,所以我先走基本上尽量都使用docker运行。这样做,首先就是让我不必关心配置复杂的运行环境,另外也可以让我更加熟练的使用docker。

示例程序

//go-sample.go
package main
import "fmt"
func main() {
	fmt.Println("hello world");
}

Golang:onbuild

现在关于go的docker镜像也发布了很多个版本,我们首先介绍一下golang:onbuild以及如何使用。
golang:onbuild是go语言官方发布的一款很小的镜像(只有几KB大小),目的是为了让我们可以编译go文件,并且运行。使用的方式很简单,只需要创建一个Dockerfile,然后在首行加上FROM golang:onbuild

-rw-r--r--@ 1 feilong wheel 20 9 2 00:23 Dockerfile
-rw-r--r-- 1 feilong wheel 72 9 2 00:03 go-sample.go
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ cat Dockerfile
FROM golang:onbuild
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ docker build -t golang_onbuild .
Sending build context to Docker daemon  3.072kB
Step 1/1 : FROM golang:onbuild
onbuild: Pulling from library/golang
ad74af05f5a2: Pull complete
2b032b8bbe8b: Pull complete
a9a5b35f6ead: Pull complete
25d9840c55bc: Pull complete
d792ec7d64a3: Pull complete
be556a93c22e: Pull complete
3a5fce283a1e: Pull complete
0621865a0c2e: Pull complete
Digest: sha256:c0ec19d49014d604e4f62266afd490016b11ceec103f0b7ef44875801ef93f36
Status: Downloaded newer image for golang:onbuild
# Executing 3 build triggers
 ---> Running in 109c7a7ebeb5
+ exec go get -v -d
Removing intermediate container 109c7a7ebeb5
 ---> Running in c0dfd28de95e
+ exec go install -v
app
Removing intermediate container c0dfd28de95e
 ---> 820e315d7160
Successfully built 820e315d7160
Successfully tagged golang_onbuild:latest
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ docker run -it --rm --name go_onbuild golang_onbuild
+ exec app
hello world

我们根据docker:onbuild的Dockerfile文件具体分析一个整个编译的过程(以1.3.1版本为例)

FROM golang:1.3.1

RUN mkdir -p /go/src/app
WORKDIR /go/src/app

# this will ideally be built by the ONBUILD below 😉
CMD ["go-wrapper", "run"]

ONBUILD COPY . /go/src/app
ONBUILD RUN go-wrapper download
ONBUILD RUN go-wrapper install

从Dockerfile和build过程可以看出,在进行build的时候,经历了三次触发器:

  1. 首先,将当前目录拷贝到. /go/src/app
  2. 下载对应的依赖包
  3. 编译安装

编译之后,golang:onbuild镜像默认包含了一个CMD [“app”] 命令,用来执行编译后的go文件。

我们通过实际run一个容器验证一下:

feilongdeMBP:go feilong$ docker run -it --rm --name golang_onbuild golang_onbuild
+ exec app
hello world

 Golang:latest

相比较golang:onbuild的便利性,golang:latest就变得很灵活了,需要我们手动编译go文件,然后手动执行编译后的文件。因为毕竟电脑并不知道你具体想要编译的顺序,以及你要想要执行的编译文件。运行过程如下:

feilongdeMBP:go feilong$ ll
total 16
-rw-r--r--@ 1 feilong  wheel  133  9  2 00:04 Dockerfile
-rw-r--r--  1 feilong  wheel   72  9  2 00:03 go-sample.go
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ cat Dockerfile
FROM golang:latest

RUN mkdir -p /go/src/app
WORKDIR /go/src/app

COPY . /go/src/app
RUN go build -o app .
CMD [ "/go/src/app/app" ]
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$
feilongdeMBP:go feilong$ docker build -t go_go .
Sending build context to Docker daemon 3.072kB
Step 1/6 : FROM golang:latest
 ---> 7e9ac7032e33
Step 2/6 : RUN mkdir -p /go/src/app
 ---> Running in b5d3f63578ed
Removing intermediate container b5d3f63578ed
 ---> 95c2beb49121
Step 3/6 : WORKDIR /go/src/app
 ---> Running in 3011d74944c9
Removing intermediate container 3011d74944c9
 ---> 82d6a45aa3e3
Step 4/6 : COPY . /go/src/app
 ---> 475b2bdd5769
Step 5/6 : RUN go build -o app .
 ---> Running in 5802ac0c98b4
Removing intermediate container 5802ac0c98b4
 ---> 7a019370f09d
Step 6/6 : CMD [ "/go/src/app/app" ]
 ---> Running in a3f6ad19d2ef
Removing intermediate container a3f6ad19d2ef
 ---> 635417bdcda8
Successfully built 635417bdcda8
Successfully tagged go_go:latest

run一个容器,查看运行效果


feilongdeMBP:go feilong$ docker run -it --rm --name go go_go
hello world

总结

golang:onbuild和golang:lastest各有利弊,前者更加简单,能够更加简明扼要的告诉我们运行过程,而后者更加灵活,将更多的操作命令交给了开发人员。

参考文献

  • https://time-track.cn/build-minimal-go-image.html