时间:2020-05-24来源:电脑系统城作者:电脑系统城
借助于kubernetes优秀的弹性扩缩功能,运行其中的应用程序能够在流量突增的时候坦然应对,在流量低谷的时候无需担心成本。但于此同时,也带来了极大的挑战: 弹性扩缩导致容器IP动态变化,客户端无法直接依赖于容器IP进行访问,我们必须通过某种方式固定流量入口,将流量通过该固定入口均衡地分发到后端,在容器扩缩的过程能够随着容器启停动态更新后端地址。
在这种场景下,我们自然而然地会想到广泛使用的LoadBalancer。kubernetes中service资源其实就是一种LoadBalancer。 service可以会产生一个serviceIP,通过label selecter选定一组pod,流量会通过该serviceIp负载均衡到后端的pod。
service有很多类型: ClusterIP
,NodePort
,LoadBalancer
。在应用于实际复杂的业务场景,以上类型各有利弊:
ClusterIP
是通过分配一个虚拟IP给每个service,通过kube-proxy实现转发,这个虚拟的IP在集群外无法被直接访问到,只适合集群内部的互相调用。NodePort
是通过将流量转发到宿主机上,然后通过kube-proxy转发到对应的pod, 每创建一个该类型的service就会占用一个宿主机的端口用做转发。此种类型的service虽然可以实现集群外部访问, 但是无法大规模应用,因为service比较多的时候,端口容易冲突,管理起来比较麻烦。LodaBalacner
会创建一个真实的LoadBalancer, 然后将流量转发到NodePort service之上,因为此时NodePort端口对用户透明,由kubernetes自动分配并管理,所以不存在上述提到的端口冲突的问题。 但是缺点就是性能,功能和扩展性
externalTraficPolicy
这种机制,只挂载pod所在的node到LB, 但是这样会导致流量转发不均衡,例如如果nodeA上面有两个pod,nodeB上面有一个pod, LB是将流量平均的转达到两个node上面, 而不是根据pod数目设置不同的权重, 参见社区Caveats and Limitations when preserving source IPs。除了service之外,还有ingress用来实现负载均衡。 ingress本质上是一个代理,广泛用于七层协议,对于一些四层或者gRPC类型的支持不太好。同时ingress controller容器本身也会发生容器漂移等现象,也需要一个四层的负载均衡动态地转发流量到后端。
明确了上述各种类型的service的特点之后,我们需要明确我们所需要的service到底是什么样子,主要体现为: 功能,可用性,性能。
能够在集群外部被访问到,将流量从外部均匀地传递到集群内的多个容器。这其实就是kubernetes中LoadBalacner类型的service,对于每一个service我们使用一个真实的负载均衡器,借助于公司内部的或者公有云厂商提供的负载均衡设备即可,这些产品一般都比较成熟。
流量能够高效地转发到容器中,LoadBalancer作为底层基础架构,需要满足各种各样业务对网络性能的要求。流量能够高效的转发到容器内, 这点需要我们LB后端直接挂载容器,不用再经过NodePort或者iptable转发, 对于这点我们需要对底层网络有一定的要求,需要LB能够连接到podIP上,需要VPC直连的容器网络方案,而overlay方式的容器网络在容器集群外是无法直接访问的,此处就无法使用。不过一般情况下,真正在生产环境中被广泛使用的也就是VPC直连的容器网络方案,各个云厂商也有提供相应的解决方案。
VPC直连的网络方案现在被广泛采用,不光是为了解决LB连接的问题, 还具有其他优势:
- 首先业务需要podIP可以被直接访问到,便于架构上云时进行迁移,有些时候, 部分业务在容器里, 部分还在物理机上,他们需要能够互通。
- 性能需求,VPC直连的没有overlay封包解包的性能损耗
- 方便诊断,维护起来更加简单,可以直接看做是物理机使用
- 目前各个云厂商都有相关的CNI插件, 有利于多云架构的实现
对于LB直接连接容器, 其实在之前的架构下也是这么做的,已经证明了可行性。 只是老的架构是通过在富容器中启动一个agent,由agent注册自身容器IP到LB。老架构由于设计的较早,当时容器还是被当作虚拟机使用,当时还没有kubernetes,没有controller模式, 随着慢慢发展暴露出很多问题:
- 权限管理难以实现, 分散在各个容器之中
- 异常处理不集中, 在容器被暴力清理掉之后,来不及从LB上解绑就退出, 进而导致流量继续转发到该容器之中, 或者需要另一个异步清理的进程来实现清理
- 系统调用耦合严重,接口难以升级, 升级接口需要重启所有的容器
- 耗费资源,每个富容器中都会有相关的agent
由于老的架构设计较早,问题比较多,再重新思考这个问题的时候, 希望用云原生的方式,运用operater模式实现整个流程。
在容器动态扩缩过程中,需要保证流量平滑迁移,不能导致业务流量丢失。这是最基本的可用性保证。也是需要考虑最多的地方。kubernetes为了架构的简单,将功能分成多个模块异步执行,例如pod启动和健康检查是由kubelet负责,但是流量转发是由kube-proxy负责,他们之间没有直接的交互,这就会碰到分布式系统中执行时序的问题。如果容器还没启动流量就已经转发过来了就导致流量的丢失,或者容器已经退出但流量继续转发过来也会导致流量的丢失,这些情况对于滚动更新的pod尤其明显。 因为所有的操作都需要远程调用来操作LoadBalaner, 我们不得不考虑执行速度带来的影响。
一般情况下对于容器启动的时候我们无需过多担心, 只有启动之后才能接收流量, 需要担心的容器退出的过程中,需要确保流量还没有摘掉前容器不能退出,否则就会导致流量丢失。主要体现为两点:
对于滚动更新, 该过程一般是由对应的workload controller负责的, 例如deployment,statfulSet。 以deployment滚动更新为例,如果不加干预整个流程为: 新版本pod启动,readiness探针通过, controller将podIP挂载到LB上面, LB生效一般都需要时间,此时流量还不能转发到新版本pod里面。于此同时deployment认为新容器已经就绪,就进行下一步,删除掉老版本的pod。 此时新老版本都不能接收流量了,就导致了整个服务的不可用。这里根本原因是deployment认为pod就绪并不会考虑LB是否就绪,LB是k8s系统外部的资源,deployment并不认识。退一步来讲,我们平时使用的InCluster类型的service也是有这个问题的,kubelet中容器退出和kube-proxy流量摘除似乎是同时进行的,并没有时序保证,如果kube-proxy执行的稍微慢一点,kubelet中容器退出的稍微快一点,就会碰到流量丢失地情况。幸运的是目前kub-proxy是基于iptables实现的转发,刷新iptables规则在一般情况下执行速度足够快,我们很难碰到这种情况。 但是如果我们基于LoadBalancer直接挂载容器IP,就没有这么幸运了,我们需要远程调用操作LB,而且需要云厂商的LB生效都比较慢,鉴于此,我们需要想办法等到LB就绪之后才能认为整个pod就绪, 即pod就绪等于容器就绪(健康检查探针通过) + LB挂载就绪, pod就绪后才能进行滚动更新。
社区也碰到过过这个问题,开发了Pod Readiness Gates(ready++)的特性,用户可以通过 ReadinessGates 自定义 Pod 就绪的条件,当用户自定义的条件以及容器状态都就绪时,kubelet 才会标记 Pod 准备就绪。 如下所示,用户需要设置readinessGate:
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
labels:
run: nginx
name: nginx
spec:
replicas: 1
selector:
matchLabels:
run: nginx
template:
metadata:
labels:
run: nginx
spec:
readinessGates:
- conditionType: cloudnativestation.net/load-balancer-ready # <- 这里设置readinessGatea
containers:
- image: nginx
name: nginx
当我们给deployment设置了readinessGate这个字段之后, 当pod启动成功通过reainess的检查之后,并不会认为整个pod已经就绪,因为此时LB还没有就绪, 如果我们此时观察pod的status会发现如下信息
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:34:18Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:34:18Z"
message: corresponding condition of pod readiness gate "cloudnativestation.net/load-balancer-ready"
does not exist.
reason: ReadinessGatesNotReady
status: "False"
type: Ready # <--- Ready为False
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:34:20Z"
status: "True"
type: ContainersReady # <--- container Ready为Ture
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:34:18Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: docker://42e761fd53ccb2b2886c500295ceeff8f1d2ffc2376eb66dd95a436c395b95c0
image: nginx:latest
imageID: docker-pullable://nginx@sha256:2e6775f4300fc79b9d7fe6bb60c83b5fefe584258d9318ed408746789af48885
lastState: {}
name: nginx
ready: true
restartCount: 0
state:
running:
startedAt: "2020-03-14T11:34:19Z"
conditions信息中 ContainerReady为True, 但是Ready却为False, message中提示"对应的readiness gate condition还不存在", 那我们只需要patch上对应的condition即可, 如下所示:
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:38:03Z"
message: LB synced successfully
reason: LBHealthy
status: "True"
type: cloudnativestation.net/load-balancer-ready # <--- 增加readiness gate condtion
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:38:03Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:38:05Z"
status: "True"
type: Ready # <--- pod状态变为ready
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:38:05Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2020-03-14T11:38:03Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: docker://65e894a7ef4e53c982bd02da9aee2ddae7c30e652c5bba0f36141876f4c30a01
image: nginx:latest
imageID: docker-pullable://nginx@sha256:2e6775f4300fc79b9d7fe6bb60c83b5fefe584258d9318ed4087467
手动设置完readiness gate的condtion之后整个pod才能变为ready。
对于容器退出的过程中, 我们需要及时将流量从LB上面摘除。 一个pod典型的退出流程为: 我们从控制台下达删除pod的命令时,apiserver会记录pod deletionTimestamp 标记在pod的manifest中, 随后开始执行删除逻辑,首先发送SIGTERM 信号, 然后最大等待terminationGracePeriodSeconds
发送SIGKILL信号强制清理, terminationGracePeriodSeconds
该值用户可以自行在pod的manifest中指定。
结合整个退出过程,我们需要在监听到容器退出开始时(也就是deletionTimestamp被标记时) 在LB上将该pod流量权重置为0, 这样新建连接就不到达该容器,同时已有连接不受影响,可以继续提供服务。等到容器真正退出时才将该pod从LB上面摘除。用户如果想要更加安全的流量退出逻辑,可以设置一个稍长一点的terminationGracePeriodSeconds
, 甚至设置prestop逻辑或者处理SIGTERM信号, 让pod在退出前等待足够长的时间将流量彻底断掉,
明确了整个架构中的关键点后,就是具体的实现环节了。 这部分我们可以借鉴社区提供的service controller及各个云厂商LB在kubernetes中的应用。 社区为了屏蔽掉不同云厂商产品的差异,开发了cloud-controller-manager, 其内部定义了很多接口, 各个云厂商只需要实现其中的接口就可以在合适的时候被调用。 对于LoadBalancer定义接口如下:
// LoadBalancer is an abstract, pluggable interface for load balancers.
type LoadBalancer interface {
// TODO: Break this up into different interfaces (LB, etc) when we have more than one type of service
// GetLoadBalancer returns whether the specified load balancer exists, and
// if so, what its status is.
// Implementations must treat the *v1.Service parameter as read-only and not modify it.
// Parameter 'clusterName' is the name of the cluster as presented to kube-controller-manager
GetLoadBalancer(ctx context.Context, clusterName string, service *v1.Service) (status *v1.LoadBalancerStatus, exists bool, err error)
// GetLoadBalancerName returns the name of the load balancer. Implementations must treat the
// *v1.Service parameter as read-only and not modify it.
GetLoadBalancerName(ctx context.Context, clusterName string, service *v1.Service) string
// EnsureLoadBalancer creates a new load balancer 'name', or updates the existing one. Returns the status of the balancer
// Implementations must treat the *v1.Service and *v1.Node
// parameters as read-only and not modify them.
// Parameter 'clusterName' is the name of the cluster as presented to kube-controller-manager
EnsureLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) (*v1.LoadBalancerStatus, error)
// UpdateLoadBalancer updates hosts under the specified load balancer.
// Implementations must treat the *v1.Service and *v1.Node
// parameters as read-only and not modify them.
// Parameter 'clusterName' is the name of the cluster as presented to kube-controller-manager
UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error
// EnsureLoadBalancerDeleted deletes the specified load balancer if it
// exists, returning nil if the load balancer specified either didn't exist or
// was successfully deleted.
// This construction is useful because many cloud providers' load balancers
// have multiple underlying components, meaning a Get could say that the LB
// doesn't exist even if some part of it is still laying around.
// Implementations must treat the *v1.Service parameter as read-only and not modify it.
// Parameter 'clusterName' is the name of the cluster as presented to kube-controller-manager
EnsureLoadBalancerDeleted(ctx context.Context, clusterName string, service *v1.Service) error
}
当用户创建LoabBalancer类型的service时,cloud-controller-manager中的service controller就会利用informer监听service的创建、更新、删除事件,然后调用各个云厂商注册的接口,云厂商只需要提供以上的接口就行了。
对于Loadbalancer,具体各个厂商实现不同, 但是目前的实现基本都是直接挂载nodePort, 可以看到上述EnsureLoadBalancer
中传递的参数也是nodes列表。 上述的接口我们无法直接使用,需要对其改造, 实现一个自定义的service controller。在EnsureLoadBalancer
的时候传递的参数也应该是pod的IP列表, 我们挂载的是pod而不是node。所以此处需要不断监听pod的变化,然后选择判断该pod是否被service label selector选中,如果选中则该pod是service的后端,需要设置将流量转发到该pod上面, 这里很多熟悉kubernetes的小伙伴就会好奇,这里不是和endpoints的功能一模一样吗? 为什么不直接监听endpoint, 然后将endpoint中的ip列表拿出来直接使用?
要弄明白这个问题,我们需要回顾我们在保证流量不丢的时候设置了readinessGate, 此时pod就绪状态会变为: 容器就绪+LB就绪。但是在endpoint的工作原理中, endpoint controller会判断pod是否就绪,pod就绪之后才会将podIP放在endpoint的结构体中。而我们期望容器就绪之后就在endpoint显示出来,这样我们就可以拿着这个enpoint的ip列表去注册到LB上, LB注册成功之后,pod才能变为就绪。 社区endpoint中iplist的顺序和我们期望的略有差异, 只能自己实现一个类似的结构体了,和社区的使用方式大部分相同, 只是判断就绪的逻辑略有不同。
自定义endpoint的另外一个原因是: endpoint controller会将service选中的所有pod分为ready和unready两组, 当pod刚启动时, 还未通过readiness探针检查时会将pod放置在unReadAddress列表中,通过readiness检查后会移动到address列表中,随后在退出时会直接将pod移出address列表中。 在我们的场景下,更加合理的逻辑应该是在退出过程中应该从endpoint中address列表移动到unReadyAddress列表,这样我们就可以根据unReadyAddress来决定在退出的时候将哪些podIP在LB上面将权重置为0。
自定义endpoint controller并没有更改kubernetes原来的endpoint controller的代码, 这里我们只是作为一个内部的数据结构体使用, 直接结合在service controller中即可,也无需监听endpoint变化,直接监听pod变化生成对应的service 即可。
在落地kubernetes的过程中, 相信kube-proxy被不少人诟病,甚至有不少公司完全抛弃了kube-proxy。 不好的东西我们就要积极探索一种更好,更适合公司内部情况的解决方案。目前该满足了不同业务上云时的网络需求,承载了不同的流量类型。 同时很好地应用在多云环境下,私有云和公有云下都可以适配, 尽管私有云或者公有云的底层网络方案或者LB实现不同,但是整个架构相同,可以无缝地在私有云,aws, 阿里,金山云直接迁移。
kubernetes的快速发展为我们带来了很多惊喜,但是于此同时很多细节的地方需要打磨,需要时间的沉淀才能更加完美, 相信在落地kubernetes的过程中不少人被kubernetes的网络模型所困扰,此时我们需要根据企业内部的情况, 结合已有的基础设施,根据社区已经提供的和尚未提供的功能进行一些大胆的微创新,然后探索更多的可能性。
2024-07-07
Java框架如何实现非阻塞式编程?2023-03-11
Android Jetpack 组件LiveData源码解析2023-03-11
hbuilderx设置Firefox浏览器安装路径教程 hbuilderx怎么设置Firefox浏览器安装路径?一、AVL树的概念 二、AVL树节点的定义 三、AVL树的插入 四、AVL树的旋转 1.左单旋 2.右单旋 3.左右双旋 4.右左双旋 五、进行验证 六、AVLTree的性能...
2023-03-09