0%

【Kubernetes】Service

Kubernetes 中的 Service 将一组 Pod 以统一的形式对外暴露成一个服务,它利用运行在内核空间的 iptables 或者 ipvs 高效地转发来自节点内部和外部的流量。作为非常重要的 Kubernetes 对象,Service 不仅在逻辑上提供了微服务的概念,还引入 LoadBalancer 类型的 Service 无缝对接云服务商提供的复杂资源。

Background

我们知道,Kubernetes 中的每一个 Pod 都可以通过 podIP 被直接访问,但是 Pod 是有生命周期的对象,它们可以被创建,而且销毁之后不会再启动。如果 DeploymentReplicaSet 等对象管理 Pod,则它们可以动态地创建和销毁 Pod。在这种情况下,Deployment当前时刻运行的 Pod 集合可能与稍后运行该应用程序的 Pod 集合不同。

这就造成了一个问题,如果一组backend Pod 为集群中的另一组frontend Pod 提供服务时,由于每一个 Pod 都有自己的IP地址,并且这组Pod是会动态变化的,那么Frontend如何做服务发现以及会话保持,从而可以使用 Backend Pod 的服务?

为了解决这个问题,Kubernetes提出了 Service 这个概念:Service 是一组 Pod的逻辑集合和访问方式的抽象。举个例子,考虑一个图片处理后端,它运行了 3 个副本。这些副本是可互换的 —— 前端不需要关心它们调用了哪个后端副本。 然而组成这一组后端程序的 Pod 实际上可能会发生变化, 前端客户端不应该也没必要知道,而且也不需要跟踪这一组后端的状态。 Service 定义的抽象能够解耦这种关联。

Introduction

下面是一个 Service 的典型定义:

1
2
3
4
5
6
7
8
9
10
11
12
kind: Service
apiVersion: v1
metadata:
name: nginx
spec:
selector:
app: nginx
ports:
- name: http
protocol: TCP
port: 8080
targetPort: 80

这里注意几个Port的定义区分:

  • port:service暴露在cluster ip上的端口,<cluster ip>:port 是提供给集群内部客户访问service的入口
  • targetPort:Pod监听的端口,service会把流量转发到对应的Pod,Pod中的容器也需要监听这个端口
  • nodePort:对应于NodePort类型的Service时指定的节点上的Port,详见 NodePort

在命令行中可以看到集群为Service创建了一个 ClusterIP

1
2
3
[root@VM-1-28-centos nginx]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx ClusterIP 172.18.255.55 <none> 8080/TCP 5s

创建 Nginx 实际对应的 Deployment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
selector:
matchLabels:
app: nginx
replicas: 2
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80

对应于实际服务请求的路径如下图所示:

Kubernetes Service

Publish Services

Kubernetes 中支持四种服务暴露的方式: ClusterIP、NodePort、LoadBalancer、ExternelName

ClusterIP

ClusterIP 类型的 service 是 kubernetes 集群默认的服务暴露方式,它只能用于集群内部通信,可以被各 pod 访问,其访问方式为:

1
pod ---> ClusterIP:ServicePort --> (iptables)DNAT --> PodIP:containePort

ClusterIP Service 类型的结构如下图所示:

Cluster IP

Headless service

当不需要负载均衡以及单独的 ClusterIP 时,可以通过指定 spec.clusterIP 的值为 None 来创建 Headless service,它会给一个集群内部的每个成员提供一个唯一的 DNS 域名来作为每个成员的网络标识,集群内部成员之间使用域名通信。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
clusterIP: None
ports:
- nodePort: 30080
port: 80
protocol: TCP
targetPort: 8080
selector:
app: nginx

NodePort

如果想要在集群外访问集群内部的服务,可以使用这种类型的 service,NodePort 类型的 service 会在集群内部署了 kube-proxy 的每个节点打开一个指定的端口,之后所有的流量直接发送到这个端口,然后会被转发到 service 后端真实的服务进行访问。Nodeport 构建在 ClusterIP 上,其访问链路如下所示:

