Kubernetes之Services, Ingress, Network基础介绍

  1. Service
    1. 云原生服务发现
    2. Service的创建
      1. ServiceSpec结构详解
        1. ServicePort
        2. Selector
        3. ServiceType
        4. InternalTrafficPolicy
        5. ExternalTrafficPolicy
        6. TrafficDistribution流量分发
        7. SessionAffinity会话亲和性
        8. ExternalIPs
    3. Service type
      1. ClusterIP
      2. NodePort
        1. 预留端口范围以避免冲突
        2. 配置监听指定 IP 地址
      3. LoadBalancer
        1. LoadBalancerIP
        2. 节点存活状态对负载均衡流量的影响
        3. 支持多协议的负载均衡器
        4. 禁用 LoadBalancer 的 NodePort 分配
        5. 指定负载均衡器实现的类别
        6. 负载均衡器 IP 地址模式
      4. ExternalName
      5. Service的分层设计
    4. EndpointSlice
      1. Services without selectors
      2. 自定义 EndpointSlice
      3. 访问无选择器的 Service
      4. Endpoints(已弃用)
      5. 端点超容量(Over-capacity endpoints)
    5. Headless Services
    6. Discovering services
      1. Environment variables
      2. DNS
    7. Virtual IPs实现的基石之Service Proxy:kube-proxy
      1. iptables代理模式
        1. 优化 iptables模式的性能
          1. minSyncPeriod
          2. syncPeriod
      2. IPVS代理模式
        1. 流量调度算法
      3. nftables代理模式
        1. 从 iptables模式迁移到nftables
      4. kernelspace代理模式
      5. kube-proxy的核心语义
      6. Connection Tracking
  2. Ingress
    1. IngressSpec结构详解
      1. IngressClassName
      2. DefaultBackend
      3. IngressRule
    2. IngressClass
      1. IngressClass 的作用域
      2. Deprecated annotation
      3. Default IngressClass
    3. Ingress的使用
      1. Simple fanout
      2. Name based virtual hosting
      3. Load balancing
  3. 总结

之前写过容器化技术之Linux NamespaceDocker容器网络浅析,有了相关的了解后,我们再来看Kubernetes 的网络模型会更有体会和深入一点。

本文将介绍Kubernetes 的网络模型,其核心思想的关键部分有以下几个部分组成:

  1. 每个 Pod 有一个集群内唯一的 IP 地址
  • 在 Kubernetes 集群中,每个 Pod 都像一台独立的小机器,拥有自己在整个集群中唯一的 IP。
  • 这个 IP 是集群内可见、稳定可用的(Pod 重建后 IP 会变,但 Service 可以解决这个问题)。
  • Pod内所有容器共享这个 IP ,因为它们在同一个 Pod 的Network Namespace中,所有Pod的容器间可以通过 localhost 互相通信,就像在一台机器上跑多个进程。

✅ 类比:Pod ≈ 虚拟机(有独立 IP),容器 ≈ 这台虚拟机上的进程。

  1. Pod 之间可以直接通信(无 NAT)
  • Pod 网络(也称为集群网络)负责处理 Pod 之间的通信,无论两个 Pod 是在同一个节点,还是在不同节点上,它们都可以直接通过 IP+端口通信
  • Kubernetes 要求底层网络(比如 CNI 插件)实现这种“全连通”模型,不需要做端口映射或 NAT

🌐 这是 Kubernetes 和传统 Docker 网络最大的区别之一 —— 不再需要 -p 8080:80 这种端口映射。

  1. Service:稳定的访问入口
  • Pod 会经常被创建、销毁、重建(比如滚动更新),IP 会变。
  • 为了解决“如何稳定访问一组 Pod”的问题,Kubernetes 引入了 Service
    • Service 有固定的 ClusterIP 和 DNS 名称(比如 my-app.default.svc.cluster.local)。
    • Service 会自动将流量转发到后端健康的 Pod(通过 EndpointSlice 动态维护后端列表)。

🔁 类似于“反向代理 + 负载均衡器”,但完全由 Kubernetes 自动管理。

  1. 流量如何被转发?—— kube-proxy 或替代方案
  • 每个节点上运行 kube-proxy(默认实现),它会监听 Service 和 EndpointSlice 的变化。
  • 当你访问一个 Service IP 时,kube-proxy 会通过 iptablesIPVS 或 eBPF 等技术,把流量透明地转发到某个后端 Pod
  • 有些高级网络插件(比如 Cilium、Calico)会用自己的代理机制替代 kube-proxy,性能更好。
  1. 从集群外部访问服务:Ingress / Gateway / LoadBalancer
  • LoadBalancer 类型 Service:在云厂商(如 AWS、GCP)上自动创建一个公网 IP 的负载均衡器,把流量引入集群。
  • Ingress:更灵活的 HTTP/HTTPS 7 层路由(比如基于域名或路径路由到不同服务)。
  • Gateway API:Ingress 的新一代标准,功能更强、更模块化(但还在演进中)。
  1. 网络安全:NetworkPolicy
  • 默认情况下,所有 Pod 可以互相访问。
  • 如果你想限制流量(比如“只允许前端 Pod 访问后端 Pod”),就用 NetworkPolicy
  • 但注意:不是所有网络插件都支持 NetworkPolicy(比如 Flannel 默认不支持,Calico、Cilium 支持)。

在早期的容器系统中,不同主机上的容器之间通常没有自动连通性,因此往往需要显式创建容器之间的链接,或将容器端口映射到主机端口,以便让其他主机上的容器能够访问。而在 Kubernetes 中则不需要这样做;Kubernetes 的模型允许从端口分配、命名、服务发现、负载均衡、应用配置和迁移等角度来看,将 Pod 视作类似于虚拟机(VM)或物理主机。

该模型中仅有少部分由 Kubernetes 本身实现。其余部分由 Kubernetes 定义 API,而具体功能则由外部组件提供(其中部分组件是可选的):

  • 容器运行时(CRI):创建 Pod 的网络命名空间

  • 网络插件(CNI):负责 Pod IP、跨节点通信、NetworkPolicy

  • kube-proxy(或替代品):负责 Service 流量转发

总结来说:Kubernetes 网络的核心哲学是:让所有 Pod 像在同一个局域网里的物理机一样自由通信,同时通过 Service、Ingress、NetworkPolicy 等抽象,提供稳定访问、外部接入和安全控制。

看到一个针对Kubernetes的网络模型很生活化也很形象的类比: 🏙️

  • Pod就像一个办公室,里面可以有一个或多个同事(容器)一起工作,共享办公室空间(Namespace),使用同一个门牌号(IP地址),可以直接面对面交流(通过localhost通信),之间相互协作。
  • Service一个公司前台电话/前台接待,当有人想找”财务部门”时,前台会帮你转接到可用的财务人员(负载均衡),即使财务人员换了办公室,前台依然知道怎么找到他们
  • kube-proxy / CNI:道路 + 交通规则
  • Ingress / Gateway大楼入口的门卫:检查外来访客的身份,处理安全检查(SSL/TLS),把外部流量引导到正确的Service(公司的前台)。
  • NetworkPolicy:办公室的门禁

Service

Service:将运行在一组Pods上的应用公开为网络服务的抽象方法;即使工作负载被拆分到多个后端,也可以通过一个对外暴露的统一入口点,将集群中运行的应用暴露出来。

Kubernetes Pod是动态创建的,每个Pod都有自己的IP地址,这就导致了一个问题:如果一组Pod依赖集群的另一组Pod提供的功能,那么前端Pod如何找出后端提供服务的Pod呢

这时,Service 登场了。Kubernetes Service:定义了一个抽象,一种可以访问一组Pod的策略;以此解耦前后端的Pod,后端的Pod发生变化,前端的Pod无须关心,Service的抽象能够解耦这种关联;

Service关联后端Pod的最基本方式是通过Selector标签选择运算符来确定的;Service的选择运算符的控制器会不断扫描标签与之匹配的Pod,然后将所有Pod的的更新发布到对应的EndPoint对象;

所以可以说:Service为云原生应用提供了服务发现和负载均衡的能力

如果你的工作负载使用 HTTP,你可能会选择使用 Ingress 来控制 Web 流量如何到达该工作负载。Ingress 并不是一种 Service 类型,但它充当了集群的入口点。Ingress 允许你将路由规则整合到一个资源中,从而通过一个统一的监听入口,暴露集群中分别运行的多个工作负载组件。

Kubernetes 的 Gateway API 提供了比 Ingress 和 Service 更强的能力。你可以在集群中添加 Gateway——它是一组通过自定义资源定义(CRD)实现的扩展 API——并使用它们来配置对集群中网络服务的访问。

云原生服务发现

如果你的应用能够使用 Kubernetes API 进行服务发现,你可以向 API Server 查询匹配的 EndpointSlice。当某个 Service 中的 Pod 集合发生变化时,Kubernetes 会自动更新该 Service 对应的 EndpointSlice。

对于非原生应用,Kubernetes 也提供了在应用与后端 Pod 之间放置网络端口或负载均衡器的方式。

无论采用哪种方式,你的工作负载都可以使用这些服务发现机制,找到它希望连接的目标。

Service的创建

如下:声明一个my-service的Service对象,对外监听80端口,它会将请求代理到使用 TCP 端口 9376,并且具有标签 "app=MyApp" 的 Pod 上。

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376

上面的manifest会创建一个名为 “my-service” 的新 Service,其类型为默认的 ClusterIP。该 Service 会将流量转发到所有带有 app.kubernetes.io/name: MyApp 标签的 Pod 上的 TCP 9376 端口。

Kubernetes 会为这个 Service 分配一个 IP 地址(即 Cluster IP),该地址由虚拟 IP 机制使用。关于这一机制的更多细节,请参阅 Virtual IPs and Service Proxies

该 Service 的控制器会持续扫描与其选择器匹配的 Pod,并在需要时对该 Service 对应的 EndpointSlice 集合进行更新。

注意:
Service 可以将任意进入端口映射到一个 targetPort。默认情况下、为了方便起见,targetPort 会被设置为与 port 字段相同的值。

我们下面从ServiceSpec的结构定义来看一下Service的一些详细细节。

ServiceSpec结构详解

如下是ServiceSpec的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go

