深入理解StatefulSet(一)

前言

首先聊一下Deployment,一个应用的所有Pod是完全一样的。它们相互之间没有顺序,也无所谓运行在哪台宿主机上,需要的时候就创建,不需要的时候就可以中止任意一个Pod。

有些分布式应用,在多个实例之间存有依赖关系,比如主从、主备关系。还有些数据库存储类应用,在本地磁盘会存放一些数据,而实例一旦被杀掉,即使重建出来,实例和数据之间的对应关系都已经丢失。

所以实例之间有不对等关系,以及实例对外部数据有依赖关系的应用,被称为有状态应用(Stateful Appication)。

Statefulset

StatefulSet 将应用状态抽象为两种情况:

  1. 拓扑状态。意味着应用之间的多个实例之间不是对等关系。这些应用必须按照某些顺序启动,并且新创建的Pod,必须和原来的网络标识一样,这样原来的访问者才能使用同样的方法,访问到这个新的Pod。
  2. 存储状态。这种情况意味着,应用的多个实例分别绑定了不同的存储数据。典型例子就是一个数据库应用的多个存储实例。

StatefulSet 的核心功能就是通过某种方式记录这些状态,然后在Pod被重新创建时,能够为新Pod恢复这些状态。

StatefulSet中每个Pod的DNS格式为:

1
statefulSetName-{0..N-1}.serviceName.namespace.svc.cluster.local
  • serviceName 为Headless Service name
  • 0…N-1 为Pod的序号,从0开始
  • statefulSetName 为StatefulSet name
  • namespace 为服务所在的namespace
  • cluster.local为Cluster Domain

图解

1649399502095.png

扩容

1649399564902.png

收缩

1649399581462.png

持久卷的创建和删除

1649399652857.png

  1. 增加副本时,会创建对应的pvc;
  2. 减少副本时,从高索引值的Pod名开始删除Pod,但是PVC不会被删除,需要手动释放;
  3. 当先收缩再扩容时,重建后的Pod实例会绑定到对应序号的PVC上。

Headless Service

首先聊一下Service是如何被访问的呢?

  1. 以service的VIP模式。比如访问10.0.23.1 这个service的IP地址时,10.0.23.1就是一个VIP,会把请求转发到该service所代理的某一个Pod上。
  2. 以service的DNS方式。比如访问“my-svc.my-namspace.svc.cluster.local”这条DNS记录,就可以访问到名为my-svc的service所搭理的某一个Pod。

针对于第二种方式,具体还可以分为两种处理方法:

  1. Normal Service。访问“my-svc.my-namspace.svc.cluster.local” 解析到的,正是my-svc这个service的VIP,后面的流程和VIP的方式一致
  2. Headless Service。访问“my-svc.my-namspace.svc.cluster.local”解析到的,直接就是my-svc代理的某一个Pod的IP地址。这里的区别在于Headless Service 不需要分配一个VIP,而是直接以DNS记录的方式解析出被代理POD的IP地址。

比如eureka-0.register-server.test.svc.cluster.local

标签规则:

  1. spec.containers.name 容器的名字,如何配置基本无影响
  2. template.metadata.labels 设定的label,注意和selector保持一致。并且该标签需要和headless 中的selector,以及nodePort中的selector保持一致。

示例

创建一个service

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

再创建一个StatefulSet

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

部署eureka集群

eureka集群部署的主要难点在于如何指定 eureka.client.service-url.defaultZone的值?

对于上述问题可以通过headless机制,不给cluster分配IP,而是通过域名访问服务来解决。在application.yaml文件中使用环境变量,传值给eueka。

每个eureka会注册到另外的eureka上,也就是eureka.client.serviceUrl.dafaultZone;
通过StatefulSet,可以知道每个eureka的name;
通过Headless,可以访问到每个eureka;
所以eureka.client.serviceUrl.defaultZone的值就是http://eureka-0.eureka:8000/eureka/,http://eureka-1.eureka:8000/eureka/,http://eureka-2.eureka:8000/eureka/

由于三个pod在同一个命名空间内,因此可以省略.namespace.svc.cluster.local。

配置文件

application.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
application:
name: DiscoveryServer
info:
version: $project.version$

server:
port: 8761
eureka:
instance:
hostname: ${POD_HOST_NAME} #设置eureka hostname
prefer-ip-address: false # 使用服务名注册到eureka server
client:
register-with-eureka: true #表示是否将自己注册在EurekaServer上,默认为true
fetch-registry: true #表示表示是否从EurekaServer获取注册信息,默认为true
service-url:
defaultZone: ${EUREKA_INSTANCE_LIST} #这里在部署的时候会使用环境变量替换
joy:
logback:
path: logs/EUREKA_INSTANCE_HOSTNAME

需要注意的是hostname要使用pod的主机名,否则会出现unavailable-replicas。此外还有其他可能:

  1. eureka.instance.preferIpAddress = false
  2. defaultZone 后面的eureka注册中心的地址要写成域名;
  3. eureka.client.register-with-eureka的值要写为true
  4. eureka.client.fetch-registry的值要写为true
  5. eureka集群中多个eureka服务的spring.application.name的值要一致
  6. eureka.instance.prefer-ip-address的值必须设置为false

service.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Service
metadata:
name: eureka
labels:
service: eureka
spec:
clusterIP: None
type: ClusterIP
ports:
- port: 8761
targetPort: 8761
name: eureka
selector:
app: eureka

