1 Service 详解
1.1 Service 介绍
在 kubernetes 中,pod 是应用程序的载体,我们可以通过 pod 的 ip 来访问应用程序,但是 pod 的 ip 地址不是固定的,这也就意味着不方便直接采用 pod 的 ip 对服务进行访问。
为了解决这个问题,kubernetes 提供了 Service 资源,Service 会对提供同一个服务的多个 pod 进行聚合,并且提供一个统一的入口地址。通过访问 Service 的入口地址就能访问到后面的 pod 服务。

Service 在很多情况下只是一个概念,真正起作用的其实是 kube-proxy 服务进程,每个 Node 节点上都运行着一个 kube-proxy 服务进程。当创建 Service 的时候会通过 api-server 向 etcd 写入创建的 service 的信息,而 kube-proxy 会基于监听的机制发现这种 Service 的变动,然后它会将最新的 Service 信息转换成对应的访问规则。

# 10.97.97.97:80 是 service 提供的访问入口
# 当访问这个入口的时候,可以发现后面有三个 pod 的服务在等待调用,
# kube-proxy 会基于 rr(轮询)的策略,将请求分发到其中一个 pod 上去
# 这个规则会同时在集群内的所有节点上都生成,所以在任何一个节点,访问都可以。
[ root@node1 ~]# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.97.97.97:80 rr
-> 10.244.1.39:80 Masq 1 0 0
-> 10.244.1.40:80 Masq 1 0 0
-> 10.244.2.33:80 Masq 1 0 0
Service 服务的 IP 地址
- Service 创建后的整个生命周期中,Service 的 IP 是不会变的。
- 节点初始化的时候,可以规定 Service 的 IP 范围,例如下面规定 service 都在 10.96.0.0/16 子网内。

访问 Service 服务的方式(ClusterIP vs NodePort) - 默认创建并暴露端口时,service 的 type 类型是 ClusterIP。
- 当 service 的类型为 ClusterIP 时,该 service 只能在 K8S 集群内部进行访问。
- 当 service 的类型为 NodePort 时,该 service 可以在外网进行访问。
- NodePort 随机在 30000-32767 之间暴露端口
- 例如,当前更改为 NodePort 后,开放了 30948 端口供外网访问。

- 开放了 30948 端口也是为三个机器均开了对应的端口供访问,如果有公网 IP,可以直接
公网 IP:30948访问。

1.2 Service 基础命令
# 暴露 Deploy(默认 ClusterIP)
kubectl expose deployment my-dep --port=8000 --target-port=80
# 暴露 Deploy(ClusterIP 类型)
kubectl expose deployment my-dep --port=8000 --target-port=80 --type=ClusterIP
# 暴露 Deploy(NodePort 类型)
kubectl expose deployment my-dep --port=8000 --target-port=80 --type=NodePort
# 查看创建的 service
kubectl get services
kubectl get svc
# 删除 service
kubectl delete svc my-dep -n dev
# 基于 yaml 文件创建和删除
kubectl create -f svc-nginx.yaml
kubectl delete -f svc-nginx.yaml
# 查看标签
kubectl get pod --show-labels
# 使用标签检索Pod
kubectl get pod -l app=my-dep
# 集群内访问内部 service
# IP:端口号 访问 service
curl service_ip:8000
# 服务名.所在命名空间.svc 访问 service
curl my-dep.default.svc:80001.3 kube-proxy 工作模式
1.3.1 userspace 模式
userspace 模式下,kube-proxy 会为每一个 Service 创建一个监听端口,发向 Cluster IP 的请求被 Iptables 规则重定向到 kube-proxy 监听的端口上,kube-proxy 根据 LB 算法 (round-robin) 选择一个提供服务的 Pod 并和其建立链接,以将请求转发到 Pod 上。该模式下,kube-proxy 充当了一个四层负责均衡器的角色。由于 kube-proxy 运行在 userspace 中,在进行转发处理时会增加内核和用户空间之间的数据拷贝,虽然比较稳定,但是效率比较低。

1.3.2 iptables 模式
iptables 模式下,kube-proxy 为 service 后端的每个 Pod 创建对应的 iptables 规则,直接将发向 Cluster IP 的请求重定向到一个 Pod IP。该模式下 kube-proxy 不承担四层负责均衡器的角色,只负责创建 iptables 规则。该模式的优点是较 userspace 模式效率更高,但不能提供灵活的 LB 策略,当后端 Pod 不可用时也无法进行重试。

1.3.3 ipvs 模式
ipvs 模式和 iptables 类似,kube-proxy 监控 Pod 的变化并创建相应的 ipvs 规则。ipvs 相对 iptables 转发效率更高。除此以外,ipvs 支持更多的 LB 算法。
与 iptables 类似,ipvs 基于 netfilter 的 hook 功能,但使用哈希表作为底层数据结构并在内核空间中工作。这意味着 ipvs 可以更快地重定向流量,并且在同步代理规则时具有更好的性能。此外,ipvs 为负载均衡算法提供了更多选项,例如:
rr:轮询调度lc:最小连接数dh:目标哈希sh:源哈希sed:最短期望延迟nq: 不排队调度
注意: ipvs 模式假定在运行 kube-proxy 之前在节点上都已经安装了 IPVS 内核模块。当 kube-proxy 以 ipvs 代理模式启动时,kube-proxy 将验证节点上是否安装了 IPVS 模块,如果未安装,则 kube-proxy 将回退到 iptables 代理模式。

