浅谈linux下基于UDP服务的负载均衡方法


浅谈linux下基于UDP服务的负载均衡方法

文章插图
 
UDP在并入TCP/IP族的时候 , 就是作为IP协议的第四层抽象存在的 , IP的协议单元叫做IP数据报 , UDP的名字正是来源于此 , 用户数据报协议 , 其中“用户”便有了端到端的意思 。起初UDP只是作为TCP的补充存在 , 应用于一些无需维护连接 , 无需维护状态的场合 , 然而随着TCP越来越复杂 , 随着复杂性越来越往上层转移 , 很多应用程序开始在UDP之上自行处理连接状态 , 数据序列等 , 这样便可以自行控制好复杂度 , 比较典型的是DTLS协议(SSL的UDP版本)以及OpenVPN 。
OpenVPN原理上讲不便于使用TCP作为承载协议 , 使用UDP将会是比较高效的选择 , 然而这种高效却被其单进程单处理给抵消掉了 , 特别是当你在8核CPU上运行OpenVPN服务 , 却看到始终有7个CPU核心处于空闲状态的时候 。愤慨之余要着手解决问题 。我的random redirect NAT+多OpenVPN进程绑定多端口方式已经可以作为一个解决方案 , 然而它还是利用了很多外围的东西 , 最典型的就是它用ip_conntrack为UDP灌入连接的概念 , 因此它依赖于ip_conntrack!而我希望的是 , 可以像TCP那样实现一个多处理 , 比如预分配worker进程的多处理 , 比如连接到来后再分配进程的多处理 , 使用UDP , 可以做到吗?
1.SO_REUSEADDR绑定相同的地址端口REUSEADDR没有太多复杂的东西 , 总之就是要确保四元组的唯一性即可 , 不管对于TCP还是对于UDP都一样 。对于TCP而言 , bind的时候所有潜在的可能冲突(此时还不知道对端是谁)的绑定都会被禁止 , 对于connect的时候 , 此时本端对端均已经明确 , 只有明确不会破坏4元组唯一性的connect才会发送SYN包 , 对于Listen方 , 无需考虑4元组唯一性 , 因为connect方已经可以保证4元组唯一了 。
UDP socket的4元组唯一性保证手段比较有趣 , 由于UDP无连接状态 , 没有办法通过“将新的连接匹配保存下来的已经有的4元组连接”来判断是否4元组冲突 , 那么办呢?办法就是按照固定的算法查找目标UDP socket , 这样每次查到的都是UDP socket链表(或者hash表 , 具体存储形式无关紧要)固定位置的socket , 这样自然而然把其它位置冲突的UDP socket给废掉了!你可以试着在一个进程或者多个进程内创建多个绑定相同端口 , 相同IP地址的UDP socket , 然后在另外的机器向其发送数据 , 你会发现 , 只有最后一个创建的socket会接收到数据 , 其它的都是默默地等待 , 孤独的等待 , 像小说《十年》里面的那样...此时如果再创建一个绑定同样地址端口的UDP socket , 这最后一个便开始接收数据了 。以上说的“最后一个”并不绝对 , 取决于算法 , 关键是“固定的位置” , 这个UDP socket查找算法很简单 , 只是基于目标IP地址和目标端口做查找 , 找到第一个则返回它 , 因此具体找到哪个取决于UDP socket在创建的时候是按何种顺序插入的 。
对于UDP socket的客户端 , 也一样 , 如果你创建了两个绑定相同IP地址和相同端口的UDP socket , 然后用第一个socket往一个特定的目标sendmsg , 可能会是第二个socket接收到数据 , 到底是哪一个取决于UDP socket在第四层的查找算法 。这个原理你可以看一下第四层的UDP socket查找逻辑 , 其核心在于compute_score函数:
if (inet->daddr) {if (inet->daddr != saddr)return -1;score += 2;}if (inet->dport) {if (inet->dport != sport)return -1;score += 2;}一般而言 , UDP socket如果没有调用connect , 其daddr和dport是不会赋值的 , 因此关于这两个字段的判断相当于没有结果的判断 , 如果对于客户端socket , 且调用了connect , 那么就会按照4元组完全匹配的原则来匹配 。
因此 , 创建多个绑定相同IP地址 , 相同端口的UDP程序 , 会起到热备份的作用 , 不会起到负载均衡的作用 。另外如果新创建了同样绑定的新UDP socket , 会改变原有多个socket在链表(或者别的什么容器)中的位置 , 数据到来时 , 具体会匹配到哪个socket , 依然取决于固定的算法 。


推荐阅读