微服务Spring Cloud 简介&实现

  • Post author:
  • Post category:其他


我的个人博客:https://www.blog.kamisamak.com/

https://www.blog.kamisamak.com/index.php/2020/08/16/spring-cloud-入门/

[infobox title=“微服务简介”]

微服务架构是一种架构模式或者说是一种架构风格。将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。

[collapse title=“特点”]

单一职责:微服务中每一个服务都对应唯一的业务能力,做到单一职责

微服务的服务拆分粒度很小,例如一个用户管理就可以作为一个服务。

服务间互相独立,互不干扰

团队独立:每个服务都是一个独立的开发团队,人数不能过多。

技术独立:因为是面向服务,提供Rest接口,使用什么技术没有别人干涉

前后端分离:采用前后端分离开发,提供统一Rest接口,后端不用再为PC、移动端开发不同接口

数据库分离:每个服务都使用自己的数据源

部署独立,服务间虽然有调用,但能做到服务重启不影响其它服务。有利于持续集成和持续交付。每个服务都是独立的组件,可复用,可替换,降低耦合,易维护,易集群 Docker部署服务

[/collapse]

[/infobox]

[infobox title=“SpringCloud(Netflix)”]

Spring Cloud是实现微服务的一种技术架构


http://projects.spring.io/spring-cloud/


实现了如配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等功能。


涉及的部分组件包括:


Eureka:注册中心

Zuul:服务网关

Ribbon:负载均衡

Feign:服务调用

Hystrix:熔断器等

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rO1C18Mk-1600323728001)(屏幕截图-2020-09-16-160206.png)]

[/infobox]

[successbox title=“Eureka”]

[collapse title=“介绍”]

引用:https://blog.csdn.net/qwe86314/article/details/94552801

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H7s1Mnc0-1600323728007)(屏幕截图-2020-09-16-165028.png)]

[dangerbox title=“Eureka Server:注册中心服务端”]

服务注册

服务提供者启动时,会通过 Eureka Client 向 Eureka Server 注册信息,Eureka Server 会存储该服务的信息,Eureka Server 内部有二层缓存机制来维护整个注册表

提供注册表

服务消费者在调用服务时,如果 Eureka Client 没有缓存注册表的话,会从 Eureka Server 获取最新的注册表

同步状态

Eureka Client 通过注册、心跳机制和 Eureka Server 同步当前客户端的状态。

[/dangerbox]

[dangerbox title=“Eureka Client:注册中心客户端”]

Eureka Client 是一个 Java 客户端,用于简化与 Eureka Server 的交互。Eureka Client 会拉取、更新和缓存 Eureka Server 中的信息。因此当所有的 Eureka Server 节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者,但是当服务有更改的时候会出现信息不一致。

Register: 服务注册

服务的提供者,将自身注册到注册中心,服务提供者也是一个 Eureka Client。当 Eureka Client 向 Eureka Server 注册时,它提供自身的元数据,比如 IP 地址、端口,运行状况指示符 URL,主页等。

Renew: 服务续约

Eureka Client 会每隔 30 秒发送一次心跳来续约。 通过续约来告知 Eureka Server 该 Eureka Client 运行正常,没有出现问题。 默认情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的续约,Server 端会将实例从其注册表中删除,此时间可配置,一般情况不建议更改。

服务续约的两个重要属性

服务续约任务的调用间隔时间,默认为30秒
eureka.instance.lease-renewal-interval-in-seconds=30

服务失效的时间,默认为90秒。
eureka.instance.lease-expiration-duration-in-seconds=90

[/dangerbox]


Eviction 服务剔除


当 Eureka Client 和 Eureka Server 不再有心跳时,Eureka Server 会将该服务实例从服务注册列表中删除,即服务剔除。


Cancel: 服务下线


Eureka Client 在程序关闭时向 Eureka Server 发送取消请求。 发送请求后,该客户端实例信息将从 Eureka Server 的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:

DiscoveryManager.getInstance().shutdownComponent();


GetRegisty: 获取注册列表信息


Eureka Client 从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka Client 的缓存信息不同,Eureka Client 自动处理。

