如何構(gòu)建一個?NodeJS?影院微服務并使用?Docker?部署
前言
如何構(gòu)建一個 NodeJS 影院微服務并使用 Docker 部署。在這個系列中,將構(gòu)建一個 NodeJS 微服務,并使用 Docker Swarm 集群進行部署。
以下是將要使用的工具:
- NodeJS 版本7.2.0
- MongoDB 3.4.1
- Docker for Mac 1.12.6
在嘗試本指南之前,應該具備:
- NodeJS 的基本知識
- Docker 的基本知識(并且已經(jīng)安裝了 Docker)
- MongoDB 的基本知識(并且數(shù)據(jù)庫服務正在運行)
什么是微服務?
微服務是一個單獨的自包含單元,與其他許多單元一起構(gòu)成一個大型應用程序。通過將應用程序拆分為小單元,每個部分都可以獨立部署和擴展,可以由不同的團隊和不同的編程語言編寫,并且可以單獨進行測試。
微服務架構(gòu)意味著應用程序由許多較小的、獨立的應用程序組成,這些應用程序能夠在自己的內(nèi)存空間中運行,并且可以在可能的多個獨立計算機上獨立擴展。
微服務的好處:
- 應用程序啟動更快,這使得開發(fā)人員更具生產(chǎn)力,并加快了部署速度。
- 每個服務可以獨立于其他服務部署 — 更容易頻繁部署服務的新版本。
- 更容易擴展開發(fā),也可能具有性能優(yōu)勢。
- 消除對技術(shù)棧的長期承諾。在開發(fā)新服務時,可以選擇新的技術(shù)棧。
- 微服務通常更好組織,因為每個微服務有一個非常具體的工作,不涉及其他組件的工作。
- 解耦的服務也更容易重新組合和重新配置,以服務不同應用程序的目的(例如,同時為 Web 客戶端和公共 API 提供服務)。
微服務的缺點:
- 開發(fā)人員必須處理創(chuàng)建分布式系統(tǒng)的額外復雜性。
- 部署復雜性。在生產(chǎn)環(huán)境中,部署和管理許多不同服務類型的系統(tǒng)也會帶來操作復雜性。
- 在構(gòu)建新的微服務架構(gòu)時,可能會發(fā)現(xiàn)許多交叉關(guān)注點,這些交叉關(guān)注點在設計時沒有預料到。
構(gòu)建電影目錄微服務

假設正在一家電影院的 IT 部門工作,給了我們一個任務,將他們的電影票務和雜貨店從單體系統(tǒng)重構(gòu)為微服務。
因此,在“構(gòu)建 NodeJS 電影目錄微服務”系列中,將僅關(guān)注電影目錄服務。
在這個架構(gòu)中,可以看到有 3 種不同的設備使用該微服務,即 POS(銷售點)、移動設備/平板電腦和計算機。POS 和移動設備/平板電腦都有自己的應用程序(在 electron 中開發(fā)),并直接使用微服務,而計算機則通過 Web 應用程序訪問微服務(一些專家也將 Web 應用程序視為微服務)。
構(gòu)建微服務
現(xiàn)在,來模擬在最喜歡的電影院預訂一場電影首映的過程。
首先,想看看電影院目前正在上映哪些電影。以下圖表顯示了通過 REST 進行的內(nèi)部通信,通過此 REST 通信,可以使用 API 來獲取目前正在上映的電影。

