OpenUOM移動性改造-靠新的session iD而不是IP/Port識別客戶端

寫於2014/06/07

 

設備移動性的挑戰

1.設備會經常由於小區或模式切換而更改IP地址。

這種地址更新是移動網絡的正常行爲,不應作爲故障或事故看待,因此理應對應用程序透明,應用不應被此類事件打擾,更無責做善後處理。

2.移動設備存在多張3G/4G/2.75G網卡時,希望這些網卡同時收發數據。

由於這些網卡一般屬於不同運營商網絡,其網絡架構又不同,一般要求數據包攜帶本運營商網卡的IP地址作爲源(這一般是爲了在該運營商核心網終點處做NAT),因此爲了支持多運營商多網卡負載均衡,一個應用程序業務流數據包必然要支持不同的IP地址作爲源,不幸的是,即便對於UDP而言,大多數應用也都是隻支持單一源(它們會針對UDP socket調bind),以減少服務端的複雜性。

3.經常性的失聯

電梯裏,高鐵上,山區景點盲區,公司的廁所...你會突然失聯,然後突然出現!但是應用程序卻不希望受到如此的折騰,對於OpenUOM而言,經過測試,一次重連大約要5秒時間,代價是高昂的,重新TLS握手,重新push,...實際上只要你的ping-restart時間足夠小,對於信號缺失就會很迅速的被OpenUOM感知,解決方案就是將ping-restart放大,可是你也不知道自己失聯多久。

4.RRC相關造成的額外延時

有時候,即便你處在信號很好的地方,也會發現打開一個網頁非常慢,然後迅速就會變快,這其實是移動網絡的本質,爲了節省電量損耗,設備並不是一直和網絡保持連接的,而是運行一種和Linux的NOHZ算法一樣的機制,在設備長時間沒有數據收發的時候,關閉連接,和Linux的NOHZ不同的是,NOHZ狀態的脫離時間是明確的,它由下一個timer到期的時間以及時鐘之外的任何中斷的最小值決定,但是RRC機制卻不同,數據什麼時候會發送完全取決於神一樣的用戶,因此當有數據要發送的時候,必須重新接入移動網,協商參數等等,這無疑會消耗時間。這個抖動不是用戶應用能解決的,因爲這取決於設備廠商的實現以及移動網絡的規範,這是一個純粹的網絡問題,因此本文不會涉及過多這方面的內容。

會話層真的太重要了

鑑於TCP/IP棧的搶先進化,其對手便永遠失去了機會,因此應用程序一般都是直接接口在傳輸層協議之上,這是事實!
       對於應用開發接口,應用程序的數據收發直接基於一個INET socket,而一個socket的“連接”通過五元組來標識,因此五元組的任何一個元素改變,或者說網絡的任何一個事件都會影響到這個對應的socket,socket I/O接口的手冊中明確給出了返回值和錯誤碼,而直接調用這些接口的應用程序必須處理這種錯誤,因此網絡事件便直接影響了應用程序!
       但是網絡事件不應影響應用,比如網絡斷了不一定讓應用程序必須採取善後和重連,也許這只是暫時事件,比如IP地址變了。應用程序要做的就是生成業務數據併發送,它並不需要直接從socket接口獲取並處理錯誤碼。應用程序只需要知道發送數據發出了多少即可,即便真有嚴重事件需要徹底退出,也不該是來自TCP/IP的通知。那麼一定需要一個新的層,歷史緣故,我稱它會話層吧。

OpenUOM的故事

我希望OpenUOM的處理層完全和網絡狀態脫離,即使客戶端的IP地址變了,也能用新的IP地址繼續和服務端通信,即使信號全無,一旦有了信號,通信繼續進行,也就是說,網絡狀態不會打擾到OpenUOM進程的處理。爲了一步步地滿足這個需求,我們看一下OpenUOM目前的行爲,兩端連通以後,我試着改變客戶端的IP地址,結果服務端報錯:

 

