Service

Pod的生命是有限的,死亡过后不会复活了。尽管每个Pod都有自己的IP地址,但是如果Pod重新启动了的话那么他的IP很有可能也就变化了。

Kubernetes集群就为我们提供了这样的一个对象 - ServiceService是一种抽象的对象,它定义了一组Pod的逻辑集合和一个用于访问它们的策略,其实这个概念和微服务非常类似。一个Serivce下面包含的Pod集合一般是由Label Selector来决定的。

1 三种 IP

Kubernetes系统中的三种IP:

  • Node IP:Node节点的IP地址

  • Pod IP: Pod的IP地址

  • Cluster IP: ServiceIP地址

Node IPKubernetes集群中节点的物理网卡IP地址(一般为内网)

Pod IP是每个PodIP地址,它是Docker Engine根据docker0网桥的IP地址段进行分配的(我们这里使用的是flannel这种网络插件保证所有节点的Pod IP不会冲突)

Cluster IP是一个虚拟的IP,仅仅作用于Kubernetes Service这个对象,由Kubernetes自己来进行管理和分配地址,当然我们也无法ping这个地址,他没有一个真正的实体对象来响应,他只能结合Service Port来组成一个可以通信的服务。

2 定义 Service

定义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

然后通过的使用kubectl create -f myservice.yaml就可以创建一个名为myserviceService对象,它会将请求代理到使用 TCP 端口为 8080,具有标签app=myappPod上,这个Service会被系统分配一个我们上面说的Cluster IP,该Service还会持续的监听selector下面的Pod,会把这些Pod信息更新到一个名为myserviceEndpoints对象上去,这个对象就类似于我们上面说的Pod集合了。

需要注意的是,Service能够将一个接收端口映射到任意的targetPort。 默认情况下,targetPort将被设置为与port字段相同的值。 可能更有趣的是,targetPort 可以是一个字符串,引用了 backend Pod 的一个端口的名称。 因实际指派给该端口名称的端口号,在每个 backend Pod 中可能并不相同,所以对于部署和设计 Service ,这种方式会提供更大的灵活性。

另外Service能够支持 TCP 和 UDP 协议,默认是 TCP 协议。

3 kube-proxy

Kubernetes集群中,每个Node会运行一个kube-proxy进程, 负责为Service实现一种 VIP(虚拟 IP,就是我们上面说的clusterIP)的代理形式,现在的Kubernetes中默认是使用的iptables这种模式来代理。这种模式,kube-proxy会监视Kubernetes master对 Service 对象和 Endpoints 对象的添加和移除。 对每个 Service,它会添加上 iptables 规则,从而捕获到达该 Service 的 clusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend 中的某一个个上面。 对于每个 Endpoints 对象,它也会安装 iptables 规则,这个规则会选择一个 backend Pod。

默认的策略是,随机选择一个 backend。 我们也可以实现基于客户端 IP 的会话亲和性,可以将 service.spec.sessionAffinity 的值设置为 "ClientIP" (默认值为 "None")。

另外需要了解的是如果最开始选择的 Pod 没有响应,iptables 代理能够自动地重试另一个 Pod,所以它需要依赖 readiness probes。

4 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 才支持。

5 NodePort 类型

如果设置 type 的值为 "NodePort",Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口(每个 Node 上的同一端口)代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定,如果不指定的话会自动生成一个端口。

需要注意的是,Service 将能够通过 :spec.ports[].nodePort 和 spec.clusterIp:spec.ports[].port 而对外可见。

接下来我们来给大家创建一个NodePort的服务来访问我们前面的Nginx服务:(保存为service-demo.yaml)

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: myapp
  type: NodePort
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    name: myapp-http

创建该Service:

$ kubectl create -f service-demo.yaml

然后我们可以查看Service对象信息:

$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        27d
myservice    NodePort    10.104.57.198   <none>        80:32560/TCP   14h

我们可以看到myservice的 TYPE 类型已经变成了NodePort,后面的PORT(S)部分也多了一个 32560 的映射端口。

6 ExternalName

ExternalName 是 Service 的特例,它没有 selector,也没有定义任何的端口和 Endpoint。 对于运行在集群外部的服务,它通过返回该外部服务的别名这种方式来提供服务。

kind: Service
apiVersion: v1
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: my.database.example.com

当查询主机 my-service.prod.svc.cluster.local (后面服务发现的时候我们会再深入讲解)时,集群的 DNS 服务将返回一个值为 my.database.example.com 的 CNAME 记录。 访问这个服务的工作方式与其它的相同,唯一不同的是重定向发生在 DNS 层,而且不会进行代理或转发。 如果后续决定要将数据库迁移到 Kubernetes 集群中,可以启动对应的 Pod,增加合适的 Selector 或 Endpoint,修改 Service 的 type,完全不需要修改调用的代码,这样就完全解耦了。

7 实践

# Headless service for stable DNS entries of StatefulSet members.
apiVersion: v1
kind: Service
metadata:
  name: {{ template "fullname" . }}
  labels:
    app: {{ template "fullname" . }}
    chart: {{ template "xdb.chart" . }}
    release: {{ .Release.Name | quote }}
    heritage: {{ .Release.Service | quote }}
spec:
  ports:
  - name: {{ template "fullname" . }}
    port: 3306
  - name: xagent
    port: 8500
  clusterIP: None
  publishNotReadyAddresses: true
  selector:
    app: {{ template "fullname" . }}
    release: {{ .Release.Name | quote }}
