作者:禅与计算机程序设计艺术
1.简介
在云原生时代,应用程序通常部署在容器中。容器是一个轻量级、可移植、资源隔离的平台。容器技术通过虚拟化技术将一个物理主机划分为多个逻辑容器,每个容器运行自己的应用进程及其依赖库,并共享主机内核等系统资源。容器化应用可以在不同的环境之间迁移而不需要修改,降低了开发、测试和部署的复杂度。但随着容器技术的流行和普及,如何有效地管理容器之间的网络、存储以及服务发现,成为越来越多的云原生场景下需要解决的问题。本文介绍基于容器网络和服务发现的技术实现方案。
2.基本概念术语说明
2.1 容器网络基础
首先,要理解什么是容器网络。在容器化的应用环境中,每个容器都拥有一个独立的IP地址。当容器启动时,就会分配一个内部网段(如172.17.0.0/16),不同容器间可以通过这个网段通信。每一个容器都有自己独立的路由表,并可以直接访问外部网络。容器之间和外部世界也没有隔绝的边界,因此需要配置容器网络。
一般情况下,容器网络由三个主要功能组成:
IP 分配: 为容器分配 IP 地址,并使它们可以互相通信;
路由: 将容器的出入站流量路由到正确的目的地;
服务发现: 提供容器之间服务调用的统一名称和访问方式。
这些功能是基于标准的 Linux Bridge 和自定义 CNI 插件实现的。这里重点介绍 CNI 插件——Container Network Interface 的作用,以及 Kubernetes 中如何使用 CNI 插件来实现容器网络。
2.2 网络模型
目前主流的容器网络模型有三种:
- CNM (Container Network Model): 是 Docker 默认的网络模型。CNM 模型提供了一种简单、易用且容易理解的接口,允许容器使用统一的 API 来管理网络;
- CNI (Container Networking Interface): 定义了一套接口规范,任何支持 CNI 接口的插件都可以作为容器网络插件被 Kubernetes 所使用;
- K8S NetWorkPolicy: 通过声明式的方式来控制 Pod 之间的网络流量,提高网络安全性和可用性。
Kubernetes 中使用的是第三种模型——K8S NetWorkPolicy。它提供了一个高层次的声明式 API,用于控制Pod之间的网络通信。NetworkPolicy 对象允许管理员或其他工作负载(例如,CI/CD 系统)通过指定规则来控制网络流量,包括允许哪些类型的流量通过,以及哪些类型的数据包应被允许。
总体来说,Kubernetes 使用 CNI 技术为容器提供基本的网络功能。Kubernetes 中的网络插件需要实现以下接口:
版本号获取接口 Version Get() string:该接口返回插件的版本号,方便 Kubernetes 判断是否兼容;
初始化接口 Init(kubeconfig []byte) error:该接口初始化插件,包括从 kubeconfig 配置文件中读取信息等工作;
创建网络接口 CreateNetwork(name string, network cni.NetworkConfig, ipam *cni.IPAMConfig) (current.Result, error):该接口创建容器网络,返回结果对象;
删除网络接口 DeleteNetwork(name string) error:该接口删除容器网络;
连通性测试接口 CheckHealth() error:该接口检查插件的健康状态,比如网络连接、权限等。
2.3 服务发现
服务发现是指应用程序在分布式系统中寻找自身所需资源的过程,包括获取服务名称和地址,以及对服务进行故障转移。一般来说,服务发现需要解决的几个问题是:
服务注册中心:各个节点上的服务注册中心存储着服务的信息,包括服务名称、服务端口、地址列表、健康状态等;
服务解析器:当客户端需要访问某个服务时,会向 DNS 或其他方式查询服务名称对应的 IP 地址和端口。解析器需要跟踪服务的变化,保证客户端始终能够访问最新地址;
客户端负载均衡:如果集群中存在多个相同服务的实例,客户端需要做负载均衡。负载均衡器根据服务注册信息,把请求分发给不同的实例;
服务健康检测:为了保证服务的可用性,服务的消费方应该定期或实时地发送心跳报告。服务注册中心应该根据接收到的心跳报告,更新服务的状态信息。
3.核心算法原理和具体操作步骤以及数学公式讲解
3.1 IP 分配
容器网络中的 IP 分配是最基础的功能之一。IP 分配主要涉及四个步骤:
第一步:子网划分
首先,容器网络需要划分成若干个子网,每个子网中存放一定数量的容器。对于同一网段中的不同容器,其 IP 地址可以相同也可以不同,取决于具体需求。根据具体业务需求,可以制订预留的网段大小,如 /24(256 个 IP),/16(65,536 个 IP),甚至更小的网段。
第二步:子网掩码
然后,容器网络需要设置好子网掩码。子网掩码决定了网络中每台主机上子网中可用地址的个数。一般情况下,子网掩码都比实际分配的地址少一位。
第三步:IP 地址分配
在设置完子网划分和子网掩码后,就可以开始分配 IP 地址了。IP 分配器负责从子网中分配 IP 地址,并将它们写入容器的网络命名空间。每个容器都有自己的 IP 地址,通过这种方式完成了 IP 分配。
第四步:路由规则
容器之间的通信需要通过路由规则进行路由。路由器负责根据流经容器的网络数据包的源地址、目的地址、协议等信息,计算出相应的路由表项,并安装到路由表中。
3.2 路由
路由就是将数据包从 A 端传输到 B 端的过程。容器之间的数据包,首先需要进入容器网络命名空间进行处理。如果源地址和目的地址都是容器所在网段,则可以直接通过容器之间的 veth pair 通信。否则,需要通过网关路由到达目的地。
为了确保容器之间网络通信正常,需要对容器网络进行路由配置。路由器根据流经容器的网络数据包的源地址、目的地址、协议等信息,计算出相应的路由表项,并安装到路由表中。
常用的路由配置模式有三种:
静态路由:静态路由配置好后,容器之间的所有通信都会通过静态路由路由到目的地。这种方式灵活方便,但路由信息需要手动配置,且路由信息的变动需要重启容器才能生效。
BGP 动态路由:BGP 动态路由利用 BGP 协议自动生成路由信息。BGP 协议有两个主要职责:
- 维护 AS 之间的路由关系;
- 传播 AS 之间的路由信息。
容器网络中需要使用 BGP 动态路由,首先需要安装 BGP 路由器,并且将网关设置为 BGP 路由器。然后,在 BGP 路由器上配置路由策略,使得只要某条链路可用,就可以直接到达目标网络,并发布到 BGP 路由器上。这样,所有 BGP 路由器都可以收到完整的路由信息,并自动生成相应的路由表。
OSPF 动态路由:OSPF 动态路由也是利用 OSPF 协议生成路由信息。OSPF 协议有两个主要职责:
- 构建互联网的邻域;
- 确定路由路径。
容器网络中可以使用 OSPF 动态路由,首先需要安装 OSPF 路由器,并且将网关设置为 OSPF 路由器。然后,在 OSPF 路由器上配置路由策略,使得只要某台路由器可用,就可以直接到达目标网络,并发布到 OSPF 路由器上。这样,所有 OSPF 路由器都可以收到完整的路由信息,并自动生成相应的路由表。
3.3 服务发现
服务发现机制是容器集群中最重要的功能之一。服务发现主要由四个模块构成:服务注册中心、服务解析器、客户端负载均衡、服务健康检测。
服务注册中心
服务注册中心存储着服务的信息,包括服务名称、服务端口、地址列表、健康状态等。服务注册中心通常采用中心化的方式部署,每个节点上都部署一套服务注册中心。服务注册中心提供 HTTP RESTful API 以供客户端访问,以便查询服务注册信息。
服务解析器
当客户端需要访问某个服务时,会向 DNS 或其他方式查询服务名称对应的 IP 地址和端口。解析器需要跟踪服务的变化,保证客户端始终能够访问最新地址。解析器可以基于客户端请求中的服务名进行服务查询,或者周期性地向服务注册中心同步最新路由信息。
客户端负载均衡
如果集群中存在多个相同服务的实例,客户端需要做负载均衡。负载均衡器根据服务注册信息,把请求分发给不同的实例。负载均衡器需要监控服务的健康状态,只有处于健康状态的实例才会接收客户端的请求。
服务健康检测
为了保证服务的可用性,服务的消费方应该定期或实时地发送心跳报告。服务注册中心应该根据接收到的心跳报告,更新服务的状态信息。服务健康检测模块需要定期向服务实例发送心跳报告,并接收服务响应。如果服务实例长时间没有回复心跳消息,则认为该服务已下线。
4.具体代码实例和解释说明
4.1 Kubernetes CNI 插件编写
Kubernetes 基于标准的 CNI 接口,提供了丰富的网络插件供用户选择。官方提供了以下几种插件:
- loopback: 简单的本地回环网络插件,仅供测试使用;
- bridge: 简单的桥接网络插件,不具备流量控制能力;
- ipvlan: 支持容器隧道的网络插件;
- macvlan: 支持 MACVLAN 方式的网络插件;
- dhcp: DHCP 方式分配 IP 地址的网络插件;
- flannel: Flannel 网络插件,提供纯净的 overlay 网络方案;
- calico: 适用于大规模环境的容器网络插件,支持丰富的安全特性。
如果没有特别的要求,一般建议选择第三方网络插件 Flannel 或 Calico,二者均支持容器网络的迁移和扩展。下面,我们以 Calico 为例,来编写一个自己的 CNI 插件。
4.1.1 插件基本框架
Calico 有两种类型的 CNI 插件:
- 主动防火墙型插件:Calico 通过 Felix 组件来实现主动防火墙型插件,如 canal 和 weave-net。Calico 可以针对 ingress 流量实现 ACL 过滤、DDOS 防护等功能。
- 主动声明型插件:Calico 通过 CNI 接口实现主动声明型插件,如 calico 和 canal。Calico 可以根据服务发现和负载均衡实现 Service 级别的流量调度。
本文中,我们只关注第二类插件——calico。Calico 有两套编程模型,分别对应传统的 CNI 插件和 Kubernetes 集群环境下的 CRD-base 插件。我们采用 CRD-based 插件来编写 Calico CNI 插件。
Calico CNI 插件基本框架如下图所示:
图中,CNI Plugin 是 Calico 的核心组件。它接受来自 Kubernetes Master 的请求,负责执行各种网络配置操作,包括 IP 分配、路由配置、子网管理等。Calico CNI 插件封装了底层的 etcd 数据结构,提供高可用和扩展性。
Kubelet 驱动的 CRI-O 运行时通过 gRPC 调用 CNI 插件接口向 CNI 插件传递相关参数,包括 pod 的配置信息、namespace 和网络配置等,进而为容器配置网络。Calico CNI 插件通过调用 Calico CLI 命令,实现 IP 分配、路由配置、子网管理等操作。
4.1.2 IP 分配
IP 分配是 Calico CNI 插件的核心功能。IP 分配器负责从子网中分配 IP 地址,并将它们写入容器的网络命名空间。每个容器都有自己的 IP 地址,通过这种方式完成了 IP 分配。
Calico CNI 插件采用的是 IPAM 模式,即使用 etcd 数据库来管理 IP 地址,而不是像 kube-controller-manager 和 kubelet 那样依赖于自身的 IPAM 模块。Calico CNI 插件通过 calicoctl 执行 IP 分配操作,并将结果写入网络配置文件,以便 kubelet 加载。
Calico 还提供了通过 Calico API Server 获取 IP 地址的模式。Calico API Server 监听 Kubernetes 集群中的事件,包括 pod 生命周期事件、service 变化事件等。当创建新的 pod 时,Calico 会调用 Calico IPAM Controller 来分配 IP 地址,并记录到 Calico 数据库中。API Server 会将分配的 IP 地址返回给 kubelet,kubelet 根据 IP 地址信息配置容器网络。
4.1.3 路由配置
Calico CNI 插件还负责配置路由。路由器负责根据流经容器的网络数据包的源地址、目的地址、协议等信息,计算出相应的路由表项,并安装到路由表中。
Calico CNI 插件通过以下方式配置路由:
- 在节点上配置默认路由;
- 安装到主机路由表中的路由项,并配置到 Calico 路由规则中;
- 配置到 etcd 中的路由规则,供 Calico Felix 控制器使用。
其中,第一步和第三步是在所有节点上配置的。第二步是在创建 pod 时配置的。Calico CNI 插件通过 PodSpec 请求的 annotations 中获取路由信息,并向路由控制器提交路由信息。路由控制器解析路由信息,创建相应的路由规则。路由规则包含三部分信息:
- 目的网络:目的网络表示要访问的子网,由 IP/CIDR 组成。
- 下一跳类型:下一跳类型可以是
interface
、gateway
或blackhole
。interface
表示通过某个网卡访问,gateway
表示通过默认路由访问,blackhole
表示丢弃流量。 - 下一跳 IP:下一跳 IP 是路由指向的 IP 地址,如默认路由指向的网关 IP。
当 kubelet 创建 pod 时,kubelet 会解析 pod spec 请求的 annotations,并根据 annotations 中的路由信息创建路由控制器请求。路由控制器会根据路由规则生成相应的路由配置,并通过 gRPC 将配置发送给 Calico CNI 插件。Calico CNI 插件接收到路由配置后,会将其转换为实际的路由命令,并安装到路由表中。
4.1.4 管理子网
Calico CNI 插件还负责管理子网。Calico CNI 插件通过指定子网的 CIDR 和子网的网关 IP,向 Calico 控制器提交子网配置。控制器根据子网配置,生成相应的路由配置。Calico 控制器根据 IP/CIDR 计算出子网中的可用 IP 地址,并存储到 Calico 数据库中。控制器还会记录子网的网关 IP 地址、子网掩码、隧道类型(vxlan or gre)等信息。当新 pod 创建时,kubelet 驱动的 CRI-O 运行时会通过 CNI 插件获取路由配置信息,并安装到路由表中。
4.1.5 处理异常情况
除了 IP 分配、路由配置、子网管理,Calico CNI 插件还需处理以下异常情况:
- 拒绝策略:当容器不能访问某个网络时,Calico CNI 插件会安装 deny 路由到此网络,阻止流量通过。
- 网络重连:Calico CNI 插件会定时检查网络连接状态,当网络出现故障时,Calico CNI 插件会尝试重连。
- 防火墙规则:当 Calico 启用了网络策略时,Calico CNI 插件会安装 firewall 规则。
- DDOS 防护:当 Calico 启用了 DDOS 防护时,Calico CNI 插件会限制某些连接速率。
以上异常情况都可以通过 Calico 设置选项来开启或关闭。
5.未来发展趋势与挑战
当前,容器网络技术已经日臻成熟,开源社区也提供了丰富的解决方案,但是如何充分利用这些解决方案,以满足云原生时代的需求,仍然是一个值得探索的研究课题。
未来的主要挑战是:
- 混合云环境下的复杂网络架构;
- 对容器流量的透明感知和流量控制;
- 大规模集群中统一的服务发现机制;
- 可观测性:收集、分析和监控容器网络、服务发现和负载均衡等关键指标。
未来需要考虑的方向还有很多。
6.附录常见问题与解答
为什么 Calico CNI 插件和其他 CNI 插件不一样?
Calico CNI 插件和其他 CNI 插件的区别主要在于:
- Calico CNI 插件没有为每个容器单独设置网络命名空间,而是为整个主机上的所有容器共用网络命名空间;
- Calico CNI 插件通过执行 Calico CLI 命令实现 IP 分配、路由配置、子网管理等操作;
- Calico CNI 插件通过调用 Calico API Server 获取 IP 地址;
- Calico CNI 插件还包括了一些额外的功能,如管理子网、处理异常情况等。
Kubernetes 上可用的 CNI 插件有哪些?
Kubernetes 上可用的 CNI 插件主要有以下几种:
- flannel: Flannel 网络插件,提供纯净的 overlay 网络方案;
- calico: 适用于大规模环境的容器网络插件,支持丰富的安全特性;
- contiv: Contiv 提供了微服务网络的解决方案;
- weave-net: Weave Net 提供了跨主机容器网络。