通过构建Microservice来学习 Docker

分享于 

27分钟阅读

数据库

  繁體

在这篇文章中,我将向展示Docker是如何工作的,以及Docker如何帮助你完成一个基本的开发任务-构建一个微服务。

我们将以带有MySQL后端的简单Node.js服务为例,从本地运行的代码到运行微服务和数据库的容器。

安装Docker

要继续使用本文,你需要Docker。

安装指南,docs.docker.com/engine/installation

试试看

输入以下命令:

<code>docker run -it ubuntu </code>

您会看到如下提示:

<code>root@719059da250d:/# </code>

尝试一些命令,然后退出容器:

<code>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 </code>

尝试以下操作:

<code>docker run -it haskell 
docker run -it java 
docker run -it python </code>

你可以看到,运行一个环境非常简单。

以一个简单的microservice为例。

简要

我们将构建一个微服务,使我们可以使用Node.js和MySQL管理电子邮件地址和电话号码目录。

入门

为了进行本地开发,我们需要安装MySQL,并为我们创建一个测试数据库。

...不用啦。

步骤1 :在Docker中创建测试数据库服务器

这是一个很好的Docker用例,

运行以下命令:

<code>docker run --name db -e MYSQL_ROOT_PASSWORD=123 -p 3306:3306 mysql:latest </code>

