對于一個可靠的IM系統(tǒng),需要保證消息的百分之百到達對端。即使是在極端情況下丟失一條消息也是不能容忍的。一個極其極其低概率的事件,若是放大到分布式系統(tǒng)中,那這個概率事件就成了必然事件。在開發(fā)測試中如果發(fā)現(xiàn)一次偶然的消息丟失問題而忽略不查,那上線之后就必然會發(fā)生消息丟失。所以作為技術,一定不能放過任何一個極端情況下發(fā)生的問題。
在服務器給客戶端發(fā)送消息的過程中,有兩種方式,1.主動推送消息給客戶端;2.客戶端來拉消息。這兩種方式都可以達到目的,下面就來分析一下兩者的區(qū)別。
A ? -> ?SERVER ?-> B
上述描述一種簡單模型,A給B發(fā)送一條消息,首先會到達服務器,然后服務器將消息轉發(fā)給客戶端B。這是推的方式,服務器是直接將消息推給客戶端的。那在復雜的網(wǎng)絡環(huán)境中,如何保證消息能夠到達B端?
這個例子有兩處需要做保證,第一是如何保證A發(fā)出去的消息成功到達服務器。第二處是服務器推給B的消息如何知道已經(jīng)成功送達。
本文主要分析第二條。
在正常情況下,服務器直接把消息下發(fā)給B端就完事了,這也是大家最希望看到的結果。如果僅僅這樣處理,那系統(tǒng)會常常因為這個環(huán)節(jié)丟消息,而且非常嚴重。我們需要考慮以下幾種情況。第一,對方不在線怎么辦。第二,在移動網(wǎng)絡下,信號經(jīng)常會不穩(wěn)定,比如乘坐地鐵過隧道,信號會中斷,會導致消息沒有成功到達對端。如何保證消息可靠抵達?
1.當知道對端不在線的情況下,將消息存在服務器,等待客戶端下次登陸來拉取。
2.對于沒推成功的情況,服務器增加重推的機制,客戶端收到消息后給服務器回復確認,服務器取消后續(xù)推送。
新增的邏輯引入新的復雜度,需要解決。
1.要確保成功將消息存儲在服務器,如果存儲失敗,算是丟失消息。這樣就要對存儲失敗的情況做檢測。一種是明確知道存失敗了,另一種是后端服務超時,不知道有沒有存成功。存儲失敗可以重試,存儲超時也可以簡單認為是存儲失敗,再重試。只要保證多次存儲同一消息是冪等操作就可以,防止存了兩條。
2.對于重推,服務器要實現(xiàn)重推邏輯,把推送操作加到定時器里面,同時緩存這條消息。超時未收到客戶端的確認就再推一次。由于網(wǎng)絡原因或者客戶端卡住,會導致推送的消息到達了客戶端,但是客戶端的確認一直沒有到達服務器,導致服務器推送了多次消息,所以客戶端需要對消息做重復消息的過濾。其次是多次推送后,客戶端一直沒有回復確認,這個可能是網(wǎng)絡原因,客戶端真沒收到,也可能客戶端收到了,客戶端的確認還在路上,但是已經(jīng)到了服務器重推次數(shù),服務器決定要不要將消息存儲到服務器?鑒于客戶端實現(xiàn)了消息過濾機制,此處可以簡單地存儲消息到服務器。這就走1的邏輯。后續(xù)客戶端再上線時拉服務端存儲的消息,并做重復過濾。能保證消息不丟失。
3.既然會拉取之前存儲在服務器的消息,那拉取完成之后需要將服務器存儲的消息刪除,這一步客戶端在確認收到消息后再發(fā)刪除請求即可。否則每次都會拉一遍,耗費流量,而且消息多了會導致登陸后的收消息流程越來越卡,由于有過濾機制,不會出現(xiàn)重復消息顯示。
上述是推的方式實現(xiàn)消息可靠送達的復雜度。之間還有些邏輯沒包含進來,比如push。客戶端沒收到消息應該改推push。那這樣一來推push的情況就有很多。公司之前的老系統(tǒng)是采用推的方式,我們在這一塊踩過很多坑,服務端的實現(xiàn)邏輯也相當復雜,各種判斷,包括存消息到服務器,重推消息給客戶端,推push給客戶端,考慮多設備問題,根據(jù)客戶端的確認做重推取消等等。個人看法是:相信我,如果這樣做,后果很嚴重,你會因頻繁的消息丟失問題沉浸在復雜的代碼邏輯中無法自拔,甚至,開始懷疑人生。
接下來分析下拉消息的實現(xiàn)方式。
A ?-> SERVER -> ?B
B <--> SERVER
如果說推的方式是一步到位,那拉消息的實現(xiàn)方式分為兩步。第一,A將消息發(fā)送到服務器,服務器存儲這條消息,并發(fā)送一個通知給客戶端B,告訴他有消息來了,快來拉取。第二,客戶端來拉取這條消息,收到后刪除服務器的這條消息。同樣的問題,有兩點需要注意。第一,網(wǎng)絡原因導致通知沒到對端,第二,對方不在線怎么辦?如何保證消息可靠抵達?
拉的方式下,可以先將消息存儲到服務器,再來給發(fā)送者和接收者推通知。如果消息存儲失敗,就可以簡單回復消息發(fā)送失敗給發(fā)送者,讓發(fā)送者手動重發(fā)。對于后續(xù)流程,如下:
1.如果對方不在線,就不推送通知,直接結束消息發(fā)送流程,等待后續(xù)對方上線拉消息。這里就不用考慮存儲消息失敗的情況了,因為存儲步驟在之前已保證ok。
2.對方在線,如果通知推送失敗怎么處理?對于沒推成功的情況,不再重推,等待下次上線拉消息!沒錯,就是這么暴力和任性。
如此可能出現(xiàn)的問題是消息亂序??蛻舳丝梢愿鶕?jù)消息在服務端的生成時間排序,可以解決這個問題。就是會出現(xiàn)消息突然跳躍順序。
在實現(xiàn)中,客戶端拉消息應該是按照msgid范圍拉一批消息,而且在服務端的實現(xiàn)中,msgid要保證遞增,無重復??蛻舳酥匦侣?lián)網(wǎng)后應先保證處理拉服務器消息的流程先走完,再處理新的消息通知。防止新的消息打亂上次的拉取邏輯,中間出現(xiàn)丟消息的情況。
另外一個問題是服務器連續(xù)推過來n條消息通知,客戶端是不是應答這n條消息通知,去拉n次?因為客戶端一次會拉一批消息,或許處理第一條的消息通知就已經(jīng)把后續(xù)的新消息都拉下來了,后面的拉取就成了重復動作,會導致消息拉重復了。這種問題也好解決,從服務器拉取回來后,判斷最大msgid是否比收到的通知中msgid要大,如果是,就忽略小的通知,不拉。如果拉到的最大msgid要比通知里的msgid小,就應該繼續(xù)拉取。當然,客戶端對消息的重復過濾邏輯還是要有的。
這樣的做法是在網(wǎng)絡交互上,多了一步通知和拉取,耗費一些客戶端的流量。服務端的實現(xiàn)邏輯復雜度大大降低,客戶端需要多處理一些邏輯。
這兩種方案,我個人傾向于拉的方式。然后其中有幾個技術實現(xiàn)細節(jié)后續(xù)再寫。比如如何保證在分布式環(huán)境下msgid連續(xù)遞增不重復,如何保證客戶端對消息的排序,消息同步的具體方案,服務端對消息的存儲等。
總體而言,推的方案,服務端需要處理復雜的邏輯,客戶端需要處理的相對較少。拉的方案,服務端需要處理的邏輯比較簡單,客戶端需要配合做一些保證。對于消息可靠的保證方面,個人傾向于拉的方案,更靠譜。