建议阅读本篇文章之前先阅读之前的一篇service介绍的文章, 《k8s 核心概念 2 service》,本文基本上是对该篇文章进行细化和扩展。

关联文章:
第五章 服务service 一 ( dns、ENDPOINTS) service 作用
第五章 服务service 二

【k8s】Service种类、类型(ClusterIP、NodePort、LoadBalancer、ExternalName) service的几种类型
K8S中的IP地址(Node IP、Pod IP、Cluster IP、External IP) 和service有关联的一些ip概念

前言

pod 通常需要对来自集群内部其他 pod ,以及来自集群外部的客户端的 HTTP 请求作出响应,所以需要一种寻找其他 pod 的方法来使用其他 pod 提供的服务。

在 Kubernetes 中通过服务 (service) 解决以下问题:

  • pod 是短暂的: pod 随时启动和关闭
  • Kubernetes 在 pod 启动前会给已经调度到节点上的 pod 分配 IP 地址:客户端不能提前知道 pod 的 IP 地址
  • 水平伸缩意味着多个 pod 可能提供相同的服务:每个 pod 都有自己的 IP 地址

5.1 介绍服务

Kubernetes 服务是一种为一组功能相同的 pod 提供但以不变的接入点的资源。当服务存在时,它的 IP 地址和端口不会改变。与服务建立的连接会被路由到提供该服务的任意一个 pod 上。
在这里插入图片描述

5.1.1 创建服务

服务使用标签选择器来指定属于同一组的 pod 。
在这里插入图片描述

5.1.1.1 通过 kubectl expose 创建服务

创建服务的最简单的方法就是通过 kubectl expose ,在 第2章曾使用该方法来暴露创建的rc

5.1.1.2 通过 YAML 描述文件来创建服务

先创建2个nodesjs的pod,kubia-manual.yaml和kubia-manual2.yaml

kubia-manual.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: kubia-manual
  labels:
   app: kubia
spec:
  nodeSelector:
   install_node: "true"
  containers:
  - image: docker.artnj.test.com.cn/cci/kubia:v3
    name: kubia
    ports:
    - containerPort: 8080
      protocol: TCP

kubia-manual2.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: kubia-manual2
  labels:
   app: kubia
spec:
  nodeSelector:
   install_node: "true"
  containers:
  - image: docker.artnj.test.com.cn/cci/kubia:v3
    name: kubia
    ports:
    - containerPort: 8080
      protocol: TCP

代码清单5.1 kubia-svc.yaml

必须指定label标签,否则service不会管理该pod

在这里插入图片描述

# 遵循 v1 版本的 Kubernetes API
apiVersion: v1
# 资源类型为 Service
kind: Service
metadata:
  # Service 的名称
  name: kubia
spec:
  # 该服务可用的端口
  ports:
    # 第一个可用端口的名字
    - name: http
      # 可用端口为 80
      port: 80
      # 服务将连接转发到容器的 8080 端口
      targetPort: 8080
    # 第二个可用端口的名字
    - name: https
      # 可用端口为 443
      port: 443
      # 服务将连接转发到容器的 8443 端口
      targetPort: 8443
  # 具有 app=kubia 标签的 pod 都属于该服务
  selector:
    app: kubia

    

注意: selector标签,只管理带 app: kubia的pod

可以看到基本svc的配置非常简单,只定义了两个端口和一个选择器,我们在选择器中注明了app: kubia,意思就是让这个svc去将所有携带app: kubia标签的 pod 纳入自己的管理范围。

执行创建命令,并查看已创建的svc
kubectl create -f kubia-svc.yaml

$ kubectl create -f kubia-svc.yaml
service/kubia created
$ kubectl get svc     'kubectl get services: 查看当前所有服务,svc是简写'
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.254.0.1      <none>        443/TCP   17d
kubia        ClusterIP   10.254.25.196   <none>        80/TCP    3h26m

我们发现新的服务已经被分配了一个内部集群IP 10.254.25.196,type为ClusterIP ,这个是服务的四种类型之一,默认项,其他项分别为NodePortLoadBalancerExternalName因为只是集群的IP地址,只能在集群内部可以被访问。也就是说,其他 pod 就可以通过这个 ip 访问到其后面的 pod。

5.1.1.3 从集群内部测试服务