# 此模式必须安装 ipvs 内核模块,否则会降级为 iptables
# 开启 ipvs
[ root@k8s-master01 ~]# kubectl edit cm kube-proxy -n kube-system
# 修改 mode: "ipvs"
[ root@k8s-master01 ~]# kubectl delete pod -l k8s-app=kube-proxy -n kube-system
[ root@node1 ~]# ipvsadm -Ln
IP Virtual Server version 1.2.1 (size=4096)
Prot LocalAddress:Port Scheduler Flags
-> RemoteAddress:Port Forward Weight ActiveConn InActConn
TCP 10.97.97.97:80 rr
-> 10.244.1.39:80 Masq 1 0 0
-> 10.244.1.40:80 Masq 1 0 0
-> 10.244.2.33:80 Masq 1 0 01.4 Service 类型
Service 的资源清单文件:
kind: Service # 必要,资源类型
apiVersion: v1 # 必要,资源版本
metadata: # 必要,元数据
name: service # 必要,资源名称
namespace: dev # 命名空间
spec: # 描述
selector: # 必要,标签选择器,用于确定当前 service 代理哪些 pod
app: nginx
type: # Service 类型,指定 service 的访问方式
clusterIP: # 虚拟服务的 ip 地址
sessionAffinity: # session 亲和性,支持 ClientIP、None 两个选项
ports: # 必要,端口信息
- protocol: TCP # 支持 TCP、UDP,默认 TCP
port: 3017 # service 端口
targetPort: 5003 # pod 端口
nodePort: 31122 # 主机端口1.4.1 Service 的四种类型
- ClusterIP:默认值,它是 Kubernetes 系统自动分配的虚拟 IP,只能在集群内部访问
- NodePort:将 Service 通过指定的 Node 上的端口暴露给外部,通过此方法,就可以在集群外部访问服务
- LoadBalancer:使用外接负载均衡器完成到服务的负载分发,注意此模式需要外部云环境支持
- ExternalName: 把集群外部的服务引入集群内部,直接使用
1.4.2 ExternalName 类型的 Service
ExternalName Service 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。相反地,对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。
当查询主机 my-service.prod.svc.CLUSTER 时,集群的 DNS 服务将返回一个值为 my.database.example.com 的 CNAME 记录。访问这个服务的工作方式与其它的相同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改 Service 的 type。
kind: Service
apiVersion: v1
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com1.4.3 没有 selector 的 Service
Service 抽象了该如何访问 Kubernetes Pod,但也能够抽象其它类型的 backend,例如:
- 希望在生产环境中使用外部的数据库集群,但测试环境使用自己的数据库。
- 希望服务指向另一个
Namespace中或其它集群中的服务。 - 正在将工作负载转移到 Kubernetes 集群,和运行在 Kubernetes 集群之外的 backend。
在任何这些场景中,都能够定义没有 selector 的 Service :
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
ports:
- protocol: TCP
port: 80
targetPort: 9376由于这个 Service 没有 selector,就不会创建相关的 Endpoints 对象。可以手动将 Service 映射到指定的 Endpoints:
kind: Endpoints
apiVersion: v1
metadata:
name: my-service
subsets:
- addresses:
- ip: 1.2.3.4
ports:
- port: 9376注意:Endpoint IP 地址不能是 loopback(127.0.0.0/8)、link-local(169.254.0.0/16)、或者 link-local 多播(224.0.0.0/24)。
访问没有 selector 的 Service,与有 selector 的 Service 的原理相同。请求将被路由到用户定义的 Endpoint(该示例中为 1.2.3.4:9376)。
1.5 Service 使用
1.5.1 实验环境准备
在使用 service 之前,首先利用 Deployment 创建出 3 个 pod,注意要为 pod 设置 app=nginx-pod 的标签
创建 deployment.yaml,内容如下:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pc-deployment
namespace: dev
spec:
replicas: 3
selector:
matchLabels:
app: nginx-pod
template:
metadata:
labels:
app: nginx-pod
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80[ root@k8s-master01 ~]# kubectl create -f deployment.yaml
deployment.apps/pc-deployment created
# 查看 pod 详情
[ root@k8s-master01 ~]# kubectl get pods -n dev -o wide --show-labels
NAME READY STATUS IP NODE LABELS
pc-deployment-66cb59b984-8p84h 1/1 Running 10.244.1.39 node1 app=nginx-pod
pc-deployment-66cb59b984-vx8vx 1/1 Running 10.244.2.33 node2 app=nginx-pod
pc-deployment-66cb59b984-wnncx 1/1 Running 10.244.1.40 node1 app=nginx-pod
# 为了方便后面的测试,修改下三台 nginx 的 index.html 页面(三台修改的 IP 地址不一致)
# kubectl exec -it pc-deployment-66cb59b984-8p84h -n dev /bin/sh
# echo "10.244.1.39" > /usr/share/nginx/html/index.html
#修改完毕之后 ,访问测试
[ root@k8s-master01 ~]# curl 10.244.1.39
10.244.1.39
[ root@k8s-master01 ~]# curl 10.244.2.33
10.244.2.33
[ root@k8s-master01 ~]# curl 10.244.1.40
10.244.1.401.5.2 ClusterIP 类型的 Service
创建 service-clusterip.yaml 文件
apiVersion: v1
kind: Service
metadata:
name: service-clusterip
namespace: dev
spec:
selector:
app: nginx-pod
clusterIP: 10.97.97.97 # service 的 ip 地址,如果不写,默认会生成一个
type: ClusterIP
ports:
- port: 80 # Service 端口
targetPort: 80 # pod 端口# 创建 service
[ root@k8s-master01 ~]# kubectl create -f service-clusterip.yaml
service/service-clusterip created
# 查看 service
[ root@k8s-master01 ~]# kubectl get svc -n dev -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service-clusterip ClusterIP 10.97.97.97 <none> 80/TCP 13s app=nginx-pod
# 查看 service 的详细信息
# 在这里有一个 Endpoints 列表,里面就是当前 service 可以负载到的服务入口
[ root@k8s-master01 ~]# kubectl describe svc service-clusterip -n dev
Name: service-clusterip
Namespace: dev
Labels: <none>
Annotations: <none>
Selector: app=nginx-pod
Type: ClusterIP
IP: 10.97.97.97
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.39:80,10.244.1.40:80,10.244.2.33:80
Session Affinity: None
Events: <none>
# 查看 ipvs 的映射规则
[ root@k8s-master01 ~]# ipvsadm -Ln
TCP 10.97.97.97:80 rr
-> 10.244.1.39:80 Masq 1 0 0
-> 10.244.1.40:80 Masq 1 0 0
-> 10.244.2.33:80 Masq 1 0 0
# 访问10.97.97.97:80观察效果
[ root@k8s-master01 ~]# curl 10.97.97.97:80
10.244.2.331.5.3 Endpoint
Endpoint 是 kubernetes 中的一个资源对象,存储在 etcd 中,用来记录一个 service 对应的所有 pod 的访问地址,它是根据 service 配置文件中 selector 描述产生的。
一个 Service 由一组 Pod 组成,这些 Pod 通过 Endpoints 暴露出来,Endpoints 是实现实际服务的端点集合。换句话说,service 和 pod 之间的联系是通过 endpoints 实现的。

