0%

Kubernetes 网络权威指南(03):Kubernetes 网络原理与实践

全面云化推进过程中最引人注目的便是 云原生(Cloud Native)的概念。云原生技术有利于各组织在公有云
私有云、混合云等新型动态环境中,构建和运行 可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式 API 等。这些技术能够构建容错性好、易于管理、便于观察的松耦合系统。

一次构建、到处运行–容器良好的可移植性、敏捷性和 Docker 革新的镜像打包方式相比较于云计算最初的 IaaS 虚拟机形态,更容易成为公有云全新的基础设施和交付手段。但是 Docker 只是一个运行时,我们需要一个编排框架作为系统核心来串联开发、测试、发布、运行、升级等整个软件生命周期,这个编排框架就是 Kubernetes。

Kubernetes 简介

Kubernetes(k8s)已经成为容器编排甚至是云原生事实上的标准了。k8s 将容器分类为 Pod,Pod 是 k8s 中特有的一个概念,它是容器分组的一层抽象,也是 k8s 最小的部署单元。下面首先介绍一些 k8s 中的术语:

  • Master(主节点):控制 k8s 工作节点的机器,运行 k8s 的管理面,也是创建 k8s 工作负载的地方
  • Node(工作节点):这些机器在 k8s 主节点的控制下将工作负载以容器的方式运行起来
  • Pod:由一个或多个容器构成的集合,作为一个整体部署在一个节点上。一个 Pod 内的容器共享网络协议栈、进程间通信 IPC、主机名、存储等资源。Pod 运行一个或多个容器、节点运行 0 个或多个 Pod
  • Replication Controller/Replication Set:控制 Pod 在集群中运行的副本数量
  • Service:将服务访问信息与具体的 Pod 解耦。k8s 负责自动将服务请求分发到正确的后端 Pod 处
  • kubelet:守护进程,运行在每个工作节点上,保证该节点上容器的正常运行
  • kubectl:k8s 命令行工具

Pod 内的容器共享以下资源:

  • PID namespace
  • network namespace
  • IPC namespace
  • UTS namespace
  • Volumes(共享存储卷)

k8s 提供了容器大规模部署、编排与管理的能力。k8s 提供的工作负载抽象能够让用户方便地构建多容器的应用服务,并且支持弹性伸缩、滚动升级和健康检查等功能。

可以将 k8s 视为云计算时代的操作系统,它管理着集群中的节点以及在节点上运行的容器。k8s 的 Master 节点从管理员处接收命令,在将指令转交给管理的工作节点,然后在该节点上分配资源并指派 Pod 来完成任务。所以对容器的控制在更高层次进行,也提供了更佳的控制方式,无需用户微观管理每个单独的容器或者节点。

当 k8s 把 Pod 调度到节点上,节点上的 kubelet 会指示 Docker 启动特定的容器。接着 kubelet 会通过 cgroup 持续收集容器的信息,然后提交到 k8s 的管理面。随着 CRI 标准的提出与成熟,k8s 作为编排层完全可以替用户屏蔽底层容器技术,提供一个简单和通用的容器接口层,让 containerd、cri-o 等更轻量级的容器技术集成到 k8s 中。

k8s 网络

k8s 网络包括网络模型、CNI、service、Ingress、DNS 等等。在 k8s 网络模型中,每台服务器上的容器有自己独立的 IP 段,各个服务器之间的容器可以根据目标容器的 IP 地址进行访问。为了实现这一目标,需要解决以下两个问题:

  • 各台服务器上容器 IP 段不能重叠,所以需要某种 IP 段分配机制,为每台服务器分配独立的 IP 段
  • 从某个 Pod 发出的流量到达其所在的服务器时,服务器网络层应该具备根据目标地址,将流量转发到该 IP 所属 IP 段对应的目标服务器的能力

即 k8s 容器网络重点需要关注:IP 地址分配和路由问题。

k8s 网络基础

k8s 使用各种 IP 范围为节点、Pod 和服务分配 IP 地址。

  • 系统会从集群的 VPC 网络为每个节点分配一个 IP 地址,节点 IP 用于提供从控制组件(kube-proxy、kubelet)到 k8s Master 的连接
  • 系统会为每个 Pod 分配一个地址块内的 IP 地址。用户在创建集群时通过 -pod-cidr 指定该范围
  • 系统会从集群的 VPC 网络为每项服务分配一个 IP 地址(称为 ClusterIP)