// ServiceSpec describes the attributes that a user creates on a service.
type ServiceSpec struct {
// The list of ports that are exposed by this service.
Ports []ServicePort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"port" protobuf:"bytes,1,rep,name=ports"`

// Route service traffic to pods with label keys and values matching this
// selector. If empty or not present, the service is assumed to have an
// external process managing its endpoints, which Kubernetes will not
// modify. Only applies to types ClusterIP, NodePort, and LoadBalancer.
// Ignored if type is ExternalName.
Selector map[string]string `json:"selector,omitempty" protobuf:"bytes,2,rep,name=selector"`

// clusterIP is the IP address of the service and is usually assigned
// randomly.
ClusterIP string `json:"clusterIP,omitempty" protobuf:"bytes,3,opt,name=clusterIP"`

// ClusterIPs is a list of IP addresses assigned to this service, and are
// usually assigned randomly.
ClusterIPs []string `json:"clusterIPs,omitempty" protobuf:"bytes,18,opt,name=clusterIPs"`

// type determines how the Service is exposed. Defaults to ClusterIP. Valid
// options are ExternalName, ClusterIP, NodePort, and LoadBalancer.
Type ServiceType `json:"type,omitempty" protobuf:"bytes,4,opt,name=type,casttype=ServiceType"`

// externalIPs is a list of IP addresses for which nodes in the cluster
// will also accept traffic for this service. These IPs are not managed by
// Kubernetes.
ExternalIPs []string `json:"externalIPs,omitempty" protobuf:"bytes,5,rep,name=externalIPs"`

// Supports "ClientIP" and "None". Used to maintain session affinity.
// Enable client IP based session affinity.
// Must be ClientIP or None.
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty" protobuf:"bytes,7,opt,name=sessionAffinity,casttype=ServiceAffinity"`

// Only applies to Service Type: LoadBalancer.
LoadBalancerIP string `json:"loadBalancerIP,omitempty" protobuf:"bytes,8,opt,name=loadBalancerIP"`

// If specified and supported by the platform, this will restrict traffic through the cloud-provider
// load-balancer will be restricted to the specified client IPs. This field will be ignored if the
// cloud-provider does not support the feature."
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty" protobuf:"bytes,9,opt,name=loadBalancerSourceRanges"`

// externalName is the external reference that discovery mechanisms will
// return as an alias for this service (e.g. a DNS CNAME record).
ExternalName string `json:"externalName,omitempty" protobuf:"bytes,10,opt,name=externalName"`

// externalTrafficPolicy describes how nodes distribute service traffic they
// receive on one of the Service's "externally-facing" addresses (NodePorts,
// ExternalIPs, and LoadBalancer IPs).
ExternalTrafficPolicy ServiceExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty" protobuf:"bytes,11,opt,name=externalTrafficPolicy"`

// healthCheckNodePort specifies the healthcheck nodePort for the service.
// This only applies when type is set to LoadBalancer and
// externalTrafficPolicy is set to Local. If a value is specified, is
// in-range, and is not in use, it will be used.
HealthCheckNodePort int32 `json:"healthCheckNodePort,omitempty" protobuf:"bytes,12,opt,name=healthCheckNodePort"`

// publishNotReadyAddresses indicates that any agent which deals with endpoints for this
// Service should disregard any indications of ready/not-ready.
PublishNotReadyAddresses bool `json:"publishNotReadyAddresses,omitempty" protobuf:"varint,13,opt,name=publishNotReadyAddresses"`

// sessionAffinityConfig contains the configurations of session affinity.
SessionAffinityConfig *SessionAffinityConfig `json:"sessionAffinityConfig,omitempty" protobuf:"bytes,14,opt,name=sessionAffinityConfig"`

// IPFamily is tombstoned to show why 15 is a reserved protobuf tag.
// IPFamily *IPFamily `json:"ipFamily,omitempty" protobuf:"bytes,15,opt,name=ipFamily,Configcasttype=IPFamily"`
IPFamilies []IPFamily `json:"ipFamilies,omitempty" protobuf:"bytes,19,opt,name=ipFamilies,casttype=IPFamily"`

// IPFamilyPolicy represents the dual-stack-ness requested or required by
// this Service. If there is no value provided, then this field will be set
// to SingleStack.
IPFamilyPolicy *IPFamilyPolicy `json:"ipFamilyPolicy,omitempty" protobuf:"bytes,17,opt,name=ipFamilyPolicy,casttype=IPFamilyPolicy"`

// allocateLoadBalancerNodePorts defines if NodePorts will be automatically
// allocated for services with type LoadBalancer. Default is "true".
AllocateLoadBalancerNodePorts *bool `json:"allocateLoadBalancerNodePorts,omitempty" protobuf:"bytes,20,opt,name=allocateLoadBalancerNodePorts"`

// loadBalancerClass is the class of the load balancer implementation this Service belongs to.
LoadBalancerClass *string `json:"loadBalancerClass,omitempty" protobuf:"bytes,21,opt,name=loadBalancerClass"`

// InternalTrafficPolicy describes how nodes distribute service traffic they
// receive on the ClusterIP.
InternalTrafficPolicy *ServiceInternalTrafficPolicy `json:"internalTrafficPolicy,omitempty" protobuf:"bytes,22,opt,name=internalTrafficPolicy"`

// TrafficDistribution offers a way to express preferences for how traffic
// is distributed to Service endpoints.
TrafficDistribution *string `json:"trafficDistribution,omitempty" protobuf:"bytes,23,opt,name=trafficDistribution"`
}

ServicePort

我们看一下Ports的ServicePort的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ServicePort contains information on service's port.
type ServicePort struct {
// The name of this port within the service. This must be a DNS_LABEL.
// All ports within a ServiceSpec must have unique names. When considering
// the endpoints for a Service, this must match the 'name' field in the
// EndpointPort.
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`

// The IP protocol for this port. Supports "TCP", "UDP", and "SCTP".
// Default is TCP.
Protocol Protocol `json:"protocol,omitempty" protobuf:"bytes,2,opt,name=protocol,casttype=Protocol"`

// The application protocol for this port.
// This is used as a hint for implementations to offer richer behavior for protocols that they understand.
AppProtocol *string `json:"appProtocol,omitempty" protobuf:"bytes,6,opt,name=appProtocol"`

// The port that will be exposed by this service.
Port int32 `json:"port" protobuf:"varint,3,opt,name=port"`

// Number or name of the port to access on the pods targeted by the service.
// Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.
// If this is a string, it will be looked up as a named port in the
// target Pod's container ports.
TargetPort intstr.IntOrString `json:"targetPort,omitempty" protobuf:"bytes,4,opt,name=targetPort"`

// The port on each node on which this service is exposed when type is
// NodePort or LoadBalancer. Usually assigned by the system.
NodePort int32 `json:"nodePort,omitempty" protobuf:"varint,5,opt,name=nodePort"`
}

Service 的 ports 用于定义 Service 如何对外(或对内)暴露端口,以及这些端口如何映射到后端 Pod 的端口,相关字段的含义:

  • Name:Service对外暴露的端口的名称,因为一个Service可以暴露多个端口,所需要标识区分;
  • Protocol:对外暴露的端口的支持的协议,支持TCP,UDP,SCTP;这里需要注意的是:同一个端口号不能同时绑定多个协议,如果需要可以定义多个条目来解决;
  • Port:Service 对外暴露的端口,直接对客户端调用使用;
  • TargetPort:后端 Pod 真正接收流量的端口,可以是一个数字端口号,也支持Pod中定义的端口名字;如果不指定,默认和Port字段的值一样;
  • NodePort:只在** type: NodePortLoadBalancer 时使用**,表示在Node上分配指定的端口来对外提供服务,如果不设置,会随机分配,当然这里有一个端口范围,apiserver的配置;
  • AppProtocol:来声明端口上承载的“应用层协议”,而不仅仅是传输层协议(TCP / UDP)。它是一个 **语义提示(hint)***,主要供 Ingress、Gateway、Service Mesh、负载均衡器 等上层组件使用。不会直接影响 Service 的流量转发。

如下是一个Service的Port示例,通过名字关联Pod的端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: proxy
spec:
containers:
- name: nginx
image: nginx:stable
ports:
- containerPort: 80
name: http-web-svc

---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app.kubernetes.io/name: proxy
ports:
- name: name-of-service-port
protocol: TCP
port: 8080
targetPort: http-web-svc

针对Service我们可以同时暴露多个端口,如下,Port的名字必须不一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
- name: https
protocol: TCP
port: 443
targetPort: 9377

Selector

和之前介绍过的Workload Resource的selector含义都是一样的,Service 通过selector定义的一组标签 是来选择后端 的Pod 。selector 的工作原理其实比较简单,Service 创建后:

  1. Service Controller 持续监控集群中的 Pod;
  2. 找到 标签匹配 selector 的 Pod
  3. 将这些 Pod 的 IP + Port 写入:EndpointSlice(新机制)(旧版本是 Endpoints);
  4. kube-proxy 根据 EndpointSlice 建立转发规则;

📌 Pod 的 IP 变化、增减、重建,Service 都会自动感知

关于EndpointSlice,我们在下一个章节再解释。

ServiceType

针对应用的某些部分(例如前端),你可能希望将一个 Service 暴露到一个外部 IP 地址上,使其能够从集群外部访问。也有可能只是需要将某个服务暴露一个集群内的IP,仅供集群内部的其他服务访问。

Kubernetes 的 Service 类型(Service types) 允许你指定想要的 Service 访问方式。Service的Type定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type ServiceType string

const (
// ServiceTypeClusterIP means a service will only be accessible inside the
// cluster, via the cluster IP.
ServiceTypeClusterIP ServiceType = "ClusterIP"

// ServiceTypeNodePort means a service will be exposed on one port of
// every node, in addition to 'ClusterIP' type.
ServiceTypeNodePort ServiceType = "NodePort"

// ServiceTypeLoadBalancer means a service will be exposed via an
// external load balancer (if the cloud provider supports it), in addition
// to 'NodePort' type.
ServiceTypeLoadBalancer ServiceType = "LoadBalancer"

// ServiceTypeExternalName means a service consists of only a reference to
// an external name that kubedns or equivalent will return as a CNAME
// record, with no exposing or proxying of any pods involved.
ServiceTypeExternalName ServiceType = "ExternalName"
)
  • ClusterIP:在集群内部的 IP 地址上暴露 Service。选择该类型会使 Service 只能在集群内部访问。如果你没有显式为 Service 指定 type,默认就是 ClusterIP。你可以通过 IngressGateway 将该 Service 暴露到公网。
  • NodePort:在每个节点(Node)的 IP 地址上的一个静态端口(NodePort)上暴露 Service。为了使 NodePort 可用,Kubernetes 会像创建 ClusterIP 类型 Service 一样,也同时会为该 Service 设置一个ClusterIP 地址
  • LoadBalancer:通过一个外部负载均衡器将 Service 暴露到集群外部。 Kubernetes 本身并不直接提供负载均衡组件;你需要自行提供,或者将 Kubernetes 集群与云厂商的负载均衡服务进行集成。
  • ExternalName:将 Service 映射到 externalName 字段指定的内容(例如主机名 api.foo.bar.example)。该映射会配置集群的 DNS 服务器返回一个 CNAME 记录,指向该外部主机名。 这种类型 不会设置任何形式的代理或转发

这里需要注意的很重要的设计:Service API 中的 type 字段采用的是分层(嵌套)功能设计:每一层类型都会在前一层的基础上增加功能,不过,这种分层设计有一个例外:你可以通过禁用 LoadBalancer Service 的 NodePort 分配,来定义一个 LoadBalancer 类型的 Service。

我们该如何理解ServiceType的这个分层嵌套的设计呢?在下一个章节我们再详细展开。

InternalTrafficPolicy

字段 作用对象 控制的是
.spec.internalTrafficPolicy 集群内流量 Pod → Service 的流量如何选后端
.spec.externalTrafficPolicy 集群外流量 外部 → Service 的流量如何选后端

这两种流量策略的取值定义是一样的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go

type ServiceInternalTrafficPolicy string

const (

ServiceInternalTrafficPolicyCluster ServiceInternalTrafficPolicy = "Cluster"
ServiceInternalTrafficPolicyLocal ServiceInternalTrafficPolicy = "Local"
)

type ServiceInternalTrafficPolicyType = ServiceInternalTrafficPolicy
type ServiceExternalTrafficPolicy string

const (
ServiceExternalTrafficPolicyCluster ServiceExternalTrafficPolicy = "Cluster"
ServiceExternalTrafficPolicyLocal ServiceExternalTrafficPolicy = "Local"
)

.spec.internalTrafficPolicy(集群内流量):只影响集群内客户端(Pod、Node、kubelet 等)在请求其他Service 的流量时如何选后端的Pod。

  • Cluster,默认值:Pod 访问 Service 的 ClusterIP,流量可以被转发到 集群中任意 Node 上的任意就绪 Pod;可以带来最大化负载均衡和最佳可用性,但可能产生跨节点网络流量(跨 AZ 更明显);
1
2
3
4
5
Pod A (node-1)

Service ClusterIP

Pod B (node-3) ← 允许跨节点
  • Local:Pod 访问 Service 的 ClusterIP,只会把流量转发到“本节点(local node)”上的就绪 Pod,如果本节点没有匹配的 Pod:直接丢弃流量,不会 fallback 到其他节点。
1
2
3
4
5
Pod A (node-1)

Service ClusterIP

Pod C (node-1) ← 必须在同一节点

✔️ 适合 Local 的场景

  • DaemonSet(每个节点都有 Pod)
  • Node-local 缓存 / Agent
  • 想避免 hairpin + 跨节点流量
  • 对延迟极度敏感

❌ 不适合 Local 的场景

  • 副本数少
  • Pod 分布不均
  • 强可用要求

ExternalTrafficPolicy

参考上面的InternalTrafficPolicy,.spec.externalTrafficPolicy(集群外流量):只影响集群外部请求Service 的流量如何选后端。仅对 Service 类型NodePortLoadBalancer生效, 对 ClusterIP 无效。

  • Cluster,默认值:外部流量进入任意 Node,kube-proxy 可以转发到 任意 Node 上的就绪 Pod,会发生 SNAT;特点是高可用,但是源 IP 丢失
1
2
3
4
5
Client

Node A (无 Pod)
↓ SNAT
Pod B (node-3)
  • Local:只会把流量转发到:当前接收流量的 Node 上的 Pod,如果该 Node 上没有 Pod:直接丢弃流量,不做 SNAT。特定是:保留客户端真实 IP,避免多一跳转发,但可用性下降(依赖 Pod 分布)
1
2
3
4
5
Client (真实 IP)

Node B (有 Pod)

Pod B

为了避免把流量打到没有 Pod 的节点:云厂商 LB:只会把流量转发到有就绪 Pod 的 Node ,这就是 externalTrafficPolicy: Local 能“安全使用”的前提。

典型组合模式:

1️⃣ DaemonSet + 本地优先

1
2
internalTrafficPolicy: Local
externalTrafficPolicy: Local

适合:

  • Node Exporter
  • 日志 / 监控 agent
  • 边缘代理

2️⃣ 普通 Web 服务(推荐默认)

1
2
internalTrafficPolicy: Cluster
externalTrafficPolicy: Cluster

适合:

  • 大多数无状态服务

3️⃣ 保留客户端 IP 的公网服务

1
externalTrafficPolicy: Local

常见于:

  • WAF
  • 日志记录真实来源 IP
  • GeoIP

详细的策略策略可以参考官方的Traffic Polices

TrafficDistribution流量分发

.spec.trafficDistribution 流量分发控制字段提供了另一种方式,用于影响 Kubernetes Service 内部的流量路由。流量策略侧重于严格的语义保证,而流量分发则允许你表达偏好(例如,将流量路由到在拓扑上更接近的 endpoint)。

这有助于在性能、成本或可靠性方面进行优化。在 Kubernetes 1.35 中,支持以下取值:

  • PreferSameZone
    表示优先将流量路由到与客户端位于同一可用区(zone)的 endpoint。
  • PreferSameNode
    表示优先将流量路由到与客户端位于同一节点(node)的 endpoint。
  • PreferClose(已弃用)
    这是 PreferSameZone 的旧别名,对语义的表达不够清晰。

如果未设置该字段,实现将采用其默认的路由策略。

SessionAffinity会话亲和性

发往 Service 的 IP:Port 的流量会被代理到合适的后端,而客户端无需了解 Kubernetes、Service 或 Pod 的任何细节。

如果你希望来自某个特定客户端的连接每次都被转发到同一个 Pod,可以通过将 Service 的 .spec.sessionAffinity 设置为 ClientIP,基于客户端的 IP 地址来启用会话亲和性(默认值为 None)。

我们可以看一下关于会话亲和性结构的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go

type ServiceAffinity string

const (
ServiceAffinityClientIP ServiceAffinity = "ClientIP"
ServiceAffinityNone ServiceAffinity = "None"
)

const DefaultClientIPServiceAffinitySeconds int32 = 10800

// SessionAffinityConfig represents the configurations of session affinity.
type SessionAffinityConfig struct {

ClientIP *ClientIPConfig `json:"clientIP,omitempty" protobuf:"bytes,1,opt,name=clientIP"`
}

type ClientIPConfig struct {
// timeoutSeconds specifies the seconds of ClientIP type session sticky time.
// The value must be >0 && <=86400(for 1 day) if ServiceAffinity == "ClientIP".
// Default value is 10800(for 3 hours).
TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty" protobuf:"varint,1,opt,name=timeoutSeconds"`
}