这将启动一个运行MySQL实例,允许使用root密码123通过端口3306进行访问。

  • docker run告诉引擎我们要运行一个镜像(镜像在末尾,mysql:vlatest
  • 看看哪些容器正在运行:

    <code>$ docker ps
    CONTAINER ID IMAGE.. . NAMES 
    36e68b966fd0 mysql:latest.. . db </code>

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

    <code>$ docker exec -it db/bin/bash
    root@36e68b966fd0:/# mysql -uroot -p123 
    mysql> show databases; 
    +--------------------+
    | Database |
    +--------------------+
    | information_schema |
    +--------------------+1 rows inset (0.01 sec)
    mysql> exit 
    Bye 
    root@36e68b966fd0:/# exit </code>
  • docker exec -it db
  • 包装测试数据库

    现在,创建一个脚本的test_database文件夹来启动数据库,停止数据库,并设置测试数据:

    <code>test_databasesetup.sh 
    test_databasestart.sh 
    test_databasestop.sh </code>

    开始很简单:

    <code class="language-bash">#!/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 </code>

    setup.sql是:

    <code class="language-sql">create table directory (user_id INTNOTNULL AUTO_INCREMENT PRIMARYKEY, email TEXT, phone_number TEXT); insertinto directory (email, phone_number) values ('homer@thesimpsons.com', '+1 888 123 1111'); insertinto directory (email, phone_number) values ('marge@thesimpsons.com', '+1 888 123 1112'); insertinto directory (email, phone_number) values ('maggie@thesimpsons.com', '+1 888 123 1113'); insertinto directory (email, phone_number) values ('lisa@thesimpsons.com', '+1 888 123 1114'); insertinto directory (email, phone_number) values ('bart@thesimpsons.com', '+1 888 123 1115'); 
    </code>
    <code class="language-bash">#!/bin/sh
    # Stop the db and remove the container.
    docker stop db && docker rm db </code>

    步骤2:在Node.js中创建Microservice

    本文的重点是学习Docker,所以我不会在Node.js microservice上花时间,

    <code>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</code>

    要查看的第一部分是repository,可以用某种类或抽象包装数据库访问,以允许模拟它,以便进行测试:

    <code class="language-javascript">// 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() {
     returnnew 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) {
     returnnew 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) => { 
     returnnew Promise((resolve, reject) => {
     if(!connectionSettings.host) thrownew Error("A host must be specified.");
     if(!connectionSettings.user) thrownew Error("A user must be specified.");
     if(!connectionSettings.password) thrownew Error("A password must be specified.");
     if(!connectionSettings.port) thrownew Error("A port must be specified.");
     resolve(new Repository(mysql.createConnection(connectionSettings)));
     });
    };</code>

    我们可以创建一个repository对象,如下所示:

    <code class="language-javascript">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();
    });</code>

    repository/repository.spec.js文件中还有一组单元测试,现在我们有了repo,我们可以创建一个服务器,这是server/server.js

    <code class="language-javascript">// server.jsvar express = require('express'); var morgan = require('morgan');
    module.exports.start = (options) => {
     returnnew Promise((resolve, reject) => {
     // Make sure we have a repository and port provided.if(!options.repository) thrownew Error("A server must be started with a connected repository.");
     if(!options.port) thrownew 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);
     });
     });
    };</code>

    这个模块公开了一个start函数,我们可以这样使用:

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

    注意,server.js使用api/users/js

    <code class="language-javascript">// 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) {
     thrownew 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);
     });
    };</code>

    我们将需要配置。而不是使用专门的库,一个简单的文件就能解决问题-config/config.js:

    <code class="language-javascript">// 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 }
    };</code>

    我们可以根据需要require配置,目前,大多数配置都是硬编码的,但是从port可以看到,将环境变量作为选项很容易。

    最后一步-将其与组成所有内容的index.js文件在一起:

    <code class="language-javascript">// 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();
     });
    });</code>

    我们有一些错误要处理,除此之外,我们只是加载配置,创建存储库并启动服务器。

    这是microservice,它允许获取所有用户,或搜索用户:

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

    如果签出代码,你将看到有几个可用的命令:

    <code>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 </code>

    就是这样!

    使用以下命令运行测试数据库:

    <code>cd test-database/
    ./start.sh</code>

    然后使用:

    <code>cd../users-service/
    npm start </code>

    你可以将浏览器指向本地主机:,并实际看到它。

    我们已经快速构建服务,

    步骤3:Dockerising Microservice

    好了,现在有趣了!

    创建Dockerfile

    创建一个Dockerfile的新文本文件,位于users-service/,内容如下:

    <code># Use Node v4 as the base image.
    FROM node:4# Run node 
    CMD ["node"]</code>

    现在运行下面的命令来构建镜像,并从中运行容器:

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

    运行此镜像时,我们得到一个node repl,检查当前版本,如下所示:

    <code class="language-javascript">> process.version'v4.4.0'> process.exit(0)</code>

    检查Dockerfile

    通过添加更多命令,我们可以更新Dockerfile,以便它运行服务:

    <code># 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"] </code>

    确保测试数据库服务正在运行,然后再次生成,并运行镜像:

    <code>docker build -t users-service. 
    docker run -it -p 8123:8123 users-service </code>

    如果在浏览器中导航到localhost:8123/users,你应该会看到一个错误,检查控制台显示容器报告了一些问题:

    <code>--- Customer Service---
    Connecting to customer repository... 
    Connected. Starting server... 
    Server started successfully, running on port 8123. 
    GET/users 50023.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)</code>

    所以从users-service容器到test-database容器的连接被拒绝,我们尝试运行docker ps以查看所有正在运行的容器:

    <code>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</code>

    链接容器

    如果要从一个容器连接到另一个容器,我们需要将它们链接起来,这告诉Docker,我们显式地希望允许两个容器之间的通信,这样做有两种方法,第一种是"老式的",但很简单,第二种我们稍后再介绍。

    使用'link'参数链接容器

    在运行容器时,我们可以告诉Docker,我们打算使用link参数连接到另一个容器,在例子中,我们可以像这样正确运行服务:

    <code>docker run -it -p 8123:8123 --link db:db -e DATABASE_HOST=DB users-service </code>
  • docker run -it使用交互式终端在容器中运行Docker镜像
  • -p 8123:8123将主机端口8123映射到容器端口8123
  • link db:db链接到名为db的容器,并将它作为db引用
  • -e DATABASE_HOST=dbDATABASE_HOST环境变量设置为db
  • users-service要在容器中运行的镜像的名称
  • 现在,当我们转到localhost:8123/users时,一切正常。

    它允许使用环境变量指定数据库主机:

    <code class="language-javascript">// 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 }
    };</code>

    当我们运行容器时,这个环境变量设置为db,这意味着我们正在连接到一个db的主机,当我们链接到一个容器时,Docker引擎会自动为我们设置。

    要查看实际运行情况,请尝试运行docker ps列出所有正在运行的容器,查找运行users-service的容器的名称,该名称是一个随机名称,例如trusting_jang

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

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

    <code>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 </code>

    请记住docker exec的工作方式>选择容器名,然后执行下面的命令,在例子中是cat/etc/hosts

    ok hosts文件没有# linking magic注释,也就是说,你可以看到已经添加了db,因此我们引用了,!这是链接的一个结果,下面是另一个:

    <code>docker exec trusting_jang printenv | grep DB 
    DB_PORT=tcp://172.17.0.2:3306DB_PORT_3306_TCP=tcp://172.17.0.2:3306DB_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 </code>

    从这个命令可以看到,当Docker链接一个容器时,它还提供了一组环境变量,其中包含一些有用的信息,我们知道主机,tcp端口和容器名称。

    步骤4:集成测试环境

    现在我们可以编写一个集成测试来调用实际的服务器,作为Docker容器运行,调用containerised测试数据库。

    在一个integration-tests的新文件夹中,我们只有一个index.js

    <code class="language-javascript">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);
     });
    });</code>

    这将检查一个API调用,并显示测试结果2

    只要users-servicetest-database正在运行,测试就会通过,然而,在这个阶段,服务正在变得越来越难处理:

  • 必须使用shell script启动和停止数据库
  • 我们必须记住一系列命令来对数据库启动用户服务
  • 我们必须直接使用node来运行集成测试
  • 既然我们更熟悉Docker,我们可以修复这些问题。

    Simplifiying测试数据库

    目前我们有以下测试数据库文件:

    <code>/test-database/start.sh
    /test-database/stop.sh
    /test-database/setup.sql</code>

    既然我们更喜欢Docker,我们可以对此进行改进,

    这意味着我们可以用start.shstop.sh脚本替换Dockerfile

    <code>FROM mysql:5ENV MYSQL_ROOT_PASSWORD 123 
    ENV MYSQL_DATABASE users 
    ENV MYSQL_USER users_service 
    ENV MYSQL_PASSWORD 123ADD setup.sql/docker-entrypoint-initdb.d </code>

    现在运行测试数据库只是:

    <code>docker build -t test-database. 
    docker run --name db test-database </code>

    构建和每个容器仍然有些耗时,我们可以使用Docker Compose工具更进一步。

    Docker Compose允许你创建一个文件,该文件定义系统中的每个容器,它们之间的关系,以及构建或运行它们。

    首先,安装Docker Compose,现在在root中创建一个docker-compose.yml的新文件:

    <code class="language-yml">version: '2' 
    services: 
     users-service:
     build:./users-service
     ports:
     - "8123:8123" depends_on:
     - db
     environment:
     - DATABASE_HOST=db
     db:
     build:./test-database</code>

    现在看看这个:

    <code>docker-compose build 
    docker-compose up </code>

    Docker Compose已经构建了我们应用程序所需的所有镜像,并从中创建了容器,并以正确的顺序运行它们并启动了整个堆栈!

    docker-compose build命令生成docker-compose.yml文件中列出的每个镜像:

    <code class="language-yml">version: '2' 
    services: 
     users-service:
     build:./users-service
     ports:
     - "8123:8123" depends_on:
     - db
     environment:
     - DATABASE_HOST=db
     db:
     build:./test-database</code>

    DOC  构建  micr  Docker  learn  DOCK  
    相关文章