从容错到限流:保障微服务可靠性的关键策略分析

从容错到限流:保障微服务可靠性的关键策略分析本文探讨了微服务架构中常见的容错与故障应对机制 包括限流 降级和熔断

目录

一、服务访问失败的原因和应对策略

(一)服务访问失败的4大原因和分类

1.硬件失败

2.分布式环境的固有原因

3.服务自身失败

4.服务依赖失败

(二)服务访问的雪崩效应

(三)服务访问失败的应对策略

二、服务容错策略

(一)Failover(失效转移)

(二)Failback(失败通知)

(三)Failsafe(失败安全)

(四)Failfast(快速失败)

(五)Forking(分支机制)

(六)Broadcast(广播机制)

(七)总结

三、服务隔离

1.线程隔离

2.进程隔离

3.集群隔离

4.机房隔离

5.读写隔离

四、服务限流

1.计数器法

2.滑动窗口法

3.漏洞算法

4.令牌桶算法

五、服务降级

1.服务分级

2.服务熔断

3.服务降级与服务熔断的区别

六、总结

参考书籍、文献和资料:


干货分享,感谢您的阅读!

在现代软件架构中,尤其是在分布式系统和微服务架构中,服务的可用性与稳定性是至关重要的。随着系统复杂性的增加,服务间的相互依赖和外部环境的变化,容错与故障应对成为了保障系统稳定性和高可用性的核心策略之一。为了应对瞬息万变的系统负载和突发的流量高峰,限流、降级和熔断等机制已成为构建健壮系统的必备工具。

本篇文章将深入探讨如何通过有效的容错设计、隔离策略以及限流降级机制,在微服务架构中实现系统的高可用性与稳定性。我们将从具体的实践案例出发,分析如何在面临各种系统故障和资源瓶颈时,通过一系列策略的组合保障服务不中断、客户体验不受影响。无论你是架构师、开发人员,还是运维工程师,本文的内容都将为你提供有力的指导,帮助你应对日益复杂的系统挑战。

一、服务访问失败的原因和应对策略

(一)服务访问失败的4大原因和分类

1.硬件失败

一旦出现便是灾难性的,一般分为两类:

例如机房失火、机器损害等不可抗力导致的、发生概率极低的情况;

例如由于日志文件过大导致硬盘无法写入、网络路由无效等可以通过调整硬件状态进行恢复的失败情况。

2.分布式环境的固有原因

分布式系统中会由于网络的三态性、异构系统集成等因素导致远程调用发生异常情况,微服务作为分布式系统的延伸这些问题依旧存在并无法完全消除,只能在设计和实现时加以预防,以及在发生时降低其所造成的影响。

3.服务自身失败

由于设计上考虑不周、代码中存在的问题造成的失败,需要深入分析并找到解决问题的方法。

4.服务依赖失败

服务依赖失败相比服务自身失败造成的影响更大且难以发现和处理,是我们重点考虑的失败原因,因为依赖失败的扩散会导致服务访问的雪崩效应。

(二)服务访问的雪崩效应

服务雪崩效应是一种因 服务提供者 的不可用导致 服务调用者 的不可用,并将不可用 逐渐放大 的过程.如果所示: 

上图中,A为服务提供者,B为A的服务调用者,C和D是B的服务调用者。当A的不可用,引起B的不可用,并将不可用逐渐放大C和D时, 服务雪崩就形成了。

我把服务雪崩的参与者简化为 服务提供者 和 服务调用者, 并将服务雪崩产生的过程分为以下三个阶段来分析形成的原因:

  1. 服务提供者不可用
  2. 重试加大流量
  3. 服务调用者不可用                                                              

服务雪崩的每个阶段都可能由不同的原因造成, 比如造成 服务不可用 的原因有:

  • 硬件故障
  • 程序Bug
  • 缓存击穿
  • 用户大量请求

