我是如何找到 Express 應用延遲原因的

馬達資料MadaData發表於2017-03-31
  • 作者/ 馬達資料CTO 劉嘉瑜

最近我發現我的 Express 應用特別慢。

背景介紹是這樣:我們在 Madadata 裡建立平臺,在這個平臺中,我們使用了一個外部服務 Leancloud 來提供使用者身份驗證和註冊。 但是,為了在對 API 進行測試時,不用忍受從加州(我們 CI 伺服器所在地)到上海(我們的服務提供商伺服器所在地)發請求的痛苦,我用 Express 和 Mongoose 寫了一個簡單的模擬 API 服務。

在我們最近開始進行負載測試前,我們都沒有意識到這個模擬服務的延遲:超過一半的請求在1秒內沒有返回,從而導致負載測試失敗。作為一個簡單的使用 Mongoose 的 Express 應用程式,幾乎沒有任何寫錯的機會,至少,不會延遲1秒這麼多。

我是如何找到 Express 應用延遲原因的

上面,本地進行 mocha 測試時的截圖顯示,很明顯 API 服務確實有些問題!

什麼地方出了錯?

從螢幕截圖我可以看出,並不是所有API都是慢的:使用者登出的那個 API,以及顯示當前個人檔案的 API 都速度正常。此外,從我用 morgan 列印出來的開發日誌中,我發現那些速度緩慢的 API,由 Express 收集的響應時間都顯示出一致的延遲水平(即,那些用紅色標記的 API,你可以看到,他們的總延遲大致分別來自兩個請求)。

這實際上排除了“延遲是因為連線問題”的可能性(而是因為 Express 應用本身)。所以下一步,我看了一下我的 Express 應用程式。(注意,這實際上是值得排除,我個人建議嘗試一兩個其它工具,而不是來關注 mocha ,例如嘗試 curl 甚至 nc ,然後再繼續,因為它們幾乎總是比你寫的測試程式碼更可靠)。

Express 應用內部

如果提起 Node 的 Web 伺服器,Express 真的是一個很好的框架,它在速度和可靠性方面已經取得了很大進展。我想,延遲可能主要是因為我用的 Express 中的外掛和中介軟體。

為了使用 MongoDB 作為會話儲存,我使用了 connect-mongo 來配合我的 expression session。我也使用了相同的 MongoDB instance 作為我的主憑證和配置檔案儲存(為什麼不呢?畢竟,這是個 CI 測試的服務)。因此,我使用了 Mongoose 作為 ODM 。

起初,我懷疑可能是因為使用了 Mongoose 內建的 Promise library。但是,在我換成了 ES6 原生實現之後,問題並沒有解決。

然後,我就覺得應該檢查一下模型序列化和驗證部分。 應用裡只有一個模型,它相當簡單直接:

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 執行中更詳細的資訊:

我是如何找到 Express 應用延遲原因的

顯然,文件驗證和儲存根本不是造成延遲的主要原因。它也排除了這兩個可能性: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 測試輸出如下:

我是如何找到 Express 應用延遲原因的

最後,終於,這個延遲和安全性變得可以接受了,畢竟它只是個測試應用程式!注意,我為我的測試應用程式做了這個更改,並不意味著你也要減少你的應用程式的迭代次數。另外,將迭代次數設定得太高,會使應用程式容易受到 DoS 攻擊。

最後的想法

我想,分享一些 debug 經驗還是有意義的,我很高興這不真的是一個 bug(對,是一個偽裝起來的功能)。

另外值得一提的是,對於對電腦保安或密碼學不是很瞭解的開發人員來說,通常,最好不要自己寫一些與 會話 / 金鑰 / 令牌管理 相關的程式碼。使用好的、如 passport 這樣的開源庫,會更好。

不過,你永遠不會知道在 debug Web 伺服器時會遇到什麼坑——但這才是它最有趣的地方!

相關文章