自己动手写Docker(txt+pdf+epub+mobi电子书下载)


发布时间:2020-07-09 09:10:28

点击下载

作者:陈显鹭

出版社:电子工业出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

自己动手写Docker

自己动手写Docker试读:

前言

为什么要写这本书

Docker 技术可谓是近年最火热的技术之一,铺天盖地的技术论坛和各种讲座,大家都在分享关于如何容器化及如何使用Docker优化自己运维和开发流程的经验。随着Docker技术的逐渐普及,使用Docker已经不再是一个难题。现在更加重要的是生产环境容器化的最佳实践,另外就是容器的编排框架之争。但是,对于技术人员来说,除去Docker外表的繁华,什么是容器,容器到底是怎么创建的,容器底层的技术探秘也是非常重要的。

我在2014年开始接触Docker,经历了从最初的新奇ü 感叹竟然还有Docker 这样的好工具,到逐渐熟悉Docker的各种功能,尝试在生产环境中使用Docker技术的过程。但是,每每被人问到:“Docker技术到底是怎么实现的呢?”我只能粗粗浅浅地说:“Docker是使用Linux Kernel的Namespace 和Cgroups实现的一种容器技术。”那么,什么是Namespace,什么是Cgroups,Docker是怎么使用它们的,容器到底是怎么一步步被创建出来的?问到这些,我就会支支吾吾地不知所以。由此可见,了解容器技术的底层技术,然后明白它们是如何工作的,尤为重要,这些才是整个容器技术的基石,掌握了这些基石才能更加容易地向上攀登。

单单讲解底层的技术实现细节和源码解读是很枯燥的一件事,一般来说很难有耐心去一点点细读然后揣摩其中的奥妙,这样囫囵吞枣地过一遍技术细节,作用不大。因此,我便萌生了写一本《自己动手写Docker》这样的书的想法。本书不去刻意讲解容器技术的细节,用到什么讲解什么,点到为止,更加细节的内容留给读者自己探索。通过阅读本书,可以一步步地了解容器技术的实现细节,更可以一步步地用自己的代码去实现它。本书最大的乐趣莫过于用自己最新了解到的知识去动手实现自己的容器。由此可以进一步打开你进入容器技术社区的大门。

本书的内容

本书的目的是去引导读者通过学习容器技术的实现细节,一步步去构建一个简单的容器。从这个过程中,了解整个容器技术领域和实现细节。本书注重原理的讲解与实践,每一部分都会有详细的代码解析,力争用最少、最精简的代码,帮助读者构建自己的容器。

本书的内容主要分为“容器与开发语言”“基础技术”“构造容器”“构造镜像”“构造容器进阶”“容器网络”“高级实践”这7章。

容器与开发语言:主要介绍Docker的基本功能和特点,并且对后面即将使用的Go语言做一个简单的介绍。

基础技术:主要介绍实现容器的底层技术,如Namespace、Cgroups、Union File System。每一节都会有文字性介绍,并且附有一个简短的小例子程序,介绍在容器上是如何使用这项技术的,方便读者清晰地理解各个技术点在容器上的作用。

构造容器:使用前面两章介绍的基础技术,构造一个最简单的容器环境,会将整体实现细节及代码解析一点点展现,直接使用前面介绍的基础技术,从而更加有实战感。

构造镜像:使用2.3节介绍的分层文件系统技术,构建一个简单的容器镜像,体现容器镜像的分层思想。

构造容器进阶:更加贴近真实的容器实现,在原来的基础上,增加更丰富的功能。通过这一章的学习,读者可以更好地了解各种技术是如何整合在一起去实现容器整体功能的。

容器网络:除实现一个容器环境之外,这一章还会讲解如何使自己的容器和宿主机通信,以及如何让不同的容器之间进行通信,更加贴近真实环境。

高级实践:使用自己编写的容器,运行一些通用程序,验证容器的可用性。此外,本章还介绍了目前Docker使用的容器运行引擎,以及目前容器运行态引擎的概况。

