后端的轮子(四)--- 容器

容器,目前最火的话题了,在后端的开发中,容器的运用也已是主流技术了,今天,咱们就来讲说容器技术,以前我对这一块的了解不是不少,可是最近有些特殊缘由转成运维工程师了,而公司的全线服务都是docker的,以一个开发人员的习惯,转成运维之后,仍是想对这种东西总想深刻了解一下,因而看了很多相关资料而且看了一下docker的源代码,发现这东西确实很厉害,和以前脑中的docker印象彻底不一样,因而有了这篇文章。linux

先说结论,容器真的很好,很轻量级,功能又很重量级。git

前言

首先,虽然目前docker技术如此火爆,可是其实容器技术本上并非什么高大上的东西,总的来说,就是对目前的Linux底层的几个API的封装,而后围绕着这几个API开发出了一套周边的环境。程序员

以前全部的讲关于容器的文章,一开始就开始讲UTC隔离,PID隔离,IPC隔离,文件系统隔离,CGroups系统,今天这一篇,咱们换一个视角,咱们从如下几个方面来讲一下容器技术。github

  • 首先,咱们从容器和虚拟机提及,都说容器是很是轻量级的,那么和虚拟机比起来,到底轻在什么地方呢。
  • 第二部分,咱们会经过一步一步的说明,经过构造一个监狱,来讲明如何创建一个简单的容器,会涉及到容器的各类技术,固然还有一些没有涉及到的,我认为不影响理解。
  • 第三部分,咱们会经过代码实战一把,看看如何一步一步按照第二部分的说明启动一个容器并运行本身的代码,这一部分的所有代码都在github上。
  • 最后,我会再说一下docker技术,由于docker从代码来看,容器技术只是他的一小部分,完整的docker远比单纯的容器要复杂,我会简单的说一下我对docker的理解,包括docker使用的其余技术点。

容器和虚拟机

要说容器,跑不了和虚拟机进行比较,虚拟机是比较古老的技术了,虚拟机的架构图以下所示。golang

虚拟机核心是什么?是模拟硬件,让虚拟机的操做系统觉得本身跑在一个真实的物理机器上,用软件模拟出来CPU,内存,硬盘,网卡,让虚拟机里面的操做系统以为本身是在操做真实的硬件,因此虚拟机里面的CPU啊,内存啊都是假的,都是软件模拟出来的(如今有硬件虚拟化技术了,比纯软件模拟要高级一些,但操做系统无论这些),既然操做系统都骗过去了,固然跑在操做系统上的进程一样也骗过去了呗,因此这些进程都彻底感知不到底层硬件的区别,还觉得本身很欢乐的跑在一台真实的物理机上了。redis

那么容器又是什么鬼呢?容器的架构图以下(这张图网上找的,侵权立刻删)docker

和虚拟机一个很明显的区别就是容器其实并无模拟硬件,仍是那个硬件,仍是那个操做系统,只不过是在操做系统上作了一点文章【这张图中就是docker engine了】,让进程觉得本身运行在了一个全新的操做系统上,有一个很形象的词来描述他就是软禁!就是把进程软禁在一个环境中,让进程以为本身很happy,其实一切尽在操做系统的掌控之中,其实虚拟机也是,虚拟机是把操做系统软禁起来了,让操做系统以为很happy,容器是把进程软禁起来,你看,一个是软禁操做系统,一个是软禁进程,这两个明显不是一个级别的东西,谁轻谁重不用说了吧。shell

既然容器和虚拟机的区别在于一个是经过模拟硬件来软禁操做系统,一个是经过作作操做系统的手脚来软禁进程,那么他们能达到的效果也是不同的。bootstrap

  • 对于虚拟机来讲,既然是模拟的硬件,那么就能够在windows上装linux虚拟机了,由于反正是模拟硬件嘛,虚拟机内部的操做系统也不知道外面的宿主机是什么系统。
  • 容器就不同了,由于是在操做系统上作文章,因此不可能在linux上装windows了,而且还有一点就是,容器内的操做系统其实就是外面宿主机的操做系统,二者实际上是一个,你在容器内用uname -a看到的内核版本和外面看到的是同样的。本质上容器就是一个进程,他和宿主机上任何其余进程没什么本质的区别。