在开启会话保持的设置时,我们还可以通过为 Service 设置 .spec.sessionAffinityConfig.clientIP.timeoutSeconds,来指定会话保持的最大时间(默认值为 10800 秒,即 3 小时)。

注意:
在 Windows 上,Service 不支持设置会话粘性的最大超时时间。

ExternalIPs

如果存在能够路由到一个或多个集群节点的外部 IP,Kubernetes Service 就可以通过这些 externalIPs 对外暴露。当网络流量进入集群时,如果其目标 IP 是该外部 IP,且端口与对应 Service 的端口匹配,Kubernetes 所配置的规则和路由会确保这些流量被转发到该 Service 的某个 endpoint。

在定义 Service 时,你可以为任何类型的 Service 指定 externalIPs。在下面的示例中,名为 my-service 的 Service 可以通过 TCP 协议、使用地址 198.51.100.32:80 被客户端访问(该地址由 .spec.externalIPs[].spec.ports[].port 组合得出)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 49152
externalIPs:
- 198.51.100.32

注意:
Kubernetes 不会负责 externalIPs 的分配或管理,这些 IP 的分配与路由配置由集群管理员自行负责。它只是告诉 kube-proxy:“如果有流量打到这个 IP:Port,就按这个 Service 的规则转发”

externalIPs 的本质用途是:让 Service 监听一个“已经能路由到集群节点的外部 IP”,并把流量转发到后端 Pod。

比较常见的用途是在早期或传统环境:裸机 / 私有机房(没有云 LB)

  • Bare Metal
  • 私有 IDC
  • 没有 LoadBalancer
  • Ingress 尚不成熟

管理员可能已经有:公网 IP,静态路由 / NAT,BGP / VRRP;

Service type

前面介绍ServiceSpec结构的时候简单介绍过Service type的定义和含义,这里我们再详细展开一下,因为这是理解Kubernetes Network很关键的一个API。

ClusterIP

集群内部的 IP 地址上暴露 Service。选择该类型会使 Service 只能在集群内部访问。如果你没有显式为 Service 指定 type,默认就是 ClusterIP。你可以通过 IngressGateway 将该 Service 暴露到公网。

该默认的 Service 类型会从集群为此用途预留的 IP 地址池中分配一个 IP 地址。其他几种 Service 类型均以 ClusterIP 为基础进行构建

如果你定义的 Service 将 .spec.clusterIP 设置为 "None",Kubernetes 就不会为其分配 IP 地址。这就是所谓的:“无头服务(Headless Services)”,后面会进行介绍;

当然,我们可以将 .spec.clusterIP 设置为自定义 的集群IP 地址。例如,当你希望复用一个已存在的 DNS 记录,或者已有遗留系统被配置为使用特定 IP 地址且难以重新配置时,这一功能就非常有用。你所选择的 IP 地址必须是 API 服务器配置的服务集群 IP 范围(service-cluster-ip-range CIDR)内的有效 IPv4 或 IPv6 地址。如果你尝试创建一个包含无效 clusterIP 地址值的 Service,API 服务器将返回 HTTP 状态码 422,以表明存在问题。

下面是一个显示设置ClusterIP的Service配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
type: ClusterIP
clusterIP: 10.96.100.50 # 显式分配的 ClusterIP
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 8080

这里引用 Difference between ClusterIP, NodePort and LoadBalancer Service一文中的图来简单的了解一下ClusterIP Service对后端Pod服务的抽象:

NodePort

每个节点(Node)的 IP 地址上的一个静态端口(NodePort)上暴露 Service。为了使 NodePort 可用,Kubernetes 会像创建 ClusterIP 类型 Service 一样,也同时会为该 Service 设置一个ClusterIP 地址

如果你将 .spec.type 字段设置为 NodePort,Kubernetes 控制平面会从由 --service-node-port-range 标志指定的端口范围(默认为 30000–32767)中分配一个端口。集群中的每个节点都会在该端口(所有节点使用相同的端口号)上代理流量到你的 Service。分配的端口号会记录在 Service 的 .spec.ports[*].nodePort 字段中。

使用 NodePort 类型的 Service,你可以自由地设置自己的负载均衡解决方案,配置 Kubernetes 尚未完全支持的环境,甚至可以直接暴露一个或多个节点的 IP 地址。

对于 NodePort 类型的 Service,集群中的每个节点都会在该端口上监听,并将流量转发到与该 Service 关联的、处于就绪状态的某个 Endpoint。你可以从集群外部通过任意节点的 IP 地址,使用相应的协议(例如 TCP)和分配的端口来访问该 Service。

当然,我们也可以针对NodePort类型的Service,通过spec.ports[*].nodePort字段手动指定对应的Node端口号。控制平面会尝试为你分配该端口;如果该端口已被占用或无效,则 API 请求将失败。这意味着你需要自行避免端口冲突。此外,你指定的端口号必须是有效的,并且必须位于为 NodePort 配置的端口范围内

以下是一个显式指定 nodePort 值(本例中为 30007)的 NodePort 类型 Service 示例清单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app.kubernetes.io/name: MyApp
ports:
- port: 80
# 为方便起见,默认情况下 targetPort 与 port 字段值相同
targetPort: 80
# 可选字段
# 默认情况下,Kubernetes 控制平面会从指定范围(默认:30000–32767)中自动分配一个端口
nodePort: 30007

这里引用 Difference between ClusterIP, NodePort and LoadBalancer Service一文中的图来简单的了解一下NodePort Service到后端Pod服务的请求模型,这里相比上一节ClusterIP的Service的请求模型更加详尽一点,展示了四层端口流量的转发,其实从逻辑上Service的Port时一个,但是在各个Node的转发策略上都会有Service Port相关的配置,所以这么画算是真是的实现。

预留端口范围以避免冲突

NodePort 服务的端口分配策略同时适用于自动分配和手动分配场景。当用户希望创建一个使用特定端口的 NodePort Service 时,该端口可能与已分配的其他端口发生冲突。为避免此类问题,NodePort 的端口范围被划分为两个区间(bands):

  • 动态分配 默认使用高区间
  • 当高区间耗尽后,才会使用低区间
  • 用户可手动从低区间中选择端口,以降低冲突风险。

配置监听指定 IP 地址

你可以配置集群中的Node,使其使用特定的 IP 地址来提供 NodePort 服务。当你每个节点连接到多个网络时(例如:一个网络用于应用流量,另一个用于节点与控制平面之间的通信),这种配置会很有用。可通过以下方式配置:

  • 在启动 kube-proxy 时设置 --nodeport-addresses 参数,或
  • kube-proxy 配置文件中设置对应的 nodePortAddresses 字段。

该参数接受一个以逗号分隔的 IP 地址块列表(例如 10.0.0.0/8, 192.0.2.0/25),用于指定 kube-proxy 应视为“本地”的 IP 地址范围。

例如,若以 --nodeport-addresses=127.0.0.0/8 启动 kube-proxy,则它只会选择 loopback(回环)接口用于 NodePort 服务。

默认情况下,--nodeport-addresses 为空列表,表示 kube-proxy 应在所有可用的网络接口上监听 NodePort(这也与早期 Kubernetes 版本的行为兼容)。

NodePort的Service 可通过以下两种方式访问:

  • <NodeIP>:<nodePort>,从集群外部
  • <ClusterIP>:<port>,从集群内部

LoadBalancer

我们可以在支持外部负载均衡器的云提供商上,将 type 字段设置为 LoadBalancer 会为我们的 Service 申请(创建)一个负载均衡器将 Service 暴露到集群外部。 负载均衡器的实际创建是异步进行的,已创建负载均衡器的信息会发布在 Service 的 .status.loadBalancer 字段中。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
clusterIP: 10.0.171.239
type: LoadBalancer

来自外部负载均衡器的流量会被转发到后端 Pods。具体如何进行负载均衡由云提供商决定。

为了实现 type: LoadBalancer 的 Service,Kubernetes 通常会先执行与创建 type: NodePort Service 等效的操作。随后,cloud-controller-manager 组件会配置外部负载均衡器,将流量转发到分配好的 NodePort 上。

如果云提供商的实现支持,你也可以配置LoadBalancer Service 不分配 NodePort,这种流量直通的方式,以后再介绍。

这里引用 Difference between ClusterIP, NodePort and LoadBalancer Service一文中的图来简单的了解一下LoadBalancer Service到后端Pod服务的请求模型。我们可以看到基于分层的设计,流量从外部的Load Balancer到Node的NodePort,然后到Service的Port,最终转发到具体的Pod上,后面我们会详细介绍一下这个分层设计。

LoadBalancerIP

一些云提供商允许你指定 loadBalancerIP。在这种情况下,负载均衡器会使用用户指定的 loadBalancerIP 创建。如果未指定 loadBalancerIP,负载均衡器会使用一个临时(ephemeral)IP 地址。 如果你指定了 loadBalancerIP,但云提供商不支持该特性,那么你设置的 loadBalancerIP 字段会被忽略。

Service 的 .spec.loadBalancerIP 字段在 Kubernetes v1.24 中已被弃用

该字段的定义不够明确,在不同实现中的含义也不同,并且无法支持双栈(dual-stack)网络。该字段可能会在未来的 API 版本中被移除。

如果你正在与某个支持通过(云厂商特定)注解来指定 Service 负载均衡 IP 地址的提供商集成,你应该改为使用这种方式,而不是通过LoadBalanceIP来配置。

如果你正在编写与 Kubernetes 集成的负载均衡器代码,请避免使用该字段。你可以选择与 Gateway 集成,而不是 Service;或者在 Service 上定义你自己的(云厂商特定)注解来指定等效信息。

节点存活状态对负载均衡流量的影响

负载均衡器的健康检查对现代应用至关重要。它们用于判断负载均衡器应将流量分发到哪个服务器(虚拟机或 IP 地址)。

