• Spring Cloud---服务熔断Hystrix


    哈喽大家好我是yangerkong!今天跟大家探讨下微服务中的熔断机制。

    本文中部分介绍和部分图片摘自官网,官网地址:Home · Netflix/Hystrix Wiki · GitHub

    SpringCloud之服务熔断与降级

    熔断器原理介绍

    雪崩效应(熔断器背景)

    在微服务系统中,一个应用由多个服务组成。相互依赖,依赖关系错综复杂。一个业务操作会由多个不同的服务共同完才。所以这些 依赖服务的稳定性对系统的影响非常大。但是由于系统存在很多不可控问题:网络延时,资源繁忙,服务宕机等。若有一个服务因为故障原因,可能会导致整个服务崩溃。

    我们先来看一张图:

    上图所示:举例说明了依赖服务由于支撑服务暂时不可用问题导致的系统问题

    用户请求由于server2服务不可用导致请求需要等待10秒才能响应(假设app端请求不超时),那么之后所有请求都会阻塞,堆积直到系统上限直接打死所有服务。

    这个过程就称为"雪崩效应",为了防止此类事件发生,微服务架构引入了"熔断器"的一些列的服务容错和保护措施。

    Spring Cloud Hystrix

    Hystrix简介

    Hystrix是Netflix开源的一款分布式容错框架和保护组件,也是Spring Cloud的重要组件之一。

    Spring Cloud Hystrix是基于Netfilx公司的开源组件Hystrix实现的。提供熔断器功能,能够有效避免故障在微服务系统中蔓延,导致雪崩效应的产生;以提高微服务系统的弹性。

    Hystrix容错

    资源隔离:防止单个服务故障耗尽系统中所有的线程资源。

    服务降级:当某个服务发生了故障,不让服务调用方一直等待,在可能的情况下进行回退并优雅地降级。提供降级方案在请求失败后,提供一个设计好的降级方案,当请求失败后调用此方法。

    服务熔断:使用熔断机制,在复杂的分布式系统中停止级联故障。提供熔断器故障监控组件Hystrix Dashboard,随时监控熔断器的状态。

    当服务请求一切正常时,请求流如下所示:

    当微服务系统请求故障时,它可以阻塞整个用户请求:

    在高流量请求服务的情况下因某服务故障,可能导致所有服务器上的所有资源在几秒钟内饱和。

    应用程序中通过网络或进入可能导致网络请求的客户端库的每一点都是潜在故障的来源。比故障更糟糕的是,这些应用程序还会导致服务之间的延迟增加,从而备份队列、线程和其他系统资源,从而导致更多跨系统的级联故障。

    Hystrix是这样做的:

    将所有对外部系统(或“依赖项”)的调用包装在一个HystrixCommand或HystrixObservableCommand对象中,该对象通常在单独的线程中执行(这是命令模式的一个例子)。

    超时调用的时间超过您定义的阈值。有一个默认值,但是对于大多数依赖项,您可以通过“属性”的方式自定义设置这些超时,这样每个依赖项的性能就会略高于99.5%。

    为每个依赖项维护一个小的线程池(或信号量);如果该依赖项已满,那么指向该依赖项的请求将立即被拒绝,而不是排队。度量成功、失败(客户端抛出的异常)、超时和线程拒绝。

    触发一个断路器,在一段时间内停止对特定服务的所有请求,如果服务的错误百分比超过一个阈值,可以手动停止,也可以自动停止。当请求失败、被拒绝、超时或短路时执行回退逻辑。几乎实时地监控指标和配置更改。

    使用Hystrix包装每个底层依赖项时,上面图表中所示的体系结构将更改为类似于下面的图表。每个依赖项都是相互隔离的,在发生延迟时,它所能饱和的资源受到限制,并包含在当依赖项中发生任何类型的故障时决定做出何种响应的回退逻辑中:

    Hystrix工作原理

     下面的图显示了当你通过Hystrix向一个服务依赖项发出请求时会发生什么:

    以下部分将更详细地解释这个流程:

    1. 构造一个HystrixCommand或hystrixobservableccommand对象
    2. 执行命令
    3. 缓存是否响应
    4. 电路是否开放
    5. 线程池/队列/信号量是否已满
    6. HystrixObservableCommand.construct()或HystrixCommand.run ()
    7. 计算电路健康状态
    8. 得到回退
    9. 返回成功响应

     如果Hystrix命令成功,它将以Observable的形式返回响应或响应给调用者:

    流程1-9详细介绍 官网地址如下:How it Works · Netflix/Hystrix Wiki · GitHub

     Hystrix服务熔断

    熔断机制是为了应对雪崩效应而出现一种微服务链路保护机制。

    熔断器最重要的在于保证服务调用者在调用异常服务时,快速返回结果,避免大量的同步等待。并且在一段时间后熔断器能自动判断服务恢复调用的可能。

    Hystrix工作模式

    在熔断器中,最重要的一个概念是:服务器的健康情况。熔断器的设计基本上都是围绕该概念进行的。健康情况=请求失败数/请求总数

     如图所示:定义了熔断器的工作模式和开关相互转换的逻辑。

    熔断器定义了三种状态来决定是否允许请求通过:

    1.熔断关闭(Close):关闭状态时,允许请求通过,调用服务方可以正常调用服务

    2.熔断开启(Open):在固定时间内如果接口调用出错率达到一个阈值,熔断器会将状态设置为开启,在该状态下请求被禁止通过。熔断器会执行降级(FallBack)方法。

    3.半熔断(HalfOpen):熔断开启一段时间后,熔断器会进入半开状态,这时会允许部分请求调用服务,并监听是否调用成功,如果成功率达到预期说明服务正常恢复,熔断器恢复到关闭状态( Close),如果成功率很低达不到预期,熔断器重新设置为开启状态(Open)

    当微服务系统的某个微服务故障或响应时间太长,为了保护系统的服务整体可用性,熔断器会暂时切断对该服务的请求调用,熔断器及时作出向服务调用方返回一个符合预期的,可处理的降级响应(FallBack),而不是让用户长时间的等待或者抛出用户无法处理的异常。熔断状态是暂时的,在熔断一定时间后,熔断器会再次检测该微服务是否恢复正常,若恢复正常则恢复调用链路。这样保证了服务提供方不会对系统资源长时间的,不必要的占用,避免故障在微服务系统中的蔓延,防止雪崩效应的产生。

    Hystrix实现服务熔断机制

    Spring Cloud中熔断机制是通过Hystrix实现的,Hystrix监控微服务调用情况,当调用失败率达到一定阈值,熔断机制就会启动。

    1. 当服务的调用出错率超过Hystirx规定的出错比率(默认为50%),熔断器状态进入开启状态。
    2. 熔断器进入开启状态后,Hystrix会启动一个休眠时间窗,在这个时间窗内,该服务的提前设定好的降级逻辑会临时充当业务主逻辑,而原来的业务主逻辑不可用。
    3. 当有请求再次调用该服务时,会直接调用降级逻辑快速返回失败响应,以避免系统雪崩。
    4. 当休眠时间窗到期后,熔断器会进入半开状态,允许部分请求进行对该服务调用,并监控其成功率。
    5. 如果调用成功率达到预期,则服务恢复正常,熔断器进入关闭状态,服务链路恢复。否则肉熔断器将进入开启状态,休眠时间窗口重新计时。

    Hystrix熔断实现

    如何集成Hystrix:在网关项目中集成Hystrix

            1.pom.xml配置maven依赖     

    1. <dependency>
    2. <groupId>org.springframework.cloudgroupId>
    3. <artifactId>spring-cloud-starter-netflix-hystrixartifactId>
    4. dependency>

           2. application.yml配置  

    1. server:
    2. port: 8082
    3. spring:
    4. application:
    5. name: msa-eureka-gateway
    6. # 注册服务到eureka
    7. eureka:
    8. client:
    9. serviceUrl:
    10. defaultZone: http://localhost:8761/eureka/
    11. # 配置eureka上服务的默认描述信息
    12. instance:
    13. instance-id: springcloud-provider-hystrix
    14. # 显示访问路径的ip地址
    15. prefer-ip-address: true

    3.创建测试controller

    1. @RestController
    2. public class TestController {
    3. @GetMapping("/testHystrix")
    4. @HystrixCommand(fallbackMethod = "getHystrix")
    5. public User user(String id){
    6. if(StringUtils.isBlank(id))
    7. {
    8. throw new RuntimeException("传入用户id为空,找不到用户信息");
    9. }
    10. //模拟一下
    11. User user = new User();
    12. user.setId(id);
    13. user.setName("yangerkong");
    14. user.setAge("18");
    15. return user;
    16. }
    17. public User getHystrix(String id)
    18. {
    19. User user = new User();
    20. user.setId(id);
    21. user.setName(id+"没有找到对应信息,--@Hystrix");
    22. return user;
    23. }
    24. }

    @HystrixCommand(fallbackMethod = "getHystrix")注解

     fallbackMethod:@HystrixCommand注解修饰函数的回调函数,@HystrixCommand修饰的函数跟回调函数必须在同一个类中。

    4.启动测试:

    访问成功,返回正常处理逻辑

    访问失败,返回降级方法中的失败信息 。接口返回友好的失败提示信息。

     没有集成Hystrix,没有使用@HystrixCommand(fallbackMethod ="")时:控制台直接出现异常。返回的失败信息也不友好

     

    HystrixProperty配置

    1. //========================All
    2. @HystrixCommand(fallbackMethod = "fallbackMetodName",
    3. groupKey = "strGroupCommand",
    4. commandKey = "strCommand",
    5. threadPoolKey = "strThreadPool",
    6. commandProperties = {
    7. // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离
    8. @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
    9. // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数)
    10. @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"),
    11. // 配置命令执行的超时时间
    12. @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"),
    13. // 是否启用超时时间
    14. @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
    15. // 执行超时的时候是否中断
    16. @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"),
    17. // 执行被取消的时候是否中断
    18. @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"),
    19. // 允许回调方法执行的最大并发数
    20. @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"),
    21. // 服务降级是否启用,是否执行回调函数
    22. @HystrixProperty(name = "fallback.enabled", value = "true"),
    23. // 是否启用断路器
    24. @HystrixProperty(name = "circuitBreaker.enabled", value = "true"),
    25. // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候,
    26. // 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。
    27. @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"),
    28. // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过
    29. // circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50,
    30. // 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。
    31. @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
    32. // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后,
    33. // 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态,
    34. // 如果成功就设置为 "关闭" 状态。
    35. @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"),
    36. // 断路器强制打开
    37. @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"),
    38. // 断路器强制关闭
    39. @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"),
    40. // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间
    41. @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"),
    42. // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据
    43. // 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。
    44. // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常
    45. @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"),
    46. // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。
    47. @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"),
    48. // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。
    49. @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"),
    50. // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。
    51. @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"),
    52. // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数,
    53. // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行,
    54. // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。
    55. @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"),
    56. // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。
    57. @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"),
    58. // 是否开启请求缓存
    59. @HystrixProperty(name = "requestCache.enabled", value = "true"),
    60. // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中
    61. @HystrixProperty(name = "requestLog.enabled", value = "true"),
    62. },
    63. threadPoolProperties = {
    64. // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量
    65. @HystrixProperty(name = "coreSize", value = "10"),
    66. // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列,
    67. // 否则将使用 LinkedBlockingQueue 实现的队列。
    68. @HystrixProperty(name = "maxQueueSize", value = "-1"),
    69. // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。
    70. // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue
    71. // 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。
    72. @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"),
    73. }
    74. )

    配置文件中全部配置参数:

    1. #转自博客:https://blog.csdn.net/fk1778770286/article/details/120912303
    2. hystrix:
    3. # 一、命令配置
    4. command:
    5. # 默认全局配置
    6. default:
    7. # 1、命令执行(execution)配置
    8. execution:
    9. timeout:
    10. enabled: true # 是否允许超时:true(默认值)、false
    11. isolation:
    12. strategy: THREAD # 隔离策略:THREAD(默认值)、SEMAPHORE ---------【参考 ExecutionIsolationStrategy.THREAD】
    13. thread:
    14. interruptOnCancel: false # 当发生取消时,执行是否应该中断:true、false(默认值)---------【THREAD模式有效】
    15. interruptOnTimeout: true # 在发生超时时是否应中断:true(默认值)、false---------【THREAD模式有效】
    16. timeoutInMilliseconds: 1000 # 超时时间上限:单位是毫秒 默认1000---------【这个配置生效的前提是hystrix.command.default.execution.timeout.enabled或者hystrix.command.[HystrixCommandKey].execution.timeout.enabled为true; 在THREAD模式下,达到超时时间,可以中断,在SEMAPHORE模式下,会等待执行完成后,再去判断是否超时 设置标准:有retry,99meantime+avg meantime 没有retry,99.5meantime】
    17. semaphore:
    18. maxConcurrentRequests: 10 # 最大并发请求上限(SEMAPHORE):10(默认值);ExecutionIsolationStrategy.SEMAPHORE隔离策略下并发请求数量的最高上限
    19. # 2、命令降级(fallback)配置:对策略ExecutionIsolationStrategy.THREAD或者ExecutionIsolationStrategy.SEMAPHORE都生效
    20. fallback:
    21. enabled: true # 是否开启降级:false、true(默认值)
    22. isolation:
    23. semaphore:
    24. maxConcurrentRequests: 10 # 最大并发降级请求处理上限:10(默认值)
    25. # 3、断路器(circuit breaker)配置
    26. circuitBreaker:
    27. enabled: true # 是否启用断路器:false、true(默认值)
    28. requestVolumeThreshold: 10 # 断路器请求量阈值:20(默认值)---------【滑动窗口中的请求数量最少要达到这个阈值,断路器才可能被打开】
    29. sleepWindowInMilliseconds: 5000 # 断路器等待窗口时间:5000(默认值) ---------【断路器打开后,拒绝多长时间的请求】
    30. errorThresholdPercentage: 50 # 断路器错误百分比阈值:50(默认值) ---------【当请求错误率超过设定值,断路器就会打开】
    31. forceOpen: false # 是否强制打开断路器:false(默认值)、true---------【强制打开断路器会使所有请求直接进入降级逻辑】
    32. forceClosed: false # 是否强制关闭断路器:false(默认值)、true---------【强制关闭断路器会导致所有和断路器相关的配置和功能都失效】
    33. # 4、度量统计(metrics)配置:度量统计配置会对HystrixCommand或者HystrixObservableCommand执行时候的统计数据收集动作生效
    34. metrics:
    35. rollingStats:
    36. timeInMilliseconds: 10000 # 滑动窗口持续时间:10000(默认值)
    37. numBuckets: 10 # 滑动窗口Bucket总数:10(默认值)---------【需要满足metrics.rollingStats.timeInMilliseconds % metrics.rollingStats.numBuckets == 0,要尽量小,否则有可能影响性能】
    38. rollingPercentile:
    39. enabled: true # 是否启用百分数计算:true(默认值)、false
    40. timeInMilliseconds: 60000 # 百分数计算使用的滑动窗口持续时间:60000(默认值)
    41. numBuckets: 6 # 百分数计算使用的Bucket总数:6(默认值)---------【满足metrics.rollingPercentile.timeInMilliseconds % metrics.rollingPercentile.numBuckets == 0,要尽量小,否则有可能影响性能】
    42. bucketSize: 100 # 百分数计算使用的Bucket容量:100(默认值)
    43. healthSnapshot:
    44. intervalInMilliseconds: 500 # 健康状态快照收集的周期:500(默认值)
    45. # 5、请求上下文配置:主要涉及到HystrixRequestContext和HystrixCommand的使用
    46. requestCache:
    47. enabled: true # 是否启用请求缓存:true(默认值)、false
    48. requestLog:
    49. enabled: true # 是否启用请求日志:true(默认值)、false
    50. # 实例配置
    51. CustomCommand:
    52. requestLog:
    53. enabled: true
    54. requestCache:
    55. enabled: true
    56. metrics:
    57. healthSnapshot:
    58. intervalInMilliseconds: 500
    59. rollingPercentile:
    60. bucketSize: 100
    61. numBuckets: 6
    62. timeInMilliseconds: 60000
    63. enabled: true
    64. rollingStats:
    65. numBuckets: 10
    66. timeInMilliseconds: 10000
    67. circuitBreaker:
    68. forceClosed: false
    69. forceOpen: false
    70. errorThresholdPercentage: 50
    71. sleepWindowInMilliseconds: 5000
    72. requestVolumeThreshold: 10
    73. enabled: true
    74. fallback:
    75. enabled: true
    76. isolation:
    77. semaphore:
    78. maxConcurrentRequests: 10
    79. execution:
    80. timeout:
    81. enabled: true
    82. isolation:
    83. semaphore:
    84. maxConcurrentRequests: 10
    85. thread:
    86. interruptOnCancel: false
    87. interruptOnTimeout: true
    88. timeoutInMilliseconds: 1000
    89. strategy=THREAD: THREAD
    90. # 二、请求合成器配置:主要控制HystrixCollapser的行为
    91. collapser:
    92. # 默认全局配置
    93. default:
    94. maxRequestsInBatch: 10 # 请求合成的最大批次量:Integer.MAX_VALUE (默认值)
    95. timerDelayInMilliseconds: 10 # 延迟执行时间:10(默认值)
    96. requestCache:
    97. enabled: true # 是否启用请求合成缓存:true(默认值)、false
    98. # 实例配置
    99. CustomHystrixCollapser:
    100. requestCache:
    101. enabled: true
    102. timerDelayInMilliseconds: 10
    103. maxRequestsInBatch: 10
    104. # 三、线程池配置:Hystrix使用的是JUC线程池ThreadPoolExecutor,线程池相关配置直接影响ThreadPoolExecutor实例。Hystrix的命令执行选用了线程池策略,那么就是通过线程池隔离执行的,最好为每一个分组设立独立的线程池。笔者在生产实践的时候,一般把HystrixCommandGroupKey和HystrixThreadPoolKey设置为一致
    105. threadpool:
    106. # 默认全局配置
    107. default:
    108. coreSize: 10 # 核心线程数:10(默认值)
    109. maximumSize: 10 # 最大线程数:10(默认值)---------【此属性只有在allowMaximumSizeToDivergeFromCoreSize为true的时候才生效】
    110. maxQueueSize: -1 # 最大任务队列容量:-1(默认值)或者大于0的整数---------【配置为-1时使用的是SynchronousQueue,配置为大于1的整数时使用的是LinkedBlockingQueue;如果要从-1换成其他值则需重启,即该值不能动态调整】
    111. queueSizeRejectionThreshold: 5 # 任务拒绝的任务队列阈值:5(默认值)---------【当maxQueueSize配置为-1的时候,此配置项不生效】
    112. keepAliveTimeMinutes: 1 # 非核心线程存活时间:1(默认值)---------【当allowMaximumSizeToDivergeFromCoreSize为true并且maximumSize大于coreSize时此配置才生效】
    113. allowMaximumSizeToDivergeFromCoreSize: true # 是否允许最大线程数生效:true、false(默认值)
    114. metrics:
    115. rollingStats:
    116. timeInMilliseconds: 10000 # 线程池滑动窗口持续时间: 10000(默认值)
    117. numBuckets: 10 # 线程池滑动窗口Bucket总数:10(默认值)---------【满足metrics.rollingStats.timeInMilliseconds % metrics.rollingStats.numBuckets == 0,值要尽量少,否则会影响性能】
    118. # 实例配置
    119. CustomCommand:
    120. metrics:
    121. rollingStats:
    122. numBuckets: 10
    123. timeInMilliseconds: 10000
    124. allowMaximumSizeToDivergeFromCoreSize: true
    125. keepAliveTimeMinutes: 1
    126. queueSizeRejectionThreshold: 5
    127. maxQueueSize: -1
    128. maximumSize: 10
    129. coreSize: 10

     

    目录

    SpringCloud之服务熔断与降级

    熔断器原理介绍

    雪崩效应(熔断器背景)

    Spring Cloud Hystrix

    Hystrix简介

    Hystrix容错

    Hystrix工作原理

     Hystrix服务熔断

    Hystrix工作模式

    Hystrix实现服务熔断机制

    Hystrix熔断实现

    HystrixProperty配置

     


     

  • 相关阅读:
    嵌入式学习笔记(17)代码重定位实战 上篇
    python爬虫之JS逆向
    leetcode 28. 实现 strStr() (KMP算法实现)
    天猫复购预测训练赛技术报告
    Java集合的lastlastIndexOfSubList()方法具有什么功能呢?
    分布式链路追踪-skywalking基础
    设计模式之代理模式
    【面试精讲】Java:String、StringBuffer、StringBuilder有什么区别?
    leetcode题目分析(一)leetcode155最小栈
    腾讯云服务器地域怎么选?不同地域有啥区别?
  • 原文地址:https://blog.csdn.net/kongkongyanan/article/details/126426926