适用读者

希望更加深入地了解容器技术的读者。

已经使用过Docker,希望探查细节的读者。

Go语言程序员,了解如何使用Go语言来编写自己的容器。

容器技术爱好者。

如何阅读

由于本书的定位是自己动手写Docker,侧重于实战,因此仅仅纸面阅读是无法体会所有要点的。所有的源码均托管在http://github.com/xianlubird/mydocker,您可以在这里下载到本书的所有源码。每一章节都会有一个对应的tag,建议您在阅读书中讲解内容的同时,也将代码下载下来,在本机尝试运行,了解整个代码的运行流程。我们非常欢迎向本项目提交Pull Request,在阅读思考中不断交流学习。

关于勘误

由于时间和水平都比较有限,因此本书难免会存在一些纰漏和错误。如果读者发现了问题,请及时与我们联系,我们也会在后面的版本中加以改正,邮箱是xianlubird@gmail.com。非常希望与大家共同学习容器技术。

致谢

最后,向本书编写过程中给予我们巨大帮助的人们表示诚挚的感谢。感谢女友的支持,没有她包揽所有家务,我不会有大量空余时间编写此书。感谢同事姜继忠和谢瑶瑶对于本书containerd和Kubernetes相关章节内容的支持。感谢阿里云容器服务团队在本书编写期间给予的理解与包容。最后,感谢张春雨编辑,是他的帮助与支持才使得本书由一个想法变成了实体,展现给各位读者。陈显鹭

第1章 容器与开发语言

1.1 Docker

最近一段时间,云计算领域最火的莫过于“容器”一词。提到容器,就不得不提Docker,可以说Docker已经成为了容器的代名词。那么,什么是Docker?Docker又能做什么呢?本章我们就来简单介绍一下Docker。

1.1.1 简介

Docker 是一个开源工具,它可以将你的应用打包成一个标准格式的镜像,并且以容器的方式运行。Docker 容器将一系列软件包装在一个完整的文件系统中,这个文件系统包含应用程序运行所需要的一切:代码、运行时工具、系统工具、系统依赖,几乎有任何可以安装在服务器上的东西。这些策略保证了容器内应用程序运行环境的稳定性,不会被容器外的系统环境所影响。

图1.1是一个容器镜像的结构图。从中可以看到,镜像可以把系统级依赖都打包成一个文件,所有的容器会共享一个Kernel,因此在同一个Kernel下可以运行各种Linux发行版的容器。图1.1

Docker容器具有以下3个特点。

轻量级:在同一台宿主机上的容器共享系统Kernel,这使得它们可以迅速启动而且占用内存极少。镜像是以分层文件系统构造的,这可以让它们共享相同的文件,使得磁盘使用率和镜像下载速度得到提高。

开放:Docker 容器基于开放标准,这使得Docker 容器可以运行在主流Linux 发行版和Windows操作系统上。

安全:容器将各个应用程序隔离开来,这给所有的应用程序提供了一层额外的安全防护。

1.1.2 容器和虚拟机比较

容器和虚拟机同样有着资源隔离和分配的优点,但是由于其架构的不同,容器比虚拟机更加便携和高效。

虚拟机包含用户的程序,必要的函数库和整个客户操作系统,所有的这些差不多需要占用好几个GB的空间。图1.2为虚拟机的架构图。图1.2

容器包含用户的程序和所有的依赖,但是容器之间是共享Kernel的。各个容器在宿主机上互相隔离,并且在用户态下运行。Docker 容器不和任何基础设施绑定,它可以运行在任何电脑、IDC和云上。图1.3为Docker容器的架构图。图1.3

1.1.3 容器加速开发效率

Docker 容器可以帮助开发者跳过设置冗杂的开发环境,专注于开发软件的新功能,具体有如下3项。

加速开发:再也不用等待数小时设置开发环境,可以很方便地使生产环境的代码在本地跑起来。

