service介绍
解决什么问题
Pod
的生命是有限的,死亡过后不会复活了,尽管每个Pod
都有自己的IP
地址,但是如果Pod
重新启动了的话那么他的IP
很有可能也就变化了。这就会带来一个问题:比如我们有一些后端的Pod
的集合为集群中的其他前端的Pod
集合提供API
服务,如果我们在前端的Pod
中把所有的这些后端的Pod
的地址都写死,然后去某种方式去访问其中一个Pod
的服务,这样看上去是可以工作的,对吧?但是如果这个Pod
挂掉了,然后重新启动起来了,是不是IP
地址非常有可能就变了,这个时候前端就极大可能访问不到后端的服务了。
在部署 WEB 服务时,通常会使用 Nginx 作为服务的入口,Nginx 后面会挂载大量的后端服务。以前,我们可能需要手动更改 Nginx 配置中的 upstream 选项来动态改变提供服务的数量,但随着服务发现工具的出现,如 Consul、ZooKeeper 和 etcd 等,我们现在只需要将服务注册到这些服务发现中心,然后让工具动态地更新 Nginx 的配置就可以了,完全不需要手工操作,非常方便。
同样的,如果我们要解决前端 Pod 如何连接到后端 Pod 集合的问题,也可以使用类似的方式。当后端 Pod 被销毁或新建时,我们可以将这些 Pod 的地址注册到服务发现中心。然后,前端 Pod 可以连接到这个服务发现中心,从中获取后端 Pod 的地址信息,实现动态的服务发现和连接。这样,我们就可以将服务发现的逻辑从应用中解耦出来,让前端 Pod 只需要连接到一个能够做服务发现的中间件上面,而不需要直接与后端 Pod 集合交互。
三种IP
学习Service
之前,我们需要先弄明白Kubernetes
系统中的三种IP这个问题
定义service
定义一个名为 myservice
的 Kubernetes Service,假定我们有一组Pod
服务,它们对外暴露了 8080 端口,同时都被打上了app=myapp
这样的标签,那么我们就可以像下面这样来定义一个Service
对象:
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
selector:
app: myapp
ports:
- protocol: TCP
port: 80
targetPort: 8080
name: myapp-http
apiVersion: v1
:指定了 Kubernetes API 的版本,这里使用的是 v1 版本。
kind: Service
:指定了要创建的 Kubernetes 资源类型,这里是一个 Service。
metadata: name: myservice
:指定了 Service 的元数据,其中name
字段为myservice
,表示该 Service 的名称。
spec: selector: app: myapp
:这里定义了 Service 的选择器,指定了哪些 Pod 将会被该 Service 匹配到。这个 Service 会将流量转发给具有标签app: myapp
的 Pod。
ports: - protocol: TCP port: 80 targetPort: 8080 name: myapp-http
:这个部分定义了 Service 暴露的端口。具体解释如下:
protocol: TCP
:指定了端口使用的协议,这里是 TCP 协议。port: 80
:指定了 Service 暴露给其他 Pod 或外部客户端的端口号,这里是 80 端口。targetPort: 8080
:指定了后端 Pod 上实际运行服务的端口号,这里是 8080 端口。当请求到达 Service 时,Service 会将流量转发到具有标签app: myapp
的 Pod 上的 8080 端口。name: myapp-http
:指定了该端口的名称,用于标识这个端口。
运行命令
kubectl create -f myservice.yaml
就可以创建一个名为myservice
的Service
对象,这个Service
会被系统分配一个我们上面说的Cluster IP,
它会将请求代理到使用 TCP 端口为 8080,具有标签app=myapp
的Pod
上,这个过程需要负载均衡。
怎么实现负载均衡
在Kubernetes
集群中,每个Node
会运行一个kube-proxy
进程, 负责为Service
实现一种 VIP(虚拟 IP,就是我们上面说的clusterIP
)的代理形式,现在的Kubernetes
中默认是使用的iptables
这种模式来代理。
kube-proxy 是 Kubernetes 中的一个重要组件,负责为 Service 实现负载均衡和服务发现。它可以工作在多种模式下,其中最常见的是 iptables 模式。在 iptables 模式下,kube-proxy 监视 Kubernetes master 上的 Service 和 Endpoints 对象的变化,并根据这些信息为每个 Service 和对应的 Pod 部署 iptables 规则。
在 iptables 模式下,kube-proxy 会做以下工作:
- 为 Service 创建负载均衡规则:当创建一个 Service 时,kube-proxy 会为该 Service 创建 iptables 规则,用于将访问 Service 的请求转发到后端 Pod。这些规则会捕获到达 Service 的请求,并将其重定向到 Service 后端的一组 Pod 中的某一个。
- 为 Endpoints 创建负载均衡规则:kube-proxy 也会为每个 Endpoints 对象创建 iptables 规则,用于选择一个后端 Pod。这些规则允许 kube-proxy 在每个请求中选择一个后端 Pod,以实现负载均衡。
- 实现会话亲和性:kube-proxy 支持会话亲和性,可以根据客户端 IP 来路由请求到同一个后端 Pod,以保持会话的一致性。这可以通过将 Service 的 sessionAffinity 属性设置为 "ClientIP" 来实现。
- 自动重试后端 Pod:如果最初选择的后端 Pod 没有响应,kube-proxy 可以自动重试另一个后端 Pod。为了实现这一点,后端 Pod 必须具有 readiness probes,以确保它们能够接收流量。
kube-proxy 通常在 Kubernetes 集群中的每个物理机节点上都运行一个实例。kube-proxy 是 Kubernetes 中的一个核心组件,负责实现 Service 的负载均衡和服务发现功能。为了保证集群中的每个节点都能够访问到 Service,并实现负载均衡,kube-proxy 需要在每个节点上运行。
每个 kube-proxy 实例都会监听集群中的 Service 和 Endpoints 对象的变化,并根据这些变化来更新本地的负载均衡规则。通过在每个节点上运行 kube-proxy 实例,可以确保集群中的所有节点都能够正确地处理流量,并将请求转发到相应的后端 Pod。
Service 类型
定义Service
的时候可以指定一个自己需要的类型的Service
,如果不指定的话默认是ClusterIP
类型。可以使用的服务类型如下:
- ClusterIP:通过集群的内部 IP 暴露服务,选择该值,服务只能够在集群内部可以访问,这也是默认的ServiceType。
- NodePort:通过每个 Node 节点上的 IP 和静态端口(NodePort)暴露服务。NodePort 服务会路由到 ClusterIP 服务,这个 ClusterIP 服务会自动创建。通过请求 :,可以从集群的外部访问一个 NodePort 服务。
- LoadBalancer:使用云提供商的负载局衡器,可以向外部暴露服务。外部的负载均衡器可以路由到 NodePort 服务和 ClusterIP 服务,这个需要结合具体的云厂商进行操作。
- ExternalName:通过返回 CNAME 和它的值,可以将服务映射到 externalName 字段的内容(例如, foo.bar.example.com)。没有任何类型代理被创建,这只有 Kubernetes 1.7 或更高版本的 kube-dns 才支持。
NodePort 类型
如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定,如果不指定的话会自动生成一个端口。编辑
创建了一个 NodePort 类型的服务,名为 myservice。这意味着该服务将会在每个节点上绑定一个端口,并将流量转发到后端 Pod 的 80 端口。
myservice:这是你创建的 Service 的名称。
NodePort:这是 Service 的类型,表示这是一个 NodePort 类型的服务。
10.104.57.198:这是 Service 的 ClusterIP,即 Service 在集群内部的虚拟 IP 地址。
:这表示该服务没有关联的外部负载均衡器 IP 地址。
80:32560/TCP:这是 Service 的端口映射规则。其中,80 是 Service 的端口,32560 是每个节点上绑定的 NodePort 端口,用于从节点外部访问该服务。它告诉 Kubernetes 在每个节点上将来自此端口的流量转发到 Service 的端口。
因此,当你访问任何物理节点的 32560 端口时,Kubernetes 将会将请求转发到集群内部的service的 80 端口,然后进一步将请求转发到具有标签 app: myapp 的 Pod 上的 80 端口,而定义Pod的时候设置了Pod里的容器会把它的服务监听的端口映射到Pod的80端口,从而实现了对后端 Pod 中的web服务的访问。
实际应用
假设我们需要让一个WordPress
来访问mysql数据库,使用Deployment
来管理Pod,首先创建mysql的Deployment
对象,指明容器监听的端口为3306,端口的名称是"dbport" 。
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: mysql-deploy
namespace: blog
labels:
app: mysql
spec:
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:5.7
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
name: dbport
env:
- name: MYSQL_ROOT_PASSWORD
value: rootPassW0rd
- name: MYSQL_DATABASE
value: wordpress
- name: MYSQL_USER
value: wordpress
- name: MYSQL_PASSWORD
value: wordpress
volumeMounts:
- name: db
mountPath: /var/lib/mysql
volumes:
- name: db
hostPath:
path: /var/lib/mysql
然后需要让WordPress
来访问,由于它们不在同一个Pod中,需要访问就可以用到service。
所以在yaml文件中添加service信息
---
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: blog
spec:
selector:
app: mysql
ports:
- name: mysqlport
protocol: TCP
port: 3306
targetPort: dbport
运行
$ kubectl create -f wordpress-db.yaml
service "mysql" created
deployment.apps "mysql-deploy" created
然后查看Service
的详细情况:
$ kubectl describe svc mysql -n blog
Name: mysql
Namespace: blog
Labels: <none>
Annotations: <none>
Selector: app=mysql
Type: ClusterIP
IP: 10.98.27.19
Port: mysqlport 3306/TCP
TargetPort: dbport/TCP
Endpoints: 10.244.2.213:3306
Session Affinity: None
Events: <none>
可以看到Endpoints
部分匹配到了一个Pod
,生成了一个clusterIP
:10.98.27.19
,现在我们就可以通过这个clusterIP
加上定义的3306端口就可以正常访问我们这个mysql
服务了。(clusterIP是service提供给其他Pod访问的虚拟IP,Endpoints是service代理的Pod的实际IP和端口,其他Pod访问service使用的是clusterIP
)。
接下来创建WordPress
服务,环境变量WORDPRESS_DB_HOST
的值为上面mysql
服务的clusterIP
地址,这就是clusterIP类型service的使用。
apiVersion: apps/v1beta1
kind: Deployment
metadata:
name: wordpress-deploy
namespace: blog
labels:
app: wordpress
spec:
template:
metadata:
labels:
app: wordpress
spec:
containers:
- name: wordpress
image: wordpress
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: wdport
env:
- name: WORDPRESS_DB_HOST
value: 10.98.27.19:3306
- name: WORDPRESS_DB_USER
value: wordpress
- name: WORDPRESS_DB_PASSWORD
value: wordpress
接下来需要让外网访问开启的wordpress
服务,就需要建立一个能让外网用户访问的Service,这里就需要NodePort
类型的Service。
---
apiVersion: v1
kind: Service
metadata:
name: wordpress
namespace: blog
spec:
type: NodePort
selector:
app: wordpress
ports:
- name: wordpressport
protocol: TCP
port: 80
targetPort: wdport
创建并查看信息
$ kubectl apply -f wordpress.yaml
service "wordpress" created
Warning: kubectl apply should be used on resource created by either kubectl create --save-config or kubectl apply
deployment.apps "wordpress-deploy" configured
$ kubectl get svc -n blog
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mysql ClusterIP 10.98.27.19 <none> 3306/TCP 25m
wordpress NodePort 10.101.7.69 <none> 80:32255/TCP 1m
可以看到wordpress
服务产生了一个32255的端口,现在可以通过任意节点的NodeIP(公网IP)
加上32255端口,就可以访问我们的wordpress
应用了。
架构图解
NodePort模式的service绑定在每个k8s物理节点的32057端口,访问它就可以访问到service,然后service又会通过endpoint找到对应的Pod(因为它记录了这些Pod的内部 IP 地址),并访问它们的80端口。如果想让内部Pod访问service的时候,能访问到集群外部的服务,那么可以单独配置endpoint,这样service通过endpoint找的就是外部的ip地址。一般情况下不用NodePort模式的service来对外暴露服务,因为效率低,并且不支持http/https的通信,仅用于集群内部的通信,对外暴露服务需要使用ingress。
编辑
Ingress介绍
解决什么问题
把集群内部的Service暴露给外网访问,实现南北流量的互通。Service仅支持通过IP地址和端口来进行路由,不能进行HTTP请求的路径或主机头的路由。Ingress的功能和Nginx类似。
定义Ingress
路径匹配和虚拟主机匹配是两种常见的路由规则,它们根据请求的路径或主机头来路由流量到不同的后端服务。
路径匹配
路径匹配是根据请求的URL路径来进行路由的,将请求路由到不同的后端服务。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
spec:
rules:
- host: example.com
http:
paths:
- path: /app1
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
- path: /app2
pathType: Prefix
backend:
service:
name: app2-service
port:
number: 80
在上面的示例中,当请求的路径是 /app1
时,流量将被路由到 app1-service
;当请求的路径是 /app2
时,流量将被路由到 app2-service
。
虚拟主机匹配
虚拟主机匹配是根据请求的主机头(即域名)来进行路由的。
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
spec:
rules:
- host: app1.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
- host: app2.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: app2-service
port:
number: 80
在上面的示例中,当请求的主机头是 app1.example.com
时,流量将被路由到 app1-service
;当请求的主机头是 app2.example.com
时,流量将被路由到 app2-service
。
路径匹配适用于将不同的路径映射到不同的服务,通常用于在同一个域名下托管多个应用的情况。
虚拟主机匹配适用于将不同的主机头(域名)映射到不同的服务,通常用于在同一个集群中托管多个域名的情况。
ConfigMap介绍
解决什么问题
- 将应用程序的配置信息(如数据库连接字符串、日志级别、端口号等)存储在ConfigMap中,然后在应用程序中通过环境变量或文件挂载的方式引用这些配置信息。
- 对于需要加载静态文件的应用程序(如网页服务器、静态资源服务器等),可以将静态文件的路径、URL映射等配置信息存储在ConfigMap中,然后在应用程序中读取这些配置信息来加载静态文件。
定义ConfigMap
一个简单的Web应用程序,它连接到一个数据库,并且需要以下配置信息:
- 数据库主机地址
- 数据库端口号
- 数据库用户名
- 数据库密码
首先,创建一个ConfigMap来存储这些配置信息:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
data:
DB_HOST: "db.example.com"
DB_PORT: "3306"
DB_USERNAME: "admin"
DB_PASSWORD: "password123"
然后,在部署应用程序的Deployment或Pod配置中,将这些配置信息注入到应用程序的环境变量中:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app-container
image: my-app-image:latest
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: my-app-config
我们通过envFrom
字段将ConfigMap中的配置信息注入到了应用程序的环境变量中。这样,在应用程序启动时,就可以通过环境变量来访问数据库的配置信息。
Volumes介绍
解决什么问题
用于持久化存储数据。
Volume 类型
HostPath
和docker中的挂载主机目录类似。用例:一个基于 MySQL 数据库的应用程序,并且希望将数据库文件存储在宿主机上的特定目录中。
apiVersion: v1
kind: Pod
metadata:
name: mysql-pod
spec:
containers:
- name: mysql
image: mysql:latest
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
volumes:
- name: mysql-data
hostPath:
path: /data/mysql
创建了一个 Pod,其中包含一个名为 mysql 的容器,它使用 MySQL 镜像。我们定义了一个 HostPath Volume,它将宿主机上的 /data/mysql
目录挂载到容器内的 /var/lib/mysql
目录。简单来说相当于volumeMounts指定了用volumes作为磁盘加载到容器中。
EmptyDir
EmptyDir 是一个临时目录,它的生命周期与 Pod 的生命周期相同。当 Pod 被删除时,EmptyDir 中的数据也会被清除。常见使用场景是一个Pod中的不同容器共享数据。编辑
用例:一个 Web 服务器应用程序,它由两个容器组成:一个容器负责生成静态网页文件,另一个容器负责提供这些网页文件。你希望生成的网页文件能够在提供容器中进行访问。在这种情况下,你可以使用 EmptyDir 来共享生成的网页文件。
apiVersion: v1
kind: Pod
metadata:
name: web-server-pod
spec:
containers:
- name: webpage-generator
image: webpage-generator:latest
volumeMounts:
- name: shared-data
mountPath: /data
- name: web-server
image: nginx:latest
volumeMounts:
- name: shared-data
mountPath: /usr/share/nginx/html
volumes:
- name: shared-data
emptyDir: {}
创建了一个 Pod,其中包含两个容器:一个是 webpage-generator
,负责生成网页文件;另一个是 web-server
,负责提供网页文件。我们创建了一个 EmptyDir Volume,并将其挂载到两个容器中。webpage-generator
容器可以将生成的网页文件写入 EmptyDir,而 web-server
容器可以读取并提供这些网页文件。在这个例子中,EmptyDir 提供了一个临时的共享存储介质,使得两个容器可以轻松地共享数据。
NFS介绍
解决什么问题
NFS 卷是一种用于挂载远程 NFS 服务器上的目录的卷类型,可以将远程存储中的文件或目录挂载到 Kubernetes Pod 中的容器中,相当于把 ****HostPath和EmpytDir结合起来,既可以实现数据共享,又可以持久化。
使用场景
有一个Web 应用程序,需要在多个 Pod 中共享静态文件,比如图片、视频或者其他资源文件。
- 设置一个 NFS 服务器。假设 NFS 服务器的 IP 地址为 192.168.1.100,共享的目录为
/nfs/shared
。 - 在Kubernetes 中创建一个 NFS 卷,以便 Pod 可以挂载这个共享目录。
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-volume
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
nfs:
path: /nfs/shared
server: 192.168.1.100
- 在应用程序的 Pod 配置中引用这个 NFS 卷。
apiVersion: v1
kind: Pod
metadata:
name: webapp-pod
spec:
containers:
- name: webapp-container
image: your-webapp-image
volumeMounts:
- name: nfs-volume
mountPath: /app/static
volumes:
- name: nfs-volume
persistentVolumeClaim:
claimName: nfs-pvc
- 在 Kubernetes 集群中创建一个持久卷声明,以便动态地分配 NFS 卷给 Pod。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nfs-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
通过这个示例,Web 应用程序的所有 Pod 都可以共享 NFS 服务器上的/nfs/shared目录中的静态文件,无论 Pod 在 Kubernetes 集群中的哪个节点上运行,它们都可以访问相同的文件。
PV和PVC介绍
PV是Kubernetes中的存储资源的抽象,独立于任何单个Pod,PVC则是Pod对PV的请求。
解决什么问题
编辑
在没有pv的情况,每个节点使用的远程存储的方案不规范,各用各的,每个服务器都要管理自己的配置和依赖,没有统一的标准。pv对这些资源进行了抽象,关联了这些存储卷,通过在Pod上绑定PVC,就可以让Pod通过PVC去请求PV。
编辑 使用场景
下面是一个包含标签选择器的 PV 的 YAML 示例:
apiVersion: v1
kind: PersistentVolume
metadata:
name: my-pv
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /data/my-pv
persistentVolumeReclaimPolicy: Retain
labels:
app: my-app
apiVersion: v1
指定了 Kubernetes API 的版本。kind: PersistentVolume
表示这是一个 PersistentVolume 对象。metadata.name
指定了 PV 的名称。spec.capacity.storage
指定了 PV 的容量。spec.accessModes
指定了 PV 的访问模式,这里设置为 ReadWriteOnce,表示可以被单个节点挂载为读写模式。spec.nfs.path
指定了 PV 的路径,这里使用 NFS 协议。spec.nfs.server
指定了 NFS 服务器的地址。spec.persistentVolumeReclaimPolicy
指定了 PV 回收策略,这里设置为 Recycle,表示删除 PV 时会清空其中的数据。
创建一个 PVC 的 YAML 文件,如下所示:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
selector:
matchLabels:
app: my-app
创建一个 Pod 的 YAML 文件,如下所示:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: nginx
volumeMounts:
- name: my-volume
mountPath: /usr/share/nginx/html
volumes:
- name: my-volume
persistentVolumeClaim:
claimName: my-pvc
这样就创建了一个 PV、一个 PVC 和一个 Pod,并将它们关联起来。Pod 将使用 PVC 中的存储资源,并将其挂载到容器中的指定路径。
PV的回收策略
有不同的回收策略,用于确定当 PV 不再被使用时应如何处理其资源。
PV的访问模式
指定了如何将存储资源挂载到 Pod 中。
要选择正确的访问模式,需要根据你的应用程序和场景来确定。例如,如果你的应用程序需要多个 Pod 同时读写存储资源,那么就需要选择 RWX 访问模式。如果只需要一个 Pod 读写存储资源,那么就可以选择 RWO 访问模式。
PVC的访问模式
PV 和 PVC 的访问模式并不需要设置为完全相同。但是,PV 的访问模式必须满足 PVC 的访问需求。换句话说,PVC 可以请求比 PV 更灵活的访问模式,但不能请求更严格的访问模式。
例如:
- 如果一个 PV 的访问模式是 ReadWriteOnce(RWO),那么它只能被单个 Pod 以读写模式挂载。其他 Pod 只能以只读模式挂载该 PV。
- 如果一个 PVC 请求的访问模式是 ReadOnlyMany(ROX),那么它可以绑定到支持 ReadOnlyMany 或 ReadWriteMany(RWX)访问模式的 PV 上。
因此,PV 和 PVC 的访问模式可以不完全一样,但必须满足 PVC 的访问需求。
Storageclass介绍
以上构建PV的方式为静态构建,即提前手动创建好PV,等待PVC来绑定。利用storageclass可以实现动态构建,storageclass关联PVC后,可以根据PVC的需求自动创建PV。
使用场景
storageClassName
属性指定所需的 StorageClass。下面是一个 StorageClass 的简单示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp2
zone: us-west-1
这个示例中的 StorageClass 名称为 "fast",使用 AWS EBS 作为存储提供者,并指定了一些参数,如存储类型为 gp2、区域为 us-west-1 等。创建 PVC 时,可以通过 storageClassName: fast
来使用这个 StorageClass,然后 Kubernetes 将会根据定义的策略动态地创建 PV。