K8S 环境下如何对外提供服务访问

976 阅读7分钟

从一个网络工程师的角度出发,观察 K8S 的世界。

开篇

在传统业务场景下,我们知道一般情况业务流量访问时,会从客户端发起 --> DNS 解析 --> LB --> 防火墙 --> 服务集群 --> 服务集群内部负载 --> 真实服务器,最终对外提供服务访问。而在 K8S 的系统环境下,基本的流量访问是类似的,只是在其中和传统网络又有什么地方不同呢?这个是我们这次探索之路的主要目的。

接下来,就以访问一个搭建好的 eureka-web 为例子,一起来看看业务访问过程中 K8S 的不同。

K8S 业务访问方式

在开始之前,我们需要先了解 K8S 的几种服务暴露的方式,掌握基本的规则。

NodePort

不知道你是否使用过才 API 或者用 docker 直接启动一个服务端口,然后通过本地直接调用 IP+Port 的方式来测试你的代码。没错,这个就是最基本的访问场景,也是我们大家应该比较熟悉的方式。而 K8S 也支持这种方式实现业务访问。

这种方式就是 NodePort,在 K8S 之中一个 Service 的 Pod 启动之后,会暴露一个 Port,将这个 Port 直接映射到当前 Node 的一个指定 Port 上,那么此时通过访问 NodeIP + Port 的方式就能实现业务访问的最基本的需求。

这种方式简单粗暴,但是存在很多问题:

  • Node 端口有限,默认设置如下图 20000 - 65000 image.png
  • Pod 会在 Node 上面迁移,如果 Pod 被迁移了,对外暴露的地址就需要变更
  • 不利于管理
  • 端口直接暴露不合规

所以这种方式不是主要的提供服务的方式,只有服务比较稳定,或者在测试环境为了方便才会这么干。实际业务场景下极其不推荐这种方法。

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  type: NodePort
  selector:
    app: MyApp
  ports:
      # By default and for convenience, the `targetPort` is set to the same value as the `port` field.
    - port: 80
      targetPort: 80
      # Optional field
      # By default and for convenience, the Kubernetes control plane will allocate a port from a range (default: 30000-32767)
      nodePort: 30007

Loadbalance

NodePort 可以在内部直接提供服务访问,但是如果有个公网的请求呢?那私网地址肯定无法被直接访问到。所以在在 K8S 请求层面需要创建一个负载均衡器(loadbalance),loadbalance 通过将每个 Node 作为后端,而 Service 挂在 NodePort 之后。以此对外提供服务访问。如下图参考:

image.png

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: MyApp
  ports:
    - protocol: TCP
      port: 80
      targetPort: 9376
  clusterIP: 10.0.171.239
  type: LoadBalancer
status:
  loadBalancer:
    ingress:
    - ip: 192.0.2.127

当需要设置公网的 load balancer 时,每个云厂商的声明方式不同,比如 alibaba cloud 和 AWS

metadata:
  annotations:
    service.beta.kubernetes.io/alibaba-cloud-loadbalancer-address-type: "intranet"

metadata:
    name: my-service
    annotations:
        service.beta.kubernetes.io/aws-load-balancer-internal: "true"

ClusterIP

K8S 主要的方式,Pod 在 K8S 环境中可能会经常发生变化,被驱逐、被重拉、被删除,因为它本身也是一个 container。当 Pod 发生了改变时,Pod 的地址都和所在的 Node 都有可能会变化,所以很难提供一个稳定的访问。所以 K8S 提供了一个解决方案,把每个 Service 固定在一个虚拟 IP 地址上(ClusterIP),自动且动态绑定后面的 Pod。而其底层依然是依赖 kube-proxy 来实现。

是默认的服务类型。

ClusterIP 的特点:

  1. 仅用于 kubernetes Service 这个对象,并由 Kubernetes 管理和分配 IP 地址;
  2. cluster ip 无法被ping ,它没有一个“实体网络对象”;
  3. cluster ip 只能结合 Service Port 组合成一个具体的通信端口,单独 ClusterIP 不具备通信的基础,并且他们属于 kubernetes 集群这样一个封闭的空间。

ClusterIP 的方式:

  • 普通 Service : 通过为 Kubernetes 的 Service 分配一个集群内部可访问的固定虚拟 IP,实现集群内的访问;
  • Headless Service:不分配 cluster ip,通过 dns 提供稳定的网络 ID 来访问,dns 会将 headless service 的后端直接解析为 pod IP,主要供 statefulSet 使用。

ExternalName