赋能创造力:Docker容器的隔离特性可以让开发者摆脱限制。开发者可以为自己的应用选择最好的语言和工具,再也不用担心产生内部工具的冲突。

消除环境不一致:将应用程序的配置和所有依赖打包成一个镜像在容器中,可以保证应用在任何环境中都可以按照预期来运行,再也不用担心不得不在不同环境中安装相同软件和配置的问题。

1.1.4 利用容器合作开发

Docker 镜像可以存储到Docker Hub中,团队成员可以通过Docker Store、Docker Hub管理分享镜像。所有的变化和历史都可以在整个组织间查看。

而且,你可以很简单地分享你的容器,不需要担心环境依赖产生的不一致问题,其他团队也可以很简单地引用你的容器,而不需要去关心它是如何工作的。

1.1.5 利用容器快速扩容

Docker 允许动态地改变应用程序,可以通过扩容快速提高应用程序的能力并及时修复缺陷。Docker容器可以秒级启动和停止,因此,它可以在需要的时候快速扩容出大量的应用程序,扛住并发的压力。

1.1.6 安装使用Docker

Docker就是一个这样的工具。它可以帮助开发者很方便地去构建、部署、运行自己的程序,还可以让你非常迅速地测试你的项目并将其部署到生产环境中。

首先,你需要在自己的机器上安装Docker,这里以在Ubuntu 14.04系统上安装Docker为例。

curl-sSL https://get.docker.com|sh

运行以上命令,一段美妙的小脚本就这样被安装到了你的机器上,它完成了安装Docker需要的所有内容。下面,就开始使用它吧。以安装一个WordPress为例,看看Docker是如何快速安装一个WordPress的。以前安装WordPress,可能需要去了解PHP、MySQL,还有服务器的系统,最后才去安装WordPress,非常麻烦。但是,如果换一种方式,使用Docker来安装呢?

docker run-d-p 80:80--name wordpress wordpress

运行以上命令,Docker会自动从Docker Hub中拉取WordPress镜像,这个镜像是已经被build好的,包含了PHP、MySQL和WordPress。你所做的工作就是等待Docker帮你把这个服务启动起来以后,在浏览器上访问你的服务器IP,就可以看到WordPress的安装页面,然后一步步点击页面按钮完成安装即可。对于MySQL密码,可以使用如下命令获取。

echo $(docker logs wordpress|grep password)

上面这条命令就可以获得MySQL密码,将其填写到网页中,就得到了一个可以运行的WordPress,然后开始愉快地使用它吧。

是不是感受到了Docker的威力?其实这只是Docker强大功能的冰山一角。快速部署是Docker其中的一个特性。你不需要登录到服务器,将运行环境一个一个地安装好,最后再部署自己的代码。Docker像集装箱一样,帮助你打包好了一切,你只需要开箱使用即可。就像刚才的例子,还可以非常简单地再次运行刚才的命令,只需要换一下映射的端口,就可以再启动一个WordPress,这是安装原生应用所不敢想象的。

1.2 Go

Go 语言又称Golang,是Google开发的一种静态强类型、编译型、并发型并具有垃圾回收功能的编程语言。Go语言在2009年第一次被披露,并在2012年发布了1.0版本,可以说是一门非常年轻的语言。Go语言的创造者可谓众星云集,包括UNIX操作系统和B语言(C语言的前身)的创造者、UTF-8编码的发明者Ken Thompson,UNIX项目的参与者、UTF-8编码的联合创始人和Limbo编程语言(Go语言的前身)的创造者Rob Pike,以及著名的JavaScript引擎V8的创造者Robert Griesemer。

1.2.1 描述

Go语言的语法虽然接近C语言,但还是有一些不同,比如两者对于变量的声明是不同的,且Go语言中的for循环和if判断语句不需要用小括号括起来。Go语言的并行模型是以东尼·霍尔的通信顺序进程(CSP)为基础的,并采取了类似模型的其他语言(包括Occam和Limbo),但它也具有Pi运算的特征,比如通道传输。