StatefulSet.yaml

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: eureka
spec:
serviceName: "eureka"
selector:
matchLabels:
app: eureka-pod
replicas: 3
template:
metadata:
labels:
app: eureka-pod
spec:
containers:
- env:
name: eureka
image: 192.168.1.232:5000/k8s/eureka:statefull
imagePullPolicy: Always
env:
- name: APP_NAME
value: "eureka" # statefulSet name
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_HOST_NAME
value: "$(POD_NAME).$(APP_NAME).default.svc.cluster.local"
- name: EUREKA_INSTANCE_LIST
value: "http://eureka-0.$(APP_NAME).default.svc.cluster.local:8761/eureka/,http://eureka-1.$(APP_NAME).default.svc.cluster.local:8761/eureka/,http://eureka-2.$(APP_NAME).default.svc.cluster.local:8761/eureka/"
ports:
- containerPort: 8761
livenessProbe:
failureThreshold: 10
httpGet:
path: /health
port: 8761
scheme: HTTP
initialDelaySeconds: 30
periodSeconds: 60
successThreshold: 1
timeoutSeconds: 2
readinessProbe:
failureThreshold: 1
httpGet:
path: /health
port: 8761
scheme: HTTP
initialDelaySeconds: 10
periodSeconds: 3
successThreshold: 1
timeoutSeconds: 2

传递三个变量:

  1. APP_NAME 取值于metadata.name
  2. POD_NAME 取值于metadata.name
  3. POD_HOST_NAME 拼接单个Pod的主机名
  4. EUREKA_INSTANCE_LIST 由Pod的域名拼接组成

创建完成后会出现三个带序号的Pod和statfulSet,这个时候请求eureka.default.svc.cluster.local就可以使用服务了。但是如果想在集群外访问,可以使用nodeport的方式暴露。

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: eureka-node-port
spec:
type: NodePort
ports:
- port: 31331
targetPort: 8761
nodePort: 31331
selector:
app: eureka-pod

1649398888400.png

案例分析

headless service和普通service的区别

  • headless 不分配clusterIP,配置clusterIP: None
  • headless service可以通过解析service的DNS,返回所有Pod的地址和DNS
  • 普通service,只能通过解析service的DNS返回service的CLusterIP

statefulSet和Deployment的区别

  • statefulSet 的Pod有DNS地址,通过解析Pod的DNS可以返回Pod的IP
  • Deployment下的Pod没有DNS

普通service解析

Service的ClusterIP原理:
一个service可以对应一组endpoints,client访问ClusterIP,通过iptables或ipvs转发到real server。

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
# 获取svc,clusterIP为10.105.146.146
[root@uat-master ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
eureka-node-port NodePort 10.105.146.146 <none> 31331:31331/TCP 8m7s

# 查看svc详情
[root@uat-master ~]# kubectl describe svc eureka-node-port
Name: eureka-node-port
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=eureka
Type: NodePort
IP Families: <none>
IP: 10.105.146.146
IPs: 10.105.146.146
Port: <unset> 31331/TCP
TargetPort: 8761/TCP
NodePort: <unset> 31331/TCP
Endpoints: 10.200.128.133:8761,10.200.128.134:8761,10.200.128.137:8761
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>

# 测试解析

/ # nslookup eureka-node-port.default.svc.cluster.local
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name: eureka-node-port.default.svc.cluster.local
Address 1: 10.105.146.146 eureka-node-port.default.svc.cluster.local

综上所述,DNS查询时只会返回service的clusterIP,具体client访问的是哪个real server,由iptabels或者ipvs决定。

headless service解析

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
37
38
39
40
41
42
# 获取svc
[root@uat-master ~]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
eureka ClusterIP None <none> 8761/TCP 27m

# 查看详情
[root@uat-master ~]# kubectl describe svc eureka
Name: eureka
Namespace: default
Labels: service=eureka
Annotations: <none>
Selector: app=eureka
Type: ClusterIP
IP Families: <none>
IP: None
IPs: None
Port: eureka 8761/TCP
TargetPort: 8761/TCP
Endpoints: 10.200.128.133:8761,10.200.128.134:8761,10.200.128.137:8761
Session Affinity: None
Events: <none>

# 测试解析
/ # nslookup eureka.default.svc.cluster.local
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name: eureka.default.svc.cluster.local
Address 1: 10.200.128.134 eureka-1.eureka.default.svc.cluster.local
Address 2: 10.200.128.133 eureka-0.eureka.default.svc.cluster.local
Address 3: 10.200.128.137 eureka-2.eureka.default.svc.cluster.local


# 解析单独pod的DNS记录

/ # nslookup eureka-2.eureka.default.svc.cluster.local
Server: 10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local

Name: eureka-2.eureka.default.svc.cluster.local
Address 1: 10.200.128.137 10-200-128-137.eureka-node-port.default.svc.cluster.local

综上所述,DNS查询会返回所有的endpoint,通过解析Pod的DNS记录,也能返回Pod的IP。

headless service使用场景

  • 自助选择权,client可以自己决定使用哪个real server,可以通过DNS来获取real server信息。
  • headless service关联的每个pod,都会有对应的DNS域名,这样Pod直接可以互相访问。这样对于一些集训类型的应用就可以解决身份是吧的问题了。

为什么要用headless+statefulSet部署有状态应用

  1. headless service会为关联的Pod分配一个域 <service name>.$<namespace name>.svc.cluster.local
  2. statefulSet 会为关联的Pod保持一个不变的Pod Name $(statefulSet name)-$(pod序号)
  3. statefulSet会为关联的Pod分配一个dnsName $<Pod Name>.$<service name>.$<namespace name>.svc.cluster.local