我是如何找到 Express 应用延迟缘由的

  • 做者/ 马达数据CTO 刘嘉瑜

最近我发现个人 Express 应用特别慢。javascript

背景介绍是这样:咱们在 Madadata 里创建平台,在这个平台中,咱们使用了一个外部服务 Leancloud 来提供用户身份验证和注册。 可是,为了在对 API 进行测试时,不用忍受从加州(咱们 CI 服务器所在地)到上海(咱们的服务提供商服务器所在地)发请求的痛苦,我用 Express 和 Mongoose 写了一个简单的模拟 API 服务。java

在咱们最近开始进行负载测试前,咱们都没有意识到这个模拟服务的延迟:超过一半的请求在1秒内没有返回,从而致使负载测试失败。做为一个简单的使用 Mongoose 的 Express 应用程序,几乎没有任何写错的机会,至少,不会延迟1秒这么多。node

上面,本地进行 mocha 测试时的截图显示,很明显 API 服务确实有些问题!算法

什么地方出了错?

从屏幕截图我能够看出,并非全部API都是慢的:用户登出的那个 API,以及显示当前我的档案的 API 都速度正常。此外,从我用 morgan 打印出来的开发日志中,我发现那些速度缓慢的 API,由 Express 收集的响应时间都显示出一致的延迟水平(即,那些用红色标记的 API,你能够看到,他们的总延迟大体分别来自两个请求)。express

这实际上排除了“延迟是由于链接问题”的可能性(而是由于 Express 应用自己)。因此下一步,我看了一下个人 Express 应用程序。(注意,这其实是值得排除,我我的建议尝试一两个其它工具,而不是来关注 mocha ,例如尝试 curl 甚至 nc ,而后再继续,由于它们几乎老是比你写的测试代码更可靠)。安全

Express 应用内部

若是提起 Node 的 Web 服务器,Express 真的是一个很好的框架,它在速度和可靠性方面已经取得了很大进展。我想,延迟可能主要是由于我用的 Express 中的插件和中间件。服务器

为了使用 MongoDB 做为会话存储,我使用了 connect-mongo 来配合个人 expression session。我也使用了相同的 MongoDB instance 做为个人主凭证和配置文件存储(为何不呢?毕竟,这是个 CI 测试的服务)。所以,我使用了 Mongoose 做为 ODM 。session

起初,我怀疑多是由于使用了 Mongoose 内置的 Promise library。可是,在我换成了 ES6 原生实现以后,问题并无解决。框架

而后,我就以为应该检查一下模型序列化和验证部分。 应用里只有一个模型,它至关简单直接:curl

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const isEmail = require('validator/lib/isEmail')
const isNumeric = require('validator/lib/isNumeric')
const passportLocalMongoose = require('passport-local-mongoose')

mongoose.Promise = Promise

const User = new Schema({
  email: {
    type: String,
    required: true,
    validate: {
      validator: isEmail
    },
    message: '{VALUE} 不是一个合法的 email 地址'
  },
  phone: {
    type: String,
    required: true,
    validate: {
      validator: isNumeric
    }
  },
  emailVerified: {
    type: Boolean,
    default: false
  },
  mobilePhoneVerified: {
    type: Boolean,
    default: false
  },
  turbineUserId: {
    type: String
  }
}, {
  timestamps: true
})

User.virtual('objectId').get(function () {
  return this._id
})

const fields = {
  objectId: 1,
  username: 1,
  email: 1,
  phone: 1,
  turbineUserId: 1
}

User.plugin(passportLocalMongoose, {
  usernameField: 'username',
  usernameUnique: true,
  usernameQueryFields: ['objectId', 'email'],
  selectFields: fields
})

module.exports = mongoose.model('User', User)复制代码

Mongose Hooks

Mongoose 有一个很好的功能,你可使用 pre-post-hooks 来检查文档的验证和保存过程。

使用 console.timeconsole.timeEnd ,咱们就能够实际测量在这些进程中花费的时间。

User.pre('init', function (next) {
  console.time('init')
  next()
})
User.pre('validate', function (next) {
  console.time('validate')
  next()
})
User.pre('save', function (next) {
  console.time('save')
  next()
})
User.pre('remove', function (next) {
  console.time('remove')
  next()
})
User.post('init', function () {
  console.timeEnd('init')
})
User.post('validate', function () {
  console.timeEnd('validate')
})
User.post('save', function () {
  console.timeEnd('save')
})
User.post('remove', function () {
  console.timeEnd('remove')
})复制代码

而后咱们获得了 mocha 运行中更详细的信息:

显然,文档验证和保存根本不是形成延迟的主要缘由。它也排除了这两个可能性:1)延迟来自于 Express 应用程序和 MongoDB 服务器之间的链接问题,或者 2)MongoDB 服务器自己运行缓慢。

Passport + Mongoose

当我把焦点从 Mongoose 身上移开,我开始看到我使用的 passport 插件:passport-local-mongoose。

这个名字是有点长,但它基本上告诉了你它是干吗的。Passport 负责会话管理、注册和登陆样板,而Passport-local-mongoose 则将 Mongoose 转变为 passport 的本地策略。

这个 library 小并且简单,因此我开始直接在 node_modules/文件夹 中编辑个人 index.js 文件。因为函数 #register (user, password, cb) 调用函数 #setPassword (password, cb) ,也就是这一行,因此我开始注意后者。在添加了更多的 console.timeconsole.timeEnd 以后,我确认了,原来延迟主要是因为这个函数调用:

pbkdf2(password, salt, function(pbkdf2Err, hashRaw) {
  // omit
}复制代码

PBKDF2

这个名称自己就表示它是一个加密 library 的调用。再看 README 能够发现,这个 library 使用了 25,000 次迭代。

像 bcrypt 同样,pbkdf2 也是一个缓慢的哈希算法,也就是说,它就是偏延迟的,这个延迟能够在迭代时进行调整,来适应不断加强的计算能力。这个概念被称为:密钥延伸 (key streching)。

如维基百科里写的,最开始提出的迭代次数是 1,000 次,而最近一次的更新达到了 100,000 次。因此其实默认的 25,000 次是合理的。

在将迭代减小到 1,000 后,个人 mocha 测试输出以下:

最后,终于,这个延迟和安全性变得能够接受了,毕竟它只是个测试应用程序!注意,我为个人测试应用程序作了这个更改,并不意味着你也要减小你的应用程序的迭代次数。另外,将迭代次数设置得过高,会使应用程序容易受到 DoS 攻击。

最后的想法

我想,分享一些 debug 经验仍是有意义的,我很高兴这不真的是一个 bug(对,是一个假装起来的功能)。

另外值得一提的是,对于对计算机安全或密码学不是很了解的开发人员来讲,一般,最好不要本身写一些与 会话 / 密钥 / 令牌管理 相关的代码。使用好的、如 passport 这样的开源库,会更好。

不过,你永远不会知道在 debug Web 服务器时会遇到什么坑——但这才是它最有趣的地方!

相关文章
相关标签/搜索