与C++相比,Go语言并不包括如异常处理、继承、泛型、断言、虚函数等功能,但增加了slice型、并发、管道、垃圾回收、接口(Interface)等特性的语言级支持。当然,Google对于泛型的态度还是很开放的,但在该语言的常见问题列表中,对于断言的存在,则持负面态度,同时也在为自己不提供类型继承辩护。不同于Java,Go语言内嵌了关联数组(也称为哈希表(Hash)或字典(Dictionary)),就像字符串类型一样。

可以在Go语言官网首页看到一个Go语言的Hello World示例,代码如下。

目前使用Go开发的项目非常多,其中就有国人开发的beego(用来开发Go应用程序的开源框架),另外一个就是大名鼎鼎的Docker。因此,本书会以Go语言代码为示例开发我们自己的Docker应用。

1.2.2 安装Go

本书的开发测试环境如下。

Ubuntu 14.04.

内核版本3.13.0-83-generic。

Go版本1.7.1。

可以在Go语言官网https://golang.org/dl/根据操作系统下载对应的安装包。这里以Linux为例进行安装,首先下载安装包go1.7.1.linux-amd64.tar.gz,然后执行tar-C/usr/local-xzf go1.7.1.linux-amd64.tar.gz,将安装包解压到/usr/local目录下。编辑$HOME/.profile或$HOME/.bashrc,将export PATH=$PATH:/usr/local/go/bin命令添加到文件中,然后执行source$HOME/.bashrc,使修改生效。这时就可以在系统中使用Go命令了,执行go version来看一下,命令如下。

1.2.3 配置GOPATH

GOPATH 是真正存放代码的路径,Go寻找依赖包时会根据$GOPATH来寻找,GOPATH目录约定有如下3个子目录。

src存放源代码。

pkg存放编译后生成的文件。

bin存放编译后的可执行文件。

这里以/go为GOPATH路径,编辑~/.bashrc文件,将命令export GOPATH=/go添加到文件中,然后执行source~/.bashrc,之后再执行go env看一下效果,结果如下。

可以看到,$GOPATH已经被指定了。

本书中的代码都会基于以上配置,代码路径为$GOPATH/src/github.com/xianlubird/mydocker,项目名称为mydocker,后面会基于这个路径进行开发。

1.3 小结

本章主要介绍了什么是Docker,以及使用容器技术带来的优势。之后介绍了如何安装和使用Docker做一个简单的demo。同时简单讲解了Go语言的不同,以及基本的开发环境配置与使用。下面,将以此为基础进行自己的容器开发。

第2章 基础技术

2.1 Linux Namespace介绍

我们经常听到,Docker是一个使用了Linux Namespace和Cgroups的虚拟化工具。但是,什么是Linux Namespace,它在Docker内是怎么被使用的?说到这里,很多人就会迷茫。下面就先来介绍一下Linux Namespace及它们是如何在容器中使用的。

2.1.1 概念

Linux Namespace是Kernel的一个功能,它可以隔离一系列的系统资源,比如PID(Process ID)、User ID、Network等。一般看到这里,很多人会想到一个命令chroot,就像chroot允许把当前目录变成根目录一样(被隔离开来的),Namespace也可以在一些资源上,将进程隔离起来,这些资源包括进程树、网络接口、挂载点等。