硬件故障可能为硬件损坏造成的服务器主机宕机, 网络硬件故障造成的服务提供者的不可访问. 
缓存击穿一般发生在缓存应用重启, 所有缓存被清空时,以及短时间内大量缓存失效时. 大量的缓存不命中, 使请求直击后端,造成服务提供者超负荷运行,引起服务不可用. 
在秒杀和大促开始前,如果准备不充分,用户发起大量请求也会造成服务提供者的不可用.

而形成 重试加大流量 的原因有:

  • 用户重试
  • 代码逻辑重试

在服务提供者不可用后, 用户由于忍受不了界面上长时间的等待,而不断刷新页面甚至提交表单.
服务调用端的会存在大量服务异常后的重试逻辑. 
这些重试都会进一步加大请求流量.

最后, 服务调用者不可用 产生的主要原因是:

  • 同步等待造成的资源耗尽

当服务调用者使用 同步调用 时, 会产生大量的等待线程占用系统资源. 一旦线程资源被耗尽,服务调用者提供的服务也将处于不可用状态, 于是服务雪崩效应产生了.

(三)服务访问失败的应对策略

分别站在服务提供者和消费者的角度出发来发现应对服务失败场景的策略和方法,基本原理如下图:

对于服务提供者而言,一旦自身服务发生错误,应该快速返回合理的处理结果;

对于服务消费者而言,重点关注不要被服务提供者所产生的错误影响到自身服务的可用性。

基本策略来看,基本上包括超时、重试和异步解耦。

对于服务消费者而言,为了保护自身服务的可用性,可以使用超时机制降低它所依赖的服务对其造成的影响。同时,设置较短的超时时间有助于解决这个问题。

为降低网络瞬态异常所造成的网络通信问题,可以使用重试机制。

为降低系统耦合度,通过使用一些中间件系统实现服务提供者和服务消费者之间的异步解耦,也能把服务依赖失败的影响分摊到中间件上,从而降低服务失败的概率。

业界一些更为系统的方法和机制确保服务的可靠性有服务容错、服务隔离、服务限流和服务降级。

二、服务容错策略

在分布式系统中,服务可能会遇到各种故障问题,例如网络延迟、服务崩溃等。容错机制的核心思想是通过冗余和重试等手段,确保系统在遇到故障时仍然能够持续运行,并最终恢复到正常状态。冗余可以通过集群来实现,而重试机制则通过不同的策略来解决。以下是几种常见的容错策略,帮助我们根据不同场景来选择最合适的方案

(一)Failover(失效转移)

定义:当一个服务提供者出现故障时,系统会自动将请求转移到另一个可用的服务提供者上,以确保服务的高可用性。同时为防止无限重试,通常对失败重试最大次数进行限制。

实现方式

  • 在集群中保持多个服务实例,通过负载均衡或其他策略来选择健康的实例。
  • 重试机制一般会限制最大重试次数,防止因服务长期不可用而无限重试,造成性能浪费。

适用场景

  • 适合对高可用性要求较高的系统,如金融支付、在线购物等。
  • 适用于能够容忍一定延迟的场景,因为重试会增加一定的响应时间。

举例说明:利用冗余保障系统高可用性

假设我们有一个在线电商平台,系统的核心部分包括商品查询服务库存服务订单服务。为了保证系统的高可用性,我们采用了微服务架构,并且对每个核心服务进行了冗余设计,部署了多个服务实例在不同的节点上。

通过合理设计和部署冗余服务节点,我们可以在服务发生故障时,通过负载均衡和自动恢复机制将流量切换到健康节点,从而确保系统持续高可用。冗余不仅提高了服务的可靠性,还降低了单点故障的风险,使得整个系统能够抵御硬件故障、网络故障等问题,保证用户的业务体验不受影响。

(二)Failback(失败通知)

定义:当服务调用失败时,直接将故障信息反馈给消费者,由消费者根据具体的业务需求来处理异常。

实现方式

  • 在服务调用过程中,当遇到异常时,直接返回错误信息或失败回调,消费者可以选择进行重试、报警或其他操作。
  • 失败后的后续处理由消费者自行决定,可以通过业务逻辑来定义错误的容忍度。

