# 56 | 容器:大公司为保持创新,鼓励内部创业 上一章,我们讲了虚拟化的原理。从一台物理机虚拟化出很多的虚拟机这种方式,一定程度上实现了资源创建的灵活性。但是你同时会发现,虚拟化的方式还是非常复杂的。这有点儿像,你去成立子公司,虽然说公司小,但毕竟是一些独立的公司,麻雀虽小,五脏俱全,因而就像上一章我们看到的那样,CPU、内存、网络、硬盘全部需要虚拟化,一个都不能偷懒。 那有没有一种更加灵活的方式,既可以隔离出一部分资源,专门用于某个进程,又不需要费劲周折的虚拟化这么多的硬件呢?毕竟最终我只想跑一个程序,而不是要一整个Linux系统。这就像在一家大公司搞创新,如果每一个创新项目都要成立一家子公司的话,那简直太麻烦了。一般方式是在公司内部成立一个独立的组织,分配独立的资源和人力,先做一段时间的内部创业。如果真的做成功了,再成立子公司也不迟。 在Linux操作系统中,有一项新的技术,称为容器,它就可以做到这一点。 容器的英文叫Container,Container的另一个意思是“集装箱”。其实容器就像船上的不同的集装箱装着不同的货物,有一定的隔离,但是隔离性又没有那么好,仅仅做简单的封装。当然封装也带来了好处,一个是打包,二是标准。 在没有集装箱的时代,假设我们要将货物从A运到B,中间要经过三个码头、换三次船。那么每次都要将货物卸下船来,弄得乱七八糟,然后还要再搬上船重新摆好。因此在没有集装箱的时候,每次换船,船员们都要在岸上待几天才能干完活。 有了尺寸全部都一样的集装箱以后,我们可以把所有的货物都打包在一起。每次换船的时候,把整个集装箱搬过去就行了,几个小时就能完成。船员换船时间大大缩短了。这是集装箱的“打包”和“标准”两大特点在生活中的应用。 其实容器的思想就是要变成软件交付的集装箱。那么容器如何对应用打包呢? 我们先来学习一下集装箱的打包过程。首先,我们得有个封闭的环境,将货物封装起来,让货物之间互不干扰,互相隔离,这样装货卸货才方便。 容器实现封闭的环境主要要靠两种技术,一种是看起来是隔离的技术,称为**namespace**(命名空间)。在每个namespace中的应用看到的,都是不同的 IP地址、用户空间、进程ID等。另一种是用起来是隔离的技术,称为**cgroup**(网络资源限制),即明明整台机器有很多的 CPU、内存,但是一个应用只能用其中的一部分。 有了这两项技术,就相当于我们焊好了集装箱。接下来的问题就是,如何“将这些集装箱标准化”,在哪艘船上都能运输。这里就要用到镜像了。 所谓**镜像**(Image),就是在你焊好集装箱的那一刻,将集装箱的状态保存下来。就像孙悟空说:“定!”,集装箱里的状态就被“定”在了那一刻,然后这一刻的状态会被保存成一系列文件。无论在哪里运行这个镜像,都能完整地还原当时的情况。 当程序员根据产品设计开发完毕之后,可以将代码连同运行环境打包成一个容器镜像。这个时候集装箱就焊好了。接下来,无论是在开发环境、测试环境,还是生产环境运行代码,都可以使用相同的镜像。就好像集装箱在开发、测试、生产这三个码头非常顺利地整体迁移,这样产品的发布和上线速度就加快了。 下面,我们就来体验一下这个Linux上的容器技术! 首先,我们要安装一个目前最主流的容器技术的实现Docker。假设我们的操作系统是CentOS,你可以参考[https://docs.docker.com/install/linux/docker-ce/centos/](https://docs.docker.com/install/linux/docker-ce/centos/)这个官方文档,进行安装。 第一步,删除原有版本的Docker。 ``` yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine ``` 第二步,安装依赖的包。 ``` yum install -y yum-utils \ device-mapper-persistent-data \ lvm2 ``` 第三步,安装Docker所属的库。 ``` yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo ``` 第四步,安装Docker。 ``` yum install docker-ce docker-ce-cli containerd.io ``` 第五步,启动Docker。 ``` systemctl start docker ``` Docker安装好之后,接下来我们就来运行一个容器。 就像上面我们讲过的,容器的运行需要一个镜像,这是我们集装箱封装的那个环境,在[https://hub.docker.com/](https://hub.docker.com/)上,你能找到你能想到的几乎所有环境。 最基础的环境就是操作系统。 咱们最初讲命令行的时候讲过,每种操作系统的命令行不太一样,就像黑话一样。有时候我们写一个脚本,需要基于某种类型的操作系统,例如,Ubuntu或者centOS。但是,Ubuntu或者centOS不同版本的命令也不一样,需要有一个环境尝试一下命令是否正确。 最常见的做法是有几种类型的操作系统,就弄几台物理机。当然这样一般人可玩不起,但是有了虚拟机就好一些了。你可以在你的笔记本电脑上创建多台虚拟机,但是这个时候又会有另一个苦恼,那就是,虚拟机往往需要比较大的内存,一般一台笔记本电脑上无法启动多台虚拟机,所以做起实验来要经常切换虚拟机,非常麻烦。现在有了容器,好了,我们可以在一台虚拟机上创建任意的操作系统环境了。 比方说,你可以在[https://hub.docker.com/](https://hub.docker.com/)上搜索Ubuntu。点开之后,找到Tags。镜像都有Tag,这是镜像制作者自己任意指定的,多用于表示这个镜像的版本号。 ![](https://static001.geekbang.org/resource/image/16/fb/160b839adb2bd7390c16c4591204befb.png) 如果仔细看这些Tags,我们会发现,哪怕非常老版本的Ubuntu,这里面都有,例如14.04。如果我们突然需要一个基于Ubuntu 14.04的命令,那就不需要费劲去寻找、安装一个这么老的虚拟机,只要根据命令下载这个镜像就可以了。 ``` # docker pull ubuntu:14.04 14.04: Pulling from library/ubuntu a7344f52cb74: Pull complete 515c9bb51536: Pull complete e1eabe0537eb: Pull complete 4701f1215c13: Pull complete Digest: sha256:2f7c79927b346e436cc14c92bd4e5bd778c3bd7037f35bc639ac1589a7acfa90 Status: Downloaded newer image for ubuntu:14.04 ``` 下载完毕之后,我们可以通过下面的命令查看镜像。 ``` # docker images REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu 14.04 2c5e00d77a67 2 months ago 188MB ``` 有了镜像,我们就可以通过下面的启动一个容器啦。 启动一个容器需要一个叫entrypoint的东西,也就是入口。一个容器启动起来之后,会从这个指令开始运行,并且只有这个指令在运行,容器才启动着。如果这个指令退出,整个容器就退出了。 因为我们想尝试命令,所以这里entrypoint要设置为bash。通过cat /etc/lsb-release,我们可以看出,这里面已经是一个老的Ubuntu 14.04的环境。 ``` # docker run -it --entrypoint bash ubuntu:14.04 root@0e35f3f1fbc5:/# cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=14.04 DISTRIB_CODENAME=trusty DISTRIB_DESCRIPTION="Ubuntu 14.04.6 LTS" ``` 如果我们想尝试centOS 6,也是没问题的。 ``` # docker pull centos:6 6: Pulling from library/centos ff50d722b382: Pull complete Digest: sha256:dec8f471302de43f4cfcf82f56d99a5227b5ea1aa6d02fa56344986e1f4610e7 Status: Downloaded newer image for centos:6 # docker images REPOSITORY TAG IMAGE ID CREATED SIZE ubuntu 14.04 2c5e00d77a67 2 months ago 188MB centos 6 d0957ffdf8a2 4 months ago 194MB # docker run -it --entrypoint bash centos:6 [root@af4c8d598bdf /]# cat /etc/redhat-release CentOS release 6.10 (Final) ``` 除了可以如此简单地创建一个操作系统环境,容器还有一个很酷的功能,就是镜像里面带应用。这样的话,应用就可以像集装箱一样,到处迁移,启动即可提供服务。而不用像虚拟机那样,要先有一个操作系统的环境,然后再在里面安装应用。 我们举一个最简单的应用的例子,也就是nginx。我们可以下载一个nginx的镜像,运行起来,里面就自带nginx了,并且直接可以访问了。 ``` # docker pull nginx Using default tag: latest latest: Pulling from library/nginx fc7181108d40: Pull complete d2e987ca2267: Pull complete 0b760b431b11: Pull complete Digest: sha256:48cbeee0cb0a3b5e885e36222f969e0a2f41819a68e07aeb6631ca7cb356fed1 Status: Downloaded newer image for nginx:latest # docker run -d -p 8080:80 nginx 73ff0c8bea6e169d1801afe807e909d4c84793962cba18dd022bfad9545ad488 # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 73ff0c8bea6e nginx "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp modest_payne # curl http://localhost:8080
If you see this page, the nginx web server is successfully installed and working. Further configuration is required.
For online documentation and support please refer to
nginx.org.
Commercial support is available at
nginx.com.
Thank you for using nginx.
``` 那我们如何将这些代码放到容器镜像里面呢?要通过Dockerfile,Dockerfile的格式应该包含下面的部分: * FROM 基础镜像 * RUN 运行过的所有命令 * COPY 拷贝到容器中的资源 * ENTRYPOINT 前台启动的命令或者脚本 按照上面说的格式,可以有下面的Dockerfile。 ``` FROM ubuntu:14.04 RUN echo "deb http://archive.ubuntu.com/ubuntu trusty main restricted universe multiverse" > /etc/apt/sources.list RUN echo "deb http://archive.ubuntu.com/ubuntu trusty-updates main restricted universe multiverse" >> /etc/apt/sources.list RUN apt-get -y update RUN apt-get -y install nginx COPY test.html /usr/share/nginx/html/test.html ENTRYPOINT nginx -g "daemon off;" ``` 将代码、Dockerfile、脚本,放在一个文件夹下,以上面的Dockerfile为例子。 ``` [nginx]# ls Dockerfile test.html ``` 现在我们编译这个Dockerfile。 ``` docker build -f Dockerfile -t testnginx:1 . ``` 编译过后,我们就有了一个新的镜像。 ``` # docker images REPOSITORY TAG IMAGE ID CREATED SIZE testnginx 1 3b0e5da1a384 11 seconds ago 221MB nginx latest f68d6e55e065 13 days ago 109MB ubuntu 14.04 2c5e00d77a67 2 months ago 188MB centos 6 d0957ffdf8a2 4 months ago 194MB ``` 接下来,我们就可以运行这个新的镜像。 ``` # docker run -d -p 8081:80 testnginx:1 f604f0e34bc263bc32ba683d97a1db2a65de42ab052da16df3c7811ad07f0dc3 # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f604f0e34bc2 testnginx:1 "/bin/sh -c 'nginx -…" 2 seconds ago Up 2 seconds 0.0.0.0:8081->80/tcp youthful_torvalds 73ff0c8bea6e nginx "nginx -g 'daemon of…" 33 minutes ago Up 33 minutes 0.0.0.0:8080->80/tcp modest_payne ``` 我们再来访问我们在nginx里面写的代码。 ``` [root@deployer nginx]# curl http://localhost:8081/test.html