比如,一家公司向外界出售自己的计算资源。公司有一台性能还不错的服务器,每个用户买到一个tomcat实例用来运行它们自己的应用。有些调皮的客户可能不小心进入了别人的tomcat实例,修改或关闭了其中的某些资源,这样就会导致各个客户之间互相干扰。也许你会说,我们可以限制不同用户的权限,让用户只能访问自己名下的tomcat实例,但是,有些操作可能需要系统级别的权限,比如root权限。我们不可能给每个用户都授予root权限,也不可能给每个用户都提供一台全新的物理主机让他们互相隔离。因此,Linux Namespace在这里就派上了用场。使用Namespace,就可以做到UID级别的隔离,也就是说,可以以UID为n的用户,虚拟化出来一个Namespace,在这个Namespace里面,用户是具有root权限的。但是,在真实的物理机器上,他还是那个以UID为n的用户,这样就解决了用户之间隔离的问题。当然这只是Namespace其中的一个简单功能。

除了User Namespace,PID也是可以被虚拟的。命名空间建立系统的不同视图,从用户的角度来看,每一个命名空间应该像一台单独的Linux计算机一样,有自己的init进程(PID为1),其他进程的PID依次递增,A和B空间都有PID为1的init进程,子命名空间的进程映射到父命名空间的进程上,父命名空间可以知道每一个子命名空间的运行状态,而子命名空间与子命名空间之间是隔离的。从图2.1所示的PID映射关系图中可以看到,进程3在父命名空间中的PID为3,但是在子命名空间内,它的PID就是1。也就是说用户从子命名空间A内看进程3就像init进程一样,以为这个进程是自己的初始化进程,但是从整个host来看,它其实只是3号进程虚拟化出来的一个空间而已。图2.1

当前Linux一共实现了6种不同类型的Namespace。

Namespace的API主要使用如下3个系统调用。

clone()创建新进程。根据系统调用参数来判断哪些类型的Namespace被创建,而且它们的子进程也会被包含到这些Namespace中。

unshare()将进程移出某个Namespace。

setns()将进程加入到Namespace中。

2.1.2 UTS Namespace

UTS Namespace主要用来隔离nodename和domainname两个系统标识。在UTS Namespace里面,每个Namespace允许有自己的hostname。

下面将使用Go来做一个UTS Namespace的例子。其实对于Namespace这种系统调用,使用C语言来描述是最好的,但是本书的目的是去实现Docker,由于 Docker就是使用Go开发的,所以就整体使用Go来讲解。先来看一下如下代码,非常简单。

解释一下代码,exec.Command("sh")用来指定被fork出来的新进程内的初始命令,默认使用sh来执行。下面就是设置系统调用参数,像2.1.1小节中讲到的一样,使用CLONE_NEWUTS这个标识符去创建一个UTS Namespace。Go帮我们封装了对clone()函数的调用,这段代码执行后就会进入到一个sh运行环境中。

在Ubuntu 14.04上运行这个程序,Kernel版本为3.13.0-65-generic,Go版本为1.7.3,执行go run main.go命令,在这个交互式环境里,使用pstree-pl查看一下系统中进程之间的关系,如下。

然后,输出一下当前的PID,代码如下。

验证一下父进程和子进程是否不在同一个UTS Namespace中,验证代码如下。

可以看到它们确实不在同一个UTS Namespace中。由于UTS Namespace对hostname做了隔离,所以在这个环境内修改hostname应该不影响外部主机,下面来做一下实验。

在这个sh环境内执行如下代码示例。

另外启动一个shell,在宿主机上运行hostname,看一下效果。

可以看到,外部的hostname并没有被内部的修改所影响,由此可了解UTS Namespace的作用。

2.1.3 IPC Namespace

IPC Namespace用来隔离System V IPC和POSIX message queues。每一个IPC Namespace都有自己的System V IPC和POSIX message queue。

在上一版本的基础上稍微改动了一下代码。

可以看到,仅仅增加syscall.CLONE_NEWIPC代表我们希望创建IPC Namespace。下面,需要打开两个shell来演示隔离的效果。

首先在宿主机上打开一个shell。

这里,能够发现可以看到一个queue了。下面,使用另外一个shell去运行程序。

通过以上实验,可以发现,在新创建的Namespace里,看不到宿主机上已经创建的message queue,说明IPC Namespace创建成功,IPC已经被隔离。

