Docker构建多容器应用栈

前言

在这个例子中,需要做的是把一个使用Express框架的并且带有Redis后端的Node.js应用完全Docker化。目的是能够将Docker的特性结合起来使用。主要的工作内容是

  • 一个Node容器,用来服务于Node应用
  • 一个Redis主容器,用于保存和集群化应用状态
  • 两个Redis副本容器,用于集群化应用状态
  • 一个日志容器,用于捕获应用日志

最终我们的Node应用程序会运行在一个容器中,它后面会有一个配置为”主-副本”模式运行在多个容器中的Redis集群。

Node.js镜像

首先我们需要构建一个安装了Node.js的镜像。这个镜像有Express应用和相应的必要的软件包。

第一步先在本地创建一个名为nodejs的文件夹

1
mkdir nodejs && cd nodejs

第二步在nodejs文件夹下创建一个存放node.js应用源码的文件夹名为nodeapp

1
mkdir nodeapp && cd nodeapp

获取node.js源码

如果下载不下来的话,代码地址:https://github.com/turnbullpress/dockerbook-code/tree/master/code/6/node/nodejs/nodeapp

1
wget https://raw.githubusercontent.com/turnbullpress/dockerbook-code/master/code/6/node/nodejs/nodeapp/package.json
1
wget https://raw.githubusercontent.com/turnbullpress/dockerbook-code/master/code/6/node/nodejs/nodeapp/server.js

第三步回到nodejs目录下,创建Dockerfile文件。这个镜像安装了Node,然后我们将nodeapp的源代码通过ADD指令添加到/opt/nodeapp目录。这个Node.js应用是一个简单的Express服务器,包含了package.json文件和实际应用代码的server.js文件。server.js文件引入了所有的依赖,并启动了Express应用,Express应用把session信息保存到Redis里,并创建了一个以JSON格式返回状态信息的节点,这个节点默认使用redis_primary作为主机名去连接Redis。这个应用会把日志记录到/var/log/nodeapp/nodeapp.log文件里,并监听3000端口。我们还将工作目录设置为/opt/nodeapp,并且安装了Node应用的必要软件包,还创建了用于存放Node应用日志的卷/var/log/nodeapp,最后公开了3000端口,并使用ENTRYPOINT指定了运行Node应用的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FROM ubuntu:18.04

RUN apt-get -qq update
RUN apt-get -qq install nodejs npm
RUN mkdir -p /var/log/nodeapp

ADD nodeapp /opt/nodeapp/

WORKDIR /opt/nodeapp
RUN npm install

VOLUME [ "/var/log/nodeapp" ]

EXPOSE 3000

ENTRYPOINT [ "nodejs", "server.js" ]

第四步构建镜像

1
docker build -t elssm/nodejs .

Redis基础镜像

第一步先在本地创建一个名为redis_base的文件夹

1
mkdir redis_base && cd redis_base

第二步创建Dockerfile文件。这个Redis基础镜像从PPA库安装了最新版本的Redis包。并指定了两个卷,公开了Redis的默认端口是6379。我们只是讲这个镜像作为基础镜像从而构建别的镜像,因此不会执行这个镜像。

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM ubuntu:18.04

RUN apt-get -qq update
RUN apt-get install -qq software-properties-common
RUN add-apt-repository ppa:chris-lea/redis-server
RUN apt-get -qq update
RUN apt-get -qq install redis-server redis-tools

VOLUME [ "/var/lib/redis", "/var/log/redis" ]

EXPOSE 6379

CMD []

第三步构建Redis基础镜像

1
docker build -t elssm/redis .

Redis主镜像

第一步先在本地创建一个名为redis_primary的文件夹

1
mkdir redis_primary && cd redis_primary

第二步创建Dockerfile文件。Redis主镜像基于Redis基础镜像,并通过ENTRYPOINT指定了Redis服务启动命令,将Redis服务的日志文件保存到/var/log/redis/redis-server.log

1
2
3
FROM elssm/redis

ENTRYPOINT [ "redis-server", "--protected-mode no", "--logfile /var/log/redis/redis-server.log" ]

第三步构建Redis主奖项

1
docker build -t elssm/redis_primary .

Redis副本镜像

为了配合Redis主镜像,我们会创建Redis副本镜像,保证为Node.js应用提供Redis服务的冗余度。

第一步先在本地创建一个名为redis_replica的文件夹

1
mkdir redis_replica && cd redis_replica

第二步创建Dockerfile文件。Redis副本镜像也是基于Redis基础镜像构建的。并通过ENTRYPOINT指定了运行Redis服务器的命令,设置了日志文件存储位置和slaveof选项,这样就把Redis配置为主-副本模式,从这个镜像构建的任何容器都会将redis_primary主机的Redis作为主服务,连接其6379端口,成为对应的副本服务器

1
2
3
FROM elssm/redis

ENTRYPOINT [ "redis-server", "--protected-mode no", "--logfile /var/log/redis/redis-replica.log", "--slaveof redis_primary 6379" ]

第三步构建Redis副本镜像

1
docker build -t elssm/redis_replica .

创建Redis后端集群

现在我们已经有了Redis主镜像和副本镜像。如下图所示

1

接下来我们可以构建自己的Redis环境了。首先我们创建一个用来运行我们的Express应用程序的网络,名为express

1
docker network create express

使用docker network ls查看网络

1
docker network ls

2

现在让我们在这个网络中运行Redis主容器,如下代码所示

1
docker run -d -h redis_primary --net express --name redis_primary elssm/redis_primary

