• Post author:
  • Post category:Kubernetes
  • Post comments:0评论
基于 Kubernetes 1.22 版本

一、基础概念

  Service 是 Kubemetes 最核心的概念,它定义了一组逻辑 Pod 和访问它们的策略(有时这种模式称为微服务)。即通过创建 Service ,可以为一组相同功能的 Pod 提供一个统一的入口地址,井且将请求负载均衡分发到后端的各个 Pod 上。Service 所管理的 Pod 集通常由 Label Selector 确定。
  运行在每个 Node 上的 kube-proxy 进程其实就是一个智能的软件负载均衡器,它负责把对 Service 的请求转发到后端的某个 Pod 上,井在内部实现服务的负载均衡与会话保持机制。每个 Service 会分配到一个全局唯一的虚拟 IP 地址,这个虚拟 IP 被称为 Cluster IP,这个虚拟 IP 在 Service 的整个生命周期内,它是不会发生改变的。
  要知道 Pod 的 Endpoint 地址随着其销毁和重建而发生改变,像使用 Deployment 来运行应用程序,它可以动态地创建和销毁 Pod。那么这样前端如何去找到后端呢,此时我们的 Servcie 就派上用场了,它通过 Label Selector 去对接后端 Pod,前端只需访问 Servcie 定义的服务入口地址即可。
  官方文档:https://kubernetes.io/docs/concepts/services-networking/service/

二、代理模式

  我们知道,Kubernetes 集群中的每个节点都运行一个 kube-proxy 进程,kube-proxy 负责为除 ExternalName 类型以外的 Service 实现虚拟 IP。
  在 Kubernetes 中,kube-proxy 有三种代理模式,分别是 Userspace、Iptables 和 Ipvs。官方推荐使用 Ipvs,不过当集群不支持 Ipvs 的时候,集群会自动使用 Iptables。

1、Userspace

  在这种模式下,kube-proxy 监控着 Service 和 Endpoint。对于每个 Service,它在本地节点上打开一个端口(随机选择),任何到此代理端口的连接都将代理到 Service 的一个后端 Pod。kube-proxy 在决定使用哪个后端 Pod 时会考虑 Service 的 SessionAffinity 设置。
  最后,Userspace 代理模式会创建 iptables 规则,用于捕获到服务的 ClusterIP 和端口的流量,规则将该流量重定向到代理后端 Pod 的代理端口。
  默认情况下,Userspace 代理模式下的 kube-proxy 通过轮询算法选择后端。

  例如,当创建 Service 时,Kubernetes 会分配一个虚拟 IP 地址(ClusterIP),例如 10.0.0.1。假设 Service 端口为 1234,集群中的所有 kube-proxy 实例都会观察到该 Service,当它看到一个新 Service 时,它会打开一个新的随机端口,建立一个从虚拟 IP 地址到这个新端口的 iptables 重定向,并开始接受该端口上的连接。当客户端连接到 Service 的虚拟 IP 地址时,iptables 规则生效,并将数据包重定向到代理自己的端口。然还代理将选择后端,并开始将流量从客户端代理到后端。

2、Iptables

  在这种模式下,kube-proxy 监控着 Service 和 Endpoint。对于每个 Service,它都创建 iptables 规则,这些规则捕获到 Service 的 ClusterIP 和端口的流量,并将该流量重定向到 Service 的一个后端集。对于每个端点对象,它创建 iptables 规则,这些规则会选择一个后端 Pod。
  默认情况下,iptables 代理模式下的 kube-proxy 随机选择后端。
  使用 iptables 处理流量具有较低的系统开销,因为流量由 Linux netfilter 处理,无需在用户空间和内核空间之间切换,这种方法也更加可靠。
  如果 kube-proxy 在 iptables 代理模式下运行,并且选择的第一个 Pod 没有响应,则连接失败。这与 Userspace 代理模式不同的是:在这种情况下,kube-proxy 将检测到与第一个 Pod 的连接失败,并将自动使用不同的后端 Pod 重试。
  我们也可以使用 Pod 就绪探测来验证后端 Pod 是否正常工作,这样在 iptables 代理模式下的 kube-proxy 只会看到测试正常的后端。这样做意味着可以避免通过 kube-proxy 将流量发送到已知失败的 Pod。

