zoukankan      html  css  js  c++  java
  • Spring Cloud微服务实战-服务治理(Spring Cloud Eureka)

     Spring Cloud微服务实战-服务治理(Spring Cloud Eureka)

    1. Spring Cloud Eureka简介

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

    2. 服务治理背景

      在微服务开发工程中,整个系统微服务应用非常多,并且随着业务的发展,微服务的数量在不断增加。而微服务之间的调用非常平凡,假如一个微服务(A)的地址发生变化,调用A接口的其他应用都要进行修改,如果只是通过手工维护的方式,那么很容易出现错误并消耗大量的人力。为了解决微服务架构中的服务实例维护,我们可以通过Spring Cloud Eureka进行服务治理。服务治理包括服务注册和服务发现。
    • 服务注册中心
      构建一个注册中心,每个服务单元想注册中心等级自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。
    • 服务提供者
      在服务注册中心注册,给其他应用提供服务(比如提供接口给其他应用调用)
    • 服务发现消费者
      服务间的调用不再通过指定具体的实例地址来实现,而是通过向服务名发起请求调用的实现。服务调用方在调用服务提供方接口的时候,并不知道具体的服务实例位置。因此,调用方需要向服务注册中心咨询服务,并获取所有服务的实例清单,以实现对具体服务实例的访问。

    3. 简单服务注册中心代码实例

    • eureka-server:服务注册中心,各个服务都注册在此。只需要一个注解@EnableEurekaServer,这个注解需要在springboot工程的启动application类上加
    • eureka-service:服务提供者,注册在eureka-server,注册的名称叫做EUREKA-SERVICE,其他服务调用该服务时就不需要手动配置IP和端口了,可以直接使用EUREKA-SERVICE加端口进行访问。通过注解@EnableEurekaClient 表明自己是一个eurekaclient,但是仅仅@EnableEurekaClient是不够的,还需要在application.yml配置文件中注明自己的服务注册中心的地址
    • eureka-client:服务发现和消费,调用eureka-service的接口。这里是通过spring cloud ribbon负载均衡客户端来调用eureka-service,关于ribbon后面文章会介绍。
     
    application.properties 
     1 spring.application.name=eureka-server
     2 server.port=8761
     3 
     4 eureka.instance.hostname=localhost
     5 
     6 # 代表不向注册中心注册自己
     7 eureka.client.register-with-eureka=false
     8 
     9 # 不去检索服务
    10 eureka.client.fetch-registry=false
    11 eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka
    EurekaServerApplication.java
     1 package com.suns.eurekaserver;
     2 
     3 
     4 import org.springframework.boot.SpringApplication;
     5 import org.springframework.boot.autoconfigure.SpringBootApplication;
     6 import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
     7 
     8 @EnableEurekaServer
     9 @SpringBootApplication
    10 public class EurekaServerApplication {
    11 
    12     public static void main(String[] args) {
    13         SpringApplication.run(EurekaServerApplication.class, args);
    14     }
    15 }
     
    application.properties
    spring.application.name=eureka-service
    server.port=8762
    
    eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
      
    EurekaServiceApplication.java
     1 package com.suns.eurekaservice;
     2 
     3 import org.springframework.boot.SpringApplication;
     4 import org.springframework.boot.autoconfigure.SpringBootApplication;
     5 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
     6 
     7 @EnableDiscoveryClient
     8 @SpringBootApplication
     9 public class EurekaServiceApplication {
    10 
    11     public static void main(String[] args) {
    12         SpringApplication.run(EurekaServiceApplication.class, args);
    13     }
    14 }
    HelloController.java
     1 package com.suns.eurekaservice.api;
     2 
     3 import org.apache.log4j.Logger;
     4 import org.springframework.beans.factory.annotation.Autowired;
     5 import org.springframework.cloud.client.ServiceInstance;
     6 import org.springframework.cloud.client.discovery.DiscoveryClient;
     7 import org.springframework.web.bind.annotation.RequestMapping;
     8 import org.springframework.web.bind.annotation.RequestMethod;
     9 import org.springframework.web.bind.annotation.RestController;
    10 
    11 
    12 @RestController
    13 public class HelloController {
    14 
    15     private final Logger logger = Logger.getLogger(getClass());
    16 
    17     @Autowired
    18     private DiscoveryClient client;
    19 
    20     @RequestMapping(value = "/hello", method = RequestMethod.GET)
    21     public String index() {
    22         ServiceInstance instance = client.getLocalServiceInstance();
    23         logger.info("/hello, host: " + instance.getHost() + ", service_id: " + instance.getServiceId());
    24         return "Hello World";
    25     }
    26 }
    启动eureka-server和eureka-service后,打开服务注册中心管理页面:发现服务提供者已成功注册到注册中心。
      

     服务发现与消费(eureka-client)

    application.properties

    1 spring.application.name=eureka-client
    2 server.port=8763
    3 
    4 eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

    EurekaClientApplication.java

     1 package com.suns.eurekaclient;
     2 
     3 import org.springframework.boot.SpringApplication;
     4 import org.springframework.boot.autoconfigure.SpringBootApplication;
     5 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
     6 import org.springframework.cloud.client.loadbalancer.LoadBalanced;
     7 import org.springframework.context.annotation.Bean;
     8 import org.springframework.web.client.RestTemplate;
     9 
    10 @EnableDiscoveryClient
    11 @SpringBootApplication
    12 public class EurekaClientApplication {
    13 
    14     @Bean
    15     @LoadBalanced
    16     RestTemplate restTemplate() {
    17         return new RestTemplate();
    18     }
    19 
    20     public static void main(String[] args) {
    21         SpringApplication.run(EurekaClientApplication.class, args);
    22     }
    23 }

    ConsumerController.java

     1 package com.suns.eurekaclient.api;
     2 
     3 import org.springframework.beans.factory.annotation.Autowired;
     4 import org.springframework.web.bind.annotation.RequestMapping;
     5 import org.springframework.web.bind.annotation.RequestMethod;
     6 import org.springframework.web.bind.annotation.RestController;
     7 import org.springframework.web.client.RestTemplate;
     8 
     9 
    10 @RestController
    11 public class ConsumerController {
    12 
    13     @Autowired
    14     RestTemplate restTemplate;
    15 
    16     @RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET)
    17     public String helloConsumer() {
    18         return restTemplate.getForEntity("http://EUREKA-SERVICE/hello", String.class).getBody();
    19     }
    20 }
    通过访问http://localhost:8763/ribbon-consumer,发现页面成功显示Hello World
    查看eureka-service日志,成功打印出如下日志:
    2020-02-10 07:33:01.234  INFO 4036 --- [nio-8762-exec-9] c.s.eurekaservice.api.HelloController    : /hello, host: DCNA7245.APAC.Local, service_id: eureka-service

    4. 高可用注册中心代码实例

    Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就形成了一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。
    • eureka-high-avai-server:服务注册中心。通过spring boot profile启动2个不同实例的eureka-server。
    • eureka-high-avai-service:服务提供者。通过spring boot profile启动2个eureka-service
    • eureka-high-avai-client:服务消费者。
    需要修改C:WindowsSystem32driversetchosts,在文件末尾加上一下信息
    127.0.0.1    server1
    127.0.0.1    server2
    在配置高可用注册中心时,要配置一下2个参数,这2个参数是想服务注册中心注册自己以实现达到高可用。:
    eureka.client.register-with-eureka=true(默认是true)
    eureka.client.fetch-registry=true(默认是true)
     
    application.yml
    ---
    spring:
      profiles: server1                                 # 指定profile=server1
      application:
        name: eureka-high-avai-server
    server:
      port: 8761
    eureka:
      instance:
        hostname: server1                               # 指定当profile=server1时,主机名
      client:
        serviceUrl:
          defaultZone: http://server2:8762/eureka/      # 将自己注册到server2这个Eureka上面去
      server:
        enableSelfPreservation: false                   # 关掉Eureka的自我保护机制
    
    ---
    spring:
      profiles: server2
      application:
        name: eureka-high-avai-server
    server:
      port: 8762
    eureka:
      instance:
        hostname: server2
      client:
        serviceUrl:
          defaultZone: http://server1:8761/eureka/
      server:
        enable-self-preservation: false
    EurekaHighAvaiServerApplication.java
     1 package com.suns.eurekahighavaiserver;
     2 
     3 import org.springframework.boot.SpringApplication;
     4 import org.springframework.boot.autoconfigure.SpringBootApplication;
     5 import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
     6 
     7 @EnableEurekaServer
     8 @SpringBootApplication
     9 public class EurekaHighAvaiServerApplication {
    10 
    11     public static void main(String[] args) {
    12         SpringApplication.run(EurekaHighAvaiServerApplication.class, args);
    13     }
    14 }
    通过:
    java -jar eureka-high-avai-server-0.0.1-SNAPSHOT.jar –spring.profiles.active=server1
    java -jar eureka-high-avai-server-0.0.1-SNAPSHOT.jar –spring.profiles.active=server2
    来启动2个eureka-high-avai-server
     
    成功启动后,访问localhost:8761或者localhost:8762,可以看到1个服务2个实例在注册中心都已注册。
     
    application.yml
    我这里配置service1和service2同时向server1和server2注册,如果只配置service1向server1注册,service2向server2注册。在2个service启动后,注册中心会自动同步从而实现注册中心之间服务同步。这是由于服务注册中心之间因互相注册为服务,当服务提供者发送注册请求到一个服务注册中心时,它会将该请求转发给集群中项链的其他注册中心。
     1 ---
     2 spring:
     3   profiles: service1
     4   application:
     5     name: eureka-high-avai-service
     6 server:
     7   port: 8763
     8 eureka:
     9   client:
    10     serviceUrl:
    11       defaultZone: http://server1:8761/eureka/,http://server2:8762/eureka/
    12 
    13 ---
    14 spring:
    15   profiles: service2
    16   application:
    17     name: eureka-high-avai-service
    18 server:
    19   port: 8764
    20 eureka:
    21   client:
    22     serviceUrl:
    23       defaultZone: http://server1:8761/eureka/,http://server2:8762/eureka/

    EurekaHighAvaiServiceApplication.java

     1 package com.suns.eurekahighavaiservice;
     2 
     3 import org.springframework.boot.SpringApplication;
     4 import org.springframework.boot.autoconfigure.SpringBootApplication;
     5 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
     6 
     7 @EnableDiscoveryClient
     8 @SpringBootApplication
     9 public class EurekaHighAvaiServiceApplication {
    10 
    11     public static void main(String[] args) {
    12         SpringApplication.run(EurekaHighAvaiServiceApplication.class, args);
    13     }
    14 }
    HelloController.java
     1 package com.suns.eurekahighavaiservice.api;
     2 
     3 import org.apache.log4j.Logger;
     4 import org.springframework.beans.factory.annotation.Autowired;
     5 import org.springframework.beans.factory.annotation.Value;
     6 import org.springframework.cloud.client.ServiceInstance;
     7 import org.springframework.cloud.client.discovery.DiscoveryClient;
     8 import org.springframework.web.bind.annotation.RequestMapping;
     9 import org.springframework.web.bind.annotation.RequestMethod;
    10 import org.springframework.web.bind.annotation.RestController;
    11 
    12 
    13 @RestController
    14 public class HelloController {
    15 
    16     private final Logger logger = Logger.getLogger(getClass());
    17 
    18     @Autowired
    19     private DiscoveryClient client;
    20 
    21     @Value("${server.port}")
    22     String port;
    23 
    24     @RequestMapping(value = "/hello", method = RequestMethod.GET)
    25     public String index() {
    26         ServiceInstance instance = client.getLocalServiceInstance();
    27         logger.info("/hello, host: " + instance.getHost() + ", service_id: " + instance.getServiceId() + ", port:" + port);
    28         return "/hello, host: " + instance.getHost() + ", service_id: " + instance.getServiceId() + ", port:" + port;
    29     }
    30 }
    通过:
    java -jar eureka-high-avai-service-0.0.1-SNAPSHOT.jar –spring.profiles.active=service1
    java -jar eureka-high-avai-service-0.0.1-SNAPSHOT.jar –spring.profiles.active=service2
    来启动2个eureka-high-avai-service,启动完成后,发现eureka-high-avai-service服务的2个实例都已在注册中心成功注册。
     
    1 spring.application.name=eureka-client
    2 server.port=8765
    3 
    4 eureka.client.service-url.defaultZone=http://server1:8761/eureka/,http://server2:8762/eureka/
    EurekaHighAvaiClientApplication.java
     1 package com.suns.eurekahighavaiclient;
     2 
     3 import org.springframework.boot.SpringApplication;
     4 import org.springframework.boot.autoconfigure.SpringBootApplication;
     5 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
     6 import org.springframework.cloud.client.loadbalancer.LoadBalanced;
     7 import org.springframework.context.annotation.Bean;
     8 import org.springframework.web.client.RestTemplate;
     9 
    10 @EnableDiscoveryClient
    11 @SpringBootApplication
    12 public class EurekaHighAvaiClientApplication {
    13 
    14     @Bean
    15     @LoadBalanced
    16     RestTemplate restTemplate() {
    17         return new RestTemplate();
    18     }
    19 
    20     public static void main(String[] args) {
    21         SpringApplication.run(EurekaHighAvaiClientApplication.class, args);
    22     }
    23 }
    ConsumerController.java
     1 package com.suns.eurekahighavaiclient.api;
     2 
     3 import org.springframework.beans.factory.annotation.Autowired;
     4 import org.springframework.web.bind.annotation.RequestMapping;
     5 import org.springframework.web.bind.annotation.RequestMethod;
     6 import org.springframework.web.bind.annotation.RestController;
     7 import org.springframework.web.client.RestTemplate;
     8 
     9 @RestController
    10 public class ConsumerController {
    11 
    12     @Autowired
    13     RestTemplate restTemplate;
    14 
    15     @RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET)
    16     public String helloConsumer() {
    17         return restTemplate.getForEntity("http://EUREKA-HIGH-AVAI-SERVICE/hello", String.class).getBody();
    18     }
    19 }
    启动完成后,访问eureka-high-avai-client服务:http://localhost:8765/ribbon-consumer。多次调用以后发现页面返回的服务端口不一致:
    /hello, host: DESKTOP-1BVDM6L, service_id: eureka-high-avai-service, port:8763
    /hello, host: DESKTOP-1BVDM6L, service_id: eureka-high-avai-service, port:8764

    5. Eureka详解

    • 服务注册:“服务提供者”在启动的时候会通过发送REST请求的方式将自己注册到Eureka Server上,同时带上了自身服务的一些元数据信息。Eureka Server接收到这个REST请求之后,将元数据信息存储在一个双层结构Map中,其中第一层的key是服务名,第二层的key是具体服务的实例名。Eureka server会维护一份只读的服务清单来返回给服务消费者,同时改缓存清单会每隔30秒更新一次。
    • 服务同步:当服务提供者发送注册请求到一个服务注册中心时,该注册中心会将请求转发给集群中相连的其他注册中心,从而实现注册中心之间的服务同步。
    • 服务续约:在注册完服务之后,服务提供者会维护一个心跳来告诉Eureka Server:“我还活着”,防止Eureka Server的“剔除任务”将该服务实例从服务列表中排除出去,我们称该操作为服务预约(Renew)。可以通过一下参数进行相关配置。   
    1 eureka:
    2     instance:
    3         leaseRenewalIntervalInSeconds: 1 #心跳间隔时间1s
    4         leaseExpirationDurationInSeconds: 2 #连续2s未响应时将服务注销                        
    • 获取服务:服务消费者会发送一个REST请求给服务注册中心,来获取注册中心的服务清单。
    • 服务调用:服务消费者获取到服务清单后,通过服务名获得具体提供服务的实例名和该实例的元数据信息。再根据自己的需要决定具体调用那个实例。对于访问实例的选择,Eureka中有Region和Zone的概念,一个Region包含多个Zone,每个服务客户端需要注册到一个Zone中,所以客户端对应一个Region和一个Zone。在进行服务调用的时候,优先访问同处一个Zone中的服务提供方,若访问不到,就访问其他的Zone。
    • 服务下线:当服务实例进行正常的关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务端接收到请求以后,将该服务状态设置为下线(DOWN),并把改下线事件传播出去。
    • 失效剔除:对于非正常关闭的服务,Eureka Server会启动一个定时任务,每60S(默认)将清单没有续约的服务(90秒没有收到服务提供者的心跳)移除。
    • 自我保护:Eureka Server每30S接收其他注册服务的心跳,同时Eureka Server会统计心跳失败的比例(15分钟之内是否低于85%),如果低于85%,Eureka Server会将当前的实例注册信息保护起来,让这些实例不会过期。可以通过一下配置确保注册中心可以将不可用的实例正确移除。低于85%时,访问Eureka Server会出现:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY’RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.报错
    1 eureka:
    2     server:
    3         enable-self-preservation: false    

    6. 配置详解

    服务注册类配置
    通过org.springframework.cloud.netflix.eureka.EurekaClientConfigBean类来查看详细配置,这些配置信息都是eureka.client为前缀。
    参数名 说明 默认值
    enabled 启用Eureka客户端 true
    registryFetchIntervalSeconds 从Eureka服务端获取注册信息的时间间隔 30S
    instanceInfoReplicationIntervalSeconds 更新实例信息的变化到Eureka服务端的时间间隔 30S
    initialInstanceInfoReplicationIntervalSeconds 初始化实例信息到Eureka服务端的时间间隔 40S
    eurekaServiceUrlPollIntervalSeconds 轮询Eureka服务端地址更改的时间间隔。当与Spring Cloud Config配合,动态刷新Eureka的serviceURL地址需要关注 300S
    eurekaServerReadTimeoutSeconds 读取Eureka Server信息的超时时间 8S
    eurekaServerConnectTimeoutSeconds 连接Eureka Server信息的超时时间 5S
    eurekaServerTotalConnections 从Eureka客户端到所有Eureka服务端的连接总数 200S
    eurekaServerTotalConnectionsPerHost 从Eureka客户端到所有Eureka服务端主机的连接总数 50S
    eurekaConnectionIdleTimeoutSeconds Eureka服务端连接的空闲关闭时间 30S
    heartbeatExecutorThreadPoolSize 心跳连接池的初始化线程数 2S
    heartbeatExecutorExponentialBackOffBound 心跳超时重试延迟时间的最大乘数值 10S
    cacheRefreshExecutorThreadPoolSize 缓存刷新线程池的初始化线程数 2S
    cacheRefreshExecutorExponentialBackOffBound 缓存刷新重试延迟时间的最大乘数值 10S
    useDnsForFetchingServiceUrls 使用DNS来获取Eureka服务端的serviceUrl false
    registerWithEureka 是否要将自身的实例信息注册到Eureka服务端 true
    preferSameZoneEureka 是否偏好使用处于相同Zone的Eureka服务端 true
    filterOnlyUpInstances 获取实例时是否过滤,仅保留UP状态的实例 true
    fetchRegistry 是否从Eureka服务端获取注册信息 true
    服务实例类配置
    服务实例类的配置信息,可以通过org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean源码来获取详细信息,这些配置以eureka.instance为前缀。EurekaInstanceConfigBean中大部分都是对服务实例元数据的配置,元数据是用来描述自身服务信息的对象,其中包含了一些标准话的元数据,比如服务名称、实例名称、实例IP、实例端口等用于服务治理的重要信息;以及一些用于负载均衡策略或是其他特殊用途的自定义元数据信息。
    服务实例类的配置信息是通过EurekaInstanceConfigBean来加载,但真正注册时候会包装成com.netflix.appinfo.InstanceInfo。
    • 实例名配置
    实例名,即InstanceInfo中的instanceId参数,它是区分同一服务中不同实例的唯一标识。Spring Cloud Eureka默认配置实例名的规则如下:
    ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port} 
    只要满足instance_id或者port不一致就能启动同一个服务的多个实例
    • 断点配置
    在InstanceInfo中homePageUrl、statusPageUrl、healthCheckUrl,他们分别代表了应用主页的URL、状态页的URL、健康检查的URL。其中状态页和健康检查的URL在Spring Cloud Eureka中默认使用了spring-boot-actuator模块提供的/info端点和/health端点。为了确保服务正常,我们必须确保Eureka客户端的/health断点在发送元数据的时候,是一个能够被注册中心访问到的地址,否则注册中心不会根据应用的健康检查来更改状态。而/info端点如果不正确,会导致在Eureka面板中单机服务实例时,无法访问到服务实例提供的信息接口。
    如果设置了context-path,所有spring-boot-actuator模块的监控端点都会增加一个前缀:
    1 management.context-path=/hello
    2  
    3 eureka.instance.statusPageUrlPath=${management.context-path}/info
    4 eureka.instance.healthCheckUrlPath=${management.context-path}/health
    • 健康检测
    默认情况下,Eureka中各个服务实例的健康检测并不是通过spring-boot-actuator模块的/health端点来实现,而是依靠客户端心跳的方式来保持服务实例的存货。这样无法真正监控服务提供者是否能正常提供服务(比如服务提供者数据库连接不上),但是在注册中心它还是UP状态。为了避免这种情况,我们可以把Eureka客户端的健康检测交给spring-boot-actuator模块的/health端点,以实现更加全面的健康维护。
    1)在pom.xml中引入spring-boot-actuator模块。
    2)在application.properties中增加参数配置eureka.client.healthcheck.enabled=默认是true
    3)如果客户端的/health端点路径做了特殊配置,参考前面的端点配置
    • 其他配置
    参数名 说明 默认值
    preferIpAddress 是否优先使用IP作为主机名的标识 false
    leaseRenewalIntervalInSeconds Eureka客户端向服务端发送心跳的时间间隔 30S
    leaseExpirationDurationInSeconds Eureka服务端收到最后一次心跳之后等待的时间上限。超过之后服务端将该服务实例从服务清单中剔除,从而禁止服务调用请求被发送到该实例 90S
    nonSecurePort 非安全的通信端口号 80
    securePort 安全的通信端口号 443
    nonSecurePortEnabled 是否启用非安全的通信端口号 true
    securePortEnabled 是否启用非安全的通信端口号  
    appname 服务名,默认取spring.application.name配置值,如果没有则为unknown  
    hostname 主机名,不配置的时候将根据操作系统的主机名来获取
     

    7. 鸣谢

    感谢《Spring Cloud微服务实战》作者:瞿永超

    8. 说明

    这篇文章主要是《Spring Cloud微服务实战》的读书笔记。
  • 相关阅读:
    给object数组进行排序(排序条件是每个元素对象的属性个数)
    转化一个数字数组为function数组(每个function都弹出相应的数字)
    找出数字数组中最大的元素(使用Math.max函数)
    JavaFX学习笔记索引
    JavaFX学习:第一个程序 Hello World
    Notion笔记链接
    Windows 下 Chocolatey 的安装与使用
    Bootstrap3 文档整理
    (转)OpenCV视频生成报错 Incorrect library version loaded Could not open codec ‘libopenh264‘: Unspecified error
    OpenCV不能读取 mp4 的问题(转)
  • 原文地址:https://www.cnblogs.com/kesuns/p/12284238.html
Copyright © 2011-2022 走看看