写于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天的软件可能会下一秒彻底崩溃!受过这样洗脑的软件人整体上都活在紧张的杞人忧天状态,当然不好理解不仿真得出肯定结论的道理了。但即便如此,在解决点上而非工程意义上的问题时,模拟不仿真思想是万万不能丢的!