Atlantis
GitHub Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage

3. CIDR冲突

1. 前言

在搭建 K8s 集群时,通常需要关注 5 个 CIDR:

  1. 公网 CIDR:对于公有云来说,云上网络一般只会采用内网网段(10.0.0.0/8、172.16.0.0/12、192.168.0.0/16),不存在与公网网段冲突的情况,而私有云情况会复杂一些,有可能选择一个公网网段作为公网 CIDR,也有可能从内网网段中选取一段,甚至直接使用大内网模式(类似阿里云经典网),这时云上云下处于一个大内网下,设备可以直接互通
  2. 节点 CIDR:组成 K8s 集群的节点网络,从内网网段中分配:先建 VPC,再建 Subnet,最后选 Subnet 建虚拟机。overlay 网络需要避开节点网段,underlay 网络则会选择与节点能直接互通的网段,根据网络插件的实现可能预分配一个与节点 CIDR 不冲突的网段,也可能直接从节点 CIDR 中分配
  3. Service CIDR:用于为 Service 分配 IP 的网段,选择与 节点 CIDR 不冲突的网段,不能与 Pod CIDR冲突
  4. Pod CIDR:用于为 Pod 分配 IP 的网段,选择与 节点 CIDR 不冲突的网段,不能与 Service CIDR 冲突
  5. 内网公共服务 CIDR:用于为内网公共服务分配 IP 的网段,比如内网 OpenAPI、内网公共镜像仓库等,可能固定占用一部分内网网段,也可能使用 100.64.0.0/10 这种预留网段

了解完上述背景,就可以开始讲故事了。

2. STAR

2.1 Situation

测试同学在一个公司内部私有云测试环境上创建了一个容器集群后,发现上面的 Pod 无法访问公共服务,比如对象存储、文件存储,阻塞 POC 项目的测试用例,而且测试同学引用了一个驻场同学反馈的无法访问存储的情况,这将问题提升到了另一个高度。

2.2 Task

由于开出了 P1 级别的问题单,且上了领导们晚上的夕会讨论,Leader 让我放下开发任务,优先定位问题,先提供绕过方案,再考虑修复方案。

2.3 Action

当前最重要的任务是解决测试同学的问题阻塞,只有将问题降级并移出夕会讨论列表,才能避免 Leader 被反复拷问。

首先笔者联系了测试同学要来了测试环境信息,拿到了容器集群访问权限、集群节点的登录密码、S3 存储的访问信息(域名、bucket、密钥),测试同学阻塞的用例是在 Pod 里访问 S3 存储,演示步骤是 kubectl exec 进入容器,然后使用 mc 命令行上传和下载示例文件。

异常 Pod 所在的集群使用 Flannel 网络插件,在容器内访问某个不在 Pod CIDR、Service CIDR 的 IP 时,固定要经过 SNAT 地址转换为节点 IP 出去,如果节点无法访问目标地址,它上面的容器也无法访问。

笔者首先登录节点尝试 ping 域名,发现 S3 存储的 IP 地址为 10.255.x.x,使用 telnet 连接 S3 存储服务的 80 端口正常,而该容器集群的 PodCIDR 为10.255.0.0/16,ServiceCIDR 为 172.16.0.0/16,显然存在地址冲突。

笔者将问题反馈给测试同学后,一起拉了存储部门的 SRE 确认了下:

  1. 该私有云环境是 ARM 架构,与另一个 X86 架构的私有云共用一套公司内部的公共存储集群
  2. 公司内部的网络规划是取一部分 10 段地址作为内网 EIP,跨区域打通
  3. 生产环境中存储集群使用的内网 EIP(或者叫经典网 IP),一般不与三个私有网段重叠

到这里已经明确问题原因归属环境问题,只要环境正常,加上控制台创建集群的页面中有网段冲突检测防护,不会在生产环境中发生类似问题,和测试同学沟通后,她也同意降级问题,但私下又跟我反馈了另外两个问题:

  1. 还有一个测试环境,没有上述的网段冲突问题,但是访问一些 SAAS 服务也会异常
  2. 控制台创建集群中选择网段的地方有改过一次,但是快速乱点的时候,可以选到一个和 VPC 网段重叠的网段

笔者意识到问题没有那么简单,找 Leader 沟通后拉了网络部门 SRE 确认了内网 IP 的是如何划分的,情况比较复杂:

  1. 对于云上云下完全隔离的私有云环境,会使用 100 段地址作为内网 EIP,通常使用一段公网 IP 作为外网 EIP,用于打通云上云下(私有云本身是与公网隔离的环境,占用公网 IP 作为外网 EIP 没有毛病……)
  2. 对于云上云下网络直通的私有云环境,可以理解为一个大内网环境,需要驻场同学引导客户规划网络,比如将 10.0.0.0/8 划分成几个大网段,分别用于创建内外网 EIP、用户 VPC,这种情况下在 VPC 中创建好一台虚拟机后,就可以与云下的设备互访
  3. 还有一些环境由于部署原因或者历史原因,只能创建一些特定网段的 VPC 避免网络冲突才可以使用
  4. 目前没有接口查询这些预占网段的信息,因为没有需求……客户现场的私有云都是部署人员规划的,这样更灵活(笔者当时已经翻起白眼)

