Docker例子&核心原理

本文会介绍一个Docker启动MySQL实例的例子,借此来简单说明下Docker的核心原理。

1. Docker基本用法

下面来看一个基本用法的例子,对于例子中所讲的daemon、容器、镜像等术语我们会在下文进行说明。我们的例子是需要启动一个运行MySQL的容器。命令如下:

#启动docker daemon:
[root@dev ~]# service docker restart

#查看目前所拥有的镜像:
[root@dev ~]# docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
libnetwork-build                 latest              c99f5cf5eb23        12 days ago         587.5 MB
docker.io/golang                 1.4                 124e2127157f        2 weeks ago         517.2 MB
docker.io/berngp/docker-zabbix   latest              ad689c775bbf        6 weeks ago         1.134 GB
docker.io/centos                 7                   7322fbe74aa5        6 weeks ago         172.2 MB
docker.io/centos                 latest              7322fbe74aa5        6 weeks ago         172.2 MB

#下载一个MySQL镜像,其源托管在https://registry.hub.docker.com/_/mysql/上:
[root@dev ~]# docker pull mysql
latest: Pulling from docker.io/mysql
4c8cbfd2973e: Downloading [=>                                                 ] 1.129 MB/37.21 MB
60c52dbe9d91: Download complete 
4c8cbfd2973e: Pull complete 
60c52dbe9d91: Pull complete 
c2b0136be90f: Pull complete 
273cd71eacf0: Pull complete 
543ff72402d8: Pull complete 
aa3022270c68: Pull complete 
39130042665d: Pull complete 
2e4d19227c16: Pull complete 
1f877cc70688: Pull complete 
7e6d170eec04: Pull complete 
07264b223269: Pull complete 
b95dfc449f80: Pull complete 
45d84ed3b24d: Pull complete 
bcf7334ef42a: Pull complete 
a128139aadf2: Already exists 
docker.io/mysql:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security.
Digest: sha256:3e633be4546d5549c35f9731280e7c7ef0a272c23dfbe241e08a655920c2ffa1
Status: Downloaded newer image for docker.io/mysql:latest

#从上面给出的连接中可以看出这个镜像提供了一些参数可以在启动的时候使用,包括:MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD等等。这里我们使用MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD这三个操作来让我们的镜像启动的时候自动建立一个数据库并建立有权限操作该数据库的相关用户。同时我们映射启动后容器中的3306端口对应物理机的6033端口:
[root@dev ~]# docker run --name NeutronMySQL -p 6033:3306 -e MYSQL_USER=neutron -e MYSQL_PASSWORD=password -e MYSQL_DATABASE=neutron -e MYSQL_ROOT_PASSWORD=password docker.io/mysql
Running mysql_install_db
2015-08-02 15:06:58 0 [Note] /usr/sbin/mysqld (mysqld 5.6.26) starting as process 33 ...
2015-08-02 15:06:58 33 [Note] InnoDB: Using atomics to ref count buffer pool pages
2015-08-02 15:06:58 33 [Note] InnoDB: The InnoDB memory heap is disabled
2015-08-02 15:06:58 33 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins
2015-08-02 15:06:58 33 [Note] InnoDB: Memory barrier is not used
......

#现在我们在物理机上连接一下我们的neutron数据库试试:
[root@dev ~]# mysql -uneutron -ppassword -P6033 -h127.0.0.1 neutron
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.6.26 MySQL Community Server (GPL)

Copyright (c) 2000, 2014, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [neutron]> 

可以看到通过docker run这么一个命令我们就运行了一个完整的数据库,并且这个运行的时候可以指定某些参数的值,这一点如果要通过虚拟机来实现那么是比较麻烦的。

2. Docker实现原理

上面看过一个例子后,这里我们来讲一下Docker的实现原理。Docker的实现依赖两个东西:

1. namespace
2. CGroup

namespace在前面我们讲过,主要用于提供名字空间的隔离。我们之前分析过网络namespace的实现,因此这里就不多介绍了。CGroup的作用大家可以参考这个连接的介绍:

* http://coolshell.cn/articles/17049.html

简单的说,CGroup的作用就是限制某个进程可以使用的系统资源。比如限制一个进程只可以使用1个G的内存,或者限制这个进程的IO速率等等。

当我们运行docker XXX命令的时候,大部分命令发送给了一个叫做daemon的进程。这个daemon进程的启动、停止方式也是通过docker命令完成的:

# 启动daemon
[root@dev ~]# docker -d

接下来当我们执行如docker ps这种查看当前有哪些正在运行的容器的命令的时候,docker ps会的转成一个满足RESTful要求的HTTP请求,然后这个HTTP请求会通过TCP或者本地套接字的方式发送给我们的daemon进程。daemon守护进程进行相关操作后会返回结果给我们运行docker ps的客户端,接着docker ps会的输出结果。也就是说docker其实就是在我们的本机启动了一个server,然后docker ps这类命令就是和我们的server进行普通的交互,且这种交互是基于HTTP的请求。

