0x01 背景
Pod需要使用远程存储的PV,由同k8s集群内的服务提供的存储服务。一开始的做法是:
- CSI中解析Service的clusterIP。
- 然后使用clusterIP挂载PV卷。
但因为走clusterIP时,经过多次转换:
- clusterIP到Pod IP 经过了1次NAT
- Pod IP到最终服务。经过1次转发,具体性能损耗跟 CNI 实现相关。
导致了最终client写PV的性能损失严重。
0x02 解决方法
既然走容器网络导致性能差,修改服务端的部署形式为 hostNetwork,绕过容器网络。但带来一个问题,存储服务可能切换节点,导致 client 端无法正常重连(切换节点带来的数据不一致的问题能处理),这一点不能接受。
新的方案:
为服务端创建一个 Headless Service,针对 Deployment 类型的负载Headless Service会解析提到所有Pod的 IP 地址列表具体见官方文档,那唯一的问题还剩下 client 重连时,这个域名怎么解析?因为使用的驱动是内核提供的,内核中无法直接使用glibc的域名解析功能,即无法使用外部的DNS Server,即使是/etc/resolv.conf中指定的。
0x03 request-key 机制
通过调查了解到内核提供了request-key机制,可以从内核调用到用户态的应用。request-key本来是用于内核和用户态之间的安全token管理的,后也扩展用于其他用途。以内核解析域名来说,大概流程如下:
- 内核发起域名解析转到dns_resolver模块【内核态】。
- 发起request-key请求转到key管理模块【内核态】。
- key管理模块调用/sbin/request-key,调用到用户态【内核态】。
- /sbin/request-key根据/etc/request-key.conf中的配置,分发到对应的命令调用,示例为/sbin/key.dns_resolver【用户态】。
- /sbin/key.dns_resolver调用glibc域名解析,完成解析,并调用request-key相关系统调用,设置好payload,即域名对应的IP地址【用户态】。
但还有新的问题:key.dns_resolver只能使用/etc/hosts和/etc/resolv.conf解析域名,不支持从额外的dns server解析域名。
0x04 具体的方案
所有的方法都要通过修改 /etc/request-key.conf配置文件指定自己的程序进行解析。
后面的流程有以下方案:
自己写脚本,通过/etc/request-key.conf配置文件指定自己的脚本,通过kubectl去查询 Pod IP地址,调用/sbin/request-key将结果写回。
问题:C语言中对字符串的处理在dns_resolver和request-key两个模块之间发生了冲突,使用/sbin/request-key写入的IP地址被dns_resolver内核模块认为非法,这个方案行不通,详见QA部分解释。
通过C调用key-utils的SDK,可以实现同样的功能,但基本上照抄key.dns_resolver的实现。突然想到可以用Python调用so库的方法,验证了下基本可行。但又有一个新的问题:
Python标准库中的域名解析同样不支持指定域名,想要支持就要引入第3方的dns模块。
最终方案比较:
方案 |
优点 |
缺点 |
写Python调用key-utils的SDK so完成IP写回内核 |
灵活控制对coreDNS的访问。 |
需要调用第3方的dns解析服务、或者直接访问 kube-apiserver获取IP,加重kube-apiserver的负担。 |
shell脚本通过unshare mount namespace 隔离,生成临时的/etc/resolv.conf,调用/sbin/key.dns_resolver实现 |
不用访问 kube-apsierver,根据kubelet的配置可获取coreDNS的地址,不用感知具体的DNS解析细节。更通用,其他的 headless也可以用 |
无法控制调用频率 |
考虑这种异常切换解析并不会太频繁,最终选择了第2种方案。mount namespace 可以方便地通过 unshare -m 来实现。
0x05 补充QA
Q:/sbin/key.dns_resolver支持从/etc/hosts解析域名,为什么不修改 /etc/hosts?
A:/etc/hosts是全局配置,修改冲突不容易控制,出现冲突时影响不可控。
Q:为什么不能修改/etc/resolv.conf配置,指向coreDNS?
A:虽然coreDNS也支持将非k8s域名转向宿主中/etc/resolv.conf中的指定的DNS,但这种机制依赖 coreDNS,对整个系统的影响过大。
Q: 为什么不用/sbin/request-key回写解析到的IP地址?
A:这种实现了验证了,发现request-key和dns_resolver的实现关于C中字符串的处理有不一致的地方,前者payload长度未包含\0,后者要求包含。这一点是通过bpf钩子确认的。
0x06 总结
问题的解决过程中尝试了多种方案,最终最适合的方案巧妙运用了命名空间隔离机制,这也是了解容器底层原理的好处。
同时带来一点关于命名空间的用途回顾:
- 容器内不希望被宿主机影响。
- 容器内不期望影响宿主机(本文中的场景),可随意设置/etc/resolv.conf。