本章重点:
k8s 如何利用 ip_vs 实现源 IP 会话亲和性。
ip_vs hook 后
NF_INET_LOCAL_IN
根据优先级依次是 ip_vs_reply4,ip_vs_remote_request4
ip_vs_reply4
| -- ip_vs_out
| -- skb_to_full_sk(skb)
| -- ip_vs_fill_iph_skb // 解析 IP 头
| -- ip_vs_out_icmp // 如果是 icmp
| -- cp = pp->conn_out_get // 如果没有 conn 链接,直接 ACCEPT
| -- handle_response_icmp // 回复 icmp
| -- ip_vs_nat_icmp // 做 nat
| -- ip_vs_update_conntrack // 更新 nf_ct
| -- __ip_vs_conn_put // 更新 ipvs conn 引用计数
| -- ip_vs_proto_data_get // 通过 协议找到 ipvs 链表
| -- pp->conn_out_get // 查找是否有 conn,属于一个存在的 out 连接
| -- handle_response // 查到连接,回复做 snat 处理
--- 没有连接的特殊情况
| -- 进来的报文根据源 ip 和 源 port 也就是 ipvs 的 rs 的 ip 和端口进行查询,如果有且是 UDP 或 TCP&!RESET 的情况,发送 icmp_dest_unreach/icmp_port_unreach,drop。
全是查 conn_out_get,没有再 ACCEPT; 说明,这个 hook 是处理回复的 ip_vs,然后全是 nat 模式的 ipvs。即处理 realserver 回复报文时的处理。
conn_out_get 对应对应协议的连接查询,如 tcp/udp/sctp 为 ip_vs_conn_out_get_proto;
像 k8s 的 service 需要重点关注,这是后端 pod 回复到 client 所在节点后的处理流程。
ip_vs_remote_request4
ip_vs_remote_request4
| -- ip_vs_in
| -- (skb->pkt_type != PACKET_HOST && hooknum != NF_INET_LOCAL_OUT) 不是本地包,ipvs 不处理
| -- ip_vs_fill_iph_skb // 解析 IP 头
| -- pd = ip_vs_proto_data_get(ipvs, iph.protocol);
| -- pp = pd->pp;
| -- cp = pp->conn_in_get(ipvs, af, skb, &iph); // 根据报文协议,找到对应的 conn 表,查询报文是否属于已存在的 连接
| -- (conn_reuse_mode && !iph.fragoffs && is_new_conn(skb, &iph) && cp) // 判断 内核开启 reuse_mode,不是分片,新连接(tcp syn),查到 连接 (连接 reuse)
| -- tcp 连接的状态是否允许 reuse (TIME_WAIT,CLOSE...)
| -- 没有 连接和可 reuse 的连接
| -- ip_vs_try_to_schedule
| -- conn_schedule 对应协议的 conn_schedule // tcp 的 tcp_conn_schedule
| -- tcp_conn_schedule 根据报文查询 ipvs 的 service
| -- ip_vs_schedule ipvs 主要函数,调度去获取真实的后端
| -- 检查是否已存在连接,只调度未存在连接的情况
| -- 有连接
| -- 对 连接 加计数
| -- 没有连接
| -- ip_vs_sched_persist // 持久属性的 svc,详细内容见下一块
| -- sched->schedule // 通过 svc 的调度器调度处 dest
| -- ip_vs_conn_new 创建连接
| -- ip_vs_bind_xmit,根据 svc 的模式绑定发包规则(包括 nat/tunnel/dr/bypass,k8s service 为 ip_vs_nat_xmit)
| -- cp->packet_xmit 用 绑定的方法处理报文
关于 conn_reuse,是在考虑 ip_vs 的优雅关闭后端和 高并发下 端口重用的兼容问题。大致意思:在连接尚未结束时,还是会将数据发到准备去掉的 rs,端口重用会丢掉第一个 syn 包导致重传;所以在 k8s 场景下不建议两个全开,ps 我们不开 reuse。
ip_vs_sched_persist
在 k8s 环境中通常是 sessionAffinity: ClientIP
属性的 service 配置的。设置源 IP 亲和性,同一个 clientIP 调度到同一个 real server。
| -- ip_vs_sched_persist 根据模版创建连接,如果没有创建模板,支持 TCP/UDP,通常 sip 模式使用;
| -- ip_vs_conn_fill_param_persist 填充 param 并查找对应的 template,填充如果是 persist 的svc,还需要调用 对应调度器的 fill_param 函数
| -- ip_vs_ct_in_get 根据 param 查找 ct template
| -- ip_vs_check_template 如果有 模板,进行判断,如果有后端,后端是否正常,如果不正常,将 ct 目的端口,svc 端口改为 65535,然后更新
| -- dest = sched->schedule 如果没找到模板 或者 ct 后端无效,进行此方法调度,调度为创建 svc 的方法,前文已介绍
| -- ip_vs_conn_new 用新调度到的后端创建连接模板 即 ip_vs 的 ct,ct 的 timeout 由 svc 的 timeout 获取。
| -- 查到 template ct
| -- dest = ct->dest; 直接获取 ct 的 dest
| -- 修改 目的 port 从 svc 的 port 到 dest 的 port
| -- ip_vs_conn_new 根据 template 创建连接
| -- ip_vs_control_add 将连接的 controller 改为 模板
| -- ip_vs_conn_put 将模板的计时重置
| -- ip_vs_conn_stats 更新计数等
可以大致清楚 k8s service 的会话亲和性是如何利用 ip_vs 的 sip 来实现的 ,ip_vs sip 实际上开始是为了支持 SIP 协议(会话初始协议),pe 的数据主要是存储 SIP 协议中的 callid,通过 pe 的匹配来保证会话亲和性。
会话亲和性,主要实现是先生成一个根据源 IP 创建的 template 连接,然后在根据模板连接生成真实连接,生成后会更新模板的有效时间。模板连接的特征是源端口为 0,且当生成的模板后端 rs 失效时,会重新生成,并把原模板更新为不可用(具体为设置 源端口为0,service 端口和目的端口为 65535)