Service对象

Service对象用来描述负载均衡对象,通过selector定义基于Pod标签的过滤规则,从而选择服务的上游应用实例。

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

Endpoint对象

service和pod是多对多的关系,一个service可以对应多个pod,一个pod也可以属于多个service。 在数据库中,多对多通常使用一个中间表去关联,k8s的endpoint用来关联service和pod的多对多关系。

kube-proxy会watch service和endpoint对象,然后调用主机接口,配置主机的网络负载均衡。相当于在每个node上都运行了LB。 endpoint的信息会被推送到每个node。

当Service的selector不为空时,k8s endpoint的控制器会创建与service同名的endpoint对象。selector能够选取的所有PodIP都会被配置到addresses属性中

  • 如果selector查询不到相应的pod,则addresses列表为空
  • 默认配置下,如果pod为not ready状态,则podIP只会出现在subsets的notReadyAddresses属性中,不能作为流量转发目标
    • 只有当pod时readiness时,其ip才会出现在endpoint的readyAddresses列表中

EndpointSlice对象

当某个Service对应的backend Pod较多时,endpoint对象就会保存的地址信息过多变得庞大

pod状态的变更会引起endpoint的变更,endpoint的变更会推送到所有节点(每个node上都要配置转发策略),从而导致持续占用大量网络带宽。

EndpointSlice对象用于对Pod较多的Endpoint进行切片,切片大小可以自定义。通过使用EndpointSlice,推送数据量减小了,只需要推送某个EndpointSlice对应的部分。

不定义selector的Service

  • 不会自动创建Endpoint
  • 用户可以手动创建同名Endpoint,并设置任意IP到address属性,可以为集群外的目标IP
  • 访问该服务的请求会被转发到指定目标
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: v1
kind: Service
metadata:
  name: service-without-selector
spec:
  ports:
    - port: 80
      protocol: TCP
      name: http
------
apiVersion: v1
kind: Endpoints
metadata:
  name: service-without-selector
subsets:
  - addresses:
      - ip: 220.181.38.148
    ports:
      - name: http
        port: 80
        protocol: TCP

Service的类型

  • clusterIP Service的默认类型,服务被发布到一个虚拟IP,这个虚拟IP仅集群内的Node或Pod上可见
    • APIServer启动时,通过service-cluster-ip-range参数配置clusterIP地址段
  • nodePort 在Node上分配的一个高端口,外部服务可以通过 nodeIP:nodePort 访问
    • APIServer启动时,通过node-port-range参数配置nodePort的范围,默认2k个,制约了集群规模
    • 每个节点的kube-proxy会尝试在服务分配的nodePort上监听请求并转发给后端Pod实例
  • LoadBalancer 需要厂商的LB配合,不同厂商提供自己的LB provider
  • Headless Service 将clusterIP显式指定为None,不会分配clusterIP和nodePort。
  • ExternalNamespace 为服务创建一个别名

前三种类型不是“或”的关系,也不是三选一

  • 如果是clusterIP类型,那么就只有一个clusterIP
  • 如果是nodePort类型,会同时有clusterIP和nodePort
  • 如果是LoadBalancer,会同时有LB的VIP,内网clusterIP和nodePort。

service是一个静态的对象,只要创建出来不删除,clusterIP/nodePort就固定不变了,相对PodIP稳定。 Pod是和node关联的,pod调度到某个node后,才会分配IP,Pod漂移后IP还会变化。

 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:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80 # 这个port应该没有意义
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 30007 # 可以指定,也可以分配

Headless Service

使用场景,和带有hostname+subdomain的Pod配合,为Pod提供唯一DNS域名。如用于StatefulSet每个Pod的DNS解析。

Headless Service的spec中指定clusterIP为None,APIServer不会为其分配clusterIP。 CoreDNS会为Headless Service创建A记录,格式为 $svcname.$namespace.svc.$clusterdomain,解析结果为包含每个就绪的Pod IP的列表。 如果是非Headless Service,CoreDNS同样会创建该域名格式的记录,解析的结果就是clusterIP。

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

Pod的域名&hostname&subdomain

一般,Pod的IP会对应如下DNS解析 $pod-ip.$namespace.pod.$clusterdomain,如172-17-0-3.default.pod.cluster.local

通过Service暴露出来的所有就绪Pod都会有DNS解析名称 $pod-ip.$svcname.$namespace.svc.$clusterdomain