2.1.4 PID Namespace

PID Namespace是用来隔离进程ID的。同样一个进程在不同的PID Namespace 里可以拥有不同的PID。这样就可以理解,在docker container 里面,使用ps-ef经常会发现,在容器内,前台运行的那个进程PID是1,但是在容器外,使用ps-ef会发现同样的进程却有不同的PID,这就是PID Namespace做的事情。

在2.1.3小节中代码的基础上,再修改一下代码,添加一个syscall.CLONE_NEWPID,代表为fork出来的子进程创建自己的PID Namespace。

我们需要打开两个shell。首先在宿主机上看一下进程树,找一下进程的真实PID。

可以看到,go main函数运行的PID为20190。下面,打开另外一个shell运行一下如下代码。

可以看到,该操作打印了当前Namespace的PID,其值为1。也就是说,这个20190的PID被映射到Namespace里后PID 为1。这里还不能使用ps来查看,因为ps和top等命令会使用/proc内容,具体内容在下面的Mount Namespace部分会进行讲解。

2.1.5 Mount Namespace

Mount Namespace用来隔离各个进程看到的挂载点视图。在不同Namespace的进程中,看到的文件系统层次是不一样的。在Mount Namespace中调用mount()和umount()仅仅只会影响当前Namespace内的文件系统,而对全局的文件系统是没有影响的。

看到这里,也许就会想到chroot()。它也是将某一个子目录变成根节点。但是,Mount Namespace不仅能实现这个功能,而且能以更加灵活和安全的方式实现。

Mount Namespace是Linux 第一个实现的Namespace 类型,因此,它的系统调用参数是NEWNS(New Namespace 的缩写)。当时人们貌似没有意识到,以后还会有很多类型的Namespace加入Linux大家庭。

针对2.1.4小节中的代码做了一点改动,增加了NEWNS标识,如下。

首先,运行代码,然后查看一下/proc的文件内容。proc是一个文件系统,提供额外的机制,可以通过内核和内核模块将信息发送给进程。

因为这里的/proc还是宿主机的,所以看到里面会比较乱,下面,将/proc mount到我们自己的Namespace下面来。

可以看到,瞬间少了好多文件。下面就可以使用ps来查看系统的进程了。

可以看到,在当前的Namespace中,sh 进程是PID 为1 的进程。这就说明,当前的Mount Namespace 中的mount 和外部空间是隔离的,mount 操作并没有影响到外部。Docker volume也是利用了这个特性。

2.1.6 User Namespace

User Namespace 主要是隔离用户的用户组ID。也就是说,一个进程的User ID 和Group ID在User Namespace内外可以是不同的。比较常用的是,在宿主机上以一个非root用户运行创建一个User Namespace,然后在User Namespace里面却映射成root 用户。这意味着,这个进程在User Namespace里面有root权限,但是在User Namespace外面却没有root的权限。从Linux Kernel 3.8开始,非root进程也可以创建User Namespace,并且此用户在Namespace里面可以被映射成root,且在Namespace内有root权限。

下面,继续以一个例子来描述,代码如下。

本例在原来的基础上增加了syscall.CLONE_NEWUSER。首先,以root来运行这个程序,运行前在宿主机上看一下当前的用户和用户组,显示如下。

可以看到我们是root用户,接下来运行一下程序。

可以看到,它们的UID是不同的,因此说明User Namespace生效了。

2.1.7 Network Namespace

Network Namespace 是用来隔离网络设备、IP地址端口等网络栈的Namespace。Network Namespace可以让每个容器拥有自己独立的(虚拟的)网络设备,而且容器内的应用可以绑定到自己的端口,每个Namespace内的端口都不会互相冲突。在宿主机上搭建网桥后,就能很方便地实现容器之间的通信,而且不同容器上的应用可以使用相同的端口。

同样,在2.1.6小节的代码的基础上增加syscall.CLONE_NEWNET标识符,如下。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载