适用场景

  • 当系统允许一定的业务中断或容忍服务调用失败时,适用于这种策略。例如,非核心服务或业务较为简单的场景。

案例说明:如何设计“失败通知”业务逻辑并结合“重试”策略处理错误

假设我们正在为一家银行开发一个微服务系统,其中一个重要的功能是用户资金转账服务。该服务由多个微服务组成,其中包括账户服务(用于查询账户余额和更新账户信息)、支付网关服务(用于处理转账交易)和通知服务(用于通知用户转账状态)。我们希望确保转账失败时能够及时通知用户,并且在必要时进行重试,确保服务的稳定性和可靠性。

在转账服务中,如果支付网关出现故障,系统会先进行多次重试,确保临时问题不会影响用户体验。例如,系统会尝试最多三次,每次延迟10秒。若三次重试都失败,系统会发送失败通知给用户,告知转账未成功,并建议稍后再试,同时记录错误日志供技术支持排查。这个设计结合了重试机制失败通知,既保证了临时故障的容错性,又确保用户及时了解问题,避免了无限重试浪费资源,提升了系统的可靠性和用户体验。

(三)Failsafe(失败安全)

定义:在服务调用异常时,不做任何处理,直接忽略异常,确保服务调用链路不中断。

实现方式

  • 当出现服务故障时,系统不会进行任何补救措施,直接跳过当前步骤。
  • 异常信息通常会被写入日志,供后续分析和排查。

适用场景

  • 适用于非关键业务功能,允许一定的业务错误发生,但需要记录错误信息,方便后期排查。
  • 常用于日志、缓存更新等场景,确保系统继续运行。

案例说明:在线商城系统中的失败安全策略应用

在一个在线商城系统中,用户购买商品时会调用多个微服务,如库存服务、支付服务和配送服务。假设在某次购买中,库存服务出现了问题,无法查询到商品库存。为了保证系统的整体稳定性,系统采用了失败安全策略,即忽略该错误并继续执行其他服务的调用。

尽管库存查询失败,支付和配送服务仍然会被正常调用。此时,系统会记录失败的库存查询请求,并将错误信息写入审计日志。管理员可以通过日志追踪到该请求失败的原因,了解是由于服务不可用还是其他原因。

例如,日志中可能显示:"库存服务请求失败,原因:服务超时"。基于这些信息,技术团队可以分析并解决库存服务的性能问题,从而避免类似问题再次发生。

这种失败安全策略确保了用户的购物流程不受影响,尽管某个非核心环节出现了问题,而日志审计提供了后续修复的依据。

(四)Failfast(快速失败)

定义:当服务调用异常发生时,立即报错,彻底放弃后续操作。

实现方式

  • 系统在遇到故障时立即终止服务调用,不进行重试,也不做任何补救。
  • 适用于只需要快速反馈故障信息,不需要继续等待或执行其他操作的场景。

适用场景

  • 在性能敏感、对响应时间要求极高的系统中,例如金融系统中的单笔交易处理、抢票系统等。
  • 当服务调用的错误会影响到整个业务流程时,避免不必要的等待。

案例说明:高并发查询中的快速失败策略

在一个新闻推荐系统中,用户每天会向系统请求个性化新闻推荐。该系统需要调用多个数据源服务来汇总推荐信息,比如热点新闻、用户历史记录、社交媒体动态等。在高并发场景下,系统需要高效地返回结果,而不是等待所有数据源的响应。

假设某个外部数据源由于网络延迟或故障无法及时响应。如果继续等待该数据源的响应,会浪费大量的系统资源,导致其他请求的响应时间变长,甚至影响系统的整体性能。因此,系统采用了“快速失败”策略,一旦发现某个数据源超时或失败,立即返回失败响应,而不是进行重试或等待。

在这个场景中,快速失败策略的优势是:

  1. 避免资源浪费:如果一个数据源的调用失败,系统会立刻放弃,避免了不必要的资源占用。
  2. 提升响应速度:通过快速失败,系统能够在较短时间内响应用户请求,即使某些数据源无法提供数据,也不会阻塞整个流程。
  3. 业务容错:系统会提示用户某些信息不可用,而不会因为一个服务的失败影响整个推荐流程。

