Docker是近来很热的一个概念.
什么是容器?
提到Docker, 先要说说它的上级概念, 就是容器. 以下内容摘自百度百科:
容器技术已经成为一种被大家广泛认可的服务器资源共享方式,容器技术可以在按需构建操作系统实例的过程当中为系统管理员提供极大的灵活性。
我个人认为最好懂的理解是, 容器就是一个简化版的虚拟机. 如果本身有一定的虚拟机操作经验, 再看看docker中这些流程, 就非常好理解了.
应用场景
比如我现在有个需求, 要在物理机上部署一台VPN服务器. 使用虚拟机的操作流程如下:
- 在物理机上开一台虚拟机.
- 使用编写好脚本去安装VPN服务端程序.
- 配置物理机的端口转发规则.
以上流程并不复杂. 但是如果要部署100个VPN服务器并在前端挂负载均衡呢? 其实问题也不复杂, 配置好一台虚拟机后, 直接复制99台出来就可以.
不过, 问题也来了:
- 资源开销大. 集群规模增长到100台之后, 虚拟机本身资源消耗就变得不能忽视了, 物理机未必能开出100台虚拟机.
- 备份成本高. 备份的时候, 通常要备份整个虚拟机, 而虚拟机镜像本身都很大, 在GB级别.
- 不够灵活. 对单一不变的需求, 虚拟机复制是方便的. 如果其中50台跑IKEV2, 50台跑PPTP, 这50台中有25台还要跑L2TP, 就非常麻烦了.
而改用容器之后, 则变为如下流程:
- 在物理机上装好docker.
- 打包好VPN服务器的镜像.
- 用docker启动100个容器, 装载打包好的镜像.
容器能够很好的解决之前提到的虚拟机方式的缺点:
- 极轻量, 一台机器很难开100个虚拟机, 但是几百个容器通常都没有什么问题.
- 镜像大小通常在MB级别.
- 非常灵活. 有多少个服务, 开多少个容器.
如何使用docker?
需求
现在, 假设需要部署一台运行node.js的ubuntu服务器, 程序入口在main.js里.
虚拟机部署流程
仍然以传统的虚拟机为例, 部署流程如下:
- 安装ubuntu操作系统
apt-get install nodejs
node main.js
从自动化部署的角度考虑, 通常会将这些东西写成脚本, 直接用scp把main.js传输到服务器上, 并执行.
docker部署流程
创建dockerfile
使用docker, 你唯一需要准备的, 就是dockerfile. 这个dockerfile非常类似于上面虚拟机流程中的部署脚本.
以这个需求举例, dockerfile如下:
FROM ubuntu:latest
RUN apt-get install nodejs
WORKDIR /home
COPY ./main.js /home
EXPOSE 8000
ENV NODE_ENV production
CMD ["node", "main.js"]
逐行解释这个脚本.
FROM ubuntu:latest
: 可以理解为, 我现在要装一个ubuntu的操作系统进去.RUN apt-get install nodejs
: RUN为执行命令的指令, 即在刚刚装好系统的容器里面, 安装nodejsWORKDIR /home
设置工作目录到/home下. 之后所有相对路径将以此为参考.COPY ./main.js /home
将物理机的main.js拷贝到容器内的/home目录下.EXPOSE 8000
该镜像占用8000端口。这句话其实只有注释的作用,它并不会在镜像执行时自动把容器8000端口打开。在实际运行的时候需要去指定是否暴露到公网,或是仅在容器内访问。ENV NODE_ENV production
设置环境变量NODE_ENV为production.CMD ["node", "./main.js"]
设置容器在启动的时候需要执行的命令.
docker build
光有这个dockerfile, 还是没法完成部署工作的.
好比你现在有一台空的VM和一个部署脚本, 你得在VM上执行这个部署脚本, 装系统, 设环境变量, 装程序之类的, 服务器才能完成部署. 部署完了以后, 导出当前状态虚拟机的镜像文件, 才能复制出别的虚拟机.
这里的dockerfile就像部署脚本一样, 需要有个执行的操作. 执行完了之后, 也需要把这个超轻量的虚拟机的状态保存成镜像. 镜像文件, 才是最终部署时需要用到的直接文件.
而这个执行并导出镜像的操作, 就是docker build
.
具体操作如下:
docker build -t nodeapp -f "dockerfile" .
-t
参数指定了该镜像的标签. 对同一个物理机来说, 标签永远指向最近的那个镜像, 之前的同名标签会自动删除(但镜像仍然存在, 只是你不能用标签去访问了).-f
参数指定了要使用的dockerfile文件名. 如果不指定名字, 默认是Dockerfile.
最后的那个.
是指定当前目录作为物理机的工作目录. 如果在dockerfile里有用到物理机的相对路径时, 会以此为参考.(如COPY ./main.js /home
)
关于标签的选取, 在这里多说几句, 是有讲究的. 通常每个程序版本的发布对应一个镜像以方便服务端部署. 如果这里的标签随便编写, 或者只是打个临时的tag, 用完下次再覆盖, 有两个问题:
- 版本一旦多了之后,
docker images
里面一堆只有id没有标签的镜像, 谁都不知道它们是干嘛的. - 导出镜像后, 服务端再导入此镜像是, 得到的依然是空标签的镜像, 即便它在本机上是刚打好标签的.
这个问题docker解决很方便, build的时候按照${APP_NAME}:${APP_VERSION}
的格式书写, 如:
docker build -t myapp:0.0.1 -f ".dockerfile" .
效果如下图所示:
build结束后, 执行docker images
, 可以发现REPOSITORY / TAG下面已经是指定好的值了, 很方便.
docker run
为方便文件传输, 还需要进行镜像导出操作, 命令是:
docker save IMAGE_ID > deploy.tar
IMAGE_ID里填入刚生成的镜像id, deploy.tar是你需要保存的镜像文件的名字.
对应的导入命令则是:
docker load -i deploy.tar
将镜像文件传输到远程服务器并导入后, 就可以用docker run
来启动服务了.命令如下:
docker run -p 8000:8000 IMAGE_ID
-p 8000:8000
意为将物理机的8000端口映射到容器的8000端口.- IMAGE_ID是导入镜像之后的镜像id.
高级一点的玩法还可以这样:
docker run -p 8000:8000 -i -t IMAGE_ID
新增加的-i -t
参数会创建一个伪命令行终端, 你可以连接到这个轻量级的虚拟机上, 并直接查看容器内的一举一动.
也可以让容器共享宿主机的目录:
docker run -v /home/app:/home/app_in_docker IMAGE_ID
此时在容器内可以直接访问到宿主机的/home/app
目录.
总结
再回到之前的定义, 我说容器就是一个简化版的虚拟机.
docker build
命令可以理解为: 我要新建一个容器(虚拟机)模板, 这个容器(虚拟机)里装了什么操作系统和哪些软件都写在了dockerfile里, 请开一个临时的容器(虚拟机)把这些系统和软件都装进去, 然后把模板镜像保存好就可以了. 镜像生成完了以后, 这个容器(虚拟机)就可以关掉了.
docker run IMAGE_ID
命令可以理解为: 我要新开一个容器(虚拟机), 这个容器(虚拟机)要从IMAGE_ID这个镜像中复制出来并运行.
改进
镜像瘦身
使用精简版镜像
本文中的例子是基于官方镜像的, 其实对于特定的应用, dockerhub上有大量的第三方镜像可以使用. 比如只需要跑nodejs的话, 可以用mhart alpine node这个镜像. 这个镜像不带npm版本的通常只有二十兆出头.
其他
一些其他的技巧, 比如合并dockerfile中的操作步骤等等, 就不一一赘述了, 可以搜索网络上现成的资料.
容器的不足
容器本身的轻量是有代价的. 对比虚拟机, 其主要区别体现在资源的隔离能力上, 如CPU, 内存, 磁盘等. 虚拟机内的内存泄漏通常会导致这个虚拟机挂掉或是失去响应, 但是物理机是不会受到影响的. 而容器内的应用程序如果出现了内存泄漏, 在不加限制的情况下, 是有可能危及物理机的.
从整体来看, 容器和虚拟机各有利弊. 抛开应用场景光谈性能是一件扯淡的事情. 应当结合具体的实际情况, 去选择合适的方案.