Docker in action

各位好久不见 && 各位晚上好。

这里是AS。

很久没有写的技术向文章,这次想说的是Docker。

虽然一向我挺喜欢分析原理什么的,不过很遗憾这篇不是,正如标题所述,只是一些自己最近docker部署的实践。

本文是笔记性质,不会解释命令或者原理,单纯是:请就这样用就好。在实践的过程中,很明显我遇到了很多奇妙的问题,不过这里不涉及,有可能会做一些讨论。

如果您期望从这里了解到docker是什么,container是什么,volume是什么,大概这篇文章不适合您,请先阅读docker文档。讲道理,写得真的很不错。

在开始一切的叙述之前,请允许我描述一下我这次要部署的项目以及我本地的情况。

本地是一台Mac,使用了推荐的Docker Toolbox。版本为1.10.3。

要部署的项目,由admin,api,view三个服务组成,其中,view依赖redis,admin和api依赖postgresql和redis。

这里说一下,我去年最开始接触docker的时候,我是把docker当成一个虚拟机看的。于是当时的做法是,从线上拉一个lastest的debian或者ubuntu镜像,然后attach进去,配置环境,解决各种依赖 —- 说实在的,这种做法我估摸现在也有人在用。

不过当然,我这里把这种做法单独拎出来自然说明这样的做法是不对的。

真正的做法应该是每个服务一个单独的container,或者一个单独的image。

就我这里为例,我的admin,api和view都是不同的服务,我把这三块都打包成了不同的image。

这里的一个问题是,虽然这三个是不同的服务,但是我代码有复用,实际上是一个工程。于是依赖也几乎一样,这时候如果每个都去build一次,中间的pip install -r requirements会被执行多次,比较耗时,我推荐可以写一份Base Dockerfile,剩下镜像的可以从这个Dockerfile生成的镜像派生出来。

我上面这一段说得有点跳。如果您还不知道Dockerfile是什么,以及怎么写一个简单的Dockerfile,请至少看完docker官网的get start。

我的项目基于python3.5,那么我就可以直接基于python3.5这个打好的docker进行增加layer的动作。由于官方的python:latest已经是3.5的版本,所以我可以直接FROM python。如果以后升级成3.6了,这样写就是不对的。我姑且在这里提醒一下自己。

我这里的DockerfileBase是这样:

FROM python
RUN mkdir /web
WORKDIR /web
ADD ./requirements /web

RUN echo "Asia/Shanghai" > /etc/timezone
RUN dpkg-reconfigure -f noninteractive tzdata
RUN pip install -r requirements

这个Dockerfile是写在项目文件夹最外层,和requirements平级。要build的时候,cd到项目文件夹,然后docker build -t base DockerfileBase .

我在这里面加了更换时区的配置,否则会默认走UTC,看日志不太方便,挺烦的。

这样,requirements只需要download一次,build其他的服务就很快了。

推荐build之后对这个镜像打个tag。没有tag,会有些莫名其妙的问题。很烦。tag一般格式是 user/xxx, 比如我会叫aprocysanae/base。

然后就是扩展这个base。譬如说这是我的admin的Dockerfile,姑且叫做DockerfileAdmin。

FROM aprocysanae/base
ADD . /web
WORKDIR /web
ENV PYTHONPATH /web
CMD ["python", "koishi/koishi.py"]
EXPOSE 6614

这个base已经被我打过tag了,然后把文件夹Add进去。我看了看,这个ADD的原理实际是对这个文件夹打了一个压缩包,然后通过docker的守护进程提交进了container。

如果在这里不设置WORKDIR的话,会有一些迷一样的问题出现,困扰了很久。

接下来设置环境变量。

CMD虽然不一定是上面写作的列表式,可以 CMD python koishi/koishi.py。不过我推荐列表式。直接写会默认在前面加上奇怪的 bash -c 来着?总之常常不能如愿执行命令,所以列表式可以保证不乱来,就是这样。

EXPOSE暴露端口。这里有一个误区,EXPOSE暴露端口只是意味着,这个容器有了这个开放端口可以通信,而不是意味着宿主机可以通过这个端口去访问。

虽然是很浅显的道理,不过我很是纠结了一阵。

build出来的只是镜像,而跑服务的是容器。docker run可以创建容器。docker run --name admin -p 6614:6614-p 可以指定让container端口和宿主机建立映射。记得之前提到的,在Dockerfile里面EXPOSE接口么?如果这时候不指定container的端口,使用 -P 随机选择端口,宿主机会随机mapping一个端口到EXPOSE出来的端口上。

上面说了,docker对服务的看法是一个container一个服务,这样说起来,redis是一个服务,postgre也是一个服务。那么如果我需要在admin里面访问redis或者postgre,该怎么做?

自然,可以把redis和postgre的端口映射出来,然后代码里面通过docker-machine ip + mapped port进行访问服务。这是可行的。

不过,这种方式实际上暴露了redis服务或者postgre服务,只要被扫了端口,理论上可能被滥用或者受到攻击。docker提供了一种桥接模式—实际上就是开了一个局域网—服务通过这个网络进行通信,并且无法从外界被直接访问。