1
client ---> NodeIP:NodePort ---> ClusterIP:ServicePort ---> (iptables)DNAT ---> PodIP:containePort

其对应具体的 iptables 规则会在后文进行讲解。

NodePort service 类型的结构如下图所示:

Node Port

修改 service 定义如下,其中 nodeport 字段表示通过 nodeport 方式访问的端口,port 表示通过 service 方式访问的端口,targetPort 表示 pod port。如果这里的 nodePort 字段不指定,Kubernetes会自动申请一个Node Port。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kind: Service
apiVersion: v1
metadata:
name: nginx
spec:
type: NodePort
selector:
app: nginx
ports:
- name: http
# By default and for convenience, the `targetPort` is set to the same value as the `port` field.
port: 80
targetPort: 80
# Optional field
# By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
nodePort: 30080

执行 kubectl get service 可以看到:

1
2
3
$ kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx NodePort 172.18.252.115 <none> 80:30080/TCP 20s

这时候,在浏览器中选择集群中任意一节点的IP作为 nodeIP,通过浏览器 http://<nodeIP>:<nodePort> 可以看到 Nginx 的欢迎界面。

LoadBalancer

LoadBalancer 类型的 service 通常和云厂商的 LB 结合一起使用,用于将集群内部的服务暴露到外网,云厂商的 LoadBalancer 会给用户分配一个 IP,之后通过该 IP 的流量会转发到你的 service 上。

LoadBalancer service 类型的结构如下图所示:

Load Balancer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 146.148.47.155

ExternelName

类型为 ExternalName 的服务将服务映射到 DNS 名称,而不是典型的选择器,例如 my-service 或者 cassandra。 您可以使用 spec.externalName 参数指定这些服务。

例如,以下 Service 定义将 prod 名称空间中的 my-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

当查找主机 my-service.prod.svc.cluster.local 时,集群 DNS 服务返回 CNAME 记录, 其值为 my.database.example.com。 访问 my-service 的方式与其他服务的方式相同,但主要区别在于重定向发生在 DNS 级别,而不是通过代理或转发。 如果以后您决定将数据库移到群集中,则可以启动其 Pod,添加适当的选择器或端点以及更改服务的 type

Controllers

Service 是一组具有相同 label pod 集合的抽象,集群内外的各个服务可以通过 service 进行互相通信。在 Kubernetes 中创建一个新的 Service 对象需要两大模块同时协作:

  • Controller:在每次创建新的 Service 对象时,会同时创建一个 Endpoint 对象。Endpoint 是用于容器发现,Service 只是将多个 Pod 进行关联。Endpoints Controller 是负责生成和维护所有 Endpoints 对象的控制器,监听 Service 和对应 Pod 的变化,更新对应 Service 的 Endpoints 对象。当 Pod 处于 running 且准备就绪时,Endpoints Controller 会将 Pod IP 记录到 Endpoints 对象中。
  • kube-proxy:它运行在 Kubernetes 集群中的每一个节点上,会根据 Service 和 Endpoint 的变动改变节点上 iptables 或者 ipvs 中保存的规则。

Kubernetes Service

Service

每当有服务被创建或者销毁时,Informer 都会通知 ServiceController,它会将这些任务投入工作队列中并由其本身启动的 Worker 协程消费:

sequenceDiagram
    participant I as Informer
    participant SC as ServiceController
    participant Q as WorkQueue
    participant B as Balancer
    I->>+SC: Add/Update/DeleteService
    SC->>Q: Add
    Q-->>SC: return
    deactivate SC
    loop Worker
        SC->>+Q: Get
        Q-->>-SC: key
        SC->>SC: syncService
        SC->>+B: EnsureLoadBalancer
        B-->>-SC: LoadBalancerStatus
    end

Endpoint

我们在使用 Kubernetes 时虽然很少会直接与 Endpoint 资源打交道,但是它却是 Kubernetes 中非常重要的组成部分。

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
// 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}]
// },
// ]
type Endpoints struct {
metav1.TypeMeta
// +optional
metav1.ObjectMeta

// The set of all endpoints is the union of all subsets.
Subsets []EndpointSubset
}