---
# Client service for connecting to any MySQL instance for writes.
apiVersion: v1
kind: Service
metadata:
  {{- if .Values.mysql.leaderServiceNameOverride }}
  name: {{ .Values.mysql.leaderServiceNameOverride }}
  {{- else }}
  name: {{ template "fullname" . }}-leader
  {{- end }}
  labels:
    app: {{ template "fullname" . }}
    chart: {{ template "xdb.chart" . }}
    release: {{ .Release.Name | quote }}
    heritage: {{ .Release.Service | quote }}
spec:
  type: {{ .Values.mysql.leaderService.type }}
  {{- if (and (eq .Values.mysql.leaderService.type "ClusterIP") (not (empty .Values.mysql.leaderService.clusterIP))) }}
  clusterIP: {{ .Values.mysql.leaderService.clusterIP }}
  {{- end }}
  {{- if (and (eq .Values.mysql.leaderService.type "LoadBalancer") (not (empty .Values.mysql.leaderService.loadBalancerIP))) }}
  loadBalancerIP: {{ .Values.mysql.leaderService.loadBalancerIP }}
  {{- end }}
  ports:
  - name: mysql
    port: {{ .Values.mysql.leaderService.port }}
    targetPort: mysql
    {{- if .Values.mysql.leaderService.nodePort }}
    nodePort: {{ .Values.mysql.leaderService.nodePort }}
    {{- end }}
  selector:
    app: {{ template "fullname" . }}
    release: {{ .Release.Name | quote }}
    role: leader
{{- if lt 1 (.Values.replicaCount | int64) }}
---
# Client service for connecting to any MySQL instance for reads.
apiVersion: v1
kind: Service
metadata:
  {{- if .Values.mysql.followerServiceNameOverride }}
  name: {{ .Values.mysql.followerServiceNameOverride }}
  {{- else }}
  name: {{ template "fullname" . }}-follower
  {{- end }}
  labels:
    app: {{ template "fullname" . }}
    chart: {{ template "xdb.chart" . }}
    release: {{ .Release.Name | quote }}
    heritage: {{ .Release.Service | quote }}
spec:
  type: {{ .Values.mysql.followerService.type }}
  {{- if (and (eq .Values.mysql.followerService.type "ClusterIP") (not (empty .Values.mysql.followerService.clusterIP))) }}
  clusterIP: {{ .Values.mysql.followerService.clusterIP }}
  {{- end }}
  {{- if (and (eq .Values.mysql.followerService.type "LoadBalancer") (not (empty .Values.mysql.followerService.loadBalancerIP))) }}
  loadBalancerIP: {{ .Values.mysql.followerService.loadBalancerIP }}
  {{- end }}
  ports:
  - name: mysql
    port: {{ .Values.mysql.followerService.port }}
    targetPort: mysql
    {{- if .Values.mysql.followerService.nodePort }}
    nodePort: {{ .Values.mysql.followerService.nodePort }}
    {{- end }}
  selector:
    app: {{ template "fullname" . }}
    release: {{ .Release.Name | quote }}
    role: follower
{{- end }}

7.1 无头服务(Headless Service)(第一个服务):

  • 为 StatefulSet 的成员提供稳定的 DNS 条目

  • 通过设置 clusterIP: None 使其成为无头服务

  • publishNotReadyAddresses: true 确保 DNS 包含所有 Pod,即使它们尚未就绪

  • 暴露了两个端口:MySQL(3306)和 xagent(8500)

  • 选择具有匹配的 apprelease 标签的 Pod

Headless Services 是一种特殊的 service,其 spec:clusterIP 表示为 None,这样在实际运行时就不会被分配ClusterIP,也被称为无头服务,通过 DNS 解析提供服务发现。与普通服务不同的是 Headless Services 不提供负载均衡功能,每个 Pod 都有唯一的 DNS 记录,直接映射到其 IP 地址,适用于有状态应用的场景,如与StatefulSet 一起部署数据库。这种服务使得直接访问单个 Pod 成为可能,而不经过负载均衡器。

7.2 主节点服务(Leader Service)(第二个服务):

  • 用于连接到当前的主节点以进行写操作

  • 服务名称可以通过 mysql.leaderServiceNameOverride 进行覆盖

  • 默认名称为 <fullname>-leader

  • 支持不同的服务类型(如 ClusterIP、LoadBalancer)

  • 允许指定集群 IP(ClusterIP)或负载均衡器 IP(LoadBalancerIP)

问:{{- 中的 - 起什么作用?

在 Helm 模板语法中,{{--}} 中的减号(-)用于控制模板渲染时的空白字符(空格、换行符等)的输出。具体来说:

  • {{-:这个减号表示删除模板渲染结果前面的空白字符。这意味着在 {{- 之前的所有空白字符(如换行符和缩进空格)将不会出现在最终的渲染输出中。

  • -}}:这个减号表示删除模板渲染结果后面的空白字符。这意味着在 -}} 之后的所有空白字符将不会出现在最终的渲染输出中。

7.3 从节点服务(Follower Service)(第三个服务,仅在副本数大于 1 时创建):

  • 用于连接到任何从节点以进行读操作

  • 服务名称可以通过 mysql.followerServiceNameOverride 进行覆盖

  • 默认名称为 <fullname>-follower

  • 支持与主节点服务类似的配置选项

  • 选择具有 role: follower 标签的 Pod

Last updated