k8s 处理 Pod 的出站流量主要有 3 种方式:

  • Pod 到 Pod:每个 Pod 有自己的 IP 地址,所有 Pod 之间保持 3 层网络的连通性。CNI 就是用来实现这些网络功能的标准接口
  • Pod 到 Service:可以认为 Service 是 Pod 前面的 4 层负载均衡器,总共有 4 种类型的 Service,最常见的是 ClusterIP,该类型的 service 会自动分配一个仅集群内部可以访问的虚拟 IP。k8s 通过 kube-proxy 组件实现 Service 功能,每个计算节点都运行 kubeproxy 进程,通过复杂的 iptables/IPVS 规则在 Pod 和 Service 之间进行过滤和 NAT
  • 从 Pod 到集群外的流量,k8s 会通过 SNAT 来处理,SNAT 将数据包的源从 Pod 内部的 IP:Port 替换为宿主机的 IP:Port,当数据包返回时,再将目的地址从宿主机的 IP:Port 转换为 Pod 内部的 IP:Port,然后发送给 Pod

k8s 网络架构综述

k8s 中,每个 Pod 都有一个独立的 IP(单 Pod 单 IP),Pod 内的所有容器共享 network namespace。与 CRI 之于 k8s 的 runtime 类似,k8s 使用使用 CNI 作为 Pod 网络配置的标准接口。

如下展示了 k8s 网络的总体架构:

  • 当需要创建新的 Pod 时,kubelet 观察到新的 Pod 的创建,调用 CRI 创建 Pod 内的若干个容器
  • 在这些容器里,第一个被创建的容器 pause 容器比较特殊,它运行一个功能简单的 C 程序,具体逻辑是一启动就把自己永远阻塞在那里。Pod 内的第一个容器 pause 的作用就是占用一个 Linux network namespace,由于该进程一直不会退出,所以这个 namespace 可以一直存在
  • Pod 内的其他容器通过加入这个 network namespace 的方式共享同一个 network namespace。所以创建用户容器时,使用的是 docker run --net=container:{pause_container_id}
  • CNI 负责容器的网络设备初始化,CNI 有多个实现,官方自带的有 p2p、bridge 等,这些插件负责初始化 pause 容器的网络设备,即给 pause 容器内的 eth0 分配 IP

k8s 主机内组网模型

由于同一个 Pod 内的多个容器共享同一个 network namespace,每个容器通过 localhost 就能访问同一个 Pod 内的其他容器。k8s 使用 veth pair 将容器与主机的网络协议栈连接起来。veth pair 的一端连接到 Linux 网桥(位于主机的 root network namespace 中),所以同一节点上的各 Pod 之间可以相互通信。

k8s 跨节点组网模型

k8s 典型的跨节点通信解决方案有 bridge、overlay。首先介绍 bridge 跨节点通信的网络模型:

可以看出,bridge 网络本身不解决容器的跨节点通信问题,而是需要显式书写主机路由表,映射目标容器网络和主机 IP 的关系,因此集群内有 N 个节点,则需要 N-1 条路由表项。

如下展示了 overlay 网络的解决方案

这里,跨节点的数据包是直接发送给本机的 tun/tap 设备 tun0,而 tun0 则是 overlay 隧道网络的入口。当然要正确地完成封装、使隧道报文能够发送到正确的目的节点上,背后还需要一些工作,后续会详细介绍。

Pod 的 hosts 文件

可以向 Pod 的 /etc/hosts 文件添加条目,可以在 Pod 级别覆盖对 hostname 的解析。k8s 也提供了 downward API,支持用户通过 PodSpec 的 HostAliases 字段添加这些自定义的条目(前提是非 hostNetwork 类型)。

并不推荐手动修改 /etc/hosts 文件,因为该文件由 kubelet 托管,用户修改该 hosts 文件的任何内容都会在容器重启或 Pod 重新调度后被 kubelet 覆盖。

Pod 的 hostname

Pod 之间的主机名相互隔离,但 Pod 内容器分享同一个主机名。当然,k8s 在处理 UTS namespace 同样会考虑 Pod 的网络模式。如果 Pod 是使用宿主机网络,那么 Pod 就是使用物理机的 UTS namespace。

Pod 的核心:pause 容器

