Kubernetes 基于 DNS 的服务注册与服务发现

通常情况下,直接访问 Pod 会有如下几个问题:

  • Pod 会随时被 Deployment 这样的控制器删除重建,那访问 Pod 的结果就会变得不可预知。
  • Pod 的 IP 地址是在 Pod 启动后才被分配,在启动前并不知道 Pod 的 IP 地址。
  • 应用往往都是由多个运行相同镜像的一组 Pod 组成,一个个 Pod 的访问也变得不现实。

Kubernetes 中的 Service 对象就是用来解决上述 Pod 访问问题的。Service 有一个固定 IP 地址,这个地址在集群内部是固定不变的,不同集群中会不一样,它将访问该地址的流量转发给 Pod,具体转发给哪些 Pod 可以通过 label selectors 确定,而且 Service 可以给这些 Pod 做负载均衡。

Service 是由 kube-proxy 组件加上 iptables 来共同实现的(还有一种是 IPVS 模式实现),在 Kubernetes 集群中,每个 Node 运行一个 kube-proxy 进程,负责维护节点上的网络规则,这些网络规则允许从集群内部或外部与 Pod 进行网络通信。

如上图所示,当 Kubernetes 监测到 Service、Pod 对象的创建和销毁时,kube-proxy 会配置相应的 iptables 规则,以拦截到达该 Service 的 IP 和 Port 的请求,并且会将请求重定向到它所代理的 Endpoint。

下面是一个 Service(NodePort) 和它所代理的 Endpoint 的 iptables 信息:

  1. 是 Service 通过 NodePort 方式向集群外部暴露访问入口。
  2. 是从集群内访问服务 Service,10.98.76.27 这个 IP 只是一条 iptables 规则上的配置,并没有真正的网络 设备,所以 ping 这个地址,是不会有任何响应的。
  3. 是一组随机模式下(–mode random)的 iptables 规则链,也就是 Service 实现负载均衡的位置。
  4. 是这个 Service 代理的三个 Endpoint,通过 DNAT 做目标地址转换,访问具体的 Pod 服务。

下面进入正题,看下 Kubernetes 支持的两种基本的服务发现模式 —— 环境变量 和 DNS。

  • 环境变量

在每个 Pod 启动的时候,Kubernetes 会把当前命名空间下面的所有 Service 的 IP 和端口通过环境变量的形式注入到 Pod 中去,这样 Pod 中的应用可以通过读取环境变量,来获取依赖服务的地址信息。

这种方法使用起来相对简单,但是有一个很大的问题,就是依赖的服务必须在 Pod 启动之前就存在,不然是不会被注入到环境变量中的。

下图展示的就是一个 Pod 中的环境变量,它记录了其它 Service 服务的 IP、Port。

/ # env | sort
......
ORDER_SERVER_PORT=tcp://10.103.88.0:9099
ORDER_SERVER_PORT_9099_TCP=tcp://10.103.88.0:9099
ORDER_SERVER_PORT_9099_TCP_ADDR=10.103.88.0
ORDER_SERVER_PORT_9099_TCP_PORT=9099
ORDER_SERVER_PORT_9099_TCP_PROTO=tcp
ORDER_SERVER_SERVICE_HOST=10.103.88.0
ORDER_SERVER_SERVICE_PORT=9099
ORDER_SERVER_SERVICE_PORT_TCP=9099
......
USER_SERVER_PORT=tcp://10.110.228.7:9097
USER_SERVER_PORT_9097_TCP=tcp://10.110.228.7:9097
USER_SERVER_PORT_9097_TCP_ADDR=10.110.228.7
USER_SERVER_PORT_9097_TCP_PORT=9097
USER_SERVER_PORT_9097_TCP_PROTO=tcp
USER_SERVER_SERVICE_HOST=10.110.228.7
USER_SERVER_SERVICE_PORT=9097
USER_SERVER_SERVICE_PORT_TCP=9097
......
  • DNS

这种方式我们不需要去关心分配的 ClusterIP 的地址,因为 VIP 地址并不是固定不变的,虽然在一个集群中是不变的,但是我们如果存在 DEV、TEST、UAT 等环境,是不能保证在每个集群中的 IP 是相同的。

如果能够直接使用 Service 的名称(Service 的名称一般使用微服务名称,而且一旦使用基本上不会变化),其对应的 ClusterIP 地址的解析能够自动完成就好了。