建造容器监狱

如何来作一个容器呢?或者说容器是怎么实现的呢?咱们从几个方面来讲一下容器的实现,一是最小系统,二是网络系统,三是进程隔离技术,四是资源分配。最小系统告诉你软禁进程所须要的那个温馨的监狱环境,网络系统告诉你软禁的进程如何和外界交互,进程隔离技术告诉你若是把进程关到这个温馨的监狱中去,资源分配告诉你监狱里的进程如何给他分配资源让他不能胡来。ubuntu

建设基本监狱【最小系统打造】

要软禁一个进程,固然须要有个监狱了,在说监狱以前,咱们先看看操做系统的结构,一个完整的操做系统【Linux/Unix操做系统】分红三部分,以下图所示【本图也是网上找的,侵权立刻删,这个图是四个部分,包括一个boot参数部分,这不是重点】。

首先是bootloader,这部分启动部分是汇编代码,CPU从一个固定位置读取第一行汇编代码开始运行,bootloader先会初始化CPU,内存,网卡(若是须要),而后这部分的主要做用是把操做系统的kernel代码从硬盘加载到内存中,而后bootloader使命完成了,跳转到kernel的main函数入口开始执行kernel代码,kernel就是咱们熟悉的linux的内核代码了,你们说的看内核代码就是看的这个部分了,kernel代码启动之后,会从新初始化CPU,内存,网卡等设备,而后开始运行内核代码,最后,启动上帝进程(init),开始正常运行kernel,而后kernel会挂载文件系统

好了,到这里,对进程来讲都是无心义的,由于进程不关心这些,进程产生的时候这些工做已经作完了,进程能看到的就是这个文件系统了,对进程来讲,内存空间,CPU核心数,网络资源,文件系统是他惟一能看得见使用获得的东西,因此咱们的监狱环境就是这么几项核心的东西了。

kernel和文件系统是能够分离的,好比咱们熟悉的ubuntu操做系统,可能用的是3.18的Linux Kernel,再加上一个本身的文件系统,也能够用2.6的Kernel加上一样的操做系统。每一个Linux的发行版都是这样的,底层的Kernel可能都是同一个,不一样的只是文件系统不一样,因此,能够简单的认为,linux的各类发行版就是kernel内核加上一个独特的文件系统,这个文件系统上有各类各样的工具软件。

既然是这样,那么咱们要软禁一个进程,最基础的固然要给他一个文件系统啦,文件系统简单的说就是一堆文件夹加上一堆文件组成的,咱们先来生成一个文件系统,我以前是作嵌入式的,嵌入式的Linux系统生成文件系统通常用busybox,只须要在在ubuntu上执行下面的命令,就能生成一个文件系统

apt-get install busybox-static mkdir rootfs;cd rootfs mkdir dev etc lib usr var proc tmp home root mnt sys /bin/busybox --install -s bin

大概这么几步就制做完成了一个文件系统,也就是监狱的基本环境已经有了,记得把lib文件夹的内容拷过去。制做完了之后,文件系统就这样了。

还有一种方式,就是使用debootstap这个工具来作,也是几行命令就作完了一个debian的文件系统了,里面连apt-get都有,docker的基础文件系统也是这个。

apt-get install qemu-user-static debootstrap binfmt-support mkdir rootfs debootstrap --foreign wheezy rootfs //wheezy是debian的版本 cp /usr/bin/qemu-arm-static rootfs/usr/bin/

完成之后,这个wheezy的文件系统就是一个标准的debian的文件系统了,里面的基本工具包罗万象。

OK,基本的监狱环境已经搭建好了,进程住进去之后就跟在外面同样,啥都能干,但就是跑不出来。

要测试这个环境,可使用linux的chroot命令,chroot ./rootfs就进入了这个制做好的文件系统了,你能够试试,看不到外面的东西了哦。