3、Ipvs

  在这种模式下,kube-proxy 监控着 Service 和 Endpoint,调用 netlink 接口创建相应的 ipvs 规则,并定期将 ipvs 规则与 Service 和 Endpoint 同步。该控制循环可确保 ipvs 状态与所需状态匹配。访问 Service 时,ipvs 将流量定向到后端 Pod 之一。
  Ipvs 代理模式基于 netfilter hook 函数,该函数类似于 iptables 模式,但使用哈希表作为底层数据结构,并在内核空间中工作。这意味着 ipvs 模式下的 kube-proxy 重定向流量的延迟比iptables 模式下的kube-proxy 低,在同步代理规则时具有更好的性能。与其他代理模式相比,ipvs 模式还支持更高的网络流量吞吐量。像 iptables 在大规模集群(例如10000个服务)中的运行速度显著降低。
  Ipvs 为平衡后端 POD 的流量提供了更多选项,几种调度算法如下:

• rr:轮询,即依次循环分配流量。
• lc:最少打开连接数,即优先分配流量给打开连接数最少的后端。
• dh:目标哈希,即流量按照目标 IP 的 hash 结果分配。
• sh:源哈希,即流量按照源 IP 的 hash 结果分配,这样会使得来自同一 IP 的客户端固定访问一个后端。
• sed:最短的预期延迟,即响应时间越短的后端优先分配流量
• nq:从不排队,即当后端有空闲时,先调度到空闲的上,否则依据 sed 算法调度。

  要在 IPVS 模式下运行 kube-proxy,必须在启动 kube-proxy 之前保证节点上的 IPVS 可用。当 kube-proxy 在 IPVS 代理模式下启动时,它会验证 IPVS 内核模块是否可用。如果未检测到 IPVS 内核模块,则 kube-proxy 将回退到以 iptables 代理模式运行。

  以上不论哪种模式,kube-proxy 都监控着 kube-apiserver 写入 etcd 中关于 Pod 的最新状态信息,它一旦检查到那个 Pod 资源添加或者删除,它会立即将这些变化同步到 iptables 或 ipvs 规则中,以便 iptables 和 ipvs 在调度客户端请求到后端 Pod 时,不会出现 Pod 不存在的情况。

三、定义详解

Service 定义官方文档:https://kubernetes.io/docs/reference/kubernetes-api/service-resources/
yaml 格式的 Service 定义文件常用内容如下:

apiVersion: v1          # 必选,API版本号
kind: Service           # 必选,类型为Service
metadata:               # 必选,元数据
  name: string          # 必选,Service名称
  namespace: string     # 命名空间,默认为default
  labels:               # 标签列表
    name: string
  annotations:          # 注释列表
    name: string
spec:                   # 必选,定义Service的行为
  selector:             # 标签选择器,选择具有指定标签的Pod进行管理
    name: string
  type: string          # Service的类型,用于指定Service的访问方式,默认为CusterIP
  ports:                # 必选,定义Servcie端口相关信息
  - name: string        # 端口名称
    port: int           # 必选,端口,Service将公开的端口
    targetPort: int     # 目标端口,目标Pod的容器暴露端口,默认和port相同。
    protocol: string    # 端口协议,支持TCP、UDP和SCTP,默认为TCP
    nodePort: int       # 当type为NodePort或LoadBalancer时,Servic在宿主机节点上公开的端口,不指定随机分配
  clusterIP: string     # Service的IP地址,默认随机分配
  sessionAffinity: string   # Session亲和,即基于客户端IP地址进行会话保持的模式,可选值为ClientIP和None(默认)
                            # ClientIP表示将同一个客户端(IP)的访问请求都转发到同一个后端Pod上

Service 的类型(spec.type):

• ClusterIP:在集群内部 IP 上公开此 Service,此类型服务只能从集群内部访问,这是默认的 Service 类型。
• NodePort:在宿主机节点 IP 的端口上公开此 Service,此类型将能够通过 NodePort 从集群外部访问服 Service。
• LoadBalancer:使用外部负载均衡器对外公开 Service,用于公有云环境。
• ExternalName:通过返回 CNAME 记录及其值,将 Service 映射到 externalName 字段的内容。

