返回 登录
0

通过构建微服务来学习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以支持大多数的开发活动。这种方式对于在各种实验中拍摄快照,中断以及恢复时是非常方便的。

试验开始

输入以下命令:

docker run -it ubuntu  

很快你就将会看到以下的命令提示符:

root@719059da250d:/#  

下面再测试几条命令然后终结这个容器:

root@719059da250d:/# lsb_release -a  
No LSB modules are available.  
Distributor ID:    Ubuntu  
Description:    Ubuntu 14.04.4 LTS  
Release:    14.04  
Codename:    trusty  
root@719059da250d:/# exit  

这看起来好像并没有什么,但是实际上背后发生了很多。你们看到的是Ubuntu的一个bash shell,它运行于在你的机器上隔离的容器中。在这里,你可以安装任何东西,运行任何软件,或者其他任何你想要做的。以下是上述动作的流程分解图(该图表来自于Docker文档库的“理解架构”,非常值得推荐)

图片描述

1.输入一条Docker命令:

odocker: 运行docker客户端
orun: 该命令启动一个新的容器
o-it: 是否启动交互式终端模式的可选项
oubuntu: 容器启动所基于的镜像名

2.在主机上运行的Docker的服务首先检查本地是否有所请求的镜像拷贝,没有的话则执行下一步。

3.Docker服务检查公共的版本库(Docker Hub)是否有名字为ubuntu 的镜像存在,找到然后执行下一步。

4.Docker服务下载镜像并存储于本地缓存中,以备下次使用。

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

尝试更多命令如下:

docker run -it haskell  
docker run -it java  
docker run -it python  

我们使用Haskell ,但是就像你所看到的那样,配置运行这个环境也是非常容易的。

这个范例描述了如何创建自己的镜像,并包含我们的服务程序、数据库以及其他一切所需的。我们可以在任何安装有Docker的机器上运行它们,这些镜像都会以同样的、可预测的方式执行。因此我们可以非常方便的构建软件以及编码和部署用于软件运行所需的环境。接下来让我们来看一个简单的微服务范例。

概述

以下将要建立一个微服务,它可以让我们通过使用使用Node.js和MySQL来管理电子邮件目录中的电话号码。

启程

为了开始本地开发,我们需要安装MySQL以及创建一个测试数据库… …
创建一个本地数据库并执行脚本是一个简单的开端,但也可能是一团混乱。很多不可控的事情会发生。它可能会正常工作,我们甚至可以执行一些脚本来检查我们的版本库,但是假如已经有其他开发人员在机器上安装了MySQL呢?并且假设他们所使用的数据库已经占用了我们想要使用的数据库名‘users’?

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

这是一个非常好的Docker用户案例。或许我们不会把生产数据库跑在Docker上,但是可以在任何时间为开发人员快速部署一个基于Docker容器的纯净MySQL数据库,保持我们干净的开发机器环境并且一切都是可控且可重复的。

执行以下命令:

docker run --name db -d -e MYSQL_ROOT_PASSWORD=123 -p 3306:3306 mysql:latest  