默认情况,创建 Pod 时其主机名取⾃ Pod 的 metadata.name 值。 Pod 规约中包含⼀个可选的 hostname 字段,可以⽤来指定 Pod 的主机名。 当这个字段被设置时,它将优先于 Pod 的名字成为该 Pod 的主机名。

Pod 规约还有⼀个可选的 subdomain 字段,可以⽤来指定 Pod 的⼦域名。

使⽤HeadlessService,如果指定Pod的hostname及subdomain,并且subdomain与HeadlessService name相同,则CoreDNS为Pod分配格式为 $hostname.$subdomain.$namespace.svc.$clusterdomain 的A记录解析到Pod,替代pod默认的$pod-ip开头的域名。 如果Pod发⽣漂移,解析结果⾃动更新。 如果Pod状态为⾮就绪,对应的域名解析也会失效。

StatefulSet&Headless Service

StatefulSet⾥必须使⽤⼀个Headless Service, StatefulSet会把每⼀个Pod的hostName和subDomain都填上,每个Pod都有⼀个独⽴的域名。 StatefulSet的实例名字是XX-0/1/2的固定格式,每个副本的fqdn也就唯⼀确定了,就可以使⽤域名访问对应的 pod。

ExternalName

⽤来引⽤⼀个已存在的域名,CoreDNS会为该service创建⼀个CName的记录指向该域名。

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

Service Topology

网络调用的延迟受客户端和服务端所处位置的影响,k8s提供通用标签来标记节点所处的物理位置。

Service引入了topologyKeys属性,来控制流量走向。

  • [“kubernetes.io/hostname”] 调用同一个Node上的服务,如果不存在则失败
  • [“kubenetes.io/hostname”, “topology.kubenetes.io/zone”, “topology.kubenetes.io/region”] 优先调用同节点服务,不存在时再查找所在zone/region。
  • [“kubenetes.io/zone”,"*"] 优先调用当前zone内的服务,不存在则发送至任意实例。

kube-proxy

每台机器上都有一个kube-proxy服务,它监听APIServer中Service和Endpoint的变化,并通过iptables/ipvs在所有节点上配置负载均衡。

kube-proxy可以直接运行在物理机,也可以通过static pod或DaemonSet的方式运行。目前支持:

  • iptables 通过iptables规则实现service负载均衡,四表五链,在刷规则和转发包时都有性能瓶颈。
    • 通过随机转发概率的设置,实现负载均衡
    • 主要问题是服务多的时候生大量iptables规则,而且不支持增量更新,本质还是文件修改,有性能问题
    • 转发时TCP首包需要从上到下匹配
  • ipvs 支持增量更新,保证service更新期间连接不断开
    • 专用负载均衡组件,性能好

iptables和ipvs都是内核态实现。基于eBPF的cilium完全绕过内核netfilter框架去处理数据包,使用cilium插件可以不用启动kube-proxy。但是实现原理类似。

很多技术都在解决数据包在内核态和⽤户态之间多次流转的性能问题

  • 直接在⽹卡上做转发:SDP技术
  • 如果⽹卡不⽀持,在内核中不⽤netfilter框架:eBPF技术
  • ⽤户态直接去⽹卡取数据,在⽤户态处理:DPDK技术

集群外部访问

客户端可以直接访问nodeIP:nodePort来访问Pod,问题是

  • 如果客户端直接使用nodePort方式访问,将哪些node节点IP提供给客户端呢;
  • nodeIP变了怎么办;
  • 而且NodePort一般是高端口,高端口有可能会被墙拦住怎么办?

通过外置LB: LB上的VIP相当于公网IP,而Pod的IP一般是局域网IP,不在一个IP段,LB是怎么转发到对应Pod的呢?

在node节点外部使用 LB,客户端请求LB转发到后端的nodeIP:nodePort上,再由某个node节点查询iptables规则进行转发;

  • 这些LB需要实现自己的控制器,用来获取nodePort等信息。
  • 这种方案,在LB上会跳转一次,在node上也会跳转一次,链路较长;负载也不一定真的平衡。

CoreDNS

CoreDNS包含一个内存态DNS,以及控制器。控制器监听Service和Endpoint的变化并配置DNS,客户端Pod在进行域名解析时,从CoreDNS中查询服务对应的地址记录。

前面已经提到过,不同的service及pod有不同类型的dns记录。

Pod有一个与DNS策略相关的属性dnsPolicy,取值为:

  • ClusterFirst,默认值,使用CoreDNS解析
  • Default,与主机一致
  • None,自定义

