《K8s 实战》——Service 概述

一、服务概述

1. 服务简介

  • Service:为一组功能相同的 Pod 提供单一不变的接入点的资源
  • 服务创建后,其 IP 和端口不会改变。访问服务的 IP 和端口会被路由到提供该服务的任意一个 Pod 上

  • 服务通过标签选择器判断哪些 Pod 属于该服务
  • 服务的必要性:
    • Pod 需要对集群内部其他 Pod 或集群外部客户端的 HTTP 请求作出响应
    • Pod 生命周期短,随时启动或关闭。K8s 在 Pod 启动前为其分配 IP 地址,因此客户端不能提前知道 Pod 的 IP 地址
    • 多个 Pod 可能提供相同的服务,因此需要单一的 IP 地址访问

2. 创建服务

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
# 通过命令直接创建
$ kubectl expose rc nginx --port=80 --target-port=8000

# 通过yaml创建
$ cat svc.yaml
apiVersion: v1
kind: Service
metadata:
name: kubia
spec:
selector: # 管理app=kubia的Pod
app: kubia
ports:
- port: 80 # 服务暴露的端口
targetPort: 8080 # 容器端口
$ kubectl apply -f svc.yaml

# CLUSTER-IP只能在集群内部访问
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubia ClusterIP 10.111.249.153 <none> 80/TCP 2d16h
$ curl 10.111.249.153
You've hit kubia-5fje3
$ kubectl exec kubia-7nog1 -- curl -s http://10.111.249.153
You've hit kubia-gzwli

3. 服务的会话亲和性

  • 将来自同一个 client IP 的请求转发到同一个 Pod
1
2
spec:
sessionAffinity: ClientIP # 默认None
  • Kubernetes 仅仅支持两种形式的会话亲和性服务:None 和 ClientIP
  • 不支持 Cookie:服务不是在 HTTP 层面上工作。服务处理 TCP 和 UDP 包,并不关心其中的荷载内容。而 Cookie 是 HTTP 协议的一部分
  • 会话亲和性和 Web 浏览器:浏览器使用 keep-alive 连接,通过单个连接发送所有请求。服务在连接级别工作,因此当首次与服务连接时会随机,然后将属于该连接的所有网络数据包全部发送到单个 Pod(即使服务会话亲和性设置为 None),直到连接关闭。而 curl 每次打开一个新连接

4. 服务暴露多个端口

  • 多个端口的服务必须指定端口名字
  • 标签选择器作用于整个服务,不能对每个端口做单独的配置
1
2
3
4
5
6
7
8
9
10
spec:
ports:
- name: http
port: 80
targetPort: 8080
- name: https
port: 443
targetPort: 8443
selector:
app: kubia

5. 使用命名的端口(推荐)

  • 好处:更换 Pod 端口号无需改变服务配置
1
2
3
4
5
6
7
8
9
kind: Pod
spec:
containers:
- name: kubia
ports:
- name: http # 端口8080被命名为http
containerPort: 8080
- name: https
containerPort: 8443
1
2
3
4
5
6
7
8
9
kind: Service
spec:
ports:
- name: http
port: 80
targetPort: http # 映射到容器中被称为http的端口
- name: https
port: 443
targetPort: https

6. 服务发现

  • Pod 获取服务的 IP 和端口

(1) 通过环境变量发现服务

  • Pod 启动时,K8s 会初始化一系列环境变量指向现存的服务。若服务早于 Pod 创建,Pod 进程便可根据环境变量获取服务信息
1
2
3
4
5
6
$ kubectl exec kubia-3inly -- env
KUBIA_SERVICE_HOST=10.111.249.153 # kubia
KUBIA_SERVICE_PORT=80
BACKEND_DATABASE_SERVICE_HOST=10.111.249.155 # backend-database
BACKEND_DATABASE_SERVICE_PORT=80
...

(2) 通过 DNS 发现服务

  • kube-system 命名空间下的 kube-dns Pod 运行 DNS 服务,集群中的其他 Pod 都被配置成使用其作为 DNS(K8s 通过修改每个容器的 /etc/resolve.conf 实现)
  • 运行在 Pod 上的进程查询 DNS 时都会被 K8s 自身的 DNS 服务器响应,该服务器知道系统中运行的所有服务
    • Pod 是否使用内部的 DNS 服务器是根据 Pod 中 spec.dnsPolicy 决定