如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 则会重新获取整个注册表信息。 Eureka Server 缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka Client 和 Eureka Server 可以使用 JSON/XML 格式进行通讯。在默认情况下 Eureka Client 使用压缩 JSON 格式来获取注册列表的信息。

获取服务是服务消费者的基础,所以必有两个重要参数需要注意:

# 启用服务消费者从注册中心拉取服务列表的功能
eureka.client.fetch-registry=true

# 设置服务消费者从注册中心拉取服务列表的间隔
eureka.client.registry-fetch-interval-seconds=30


Remote Call: 远程调用


当 Eureka Client 从注册中心获取到服务提供者信息后,就可以通过 Http 请求调用对应的服务;服务提供者有多个时,Eureka Client 客户端会通过 Ribbon 自动进行负载均衡。





自我保护机制


默认情况下,如果 Eureka Server 在一定的 90s 内没有接收到某个微服务实例的心跳,会注销该实例。但是在微服务架构下服务之间通常都是跨进程调用,网络通信往往会面临着各种问题,比如微服务状态正常,网络分区故障,导致此实例被注销。

固定时间内大量实例被注销,可能会严重威胁整个微服务架构的可用性。为了解决这个问题,Eureka 开发了自我保护机制,那么什么是自我保护机制呢?

Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 即会进入自我保护机制。

Eureka Server 触发自我保护机制后,页面会出现提示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nGqTXDQ6-1600323728010)(20190703103514416.png)]


Eureka Server 进入自我保护机制,会出现以下几种情况:


(1 Eureka 不再从注册列表中移除因为长时间没收到心跳而应该过期的服务

(2 Eureka 仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用)

(3 当网络稳定时,当前实例新的注册信息会被同步到其它节点中

Eureka 自我保护机制是为了防止误杀服务而提供的一个机制。当个别客户端出现心跳失联时,则认为是客户端的问题,剔除掉客户端;当 Eureka 捕获到大量的心跳失败时,则认为可能是网络问题,进入自我保护机制;当客户端心跳恢复时,Eureka 会自动退出自我保护机制。

如果在保护期内刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,即会调用失败。对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。


通过在 Eureka Server 配置如下参数,开启或者关闭保护机制,生产环境建议打开:

eureka.server.enable-self-preservation=true




Eureka 集群原理

再来看看 Eureka 集群的工作原理。我们假设有三台 Eureka Server 组成的集群,第一台 Eureka Server 在北京机房,另外两台 Eureka Server 在深圳和西安机房。这样三台 Eureka Server 就组建成了一个跨区域的高可用集群,只要三个地方的任意一个机房不出现问题,都不会影响整个架构的稳定性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pcPdOS9J-1600323728014)(20190703103823398.png)]

从图中可以看出 Eureka Server 集群相互之间通过 Replicate 来同步数据,相互之间不区分主节点和从节点,所有的节点都是平等的。在这种架构中,节点通过彼此互相注册来提高可用性,每个节点需要添加一个或多个有效的 serviceUrl 指向其他节点。

如果某台 Eureka Server 宕机,Eureka Client 的请求会自动切换到新的 Eureka Server 节点。当宕机的服务器重新恢复后,Eureka 会再次将其纳入到服务器集群管理之中。当节点开始接受客户端请求时,所有的操作都会进行节点间复制,将请求复制到其它 Eureka Server 当前所知的所有节点中。

另外 Eureka Server 的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。所以,如果存在多个节点,只需要将节点之间两两连接起来形成通路,那么其它注册中心都可以共享信息。每个 Eureka Server 同时也是 Eureka Client,多个 Eureka Server 之间通过 P2P 的方式完成服务注册表的同步。

Eureka Server 集群之间的状态是采用异步方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。


Eureka 分区


Eureka提供了 Region 和 Zone 两个概念来进行分区,这两个概念均来自于亚马逊的 AWS:


region

可以理解为地理上的不同区域,比如亚洲地区,中国区或者深圳等等。没有具体大小的限制。根据项目具体的情况,可以自行合理划分 region。


zone

可以简单理解为 region 内的具体机房,比如说 region 划分为深圳,然后深圳有两个机房,就可以在此 region 之下划分出 zone1、zone2 两个 zone。

