构建一个基于NodeJS的影院服务并使用Docker进行部署(2)
书接上回如何构建一个NodeJS影院微服务,并使用Docker进行部署
在本文中,我们将继续开发我们的电影微服务,这次我们将开发电影目录服务来完成如下的设计图:
我们将会用到如下几个技术:
NodeJS version 7.2.0MongoDB 3.4.1Docker for Mac 1.13
在如何构建一个NodeJS影院微服务,并使用Docker进行部署中,我们实现了一个简单的微服务,它实现了HTTP/1.1协议。HTTP/2是15年来HTTP协议的第一个重大升级,它进行了高度优化,性能更好。HTTP/2 是新的Web标准,它起源于谷歌的SPDY协议。它已经被许多流行的网站使用,并被大多数主流浏览器支持。
HTTP/2只有少数必须遵守的规则才能实现它:
• 它只能与HTTPS协议一起工作(我们需要一个有效的SSL证书);
• 它具有向后兼容性。如果浏览器或运行应用程序的设备不支持HTTP/2,它将倒退到HTTP1.1;
• 开箱即用,性能有很大提高;
• 在客户端不需要做任何事情,只需要在服务器端进行基本实现;
• 几个新的有趣的功能将以HTTP1.1实现所无法想象的方式加快您的Web项目的加载时间。
这意味着我们需要在客户端和服务器之间启用单个连接,然后利用Y轴分片(在系列上更多地讨论规模立方体)等功能在“网络”中维护HTTP/2给客户端的性能优势,同时启用微服务架构的所有运营和开发优势。
所以,为什么我们要实现全新的HTTP/2协议?好吧,因为作为优秀的开发人员,我们必须尽可能地加固我们的应用程序、基础架构和通信,以防止恶意攻击。而且,作为优秀的开发人员,我们遵循我们认为对我们有利的最佳实践,比如这个。
微服务的一些安全最佳实践如下:
安全性明显将在决定采用和部署微服务应用程序用于生产使用方面发挥重要作用。根据451 Research的研究说明,近45%的企业要么已经实施,要么计划在未来12个月内推出基于容器的应用程序。随着DevOps实践在企业中获得立足点,容器应用程序变得更加普遍,安全管理员需要掌握保护应用程序的知识。
发现和监控服务间通信分段和隔离应用程序和服务加密传输中和静态的数据
我们要做的就是加密我们的微服务通信,以满足合规性要求并提高安全性,特别是当流量跨越公共网络时,这也是我们要实现HTTP/2的原因之一,以实现更好的性能和安全性提高。
首先,让我们更新上一章中的movies服务,并实现HTTP/2协议,之后我们将在config文件夹中创建一个ssl文件夹。
movies-service/config:/ $ mkdir ssl
movies-service/config:/ $ cd ssl
现在,在ssl文件夹内,让我们创建一个自签名的SSL证书,以开始在我们的服务中实现HTTP/2协议。
# 生成 server pass key
ssl/: $ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
# 开始生成
ssl/: $ openssl rsa -passin pass:x -in server.pass.key -out server.key
# 删除server pass key
ssl/: $ rm server.pass.key
# 创建 .csr 文件
ssl/: $ openssl req -new -key server.key -out server.csr
...
Country Name (2 letter code) [AU]:MX
State or Province Name (full name) [Some-State]:Michoacan
...
A challenge password []:
...
# 创建
ssl/: $ openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt
接下来使用如下命令安装SPDY:
cinema-catalog-service/: $ npm i -S spdy --silent
首先,让我们在ssl/文件夹中创建一个index.js文件,代码如下,这是我们加载密钥和证书文件的地方,这可能是我们可以使用fs.readFileSync()的少数情况之一:
const fs = require('fs')
module.exports = {
key: fs.readFileSync(`${__dirname}/server.key`),
cert: fs.readFileSync(`${__dirname}/server.crt`)
}
然后,我们需要修改几个文件,首先修改config.js:
const dbSettings = { ... }
// The first modification is adding the ssl certificates to the
// serverSettings
const serverSettings = {
port: process.env.PORT || 3000,
ssl: require('./ssl')
}module.exports = Object.assign({}, { dbSettings, serverSettings })
接下来让我们修改server.js文件,如下:
...
const spdy = require('spdy')
const api = require('../api/movies')
const start = (options) => {
...
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!')
})
api(app, options)
// 在这里,我们进行了修改,我们创建了一个spdy服务器,然后传递了ssl证书和express应用程序。
const server = spdy.createServer(options.ssl, app)
.listen(options.port, () => resolve(server))
})
}
module.exports = Object.assign({}, {start})
最后,让我们修改主文件index.js:
'use strict'
const {EventEmitter} = require('events')
const server = require('./server/server')
const repository = require('./repository/repository')
const config = require('./config/')
const mediator = new EventEmitter()
...
mediator.on('db.ready', (db) => {
let rep
repository.connect(db)
.then(repo => {
console.log('Connected. Starting Server')
rep = repo
return server.start({
port: config.serverSettings.port,
// 这里我们传递ssl配置项给server.js文件
ssl: config.serverSettings.ssl,
repo
})
})
.then(app => { ... })
})
...
现在,我们需要使用以下命令重建我们的Docker镜像:
$ docker build -t movies-service .
然后,使用以下参数运行我们的movies-service镜像:
$ docker run --name movies-service -p 443:3000 -d movies-service
最后,我们使用Chrome浏览器测试它,可以证实我们的HTTP/2协议完全正常工作。
我们也可以使用Wireshark进行某些网络捕获来证实SSL真的起作用。
是的,另一种加密和保护微服务通信的方式是使用JSON Web令牌(JWT)协议, 我们将在后续系列中看到此实现。
好的,现在我们知道如何实现HTTP/2协议,让我们继续构建电影目录服务。我们将使用与movies服务相同的项目结构,所以少说话,多编码。
在开始设计API之前,这次我们需要为数据库设计Mongo模式,因为我们将使用以下内容:
Locations (countries, states and cities)Cinemas (cinemas, schedules, movies)
这篇文章的重点在于创建微服务,所以我不会在影院数据库的“模型数据设计”上花费很长时间,而是会突出重点和要点。
# 电影数据库的collections.
# locations
- countries
- states
- cities
# cinemas
- cinemas
- cinemaRooms
- schedules
对于我们的locations,一个国家有多个州,一个州有一个国家,所以第一个关系是一对多,但这也适用于一个州有多个城市,一个城市有一个州,所以让我们看看我们的关系示例。
是的,这种类型的关系也适用于:一个城市有许多影院,一个影院属于一个城市。我们可以看到的另一种关系是:一个影院屏幕有多个时间表,一个时间表属于一个影院屏幕。所以让我们看看这些关系:
如果影院数组或时间表数组的增长受限,上图中的这种引用可能很有用。假设一个影院屏幕每天最多有5个时间表,那么这里我们可以将时间表文档嵌入到影院文档中。
嵌入式数据模型允许应用程序将相关信息片段存储在同一数据库记录中。因此,应用程序可能需要发出更少的查询和更新来完成常见操作。—— MongoDB文档
所以上图是数据库的最终设计结果。
我已经准备了一些数据示例,其模式设计如上所示,文件位于GitHub仓库cinema-catalog-service/src/mock中,有4个json文件,因此您可以将其导入cinemas数据库,但首先我们需要知道哪个数据库服务器是主服务器,所以请找出并执行以下命令:
# 首先,我们需要逐个复制这些文件,或者我们可以将其打包成zip文件并传递zip文件。
$ docker cp countries.json mongoNodeContainer:/tmp
$ docker cp state.json mongoNodeContainer:/tmp
$ docker cp city.json mongoNodeContainer:/tmp
$ docker cp cinemas.json mongoNodeContainer:/tmp
一旦我们执行上述命令,让我们像下面这样导入数据库:
$ docker exec mongoNode{number} bash -c 'mongoimport --db cinemas --collection countries --file /tmp/countries.json --jsonArray -u $MONGO_USER_ADMIN -p $MONGO_PASS_ADMIN --authenticationDatabase "admin"'
$ docker exec mongoNode{number} bash -c 'mongoimport --db cinemas --collection states --file /tmp/states.json --jsonArray -u $MONGO_USER_ADMIN -p $MONGO_PASS_ADMIN --authenticationDatabase "admin"'
$ docker exec mongoNode{number} bash -c 'mongoimport --db cinemas --collection cities --file /tmp/cities.json --jsonArray -u $MONGO_USER_ADMIN -p $MONGO_PASS_ADMIN --authenticationDatabase "admin"'
$ docker exec mongoNode{number} bash -c 'mongoimport --db cinemas --collection cinemas --file /tmp/cinemas.json --jsonArray -u $MONGO_USER_ADMIN -p $MONGO_PASS_ADMIN --authenticationDatabase "admin"'
现在我们有准备好的数据库模式设计和准备查询的数据,所以我们现在可以为电影目录服务设计API了,定义路由的一种方法是制作一些句子,如下所示:
• 我们需要一个城市来显示可用影院。
• 我们需要影院来显示电影首映。
• 我们需要电影首映并显示时间表。
• 我们需要时间表来查看是否有座位可供预订。
让我们假设IT部门的其他团队正在执行其他CRUD操作,而我们的任务是执行“R”读取数据,并假设一些电影院运营人员已经制定了电影院的时间表,所以我们的任务是检索这些时间表。
关于电影目录服务,我们只关心影院和时间表,不会有更多的内容。在上面,我们看到我们创建了位置集合,但这是另一个微服务的关注点。但是,我们依赖位置才能显示影院和时间表。
现在我们已经确定了需求,我们可以构建我们的RAML文件,如下所示:
#%RAML 1.0
title: Cinema Catalog Service
version: v1
baseUri: /
uses:
object: types.raml
stack: ../movies-service/api.raml
types:
Cinemas: object.Cinema []
Movies: stack.MoviePremieres
Schedules: object.Schedule []
traits:
FilterByLocation:
queryParameters:
city:
type: string
resourceTypes:
GET:
get:
responses:
200:
body:
application/json:
type: <<item>>
/cinemas:
type: { GET: {item : Cinemas } }
get:
is: [FilterByLocation]
description: we already have the location defined to display the cinemas
/cinemas/{cinema_id}:
type: { GET: {item : Movies } }
description: we have selected the cinema to display the movie premieres
/cinemas/{cinema_id}/{movie_id}:
type: { GET: {item : Schedules } }
description: we have selceted a movie to display the schedules
这里我们可以定义三个函数:
getCinemasByCity: 这个函数将为我们提供城市内所有可用影院,我们传入city_id来查找影院,该函数的结果将帮助我们调用下一个函数。getCinemaById: 这个函数将通过查询cinema_id来检索名称、ID和可用的电影首映,该函数的结果将最终帮助我们获取时间表。getCinemaScheduleByMovie: 这个函数将为一座城市内所有影院的一部电影提供所有时间表。
可能需要另一个函数,或者我们可以修改getCinemaById,以显示当前影院的时间表,这可能是对您的一 个良好挑战,如果您想练习的话,这并不那么困难,因为我已经为您提供了所有必要的信息。
下一个要检查的文件是我们的API文件cinemas-catalog.js。
'use strict'
const status = require('http-status')
module.exports = (app, options) => {
const {repo} = options
app.get('/cinemas', (req, res, next) => {
repo.getCinemasByCity(req.query.cityId)
.then(cinemas => {
res.status(status.OK).json(cinemas)
})
.catch(next)
})
app.get('/cinemas/:cinemaId', (req, res, next) => {
repo.getCinemaById(req.params.cinemaId)
.then(cinema => {
res.status(status.OK).json(cinema)
})
.catch(next)
})
app.get('/cinemas/:cityId/:movieId', (req, res, next) => {
const params = {
cityId: req.params.cityId,
movieId: req.params.movieId
}
repo.getCinemaScheduleByMovie(params)
.then(schedules => {
res.status(status.OK).json(schedules)
})
.catch(next)
})
}
正如您所见,在这里我们实现了我们在RAML文件中定义的端点,并根据路由调用repository.js函数。
在我们的第一个路由中,我们使用req.query.cityId来获取值和查询我们的数据库以按城市city_id获取影院,在其他路由中,我们使用req.params来获取cinemaId和movieId的值,以便查询时间表。
最后,我们可以看到cinema-catalog.spec.js文件用于测试:
#%RAML 1.0
title: Cinema Catalog Service
version: v1
baseUri: /
uses:
object: types.raml
stack: ../movies-service/api.raml
types:
Cinemas: object.Cinema []
Movies: stack.MoviePremieres
Schedules: object.Schedule []
traits:
FilterByLocation:
queryParameters:
city:
type: string
resourceTypes:
GET:
get:
responses:
200:
body:
application/json:
type: <<item>>
/cinemas:
type: { GET: {item : Cinemas } }
get:
is: [FilterByLocation]
description: we already have the location defined to display the cinemas
/cinemas/{cinema_id}:
type: { GET: {item : Movies } }
description: we have selected the cinema to display the movie premieres
/cinemas/{cinema_id}/{movie_id}:
type: { GET: {item : Schedules } }
description: we have selceted a movie to display the schedules
最终,我们可以构建我们的docker映像cinema-catalog-service,并在容器内运行它。我们将使用movies service中的相同dockerfile,为了使这个过程自动化一些,让我们像下面这样为此任务创建一个bash脚本:
#!/usr/bin/env bash
eval `docker-machine env manager1`
docker build -t catalog-service .
docker run --name catalog-service -p 3000:3000 --env-file env -d catalog-service
随着我们开始开发更多服务,我们需要仔细考虑我们服务可用的端口,所以这次,我将使用3000端口,另外,我将使用一个env文件开始在我们的NodeJS服务中使用process.env配置,我们的env文件将如下所示:
DB=cinemas
DB_USER=cristian
DB_PASS=cristianPassword2017
DB_REPLS=rs1
DB_SERVERS='192.168.99.100:27017 192.168.99.101:27017 192.168.99.102:27017'
PORT=3000
这被视为DevOps领域的最佳实践。
最后,我们需要像下面这样运行我们的小bash脚本:
# 执行脚本
$ bash < start-service.sh
# 检查运行中的Docker容器
$ docker ps
然后我们会看到如下界面:
我们可以在chrome浏览器中测试我们的服务,并验证我们的HTTP/2协议是否工作,以及我们的服务是否工作。
为了好玩,我们可以进行压力测试,使用JMeter,压力测试文件也位于Github仓库的integration-test/文件夹中。
- 0000
- 0000
- 0002
- 0000
- 0000