除了文件定义,我们还可以使用命令快速创建 Service:

[root@master ~]# kubectl expose deployment nginx-deployment --port=80 --type=NodePort
service/nginx-deployment exposed
[root@master ~]# kubectl get svc
NAME               TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
nginx-deployment   NodePort    10.1.82.56    <none>        80:32080/TCP   11s

[root@master ~]# curl -s localhost:32080 | head -n4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

四、基本使用

1、ClusterIP

  下面是一个简单的示例,创建了一个 Deployment,并定义了一个名为 "nginx-service" 的 Service ,它的服务端口为80 ,将对接拥有 "app: nginx" 这个 label 的所有 Pod。

[root@master ~]# vim nginx-deployment.yaml 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.18.0
        ports:
        - containerPort: 80

[root@master ~]# vim nginx-service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80  
    protocol: TCP 

  我们来简单解读 Service 的定义文件 nginx-service.yaml:spec.selector 定义选择具有指定标签的 Pod 进行管理;在 spec.ports 的定义中, port 定义了 Service 的端口,targetPort 则定义了该服务的容器所暴露(EXPOSE)的端口,如果没有指定 targetPort,则默认 targetPort 和 port 相同。

[root@master ~]# kubectl apply -f nginx-deployment.yaml,nginx-service.yaml 
deployment.apps/nginx-deployment created
service/nginx-service created

[root@master ~]# kubectl get endpoints
NAME            ENDPOINTS                                         AGE
kubernetes      10.0.0.100:6443                                   23d
nginx-service   10.244.2.120:80,10.244.2.121:80,10.244.2.122:80   12s

[root@master ~]# kubectl get svc
NAME            TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
kubernetes      ClusterIP   10.1.0.1      <none>        443/TCP   23d
nginx-service   ClusterIP   10.1.10.223   <none>        80/TCP    18s

[root@master ~]# curl -s 10.1.10.223 | head -n4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

2、NodePort

现在将 nginx-service 改为 NodePort 模式,以使得能从集群外部访问:

# 将spec.type改为NodePort
[root@master ~]# kubectl edit service nginx-service 
service/nginx-service edited

[root@master ~]# kubectl get svc nginx-service 
NAME            TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
nginx-service   NodePort   10.1.10.223   <none>        80:30577/TCP   68m

[root@master ~]# curl -s localhost:30577 | head -n4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

3、多端口

  对于某些服务,是需要公开多个端口。Kubernetes 允许给 Service 定义多个端口。为一个 Service 使用多个端口时,必须配置所有端口的名称,例如:

[root@master ~]# cat nginx-service-port.yaml 
apiVersion: v1
kind: Service
metadata:
  name: nginx-service-port
spec:
  selector:
    app: nginx
  ports:
  - name: http
    protocol: TCP
    port: 80
  - name: https
    protocol: TCP
    port: 443

[root@master ~]# kubectl apply -f nginx-service-port.yaml 
service/nginx-service-port created

[root@master ~]# kubectl get svc nginx-service-port 
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
nginx-service-port   ClusterIP   10.1.92.51   <none>        80/TCP,443/TCP   6s

五、外部服务

  Service 最常见的用法是抽象对 Pod 的访问,但它们也可以抽象其它类型的后端,即定义一个没有选择器的服务。
  例如以下场景:

• 将外部数据库作为后端服务进行连接。
• 将另一个集群或 Namespace 中的服务作为服务的后端。

例如下例,10.0.0.100 上有一个 web 服务,我们创建一个Service指向它。

[root@master ~]# curl -s 10.0.0.100 | head -n4
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <title>Welcome to CentOS</title>

[root@master ~]# vim ext-service.yaml 
apiVersion: v1
kind: Service
metadata:
  name: ext-service
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80

[root@master ~]# kubectl apply -f ext-service.yaml 
service/ext-service created

[root@master ~]# kubectl get svc ext-service 
NAME          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
ext-service   ClusterIP   10.1.179.200   <none>        80/TCP    8s

