【原创】UDP 与 keepalived 组合使用遇到的问题

2019独角兽企业重金招聘Python工程师标准>>> hot3.png


问题场景

业务 A(css) -- 通过 UDP 与服务 B 进行交互;  
服务 B(logserver)-- 处理来自业务的 UDP 请求,并回复应答;

原本业务 A 和服务 B 分别部署在两台机器上,都使用实际 IP 地址进行交互,在这种情况下一切正常;
突然有一天,部署场景发生了变化,业务 A 和服务 B 由于热备需求,开始 需要基于 keepalived 的 VIP 向外提供服务,结果发生了问题。

问题复现

      测试人员发现,在各种不同虚实地址组合的情况下,测试的结果有所不同,测试 case 如下图所示
(下图中“仅实”代表业务 A 所在物理机仅具有实际 IP 地址,未启用虚地址;“虚实
”代表业务 A 所在物理机同时具有虚和实 IP 地址)
【原创】UDP 与 keepalived 组合使用遇到的问题_第1张图片

对应的抓包情况分别如下

实验一
【原创】UDP 与 keepalived 组合使用遇到的问题_第2张图片

实验二
【原创】UDP 与 keepalived 组合使用遇到的问题_第3张图片

实验三
【原创】UDP 与 keepalived 组合使用遇到的问题_第4张图片

实验四
【原创】UDP 与 keepalived 组合使用遇到的问题_第5张图片

可以看到,实验一和实验三中的 UDP 交互失败了。失败的原因,已经由 ICMP 包的 Info 信息给出了,即 "Destination unreachable (Port unreachable)" 。

问题解决

解决该问题,需要理解一下几个知识点:
  • ICMP 协议
  • UDP socket 中的 bindconnect 问题
  • UDP socket 发包时的源地址选择问题(或者称作路由问题)

-=-=-=-=-=-=-= 我是正在看《少帅》的分隔线  =-=-=-=-=-=-=-

(以下内容来自《TCP/IP 详解(卷一 )》,略有语言上的调整)

【ICMP 协议】

ICMP 经常被认为是 IP 层的一个组成部分。它传递差错报文以及其他需要注意的信息;
ICMP 报文通常被 IP 层或更高层协议(TCP 或 UDP)使用。一些 ICMP 报文把差错报文返回给用户进程;
ICMP 报文是在 IP 数据报内部被传输的;
ICMP 报文是在主机之间交换的(根据 IP 地址),而不是基于目的端口号的;

类型字段(Type)可以有 15 个不同的值,以描述特定类型的 ICMP 报文。某些 ICMP 报文还使用代码字段(Code)的值来进一步描述不同的条件。
不同类型由报文中的类型字段和代码字段来共同决定。

ICMP 报文分为查询报文差错报文;针对 ICMP 差错报文有时需要作特殊处理(例如,在对 ICMP 差错报文进行响应时,永远不会生成另一份 ICMP 差错报文,否则会导致无休止地循环);

当发送一份 ICMP 差错报文时,报文始终会包含导致差错的报文的 IP 层首部,以及网络层(TCP/UDP)首部的前 8 个字节。这样,接收 ICMP 差错报文的模块(内核?)就会把它与某个特定的协议和用户进程联系起来;

【ICMP 端口不可达差错】

ICMP 差错报文“端口不可达”报文,是 ICMP 目的不可到达报文中的一种;
UDP 规则之一:如果收到一份 UDP 数据报,而目的端口(原文为“与某个正在使用的进程不相符”)无法匹配到任何运行中的进程,那么 UDP 会返回(内核返回?)一个 ICMP 不可达报文。

由于网络编程中的一个因素,当 ICMP 报文返回时,BSD 系统不把从 socket 接收到的 ICMP 报文中的 UDP 数据通知给用户进程,除非该进程已经发送了一个 connect 命令给该 socket 。 (啥意思?)

【ICMP 报文的处理】

ICMP 优先由内核来进行处理;
  • 有些 ICMP 报文会被传送到所有在内核中登记的用户进程,由用户进程再进一步处理;
  • 如果不存在任何这样的用户进程,那么报文就悄悄地被丢弃;


类型
代码
描述
查询 差错 处理方法
0 0 回显应答(Ping 应答)


用户进程
3
目的不可达




0 网络不可达


无路由到达主机

1 主机不可达


无路由到达主机

2 协议不可达


连接被拒绝

3 端口不可达


连接被拒绝

4 需要进行分片但设置了不分片比特


报文太长

5 源站选路失败


无路由到达主机

6 目的网络不认识


无路由到达主机

7 目的主机不认识


无路由到达主机

8 源主机被隔离(作废不用)


无路由到达主机

9 目的网络被强制禁止


无路由到达主机

10 目的主机被强制禁止


无路由到达主机

11 由于服务类型 TOS,网络不可达


无路由到达主机

12 由于服务类型 TOS,主机不可达


无路由到达主机

13 由于过滤,通信被强制禁止


(忽略)

14 主机越权


(忽略)

15 优先权中止生效


(忽略)


-=-=-=-=-=-=-= 我是正在看《少帅》的分隔线  =-=-=-=-=-=-=-

【UDP 服务器的设计】

当一个应用程序接收到 UDP 数据报时,操作系统必须告诉它是谁发送了这份消息,即源 IP 地址端口号
一些应用程序需要知道数据报是发送给谁的,这要求操作系统从接收到的 UDP 数据报中将目的 IP 地址交给应用程序。不幸的是,并非所有的实现都提供这个功能。