该命令启动一个MySQL数据库实例,并且允许root用户以及123的密码通过3306端口访问它.

  1. docker run 这里我们告诉Docker引擎我们需要加载一个镜像(这个镜像名在命令最后:mysql:vlatest)。
  2. –name db 这里给容器命名db。
  3. -d (or –detach) 分离,即在后台运行的容器。
  4. -e MYSQL_ROOT_PASSWORD=123 (or –env) 环境变量-告诉Docker我们需要提供的环境变量,随后的这个变量就是MySQL镜像需要检查配置的root默认密码。
  5. -p 3306:3306 (或者 –publish 告诉Docker引擎我们需要映射容器内部的3306端口到外部的3306端口。

这个命令的返回值就是容器的id,它是容器的引用代码可以用来针对具体容器停止、重启、执行命令等等。接下来就让我们来看看哪些容器当前正在运行:

$ docker ps
CONTAINER ID  IMAGE         ...  NAMES  
36e68b966fd0  mysql:latest  ...  db  

这里的关键信息就是容器ID,镜像以及容器名。下面让我们连接上这个镜像并且看看上面到底有什么:

$ docker exec -it db /bin/bash

root@36e68b966fd0:/# mysql -uroot -p123  
mysql> show databases;  
+--------------------+
| Database           |
+--------------------+
| information_schema |
+--------------------+
1 rows in set (0.01 sec)

mysql> exit  
Bye  
root@36e68b966fd0:/# exit  

这里的实现也是相当的巧妙:

  1. docker exec -it db 这里告诉Docker,我们要在容器名为db(这里我们也可以使用容器id,或者是id头上的部分缩写)内执行一条命令。
  2. mysql -uroot -p123 这条是真正在容器内运行具体进程的命令,在本例中就是启动了mysql的客户端。

至此,我们已经可以创建数据库、表、用户以及其他一切所需。

测试数据库总结

上述文章中介绍了一些在容器中运行MySQL的Docker技巧,不过暂停一下,转移到服务上。现在,我们将要创建一个test-database 文件夹以及使用脚本来启动数据库,停止数据库和设置测试数据:

test-database\setup.sql  
test-database\start.sh  
test-database\stop.sh  

启动命令非常简单:

#!/bin/sh

# Run the MySQL container, with a database named 'users' and credentials
# for a users-service user which can access it.
echo "Starting DB..."  
docker run --name db -d \  
  -e MYSQL_ROOT_PASSWORD=123 \
  -e MYSQL_DATABASE=users -e MYSQL_USER=users_service -e MYSQL_PASSWORD=123 \
  -p 3306:3306 \
  mysql:latest

# Wait for the database service to start up.
echo "Waiting for DB to start up..."  
docker exec db mysqladmin --silent --wait=30 -uusers_service -p123 ping || exit 1

# Run the setup script.
echo "Setting up initial data..."  
docker exec -i db mysql -uusers_service -p123 users < setup.sql  

这个脚本在一个分离式的容器(即后台运行的容器)上运行数据库镜像,同时用户设置访问users 数据库,然后等待数据库服务器启动,最后执行setup.sql脚本设置初始数据。
setup.sql 的内容如下:

create table directory (user_id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, email TEXT, phone_number TEXT);  
insert into directory (email, phone_number) values ('homer@thesimpsons.com', '+1 888 123 1111');  
insert into directory (email, phone_number) values ('marge@thesimpsons.com', '+1 888 123 1112');  
insert into directory (email, phone_number) values ('maggie@thesimpsons.com', '+1 888 123 1113');  
insert into directory (email, phone_number) values ('lisa@thesimpsons.com', '+1 888 123 1114');  
insert into directory (email, phone_number) values ('bart@thesimpsons.com', '+1 888 123 1115');  

stop.sh 脚本将会停止容器并删除它(默认情况下容器并不会被Docker立即删除,从而它们可以在需要时被快速恢复,在本例中我们并不需要这个功能):

#!/bin/sh

# Stop the db and remove the container.
docker stop db && docker rm db  

接下来将要让这些步骤更加平滑和简洁,具体可以查看在这个阶段的第一步的版本库分支代码。

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

由于本文的主要关注点在于学习Docker,所以我将不会在Node.js如何实现微服务上花费太多笔墨,相反,我将重点关注Docker的领域和结论。

test-database/          # contains the code seen in Step 1  
users-service/          # root of our node.js microservice  
- package.json          # dependencies, metadata
- index.js              # main entrypoint of the app
- api/                  # our apis and api tests
- config/               # config for the app
- repository/           # abstraction over our db
- server/               # server setup code

首先来看repository。它是对于封装你的数据库访问类和抽象非常有用的方式,并且允许模拟它作为测试目的:

//  repository.js
//
//  Exposes a single function - 'connect', which returns
//  a connected repository. Call 'disconnect' on this object when you're done.
'use strict';

var mysql = require('mysql');

//  Class which holds an open connection to a repository
//  and exposes some simple functions for accessing data.
class Repository {  
  constructor(connection) {
    this.connection = connection;
  }

  getUsers() {
    return new Promise((resolve, reject) => {

      this.connection.query('SELECT email, phone_number FROM directory', (err, results) => {
        if(err) {
          return reject(new Error("An error occured getting the users: " + err));
        }

        resolve((results || []).map((user) => {
          return {
            email: user.email,
            phone_number: user.phone_number
          };
        }));
      });

    });
  }

  getUserByEmail(email) {

    return new Promise((resolve, reject) => {

      //  Fetch the customer.
      this.connection.query('SELECT email, phone_number FROM directory WHERE email = ?', [email], (err, results) => {

        if(err) {
          return reject(new Error("An error occured getting the user: " + err));
        }

        if(results.length === 0) {
          resolve(undefined);
        } else {
          resolve({
            email: results[0].email,
            phone_number: results[0].phone_number
          });
        }

      });

    });
  }

  disconnect() {
    this.connection.end();
  }
}

//  One and only exported function, returns a connected repo.
module.exports.connect = (connectionSettings) => {  
  return new Promise((resolve, reject) => {
    if(!connectionSettings.host) throw new Error("A host must be specified.");
    if(!connectionSettings.user) throw new Error("A user must be specified.");
    if(!connectionSettings.password) throw new Error("A password must be specified.");
    if(!connectionSettings.port) throw new Error("A port must be specified.");

    resolve(new Repository(mysql.createConnection(connectionSettings)));
  });
};

这里或许有很多种更好的方式来实现,但是基本上我们可以用以下方式来创建Repository 对象:

repository.connect({  
  host: "127.0.0.1",
  database: "users",
  user: "users_service",
  password: "123",
  port: 3306
}).then((repo) => {
  repo.getUsers().then(users) => {
    console.log(users);
  });
  repo.getUserByEmail('homer@thesimpsons.com').then((user) => {
    console.log(user);
  })
  //  ...when you are done...
  repo.disconnect();
});

