Spring Cloud 服务治理:Spring Cloud Eureka (一)

原文:Spring Cloud 服务治理:Spring Cloud Eureka (一)

Spring Cloud Eureka

Spring Cloud Eureka 是Spring Cloud Netflix微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。Spring Cloud 通过为Eureka增加了Spring Boot风格的自动化配置,我们只需要通过简单引入依赖和注解配置就能让Spring Boot构建的微服务应用轻松地与Eureka服务治理体系进行整合。

这篇文章将从以下几部分来介绍Eureka:

  • 服务治理
  • Eureka介绍
  • 构建服务注册中心
  • 服务注册
  • 服务发现

服务治理

在开始之前还是想再对服务治理最一些说明。服务治理可以说是微服务中最为核心和基础的模块,他主要用来实现各个微服务实例的自动化注册于发现。

为什么这么需要服务治理模块呢?

在早期开始构建微服务架构时,我们的微服务系统可能并不多,这时候我们只需要通过一些静态配置来完成服务的调用。比如 有两个服务A、B,其中A服务需要调用服务B来完成一个业务操作时候。为了实现服务B的高可用,不论采用服务端负载均衡还是客户端复杂均衡,都需要手工维护服务B的具体实例清单。但是随着业务的发展,系统功能越来越来维护(通过手工静态配置的方式)。并且随着业务的飞速发现,我们的集群规模、服务的位置、服务的命名等都有可能发生变化,如果还是通过手工维护的方式很容易出错或者命名冲突等问题,同时对于静态文件的维护也必将提高人力成本。 画了个示意图如下:

image.png

可以看到早期的微服务架构需要在服务A维护服务B的服务列表,包含服务B的集群、服务等信息,相对来说还比较简单、明了。而当业务发展很快一段时间之后,这个列表信息可能想下面这样子:

image.png

可以看到需要维护的服务列表越来越多、越来越复杂,然后这些服务信息也会随着业务的增长不断变化,最终导致的结构就是人工维护已经不现实了,所以就有了服务治理的出现。

服务治理的核心:围绕服务注册和服务发现机制来完成对微服务应用实例的自动化管理。

服务注册

通常都会构建一个注册中心,每个注册单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。

比如我们有两个提供服务A的进程分别运行与192.168.0.10:8080与192.168.0.11:8081.另外有三个提供服务B的进程运行与192.168.0.20:8080、192.168.0.20:8081与192.168.0.20:8082位置上。当这些进程均启动并想注册中心注册自己的服务之后,注册中心就会维护一个类似下面的清单:

image.png

服务发现

由于在服务治理框架下运作,服务间的调用不在通过指定具体的服务实例地址来实现,而是通过向服务名发起请求调用实现。所以,服务调用方在调用服务提供接口时,是不知道具体的服务实例位置。因此调用方需要向注册中心咨询,并获取所有实例的清单,以实现对具体服务实例的访问。

举个例子,比如服务C需要调用服务A,首先服务C需要向注册中心发起服务A实例清单的拉取请求,会拿到一个服务A的可用位置:192.168.0.10:8080与192.168.0.11:8081,便从该清单中通过某种轮训策略取出一个位置来进行服务调用,这个策略就是大名鼎鼎的复杂均衡策略,后面在介绍。

Netflix Eureka

上面服务治理中我们提供一个很重要的组件 —— 服务注册与发现。Spring Cloud Eureka正式充当这个注册中心的角色,Spring Cloud Eureka使用Netflix Eureka来实现服务注册和发现,它即包含的服务端组件,也包含了客户端组件,并且服务端与客户端均采用Java编写, 所以Eureka主要使用与通过Java实现的分布式系统,或是与JVM兼容语言构建的系统。 但是,Eureka服务端的服务治理机制提供了完备的RESTful API,所以它也支持将非Java语言构建的微服务应用纳入Eureka的服务治理体系中来。

Eureka服务端,我们也成为服务的注册中心。它同其他服务注册中心一样,支持高可用配置。
Eureka客户端,主要处理服务的注册于发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序中运行,Eureka客户端向注册中心注册自身提供服务并周期性地发送心跳来更新它的服务状态。同时,它也能从服务端查询当前注册的服务信息并把他们缓存到本地并周期性地刷新服务状态。

开始搭建服务注册中心

首先,创建一个基础的Spring Boot工程,命名为eureka-server

image.png

