spring cloud深入学习(二)-----服务注册中心spring cloud eureka

服务治理

主要用来实现各个微服务实例的自动化注册与发现,为啥需要这玩意呢?在一开始比如A系统调用B服务,可能通过手工维护B服务的实例,并且还得采用负载均衡等方式,这些全部都得需要手工维护,等后面系统越来越多,这维护起来可就费了劲了。那么服务治理就是帮你管理这些服务,这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。

由于Spring Cloud为服务治理做了一层抽象接口,所以在Spring Cloud应用中可以支持多种不同的服务治理框架,比如:Netflix Eureka、Consul、Zookeeper。在Spring Cloud服务治理抽象层的作用下,我们可以无缝地切换服务治理实现,并且不影响任何其他的服务注册、服务发现、服务调用等逻辑。

服务中心又称注册中心,管理各种服务功能包括服务的注册、发现、熔断、负载、降级等,比如dubbo admin后台的各种功能。

有了服务中心调用关系会有什么变化,画几个简图来帮忙理解

项目A调用项目B

正常调用项目A请求项目B

 

有了服务中心之后,任何一个服务都不能直接去掉用,都需要通过服务中心来调用

项目A调用项目B,项目B在调用项目C

这时候调用的步骤就会为两步:第一步,项目A首先从服务中心请求项目B服务器,然后项目B在从服务中心请求项目C服务。

上面的项目只是两三个相互之间的简单调用,但是如果项目超过20个30个呢,在15年底的时候我司分布式的项目就达到了二十几个,画一张图来描述几十个项目之间的相互调用关系全是线条,任何其中的一个项目改动,就会牵连好几个项目跟着重启,巨麻烦而且容易出错。通过服务中心来获取服务你不需要关注你调用的项目IP地址,由几台服务器组成,每次直接去服务中心获取可以使用的服务去调用既可。

由于各种服务都注册到了服务中心,就有了去做很多高级功能条件。比如几台服务提供相同服务来做均衡负载;监控服务器调用成功率来做熔断,移除服务列表中的故障点;监控服务调用时间来对不同的服务器设置不同的权重等等。

spring cloud版本

spring cloud目前属于快速更新状态,因此版本也是非常多,cloud与boot版本对应如下:

目前可以简单的理解为A-F版本,F版本只支持springboot 2.x版本,不支持1.5版本。不过目前貌似G版本都出来了。。。

CAP

CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可兼得,Netflix在设计Eureka时遵守的就是AP原则。

Spring Cloud Eureka

Eureka是Netflix开源的一款提供服务注册和发现的产品,它提供了完整的Service Registry和Service Discovery实现。也是springcloud体系中最重要最核心的组件之一。

Netflix

以下介绍来自于百度百科:

Netflix是一家美国公司,在美国、加拿大提供互联网随选流媒体播放,定制DVD、蓝光光碟在线出租业务。该公司成立于1997年,总部位于加利福尼亚州洛斯盖图,1999年开始订阅服务。2009年,该公司可提供多达10万部DVD电影,并有1千万的订户。2007年2月25日,Netflix宣布已经售出第10亿份DVD。HIS一份报告中表示,2011年Netflix网络电影销量占据美国用户在线电影总销量的45%。

我第一次看到这个单词的时候,是在各种美剧或者电影的开头,Netflix拍摄的代表性的美剧有《纸牌屋》、《毒枭》、《怪奇物语》。后来研究springcloud的时候发现了Netflix公司,就在想它们是不是同一家公司,经过核对github上面邮件后缀判定确实是同一家公司,其实springcloud的微服务就基于Netflix公司的开源产品来做的。

Netflix的开源框架组件已经在Netflix的大规模分布式微服务环境中经过多年的生产实战验证,正逐步被社区接受为构造微服务框架的标准组件。Spring Cloud开源产品,主要是基于对Netflix开源组件的进一步封装,方便Spring开发人员构建微服务基础框架。对于一些打算构建微服务框架体系的公司来说,充分利用或参考借鉴Netflix的开源微服务组件(或Spring Cloud),在此基础上进行必要的企业定制,无疑是通向微服务架构的捷径。

Eureka

Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册和发现(请对比Zookeeper)。 Eureka 采用了 C-S 的设计架构。Eureka Server 作为服务注册功能的服务器,它是服务注册中心。 而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。SpringCloud 的一些其他模块(比如Zuul)就可以通过 Eureka Server 来发现系统中的其他微服务,并执行相关的逻辑。请注意和Dubbo的架构对比         Eureka包含两个组件:Eureka Server和Eureka ClientEureka Server提供服务注册服务各个节点启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到  EurekaClient是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒)。                

按照官方介绍:

Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers.

