微服务的服务范围越来越广泛,尤其是在构建复杂应用中,下面我主要从以下几点分享如何使用 Node.js 和 Docker 构建高质量的微服务,以及使用 Kong 构建 API gateway。
微服务架构是一种构造应用程序的替代性方法。应用程序被分解为更小、完全独立的组件,这使得它们拥有更高的敏捷性、可伸缩性和可用性。一个复杂的应用被拆分为若干微服务,微服务更需要一种成熟的交付能力。持续集成、部署和全自动测试都必不可少。编写代码的开发人员必须负责代码的生产部署。
在实际基于 Docker 构建微服务架构中,我们主要解决了以下5个问题:
-
编写高质量的微服务
-
微服务的持续集成与快速部署
-
客户端到服务端以及微服务之间的高效通信
-
服务器快速配置
-
完善的运维与监控体系
微服务架构是由一个个微小的应用程序组成的,一个高质量的微服务是构建微服务架构的前提;在实际开发中还需要一个一体化的 DevOps 平台,这样才可以解决微服务的持续集成与快速部署;微服务多了之后,还需要解决客户端到服务端以及微服务之间的高效通信,我们通过 Kong 构建微服务的 API gateway,为客户端提供一个统一的 Rest API,微服务之间也通过 Rest API 进行通信。今天我们主要讨论前三个问题。
Node.js 是构建微服务的利器,为啥这么说呢,请往下看:
-
Node.js 采用事件驱动、异步编程,为网络服务而设计
-
Node.js 非阻塞模式的IO处理给 Node.js 带来在相对低系统资源耗用下的高性能与出众的负载能力,非常适合用作依赖其它IO资源的中间层服务
-
Node.js轻量高效,可以认为是数据密集型分布式部署环境下的实时应用系统的完美解决方案
这些优势正好与微服务的优势:敏捷性、可伸缩性和可用性相契合(捂脸笑)。
但是 Node.js 的异步特性也带来了一些问题,比如 Callback 回调地狱以及“脆弱”的异常处理,当然我们可以通过使用 ES2015 的特性来控制异步流程,解决回调地狱,也可以加强异常处理机制规避一些未处理异常引起的程序崩溃,最终在实际部署中,通过多实例以及 Kubernetes 的负载均衡特性保证程序的高可用。
目前 Node.js 的
LTS
版本早就支持了
Generator
,
Promise
这两个特性,也有许多优秀的第三方库 Bluebird、Q 这样的模块支持的也非常好,性能甚至比原生的还好,可以用 Bluebird 替换 Node.js 原生的 Promise:
global.Promise = require('bluebird')
Bluebird 的性能是 V8 里内置的 Promise 3 倍左右(Bluebird 的优化方式)。
2.1 Node.js 异步流程控制
2.1.1 ES2015 Generator
Generators are functions which can be exited and later re-entered. Their context (variable bindings) will be saved across re-entrances. — ctionhttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function
Generator 就像一个取号机,你可以通过取一张票来向机器请求一个号码。你接收了你的号码,但是机器不会自动为你提供下一个。换句话说,取票机“暂停”直到有人请求另一个号码(
next()
),此时它才会向后运行。下面我们看一个简单的示例:
function* idMaker(){
var index = 0
while(index ![]()
从上面的代码的输出可以看出:
Generator 函数的定义,是通过 function *(){}
实现的
对 Generator 函数的调用返回的实际是一个遍历器,随后代码通过使用遍历器的 next()
方法来获得函数的输出
通过使用yield
语句来中断 generator 函数的运行,并且可以返回一个中间结果
每次调用next()
方法,generator 函数将执行到下一个yield
语句或者是return
语句。
下面我们就对上面代码的每次next调用进行一个详细的解释:
第1次调用next()
方法的时候,函数执行到第一次循环的yield index++
语句停了下来,并且返回了0
这个value
,随同value
返回的done
属性表明 Generator 函数的运行还没有结束
第2次调用next()
方法的时候,函数执行到第二循环的yield index++
语句停了下来,并且返回了1
这个value
,随同value
返回的done
属性表明 Generator 函数的运行还没有结束
… …
第4次调用next()
方法的时候,由于循环已经结束了,所以函数调用立即返回,done
属性表明 Generator 函数已经结束运行,value
是undefined
的,因为这次调用并没有执行任何语句
2.1.2 ES2015 Promise
The Promise object is used for asynchronous computations. A Promise represents an operation that hasn’t completed yet, but is expected in the future. —https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise所谓 Promise,就是一个对象,用来传递异步操作的消息。它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个事件提供统一的 API,可供进一步处理。
![]()
一个 Promise 一般有3种状态:
pending
:初始状态, 不是fulfilled
,也不是rejected
fulfilled
:操作成功完成
rejected
:操作失败
一个 Promise 的生命周期如下图:
![]()
下面我们看一段具体代码:
function asyncFunction() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('Async Hello world')
}, 16)
})
}
asyncFunction().then(function (value) {
console.log(value) // => 'Async Hello world'
}).catch(function (error) {
console.log(error)
})
asyncFunction
这个函数会返回 Promise 对象, 对于这个 Promise 对象,我们调用它的then
方法来设置resolve
后的回调函数,catch
方法来设置发生错误时的回调函数。
该 Promise 对象会在setTimeout
之后的16ms
时被resolve
, 这时then
的回调函数会被调用,并输出 ‘Async Hello world’ 。
在这种情况下catch
的回调函数并不会被执行(因为 Promise 返回了resolve
), 不过如果运行环境没有提供 setTimeout 函数的话,那么上面代码在执行中就会产生异常,在 catch 中设置的回调函数就会被执行。
![]()
小结
如果是编写一个 SDK 或 API,推荐使用传统的 Callback 或者 Promise,不使用 Generator 的原因是:
《如何用 Node.js 编写一个 API 客户端》@leizongmin(https://cnodejs.org/topic/572d68b1afd3b34a17ff40f0)
由此看来学习 Promise 是水到渠成的事情。
2.2 Node.js 异常处理
一个友好的错误处理机制应该满足三个条件:
-
对于引发异常的用户,返回 500 页面
-
其他用户不受影响,可以正常访问
-
不影响整个进程的正常运行
下面我们就以这三个条件为原则,具体介绍下 Express、Koa 中的异常处理:
2.2.1 Express 异常处理
在 Express 中有一个内置的错误处理中间件,这个中间件会处理任何遇到的错误。如果你在 Express 中传递了一个错误给
next()
,而没有自己定义的错误处理函数处理这个错误,这个错误就会被 Express 默认的错误处理函数捕获并处理,而且会把错误的堆栈信息返回到客户端,这样的错误处理是非常不友好的,还好我们可以通过设置
NODE_ENV
环境变量为
production
,这样 Express 就会在生产环境模式下运行应用,生产环境模式下 Express 不会把错误的堆栈信息返回到客户端。
在 Express 项目中可以定义一个错误处理的中间件用来替换 Express 默认的错误处理函数:
app.use(errorHandler)
function errorHandler(err, req, res, next) {
if (res.headersSent) {
return next(err)
}
res.status(500)
switch(req.accepts(['html', 'json'])) {
case 'html':
res.render('error', { error: err })
break
default:
res.send('500 Internal Server Error')
}
}
在所有其他
app.use()
以及路由之后引入以上代码,可以满足以上三个友好错误处理条件,是一种非常友好的错误处理机制。
2.2.2 Koa 异常处理
我们以
Koa 1.x
为例,看代码:
app.use(function *(next) {
try {
yield next
} catch (err) {
this.status = err.status || 500
this.body = err
this.app.emit('error', err, this)
}
})
把上面的代码放在所有
app.use()
函数前面,这样基本上所有的同步错误均会被
try{} catch(err){}
捕获到了,具体原理大家可以了解下 Koa 中间件的机制。
2.2.3 未捕获的异常
uncaughtException
上面的两种异常处理方法,只能捕获同步错误,而异步代码产生的错误才是致命的,
uncaughtException
错误会导致当前的所有用户连接都被中断,甚至不能返回一个正常的
HTTP
错误码,用户只能等到浏览器超时才能看到一个
no data received
错误。
这是一种非常野蛮粗暴的异常处理机制,任何线上服务都不应该因为
uncaughtException
导致服务器崩溃。在Node.js 我们可以通过以下代码捕获
uncaughtException
错误:
process.on('uncaughtException', function (err) {
console.error('Unexpected exception: ' + err)
console.error('Unexpected exception stack: ' + err.stack)
// Do something here:
// Such as send a email to admin
// process.exit(1)
})
捕获
uncaughtException
后,Node.js 的进程就不会退出,但是当 Node.js 抛出
uncaughtException
异常时就会丢失当前环境的堆栈,导致 Node.js 不能正常进行内存回收。也就是说,每一次
uncaughtException
都有可能导致内存泄露。既然如此,退而求其次,我们可以在满足前两个条件的情况下退出进程以便重启服务。当然还可以利用
domain
模块做更细致的异常处理,这里就不做介绍了。
Kong 是一个基于 Nginx 开发的开源 API gateway,下面主要从以下3个方面介绍 Kong:
-
Docker 中运行 Kong
-
Kong 高可用
-
Kong Plugin 使用举例
3.1 Docker 中运行 Kong
-
启动数据库容器,以 postgres 为例
-
docker run -d --name kong-database \
-p 5432:5432 \
-e "POSTGRES_USER=kong" \
-e "POSTGRES_DB=kong" \
postgres:9.4
-
启动 Kong
-
docker run -d --name kong \
--link kong-database:kong-database \
-e "KONG_DATABASE=postgres" \
-e "KONG_CASSANDRA_CONTACT_POINTS=kong-database" \
-e "KONG_PG_HOST=kong-database" \
-p 8000:8000 \
-p 8443:8443 \
-p 8001:8001 \
-p 7946:7946 \
-p 7946:7946/udp \
kong
-
检查 Kong 是否运行正常
Kong 启动以后,会监听 8000 和 8001 两个端口。其中 8001 作为 Admin API Server