1
2
3
4
5
6
7
$ kubectl get svc -n kube-system
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP,9153/TCP 3d20h
$ kubectl exec kubia-3inly -- cat /etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
...
  • 每个服务从内部 DNS 服务器中获得一个 DNS 条目,客户端 Pod 在知道服务名称的情况下可通过 FQDN(全限定域名)来访问,格式为:<服务名称>.<服务命名空间>.svc.cluster.local。其中 svc.cluster.local 是在所有集群本地服务名称中使用的可配置集群域后缀
  • 客户端仍需知道服务的端口号。服务可直接使用标准端口号(如 HTTP 的 80 端口或 Postgres 的 5432 端口)或从环境变量中获取端口号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ kubectl exec -it kubia-3inly -- bash

# FQDN代替IP访问服务
root@kubia-3inly:/# curl kubia.default.svc.cluster.local
You've hit kubia-3inly

# 若两个Pod在同一个命名空间,可直接使用服务名称
root@kubia-3inly:/# curl kubia
You've hit kubia-5asi2
root@kubia-3inly:/# cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local ...


# 无法ping通服务IP:服务的集群IP为虚拟IP,且只有与服务端口结合时才有意义
root@kubia-3inly:/# ping kubia
6 packets transmitted, 0 packets received, 100% packet loss

二、连接集群外部的服务

  • 将请求重定向到外部 IP 和端口

1. Endpoint

  • 服务并不是和 Pod 直接相连,而是通过 Endpoint 资源:暴露一个服务的 IP 和端口的列表
  • 服务的 Pod 选择器仅用来构建 IP 和端口列表,然后存储在 Endpoint 资源中。当客户端连接到服务时,服务代理会从列表中选择一个进行重定向
1
2
3
$ kubectl get endpoints kubia
NAME ENDPOINTS AGE
kubia 10.108.1.4:8080,10.108.2.5:8080,10.108.2.6:8080 2h

2. 手动配置 Endpoint

  • 创建不包含 Pod 选择器的服务将不会创建 Endpoint 资源,此时需要手动创建 Endpoint
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Service
metadata:
name: external-service
spec: # 没有指定Pod选择器
ports:
- port: 80 # 接收端口80上的传入连接

---

apiVersion: v1
kind: Endpoints
metadata:
name: external-service # Endpoint名称需和服务名称匹配
subsets:
- addresses: # 服务将连接重定向到Endpoint的IP地址
- ip: 11.11.11.11
- ip: 22.22.22.22
ports: # Endpoint的目标端口
- port: 80

3. 为外部服务创建别名

  • 除了手动配置 Endpoint,还可以通过 FQDN(完全限定域名)访问外部服务
1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Service
metadata:
name: external-service
spec:
type: ExternalName # 创建一个具有别名的外部服务的服务
externalName: someapi.somecompany.com # 实际服务的全限定名
ports:
- port: 80
  • Pod 通过 external-service.default.svc.cluster.local 就能访问外部服务
  • ExternalName 服务仅在 DNS 级别实施,即为服务创建了简单的 CNAME DNS 记录。因此连接到服务的客户端将直接连接到外部服务,完全绕过服务代理。因此该类型服务不会获得集群 IP

CNAME 记录指向完全限定域名而不是数字 IP 地址

三、headless 服务

  • 如果客户端需要连接到所有的 Pod,就需要找到每个 Pod 的 IP。一种选择是通过调用 API 服务器获取 Pod 及其 IP 地址列表,不太好
  • K8s 允许客户通过 DNS 查找发现 Pod IP。通常,当执行服务的 DNS 查找时,DNS 服务器会返回单个 IP,即服务的集群 IP。但是,如果将服务的 clusterIp 字段设置为 None,表明不需要为服务提供集群 IP,此时 DNS 服务器将不再返回单个 DNS A 记录,而是返回多个 A 记录,每个记录指向支持该服务的 Pod 的 IP

1. 创建 headless 服务

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: kubia-headless
spec:
clusterIP: None # headless
selector:
app: kubia
ports:
- port: 80
targetPort: 8080

2. 通过 DNS 发现 Pod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 创建支持DNS查找的Pod
$ kubectl run dnsutils --image=tutum/dnsutils --generator=run-pod/v1 --command -- sleep infinity
pod/dnsutils created
# headless服务返回的是就绪的Pod IP
$ kubectl exec dnsutils -- nslookup kubia-headless
...
Name: kubia-headless.default.svc.cluster.local
Address: 10.42.0.20
Name: kubia-headless.default.svc.cluster.local
Address: 10.42.0.19
# 常规服务返回的是Cluster IP
$ kubectl exec dnsutils -- nslookup kubia
...
Name: kubia.default.svc.cluster.local
Address: 10.43.99.228
  • headless 服务通过 DNS 轮询机制提供 Pod 的负载均衡,而非通过服务代理