在repository/repository.spec.js文件中也包含了一系列的单元测试。现在我们已经得到了一个仓库,可以在这个仓库中创建一个服务器。代码见如下文件

server/server.js:
//  server.js

var express = require('express');  
var morgan = require('morgan');

module.exports.start = (options) => {

  return new Promise((resolve, reject) => {

    //  Make sure we have a repository and port provided.
    if(!options.repository) throw new Error("A server must be started with a connected repository.");
    if(!options.port) throw new Error("A server must be started with a port.");

    //  Create the app, add some logging.
    var app = express();
    app.use(morgan('dev'));

    //  Add the APIs to the app.
    require('../api/users')(app, options);

    //  Start the app, creating a running server which we return.
    var server = app.listen(options.port, () => {
      resolve(server);
    });

  });
};

这个模块暴露了一个start 函数接口,我们可以以如下方式使用它:

var server = require('./server/server);  
server.start({port: 8080, repo: repository}).then((svr) => {  
  // we've got a running http server :)
});

请注意这里这里使用的 server.js 是在 api/users/js 下,具体如下:

//  users.js
//
//  Defines the users api. Add to a server by calling:
//  require('./users')
'use strict';

//  Only export - adds the API to the app with the given options.
module.exports = (app, options) => {

  app.get('/users', (req, res, next) => {
    options.repository.getUsers().then((users) => {
      res.status(200).send(users.map((user) => { return {
          email: user.email,
          phoneNumber: user.phone_number
        };
      }));
    })
    .catch(next);
  });

  app.get('/search', (req, res) => {

    //  Get the email.
    var email = req.query.email;
    if (!email) {
      throw new Error("When searching for a user, the email must be specified, e.g: '/search?email=homer@thesimpsons.com'.");
    }

    //  Get the user from the repo.
    options.repository.getUserByEmail(email).then((user) => {

      if(!user) { 
        res.status(404).send('User not found.');
      } else {
        res.status(200).send({
          email: user.email,
          phoneNumber: user.phone_number
        });
      }
    })
    .catch(next);

  });
};

上述这些文件都有相应的单元测试覆盖源代码。需要做一些配置,而不是使用一个定制化的库,一个简单的文件即可实现这个技巧,如- config/config.js:

//  config.js
//
//  Simple application configuration. Extend as needed.
module.exports = {  
    port: process.env.PORT || 8123,
  db: {
    host: process.env.DATABASE_HOST || '127.0.0.1',
    database: 'users',
    user: 'users_service',
    password: '123',
    port: 3306
  }
};

下面代码的require 可以按需配置。当前绝大多数的配置都是硬编码的,但是你可以port 为例它可以非常方便的增加环境变量作为可选模式的。最终,把这一切字符串组合在一起写在 index.js 文件中:

//    index.js
//
//  Entrypoint to the application. Opens a repository to the MySQL
//  server and starts the server.
var server = require('./server/server');  
var repository = require('./repository/repository');  
var config = require('./config/config');

//  Lots of verbose logging when we're starting up...
console.log("--- Customer Service---");  
console.log("Connecting to customer repository...");

//  Log unhandled exceptions.
process.on('uncaughtException', function(err) {  
  console.error('Unhandled Exception', err);
});
process.on('unhandledRejection', function(err, promise){  
  console.error('Unhandled Rejection', err);
});

repository.connect({  
  host: config.db.host,
  database: config.db.database,
  user: config.db.user,
  password: config.db.password,
  port: config.db.port
}).then((repo) => {
  console.log("Connected. Starting server...");

  return server.start({
    port: config.port,
    repository: repo
  });

}).then((app) => {
  console.log("Server started successfully, running on port " + config.port + ".");
  app.on('close', () => {
    repository.disconnect();
  });
});

我们会有一些小的错误需要处理,除此之外我们需要做的仅仅是加载config,创建仓库以及启动服务器。

这就是一个微服务。它允许我们获得所有的用户,以及搜索任一用户:

HTTP GET /users                              # gets all users  
HTTP GET /search?email=homer@thesimpons.com  # searches by email  

假如你checkout代码,你可以看到这里有一些可用的命令如下:

cd ./users-service  
npm install         # setup everything  
npm test            # unit test - no need for a test database running  
npm start           # run the server - you must have a test database running  
npm run debug       # run the server in debug mode, opens a browser with the inspector  
npm run lint        # check to see if the code is beautiful  

除了代码之外你可以看到我们还有如下内容:

  1. Node Inspector用于调试
  2. Mocha/shoud/supertest提供单元测试
  3. ESLint 代码检查

就这些,下面跑一个测试数据库试试:

cd test-database/  
./start.sh

然后可以看到我们的服务:

cd ../users-service/  
npm start  

现在你可以在浏览器中访问 localhost:8123/users 并看到相应回应。假如你正在使用Docker机器(例如:你跑着Mac或者Windows上),那么localhost 不会工作,你需要使用Docker机器的IP来替代上述的localhost。你可以使用命令docker-machine ip 来获取docker IP。

上面我们已经快速简略的描述了如何构建一个微服务。

第三步: Dockerising(Docker化)我们的微服务

接下来让我们享受一下docker的乐趣!这里我们已经有了一个可以跑在开发盒子里的微服务,只要有任一兼容的Node.js已经安装。接下来我们要做的就是设置我们的服务从而可以基于它创建Docker镜像,允许把服务部署到任何支持docker的地方。

这里的实现方式是通过创建一个Dockerfile。Dockerfile告诉Docker引擎应该如何创建你的镜像。我们将会在users-service 目录下创建一个简单的Dockerfile并且开始探讨如何使之适应我们的需要。

创建Dockerfile

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

# Use Node v4 as the base image.
FROM node:4

# Run node 
CMD ["node"]

然后执行下列命令创建镜像以及运行基于这个镜像的一个容器:

docker build -t node4 .    # Builds a new image  
docker run -it node4       # Run a container with this image, interactive  

先来看看build命令。

  1. docker build 这里告诉docker引擎我们需要创建一个新镜像
  2. -t node4 命名镜像标签为node4。然后我们可以通过该标签引用这个镜像。
  3. 使用当前目录作为Dockerfile文件目录

当这些命令台输出完毕之后,我们就可以看到一个新镜像创建完毕。你可以通过命令docker images查看当前系统的所有镜像。下一步的命令基于之前所做的练习你应该已经相当熟悉了:

  1. docker run 基于一个镜像运行一个新的容器run a new container from an image。
  2. -it 使用交互式终端模式。
  3. node4 我们所希望在容器中使用的镜像标签。

当我们的镜像运行起来之后,我们就得到了一个Node repl,可以通过如下命令检查版本号:

> process.version
'v4.4.0'  
> process.exit(0)

这里有潜在的可能性,docker上的node版本和你本地机器的node版本并不一致。

检查Dockerfile

纵览dockerfile我们可以很容易了解它具体在做什么:

  1. FROM node:4 在dockerfile中指定的第一件就是基本镜像。通过google的node organisation page on the docker hub可以快速检索出所有可用的镜像。这也是已经安装node.js的ubuntu的基本骨架。
  2. CMD [“node”] CMD 命令告诉docker这个镜像是支持node可执行的。当node执行终止时,容器也应该被关闭。

通过以下额外的几条命令,可以更新dockerfile来执行我们的服务:

# Use Node v4 as the base image.
FROM node:4

# Add everything in the current directory to our image, in the 'app' folder.
ADD . /app

# Install dependencies
RUN cd /app; \  
    npm install --production

# Expose our server port.
EXPOSE 8123

# Run our app.
CMD ["node", "/app/index.js"]  

这里唯一增加的内容就是会使用ADD 命令来拷贝当前目录下的所有文件到容器中的app/目录下。然后可以使用RUN 来执行镜像中的一个命令,从而安装我们的模块。最终,我们暴露服务器端口,告诉docker我们需要在端口8123上支持入向连接,之后启动我们的服务代码。

以下命令用来检查确认测试数据库服务运行,服务的运行,然后创建和再次运行镜像:

docker build -t users-service .  
docker run -it -p 8123:8123 users-service  

这里假如你在浏览器中直接访问 localhost:8123/users 将会看到错误,检查控制台终端你将会看到容器报告了一些问题:

--- Customer Service---
Connecting to customer repository...  
Connected. Starting server...  
Server started successfully, running on port 8123.  
GET /users 500 23.958 ms - 582  
Error: An error occured getting the users: Error: connect ECONNREFUSED 127.0.0.1:3306  
    at Query._callback (/app/repository/repository.js:21:25)
    at Query.Sequence.end (/app/node_modules/mysql/lib/protocol/sequences/Sequence.js:96:24)
    at /app/node_modules/mysql/lib/protocol/Protocol.js:399:18
    at Array.forEach (native)
    at /app/node_modules/mysql/lib/protocol/Protocol.js:398:13
    at nextTickCallbackWith0Args (node.js:420:9)
    at process._tickCallback (node.js:349:13)

因此,可以看出所有从我们的users-service容器到test-database容器的连接请求都被拒绝了。我们可以试着运行docker ps看看所有正在运行的容器:

CONTAINER ID  IMAGE          PORTS                   NAMES  
a97958850c66  users-service  0.0.0.0:8123->8123/tcp  kickass_perlman  
47f91343db01  mysql:latest   0.0.0.0:3306->3306/tcp  db

很显然,它们都在,那么是怎么回事呢?

连接容器

其实我们看到的这个问题是预期的,Docker容器都应该是彼此隔离的,所以创建容器之间的连接本来就是不合理的,除非我们有显式的允许它们这么做。

是的,我们可以连接我们的主机和容器,因为我们已经对此开放了端口(例如通过-p 8123:8123)。假设我们允许容器之间也能同样交流,那么在同一个主机上的两个容器能够相互沟通,甚至开发者完全没有计划这么做,那这将是一个彻底的灾难,特别是当我们在一个集群的大量机器上运行基于容器的各种不同应用程序时。

假如我们需要连接不同的容器时,我们需要连接(link)它们,它的目的在于显式的告诉docker我们允许它们相互通讯。这里有两种方式可以实现,第一种是老的模式,但是毕竟简单,第二种我们将在稍后介绍。

通过’link’ 参数连接容器

当我们在运行容器时,可以通过link 参数告诉docker我们计划连接到另一个容器。在我们的例子中,我们可以通过如下命令来正确运行我们的服务:

docker run -it -p 8123:8123 --link db:db -e DATABASE_HOST=DB users-service  
  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 并测试,一切都应该工作正常了。

它是如何工作的

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

//  config.js
//
//  Simple application configuration. Extend as needed.
module.exports = {  
    port: process.env.PORT || 8123,
  db: {
    host: process.env.DATABASE_HOST || '127.0.0.1',
    database: 'users',
    user: 'users_service',
    password: '123',
    port: 3306
  }
};

当运行容器时,我们设置了环境变量DB ,这意味着我们要连接到一个叫做DB的主机。 这是连接到容器时由docker引擎自动设置的。

要看到这个动作,可以尝试运行docker ps列出所有正在运行的容器。 查找运行的容器镜像名称users-service ,然后可以得到一个随机的名字,如下例trusting_jang :

docker ps  
CONTAINER ID  IMAGE          ...   NAMES  
ac9449d3d552  users-service  ...   trusting_jang  
47f91343db01  mysql:latest   ...   db  

现在可以看一下我们的容器上可用的主机:

docker exec trusting_jang cat /etc/hosts  
127.0.0.1    localhost  
::1    localhost ip6-localhost ip6-loopback
fe00::0    ip6-localnet  
ff00::0    ip6-mcastprefix  
ff02::1    ip6-allnodes  
ff02::2    ip6-allrouters  
172.17.0.2    db 47f91343db01    # linking magic!!  
172.17.0.3    ac9449d3d552  

还记得docker exec是工作的吗?首先选择容器名称,然后跟随的命令就是无论任何你想要在容器中执行的,在本例中 cat /etc/hosts 。

很明显hosts文件并没有什么linking魔法。所有这里你可以看到docker已经把 db加到了我们的hosts文件中,这样我们就可以通过主机名链接到容器。这就是linking的其中一个结果。以下是其他更多详细内容:

docker exec trusting_jang printenv | grep DB  
DB_PORT=tcp://172.17.0.2:3306  
DB_PORT_3306_TCP=tcp://172.17.0.2:3306  
DB_PORT_3306_TCP_ADDR=172.17.0.2  
DB_PORT_3306_TCP_PORT=3306  
DB_PORT_3306_TCP_PROTO=tcp  
DB_NAME=/trusting_jang/db  

从上述命令我们可以看到当docker连接容器时,它同样提供了一系列的环境变量以及一些非常有用的信息。我们可以知道host,tcp端口以及容器名。

至此第三步就结束了。我们已经有了一个在容器上顺利运行的MySQL数据库,也有一个既可以在本地也可以在容器上运行的node.js微服务,并且知道如何把这两个容器连接在一起。

你可以学习了解更多本阶段的代码细节,具体见step3

第四步:集成测试环境

现在,我们可以调用实际的服务器写一个集成测试,以docker的容器模式运行,调用容器化的测试数据库。

在合理范围内,我们可以使用任何一种语言或者平台来编写集成测试,但为了简便,这里我使用Node.js,就像在我们的项目中已经看到的Mocha和Supertest一样。
建立一个新的目录,命名为 integration-tests 并创建一个 index.js文件如下:

var supertest = require('supertest');  
var should = require('should');

describe('users-service', () => {

  var api = supertest('http://localhost:8123');

  it('returns a 200 for a known user', (done) => {

    api.get('/search?email=homer@thesimpsons.com')
      .expect(200, done);
  });

});

这将检查API调用并显示测试结果。只要你的users-service和test-database都在运行,测试就会通过。 然而,到了这个阶段,服务将会越来越难处理:

  1. 我们必须使用一个shell脚本来启动和停止数据库
  2. 我们必须记住针对数据库的用户服务启动命令序列
  3. 我们必须使用node直接运行集成测试

现在我们已经对docker有了更多了解,我们可以用docker解决这些问题。

简化测试数据库

当前,对于测试数据库我们有如下文件:

/test-database/start.sh
/test-database/stop.sh
/test-database/setup.sql

现在我们对docker有了更多了解,我们可以来着手改善这个。在Docker Hub的mysql image documentation 有一个提示告诉我们所有被添加到镜像/docker-entrypoint-initdb.d 目录的.sql 或 .sh 文件在配置数据库时都会被自动执行。
这就意味这我们可以用dockerfile来替代start.sh 和 stop.sh脚本。

FROM mysql:5

ENV MYSQL_ROOT_PASSWORD 123  
ENV MYSQL_DATABASE users  
ENV MYSQL_USER users_service  
ENV MYSQL_PASSWORD 123

ADD setup.sql /docker-entrypoint-initdb.d  

现在运行我们的测试数据库只需要如下命令:

docker build -t test-database .  
docker run --name db test-database  

编排(Composing)

至此,构建和运行每个容器仍然需要消耗一定时间。我们可以采用Docker Compose工具来更进一步提高。

Docker Compose可以让你创建一个文件定义系统中的每个容器,它们之间的关系,并建立或运行它们。

首先,我们需要安装 Docker Compose。然后在根目录下创建如下新文件命名为 docker-compose.yml:

version: '2'  
services:  
  users-service:
    build: ./users-service
    ports:
     - "8123:8123"
    depends_on:
     - db
    environment:
     - DATABASE_HOST=db
  db:
    build: ./test-database

然后检查运行结果:

docker-compose build  
docker-compose up  

Docker Compose已经创建了我们应用程序所需的所有镜像,基于它们创建了相应容器,并以正确的序列执行它们从来启动完整的技术栈。

docker-compose build 命令负责创建在文件docker-compose.yml中列出的所有镜像。

version: '2'  
services:  
  users-service:
    build: ./users-service
    ports:
     - "8123:8123"
    depends_on:
     - db
    environment:
     - DATABASE_HOST=db
  db:
    build: ./test-database

这里的 build 值是我们的每个服务告诉docker到哪里可以找到相应的Dockerfile。当执行 docker-compose up时,docker启动所有的服务。请注意,从Dockerfile 我们可以指定端口和依赖关系。事实上,这里有大量的配置是我们可以改变的。在另一个终端执行 docker compose down 正常关闭所有的容器。

总结

在本文中我们已经看了大量docker的介绍,但是这远远不够。我希望这些能够带你发现一些感兴趣和实用的东西,从而帮助你在实际的工作中应用docker。

像往常一样,非常欢迎给我问题和建议。同时我也强烈推荐阅读下列文档: Understanding Docker 从而得到对docker工作机制更深的理解。

你也可以访问以下链接得到本文中项目最终的所有源代码:
github.com/dwmkerr/node-docker-mircroservice

提示

  1. 拷贝一切是个坏主意,因为我们会同时拷贝node_modules目录。通常,你最好是显式的列出需要拷贝的文件和目录,或者使用一个.dockerignore,就像使用.gitignore一样。
  2. 假如服务器并没有按预期运行,这可能并不是真正麻烦的异常,而可能是supertest的一个bug。具体见github.com/visionmedia/supertest/issues/314

原文链接: http://www.dwmkerr.com/learn-docker-by-building-a-microservice/

翻译:王旭敏,Nokia开发工程师,关注云计算、高性能及可用架构、容器等。
责编:魏伟,欢迎加入微服务架构群探讨技术和经验,微信搜索“k15751091376”进入。

评论