# vue-flow **Repository Path**: opensnail/vue-flow ## Basic Information - **Project Name**: vue-flow - **Description**: 12342342341234123 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-08-02 - **Last Updated**: 2025-12-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README 前言 在当今微服务架构盛行的背景下,系统被拆分为多个小服务,服务间频繁的RPC调用可能因网络抖动导致失败。通过重试机制,我们可以提升请求的成功率,降低故障影响,从而增强系统的稳定性。此外,也会面临服务拆分带来的事务一致性挑战,大多数情况下我们不会采用分布式事务,因为它复杂且影响性能,通常通过重试补偿来实现最终一致性。因此不得不让我们重新审视重试机制的作用。 重试的陷阱 可以短暂停留几秒思考一下遇到重试场景的时候我们应该怎么做? 是不是曾经或者现在正在写过类似下面的代码? public class RetryKit { public static T retry(Supplier supplier, int maxAttempts, long delayMs) { Exception lastException = null; for (int attempt = 1; attempt <= maxAttempts; attempt++) { try { return supplier.get(); } catch (Exception e) { lastException = e; if (attempt < maxAttempts) { try { Thread.sleep(delayMs); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException("重试被中断", ie); } } } } throw new RuntimeException("重试失败,已尝试 " + maxAttempts + " 次", lastException); } } public class AddOrderEventMQ { // MQ的消费者 public void consume() { // 业务逻辑 ...... // 调用第三方发起重试 RetryKit.retry(new Supplier() { @Override public Void get() { // 请求其他的服务 Result r = call(); if (r.getCode != 200) { throw new RuntimeException("接口异常"); } } }, 3, 1); } } 其实这是一个足够简单的重试案例,当请求遭遇异常,无法返回预期的结果时。则通过循环进行三次重试。但是这样简单的写法存在一个最直观的问题, 1. 重试业务的代码对于正常业务的侵入性太高,导致代码的可读性变差 2. 引发重试风暴的风险, 假设MQ也重试3次 那么总共就会重试 3 * 3 = 9次 虽然这个只是重试的一个极其简单的案例但是足够让我重新审视一下重试的危害 重试风暴 所谓重试风暴,指的是当下游被重试的服务出现故障时,上游的业务获取下游成功的状态码,故而无脑重试,由此引发的一系列问题。 我们一起来看重试风暴可能会引发的问题的场景。 1.2.1 重试导致的链路放大效应 假设有四个服务A、B、C、D,他们的调用关系是A调用B,B调用C和D。在微服务中,熔断和限流是非常常见的动作,假如A服务的调用量激增,触发了B服务的限流。这个时候B服务没有办法返回A服务理想的成功状态码,此时A服务就会发起重试,由此将会带来两个重大恶果。 - A服务的重试会导致B服务请求调用量激增,由此引发B服务的下游C和D服务量激增,下游也会出现熔断风险 [图片] - 当触发限流之后,B服务的请求中会充斥着大量的重试请求,由此会导致A服务中正常请求不可用的比例增加,造成服务更大范围的不可用 [图片] 同理,假如B服务没有触发限流,而是在高负载状态下导致了短暂的不可用,同样也会导致上图中出现的链路放大效应,可能会诱发出现服务雪崩的风险。 甚至在这样引入重试机制后,当B服务从短暂不可用状态恢复,由于A服务中包含了大量的重试,使得瞬时实际访问量剧增,从而当B服务恢复正常后,整体服务的恢复时间变长。 我们重试治理的关键就是 不能让重试流量反客为主影响用户流量 重试引发的告警升级 这个很好理解,假设用户实际的瞬时请求量是500,当经过重试的n次放大之后则会将请求数量变成500*n,此时如果系统中存在告警处理则会使得告警的数量增加三倍,引发告警的升级。 当然上述讲的这些都是人为因素,我们接下来再来讨论一个这种场景下可能会引发的技术问题。下面是一个真实的线上事故 这个业务中Api层交由Nginx来做负载均衡,然后Api层再去调用RPC进行数据读取操作,重试机制做在了Api层,此时出现了一个bug,由于Api层某个参数传递错误,导致RPC服务返回5xx错误。 随后Api层接收到5xx错误后进行重试,导致5xx的问题翻倍。 由于接入了Nginx,此时触发了Nginx的健康检查机制,由于Nginx发现了大量 5xx 错误时,Nginx 可以自动将该节点从负载均衡的池中摘除。 随后导致Api节点被逐个摘除,然后出现了服务大规模的不可用,导致了生产事故。 [图片] 尽管在这个案例中,重试机制不是直接原因。但是毋庸置疑的是由于引入了这种重试,导致原本可控的问题被放大,演变成了更加严重的问题。 重试的蝴蝶效应 [图片] 我们假设微服务的调用方式如上图所示,当服务E出现异常时,服务E上报异常,随后触发服务C重试,而服务C调用服务E异常后同样选择抛出异常,这样子又会引发服务B重试,假设每个服务都接入重试机制,每个服务的重试次数为n,这样子就会导致最终的请求变成了n的4次方。 同样也会引发服务的不稳定,甚至可能造成雪崩。 如何优雅重试呢? 因此,如何降低重试机制的使用成本,并探索更优雅的重试策略,是一个值得深入研究的问题。 2.1 重试风暴问题 在单点重试的场景中,一个服务不能无限制的对下游发出重试请求,这样会增加下游被打挂的场景。因此需要限制重试次数的上线和重试请求的成功率。 同时由于在微服务体系中重试可能会在多级链路中引发指数级调用量增长,我们还需要限制重试发生在微服务的每一层均发生,最为理想的情况是重试仅仅发生在服务的最下一层。 2.2 退避策略 在某些场景下,会出现一些暂时性的错误,如网络抖动、服务重新部署等场景,可能立即发起重试结果依然是失败的,通常等待一段时间后再重试的话成功率会较高,并且也可以分散上游重试的时间,可以避免因为同时都重试而导致的下游瞬间流量高峰。决定等待多久之后再重试的方法叫做退避策略,在重试系统中支持灵活的退避策略是很必要的。 2.3 动态配置 当指定重试策略后,处置策略可能会因为业务场景的变化而变动,在这种情况下,如果我们需要每次通过代码编译部署上线的流程去修改其中的一些重试参数的话,会使得我们的使用非常不便捷,因此如果重试组件中可以支持动态配置,我们就可以通过后台修改配置的方式,根据业务场景的变化而修改策略。 重试的常见解决方案 暂时无法在飞书文档外展示此内容 注: 携程落地实现的重试方案基于SnailJob的分布式重试为基础实现,内部名称SnailRetry SnailRetry简介 SnailRetry是一款服务治理重试组件,具备操作简便、实时监控、后台配置等优势,支持多种退避策略和告警方式。它提供本地和远程重试模式,并通过管理后台实现重试任务的可视化,方便查看和管理。此外,用户可以在后台配置多种策略,根据不同业务场景进行实时调整。SnailRetry的核心功能在于流量管控,主要涵盖以下场景: - 单机链路管控 - 限制链路重试 - 重试流速管控 - 支持多种退避策略 - 支持可视化配置 - 支持动态关闭重试场景 - 丰富的参数注解(16个) 流量管控的实现 那么我们来看一下,核心功能点中的流量管控是怎么做到的呢? 主要是通过两种方式: 单机多注解循环引用问题 重试执行过程中标记了重试入口,触发重试时只从标记的重试入口进入 [图片] 链路重试流量管控 对于重试的请求,我们在请求头中下发一个特殊的标识(easyRetry:boolean), 在 Service A ->Service B ->Service C 的调用链路中,当Service B 收到Service A 的请求时会先读取这个 easyRetry 判断这个请求是不是重试请求, 如果是,那它调用Service C 即使失败也不会重试;否则将触发重试 。 同时Service B 也会把这个 easyRetry 下传,它发出的请求也会有这个标志,它的下游也不会再对这个请求重试。 [图片] 特殊的 status code 限制链路重试 如果每层都配置重试可能导致调用量指数级扩大,这样对底层服务来说压力是非常之大的, 通过对流量的标记 ,用户可以判断是否是重试的流量来判断是否继续处理,我们使用 Google SRE 中提出的内部使用特殊错误码的方式来实现: 1 统一约定一个特殊的 status code ,它表示:调用失败,但别重试。 2 任何一级重试失败后,生成该 status code 并返回给上层。 3 上层收到该 status code 后停止对这个下游的重试,并将错误码再传给自己的上层。 这种方式理想情况下只有最下一层发生重试,它的上游收到错误码后都不会重试,但是这种策略依赖于业务方传递错误码, 对业务代码有一定入侵,而且通常业务方的代码差异很大, 调用 RPC 的方式和场景也各不相同,需要业务方配合进行大量改造, 很可能因为漏改等原因导致没有把从下游拿到的错误码传递给上游。 重试流速管控 1、耗时: 当前调用命令耗时大于N,管控重试流速 2、成功率: 当前调用命令成功率小于N, 管控流速 丰富的注解 暂时无法在飞书文档外展示此内容 重试策略 SnailRetry开发了多种重试模式,以满足不同场景下的重试需求。 暂时无法在飞书文档外展示此内容 [图片] 可以看到本地重试模式中仅仅是使用内存来完成重试,不涉及到和服务端的交互。 而远程重试模式则是在遭遇异常后构建异常快照,随后进行重试数据上报,上传重试数据到服务端,服务端进行数据持久化后再由调度器提取上报的异常快照,还原重试的上下文后回调客户端进行重试。 先本地重试,再远程重试模式顾名思义则是两者的结合,优先在本地进行重试,如果本地重试后依然返回异常结果,则进行数据上报,将未完成的异常数据上报至服务端,根据服务端指定的策略进行下一步的处理方案。 回调任务 回调任务是SnailRetry引入并应用的一个功能,使客户端能够感知任务的状态和结果。这一功能通过在任务完成后触发回调,使客户端可以实时获取任务执行情况,从而增强了系统的交互性和响应能力。通过这种机制,客户端不仅可以监控任务的进展,还可以根据回调信息进行相应的处理和调整,提高了系统的灵活性水平。 [图片] 哪些场景适合使用呢? 在某些业务场景下,需要强制保证将通知、消息等数据发送到目标端接口。由于网络的不确定性以及目标系统、应用、服务的不确定性,可能会造成通知消息的发送失败。 此类场景下可以使用 LOCAL_REMOTE 或者 ONLY_REMOTE 模式进行重试。 消息队列场景 消息队列在业务系统中承担着异步、削峰、解耦的重要角色,保障消息的可达性尤为重要。下面以携程大交通业务下单流程为例: [图片] 订单中心下单完成后会发送下单成功消息,从而解耦了订单和其他业务系统的耦合关系。其他相关的业务系统只需要监听订单的下单成功消息即可完成自己的业务逻辑。 但是,由于网络不稳定、消息队列故障等原因,可能导致消息未发送出去,这时候就需要增加重试流程来保障消息的强可达性。 [图片] 接入 SnailRetry后,您只需要一个简单的注解就能保障消息的强可达性: 代码示例 @Retryable(scene = "create-order-success", retryStrategy = RetryType.ONLY_REMOTE) public void sendCreateOrderSuccessMessage(Message message) { // 发送消息 mqProducer.publish("主题", "key", message); } 回调场景 这里引用一个使用 SnailRetry 的真实案例:携程机车合销场景履约失败回调流程 回调场景可以保证业务方具备任务感知能力, 通过回调任务可以知道任务的执行情况, 若是重试失败做一些兜底逻辑,比如发送告警、通知上游业务失败等, 保证了重试流程完整性. [图片] 用户在购买机票时可以同时下单网约车服务,机票侧会通知用车侧下单. 用车侧完成下单后就会进行履约流程, 若用户发生退订或者系统原因无法履约, 会把用车订单进行退赔, 若退赔失败会进行重试,若重试失败则需要感知重试的状态,通知机票和用车业务组进行处理 下面是具体的回调处理逻辑 public class RestituteCallback implements RetryCompleteCallback { /** * 重试成功后的回调函数 * 参数1-场景名称 * 参数2-执行器名称 * 参数3-入参信息 */ @Override public void doSuccessCallback(String sceneName, String executorName, Object[] objects) { // 重试成功 } /** * 重试达到最大次数后的回调函数 * 参数1-场景名称 * 参数2-执行器名称 * 参数3-入参信息 */ @Override public void doMaxRetryCallback(String sceneName, String executorName, Object[] objects) { // 发送告警、通知上游业务失败 } } 异步场景 在核心接口上,我们总是希望不断提高接口性能。提高接口性能的常用方式包括异步、缓存、并行等。这里我们重点讨论异步场景: [图片] 下单完成后会有一些非核心流程,主要特点是实时性要求不高、耗时较长的操作。一般会将这些流程进行异步化处理: 1. 进程异步化:通过发送 MQ 消息(可参考发送 MQ 场景) 2. 线程异步化:开启异步线程处理,但出现异常会导致数据丢失,因此需要重试保证数据一致性 - 可以使用 LOCAL_REMOTE 先本地重试 - 如果本地重试未解决,则上报服务端 代码示例 @Retryable(scene = "sendEmail", retryStrategy = RetryType.LOCAL_REMOTE) public void sendEmail(EmailDTO email) { // 发送下单确认邮件 String responseStr = restTemplate.postForObject("邮箱地址", email, String.class); } 总结 自5月在携程内部上线以来,SnailRetry已应用于火车票、用车、智行机票等项目,覆盖数百个场景,调度总量已超过100万次。