打造探视系统【网络系统】

刚刚只创建了一个基本的监狱环境,对于现代的监狱,只有个房子不能上网怎么行?因此对于监狱环境,还须要创建一个网络环境,好让里面的进程们能够很方便的和监狱外的亲友们联系啊,否则谁愿意一我的呆在里面啊。

如何来创建一个网络呢?对于容器而言,不少地方是可配置的,这里说可配置,其实意思就是可配置也能够不配置,对于网络就是这样,通常的容器技术,对网络的支持有如下几个方式。

  • 无网络模式,就是不配置模式了,不给他网络,只有文件系统,适合单机版的程序。
  • 直接和宿主机使用同一套网络,也是不配置模式,可是这个不配置是不进行网络隔离,直接使用宿主机的网卡,ip,协议栈,这是最奔放的模式,各个容器若是启动的是同一套程序,那么须要配置不一样的端口了,好比有3个容器,都是redis程序,那么须要启动三个各不一样的端口来提供服务,这样各个容器没有作到彻底的隔离,可是这也有个好处,就是网络的吞吐量比较高,不用进行转发之类的操做。
  • 网桥模式,也是docker默认使用的模式,咱们安装完docker之后会多一个docker0的网卡,其实这是一个网桥,一个网桥有两个端口,两个端口是两个不一样的网络,能够对接两块网卡,从A网卡进去的数据会从B网卡出来,就像黑洞和白洞同样,咱们创建好网桥之后,在容器内建一块虚拟网卡,把他和网桥对接,在容器外的宿主机上也创建一块虚拟网卡,和网桥对接,这样容器里面的进程就能够经过网桥这个探视系统和监狱外联系了。

咱们能够直接使用第二种不配置模式,直接使用宿主机的网络,这也是最容易最方便的,可是咱们在这里说的时候稍微说一下第三种的网桥模式吧。

网桥最开始的做用主要是用来链接两个不一样的局域网的,更具体的应用,通常是用来链接两个不一样的mac层的局域网的,好比有线电视网和以太网,通常网桥只作数据的过滤和转发,也能够适当的作一些限流的工做,没有路由器那么复杂,实现起来也比较简单,对高层协议透明,他能操做的都是mac报文,也就是在ip层如下的报文。

对于容器而言,使用网桥的方式是在宿主机上使用brctl命令创建一个网桥,做为容器和外界交互的渠道,也就是你们使用docker的时候,用ifconfig命令看到的docker0网卡,这实际上就是一个网桥,而后每启动一个容器,就用brctl命令创建一对虚拟网卡,一块给容器,一块连到网桥上。这样操做下来,容器中发给虚拟网卡的数据都会发给网桥,而网桥是宿主机上的,是能链接外网的,因此这样来作到了容器内的进程能访问外网。

容器的网络我没有深刻研究,感受不是特别复杂,最复杂的方式就是网桥的方式了,这些网络配置均可以经过命令行来进行,可是docker的源码中是本身经过系统调用实现的,说实话我没怎么看明白,功力仍是不够啊。 我使用的就是最最简单的不隔离,和宿主机共用网卡,只能经过端口来区分不一样容器中的服务。

监禁皮卡丘【隔离进程】

好了,监狱已经建好了,探视系统也有了,得抓人了来软禁了,把进程抓进来吧。咱们以一个最最基本的进程/bin/bash为例,把这个进程抓进监狱吧。

说到抓进程,这时候就须要来聊聊容器的底层技术了,Linux提供几项基础技术来进行轻量级的系统隔离,这些个隔离技术组成了咱们熟悉的docker的基础。本篇不会大段的描述这些技术,文章后面我会给出一些参考连接,由于这类文章处处均可以找到,本篇只是让你们对容器自己有个了解。 下面所说的全部基础技术,其实就是一条系统调用,包括docker的基础技术,也是这么一条系统调用(固然,docker还有不少其余的,可是就容器来讲,这条是核心的了)

