Kubernetes学习笔记(3)-Service与数据存储

Service

介绍

在Kubernetes中,Pod是应用程序的载体,我们可以通过Pod的ip来访问应用程序。但是Pod的ip地址不是固定的,这也就意味着不方便直接采用Pod的Ip对服务进行访问。

为了解决这个问题,Kubernetes提供了Service这种类型的Resource。Service会对提供同一个服务的多个Pod进行聚合,并且提供一个统一的入口地址。通过访问Service的入口地址就能访问到后面的Pod服务。

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

下面是Service的资源清单描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
port: 3017 # service端口
targetPort: 5003 # pod端口
nodePort: 31122 # 主机端口

其中type表示Service的类型,可选值有:

  • ClusterIP:默认类型。这种Service只能在集群内部,通过Kubernetes系统自动分配的虚拟IP访问
  • NodePort:将Service通过指定的Node上的端口暴露给外部,通过此方法,就可以在集群外部访问服务
  • LoadBalancer:使用外接负载均衡器完成到服务的负载分发,注意此模式需要外部云环境支持
  • ExternalName: 把集群外部的服务引入集群内部,直接使用

ClusterIP

一个ClusterIP类型的Service,对应配置文件示例如下,其中直接指定type为ClusterIP,并通过Label Selector来决定代理Pod的范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: service-clusterip
namespace: dev
spec:
selector:
app: nginx-pod
clusterIP: xxx.xxx.xxx.xxx # service的ip地址,如果不写,默认会生成一个
type: ClusterIP
ports:
- port: 80 # Service端口
targetPort: 80 # pod端口

Service配置文件中的Selector描述可以对应到一系列Pod,在Kubernetes中用Endpoint来描述这种对应关系。Endpoint是Kubernetes中的一个资源对象,存储在etcd中,用来记录一个Service对应的所有Pod的访问地址。这也就是说,Service和Pod之间的联系是通过Endpoints来实现的,一个Service由一组Pod组成,这些Pod通过Endpoints暴露出来。

默认情况下,对于Service的访问会转发到后面真实的Pod上,转发策略默认使用kube-proxy的策略,例如随机,轮询等。不过Kubernetes中也提供了基于客户端地址的会话保持模式,可以通过设置spec.sessionAffinity:ClientIP来开启。在这种模式下,来自同一个客户端发起的所有请求都会转发到固定的一个Pod上。

当然在某些场景中,开发人员可能不想使用Service提供的负载均衡功能,而希望自己来控制负载均衡策略。针对这种情况,Kubernetes提供了HeadLiness Service,这类Service不会分配Cluster IP,如果想要访问Service,只能通过Service的域名进行查询。要创建一个HeadLiness Service,只需要在配置文件中指定spec.type为ClusterIP,同时指定spec.clusterIP为None即可。

NodePort

ClusterIP类型的Service只能在集群内部访问,如果希望将Service暴露给集群外部使用,则需要NodePort类型的Service。NodePort的工作原理实际上就是将service的端口映射到Node的一个端口上,然后就可以通过NodeIp:NodePort来访问Service了。

一个NodePort的配置文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
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

LoadBalancer

LoadBalancer和NodePort很相似,目的都是向外部暴露一个端口,区别在于LoadBalancer会在集群的外部再来做一个负载均衡设备,而这个设备需要外部环境支持。外部服务发送到这个设备上的请求,会被设备负载之后转发到集群中。

ExternalName

ExternalName类型的Service用于引入集群外部的服务,它通过externalName属性指定外部一个服务的地址,然后在集群内部访问此service就可以访问到外部的服务了。

数据存储

介绍

在Kubernetes中,容器的生命周期可能很短,会被频繁地创建和销毁。容器在被销毁时,保存在其中的数据也会被清除。Kubernetes引入Volume的概念来持久化保存容器中的数据。