電影服務的 API 將具有以下 RAML 規(guī)范:
#%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: <<item>>
/movies:
/premieres:
type: { Collection: {item : MoviePremieres } }
/{id}:
type: { Collection: {item : Movie } }如果不了解 RAML,可以查看這個很好的教程。
API 項目的結(jié)構(gòu)將如下所示:
- api/ # 我們的API
- config/ # 應用程序配置
- mock/ # 不是必需的,僅用于數(shù)據(jù)示例
- repository/ # 抽象出數(shù)據(jù)庫
- server/ # 服務器設置代碼
- package.json # 依賴項
- index.js # 應用程序的主入口
首先要看的部分是 repository。這是對數(shù)據(jù)庫進行查詢的地方。
'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})可能已經(jīng)注意到,向 repository 的 connect ( connection ) 方法提供了一個 connection 對象。在這里,使用了 JavaScript 的一個重要特性“閉包”,repository 對象返回了一個閉包,其中的每個函數(shù)都可以訪問 db 對象和 collection 對象, db 對象保存著數(shù)據(jù)庫連接。在這里,抽象了連接的數(shù)據(jù)庫類型,repository 對象不知道數(shù)據(jù)庫是什么類型的,對于這種情況來說,是一個 MongoDB 連接。甚至不需要知道是單個數(shù)據(jù)庫還是復制集連接。雖然使用了 MongoDB 語法,但可以通過應用 SOLID 原則中的依賴反轉(zhuǎn)原則,將存儲庫功能抽象得更深,將 MongoDB 語法轉(zhuǎn)移到另一個文件中,并只調(diào)用數(shù)據(jù)庫操作的接口(例如,使用 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})在這里,創(chuàng)建了一個新的 express 應用程序,驗證是否提供了 repository 和 server port 對象,然后為 express 應用程序應用一些中間件,例如用于日志記錄的 morgan ,用于安全性的 helmet ,以及一個錯誤處理函數(shù),最后導出一個 start 函數(shù)來啟動服務器。
Helmet 包含了整整 11 個軟件包,它們都用于阻止惡意方破壞或使用應用程序來傷害其用戶。
好的,現(xiàn)在既然服務器使用了電影的 API,繼續(xù)查看 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)
})
}在這里,為API創(chuàng)建了路由,并根據(jù)監(jiān)聽的路由調(diào)用了 repo 函數(shù)。repo 在這里使用了接口技術(shù)方法,在這里使用了著名的“為接口編碼而不是為實現(xiàn)編碼”,因為 express 路由不知道是否有一個數(shù)據(jù)庫對象、數(shù)據(jù)庫查詢邏輯等,它只調(diào)用處理所有數(shù)據(jù)庫問題的 repo 函數(shù)。
所有文件都有與源代碼相鄰的單元測試,看看 movies.js 的測試是如何進行的。
可以將測試看作是對正在構(gòu)建的應用程序的安全保障。不僅會在本地機器上運行,還會在 CI 服務上運行,以確保失敗的構(gòu)建不會被推送到生產(chǎn)系統(tǒng)。
為了編寫單元測試,必須對所有依賴項進行存根,即為模塊提供虛擬依賴項??纯?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)
})
})/* 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/)
})
})可以看到,為 movies API 存根了依賴項,并驗證了需要提供一個 server port 和一個 repository 對象。
繼續(xù)看一下如何創(chuàng)建傳遞給 repository 模塊的 db 連接對象,現(xiàn)在定義說每個微服務都必須有自己的數(shù)據(jù)庫,但是對于示例,將使用一個 MongoDB 復制集服務器,但每個微服務都有自己的數(shù)據(jù)庫。
從 NodeJS 連接到 MongoDB 數(shù)據(jù)庫
以下是需要從 NodeJS 連接到 MongoDB 數(shù)據(jù)庫的配置。
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})這里可能有更好的方法,但基本上可以這樣創(chuàng)建與 MongoDB 的復制集連接。
傳遞了一個 options 對象,其中包含 Mongo 連接所需的所有參數(shù),并且傳遞了一個事件 — 中介者對象,當通過認證過程時,它將發(fā)出 db 對象。
注意 在這里,使用了一個事件發(fā)射器對象,因為使用 promise 的方法在某種程度上并沒有在通過認證后返回 db 對象,順序變得空閑。所以這可能是一個很好的挑戰(zhàn),看看發(fā)生了什么,并嘗試使用 promise 的方法。
現(xiàn)在,既然正在傳遞一個 options 對象來進行參數(shù)設置,讓我們看看這是從哪里來的,因此要查看的下一個文件是 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 })這是配置文件,大部分配置代碼都是硬編碼的,但正如看到的,一些屬性使用環(huán)境變量作為選項。環(huán)境變量被視為最佳實踐,因為這可以隱藏數(shù)據(jù)庫憑據(jù)、服務器參數(shù)等。
最后,對于構(gòu)建電影服務 API 的最后一步是使用 index.js 將所有內(nèi)容組合在一起。
'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')在這里,組合了所有的電影 API 服務,添加了一些錯誤處理,然后加載配置、啟動存儲庫,并最后啟動服務器。
因此,到目前為止,已經(jīng)完成了與 API 開發(fā)相關(guān)的所有內(nèi)容。
下面是項目中需要用到的初始化以及運行命令:
- npm install # 設置Node依賴項
- npm test # 使用mocha進行單元測試
- npm start # 啟動服務
- npm run node-debug # 以調(diào)試模式運行服務器
- npm run chrome-debug # 使用chrome調(diào)試Node
- npm run lint # 使用standard進行代碼lint
最后,第一個微服務已經(jīng)在本地運行,并通過執(zhí)行 npm start 命令啟動。
現(xiàn)在是時候?qū)⑵浞湃?Docker 容器中。
首先,需要使用“使用 Docker 部署 MongoDB 復制集”的文章中的 Docker 環(huán)境,如果沒有,則需要進行一些額外的修改步驟,以便為微服務設置數(shù)據(jù)庫,以下是一些命令,進行測試電影服務。
首先創(chuàng)建 Dockerfile,將 NodeJS 微服務制作成 Docker 容器。
# Node v7作為基本映像以支持ES6
FROM node:7.2.0
# 為新容器創(chuàng)建一個新用戶,并避免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"]使用 NodeJS 鏡像作為 Docker 鏡像的基礎,然后為鏡像創(chuàng)建一個用戶以避免非 root 用戶,接下來,將 src 復制到鏡像中,然后安裝依賴項,暴露一個端口號,并最后實例化電影服務。
接下來,需要構(gòu)建 Docker 鏡像,使用以下命令:
$ docker build -t movies-service .
首先看一下構(gòu)建命令。
docker build告訴引擎要創(chuàng)建一個新的鏡像。-t movies-service用標記movies-service標記此鏡像。從現(xiàn)在開始,可以根據(jù)標記引用此鏡像。- .:使用當前目錄來查找
Dockerfile。
經(jīng)過一些控制臺輸出后,新鏡像中就有了 NodeJS 應用程序,所以現(xiàn)在需要做的就是使用以下命令運行鏡像:
$ 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
在上面的命令中,傳遞了一個環(huán)境變量,它是一個服務器數(shù)組,需要連接到 MongoDB 復制集的服務器,這只是為了說明,有更好的方法可以做到這一點,比如讀取一個環(huán)境變量文件。
現(xiàn)在,容器已經(jīng)運行起來了,獲取 docker-machine IP地址,以獲取微服務的 IP 地址,現(xiàn)在準備對微服務進行一次集成測試,另一個測試選項可以是JMeter,它是一個很好的工具,可以模擬HTTP請求。
這是集成測試,將檢查一個 API 調(diào)用。
/* 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)
})
})總結(jié)
創(chuàng)建了用于查詢電影院正在上映的電影的 movies 服務,使用 RAML 規(guī)范設計了 API,然后開始構(gòu)建 API,并進行了相應的單元測試,最后,組合了所有內(nèi)容,使微服務完整,并能夠啟動 movies 服務服務器。
然后,將微服務放入 Docker 容器中,以進行一些集成測試。
微服務架構(gòu)可以為大型應用程序帶來許多好處,但也需要小心管理和設計,以處理分布式系統(tǒng)的復雜性和其他挑戰(zhàn)。使用 Docker 容器可以簡化微服務的部署和管理,使其更加靈活和可擴展。
到此這篇關(guān)于如何構(gòu)建一個 NodeJS 影院微服務并使用 Docker 部署的文章就介紹到這了,更多相關(guān)Docker 部署NodeJS 影院微服務內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Node.js Sequelize如何實現(xiàn)數(shù)據(jù)庫的讀寫分離
Sequelize是一個易于使用,支持多SQL方言(dialect)的對象-關(guān)系映射框架(ORM),這個庫完全采用JavaScript開發(fā)并且能夠用在Node.JS環(huán)境中。它當前支持MySQL, MariaDB, SQLite 和 PostgreSQL 數(shù)據(jù)庫。在Node.js中,使用 Sequelize操作數(shù)據(jù)庫時,同樣支持讀寫分離。2016-10-10
node實現(xiàn)將json轉(zhuǎn)為excel
平時我們寫代碼處理的數(shù)據(jù)格式一般都是json格式的數(shù)據(jù),但有時候我們也需要將數(shù)據(jù)轉(zhuǎn)為excel格式進行保存或分享,所以下面我們就來學習一下如何通過node實現(xiàn)json轉(zhuǎn)excel吧2024-11-11
使用?Node-RED對?MQTT?數(shù)據(jù)流處理
本文將介紹使用 Node-RED 連接到 MQTT 服務器,并對 MQTT 數(shù)據(jù)進行過濾和處理后再將其發(fā)送至 MQTT 服務器的完整操作流程。讀者可以快速了解如何使用 Node-RED 對 MQTT 數(shù)據(jù)進行簡單的流處理2022-05-05
為nuxt項目寫一個面包屑cli工具實現(xiàn)自動生成頁面與面包屑配置
這篇文章主要介紹了為nuxt項目寫一個面包屑cli工具實現(xiàn)自動生成頁面與面包屑配置,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09