// 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 ]
type EndpointSubset struct {
Addresses []EndpointAddress
NotReadyAddresses []EndpointAddress
Ports []EndpointPort
}

While you’re correct that in the glossary there’s indeed no entry for endpoint, it is a well defined Kubernetes network concept or abstraction. Since it’s of secondary nature, you’d usually not directly manipulate it. There’s a core resource Endpoint defined and it’s also supported on the command line:

1
2
3
$ kubectl get ep
NAME ENDPOINTS AGE
kubernetes 192.168.64.13:8443 10d

And there you see what it effectively is: an IP address and a port. Usually, you’d let a service manage endpoints (one EP per pod the service routes traffic to) but you can also manually manage them if you have a use case that requires it.

  服务和pod不是直接连接,而是通过Endpoint资源进行连通。endpoint资源是暴露一个服务的ip地址和port的列表。  选择器用于构建ip和port列表,然后存储在endpoint资源中。当客户端连接到服务时,服务代理选择这些列表中的ip和port对中的一个,并将传入连接重定向到在该位置监听的服务器。  endpoint是一个单独的资源并不是服务的属性,endpoint的名称必须和服务的名称相匹配

  为没有选择器的服务创建endpoint资源: $ kubectl create -f endpoint.yml  endpoint对象需要与服务相同的名称,并包含该服务的目标ip和port列表,服务和endpoint资源都发布到服务器后,这样服务就可以像具有pod选择器那样的服务正常使用。

endpoint yml 模板

EndpointController 本身并没有通过 Informer 监听 Endpoint 资源的变动,但是它却同时订阅了 Service 和 Pod 资源的增删事件,对于 Service 资源来讲,EndpointController 会通过以下的方式进行处理:

sequenceDiagram
    participant I as Informer
    participant EC as EndpointController
    participant Q as WorkQueue
    participant PL as PodLister
    participant C as Client
    I->>+EC: Add/Update/DeleteService
    EC->>Q: Add
    Q-->>EC: return
    loop Worker
        EC->>+Q: Get
        Q-->>-EC: key
        EC->>+EC: syncService
        EC->>+PL: ListPod(service.Spec.Selector)
        PL-->>-EC: Pods
        loop Every Pod
            EC->>EC: addEndpointSubset
        end
        EC->>C: Create/UpdateEndpoint
        C-->>-EC: result
    end

EndpointController 中的 syncService 方法是用于创建和删除 Endpoint 资源最重要的方法,在这个方法中我们会根据 Service 对象规格中的选择器 Selector 获取集群中存在的所有 Pod,并将 Service 和 Pod 上的端口进行映射生成一个 EndpointPort 结构体:

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
func (e *EndpointController) syncService(key string) error {
namespace, name, _ := cache.SplitMetaNamespaceKey(key)
service, _ := e.serviceLister.Services(namespace).Get(name)
pods, _ := e.podLister.Pods(service.Namespace).List(labels.Set(service.Spec.Selector).AsSelectorPreValidated())

subsets := []v1.EndpointSubset{}
for _, pod := range pods {
epa := *podToEndpointAddress(pod)

for i := range service.Spec.Ports {
servicePort := &service.Spec.Ports[i]

portName := servicePort.Name
portProto := servicePort.Protocol
portNum, _ := podutil.FindPort(pod, servicePort)

epp := &v1.EndpointPort{Name: portName, Port: int32(portNum), Protocol: portProto}
subsets, _, _ = addEndpointSubset(subsets, pod, epa, epp, tolerateUnreadyEndpoints)
}
}
subsets = endpoints.RepackSubsets(subsets)

currentEndpoints = &v1.Endpoints{
ObjectMeta: metav1.ObjectMeta{
Name: service.Name,
Labels: service.Labels,
},
}

newEndpoints := currentEndpoints.DeepCopy()
newEndpoints.Subsets = subsets
newEndpoints.Labels = service.Labels
e.client.CoreV1().Endpoints(service.Namespace).Create(newEndpoints)

return nil
}