而针对测试同学反馈的第二个问题,笔者与前端同学核对了一下,集群网段冲突是纯前端校验的,采用前缀匹配的办法

  1. 首先确认 VPC 网络的第一个数字(比如 192.168.0.0/16 的 192)
  2. 对于 overlay 网络,确保 VPC 的 CIDR 与 Pod CIDR 和 Service CIDR 网段的第一个数字不冲突
  3. 对于 underlay 网络,确保 Pod CIDR 与 Service CIDR 都落在 VPC 的 CIDR 中,两者不冲突,且不与 VPC 下已存在的 Subnet 冲突(使用子网掩码比较)。

检验逻辑看起来没毛病,实际操作下来在乱点一通后会突破限制,比如 Pod CIDR 与 Service CIDR 或者 VPC CIDR 重叠,创建集群的请求一路透传到底座服务,后端没有任何网段检查!!!

这下变成一个严重问题了,网段冲突就像一颗定时炸弹,如果客户创建了一个异常容器集群并部署的大量服务,那时遇到网络不通几乎也无法修复了。

笔者和 Leader 紧急拉了一个会议,梳理了以下的应对方案:

  1. 公司内部环境遇到类似问题,统一回复环境问题,然后协助测试同学重建集群,规避网络冲突
  2. 客户私有云环境的网络规划不可控,容器产品只能通过文档和操作手册引导网络规划,确保虚拟机可以访问的服务,Pod 也可以访问,其他的情况暂不考虑
  3. 后端新增一个校验 CIDR 的接口,前端在选择好网段进入下一个页面前调用一次,接口需要考虑 overlay 与 underlay 网络、IPv4、IPv6
  4. 前端需要拦截异常操作
  5. 确认测试同学引用的那个现场问题具体情况

2.4 Result

  1. 协助测试同学新建了一个 Pod CIDR 为 192.168.0.0/16 的集群后,测试访问 S3 正常,问题单从 P1 降低到 P3
  2. 新开发了一个校验网段接口,与前端同学联调后测试通过,使用这个 P3 问题单合入了四个生产分支
  3. 根据新的交互流程梳理了操作手册并归档
  4. 对于重点客户的私有云环境,检查存量集群是否有类似问题,测试同学引用的那个现场环境问题原因是配置错误,已经解决

3. 补充信息

3.1 网段冲突时产生的现象

  1. 如果目标 IP 与 Pod CIDR 冲突,当 Pod 内应用程序尝试访问外部 IP 时,首先会查询 Pod 内部的路由表,路由表会将流量导向 CNI 网络接口,认为目标 IP 在 Pod 网络内,最终网络请求不可达,或者发送到集群内已经分配了相同 IP 的 Pod
  2. 如果目标 IP 与 Service CIDR 冲突,对于启用 IPVS 的集群,只要该 IP 未分配给 Service,Pod 内应用程序还可以正常访问目标 IP
  3. 节点拥有独立的网络配置和路由表,如果目标 IP 未分配给 Pod 或 Service,节点就可以正常访问该 IP

3.2 网段校验

以下是 golang 实现

检查两个CIDR是否重叠,支持IPv4与IPv6

func CheckCIDROverlap(cidrAStr string, cidrBStr string) error {
	_, cidrA, err := net.ParseCIDR(cidrAStr)
	if err != nil {
		return err
	}
	_, cidrB, err := net.ParseCIDR(cidrBStr)
	if err != nil {
		return err
	}
	if cidrA.Contains(cidrB.IP) || cidrB.Contains(cidrA.IP) {
		return errors.New("cidr overlap")
	}
	return nil
}

检查第一个CIDR是否包含第二个CIDR,支持IPv4与IPv6

func CheckCIDRContains(cidrAStr string, cidrBStr string) error {
	_, cidrA, err := net.ParseCIDR(cidrAStr)
	if err != nil {
		return err
	}
	onesA, _ := cidrA.Mask.Size()
	_, cidrB, err := net.ParseCIDR(cidrBStr)
	if err != nil {
		return err
	}
	onesB, _ := cidrB.Mask.Size()
	if cidrA.Contains(cidrB.IP) && onesA <= onesB {
		return nil
	}
	return errors.New("cidr out of range")
}

生成建议集群网段(overlay 网络)

根据 VPC CIDR 计算建议的 IPv4 集群网段,默认顺序为 Pod CIDR、Service CIDR

func GetSuggestK8SCIDR(vpcCIDR string) ([]string, error) {
	// 检查VPC网段
	if _, _, err := net.ParseCIDR(vpcCIDR); err != nil {
		return nil, errors.Wrap(err, "parse vpc cidr")
	}
	// IPv4返回两个不冲突的掩码为16的CIDR
	// 192.168.0.0/16 172.16-31.0.0/16 10.0~255.0.0/16
	v4CIDRMap := map[string]func(seed int) string{
		"192.168.0.0/16": func(seed int) string { return "192.168.0.0/16" },
		"172.16.0.0/12": func(seed int) string {
			index := seed % 16
			return fmt.Sprintf("172.%d.0.0/16", 16+index)
		},
		"10.0.0.0/8": func(seed int) string {
			index := seed % 256
			return fmt.Sprintf("10.%d.0.0/16", index)
		},
	}
	getK8SCIDR := func(vpcCIDR string, seed int) string {
		var cidr string
		for k, fn := range v4CIDRMap {
			if err := CheckCIDROverlap(vpcCIDR, k); err != nil {
				continue
			}
			cidr = fn(seed)
			delete(v4CIDRMap, k)
			break
		}
		return cidr
	}
	seed := rand.Int()
	podCIDR := getK8SCIDR(vpcCIDR, seed)
	serviceCIDR := getK8SCIDR(vpcCIDR, seed)
	cidrList := []string{podCIDR, serviceCIDR}
	return cidrList, nil
}