阿里微服务架构师随手笔记:教你如何手写Docker

模拟Docker实现一个简单的容器,不到 200行代码(包括空行、注释、异常处理),这并非吹牛B。容器技术几乎是Linux kernel内置的模块,咱们简单调用一下API就能搞定不少事情。固然你要考虑各类商业因素、政治因素那就会成长为Docker这种量级的代码量了。git

盗用一下朋友圈里的段子:小公司与大公司的区别就是,以杀猪为例,小公司是找到猪直接乱刀砍死。大公司要先作一套笼具抓猪,再作一套流程磨刀,再发明一套刀法(工程师一般会就刀法争论好久)杀猪。抓猪的笼具除了能抓猪还能抓跳骚,磨刀的工具除了能磨柴刀,还能磨指甲刀。杀猪的流程除了能杀猪,也能杀鸡。作完了以后你只敲一个杀猪的命令就行。你不知道猪在哪里,由于这是另外一我的负责的,代码放在你不知道的某个目录下;你也不知道刀在哪里,由于目录不可见,格式不可读。刀法是啥你也不知道。这套系统理论上威力无比,一群人费了老大劲作出来,除了用柴刀杀猪没干过别的,杀鸡历来没测试过,杀跳骚代码都不完整。可是公司里的全部人都以为,杀猪就应该这样。因此你们天天忙忙碌碌,猪快活的过了一年又一年。github

因此这系列文章我主要介绍如何找到猪、怎么持刀不伤到本身,如何发力可以更凶狠;而后现场表演一下把一头活蹦乱跳的猪捅死。docker

涉及到的技术

写一个容器只须要两个技术——Namespace和CGroup,而这两个东西都是Linux kernel提供的,咱们要作的就是——调用一下。无耻的盗用一下Brendan Gregg大神的图。shell

这张图中蕴含了一个常常被忽视的细节——容器是共享内核的,它们属于多个进程同时运行在一个内核上,只不过是利用Namespace把它们隔离开,用CGroup限制可用资源。而虚拟机是共享“硬件”的,每一个虚拟机都有本身独立的操做系统。因此,虚拟机是可引导的、绝对安全的隔离技术;而容器是很是脆弱的,不安全的隔离技术。安全

Namespace是Linux内核提供的一种隔离技术,它提供了六种隔离空间:性能优化

看的一脸懵逼对不对?不要紧,简单的解释一下。bash

学过操做系统原理的同窗都知道(没学过?你还敢在这个行业混?),在一个内核全部进程都共享操做系统定义的资源——主机名、域名、ARP表、路由表、NAT表;文件系统、用户和组、进程编号。以主机名为例,它是由操做系统定义在一块内存空间中的,因此进程A能看到,进程B也能看到(若是有权限甚至能够修改)。Namespace提供了一种隔离技术,可让每一个进程都定义“本身的主机名”。你能够理解为内核为每一个进程都提供了一份当前主机名的备份,进程固然能够修改这份数据,可是这个修改只能做用于本身,其余进程感知不到——由于它再也不是“全局”的。网络

常常有人问是否是全部应用均可以作容器化?理解Namespace就很容易回答这个问题。容器技术本质上仍是共享内核,因此任何须要修改内核的应用都不能够被容器化。好比LVS、OpenvSwtich这些须要加载内核模块的应用都没有办法作成容器。架构

推荐一个交流学习群:478030634 里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
并发

Hello world

调用Namespace很是简单,只须要一个API(没错,一个,只要一个)——clone。

它会建立一个新的线程(内核不会太区分线程和进程),第一个参数指定了线程的代码入口,第二个参数是线程栈,第三个参数是标志位,第四个参数是代码入口的参数指针。

咱们上面所罗列的Namespace参数就是经过第三个参数——标志位传递的。

咱们先测试一下UTS(主机名)是否能正常工做,由于子进程不涉及到递归调用因此定义1024字节的stack大小应该足够了。main方法里的os.waitpid(pid, 0)是必须的,不然子进程会由于父进程终止而提早退出。

child_func是子进程的入口,这段代码里咱们调用sethostname修改主机而后再执行hostname验证修改是否生效了。

libc是我封装好的系统调用,很是简单。

小试牛刀一下:

首先在父进程中输出本身的进程编号和子进程的编号,而后在子进程中输出本身的进程编号和父进程的编号。在子进程中咱们调用sethostname修改了主机名而且经过hostname验证了调用结果。可是这个修改并无波及到内核,最后咱们在shell中调用hostname验证了这一结果。

要有Shell

上面只是执行一次修改hostname的动做,动做有点小,不够过瘾。咱们但愿可以在独立的Namespace中拿到一个shell。

只须要更改两行代码。父进程里面增长NEW_PID、NEW_IPC的标志位,子进程里调用execle执行bash,经过最后一个参数指定了环境变量PS1,这个表示提示符。

再次执行,咱们发现shell已经变化了。经过hostname验证咱们已经“在容器里面”了。键入exit,退出容器。

是否是已经没法掩盖本身心里的兴奋了。别急,还有更兴奋的,咱们进行第三步——分离文件系统。

推荐一个交流学习群:478030634  里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多:

完全分离

若是你在上一部的shell中输入一些top、ps、ls命令会发现几乎和“Host”环境中一摸同样。这是由于咱们尚未作最重要的一部——分离文件系统。

Docker提供的有Ubuntu、CentOS的镜像,其实这些并非严格意义上的镜像,它们准确的叫法应该是——根文件系统(root filesystem)。

容器是共享内核的,因此不管是Ubuntu、CentOS它们里面都使用Host的内核,若是你在Docker中经过uname查看会发现不管什么镜像它们的内核版本都和Host一摸同样。因此,不一样“操做系统”Docker镜像其实就是不一样的根文件系统。

不少人用BusyBox的rootfs作演示,做为一个风骚的男人怎么怎么可能如此俗套。因此我用CentOS 7做为演示。

真正的缘由切换容器中的根目录,后续的代码执行会使用新的根文件系统,然后续的代码是依赖Python运行环境的。因此咱们须要一个带Python的rootfs,CentOS 7恰好知足这个。若是咱们用C或者Golang就不会有这个限制了。

你能够经过CentOS提供的Dockerfile找到相关的rootfs的下载,好比:https://github.com/CentOS/sig- ... ocker

把下载到的文件解压到/tmp目录下。

分离文件系统分为三个步骤,首先咱们创建容器里面的/proc文件系统,不少Linux命令都是读取这个文件系统下的内容(好比top中显示的进程列表);其次咱们要把如今的用户和容器里面的用户作映射,不然会提示权限不足;最后咱们要经过pivot_root 函数把“切换”根文件系统。

不要忘记修改main方法,为标志位增长三个参数,映射用户。

再次执行。

和CentOS 7一摸同样,你甚至能够用yum命令,固然因为咱们如今尚未实现网络功能因此yum会告诉你没法访问网络。

再多执行几个添加文件、删除文件看看?你会发现不管作什么动做最终的数据都会被紧紧地固定在/tmp/rootfs下,也就是说——在容器里面咱们是没有办法访问host的文件的。

完整代码:https://github.com/fireflyc/mini-docker。