可以通过以下三种方式向服务发送请求:

  • 创建一个 pod ,它将请求发送到服务的集群 IP 并记录响应。可以通过 kubectl logs 查看 pod 日志检查服务的响应
  • 使用 ssh 远程登录到其中一个 Kubernetes 节点上,然后使用 curl 命令
  • 通过 kubectl exec 命令在一个已经存在的 pod 中执行 curl 命令

都是集群内部的通信方式

5.1.1.4 在运行的容器中远程执行命令

kubectl exec kubia-manual -- curl -s http://10.254.25.196

[root]$ kubectl exec kubia-manual -- curl -s  http://10.254.25.196
This is v3 running in pod kubia-manual         '从日志来看是 kubia-manual回的消息'
[root]$ kubectl exec kubia-manual -- curl -s  http://10.254.25.196
This is v3 running in pod kubia-manual2         '从日志来看是 kubia-manual2回的消息'

-- 代表 kubectl 命令项的结束,在 -- 之后的内容是指在 pod 内部需要执行的命令。如果需要执行的命令没有以 - 开始的参数,那么 -- 不是必须的。这里面的 -s是为了隐藏额外信息

访问多次相同的请求,发现会由不同的pod处理,说明servicec自动帮我做了负载均衡。

上面的命令是模拟在容器kubia-manual 中尝试访问service,service会自动分配请求至其背后的pod,可能创建的3个pod中的任意一个给响应。

如果多次执行同样的命令,每次调用执行应该在不同的pod上。因为服务代理通常将每个连接随机指向选中的后端Pod中的一个,即使连接来自于同一个客户端。

可以看到,svc同时也实现了负载均衡,合理的将请求平摊到了每一个 pod 上。

在这里插入图片描述

5.1.1.5 配置服务上的会话亲和性

如果希望特定客户端产生的所有请求每次都指向同一个 pod ,可以设置服务的 spec.sessionAffinity 属性为 ClientIP ,而不是默认值 None 。

...
spec:
  sessionAffinity: ClientIP
  ...

这种方式会使服务代理将来自同一个客户端 IP 的所有请求转发至同一个 pod 。 Kubernetes 仅支持两种形式的会话亲和性服务: None 和 ClientIP 。

5.1.1.6 同一个服务暴露多个端口

创建的服务可以暴露一个端口,也可以暴露多个端口,比如你的pod监听2个端口,比如HTTP监听8080端口,HTTPS监听8443端口,可以使用一个服务,从端口80和443转发至8080和8443,无需创建2个服务,这样通过一个集群 IP ,使用一个服务就可以将多个端口全部暴露出来。

注意:在创建一个有多个端口的服务的时候,必须给每个端口指定名字。

...
kind: Pod
spec:
  containers:
    - name: kubia
      ports:
        # 应用监听端口 8080 ,并命名为 http
        - name: http
          containerPort: 8080
        # 应用监听端口 8443 ,并命名为 https
        - name: https
          containerPort: 8443

注意:标签选择器应用于整个服务,不能对每个端口做单独的配置。如果不同的 pod 有不同的端口映射关系,需要创建两个服务。

5.1.1.7 使用命名的端口

我们就可以将在服务中引用命名的端口:

...
kind: Service
spec:
  ports:
    - name: http
      port: 80
      targetPort: 8080
    - name: https
      port: 443
      targetPort: https

采用命名端口的方式可以使得更换 pod 端口时无须更改服务的 spec ,并且不同的 pod 可以使用不同的端口。

5.1.2 服务发现

服务发现在微服务架构里,服务之间经常进行通信,服务发现就是解决不同服务之间通信的问题。比如一个nginx的pod,要访问一个mysql服务,就需要知道mysql服务的ip和port,获取ip和port的过程就是服务发现。

通过创建服务,现在可以通过一个单一稳定的IP地址访问到Pod。在服务整个生命周期内这个地址保持不变。在服务后面的pod可能删除重建,pod地址肯改变,数量也可能改变,但是始终可以通过服务的单一不变的IP地址访问到这些pod。

但客户端如何知道服务的IP和端口?是否需要先创建服务,然后手动查找其IP并将IP传递给客户端pod的配置选项?当然不是。k8s还未客户端提供了发现服务ip和port的方式。

kubernetes 支持两种服务发现模式

5.1.2.1 环境变量

Pod创建的时候,服务的ip和port会以环境变量的形式注入到pod里,比如kubia服务,会把下面一系列环境变量注入到pod里,通过这些环境变量访问kubia服务。