上图中的 us-east-1c、us-east-1d、us-east-1e 就代表了不同的 Zone。Zone 内的 Eureka Client 优先和 Zone 内的 Eureka Server 进行心跳同步,同样调用端优先在 Zone 内的 Eureka Server 获取服务列表,当 Zone 内的 Eureka Server 挂掉之后,才会从别的 Zone 中获取信息。


Eurka 保证 AP


Eureka Server 各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而 Eureka Client 在向某个 Eureka 注册时,如果发现连接失败,则会自动切换至其它节点。只要有一台 Eureka Server 还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。





Eurka 工作流程


1、Eureka Server 启动成功,等待服务端注册。在启动过程中如果配置了集群,集群之间定时通过 Replicate 同步注册表,每个 Eureka Server 都存在独立完整的服务注册表信息

2、Eureka Client 启动时根据配置的 Eureka Server 地址去注册中心注册服务

3、Eureka Client 会每 30s 向 Eureka Server 发送一次心跳请求,证明客户端服务正常

4、当 Eureka Server 90s 内没有收到 Eureka Client 的心跳,注册中心则认为该节点失效,会注销该实例

5、单位时间内 Eureka Server 统计到有大量的 Eureka Client 没有上送心跳,则认为可能为网络异常,进入自我保护机制,不再剔除没有上送心跳的客户端

6、当 Eureka Client 心跳请求恢复正常之后,Eureka Server 自动退出自我保护模式

7、Eureka Client 定时全量或者增量从注册中心获取服务注册表,并且将获取到的信息缓存到本地

8、服务调用时,Eureka Client 会先从本地缓存找寻调取的服务。如果获取不到,先从注册中心刷新注册表,再同步到本地缓存

9、Eureka Client 获取到目标服务器信息,发起服务调用

10、Eureka Client 程序关闭时向 Eureka Server 发送取消请求,Eureka Server 将实例从注册表中删除





总结


Eureka 为了保障注册中心的高可用性,容忍了数据的非强一致性,服务节点间的数据可能不一致, Client-Server 间的数据可能不一致。比较适合跨越多机房、对注册中心服务可用性要求较高的使用场景。

[/collapse]

[infobox title=“具体实现”]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N3HSI8oS-1600323728017)(屏幕截图-2020-09-16-162227.png)]

[collapse title=“Maven配置”]

<?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 https://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.3.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF- 8</project.reporting.outputEncoding>
<!-- <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>-->
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- spring cloud 默认配置启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
<!-- spring cloud Eureka Server 启动器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.2.4.RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

[/collapse]

[warningbox title=“eureka-server01”]

[collapse title=“Application”]

package com.example.demo01;

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

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableEurekaServer
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

[/collapse]

[collapse title=“properties”]

# 设置spring应用命名,可以自定义,非必要
spring.application.name=eureka-server01
# 设置Eureka Server WEB控制台端口,自定义
server.port=9871
#是否将自己注册到Eureka-Server中,默认的为true
#eureka.client.registerWithEureka=false
#是否从Eureka-Server中获取服务注册信息,默认为true
#eureka.client.fetchRegistry=false
#EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的 地址。
eureka.client.service-url.defaultZone: http://127.0.0.1:9872/eureka
# 扫描失效服务的间隔时间
eureka.server.eviction-interval-timer-in-ms=10000
# 关闭自我保护模式(缺省为打开)
eureka.server.enable-self-preservation: false
#logging.level.ROOT=DEBUG

[/collapse]

[/warningbox]

[warningbox title=“eureka-server02”]

[collapse title=“Application”]

package com.example.demo01;

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

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableEurekaServer
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

[/collapse]

[collapse title=“properties”]

# 设置spring应用命名,可以自定义,非必要
spring.application.name=eureka-server02
# 设置Eureka Server WEB控制台端口,自定义
server.port=9872
#是否将自己注册到Eureka-Server中,默认的为true
#eureka.client.registerWithEureka=false
#是否从Eureka-Server中获取服务注册信息,默认为true
#eureka.client.fetchRegistry=false
#EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的 地址。
#eureka.client.service-url.defaultZone: http://127.0.0.1:${server.port}/eureka
eureka.client.service-url.defaultZone: http://127.0.0.1:9871/eureka
# 扫描失效服务的间隔时间
eureka.server.eviction-interval-timer-in-ms=10000
# 关闭自我保护模式(缺省为打开)
eureka.server.enable-self-preservation: false
#logging.level.ROOT=DEBUG