ExternaleName 是 K8S 中一个特殊的 service 类型, 通过 CNAME 的方式匹配一个 Service 到一个 DNS 域名,而不是通过匹配 Selector的方式,可以通过 spec.externalName 的参数来声明。

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

访问流程

通过上面对 K8S 的访问学习,我们基本了解到,要想成功访问 K8S 的业务有四种方式:NodePort、Loadbalance、ClusterIp、ExternalName,而 ClusterIP 是 K8S 内部访问的,不可以被外部直接访问,所以选择 NodePort 的方式是常规操作了。但是这种方式也存在很多弊端,在前面介绍 NodePort 已经提到了。而 ExternalName 的方式就很直接,而它更多适合将外部服务映射到内部服务。

在 k8s 官网上有个知识点,它就是 Ingress,从 K8S 官网摘录以下内容,关于 Ingress 的定义(出处kubernetes.io/docs/concep…

An API object that manages external access to the services in a cluster, typically HTTP.Ingress may provide load balancing, SSL termination and name-based virtual hosting.

直白地翻译一下:Ingress 是一个管理外部访问集群内服务的 API 对象。Ingress 可以提供 LB、 SSL 认证、基于名称的虚拟主机。

Ingress 可以更简单的理解为是暴露 HTTP 和 HTTPS 路由到外部集群,提供外部访问,非常类似 Nginx。访问流量的路由信息,通过 Ingress 资源定义的规则来控制,如下图中,外部客户端访问资源,先通过 Ingress-managed 访问到集群内的 Ingress,通过路由规则对应到了相应的 service,再基于 kube-proxy 将 Pod 的IP 和端口,以 endpoints 的方式暴露出来。所以如果在业务访问中发现没有 endpoints,那实际业务访问肯定也会出现问题。接下来我们看看具体的流程是如何一步一步关联起来的。

  1. Ingress : 在大致了解流程的情况下, 我们先查看 Ingress 的信息, 命令如下:

     kubectl get ingressroute -n {namespace} eureka-web -o yaml
    

    结果如下图,我们可以关注到这几个信息
    image.png

     API Version: traefik.containo.us/v1a1pha1
     # Ingress 无法独自完成工作,需要 IngressController 来配合,这里使用的是 traefik ,还有其他很多类型的 Ingress Controller。
     Spec.Routes.Match :Host('xxx.xxx.com') && PathPrefix('/eureka-web')
     # 定义了路由信息,即访问时的请求地址
     Spec.Routes.Services.Name :eureka-server
     # 关联后端服务名称,是下一步我们要查的依据
     
    
  2. svc

    通过上一步的查看,我们找到的 Ingress 是如何对应 Service 的,那 Service 的信息如何查看?

     kubectl get svc -n {namespace} eureka-web
    

    查询结果如下图

    image.png

    在这个图中出现了比较多的关键字段,我们先关注下面这些字段,这里终于出现了 ClusterIP 以及一些 Port。

     Labels:  xxx # 用于和后端 Pod 绑定
     Selector: xxx # 用于告知 endpoint
     Type: ClusterIP  # 这是一个内部访问 IP 地址,也可以是 NodePort、lb、externalName
     IP : 10.x.x.x  # CLusterIP 地址
     Port: 80/TCP # 映射出的端口
     TargetPort: 20001/TCP # 实际后端的端口
     Endpoints: 172.25.x.x:20001 # endpoints 记录
    
  3. Endpoints

    Endpoint 是 K8S 集群资源对象,用来表示一个 service 对应的所有 Pod 的访问地址。service 只有配置 selector,endpoint 才会自动创建对应的 endpoint 对象。而 Endpoints Controller 负责生成和维护所有 Endpoints 对象的控制器。

    查看命令

     kubectl describe endpoints -n {namespace} eureka-server
    

    image.png

    Subsets.addresses: 172.25.x.x # 是后端 Pod 的地址,这里查看到的信息和 SVC 中看到的信息是一样的

  4. Pod

    SVC 中定义的 Labels 前后关联,把 pod 和 endpoints 整个串起来。

    image.png

    而 Pod 可以先粗略地理解为一个 sandbox,它框住了一些资源,在 Pod 下启动的 docker 可以共享存储、网络、进程等信息,而一个或多个 Pod 可以共同为一个 service 提供服务。

    image.png

    在 Service 和 Pod 绑定且启动之后,就会生成 Endpoint,以此对外提供服务,这里记录的是实际后端 PodIP 以及对外提供的服务端口。

总结

通过目前的梳理,已经大致完成业务访问流程的摸索,但未谈及具体的流量转发和控制的细节,毕竟 K8S 的精髓在于调度控制,后面会继续详细介绍每一步的细节。