介绍:在分布式场景下,微服务进程以容器的形式存在,并在k8s等容器调度系统的支持下运行。容器组Pod是K8S单元的最小资源。随着服务的迭代和更新,当新版本上线时,需要更换在线运行的服务来发布新版本。作者|李志新来源|阿里巴巴科技公众号一背景1优雅的离线与离线在分布式场景下,微服务进程以容器的形式存在,在k8s等容器调度系统的支持下运行,容器组Pod是K8S的最小资源单元。随着服务的迭代和更新,当新版本上线时,需要更换在线运行的服务来发布新版本。在稳定生产过程中,容器调度完全由k8s掌控,微服务治理由服务框架或运维人员维护管理。在发布新版本或扩容或缩容的情况下,旧的容器实例将被终止并替换为新的容器实例。对于承载大流量的线上生产环境,如果替换进程的连接出现问题,将导致短时间内出现大量错误请求,触发告警,甚至影响正常业务。对于规模较大的厂商来说,发布过程中出现问题造成的损失将是巨大的。因此,优美线上线下的诉求被提出。这就要求服务框架在具备稳定的服务调用能力和传统服务治理能力的基础上,在上线和下线过程中提供稳定的保障,从而降低运维成本,提高应用稳定性。2预期效果我认为优雅离线下线的理想效果是在一个承载大量流量的分布式系统中,所有的组件实例都可以随意扩展、缩减、滚动。在这种情况下,需要保证更新过程中稳定的tps(每秒请求数)和rt(请求延迟),保证不会因为上线或下线导致请求错误。更进一步是系统的容灾能力,可以保证在一个或多个节点不可用时,流量的合理调度,尽可能减少错误请求的发生。Dubbo-go的优雅退出能力Dubbo-go对优雅退出的探索可以追溯到三年前。早在1.5早期版本,Dubbo-go就已经具备了优雅退出的能力。通过对终止信号量的监听,实现反注册、端口释放等善后工作,去除流量,保证正确响应客户端请求。前段时间,随着Dubbo-go3.0的正式发布,我在一个proposalissue(dubbo-goissue1685)[1]中提到了一些生产用户看重的问题,作为3.x版本方向的力量,请大家谈谈他们对这些方向的看法。其中,最受用户欢迎的功能是可以无损登录和退出。再次感谢社区王晓伟的贡献。经过不断完善和生产环境测试,Dubbo-go已经具备了该能力,将在后续版本中正式与大家见面。2Dubbo-go优雅线上线下实现思路优雅线上线下离线可以分为三个角度。服务器上线,服务器下线,客户端容灾策略。这三个角度保证了生产实例在正常的发布迭代过程中不会出现错误请求。1客户端负载均衡机制以Apache顶级项目Dubbo为例的微服务架构在此不再赘述。在分布式场景下,即使在K8S中,大部分用户也会使用第三方注册组件提供的服务发现能力。从运维成本、稳定性、分层解耦等角度考虑,除了一些特殊情况,很少会直接使用原生服务进行服务发现和负载均衡,所以这些能力成为了微服务框架的标配能力。熟悉Dubbo的同学一定知道,Dubbo支持多种负载均衡算法,通过可扩展的机制集成到框架中。Dubbo-go也是如此。针对多实例场景,可以支持多种负载均衡算法,如RR、随机数、灵活负载均衡等。下图摘自Dubbo官网Dubbo-go的负载均衡组件Dubbo-go服务框架有一套接口级的扩展机制,可以根据配置加载同一个组件接口的不同实现。其中随机算法负载均衡策略,是Dubbo-go默认的负载均衡算法。在使用这种算法进行负载均衡的情况下,所有的provider都会按照一定的权重策略随机选择。所有提供者实例都可能在下游。这种比较传统的负载均衡算法会带来隐患,即前一次调用的结果不会影响后续调用过程中下游实例的选择。因此,如果下游部分实例处于下线阶段,导致服务短时间不可用,所有对该实例的随机请求都会报错,在大流量场景下,会造成巨大的损失。集群重试策略下图摘自Dubbo官网。Dubbo-go的集群重试策略是借鉴了Dubbo。默认情况下,使用故障转移(failover)逻辑。当然还有failback、fallfast等策略,同样依赖组件扩展性的整合。框内。无论是上面提到的负载均衡,还是重试逻辑,都是基于“面向切面编程”的思想,构造一个抽象的调用者实现,从而将流量逐层传递给下游。对于Failover策略,会在负载均衡选择下游实例的基础上增加错误请求重试的逻辑。一旦请求报错,将选择下一个调用者进行尝试,直到请求成功或超过最大请求数。集群重试策略只是增加了尝试次数,降低了错误率,但本质上仍然是无状态的。当下游服务不可用时,将造成灾难性的后果。黑名单机制黑名单机制是我去年实习安排的第一个要求。总体思路很简单。将抛错请求的invoker对应的实例ip添加到黑名单,然后不再向该实例导入流量等,过一段时间再尝试请求,成功则从黑名单中移除。该机制的实现逻辑非常简单,但本质上是将无状态负载均衡算法升级为有状态负载均衡算法。对于不可用的下游实例,一个请求会快速阻塞该实例,其他请求会识别该实例存在于黑名单中,从而避免其他流量。对于这个策略,保持在黑名单中的超时时间,试图从黑名单中移除的策略等,这些变量要结合具体的场景来考虑,本质上是一种有状态的故障转移策略。通用性强。P2C灵活的负载均衡算法灵活的负载均衡算法是Dubbo3生态的一个重要特性,Dubbo-go社区正在与Dubbo进行探索和实践。有读者应该看过Dubbo-go3.0之前发表的文章中的相关介绍。简单来说,它是一种有状态的负载均衡策略,不像黑名单那样“一刀切”。它考虑的变量范围更广,也更全面。它会在P2C算法的基础上考虑各个下游实例的请求。延迟、机器资源性能等变量,通过一定的策略来确定下游实例最适合,具体的策略,结合具体的应用场景,有兴趣的社区成员自行探讨。目前由牛雪薇(来自字节跳动的github@justxuewei)负责。上面的很多负载均衡策略都是站在客户端的角度,尽量让请求访问到健康的实例。从无损注销和下线的角度出发,客户端可以通过合理的算法和策略,比如黑名单机制,在发布阶段过滤掉异常工作的实例。我觉得客户端负载均衡是一个通用的能力,对于无损的上下线场景的作用只是锦上添花,不是核心要素。本质上还是要从“下线”的服务器实例上去考虑,才能从根本上解决问题。2服务端优雅的在线逻辑与客户端相比,服务端是服务提供者,是用户业务逻辑的实体。在我们讨论的场景中,逻辑更加复杂。在讨论服务器端之前,让我们回顾一下基本的服务调用模型。传统的服务调用模型参考Dubbo官网给出的架构图。完成一次服务调用,一般需要三个组件:注册中心、服务器、客户端。服务端首先需要暴露服务并监听端口,使其具备接受请求的能力。服务器将当前的服务信息,如ip、port等注册到一个集中的注册中心,如Nacos。客户端访问注册中心,获取需要调用的服务ip和端口,完成服务发现。服务调用,客户端请求对应的ip和端口。这四个简单的步骤是Dubbo-go优雅的线上线下策略的核心重点。一般情况下,这四个步骤执行的很顺利,逻辑也很清晰。但是在一个大规模的生产集群中,服务上线下线会有很多值得考虑的细节。我们要明白,上线下线的过程中,错误是怎么产生的?我们只需要注意两个错误,即:“一个请求被发送到一个不健康的实例”和“正在处理请求的进程被杀死”。几乎所有上线下线过程中的错误都来自于他们。优雅的在线服务逻辑详解服务上线后,按照上面的步骤,首先暴露服务,监听端口。在确保服务提供者可以正常提供服务后,它会在注册中心注册自己的信息,这样来自客户端的流量就会发送到自己的ip上。这个顺序一定不能乱,否则会出现服务还没准备好收到请求的情况,导致出错。以上只是简单的案例。在实际场景中,我们所说的服务器实例通常包括一组相互依赖的客户端和服务器。在Dubbo生态的配置中,称为Service(服务)和Reference(引用)。举个商科学生非常熟悉的例子,在一个服务函数中,会执行一些业务逻辑,调用下游的多个服务。这些下游服务可能包括数据库、缓存或其他服务提供者。执行完返回得到结果。这对应了Dubbo生态的概念。它的实现是:Service负责监听端口和接受请求。接受的请求会向上层转发给应用业务代码,开发者编写的业务代码会通过客户端请求,即Reference。下游对象。当然这里下游的协议很多,我们只考虑dubbo协议栈。从上面提到的常见服务模型,我们可以认为Service是依赖于Reference的,一个Service的所有Reference必须正常工作,当前Service才能正确接受来自上游的服务。这也推断出Service应该在Reference之后加载。当所有的References加载完成后,确保这些客户端可用,然后加载Service,暴露工作服务,最后注册到注册中心,调用upstream调用。另一方面,如果Service已准备好但Reference尚未准备好,则会导致请求错误。因此,服务上线逻辑为Consumer加载->Provider加载->Registry服务注册。有的读者可能会想,如果Consumer依赖于当前实例本身的Provider怎么办。Dubbo的实现可以不通过网络直接发起函数调用。Go也可以这样处理,但是实现还有待开发。这种情况比较少见,更多的是上面提到的熟悉的情况。3、服务端优雅的离线逻辑服务离线相对于在线服务,需要考虑的点更多。让我们回到上一节提到的服务调用模型的四个步骤:服务端首先需要暴露服务并监听端口,这样才能具备接受请求的能力。服务器将当前的服务信息,如ip、port等注册到一个集中的注册中心,如Nacos。客户端访问注册中心,获取需要调用的服务ip和端口,完成服务发现。服务调用,客户端请求对应的ip和端口。如果某项服务即将下线,一定要做好相关的善后工作。现在线上的情况是这样的:客户端不断的请求当前实例。如果此时终止当前进程,一方面,瞬间会出现大量的tcp连接失败,所以只能寄希望于第1章提到的客户端负载均衡策略;另一方面,大量的处理请求被强制丢弃。这不优雅!所以当实例知道自己要终止时,首先要做的就是告诉客户端:“我的服务要终止了,切断流量”。这个体现在实现上,就是从注册中心删除自己的服务信息。客户端获取不到当前实例的IP后,将不会发送请求。这个时候终止进程才是优雅的。以上只是一个简单的案例。真实场景下,客户端可能不会这么快断流,当前服务手里还有大量的处理任务。如果贸然终止进程,可以形象地理解为手中的锅。水洒得到处都是。有了这些铺垫,我们就来详细说下服务下线的步骤。上面的故事中提到了gracefuloffline的使用和触发,进程首先要知道自己是“待终止”,这样才能触发gracefuloffline的逻辑。该消息可以是信号量。当k8s要终止容器进程时,kubelet会向进程发送SIGTERM信号量。Dubbo-go框架中预置了一系列终止信号量的监听逻辑,使得进程在收到终止信号后,仍然可以控制自己的动作,即执行优雅的离线逻辑。但是,某些应用程序会监视SIGTERM信号以处理离线逻辑。比如关闭db连接,清除缓存等,尤其是作为接入层的网关类应用,web容器和RPC容器是同时存在的。这时候,先关闭web容器还是先关闭RPC容器就显得尤为重要。所以Dubbo-go允许用户通过配置internal.signal来控制信号信号监听的时机,通过graceful_shutdown.BeforeShutdown()在合适的时间优雅的关闭rpc容器。同样,Dubbo-go也允许用户在配置中选择是否开启新号监控。注销如上所述,服务器需要告诉客户端它即将终止。这个过程就是通过注册中心注销。常见的服务注册中间件,如Nacos、Zookeeper、Polaris等,都会支持服务反注册,将删除动作以事件的形式通知上游客户端。客户端必须时刻监控注册中心。请求能否成功很大程度上取决于注册中心的消息是否被客户端及时监听和响应。在Dubbo-go的实现中,客户端会在第一时间拿到删除事件,将调用者对应的实例从缓存中删除。这样可以保证后续的请求不会再流向调用者对应的下游。反注册的过程虽然很快,但毕竟是三个组件之间的事情,不能保证瞬间完成。于是就有了下一步:等待客户端更新。跟后面的步骤有点关系,现阶段只进行了反注册,不能进行反订阅,因为在优雅离线执行的过程中,还是会有自己client的下游请求。如果反订阅,将无法从下游接收更新,可能会导致错误。等待客户端更新优雅退出逻辑注销后,服务端无法快速kill当前服务,而是会短时间阻塞当前优雅退出逻辑。这段时间由开发者配置。默认是3s,应该比从Deregistration到client删除缓存的时间要长。经过这段等待更新的时间,服务端可以认为客户端没有发送任何新的请求,所以可以亮红灯,逻辑是拒绝所有新的请求。在这里等待上游的请求完成,还是杀不掉当前进程。就好像手里拿着一盆水。你之前所做的只是把装满水的水龙头留下,而没有把脸盆里的水倒掉。所以我们要做的就是等待,等待当前实例处理完来自上游的所有请求。服务器会在第一层过滤器维护一个并发安全计数器,记录进入当前实例但还未返回的请求数。优雅的离线逻辑此时会轮询计数器。一旦计数器归零,就认为上游不再有请求,手上的上游的水就会被倒掉。等待自己发送的请求得到响应这一步,在整个链路中,完全去掉了自己的上游请求。但是,下游请求仍然未知。此时可能会有大量请求被当前实例发送但没有得到响应。如果此时贸然终止当前进程,将会引起不可预知的问题。因此,与上述逻辑类似,服务在客户端过滤器中维护了一个线程安全的计数器,由优雅的离线逻辑轮询,等待所有请求返回且计数器归零后,才完成这一阶段的等待。如果当前实例中有客户端不断向下游发起请求,计数器可能不会一直归零,那么就需要依赖这个阶段的超时配置来强行结束这个阶段。销毁协议,释放端口此时,可以放心大胆的做最后的工作了,销毁协议,关闭监听,释放端口,从注册中心退订。用户可能希望在注销逻辑完全完成并释放端口后执行一些逻辑,因此可以为开发者提供回调接口。3.优雅注销注销的效果根据上面的介绍,我们在集群中进行了压测实验和模拟注销注销实验。使用一个客户端实例、5个代理实例和5个提供者实例。请求链接为:client->proxy->provider。因为资源问题,我们选择让客户端保证5000tps的压力,通过dubbo-go的prometheus可视化界面暴露成功率和错误请求数,然后进行滚动发布、扩容、对链路中间的代理实例和链路下游的提供者实例进行缩容、实例删除,模拟生产发布过程。这期间记录了很多数据,可以展示比较明显的对比。不要使用优雅的注销逻辑:更新时成功率明显下降,错误数持续上升,客户端被强制重启。优雅的离线离线优化:无错误请求,成功率保持在100%4Dubbo-go对服务治理能力的展望Dubbo-gov3.0于去年底正式发布,至今已有一个多月现在,3.0的发布对我们来说并不是圆满成功,而是我们对未来愿景迈出的新一步。我们即将发布3.1版本,它将具备优雅下线的能力。在3.0的筹备阶段,我想过一个服务框架如果从传统设计走向未来,需要一步步往下走,一定有多个路径:从最基础的人性化支持,配置重构,以及易用性能、集成测试和文档构建;实现传输协议(Dubbo3)Triple-go的跨生态、稳定、高性能、可扩展、生产可用;然后是我们3.0发布后的服务治理能力、运维能力、可视化能力和稳定性,包括优雅的上下线、流量管理、proxyless;然后形成生态,跨生态融合。这样,我们就可以一步一个脚印,不断积累,不断迭代。运维能力和服务治理的丰富和优化将是后续版本的重要特性。我们将进一步完善流量治理、路由、ProxylessServiceMesh,以及文中提到的灵活的负载均衡算法。社区工作的重点。原文链接本文为阿里云原创内容,未经许可不得转载。