通过这个案例,可以帮助学员理解快速失败策略如何在高并发、低延迟的环境中实现“高性能”,同时避免因过度重试或等待导致的系统资源浪费。在这种场景中,快速失败实际上是权衡“高可用”和“高性能”之间的折衷,能够确保系统在面对故障时不至于完全崩溃,反而能保持快速响应。

(五)Forking(分支机制)

定义:系统并行调用多个服务提供者,只要一个成功即可返回结果。

实现方式

  • 同时向多个服务实例发送请求,优先返回第一个成功响应的结果。其他的响应可以被丢弃。
  • 适合对实时性要求较高的场景,但会增加额外的资源开销。

适用场景

  • 高并发读操作,如读取缓存、查询数据库等。
  • 需要高可用性并容忍一定的资源浪费的场景。

案例说明:实时视频直播中的分支机制应用

在一个大型的实时视频直播平台中,平台需要从多个视频源获取用户请求的视频流。由于直播需要尽可能快地响应用户请求,且直播视频流的质量和稳定性直接影响用户体验,因此该平台使用了“分支机制”来提高请求成功的概率。

假设用户请求观看某个直播节目的视频流,而该视频流由多个服务器提供(例如,不同地理位置的服务器)。为了保证用户请求的高可用性和低延迟,系统并行向多个服务器发送请求,使用“分支机制”来加速视频流的获取过程。

分支机制工作流程:

  1. 并行请求:系统同时向多个服务器发起视频流请求,所有请求是并行进行的。
  2. 成功即返回:一旦有任何一个服务器返回有效的视频流,系统就会立即停止其他请求,直接将视频流返回给用户。
  3. 失败处理:如果某些服务器请求失败,系统会继续向其他服务器请求,直到成功为止。

(六)Broadcast(广播机制)

定义:系统会逐个调用所有提供者,任何一台服务提供者出现故障时,整个请求被视为失败。

实现方式

  • 所有服务提供者都需要参与处理,若其中任何一个失败,则整个操作失败。
  • 这种机制通常用于业务层面需要同步所有服务操作的场景,如批量更新缓存或广播通知等。

适用场景

  • 适用于需要同时通知所有服务提供者进行操作的场景,如批量更新缓存、日志通知等。
  • 适合对失败容忍度极低,且无法容忍部分成功的情况。

案例说明:电商平台的库存和价格更新

在一个电商平台上,当用户在结算时提交订单,系统需要确保相关服务(如库存服务、价格服务和促销服务)缓存同步更新,以保证一致性。例如,库存数量和商品价格需要同步更新,避免库存不足或价格不正确的问题。

业务场景:

  1. 用户将商品添加到购物车并提交订单。
  2. 系统使用广播机制,向库存服务、价格服务、促销服务等多个服务发出更新请求,要求它们更新各自的缓存。
  3. 如果库存服务和促销服务成功更新了缓存,但价格服务由于某种原因失败(比如服务超时),则整个请求将被视为失败。
  4. 系统回滚操作,取消订单提交,返回用户错误信息:更新失败,稍后重试。

广播机制适用于需要全局一致性的场景,但它的“全有或全无”特性也意味着服务间的任何失败都会导致整个操作的回滚,这可能会带来性能开销。因此,选择广播机制时需要仔细考虑是否能够接受这种性能上的牺牲。

(七)总结

服务容错机制是分布式系统中的关键组成部分,它能够帮助系统在面对异常时保持稳定性。理解和选择合适的容错策略,是设计可靠、可扩展系统的基础。不同的容错策略各有其特点和应用场景,开发者应该根据实际的业务需求和技术架构来选择合适的容错方式。通过合理的容错设计,可以有效提高系统的可用性、容错性和扩展性。

三、服务隔离

