裝置移動性的挑戰
1.裝置會經常由於小區或模式切換而更改IP地址。
這種地址更新是移動網路的正常行為,不應作為故障或事故看待,因此理應對應用程式透明,應用不應被此類事件打擾,更無責做善後處理。
2.行動裝置存在多張3G/4G/2.75G網卡時,希望這些網卡同時收發資料。
由於這些網卡一般屬於不同電訊廠商網路,其網路架構又不同,一般要求資料包攜帶本電訊廠商網卡的IP地址作為源(這一般是為了在該電訊廠商核心網終點處做NAT),因此為了支援多電訊廠商多網卡負載平衡,一個應用程式業務流資料包必然要支援不同的IP地址作為源,不幸的是,即便對於UDP而言,大多數應用也都是只支援單一源(它們會針對UDP socket調bind),以減少服務端的複雜性。
3.經常性的失聯
電梯裡,高鐵上,山區景點盲區,公司的廁所...你會突然失聯,然後突然出現!但是應用程式卻不希望受到如此的折騰,對於OpenVPN而言,經過測試,一次重連大約要5秒時間,代價是高昂的,重新TLS握手,重新push,...實際上只要你的ping-restart時間足夠小,對於訊號缺失就會很迅速的被OpenVPN感知,解決方案就是將ping-restart放大,可是你也不知道自己失聯多久。
4.RRC相關造成的額外延時有時候,即便你處在訊號很好的地方,也會發現開啟一個網頁非常慢,然後迅速就會變快,這其實是移動網路的本質,為了節省電量損耗,裝置並不是一直和網路保持串連的,而是運行一種和Linux的NOHZ演算法一樣的機制,在裝置長時間沒有資料收發的時候,關閉串連,和Linux的NOHZ不同的是,NOHZ狀態的脫離時間是明確的,它由下一個timer到期的時間以及時鐘之外的任何中斷的最小值決定,但是RRC機制卻不同,資料什麼時候會發送完全取決於神一樣的使用者,因此當有資料要發送的時候,必須重新接入移動網,協商參數等等,這無疑會消耗時間。這個抖動不是使用者應用能解決的,因為這取決於裝置廠商的實現以及移動網路的規範,這是一個純粹的網路問題,因此本文不會涉及過多這方面的內容。
會話層真的太重要了
鑒於TCP/IP棧的搶先進化,其對手便永遠失去了機會,因此應用程式一般都是直接介面在傳輸層協議之上,這是事實! 對於應用開發介面,應用程式的資料收發直接基於一個INET socket,而一個socket的“串連”通過五元組來標識,因此五元組的任何一個元素改變,或者說網路的任何一個事件都會影響到這個對應的socket,socket I/O介面的手冊中明確給出了傳回值和錯誤碼,而直接調用這些介面的應用程式必須處理這種錯誤,因此網路事件便直接影響了應用程式! 但是網路事件不應影響應用,比如網路斷了不一定讓應用程式必須採取善後和重連,也許這隻是暫時事件,比如IP地址變了。應用程式要做的就是產生業務資料並發送,它並不需要直接從socket介面擷取並處理錯誤碼。應用程式只需要知道發送資料發出了多少即可,即便真有嚴重事件需要徹底退出,也不該是來自TCP/IP的通知。那麼一定需要一個新的層,曆史緣故,我稱它會話層吧。
OpenVPN的故事
我希望OpenVPN的處理層完全和網路狀態脫離,即使用戶端的IP地址變了,也能用新的IP地址繼續和服務端通訊,即使訊號全無,一旦有了訊號,通訊繼續進行,也就是說,網路狀態不會打擾到OpenVPN進程的處理。為了一步步地滿足這個需求,我們看一下OpenVPN目前的行為,兩端連通以後,我試著改變用戶端的IP地址,結果服務端報錯:
Wed Jan 1 00:58:46 2014 us=822941 TLS State Error: No TLS state for client 192.168.1.199:33512, opcode=6
解決問題的步驟
寫這篇文章並不是為了表達OpenVPN這個程式如何被用在行動裝置上,這個可以寫上一本書,本文的主要目的是想展示一種解決問題的方式,我在有了上面的思路後是如何驗證其確實可行的呢?我並沒有一頭紮進那沸騰的代碼,去實現最終的方案,比如直接就去修改OpenVPN的協議,而是先將代碼寫死,瞬間得到一個行或者不行的結論。這個過程要修改最少的代碼!為了找到修改何處,還得從上面的報錯入手。其在ssl.c的tls_pre_decrypt_lite函數報錯,該函數沒有任何關於multi_instance的資訊,因此我知道在這個tls_pre_decrypt_lite函數調用之前,程式已經進入異常流了,因此就找tls_pre_decrypt_lite的調用代碼,在mudp.c中的multi_get_create_instance_udp找到了:
struct multi_instance * multi_get_create_instance_udp (struct multi_context *m) { ... if (mroute_extract_openvpn_sockaddr (&real, &m->top.c2.from.dest, true)) { struct hash_element *he; const uint32_t hv = hash_value (hash, &real); struct hash_bucket *bucket = hash_bucket (hash, hv); hash_bucket_lock (bucket); he = hash_lookup_fast (hash, bucket, &real, hv); if (he) { mi = (struct multi_instance *) he->value; } else { // 找不到multi_instance的異常流處理 if (!m->top.c2.tls_auth_standalone || tls_pre_decrypt_lite (m->top.c2.tls_auth_standalone, &m->top.c2.from, &m->top.c2.buf)) { // 異常流處理 } } ... }
執果索因,可行性驗證,測試,D部門側重設計,代碼品質,進度控制,專案管理以及各種模型(迭代瀑布...)。 因此就重新定義hash_function以及hash_compare,讓其返回定值!背後的思想是固定了hash key和hash compare結果之後,如果此時改變了用戶端IP地址而依然不出錯,就說明hash尋找的過程已經和收到資料包的源IP地址和連接埠沒有關係了,剩下的就是將這個hash key從固定值改為從收到的OpenVPN資料的協議頭裡面取就可以了。我的新版hash函數如下:
m->hash = hash_init (t->options.real_hash_size, fake_addr_hash_function, fake_addr_compare_function);
uint32_t fake_addr_hash_function(const void *key, uint32_t iv) { return 0x10101010; } bool fake_addr_compare_function(const void *key1, const void *key2) { return true; }
Wed Jan 1 00:02:11 2014 us=389812 zhaoya/192.168.1.199:38310 UDPv4 WRITE [77] to 192.168.1.199:38310: P_DATA_V1 kid=0 DATA len=76
ASSERT (link_socket_actual_defined (c->c2.to_link_addr));
可見,這個to_link_addr是關鍵,這個值是OpenVPN用戶端接入的時候產生的,以後不會變化,我只要將其改為即時更新的即可,就是說,無條件使用上次資料包的from地址,這些都在context_2結構體:
struct context_2 { ... struct link_socket_actual *to_link_addr; /* IP address of remote */ struct link_socket_actual from; /* address of incoming datagram */ ... }
void process_outgoing_link (struct context *c) { struct gc_arena gc = gc_new (); perf_push (PERF_PROC_OUT_LINK); #if 1 // 吐嘈時罵過的,實際上我經常這麼玩 { c->c2.to_link_addr = &c->c2.from; } #endif ... }
關於本文的引申
不能杜絕問題的發生,那就忽略掉問題,使其對自身毫無影響。多加一個層就可以隔離問題!你不能讓天公不下雨,但你能帶上傘或穿上雨衣雨鞋,或者將活動改在室內,再或者像我這樣,盡情雨中歡呼...如果你為了不下雨而去研究大氣運行原理,研究讓雲層散開的炮彈,那你就走偏了,雖然最終你可能會成為偉大科學家,但目前,你可能只是因為下雨影響了你的心情,而已。
關於類比不模擬
我這又像是吐槽!有人特別不喜歡類比,特別喜歡沒有意義且又浪費時間的還原真實情景,實際上這是絕對不可能的,你只能儘可能地還原,實際上你也在類比,你也在模擬。 升華意義的類比去掉了模擬的內容!而如果你擁有一個分層的模型的話,那你就太幸運了,因為它可以告訴你要類比什麼。一個Web服務不通,首先你會去telnet而不是去查什麼Web服務的故障!為了在伺服器上實現HTTP請求從哪個網口進來HTTP回應便從同樣網口回複這種需求,使用帶源的ping即可測試,根本無需搭建什麼HTTP伺服器,因為這是IP路由的職責,和HTTP無關,也正因為這樣抓住主要問題,才可以將這個任務交給不懂HTTP的網路工程師。 提到創新,更需要非模擬的類比,即類比次要環境,解決主要問題,對於本文提及的OpenVPN的修改的例子,如果一開始你就想實現一個完備的版本,光看懂代碼就能噁心死,然後修改,調試過程肯定耗時又痛苦,最後還不一定行...一定要控制住那些易變的變數,你每次只能操縱一個把手! 有時候,當我異常堅定宣布肯定的結論時,很多人都不愛聽,他們寧願我說一個帶點餘地的結論,因為他們都知道我是在無法模擬的情況下信口開河的,事實上,科學的思想就是不模擬,只類比!當然這並不適合軟體工程,因為軟體更像是社交工程學而不是科學!你永遠也不能肯定這個軟體沒有任何漏洞了,你不能在軟體工程中搞光滑平面或者思想實驗,正常運行了1000000天的軟體可能會下一秒徹底崩潰!受過這樣洗腦的軟體人整體上都活在緊張的杞人憂天狀態,當然不好理解不模擬得出肯定結論的道理了。但即便如此,在解決點上而非工程意義上的問題時,類比不模擬思想是萬萬不能丟的!