这里使用docker run命令从elssm/redis_primary镜像创建了一个容器。-h用来设置容器的主机名。这会覆盖默认的行为(默认将容器的主机名设置为容器ID)并允许我们指定自己的主机名,使用这个标志可以确保容器使用redis_primary作为主机名,并被本地的DNS服务正确解析。--net确保该容器在express网络中运行,--name确保容器的名字是redis_primary

接下来使用docker logs命令来查看Redis主容器的运行情况

1
docker logs redis_primary

执行命令后发现什么都没有,这是因为Redis服务会将日志记录到一个文件而不是记录到标准输出,所有使用Docker查不到任何日志。因此我们可以使用之前创建的/var/log/redis卷。

1
docker run -it --rm --volumes-from redis_primary ubuntu cat /var/log/redis/redis-server.log

其中-it是以交互方式运行了另一个容器,--rm会在进程运行完后自动删除容器。--volumes-from告诉它从redis_primary容器挂载了所有的卷,然后我们指定了一个ubuntu基础镜像,并执行cat命令来查看日志,这种方式利用了卷的优点,可以直接从 redis_primary容器挂载/var/log/redis目录并读取里面的日志文件,日志文件内容如下图所示

3

接下来我们创建一个Redis副本容器

1
docker run -d -h redis_replica1 --name redis_replica1 --net express elssm/redis_replica

这里我们运行了另一个容器,这个容器来自elssm/redis_replica镜像,-d标志在后台运行,-h指定主机名,--name指定容器名,--net指定在express网络中运行Redis副本容器。

和查看Redis主容器一样,我们来查看一下Redis副本容器中的日志

1
docker run -it --rm --volumes-from redis_replica1 ubuntu cat /var/log/redis/redis-replica.log

日志文件内容如下图所示

4

到这里我们已经成功启动了redis_primary和redis_replica1容器,并让这两个容器进行主从复制。现在我们再来加入另一个容器副本redis_replica2,从而确保万无一失!

1
docker run -d -h redis_replica2 --name redis_replica2 --net express elssm/redis_replica

查看副本redis_replica2容器中的日志

1
docker run -it --rm --volumes-from redis_replica2 ubuntu cat /var/log/redis/redis-replica.log

如下图所示

5

创建Node容器

现在我们已经让Redis集群运行了,我们可以为Node.js应用启动一个容器,代码如下所示

1
docker run -d --name nodeapp -p 3000:3000 --net express elssm/nodejs

这里我们从elssm/nodejs镜像创建了一个新容器,命名为nodeapp,并将容器内的3000端口映射到宿主机的3000端口,同样我们的nodeapp容器也是运行在express网络中的。

使用docker logs命令来查看nodeapp容器在做什么

1
2
$ docker logs nodeapp
Listening on port 3000

访问本地3000端口,如下图所示

6

这个输出表明应用正在工作,浏览器的会话状态(session)回先被记录到Redis主容器redis_primary,然后复制到两个Redis副本容器redis_replica1和redis_replica2

捕获应用日志

现在应用已经正常运行了,我们需要把这个应用放到生产环境中,在生产环境里需要确保可以捕获日志并将日志保存到日志服务器。我们将使用Logstash来完成这件事,首先创建一个Logstash镜像。

第一步还是在本地创建一个名为logstash的文件夹

1
mkdir logstash && cd logstash

第二步创建Dockerfile文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FROM ubuntu:18.04

RUN apt-get -qq update
RUN apt-get -qq install wget gnupg2 openjdk-8-jdk
RUN wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | apt-key add -
RUN echo "deb https://artifacts.elastic.co/packages/5.x/apt stable main" | tee -a /etc/apt/sources.list.d/elastic-5.x.list
RUN apt-get -qq update
RUN apt-get -qq install logstash

WORKDIR /usr/share/logstash

ADD logstash.conf /usr/share/logstash/

ENTRYPOINT [ "bin/logstash" ]
CMD [ "-f", "logstash.conf", "--config.reload.automatic" ]

这个镜像在安装了Logstash之后,将logstash.conf文件使用ADD指令添加到了/usr/share/logstash/目录。logstash.conf文件和Dockerfile在统计目录下,它的内容如下

1
2
3
4
5
6
7
8
9
10
11
input {
file {
type => "syslog"
path => ["/var/log/nodeapp/nodeapp.log", "/var/log/redis/redis-server.log"]
}
}
output {
stdout {
codec => rubydebug
}
}

这个logstash配置很简单,它监控两个文件/var/log/nodeapp/nodeapp.log/var/log/redis/redis-server.log。logstash会一直监控这两个文件,将其中新的内容发送个Logstash,配置文件的第二部分是output部分,接受所有logstash输入的内容并将其输出到标准输出上,在实际情况中,一般会将logstash配置为输出到ElasticSearch集群或者是其他的目的地。

第三步构建logstash镜像

1
docker build -t elssm/logstash .

镜像构建成功之后,我们从这个镜像启动一个容器,代码如下

1
docker run -d --name logstash --volumes-from redis_primary --volumes-from nodeapp elssm/logstash

这样我们就启动了一个名为logstash的新容器,并通过--volumes-from标志分别挂载了redis_primary和nodeapp容器的卷,这样就可以访问Redis和Node的日志文件了,任何加到这些日志文件里的内容都会反应在logstash容器的卷里。

现在可以使用-f标志来查看logstash容器的日志

1
docker logs -f logstash

在浏览器里刷新Web应用之后,会产生一个新的日志时间,我们就能在logstash容器的输出中看到这个事件。如下图所示

7

相应的当我们启动redis_primary容器后,也可以在logstash中看到相应的日志事件

8