通过做一个基于Node的微服务器来学习Docker

如果你正准备着手学习 Docker,别再观望,动起手来吧!

在这篇文章中,我将告诉你 Docker 是如何工作的?使用中会遇到什么问题?如何通过 Docker 完成一个基本的开发任务——构建一个微服务器。

我们将以一台配有 Node.js 服务和 MySQL 后台的服务器为例,从在本地运行代码开始,完成一个运行着微服务和数据库的容器。

什么是 Docker ?

从本质上来说,Docker 是一种软件,让用户创建镜像文件(就像虚拟机中的模板),然后在容器中运行这个镜像的实例。

Docker 维护着有着大量镜像的存储库,名字叫 Docker Hub ,你可以将它作为尝试镜像的起始点,或者用来免费存储你的镜像。你可以安装 Docker ,选择你喜欢的镜像,然后在容器中运行它的实例。

本文我们将介绍创建镜像、从镜像创建容器等一系列内容。

安装 Docker

如果你想跟上本文的节奏,那么你需要安装 Docker 。

点击 docs.docker.com/engine/installation 这个链接,在上面查看适合你的系统的安装向导。

如果你是 Mac 或者 Windows 操作系统,那么你需要使用虚拟机。我在 Mac OS X 上使用 Parallels 安装 Ubuntu 虚拟机来应付大多数的开发任务。因为它支持快照功能,当你做实验的时候,他可以方便的将破坏了的环境恢复回去。

试试看

输入以下命令:

一段时间后,你将会看到如下提示:

试试如下的命令,然后退出容器:

这看起来没什么,但是其实在后台发生了很多事情。

你看到的是在你的机器上运行着的 Ubuntu 的隔离容器环境里的 bash shell。这个环境完全归你所有——可以在上面安装软件,运行软件,可以做任何你想做的事情。

下图表明了刚刚发生了什么(图来自于《 理解 Docker 架构 》一文):

Docker Run Flow

1. 列出如下的 Docker 指令:

  • docker : 运行 docker 客户端
  • run : 运行一个新的容器
  • -it :让容器带有“交互终端”的一个参数
  • ubuntu : 容器所依赖的基础镜像

2. 在主机(我们的机器)上运行的 docker 服务检查本地是否有所请求的镜像拷贝——这里发现没有。

3. docker 服务检查公有存储库(the docker hub),看是否有可用的名为 ubuntu 的镜像——这里发现有。

4. docker 服务下载镜像,将其存储到本地缓存里(为了下一次直接使用)。

5. docker 服务基于 ubuntu 镜像创建新的容器。

Try any of these:

试试下面这些命令:

我们没准备使用 Haskell ,但是你可以看到,搭建一个环境是多么容易。

构建自己的镜像也很轻松,可以在这上面安装应用程序或者服务,可以是数据库,或者是其他你需要的。随后就可以在任意安装了 Docker 的机器上运行它们——要保证镜像是相同的、可预测的方式在每台机器上运行。我们可以将软件及其运行所需的环境整体构建成代码,并且轻松部署。

让我们以一个简单微服务器为例。

概述

我们将要用 Node.js 和 MySQL 创建一个让我们管理邮件地址到电话号码目录的微服务。

开始

要完成本地开发,需要安装MySQL,并且创建一个测试数据库…

…摇头。

创建本地数据库,并且上面运行脚本,这很容易,但是可能会带来一些问题。很多不受控制的事情开始了。它可能工作,我们甚至可以通过提交进代码库的 shell 脚本来控制这些步骤,但是如果其他开发人员已经安装了 MySQL 了呢?如果他们的数据库已经使用了我们想要创建的名称  ‘users’  了呢?

第一步:在 Docker 中创建一个数据库测试服务器

这是很好的 Docker 应用场景。我们可能不想在 Docker 里运行生产环境数据库(比如可能会使用 Amazon RDS),但是可以使用 Docker 容器创建一个干净的 MySQL 数据库做开发——让我们的开发及其保持干净,并且保证所有东西都在控制中,并且可重复使用。

运行下面的命令:

该命令启动一个运行着的 MySQL 实例,通过 3306 端口访问,root 密码为 123 。

  1.  docker run 告诉引擎,用户想要运行一个镜像(在最后传入的是镜像,mysql:latest
  2.  –name db 将整个容器命名为 db 。
  3.  -d detach,在后台运行容器。
  4.  -e MYSQL_ROOT_PASSWORD=123(或者是 –env)环境变量 – 参数告诉 docker 所提供的环境变量。这之后跟着的变量正是 MySQL 镜像检查且用来设置的默认 root 密码。
  5.  -p 3306:3306(或者 --publish) 告诉引擎用户想要将容器内的3306端口映射到外部的3306端口上。

最后一部分很重要——即使这是 MySQL 的默认端口,如果用户不显式告诉 docker 想要映射的端口,docker 就会阻塞该端口的访问(因为容器默认是隔离的,直到用户告诉 docker 想要访问它们)。

该命令返回值是容器 id,这是容器的指针,用户可以用它来停止容器,向容器发送命令等等。让我们看看正在运行的是哪些容器:

关键的信息是容器 ID,镜像和名称。连接到这个镜像看看里面有什么:

下面这么做也很有意思:

1. docker exec -it db :告诉 docker 用户想要在名为 db 的容器里执行一个命令(我们也可以使用 id,或者 id 的前几个字母)。 -it 确保用户有交互型终端。

2. mysql -uroot -p123 :我们实际在容器里作为进程运行的命令,这里是 mysql 客户端。

我们可以创建数据库,表,用户,其他你需要的等等。

打包测试数据库

在容器内运行 MySQL 需要一些 Docker 技巧,但是让我们先打住,看看服务。现在,使用脚本创建一个 test-database 目录来启动数据库,停止数据库以及搭建测试数据:

启动脚本很简单:

该脚本在一个分离容器钟运行数据库镜像(比如,在后台运行),创建了一个用户来访问 users 数据库,然后等待数据库服务器启动,随后运行 setup.sql 脚本来设置初始数据。

setup.sql 的内容是:

stop.sh 脚本会停止容器并且删除容器(docker 默认会保留容器,这样能够快速重启,本示例中并不需要这样):

之后会进一步简化这个过程,让它更加顺畅。在 repo 里的 step1 分支里查看这一阶段的代码。

第二步:用 Node.js 创建一个微服务

本文的主题是 Docker 的学习,因此并不会花太多篇幅讲解 Node.js 的微服务。只是强调一些重点。

让我们仔细看看这一部分。首先看看这个代码库。最好将你的数据库访问封装和抽象成一些类,允许模拟它来实现测试目的:

其实有很多种其他实现方式!但是我们可以像下面这样创建 Repository 对象:

在 repository/repository.spec.js 文件里也有一系列的单元测试。得到 repo 后,就可以创建服务器了。server/server.js 如下:

该模块暴露了一个 start 函数,可以像下面这样使用:

注意到 server.js 中使用了 api/users/js 吧?代码如下:

这些文件都有和源码匹配的单元测试。

我们还需要配置。与其使用特定的库函数,不如使用一个简单的文件 – config/config.js :

我们可以按需进行配置。目前,大部分配置是硬编码的,但是从端口的配置中可以看出,我们可以很容易的通过添加环境变量的方式来改变它。
最后一步 – 将它和包含所有东西的 index.js 文件连接到一起:

我们做了一点错误处理,在此之上仅仅加载了配置,创建了 repo 并且启动了服务器。
这就是微服务,它让用户能够得到所有用户,或者搜索某个用户:

如果下载了相关代码,可以发现有一些可用的命令:

除了代码之外,我们完成了:
1. 用于调试的 Node 面板
2. 用于单元测试的 Mocha/shoud/supertest
3. 用于 linting 的 ESlint

大功告成!
使用如下命令运行数据库测试:

然后启动服务:

可以用浏览器打开 localhost:8123/users,就可以看到数据库已经可以使用了。如果使用的是 Docker Machine(假设,在 Mac 或者 Windows 上),那么 localhost 无法访问,你需要使用 docker 的 IP。可以通过 docker-machine ip 得到 IP 地址。
可以看到,我们很快完成了服务的创建。继续下一步之前如果想查看代码,见 step2 分支。

第三步:微服务 Docker 化

现在开始变得有趣啦!

我们已经有了一个可以运行在开发环境里的微服务,只要它和安装的 Node.js 版本兼容即可。这一步想做的是搭建起我们的服务,这样可以从其中创建一个 Docker Image,从而可以将服务部署到任何支持 docker 的地方。

要达到这一目的,需要创建一个 Dockerfile。Dockerfile 告诉 Docker 引擎如何构建镜像。我们会在 users-service 目录下创建一个简单的 Dockerfile,并且研究如何通过修改它来适应需求。

创建 Dockerfile

在 users-service/ 目录下创建名为 Dockerfile 的文本文件,内容如下:

运行如下命令行构建镜像,并在镜像上运行容器:

首先看看构建命令。
1. docker build 告诉引擎用户需要创建一个新的镜像
2. -t node4 使用标签 node4 标记该镜像。之后就可以使用这个标签来指代该镜像。
3. 在当前目录里查找 Dockerfile.

控制台打出一些输出之后,就可以看到新的镜像创建好了。使用 docker images 命令可以在系统里看到所有镜像。下面的命令和之前的很类似:
1. docker run 从某个镜像里运行新容器
2. -it 使用交互式终端
3. node4 是想要在容器里使用的镜像的标签。

当运行该镜像时,会得到 node repl,运行如下命令检查当前版本:

这很可能和你当前机器上的 node 版本不同。

检查 Dockerfile

从 Dockerfile 里可以很容易看出发生了什么:
1. FROM node:4 在 Dockerfile 里指定的第一件事就是基础镜像。docker hub上的 Node 官方页面可以搜索列出所有可用镜像。这里用的是安装了 node 的 ubuntu。
2. CMD ["node"] 里的  CMD 告诉 docker 该镜像需要运行 node 程序。当 node 程序终止时,容器会关闭。

使用额外的几个命令,可以更新 Dockerfile,从而运行服务:

唯一的改变是使用了 ADD 命令将当前目录下的所有东西拷贝到名为 app/ 的容器目录里。随后使用 RUN 在镜像里运行命令,该命令安装了模块。最后,EXPOSE 了服务器端口,告诉 docker 想要支持 8123 端口的连接,然后运行服务器代码。
确保 test-database 服务已经运行着,然后再次构建并且运行镜像:

如果在浏览器里查看 localhost:8123/users,会看到一个错误,检查控制台,提示容器报告了一些问题:

我勒个去!从 users-service 容器到 test-database 容器的连接被拒绝了。运行 docker ps 查看所有运行着的容器:

这两个容器都运行着呢,到底怎么回事呢?

连接容器

我们看到的问题实际上是可以预期的。Docker 容器应该是互相隔离的,因此如果不显式地允许容器间连接的话就容器间就无法互联。

是的,用户可以从自己的机器(宿主机)连接到容器里,因为我们为这样的连接开启了端口(比如,使用了-p 8123:8123)。如果以同样的方式允许容器间互联,那么运行在同一台机器上的两个容器之间就应该能够通信,即使开发人员不想这么做。并且这是灾难性的,尤其是我们在集群的机器上利用容器运行不同的应用程序的时候。

如果想要从某个容器连接到另一个容器,需要连接这两个容器,告诉 docker,用户显式想要允许这两个容器间通信。有两种方式可以完成这一目标,第一种是“不流行的旧方式”但是非常简单,第二种之后会介绍。

使用 link 参数连接容器

当运行容器时,可以使用 link 参数告诉 docker 我们想要连接到另外的容器上。本文示例中,可以通过如下命令正确运行服务:

1. docker run -it 在容器里运行 docker 镜像,使用交互式终端。
2. -p 8123:8123 将宿主机的 8123 端口映射到容器的 8123 端口上
3. link db:db 连接到名为 db的 容器上,并且用 db 指代该容器
4. -e DATABASE_HOST=db 将环境变量 DATABASE_HOST 设置为 db
5. users-service 是在容器内运行的镜像名称

现在当我们访问 localhost:8123/users 时一切工作正常。

它是如何工作的呢?

还记得服务的配置文件么?它让用户能够使用环境变量指定数据库的主机名:

运行容器时,将环境变量设置为 DB,这意味着要连接到一个名字为 DB 的主机上。当连接到容器上时,docker 引擎会自动为我们设置好一切。

尝试运行 docker ps 列出所有运行着的容器。查询运行 users-service 的容器名称,这是个随机名称,例如 trusting_jang:

现在可以看到容器可用的主机:

还记得 docker exec 是怎么工作的吗?选择一个容器名,之后跟着想在容器上执行的命令,在本例中是 cat /etc/hosts

好了,主机文件之前可没有 # linking magic!! 注释,可以看到 -docker 将 db 添加到了主机文件里,因此可以通过主机名连接到容器上。这是连接信息:

从该命令还可以看到当 docker 连接容器时,它还提供了一系列包含有用信息的环境变量,比如,主机名,tcp 端口和容器名。

第3步完成了 —— MySQL 数据库正常运行在容器里,还可以在本地或者在容器里运行 node.js 微服务,并且已经知道了如何连接这两者。

如果你想了解更多,可以在 step3 的分支里查看这一阶段的代码。

第4步:环境的集成测试

现在可以编写集成测试,调用实际服务器,作为 docker 容器运行,调用容器化的测试数据库。

可以用任何语言,或者在任何平台上完成集成测试,但是为了保持简洁,这里使用的是 Node.js,因为项目里已经使用了Mocha 和 Supertest。

在名为 integration-tests 的新目录下,创建一个 index.js:

它会检查 API 调用,并且显示测试结果。
只要 users-services 和 test-database 正在运行,测试就能够通过。但是,这时候服务开始变得有点难处理:
1. 需要使用 shell 脚本来启动和停止数据库
2. 需要记住一系列命令来基于数据库启动用户服务
3. 需要使用 node 直接运行集成测试
既然我们已经很熟悉 Docker 了,应该能够解决这些问题。

简化 Test 数据库

目前测试数据库有如下文件:

既然已经很熟悉 Docker 了,让我们尝试改进它们。

在 Docker Hub 上查看 mysql 镜像文档,有一处注释告诉用户任何添加到镜像的 /docker-entrypoint-initdb.d 目录的 .sql 或者 .sh 文件会在搭建 DB 的时候执行。

这意味着可以使用 Dockerfile 代替 start.sh 和 stop.sh 

现在运行测试数据库只需要:

组合

构建并且运行每个容器仍然有些费时。可以使用 Docker Compose 工具进一步简化。

Docker Composer 允许用户创建一个文件,在其中定义系统里的每个容器,容器间的关系,并且构建或者运行它们。
首先,安装 Docker Compose。在项目根目录下创建一个新文件,称为 docker-compose.yml:

现在就可以试一下啦:

Docker Compose 会构建出应用程序所需要的所有镜像,从其上创建出容器,并且以正确顺序运行容器,从而启动整个应用程序!
docker-compose build 命令构建 docker-compose.yml 文件里列出的每个镜像:

每个服务的 build 值告诉 docker 到哪里找到 Dockerfile 。当用户运行 docker-compose up 时,docker 会启动所有服务。注意在 Dockerfile 里,用户可以指定端口和依赖关系。实际上,在这个文件里,用户可以更改所有配置。
在另一个终端里,运行 docker compose down,可以正常关闭容器。

总结

本文里介绍了很多 docker 知识,不过这里还有一些。我希望你能够从本文找到一些有趣有用的东西,能够帮助你在工作中使用 docker。

和平常一样,欢迎提问和建议!同时强烈推荐文档《理解 Docker 》,可以帮助大家更深入地理解 docker 的工作机制。

github.com/dwmkerr/node-docker-microservice 处可以看到本文所构建的项目的最终源码。

注意

1. 将所有东西完全拷贝不是什么好主意,因为那样也会拷贝 node_modules 文件夹。通常来说显式指定想要拷贝的文件或文件夹会更好,或者使用 .dockerignore 文件,它和 .gitignore 文件类似。
2. 如果服务器不正常工作,显示一个很讨厌的 exception,这是由一个bug造成的,详见 github.com/visionmedia/supertest/issues/314

打赏支持我翻译更多好文章,谢谢!

打赏译者

打赏支持我翻译更多好文章,谢谢!

任选一种支付方式

1 11 收藏 评论

关于作者:小谢

懒懒的程序员~ 个人主页 · 我的文章 · 24 ·  

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部