• 如何构建一个 NodeJS 影院微服务并使用 Docker 部署


    在这里插入图片描述

    前言

    如何构建一个 NodeJS 影院微服务并使用 Docker 部署。在这个系列中,将构建一个 NodeJS 微服务,并使用 Docker Swarm 集群进行部署。

    以下是将要使用的工具:

    • NodeJS 版本7.2.0
    • MongoDB 3.4.1
    • Docker for Mac 1.12.6

    在尝试本指南之前,应该具备:

    • NodeJS 的基本知识
    • Docker 的基本知识(并且已经安装了 Docker)
    • MongoDB 的基本知识(并且数据库服务正在运行)

    什么是微服务?

    微服务是一个单独的自包含单元,与其他许多单元一起构成一个大型应用程序。通过将应用程序拆分为小单元,每个部分都可以独立部署和扩展,可以由不同的团队和不同的编程语言编写,并且可以单独进行测试。

    微服务架构意味着应用程序由许多较小的、独立的应用程序组成,这些应用程序能够在自己的内存空间中运行,并且可以在可能的多个独立计算机上独立扩展。

    微服务的好处:

    • 应用程序启动更快,这使得开发人员更具生产力,并加快了部署速度。
    • 每个服务可以独立于其他服务部署 — 更容易频繁部署服务的新版本。
    • 更容易扩展开发,也可能具有性能优势。
    • 消除对技术栈的长期承诺。在开发新服务时,可以选择新的技术栈。
    • 微服务通常更好组织,因为每个微服务有一个非常具体的工作,不涉及其他组件的工作。
    • 解耦的服务也更容易重新组合和重新配置,以服务不同应用程序的目的(例如,同时为 Web 客户端和公共 API 提供服务)。

    微服务的缺点:

    • 开发人员必须处理创建分布式系统的额外复杂性。
    • 部署复杂性。在生产环境中,部署和管理许多不同服务类型的系统也会带来操作复杂性。
    • 在构建新的微服务架构时,可能会发现许多交叉关注点,这些交叉关注点在设计时没有预料到。

    构建电影目录微服务

    假设正在一家电影院的 IT 部门工作,给了我们一个任务,将他们的电影票务和杂货店从单体系统重构为微服务。

    因此,在“构建 NodeJS 电影目录微服务”系列中,将仅关注电影目录服务。

    在这个架构中,可以看到有 3 种不同的设备使用该微服务,即 POS(销售点)、移动设备/平板电脑和计算机。POS 和移动设备/平板电脑都有自己的应用程序(在 electron 中开发),并直接使用微服务,而计算机则通过 Web 应用程序访问微服务(一些专家也将 Web 应用程序视为微服务)。

    构建微服务

    现在,来模拟在最喜欢的电影院预订一场电影首映的过程。

    首先,想看看电影院目前正在上映哪些电影。以下图表显示了通过 REST 进行的内部通信,通过此 REST 通信,可以使用 API 来获取目前正在上映的电影。

    电影服务的 API 将具有以下 RAML 规范:

    #%RAML 1.0
    title: cinema
    version: v1
    baseUri: /
    
    types:
      Movie:
        properties:
          id: string
          title: string
          runtime: number
          format: string
          plot: string
          releaseYear: number
          releaseMonth: number
          releaseDay: number
        example:
          id: "123"
          title: "Assasins Creed"
          runtime: 115
          format: "IMAX"
          plot: "Lorem ipsum dolor sit amet"
          releaseYear : 2017
          releaseMonth: 1
          releaseDay: 6
    
      MoviePremieres:
        type: Movie []
    
    
    resourceTypes:
      Collection:
        get:
          responses:
            200:
              body:
                application/json:
                  type: <>
    
    /movies:
      /premieres:
        type:  { Collection: {item : MoviePremieres } }
    
      /{id}:
        type:  { Collection: {item : Movie } }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    如果不了解 RAML,可以查看这个很好的教程。

    API 项目的结构将如下所示:

    • api/ # 我们的API
    • config/ # 应用程序配置
    • mock/ # 不是必需的,仅用于数据示例
    • repository/ # 抽象出数据库
    • server/ # 服务器设置代码
    • package.json # 依赖项
    • index.js # 应用程序的主入口

    首先要看的部分是 repository。这是对数据库进行查询的地方。

    
    'use strict'
    // factory function, that holds an open connection to the db,
    // and exposes some functions for accessing the data.
    const repository = (db) => {
      
      // since this is the movies-service, we already know
      // that we are going to query the `movies` collection
      // in all of our functions.
      const collection = db.collection('movies')
    
      const getMoviePremiers = () => {
        return new Promise((resolve, reject) => {
          const movies = []
          const currentDay = new Date()
          const query = {
            releaseYear: {
              $gt: currentDay.getFullYear() - 1,
              $lte: currentDay.getFullYear()
            },
            releaseMonth: {
              $gte: currentDay.getMonth() + 1,
              $lte: currentDay.getMonth() + 2
            },
            releaseDay: {
              $lte: currentDay.getDate()
            }
          }
          const cursor = collection.find(query)
          const addMovie = (movie) => {
            movies.push(movie)
          }
          const sendMovies = (err) => {
            if (err) {
              reject(new Error('An error occured fetching all movies, err:' + err))
            }
            resolve(movies)
          }
          cursor.forEach(addMovie, sendMovies)
        })
      }
    
      const getMovieById = (id) => {
        return new Promise((resolve, reject) => {
          const projection = { _id: 0, id: 1, title: 1, format: 1 }
          const sendMovie = (err, movie) => {
            if (err) {
              reject(new Error(`An error occured fetching a movie with id: ${id}, err: ${err}`))
            }
            resolve(movie)
          }
          // fetch a movie by id -- mongodb syntax
          collection.findOne({id: id}, projection, sendMovie)
        })
      }
      
      // this will close the database connection
      const disconnect = () => {
        db.close()
      }
    
      return Object.create({
        getAllMovies,
        getMoviePremiers,
        getMovieById,
        disconnect
      })
    }
    
    const connect = (connection) => {
      return new Promise((resolve, reject) => {
        if (!connection) {
          reject(new Error('connection db not supplied!'))
        }
        resolve(repository(connection))
      })
    }
    // this only exports a connected repo
    module.exports = Object.assign({}, {connect})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79

    可能已经注意到,向 repository 的 connect ( connection ) 方法提供了一个 connection 对象。在这里,使用了 JavaScript 的一个重要特性“闭包”,repository 对象返回了一个闭包,其中的每个函数都可以访问 db 对象和 collection 对象,db 对象保存着数据库连接。在这里,抽象了连接的数据库类型,repository 对象不知道数据库是什么类型的,对于这种情况来说,是一个 MongoDB 连接。甚至不需要知道是单个数据库还是复制集连接。虽然使用了 MongoDB 语法,但可以通过应用 SOLID 原则中的依赖反转原则,将存储库功能抽象得更深,将 MongoDB 语法转移到另一个文件中,并只调用数据库操作的接口(例如,使用 mongoose 模型)。

    有一个 repository/repository.spec.js 文件来测试这个模块,稍后在文章中会谈到测试。

    接下来要看的是 server.js 文件。

    'use strict'
    const express = require('express')
    const morgan = require('morgan')
    const helmet = require('helmet')
    const movieAPI = require('../api/movies')
    
    const start = (options) => {
      return new Promise((resolve, reject) => {
        // we need to verify if we have a repository added and a server port
        if (!options.repo) {
          reject(new Error('The server must be started with a connected repository'))
        }
        if (!options.port) {
          reject(new Error('The server must be started with an available port'))
        }
        // let's init a express app, and add some middlewares
        const app = express()
        app.use(morgan('dev'))
        app.use(helmet())
        app.use((err, req, res, next) => {
          reject(new Error('Something went wrong!, err:' + err))
          res.status(500).send('Something went wrong!')
        })
        
        // we add our API's to the express app
        movieAPI(app, options)
        
        // finally we start the server, and return the newly created server 
        const server = app.listen(options.port, () => resolve(server))
      })
    }
    
    module.exports = Object.assign({}, {start})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这里,创建了一个新的 express 应用程序,验证是否提供了 repository 和 server port 对象,然后为 express 应用程序应用一些中间件,例如用于日志记录的 morgan,用于安全性的 helmet,以及一个错误处理函数,最后导出一个 start 函数来启动服务器。

    Helmet 包含了整整 11 个软件包,它们都用于阻止恶意方破坏或使用应用程序来伤害其用户。

    好的,现在既然服务器使用了电影的 API,继续查看 movies.js 文件。

    'use strict'
    const status = require('http-status')
    
    module.exports = (app, options) => {
      const {repo} = options
      
      // here we get all the movies 
      app.get('/movies', (req, res, next) => {
        repo.getAllMovies().then(movies => {
          res.status(status.OK).json(movies)
        }).catch(next)
      })
      
      // here we retrieve only the premieres
      app.get('/movies/premieres', (req, res, next) => {
        repo.getMoviePremiers().then(movies => {
          res.status(status.OK).json(movies)
        }).catch(next)
      })
      
      // here we get a movie by id
      app.get('/movies/:id', (req, res, next) => {
        repo.getMovieById(req.params.id).then(movie => {
          res.status(status.OK).json(movie)
        }).catch(next)
      })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这里,为API创建了路由,并根据监听的路由调用了 repo 函数。repo 在这里使用了接口技术方法,在这里使用了著名的“为接口编码而不是为实现编码”,因为 express 路由不知道是否有一个数据库对象、数据库查询逻辑等,它只调用处理所有数据库问题的 repo 函数。

    所有文件都有与源代码相邻的单元测试,看看 movies.js 的测试是如何进行的。

    可以将测试看作是对正在构建的应用程序的安全保障。不仅会在本地机器上运行,还会在 CI 服务上运行,以确保失败的构建不会被推送到生产系统。

    为了编写单元测试,必须对所有依赖项进行存根,即为模块提供虚拟依赖项。看看 spec 文件。

    /* eslint-env mocha */
    const request = require('supertest')
    const server = require('../server/server')
    
    describe('Movies API', () => {
      let app = null
      let testMovies = [{
        'id': '3',
        'title': 'xXx: Reactivado',
        'format': 'IMAX',
        'releaseYear': 2017,
        'releaseMonth': 1,
        'releaseDay': 20
      }, {
        'id': '4',
        'title': 'Resident Evil: Capitulo Final',
        'format': 'IMAX',
        'releaseYear': 2017,
        'releaseMonth': 1,
        'releaseDay': 27
      }, {
        'id': '1',
        'title': 'Assasins Creed',
        'format': 'IMAX',
        'releaseYear': 2017,
        'releaseMonth': 1,
        'releaseDay': 6
      }]
    
      let testRepo = {
        getAllMovies () {
          return Promise.resolve(testMovies)
        },
        getMoviePremiers () {
          return Promise.resolve(testMovies.filter(movie => movie.releaseYear === 2017))
        },
        getMovieById (id) {
          return Promise.resolve(testMovies.find(movie => movie.id === id))
        }
      }
    
      beforeEach(() => {
        return server.start({
          port: 3000,
          repo: testRepo
        }).then(serv => {
          app = serv
        })
      })
    
      afterEach(() => {
        app.close()
        app = null
      })
    
      it('can return all movies', (done) => {
        request(app)
          .get('/movies')
          .expect((res) => {
            res.body.should.containEql({
              'id': '1',
              'title': 'Assasins Creed',
              'format': 'IMAX',
              'releaseYear': 2017,
              'releaseMonth': 1,
              'releaseDay': 6
            })
          })
          .expect(200, done)
      })
    
      it('can get movie premiers', (done) => {
        request(app)
        .get('/movies/premiers')
        .expect((res) => {
          res.body.should.containEql({
            'id': '1',
            'title': 'Assasins Creed',
            'format': 'IMAX',
            'releaseYear': 2017,
            'releaseMonth': 1,
            'releaseDay': 6
          })
        })
        .expect(200, done)
      })
    
      it('returns 200 for an known movie', (done) => {
        request(app)
          .get('/movies/1')
          .expect((res) => {
            res.body.should.containEql({
              'id': '1',
              'title': 'Assasins Creed',
              'format': 'IMAX',
              'releaseYear': 2017,
              'releaseMonth': 1,
              'releaseDay': 6
            })
          })
          .expect(200, done)
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    /* eslint-env mocha */
    const server = require('./server')
    
    describe('Server', () => {
      it('should require a port to start', () => {
        return server.start({
          repo: {}
        }).should.be.rejectedWith(/port/)
      })
    
      it('should require a repository to start', () => {
        return server.start({
          port: {}
        }).should.be.rejectedWith(/repository/)
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看到,为 movies API 存根了依赖项,并验证了需要提供一个 server port 和一个 repository 对象。

    继续看一下如何创建传递给 repository 模块的 db 连接对象,现在定义说每个微服务都必须有自己的数据库,但是对于示例,将使用一个 MongoDB 复制集服务器,但每个微服务都有自己的数据库。

    从 NodeJS 连接到 MongoDB 数据库

    以下是需要从 NodeJS 连接到 MongoDB 数据库的配置。

    const MongoClient = require('mongodb')
    
    // here we create the url connection string that the driver needs
    const getMongoURL = (options) => {
      const url = options.servers
        .reduce((prev, cur) => prev + `${cur.ip}:${cur.port},`, 'mongodb://')
    
      return `${url.substr(0, url.length - 1)}/${options.db}`
    }
    
    // mongoDB function to connect, open and authenticate
    const connect = (options, mediator) => {
      mediator.once('boot.ready', () => {
        MongoClient.connect( getMongoURL(options), {
            db: options.dbParameters(),
            server: options.serverParameters(),
            replset: options.replsetParameters(options.repl)
          }, (err, db) => {
            if (err) {
              mediator.emit('db.error', err)
            }
    
            db.admin().authenticate(options.user, options.pass, (err, result) => {
              if (err) {
                mediator.emit('db.error', err)
              }
              mediator.emit('db.ready', db)
            })
          })
      })
    }
    
    module.exports = Object.assign({}, {connect})
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    这里可能有更好的方法,但基本上可以这样创建与 MongoDB 的复制集连接。

    传递了一个 options 对象,其中包含 Mongo 连接所需的所有参数,并且传递了一个事件 — 中介者对象,当通过认证过程时,它将发出 db 对象。

    注意 在这里,使用了一个事件发射器对象,因为使用 promise 的方法在某种程度上并没有在通过认证后返回 db 对象,顺序变得空闲。所以这可能是一个很好的挑战,看看发生了什么,并尝试使用 promise 的方法。

    现在,既然正在传递一个 options 对象来进行参数设置,让我们看看这是从哪里来的,因此要查看的下一个文件是 config.js。

    // simple configuration file
    
    // database parameters
    const dbSettings = {
      db: process.env.DB || 'movies',
      user: process.env.DB_USER || 'cristian',
      pass: process.env.DB_PASS || 'cristianPassword2017',
      repl: process.env.DB_REPLS || 'rs1',
      servers: (process.env.DB_SERVERS) ? process.env.DB_SERVERS.split(' ') : [
        '192.168.99.100:27017',
        '192.168.99.101:27017',
        '192.168.99.102:27017'
      ],
      dbParameters: () => ({
        w: 'majority',
        wtimeout: 10000,
        j: true,
        readPreference: 'ReadPreference.SECONDARY_PREFERRED',
        native_parser: false
      }),
      serverParameters: () => ({
        autoReconnect: true,
        poolSize: 10,
        socketoptions: {
          keepAlive: 300,
          connectTimeoutMS: 30000,
          socketTimeoutMS: 30000
        }
      }),
      replsetParameters: (replset = 'rs1') => ({
        replicaSet: replset,
        ha: true,
        haInterval: 10000,
        poolSize: 10,
        socketoptions: {
          keepAlive: 300,
          connectTimeoutMS: 30000,
          socketTimeoutMS: 30000
        }
      })
    }
    
    // server parameters
    const serverSettings = {
      port: process.env.PORT || 3000
    }
    
    module.exports = Object.assign({}, { dbSettings, serverSettings })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    这是配置文件,大部分配置代码都是硬编码的,但正如看到的,一些属性使用环境变量作为选项。环境变量被视为最佳实践,因为这可以隐藏数据库凭据、服务器参数等。

    最后,对于构建电影服务 API 的最后一步是使用 index.js 将所有内容组合在一起。

    'use strict'
    // we load all the depencies we need
    const {EventEmitter} = require('events')
    const server = require('./server/server')
    const repository = require('./repository/repository')
    const config = require('./config/')
    const mediator = new EventEmitter()
    
    // verbose logging when we are starting the server
    console.log('--- Movies Service ---')
    console.log('Connecting to movies repository...')
    
    // log unhandled execpetions
    process.on('uncaughtException', (err) => {
      console.error('Unhandled Exception', err)
    })
    process.on('uncaughtRejection', (err, promise) => {
      console.error('Unhandled Rejection', err)
    })
    
    // event listener when the repository has been connected
    mediator.on('db.ready', (db) => {
      let rep
      repository.connect(db)
        .then(repo => {
          console.log('Repository Connected. Starting Server')
          rep = repo
          return server.start({
            port: config.serverSettings.port,
            repo
          })
        })
        .then(app => {
          console.log(`Server started succesfully, running on port: ${config.serverSettings.port}.`)
          app.on('close', () => {
            rep.disconnect()
          })
        })
    })
    mediator.on('db.error', (err) => {
      console.error(err)
    })
    
    // we load the connection to the repository
    config.db.connect(config.dbSettings, mediator)
    // init the repository connection, and the event listener will handle the rest
    mediator.emit('boot.ready')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    在这里,组合了所有的电影 API 服务,添加了一些错误处理,然后加载配置、启动存储库,并最后启动服务器。

    因此,到目前为止,已经完成了与 API 开发相关的所有内容。

    下面是项目中需要用到的初始化以及运行命令:

    • npm install # 设置Node依赖项
    • npm test # 使用mocha进行单元测试
    • npm start # 启动服务
    • npm run node-debug # 以调试模式运行服务器
    • npm run chrome-debug # 使用chrome调试Node
    • npm run lint # 使用standard进行代码lint

    最后,第一个微服务已经在本地运行,并通过执行 npm start 命令启动。

    现在是时候将其放入 Docker 容器中。

    首先,需要使用“使用 Docker 部署 MongoDB 复制集”的文章中的 Docker 环境,如果没有,则需要进行一些额外的修改步骤,以便为微服务设置数据库,以下是一些命令,进行测试电影服务。

    首先创建 Dockerfile,将 NodeJS 微服务制作成 Docker 容器。

    # Node v7作为基本映像以支持ES6
    FROM node:7.2.0
    # 为新容器创建一个新用户,并避免root用户
    RUN useradd --user-group --create-home --shell /bin/false nupp && \
        apt-get clean
    ENV HOME=/home/nupp
    COPY package.json npm-shrinkwrap.json $HOME/app/
    COPY src/ $HOME/app/src
    RUN chown -R nupp:nupp $HOME/* /usr/local/
    WORKDIR $HOME/app
    RUN npm cache clean && \
        npm install --silent --progress=false --production
    RUN chown -R nupp:nupp $HOME/*
    USER nupp
    EXPOSE 3000
    CMD ["npm", "start"]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    使用 NodeJS 镜像作为 Docker 镜像的基础,然后为镜像创建一个用户以避免非 root 用户,接下来,将 src 复制到镜像中,然后安装依赖项,暴露一个端口号,并最后实例化电影服务。

    接下来,需要构建 Docker 镜像,使用以下命令:

    $ docker build -t movies-service .
    
    • 1

    首先看一下构建命令。

    • docker build 告诉引擎要创建一个新的镜像。
    • -t movies-service 用标记 movies-service 标记此镜像。从现在开始,可以根据标记引用此镜像。
    • .:使用当前目录来查找 Dockerfile

    经过一些控制台输出后,新镜像中就有了 NodeJS 应用程序,所以现在需要做的就是使用以下命令运行镜像:

    $ docker run --name movie-service -p 3000:3000 -e DB_SERVERS="192.168.99.100:27017 192.168.99.101:27017 192.168.99.100:27017" -d movies-service
    
    • 1

    在上面的命令中,传递了一个环境变量,它是一个服务器数组,需要连接到 MongoDB 复制集的服务器,这只是为了说明,有更好的方法可以做到这一点,比如读取一个环境变量文件。

    现在,容器已经运行起来了,获取 docker-machine IP地址,以获取微服务的 IP 地址,现在准备对微服务进行一次集成测试,另一个测试选项可以是JMeter,它是一个很好的工具,可以模拟HTTP请求。

    这是集成测试,将检查一个 API 调用。

    /* eslint-env mocha */
    const supertest = require('supertest')
    
    describe('movies-service', () => {
    
      const api = supertest('http://192.168.99.100:3000')
    
      it('returns a 200 for a collection of movies', (done) => {
    
        api.get('/movies/premiers')
          .expect(200, done)
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    总结

    创建了用于查询电影院正在上映的电影的 movies 服务,使用 RAML 规范设计了 API,然后开始构建 API,并进行了相应的单元测试,最后,组合了所有内容,使微服务完整,并能够启动 movies 服务服务器。

    然后,将微服务放入 Docker 容器中,以进行一些集成测试。

    微服务架构可以为大型应用程序带来许多好处,但也需要小心管理和设计,以处理分布式系统的复杂性和其他挑战。使用 Docker 容器可以简化微服务的部署和管理,使其更加灵活和可扩展。

  • 相关阅读:
    从0开始的ios自动化测试
    Python Day4爬虫-selenium滚动和常见反爬
    JDBC之在IDEA中连接mysql
    Go基础——接口、并发
    ElasticSearch中基础API操作
    怎么用docker将项目打包成镜像并导出给别人适用 (dockerfile)
    java学习--day8 (面向对象)
    鸿蒙(API 12 Beta2版)媒体开发【使用OHAudio开发音频播放功能(C/C++)】
    2022-11-27阿里云物联网平台 MICROPYTHON记录
    【WLAN】Android 13 WIFI 选网机制--NetworkNominator 解读
  • 原文地址:https://blog.csdn.net/qq_36478920/article/details/132275833