標籤:
原文來自於:http://blog.csdn.net/jiao_fuyou/article/details/17090355
用戶端和服務端的互動有推和拉兩種方式:如果是用戶端拉的話,通常就是Polling;如果是服務端推的話,一般就是Comet,目前比較流行的Comet實現方式是Long Polling。
註:如果不清楚相關名詞含義,可以參考:Browser 與 Server 持續同步的作法介紹。
先來看看Polling,它其實就是我們平常所說的輪詢,大致如下所示:
Polling
因為服務端不會主動告訴用戶端它是否有新資料,所以Polling的即時性較差。雖然可以通過加快輪詢頻率的方式來緩解這個問題,但相應付出的代價也不小:一來會使負載居高不下,二來也會讓頻寬捉襟見肘。
再來說說Long Polling,如果使用傳統的LAMP技術去實現的話,大致如下所示:
Long Polling
用戶端不會頻繁的輪詢服務端,而是對服務端發起一個長串連,服務端通過輪詢資料庫來確定是否有新資料,一旦發現新資料便給用戶端發出響應,這次互動便結束了。用戶端處理好新資料後再重新發起一個長串連,如此周而復始。
在上面這個Long Polling方案裡,我們解決了Polling中用戶端輪詢造成的負載和頻寬的問題,但是依然存在服務端輪詢,資料庫的壓力可想而知,此時我們雖然可以通過針對資料庫使用主從複製,分區等技術來緩解問題,但那畢竟只是治標不治本。
我們的目標是實現一個簡單的服務端推方案,但簡單絕對不意味著簡陋,輪詢資料庫是不可以接受的,下面我們來看看如何解決這個問題。在這裡我們放棄了傳統的LAMP技術,轉而使用Nginx與Lua來實現。
Modified Long Polling
此方案的主要思路是這樣的:使用Nginx作為服務端,通過Lua協程來建立長串連,一旦資料庫裡有新資料,它便主動通知Nginx,並把相應的標識(比如一個自增的整數ID)儲存在Nginx共用記憶體中,接下來,Nginx不會再去輪詢資料庫,而是改為輪詢本地的共用記憶體,通過比對標識來判斷是否有新訊息,如果有便給用戶端發出響應。
註:服務端維持大量長串連時核心參數的調整請參考:http長串連200萬嘗試及調優。
首先,我們簡單寫一點代碼實現輪詢(篇幅所限省略了查詢資料庫的操作):
lua_shared_dict config 1m;server { location /push { content_by_lua ‘ local id = 0; local ttl = 100; local now = ngx.time(); local config = ngx.shared.config; if not config:get("id") then config:set("id", "0"); end while id >= tonumber(config:get("id")) do local random = math.random(ttl - 10, ttl + 10); if ngx.time() - now > random then ngx.say("NO"); ngx.exit(ngx.HTTP_OK); end ngx.sleep(1); end ngx.say("YES"); ngx.exit(ngx.HTTP_OK); ‘; } ...}
註:為了處理服務端不知道用戶端何時中斷連線的情況,代碼中引入逾時機制。
其次,我們需要做一些基礎工作,以便操作Nginx的共用記憶體:
lua_shared_dict config 1m;server { location /config { content_by_lua ‘ local config = ngx.shared.config; if ngx.var.request_method == "GET" then local field = ngx.var.arg_field; if not field then ngx.exit(ngx.HTTP_BAD_REQUEST); end local content = config:get(field); if not content then ngx.exit(ngx.HTTP_BAD_REQUEST); end ngx.say(content); ngx.exit(ngx.HTTP_OK); end if ngx.var.request_method == "POST" then ngx.req.read_body(); local args = ngx.req.get_post_args(); for field, value in pairs(args) do if type(value) ~= "table" then config:set(field, value); end end ngx.say("OK"); ngx.exit(ngx.HTTP_OK); end ‘; } ...}
如果要寫Nginx共用記憶體的話,可以這樣操作:
shell> curl -d "id=123" http://<HOST>/config
如果要讀Nginx共用記憶體的話,可以這樣操作:
shell> curl http://<HOST>/config?field=id
註:實際應用時,應該加上許可權判斷邏輯,比如只有限定的IP地址才能使用此功能。
當資料庫有新資料的時候,可以通過觸發器來寫Nginx共用記憶體,當然,在應用程式層通過觀察者模式來寫Nginx共用記憶體通常會是一個更優雅的選擇。
如此一來,資料庫就徹底翻身做主人了,雖然系統仍然存在輪詢,但已經從輪詢別人變成了輪詢自己,效率不可相提並論,相應的,我們可以加快輪詢的頻率而不會造成太大的壓力,從而在根本上提升使用者體驗。
突然想起另一個有趣的服務端推的做法,不妨在一起嘮嘮:如果DB使用Redis的話,那麼可以利用其提供的BLPOP方法來實現服務端推,這樣的話,連sleep都不用了,不過有一點需要注意的是,一旦使用了BLPOP方法,那麼Nginx和Redis之間的串連便會一直保持下去,從Redis的角度看,Nginx是用戶端,而用戶端的可用連接埠數量是有限的,這就意味著一台Nginx至多隻能建立六萬多個串連(net.ipv4.ip_local_port_range),有點兒少。
…
當然,本文的描述只是滄海一粟,還有很多技術可供選擇,比如Pub/Sub,WebSocket,Nginx_Http_Push_Module等等,篇幅所限,這裡就不多說了,有興趣的讀者請自己查閱。
轉:實現一個簡單的服務端推送方案