K8S网络之Service网络

116 阅读8分钟

承接上文K8S网络之Pod网络

image.png

有了pod网络,k8s集群当中的所有的pod在逻辑上都可以看做在一个平面网络内可以正常的做ip寻址和互通,

但是这个pod仅仅是k8s云平台当中的虚拟机抽象,最终我们需要在k8s集群当中运行的是应用或者服务service,

而一个service背后一般是多个pod组成的集群,这个时候就引入了服务发现discover和负载均衡load balancer,

这些问题就是第二层service网络要解决的问题。

Service网络的概念模型

image.png

假定第一层的pod网络已经存在了,account应用4个pod或虚拟机组成的集群一起提供服务,每个pod都有自己的pod ip和端口号,再假定集群内还部署了其他的应用,这些应用有些是account app的消费方,也就说有些client pod要访问account-service,这个时候就自然引入2个问题:

一个是服务发现,client pod如何发现并且定位account app集群中的pod ip,况且account app集群中的ip有可能会变(逾期的情况,比如重新发布;非逾期的情况, 比如pod挂了,k8s重新调度部署,pod ip也会变化)。

另外一个是client pod要以某种负载均衡策略访问account app集群中不同的pod实例来实现负载分摊和高可用的访问。

那么实际上k8s通过在client和account app pod集群之间引入一层account service抽象来解决服务发现和负载均衡的问题。

cluster ip来解决服务发现问题,client不关心pod集群中具体的pod数量和pod ip,即使pod ip发生变化也会被cluster ip所屏蔽(注意这里的cluster ip是虚拟ip),支持以不同的策略去访问pod集群中的不同的pod实例以实现负载分摊还有HA高可用。

k8s当中默认的负载均衡策略是Round-Robin(轮询调度算法的原理是每一次把来自用户的请求轮流分配给内部中的服务器,从1开始,直到N(内部服务器个数),然后重新开始循环),也可以采用其他的复杂的负载均衡策略。

k8s当中为什么要引入service 抽象,背后的原理是什么?

DNS方案

image.png

解决服务发现问题,其中比较古老的技术DNS是最早的一种服务发现技术。

假设在k8s当中引入DNS来解决服务发现,实际上k8s本身就支持DNS这样的组件。

k8s可以把pod集群的pod ip和端口注册到DNS上,client应用通过查询DNS就可以发现目标pod,就可以发起调用。

这个方案不仅简单而且对client无侵入,因为目前几乎所有的操作系统都自带这个DNS Client,但是基于DNS的服务发现有下面几个问题:

  • 不同DNS client实现不同的DNS功能是有差异的,有些客户端每次调用都会去查询DNS服务造成不必要的开销。另外有些客户端会缓存DNS的信息,默认的超时时间可能设置的比较长,当目标实例pod ip发生变化的时候,就会存在缓存刷新不及时的问题,会导致访问pod失效。
  • 实现的负载均衡策略都比较简单,比如Round-Robin,有些甚至还不支持负载均衡调用。

考虑上述DNS客户端实现功能的差异 ,这些都不在k8s的控制范围内,所以k8s并没有直接采用DNS来做服务发现。

k8s实际还是引入了Kube-DNS来实现通过域名访问服务,不过这个是建立在cluster ip和service网络之上的。

Service Registry + Client方案

image.png在当前微服务时代这个是比较流行的做法,这个方案典型的代表比如Eureka+Ribbon/Consul/Nacos。

k8s自身就带有分布式存储etcd,就可以实现service registry集群。

k8s把account-app pod集群的信息(ip和端口)自动注册到service registry,client 应用通过service registry可以查询和发现这些目标pod,然后就可以发起调用。

这个方案实现也不复杂,客户端可以实现比较灵活的负载均衡策略,但是需要客户端进行配合,对客户应用是有侵入性的,所以k8s也没有直接采用这种方案。

image.png

这种service网络实现方案是在前面两种方案的基础之上发展演化出来的,融合了前面两种技术的优点同时也解决了它们的不足,下面来剖析下k8s网络具体的实现原理。

