近期公司的服务打算在容器这条路上做一些尝试,我也趁机试一下容器的水。这篇文章主要是对容器技术的一些概念做个简单总结。首先我会把容器技术与传统的虚拟化技术做个简单的比较,然后再从编程的角度来理解一些容器的相关概念。

1. 容器与虚拟机

Docker-vs..png

a. 传统的虚拟化技术

在传统的虚拟化技术里面,如果想要充分地利用主机资源,人们则需要以虚拟机的方式划分出部分的资源,然后在这部分被划分出的的硬件资源上安装一个虚拟的操作系统。而这个操作系统,与我们平日里在物理机中使用的操作系统并无二致。

我们可以简单地把这种技术理解成,把宿主主机上的剩余的硬件资源以某种方式“切割”出来,进而形成一台台小型的“物理机”。接着我们可以在这台新的“物理机”上安装操作系统,这台被分割的机器就会被称之为虚拟机。这种技术支持下可选择的操作系统会比较多,如各种发行版的Linux,Windows,甚至MacOS。

好处:机器管理起来比较便捷,就相当于多了几台真实的主机,操作系统的选择比较多样化,如果是Linux的话还可直接通过ssh进行远程管理。 弊端:资源的划分的方式不够灵活,能够被划分出来的机器数量非常有限。

b. 基于容器的虚拟化技术

而像Docker这种基于容器的虚拟化技术,利用起资源来似乎更加灵活些

如果说传统的虚拟化技术是以虚拟机作为基本单元,那么容器化技术则是以容器来作为基本的虚拟单元了。容器在我看来它就是一个极度精简版的Linux,而容器里除了你预先设定好的软件之外(通过镜像)几乎什么都没有。为了证明这一点我可以通过以下命令来进入一个正在运行的容器里看看里面都有些什么。

以Redis的镜像所创建的容器为例(假设容器已经在后台运行中)。

// 进入容器
> docker exec -it [redis容器ID] sh

// 默认进入了 `/data` 目录,先切换到根目录
/data # cd /

// 列出根目录的内容
/ # ls
bin    data   dev    etc    home   lib    media  mnt    proc   root   run    sbin   srv    sys    tmp    usr    var

可见这就是一个不折不扣的Linux操作系统。多运行几条命令之后你会发现该容器里似乎只有一些基础的文件系统管理命令,某些镜像甚至连编辑器都没有,如果我们需要修改镜像中的文件,则通过apt-get(一般是Debian系的操作系统)命令进行安装。

好处:划分资源比较更为灵活,容器较为轻量创建起来比较快捷,生产环境下会有比较好的市场。 弊端:操作系统的选择并没有虚拟机那么丰富,目前以Linux为主。系统里面包含的软件非常有限,临时需要一些软件还需自己手动安装,否则要修改镜像。但这种修改容器的行为比较脆弱,属于改变容器自身的状态,一旦容器被销毁则所有修改都会丢失。并且容器的管理需要配合docker的相关命令,管理起来相对麻烦一些。

镜像

装过操作系统的人都知道,我们一般会借助一种叫做系统镜像的东西来安装对应的操作系统。一个镜像包含了所需要安装操作系统的所有信息,通过使用同一个镜像,我们可以在不同的机器上安装完全一样的操作系统。所安装的操作系统会根据用户的偏好设置的不同而有不同的行为,容器中的镜像也与此类似。从编程的角度来看,镜像则有点像是面向对象语言中的类(或者原型)。

class VirtualTech
  attr_accessor :name
  def initialize(name)
    self.name = name
  end
end

c = VirtualTech.new('Docker')
puts c.name
# => Docker

c = VirtualTech.new('Virtual Box')
puts c.name
# => Virtual Box

一个类提供了一个基础的“样板”,用户可以根据自己的喜好传入不同的参数,借此产生各不相同的实例。在平日里写代码的时候,我们会根据业务需求的不同而抽象出不同的类。而在Docker中我们则是对基础环境进行了抽象,进而形成了各种各样不同的镜像。

实践过程中通常会使用Dockerfile来定义基础环境。Docker的官方仓库里面包含了许多常用的镜像,原则上都是以一个操作系统为起点,然后再在操作系统上安装所需要的依赖软件。假设我只需要一个包含Vim编辑器的镜像则Dockerfile可以这样写

FROM ubuntu
RUN apt-get update && apt-get install -y vim

接着在Dockerfile所在目录下进行构建即可

docker build -t vim:hello .

我把镜像命名为vim并给它打上hello标签。整个过程感觉就像是先安装了一个极端精简的操作系统,然后再在上面安装vim编辑器。

> docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
vim                 hello               5d2690ced0fd        6 seconds ago       170MB

容器

镜像会包含容器中所需要的基本信息,我们可以通过指定镜像来创建出许多容器。首先通过以下命令来查看当前服务中容器的运行情况

> docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS

现在一个容器也没有,接下来我们基于一个打了alpine标签的redis镜像来启动一个容器,命令也很简单

docker run redis:alpine

完成之后再次运行ls命令,则可以看到有一个容器正在运行,并占用着6379这个端口

> docker container ls
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
f9eb72b8acd9        redis:alpine        "docker-entrypoint.s…"   2 seconds ago       Up 1 second         6379/tcp            serene_kowalevski

当然并不是所有容器都会以服务的方式运行,前面的例子那个只有Vim编辑器容器创建之后就马上停止运行了。但是停止运行并不代表容器已经销毁了,他们只是在后台“休眠”。如同编程的时候我们可以通过类创建出许多实例,但是并不是所有实例你都会用得上,总会有那么些实例一旦创建了就等待着销毁。不过Docker似乎没有垃圾回收机制,如果有需要我们可以通过以下命令来批量删除一些没有启动的容器。

docker container prune

总结

镜像本身是不能改变的,如果你想要对镜像做出调整唯一的方式就是修改Dockerfile然后重新构建一个新的镜像。创建容器需要依赖镜像,我们可以通过不同的偏好设置来定制容器的特殊行为,比如可以以特定参数来暴露容器的端口号让容器服务可以与外界交互,让容器以daemon的形式在后台运行,指定容器数据卷等等。

一般来说容器本身都有自己的状态,Docker的容器提供了可写访问层,我们可以动态地修改容器的相关信息(比如可以通过bash,sh登入容器内部,对容器的配置进行各种修改)。然而这种在容器本身进行的修改是很脆弱的,虽说容器的重启并不会使相关的修改失效,但是一旦容器被销毁则之前的修改就完全作废了。就像是编程里的实例一旦被垃圾回收,本身的信息也就丢失了。

为了解决这种配置丢失的问题,最好的方式就是让配置数据持久化。简单地来说我们可以在容器之外维护一份配置文件,每次容器启动的时候都读取这份文件作为自己的默认配置。前几日还听运维的小伙伴说K8S集群是用etcd来存储这些配置以达到共享的目的,在此稍稍惊叹Go的生态。

这篇文章主要针对自己对容器技术的理解做个简单总结,然而它只是容器生态的冰山一角而已。容器技术还会涉及数据卷,网络等概念。官方网站上面会建议我们对开发,生产环境进行容器化,个人也对两方面都做了尝试,说实在的生成环境容器化乃是大势所趋,个人也比较赞同,搭配上平台之后应用的部署,扩容,CI这些事情做起来都会比较方便。然而开发环境的容器化个人觉得必要性不是太大(当然你有特殊需求就另当别论),毕竟本来简单就能启动的项目,加入Docker使其容器化则显得有点过了。