量變引起質變。
專案需求
由於我們現在開發的雲平臺專案是一個跨雲排程的重型計算平臺,所以會用到不同的雲服務廠商的計算例項伺服器,比如阿里雲的ECS、亞馬遜的EC2或者谷歌雲的compute engine等,同時也會在這些計算例項之間進行資料傳輸。 這些伺服器之間的傳輸速度通常是不同的,即使是同一個雲服務廠商內的不同區域伺服器之間傳輸資料,頻寬也會有所不同。 所以需要對這些伺服器之間的頻寬速度進行測量,以供排程程式分配任務和傳輸資料。 總結起來這個功能實現起來有3個要求:
- 對不同區域內的雲伺服器的TCP/UDP傳輸速度進行檢測。
- 必須是滿頻寬的測速,也就是說兩臺伺服器在測速的時候不能有其它網路程式執行。
- 多臺伺服器之間需要高效地建立兩兩連線。
針對這3個問題,我實行了以下解決方案。
基本結構
一種簡單的設想就是啟動測速所需的客戶端和服務端,讓這些程式之間互相爭搶進行測速。
{% asset_img tcp-udp-server-client.jpg %}
但是這種方式在程式的管理上很容易出現問題,因為每個程式既要操作自己的狀態還需要操作其它程式的狀態,同時一下子起4個程式也比較浪費,因為事實上每次測速只會有一個程式工作。 所以更好的方式是用主從結構,由一個主程式來啟停負責測速的客戶/服務端子程式。
{% asset_img master-slave.jpg %}
這樣不但能有效地避免資源的浪費和爭搶,同時主程式中也可以整合很多邏輯功能,比如對外提供 REST API,記錄日誌、備份測試結果等。
當然為了方便部署和程式控制,所有的程式都是部署在 Docker 容器中。
由於主程式需要訪問宿主機的 Docker 服務,所以需要開啟 Docker 的 remote API 服務,對容器提供REST API進行操作。
功能實現
基本測速
TCP與UDP
網路協議是一層一層封裝起來的,而TCP和UDP屬於同一層的兩種協議。 其中TCP協議在前後端開發中非常常用,因為REST API請求依賴的HTTP(S)協議就是TCP的上層協議,而UDP協議在視訊、遊戲、下載等業務中使用也非常多。 它們有一些共同點:請求的發起方稱為客戶端,請求的接收方稱為服務端,服務端和客戶端可以雙向通訊。 而它們的側重點有所區別。TCP更注重穩定,客戶端和服務端之間需要建立連線之後才能互相傳送資料。 UDP則更注重速度,客戶端不需要和服務端建立連線即可直接傳送資料,但是如果傳送速度太快或者網路不穩定可能會造成丟包,導致對方接收的資料部分丟失。
測速工具
常用的命令列測速工具有iperf和speedtest,相較之下選擇了功能更強大的iperf。 iperf是一個比較理想的測速工具,支援TCP、UDP協議,還可以通過引數來制定傳輸資料大小、傳輸次數或者傳輸時間,以及輸出結果的格式。 但是由於前面UDP協議的特性,測速會略微麻煩一些,需要找到合適的頻寬。 比如按照1Gbps的速度傳送資料,丟包率是70%和按照10Mbps的速度傳送資料,丟包率是0,那麼對資料完整性有要求的話肯定更偏向於後者。 當然實際情況並不是對於丟包率為0就是最好的,而是在可容忍的範圍內採用最大速度傳輸(資料丟了還可以重傳不是~)。 這就意味著需要根據實際網路狀況不斷調整和嘗試。 而iperf並沒有這麼智慧,所以UDP這一塊採用團隊內部開發的一款UDP傳輸工具,來找到理想的傳輸速度。
滿頻寬
要保證滿頻寬只需要保證測速時沒有其它程式佔用頻寬即可。 由於我們可以啟動一臺獨立的搶佔式伺服器來執行測速程式,所以其它非測速程式的程式不太可能佔用頻寬,而容易爭搶頻寬的是用來測速的子程式。 所以需要讓子程式之間是互斥執行,甚至是互斥存在的。 採用狀態管理基本上就可以實現,主程式在每次有程式啟動的時候將狀態置為"connecting",測速完成後置為"waiting",只有在"waiting"狀態下才可以啟動新的子程式進行測速。 但是這只是從程式碼邏輯層面控制,對於穩定健壯的程式而言,最好還有其它的硬性控制方式。 這時候使用容器的話就可以輕鬆辦到。 凡是需要進行測速的程式都在容器中啟動,同時容器的名稱都統一,那麼一旦程式出現bug,同時啟動多個子程式時,Docke r服務則會報錯,告知容器名稱衝突,從而建立失敗。 當然這種方式也有一定的風險,比如上一個程式測速過程中出現問題沒有按時退出,那麼則無法進行新的測速,所以需要需要設定一個超時時間,超過一段時間後主動停止當前測速子程式。 同時如果主程式意外退出,導致停止失敗的話,也要進行處理:在每次啟動主程式的時候進行檢查,及時銷燬未停止的子程式。
多節點
多節點算是非常棘手的問題。試想如果在一段時間內同時在多個雲伺服器上啟動多個測速程式,如何保證他們有序的進行測速呢? 要解決這個問題,先思考一個簡單些的問題: 在一段時間內,如何決定哪些雲伺服器啟動服務端子程式哪些雲伺服器啟動客戶端子程式呢? 如果按照“主-從”模式的話需要建立一箇中心節點來進行控制,但是這樣的缺點很多,最重要的一個缺點是如果某個節點與中心節點無法通訊那麼就無法獲得與其它節點通訊的機會,及時它和其它節點之間網路暢通。 同時中心節點和其它節點之間也存在多節點通訊的問題。 總而言之這種方式下通訊的成本太高,服務端與客戶端傳輸資料需要的中間環節太多,很容易出現問題。 所以簡單的方式是讓雲伺服器之間互相發起測速請求並響應。 這樣的話,主程式的邏輯要分為兩個模組,一個模組用來響應請求、、分配埠、啟動服務端容器。 另一個用來輪詢帶測速佇列併發起請求、啟動客戶端容器建立連線。
工作流程大致如下:
{% asset_img interval-web.jpg %}
這種處理方式還有一種極端情況,就是兩個雲伺服器之間互相請求進行測試,如果雙方請求到達時間一致,那麼就會同時給對方分配埠,然後同時受到對方分配的埠之後發現服務端已啟動於是放棄連線。 於是出現了類似程式“死鎖”的狀態。 對於這種情況的處理方式是使用時間戳來記錄請求發起的時刻,雙方通過時間戳的先後來決定是否啟動客戶端或服務端。 即使更極端的情況出現——雙方時間戳相同。那麼通過超時回收或者發訊息釋放埠來建立下一次連線。
弱網路下的處理
弱網路指的是網路不穩定或者頻寬較小的情況。 這種情況的處理方式原則上就是重測,但是關於重測有幾個需要注意的地方:
- 對於頻寬較小的情況需要考慮減小傳輸資料的體積以保證在制定的超時時間內完成測速。
- 對於測速失敗的情況進行判斷並重測,同時限定重測次數,避免無限重測。
- 重測時可以讓客戶端與服務端進行互換測試,通過限定一方發起重測可實現。
總結
很多時候實現一個功能並不困難,但是要把功能實現好卻是一件不簡單的事。 雖然理論上實現起來只是簡單的呼叫測速工具就可以得到結果,但在實際場景下可用性會變得很低。 比如沒有對弱網路的重測機制,那麼偶然的網路抖動就會影響到測速結果。 如果沒有考慮到多節點爭搶連線的問題,那麼實際執行在多個雲伺服器上可能會造成程式錯誤或測速結果不準確的問題。 要怎麼樣把功能實現好呢? 至少有兩個考慮方向:
- 倍數思維。比如當前框架支援10個頁面沒問題,那麼如果100個、1000個會不會有效能問題?
- 極限思維。就是一些極端情況下的處理機制,比如在本文中對超時的處理,對容器互斥的處理等。
作者資訊:朱德龍,人和未來高階前端工程師。