在k8s平台的每个worker节点上面都部署2个组件(kubelet和kube-proxy),这两个组件和master是实现k8s service网络的关键,pod实例发布的时候 ,kubelet会负责启动这些pod实例,启动完之后,kubelet会把服务的pod ip列表汇报注册到master节点上,这就是服务注册。

k8s当中有service发布的概念,k8s会为服务分配相应的cluster ip,相关的信息也会记录在cluster上面,并且cluster ip和pod ip是有映射关系的。

kube-proxy会监听master,并且发现服务的cluster ip和pod之间的映射的列表,并且修改本地的linux iptables的转发规则,这个iptables的转发规则在接受到某个cluser ip请求的时候进行负载均衡并且转发到对应的pod ip上面去,这个就是服务发现的过程。

在实际运行的时候,当有消费者pod需要访问某个服务的时候,就通过cluser ip发请求,这个cluser ip调用会被本地的iptables所截获,然后通过负载均衡转发到目标服务的这些pod实例上面去,这就是服务实际调用时候的过程。

实际消费者pod也不是直接调用服务的cluster ip的,而是先调用服务名,因为cluster ip也可能会变化,比如针对不同的环境下(test环境、uat环境),只有服务名称一般是不变的,为了屏蔽cluster ip的变化,k8s在每个worker节点上面还有一个Kube-DNS组件,它也监听master节点并且发现cluster ip和服务名它们之间的映射关系,这样消费者pod通过Kube-DNS可以间接的发现cluster ip,然后cluster ip就可以通过iptables转发到这些目标pod上面去,这就是k8s内部完整的服务注册发现和调用的流程。

k8s服务注册发现机制和目前主流的微服务注册发现机制(比如Eureka和Ribbon)区别?

原理是类似的,但是也有一些显著的区别 ,这些区别主要体现在客户端:

  • 首先二者都是采用客户端代理proxy的机制,和ribbon一样,k8s的代理转发和负载均衡也是在客户端实现的,但是Ribbon的话,它是以library的形式,嵌入客户应用当中的,有侵入性。而k8s的kube-proxy是独立的组件,每个worker节点上面的都有一个kube-proxy,对客户应用是无侵入的,k8s的做法类似Service Mesh当中的边车sidecar。
  • 第二点区别Ribbon转发是通透的,而k8s中的转发是通过iptables转发,虽然k8s有Kube-Proxy,但它只负责服务发现和修改iptables或者ipvs的规则,实际请求是不穿透Kube Proxy的。早期的Kube-Proxy是代理穿透的 考虑到有性能损耗和单点问题,新的版本的k8s就改成iptables直接转发,不穿透Kube-Proxy。
  • 第三点差别Ribbon实现服务名到服务实例ip地址的映射,只有一层映射。在k8s当中是有两层映射的,Kube-Proxy是实现cluster ip到pod ip的映射,Kube-DNS是实现服务名到cluster ip的映射,是第二层映射地址。

个人认为对比目前主流的微服务发现机制,k8s服务发现机制抽象的更好,通过cluster ip统一屏蔽服务发现和负载均衡的问题,一个服务就是一个cluster ip,这个模型和传统的ip网络模型更贴近和更易于理解。

cluster ip是一个ip,但是这个ip后面跟的不是一个服务实例,而是一个服务集群,同时对客户应用是无侵入的,不穿透这个proxy,所以没有额外的性能损耗。

总结

  • k8s的service网络构建与pod网络之上的,主要目标是解决服务发现和负载均衡。
  • k8s当中通过service name+cluster ip可以统一屏蔽服务发现和负载均衡,底层技术是DNS+service registry基础之上发展演变而来。
  • k8s的服务发现和负载均衡是通过kube-proxy和iptables实现的,对应用是无侵入的,而且不穿透这个proxy,性能损耗比较小。
  • k8s的服务发现机制是现代服务发现机制和传统的linux内核机制的结合。