nodejs 过载保护

2,393 阅读5分钟

| 导语 过载保护对于to c业务的服务来说是非常重要的。试想一下,如果有一个运营活动或者特殊的日子导致流量暴增,用户每秒请求数远远超出了我们的服务所能处理的最大值,假如我们没有做过载保护,很快这个服务所能利用的资源将会被耗尽,无法处理任何请求,如果这个服务所在机器的其他服务没有和这个服务资源隔离,也会影响到其他正常的服务,那么一次严重的事故就发生了。

什么是过载保护

过载

什么是过载? 简单来说,就是当前负载超过了系统的最大处理能力,如:

系统实际每秒能处理的请求量为1000个,但实际每秒的请求量却远大于1000个,可以判定系统过载。

过载保护

对于web应用,就是对请求量的控制问题,控制请求量不要超过系统的最大处理能力。

过载保护怎么做

通过请求数计数控制

直接的做法就是控制当前服务同时处理的请求数,这个很好控制,请求来一个就计数+1, 处理完请求回包就减1,如果当前在处理的请求总数超过了设置的最大值,那么就直接回包503错误。 nodejs koa 代码实现:

// 写一个中间件 overloadProtection.js
const maxCount = 1000;
let totalCount = 0;

moudle.exports = async(ctx, next) => {
	totalCount += 1;
	 // TODO 这里可以加一些监控上报
	 
	if (totalCount > maxCount) {  // 直接丢弃
		ctx.status = 503;
		ctx.body = 'Service heavy load!!!';
		return;
	}
	await next();
	totalCount -= 1;
}

然后koa 服务在最开始的时候使用这个中间件即可。

这种方法有个前提条件: 必须要知道这个服务同时能处理的最大请求数(maxCount),一般情况下这个服务会有多个api,处理每个api请求所消耗的资源可能不一样,我们很难模拟真实场景得到准确的maxCount。你如果想说我们可以限制每个接口的最大请求数啊,并不一定要限制这个服务的最大请求数,那你就要考虑到一种极端场景,所有的接口都达到最大请求数的时候你的服务能否抗住,这可能需要更高性能或更多的服务器才能满足这种场景。

通过实时查cpu使用率控制请求数

在web服务的场景,CPU是服务最主要的瓶颈,所以我们可以在处理请求之前首先判断下当前CPU负载是否达到了我们设置的CPU最大使用率,然后丢弃或者随机丢弃。 我们需要的是当前服务的进程占用的cpu百分比,github上找到一个 node-usage 。 稍微改造一些,思路如下: 首先node-usage,如果实时获取当前进程cpu利用率,会增加系统的负载,我们可以自己控制采样cpu使用率的评率,得到cpu利用率。

nodejs koa 代码实现:

// 写一个中间件 overloadProtection.js
const usage = require('usage');
const pid = process.pid;    // 当前node服务的进程pid
let sampleCpuInterval = 1000;  // 采样cpu使用率的频率
let cpuUsedLimit = 85;         // cpu使用率上限

let getCupUsed = function getCupUsed() {
        setInterval(() => {
            usage.lookup(pid, {keepHistory: true}, (err, result) => {
                if (result) {
                    global.cpuUsed = result.cpu || 0;
                }
            })
        }, sampleCpuInterval);
};

getCupUsed();

module.exports = async(ctx, next) => {
    // TODO 这里可以加一些监控上报
    if (cpuUsedLimit && global.cpuUsed > cpuUsedLimit) {
	   // 这里是随机丢弃的算法,可以思考下为什么这么做
        let rejectRate = Math.pow((global.cpuUsed - cpuUsedLimit) / (100 - cpuUsedLimit), 2);
        if (Math.random() > rejectRate) {
            ctx.status = 503;
		    ctx.body = 'Service heavy load!!!';
		   return;
        }
    }
    await next();
};

压测很顺利, 但是如果请求量特别特别大的时候,即使node服务没有做任何逻辑处理,只是简单回包cpu都能耗尽的场景下,我们要考虑在上层或系统层做过载保护。

利用nodejs event-loop的机制控制请求数

github上找到一个overload-protection,使用非常容易。但是我自己压测后发下有很大问题。我个人理解作者意图如下: 处理请求之前,先设置一个setInterval, 通过计算setInterval里的回调函数执行延时来预估当前cpu负载。可以预见的是延迟越高,cpu负载越高。延迟在某个值时可能是我们期望限制的cpu最大使用率。 我们看下源码

var xtend = require('xtend')
var EE = require('events').EventEmitter

var defaults = {
  limit: 42,   // 默认延迟是42,
  sampleInterval: 5
}

function loopbench (opts) {
  opts = xtend(defaults, opts)

  var timer = setInterval(checkLoopDelay, opts.sampleInterval)
  timer.unref()   // 参考 https://zhuanlan.zhihu.com/p/38091559

  var result = new EE();  // EventEmitter实例

  result.delay = 0
  result.sampleInterval = opts.sampleInterval
  result.limit = opts.limit
  result.stop = clearInterval.bind(null, timer)

  var last = now()

  return result

  function checkLoopDelay () {
    var toCheck = now()
    var overLimit = result.overLimit
    result.delay = toCheck - last - result.sampleInterval  // 实际执行的延时,系统负载越高(越忙)延时越大
    last = toCheck

    result.overLimit = result.delay > result.limit

    if (overLimit && !result.overLimit) {
      result.emit('unload');   // 由过载变成正常,外面监听这个事件然后告诉服务变成正常状态了,正常处理请求
    } else if (!overLimit && result.overLimit) {
      result.emit('load');     // 由正常变为过载,外面监听这个事件然后告诉服务变成过载状态了,要丢弃请求
    }
  }

  function now () {
    var ts = process.hrtime()
    return (ts[0] * 1e3) + (ts[1] / 1e6)
  }
}

但是我持续压测了几分钟,cpu全层使用率是98%-100%,并没有触发到它的过载机制,我试着打印了下 result.delay 发现特别小:

并且还有比较小的负数,我想拌一个黑人问号脸表情,整个压测过程中只零星出现了几个503(触发过载)。不知是我压测的姿势不对还是其他因素,这种方法宣告失败。

但是我觉得作者的是思想是可以借鉴的,可以用于其他的应用场景。

总结

上面描述了过载保护的三种实践方法,前两种都可以应用于特定的场景下,第三种方法实践失败。个人认为第二种通过判断cpu实时使用率的方法,更具有通用性。

参考资料:

zhuanlan.zhihu.com/p/30342671

github.com/arunoda/nod…

github.com/davidmarkcl…

zhuanlan.zhihu.com/p/38091559

本文首发在掘金转载请注明原作者,如果你觉得这篇文章对你有帮助或启发,也可以来请我喝咖啡。 利益相关:本篇文章所有涉及到的软件均为笔者日常所用工具,无任何广告费用。