桥接模式的语法是 –link, 有多少个需要link的服务就link多少次。比如 --link postgres:db --link redis:redis

那么,这之后怎么访问桥接的服务?比如说,我怎么从admin里面访问postgres?

请关注一下--link postgres:db这个语法。: 前面的postgres,指代一个镜像,后面的db,是一个别名。link在初始化的时候会初始化一些环境变量,在这里,db这个服务的ip,实际上已经被初始化到admin中了。如果有兴趣,您可以attach或者exec -it 到admin这个运行中的服务中,cat出来/etc/hosts,会发现这里面已经有了几行增加的DNS,其中会有 xxxxxx db。db是postgre定义的别名。于是就很明显了:如果之前代码里面,连接db是使用 http://localhost:5432来访问数据库的,把这个链接改成 db就可以了,因为local DNS会自动解析到对应的ip上去。

前面已经提到过,Dockerfile中的ADD操作,本质上是把源文件夹压缩后发送到特定image的container上解压成为一个新的layer。这也就说明了,一旦这个动作结束,之后我们在源文件夹中修改的数据,并不会在docker中体现。这其实很好,因为通常docker用在生产环境,生产环境的代码并不会轻易变动。但是日志除外。如果您开了多个docker,做了负载均衡或者别的什么,这时候各个container中日志都各写各的,查看日志需要attach到不同的container中,很不舒服。我知道,有的公司使用了中央日志系统或者别的什么,日志走接口,这样的情况不在讨论范围之中。我现在关心的只有一个问题:如何映射docker中的文件或者文件夹到本地。

答案是volume。

docker中volume官方提及作用是“持久化数据”。不过不管怎么说,这是docker提供的一种机制,可以mapping宿主机和container的文件夹。宿主机在这个文件夹下的所有更改会反应到container中,反之也是一样。这样,我如果在所有的服务中,把宿主机上的某个文件夹—比如说logs,映射到这些服务中记录日志的文件夹,那么之后监视宿主机这个文件夹就可以。

volume的语法是 -v src_dir:dest_dir。不过请注意,如果您希望映射的不是一个文件夹而是一个文件,那么文件的地址,请您还是写全一点,譬如 $(pwd)/config.py,如果只写config.py,docker会认为这是一个named volume,不管怎么说都不是您希望的意思。还有,这部分有一些莫名其妙的玄学问题,我之前被卡了很久 mounting into \ prohibited。如果您看到了同样的提示,觉得您可以试试在Dockerfile里面把 WORKDIR 重指定一次然后重新build。这个地方异常玄学,如果这样并不能解决问题,请留言联系我。

说到持久化,似乎有一个误解是,docker的container不是持久化的。不过这个很明显是错误的看法,如果container不能持久化,那么运行着数据库的docker不就毫无意义了么?

实际上,一个container对应一个新的linux 虚拟机,一旦被创建,除非删除,里面的数据都是持久的。如果需要查看运行着的容器,使用 docker ps, 查看所有容器,使用 docker ps -a。BTW,我总是推荐run的时候使用 –name 来重新起一个名字,这样很多时候很方便。

另外说一下posgres的问题。官方下来的postgres都是默认配置,如果打算创建新的用户,设置密码都需要扩展配置文件。如果希望初始化表,请ADD进 docker-entrypoint-initdb.d

我姑且提供一下我的BasePostgresDockerfile作为示例。这个帐号密码都是我现在随便写的,所以不要当真了……

FROM postgres
ENV POSTGRES_USER test
ENV POSTGRES_PASSWORD test
ENV POSTGRES_DB db_test
ADD tables /docker-entrypoint-initdb.d/

tables是我本地的一个文件夹,我把初始化的sql语句全部放在这里面。

redis同样,如果您需要修改默认配置,增加密码,都需要扩展原有的docker。

最后需要说的是docker-compose.yml。

这实在是一个很方便的东西,基本上只需要 docker-compose up就可以跑起来所有的服务,使用体验很棒。

docker-compose其实就是把上面说的所有东西聚合起来。我这里使用的docker版本是 1.10, docker-compose.yml我采用的version:2的写法,这个写法似乎是在1.10之后引进的,所以docker版本太低请不要这样写。

全文如下:

version: '2'
services:
  views:
    image: qingdan_view
    links:
      - apis
      - redis
    ports:
      - "8080:8080"
    volumes:
      - ./logs:/web/logs
  apis:
    image: qingdan_api
    links:
      - postgres:db
      - redis:redis
    ports:
      - "6613:6613"
    volumes:
      - ./logs:/web/logs
  admin:
    image: qingdan_admin
    links:
      - postgres:db
      - redis
    ports:
      - "6614:6614"
    volumes:
      - ./logs:/web/logs
  postgres:
    image: qd_postgres
  redis:
    image: redis

说起来,其实应该是从build开始的,这样只要build的当前文件夹发生变动就会自动build一次。不过我嫌麻烦于是从image开始了。

如果您的image有变动,docker-compose up会更新对应的container,还是很贴心的。

于是全部内容到此为止,感谢阅读。

谢谢。

以上。

AS,起笔于四月的某个凌晨,完成于四月某个最明媚的早上。