1)pod创建在kubia服务之前

在创建我们自己的服务kubia之前,已经存在KUBERNETES服务了,因此此时只有KUBERNETES服务相关的环境变量。

$  kubectl exec kubia-5rskv  env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=kubia-5rskv
KUBERNETES_SERVICE_HOST=fd00::1     '只能看到自带的KUBERNETES服务'
KUBERNETES_SERVICE_PORT=443
KUBERNETES_SERVICE_PORT_HTTPS=443
KUBERNETES_PORT=tcp://[fd00::1]:443
KUBERNETES_PORT_443_TCP=tcp://[fd00::1]:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_ADDR=fd00::1
NODE_VERSION=12.2.0
YARN_VERSION=1.15.2
HOME=/root

环境变量中,服务名作为前缀时,所有字母变为变为大写,且服务名称中的 - 将被转换为 _

这种情况下需要先删除pod,重新创建pod。因为k8s 只会在创建时间晚于服务的 pod 中注入服务域名

执行kubectl delete po -l app=kubia重建 pod

2)pod创建在kubia服务之后

我们删除kubia-5rskv Pod实例,rc会自动重新拉起一个Pod

$ kubectl delete pod kubia-5rskv
pod "kubia-5rskv" deleted


我们查看现在的3个pod,发现kubia-5rskv已经被删除,新拉起了一个zkrzf

$ kubectl get pod
NAME           READY   STATUS              RESTARTS   AGE
kubia-694qf    1/1     Running             0          3d5h
kubia-j29mx    1/1     Running             0          4d2h
kubia-zkrzf    0/1     ContainerCreating   0          40s

我们再看下环境变量

$  kubectl exec kubia-zkrzf  env
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=kubia-zkrzf
KUBIA_PORT_80_TCP=tcp://[fd00::388a]:80
KUBIA_PORT_80_TCP_PROTO=tcp
KUBERNETES_SERVICE_HOST=fd00::1
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://[fd00::1]:443
KUBERNETES_PORT_443_TCP_PORT=443
KUBIA_PORT=tcp://[fd00::388a]:80
KUBERNETES_PORT_443_TCP=tcp://[fd00::1]:443
KUBERNETES_PORT_443_TCP_PROTO=tcp
KUBERNETES_PORT_443_TCP_ADDR=fd00::1
KUBIA_SERVICE_HOST=fd00::388a                  '多了KUBIA服务'
KUBIA_SERVICE_PORT=80
KUBIA_PORT_80_TCP_PORT=80
KUBIA_PORT_80_TCP_ADDR=fd00::388a
KUBERNETES_SERVICE_PORT_HTTPS=443
NODE_VERSION=12.2.0
YARN_VERSION=1.15.2
HOME=/root

此时,有2个服务相关的参数,分别是KUBERNETES服务KUBIA服务

5.1.2.2 通过 DNS 发现服务

K8s集群会内置一个dns服务器,service创建成功后,会在dns服务器里新增一些记录,想要访问某个服务,通过dns服务器解析出对应的ip和port,从而实现服务访问。

上面的描述可能有点难以理解,我们换个方式解释。你可能已经发现了,在上文的测试中,我们使用了curl http://x.x.x.x的方式访问的svc,这样的话如果svc重建导致ip 地址改变了,那我们岂不是访问不到了?k8s 也想到了这一点,所以提供了通过FQDN(全限定域名)访问服务的方式,你可以在任意一个 pod 上直接使用服务的名称来访问服务:

FQDN类似dns,通过域名方式来访问,不知过FQDN的域名规则有点不一样

FQDN域名语法:
服务名.命名空间.集群后缀

在这里插入图片描述

下面,我们通过服务访问:

$ kubectl exec kubia-manual -- curl -s http://kubia:80
This is v3 running in pod kubia-manual
$ kubectl exec kubia-manual -- curl -s http://kubia:80
This is v3 running in pod kubia-manual2

注意:客户端仍然必须知道服务的端口号。如果服务使用标准端口号(例如HTTP的80端口或PostgreSQL的5432端口),这样是没有问题的。如果不是标准端口,客户端可以从环境变量获取端口号。

可以将命名空间和集群后缀省略,一下3种方式相同:

kubectl exec kubia-manual -- curl -s http://kubia:80
kubectl exec kubia-manual -- curl -s http://kubia.default:80
kubectl exec kubia-manual -- curl -s http://kubia.default.svc.cluster.local:80

