当前位置: 首页 > 科技观察

重构

时间:2023-03-19 10:31:37 科技观察

如有异议,大家好,我是小楼。我前段时间不是很忙吗?我忙的一件事是花了一些时间重构了一个服务的健康检查组件,这个服务已经在灰度慢慢上线了。本文将分享这段重构之旅,也算是总结一下吧。背景服务健康检查介绍服务健康检查是针对分布式应用中服务节点不健康问题的解决方案。如下图,消费者调用提供者集群,通常是通过注册中心获取提供者的地址,根据负载均衡算法选择特定的机器发起调用。假设一台机器意外宕机,服务消费者无法感知,就会导致流量丢失。如果有一种机制可以检测服务节点的健康状态并及时淘汰,可以大大提高在线服务的稳定性。原服务健康检查的实现原理我们是自研的注册中心,健康检查也是注册中心的一部分。原理很简单,可以分为三个阶段:从注册中心获取待查实例(即地址,由ip,端口组成)对每个地址发起TCP建链请求,链接成功建筑物被认为是健康的。判断为不健康的实例将被移除,恢复原来不健康现在健康的实例。移除恢复是通过调用注册中心提供的接口实现的。当然,这是大概的流程,还有很多细节。例如,一些不需要检测的服务在获取检测到的实例时会被排除掉(比如MySQL、Redis等一些基础服务);一些判断策略,比如连续N次建立连接失败,就认为是不健康的;移除不健康实例时,也会计算移除阈值。拣货和不拣货区别不大(请求会报错),即使是全拣货也还是要承担风险。考虑到集群容量,可以设置一个阈值,比如最多只挑三分之一的机器。原生服务健康检查存在的问题1.容量问题原生组件是物理机时代的产物。当时实例数量并不多,所以本来就是单机设计,只部署在一台物理机上。随着公司业务的发展,实例数量增加,单机达到了瓶颈,于是做了升级,通过配置文件指定各个节点的健康检查任务分片。2.容灾问题单机难免有宕机的风险。即使巡检任务已经分片,也不能动态分配,因为是写在配置里的。当一个节点宕机时,它负责的实例的健康检查就会失效。3、部署效率问题部署在物理机上,分片写在配置里,无论是扩容还是机器超保更换,都要修改配置,人操作效率太低,容易出错。4.新需求支撑效率问题随着云原生时代的推进,对健康检查提出了一些新的需求。比如仅仅端口的连通不一定代表服务的健康,甚至公司还有一些其他的服务没有在注册中心注册。网络上的服务也想复用这个健康检查组件的能力,不断增长的需求与原有组件沉重的历史包袱之间存在着不可调和的矛盾。5.迭代过程中的稳定性问题。原始组件没有灰度机制。开发新功能并推出它需要很短的时间。有太多的问题需要解决。如果改变了原有的基础,稳定性和效率都会很麻烦,于是一个想法油然而生:重构!技术方案研究行业通用服务健康检查方案在设计新方案之前,我们先看看行业是如何做健康检查的,从两个角度进行研究,注册中心健康检查和非注册中心健康检查注册中心健康检查方案代表产品优缺点SDK心跳上报Nacos1.x临时实例处理心跳太耗资源SDK长连接+心跳维护Nacox2.x临时实例、SofaRegistry、Zookeeper感知快速SDK实现复杂集中主动健康检查Nacos永久instance无需SDK参与,即可实现语义探测。当集中压力大时,延迟会增加。非注册中心健康检查K8S健康检查-LivenessProbe与集中式健康检查对比LivenessProbe原生健康检查组件实现方式k8s原生,分布式(sidecar模式)自研,集中检查发起者kubelet,服务集中部署在同一台物理机上业务容器适用范围k8s容器(弹性云)容器、物理机、虚拟机等支持的检查方式tcp、http、exec、grpctcp、http健康检查基本配置容器启动延时检查时间、检查间隔时间、检查超时时间、最小连续成功次数,最小连续失败次数检查超时时间,连续失败次数,最大去除率当检测到不健康的动作杀死容器时,容器然后根据重启策略决定是否重启。注册中心去掉底线,根据公司背景选择可以配置和去掉的比例。我们的背景是技术栈不统一。编程语言有Java、Go、PHP、C++等,根据成本。考虑到这一点,我们更喜欢瘦SDK解决方案。因此排除注册中心常见的SDK长连接+心跳维护方案,不考虑SDK主动上报心跳。K8S健康检查方案只在K8S系统中使用,我们还有物理机,K8S的LivenessProbe不能开箱即用。至少我们不希望节点在不健康的时候被杀死,底线策略需要重新制定。所以最终我们选择了和原来的健康检查组件一样的方案——中心化主动健康检查。理想状态基于原有健康检查组件在使用过程中出现的各种问题,我们总结出一个好的健康检查组件应该是什么样子:自动故障转移、水平扩展、快速支持丰富灵活的需求和新的需求迭代,以及自身的稳定性需要保证设计开发的整体设计组件由四个模块组成:根据健康检查的结果改变健康状态Performer:根据决策结果执行相应的动作,各模块对外暴露接口,隐藏内部实现。数据源面向接口编程,可替换。服务发现模型在详细介绍各个模块的设计之前,先简单介绍一下我们的服务发现模型,有助于后续的表达和理解。服务名称在公司内是唯一的。调用时需要指定服务名,获取对应的地址。一个服务可以包含多个集群。簇可以是物理上隔离的簇,也可以是逻辑上隔离的簇,簇中包含地址。我们选择Go作为协程模型设计的编程语言。原因有二:一是健康检查这种IO密集型任务,比较适合Go的协程调度,开发速度和资源占用都还不错;二是我们团队一直在使用Go,经验丰富,所以在语言的选择上没有考虑太多。但是在协程模型的设计上,我们做了一些思考。对于数据源的获取,由于服务和集群信息不经常变化,缓存在内存中,每分钟同步一次,需要实时拉取地址数据。Dispatcher先获取所有服务,然后根据服务获取集群。至此,在一个协程中完成。接下来获取地址有网络开销,所以开了N个协程,每个协程负责一部分集群地址。每个地址生成一个单独的任务,该任务被分派给探测器。Prober负责健康检查,完全是IO操作。内部是用一个队列来存放派发的任务,然后开很多协程从队列中取出任务进行健康检查。检查完成后,将结果交给Decider进行决策。做决定时,更重要的是弄清楚是否会被覆盖。这里需要考虑两点:一是最初获取的实例状态可能不是最新的,需要重新获取;二是同一个集群不能同时做决策。决策应按顺序进行,以免造成决策混乱。举个反例,如果一个集群有3台机器,那么最多移除1台机器。如果2台机器同时挂掉,在并发决策的时候,2个协程都认为自己可以被pick。最后的结果是Twounitswereremoved,这与预期只会移除一个单元不一致。如何解决这个问题?我们最后设置N个队列来存放健康检查结果,并根据服务+集群的hash值路由到队列中,保证每个集群的检测结果路由到同一个队列,然后开启N个协程,并且每个协程消费一个队列,实现了顺序执行。决策后的action执行是调用update接口,所以直接共享决策协程。用一张大图来概括一下:Horizo??ntalexpansion&failoverautomatictransferHorizo??ntalexpansion&failoverautomatictransfer只要能做到数据动态分片,每个健康检查组件都会在启动时向一个centralcoordinator注册自己(可以是etcd),以及监控其他节点的在线状态。调度任务时,根据服务名进行hash,判断任务是否自行调度。如果是,则执行它,否则丢弃它。当一个节点挂掉或扩容时,其他节点可以感知到当前集群的变化,自动对数据进行重新分片。小流量机制小流量的实现采用部署两个集群的方式,一个普通集群,一个小流量集群。小流量集群负责一些不重要的服务。作为灰度,普通集群负责其他服务的健康检查任务。您只需要共享一个小流量配置。我们按照组织、服务、集群、环境等维度来设计这个配置。基本上,它可以以任何粒度进行配置。Scalability可扩展性也是设计中非常重要的一部分,我们可以从数据源、检查方法扩展、过滤器等方面来谈。数据源可插拔,面向接口编程。我们将数据源抽象为读数据源和写数据源。只要数据源符合这两个接口,就可以无缝对接。检查方法易于扩展。健康检查其实就是给个地址,加一堆配置去检查。至于怎么查,可以自己实现。目前有TCP和HTTP方式。未来可能还会实现Dubbo、gRPC、thrift。和其他语义级检查方法。filter在派发任务的时候,有一个逻辑可以随时修改,过滤掉一些不需要检查的服务、集群、实例。使用责任链模型可以很好地实现这一点。如果以后要添加或删除,只需要插入链中的一个环节即可。可扩展性在代码层面,所以这里只列出一些典型的例子。灰度上线由于我们重写了一个组件来替换原来的组件,所以我们需要顺利地替换旧系统。为此,我们做了两件事:设计了一个可以按组织、服务、集群、环境等维度维度的降级开关,降级分为3个级别,不降级、半降级、全降级。不降级很好理解,就是正常工作,全降级就是检查,不清除不恢复,相当于空跑,半降级就是只恢复健康不清除。试想一下,如果在上线过程中误将健康检查去掉,此时降级,岂不是无法恢复健康?所以我们让它保持弹性。我们采用上述的小流程设计,逐步将服务迁移到新的组件中。灰度服务的新组件负责,非灰度服务的旧组件负责。全部灰度完成后,停止旧组件,对新组件进行灰度化。然后集群切换到正常集群。在踩坑调优的灰度过程中,我们发现了一个问题。有的集群机器数量很大,有1000多台机器,我们的决策是顺序执行的,有时候我们会实时查询实例状态。假设平均A决策需要10ms,完成1000个单元的顺序决策需要10s。我们期望每轮检测在大约3秒内完成。仅仅这个集群就需要10秒,这显然是不能接受的。给我们做了第一次优化:当时我们在测试线上环境。一个集群有2000多台机器,但大多数都被禁用了。不管健康不健康,都不会被消费,所以我们的第一个优化就是在派发任务的时候过滤掉不健康的机器,这样就解决了离线环境的问题。但是到了生产环境,还是发现决策很慢。在线集群中只有少量机器被禁用。第一次优化基本没有效果。而且在线的机器数量可能会更多,任务堆积会很严重。我们发现其他队列可能比较空闲,只有大集群所在的队列比较忙。所以我们进行了第二次优化:从业务角度来说,其实只需要顺序判断不健康的实例,健康实例的判断不需要考虑底线,所以我们会根据检查进行分类结果,并将健康的检测结果随机分发到任意队列处理,不健康的检测结果严格按照服务+集群处理路由到特定队列,既保证了自下而上的决策顺序,又解决了队列负载不均衡的情况。总结本文从健康检查的背景、原有组件存在的问题以及我们的理想状态出发,调研业界的解决方案,结合实际情况,选择合适的解决方案,并对之前系统存在的问题进行总结设计一个更合理的新的。系统,从闭环开发到上线。我认为系统设计是一个取舍的过程。别人的方案不一定是最好的,适合的才是最好的,有时候解决问题并不是单纯的技术。站在商业角度思考或许更有启发性。

猜你喜欢