从网络的角度来看,同一个 Pod 的不同容器犹如运行在同一个主机上,可以通过 localhost 进行通信。在 k8s 中,pause 容器被当做 Pod 中所有业务容器的父容器,并为每个业务容器提供以下功能:

  • 在 Pod 中,它作为共享 Linux namespace 的基础
  • 启用 PID namespace 共享,为每个 Pod 提供 1 号进程,并收集 Pod 内的僵尸进程

pause 容器运行一个非常简单的进程,它一启动就把自己阻塞住,另外它也扮演 PID 1 的角色,在子进程成为孤儿进程的时候,通过 wait 回收这些僵尸进程。

在启动容器时,可以通过 --net=container:pause--ipc=container:pause 等方式让新容器和 pause 容器共享 namespace。

在容器中,必须有一个进程充当每个 PID namespace 的 init 进程,使用 Docker 的话,ENTRYPOINT 进程是 init 进程,如果多个容器之间共享 PID namespace,那么拥有 PID namespace 的那个进程必须承担 init 进程的角色,其他容器作为 init 进程的子进程添加到 PID namespace 中。而并不是任意进程都能完成 init 的任务,init 的核心任务之一是需要回收那些孤儿进程,避免其成为僵尸进程。

所以 Pod 的 init 进程,pause 容器非常适合。

k8s 1.8 之前,默认是启用 PID namespace 共享的,而 1.8 之后则默认禁止 PID 共享,可以通过 --docker-disable-shared-pid 来开启或关闭 PID namespace 的共享。k8s 提供了 podSpec.shareProcessNamespace 指示是启用 PID namespace 共享。

为什么要禁止 PID namespace 的共享呢?一些应用程序不会产生其他进程,因此僵尸进程的问题可以忽略不计,也就用不到 PID namespace 的共享。而有些场景下,用户的确希望 Pod 内的容器与其他容器隔离 PID namespace:

  • 业务容器内的进程需要是 PID 1
  • 不希望看见其他容器内的进程(包括 /proc 等信息)

k8s 网络驱动

k8s 支持两种网络驱动:kubenet 和 CNI。kubenet 是 k8s 早期的网络驱动,提供非常简单和基本的单机容器网络能力。随着 CNI 的广泛使用,kubenet 正在被慢慢弃用。

CNI 是容器网络的标准化,试图通过 JSON 描述一个容器网络配置。CNI 是 k8s 与底层网络之间的一个抽象层,为 k8s 屏蔽底层网络实现的复杂度,同时解耦了 k8s 的具体网络插件实现。

CNI 主要有两类接口:

  • 创建容器时调用的配置网络接口 AddNetwork
  • 删除容器时调用的清理网络接口 DelNetwork

kubelet 要使用 CNI 网络驱动需要配置启动参数 --network-plugin=cni,kubelet 会从 --cni-conf-dir(默认为 /etc/cni/net.d)中读取文件,并使用该文件中的 CNI 配置配置每个 Pod 网络。CNI 插件二进制文件所放置的目录通过 kubelet 的 --cni-bin-dir 参数配置,默认为 /opt/cni/bin

从集群内访问 Service

客户端访问容器应用最简单的方式是通过 容器 IP + 端口,但是当有多个后端实例时,需要考虑:

  • 如何做到负载均衡
  • 如何保持会话亲和性
  • 容器迁移后,IP 发生变化如何处理
  • 健康检查怎么做
  • 怎么通过域名访问

k8s 的解决方案是引入 Service。k8s 使用 Labels 将多个相关的 Pod 组合成一个逻辑单元,称为 Service。Service 具有固定的 IP 地址(区别于容器不固定的 IP 地址)和端口,并在一组匹配的后端 Pod 之间提供负载均衡,匹配条件就是 Service 的 Label Selector 与 Pod 的 Labels 相匹配。

k8s Service 详解

k8s 的 Service 代表的是服务的入口,主要包含服务的访问 IP(虚拟 IP)和端口,因此主要工作在 L4。Service 通过 Label Selector 选择与之匹配的 Pod。被 Service 选中的 Pod,当它们运行且能够对外提供服务后,k8s 的 Endpoint Controller 会生成新的 Endpoints 对象,记录 Pod 的 IP 和端口。另外 Service 的访问 IP 和 Endpoints/Pod IP 会在 k8s 的 DNS 服务器里存储域名和 IP 的映射关系。

