我是如何找到 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 服務器時會遇到什麼坑——但這纔是它最有趣的地方!

相關文章
相關標籤/搜索