Kubernetes API 并未定义 Kubernetes 管理的负载均衡器必须如何实现健康检查,而是由云提供商(以及集成实现者)自行决定具体行为。

在支持 Service 的 externalTrafficPolicy 字段时,负载均衡器的健康检查被大量使用。

支持多协议的负载均衡器

特性状态:Kubernetes v1.26 [稳定](默认启用)

默认情况下,对于 LoadBalancer 类型的 Service,当定义了多个端口时,所有端口必须使用相同的协议,并且该协议必须是云提供商支持的。

特性开关 MixedProtocolLBService(从 v1.24 起在 kube-apiserver 中默认启用)允许 LoadBalancer 类型的 Service 在定义多个端口时使用不同的协议

可用于负载均衡 Service 的协议集合由你的云提供商定义,他们可能会施加 Kubernetes API 之外的额外限制。

禁用 LoadBalancer 的 NodePort 分配

特性状态:Kubernetes v1.24 [稳定]

你可以通过将 spec.allocateLoadBalancerNodePorts 设置为 false,来可选地禁用 type: LoadBalancer Service 的 NodePort 分配。

这仅应在负载均衡器直接将流量路由到 Pod,而不是通过 NodePort 的实现中使用。

默认情况下,spec.allocateLoadBalancerNodePortstrueLoadBalancer 类型的 Service 仍然会分配 NodePort。

如果你在一个已经分配了 NodePort 的 Service 上将该字段设置为 false,这些 NodePort 不会被自动释放。你必须显式地从每个 Service 端口中移除 nodePorts 条目,才能释放这些 NodePort。

指定负载均衡器实现的类别

特性状态:Kubernetes v1.24 [稳定]

对于 type: LoadBalancer 的 Service,.spec.loadBalancerClass 字段允许你使用非云提供商默认实现的负载均衡器。

默认情况下,.spec.loadBalancerClass 未设置。如果集群使用 --cloud-provider 参数配置了云提供商,那么 LoadBalancer 类型的 Service 会使用云提供商的默认负载均衡器实现。

如果你设置了 .spec.loadBalancerClass,则假定存在一个与该 class 匹配的负载均衡器实现正在监听这些 Service。任何默认的负载均衡器实现(例如云提供商提供的实现)都会忽略设置了该字段的 Service

spec.loadBalancerClass 只能设置在 LoadBalancer 类型的 Service 上,并且一旦设置便不可更改

spec.loadBalancerClass 的值必须是标签风格(label-style)的标识符,可以带有前缀,例如 "internal-vip""example.com/internal-vip"不带前缀的名称保留给最终用户使用。

负载均衡器 IP 地址模式

对于 type: LoadBalancer 的 Service,控制器可以设置 .status.loadBalancer.ingress.ipMode

.status.loadBalancer.ingress.ipMode 用于指定负载均衡 IP 的行为方式。该字段只能在同时指定了 .status.loadBalancer.ingress.ip 时设置

可能的取值有两个:

  • "VIP"(默认):流量被发送到节点,目标地址仍然是负载均衡器的 IP 和端口。
  • "Proxy": 根据云提供商负载均衡器的流量转发方式,分为两种情况:
    • 流量先到达节点,再通过 DNAT 转发到 Pod:目标地址为节点的 IP 和 NodePort;
    • 流量直接发送到 Pod:目标地址为 Pod 的 IP 和端口。

Service 的实现可以使用这些信息来调整流量路由策略。

ExternalName

ExternalName 类型的 Service 用于将一个 Service 映射到一个 DNS 名称,而不是映射到常见的 selector(例如 my-servicecassandra)。你可以通过 spec.externalName 参数来指定这类 Service。

例如,下面这个 Service 定义将 prod 命名空间中的 my-service Service 映射到 my.database.example.com

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com

ExternalName 类型的 Service 可以接受一个 IPv4 地址形式的字符串,但 Kubernetes 会将该字符串当作由数字组成的 DNS 名称来处理,而不是当作 IP 地址(不过,互联网的 DNS 实际上并不允许这种名称存在)。因此,看起来像 IPv4 地址的 externalName 无法被 DNS 服务器解析

如果你希望将一个 Service 直接映射到某个具体的 IP 地址,可以考虑使用 headless Service(无头 Service)

当解析主机名 my-service.prod.svc.cluster.local 时,集群的 DNS Service 会返回一条 CNAME 记录,其值为 my.database.example.com。访问 my-service 的方式与访问其他 Service 类似,但有一个关键区别: 重定向是在 DNS 层完成的,而不是通过代理或流量转发来实现的。

如果你之后决定将数据库迁移到集群内部,只需启动相应的 Pods,添加合适的 selector 或 endpoints,并修改该 Service 的类型即可。

警告:在某些常见协议(包括 HTTP 和 HTTPS)下使用 ExternalName 可能会遇到问题。当你使用 ExternalName 时,集群内客户端使用的主机名,与 ExternalName 所指向的主机名是不同的。对于依赖主机名的协议,这种差异可能会导致错误或不可预期的响应:

  • HTTP 请求中的 Host: 头部,可能是源服务器无法识别的值;
  • TLS 服务器无法提供与客户端连接时所使用主机名相匹配的证书。

下图是引用自Understanding Kubernetes Service Types,简单的画出了各个Service Type的流量,这里Load Balancer没有画出经过NodePort的请求模型,其实不太容易理解,但是Load Balancer支持直通Pod的模式,所以也不能说不对。

Service的分层设计

前面在介绍ServiceSpec结构的ServiceType的时候,了解到Service很重要的设计:Service API 中的 type 字段采用的是分层(嵌套)功能设计:每一层类型都会在前一层的基础上增加功能,我们该如何理解ServiceType的这个分层嵌套的设计呢?

其实前面介绍ClusterIP,NodePort,LoadBalancer三种Service的时候,我们已经了解他们的请求转发模型了。

  • ClusterIP Service(基础层):分配一个集群内部的虚拟IP (ClusterIP),特点是只能在集群内部访问,提供基础的服务发现和负载均衡。
  • NodePort Service(在ClusterIP基础上扩展):在每个Node上开放一个端口(nodePort),可以通过 <NodeIP>:<NodePort> 从集群外部访问,保留了ClusterIP的所有规则,只是额外在每个节点上监听NodePort,所以这里可以看到,底层完全复用ClusterIP的实现,从程序的角度是一个扩展性很好的设计,通过组合来进行功能的增加。
  • LoadBalancer Service(在NodePort基础上扩展):自动创建云平台的外部负载均衡器(如AWS ELB、GCP Load Balancer) ,提供一个稳定的外部IP地址,保留ClusterIP和NodePort的所有功能,流量可以从NodePort的端口进入进群内部,完全复用了下面两层Service的功能。这里我们先不考虑LB流量直通Pod的模式

即使使用LoadBalancer你仍然可以:

  • 通过ClusterIP在集群内访问 ;
  • 通过NodePort直接访问节点;
  • 通过LoadBalancer IP访问;

如下是引用Kubernetes Service Types: LoadBalancer, NodePort, and ClusterIP的分层图示:

EndpointSlice

EndpointSlice API 是 Kubernetes 用来让你的 Service 扩展到处理大量后端的机制,并允许集群高效地更新其健康的后端列表。

特性状态: Kubernetes v1.21 [稳定]

EndpointSlices 跟踪后端端点的 IP 地址。EndpointSlices 通常在Service 创建的时候自动创建,后端端点通常代表 Pods。

在 Kubernetes 中,EndpointSlice 包含一组网络端点的引用。控制平面会自动为任何指定了 selector 的 Kubernetes Service 创建 EndpointSlices。这些 EndpointSlices 包含对所有匹配该 Service selector 的 Pods 的引用。EndpointSlices 根据 IP 家族、协议、端口号和 Service 名称的唯一组合来将网络端点分组。EndpointSlice 对象的名称必须是有效的 DNS 子域名。

默认情况下,控制平面会创建和管理每个 EndpointSlice,最多包含 100 个端点。你可以通过设置 --max-endpoints-per-slice 参数来调整此配置,最多可以设置为 1000 个端点

如果某个 Service 的端点数量过多并达到阈值,Kubernetes 会创建一个新的空 EndpointSlice,并将新的端点信息存储到其中。

EndpointSlices 作为 kube-proxy 路由内部流量的事实来源。当有流量进入 Service 时,kube-proxy 会参考这些 EndpointSlices 来决定如何将流量转发到相应的后端 Pod。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
// EndpointSlice represents a set of service endpoints. Most EndpointSlices are created by
// the EndpointSlice controller to represent the Pods selected by Service objects. For a
// given service there may be multiple EndpointSlice objects which must be joined to
// produce the full set of endpoints; you can find all of the slices for a given service
// by listing EndpointSlices in the service's namespace whose `kubernetes.io/service-name`
// label contains the service's name.
type EndpointSlice struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

// addressType specifies the type of address carried by this EndpointSlice.
// All addresses in this slice must be the same type. This field is
// immutable after creation. The following address types are currently
// supported:
// * IPv4: Represents an IPv4 Address.
// * IPv6: Represents an IPv6 Address.
// * FQDN: Represents a Fully Qualified Domain Name. (Deprecated)
// The EndpointSlice controller only generates, and kube-proxy only processes,
// slices of addressType "IPv4" and "IPv6". No semantics are defined for
// the "FQDN" type.
AddressType AddressType `json:"addressType" protobuf:"bytes,4,rep,name=addressType"`

// endpoints is a list of unique endpoints in this slice. Each slice may
// include a maximum of 1000 endpoints.
Endpoints []Endpoint `json:"endpoints" protobuf:"bytes,2,rep,name=endpoints"`

// ports specifies the list of network ports exposed by each endpoint in
// this slice. Each port must have a unique name. Each slice may include a
// maximum of 100 ports.
Ports []EndpointPort `json:"ports" protobuf:"bytes,3,rep,name=ports"`
}

// Endpoint represents a single logical "backend" implementing a service.
type Endpoint struct {
// addresses of this endpoint. For EndpointSlices of addressType "IPv4" or "IPv6",
// the values are IP addresses in canonical form. The syntax and semantics of
// other addressType values are not defined. This must contain at least one
// address but no more than 100. EndpointSlices generated by the EndpointSlice
// controller will always have exactly 1 address. No semantics are defined for
// additional addresses beyond the first, and kube-proxy does not look at them.
// +listType=set
Addresses []string `json:"addresses" protobuf:"bytes,1,rep,name=addresses"`

// conditions contains information about the current status of the endpoint.
Conditions EndpointConditions `json:"conditions,omitempty" protobuf:"bytes,2,opt,name=conditions"`

// hostname of this endpoint. This field may be used by consumers of
// endpoints to distinguish endpoints from each other (e.g. in DNS names).
// Multiple endpoints which use the same hostname should be considered
// fungible (e.g. multiple A values in DNS). Must be lowercase and pass DNS
// Label (RFC 1123) validation.
// +optional
Hostname *string `json:"hostname,omitempty" protobuf:"bytes,3,opt,name=hostname"`

// targetRef is a reference to a Kubernetes object that represents this endpoint.
TargetRef *v1.ObjectReference `json:"targetRef,omitempty" protobuf:"bytes,4,opt,name=targetRef"`

// deprecatedTopology contains topology information part of the v1beta1
// API. This field is deprecated, and will be removed when the v1beta1
// API is removed (no sooner than kubernetes v1.24).
DeprecatedTopology map[string]string `json:"deprecatedTopology,omitempty" protobuf:"bytes,5,opt,name=deprecatedTopology"`

// nodeName represents the name of the Node hosting this endpoint. This can
// be used to determine endpoints local to a Node.
NodeName *string `json:"nodeName,omitempty" protobuf:"bytes,6,opt,name=nodeName"`

// zone is the name of the Zone this endpoint exists in.
Zone *string `json:"zone,omitempty" protobuf:"bytes,7,opt,name=zone"`

// hints contains information associated with how an endpoint should be consumed.
Hints *EndpointHints `json:"hints,omitempty" protobuf:"bytes,8,opt,name=hints"`
}

参考这里

Services without selectors

Service 最常见的用法是通过 selector 抽象访问 Kubernetes Pod;但当 Service 不使用 selector,并且配合一组对应的 EndpointSlice 对象 时,Service 也可以抽象其他类型的后端,包括 运行在集群外部 的后端。例如:

  • 你在生产环境中使用一个外部数据库集群,但在测试环境中使用自建数据库。
  • 你希望将 Service 指向 另一个命名空间中的 Service,或者 另一个集群中的服务
  • 你正在将一个工作负载迁移到 Kubernetes。在评估过程中,只有一部分后端运行在 Kubernetes 中。

