网站建设需要的软件是什么,互联网 网站定制,法律推广网站,自己做头像网站文章目录 前言什么是微服务#xff1f;构建电影目录微服务构建微服务从 NodeJS 连接到 MongoDB 数据库总结 前言
如何构建一个 NodeJS 影院微服务并使用 Docker 部署。在这个系列中#xff0c;将构建一个 NodeJS 微服务#xff0c;并使用 Docker Swarm 集群进行部署。
以下… 文章目录 前言什么是微服务构建电影目录微服务构建微服务从 NodeJS 连接到 MongoDB 数据库总结 前言
如何构建一个 NodeJS 影院微服务并使用 Docker 部署。在这个系列中将构建一个 NodeJS 微服务并使用 Docker Swarm 集群进行部署。
以下是将要使用的工具
NodeJS 版本7.2.0MongoDB 3.4.1Docker for Mac 1.12.6
在尝试本指南之前应该具备
NodeJS 的基本知识Docker 的基本知识并且已经安装了 DockerMongoDB 的基本知识并且数据库服务正在运行
什么是微服务
微服务是一个单独的自包含单元与其他许多单元一起构成一个大型应用程序。通过将应用程序拆分为小单元每个部分都可以独立部署和扩展可以由不同的团队和不同的编程语言编写并且可以单独进行测试。
微服务架构意味着应用程序由许多较小的、独立的应用程序组成这些应用程序能够在自己的内存空间中运行并且可以在可能的多个独立计算机上独立扩展。
微服务的好处
应用程序启动更快这使得开发人员更具生产力并加快了部署速度。每个服务可以独立于其他服务部署 — 更容易频繁部署服务的新版本。更容易扩展开发也可能具有性能优势。消除对技术栈的长期承诺。在开发新服务时可以选择新的技术栈。微服务通常更好组织因为每个微服务有一个非常具体的工作不涉及其他组件的工作。解耦的服务也更容易重新组合和重新配置以服务不同应用程序的目的例如同时为 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: stringtitle: stringruntime: numberformat: stringplot: stringreleaseYear: numberreleaseMonth: numberreleaseDay: numberexample:id: 123title: Assasins Creedruntime: 115format: IMAXplot: Lorem ipsum dolor sit ametreleaseYear : 2017releaseMonth: 1releaseDay: 6MoviePremieres: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 项目的结构将如下所示
api/ # 我们的APIconfig/ # 应用程序配置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 syntaxcollection.findOne({id: id}, projection, sendMovie)})}// this will close the database connectionconst 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})可能已经注意到向 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 portif (!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))}// lets init a express app, and add some middlewaresconst 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 APIs to the express appmovieAPI(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})在这里创建了一个新的 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 premieresapp.get(/movies/premieres, (req, res, next) {repo.getMoviePremiers().then(movies {res.status(status.OK).json(movies)}).catch(next)})// here we get a movie by idapp.get(/movies/:id, (req, res, next) {repo.getMovieById(req.params.id).then(movie {res.status(status.OK).json(movie)}).catch(next)})
}在这里为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 nulllet 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 对象。
继续看一下如何创建传递给 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})这里可能有更好的方法但基本上可以这样创建与 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 })这是配置文件大部分配置代码都是硬编码的但正如看到的一些属性使用环境变量作为选项。环境变量被视为最佳实践因为这可以隐藏数据库凭据、服务器参数等。
最后对于构建电影服务 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 reprepository.connect(db).then(repo {console.log(Repository Connected. Starting Server)rep reporeturn 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 服务添加了一些错误处理然后加载配置、启动存储库并最后启动服务器。
因此到目前为止已经完成了与 API 开发相关的所有内容。
下面是项目中需要用到的初始化以及运行命令
npm install # 设置Node依赖项npm test # 使用mocha进行单元测试npm start # 启动服务npm run node-debug # 以调试模式运行服务器npm run chrome-debug # 使用chrome调试Nodenpm 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 --progressfalse --production
RUN chown -R nupp:nupp $HOME/*
USER nupp
EXPOSE 3000
CMD [npm, start]使用 NodeJS 镜像作为 Docker 镜像的基础然后为镜像创建一个用户以避免非 root 用户接下来将 src 复制到镜像中然后安装依赖项暴露一个端口号并最后实例化电影服务。
接下来需要构建 Docker 镜像使用以下命令
$ docker build -t movies-service .首先看一下构建命令。
docker build 告诉引擎要创建一个新的镜像。-t movies-service 用标记 movies-service 标记此镜像。从现在开始可以根据标记引用此镜像。.使用当前目录来查找 Dockerfile。
经过一些控制台输出后新镜像中就有了 NodeJS 应用程序所以现在需要做的就是使用以下命令运行镜像
$ docker run --name movie-service -p 3000:3000 -e DB_SERVERS192.168.99.100:27017 192.168.99.101:27017 192.168.99.100:27017 -d movies-service在上面的命令中传递了一个环境变量它是一个服务器数组需要连接到 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)})
})总结
创建了用于查询电影院正在上映的电影的 movies 服务使用 RAML 规范设计了 API然后开始构建 API并进行了相应的单元测试最后组合了所有内容使微服务完整并能够启动 movies 服务服务器。
然后将微服务放入 Docker 容器中以进行一些集成测试。
微服务架构可以为大型应用程序带来许多好处但也需要小心管理和设计以处理分布式系统的复杂性和其他挑战。使用 Docker 容器可以简化微服务的部署和管理使其更加灵活和可扩展。