[/collapse]

[/warningbox]

[warningbox title=“user-service”]

[collapse title=“properties①”]

# 设置spring应用命名,可以自定义,非必要
spring.application.name=user-service
server.port=9873
server.tomcat.uri-encoding=utf-8
#数据库相关配置
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#spring.datasource.url=jdbc:mysql://localhost:3306/sql_test
#spring.datasource.username=root
#spring.datasource.password=123456
#spring.datasource.max-idle=10
#spring.datasource.max-wait=10000
#spring.datasource.min-idle=5
#spring.datasource.initial-size=5
#session生命周期
server.servlet.session.timeout=30m
# EurekaServer地址
eureka.client.service-url.defaultZone:http://127.0.0.1:9871/eureka,http://127.0.0.1:9872/eureka
#当遇到同样名字的时候,是否允许覆盖注册
eureka.client.allow-redirects=true
# 当调用getHostname获取实例的hostname时,返回ip而不是host 名称
eureka.instance.prefer-ip-address=true
# 指定自己的ip信息,不指定的话会自己寻找
eureka.instance.ip-address=127.0.0.1

[/collapse]

[collapse title=“properties①”]

# 设置spring应用命名,可以自定义,非必要
spring.application.name=user-service
server.port=9874
server.tomcat.uri-encoding=utf-8
#数据库相关配置
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#spring.datasource.url=jdbc:mysql://localhost:3306/sql_test
#spring.datasource.username=root
#spring.datasource.password=123456
#spring.datasource.max-idle=10
#spring.datasource.max-wait=10000
#spring.datasource.min-idle=5
#spring.datasource.initial-size=5
#session生命周期
server.servlet.session.timeout=30m
# EurekaServer地址
eureka.client.service-url.defaultZone:http://127.0.0.1:9871/eureka,http://127.0.0.1:9872/eureka
#当遇到同样名字的时候,是否允许覆盖注册
eureka.client.allow-redirects=true
# 当调用getHostname获取实例的hostname时,返回ip而不是host 名称
eureka.instance.prefer-ip-address=true
# 指定自己的ip信息,不指定的话会自己寻找
eureka.instance.ip-address=127.0.0.1

[/collapse]

[collapse title=“SpringApplication实例”]

SpringApplication实例添加EnableDiscoveryClient或@EnableEurekaClient注解,都可以让该服务注册到注册中心。

但@EnableEurekaClient仅支持Eureka注册中心,@EnableDiscoveryClient支持Eureka、Zookeeper、Consul

[/collapse]

[/warningbox]

[/infobox][/successbox]

[successbox title=“负载均衡Ribbon”]

[collapse title=“简介”]

Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP的客户端的行为。为Ribbon配置服务提供者地址后,Ribbon就可基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。

在Spring Cloud中,当Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。展示了Ribbon与Eureka配合使用时的架构。

[/collapse]

[collapse title=“示例”]

在RestTemplate的配置方法上添加 @LoadBalanced 注解:

@Configuration
public class RestTemplateConfig {
    @Bean
    //Ribbon负载均衡
    @LoadBalanced
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getMessageConverters().add(new StringHttpMessageConverter(Charset.forName("UTF-8")));
        return restTemplate;
    }
}

修改调用方式,通过服务名称调用

@Component
public class UserService {

    // 导入org.springframework.cloud.client.discovery.DiscoveryClient
    @Autowired
    private DiscoveryClient discoveryClient;

    @Autowired
    private RestTemplate restTemplate;

    public List<User> queryUserByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        // 地址直接写服务名称即可
        String baseUrl = "http://user-service/user/";
        ids.forEach(id -> {
            // 测试多次查询,
            users.add(this.restTemplate.getForObject(baseUrl + id, User.class));
            // 每次间隔500毫秒
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        return users;
    }
}

[/collapse]

te.getForObject(baseUrl + id, User.class));

// 每次间隔500毫秒

try {


Thread.sleep(500);

} catch (InterruptedException e) {


e.printStackTrace();

}

});

return users;

}

}

[/collapse]

[/successbox]



版权声明:本文为qq_33887096原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。