负载分发策略
对 Service 的访问被分发到了后端的 Pod 上去,目前 kubernetes 提供了两种负载分发策略:
-
如果不定义,默认使用 kube-proxy 的策略,比如随机、轮询
-
基于客户端地址的会话保持模式,即来自同一个客户端发起的所有请求都会转发到固定的一个 Pod 上
此模式可以使在 spec 中添加
sessionAffinity:ClientIP选项
# 查看 ipvs 的映射规则【rr 轮询】
[ root@k8s-master01 ~]# ipvsadm -Ln
TCP 10.97.97.97:80 rr
-> 10.244.1.39:80 Masq 1 0 0
-> 10.244.1.40:80 Masq 1 0 0
-> 10.244.2.33:80 Masq 1 0 0
# 循环访问测试
[ root@k8s-master01 ~]# while true;do curl 10.97.97.97:80; sleep 5; done;
10.244.1.40
10.244.1.39
10.244.2.33
10.244.1.40
10.244.1.39
10.244.2.33
# 修改分发策略----sessionAffinity:ClientIP
# 查看 ipvs 规则【persistent 代表持久】
[ root@k8s-master01 ~]# ipvsadm -Ln
TCP 10.97.97.97:80 rr persistent 10800
-> 10.244.1.39:80 Masq 1 0 0
-> 10.244.1.40:80 Masq 1 0 0
-> 10.244.2.33:80 Masq 1 0 0
# 循环访问测试
[ root@k8s-master01 ~]# while true;do curl 10.97.97.97; sleep 5; done;
10.244.2.33
10.244.2.33
10.244.2.33
# 删除 service
[ root@k8s-master01 ~]# kubectl delete -f service-clusterip.yaml
service "service-clusterip" deleted1.5.4 HeadLiness 类型的 Service
在某些场景中,开发人员可能不想使用 Service 提供的负载均衡功能,而希望自己来控制负载均衡策略,针对这种情况,kubernetes 提供了 HeadLiness Service,这类 Service 不会分配 Cluster IP,如果想要访问 service,只能通过 service 的域名进行查询。
创建 service-headliness.yaml
apiVersion: v1
kind: Service
metadata:
name: service-headliness
namespace: dev
spec:
selector:
app: nginx-pod
clusterIP: None # 将 clusterIP 设置为 None,即可创建 headliness Service
type: ClusterIP
ports:
- port: 80
targetPort: 80# 创建 service
[ root@k8s-master01 ~]# kubectl create -f service-headliness.yaml
service/service-headliness created
# 获取 service,发现 CLUSTER-IP 未分配
[ root@k8s-master01 ~]# kubectl get svc service-headliness -n dev -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
service-headliness ClusterIP None <none> 80/TCP 11s app=nginx-pod
# 查看 service 详情
[ root@k8s-master01 ~]# kubectl describe svc service-headliness -n dev
Name: service-headliness
Namespace: dev
Labels: <none>
Annotations: <none>
Selector: app=nginx-pod
Type: ClusterIP
IP: None
Port: <unset> 80/TCP
TargetPort: 80/TCP
Endpoints: 10.244.1.39:80,10.244.1.40:80,10.244.2.33:80
Session Affinity: None
Events: <none>
# 查看域名的解析情况
[ root@k8s-master01 ~]# kubectl exec -it pc-deployment-66cb59b984-8p84h -n dev /bin/sh
/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search dev.svc.cluster.local svc.cluster.local cluster.local
[ root@k8s-master01 ~]# dig @10.96.0.10 service-headliness.dev.svc.cluster.local
service-headliness.dev.svc.cluster.local. 30 IN A 10.244.1.40
service-headliness.dev.svc.cluster.local. 30 IN A 10.244.1.39
service-headliness.dev.svc.cluster.local. 30 IN A 10.244.2.331.5.5 NodePort 类型的 Service
在之前的样例中,创建的 Service 的 ip 地址只有集群内部才可以访问,如果希望将 Service 暴露给集群外部使用,那么就要使用到另外一种类型的 Service,称为 NodePort 类型。NodePort 的工作原理其实就是将 service 的端口映射到 Node 的一个端口上,然后就可以通过 NodeIp:NodePort 来访问 service 了。
Service 将能够通过 <NodeIP>:spec.ports[*].nodePort 和 spec.clusterIp:spec.ports[*].port 而对外可见。