在这些场景中,你都可以定义一个 不指定 selector 的 Service。例如:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376

由于该 Service 没有 selector,因此不会自动创建对应的 EndpointSlice 对象。你可以通过 手动添加 EndpointSlice 对象,将该 Service 映射到实际运行的网络地址和端口。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: my-service-1 # 按惯例,使用 Service 名称作为 EndpointSlice 名称的前缀
labels:
# 必须设置 "kubernetes.io/service-name" 标签
# 其值应与 Service 的名称一致
kubernetes.io/service-name: my-service
addressType: IPv4
ports:
- name: http # 应与上面 Service 中定义的端口名称一致
appProtocol: http
protocol: TCP
port: 9376
endpoints:
- addresses:
- "10.4.5.6"
- addresses:
- "10.1.2.3"

自定义 EndpointSlice

当你为某个 Service 创建 EndpointSlice 对象时,可以为 EndpointSlice 使用 任意名称。在同一个命名空间中,每个 EndpointSlice 的名称必须是唯一的。

通过在 EndpointSlice 上设置 kubernetes.io/service-name 标签,你可以将该 EndpointSlice 关联到对应的 Service。前面一节也介绍了,这里需要注意:

  • Endpoint 的 IP 地址 不能是
    • 回环地址(IPv4 的 127.0.0.0/8,IPv6 的 ::1/128
    • 链路本地地址(IPv4 的 169.254.0.0/16224.0.0.0/24,IPv6 的 fe80::/64
  • Endpoint 的 IP 地址 不能是其他 Kubernetes Service 的 Cluster IP,因为 kube-proxy 不支持将虚拟 IP 作为转发目标。

对于你自己创建的 EndpointSlice(或在自定义代码中创建的),你还应为标签
endpointslice.kubernetes.io/managed-by 选择一个合适的值:

  • 如果你编写了自己的控制器来管理 EndpointSlice,建议使用类似
    my-domain.example/name-of-controller 的值。
  • 如果使用第三方工具,使用工具名称的小写形式,并将空格和标点替换为短横线(-)。
  • 如果是直接通过 kubectl 等工具手动管理 EndpointSlice,可以使用描述人工管理方式的名称,例如 staffcluster-admins
  • 应避免使用保留值 **controller**,该值用于标识由 Kubernetes 控制平面自身管理的 EndpointSlice。

访问无选择器的 Service

访问一个 没有 selector 的 Service,其方式与有 selector 的 Service 相同。

在前面的示例中,流量会被路由到 EndpointSlice 清单中定义的两个端点之一,即在端口 9376 上建立到 10.1.2.310.4.5.6 的 TCP 连接。

注意:

Kubernetes API Server 不允许代理到未映射到 Pod 的端点。因此,对于没有 selector 的 Service,像下面这样的操作将会失败:

1
kubectl port-forward service/<service-name> forwardedPort:servicePort

这是一个安全限制,用于防止 Kubernetes API Server 被用作代理,访问调用者可能无权限访问的端点。

ExternalName Service 是一种特殊情况:它同样 没有 selector,但使用的是 DNS 名称

Endpoints(已弃用)

特性状态: Kubernetes v1.33 [已弃用]

EndpointSlice API 是对旧版 Endpoints API 的演进。与 EndpointSlice 相比,已弃用的 Endpoints API 存在以下问题:

  • 不支持双栈(dual-stack)集群
  • 缺少支持新特性(如 trafficDistribution)所需的信息
  • 当端点列表过长、无法放入单个对象时,会被截断

因此,建议所有客户端 使用 EndpointSlice API,而不是 Endpoints API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/core/v1/types.go

// Endpoints is a collection of endpoints that implement the actual service. Example:
//
// Name: "mysvc",
// Subsets: [
// {
// Addresses: [{"ip": "10.10.1.1"}, {"ip": "10.10.2.2"}],
// Ports: [{"name": "a", "port": 8675}, {"name": "b", "port": 309}]
// },
// {
// Addresses: [{"ip": "10.10.3.3"}],
// Ports: [{"name": "a", "port": 93}, {"name": "b", "port": 76}]
// },
// ]
//
// Endpoints is a legacy API and does not contain information about all Service features.
// Use discoveryv1.EndpointSlice for complete information about Service endpoints.
//
// Deprecated: This API is deprecated in v1.33+. Use discoveryv1.EndpointSlice.
type Endpoints struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

// The set of all endpoints is the union of all subsets. Addresses are placed into
// subsets according to the IPs they share. A single address with multiple ports,
// some of which are ready and some of which are not (because they come from
// different containers) will result in the address being displayed in different
// subsets for the different ports. No address will appear in both Addresses and
// NotReadyAddresses in the same subset.
// Sets of addresses and ports that comprise a service.
// +optional
// +listType=atomic
Subsets []EndpointSubset `json:"subsets,omitempty" protobuf:"bytes,2,rep,name=subsets"`
}

// EndpointSubset is a group of addresses with a common set of ports. The
// expanded set of endpoints is the Cartesian product of Addresses x Ports.
// For example, given:
//
// {
// Addresses: [{"ip": "10.10.1.1"}, {"ip": "10.10.2.2"}],
// Ports: [{"name": "a", "port": 8675}, {"name": "b", "port": 309}]
// }
//
// The resulting set of endpoints can be viewed as:
//
// a: [ 10.10.1.1:8675, 10.10.2.2:8675 ],
// b: [ 10.10.1.1:309, 10.10.2.2:309 ]
//
// Deprecated: This API is deprecated in v1.33+.
type EndpointSubset struct {
// IP addresses which offer the related ports that are marked as ready. These endpoints
// should be considered safe for load balancers and clients to utilize.
Addresses []EndpointAddress `json:"addresses,omitempty" protobuf:"bytes,1,rep,name=addresses"`
// IP addresses which offer the related ports but are not currently marked as ready
// because they have not yet finished starting, have recently failed a readiness check,
// or have recently failed a liveness check.
NotReadyAddresses []EndpointAddress `json:"notReadyAddresses,omitempty" protobuf:"bytes,2,rep,name=notReadyAddresses"`
// Port numbers available on the related IP addresses.
Ports []EndpointPort `json:"ports,omitempty" protobuf:"bytes,3,rep,name=ports"`
}

// EndpointAddress is a tuple that describes single IP address.
// Deprecated: This API is deprecated in v1.33+.
type EndpointAddress struct {
// The IP of this endpoint.
// May not be loopback (127.0.0.0/8 or ::1), link-local (169.254.0.0/16 or fe80::/10),
// or link-local multicast (224.0.0.0/24 or ff02::/16).
IP string `json:"ip" protobuf:"bytes,1,opt,name=ip"`
// The Hostname of this endpoint
Hostname string `json:"hostname,omitempty" protobuf:"bytes,3,opt,name=hostname"`
// Optional: Node hosting this endpoint. This can be used to determine endpoints local to a node.
NodeName *string `json:"nodeName,omitempty" protobuf:"bytes,4,opt,name=nodeName"`
// Reference to object providing the endpoint.
TargetRef *ObjectReference `json:"targetRef,omitempty" protobuf:"bytes,2,opt,name=targetRef"`
}

// EndpointPort is a tuple that describes a single port.
// Deprecated: This API is deprecated in v1.33+.
type EndpointPort struct {
// The name of this port. This must match the 'name' field in the
// corresponding ServicePort.
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`

// The port number of the endpoint.
Port int32 `json:"port" protobuf:"varint,2,opt,name=port"`

// The IP protocol for this port.
// Must be UDP, TCP, or SCTP.
// Default is TCP.
Protocol Protocol `json:"protocol,omitempty" protobuf:"bytes,3,opt,name=protocol,casttype=Protocol"`

// The application protocol for this port.
AppProtocol *string `json:"appProtocol,omitempty" protobuf:"bytes,4,opt,name=appProtocol"`
}

端点超容量(Over-capacity endpoints)

Kubernetes 对单个 Endpoints 对象中可包含的端点数量有限制。当某个 Service 的后端端点 超过 1000 个 时,Kubernetes 会在 Endpoints 对象中 截断数据

由于一个 Service 可以关联多个 EndpointSlice,这个 1000 个端点的限制仅影响旧版 Endpoints API

在这种情况下:

  • Kubernetes 最多选择 1000 个后端端点 写入 Endpoints 对象

  • 并在 Endpoints 上设置注解:

    1
    endpoints.kubernetes.io/over-capacity: truncated
  • 当后端 Pod 数量降到 1000 以下时,控制平面会移除该注解

流量仍然会被发送到所有后端,但任何 依赖旧版 Endpoints API 的负载均衡机制,最多只会将流量发送到这 1000 个端点。

同样的 API 限制意味着:你也 无法手动更新一个 Endpoints 对象,使其包含超过 1000 个端点

Headless Services

有时你并不需要负载均衡和一个单一的 Service IP。在这种情况下,你可以创建所谓的无头(Headless)Service,方法是显式地将集群 IP 地址(.spec.clusterIP)指定为 "None"Headless Service不会分配Cluster IP,kube-proxy 也不会处理这些 Service,平台不会为它们执行任何负载均衡或代理

这种Headless Service的设计是为了对接其他服务发现机制,而不必绑定到 Kubernetes 自身的实现

Headless Service允许客户端直接连接到其所偏好的任意 Pod。无头 Service 不会通过虚拟 IP 和代理来配置路由和数据包转发;相反,它们会通过集群的 DNS 服务来暴露后段的所有Pod,通过内部 DNS 记录中返回各个 Pod 的 endpoint IP 地址

Headless Service的DNS 的自动配置方式取决于 Service 是否定义了 selector(选择器):

  • 有 selector 的情况

对于定义了 selector 的无头 Service,endpoints controller 会在 Kubernetes API 中创建 EndpointSlice,并修改 DNS 配置,使其返回直接指向该 Service 后端 Pod 的 A 记录或 AAAA 记录(分别对应 IPv4 或 IPv6 地址)。

  • 无 selector 的情况

对于未定义 selector 的无头 Service,控制平面不会创建 EndpointSlice 对象。不过,DNS 系统会查找并配置以下内容之一:

  1. 对于 type: ExternalName 的 Service,创建 DNS CNAME 记录。
  2. 对于除 ExternalName 之外的所有 Service 类型,为 Service 的所有就绪(ready)endpoint 的 IP 地址创建 DNS A / AAAA 记录。
  • 对 IPv4 endpoint,DNS 系统创建 A 记录。
  • 对 IPv6 endpoint,DNS 系统创建 AAAA 记录。

这里无selector的Endpoints,前面介绍过了,需要手动或由外部控制器维护的。当你定义一个不带 selector 的无头 Service时,port 必须与 targetPort 相匹配。

下面是引用Kubernetes Headless Service一文中,可以很清晰的理解关于Service和Headless Service的区别:针对Headless Service DNS lookup会返回所有 Pods 的 endpoint IP,针对普通的 Service (非ExternalName类型)的 DNS lookup返回的是ClusterIP(或对应的虚拟 IP)

Discovering services

对于运行在集群内部的客户端,Kubernetes 支持两种主要的 Service 发现方式:环境变量DNS

Environment variables

当一个 Pod 被调度到某个 Node 上运行时,kubelet 会为每一个已存在的(active)Service添加一组环境变量。这些环境变量的格式为 {SVCNAME}_SERVICE_HOST{SVCNAME}_SERVICE_PORT,其中 Service 名称会被转换为大写,并且将短横线(-)替换为下划线(_

例如,Service redis-primary 暴露了 TCP 端口 6379,并且被分配了集群 IP 地址 10.0.0.11,那么它会生成如下环境变量:

1
2
3
4
5
6
7
REDIS_PRIMARY_SERVICE_HOST=10.0.0.11
REDIS_PRIMARY_SERVICE_PORT=6379
REDIS_PRIMARY_PORT=tcp://10.0.0.11:6379
REDIS_PRIMARY_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_PRIMARY_PORT_6379_TCP_PROTO=tcp
REDIS_PRIMARY_PORT_6379_TCP_PORT=6379
REDIS_PRIMARY_PORT_6379_TCP_ADDR=10.0.0.11

注意:
当你有一个 Pod 需要访问某个 Service,并且你使用环境变量方式向客户端 Pod 发布该 Service 的端口和集群 IP 时,必须在客户端 Pod 创建之前先创建 Service。否则,这些客户端 Pod 的环境变量将不会被注入。

如果你只使用 DNS 来发现 Service 的 ClusterIP,则无需担心这个创建顺序的问题。

Kubernetes 还支持并提供了一组与 Docker Engine 的“传统容器链接(legacy container links)”特性兼容的环境变量。你可以查看 makeLinkVariables 以了解 Kubernetes 是如何实现这一点的。

DNS

你可以(而且几乎总是应该)通过插件(add-on)为你的 Kubernetes 集群配置一个 DNS 服务。

一个具备集群感知能力的 DNS 服务器(例如 CoreDNS)会监听 Kubernetes API 中的新 Service,并为每个 Service 创建一组 DNS 记录。如果在整个集群中启用了 DNS,那么所有 Pod 都应该能够通过 Service 的 DNS 名称自动解析到对应的 Service。

例如,如果你在 Kubernetes 的 my-ns 命名空间中有一个名为 my-service 的 Service,那么控制平面与 DNS 服务协同工作,会为其创建一个 DNS 记录:

1
my-service.my-ns
  • 位于 my-ns 命名空间中的 Pod,可以直接通过查询 my-service 来找到该 Service(当然,使用 my-service.my-ns 也可以)。

  • 位于其他命名空间中的 Pod,则必须使用完整限定名 my-service.my-ns。这些名称会解析为该 Service 被分配的 ClusterIP

Kubernetes 还支持针对命名端口(named ports)的 DNS SRV(Service)记录。如果 my-service.my-ns 这个 Service 定义了一个名为 http、协议为 TCP 的端口,那么你可以通过执行以下 DNS SRV 查询:

1
_http._tcp.my-service.my-ns

来发现 http 端口对应的端口号,以及该 Service 的 IP 地址。

如下在我的集群中的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
root@nginx-bf5d5cf98-ct79m:/# nslookup wecom-read-it-later            
;; Got recursion not available from 192.168.94.144
Server: 192.168.94.144
Address: 192.168.94.144#53

Name: wecom-read-it-later.default.svc.cluster.local
Address: 192.168.115.119
;; Got recursion not available from 192.168.94.144

root@nginx-bf5d5cf98-ct79m:/# nslookup wecom-read-it-later.default
;; Got recursion not available from 192.168.94.144
;; Got recursion not available from 192.168.94.144
Server: 192.168.94.144
Address: 192.168.94.144#53

Name: wecom-read-it-later.default.svc.cluster.local
Address: 192.168.115.119
;; Got recursion not available from 192.168.94.144

root@nginx-bf5d5cf98-ct79m:/# nslookup _http._tcp.wecom-read-it-later
;; Got recursion not available from 192.168.94.144
Server: 192.168.94.144
Address: 192.168.94.144#53

Name: _http._tcp.wecom-read-it-later.default.svc.cluster.local
Address: 192.168.115.119
;; Got recursion not available from 192.168.94.144

Kubernetes DNS 服务器也是访问 ExternalName 类型 Service 的唯一方式

Virtual IPs实现的基石之Service Proxy:kube-proxy

在 Kubernetes 集群中,默认每个节点都会运行一个 kube-proxy(除非你部署了自己的替代组件来取代 kube-proxy)。

kube-proxy 组件负责为 Service实现虚拟 IP(Virtual IP)机制,除 ExternalName Service以外。每一个 kube-proxy 实例都会监听 Kubernetes 控制平面中 Service 和 EndpointSlice 对象的新增与删除。对于每一个 Service,kube-proxy 会(根据其运行模式的不同)调用相应的 API(例如iptables模式,创建iptable规则),配置节点以捕获发往该 Service 的 clusterIP 和端口的流量,并将这些流量重定向到该 Service 的某一个 endpoint(通常是 Pod,也可能是用户提供的任意 IP 地址)。

同时存在一个控制循环,用于确保每个节点上的规则始终与 API Server 中所反映的 Service 和 EndpointSlice 状态保持可靠同步。

如下是引用Virtual IPs and Service Proxies官方手册中关于kube-proxy的工作位置的图示:某个无状态的图像处理工作负载,其后端 Pod 以 3 个副本运行。这些副本是可互换的(fungible)——前端并不关心具体使用哪一个后端 Pod。

一个经常被提出的问题是:为什么 Kubernetes 依赖代理(proxy)来将入站流量转发到后端?

是否可以采用其他方式?例如,是否可以配置包含多个 A 记录(IPv6 场景下是 AAAA 记录)的 DNS 记录,并依赖 DNS 的轮询(round-robin)解析来实现?

Kubernetes 选择通过代理机制来实现 Service,有以下几个原因:

  • DNS 实现长期以来存在**不严格遵守记录 TTL **的问题,常常在记录过期后仍然缓存解析结果。
  • 有些应用只在启动时进行一次 DNS 解析,并将结果无限期缓存。
  • 即使应用和库能够正确地重新解析 DNS,设置很低甚至为 0 的 DNS TTL 也会给 DNS 系统带来极大的负载,从而变得难以管理。

下面我们会介绍不同 kube-proxy 实现方式的工作原理。总体来说需要注意的是:运行 kube-proxy 时,内核级别的规则可能会被修改(例如创建 iptables 规则),而这些规则在某些情况下只有在重启节点后才会被清理。因此,运行 kube-proxy 应当仅由了解在计算机上运行低层、特权网络代理服务所带来后果的管理员来完成。

iptables代理模式

这种代理模式仅在 Linux 节点上可用。

在该模式下,kube-proxy 使用内核 netfilter 子系统的 iptables API 来配置数据包转发规则。对于每一个 Endpoint,kube-proxy 都会安装相应的 iptables 规则,默认情况下随机选择一个后端 Pod

集群中所有Node上的 kube-proxy 实例都会观察到Watch新Service 的创建。当某个节点上的 kube-proxy 看到一个新的 Service 时,它会安装一系列 iptables 规则,这些规则会将流量从该虚拟 IP 地址重定向到按 Service 定义的其他 iptables 规则。

这些按 Service 定义的规则会继续链接到每个后端 Endpoint 对应的规则,而每个 Endpoint 规则都会通过目的地址 NAT(DNAT)将流量重定向到对应的后端

当客户端连接到 Service 的虚拟 IP 地址时,相应的 iptables 规则会生效。kube-proxy 会选择一个后端(基于会话亲和性或随机选择),并将数据包转发到该后端,同时不会重写客户端的 IP 地址

当流量通过 type: NodePort 的 Service,或者通过负载均衡器进入集群时,也会执行同样的基本流程;不过在这些情况下,客户端 IP 地址会被修改

优化 iptables模式的性能

在 iptables 模式下,kube-proxy 会为每个 Service 创建若干条 iptables 规则,并为每个 Endpoint IP 地址再创建若干条规则。在拥有成千上万 Pods 和 Services 的集群中,这意味着会存在数以万计的 iptables 规则,当 Services(或其 EndpointSlices)发生变化时,kube-proxy 可能需要较长时间才能将规则更新到内核中。

你可以通过 kube-proxy 配置文件中 iptables 部分的选项来调整同步行为(通过 kube-proxy --config <path> 指定):

1
2
3
4
5
...
iptables:
minSyncPeriod: 1s
syncPeriod: 30s
...
minSyncPeriod

minSyncPeriod 参数用于设置两次尝试将 iptables 规则与内核重新同步之间的最小时间间隔

如果设置为 0s,那么每当任何 Service 或 EndpointSlice 发生变化时,kube-proxy 都会立即同步规则。这种方式在非常小的集群中运行良好,但在短时间内发生大量变化时,会产生大量重复工作

例如:如果你有一个 Service,其后端是一个包含 100 个 Pod 的 Deployment,当你删除该 Deployment 时:

  • minSyncPeriod: 0s 的情况下,kube-proxy 会逐个 Pod 地从 iptables 规则中移除 Endpoint,总共会触发 100 次更新
  • 如果设置了较大的 minSyncPeriod,多个 Pod 删除事件就可以被合并处理,例如只进行 5 次更新,每次移除 20 个 Endpoint。这种方式在 CPU 使用效率上更高,并且可以更快地完成全部变更的同步。

不过,minSyncPeriod 值越大,虽然可以聚合更多变更,但代价是:单个变更可能需要等待最长一个 minSyncPeriod 才会被处理,这意味着 iptables 规则在更长时间内与 API Server 的实际状态不一致

默认值 1s 在大多数集群中都能很好地工作;但在非常大的集群中,可能需要将其设置为更大的值。特别是当 kube-proxy 的 sync_proxy_rules_duration_seconds 指标显示平均耗时远大于 1 秒时,适当增大 minSyncPeriod 可能会让更新更加高效。

在较老版本的 kube-proxy 中,每次同步都会更新所有 Service 的所有规则,这在大型集群中会导致性能问题(更新延迟),因此当时的推荐做法是设置一个较大的 minSyncPeriod

Kubernetes v1.28 开始,kube-proxy 的 iptables 模式采用了更加精细的更新策略,只在 Service 或 EndpointSlice 实际发生变化时才更新对应规则

如果你之前手动覆盖了 minSyncPeriod,那么在升级后,建议尝试移除该覆盖配置,让 kube-proxy 使用默认值(1s),或者至少使用一个比之前更小的值。

syncPeriod

syncPeriod 参数用于控制一些与单个 Service 或 EndpointSlice 变化不直接相关的同步操作。尤其是,它决定了 kube-proxy 多快能够发现有外部组件干扰了其 iptables 规则

在大型集群中,kube-proxy 也只会每隔一个 syncPeriod 执行某些清理操作,以避免不必要的工作。

总体而言,增大 syncPeriod 对性能影响不大。在过去,有时会将其设置为非常大的值(例如 1h),但现在已经不再推荐这样做,因为这样做更可能损害功能性,而不是带来性能收益。

IPVS代理模式

FEATURE STATE: Kubernetes v1.35 [deprecated]

该代理模式仅在 Linux 节点上可用

在 IPVS 模式下,kube-proxy 使用内核的 IPVSiptables API 来创建规则,将流量从 Service IP 重定向到 Endpoint IP。

IPVS 代理模式基于 netfilter 的 hook 函数,其工作方式与 iptables 模式类似,但底层数据结构使用的是哈希表,并且在内核空间中运行

IPVS 代理模式最初是一次实验,目标是为 Linux 平台上的 kube-proxy 提供一种:

  • 比 iptables 模式更高效的规则同步性能
  • 更高的网络流量吞吐能力

虽然在这些目标上取得了成功,但实践表明:内核 IPVS API 与 Kubernetes Service API 的契合度并不理想,因此 IPVS 后端始终无法正确实现 Kubernetes Service 功能中的所有边界场景。

后面介绍的 nftables 代理模式,在设计上基本取代了 iptables 和 IPVS 模式,其性能优于二者,并且推荐作为 IPVS 的替代方案。如果你部署的 Linux 系统版本过旧,无法运行 nftables 代理模式,那么也应优先考虑使用 iptables 模式,而不是 IPVS,因为自 IPVS 模式最初引入以来,iptables 模式的性能已经有了显著提升。

流量调度算法

IPVS 为后端 Pod 提供了多种负载均衡调度策略,包括:

  • rr(Round Robin,轮询):流量在所有后端服务器之间平均分配。

  • wrr(Weighted Round Robin,加权轮询):根据服务器权重分配流量,权重高的服务器会接收更多新连接和请求。

  • lc(Least Connection,最少连接):将更多流量分配给当前活动连接数较少的服务器。

  • wlc(Weighted Least Connection,加权最少连接):根据“连接数 / 权重”的比值,将流量分配给相对负载较低的服务器。

  • lblc(Locality-Based Least Connection,基于局部性的最少连接):对来自同一 IP 地址的流量,优先发送到同一个后端服务器(如果该服务器未过载且可用);否则转发到连接数较少的服务器,并在后续继续保持这种分配关系。

  • lblcr(Locality-Based Least Connection with Replication,带复制的局部性最少连接): 来自同一 IP 的流量优先发送到当前连接数最少的服务器。如果所有后端服务器都过载,则选择一个负载较低的服务器加入目标集合;如果目标集合在指定时间内未发生变化,则移除负载最高的服务器,以避免过度复制。

  • sh(Source Hashing,源地址哈希):基于源 IP 地址查找静态哈希表,将流量发送到对应的后端服务器。

  • dh(Destination Hashing,目标地址哈希):基于目标地址查找静态哈希表,将流量发送到对应的后端服务器。

  • sed(Shortest Expected Delay,最短期望延迟):将流量发送到期望延迟最短的服务器。期望延迟的计算方式为: (C + 1) / U,其中,C 是服务器上的连接数,U 是服务器的固定服务速率(权重)。

  • nq(Never Queue,不排队):如果存在空闲服务器,则直接将流量发送给空闲服务器,而不是等待更快的服务器;如果所有服务器都繁忙,则退化为 sed 算法。

  • mh(Maglev Hashing,Maglev 哈希):基于 Google 的 Maglev 哈希算法分配流量。该调度器支持两个标志:

    • mh-fallback:当选定的服务器不可用时,回退到其他服务器
    • mh-port:在哈希计算中加入源端口号

    在使用 mh 时,kube-proxy 始终启用 mh-port 标志,且**不会启用 mh-fallback**。在 proxy-mode=ipvs 下,mh 的行为类似于 带端口的源地址哈希(sh)

这些调度算法通过 kube-proxy 配置中的 ipvs.scheduler 字段进行配置。

注意事项(Note)

  • 要以 IPVS 模式 运行 kube-proxy,必须在启动 kube-proxy 之前,确保节点上已经启用了 IPVS
  • 当 kube-proxy 以 IPVS 代理模式启动时,它会检查 IPVS 内核模块是否可用
  • 如果未检测到 IPVS 内核模块,kube-proxy 将 直接报错并退出

nftables代理模式

FEATURE STATE: Kubernetes v1.33 [stable](enabled by default)

该代理模式仅在 Linux 节点上可用,并且要求内核版本为 5.13 或更高

在该模式下,kube-proxy 使用内核 netfilter 子系统的 nftables API 来配置数据包转发规则。对于每一个 Endpoint,kube-proxy 都会安装相应的 nftables 规则,默认情况下随机选择一个后端 Pod

nftables API 是 iptables API 的继任者,其设计目标是提供比 iptables 更好的性能和可扩展性。与 iptables 模式相比,nftables 代理模式能够更快、更高效地处理 Service Endpoint 的变化,并且在内核中处理数据包时也更加高效(不过这一优势通常只会在拥有成千上万个 Service 的集群中才会明显体现)。

截至 Kubernetes 1.35,nftables 模式仍然相对较新,可能尚未与所有网络插件完全兼容;请查阅你所使用的网络插件的相关文档以确认兼容性。

从 iptables模式迁移到nftables

希望从默认的 iptables 模式切换到 nftables 模式的用户,需要注意:在 nftables 模式下,某些功能的行为与 iptables 模式略有不同

  1. NodePort 接口行为

在 iptables 模式下,默认情况下 NodePort Service 会监听所有本地 IP 地址。这通常并不是用户想要的行为,因此 nftables 模式下默认使用:

1
--nodeport-addresses primary

这意味着:type: NodePort 的 Service 只会在节点的主 IPv4 和/或 IPv6 地址上可访问,如果你希望监听所有本地 IPv4 地址,可以显式指定该选项,例如:

1
--nodeport-addresses 0.0.0.0/0
  1. 127.0.0.1 上的 type: NodePort Service

在 iptables 模式下:

  • 如果 --nodeport-addresses 范围包含 127.0.0.1
  • 且未传递 --iptables-localhost-nodeports=false

那么 type: NodePort 的 Service 可以通过 localhost(127.0.0.1)访问

而在 nftables 模式(以及 IPVS 模式) 下:这种行为不再支持,如果你不确定是否依赖了这一功能,可以检查 kube-proxy 的指标:

1
iptables_localhost_nodeports_accepted_packets_total

如果该指标不为 0,说明确实有客户端通过 localhost / loopback 访问过 type: NodePort 的 Service。

  1. NodePort 与防火墙的交互

iptables 模式下的 kube-proxy 会尽量兼容配置过于严格的防火墙

  • 对于每一个 type: NodePort Service
  • kube-proxy 会显式添加规则,允许该端口的入站流量
  • 以防止防火墙原本阻止这些流量

然而,这种方式 无法适用于基于 nftables 的防火墙。因此,在 nftables 模式下,kube-proxy 不会再自动添加这些防火墙放行规则。如果你的节点上存在本地防火墙:

  • 你必须确保其配置正确
  • 明确允许 Kubernetes 流量通过(例如:允许整个 NodePort 端口范围的入站流量)
  1. Conntrack Bug 的处理方式差异

Linux 6.1 之前的内核 中,存在一个 Bug,可能导致:与 Service IP 建立的长连接 TCP 会话,被异常关闭,并报错:

1
Connection reset by peer

iptables 模式下的 kube-proxy 默认会安装一个规避该 Bug 的 workaround。但后来发现,该 workaround 在某些集群中会引发其他问题。

nftables 模式 下:kube-proxy 默认不会安装任何 workaround

如果你怀疑集群依赖了该 workaround,可以检查 kube-proxy 的指标:

1
iptables_ct_state_invalid_dropped_packets_total

如果该指标非 0,说明你的集群可能依赖该行为。此时,你可以在 nftables 模式下通过以下参数来规避问题:

1
--conntrack-tcp-be-liberal

kernelspace代理模式

该代理模式仅在 Windows 节点上可用

kube-proxy 会在 Windows 虚拟筛选平台(Windows Virtual Filtering Platform,VFP) 中配置数据包过滤规则。VFP 是 Windows vSwitch 的一个扩展。这些规则会在节点级虚拟网络中的封装数据包上生效,并对数据包进行重写,使其目标 IP 地址(以及二层信息)正确,从而将数据包路由到正确的目的地。

Windows VFP 的作用类似于 Linux 上的 nftables 或 iptables。Windows VFP 是对 Hyper-V Switch 的扩展,而 Hyper-V Switch 最初是为支持虚拟机网络而实现的。

当某个节点上的 Pod 向一个虚拟 IP 地址发送流量,而 kube-proxy 选择了位于其他节点上的 Pod 作为负载均衡目标时,kernelspace 代理模式会将该数据包重写为指向目标后端 Pod

Windows 主机网络服务(Host Networking Service,HNS) 会确保正确配置这些数据包重写规则,使得返回流量看起来是来自虚拟 IP 地址,而不是来自具体的后端 Pod。

kube-proxy的核心语义

kube-proxy 组件负责为 Service实现虚拟 IP(Virtual IP)机制,除 ExternalName Service以外。

每一个 kube-proxy 实例都会监听 Kubernetes 控制平面中 ServiceEndpointSlice 对象的新增与删除。对于每一个 Service,kube-proxy 会(根据其运行模式的不同)调用相应的 API(例如iptables模式,创建iptable规则),配置节点以捕获发往该 Service 的 clusterIP 和端口的流量,并将这些流量重定向到该 Service 的某一个 endpoint(通常是 Pod,也可能是用户提供的任意 IP 地址)。

kube-proxy存在于控制面中,部署在集群的每个Node上,负责监听ServiceEndpointSlice 的变更,然后通过操作系统提供的接口,把“转发规则”写进内核,内核才是真正转发流量的人

kube-proxy的参与:只有在规则变更时,不是在转发时

下图是引用自Kubernetes Network Troubleshooting Approach,我觉得能够很好的概括kube-proxy在Service中负责的角色:即通过调用操作系统的API(例如iptables模式进行的规则写入),完成规则的变更。

流量是如何跨机完成转发呢,kube-proxy 只是转发规则的写入,用来选Pod,如何到达Pod不是kube-proxy的工作,这里其实就是CNI插件做的事情了,类似之前我在Docker容器网络浅析介绍Docker采用的VXLAN进行跨机的容器网络转发。

常见的CNI插件:Flannel / Calico(VXLAN 模式),Calico(BGP / 纯三层),Cilium(eBPF)。下面是CNI插件在Pod创建时的工作:

1
2
3
4
5
6
7
8
9
10
kubelet
|
| 调用容器运行时(containerd / CRI-O)
|
| 调用 CNI 插件(ADD)
v
CNI 插件:
1. 分配 Pod IP
2. 创建 veth pair
3. 配置路由 / 隧道 / 防火墙

下面是CNI和kube-proxy在整个网络模型的位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Client
|
v
Node A
|
| kube-proxy (iptables/IPVS/eBPF)
| DNAT: ClusterIP → PodIP
v
Linux routing
|
| CNI 网络(VXLAN / BGP / Geneve)
v
Node B
|
v
Pod

kube-proxy 和 CNI 的分工再次总结如下:

层级 负责组件 做什么
Service → Pod kube-proxy 选 Pod、做 DNAT
Pod ↔ Pod CNI 让 Pod IP 可达
Node ↔ Node 底层网络 承载流量

Connection Tracking

我们知道一个外部针对Service的请求,可能会到任何一台Node上,每个 Node 都是完整 Service 入口,所以每个 Node 上的 kube-proxy 都会 watch 所有 Service 和 EndpointSlice,然后为该 Service 创建完整的 转发规则。

这么做的设计目标是:客户端 Pod 在任何 Node 上,都能访问 Service IP,并被转发到任意一个后端 Pod

那我们想一个问题:一个外部的请求是怎么和Pod建立固定的链接的呢?

从网络基础知识我们可以知道,一个连接请求一旦在某个 Node 上建立,就会被“粘”在这个 Node 上,后续的流程如下:

  1. 第一个 SYN 到达 Node A
  2. 查找kube-proxy 在 Node A 上写入的转发规则,触发负载均衡决策,选中某一个 Endpoint(PodIP:Port),找到后做 DNAT;
  3. Linux conntrack(Windows HNS)在 Node A 记录该 TCP 连接,并进行转发;
  4. 后续所有的包都会走 conntrack 表转发的流程;

我们先看一下什么是conntrack,conntrack(Connection Tracking)是 Linux 内核 netfilter 子系统中的“连接状态机”,用于跟踪网络连接的状态,并记录 NAT 映射关系。 它跟踪的不是“Socket”,而是“连接五元组”:

1
2
3
4
5
源 IP
源端口
目的 IP
目的端口
协议(TCP / UDP / ICMP)

👉 这就是 TCP 长连接“粘住 Pod”的原因

参考What is Kube-Proxy and why move from iptables to eBPF? 一文,后面我们有时间可以了解eBPF:

Ingress

Ingress 使用一种具备协议感知能力的配置机制来对外暴露你的 HTTP(或 HTTPS)网络服务,该机制能够理解 Web 概念,例如 URI、主机名(hostname)、路径(path)等。Ingress 这一概念允许你通过 Kubernetes API 中定义的规则,将流量映射到不同的后端服务。

FEATURE STATE: Kubernetes v1.19 [stable]

Ingress公开了从集群外部到集群内Service的 HTTP 和 HTTPS 路由。 Ingress 可以提供负载均衡、SSL 终止以及基于名称的虚拟主机(name-based virtual hosting)

注意:
Kubernetes 项目建议使用 Gateway 来替代 Ingress。Ingress API 已被冻结(frozen)

这意味着:

  • Ingress API 仍然是正式可用(GA)的,并且受到 GA API 的稳定性保障。Kubernetes 项目**没有计划 **将 Ingress 从 Kubernetes 中移除。
  • Ingress API 不再继续开发未来不会再有任何功能变更或更新

下面是一个简单示例,其中一个 Ingress 将所有流量都转发到同一个 Service:

Ingress 不能暴露任意端口或协议。如果需要将 HTTP/HTTPS 之外的服务暴露到互联网,通常应使用 Service.Type=NodePortService.Type=LoadBalancer 类型的 Service。

要使 Ingress 生效,必须部署一个 Ingress Controller。仅仅创建一个 Ingress 资源本身是不会产生任何效果的。Ingress Controller负责真正实现Ingress,通常会借助负载均衡器来完成;它也可能配置你的边缘路由器或额外的前端组件来协助处理流量。

理想情况下,所有 Ingress Controller 都应符合参考规范(reference specification),但在实际中,不同的 Ingress Controller 在行为上会存在一些细微差异。

下面是一个最小化的 Ingress 资源示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minimal-ingress
spec:
ingressClassName: nginx-example
rules:
- http:
paths:
- path: /testpath
pathType: Prefix
backend:
service:
name: test
port:
number: 80

基本的 apiVersionkindmetadata 数据这里就不啰嗦了,我们下面结合IngressSpec的定义来看一下Ingress的功能,

IngressSpec结构详解

如下是IngressSpec的结构定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/networking/v1/types.go

type IngressSpec struct {
// ingressClassName is the name of an IngressClass cluster resource. Ingress
// controller implementations use this field to know whether they should be
// serving this Ingress resource, by a transitive connection
// (controller -> IngressClass -> Ingress resource).
IngressClassName *string `json:"ingressClassName,omitempty" protobuf:"bytes,4,opt,name=ingressClassName"`

DefaultBackend *IngressBackend `json:"defaultBackend,omitempty" protobuf:"bytes,1,opt,name=defaultBackend"`

// tls represents the TLS configuration.
TLS []IngressTLS `json:"tls,omitempty" protobuf:"bytes,2,rep,name=tls"`

// rules is a list of host rules used to configure the Ingress. If unspecified,
// or no rule matches, all traffic is sent to the default backend.
Rules []IngressRule `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"`
}

IngressSpec包含了配置负载均衡器或代理服务器所需的全部信息。最重要的是,其中包含了一组规则(rules),用于匹配所有传入的请求。Ingress 资源只支持用于转发 HTTP(S) 流量的规则

IngressClassName

ingressClassName were added in Kubernetes 1.18

ingressClassName 指定该 Ingress 应由哪个 Ingress Controller 负责处理。

在早期 Kubernetes 里:没有 ingressClassName 字段,Ingress Controller 靠 annotation 来“抢” Ingress,多个 Controller 可能同时处理同一个 Ingress,默认行为混乱、不确定。为了解决这个问题,Kubernetes 引入了 IngressClass 资源 + ingressClassName 字段。

如果没有设置ingressClassName 字段,可能的行为有:

  • 有default IngressClass,IngressClass 可以被标记为 默认,可以接管没有ingressClassName 字段的Ingress;
1
2
3
metadata:
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
  • 没有 default IngressClass,这样取决于集群中 Ingress Controller 的实现,有的可能会直接忽略,有的可能会全部接管。

DefaultBackend

如果一个 Ingress 没有定义任何规则,那么所有流量都会被发送到一个单一的默认后端(一个Service对象或一个Resource对象),该后端由 .spec.defaultBackend 指定。

默认后端在传统上是 Ingress Controller 的一个配置选项,而不是在 Ingress 资源中显式指定的。

  • 如果没有定义 .spec.rules,则 必须指定 .spec.defaultBackend
  • 如果未设置 defaultBackend,那么不匹配任何规则的请求如何处理将取决于具体的 Ingress Controller;
  • 如果多个Ingress都没有定义 .spec.rules,且都指定 .spec.defaultBackend,那么具体行为取决于具体的 Ingress Controller;通常只会有“一个”真正生效的 default backend,其余的会被忽略或覆盖;

如果 HTTP 请求在任何 Ingress 对象中都没有匹配到 host 或 path,则流量会被路由到默认后端。

.spec.defaultBackend的结构IngressBackend的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/networking/v1/types.go

type IngressBackend struct {
// service references a service as a backend.
Service *IngressServiceBackend `json:"service,omitempty" protobuf:"bytes,4,opt,name=service"`

// resource is an ObjectRef to another Kubernetes resource in the namespace
// of the Ingress object. If resource is specified, a service.Name and
// service.Port must not be specified.
Resource *v1.TypedLocalObjectReference `json:"resource,omitempty" protobuf:"bytes,3,opt,name=resource"`
}

// IngressServiceBackend references a Kubernetes Service as a Backend.
type IngressServiceBackend struct {
Name string `json:"name" protobuf:"bytes,1,opt,name=name"`
Port ServiceBackendPort `json:"port,omitempty" protobuf:"bytes,2,opt,name=port"`
}

// ServiceBackendPort is the service port being referenced.
type ServiceBackendPort struct {
// name is the name of the port on the Service.
Name string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"`

// number is the numerical port number (e.g. 80) on the Service.
Number int32 `json:"number,omitempty" protobuf:"bytes,2,opt,name=number"`
}

Ingress的后端对象可以是一个Service对象或一个Resource对象,Service对象我们可以理解,Resource对象是什么呢?资源后端(Resource backend) 是一个指向与 Ingress 对象位于同一命名空间内的其他 Kubernetes 资源的 ObjectRef

  • ResourceService互斥的配置
  • 如果同时指定两者,将导致校验失败

资源后端的一个常见用途是:将 Ingress 流量导入一个对象存储后端,用于提供静态资源

IngressRule

每个Ingress对象都包含一组规则Rules,每一条 HTTP 规则的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// https://github.com/kubernetes/kubernetes/blob/release-1.34/staging/src/k8s.io/api/networking/v1/types.go

type IngressRule struct {
// host is the fully qualified domain name of a network host, as defined by RFC 3986.
Host string `json:"host,omitempty" protobuf:"bytes,1,opt,name=host"`
// IngressRuleValue represents a rule to route requests for this IngressRule.
IngressRuleValue `json:",inline" protobuf:"bytes,2,opt,name=ingressRuleValue"`
}

// IngressRuleValue represents a rule to apply against incoming requests.
type IngressRuleValue struct {
HTTP *HTTPIngressRuleValue `json:"http,omitempty" protobuf:"bytes,1,opt,name=http"`
}

// HTTPIngressRuleValue is a list of http selectors pointing to backends.
type HTTPIngressRuleValue struct {
// paths is a collection of paths that map requests to backends.
Paths []HTTPIngressPath `json:"paths" protobuf:"bytes,1,rep,name=paths"`
}

// PathType represents the type of path referred to by a HTTPIngressPath.

type PathType string
const (
PathTypeExact = PathType("Exact")
PathTypePrefix = PathType("Prefix")
PathTypeImplementationSpecific = PathType("ImplementationSpecific")
)

// HTTPIngressPath associates a path with a backend. Incoming urls matching the
// path are forwarded to the backend.
type HTTPIngressPath struct {
Path string `json:"path,omitempty" protobuf:"bytes,1,opt,name=path"`
PathType *PathType `json:"pathType" protobuf:"bytes,3,opt,name=pathType"`
Backend IngressBackend `json:"backend" protobuf:"bytes,2,opt,name=backend"`
}

包含以下信息:

  • 可选的 host:匹配指定的host,在上面的示例中未指定 host,因此该规则适用于通过指定 IP 地址进入的所有 HTTP 流量。如果指定了 host(例如 foo.bar.com),则规则只会应用于该主机名。
  • 一组 path 列表(例如 /testpath):每个 path 都关联一个 backend,该 backend 通过 service.nameservice.port.nameservice.port.number 定义。只有当 host 和 path 都与传入请求匹配时,负载均衡器才会将流量转发到对应的 Service。
  • Backend(后端):Backend 是 Service 与端口名的组合,或者通过 CRD 定义的自定义资源后端。 与规则中 host 和 path 匹配的 HTTP(或 HTTPS)请求会被发送到对应的 backend。

Ingress 中的每一个 path 都必须指定一个 pathType。未显式指定 pathType 的 path 会导致校验失败。

支持的三种 pathType 如下:

  • ImplementationSpecific:路径匹配方式由 IngressClass 自行决定。实现可以将其作为一种独立的类型,或者与 Prefix / Exact 等价处理。
  • Exact:与 URL 路径进行完全匹配,并且区分大小写
  • Prefix:基于 URL 路径前缀进行匹配,并以 / 作为分隔符逐段匹配。匹配是区分大小写的,并且是按路径段(path element)进行的。如果请求路径中每一段都以前缀路径的对应段开头,则认为匹配成功。

注意:
如果 path 的最后一个元素只是请求路径最后一个元素的子串,则不算匹配。例如:/foo/bar 可以匹配 /foo/bar/baz,但不能匹配 /foo/barbaz

IngressRule的Host可以是精确匹配(例如 foo.bar.com),也可以是通配符匹配(例如 *.foo.com)。

  • 精确匹配:要求 HTTP 请求头中的 Hosthost 字段完全一致。
  • 通配符匹配:要求 HTTP 请求头中的 Host 与通配符规则的后缀部分相同
Host Host header Match?
*.foo.com bar.foo.com Matches based on shared suffix
*.foo.com baz.bar.foo.com No match, wildcard only covers a single DNS label
*.foo.com foo.com No match, wildcard only covers a single DNS label

IngressClass

前面介绍IngressSpec结构定义的时候介绍过.spec.ingressClassName ,这里再展开介绍一下IngressClass的API对象。

Ingress 可以由不同的 Controller 来实现,而这些 Controller 往往具有不同的配置方式。每一个 Ingress 都应该指定一个 class,也就是对某个 IngressClass 资源的引用。IngressClass 中包含了额外的配置,其中包括应该由哪个 Ingress Controller 来实现该 class

如下是一个IngressClass资源对象的示例:

1
2
3
4
5
6
7
8
9
10
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: external-lb
spec:
controller: example.com/ingress-controller
parameters:
apiGroup: k8s.example.com
kind: IngressParameters
name: external-lb

IngressClass 的 .spec.parameters 字段允许你引用另一个资源,该资源为该 IngressClass 提供相关的配置。具体使用哪种类型的 parameters,取决于你在 IngressClass 的 .spec.controller 字段中指定的 Ingress Controller。

IngressClass 的作用域

根据你使用的 Ingress Controller,你可能可以使用:

  • 集群级(Cluster) 参数
  • 命名空间级(Namespaced) 参数

IngressClass parameters 的默认作用域是 集群级(cluster-wide)

如果你设置了 .spec.parameters 字段,但没有设置 .spec.parameters.scope,或者你将 .spec.parameters.scope 显式设置为 Cluster,那么该 IngressClass 引用的是一个 集群级资源。此时:

  • kind(结合 apiGroup)必须指向一个 集群级 API(可能是一个自定义资源)
  • name 标识该 API 下的某一个具体的集群级资源

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: external-lb-1
spec:
controller: example.com/ingress-controller
parameters:
# 该 IngressClass 的参数定义在一个
# 名为 "external-config-1" 的 ClusterIngressParameter
# (API group 为 k8s.example.net)中。
# 该定义告诉 Kubernetes 去查找一个集群级的参数资源。
scope: Cluster
apiGroup: k8s.example.net
kind: ClusterIngressParameter
name: external-config-1

Deprecated annotation

在 Kubernetes 1.18 引入 IngressClass 资源和 ingressClassName 字段之前,Ingress class 是通过在 Ingress 上设置 kubernetes.io/ingress.class annotation 来指定的。该 annotation 从未被正式写入 API 规范,但被大量 Ingress Controller 所支持。

新的 ingressClassName 字段是对该 annotation 的替代方案,但并不是完全等价的:

  • 旧 annotation 通常直接引用要实现该 Ingress 的 Controller 名称
  • 新字段引用的是一个 IngressClass 资源,IngressClass 中不仅包含 Controller 名称,还可以包含额外的 Ingress 配置

Default IngressClass

你可以将某一个 IngressClass 标记为集群的默认 IngressClass

在 IngressClass 资源上设置如下 annotation:

1
ingressclass.kubernetes.io/is-default-class: "true"

这样可以确保:新创建且未指定 ingressClassName 的 Ingress 会自动使用该默认 IngressClass。

Ingress的使用

Simple fanout

扇出(fanout)配置可以根据所请求的 HTTP URI,将来自同一个 IP 地址的流量路由到多个 Service。 Ingress 允许你将所需的负载均衡器数量控制在最低。

例如,下图所示简单的fanout架构:

对应的Ingress的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: simple-fanout-example
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /foo
pathType: Prefix
backend:
service:
name: service1
port:
number: 4200
- path: /bar
pathType: Prefix
backend:
service:
name: service2
port:
number: 8080

Name based virtual hosting

基于名称的虚拟主机支持在同一个 IP 地址上,将 HTTP 流量路由到多个主机名。如下:

如下是对应的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: name-virtual-host-ingress
spec:
rules:
- host: foo.bar.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: service1
port:
number: 80
- host: bar.foo.com
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: service2
port:
number: 80

Load balancing

Ingress Controller 在启动时会配置一组负载均衡策略设置,并将这些策略应用到所有 Ingress 上,例如:

  • 负载均衡算法
  • 后端权重方案
  • 以及其他相关设置

一些更高级的负载均衡概念(例如:会话保持、动态权重)目前尚未通过 Ingress 暴露。如果你需要这些功能,可以通过 Service 所使用的负载均衡器 来获得。

另外需要注意的是,虽然 Ingress 本身并未直接暴露健康检查(health checks),但 Kubernetes 中存在一些并行的概念(例如 readiness probe),可以让你达到相同的效果。

总结

如下两个图很好的说明了Serivce和Ingress的概念和网络模型结构:

https://medium.com/avmconsulting-blog/service-types-in-kubernetes-24a1587677d6

https://home.robusta.dev/blog/kubernetes-service-vs-loadbalancer-vs-ingress