原理是 进入容器后,查看/etc/resolv.conf:

nameserver 194.246.9.6
search default.svc.cluster.local svc.cluster.local cluster.local service.openpalette
options ndots:5

5.2 连接集群外的服务

5.2.2 介绍服务ENDPOINTS

服务不是和pod直接相连的。service和pod是通过endpoint来进行相连的。

[root]$ kubectl describe svc kubia
Name:              kubia
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=kubia
Type:              ClusterIP
IP:                10.254.25.196
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         172.33.1.55:8080,172.33.1.56:8080     'Endpoints'
Session Affinity:  None
Events:            <none>

Endpoint资源就是暴露一个服务的IP地址和端口的列表,kubectl get endpoints kubia 查看 kubia 的 endpoints 基本信息:

[root]$ kubectl get endpoints kubia
NAME    ENDPOINTS                           AGE
kubia   172.33.1.55:8080,172.33.1.56:8080   5h46m

服务中在 spec.selector 定义了 pod 选择器,但是在重定向传入连接时不会直接使用它。选择器用于构建 IP 和端口列表,然后存储在 Endpoints 资源中。当客户端连接到服务时,服务代理选择这些 IP 和端口对中的一个,并将传入连接重定向到该位置监听的服务器。

也就是说,通过创建了kubia 服务,在我们创建pod时,才会自动创建Endpoints。反之如果没有服务,则不会自动创建Endpoints。

5.2.2 手动配置服务Endpoints

通过创建了kubia 服务,在我们创建pod时,才会自动创建Endpoints。反之如果没有服务,则不会自动创建Endpoints。

下面我们就来演示手动配置场景。

创建没有选择器的服务:

由于没有选择器,k8s不知道该服务包含哪些pod,因此,也无法自动创建Endpoints,因为Endpoints也需要指定的pod才行。

使用以下描述文件 external-service.yaml 可以创建一个不指定 pod 选择器的服务:

# 遵循 v1 版本的 Kubernetes API
apiVersion: v1
# 资源类型为 Service
kind: Service
metadata:
  # Service 的名称
  name: external-service
spec:
  # 该服务可用的端口
  ports:
    # 第一个可用端口的名字
    - name: http
      # 可用端口为 80
      port: 80
      targetPort: http
    # 第二个可用端口的名字
    - name: https
      # 可用端口为 443
      port: 443
      targetPort: https

为没有选择器的服务配置 Endpoints:

Endpoints是一种独立的资源,并不是服务的一个属性。实际上二者是协作关系,因此二者也有一些约束,例如Endpoints 对象需要与服务具有相同的名称。

使用以下描述文件 external-service-endpoints.yaml 可以创建一个 Endpoints 资源:

# 遵循 v1 版本的 Kubernetes API
apiVersion: v1
# 资源类型为 Endpoints
kind: Endpoints
metadata:
  # Endpoints 的名称,与对应的 Service 名称一致
  name: external-service
# 该 Endpoints 的子集
subsets:
  # 第一个子集的地址信息
  - addresses:
      # 地址包含以下 ip 列表
      - ip: 11.11.11.11
      - ip: 22.22.22.22
    # 第一个子集的端口信息
    ports:
      # 每个 ip 可用的端口列表
      # 【注意】这个名字必须和服务端端口的名字对应
      - name: http
        port: 80
      - name: https
        port: 443


Endpoints 对象需要与服务具有相同的名称,并包含该服务将要重定向的目标 IP 地址和端口列表。当服务和 Endpoints 都创建后,服务就会自动使用对应当 Endpoints ,并能够像具有 pod 选择器那样当服务正常使用。
在这里插入图片描述

5.2.3 为外部服务创建别名

除了手动配置服务的 Endpoints 来代替公开外部服务的方法,还可以通过其完全限定域名 (FQDN) 来访问外部服务。

5.2.3.1 创建 ExternalName 类型的服务

通过以下描述文件 external-service-externalname.yaml 可以创建一个 ExternalName 类型的服务,这个服务会将请求转发到 spec.externalName 指定的实际服务的完全限定域名。

# 遵循 v1 版本的 Kubernetes API
apiVersion: v1
# 资源类型为 Service
kind: Service
metadata:
  # Service 的名称
  name: external-service