k8s 会从集群的可用服务 IP 池中为每个新创建的服务分配一个稳定的、集群内访问 IP 地址,称为 Cluster IP。k8s 还通过添加 DNS 条目为 Cluster IP 分配域名。Cluster IP 和服务域名在集群内是唯一的,并且在整个服务生命周期内不会更改。需要注意,Cluster IP 只有和它的 port 一起使用采用作用,直接访问该 IP 或者访问该 IP 的其他端口都是徒劳的。

在 k8s Service 定义中,spec.ClusterIP 指定 cluster IP,spec.ports[].port 指定 Service 的访问端口,spec.ports[].targetPort 是后端 Pod 的端口。

k8s 通过 kube-proxy 组件管理各服务与后端 Pod 的连接,该组件在每个工作节点上运行。kube-proxy 是一个基于出站流量的负载均衡控制器,具体到工作节点上就是 iptables/IPVS 等规则。

默认情况下,服务会将请求随机转发到可用的后端,如果希望保持会话,例如同一个 client 转发到同一个 Pod,可以设置 service.spec.sessionAffinity 为 ClientIP。

另外还有一个 NodePort 需要解释下,NodePort 是 k8s 提供给集群外部(Pod 网段外部)访问 Service 入口的一种方式(另外一种方式是 Load Balancer)。可以通过 Node IP:NodePort 的方式提供集群外访问 Service 的入口。

服务的发布形式

k8s service 支持几种类型:Cluster IP、Load Balancer 和 NodePort。

Cluster IP 是默认类型,自动分配集群内部可以访问的虚拟 IP。Cluster IP 主要作用是方便集群内 Pod 到 Pod 之间的调用。Cluster IP 的核心原理是在每个 node 节点上使用 iptables,将发向 Cluster IP 对应端口的数据转发到后端 Pod 中。

  • 对于 Cluster IP 类型的 Service,集群内的 Pod 之间可以自由通信,但集群外的连接无法访问服务
  • Load Balancer 类型的 Service 需要 Cloud Provider 的支持。对于 LB 类型的 Service,集群内部仍然可以通过 Cluster IP + 端口 的方式来访问该服务,外部可以采用 EXTERNAL-IP + 端口 的方式来访问该服务
  • NodePort 类型的 Service 在 k8s 集群的每个节点上分配一个真实的端口,即 NodePort。集群内外可基于集群内任何一个节点的 IP:NodePort 的形式访问 Service。NodePort 类型的 Service 仍然拥有 Cluster IP。NodePort 的实现机制是 Kube-proxy 会创建一个 iptables 规则,所有访问本地 NodePort 的网络包都会被转发到后端 Port。NodePort 是解决服务对外暴露的最简单方法

k8s Service 发现

最开始,kubelet 在创建每个 Pod 时,会把系统当前所有服务的 IP 和端口信息都通过环境变量的方式注入容器。这样 Pod 中的应用可以通过读取环境变量获取所需服务的地址信息。

更理想的方案是应用能够直接使用服务的名字,而不需要关心它实际的 IP 地址。k8s 提供了域名服务,后面将详细介绍。

headless Service

headless Service 即没有 selector 的 Service,由于没有 selector,就不会创建相关的 Endpoints 对象,可以手动将 Service 映射到指定的 Endpoints。

ExternalName Service 是 Service 的特例,它没有 selector,也没有定义任何端口和 Endpoint。相反,对于运行在集群外部的服务,它通过返回该外部服务别名的方式提供服务。

如何访问本地服务

当访问 NodePort 或 LoadBalancer 类型 Service 的流量到达工作节点时,流量可能会转发到其他节点上的 Pod,这导致网络流量的额外一跳。如果要避免额外的转发,用户可以指定流量必须转发到最初接收流量的节点上的 Pod,可以通过将 serviceSpec.externalTrafficPolicy 设置为 Local 实现。

从集群外访问服务

从集群外访问 k8s service,大致有如下几种方式:

  • 使用 NodePort 类型的 Service,这种方式要求集群内 Node 有对外访问 IP
  • 使用 Load Balancer 类型的 Service,它要求在一些特定的云服务上运行 k8s,而且 service 只能提供 L4 负载均衡功能,一些高级的 L7 转发功能实现不了

在 k8s 中,L7 的转发功能,集群外访问 service,都是交给 Ingress 的。Ingress 可能是暴露服务最强大的方式,也是最复杂的。k8s Ingress 提供了负载均衡器的典型特性:HTTP 路由、黏性会话、SSL 终止等。Ingress 控制器有多种类型,例如 Google Cloud Load Balancer、Nginx、Istio 等。