隔离,本质上是对系统或资源进行分割,从而实现当系统发生故障时能限定传播范围和影响范围,即发生故障后只有出问题的服务不可用,保证其他服务仍然可用。基本思路如下:                                 

1.线程隔离

主要通过线程池进行隔离,也是实现服务隔离的基础。(可将图中隔离媒介换成线程池即可)

把业务进行分类并交给不同的线程池进行处理,当某个线程池处理一种业务请求发生问题时,不会讲故障扩散和影响到其他线程池,保证服务可用。

假设系统存在商品服务、用户服务和订单服务3个微服务,通过设置运行时环境得到3个服务一共使用200个线程,客户端调用这3个微服务共享线程池时可能会引发服务雪崩,将线程分别隔离后则不会触发整体雪崩。

2.进程隔离

把隔离媒介替换为JVM。

将系统拆分为多个子系统来实现物理隔离,各个子系统运行在独立的容器和JVM中,通过进程隔离使得一个子系统出现问题不会影响其他子系统。

3.集群隔离

将某些服务单独部署成集群,或对于某些服务可以进行分组集群管理,某一个集群出现问题之后就不会影响到其他集群,从而实现隔离。 

4.机房隔离

如果有条件,对于大型高可用系统,会进行多机房部署,每个机房的服务都有自己的服务分组,本机房的服务应该只调用同机房服务。

当一个机房出现故障,将请求快速切换到其他机房确保服务继续可用。

5.读写隔离

常见的隔离技术,当用于读取操作的服务器出现故障时,写服务器照常可以运作,反之也一样。

四、服务限流

即流量控制,限流的目的是在遇到流量高峰期或者流量突增时,把流量速率限制在系统所能接受的合理范围内,不至于将系统击垮。常见的限流方法有四种:

  • 通过限制单位时间段内调用量来限流;
  • 通过限制系统的并发调用程度来限流;
  • 使用漏桶限流;
  • 使用令牌桶算法限流

1.计数器法

通过限制单位时间段内调用量来限流,使用一个计数器统计单位时间段某个服务的访问量,如果超过了设定的阈值,则该单位时间内不允许服务继续响应请求,或者把接下来的请求放入队列中等待下一个单位时间段继续访问,计数器在进入下一个时间段时先重置清零。        

但存在十分致命临界问题,用户可以根据算法漏洞瞬间击垮应用。

假设我们规定一分钟最多接受100个请求,也就是每秒钟最多1.7个请求。

假设有一个恶意用户在0:59时瞬间发送了100个请求,并且在1:00又瞬间发送了100个请求,则该用户在1秒内瞬间发送了200个请求。即在时间窗口的重置点处集中发送请求会瞬间超过速率限制。

2.滑动窗口法

以上面的例子,一个时间窗口就是1分钟,然后我们将时间窗口进行划分。如果我们将滑动窗口划分成6格,所以每各代表就是10秒钟,每隔10秒钟时间窗口就会往右滑动一格,每个格子都有自己独立的计数器。当一个请求在0:35秒的时候达到,那么0:30-0:39秒对应的计数器就会加1。

看上图,0:59到达的100个请求会落在灰色的格子中,而1:00到达的请求会落在橘黄色的格 子中。当时间到达1:00时,我们的窗口会往右移动一格,那么此时时间窗口内的总请求数量一共是200个,超过了限定的100个,所以此时能够检测出来触 发了限流。

我再来回顾一下刚才的计数器算法,我们可以发现,计数器算法其实就是滑动窗口算法。只是它没有对时间窗口做进一步地划分,所以只有1格。

由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。

3.漏洞算法

漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:                                       

可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。 因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率,因此,漏桶算法对于存在突发特性的流量来说缺乏效率。

4.令牌桶算法

令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务。

牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量。

五、服务降级

在服务器压力剧增的情况下,根据当前业务情况及流量对一些服务有策略地降级,以此释放服务器资源以保证核心任务正常运行。

降级可以有计划的进行,也可以被动触发。

电商网站“双十一”期间对部分非核心业务进行手动降级

系统运行时可能出现的各种异常情况,为控制影响范围可在程序级别实现自动服务降级。