[root@master ~]# kubectl describe svc ext-service | grep -i endpoint
Endpoints:         <none>

  由于此 Service 没有选择器,无法选择后端 Pod,因此是不会自动创建相应的 Endpoint 记录的。因此需要手动创建一个和该 Service 同名的 Endpoint ,用于指向实际的后端访问地址。

[root@master ~]# vim ext-service-endpoint.yaml 
apiVersion: v1
kind: Endpoints
metadata:
  name: ext-service
subsets:
- addresses:
  - ip: 10.0.0.100
  ports:
  - port: 80

[root@master ~]# kubectl apply -f ext-service-endpoint.yaml
endpoints/ext-service created

[root@master ~]# kubectl get endpoints ext-service 
NAME          ENDPOINTS       AGE
ext-service   10.0.0.100:80   34s

[root@master ~]# kubectl describe svc ext-service | grep -i endpoint
Endpoints:         10.0.0.100:80

[root@master ~]# curl -s 10.1.179.200 | head -n4
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
  <title>Welcome to CentOS</title>

  注:端点地址不能是环回(127.0.0.0/8)、私有保留地址(169.254.0.0/16 )、组播地址(224.0.0.0/24)和其它 Kubernetes 服务的集群IP。

六、Headless Service

 Headless Service(无头服务),即不为 Service 设置 ClusterIP 入口地址 ,仅通 Label Selector 将后端的 Pod 列表返回给调用的客户端。对于 headless Services,kube-proxy 是不处理这些服务,也不会为它们做负载均衡或代理。
 例如以下场景:

• 不使用 Service 提供的默认负载均衡的功能,想自己控制负载均衡策略
• 不通过 Service 转发,想直接访问 Pod 等场景。
•  配合其它服务发现,而不受 Kubernetes 中实现的束缚。

对于 Headless Services,DNS 的自动配置方式取决于 Service 是否定义了选择器:

•  有选择器:对于定义选择器的 Headless Service,端点控制器在 API 中创建 Endpoints 记录,并修改 DNS 配置以返回直接指向支持 Service 的 POD 的记录(IP地址)。
•  无选择器:对于未定义选择器的 Headless Service,端点控制器不会创建 Endpoints 记录。但是,DNS 系统会查找并配置 ExternalName 类型服务的 CNAME 记录和A记录与服务共享名称的所有其他类型的端点。

例如,下面示例:

[root@master ~]# vim nginx-svc-headless.yaml
apiVersion: v1 
kind: Service 
metadata: 
  name: nginx-svc-headless
  labels: 
    app: nginx
spec:
  selector:
    app: nginx
  ports:
  - port: 80
  clusterIP: None

[root@master ~]# kubectl apply -f nginx-svc-headless.yaml 
service/nginx-svc-headless created

[root@master ~]# kubectl get svc nginx-svc-headless 
NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
nginx-svc-headless   ClusterIP   None         <none>        80/TCP    16s

[root@master ~]# kubectl describe service nginx-svc-headless | grep Endpoints
Endpoints:         10.244.2.120:80,10.244.2.121:80,10.244.2.122:80

# Service DNS 命名一般是为:<服务名>.<命名空间>.svc.cluster.local
# 下面通过 DNS 查询一下
[root@master ~]# kubectl run test -it --rm --image=busybox
If you don't see a command prompt, try pressing enter.
/ # nslookup nginx-svc-haeadless.default.svc.cluster.local
Server:     10.1.0.10
Address:    10.1.0.10:53

Name:   nginx-svc-haeadless.default.svc.cluster.local
Address: 10.244.2.121
Name:   nginx-svc-haeadless.default.svc.cluster.local
Address: 10.244.2.122
Name:   nginx-svc-haeadless.default.svc.cluster.local
Address: 10.244.2.120

  这样 Service 就不再具有一个特定 ClusterIP,当客户端对其进行访问将获得包含 Label "app=nginx" 的全部 Pod 。
  像 StatefulSet 就是使用 Headless Service 来指定具体的 pod。


参考文章:

https://kubernetes.io/docs/
《Kubernetes 权威指南》

发表回复

验证码: 9 + 1 =