文章目录
Eureka
- Eureka这个词来源于古希腊语, 意为"我找到了!我发现了!" 。 据传,阿基米德在洗澡时发现浮力原理, 高兴得来不及穿上衣服,跑到街上大喊 :" Eureka ! "
- Eureka是 Netflix开源的服务注册、发现组件,本身是一个基于 REST 的服务 。 它包含 Server和 Client两部分。 SpringCloud将它集成在子项目 SpringCloudNetflix中
- Eureka包含两个组件 : EurekaServer和 EurekaClient- EurekaServer 提供服务发现的能力,各个服务启动时,会向EurekaServer注册自己的信息(例如IP、端口、微服务名称等), EurekaServer会存储这些信息。- EurekaClient是一个 Java客户端,用于简化与 EurekaServer的交互
- 服务启动后,会周期性(默认 30s ) 地向 EurekaServer发送心跳以续约自己的"租期",如果EurekaServer在一定时间内(默认90s)没有接收到某个服务实例的心跳,EurekaServer将注销该实例
- 默认情况下,EurekaServer本身也会将自己作为客户端来尝试注册它自己。多个EurekaServer实例相互之间通过复制的方式来显示服务注册表中数据的同步
- EurekaClient会缓存服务注册表的信息,减少远程调用次数,降低EurekaServer的压力。即使EurekaServer全部宕机,依然可以从缓存中获取信息
- Eureka Server实例身兼三者角色:注册中心、服务提供者、注册中心客户端- 对于所有Provider Instance服务提供者实例而言,Eureka Server的角色是注册中心- 对于Eureka Server集群中其他的Eureka Server注册实例,一个Eureka Server的角色是注册中心客户端- Eureka Server对外提供REST接口的服务,当然也是服务提供者
单机EurekaServer
- 添加依赖
dependencies {compile('org.springframework.cloud:spring-cloud-starter-eureka-server')}jar { manifest { attributes 'Main-Class': 'cn.jannal.EurekaStandaloneApplication' }}
- 配置文件
application.properties``````spring.application.name=eureka-server#服务注册中心的端口server.port=1111#在默认设置下,该服务注册中心也会将自己作为客户端来尝试注册它自己,所以我们需要禁用它的客户端注册行为eureka.client.register-with-eureka=falseeureka.instance.hostname=localhost# 表示是否从EurekServer获取注册信息 ,默认为true。# 因为这是一个单点的EurekaServer,不需要同步其他的EurekaServer节点的数据,故而设为falseeureka.client.fetch-registry=false# 设置与EurekaServer交互的地址,查询服务和注册服务都需要依赖这个地址。多个地址使用逗号分隔# 默认是 http://localhost:8761/eurekaeureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
- 注意事项,单机模式下一般这两个配置成false
eureka.client.register-with-eureka=falseeureka.client.fetch-registry=false
- 创建启动类
cn.jannal.EurekaStandaloneApplication``````/** * @EnableEurekaServer注解启动一个服务注册中心提供给其他应用 * @author jannal */@EnableEurekaServer@SpringBootApplicationpublicclassEurekaStandaloneApplication{publicstaticvoidmain(String[] args){newSpringApplicationBuilder(EurekaStandaloneApplication.class).web(true).run(args);}}
- 启动
1. 项目命令行启动方式gradle :microservice-eureka:bootRun 2. 打包启动方式gradle :microservice-eureka:bootRepackagejava -jar microservice-eureka-0.1-SNAPSHOT.jar
- 访问http://localhost:1111/
集群EurekaServer
- EurekaServer除了单点运行之外,还可以通过运行多个实例,并进行互相注册的方式来实现高可用的部署,所以我们只需要将EurekaServer配置为其他可用的serviceUrl就能实现高可用部署。
- 配置hosts
127.0.0.1 a.register.center127.0.0.1 b.register.center127.0.0.1 c.register.center
- 核心配置详解-
eureka.server.enable-self-preservation
:自我保护模式打开时(缺省为打开),已关停节点是会一直显示在 Eureka 首页的(处于保护状态的Eureka Server,不剔除失效的服务提供者)。关闭自我保护模式后,由于其默认的心跳周期比较长等原因,要过一会儿才会发现已关停节点被自动踢出了-eureka.client.register-with-eureka
:在默认设置下(true),该服务注册中心也会将自己作为客户端来尝试注册它自己,在集群模式下要设为true-eureka.client.fetch-registry
:集群模式下要设置为true-eureka.server.eviction-interval-timer-in-ms
:续期时间(清理无效节点的时间间隔),即扫描失效服务的间隔时间(缺省为60*1000ms),如果Eureka Server处于保护状态,则此配置无效。-eureka.client.service-url.defaultZone
:配置其他可用的serviceUrl - 注意事项
集群模式下,以下两项必须都设置为true,否则会出现服务不可利用eureka.client.register-with-eureka=trueeureka.client.fetch-registry=true
application-register-a.properties``````server.port=1111eureka.client.register-with-eureka=trueeureka.client.fetch-registry=truespring.application.name=eureka-servereureka.instance.hostname=a.register.centereureka.client.service-url.defaultZone=http://b.register.center:1112/eureka/,http://c.register.center:1113/eureka/eureka.server.enable-self-preservation=falseeureka.server.eviction-interval-timer-in-ms=10000# 是通过设置ip让eureka让其他服务注册它。#eureka.instance.preferIpAddress=true
application-register-b.properties``````server.port=1112eureka.client.register-with-eureka=trueeureka.client.fetch-registry=truespring.application.name=eureka-servereureka.instance.hostname=b.register.centereureka.client.service-url.defaultZone=http://a.register.center:1111/eureka/,http://c.register.center:1113/eureka/eureka.server.enable-self-preservation=trueeureka.server.eviction-interval-timer-in-ms=10000# 是通过设置ip让eureka让其他服务注册它。#eureka.instance.preferIpAddress=true
application-register-c.properties``````server.port=1113eureka.client.register-with-eureka=trueeureka.client.fetch-registry=truespring.application.name=eureka-servereureka.instance.hostname=c.register.centereureka.client.service-url.defaultZone=http://a.register.center:1111/eureka/,http://b.register.center:1112/eureka/eureka.server.enable-self-preservation=falseeureka.server.eviction-interval-timer-in-ms=10000# 是通过设置ip让eureka让其他服务注册它。。#eureka.instance.preferIpAddress=true
- 创建启动类
cn.jannal.EurekaClusterApplication``````@EnableEurekaServer@SpringBootApplicationpublicclassEurekaClusterApplication{publicstaticvoidmain(String[] args){newSpringApplicationBuilder(EurekaClusterApplication.class).web(true).run(args);}}
- 项目结构
├── README.MD├── build.gradle└── src ├── main │ ├── java │ │ └── cn.jannal │ │ └── EurekaClusterApplication.java │ └── resources │ ├── application-register-a.properties │ ├── application-register-b.properties │ ├── application-register-c.properties │ └── log4j2.xml └── test ├── java └── resources
- 打包
gradle :microservice-eureka-cluster:bootRepackage
- 启动集群
java -jar microservice-eureka-cluster-0.1-SNAPSHOT.jar --spring.profiles.active=register-ajava -jar microservice-eureka-cluster-0.1-SNAPSHOT.jar --spring.profiles.active=register-bjava -jar microservice-eureka-cluster-0.1-SNAPSHOT.jar --spring.profiles.active=register-c 后台运行nohup java -jar microservice-eureka-cluster-0.1-SNAPSHOT.jar --spring.profiles.active=register-a >/dev/null&
- 访问- http://a.register.center:1111/- http://b.register.center:1112/- http://b.register.center:1113/
- 查看http://a.register.center:1111/能看到registered-replicas中已经有b和c节点。![在这里插入图片描述](https://img-blog.csdnimg.cn/90414f6705db495481925e8477478ec6.png#pic_center)
- 尝试关闭b节点,再次访问http://a.register.center:1111/可以看到,b的节点变为了不可用副本(unavailable-replicas)![在这里插入图片描述](https://img-blog.csdnimg.cn/a52e5130605d4c97af67b75542713f01.png#pic_center)
注册provider
- 添加依赖
dependencies {compile('org.springframework.cloud:spring-cloud-starter-eureka')}
- 启动类
/** * @EnableDiscoveryClient 激活Eureka中的DiscoveryClient实现, * 才能实现Controller中对服务信息的输出。 * @author jannal */@EnableDiscoveryClient@SpringBootApplicationpublicclassComputeServiceProviderApplication{publicstaticvoidmain(String[] args){newSpringApplicationBuilder(ComputeServiceProviderApplication.class).web(true).run(args);}}
- 服务接口
@RestControllerpublicclassComputeController{privatefinalLogger logger =LoggerFactory.getLogger(getClass());@AutowiredprivateDiscoveryClient client;@RequestMapping(value ="/add",method =RequestMethod.GET)publicIntegeradd(@RequestParamInteger a,@RequestParamInteger b){ServiceInstance instance = client.getLocalServiceInstance();Integer r = a + b; logger.info("/add, host:"+ instance.getHost()+", service_id:"+ instance.getServiceId()+", result:"+ r);return r;}}
application.properties
配置#可以指定微服务的名称后续在调用的时候只需要使用该名称就可以进行服务的访问。spring.application.name=compute-serviceserver.port=2222#对应服务注册中心的配置内容,指定服务注册中心的位置。eureka.client.service-url.defaultZone=http://a.register.center:1111/eureka/
- 启动并查看注册中心
- 如果启动后出现警告: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.
- 警告原因分析- 这个是Eureka的自我保护机制。Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,如果出现低于的情况(在单机调试的时候很容易满足,实际在生产环境上通常是由于网络不稳定导致),Eureka Server会将当前的实例注册信息保护起来,同时提示这个警告。- 自我保护机制下server不会删除注册信息,这就有可能导致在调用微服务时,实际上服务并不存在。这种保护状态实际上是考虑了client和server之间的心跳是因为网络问题,而非服务本身问题,不能简单的删除注册信息
- 解决办法- 关闭自我保护模式
eureka.server.enable-self-preservation=true
- 开启自注册,部署多个Server - 如果不想将服务注册到EurekaServer,需要设置
spring.cloud.service-registry.auto-registration.enabled=false或者 @EnableDiscoveryClient(autoRegister=false)
注册Consumer
- 添加依赖
dependencies {compile('org.springframework.cloud:spring-cloud-starter-eureka')}compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
- 启动类
/** *@EnableDiscoveryClient 激活Eureka中的DiscoveryClient实现, * 才能实现Controller中对服务信息的输出。 *@EnableCircuitBreaker注解开启断路器功能 *@SpringCloudApplication整合了以下三个注解 *@SpringBootApplication、@EnableDiscoveryClient、@EnableCircuitBreaker */@SpringCloudApplicationpublicclassConsumerApplication{/** * 创建RestTemplate实例 */@BeanRestTemplaterestTemplate(){returnnewRestTemplate();}publicstaticvoidmain(String[] args){SpringApplication.run(ConsumerApplication.class, args);}}
- consumer调用
@RestControllerpublicclassConsumerController{@AutowiredprivateRestTemplate restTemplate;@RequestMapping(value ="/add", method =RequestMethod.GET)publicStringadd(String a,String b){return restTemplate.getForEntity(String.format("http://localhost:3333/add?a=%s&b=%s", a, b),String.class).getBody();}}
application.properties``````##可以指定微服务的名称后续在调用的时候只需要使用该名称就可以进行服务的访问。spring.application.name=ribbon-consumerserver.port=4444#对应服务注册中心的配置内容,指定服务注册中心的位置。eureka.client.serviceUrl.defaultZone=http://a.register.center:1111/eureka/
- 启动两个provider,启动一个consumer,并查看注册中心,可以看到已经有一个客户端已经注册了
增加权限验证
- 增加权限验证
1. 增加依赖compile('org.springframework.boot:spring-boot-starter-security')2. 增加HTTP Basic 认证配置security.basic.enabled=truesecurity.user.name=jannalsecurity.user.password=jannal
- 注册到需要认证的EurekaServer
eureka.client.serviceUrl.defaultZone=http://jannal:jannal@${eureka.instance.hostname}:${server.port}/eureka/对于更复杂的场景可以创建DiscoveryClient.DiscoveryClientOptionalArgs@BeanpublicDiscoveryClient.DiscoveryClientOptionalArgsdiscoveryClientOptionalArgs(){DiscoveryClient.DiscoveryClientOptionalArgs discoveryClientOptionalArgs =newDiscoveryClient.DiscoveryClientOptionalArgs();List<ClientFilter> additionalFilters =newArrayList<>(); additionalFilters.add(newHTTPBasicAuthFilter("jannal","jannal")); discoveryClientOptionalArgs.setAdditionalFilters(additionalFilters);return discoveryClientOptionalArgs;}
自定义元数据信息
- EurekaServer的元数据信息分为- 标准元数据:主机、IP、端口号、健康信息等- 自定义元数据:用户自定义信息,可以在客户端获取到,不会改变客户端的行为
- 自定义配置信息
# 自定义元数据信息eureka.instance.metadataMap.user=jannaleureka.instance.metadataMap.age=28
- 可以在客户端通过API获取元数据信息
DiscoveryClient.getInstances(serviceld)
多网卡IP选择
- 对于多网卡服务器,比如eth0、eth1、eth2,但是只有eth1可以被其他服务器访问。此时可以指定网卡,避免注册到不能访问的网卡上
- 指定方式
#注册时使用ip而不是主机名eureka.instance.prefer-ip-address=true# 忽略指定名称的网卡 spring.cloud.inetutils.ignored-interfaces[0]=eth0 # 正则表达式spring.cloud.inetutils.preferredNetworks[0]=^192\.168 # 手动指定ip地址 eureka.instance.ip-address=
健康状态
- EurekaServer与EurekaClient之间通过心跳机制来确定状态。如果client与server之间的心跳状态正常,则状态就是UP(只有标记为UP服务会被请求),UP只是表示服务于注册中心的心跳正常,但是并不能保证服务正常
- 默认情况下,Eureka通过客户端心跳包来检测客户端状态,并不是通过spring-boot-actuator模块的/health端点来实现的。默认的心跳实现方式可以有效地检查Eureka客户端进程是否正常运作,但是无法保证客户端应用能够正常提供服务。应用程序将自己的健康信息传递给EurekaServer需要配置
eureka.client.healthchek.enabled =true
- 警告:
eureka.client.healthcheck.enabled=true
只能在application.yml中设置,如果在bootstrap. yml中设置,会导致Eureka注册为UNKNOWN的状态。 - Eureka中的实例一共有如下几种状态:UP、DOWN、STARTING、OUT_OF_SERVICE、UNKNOWN。如果需要更多的健康检查控制,可以实现
com.netflix.appinfo.HealthCheckHandler
接口,根据自己的场景进行操作
EurekaServer事件
- Eureka定义了5个监听事件,分别是:- EurekaServerStartedEvent :Eureka服务端启动事件- EurekaRegistryAvailableEvent: Eureka服务端可用事件- EurekaInstanceRegisteredEvent :Eureka客户端服务注册事件- EurekaInstanceRenewedEvent:Eureka客户端续约事件- EurekaInstanceCanceledEvent :Eureka客户端下线事件
- 可以通过这些事件实现自定义监控报警
@ComponentpublicclassEurekaStateChangeListener{privatestaticLogger logger =LoggerFactory.getLogger(EurekaStateChangeListener.class);/** * Eureka客户端下线事件 */@EventListenerpublicvoidcanceled(EurekaInstanceCanceledEvent event){ logger.info("{},{}下线", event.getServerId(), event.getAppName());}/** * Eureka客户端服务注册事件 */@EventListenerpublicvoidregister(EurekaInstanceRegisteredEvent event){InstanceInfo instanceInfo = event.getInstanceInfo(); logger.info("{}注册", instanceInfo.getAppName());}/** * Eureka客户端续约事件 */@EventListenerpublicvoidrenew(EurekaInstanceRenewedEvent event){ logger.info("{},{}续约", event.getServerId(), event.getAppName());}@EventListenerpublicvoidregisterAvailable(EurekaRegistryAvailableEvent event){ logger.info("Eureka服务端可用事件");}@EventListenerpublicvoidstart(EurekaServerStartedEvent event){ logger.info("EurekaServer启动事件");}}
EurekaServer集群如何同步
- EurekaClient会选择
eureka.client.service-url.defaultZone
配置的第一个EurekaServer,之后如果和这个EurekaServer没有网络问题,就会一直用这个。 - 在EurekaClient向第一个EurekaServer发送注册,下线,心跳,状态改变等一切事件时,这些会在第一个EurekaServer上面同步到集群的所有Server上(这个同步是异步的)
- 集群异步同步失败补偿策略:服务实例A注册到EurekaServerA,但是同步到EurekaServerB失败。这时EurekaServerB就没有这个实例,在下次A心跳时,EurekaServerA同步心跳请求到EurekaServerB时,会返回404,触发重新注册
- 为了减少和均匀EurekaServer压力,我们对于每个微服务的不同实例,配置Eureka集群都要写的顺序不一样,和自己网段一样的写的最前面
配置详解
作为服务注册中心角色的配置
eureka.server.enable-self-preservation
:。enable-self-preservation的默认值为true,表示开启自我保护机制。为了使得失效Provider的能快速剔除,可以停用Eureka Server的保护模式,然后启用客户端的健康状态检查。- 自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。
eureka.server.eviction-interval-timer-in-ms
:配置Eureka Server清理无效节点的时间间隔,默认为60000毫秒(即60秒)。但是,如果Eureka Server处于保护状态,则此配置无效。
作为Provider提供者实例角色的配置
eureka.instance.hostname
:设置当前实例的主机名称- 两个相同的服务(端口不同),如果注册时设置的都是
${spring.application.name}
,那么Eureka首页只会看到一个服务名字,而无法区分有几个实例注册上来了。于是,可以自定义生成InstanceId的规则。Eureka默认服务名${spring.cloud.client.hostname}:${spring.application.name}:${spring.application. instance_id:${server.port}}可以在配置文件中通过eureka.intance.intance_id来自定义:eureka: instance: #修改显示的微服务名为:IP:端口 instance-id: ${spring.cloud.client.ipAddress}:${server.port}
eureka.instance.appname
:该选项设置当前实例的服务名称。默认值取自spring.application.name配置项的值,如果该选项没有值,则eureka.instance.appname值为unknown。在Eureka服务器上,Provider服务提供者的名称不区分大小写。eureka.instance.ip-address
:设置当前实例的IP地址。eureka.instance.prefer-ip-address
:如果配置为true,将使用IP地址的形式来定义Provider实例的访问地址,而不是使用主机名来定义Provider实例的地址。如果同时设置了eureka.instance.ip-address选项,则使用该选项所配置的IP,否则自动获取除网卡的IP地址。默认情况下,此配置项的值为false,使用主机名来定义Provider实例的访问地址。eureka.instance.lease-renewal-interval-in-seconds
:定义Provider实例到注册中心续约(心跳)的时间间隔,单位为秒,默认值为30秒,即每隔30s发送一次心跳。eureka.instance.lease-expiration-duration-in-seconds
:定义Provider实例失效的时间,单位为秒,默认值为90秒。在租约时间内,如果Eureka Client未续约(心跳),Eureka Server将剔除该服务。即在90s内每隔30s发送一次心跳,则最多3次心跳重试机会eureka.instance.status-page-url-path
:定义Provider实例状态页面的URL,此选项所配置的是相对路径,默认使用HTTP访问,如果需要使用HTTPS则使用绝对路径配置。默认的相对路径为/info。eureka.instance.status-page-url
:定义Provider实例状态页面的URL,此选项所配置的是绝对路径eureka.instance.health-check-url-path
:定义Provider实例健康检查页面的URL,此选项所配置的是相对路径,默认使用HTTP访问,如果需要使用HTTPS则使用绝对路径配置。默认的相对路径为/health。eureka.instance.health-check-url
:定义Provider实例健康检查页面的URL,此选项所配置的是绝对路径。
作为Eureka Client注册中心客户端角色的配置
- 如果集群中配置了多个Eureka Server,节点和节点之间是对等的,一个Eureka Server在角色上还是其他Eureka Server实例的客户端
eureka.client.register-with-eureka
:是否将自己注册到其他的Eureka Server,默认为true。如果当前集群只有一个Eureka Server,需要设置成falseeureka.client.fetch-registry
:是否从Eureka Server获取注册信息,默认为true。如果当前集群只有一个Eureka Server,需要设置成falseeureka.client.registery-fetch-interval-seconds
:从Eureka服务器端获取注册信息的间隔时间,单位为秒,默认为30秒。eureka.client.eureka-server-connect-timeout-seconds
:连接Eureka Server的超时时间,单位为秒,默认值为5eureka.client.eureka-server-read-timeout-seconds
:读取Eureka Server信息的超时时间,单位为秒,默认值为8eureka.client.eureka-connection-idle-timeout-seconds
:客户端组件到Eureka Server连接空闲关闭的时间,单位为秒,默认值为30eureka.client.healthcheck.enabled=true
:开启客户端健康检查。此配置项应该放在application.yml
文件中,而不应该放在bootstrap.yml
文件。如果该选项配置在bootstrap.yml文件中,可能导致Provider 实例在Eureka上的状态为UNKNOWN状态eureka.client.filter-only-up-instances
:从Eureka Server获取Provider实例清单时是否进行过滤,只保留UP状态的实例,默认值为trueeureka.client.service-url.defaultZone
:客户端,需要向远程的Eureka Server自我注册、发现其他的Provider实例。此配置项用于设置的Eureka Server服务端的交互地址,在注册中心集群的情况下,多个Eureka Server 之间可以使用英文逗号分隔。Region(地域)与Zone(可用区)两个概念,两者都是借鉴AWS (Amazon云)的概念。在非AWS环境下,Region和Zone(Availability Zone)可以理解为服务器的位置,Region可以理解为服务器所在的地域,Zone可以理解成服务器所处的机房。一个Region地域可以包含多个Zone机房。不同的Region地域的距离很远,一个Region地域的不同Zone机房间距离往往较近,也可能在同一个物理机房内。在网络环境跨地域、跨机房的情况下,Region与Zone都可以在配置文件- 配置Region 与Zone的主要目的是,在网络环境复杂的情况下能帮助客户端能够就近访问需要的Provider服务实例。负载均衡组件Spring Cloud Ribbon的默认策略,是优先访问同客户端处于同一个Zone中的服务端实例,只有当同一个Zone中没有可用服务端实例的时候,才会访问其他Zone中的实例。Region和Zone可以对应于现实中的大区和机房,如在华北地区有10个机房,在华南地区有20个机房,那么分别为Eureka指定合理的Region和Zone能有效避免跨机房调用,而一个地区的Eureka坏掉也不会导致整个该地区的服务都不可用。
- 如果网络环境不复杂,比如所有服务器都在于同一个地域同一个机房,则不需要配置Region与 Zone。如果不配置Region地域选项值,其默认值为
us-east-1
;如果不配置Zone可用区Key,其默认的Key为defaultZone
。可以通过eureka.client.serviceUrl.defaultZone选项设置默认Zone的注册中心Eureka Server的访问地址。 - Spring Cloud的注册中心地址的配置,是以Zone为单位进行配置的,一个Zone如果有多个注册中心,则使用逗号隔开。如果有多个机房,则配置多个eureka.client.serviceUrl.ZoneName配置项。举个例子,假设在北京区域有两个机房,每一个机房有一个Eureka Server注册中心,则Eureka Server 配置文件中有关Zone和注册中心的配置大致如下:
eureka:client:region:'Beijing #指定Region区域为北京'availabilityZones:Beijing: 'zone-2,zone-1#指定北京的机房为zone-2,zone-1 serviceUrl:zone-1: http://localhost:7777/eureka/ # zone-1机房的Eureka Serverzone-2: http://localhost:7778/eureka/ # zone-2 机房的Eureka Server 在配置服务注册中心地址时,如果Eureka Server加入了安全验证,则注册中心的URL格式为 http://<username>:<password>@localhost:8761/eureka
标签:
eureka
spring cloud
本文转载自: https://blog.csdn.net/usagoole/article/details/126201025
版权归原作者 jannals 所有, 如有侵权,请联系我们删除。
版权归原作者 jannals 所有, 如有侵权,请联系我们删除。