Eureka 是一个基于 REST 的服务,主要在 AWS 云中使用, 定位服务来进行中间层服务器的负载均衡和故障转移。

Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务注册和发现。Eureka 采用了 C-S 的设计架构。Eureka Server 作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用 Eureka 的客户端连接到 Eureka Server,并维持心跳连接。这样系统的维护人员就可以通过 Eureka Server 来监控系统中各个微服务是否正常运行。Spring Cloud 的一些其他模块(比如Zuul)就可以通过 Eureka Server 来发现系统中的其他微服务,并执行相关的逻辑。

Eureka由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡。

用一张图来认识以下:

上图简要描述了Eureka的基本架构,由3个角色组成:

1、Eureka Server

  • 提供服务注册和发现

2、Service Provider

  • 服务提供方
  • 将自身服务注册到Eureka,从而使服务消费方能够找到

3、Service Consumer

  • 服务消费方
  • 从Eureka获取注册服务列表,从而能够消费服务

Eureka-----Hello World

1、创建服务注册中心

pom.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ty</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-server</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、主类中添加注解

package com.ty.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

//通过此注解启动一个服务注册中心提供给其他应用进行对话
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

}

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

# 服务注册中心应用名
spring.application.name=eureka-server
# 服务注册中心端口
server.port=1001
# 服务注册中心主机名
eureka.instance.hostname=localhost
# 禁止服务注册中心注册自己
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
  • eureka.client.register-with-eureka :表示是否将自己注册到Eureka Server,默认为true。
  • eureka.client.fetch-registry :表示是否从Eureka Server获取注册信息,默认为true。false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务。
  • eureka.client.serviceUrl.defaultZone :设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka ;多个地址可使用 , 分隔。

然后运行http://localhost:1001/,效果如下:

说明当前没有服务。

服务提供者

服务注册中心已经创建完毕,现在再来创建一个服务提供者,eureka-provider.

首先创建一个springboot项目,项目名就叫eureka-provider。

1、pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ty</groupId>
    <artifactId>eureka-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>eureka-provider</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

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

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

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>

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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
            </dependency>

        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

2、运行主类

package com.ty.eurekaprovider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

//通过注解@EnableEurekaClient 表明自己是一个eurekaclient
@EnableEurekaClient
@SpringBootApplication
public class EurekaProviderApplication { public static void main(String[] args) { SpringApplication.run(EurekaProviderApplication.class, args); } }

3、配置文件

# 服务名称
spring.application.name=eureka-client
# 端口号
server.port=2001
# 服务注册中心地址
eureka.client.serviceUrl.defaultZone=http://localhost:1001/eureka/

4、controller

package com.ty.eurekaprovider.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProviderController {

    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/firstCall")
    public String firstCall() {
        String services = "Services: " + discoveryClient.getServices();
        System.out.println(services);
        return services;
    }

}

当启动服务提供者之后,在eureka-server控制台可以看到这样的信息:

Registered instance EUREKA-CLIENT/P1XO0D5HS8H0XRH:eureka-client:2001 with status UP (replication=false)

并且打开http://localhost:1001/,如下:

已经可以看到服务提供者了,注册成功。

当前问题

Eureka注册中心中服务含有主机名称,并且点击之后没有相关的项目信息,不够友好,可以加上如下配置:

1、pom.xml-----加上actuator坐标 add 插件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<build>
    <finalName>microservicecloud</finalName>
    <resources>
        <resource>
<!--意思是允许访问resources路径下的文件,注意filtering设置为true--> <directory>src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <delimiters>
<!--将info中配置的$project.artifactId$转换成maven配置文件中的值--> <delimit>$</delimit> </delimiters> </configuration> </plugin> </plugins> </build>

2、application.yml(application.properties)两种写法无所谓

eureka:
client: #客户端注册进eureka服务列表内
service-url:
#defaultZone: http://localhost:7001/eureka
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
instance:
instance-id: microservicecloud-dept8001
prefer-ip-address: true #访问路径可以显示IP地址
#info中就是点击eureka中项目信息后,展示的内容
info:
app.name: atguigu-microservicecloud
company.name: www.atguigu.com
build.artifactId: $project.artifactId$
build.version: $project.version$

Eureka自我保护

在使用Eureka的过程中,经常会出现如下的情况:

什么是自我保护模式? 

默认情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,EurekaServer将会注销该实例(默认90秒)。但是当网络分区故障发生时,微服务与EurekaServer之间无法正常通信,以上行为可能变得非常危险了——因为微服务本身其实是健康的,此时本不应该注销这个微服务。Eureka通过“自我保护模式”来解决这个问题——当EurekaServer节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,EurekaServer就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该Eureka Server节点会自动退出自我保护模式。 在自我保护模式中,Eureka Server会保护服务注册表中的信息,不再注销任何服务实例。当它收到的心跳数重新恢复到阈值以上时,该Eureka Server节点就会自动退出自我保护模式。它的设计哲学就是宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例。