创建 service-nodeport.yaml
apiVersion: v1
kind: Service
metadata:
name: service-nodeport
namespace: dev
spec:
selector:
app: nginx-pod
type: NodePort # service 类型
ports:
- port: 80
nodePort: 30002 # 指定绑定的 node 的端口(默认的取值范围是:30000-32767), 如果不指定,会默认分配
targetPort: 80# 创建 service
[ root@k8s-master01 ~]# kubectl create -f service-nodeport.yaml
service/service-nodeport created
# 查看 service
[ root@k8s-master01 ~]# kubectl get svc -n dev -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) SELECTOR
service-nodeport NodePort 10.105.64.191 <none> 80:30002/TCP app=nginx-pod
# 接下来可以通过电脑主机的浏览器去访问集群中任意一个 nodeip 的 30002 端口,即可访问到 pod1.5.6 LoadBalancer 类型的 Service
LoadBalancer 和 NodePort 很相似,目的都是向外部暴露一个端口,区别在于 LoadBalancer 会在集群的外部再来做一个负载均衡设备,而这个设备需要外部环境支持的,外部服务发送到这个设备上的请求,会被设备负载之后转发到集群中。

负载均衡器是异步创建的,关于被提供的负载均衡器的信息将会通过 Service 的 status.loadBalancer 字段被发布出去。
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
nodePort: 30061
clusterIP: 10.0.171.239
loadBalancerIP: 78.11.24.19
type: LoadBalancer
status:
loadBalancer:
ingress:
- ip: 146.148.47.155来自外部负载均衡器的流量将直接打到 backend Pod 上,不过实际它们是如何工作的,这要依赖于云提供商。在这些情况下,将根据用户设置的 loadBalancerIP 来创建负载均衡器。
某些云提供商允许设置 loadBalancerIP。如果没有设置 loadBalancerIP,将会给负载均衡器指派一个临时 IP。
如果设置了 loadBalancerIP,但云提供商并不支持这种特性,那么设置的 loadBalancerIP 值将会被忽略掉。
1.5.7 ExternalName 类型的 Service
ExternalName 类型的 Service 用于引入集群外部的服务,它通过 externalName 属性指定外部一个服务的地址,然后在集群内部访问此 service 就可以访问到外部的服务了。