1.服务分级

具体分级如:微服务架构服务建模方法+服务拆分和集成1:服务建模的四大切入点_服务拆分规范_张彦峰ZYF的博客-CSDN博客。

对每个微服务进行等级管理后,降级一般是从最外围、等级最低的服务开始。以移动医疗系统为例,简单分级如下:

2.服务熔断

降级有个类似的词称为服务熔断,当某个异常条件被触发,直接熔断整个服务,而不是一直等到此服务超时。

服务降级就是当某个服务熔断后,服务端准备一个本地的回退回调,返回一个缺省值。

一个基本的服务熔断器结构在实现上一般有三个状态机:

(1)Closed:熔断器关闭状态,调用失败次数积累,到了阈值(或一定比例)则启动熔断机制;

(2)Open:熔断器打开状态,此时对下游的调用都内部直接返回错误,不走网络,但设计了一个时钟选项,默认的时钟达到了一定时间(这个时间一般设置成平均故障处理时间,也就是MTTR),到了这个时间,进入半熔断状态;

(3)Half-Open:半熔断状态,允许定量的服务请求,如果调用都成功(或一定比例)则认为恢复了,关闭熔断器,否则认为还没好,又回到熔断器打开状态;

3.服务降级与服务熔断的区别

  • 触发原因不大一样。服务熔断一般是某个下游服务发生故障引起,而服务降级一般是从整体 负荷考虑。
  • 管理目标层次不一样。熔断是一个框架级的处理,每个服务都需要,无层级之分,而降级一般需要对业务有层级之分。

六、总结

在微服务架构中,系统的稳定性与高可用性依赖于合理的容错与故障应对机制。通过对限流、降级、熔断等策略的应用,我们可以在面对高并发、服务故障或资源瓶颈时,确保系统持续稳定地运行,同时为用户提供无缝的体验。本文通过一系列的理论与实践案例,详细分析了这些容错机制的工作原理、应用场景和实现方法。

首先,限流策略通过限制请求的频率,能够有效防止服务过载,并保护后端系统不受异常流量的影响。其次,降级机制通过预设的降级策略,能够在资源紧张或服务异常时,动态调整系统的服务质量,从而避免系统崩溃。最后,熔断机制则通过监控服务状态,在发生服务故障时快速切断连接,防止问题蔓延,确保系统其余部分的正常运行。

然而,容错机制的设计并非一蹴而就。在实际应用中,需要根据业务需求、系统复杂度以及技术栈的特点,灵活调整这些策略的实现。不同的业务场景可能需要不同的容错策略,因此,架构师和开发人员需要对各种机制的优缺点、适用场景有深入的了解,以便做出最合适的选择。

未来,随着微服务架构的进一步发展,容错机制将变得更加复杂与智能。如何平衡高可用性与系统性能、如何在多变的环境中保障用户体验,将是我们继续探索的方向。因此,持续优化和调整容错策略,将是确保系统长期稳定运行的关键。

通过本文的讨论,希望你能够更好地理解容错与故障应对机制的核心理念,掌握如何在复杂的微服务环境中保障服务的稳定性和高可用性。如果你正在构建或者维护一个高可用的分布式系统,这些策略将为你提供重要的参考,帮助你应对系统中的各种挑战。

参考书籍、文献和资料:

【1】郑天民. 微服务设计原理与架构. 北京:人民邮电出版社,2018.

【2】java - 防雪崩利器:熔断器 Hystrix 的原理与使用 - 编程随笔 - SegmentFault 思否.

【3】三种常见的限流算法 - Ruthless - 博客园.

【4】https://www.cnblogs.com/chn58/p/6566007.html.

【5】https://www.cnblogs.com/myfrank/p/7505108.html.

今天的文章 从容错到限流:保障微服务可靠性的关键策略分析分享到此就结束了,感谢您的阅读。
编程小号
上一篇 2024-12-14 18:51
下一篇 2024-12-14 18:46

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/bian-cheng-ji-chu/86071.html