Pod启动后/etc/resolv.conf会被改写,默认解析优先使用CoreDNS

1
2
3
4
5
6
$ cat /etc/resolv.conf
serarch ns1.svc.cluster.local svc.cluster.local cluster.local # 与options ndots 相关
nameserver 192.168.0.10 # CoreDNS地址,CoreDNS还有上级DNS,级联解析CoreDNS解析不了的域名
options ndots 4 # 表示对多支持4段域名的前缀短名搜索,分别追加上search中的域名后缀尝试进行DNS解析
# 比如需要解析的是 svcName.nsName.svc这个短名,DNS会自动追加上cluster.local完成解析;
# 类似的,只用svcName也可以,会追加上ns1.svc.cluster.local。只要有任一个组匹配,就可以解析成功。

DNS本身不是为负载均衡需求存在的。

  • 虽然可以把Service发布为headless方式,就绪Pod挂在svc域名的A记录下,但是DNS是有TTL的,不适合做LB。
  • 如果TTL设成0,对DNS服务压力大;客户端可以忽略DNS的TTL自己进行缓存

Ingress

k8s中的负载均衡技术:

  • 基于L4的Service
    • 基于iptabels/ipvs的分布式四层负载均衡,由kernel处理,只能到4层
    • 可以与企业现有的ELB整合
    • 属于k8s core
  • 基于L7的Ingress
    • 基于应用层,可以实现更多功能,L7 path forwarding, URL/http header write
    • 与7层软件相关

为什么要有Ingress

  • 以NodePort类型的Service为例,服务数量增多后,NodePort在每个节点上开放的端⼝数量巨⼤,难以维护。
  • ⼀个想法就是,通过使⽤nginx对外开放⼀个公共端⼝,然后根据域名等向后⾯的若⼲Service转发,Service⾃⼰内部再负载均衡。
  • 但是这样每次服务上下线,都要⼿动修改nginx修改配置。 因此就引⼊了Ingress和Ingress Controller,由Ingress对象定义7层的转发规则,由Ingress Controller监听并修改7层反向代理nginx的配置。

Ingress controller负责监听Service和Ingress对象,配置反向代理软件的规则。

Ingress Nginx解决方案由如下模块组成:

  • Ingress控制器(Nginx、选主、控制器逻辑),需要提前部署,否则创建Ingress对象没有任何效果
  • 业务定义的Ingress对象
  • 后端Service和POD Endpoints

Ingress 控制器监听相关资源事件,将之前service->pod的数据流拦截,改为 nodeport service->nginx->pod。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gateway
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  tls:
    - hosts:
        - k8s.io
      secretName: k8s-tls
  rules:
    - host: k8s.io
      http:
        paths:
          - path: "/"
            pathType: Prefix
            backend:
              service:
                name: nginx
                port:
                  number: 80

关于nginx是否reload:

  • 如果只是Endpoint地址之类的变化,通过lua就可以实现,不需要reload
  • 如果是新建ingress,修改path等需要修改nginx配置文件,就需要reload

流量路径:

  • external ELB承载DNS域名并且进⾏HTTPS卸载
  • Kong⽤于根据域名或者路径进⾏路由后端选择或者实施访问限制,Kong的路由⽬的可能是部署在ECS上的服务,也有可能是部署在Kubernetes上的服务
  • Kubernetes集群部署了内部和外部两个ELB,分别对应内部和外部服务,外部ELB接受Kong转过来的外部请求,内部ELB接受来⾃内部VM或者POD的请求
  • POD如果请求部署在Kubernetes的服务,可以选择使⽤内部域名,也可以选择使⽤Kubernetes的cluster service;但是对于某些使⽤gRPC协议的服务,使⽤ingress nginx或 者cluster service都⽆法进⾏很好的负载均衡,需要⾃⾏制定服务发现和负载均衡
  • 所有外部的请求域名类似于xxx.test.com,⽽内部请求的域名则类似于internal-xxx.test.com
  • Kubernetes的ELB挂载nodes,这些节点都部署了ingress nginx daemonset,⽤于接⼊对于集群中服务的访问请求(ingress controller创建了一个nodeport类型的service)
  • ELB -> nodeIP:nodePort转发到某个节点上的nginx,然后再由nginx直接转发到Pod(这里不经过service)
  • 使⽤ingress或者cluster service⽅式访问POD中的服务,不可避免存在跨worker节点的traffic relay

Ingress不能实现所有需求,如根据header决定转发路径,header改写,url改写等。