apiVersion: v1
kind: Service
metadata:
name: service-externalname
namespace: dev
spec:
type: ExternalName # service 类型
externalName: www.baidu.com #改成ip地址也可以# 创建 service
[ root@k8s-master01 ~]# kubectl create -f service-externalname.yaml
service/service-externalname created
# 域名解析
[ root@k8s-master01 ~]# dig @10.96.0.10 service-externalname.dev.svc.cluster.local
service-externalname.dev.svc.cluster.local. 30 IN CNAME www.baidu.com.
www.baidu.com. 30 IN CNAME www.a.shifen.com.
www.a.shifen.com. 30 IN A 39.156.66.18
www.a.shifen.com. 30 IN A 39.156.66.14注意:ExternalName 接受 IPv4 地址字符串,但它是由数字组成的 DNS 名称,而不是 IP 地址。类似于 IPv4 地址的 ExternalNames 不能被 CoreDNS 或 ingress-nginx 解析,因为 ExternalName 的目的是指定一个规范的 DNS 名称。要硬编码一个 IP 地址,请考虑使用 Headless Service。
对于一些常见的协议,包括 HTTP 和 HTTPS,你使用 ExternalName 可能会有问题。如果使用 ExternalName,那么你集群内的客户端使用的主机名与 ExternalName 引用的名称不同。
对于使用主机名的协议,这种差异可能导致错误或意外的响应。HTTP 请求将有一个源服务器不承认的 Host: header,TLS 服务器将不能提供与客户端连接的主机名相匹配的证书。
1.6 多端口 Service
很多 Service 需要暴露多个端口。对于这种情况,Kubernetes 支持在 Service 对象中定义多个端口。当使用多个端口时,必须给出所有的端口的名称,这样 Endpoint 就不会产生歧义,例如:
kind: Service
apiVersion: v1
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
- name: https
protocol: TCP
port: 443
targetPort: 93771.7 选择自己的 IP 地址
在 Service 创建的请求中,可以通过设置 spec.clusterIP 字段来指定自己的集群 IP 地址。比如,希望替换一个已经存在的 DNS 条目,或者遗留系统已经配置了一个固定的 IP 且很难重新配置。用户选择的 IP 地址必须合法,并且这个 IP 地址在 service-cluster-ip-range CIDR 范围内,这对 API Server 来说是通过一个标识来指定的。如果 IP 地址不合法,API Server 会返回 HTTP 状态码 422,表示值不合法。
为何不使用 round-robin DNS?
一个不时出现的问题是,为什么我们都使用 VIP 的方式,而不使用标准的 round-robin DNS,有如下几个原因:
- 长久以来,DNS 库都没能认真对待 DNS TTL、缓存域名查询结果
- 很多应用只查询一次 DNS 并缓存了结果。就算应用和库能够正确查询解析,每个客户端反复重解析造成的负载也是非常难以管理的。
1.8 服务发现
Kubernetes 支持 2 种基本的服务发现模式 —— 环境变量和 DNS。
环境变量
当 Pod 运行在 Node 上,kubelet 会为每个活跃的 Service 添加一组环境变量。它同时支持 Docker links 兼容 变量(查看 makeLinkVariables)、简单的 {SVCNAME}_SERVICE_HOST 和 {SVCNAME}_SERVICE_PORT 变量,这里 Service 的名称需大写,横线被转换成下划线。
举个例子,一个名称为 “redis-master” 的 Service 暴露了 TCP 端口 6379,同时给它分配了 Cluster IP 地址 10.0.0.11,这个 Service 生成了如下环境变量:
REDIS_MASTER_SERVICE_HOST=10.0.0.11
REDIS_MASTER_SERVICE_PORT=6379
REDIS_MASTER_PORT=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP=tcp://10.0.0.11:6379
REDIS_MASTER_PORT_6379_TCP_PROTO=tcp
REDIS_MASTER_PORT_6379_TCP_PORT=6379
REDIS_MASTER_PORT_6379_TCP_ADDR=10.0.0.11这意味着需要有顺序的要求 —— Pod 想要访问的任何 Service 必须在 Pod 自己之前被创建,否则这些环境变量就不会被赋值。DNS 并没有这个限制。
DNS
一个可选(尽管强烈推荐)集群插件 是 DNS 服务器。
DNS 服务器监视着创建新 Service 的 Kubernetes API,从而为每一个 Service 创建一组 DNS 记录。如果整个集群的 DNS 一直被启用,那么所有的 Pod 应该能够自动对 Service 进行名称解析。
例如,有一个名称为 "my-service" 的 Service,它在 Kubernetes 集群中名为 "my-ns" 的 Namespace 中,为 "my-service.my-ns" 创建了一条 DNS 记录。
在名称为 "my-ns" 的 Namespace 中的 Pod 应该能够简单地通过名称查询找到 "my-service"。在另一个 Namespace 中的 Pod 必须限定名称为 "my-service.my-ns"。这些名称查询的结果是 Cluster IP。
Kubernetes 也支持对端口名称的 DNS SRV(Service)记录。如果名称为 "my-service.my-ns" 的 Service 有一个名为 "http" 的 TCP 端口,可以对 "_http._tcp.my-service.my-ns" 执行 DNS SRV 查询,得到 "http" 的端口号。
Kubernetes DNS 服务器是唯一的一种能够访问 ExternalName 类型的 Service 的方式。
更多信息可以查看 Service 与 Pod 的 DNS | Kubernetes。
1.9 Headless Service
有时不需要或不想要负载均衡,以及单独的 Service IP。遇到这种情况,可以通过指定 Cluster IP(spec.clusterIP)的值为 "None" 来创建 Headless Service。
这个选项允许开发人员自由寻找他们自己的方式,从而降低与 Kubernetes 系统的耦合性。应用仍然可以使用一种自注册的模式和适配器,对其它需要发现机制的系统能够很容易地基于这个 API 来构建。
对这类 Service 并不会分配 Cluster IP,kube-proxy 不会处理它们,而且平台也不会为它们进行负载均衡和路由。DNS 如何实现自动配置,依赖于 Service 是否定义了 selector。
1.9.1 配置 Selector
对定义了 selector 的 Headless Service,Endpoint 控制器在 API 中创建了 Endpoints 记录,并且修改 DNS 配置返回 A 记录(地址),通过这个地址直接到达 Service 的后端 Pod 上。
1.9.2 不配置 Selector
对没有定义 selector 的 Headless Service,Endpoint 控制器不会创建 Endpoints 记录。然而 DNS 系统会查找和配置,无论是:
ExternalName类型 Service 的 CNAME 记录- 记录:与 Service 共享一个名称的任何
Endpoints,以及所有其它类型
- 记录:与 Service 共享一个名称的任何
1.10 关于虚拟 IP 的细节
Service - 关于虚拟 IP 的细节 | 云原生资料库
2 Ingress
官方地址:Welcome - Ingress-Nginx Controller
高阶功能:Annotations - Ingress-Nginx Controller
- 边界路由器:为集群强制执行防火墙策略的路由器。这可能是由云提供商或物理硬件管理的网关。
- 集群网络:一组逻辑或物理链接,可根据 Kubernetes 网络模型 实现群集内的通信。集群网络的实现包括 Overlay 模型的 flannel 和基于 SDN 的 OVS。
2.1 Ingress 介绍
在前面课程中已经提到,Service 对集群之外暴露服务的主要方式有两种:NotePort 和 LoadBalancer,但是这两种方式,都有一定的缺点:
- NodePort 方式的缺点是会占用很多集群机器的端口,那么当集群服务变多的时候,这个缺点就愈发明显
- LB 方式的缺点是每个 service 需要一个 LB,浪费、麻烦,并且需要 kubernetes 之外设备的支持
基于这种现状,kubernetes 提供了 Ingress 资源对象,Ingress 只需要一个 NodePort 或者一个 LB 就可以满足暴露多个 Service 的需求。工作机制大致如下图表示:

实际上,Ingress 相当于一个 7 层的负载均衡器,是 kubernetes 对反向代理的一个抽象,它的工作原理类似于 Nginx,可以理解成在Ingress 里建立诸多映射规则,Ingress Controller 通过监听这些配置规则并转化成 Nginx 的反向代理配置 , 然后对外部提供服务。在这里有两个核心概念:
- ingress:kubernetes 中的一个对象,作用是定义请求如何转发到 service 的规则
- ingress controller:具体实现反向代理及负载均衡的程序,对 ingress 定义的规则进行解析,根据配置的规则来实现请求转发,实现方式有很多,比如 Nginx, Contour, Haproxy 等等
Ingress(以 Nginx 为例)的工作原理如下:
- 用户编写 Ingress 规则,说明哪个域名对应 kubernetes 集群中的哪个 Service
- Ingress 控制器动态感知 Ingress 服务规则的变化,然后生成一段对应的 Nginx 反向代理配置
- Ingress 控制器会将生成的 Nginx 配置写入到一个运行着的 Nginx 服务中,并动态更新
- 到此为止,其实真正在工作的就是一个 Nginx 了,内部配置了用户定义的请求转发规则