一句话讲解:好死不如赖活着。某时刻某一个微服务不可用了,eureka不会立刻清理,依旧会对该微服务的信息进行保存

综上,自我保护模式是一种应对网络异常的安全保护措施。它的架构哲学是宁可同时保留所有微服务(健康的微服务和不健康的微服务都会保留),也不盲目注销任何健康的微服务。使用自我保护模式,可以让Eureka集群更加的健壮、稳定。 在Spring Cloud中,可以使用eureka.server.enable-self-preservation = false 禁用自我保护模式(不推荐)。

如何删除注册在注册中心上的服务?

有的时候本地不小心把服务注册到联调环境的注册中心上,这时候两个团队在进行联调就会出问题(由于负载的策略),可以强制删除注册在注册中心上的服务,方法如下:

 例如要干掉ZKR-PRODUCT服务的172.21.0.10:zkr-product:6868实例:

http://140.143.67.146:8000/eureka/eureka/apps/ZKR-PRODUCT/172.21.0.10:zkr-product:6868

Request Method = DELETE

1、http://140.143.67.146:8000/eureka是请求Eureka管理界面的url

2、eureka/apps是Eureka api 的固定url

3、ZKR-PRODUCT是服务注册的服务名一般指application:name,可公用因分布式

4、172.21.0.10:zkr-product:6868服务的唯一标识,具体体现在哪个服务,可为分布式中的一个。

高可用服务注册中心

基本原理上图是来自eureka的官方架构图,这是基于集群配置的eureka; 

- 处于不同节点的eureka通过Replicate进行数据同步 

- Application Service为服务提供者 

- Application Client为服务消费者 

- Make Remote Call完成一次服务调用服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用。当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。

从上面代码来看,服务注册中心在spring cloud整个架构中是非常重要的,因为所有的服务包括消费都需要注册到他这里,他就相当于老大,分配各种服务。由于我是在本地测试,因此目前只是单点的状态,实际情况肯定不是这样,那么万一某一个服务注册中心发生故障应该怎么预防呢?

eureka是这么解决的,其实整个架构中,服务或者说是节点都充当着两种角色,一种是服务方,一种则是消费方。因为一个服务会被其他服务调用,其他服务也会调用这个服务。同样服务注册中心也不例外,将自己作为服务向其他服务注册中心注册自己,这样在一组注册中心中,互相注册,当一个挂了,其他的补上。举个例子说明:

在咱们上面实现的eureka-server新增两个配置文件:

1、application-node1.properties

# 服务注册中心应用名
spring.application.name=eureka-server
# 服务注册中心端口
server.port=1002
# 服务注册中心主机名
eureka.instance.hostname=node1
eureka.client.serviceUrl.defaultZone=http://node2:1003/eureka/

2、application-node2.properties

# 服务注册中心应用名
spring.application.name=eureka-server
# 服务注册中心端口
server.port=1003
# 服务注册中心主机名
eureka.instance.hostname=node2
eureka.client.serviceUrl.defaultZone=http://node1:1002/eureka/

3、更改本机hosts配置

路径:C:WindowsSystem32driversetc

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host

# localhost name resolution is handled within DNS itself.
#    127.0.0.1       localhost
#    ::1             localhost

# Added by Docker for Windows
192.168.0.103 host.docker.internal
192.168.0.103 gateway.docker.internal
# End of section
127.0.0.1 node1
127.0.0.1 node2

新加两个host,其实对应还是本机,只是为了演示。

4、打包

5、执行

cd到jar包对应的路径,执行命令如下:

java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=node1
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=node2

第一条命令就是启application-node1.properties中的配置,第二条则是启application-node2.properties中的配置。node1注册到node2中,node2注册到node1中。

6、运行结果

表示node1注册到node2中了。

表示node2注册到node1中。

既然服务注册中心已经实现了相互注册,服务提供者也得做相应的改变:

只需要更改application.properties即可,

# 服务名称
spring.application.name=eureka-client
# 端口号
server.port=2001
# 服务注册中心地址
eureka.client.serviceUrl.defaultZone=http://node1:1002/eureka/,http://node2:1003/eureka/

作为注册中心,Eureka比Zookeeper好在哪? 

作为服务注册中心,Eureka比Zookeeper好在哪里著名的CAP理论指出,一个分布式系统不可能同时满足C(一致性)、A(可用性)和P(分区容错性)。由于分区容错性P在是分布式系统中必须要保证的,因此我们只能在A和C之间进行权衡。因此Zookeeper保证的是CP,Eureka则是AP。 

1、Zookeeper保证CP当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。

2、Eureka保证AP。Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况: 

  • 1. Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务 
  • 2. Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用) 
  • 3. 当网络稳定时,当前实例新的注册信息会被同步到其它节点中 因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。       
原文地址:https://www.cnblogs.com/alimayun/p/10822874.html