前幾天才說用 Tornado 實作 server push 很簡單, 且可以直接用在上線環境裡。今天要來自打嘴巴說我要改用 Node.js 了。有興趣了解相關基本知識的人, 可以參閱之前的說明。
Python 圈 event-based framework 的問題
如這篇文章所述, Python 天生不是 async, 既有函式庫自然也不是 async, 使用 event-based framework (*1) 後, 要另外找配套的函式庫。比方說 http client 用慣了 requests, 直接拿到 Tornado 裡用會出事, 因為 Tornado 是 single process single thread 的架構, 一發出網路連線就會 block 住全部動作。要嘛改用 Tornado 提供的 httpclient, 不然就要找別人用 Tornado 提供的 API 包好的 requests。
每套函式庫都這樣找頗麻煩的 (資料庫、第三方服務的 SDK、etc), 而且不見得找得到牢靠又有效率的版本, 比方說 Google API Python Client 就沒有 Tornado 版的, 要自己想辦法弄。以前用 gevent 時作過類似的事, 先搞定 Dropbox Python SDK, 再來搞不定 Google Drive API, 只好用 gevent monkey patch。感覺不是很可靠。
Node.js 的優勢
反觀 Node.js 天生就是 async, 既有函式庫自然也是 async。Google 也有提供 Google API Node.js Client。既有的社群也滿龐大的, 可以用的套件很多。對我來說, 這點最重要。
效能方面, 因為使用 V8 轉成 native code 執行, 表現也不錯。從這裡和這裡的 benchmark 來看, 數值計算大勝 CPython, 然後和 PyPy 持平。我自己比較常用 dictionary, 作個簡單的 benchmark 測試使用 for, if 和 dictionary, 結果 Node.js 小勝 PyPy, 然後大勝 CPython。整體來說執行速度夠用了, 到是記憶體有 1.7G 的限制 (64bit OS 預設 1G), 要多留意一下。
使用 Express 作 web server 提供簡單的 API, 然後用 ab 作簡單的 benchmark, 結果效能比 Tornado 好 (或可視作差不多, 都達到標準), 一杪內湧入 1k 連線可在一秒內解決; Node.js 底層使用 libuv, 承受 C10k 以上的連線也不是問題。
此外, 配合既有寫網頁的經驗, 改用 Node.js 開發少了學習新語言的成本, 這也是一個優勢。
Server Push 實作
這裡有完整的程式碼, 我只有作最基本功能, 沒控管記憶體也沒提供 timeout 之類的額外參數。寫起來很直覺, /get 沒有資料的時候, 就產生一個 callback function 存到全域變數 g_cache 裡。待 /set 再逐一呼叫這些 callback function, 完成 long polling 的要求。
var error = { ok: 0, invalid_input: 1, }; var g_cache = {}; app.get('/set', function (req, res) { var key = req.query.key; var value = req.query.value; if (key === undefined || value === undefined) { res.send(JSON.stringify({ error_code: error.invalid_input })); return; } if (!(key in g_cache)) { g_cache[key] = {}; } var entry = g_cache[key]; entry.value = value; if ('callbacks' in entry) { var cbs = entry.callbacks; for (var i = 0; i < cbs.length; i++) { cbs[i](); } while (cbs.length > 0) { cbs.pop(); } } res.send(JSON.stringify({ error_code: error.ok })); }); app.get('/get', function (req, res) { var key = req.query.key; if (key === undefined) { res.send(JSON.stringify({ error_code: error.invalid_input })); return; } if (!(key in g_cache)) { g_cache[key] = {}; } var entry = g_cache[key]; if ('value' in entry) { res.send(JSON.stringify({ error_code: error.ok, value: entry.value })); } else { if (!('callbacks' in entry)) { entry.callbacks = []; } entry.callbacks.push(function() { res.send(JSON.stringify({ error_code: error.ok, value: entry.value })); }); } });
Node.js 雜項備忘
- How to Install Node.js on Ubuntu 14.04
- Installing Node.js via package manager: 這個作法可以裝最新的 stable 版。目前最新的 stable 版本是 0.12, 上個 stable 版本是 0.10, 若要改用 0.10, 不要用 https://deb.nodesource.com/setup_0.12 , 改用 https://deb.nodesource.com/setup_0.10 。我可以用這個作法在 Ubuntu 12.04 上裝 0.10 版。
- How to create an HTTPS server in Node.js?
- Why should I use Express when developing a web app with Node.js?: Express 提供 web server 需要的基本功能如 session, POST, template, error handling, 防止 server crash 等。
- 這裡提到 Express 的架構是: http > connect > Express。有閒時再來看看底層的東西。
- javascript - How to get POST a query in Express.js/Node.js?: Express 4.x 作法不太一樣。摘錄我試成功的方法:
var express = require('express'); var app = express(); var bodyParser = require('body-parser') app.use(bodyParser.json()); // to support JSON-encoded bodies app.use(bodyParser.urlencoded({ // to support URL-encoded bodies extended: true })); app.get('/get', function (req, res) { res.send(req.query.key); }); app.post('/post', function (req, res) { res.send(req.body.key); }); var server = app.listen(3000, function () { var host = server.address().address; var port = server.address().port; console.log('web server for long polling listening at http://%s:%s', host, port); });
- Q: A tool for creating and composing asynchronous promises in JavaScrip: 遇到 callback hell 時再來看看。
備註
1. 我指使用 epoll 之類作法的架構。
2015-06-22 更新
Scott 提到 Go 也是不錯的選擇。搜了一下, 還真多人從 Node.js 跑到 Go。另一方面抽查了幾個 third-party 服務, 看起來 Node.js 的支援度比 Go 廣。對我來說, 現階段還是用 Node.js 比較划算 (學習成本和風險考量)。一兩年後再來評估看看是否值得改用 Go。