寫於2014/06/07
設備移動性的挑戰
1.設備會經常由於小區或模式切換而更改IP地址。
2.移動設備存在多張3G/4G/2.75G網卡時,希望這些網卡同時收發數據。
3.經常性的失聯
4.RRC相關造成的額外延時
有時候,即便你處在信號很好的地方,也會發現打開一個網頁非常慢,然後迅速就會變快,這其實是移動網絡的本質,爲了節省電量損耗,設備並不是一直和網絡保持連接的,而是運行一種和Linux的NOHZ算法一樣的機制,在設備長時間沒有數據收發的時候,關閉連接,和Linux的NOHZ不同的是,NOHZ狀態的脫離時間是明確的,它由下一個timer到期的時間以及時鐘之外的任何中斷的最小值決定,但是RRC機制卻不同,數據什麼時候會發送完全取決於神一樣的用戶,因此當有數據要發送的時候,必須重新接入移動網,協商參數等等,這無疑會消耗時間。這個抖動不是用戶應用能解決的,因爲這取決於設備廠商的實現以及移動網絡的規範,這是一個純粹的網絡問題,因此本文不會涉及過多這方面的內容。會話層真的太重要了
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部門側重設計,代碼質量,進度控制,項目管理以及各種模型(迭代瀑布...)。
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分。如果這是一個下雨的夜晚,我可能會做更多的修改,但是這是一個燥熱的初夏之夜!
關於本文的引申
不能杜絕問題的發生,那就忽略掉問題,使其對自身毫無影響。多加一個層就可以隔離問題!你不能讓天公不下雨,但你能帶上傘或穿上雨衣雨鞋,或者將活動改在室內,再或者像我這樣,盡情雨中歡呼...如果你爲了不下雨而去研究大氣運行原理,研究讓雲層散開的炮彈,那你就走偏了,雖然最終你可能會成爲偉大科學家,但目前,你可能只是因爲下雨影響了你的心情,而已。在定位問題的過程中,千萬不要過早扎入代碼細節,用最快的方式驗證可行性和合理性。然後再細嚼慢嚥,要分清主要問題和次要問題,主要問題簡化其本質,次要問題模擬其現象,不應該爲次要問題浪費大量時間和精力。你不可能一次搞定所有問題,學會模擬當前非本質問題造成的現象是一種技巧技能,能從簡化環境中得到真實結論是一種推理技能。
關於模擬不仿真
有時候,當我異常堅定宣佈肯定的結論時,很多人都不愛聽,他們寧願我說一個帶點餘地的結論,因爲他們都知道我是在無法仿真的情況下信口開河的,事實上,科學的思想就是不仿真,只模擬!當然這並不適合軟件工程,因爲軟件更像是社會工程學而不是科學!你永遠也不能肯定這個軟件沒有任何漏洞了,你不能在軟件工程中搞光滑平面或者思想實驗,正常運行了1000000天的軟件可能會下一秒徹底崩潰!受過這樣洗腦的軟件人整體上都活在緊張的杞人憂天狀態,當然不好理解不仿真得出肯定結論的道理了。但即便如此,在解決點上而非工程意義上的問題時,模擬不仿真思想是萬萬不能丟的!