spec:
  # Service 的类型为 ExternalName
  type: ExternalName
  # 这个服务将所有请求都转发到 someapi.somecompany.com
  externalName: leetcode-cn.com
  # 该服务可用的端口
  ports:
    # 第一个可用端口的名字
    - name: http
      # 可用端口为 80
      port: 80
      # 使用 ExternalName 时, targetPort 将被忽略
    # 第一个可用端口的名字
    - name: https
      # 可用端口为 443
      port: 443
      # 使用 ExternalName 时, targetPort 将被忽略

服务创建完成后, pod 可以通过 external-service(.default.svc.cluster.local) 域名(括号内的可不加)连接到外部服务,而不用使用外部服务的实际 FQDN 。这样允许修改服务的定义,并且在以后可以修改 externalName 指向到不同的服务,或者将类型变为 ClusterIP 并为服务创建 Endpoints 。

ExternalName 服务仅在 DNS 级别实施——为服务创建了简单 CNAME DNS记录。因此,连接到服务的客户端将直接连接到外部服务,完全绕过服务代理,所以这类型的服务不会获得集群 IP 。

注意: CNAME 记录指向完全限定的域名而不是 IP 地址。

因为svc是通过我们事先定义好的标签选择器来查找 pod 的,所以 pod 的 ip 地址变动对于svc毫无影响,其实在svc和pod之间还包含了一个资源叫做endpoint,endpoint(简称ep)是一组地址及其端口的合集,如下图,只要一个svc有标签选择器的话,他就会自动创建一个同名的ep来标记出自己的要管理的 pod。

在这里插入图片描述
我们可以通过如下命令来查看我们刚才创建的kubia服务的ep:

$ kubectl describe svc kubia
Name:              kubia
Namespace:         default
Labels:            <none>
Annotations:       <none>
Selector:          app=kubia
Type:              ClusterIP
IP:                fd00::a0f5
Port:              <unset>  80/TCP
TargetPort:        8080/TCP
Endpoints:         172.30.4.58:8080,172.30.4.59:8080,172.30.4.60:8080 
Session Affinity:  None
Events:            <none>

然后就可以在Endpoints列中找到他包含的地址及端口号,这三个用,分隔的地址正是三个pod的地址。你可以使用kubectl get pod -o wide来查看 pod 的地址。你可以执行kubectl delete po -l app=kubia命令来重建所有的kubia pod,然后再来查看ep,会发现其ENDPOINTS也会自动更换成这三个 pod 的值。

我们回顾下代码清单5.1 kubia-svc.yaml中的脚本,通过spec.selector来指定标签为kubia的Pod。

其中被selector选中的 Pod,就称为 ServiceEndpoints,可以使用kubectl get ep 命令查看。

$ kubectl  get ep
NAME         ENDPOINTS                                                                                   AGE
kubia        172.30.4.58:8080,172.30.4.59:8080,172.30.4.60:8080                                          4d1h

我们可以看到kubia的后端端点172.30.4.58:8080,172.30.4.59:8080,172.30.4.60:8080,也就是httpd应用Pod的IP地址。

需要注意的是,只有处于Running状态且readlinessProbe检查通过的Pod,才会出现在Service的Endpoints列表里,同样当某个Pod出现问题时,Service也会把这个Pod从Endpoints中摘掉

服务的原理 kube-proxy

在Kubernetes集群中,每个Node会运行一个kube-proxy进程, 负责为Service实现一种 VIP(虚拟 IP,就是clusterIP)的代理形式。

Service其实是由kube-proxy组件和Iptables(代理的具体模式之一)来实现的。

现在的Kubernetes中默认是使用的iptables这种模式来代理
这种模式,kube-proxy会监视Kubernetes master对 Service 对象Endpoints (端点)对象添加和移除。 对每个 Service,它会添加上 iptables 规则,从而捕获到达该 ServiceclusterIP(虚拟 IP)和端口的请求,进而将请求重定向到 Service 的一组 backend(后端) 中的某一个个上面。 对于每个 Endpoints 对象,它也会安装iptables 规则,这个规则会选择一个 backend Pod

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

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

参考

Kubernetes 实战 —— 05. 服务:让客户端发现 pod 并与之通信(上)
Kubernetes 实战 —— 05. 服务:让客户端发现 pod 并与之通信(下)
Kubernetes In Action 学习笔记 服务发现
Kubernetes in Action中文版.pdf 观后笔记 一

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