clone(进程函数, 进程栈空间, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET |CLONE_NEWUSER | CLONE_NEWIPC , NULL)

这是一条C语言的clone系统调用,实际上就是启动一个新的进程,后面的参数就是各类隔离了,包括UTS隔离,PID隔离,文件系统隔离,网络隔离,用户隔离,IPC通信隔离

在go语言中,没有clone这个系统调用(不知道为何不作这个系统调用,多是为了多平台的兼容吧),必须使用exec.Cmd这个对象来启动进程,在linux环境下,能够设置Cmd的attr属性,其中有个属性叫CloneFlags,能够把上面那些个隔离信息设置进去,这样,启动的进程就是咱们须要的了,咱们能够这么来启动这个进程

cmd := exec.Command("./container", args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,
    }
    cmd.Run()复制代码

这样,经过这个cmd命令启动的./container进程就是一个隔离进程了,也就是咱们把这个进程给关起来了,他已经看不到其余东西了,是否是很简单?可是你要是就直接这么运行,仍是看不到什么特别的地方。 在这个以后,咱们须要按照上面所说的,把监狱先创建好,监狱的创建在./container中进行,创建监狱也比较简单,基本上也是一堆系统调用,好比文件系统的软禁,就像下面的同样

syscall.Mount(rootfs, tmpMountPoint, "", syscall.MS_BIND, "")  //挂载根文件系统
syscall.Mount(rootfs+"/proc", tmpMountPoint+"/proc", "proc", 0, "");  //挂载proc文件夹
syscall.PivotRoot(tmpMountPoint, pivotDir)  //把进程软禁到根文件系统中复制代码

关于上面proc文件夹,作了特殊处理,在Linux中,proc文件夹的地位比较特殊,具体做用能够自行查文档,简单的说就是保存系统信息的文件夹。在这里,devsys这两个特殊文件夹也须要作特殊处理的,这里没有写出来而已。

这些都作完了之后,就能够启动真正须要执行的进程了,好比/bin/bash,或者你本身的程序,这样启动的/bin/bash或者你本身的程序就是在监狱中启动的了,那么他看到的全部东西都是监狱中的了,外面宿主机的一切对他来讲都是屏蔽的了,这样,一个docker的雏形就产生了。

这里多说一下,经过clone系统调用启动的进程,它本身看到本身的PID是1,也就是上帝进程了,这个上帝进程能够来造基础监狱【文件系统】,打造放风系统【网络系统】,而后再经过它来生成新的进程,这些进程出来就在监狱中了,咱们使用docker的时候,本身的服务实际上就是这些个在监狱中出生的进程【可能个人描述不太正确啊,我没有仔细看docker的源码,我本身感受是这样的】。

至此,咱们来总结一下,启动一个最简单的容器并运行你本身的进程,须要几步。

  • 创建一个监狱【文件系统】,使用busybox或者debootstrap创建。
  • 创建一个放风系统【网络系统】,使用网桥或者不隔离网络,直接使用宿主机的网卡。
  • 抓一个皮卡丘【启动上帝进程】并放到监狱中【挂载文件系统,初始化网络】,配置Cloneflags的值,并经过exec.Cmd来进行上帝进程的启动
  • 让皮卡丘生个孩子【启动你本身的程序】,直接调用exec.Cmd的run方法启动你本身的进程
  • 完成

经过上面几步,最简容器就完成了,是否是很简单?可是容器仅仅有这些是不够的,咱们还有三个隔离没有讲,这里稍微提一下吧。

  • 一个是UTS隔离,主要是用来隔离hostname和域名的。
  • 一个是User隔离,这样容器里面的用户和宿主机用户能够作映射,意思就是里面虽然看到的是root用户,可是实际上并非root,不可以瞎搞系统,这样容器的安全性会有保障。
  • 一个是IPC隔离,这个是进程通信的隔离,这样容器里面的进程和容器外面的进程就不能进行进程间通信了,保证了比较强的隔离性。

给犯人分配食物【资源配置】