对于每一个 Pod 都会生成一个新的 EndpointSubset,其中包含了 Pod 的 IP 地址和端口和 Service 的规格中指定的输入端口和目标端口,在最后 EndpointSubset 的数据会被重新打包并通过客户端创建一个新的 Endpoint 资源。

在上面我们已经提到过,除了 Service 的变动会触发 Endpoint 的改变之外,Pod 对象的增删也会触发 EndpointController 中的回调函数。

sequenceDiagram
    participant I as Informer
    participant EC as EndpointController
    participant Q as WorkQueue
    participant SL as ServiceLister
    I->>+EC: Add/Update/DeletePod
    EC->>+SL: GetPodServices
    SL-->>-EC: []Service
    EC->>Q: Add
    Q-->>EC: return
    deactivate EC

getPodServiceMemberships 会获取跟当前 Pod 有关的 Service 对象并将所有的 Service 对象都转换成 <namespace>/<name> 的字符串:

1
2
3
4
5
6
7
8
9
10
func (e *EndpointController) getPodServiceMemberships(pod *v1.Pod) (sets.String, error) {
set := sets.String{}
services, _ := e.serviceLister.GetPodServices(pod)

for i := range services {
key, _ := controller.KeyFunc(services[i])
set.Insert(key)
}
return set, nil
}

这些服务最后会被加入 EndpointController 的队列中,等待它持有的几个 Worker 对 Service 进行同步。

这些其实就是 EndpointController 的作用,订阅 Pod 和 Service 对象的变更,并根据当前集群中的对象生成 Endpoint 对象将两者进行关联。

Proxy Mode

在 Kubernetes 集群中的每一个节点都运行着一个 kube-proxy 进程,这个进程会负责监听 Kubernetes 主节点中 Service 的增加和删除事件并修改运行代理的配置,为节点内的客户端提供流量的转发和负载均衡等功能。在整个集群中另一个订阅 Service 对象变动的组件就是 kube-proxy 了,每当 kube-proxy 在新的节点上启动时都会初始化一个 ServiceConfig 对象,就像介绍 iptables 代理模式时提到的,这个对象会接受 Service 的变更事件:

sequenceDiagram
    participant SCT as ServiceChangeTracker
    participant SC as ServiceConfig
    participant P as Proxier
    participant EC as EndpointConfig
    participant ECT as EndpointChangeTracker
    participant SR as SyncRunner
    SC->>+P: OnServiceAdd/Update/Delete/Synced
    P->>SCT: Update
    SCT-->>P: Return ServiceMap
    deactivate P
    EC->>+P: OnEndpointsAdd/Update/Delete/Synced
    ECT-->>P: Return EndpointMap
    P->>ECT: Update
    deactivate P
    loop Every minSyncPeriod ~ syncPeriod
        SR->>P: syncProxyRules
    end

这些变更事件都会被订阅了集群中对象变动的 ServiceConfigEndpointConfig 对象推送给启动的 Proxier 实例:

1
2
3
4
5
6
7
8
9
func (c *ServiceConfig) handleAddService(obj interface{}) {
service, ok := obj.(*v1.Service)
if !ok {
return
}
for i := range c.eventHandlers {
c.eventHandlers[i].OnServiceAdd(service)
}
}

收到事件变动的 Proxier 实例随后会根据启动时的配置更新 iptables 或者 ipvs 中的规则,这些应用最终会负责对进出的流量进行转发并完成一些负载均衡相关的任务。

Userspace

作为运行在用户空间的代理,对于每一个 Service 都会在当前的节点上开启一个端口,所有连接到当前代理端口的请求都会被转发到 Service 背后的一组 Pod 上,它其实会在节点上添加 iptables 规则,通过 iptables 将流量转发给 kube-proxy 处理。

如果当前节点上的 kube-proxy 在启动时选择了 userspace 模式,那么每当有新的 Service 被创建时,kube-proxy 就会增加一条 iptables 记录并启动一个 Goroutine,前者用于将节点中服务对外发出的流量转发给 kube-proxy,再由后者持有的一系列 Goroutine 将流量转发到目标的 Pod 上。