Volume是Pod中能够被多个容器访问的共享目录,它被定义在Pod上(在Pod的配置文件中声明),然后被一个Pod里的多个容器挂载到具体的文件目录下。Kubernetes通过Volume实现同一个Pod中不同容器之间的数据共享以及数据的持久化存储。Volume的生命容器不与Pod中单个容器的生命周期相关,当容器终止或者重启时,Volume中的数据也不会丢失。

kubernetes的Volume支持多种类型,比较常见的有下面几个:

  • 简单存储:EmptyDir、HostPath、NFS
  • 高级存储:PV、PVC
  • 配置存储:ConfigMap、Secret

基本存储

EmptyDir

EmptyDir是最基础的Volume类型,一个EmptyDir就是Host上的一个空目录。EmptyDir是在Pod被分配到Node时创建的,它的初始内容为空,并且无须指定宿主机上对应的目录文件,因为Kubernetes会自动分配一个目录,当Pod销毁时, EmptyDir中的数据也会被永久删除。 EmptyDir用途如下:

  • 临时空间,例如用于某些应用程序运行时所需的临时目录,且无须永久保留
  • 一个容器需要从另一个容器中获取数据的目录(多容器共享目录)

例如下面的配置文件,在Pod.spec.volumes中声明了Volume,然后在Container的详细配置中进行对应挂载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: v1
kind: Pod
metadata:
name: volume-emptydir
namespace: dev
spec:
containers:
- name: nginx
image: nginx:1.17.1
ports:
- containerPort: 80
volumeMounts: # 将logs-volume挂在到nginx容器中,对应的目录为 /var/log/nginx
- name: logs-volume
mountPath: /var/log/nginx
- name: busybox
image: busybox:1.30
command: ["/bin/sh","-c","tail -f /logs/access.log"] # 初始命令,动态读取指定文件中内容
volumeMounts: # 将logs-volume 挂在到busybox容器中,对应的目录为 /logs
- name: logs-volume
mountPath: /logs
volumes: # 声明volume, name为logs-volume,类型为emptyDir
- name: logs-volume
emptyDir: {}

HostPath

EmptyDir中数据不会被持久化,它会随着Pod的结束而销毁,如果想简单的将数据持久化到主机中,可以选择HostPath。HostPath就是将Node主机中一个实际目录挂载到Pod中,以供容器使用,这样设计可以保证即使Pod销毁,数据依旧可以存在于Node主机上。

HostPath的使用方式与EmptyDir基本相同,只需要在声明的时候指定相关配置即可,后续Container可以用来挂载。

1
2
3
4
5
6
7
8
# HostPath Example
# ...
spec:
volumes:
- name: logs-volume
hostPath:
path: /root/logs
type: DirectoryOrCreate # 目录存在就使用,不存在就先创建后使用

其中type的可选值如下:

  • DirectoryOrCreate:目录存在就使用,不存在就先创建后使用
  • Directory:目录必须存在
  • FileOrCreate:文件存在就使用,不存在就先创建后使用
  • File:文件必须存在
  • Socket:unix套接字必须存在
  • CharDevice:字符设备必须存在
  • BlockDevice:块设备必须存在

NFS

HostPath可以解决数据持久化的问题,但是一旦Node节点故障了,或者Pod转移到了别的节点,就又会出现问题。此时需要准备单独的网络存储系统,比较常用的有NFS、CIFS。在Volume中也可以直接配置NFS相关的存储系统。

1
2
3
4
5
6
7
8
# NFS Example
# ...
spec:
volumes:
- name: logs-volume
nfs:
server: xxx.xxx.xxx.xxx # nfs服务器地址
path: /root/data/nfs # 共享文件路径

高级存储

Kubernetes支持的存储系统有很多,每种存储系统可能有互不相同的配置,学习成本较高。而为了能够屏蔽底层存储实现的细节,方便用户使用, Kubernetes引入PV和PVC两种资源对象。

PV(Persistent Volume)指持久化卷,是对底层的共享存储的一种抽象。一般情况下PV由Kubernetes管理员进行创建和配置,它与底层具体的共享存储技术有关,并通过插件完成与共享存储的对接。