咱们知道,通常的监狱中的食物是定量的,毕竟不是每一个监狱均可以吃自助餐的,容器也同样,要是咱们就启个容器啥都不限制,里面要是有个牛逼的程序员写的牛逼程序,瞬间就把你的内存和CPU给干没了。好比像下面这个fork炸弹。【下面程序请不要尝试!!】

int main(){
    while(fork());
}复制代码

在容器技术中,Cgroups【control groups】就是干这个事情的,cgroups负责给监狱设定资源,好比能用几个cpu啊,cpu能给你多少百分比的使用量啊,内存能用多少啊,磁盘能用多少啊,磁盘的速度能给你多少啊,各类资源均可以从cgroups来进行配置,把这些东西配置给容器之后,就算容器里面运行一个fork炸弹也不怕了,反正影响不到外面的宿主机,到这里,容器已经愈来愈像虚拟机了。

cgroups是linux内核提供的API,虽然是API,但它的整个实现完美知足了Linux两大设计哲学之一:一切皆文件(还有一个哲学是通信全管道),对API的调用其实是操做文件。

咱们以cpu的核心数看看如何来作一个cgroups的资源管理。假设咱们的物理机是个8核的CPU,而咱们刚刚启动的容器我只想让他使用其中的两个核,很简单,咱们用命令行直接操做sys/fs/cgroups文件夹下的文件来进行。这个配置咱们能够在启动的上帝进程中进行,也能够在容器外部进行,都是直接操做文件。

关于cgroups这个东西很复杂也很强大,其实在容器出来以前,好的运维工程师就已经把这个玩得很溜了。docker也只是把这些个文件操做封装了一下,变成了docker的启动和配置参数而已。

亲自抓一次进程吧

好了,该说的都说了,咱们来实战一把,本身启一个容器吧,而且启动之后为了更直观的看到效果,咱们启动一个ssh服务,打开22332端口,而后外面就能够经过ssh连到容器内部了,这时候你爱干什么干什么了。

制做文件系统

文件系统制做咱们直接使用debootstrap进行制做,在/root/目录下创建一个rootfs的文件夹,而后使用debootstrap --foreign wheezy rootfs制做文件系统,制做完了之后,文件系统就是下面这个样子

制做初始化脚本

初始化脚本就作两件事情,一是启动ssh服务,一是启动一个shell,提早先把/etc/ssh/sshd_config中的端口改为23322。

#!/bin/bash
service ssh start
/bin/bash复制代码

而后把这个脚本放到制做的文件系统的root目录下,加上执行权限。

启动上帝进程