在 userspace 模式下,访问服务的请求到达节点后首先进入内核 iptables,然后回到用户空间,由 kube-proxy 转发到后端的 pod,这样流量从用户空间进出内核带来的性能损耗是不可接受的,所以也就有了 iptables 模式。

为什么 userspace 模式要建立 iptables 规则,因为 kube-proxy 监听的端口在用户空间,这个端口不是服务的访问端口也不是服务的 nodePort,因此需要一层 iptables 把访问服务的连接重定向给 kube-proxy 服务。

Service Userspace Proxy Mode

这一系列的工作大都是在 OnServiceAdd 被触发时中完成的,正如上面所说的,该方法会调用 mergeService 将传入服务 Service 的端口变成一条 iptables 的配置命令为当前节点增加一条规则,同时在 addServiceOnPort 方法中启动一个 TCP 或 UDP 的 Socket:

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
func (proxier *Proxier) mergeService(service *v1.Service) sets.String {
svcName := types.NamespacedName{Namespace: service.Namespace, Name: service.Name}
existingPorts := sets.NewString()
for i := range service.Spec.Ports {
servicePort := &service.Spec.Ports[i]
serviceName := proxy.ServicePortName{NamespacedName: svcName, Port: servicePort.Name}
existingPorts.Insert(servicePort.Name)
info, exists := proxier.getServiceInfo(serviceName)
if exists {
proxier.closePortal(serviceName, info)
proxier.stopProxy(serviceName, info)
}
proxyPort, := proxier.proxyPorts.AllocateNext()

serviceIP := net.ParseIP(service.Spec.ClusterIP)
info, _ = proxier.addServiceOnPort(serviceName, servicePort.Protocol, proxyPort, proxier.udpIdleTimeout)
info.portal.ip = serviceIP
info.portal.port = int(servicePort.Port)
info.externalIPs = service.Spec.ExternalIPs
info.loadBalancerStatus = *service.Status.LoadBalancer.DeepCopy()
info.nodePort = int(servicePort.NodePort)
info.sessionAffinityType = service.Spec.SessionAffinity

proxier.openPortal(serviceName, info)
proxier.loadBalancer.NewService(serviceName, info.sessionAffinityType, info.stickyMaxAgeSeconds)
}

return existingPorts
}

这个启动的进程会监听同一个节点上,转发自所有进程的 TCP 和 UDP 请求并将这些数据包发送给目标的 Pod 对象。

在用户空间模式中,如果一个连接被目标服务拒绝,我们的代理服务能够重新尝试连接其他的服务,除此之外用户空间模式并没有太多的优势。

IPTables

另一种常见的代理模式就是直接使用 iptables 转发当前节点上的全部流量,这种脱离了用户空间在内核空间中实现转发的方式能够极大地提高 proxy 的效率,增加 kube-proxy 的吞吐量。

iptables 模式是目前默认的代理方式,基于 netfilter 实现。当客户端请求 service 的 ClusterIP 时,根据 iptables 规则路由到各 pod 上,iptables 使用 DNAT 来完成转发,其采用了随机数实现负载均衡。

iptables 模式与 userspace 模式最大的区别在于,iptables 模块使用 DNAT 模块实现了 service 入口地址到 pod 实际地址的转换,免去了一次内核态到用户态的切换,另一个与 userspace 代理模式不同的是,如果 iptables 代理最初选择的那个 pod 没有响应,它不会自动重试其他 pod。

iptables 模式最主要的问题是在 service 数量大的时候会产生太多的 iptables 规则,使用非增量式更新会引入一定的时延,大规模情况下有明显的性能问题。

Service Iptables Proxy Mode

iptables 作为一种代理模式,它同样实现了 OnServiceUpdateOnEndpointsUpdate 等方法,这两个方法会分别调用相应的变更追踪对象。