PVC(Persistent Volume Claim)指持久卷声明,是用户对于存储需求的一种声明。换句话说,PVC其实就是用户向Kubernetes系统发出的一种资源需求申请。

PV和PVC是一一对应的,整个使用过程通常会经过下面一些阶段:

  • 资源供应:首先由管理员手动创建底层存储和PV

  • 资源绑定:用户创建PVC,Kubernetes负责根据PVC的声明去寻找PV,并进行绑定。

    • 一旦找到,就将该PV与用户定义的PVC进行绑定,用户的应用就可以使用这个PVC了PV一旦绑定到某个PVC上,就会被这个PVC独占,不能再与其他PVC进行绑定了
    • 如果找不到,PVC则会无限期处于Pending状态,直到等到系统管理员创建了一个符合其要求的PV
  • 资源使用:用户可在Pod中像Volume一样使用PVC,将PVC挂载到容器内的某个路径进行使用。

  • 资源释放:用户删除PVC来释放PV。当存储资源使用完毕后,用户可以删除PVC,与该PVC绑定的PV将会被标记为“已释放”,但还不能立刻与其他PVC进行绑定。通过之前PVC写入的数据可能还被留在存储设备上,只有在清除之后该PV才能再次使用。

  • 资源回收:Kubernetes根据PV设置的回收策略进行资源的回收。对于PV,管理员可以设定回收策略,用于设置与之绑定的PVC释放资源之后如何处理遗留数据的问题。只有PV的存储空间完成回收,才能供新的PVC绑定和使用。

配置存储

ConfigMap

ConfigMap是一种比较特殊的存储卷,它的主要作用是存储配置信息。

ConfigMap也属于一种Resource,例如可以通过如下配置文件创建一个ConfigMap:

1
2
3
4
5
6
7
8
9
10
apiVersion: v1
kind: ConfigMap
metadata:
name: configmap
namespace: dev
data:
info: |
username:admin
password:123456
myConfig: test

注意这里的竖线。在yaml中竖线表示保留文本块中的换行符,因此这里的含义是向data.info传递了username:admin\npassword:123456

创建之后可以通过describe命令查看详情:

1
kubectl describe cm configmap -n dev

创建好了之后,就可以像正常使用Volume的步骤一样,使用ConfigMap。例如现在我们创建一个Pod,在其中挂载ConfigMap,对应的配置文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
apiVersion: v1
kind: Pod
metadata:
name: pod-configmap
namespace: dev
spec:
containers:
- name: nginx
image: nginx:latest
volumeMounts: # 将configmap挂载到目录
- name: config
mountPath: /configmap/config
volumes: # 引用configmap
- name: config
configMap:
name: configmap

通过进入这个Pod,我们可以发现,ConfigMap对应目录到目录/configmap/config,并且在该目录下有两个文件info, myConfig,并且这两个文件的内容对应就是yaml配置文件中的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
kubectl exec -it pod-configmap -n dev -- /bin/sh

# 以下为进入Pod后的命令执行情况

# ls
bin dev etc lib32 media proc sbin tmp
boot docker-entrypoint.d home lib64 mnt root srv usr
configmap docker-entrypoint.sh lib libx32 opt run sys var
# cd configmap/config
# ls
info myConfig
# cat info
username:admin
password:123456
# cat myConfig
test

实际上ConfigMap的核心就在于配置文件中的data字段。在data字段中允许我们以键值对的形式传入内容,并且每个键值对最终会对应到目录下的一个文件,其中Key是文件名称,Value是文件中的内容。并且如果我们更新ConfigMap的内容,Pod中的值也会动态更新。

Secret

Secret与ConfigMap非常类似,它可以完成自动解码,主要用于存储敏感信息,例如密码、秘钥、证书等等。


Kubernetes学习笔记(3)-Service与数据存储
http://example.com/2023/08/01/Kubernetes学习笔记-3-Service与数据存储/
作者
EverNorif
发布于
2023年8月1日
许可协议