Wed Jan  1 00:58:46 2014 us=439027 GET INST BY VIRT: 0e:fe:bc:a3:6f:fe -> zhaoya/192.168.1.197:33512 via 0e:fe:bc:a3:6f:fe
Wed Jan  1 00:58:46 2014 us=439981 zhaoya/192.168.42.197:33512 UDPv4 WRITE [133] to 192.168.1.197:33512: P_DATA_V1 kid=0 DATA len=132
Wed Jan  1 00:58:46 2014 us=822941 TLS State Error: No TLS state for client 192.168.1.199:33512, opcode=6
Wed Jan  1 00:58:46 2014 us=823912 GET INST BY REAL: 192.168.1.199:33512 [failed]
Wed Jan  1 00:58:47 2014 us=197871 MULTI: REAP range 128 -> 144
Wed Jan  1 00:58:47 2014 us=198861 TLS State Error: No TLS state for client 192.168.1.199:33512, opcode=6
Wed Jan  1 00:58:47 2014 us=198887 GET INST BY REAL: 192.168.1.199:33512 [failed]
...上述原則上將陌生的IP/Port看成了新的TLS session,但是OpenUOM的TLS握手和網絡根本就沒有關係。它是在BIO上完成的TLS,用Reliable層保證了傳輸過程的可靠性。但是原諒這個報錯吧,代碼的初衷可能是防止Dos攻擊而不是別的,因爲如果沒有經過成功的TLS握手,那麼一個連接是不可能正常插進來的,否則TLS就該廢掉了。現在着手自己的實現。思路是很清晰的,只是在OpenUOM的協議頭裏面增加一個字段:session ID,服務端用這個session ID識別和區別不同的客戶端,不再基於客戶端的IP/Port來識別和區別不同的客戶端,這樣的話,只要客戶端發出的OpenUOM的數據包被服務端收到了,且解析出來的session ID可以對應到一個multi_instance,那麼這個數據包就是合法的。
       因此,OpenUOM的數據收發和底層的網絡狀態徹底隔離了,只要用OpenUOM協議構造數據包即可,如果網絡狀況不好,那就發不出去,但是隻要網絡恢復,就可以發出去,只要發出去被服務端收到,就能識別和解析並對應到某個multi_instance,如果客戶端IP地址變化了,只要保持到服務端IP地址的可達性,數據就能發送到服務端,只要能到服務端,服務端就能從OpenUOM協議包中解析出session ID,從而對應到一個multi_instance。
       思路有了,也很清晰,那麼怎麼改呢?

 

 

解決問題的步驟

寫這篇文章並不是爲了表達OpenUOM這個程序如何被用在移動設備上,這個可以寫上一本書,本文的主要目的是想展示一種解決問題的方式,我在有了上面的思路後是如何驗證其確實可行的呢?我並沒有一頭扎進那沸騰的代碼,去實現最終的方案,比如直接就去修改OpenUOM的協議,而是先將代碼寫死,瞬間得到一個行或者不行的結論。這個過程要修改最少的代碼!爲了找到修改何處,還得從上面的報錯入手。其在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_openuom_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)) {
                // 異常流處理
            }
        }
...
}

關鍵是multi_instance沒有找到,爲什麼呢?我發現mroute_extract_openuom_sockaddr的傳入參數real,正是根據接收到的數據包的來源IP和端口初始化的,接下來查詢multi_instance哈希表的時候,這個real就是key的值,在客戶端的IP地址改變了之後,當然找不到任何value了,就算找到也是衝突鏈的value,最終返回的爲NULL!接下來是關鍵點,既然是查詢哈希表沒有查到,並且是由於數據包的源IP/Port改變了沒有找到,那麼就忽略掉這個查詢key,即想辦法讓這個查詢百分百可以找到結果。這個思想是快速解決問題的關鍵,正是由於找不到key對應的value才失敗,如果key能找到value的話要是能成功,問題就轉化爲了如何讓key找到value而這我們已經有辦法了,即從buffer裏面取key,實際上這是另一個問題,這難道不是一次深刻的執果索因之旅嗎?我在高中的物理競賽中用此法獲得了,唉,不提當年勇!這個思想很簡單,但用的人不多,很多人都是一開始就修改OpenUOM協議,然後到最後一起調試,對於設計方案通過的研發任務,這是常規做法,但對於預研或極限開發來講,這萬萬要不得!你根本就不知道自己的想法在OpenUOM既有框架內是否行得通,怎能一開始就大段改代碼呢?R&D沒有寫成RD是因爲它們實際上不是一個部門,起碼員工解決問題的思路是不同的,R部門側重因果推導,執果索因,可行性驗證,測試,D部門側重設計,代碼質量,進度控制,項目管理以及各種模型(迭代瀑布...)。

 

       因此就重新定義hash_function以及hash_compare,讓其返回定值!背後的思想是固定了hash key和hash compare結果之後,如果此時改變了客戶端IP地址而依然不出錯,就說明hash查找的過程已經和收到數據包的源IP地址和端口沒有關係了,剩下的就是將這個hash key從固定值改爲從收到的OpenUOM數據的協議頭裏面取就可以了。我的新版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;
}

就改這些即可!服務端已經可以將客戶端對應到唯一的那個multi_instance了,並且成功解析封裝後的IP報文,可是發現服務端往客戶端返回的時候沒有通過,我通過192.168.1.199接入OpenUOM成功,然後將OpenUOM客戶端的地址改爲了192.168.1.197,OpenUOM客戶端所在機器長ping服務端的虛擬IP地址,服務端日誌打印如下:
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
發現寫入的目標地址還是192.168.1.199,爲何沒有切換到我新改的地址?我覺得這是一個小問題。我只要找到打印上面日誌的位置就好,而這很簡單,代碼在forward.c的process_outgoing_link函數中,注意以下的代碼:
 
ASSERT (link_socket_actual_defined (c->c2.to_link_addr));
可見,這個to_link_addr是關鍵,這個值是OpenUOM客戶端接入的時候生成的,以後不會變化,我只要將其改爲實時更新的即可,就是說,無條件使用上次數據包的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 */
...
}

