告知你不为人知的 UDP:连接性和负载均衡( 四 )


为了充分利用多核 CPU (为简化讨论,不妨假设为8核),理想情况下,同时有8个工作进程在同时工作处理请求 。于是我们会初始化8个绑定相同端口,相同IP地址(SO_REUSEADDR、SO_REUSEPORT)的 UDP socket ,接下来就靠内核的查找算法来达到client请求的负载均衡了 。由于内核查找算法是固定的,于是,无形中所有的client被划分为8类,类型1的所有client请求全部被路由到工作进程1的UDP socket由工作进程1来处理,同样类型2的client的请求也全部被工作进程2来处理 。这样的缺陷是明显的,比较容易造成短时间的负载极端不均衡 。
一般情况下,如果一个 UDP 包能够标识一个请求,那么简单的解决方案是每个 UDP socket n 的工作进程 n,自行 fork 出多个子进程来处理类型n的 client 的请求 。这样每个子进程都直接 recvfrom 就 OK 了,拿到 UDP 请求包就处理,拿不到就阻塞 。
然而,如果一个请求需要多个 UDP 包来标识的情况下,事情就没那么简单了,我们需要将同一个 client 的所有 UDP 包都路由到同一个工作子进程 。为了简化讨论,我们将注意力集中在都是类型n的多个client请求UDP数据包到来的时候,我们怎么处理的问题,不同类型client的数据包路由问题交给内核了 。这样,我们需要一个master进程来监听UDP socket的可读事件,master进程监听到可读事件,就采用MSG_PEEK选项来recvfrom数据包,如果发现是新的Endpoit(ip、port)Client的UDP包,那么就fork一个新的进行来处理该Endpoit的请求 。具体如下:

  • [1] master进程监听udp_socket_fd的可读事件:pfd.fd = udp_socket_fd;pfd.events = POLLIN; poll(pfd, 1, -1); 当可读事件到来,pfd.revents & POLLIN 为true 。探测一下到来的UDP包是否是新的client的UDP包:recvfrom(pfd.fd, buf, MAXSIZE, MSG_PEEK, (struct sockaddr *)pclientaddr, &addrlen);查找一下worker_list是否为该client创建过worker进程了 。
  • [2] 如果没有查找到,就fork()处理进程来处理该请求,并将该client信息记录到worker_list中 。查找到,那么continue,回到步骤[1]
  • [3] 每个worker子进程,保存自己需要处理的client信息pclientaddr 。worker进程同样也监听udp_socket_fd的可读事件 。poll(pfd, 1, -1);当可读事件到来,pfd.revents & POLLIN 为true 。探测一下到来的UDP包是否是本进程需要处理的client的UDP包:recvfrom(pfd.fd, buf, MAXSIZE, MSG_PEEK, (struct sockaddr * )pclientaddr_2, &addrlen); 比较一下pclientaddr和pclientaddr_2是否一致 。
该fork模型很别扭,过多的探测行为,一个数据包来了,会”惊群”唤醒所有worker子进程,大家都去PEEK一把,最后只有一个worker进程能够取出UDP包来处理 。同时到来的数据包只能排队被取出 。更为严重的是,由于recvfrom的排他唤醒,可能会造成死锁 。考虑下面一个场景:
假设有 worker1、worker2、worker3、和 master 共四个进程都阻塞在 poll 调用上,client1 的一个新的 UDP 包过来,这个时候,四个进程会被同时唤醒,worker1比较神速,赶在其他进程前将 UPD 包取走了( worker1可以处理 client1的 UDP 包),于是其他三个进程的 recvfrom 扑空,它们 worker2、worker3、和 master 按序全部阻塞在 recvfrom 上睡眠( worker2、worker3 排在 master 前面先睡眠的) 。这个时候,一个新 client4 的 UDP 包packet4到来,(由于recvfrom的排他唤醒)这个时候只有worker2会从recvfrom的睡眠中醒来,然而worker而却不能处理该请求UDP包 。如果没有新UDP包到来,那么packet4一直留在内核中,死锁了 。之所以recv是排他的,是为了避免“承诺给一个进程”的数据被其他进程取走了 。
通过上面的讨论,不管采用什么手段,UDP的accept模型总是那么别扭,总有一些无法自然处理的小尾巴 。UDP的多路负载均衡方案不通用,不自然,其本因在于UPD的无连接性、无序性(无法标识数据的前续后继) 。我们不知道 client 还在不在,于是难于决策虚拟的”连接”何时终止,以及何时结束掉fork出来的worker子进程(我们不能无限 fork 吧) 。于是,在没有好的决策因素的时候,超时似乎是一个比较好选择,毕竟当所有的裁决手段都失效的时候,一切都要靠时间来冲淡 。

【告知你不为人知的 UDP:连接性和负载均衡】


推荐阅读