现在来解释下什么是容器。在上面的MySQL例子执行后,我们在系统中可以看到如下进程:

[root@dev ~]# ps -elf | grep NeutronMySQL
4 S root      2535  2204  0  80   0 - 51809 ep_pol 23:06 pts/0    00:00:00 docker run --name NeutronMySQL -p 6033:3306 -e MYSQL_USER=neutron -e MYSQL_PASSWORD=password -e MYSQL_DATABASE=neutron -e MYSQL_ROOT_PASSWORD=password docker.io/mysql

对于Docker来说,一个容器其实就是一个进程。这个进程运行在自己独立的namespace下面,所以这个pid为2535进程其看到的namespace都是独立的。当我们执行docker run命令后,docker首先会启动一个进程,然后设置这个进程使用新的namespace(如果对namespace不熟悉的话,建议去看下小秦博客中namespace的相关知识)。接着会在这个namespace中运行预定义的一些指令,比如启动一个MySQL进程,然后建立一个数据库等等。可以看到这个进程之所以能和我们的物理机隔离的关键正是在于namespace。那么CGroup有什么用呢?我们的例子里并没有限制我们的容器(也就是我们的进程)能使用多少系统资源,加入我希望我的MySQL容器只能使用10个G的系统内存,那么这里可以通过CGroup实现资源限制。

接着我们来解释下镜像。上面说了容器其实就是一个进程,生活在自己独立的namespace中。那么这个容器进程所看到的文件系统是什么呢?这个容器进程所看到的文件系统是DOcker通过chroot来实现的。chroot是Linux很早就提供的一个命令,用于修改某个进程所看到的根文件系统路径。比如在进程A的执行代码中执行类似chroot /tmp/a的命令,则在此之后A执行ls /命令看到的就是/tmp/a下面的内容了。因此在Docker中每个容器都是通过chroot命令获取自己独立的文件系统的。但是如果每次启动容器都需要安装MySQL等等才能使用的话会很麻烦,所以有人会事先建立好类似/tmp/a之类的目录,在这个目录下会建立如/tmp/a/bin/mysqld、/tmp/a/etc/my.cnf之类的文件并进行配置,接着容器启动的时候设置/tmp/a为根目录后,其就能执行mysqld命令,并且获取到预先设置的my.cnf配置文件了。这样的一个/tmp/a所打包生成的一个文件其实就是一个镜像的雏形。那么实际上的镜像和这个雏形有什么区别呢?如果按照我们刚刚说的,每个容器启动后都需要chroot自己的根文件系统,则如果有100个容器则会有100个类似于/tmp/a的目录存在,这样对于资源的利用以及容器的启动速度(也就是这个进程的初始化速度)是很不利的。因为类似于mysqld这样的文件大家都是公用的,完全没有必要在/tmp/a/bin下有一份,同时在/tmp/b/bin下也有一份。为了解决这个问题Docker使用了支持多层次的文件系统,比如AUFS或者Device Mapper。他们的作用简单的说就是对于容器A,启动的时候系统不会从/tmp/mysql复制一个完整的目录到/tmp/a下,而是建立一个空的/tmp/A。然后通过AUFS或Device Mapper的技术挂载出一个/tmp/a目录,对该目录的所有读操作会先在/tmp/A下进行,如果/tmp/A下没有找到对应的文件则会去/tmp/mysql下读取。对于该目录的所有写操作则都只会发生在/tmp/A。比如用户修改了my.cnf,则实际上的操作是从/tmp/mysql/etc/my.cnf复制一份到/tmp/A/etc/my.cnf,然后再对/tmp/A/etc/my.cnf进行修改。Docker在启动容器的时候,会先设置好这些,然后再chroot /tmp/a为根目录。可以看到通过这种多层次的文件系统用户可以对底层的/tmp/mysql进行定制后再提供服务给其他人使用,比如用户在进行上面的操作后,安装了一个Apache+PHP在容器中,此时我们的/tmp/mysql + /tmp/A合并而成的/tmp/a就能提供一个完整的LAMP服务了。于是可以将/tmp/mysql + /tmp/A一起打包成一个文件提供出去。当某个用户需要一个LAMP的容器B在上面运行一个wordpress应用的时候,其解压这个文件,然后Docker建立空的/tmp/B,并通过上面说的AUFS或Device Mapper将/tmp/mysql、/tmp/A、/tmp/B挂载为/tmp/b,然后启动的新容器chroot到/tmp/b后就能直接使用LAMP环境了。这里说到的/tmp/mysql加上/tmp/A以及一些元信息所共同打包二层的一个文件就是我们所说的镜像。可以看到镜像是分层的。

以上这些就是Docker的实现原理,说实话从原理上看真的很简单,因为Docker使用的技术都是一些现有的技术。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*