通常把跟客戶端直連的服務器稱為接入服務器,一個或多個接入服務器構成的接入層。接入層有以下功能:
- 維護與客戶端之間的網絡連接,管理客戶端的網絡狀態。
- 接收客戶端請求,將請求轉發到業務層,轉發業務層發給客戶端的數據。
- 就近接入,負載均衡,優化網絡體驗。
這里可以發現,如果把接入層跟業務層合並也可以實現以上的功能,而且節省了實現功能2需要的工作量,根據簡單性原則,接入層不應該被獨立出來。對於一個功能單一,用戶少,並發小的系統,接入層的確沒有必要獨立出來。但對於一個復雜的系統來說,如果把也接入層和業務層合並勢必導致某一業務模塊的代碼代碼復雜,如下圖所示:
這是一個電商系統架構的一部分,用戶模塊中合並了接入層。一般來說,后台系統都有一個從簡單到復雜的演進過程,在個過程中會經歷多次版本迭代,每次都會有開發-測試-發布上線,每次發布上線都會重啟服務器。拿着里的用戶模塊來說,第一個版本一般很簡單,只有用戶注冊,登錄,查詢用戶信息這幾個功能,為了提升轉轉化率,很快就會加速未注冊用戶購物購物流程,就需要用模塊增加管理臨時用戶的功能。隨着用戶量增加,用戶模塊還需要設計成分布。這樣用戶模塊就需要經常因為發布上線而重啟,重啟時就會因想用戶體驗,更嚴重的是如果用戶正在進行像支付這樣的跟錢有關的操作時,很有可能造成直接的經濟損失。同樣地,接入層的功能迭代是也會影響到新用戶注冊和老用戶登錄。如果把接入層獨立出來,則可以有效的減少這樣的故障。
功能越簡單,改動越少的服務器,越容易做得穩定。相對於業務層模塊,接入服務器功能相對簡單很多,也很少有變化。也就是說,接入服務器比業務層模塊更容易做得穩定。接入服務器是直接更用戶連接的,它直接影響用戶體驗。接入服務器故障或者是重啟肯定會影響到用戶,而其他業務模塊故障或者重啟則不一定會影響用戶體驗。從這一點上說,應該盡量避免其他模塊對接入服務器的不良影響。
接入層獨立出來有下好處:
- 降低接入層與業務耦合度,減少地穩定度模塊對高穩定度模塊的不良影響。
- 使接業務層專注於業務處理,降低業務層設計的復雜度。
- 接入層專注於消息轉發,可以有效降低消息的丟失率,從而提高系統的穩定性。
- 接入層以較小的代價大幅提高用戶接入體驗。
架構設計的終極目標是滿足業務需求,接入層設計也不能例外。設計接入層時需要搞清楚這樣幾個問題:網絡延遲有什么要求?並發有多大?但消息平均長度多少?用戶規模多少?用戶地域分布是什么情況?用戶的網絡環境怎樣?對這幾個問題有了清醒的認識就能就能設計出恰到好處的接入層架構。
接入層的架構是隨着用戶量增加,流量增大,訪問並發增大由簡單到復雜演進的,如下圖所示
1. 單個IP地址接入
2. 多個IP地址隨機接入
3. DNS根據用戶位置和用戶的網絡運營商返回接入地址
4. DNS根據用戶位置和網絡運營商返回二級引導服務器地址,二級引導服務器根據根據負載情況和業務需求返回接入地址
以上四種架構中,第1中可以用開源的DNS服務器bind架設。第2,3,4中架構需要自己開發一個DNS服務器,這里面的關鍵是需要一個IP庫,這個IP中記錄了不同的IP地址段所屬的地理區域及運營商。
客戶端狀態管理
管理客戶端的狀態是接入層的另一個主要自責,當客戶端連接到接入服務器后,這個連接會有不同的狀態,如游客狀態,登錄狀態等。對於不同類型的連接,管理狀態發方法也不一樣。一般來說,客戶端與服務器之間可以有兩種不同的連接類型:
1. 長連接。使用TCP協議,TCP連接建立成功后需要盡可能地保持,通過心跳保持連接,實現重連。一般用在客戶端需要被動接受數據的場景。
2. 端連接。TCP,UDP皆可,一個請求--返回為一次數據交換,完成之后如果是TCP會斷開連接。
消息識別
管理客戶端狀態,第一步必須要識別客戶端的消息。假如有A,B兩個客戶端連接上了服務器,服務器收到了m1, m2兩個消息,處理這兩個消息需要建立如A->m1, B->m2這樣的對應關系。現在的做法是使用session來識別消息,每個session都有一個的session ID, session ID由服務器生成,服務器要保證它的唯一性,客戶端與服務器器之間通訊過程中,每一條消息都要帶上這它。 session有過期時間,過期后需要重新建立session。
session存儲
session數據有一下幾個特點:
1. 單條數據量小, 每條session數據一般在256Byte以內。
2. 數據總規模增加速度慢,每100w個session也就300MB左右。
3. 讀寫頻率高。相對於其他數據的來說,session的讀寫平率很有可能是最高的,因為每一條消息都會觸發至少一次的session讀-寫。
基於以上3個特點發現,把session放在內存中是最划算的,占用內存空間不大,讀寫速度快。假如session的過期時間是1天,系統的日活躍有100W用戶,存儲session所需要的空間是300MB,這個量級服務器表示很輕松。筆者這里推薦使用redis存儲session,既能滿足要求,又簡單。
用戶狀態管理
在一個系統中,用戶一般有兩種:匿名用戶和注冊用戶。
用戶進入系統后沒登錄的就是匿名用戶,這個時候需要使用session來標示用戶的臨時數據。例如一個電商網站,在用沒有登錄的情況下也可以往購物車里添加商品,此時購物車里的數據就是臨時數據,當session過期后,這些數據會被刪除。
用戶進入系統登錄后,此時這個用戶就是注冊用戶,每個注冊用戶都有一個唯一的用戶ID,通過這個用戶ID,把用戶和session綁定。這樣就可以通過session找到用戶ID,通過用戶ID找到用戶的私有數據,進而向用戶提供各種服務。有時候,還有通過用戶ID找到session, 這就要求在把用戶ID和session綁定的時候同時建立用戶ID到session的key-value關系。
消息轉發
如果有需要接入層還要負責消息轉發,消息轉發有兩個過程:生成消息,推送消息。消息可以由用戶生成,如用戶的聊天消息;也可以有系統生成,如你在你在使用一個APP時收到的廣告。消息生成之后接下來就是要把消息推送給用戶,用戶能夠收到消息的前提條件是客戶端與服務器之間保持一個長連接。一條消息可以定向推送到特定的一個用戶,也可以推送到多個用戶,這兩種場景都很常見。成功地把一條消息推送給用戶,首先要確定消息需要推送給哪些用戶,然后再找出這些用戶與哪些接入服務器建立了長連接,然后把消息消息推送到相應的接入服務器,剩下的就是接入服務器的事了。
接入服務器收到需要推送的消息后,會根據消息頭中目標用戶ID找到session, 從session中取出長連接的socket文件描述符,發送消息。這樣做理論上沒問題,但實際上很容易出錯。在linux上,socket文件描述符是循環使用的。如果用戶A 的連接短開了,如果用A重連之前,用戶B建立了連接,這時用戶B的連接文件描述符與用戶先前用戶A的是同一個。這個時候試圖推送給A的消息會推送給B。這個文件的解決方案是每個連接建立的時候,給文件描述符生成一個在一個較長時間內不會重復的流水號,使用這個流水號來查找文件描述符。