大多数 UDP 服务器是交互服务器。这意味着,单个服务器进程对单个 UDP 端口上(服务器上的名知端口)的所有客户请求进行处理。通常程序所使用的每个 UDP 端口都与一个有限大小的输入队列相联系。来自不同客户的差不多同时到达的请求将由 UDP 自动排队。接收到的 UDP 数据报以其接收顺序交给应用程序(在应用程序要求交送下一个数据报时)。
由于排队溢出造成内核中的 UDP 模块丢弃数据报的可能性是存在的。

大多数 UDP 服务器在创建 UDP 端点时都使其本地 IP 地址具有通配符的特点(即绑定到 INADDR_ANY 上)。这就表明进入的 UDP 数据报如果其目的地为服务器端口,那么在任何本地接口均可接收到它。

当服务器创建 UDP socket 时,它可以把其中一个主机本地 IP 地址(包括广播地址)指定为 socket 的本地 IP 地址。那么,只有当 UDP 包的目的 IP 地址与指定的地址相匹配时,该包才能被送到创建该 UPD socket 的业务层。否则,内核将返回一个 ICMP 端口不可达差错,而服务器(业务层)永远看不到该数据报。

如果存在一个通配的 IP 地址,那么就隐含了一种优先级关系。如果为 UDP socket 指定了特定 IP 地址,那么在匹配目的地址时,始终优先匹配该 IP 地址。只有在匹配不成功时才使用通配 地址进行匹配。

经常可以看到远端 IP 地址和远端端口号都显示为 *.*(或 0.0.0.0:*),其意思是该 socket 将接受来自任何 IP 地址和任何端口号的 UDP 数据报。大多数系统允许 UDP socket 对远端地址进行限制,以令其只能接收来自特定 IP 地址和端口号的 UDP 数据报。

在伯克利派生系统中存在如下副作用如果在指定远端地址(IP 和 PORT)时没有选择本地地址,那么内核将自动选择本地地址。其值就成为“选择到达远端 IP 地址路由时”用于做路由判定的 IP 地址


-=-=-=-=-=-=-= 我是正在看《少帅》的分隔线  =-=-=-=-=-=-=-

网友的一篇文章:《 简单分析一下socket中的bind

在《UNIX 网络编程》中提到:
如果一个 TCP 客户或者服务器未曾调用 bind 捆绑一个端口,当调用 connect 或 listen 时,内核就要为相应的套接字选择一个临时接口。
      从这句话中可以判断出,其实在调用函数创建 socket 时,内核还并未给 socket 分配源地址和源端口。而对于 UDP ,我猜测在调用 sendto 发送数据时,在未捆绑端口的情况下,内核也会随机分配端口。
  而我遇到的特殊应用要求我在用 UDP 发送数据之前要告诉对方我的发送端口,这也就意味着我在 sendto 之前必须要捆绑端口,因此我在发送数据之前就得调用 bind 函数绑定一下端口了。但是我就在想内核既然有随机分配端口的能力,而我需要的也只是让它绑定到一个空闲但确定的端口上,socket 函数 中应该能够提供这种业务。果然,我发现 bind 就具备这种能力,当 bind 中的 port 参数设置为 0 时,就由内核分配可用端口。这样我就不用考虑端口重复的问题,而放心的把这个问题交给内核处理了。
  就在发现 bind 函数针对端口分配的机制的同时,又发现其实 bind 对于源地址的选择也具有类型的处理方式,当系统具有多 IP(多网卡)的情况,当我们把 bind 函数中的 ip 参数置 0 时,则由内核进行 ip 分配。
      之前一直觉得很神奇的 INADDR_ANY 其实它的值就是 0 。所以当只有单一 IP 的时候,就可以用 INADDR_ANY 去代替那个单一的 IP ,因为内核分配的时候只能选择这一个 IP 。从而造成了 INADDR_ANY 就是本机 IP 的现象。若存在多 IP ,则内核会根据某种规则进行地址选择。


问题总结

业务 A 在创建 UDP socket 后,会指定一个范围的端口进行 bind ,实际上就是由内核来选择空闲的端口进行 bind ,之后调用了 connect 接口并在其中指定了服务 B 的地址和端口信息;后续交互直接使用 send 和 recv 接口进行数据包发送和接收;
服务 B 在创建 UDP socket 后,在地址 INADDR_ANY 和特定端口上进行了 bind ,后续通过 sendto 和 recvfrom 进行数据包发送和接收;

问题来了,
      从 UDP 客户端角度,由于业务 A 在 UDP socket 上执行了 connect ,由此导致通过该 socket 仅能接收来自 connect 中指定的“目的地址”的报文;此时若受到来自其他地址的报文,则内核会自动回复 ICMP 告知上面抓包中的错误!!
      从 UDP 服务器角度,由于服务 B 是使用  INADDR_ANY 进行的地址 bind ,故全部可用的地址均会作为内核的可选地址,虚地址也会得到相同的处理。当服务 B 回复 UDP 数据包时,此时的源地址的确定应该是内核按照某种原则进行的(具体地址选取原则不清楚,猜测可能但不限于:根据路由情况、ip 地址的数值大小、虚实 IP 判定等);在上面的实验一和三中,服务 B 在通过虚地址收到了 UDP 包后,却选择了通过实地址进行回复;

【原创】UDP 与 keepalived 组合使用遇到的问题_第6张图片

综上,基本上解释清楚了该问题的原因,希望能够对 UDP 编程中遇到类似问题的同学有所帮助~~






转载于:https://my.oschina.net/moooofly/blog/603323

你可能感兴趣的:(【原创】UDP 与 keepalived 组合使用遇到的问题)