Docker简述
Docker是一种OS虚拟化技术,是一个开源的应用容器引擎。它可以让开发者将应用打包到一个可移植的容器中,并且该容器可以运行在几乎所有linux系统中(Windows10目前也原生支持,Win10前需要内置虚拟机),正所谓“一次打包,到处运行”。
Docker容器的运行是完全的沙箱机制,相互之间不会有任何关联(除非自己串联集群)。网络、存储、进程等资源,不仅对于不同的容器是相互隔离,对于宿主机和容器直接也是隔离的,除非你手动映射暴露端口或者挂载存储卷。
很多人不理解,Docker和虚拟机到底有什么区别。
从这两张结构图来看,Docker比虚拟机少了一层虚拟机操作系统,Docker的应用直接Docker引擎上运行。由于虚拟机需要一层操作系统,所以会导致虚拟机的体积非常大,通常在几G到十几G之间。并且通常一个虚拟机上,不只一个应用,因此对于整体的虚拟集群管理并不太友好,比较难做到灵活分配。
而一个Docker镜像的体积大约在几十M到几百M之间,一般一个镜像只打包一个应用,由多个镜像组成一个完整的项目,并且镜像易于复制,可以跨平台运行,这样可以使项目的部署管理有更好的灵活性。所以Docker无论从资源消耗上、管理上、使用上都在虚拟机之上,因此我们又有何理由不使用这样的容器化技术呢?
对于容器化技术的学习,可谓是深如海。从基本的镜像、容器操作,到镜像的打包、容器的部署,再到企业生产级的容器集群管理技术(Docker官方的Swarm、Google的Kubernetes),如此多的内容,并不是所有人技术人员都能一朝学会。不过除了生产级别的集群管理技术有难度意外,其他内容从学习使用的角度来说,其实是非常简单的,况且K8s这种东西,对于普通开发来说也是很少能接触到。
说到这里,可能还有很多人觉得这个是公司层面、运维层面的操作,不是很了解Docker对于普通开发来说,意味着什么,对我们有什么好处?
- 多办公环境,一键部署。假如你在公司一套开发环境,在家一套开发环境,当你公司的开发环境变更时,在家的环境就要跟着变,如果是使用Docker,将一些依赖型的应用,如Redis、ZK、Mysql等边缘服务都打包在docker里面。无论你在哪里改变了内容,只要在运行时更新下镜像,就可以按照最新的内容去执行了,不需要一个手动去安装,适配。
- 联调测试,无需依赖他人。当后端完成对外的接口后,将后端应用打包进docker,这样无论是前端、测试,在何地何时都可以自己把容器启动起来进行联调测试,而不需要自己手动一步步地搭建这个后端环境。
- ...
下面就来一步步讲解下,普通开发所需要的Docker知识。
概念介绍
学习Docker首先要了解下几个基础概念:
- 宿主机,Host,运行Docker所在的物理机,是Docker运行的系统环境。
- 镜像,Image,相当于一个程序模板,通过这个模板可以生成很多个相似的容器。可以理解为Java中的类,它本身不具备执行运行的能力,是一个对象抽象的模板。每个镜像可以有多个版本,用tag来区分。镜像可以通过Dockerfile来构建。
- 容器,Container,Docker运行的最小单位对象。它是通过镜像实例化出来的一个可运行对象。容器的修改,可以提交反作用于镜像,更新这个容器的模板。
- 仓库,Repository,用于存储管理镜像的仓库,类似于git管理代码的仓库一样,可以管理多版本的镜像。
镜像、容器、仓库的关系如下:
一句话总结就是,从仓库中拉取镜像,利用镜像生成容器。
基本操作
了解完Docker的基本概念,我们开始来开始学习下入门操作。此处省略所有的Docker安装过程,自己去官网下载就行了,基本是傻瓜式安装。
拉取镜像
通过docker pull ${image_uri}:${image_tag}
命令,可以从远程仓库(默认是Docker Hub)中拉取所需要的镜像。
在Docker Hub的网站上可以搜索下自己需要的镜像以及版本。例如Ubuntu,上面提供了几个版本。
我们拉一下16.04版本的ubuntu镜像。然后通过
docker images
命令,查看保存在本地镜像,发现多了一个ubuntu的镜像。
容器创建、启动、停止、登入
有了镜像以后,就可以通过docker run -it ${image_id}
创建启动一个容器了。
image_id
是镜像的id,通过docker images
能查看到,也可以是镜像名(REPOSITORY:TAG)。
-it
可以让你在启动后,连上容器的终端。连上终端后,就可以在里面随意操作容器里面的内容了。
exit
退出容器后,容器就会自动停止了。但是这个容器依然还存在,只是”关机“了。(可以通过ctrl+p,ctrl+q
,退出容器登入,而不关闭容器)
通过docker ps -a
可以看到我们的容器已经Exited
了。
通过docker start ${container_id}
,我们把这个容器再次启动。通过docker ps
(加上-a包含显示未启动的容器),可以看到容器的状态为UP。
同理,我们可以通过docker stop ${container_id}
来停止容器,
在用docker start
命令的时候,如果不加上-a参数,默认不会连接上容器的。不过我们可以在start后,通过docker attach ${container_id}
来登入容器。
通过以上的基本操作,你基本可以利用docker当作一个虚拟机来使用了。如果想把容器和虚拟机的网络、存储打通,可以网上搜下了解下网络与卷挂载等容器设置。
更新镜像
在上面的例子中,我们pull下来的仅仅是一个ubuntu的原始镜像,并没有过多的内容。下面我们在这个镜像的容器里面,安装一个jdk。
这样我们的容器里面就有一个jdk了,但是如果我们再用这个ubuntu原始镜像再创建一个容器,它是不会用这个jdk的。所以我们就需要把这个容器的内容,提交到镜像当中。
通过docker commit ${container_id} ${repository}:${tag}
,在本地将容器内容提交到镜像当中。然后就可以拥有一个带jdk的ubuntu镜像了。
后面我们就可以利用这个镜像,生成带jdk的容器了。
以上的更新仅限于在本地的镜像,如果想把容器推送到云端就需要用docker push
命令。前提是你已经登录了仓库拥有权限。
镜像仓库
上面提到,默认情况下,仓库是用Docker Hub。我们pull 和push都是在Docker hub上操作,但是如果镜像是内部私有使用的话,没有必要去使用Docker Hub,一个是网络慢,另一个是私有安全性问题。
针对以上问题,有两种解决方法,一个是自己搭建私有服务,另一个是用云服务的镜像管理平台(如阿里云的“容器镜像服务”)。前者对于一般开发者来说并没有必要,而且还要搞认证的,比较麻烦,这里不细说。下面介绍下如何用阿里云服务作为自己的私有仓库。
先在阿里云上创建一个镜像仓库,获得一个仓库地址,如registry.cn-shenzhen.aliyuncs.com/zackku/jdk。这里一个仓库地址,对应一种镜像(tag不同)。
利用docker login
,先对阿里云的服务进行登录。
然后对上面的jdk镜像打tag(其实也是改仓库源的过程)
最后把镜像推送到阿里云就行了。
推送后,就能在阿里云的仓库上看到这个镜像。
通过搭建私有仓库,我们就可以完全抛开宿主机的环境,构建好一个镜像,就可以到处运行了。
Dockerfile构建镜像
从上面介绍,我们已经了解到,如何从拉取一个镜像、修改容器内容、提交镜像去构建一个我们所需要的镜像。但通过这些操作去构建一个镜像,一个是太繁琐,另一个问题是不清晰,没办法直观的了解镜像的构成。
Dockerfile就可以很好的解决该问题。它可以通过编写一个构建过程,来一站式构建镜像。下面同样以ubuntu为基础镜像,安装jdk构建一个新镜像为例,看看Dockerfile是怎么写的。
然后执行docker build -t registry.cn-shenzhen.aliyuncs.com/zackku/jdk2:1.0 .
就能把镜像构建出来了。
Dockerfile高级技巧
上面是Dockerfile的基本使用,但实际情况下我们并不像(或者说不仅是)上面描述那样去构建镜像。下面介绍两个常用的使用原则。
分层构建。其实Docker的镜像是分层结构的,看回之前推送到远端仓库的例子。
红框里面就是镜像一层层的提交,如果这层已经本地构建过了,下次不需要构建了,同理如果远端已经有这层了,也不需要推送这层。而且这种分层是可以在不同镜像间共享的,例如不同的Java项目都是依赖于JDK的运行环境,那么它们就可以共用JDK这层镜像内容。
所以,基于这样的特性,我们就应该要分层去构建镜像,抽象镜像共同点。具体操作的话,我们大致可以去分两次构建镜像,先构建一个base镜像,用于不同镜像的底层,例如Java项目的所有基础运行环境,然后再通过base镜像,构建develop表层的应用镜像。相当于把应用程序打包丢到develop层里面。并且这层要告诉Docker是怎么运行程序的。
尽量构建小的base层。镜像的体积也是在使用Docker的时候要考虑的一个重要因素,因为如果镜像的体积过大,在更新镜像,拉取镜像的时候效率会低。尤其在刚刚所说的base层里面,如果base层做得太大太臃肿,里面程序过多,不仅仅体积大,还会让CPU、网络等资源消耗过大。其实我们在用Docker的时候,一般是一个容器只包含一个程序项目,关于这个程序的监控、健康等内容,在容器外通过k8s等集群管理去做,所以容器本身只需要保证自己的程序能够运行起来就行了。
至于上面我用ubuntu作为基础的操作系统是比较多余的,这里推荐只用apline操作系统作为程序的最底层镜像,它是一款轻型的Linux发行版,系统体积与运行时的资源消耗都相当低,十分适合用于Docker容器。基于apline的操作系统,我们在上面添加自己所需要的环境,例如安装一个Tomcat、JDK等,从而构建一个base的镜像。
上所说的base镜像,其实不太需要自己的写一层Dockfile,docker官方就直接提供了各种语言、环境的基础镜像,在github的docker-library里面。如果再有自己的团队的运行环境的要求,可以在这个Dockerfile基础上去添加修改即可,或者再抽象多一层。
至于Dockfile怎么写,语法是什么,网上有大把详细的说明,由于篇幅问题,不在这里展开。
docker-compose启动集群
前面已经介绍完一个单独的容器是如何构建与启动的了,但我们的项目往往不是只有一个容器的,把所有程序打包在一个容器不是正确的做法。所以我们怎么去管理启动这么多的容器,是一个必修的课题。在企业级的层面,有K8S,Swarm这种容器编排的管理工具,但稍微比较复杂,个人使用的话也没有太大必要。
这里推荐用Docker官方的docker-compose,它可以把所有的容器编排方式写在一个文件里,然后通过docker-compose up
命令,就可以把一套的容器按照你的编排全部启动起来。
在这个例子的services包含每个容器的配置,其中的redis、mongodb用的是默认的镜像、默认的配置,myproject是我们自己的项目。通过这样的编排,我们就能让我们的项目连上redis和mongodb。最后通过docker-compose up
就会自动拉取镜像,按照编排跑起来了。
具体的语法也不赘述,关键就是容器的卷挂载,网络的配置,端口的暴露,容器的依赖关系。如果把这套东西用起来,慢慢自然就会了解,重要的是动手去做一遍,尝试一下。
更多技术文章、精彩干货,请关注
博客:zackku.com
微信公众号:Zack说码