Ingress 字面意思是 入站流量,通常 Service 和 Pod 仅可以在集群内部网络通过 IP 地址访问,所有到达边界路由的流量或者丢弃、或者被转发到其他地方。Ingress 的作用就是在边界路由上开个口子,放外部流量进来。因此 Ingress 是建立在 Service 之上的 L7 访问入口。它支持通过 URL 的方式将 Service 暴露到 k8s 集群外;支持自定义 Service 的访问策略,提供按域名访问的虚拟主机功能;支持 TLS 通信等。

k8s 只提供了一个 API 定义,具体的 Ingress Controller 需要自己实现,官方给出了 Nginx 和 GCE Ingress Controller 示例。实现一个 Ingress Controller 大致框架是:List/Watch k8s 的 Service、Endpoints、Ingress 对象,并根据这些信息刷新外部 Load Balancer 的规则和配置。

简单理解,Ingress 就是一个规则定义,例如某个域名对应某个 Service,当该域名的请求到达时,转发给某个 Service。Ingress Controller 负责实现这个规则,即 Ingress Controller 将这些规则写入负载均衡器的配置中,从而实现服务的负载均衡。

通过域名访问服务

在 k8s 集群中,DNS 服务是非必须的,因此无论是 kube-dns 还是 CoreDNS,通常都是以插件形式安装。

DNS 服务基本框架

k8s 的 DNS 服务功能,是用来解析 k8s 集群内的 Pod 和 Service 域名的,一般只给集群内的容器使用,不给外部使用。通常 k8s 的 DNS 应用部署后,会对外暴露成一个服务,集群内的容器通过访问该服务的 Cluster IP + 53 端口获得域名解析服务,而这个域名解析服务的 Cluster IP 一般是固定的。

对于 Linux,容器内的进程想要获得域名解析服务,只需要将 DNS server 写入 /etc/resolv.conf 文件中。Kubelet 会负责更新这个配置文件,例如 --cluster-dns 选项即可配置集群的 DNS 服务 IP。

该流程就是 k8s 使用集群内 DNS server 的基本框架,与具体的 DNS server 无关。

域名解析基本原理

目前 k8s 的 DNS 支持正向查找(A record)、端口查找(SRV 记录)、反向 IP 地址查找(PTR 记录)等。对于 Service,k8s DNS 服务器会生成 3 类 DNS 记录:

  • A 记录:用于将域名或子域名指向某个 IP 地址的记录
  • SRV 记录:通过描述某些服务协议和地址促进服务发现
  • CNAME 记录:用于将域名或子域名指向另一个主机名

k8s 域名解析策略对应到 Pod 配置中的 dnsPolicy,有 4 种可选策略:

  • None:允许 Pod 忽略 k8s 中的 DNS 配置
  • ClusterFirstWithHostNet:对于使用 hostNetwork 运行的 Pod,应该明确设置其 DNS 策略为 ClusterFirstWithHostNet
  • ClusterFirst:任何与配置的集群域后缀(例如 cluster.local)不匹配的 DNS 查询将转发给从宿主机继承的上游域名服务器集群
  • Default:Pod 从宿主机上继承名称解析配置

k8s 网络策略:为应用保驾护航

默认情况下,k8s 网络策略是全连通的,即在同一个集群内运行的所有 Pod 都可以自由通信,但也支持用户根据其需求限制集群内 Pod 的连接。

k8s 的解决方案是 Network Policy,网络策略就是基于 Pod 源 IP(所以 k8s 网络不能随便做 SNAT)的访问控制列表,限制的是 Pod 之间的访问。通过定义访问策略,用户可以根据标签、IP 范围、端口号的任意组合限制 Pod 的入站/出站流量。网络策略作为 Pod 网络隔离的一层抽象,用白名单实现了一个访问控制列表(ACL),从 Label Selector、namespace selector、端口、CIDR 这 4 个维度限制 Pod 的流量进出。

网络策略的一些重点知识:

  • Egress 表示出站流量,即 Pod 作为客户端访问外部服务,Pod 地址作为源地址,策略可以定义目的地址和目的端口,即根据 ports 和 to 定义规则
  • Ingress 表示入站流量,Pod 地址和服务作为服务端(目的地址),可以根据 ports 和 from 定义规则
  • podSelector 用来指定网络策略在哪些 Pod 上生效,用户可以定义单个 Pod 或者一组 Pod
  • policyTypes 指定策略类型,包括 Ingress 和 Egress