通常情况下,直接访问 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 信息:
- 是 Service 通过 NodePort 方式向集群外部暴露访问入口。
- 是从集群内访问服务 Service,10.98.76.27 这个 IP 只是一条 iptables 规则上的配置,并没有真正的网络 设备,所以 ping 这个地址,是不会有任何响应的。
- 是一组随机模式下(–mode random)的 iptables 规则链,也就是 Service 实现负载均衡的位置。
- 是这个 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 ~。