sequenceDiagram
    participant SC as ServiceConfig
    participant P as Proxier
    participant SCT as ServiceChangeTracker
    participant SR as SyncRunner
    participant I as iptable
    SC->>+P: OnServiceAdd
    P->>P: OnServiceUpdate
    P->>SCT: Update
    SCT-->>P: Return ServiceMap
    deactivate P
    loop Every minSyncPeriod ~ syncPeriod
        SR->>+P: syncProxyRules
        P->>I: UpdateChain
        P->>P: writeLine x N
        P->>I: RestoreAll
        deactivate P
    end

变更追踪对象会根据 ServiceEndpoint 对象的前后变化改变 ServiceChangeTracker 本身的状态,这些变更会每隔一段时间通过一个 700 行的巨大方法 syncProxyRules 同步,在这里就不介绍这个方法的具体实现了,它的主要功能就是根据 ServiceEndpoint 对象的变更生成一条一条的 iptables 规则,比较感兴趣的读者,可以点击 proxier.go#L640-1379 查看代码。

当我们使用 iptables 的方式启动节点上的代理时,所有的流量都会先经过 PREROUTING 或者 OUTPUT 链,随后进入 Kubernetes 自定义的链入口 KUBE-SERVICES、单个 Service 对应的链 KUBE-SVC-XXXX 以及每个 Pod 对应的链 KUBE-SEP-XXXX,经过这些链的处理,最终才能够访问当一个服务的真实 IP 地址。

虽然相比于用户空间来说,直接运行在内核态的 iptables 能够增加代理的吞吐量,但是当集群中的节点数量非常多时,iptables 并不能达到生产级别的可用性要求,每次对规则进行匹配时都会遍历 iptables 中的所有 Service 链。

规则的更新也不是增量式的,当集群中的 Service 达到 5,000 个,每增加一条规则都需要耗时 11min,当集群中的 Service 达到 20,000 个时,每增加一条规则都需要消耗 5h 的时间,这也就是告诉我们在大规模集群中使用 iptables 作为代理模式是完全不可用的。

IPVS

ipvs 就是用于解决在大量 Service 时,iptables 规则同步变得不可用的性能问题。与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,但是它却使用哈希表作为底层的数据结构并且工作在内核态,这也就是说 ipvs 在重定向流量和同步代理规则有着更好的性能。

Service IPVS Proxy Mode

在处理 Service 的变化时,ipvs 包和 iptables 其实就有非常相似了,它们都同样使用 ServiceChangeTracker 对象来追踪变更,只是两者对于同步变更的方法 syncProxyRules 实现上有一些不同。

sequenceDiagram
    participant P as Proxier
    participant SR as SyncRunner
    participant IP as ipvs
    participant I as iptable
    loop Every minSyncPeriod ~ syncPeriod
        SR->>+P: syncProxyRules
        P->>P: writeLine(iptable)
        P->>IP: Add/UpdateVirtualServer(syncService)
        IP-->>P: result
        P->>IP: AddRealServer(syncEndpoint)
        IP-->>P: result
        P->>I: RestoreAll
        deactivate P
    end

我们从 ipvs 的源代码和上述的时序图中可以看到,Kubernetes ipvs 的实现其实是依赖于 iptables 的,后者能够辅助它完成一些功能,使用 ipvs 相比 iptables 能够减少节点上的 iptables 规则数量,这也是因为 ipvs 接管了原来存储在 iptables 中的规则。

除了能够提升性能之外,ipvs 也提供了多种类型的负载均衡算法,除了最常见的 Round-Robin 之外,还支持最小连接、目标哈希、最小延迟等算法,能够很好地提升负载均衡的效率。

当集群规模比较大时,iptables 规则刷新会非常慢,难以支持大规模集群,因其底层路由表的实现是链表,对路由规则的增删改查都要涉及遍历一次链表,ipvs 的问世正是解决此问题的,ipvs 是 LVS 的负载均衡模块,与 iptables 比较像的是,ipvs 的实现虽然也基于 netfilter 的钩子函数,但是它却使用哈希表作为底层的数据结构并且工作在内核态,也就是说 ipvs 在重定向流量和同步代理规则有着更好的性能,几乎允许无限的规模扩张。

参考资料