用Node.js開發(fā)memcache協(xié)議的反向代理服務器
memcache是常用的key-value緩存解決方案,它的協(xié)議也被用于nosql數(shù)據(jù)庫tokyo tyrant。
在實際項目中,出于負載均衡等考慮,php、java等客戶端需要訪問多個memcache,將一個對特定key的請求map到特定的memcache上。但這樣就需要在每個客戶端配置多個ip地址并實現(xiàn)map的算法,不便于管理和維護。最近正好在學習node.js,于是決定用node.js搭一個node.js的反向代理。對php、java等客戶端,實現(xiàn)與memcache相同的協(xié)議。將客戶端的請求,根據(jù)請求的key值分別轉(zhuǎn)發(fā)到后端的數(shù)個memcache上。
node基于v8和libevent開發(fā),主要思想是用單線程+事件循環(huán)(event loop),來實現(xiàn)異步io服務器。這種模式類似于nginx,比起傳統(tǒng)的多進程(例如apache的prefork模式)或者線程(例如tomcat等app server),速度更快。
純異步io的服務器實現(xiàn)有很多復雜的因素要考慮,有了錯誤也難以調(diào)試。使用node可以讓你從繁瑣的內(nèi)存處理等底層工作中解放出來,把精力都花在關(guān)注你的核心的模型和應用邏輯。
用node實現(xiàn)一個socket server是非常容易的,如下代碼即可實現(xiàn)簡單的echo server:
- server=net.createServer(
- function(socket) {
- socket.on('data',function(data){
- //這個this就是socket
- this.write(data);
- });
- }
- ).listen(port_number);
由于我要根據(jù)請求的key值接收和分發(fā)memcache請求,需要對收到的data做解析請求的操作。接著根據(jù)請求的key決定采用哪一個memcache,然后將請求寫入memcache連接。當memcache連接收到數(shù)據(jù)時,將數(shù)據(jù)寫回客戶端socket,代理的基本流程就結(jié)束了。
- socket.on('data',function(data){
- var request=mk_request(data);
- var mc=create_memcache_conn(request.key);
- mc.write(request);
- mc.on('data',function(data){
- socket.write(data);
- });
- });
值得注意的是,由于node異步的特性,收到的data可能比一個請求數(shù)據(jù)少,或者比一個請求數(shù)據(jù)多,或者包含好幾個請求,都是有可能的,剩下的數(shù)據(jù)需要加入到下次收到的data之前,才能保證接收請求沒有問題。于是程序修改為如下結(jié)構(gòu):
- function process_request(socket,request){
- var mc=create_memcache_conn(request.key);
- mc.write(request);
- mc.on('data',function(data){
- socket.write(data);
- });
- }
- socket.on('data',function(data){
- do{
- //將之前剩下的數(shù)據(jù)與新收到的數(shù)據(jù)合并
- data=new Buffer(this.remain_data,data);
- //如果mk_request沒解析出一個完整的請求,就返回false
- var request=mk_request(data);
- this.remain_data=data.slice(request===false?0:request.length);
- process_request(this,request);
- }while(request);
- });
但改成這樣之后,測試結(jié)果還是有問題。這是因為客戶端有可能一次發(fā)多個請求到服務器端,服務器端使用多個memcache連接處理這些請求,在memcache連接的ondata中寫回客戶端的時候,無法保證寫回的順序與請求的順序一致,導致出錯。所以需要有一個隊列來保證寫回數(shù)據(jù)的順序。
- function process_request(socket,request){
- var mc=create_memcache_conn(request.key);
- mc.write(request);
- mc.queue.push({req:request,res:false});
- mc.on('data',function(data){
- //收memcache響應也有和收客戶端請求一樣的問題,可能一次收到一半的請求
- data=new Buffer(this.remain_data,data);
- //如果mk_response沒解析出一個完整的響應,就返回false
- var response=mk_response(data);
- if(response){
- //接收完一個響應,先將響應加入隊列
- for(var i=0;i0){
- if(socket.queue[0].res){
- socket.write(socket.queue.shift().res.buffer);
- }else{
- break;
- }
- }
- this.removeListener('data',arguments.callee);
- }
- });
- }
另外需要實現(xiàn)mk_request和mk_response這兩個函數(shù),實現(xiàn)是基于memcache的協(xié)議,我的實現(xiàn)參考了文章:memcache協(xié)議中文版(http://www.ccvita.com/306.html),memcache的協(xié)議還是比較簡單的。
在最終實現(xiàn)中,我還加入了memcache連接池的功能,因為建立memcache連接需要花費的時間是很長的,memcache的文檔中也建議與服務器保持長連接,以加快效率。
加入連接池后做了簡單的壓力測試,在普通筆記本機上,幾十個進程并發(fā),每秒幾千次沒問題的,調(diào)整一下應該能上w
完整的代碼請看我的github源:https://github.com/wwwppp0801/nodeproxy
參考文檔:
memcache協(xié)議中文版:http://www.ccvita.com/306
node.js文檔:http://nodejs.org/docs/v0.4.7/api
原文:http://blog.webshuo.com/2011/05/13/%E7%94%A8node-js%E5%BC%80%E5%8F%91memcache%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E6%9C%8D%E5%8A%A1%E5%99%A8/
【編輯推薦】