文件系统制做完成了,启动脚本也作完了,咱们看看咱们这个容器的架构,架构很简单,整个容器分为两个独立的进程,两份独立的代码。

  • 一个是主进程【wocker.go】,这个进程自己就是一个http的服务,经过get方法接收参数,参数有rootfs的地址,容器的hostname,须要监禁的进程名称(这里就是咱们的第二个进程【startContainer.go】),而后经过exec.Cmd这个包启动这个进程。
  • 第二个进程启动就是以隔离方式启动的了,就是容器的上帝进程了,这个进程中进行文件系统挂载,hostname设置,权限系统的设定,而后启动正式的服务进程(也就是咱们的启动脚本/root/start_container.sh

挂载文件系统

第二个进程是容器的上帝进程,在这里进行文件系统的挂载,最重要的代码以下

syscall.Mount(rootfs, tmpMountPoint, "", syscall.MS_BIND, "") //挂载根文件系统
    syscall.Mount(procpath, tmpMountPointProc, "proc", 0, "")  //挂载proc文件夹,用来看系统信息的
    syscall.Mount(syspath, tmpMountPointSys, "sysfs", 0, "")    //挂载sys文件夹,用来作权限控制的
    syscall.Mount("udev", tmpMountPointDev, "devtmpfs", 0, "") //挂载dev,用来使用设备的
    syscall.PivotRoot(tmpMountPoint, pivotDir)//进入到文件系统中复制代码

具体代码能够看github上的文件,这样,根文件系统就挂载完了,已经进入了基本监狱中了。

启动初始化脚本

文件系统挂载完了之后,而后启动初始化脚本,这个就比较简单了,一个exec.Cmd的Run方法调用就搞定了。

cmd := exec.Command("/root/start_container.sh")复制代码

这样,ssh服务就在容器中启动了,能够看到一行Starting OpenBSD Secure Shell server: sshd.的打印信息,容器启动完成,这时候,咱们能够经过ssh root@127.0.0.1 -p 23322这个命令登陆进咱们的容器了,而后你就能够随心所欲了。

上面那个图,咱们看到登陆进来之后,hostname已经显示为咱们设定的hello了,这时这个会话已经在容器里面了,咱们ps一下看看进程们。

看到pid为1的进程了么,那个就是启动这个容器的上帝进程了。恩,到这里,咱们已经在容器中了,这里启动的任何东西都和咱们知道的docker中的进程没什么太大区别了。

但在这里,我缺失了权限的部分,你们能够本身加上去,主要是各类文件操做比较麻烦。。。

关于Docker的思考

docker这门最近两年很是火的技术,光从容器的角度来看的话,也不算什么新的牛逼技术了,和虚拟机比起来仍是要简单很多,固然,docker自己可彻底不止容器技术自己,还有AUFS文件分层技术,还有etcd集群技术,最关键的是docker经过本身的整个生态把容器包裹在里面了,提供了一整套的容器管理套件,这样让容器的使用变得异常简单,因此docker才能这么流行吧。

和虚拟机比起来,docker的优势实在是太多了。

  • 首先,从易用性的角度来讲,管理一个虚拟机的集群,有一整套软件系统,好比openstack这种,光熟悉这个openstack就够喝一壶的了,并且openstack的网络管理异常复杂,哦,不对,是变态级的复杂,要把网络调通不是那么容易的事情。

  • 第二,从性能上来看看,咱们刚刚说了容器的原理,因此实际上容器无论是对CPU的利用,仍是内存的操做或者外部设备的操做,对一切硬件的操做实际上都是直接操做的,并无通过一个中间层进行过分,可是虚拟机就不同了,虚拟机是先操做假的硬件,而后假硬件再操做真硬件,利用率从理论上就会比容器的要差,虽然如今有硬件虚拟化的技术了能提高一部分性能,但从理论上来讲性能仍是没有容器好,这部分我没有实际测试过啊,只是从理论上这么以为的,若是有不对的欢迎拍砖啊。

  • 第三,从部署的易用性上和启动时间上,容器就彻底能够秒了虚拟机了,这个不用多说吧,一个是启动一台假电脑,一个是启动一个进程。

那么,docker和虚拟机比起来,缺点在哪里呢?

我本身想了半天,除了资源隔离性没有虚拟机好之外,我实在是想不出还有什么缺点,由于cgroups的隔离技术只能设定一个上限,好比在一台4核4G的机器上,你可能启动两个docker,给他们的资源都是4核4G,若是有个docker跑偏了,一我的就干掉了4G内存,那么另一个docker可能申请不到资源了。而虚拟机就不存在这个问题,可是这也是个双刃剑,docker的这种作法能够更多的榨干系统资源,而虚拟机的作法极可能在浪费系统资源。

除了这个,我实在是想不出还有其余缺点。网上也有说权限管理没有虚拟机好,但我以为权限这东西,仍是得靠人,靠软件永远靠不住。

最后,代码都在github上,只有很是很是简单的三个文件【一个Container.go是容器类,一个wocker.go没内容,一个startContainer.go启动容器】,那个http服务留着没写,后面写http服务的时候在用一下。

恩,docker确实是个好东西。


若是你以为不错,欢迎转发给更多人看到,也欢迎关注个人公众号,主要聊聊搜索,推荐,广告技术,还有瞎扯。。文章会在这里首先发出来:)扫描或者搜索微信号XJJ267或者搜索西加加语言就行

相关文章
相关标签/搜索