KubeDNS 和 CoreDNS 是两个已建立的 DNS 解决方案,用于定义 DNS 命名规则,并将 Pod 和 Service 的 DNS 解析到它们相应的集群 IP。

通过 CoreDNS,Kubernetes 服务之间可以仅仅通过服务名称来相互访问。

Kubernetes 使用 DNS 作为服务注册表,每个 Service 都会自动注册到集群 DNS 之中,要使用服务发现功能,每个 Pod 都需要知道集群 DNS 的位置才能使用它。因此每个 Pod 中的 /etc/resolv.conf 文件都被配置为使用集群 DNS 进行解析,这也是默认的 DNS 策略:ClusterFirst。

集群中 kubelet 的启动参数有 --cluster-dns= 和 --cluster-domain= ,这两个参数分别被用来设置集群 DNS 服务器的 IP 地址和主域名后缀。

Pod 的 dnsPolicy 配置支持四种策略:

  • ClusterFirst:通过 CoreDNS 来做域名解析,Pod 内 /etc/resolv.conf 配置的 DNS 服务地址是集群 DNS 服务的 kube-dns 地址。该策略是集群工作负载的默认策略。
  • None:忽略集群 DNS 策略,需要提供 dnsConfig 字段来指定 DNS 配置信息。
  • Default:Pod 直接继承集群节点的域名解析配置。使用宿主机的 /etc/resolv.conf 文件。
  • ClusterFirstWithHostNetwork:强制在 hostNetWork 网络模式下使用 ClusterFirst 策略(默认使用 Default 策略)。

我的演示环境是用一键安装利器 Kubeadm 安装的,它会默认安装 CoreDNS 插件。

下面我们进入到一个 Pod 中查看下它的 dns 配置文件:

/ # cat /etc/resolv.conf
nameserver 10.96.0.10
search cloud.svc.cluster.local svc.cluster.local cluster.local
options ndots:5

可以看到 DNS 的服务器地址既是 coredns 的 svc ip,所有域名的解析,其实都要经过 coredns 的虚拟 IP 10.96.0.10 进行解析。

  • cloud.svc.cluster.local
  • svc.cluster.local
  • cluster.local

域名的搜索域有如上三个,在解析的时候会依次带入 /etc/resolve.conf 中的 search 域进行 DNS 查找,所以集群中同一 namespace 中的 pod 之间相互访问是可以直接使用 svc name,不同 namespace 下的服务访问需要带上 namespace。

例如访问同一 namespace 下的服务 user-server,只需要匹配一次 search 域就可以解析到正确的 IP 地址。

user-server.cloud.svc.cluster.local

下面通过 nslookup 验证下 DNS 查找是否将服务的 DNS 解析为正确的 IP(A 记录),user-server 的 ClusterIP 就是 10.110.228.7,与 DNS 查找解析到的 IP 相同的。auth-server 与 user-server 在同一个 namespace:cloud 下,可直接通过服务名称访问。

/ # nslookup user-server
Name:      user-server
Address 1: 10.110.228.7 user-server.cloud.svc.cluster.local

不同 namespace 下的服务访问,则需要携带名称空间,例如要访问 istio-system 命名空间下的istio-egressgateway 服务,就必须携带命名空间:

/ # nslookup istio-egressgateway
nslookup: can't resolve 'istio-egressgateway': Name does not resolve

/ # nslookup istio-egressgateway.istio-system
Name:      istio-egressgateway.istio-system
Address 1: 10.100.190.21 istio-egressgateway.istio-system.svc.cluster.local

Kubernetes 通过其内置的 DNS 附加组件实现了高效的服务发现。基于 DNS 的服务发现功能非常强大,因为不需要将 IP 和端口等网络参数硬编码到应用程序中。一旦 Service 管理了一组 pod,就可以使用服务的 DNS 轻松地访问它们,并且通过 iptables 实现了 Pod 的 Load Balancer。

具体哪些 Pod 才可以成为 Service 的 Endpoint 接收流量,Kubernetes 给我们提供了三种探针,来实现对 Pod 的健康检查,如下表:

探针 作用 详细说明
livenessProbe 何时需要重启容器 Liveness 指针是存活探针,它用来判断容器是否存活、判断 pod 是否 running。如果 Liveness 指针判断 容器不健康,此时会通过 kubelet 杀掉相应的 pod,并根据重启策略来判断是否重启这个容器。如果默认不配置 Liveness 指针,则默认情况下认为它这个探测默认返回是成功的。
startupProbe 启动探针 Kubelet 使用启动探针来了解容器何时启动。如果配置了这样的探测,它将禁用存活探针和准备状态检查,直到成功为止,确保这些探测不会干扰应用程序启动。这可以用于对缓慢启动的容器进行活性检查,避免它们在启动和运行之前被 kubelet 杀死。
readinessProbe 何时可以开始接收流量 Readiness 指针用来判断这个容器是否启动完成,即 pod 的 condition 是否 ready。如果探测的一个结果是不成功,那么此时它会从 pod 上 Endpoint 上移除,也就是说从 Service 接入层上面会把前一个 pod 进行摘除,直到下一次判断成功,这个 pod 才会再次挂到相应的 endpoint 之上。

我们来看下在 Kubernetes 平台中滚动更新服务时 Eureka 注册中心存在的问题。

在我们微服务示例中,服务滚动更新的策略是,“一上一下,先上后下”的原则,具体参数配置如下,即 1 个新版本 pod ready(结合 readiness 探针)之后,才会去销毁旧版本的 pod,这个保证了 Service 至少会存在一个 ready 状态的站点,是最平稳的更新方式。

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

Kubernetes 提供了两个参数,来控制 rollingUpdate 滚动更新的速度:

  • maxUnavailable:和期望 ready 的副本数比,不可用副本数最大比例(或最大值),这个值越小,越能保证服务稳定,更新越平滑。
  • maxSurge:和期望 ready 的副本数比,超过期望副本数最大比例(或最大值),这个值调的越大,副本更新速度越快。

如果由于一些特殊原因,你的容器不是以单进程模式运行的,也就是应用进程并不是容器内的 1 号进程,也没有使用 Tini 作为 init 进程,这就导致容器内的应用进程接收的是 SIGKILL 信号,以非 graceful shutdown 的方式终止。

在滚动更新完成后,虽然这时旧版本的 Pod 已经被从 Service 的 Endpoint 站点中移除,但是这种服务停止的方式不会调用 Eureka-Server 的 API 主动下线服务,而是只能等默认定时 60s 去清理 90s 内没有续约的服务。

在这期间 Eureka Server 中会有两个 instance 在线,尽管其中旧版本的 Pod 已经被销毁了。

这个销毁的旧版本的 Pod 最大可能会在注册列表中存在 180s,如果再加上三级缓存同步周期 30s,消费端拉取服务列表的周期 30s,以及 Ribbon 的缓存周期 30s,消费端拉取的服务列表对 Pod 销毁下线的感知就会有 4 分钟左右的延迟,这 4 分钟内流入这个已经销毁的 Pod 的请求都会调用失败。

因为旧 Pod 的 IP 在集群中已经不存在了,所以会提示 Host unreachable

No route to host (Host unreachable)

如果我们使用 svc 域名(即服务名称)注册到 Eureka, 微服务内部之间相互调用不再依赖于各自的服务 IP(Pod IP),直接通过 Service 的域名访问,域名解析和负载均衡都交给 Kubernetes 平台,利用 Kubernetes 中 Pod 的探针监测机制,可以自动过滤已下线或有问题的服务。

kubernetes:
  namespace: cloud

eureka:
  client:
    serviceUrl:
      defaultZone: ${eureka}
  instance:
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
    hostname: ${spring.application.name}.${kubernetes.namespace}

这种方式使用了 Kubernetes 的服务发现功能,当 Pod 不可用时就会从 Service 的 Endpoint 列表中移除,流量就不会在负载到该 Pod,可以避免 Eureka 缓存导致的一段时间内服务调用失败问题。

此时 Eureka 的服务发现、Ribbon 的负载均衡其实也就失去作用了,服务发现使用了 Kubernetes 基于 DNS 的方式,负载均衡则是 kube-proxy 组件维护的 iptables 规则来随机访问一个 Pod。

微服务之间的相互访问如下图所示:

这里完全可以废除掉 Eureka,完全融入 Kubernetes 的服务发现机制,但是因为大量的微服务都是用的 Spring Cloud Eureka,底层走的 Feign 调用,彻底替换改造有点大,所以选择了向 Eureka 注册 SVC 域名这种方案。

~ END ~。

你可能感兴趣的:(Kubernetes 基于 DNS 的服务注册与服务发现)