在pom.xml中引入必要的依赖内容,代码如下:

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>


<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Brixton.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

第二步,通过@EnableEurekaServer注解启动一个服务注册中心提供给其他应用进行对话,同时指定一些配置:

启动注册中心

// 启动一个服务注册中心
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

在默认配置下,该服务注册中心也会将自己作为客户端来尝试注册自己,所以我们需要禁用它的客户端注册行为,主需要在application.properties配置中增加如下配置:

# 注册中心端口设置8888,可自己指定其他端口
server.port=8888

eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

配置解释:

  • eureka.client.register-with-eureka:由于该应用为注册中心,所以设置为false,代表不向注册中心注册自己。
  • eureka.client.fetch-registry:由于注册中心的职责就是维护服务实例,斌不需要去检索服务,所以也设置为false。

在完成上面的配置后,启动应用并访问 http://localhost:8888/ , 可以看到如下图所示的Eureka信息面板,其中Instance currently registered with Eureka栏是空的,说明该注册中心还没有注册任何服务:

image.png

注册服务提供者

在完成了服务注册中心的搭建之后,接下来我们尝试搭建一个既有的Spring Boot应用加入eureka-server的服务治理体系中去。

搭建方式跟之前的没什么大的区别,首先需要pom.xml配置eureka客户端依赖,并增加服务注册配置:

pom.xml

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Brixton.SR5</version>
            <type>pom</type>
            <scope>import</scope>
                </dependency>
    </dependencies>
</dependencyManagement>

第二步:application.properties增加服务注册配置:

# eureka client1 server port
server.port=8001

# set spring application name
spring.application.name=eureka-client-1

# server registry config
eureka.client.serviceUrl.defaultZone=http://localhost:8888/eureka/

第三步:使用@EnableEurekaClient注解,激活Eureka中的DiscoveryClient

// 激活Eureka中的DiscoveryClient
@EnableEurekaClient
@SpringBootApplication
public class EurekaClient1Application {

    public static void main(String[] args) {
        SpringApplication.run(EurekaClient1Application.class, args);
    }
}

第四步:编写一个/hello请求处理接口,并注入DiscoveryClient对象:

@RestController
public class HelloController {

    @Autowired
    private DiscoveryClient client;

    @RequestMapping("/hello")
    public String hello(){

        ServiceInstance instance = client.getLocalServiceInstance();
        System.out.println("/hello, host:"+instance.getHost()+", service_id:"+instance.getServiceId()+);
        return "Hello !!!";
    }
}

最后我们分别启动注册中心eureka-server和eureka-client两个服务(注意先启动服务端,在启动Client,不然Client注册服务时会找不到服务端)。报错了:

java.lang.NoSuchMethodError: org.springframework.boot.builder.SpringApplicationBuilder.<init>([Ljava/lang/Object;)V

这是因为我的SpringBoot版本与SpringCloud不兼容,这里修改SpringBoot版本解决问题。

修改前:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

修改后:

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

服务启动时com.netflix.discovery.DiscoveryClient对象打印了该服务的注册信息,表示服务注册成功。

image.png

此时我们到服务注册中心的控制台 可视化 界面可以看到有服务的注册信息:

image.png

然后我们再访问http://localhost:8001/hello接口,直接向服务端发起请求,在控制台中可以看到如下输出:

/hello, host:192.168.43.73, service_id:eureka-client-1

这些输出内容就是之前我在HwlloController中注入的DiscoveryClient接口对象,从服务注册中西获取的服务相关信息。

服务高可用注册中心

上面示例中的服务注册是在单机模式下,生产环境我们一定是要保证注册中心的高可用,需要集群部署。那eureka是如何保证高可用的呢?

Eureka Server的设计一开始就考虑了高可用问题,在Eureka的服务治理设计中,所有节点即时服务提供方、也会服务消费方,服务注册中心也不例外。上面在单节点配置中,我设置了一下俩个参数来禁止服务注册中心注册自己。

eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false

Eureka Server的高可用实际就是将自己也作为服务注册到其他服务中心,这样就可以形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。

下面我再来尝试搭建高可用服务注册中心的集群。可以在上面单点的注册中心基础之上进行扩展,构建一个双节点的注册中心集群。

第一步:

1.创建application-peer1.properties,作为peer1服务中心的配置,serveiceUrl执行peer2:

server.port=8888
spring.application.name=eureka-server-1

eureka.instance.hostname=peer1
eureka.client.serviceUrl.defaultZone=http://peer2:9999/eureka/
  1. 创建创建application-peer2.properties,作为peer1服务中心的配置,serveiceUrl执行peer1:
server.port=9999
spring.application.name=eureka-server-2

eureka.instance.hostname=peer2
eureka.client.serviceUrl.defaultZone=http://peer1:8888/eureka/

第二步:
本地hosts添加对peer1和peer2的转换:

127.0.0.1 peer1
127.0.0.1 peer2

第三步:
使用java -jar 同时利用spring.profiles.active属性来分别启动peer1和peer2:

java -jar eureka.jar --spring.profiles.active=peer1

java -jar eureka.jar --spring.profiles.active=peer2

服务都启动后在访问http://localhost:8888/http://localhost:9999/ 会发现每一个服务注册中心都同时注册了两个服务:

image.png

以此方式来保证eureka服务注册中心的高可用。

稍等,还有个问题,eureka在集群部署的情况下,服务提供方服务注册方式需要做一些简单的配置才能将服务注册到Eureka Server集群中。我还是以上面那个提供/hello接口eureka-client为例子。修改application-properties:

# set spring application name
spring.application.name=eureka-client-1

# server registry config, 如果有多个逗号分割即可
eureka.client.serviceUrl.defaultZone=http://peer1:8888/eureka/,http://peer2:9999/eureka/

然后我们在启动eureka-client观察两个服务注册中心看看有什么变化:

image.png

可以看到两个注册中心同时注册了自己,另一个注册中心以及eureka-server。。因此此时加入peer1挂调了,peer2依然可以保证服务的可用性。

服务的注册于发现

创建服务Provider

通过上面的示例,我做了服务注册中心的单点搭建、高可用搭建及实现了服务的注册。现在看一下具体在项目使用中如何实现服务的注册和发现。

首先,我还是以上面同上面创建eureka-client方式及注册方式,我创建一个eureka-provider服务,作为服务提供方。首先通过java -jar 结合 server.port的方式我启动四个节点的服务提供方,并注册到peer1、peer2注册中心上。

java -jar eureka-provider.jar --server.port=8001
java -jar eureka-provider.jar --server.port=8002
java -jar eureka-provider.jar --server.port=8003
java -jar eureka-provider.jar --server.port=8004

启动成功后查看注册中心,会发现服务提供者eureka-provider-1注册了四个实例单元:

image.png

创建服务Consumer

第一步还是创建一个springBoot工程,pom.xml添加:

dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Brixton.SR5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

第二步,ConsumerApplication通过@EnableEurekaClient让该应用注册为Eureka客户单应用,以获得服务发现的能力。同时,在该主类中创建RestTemplate的Spring Bean实例,并通过@LoadBalanced注解开启客户端负载均衡:

// 激活Eureka中的DiscoveryClient
@EnableEurekaClient
@SpringBootApplication
public class ConsumerApplication {

        @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
}

第二步:创建一个ConsumerController类,实现一个/ribbon-consumer接口,在该接口中,通过在上面创建的RestTemplate来实现对EUREKA-PROVIDER-1服务 /hello 接口的调用。需要注意的是这里访问的地址是服务名EUREKA-PROVIDER-1,而不是一个具体的地址,在服务治理框架中,这是一个非常重要的特性:

@RestController
public class ConsumerController {

    @Autowired
    RestTemplate template;

    @GetMapping("/ribbon-consumer")
    public String helloConsumer(){

        return template.getForEntity("http://EUREKA-PROVIDER-1/hello", String.class).getBody();
    }
}

第三步:在application.properties中配置eureka服务注册中心的位置,需要与之前EUREKA-PROVIDER-1的配置一直,不然发现不了该服务:


server.port=9000

spring.application.name=eureka-consumer-1
eureka.client.serviceUrl.defaultZone=http://peer1:8888/eureka/,http://peer2:9999/eureka/

最后一步启动Consumer服务:

image.png

访问http://localhost:9000/ribbon-consumer发起Get请求,成功返回了“Hello !!!”信息。此时我们在consumer的服务控制台可以看到如下信息,Ribbon输出了当前客户端维护的EUREKA-PROVIDER-1服务列表情况,Ribbo就是按照这个信息进行轮训访问,以实现基于客户端的负载均衡。另外还输出其他一些有用的信息,如对各个实例的请求总数、第一次连接信息、上一次拦截信息、总的请求失败数量等:

2019-10-26 17:41:28.286  INFO 64779 --- [nio-9000-exec-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: EUREKA-PROVIDER-1.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2019-10-26 17:41:28.339  INFO 64779 --- [nio-9000-exec-1] c.n.u.concurrent.ShutdownEnabledTimer    : Shutdown hook installed for: NFLoadBalancer-PingTimer-EUREKA-PROVIDER-1
2019-10-26 17:41:28.424  INFO 64779 --- [nio-9000-exec-1] c.netflix.loadbalancer.BaseLoadBalancer  : Client:EUREKA-PROVIDER-1 instantiated a LoadBalancer:DynamicServerListLoadBalancer:{NFLoadBalancer:name=EUREKA-PROVIDER-1,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2019-10-26 17:41:28.432  INFO 64779 --- [nio-9000-exec-1] c.n.l.DynamicServerListLoadBalancer      : Using serverListUpdater PollingServerListUpdater
2019-10-26 17:41:28.496  INFO 64779 --- [nio-9000-exec-1] c.netflix.config.ChainedDynamicProperty  : Flipping property: EUREKA-PROVIDER-1.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2019-10-26 17:41:28.501  INFO 64779 --- [nio-9000-exec-1] c.n.l.DynamicServerListLoadBalancer      : DynamicServerListLoadBalancer for client EUREKA-PROVIDER-1 initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=EUREKA-PROVIDER-1,current list of Servers=[qiwangs-air:8001, qiwangs-air:8002, qiwangs-air:8004, qiwangs-air:8003],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone;  Instance count:4;   Active connections count: 0;    Circuit breaker tripped count: 0;   Active connections per server: 0.0;]
},Server stats: [[Server:qiwangs-air:8003;  Zone:defaultZone;   Total Requests:0;   Successive connection failure:0;    Total blackout seconds:0;   Last connection made:Thu Jan 01 08:00:00 CST 1970;  First connection made: Thu Jan 01 08:00:00 CST 1970;    Active Connections:0;   total failure count in last (1000) msecs:0; average resp time:0.0;  90 percentile resp time:0.0;    95 percentile resp time:0.0;    min resp time:0.0;  max resp time:0.0;  stddev resp time:0.0]
, [Server:qiwangs-air:8004; Zone:defaultZone;   Total Requests:0;   Successive connection failure:0;    Total blackout seconds:0;   Last connection made:Thu Jan 01 08:00:00 CST 1970;  First connection made: Thu Jan 01 08:00:00 CST 1970;    Active Connections:0;   total failure count in last (1000) msecs:0; average resp time:0.0;  90 percentile resp time:0.0;    95 percentile resp time:0.0;    min resp time:0.0;  max resp time:0.0;  stddev resp time:0.0]
, [Server:qiwangs-air:8001; Zone:defaultZone;   Total Requests:0;   Successive connection failure:0;    Total blackout seconds:0;   Last connection made:Thu Jan 01 08:00:00 CST 1970;  First connection made: Thu Jan 01 08:00:00 CST 1970;    Active Connections:0;   total failure count in last (1000) msecs:0; average resp time:0.0;  90 percentile resp time:0.0;    95 percentile resp time:0.0;    min resp time:0.0;  max resp time:0.0;  stddev resp time:0.0]
, [Server:qiwangs-air:8002; Zone:defaultZone;   Total Requests:0;   Successive connection failure:0;    Total blackout seconds:0;   Last connection made:Thu Jan 01 08:00:00 CST 1970;  First connection made: Thu Jan 01 08:00:00 CST 1970;    Active Connections:0;   total failure count in last (1000) msecs:0; average resp time:0.0;  90 percentile resp time:0.0;    95 percentile resp time:0.0;    min resp time:0.0;  max resp time:0.0;  stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@13b808a3

同时,多请求几次之后会发现provider的4个实例控制台会交替打印/hello, host:qiwangs-air, service_id:eureka-provider-1的信息

负载均衡

总结

本片文章花了很长的篇幅记录了自己的学习内容,包括什么是服务治理、Eureka,以及如何构建服务注册中心,包括服务注册中心的高可用构建,以及最终实现了一个服务注册和发现的例子。

原文:Spring Cloud 服务治理:Spring Cloud Eureka (一)

个人博客网站:RelaxHeart网/Tec博客

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容