# Service

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

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

### 1 三种 IP <a href="#san-zhong-ip" id="san-zhong-ip"></a>

`Kubernetes`系统中的三种IP:

* Node IP：`Node`节点的`IP`地址
* Pod IP: `Pod`的IP地址
* Cluster IP: `Service`的`IP`地址

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

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

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

## 2 定义 Service <a href="#ding-yi-service" id="ding-yi-service"></a>

定义`Service`的方式和我们前面定义的各种资源对象的方式类型，例如，假定我们有一组`Pod`服务，它们对外暴露了 8080 端口，同时都被打上了`app=myapp`这样的标签，那么我们就可以像下面这样来定义一个`Service`对象：

```yaml
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`就可以创建一个名为`myservice`的`Service`对象，它会将请求代理到使用 TCP 端口为 8080，具有标签`app=myapp`的`Pod`上，这个`Service`会被系统分配一个我们上面说的`Cluster IP`，该`Service`还会持续的监听`selector`下面的`Pod`，会把这些`Pod`信息更新到一个名为`myservice`的`Endpoints`对象上去，这个对象就类似于我们上面说的`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。

<figure><img src="https://raw.githubusercontent.com/meetbill/meetbill_static/46143f03e0b09deab8c0628474383114a2cc6e52/k8s/services_iptables.svg" alt=""><figcaption></figcaption></figure>

## 4 Service 类型 <a href="#service-lei-xing" id="service-lei-xing"></a>

我们在定义`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 类型 <a href="#nodeport-lei-xing" id="nodeport-lei-xing"></a>

如果设置 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)

```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 <a href="#externalname" id="externalname"></a>

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

```yaml
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）
* 选择具有匹配的 `app` 和 `release` 标签的 Pod

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

### **7.2 主节点服务（Leader Service）**（第二个服务）：

* 用于连接到当前的主节点以进行写操作
* 服务名称可以通过 `mysql.leaderServiceNameOverride` 进行覆盖
* 默认名称为 `<fullname>-leader`
* 支持不同的服务类型（如 ClusterIP、LoadBalancer）
* 允许指定集群 IP（ClusterIP）或负载均衡器 IP（LoadBalancerIP）

{% hint style="info" %}
问：\{{- 中的 - 起什么作用？

在 Helm 模板语法中，`{{-` 和 `-}}` 中的减号（`-`）用于控制模板渲染时的空白字符（空格、换行符等）的输出。具体来说：

* **`{{-`**：这个减号表示删除模板渲染结果前面的空白字符。这意味着在 `{{-` 之前的所有空白字符（如换行符和缩进空格）将不会出现在最终的渲染输出中。
* **`-}}`**：这个减号表示删除模板渲染结果后面的空白字符。这意味着在 `-}}` 之后的所有空白字符将不会出现在最终的渲染输出中。
{% endhint %}

### **7.3 从节点服务（Follower Service）**（第三个服务，仅在副本数大于 1 时创建）：

* 用于连接到任何从节点以进行读操作
* 服务名称可以通过 `mysql.followerServiceNameOverride` 进行覆盖
* 默认名称为 `<fullname>-follower`
* 支持与主节点服务类似的配置选项
* 选择具有 `role: follower` 标签的 Pod
