交易系统架构演进之路:服务治理
前言
微服务架构下,会引入很多服务问题,所以少不了需要做服务治理,包括:服务注册与发现、服务配置、服务限流、服务熔断、服务降级、负载均衡、链路追踪等。
关于服务治理的范畴应该包括哪些,业界其实也没有形成标准,但至少包括了前面列出来的内容,这是毋庸置疑的。另外,微服务架构下,服务集群规模会越来越大,服务治理也很难靠人工完成,因此,微服务治理的自动化程序要高。
下面,我们就根据上面列举的内容,一一讲解每一块的服务治理如何实践。
服务注册与发现
在第三篇文章最后我们也有讲到注册中心,从 CAP 理论出发,分析出注册中心应该优先选择 AP 模型,且推荐使用 Nacos。
而这次,我们来聊聊另一个问题:为什么需要注册中心?
我们知道,服务之间相互调用,就需要知道对方的 IP 和端口。在没有注册中心的情况下,每个服务会将其他服务的 IP 和端口写死在自己的配置文件里。这样的话,每次需要新增或移除一个服务实例的时候,相关联的所有服务都需要修改配置。当服务比较少的时候,问题还不大,但随着服务越来越多,服务实例的新增或移除越来越频繁,依然靠人工手动写配置和变更配置,对运维和开发来说简直就是灾难。
为了解放双手、提高效率,服务注册与发现的机制就被聪明的人类设计出来了。
每一个服务实例在启动运行的时候,都将自己的信息(包括 IP、端口和唯一的服务名字等)上报给注册中心,注册中心则会将所有服务注册的信息保存到注册表中,这就是服务注册。
有了注册中心之后,那服务 A 需要调用服务 B 时,就不是服务 A 的配置文件里写死服务 B 的 IP 和端口了,服务 A 里配置的是服务 B 的名字,服务 A 会根据服务 B 的名字向注册中心请求服务 B 的实例信息,从而拿到服务 B 的 IP 地址和端口,这就是服务发现。另外,服务 B 如果存在多个实例,那注册中心返回的可能就是服务 B 的实例信息列表,这时,服务 A 则可以选用某种负载均衡算法取得其中一个 IP,再进行调用。有些注册中心自身也提供了负载均衡算法,直接算好一个 IP 并返回,则无需服务 A 自己选择。
服务注册与发现,除了可以动态获取 IP,还有一个重要的功能就是可以自动监控管理服务器的存活状态。主要实现方式就是注册中心与每个服务器之间定时发送心跳包,做健康检查。一旦心跳包停止,则可判断为该服务器宕机了,就会标记这个实例的状态为故障,或者干脆剔除掉这台机器。当故障机器被修复后,服务重新启动后,健康检查会检查通过,然后这台机器就会被重新标记为健康。
简而言之,使用注册中心实现服务的自动注册与发现,就是服务治理的第一步。
服务配置
每个服务或多或少总有一些配置参数要管理,比如配置服务访问的端口、数据库连接参数、Redis 连接参数、日志参数、一些热开关、黑白名单等等。在微服务架构系统中,基本都是用配置中心来统一管理所有服务的配置。和注册中心一样,配置中心也是微服务架构中的一个基础设施。
为什么需要配置中心呢?我们可以看看在没有配置中心的情况下,是如何处理各种配置参数问题的。
没有配置中心的情况下,各个服务各自管理自己的配置参数,有的通过数据库管理配置,有的使用配置文件进行管理。配置文件可能还使用不同格式,有的可能用 properties 文件,有的可能用 yaml 文件,有的可能用 conf 文件,有的则可能用 xml 文件。另外,对配置项的命名规则也可能不一样。缺少统一管理,各自为政,运维人员做维护时就会非常痛苦。而且,如果参数需要修改,也很不灵活,甚至还要重启运行中的服务才能生效。一般一个服务还会部署多个实例,那该服务的配置参数需要修改的时候,所有实例也都要同步修改和重启,如果有一台不小心改错了参数,那还可能引发生产事故。
而且,项目中都会有多个环境:测试环境、UAT 环境、生产环境等。不同环境的配置参数一般是不同的,一般分开不同的配置文件进行隔离,比如像下面这样:
测试环境:conf-test.yaml
UAT 环境:conf-uat.yaml
生产环境:conf-prod.yaml
没有配置中心的情况下,不同环境的配置参数也只能手动维护,这会存在一些问题。一来,生产环境的配置信息暴露了出来,容易产生安全事故;二来,前面提到的手动改参数的弊端容易进一步扩大;第三,如果不小心将测试环境的配置带到了生产环境,也会引发生产事故。
还有一个弊端就是配置修改无法追溯,因为采用了静态配置文件方式,那修改配置之后,不容易形成记录,更无法追溯是谁修改的、什么时间修改的、修改前的内容是什么。既然无法追溯,那么当配置出错时,也就无法回滚配置了。
而使用配置中心,就可以很好地解决以上说的那些问题。使用配置中心,就可以统一管理不同环境、不同集群的配置,且可以追溯对配置的每一次修改。
再来说说配置中心的选型,目前业界比较流行的配置中心主要集中在三款:Spring Cloud Config、Apollo、Nacos。下表是这三款产品的对比:
Spring Cloud Config 更多只是结合 Spring Cloud 使用,但不管从功能还是性能,都不如 Apollo 和 Nacos。在功能方面,Nacos 目前的最新版本已经全都支持,另外在性能方面还具备较大优势,所以,我还是比较推荐 Nacos 的。不过,如果注册中心已经选用了 Nacos,那配置中心也选用 Nacos 的话,建议分开部署,即用作注册中心的 Nacos 和用作配置中心的 Nacos 要分开部署。
服务限流
限流、熔断、降级,是我们经常听到的三个名词,但对三者的区别和关系,很多人傻傻分不清楚。那我会根据我的理解,尽量讲明白三者的区别,以及如何落地应用到实际项目中。
我们知道,互联网系统,流量的突然暴涨很常见,有些场景是可以预见的,比如双 11、双 12 活动;而有些场景则是不可预见的,比如一个明星的突然官宣。如果对高流量不做任何保护措施,当请求超过服务器承载极限的时候,系统就会崩溃,导致服务不可用。
那么,在高流量的场景下,如何保证服务集群整体稳定和可用性呢?主要有两个方向,一是通过资源扩容来提升系统整体的容量,缺点就是成本比较高,且考虑到 ROI(投入产出比),也不可能无限扩容。而另一个更经济可行的选择就是限流。
所谓限流,是指当系统资源不足以应对高流量的时候,为了保证有限的资源能够正常服务,按照预设的规则,对系统进行流量限制。预设的规则中,核心指标可能是 QPS、总并发数、并发线程数,甚至是 IP 白名单,根据这些指标预设的值来决定是否对后续的请求进行拦截。
常见的限流算法有四种:计数器算法、滑动窗口算法、漏桶算法、令牌桶算法。
计数器算法,也称为固定窗口算法,比较简单粗暴,就是在固定的时间周期内(即时间窗口)累加访问次数,当达到设定的阀值时,触发限流策略,比如拒绝请求。下一个周期开始时,进行清零,重新计数。这个算法虽然简单,但存在一个严重的问题,那就是临界问题。假设 1 分钟内服务器的负载能力为 100,因此一个周期的访问量限制在 100,然而在第一个周期的最后 1 秒(0:59)和下一个周期的开始 1 秒(1:00)时间段内,分别涌入了 100 的访问量,虽然没有超过每个周期的限制量,但是整体上 2 秒内已达到 200 的访问量,已远超过服务器的负载能力。如下图所示:
滑动窗口算法,就能降低临界问题的影响,它的基本原理就是将一个时间窗口进行分割,比如将 1 分钟的时间窗口分割成 6 格,则每格代表 10 秒钟,且每一格都有自己独立的计数器。每过 10 秒钟,就把整个 1 分钟的时间窗口往右滑动 1 格。而时间窗口内的总计数,则是将该窗口内的所有格子的计数总和出来的。如下图所示:
那么,再来看看刚才的临界问题。0:59 的 100 个请求会落在灰色的格子里,而 1:00 到达的请求则落在橘黄色的格子里。当时间到达 1:00 时,时间窗口会往右移一格,而这时,整个窗口的总计数其实已经达到了 100,所以,1:00 之后到达的请求其实就会触发限流策略了。由此可见,当滑动窗口的格子划分得越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。不过,滑动窗口算法依然无法应对细时间粒度的突发流量,对流量的整形效果在细时间粒度上不够平滑。
漏桶算法也简单,我们可以把系统看成一个水桶,进来的请求理解为往桶里注水,处理请求就是桶中的水流出。水桶有固定容量,如果满了就溢出。不管注入水(请求进入)的快慢如何,只按照恒定的速率出水(处理请求)。因为桶容量是不变的,保证了整体的速率。不过,也因为速率是固定的,所以应对突发流量时就显得效率低下了。Java 自带的信号量组件 Semaphore 就是典型的基于漏桶算法实现的。
令牌桶算法应该算是最灵活的限流算法了,基本原理直接看下图:
令牌桶算法可以在运行时控制和调整数据处理的速率,一旦需要提高速率,则按需提高放入桶中的令牌的速率。一般会定时(比如 100 毫秒)往桶中增加一定数量的令牌,有些变种算法则实时的计算应该增加的令牌的数量。因此,令牌桶就可以应对突发流量了。不过,其实现相对也更复杂一些。Google Guava 的 RateLimiter 组件就是采用令牌桶算法实现的。
在实际应用中,尤其在分布式系统中,使用最广泛的组件应属 Sentinel,它的定位就是面向分布式服务架构的高可用流量控制组件。Sentinel 提供的主要功能不仅是限流,也提供了熔断降级、系统负载保护。对多语言的支持方面,除了 Java,也支持了 Go 和 C++。另外,限流方面,Sentinel 除了支持单机限流,也支持集群限流。
而具体到我们的交易系统中,应该在哪些地方做限流呢?主要就是对接口做限流,而我们的接口可以分为几大类:管理端 API、客户端 API、开放 API、服务内部 API。管理端 API 基本不会有突发流量的产生,所以也没必要做限流。客户端 API 和开放 API 则需要做限流,但两者的限流规则应该不一样,对开放 API 的限流规则应该严格一些,因为更容易被攻击。服务内部 API 在目前阶段也可以不做限流,一般微服务的规模比较大,某些服务的调用方比较多的时候需要做限流;或者其他一些场景导致下游出现突发流量的时候,比如上游调用方多线程并发跑定时任务调用下游服务的接口,这种情况下为了防止接口被过度调用,就需要对每个调用方进行细粒度的访问限流。
所以,我们就先给客户端 API 和开放 API 加限流,那么,就需要在客户端 API 网关和开放 API 网关集成限流组件,组件的选型就直接用 Sentinel。这两个 API 网关应该都是多实例部署的,所以分别需要做集群限流。部署方式采用独立部署 Sentinel 服务端的方式,且每个网关实例需要引入 Sentinel 客户端依赖。
限流规则的粒度方面,除了需要限制集群整体的访问频率,还需要限制某类接口甚至某个具体接口的访问频率。网关层的限流只能做到粗粒度的集群整体的限流,以及按不同路由名称进行限流。而具体到某个接口的,则只能在业务层的微服务进行限流了,所以还需要给对应的微服务集成限流组件客户端。具体接口层面,应该对下单请求进行限流,而限流策略可以用匀速器方式,对应的则是漏桶算法。
至此,关于限流方面的内容,就讲这么多了。
服务熔断
服务熔断主要是应对服务雪崩的一种自我保护机制,当下游的目标服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用。
对熔断机制的设计,业内基本上都是采用 熔断器模式来实现,Martin Fowler 在博文 《CircuitBreaker》 中对此设计进行了比较详细的说明。熔断器主要有三种状态:
closed:关闭状态,让请求通过的默认状态。如果请求错误率低于阈值,则状态保持不变。可能出现的错误是超过最大并发数和超时错误。
open:当熔断器打开的时候,所有的请求都会被标记为失败。这是故障快速失败机制,而不需要等待超时时间完成。
half open:半开状态时,会定期的尝试发起请求来确认系统是否恢复。如果恢复了,熔断器将转为关闭状态或者保持打开。
这三种状态如下图所示进行转换:
最开始处于 closed 状态,一旦检测到错误率到达一定阈值,便转为 open 状态;
这时候会有个 reset timeout,到了这个时间了,会转移到 half open 状态;
尝试放行一部分请求到目标服务,一旦检测成功便回归到 closed 状态,即恢复服务;
检测错误率的实现原理也比较简单,主要就是在一个滑动时间窗口内,统计下调用目标服务接口的总请求数和错误的请求数,一旦错误率达到设置的阀值,就触发熔断。另外,在这个滑动时间窗口内,应该还有个最小请求数的设置,比如 10s 内至少要有 20 次请求,如果没达到最小请求数,就算失败率达到了阀值也不触发熔断,否则,很大可能会造成误杀。
Sentinel 提供的熔断策略则有三种,除了上面说的错误率方式,即异常比例方式,还提供了另外两种统计方式:慢调用比例和异常数。这三种方式的官方描述如下:
慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
在我们的项目中,依然采用 Sentinel 作为熔断器组件,且对同步调用的每一层微服务都添加熔断保护,包括网关层、业务逻辑层、数据访问层。而异步调用的服务则无需增加熔断保护,这主要就是指撮合引擎。
而关于限流和熔断的区别,用一张图就明白了:
限流和熔断都是为了保护当前服务自身的可用性,但限流是为了防止上游服务调用量过大从而压垮当前服务,熔断则是为了避免下游服务出现故障时引发级联故障。
服务降级
我所理解的服务降级其实是一个更大的概念,存在多种降级方式,可以根据不同场景选择不同的降级方式,限流和熔断实际上也是应对两种不同场景的降级方式。
从概念上来说,所谓的服务降级,是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面进行策略性的屏蔽或降低服务质量,以此释放服务器资源以保证核心任务的正常运行。
从使用场景来说,当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时,为了保证重要或基本的服务能正常运行,我们可以将一些不重要或不紧急的服务或任务进行服务的延迟使用或暂停使用。
服务降级的方式或策略其实有多种,除了限流和熔断,常用的还有以下这些:
关闭次要服务:在服务压力过大时,关闭非核心功能的服务,避免核心功能被拖垮。比如,淘宝双 11 活动当天,订单量激增,为了保证核心的交易业务的高可用,就会暂时关闭非核心的退货服务。
丢弃部分请求:对于一些老请求——即从接收到处理的时间已经超过了一定时间(比如 1s)的请求,可以直接丢弃。还可以根据请求的优先级,有选择性地丢弃那些优先级低的请求。或者随机丢弃一定比例的请求。
读降级:对于读一致性要求不高的场景,在服务和数据库压力过大时,可以不读数据库,降级为只读缓存数据,以这种方式来减小数据库压力,提高服务的吞吐量。
写降级:在服务压力过大时,可以将同步写转为异步写,来减小服务压力并提高吞吐量。既然把同步改成了异步也就意味着降低了数据一致性,保证数据最终一致即可。
屏蔽写入:很多高并发场景下,查询请求都会走缓存,这时数据库的压力主要是写入压力。所以对于某些不重要的服务,在服务和数据库压力过大时,可以关闭写入功能,只保留查询功能,这样可以明显减小数据库压力。
数据冗余:服务调用者可以冗余它所依赖服务的数据。当依赖的服务故障时,服务调用者可以直接使用冗余数据。
以上列出来的只是部分降级方式而已,并没有涵盖所有情况。实际上,关于服务降级的方式和策略,并没有什么定式,也没有标准可言。不过,所有的降级方案都要以满足业务需求为前提,都是为了提高系统的可用性,保证核心功能正常运行。
从分类上来说,可以把服务降级分为手动降级和自动降级两大类。手动降级应用较多,主要通过开关的方式开启或关闭降级。自动降级,比如限流和熔断就属于这一类。手动降级大多也可以做成自动的方式,可根据各种系统指标配置阈值,当相应指标达到阈值时则自动开启降级。不过,在很多场景下,由于业务比较复杂,指标太多,自动降级实现起来难度比较大,而且也容易出错。所以在考虑做自动降级之前一定要充分做好评估,相应的自动降级方案也要考虑周全。
因为降级的概念比较大,且没有标准,所以只能根据实际场景需求选择对应合适的降级方式或策略去实现。
负载均衡
在微服务架构中,负载均衡也是必须使用的技术,通过它来实现系统的高可用和集群扩容等功能。当然,如果目标服务只有一个实例,那其实就无需添加负载均衡了。
负载均衡主要分两种:服务端负载均衡和客户端负载均衡。我们平时所说的负载均衡通常指的是服务端负载均衡,可通过硬件设备或软件来实现,硬件如 F5、Array 等,其优点就是功能强大、性能强大、稳定性高,但缺点就是价格昂贵且可扩展性差,所以更多还是使用软件,软件主要用 LVS、Nginx、HAproxy 等。而系统内部不同服务之间的负载则用客户端负载均衡,SpringCloud Ribbon 就是基于客户端的负载均衡组件。
服务端负载均衡主要应用在系统外部请求和网关层之间,常用的还分为四层负载均衡和七层负载均衡。
四层负载均衡工作在 OSI 模型的传输层,主要做转发,它在接收到客户端的请求之后,会通过修改数据包的地址信息将请求转发到应用服务器。实际应用中,大部分项目都是采用 LVS 作为四层负载均衡器,包括 BAT 等大厂,也都是 LVS 的重度使用者。LVS 具备可靠性、高性能、可扩展性和可操作性的特点,从而以低廉的成本实现最优的性能。
七层负载均衡,也称为内容交换,主要通过报文中的真正有意义的应用层内容,再加上负载均衡器设置的服务器选择方式(即负载均衡算法),决定最终选择的内部服务器。七层负载均衡的好处,就是使得整个网络更智能化,可以根据 URL 或请求参数路由到不同的服务器。工具的选型方面,使用最广泛的当属 Nginx,Nginx 除了做负载均衡还可以做静态 Web 服务器、缓存服务器、反向代理服务器等。
服务端负载均衡要做到高可用,常用的方案是做两层负载,第一层用 LVS+Keepalive,第二层用 Nginx。两层的负载至少都是双机热备,避免单点故障。第一层的负载需要绑定一个公网 VIP,一般,域名解析也是解析到这个 VIP。
客户端负载均衡应用在微服务系统内部,实现上下游服务之间的负载均衡。不同于服务端负载均衡是将下游的服务器列表存储在独立的负载均衡服务器里,客户端负载均衡则是将下游的服务器列表保存在上游服务里,而且下游服务的集群服务器列表是从注册中心获取并存储的,再根据实现的负载均衡算法选定对应的服务器实例进行请求的下发。
客户端负载均衡最关键的还是负载均衡算法,算法有很多,常用的有:随机法、加权随机法、轮询法、加权轮询法、一致性 Hash 法、最小连接法等。
随机法:将请求随机分配到各台服务器,适合于所有服务器都有相同的资源配置并且平均服务请求相对均衡的情况。当请求量很大的时候,请求分散的均衡性最好。如果请求量不大,则可能会出现请求集中在某些服务器的情况。
加权随机法:即给每台服务器配置权重值,权重值高的则接收到请求的概率就会较高,适合于服务器的资源配置不一样的场景。
轮询法:就是将请求按顺序轮流分发到每个服务器,和随机法一样,适合于服务器资源配置一样的情况,请求量不大的时候也适用。
加权轮询法:和加权随机法一样,不同资源配置的服务器会配置不同的权重值,权重值高的被轮询到的概率也高。
一致性 Hash 法:主要是为了让相同参数的请求总是发给同一台服务器,比如同个 IP 的请求。
最小连接法:将请求分配到当前连接数最少的服务器上,可以尽可能地提高服务器的利用效率,但实现比较复杂,需要监控服务器的请求连接数。
实际的线上业务有时候比较复杂,单纯采用任何一种负载均衡算法都无法满足需求,所以还会采用多种算法的组合。
链路追踪
微服务架构系统,每个用户请求往往涉及多个服务,且不同服务可能由不同团队开发,可能使用不同编程语言实现,还可能布在了横跨多个数据中心的几千台服务器上。在这种背景下,就需要一些能帮助理解系统行为、分析性能问题的工具,且在发生故障的时候能够快速定位和解决问题,这工具就是分布式追踪系统,也称为 APM(Application Performance Monitor) 系统。
最早的分布式追踪系统可以说就是 Google Dapper 了,Google 在 2010 年发表了《Dapper - a Large-Scale Distributed Systems Tracing Infrastructure》论文介绍了这个系统。虽然 Dapper 系统没有开源,但受该论文的启发,之后不断有团队研发出新的分布式追踪系统,比如阿里的 EagleEye、Twitter 的 Zipkin、大众点评的 CAT、京东的 Hydra、韩国的 PinPoint,Uber 的 Jaeger,还有由华为吴昕研发并在 2017 年加入 Apache 孵化器的 SkyWalking,等等。不过,这些名单中,EagleEye 没有开源,CAT 和 Hydra 早已停止维护。
另外,还有一个开源项目不得不提,那就是 OpenTracing。由于不同的分布式追踪系统的 API 不兼容,那如果需要切换追踪系统就需要做很大的改动,为了解决这个问题,就诞生了 OpenTracing。OpenTracing 并不是一个具体的分布式追踪实现系统,而只是一个轻量级的标准化层,它位于应用程序/类库和追踪或日志分析程序之间,请看下图:
OpenTracing 正在为全球的分布式追踪,提供统一的概念和数据标准。OpenTracing 通过提供平台无关、厂商无关的 API,使得开发人员能够方便地添加(或更换)追踪系统的实现。其支持的语言已经包括了 Go、Java、JavaScript、Python、Ruby、PHP、C++、C#、Objective-C。兼容 OpenTracing 的 APM 也包括了 Jaeger、SkyWalking,还有其他项目,如 LightStep、Instana、Datadog、Elastic APM 等。
那么,分布式追踪系统这么多,目前也没有一枝独大,选型其实不太好做。我这里只提供一点我的见解。首先,我会倾向于选择兼容 OpenTracing 的,毕竟 OpenTracing 已经越来越受到开源和商业团队的追捧。其次,我会考虑社区的活跃度,优先选择活跃度高的,这从 Github 上的 Star 数就可以看出来。结合这两点,最后的选择只剩下 Jaeger 和 SkyWalking,两者都支持了多种语言,包括 Java、Go、C++ 等,也支持集成到服务网格 Istio + Envoy。要说差异点的话,可能 Jaeger 更多用于 Go 系,而 SkyWalking 则主要用于 Java 系。
总结
服务治理的话题其实很大,内容还有很多,不同人的理解还不同,小小一篇文章只能讲到一些皮毛。服务注册与发现、服务配置、服务限流、服务熔断、服务降级、负载均衡、链路追踪,我们只讲到了这 7 块的内容,因为篇幅所限,每一块也并没有太深入,但对于指导实践来说,还是能达到目标的。
最后,小编还给大家整理了-份面试题库,有需要的添加小编的 vx: mxzFAFAFA 即可免费领取! ! !
版权声明: 本文为 InfoQ 作者【比伯】的原创文章。
原文链接:【http://xie.infoq.cn/article/57a0bdc2f77e4fccd203d0aa3】。文章转载请联系作者。
评论