用 NodeJS 打造影院微服务并部署到 docker 上 — Part 2
用 NodeJS 打造影院微服务并部署到 docker 上 — Part 2
本文是「使用 NodeJS 构建影院微服务」系列的第 二篇文章。
## 第一章快速回顾
- 我们讲了什么是微服务
- 我们对比了微服务的利与弊
- 我们设计了一个影院微服务的架构
- 我们使用 RAML 定义了电影服务 API 的技术规格
- 我们使用 NodeJS 和 ExpressJS 开发了 电影服务 API
- 我们针对 API 做了单元测试
- 我们将 API 组成一个服务,并使我们的电影服务运行在 Docker 容器中
- 我们对运行在 Docker 中的电影服务做了集成测试
如果你还没有阅读过第一篇文章,你可以去这里看看👀。
在本篇中我们将继续构建我们的影院微服务,这次我们将进行影院目录服务的开发,以完成下图中的功能。
我们将使用到以下技术:
- NodeJS version 7.2.0
- MongoDB 3.4.1
- Docker for Mac 1.13
要跟上本文的进度有以下要求:
- 已经完成上一篇文章中的例子代码
如果你还没有完成这些代码,我已经将代码传到了 github 上,你可以直接使用代码库分支 step-1。
# 微服务安全以及 ¿ HTTP/2 ?
在第一章中,我们实现了一个简单的微服务,它使用的是 HTTP/1.1 协议。对于已有 15 年历史的 HTTP 协议来说,HTTP/2 是第一次大升级,它被高度优化且效率更高。HTTP/2 是最新的网络标准,它起源于谷歌的 SPDY 协议。现在已有许多热门站点在使用它,并且几乎所有主流浏览器都支持该协议。
要实现 HTTP/2,只需满足以下一些规则。
- 它仅支持 HTTPS 协议(我们需要一个有效的 SSL 安全证书)
- 它是向后兼容的。当浏览器或设备不支持 HTTP/2 时,它能回退到 HTTP/1.1
- 它会带来极大的性能提升
- 它不需要客户端做任何改动,只需要服务端做一个基础实现
- 一些有趣的新特性能提高站点的加载速度,在 HTTP 1.1 中这种方式是无法想象的
一个启用了 HTTP/2 的微服务网络架构
这意味着我们要在客户端与服务端之间启用一个单独的连接,然后在“网络中”利用类似 Y 轴分片( Y-axis sharding 会在本系列文章的动态扩容篇讲到更多)的方式享受 HTTP/2 带来的性能提升,同时还能享受微服务架构在运营和开发上带来的好处。
为什么我们要使用最新的 HTTP/2 协议?因为作为一名好的开发者,我们不仅要保障应用,基础设施和通信的安全,尽最大努力来防止恶意攻击;而且作为一名好的开发者,我们还应该采用对我们有利的最佳实践方式,比如使用 HTTP/2。
微服务安全的一些好的实践方式如下:
安全,是微服务应用生产环境部署中的一个重要组成部分。根据 451 Research 的一项名为2016 软件定义设施展望的调查,在未来的 12 月内,近 45% 的企业已经或计划实现基于容器的应用部署。由于 DevOps 实践在企业中取得了稳固地位,容器应用变得更加流行,安全顾问们需要知道如何保障这些应用的安全。—— [@Ranga](/user/Ranga) Rajagopalan
- 发现并监控服务间通信
- 分段并隔离应用与服务
- 对传输中的数据以及存储的数据进行加密
我们将对微服务通信进行加密以实现安全通信,并提升公网流量的安全性,这是我们使用 HTTP/2 的原因之一,为了更好的性能以及安全提升。
# 微服务的 HTTP/2 实现
首先,让我们更新下上一章的电影服务,以支持 HTTP/2 协议,在此之前我们在 config 目录下新建一个 ssl 目录。
movies-service/config:/ $ mkdir ssl
movies-service/config:/ $ cd ssl
进入 ssl 目录后,我们创建一个自签名的 SSL 证书,用于实现 HTTP/2 协议。
# Let's generate the server pass key
ssl/: $ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048
now generate the server key from the pass key
ssl/: $ openssl rsa -passin pass:x -in server.pass.key -out server.key
we remove the pass key
ssl/: $ rm server.pass.key
now let’s create the .csr file
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 []:
…
now let’s create the .crt file
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
文件,代码如下,我们在代码中加载 key 和 cert 文件,这里是我们为数不多地使用 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)
// here is where we made the modifications, we create a spdy
// server, then we pass the ssl certs, and the express app
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,
// here we pass the ssl options to the server.js file
ssl: config.serverSettings.ssl,
repo
})
})
.then(app => { ... })
})
...
现在我们使用下面的指令重建 docker 镜像:
$ docker build -t movies-service .
再使用以下参数运行我们的 docker 镜像 moives-service
:
$ docker run --name movies-service -p 443:3000 -d movies-service
最终我们用 chrome 浏览器测试一下,可以确认我们的 HTTP/2 协议已经完全生效了。
而且我们还可以使用 wireshark 之类的抓包工具确认 ssl 确实生效了。
# 在微服务上实现 JWT
另外一种加密并保障我们的微服务通信安全的方式是使用 json web token
协议,我们将在本系列后面的文章中讲到🚉。
# 构建微服务
到此为止我们已经知道了如何实现 HTTP/2 协议,接下来让我们继续构建影院目录服务 。我们会使用与电影服务一样的项目结构,让我们少讲话🗣多编码👨🏻💻👩🏻💻这就干起来。
在开始设计 API 之前,这次我们要先为数据库设计 Mongo Schemas,我们将使用到以下数据库:
- Locations (国家,州县和城市)
- Cinemas (影院,放映计划,电影)
# 模型数据设计
本文重点关注于构建微服务,故不会在“模型数据设计”上花太多的时间,只会强调某些部分。
# posible collections for the cinemas db.
# For locations
- countries
- states
- cities
# For cinemas
- cinemas
- cinemaRooms
- schedules
对于我们的 Locations 库来说,一个国家可以有多个州而一个州则对应一个国家,所以这是一对多的关系,对于州和城市来说也是一样的关系,让我们看一下关系图:
这种关系还适用于很多情况。一个城市有多个电影院,一个电影院属于一个城市;一个放映室有许多放映计划,一个放映计划属于一个放映室,我们看一下它们之间的关系。
如果电影院数组或者放映计划数据增长比较有限,我们可以采用上图中的引用关系。假设一个放映室每天最多只有 5 个放映计划,我们也可以将放映计划文档直接嵌入到影院文档中去。
内嵌数据模型允许应用将相关性数据片断存放在同一条数据库记录中。这样做可使得应用的常用操作需要更少的查询和更新。—— MongoDB Docs
这就是我们数据库结构设计的最终结果。