註釋很清晰!怎麼改呢?可以將使用to_link_addr的地方全部使用&from,當然我不會這麼做,因爲這只是一個可行性證實,不是正兒八經的改代碼,如此魯莽是不對的,我的做法是添加一段臨時代碼:
 
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
...
}

至此,我認爲當初的想法是可行的。事後,我試着在OpenUOM的協議中增加了一個32位的session ID,然後徹底更改了hash function,傳入的key就是從BPTR(buf)中取出的這個session ID,並且我把OpenUOM客戶端的數量增加到了3個,同樣是一致的結果,時間定在了晚上1點35分。如果這是一個下雨的夜晚,我可能會做更多的修改,但是這是一個燥熱的初夏之夜!
       修改OpenUOM以適應移動性這個想法是我突然想到的,由於近期工作基本和網絡無關,且比較雜亂又有嚴格deadline,我也就不能在工作時間有那麼多的閒情雅緻,只能選一個下雨的夜晚來折騰。可是雨到底也沒來,我也就只能做個半拉子,還有哪些方面沒做呢?難道上面說的不就是全部嗎?不是全部,很多細節都沒有處理,比如客戶端改變IP地址後服務端要即使更新該客戶端對應的multi_instance中相關數據結構,比如echo機制的ping替換各發各自的ping機制,另外各種restart重連機制也要被替換,移動環境中,很多在非移動環境中發生的導致應用必須重連重置的網絡事件都是正常的,因此必須最少化重連重置操作,需要做的就兩件事,一是繼續發送,二是等!

關於本文的引申

不能杜絕問題的發生,那就忽略掉問題,使其對自身毫無影響。多加一個層就可以隔離問題!你不能讓天公不下雨,但你能帶上傘或穿上雨衣雨鞋,或者將活動改在室內,再或者像我這樣,盡情雨中歡呼...如果你爲了不下雨而去研究大氣運行原理,研究讓雲層散開的炮彈,那你就走偏了,雖然最終你可能會成爲偉大科學家,但目前,你可能只是因爲下雨影響了你的心情,而已。
       在定位問題的過程中,千萬不要過早扎入代碼細節,用最快的方式驗證可行性和合理性。然後再細嚼慢嚥,要分清主要問題和次要問題,主要問題簡化其本質,次要問題模擬其現象,不應該爲次要問題浪費大量時間和精力。你不可能一次搞定所有問題,學會模擬當前非本質問題造成的現象是一種技巧技能,能從簡化環境中得到真實結論是一種推理技能。
       我舉一個我早年的反例子。有一次因爲文件寫入出錯,搞了我一個多禮拜沒搞定,最終也沒搞定,但是卻學會了ext2文件系統的數據恢復,八杆子打不着的一個問題,而且不是用debugfs實現,是自己編程哦!對個人好奇心而言,我沒什麼損失,可是卻耽誤了進度,其實最可悲的是那些不是好奇心使然的coder,迷迷糊糊就到了內核驅動,然後一無所獲或者鼻青臉腫出來...
       要問我爲何喜歡雨天,也許它能將飄忽在天上的不穩定小結核沉澱,那是胡扯!喜歡雨天是因爲,其實我也不知道...

關於模擬不仿真

我這又像是吐槽!有人特別不喜歡模擬,特別喜歡沒有意義且又浪費時間的還原真實情景,實際上這是絕對不可能的,你只能儘可能地還原,實際上你也在模擬,你也在仿真。
       昇華意義的模擬去掉了仿真的內容!而如果你擁有一個分層的模型的話,那你就太幸運了,因爲它可以告訴你要模擬什麼。一個Web服務不通,首先你會去telnet而不是去查什麼Web服務的故障!爲了在服務器上實現HTTP請求從哪個網口進來HTTP迴應便從同樣網口回覆這種需求,使用帶源的ping即可測試,根本無需搭建什麼HTTP服務器,因爲這是IP路由的職責,和HTTP無關,也正因爲這樣抓住主要問題,纔可以將這個任務交給不懂HTTP的網絡工程師。
       提到創新,更需要非仿真的模擬,即模擬次要環境,解決主要問題,對於本文提及的OpenUOM的修改的例子,如果一開始你就想實現一個完備的版本,光看懂代碼就能噁心死,然後修改,調試過程肯定耗時又痛苦,最後還不一定行...一定要控制住那些易變的變量,你每次只能操縱一個把手!

       有時候,當我異常堅定宣佈肯定的結論時,很多人都不愛聽,他們寧願我說一個帶點餘地的結論,因爲他們都知道我是在無法仿真的情況下信口開河的,事實上,科學的思想就是不仿真,只模擬!當然這並不適合軟件工程,因爲軟件更像是社會工程學而不是科學!你永遠也不能肯定這個軟件沒有任何漏洞了,你不能在軟件工程中搞光滑平面或者思想實驗,正常運行了1000000天的軟件可能會下一秒徹底崩潰!受過這樣洗腦的軟件人整體上都活在緊張的杞人憂天狀態,當然不好理解不仿真得出肯定結論的道理了。但即便如此,在解決點上而非工程意義上的問題時,模擬不仿真思想是萬萬不能丟的!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章