2.2 Ingress 使用
2.2.1 环境准备搭建 ingress 环境
# 创建文件夹
mkdir ingress-controller
cd ingress-controller/
# 获取 ingress-nginx,本次案例使用的是 0.30 版本
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/mandatory.yaml
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/provider/baremetal/service-nodeport.yaml
# 修改 mandatory.yaml 文件中的仓库
# 修改 quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0
# 为 quay-mirror.qiniu.com/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0
# 创建 ingress-nginx
kubectl apply -f ./
# 查看 ingress-nginx
kubectl get pod -n ingress-nginx
NAME READY STATUS RESTARTS AGE
pod/nginx-ingress-controller-fbf967dd5-4qpbp 1/1 Running 0 12h
# 查看 service
kubectl get svc -n ingress-nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
ingress-nginx NodePort 10.98.75.163 <none> 80:32240/TCP,443:31335/TCP 11h
# 更新 Ingress
kubectl edit ing ingress-nginx2.2.2 准备 service 和 pod
为了后面的实验比较方便,创建如下图所示的模型

创建 tomcat-nginx.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: dev
spec:
replicas: 3
selector:
matchLabels:
app: nginx-pod
template:
metadata:
labels:
app: nginx-pod
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: tomcat-deployment
namespace: dev
spec:
replicas: 3
selector:
matchLabels:
app: tomcat-pod
template:
metadata:
labels:
app: tomcat-pod
spec:
containers:
- name: tomcat
image: tomcat:8.5-jre10-slim
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
namespace: dev
spec:
selector:
app: nginx-pod
clusterIP: None
type: ClusterIP
ports:
- port: 80
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: tomcat-service
namespace: dev
spec:
selector:
app: tomcat-pod
clusterIP: None
type: ClusterIP
ports:
- port: 8080
targetPort: 8080# 创建
[ root@k8s-master01 ~]# kubectl create -f tomcat-nginx.yaml
# 查看
[ root@k8s-master01 ~]# kubectl get svc -n dev
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx-service ClusterIP None <none> 80/TCP 48s
tomcat-service ClusterIP None <none> 8080/TCP 48s2.2.3 Http 代理
创建 ingress-http.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-http
namespace: dev
spec:
rules:
- host: nginx.itheima.com
http:
paths:
- path: /
backend:
serviceName: nginx-service
servicePort: 80
- host: tomcat.itheima.com
http:
paths:
- path: /
backend:
serviceName: tomcat-service
servicePort: 8080# 创建
[ root@k8s-master01 ~]# kubectl create -f ingress-http.yaml
ingress.extensions/ingress-http created
# 查看
[ root@k8s-master01 ~]# kubectl get ing ingress-http -n dev
NAME HOSTS ADDRESS PORTS AGE
ingress-http nginx.itheima.com,tomcat.itheima.com 80 22s
# 查看详情
[ root@k8s-master01 ~]# kubectl describe ing ingress-http -n dev
...
Rules:
Host Path Backends
---- ---- --------
nginx.itheima.com / nginx-service:80 (10.244.1.96:80,10.244.1.97:80,10.244.2.112:80)
tomcat.itheima.com / tomcat-service:8080(10.244.1.94:8080,10.244.1.95:8080,10.244.2.111:8080)
...
# 接下来,在本地电脑上配置 host 文件,解析上面的两个域名到 192.168.109.100(master)上
# 然后,就可以分别访问 tomcat.itheima.com:32240 和 nginx.itheima.com:32240 查看效果了2.2.4 Https 代理
目前 ingress 只支持 http 规则,所以创建 https 代理会麻烦些。
创建证书
# 生成证书
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/C=CN/ST=BJ/L=BJ/O=nginx/CN=itheima.com"
# 创建密钥
kubectl create secret tls tls-secret --key tls.key --cert tls.crt创建 ingress-https.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: ingress-https
namespace: dev
spec:
tls:
- hosts:
- nginx.itheima.com
- tomcat.itheima.com
secretName: tls-secret # 指定秘钥
rules:
- host: nginx.itheima.com
http:
paths:
- path: /
backend:
serviceName: nginx-service
servicePort: 80
- host: tomcat.itheima.com
http:
paths:
- path: /
backend:
serviceName: tomcat-service
servicePort: 8080# 创建
[ root@k8s-master01 ~]# kubectl create -f ingress-https.yaml
ingress.extensions/ingress-https created
# 查看
[ root@k8s-master01 ~]# kubectl get ing ingress-https -n dev
NAME HOSTS ADDRESS PORTS AGE
ingress-https nginx.itheima.com,tomcat.itheima.com 10.104.184.38 80, 443 2m42s
# 查看详情
[ root@k8s-master01 ~]# kubectl describe ing ingress-https -n dev
...
TLS:
tls-secret terminates nginx.itheima.com,tomcat.itheima.com
Rules:
Host Path Backends
---- ---- --------
nginx.itheima.com / nginx-service:80 (10.244.1.97:80,10.244.1.98:80,10.244.2.119:80)
tomcat.itheima.com / tomcat-service:8080(10.244.1.99:8080,10.244.2.117:8080,10.244.2.120:8080)
...
# 下面可以通过浏览器访问 https://nginx.itheima.com:31335 和 https://tomcat.itheima.com:31335来查看了2.2.5 TLS
可以通过指定包含 TLS 私钥和证书的 secret 来加密 Ingress。目前,Ingress 仅支持单个 TLS 端口 443,并假定 TLS termination。如果 Ingress 中的 TLS 配置部分指定了不同的主机,则它们将根据通过 SNI TLS 扩展指定的主机名(假如 Ingress controller 支持 SNI)在多个相同端口上进行复用。TLS secret 中必须包含名为 tls.crt 和 tls.key 的密钥,这里面包含了用于 TLS 的证书和私钥,例如:
apiVersion: v1
data:
tls.crt: base64 encoded cert
tls.key: base64 encoded key
kind: Secret
metadata:
name: testsecret
namespace: default
type: Opaque在 Ingress 中引用这个 secret 将通知 Ingress controller 使用 TLS 加密从将客户端到 loadbalancer 的 channel:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: no-rules-map
spec:
tls:
- secretName: testsecret
backend:
serviceName: s1
servicePort: 80请注意,各种 Ingress controller 支持的 TLS 功能之间存在差距。请参阅有关 nginx,GCE 或任何其他平台特定 Ingress controller 的文档,以了解 TLS 在你的环境中的工作原理。
Ingress controller 启动时附带一些适用于所有 Ingress 的负载平衡策略设置,例如负载均衡算法,后端权重方案等。更高级的负载平衡概念(例如持久会话,动态权重)尚未在 Ingress 中公开。你仍然可以通过 service loadbalancer 获取这些功能。随着时间的推移,我们计划将适用于跨平台的负载平衡模式加入到 Ingress 资源中。
还值得注意的是,尽管健康检查不直接通过 Ingress 公开,但 Kubernetes 中存在并行概念,例如 准备探查,可以使你达成相同的最终结果。请查看特定控制器的文档,以了解他们如何处理健康检查(nginx,GCE)。
2.2.6 单 Service Ingress
Kubernetes 中已经存在一些概念可以暴露单个 service(查看 替代方案),但是你仍然可以通过 Ingress 来实现,通过指定一个没有 rule 的默认 backend 的方式。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test-ingress
spec:
backend:
serviceName: testsvc
servicePort: 802.2.7 单域名多 Service
假如你想要创建这样的一个设置:
foo.bar.com -> 178.91.123.132 -> /foo s1:80
/bar s2:80
你需要一个这样的 ingress:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /foo
backend:
serviceName: s1
servicePort: 80
- path: /bar
backend:
serviceName: s2
servicePort: 802.2.8 基于名称的虚拟主机
Name-based 的虚拟主机在同一个 IP 地址下拥有多个主机名。
foo.bar.com --| |-> foo.bar.com s1:80
| 178.91.123.132 |
bar.foo.com --| |-> bar.foo.com s2:80
下面这个 ingress 说明基于 Host header 的后端 loadbalancer 的路由请求:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: test
spec:
rules:
- host: foo.bar.com
http:
paths:
- backend:
serviceName: s1
servicePort: 80
- host: bar.foo.com
http:
paths:
- backend:
serviceName: s2
servicePort: 802.3 Ingress 使用 2
wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.47.0/deploy/static/provider/baremetal/deploy.yaml
# 修改镜像
vi deploy.yaml
# 1、将 image k8s.gcr.io/xxxxxxx 的值改为如下值:
registry.cn-hangzhou.aliyuncs.com/lfy_k8s_images/ingress-nginx-controller:v0.46.0
# 2、在修改了 image 的上一层增加 hostNetwork: true,如下图
# 3、找到 secretName 将ingress-nginx-admission 改为 ingress-nginx-admission-token
# 安装
kubectl apply -f deploy.yaml
# 检查安装的结果
kubectl get pod,svc -n ingress-nginx
# 查看 Ingress
kubectl get ingress修改 Ingress 配置文件:

Ingress 安装成功的状态(查看 Pod):

Ingress 安装成功的状态(查看 Service,左侧 IP 为暴露的 HTTP 端口,右侧 IP 为暴露的 HTTPS 端口):

Ingress 安装失败:
容器处于 ContainerCreating,我们需要修改。参考这个 issues:kubernetes/ingress-nginx/issues/5932

Ingress 安装失败的解决步骤:
kubectl get secret -A | grep ingress-nginx
kubectl edit deployment ingress-nginx-controller -n ingress-nginx
在下面的位置增加上图 secret 的后缀
kubectl get pod -n ingress-nginx再次查看状态是否正常
2.4 IngressClass
Ingress 可以由不同的控制器实现,通常使用不同的配置。每个 Ingress 应当指定一个类,也就是一个对 IngressClass 资源的引用。IngressClass 资源包含额外的配置,其中包括应当实现该类的控制器名称。下面是一个 IngressClass 示例。
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-lbIngressClass 中的 .spec.parameters 字段可用于引用其他资源以提供额外的相关配置。
参数(parameters)的具体类型取决于你在 .spec.controller 字段中指定的 Ingress 控制器。
2.4.1 IngressClass 的作用域
IngressClass 的作用域取决于你的 Ingress 控制器,可以使用集群范围也可以是某个命名空间。
IngressClass 的默认作用于是集群级别,关于 IngressClass 作用域的详细使用说明请见 Ingress 文档。
2.4.2 默认 IngressClass
你可以将一个特定的 IngressClass 标记为集群默认 IngressClass。将一个 IngressClass 资源的 ingressclass.kubernetes.io/is-default-class 注解设置为 true 将确保新的未指定 ingressClassName 字段的 Ingress 能够分配为这个默认的 IngressClass。集群中最多只能有一个 IngressClass 被标记为默认。
2.4.3 kubernetes.io/ingress.class 注解
在 Kubernetes 1.18 版本引入 IngressClass 资源和 ingressClassName 字段之前,Ingress 类是通过 Ingress 中的一个 kubernetes.io/ingress.class 注解来指定的。这个注解从未被正式定义过,但是得到了 Ingress 控制器 的广泛支持。
ingressClassName 配置项是该注解的替代品,但并不完全等价。该注解通常用于引用实现该 Ingress 的控制器的名称,而这个新的字段则是对一个包含额外 Ingress 配置的 IngressClass 资源的引用,包括 Ingress 控制器的名称。
2.4.4 替代方案
可以通过很多种方式暴露 service 而不必直接使用 ingress:
- 使用 Service.Type=LoadBalancer
- 使用 Service.Type=NodePort
- 使用 Port Proxy
- 部署一个 Service loadbalancer 这允许你在多个 service 之间共享单个 IP,并通过 Service Annotations 实现更高级的负载平衡。
3 拓扑感知路由
在 Kubernetes 1.27 之前,此特性称为拓扑感知提示(Topology Aware Hint)。
拓扑感知路由(Toplogy Aware Routing) 调整路由行为,以优先保持流量在其发起区域内。在某些情况下,这有助于降低成本或提高网络性能。
你可以通过将 service.kubernetes.io/topology-mode 注解设置为 Auto 来启用 Service 的拓扑感知路由。当每个区域中有足够的端点可用时,系统将为 EndpointSlices 填充拓扑提示,把每个端点分配给特定区域,从而使流量被路由到更接近其来源的位置。
合适效果最佳:
- 入站流量均匀分布。否则,可能使得某个区域的端点子集过载。
- 服务在每个区域具有至少 3 个端点
3.1 工作原理
“自动”启发式算法会尝试按比例分配一定数量的端点到每个区域。请注意,这种启发方式对具有大量端点的 Service 效果最佳。
3.1.1 EndpointSlice 控制器
当启用此启发方式时,EndpointSlice 控制器负责在各个 EndpointSlice 上设置提示信息。控制器按比例给每个区域分配一定比例数量的端点。这个比例基于在该区域中运行的节点的 可分配 CPU 核心数。例如,如果一个区域有 2 个 CPU 核心,而另一个区域只有 1 个 CPU 核心,那么控制器将给那个有 2 CPU 的区域分配两倍数量的端点。
以下示例展示了提供提示信息后 EndpointSlice 的样子:
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: example-hints
labels:
kubernetes.io/service-name: example-svc
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 80
endpoints:
- addresses:
- "10.1.2.3"
conditions:
ready: true
hostname: pod-1
zone: zone-a
hints:
forZones:
- name: "zone-a"3.1.2 kube-proxy
kube-proxy 组件依据 EndpointSlice 控制器设置的提示,过滤由它负责路由的端点。在大多数场合,这意味着 kube-proxy 可以把流量路由到同一个区域的端点。有时,控制器在另一不同的区域中分配端点,以确保在多个区域之间更平均地分配端点。这会导致部分流量被路由到其他区域。
3.2 保护措施
Kubernetes 控制平面和每个节点上的 kube-proxy 在使用拓扑感知提示信息前,会应用一些保护措施规则。如果规则无法顺利通过,kube-proxy 将无视区域限制,从集群中的任意位置选择端点。
-
端点数量不足: 如果一个集群中,端点数量少于区域数量,控制器不创建任何提示。
-
不可能实现均衡分配: 在一些场合中,不可能实现端点在区域中的平衡分配。例如,假设 zone-a 比 zone-b 大两倍,但只有 2 个端点,那分配到 zone-a 的端点可能收到比 zone-b 多两倍的流量。如果控制器不能确保此“期望的过载”值低于每一个区域可接受的阈值,控制器将不添加提示信息。重要的是,这不是基于实时反馈。所以对于特定的端点仍有可能超载。
-
一个或多个 Node 信息不足: 如果任一节点没有设置标签
topology.kubernetes.io/zone,或没有上报可分配的 CPU 数据,控制平面将不会设置任何拓扑感知提示,进而 kube-proxy 也就不能根据区域来过滤端点。 -
至少一个端点没有设置区域提示: 当这种情况发生时, kube-proxy 会假设从拓扑感知提示到拓扑感知路由(或反方向)的迁移仍在进行中,在这种场合下过滤 Service 的端点是有风险的,所以 kube-proxy 回退到使用所有端点。
-
提示中不存在某区域: 如果 kube-proxy 无法找到提示中指向它当前所在的区域的端点,它将回退到使用来自所有区域的端点。当你向现有集群新增新的区域时,这种情况发生概率很高。
3.3 限制
-
当 Service 的
internalTrafficPolicy值设置为Local时,系统将不使用拓扑感知提示信息。你可以在同一集群中的不同 Service 上使用这两个特性,但不能在同一个 Service 上这么做。 -
这种方法不适用于大部分流量来自于一部分区域的 Service。相反,这项技术的假设是入站流量与各区域中节点的服务能力成比例关系。
-
EndpointSlice 控制器在计算各区域的比例时,会忽略未就绪的节点。在大部分节点未就绪的场景下,这样做会带来非预期的结果。
-
EndpointSlice 控制器忽略设置了
node-role.kubernetes.io/control-plane或node-role.kubernetes.io/master标签的节点。如果工作负载也在这些节点上运行,也可能会产生问题。 -
EndpointSlice 控制器在分派或计算各区域的比例时,并不会考虑容忍度。如果 Service 背后的各 Pod 被限制只能运行在集群节点的一个子集上,计算比例时不会考虑这点。
-
这项技术和自动扩缩容机制之间可能存在冲突。例如,如果大量流量来源于同一个区域,那只有分配到该区域的端点才可用来处理流量。这会导致 Pod 自动水平扩缩容要么不能处理这种场景,要么会在别的区域添加 Pod。
4 Gateway API
Gateway API 是一个由 SIG-NETWORK 管理的开源项目。该项目的目标是在 Kubernetes 生态系统中发展服务网络 API。Gateway API 提供了暴露 Kubernetes 应用的资源—— GatewayClass、Gateway、HTTPRoute、TCPRoute 等。
4.1 Gateway API 与 Ingress 有什么不同?
Ingress 的主要目标是用简单的、声明性的语法来暴露 HTTP 应用。Gateway API 暴露了一个更通用的代理 API,可以用于更多的协议,而不仅仅是 HTTP,并为更多的基础设施组件建模,为集群运营提供更好的部署和管理选项。
Kubernetes Gateway API 进入 Beta 阶段 | Kubernetes