0%

《重新定义SpringCloud实战》读书笔记

SpringCloud提供了快速构建分布式中常见模式的工具,包括配置管理、服务发现、断路器、智能路由、微代理、控制总线等。SpringCloud中间件是基于SpringBoot的实现,提供了对微服务完整的一套解决方案。

应用架构的发展历程:
单体应用架构 –> 分布式架构 –>面向服务的SOA架构 –> 微服务架构
SOA架构个人理解是多个应用之间通过企业数据总线ESB通信的架构,其应用程序通过网络协议提供服务,消费服务,不同业务提供不同的服务。(阿里的服务治理框架Dubbo)
微服务架构:一个大型的应用拆分为多个相互独立的微服务,每个服务之间松耦合,通过REST API或者HTTP进行通信。

SOA VS MA

SpringCloud包包含以下组件:
服务治理组件 Eureka / Consul + 客户端负载均衡组件 Ribbon + 声明式服务调用组件 Feign + API网关治理组件 Zuul / GateWay(高并发) + 熔断机制 HyStrix + 分布式配置中心组件 Spring Cloud Config / 携程 Apollo + 消息总线组件 Bus + 消息驱动组件 Stream + 分布式服务跟踪组件 Sleuth + 全链路监控 SkyWalking.


Tips: 代码基于Spring Cloud Finchley 版本


服务治理:Spring Cloud Eureka

负责微服务架构中的服务治理功能,即各个微服务实例的自动化注册与发现。
SpringCloud Eureka 是由 Netflix Eureka实现的,即包含了服务端组件也包含了客户端组件。
Eureka服务端也被称为服务注册中心,各个微服务启动时会向Eureka Server 注册自己的信息。代码如下:
https://start.spring.io/ 中新建一个Eureka Server的Demo,或者直接在Maven项目中的pom.xml文件中添加如下Dependence:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?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.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</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>
<spring-cloud.version>Greenwich.RC1</spring-cloud.version>
</properties>

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

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

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

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

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

<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>

</project>

EurekaServerApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.leezy.eureka_server;

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

@SpringBootApplication
//该注解启动一个服务注册中心提供给其他应用会话
@EnableEurekaServer
public class EurekaServerApplication
{
public static void main(String[] args)
{
System.out.println("Hello Eureka Server!");
SpringApplication.run(EurekaServerApplication.class, args);
}
}

Eureka Server 中的 application.yml 和 application-standalone.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# application.yml
spring:
profiles:
active: standalone
jackson:
serialization:
FAIL_ON_EMPTY_BEANS: false
eureka:
server:
use-read-only-response-cache: false
response-cache-auto-expiration-in-seconds: 10
management:
endpoints:
web:
exposure:
include: '*'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# application-standalone.yml
server:
port: 8761

eureka:
instance:
hostname: localhost
client:
registerWithEureka: false [[是否将自己注册到Eureka]] Server, 默认为True
fetchRegistry: false [[是否需要从Eureka]] Server获取注册信息, 默认为Ture
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 查询服务和注册服务的地址,多个用","隔开
server:
waitTimeInMsWhenSyncEmpty: 0
enableSelfPreservation: false

打开 http://localhost:8761/ 看到Eureka的控制面板。
Eureka服务注册端, Eureka Client将微服务注册到Eureka Server上。
EurekaClientApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.leezy.eureka_client;

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

@SpringBootApplication
// 该注解适配性比较好,可以用于多种服务发现组件(Zookeeper、Consul)
@EnableDiscoveryClient
public class EurekaClientApplication
{
public static void main(String[] args)
{
System.out.println("Hello Eureka Client!");
SpringApplication.run(EurekaClientApplication.class, args);
}
}

Maven依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Eureka Client的配置文件: application.yml 和 application-demo.yml

1
2
3
4
[[application]].yml
spring:
profiles:
active: demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[[application-demo]].yml
server:
port: 8081

Spring:
application:
name: demo-leezy #声明服务提供者的应用名称

eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/ [[设置与Eureka]] Server交互的地址
instance:
prefer-ip-address: true

刷新Eureka控制台就可以看到注册到Server上的服务了。

Eureka的设计理念:

  1. 服务实例如何注册到服务中心:
    (1)调用Eureka Server的REST API 的 register方法
    (2)Java语言使用者可以调用NetFlix的Eureka Client封装的API
    (3)Spring Cloud使用者在pom.xml文件中引用 spring-cloud-starter-netflix-eureka-client,基于Spring Boot的自动配置即可。
  2. 服务实例从服务中心剔除
    (1)服务实例正常关闭时,通过钩子方法或者生命周期回调方法调用Eureka Server 的REST API的de-register方法。
    (2)Eureka要求Client定时续约(30s),如果90s没有续约操作则Eureka Server主动剔除该操作。
  3. 服务实例信息的一致性问题
    服务注册与发现中心应该也是一个集群,如何保证一致性
    (1)AP优于CP (Zookeeper-CP, Eureka-AP)
    (2)Peer to Peer架构(1. 主从复制 2. 对等复制)
    (3)Zone及Region设计
    (4)SELF PRESERVATION设计

WebService客户端 Feign

Feign是一个声明式的Web Service客户端,用于服务与服务之间的调用,支持SpringMVC注解,整合了Ribbon以及Hystrix。
对应的POM依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

应用入口程序SpringCloudFeignApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.leezy.hello_feign;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class HelloFeignApplication
{
public static void main( String[] args )
{
SpringApplication.run(HelloFeignApplication.class, args);
}
}

接口类:HelloFeignService.java,作用是应用指定的URL最终转化为Github API允许的URL。
eg:https://api.github.com/search/repositories?q=spring-cloud
(Github RestAPI的文档:https://developer.github.com/v3/search/)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.leezy.hello_feign.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.leezy.hello_feign.config.HelloFeignServiceConfig;

@FeignClient(name = "github-client", url = "https://api.github.com", configuration = HelloFeignServiceConfig.class)
public interface HelloFeignService {
@RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
String searchRepo(@RequestParam(name = "q") String quertStr);
}

控制类:HelloFeignController.java,作用:调用服务提供者的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.leezy.hello_feign.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.leezy.hello_feign.service.HelloFeignService;

@RestController
public class HelloFeignController {

@Autowired
private HelloFeignService helloFeignService;

@GetMapping(value = "/search/github")
public String searchGithubRepoByStr(@RequestParam("str") String queryStr) {
return helloFeignService.searchRepo(queryStr);
}

}

配置类:HelloFeignServiceConfig.java,@Bean注解配置日志的bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.leezy.hello_feign.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import feign.Logger;

@Configuration
public class HelloFeignServiceConfig {
@Bean
Logger.Level feignLoggerLevel() {
//level有四个级别
return Logger.Level.FULL;
}
}

配置文件: application.yml

1
2
3
4
5
6
7
8
9
server:
port: 8010
spring:
application:
name: Hello Feign

logging:
level:
com.leezy.hello_feign.service.HelloFeignService: DEBUG #在这里配置日志的输出级别

启动应用后,访问网址:http://localhost:8010/search/github?str=spring-cloud
Feign支持的属性文件配置方式有两种:
application.yml(application.properties) 以及 Java方式的配置类,但是配置文件的优先级会高于Java类的优先级。
Feign默认的是JDK原生的URL Connection,并没有使用连接池,可以用Http Client和 okHttp进行替换对项目进行调优。

Feign 的 POST 和 GET 的多参数传递

Feign拦截器,将Json转化为Map。
实现Feign的RequestInterceptor中的apply方法来进行统一拦截转换处理Feign中的GET方法多参数传递问题。集成Swapper,编写服务消费者用于调用Feign进行Get或Post多参数传递。

负载均衡组件 Ribbon

Feign中集成了Ribbon,但是Ribbon可以单独使用,它是一种进程内负载均衡器(客户端负载均衡),它赋予了应用支配Http和Tcp的能力。
负载均衡策略:最常用的是RoundRobinRule 轮询策略
代码样例:
pom.xml

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

启动类:RibbonLoadbalancerApplication.java
注入一个RestTemplate的Bean,并且使用@LoadBalances注解才能使其具备负载均衡的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package cn.springcloud.book;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class RibbonLoadbalancerApplication {

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

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

TestController.java
Ribbon客户端需要创建一个API来调用Eureka源服务自定义的API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class TestController {

@Autowired
private RestTemplate restTemplate;

@GetMapping("/add")
public String add(Integer a, Integer b) {
String result = restTemplate
.getForObject("http://CLIENT-A/add?a=" + a + "&b=" + b, String.class);
System.out.println(result);
return result;
}
}

通过查找继承关系,发现接口ILoadBalancer的实现抽象类AbstractLoadBalancer的实现类BaseLoadBalancer中的chooseServer方法是真正实现负载均衡的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
* Get the alive server dedicated to key
*
* @return the dedicated server
*/
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}

熔断机制 Spring Cloud Hystrix

Hystrix is a latency and fault tolerance library designed to isolate points of access to remote systems, services and 3rd party libraries, stop cascading failure and enable resilience in complex distributed systems where failure is inevitable.
Hystrix的设计目标是:

  1. 通过客户端对延迟和故障进行保护和控制
  2. 在一个复杂的分布式系统中停止级联故障
  3. 快速失败和迅速恢复
  4. 在合理的情况下回退和优雅地降级
  5. 开启实时监控、告警和操作控制
    pom.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    <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.1.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.leezy</groupId>
    <artifactId>hystrix</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>hystrix</name>
    <url>http://maven.apache.org</url>

    <properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.RC1</spring-cloud.version>
    </properties>

    <dependencies>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional>
    </dependency>
    </dependencies>
    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    <dependencyManagement>
    <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-dependencies</artifactId>
    <version>${spring-cloud.version}</version>
    <type>pom</type>
    <scope>import</scope>
    </dependency>
    </dependencies>
    </dependencyManagement>

    <repositories>
    <repository>
    <id>spring-milestones</id>
    <name>Spring Milestones</name>
    <url>https://repo.spring.io/milestone</url>
    </repository>
    </repositories>
    </project>
    ClientApplication.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package com.leezy.hystrix;

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

    @SpringBootApplication
    @EnableHystrix
    @EnableDiscoveryClient
    public class ClientApplication {
    public static void main(String[] args) {
    SpringApplication.run(ClientApplication.class, args);
    }
    }
    TestController.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    package com.leezy.hystrix.controller;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;

    import com.leezy.hystrix.service.IUserService;

    @RestController
    public class TestController {
    @Autowired
    private IUserService userService;

    @GetMapping("/getUser")
    public String getUser(@RequestParam String username) throws Exception{
    return userService.getUser(username);
    }
    }
    IUserService.java
    1
    2
    3
    4
    5
    6
    package com.leezy.hystrix.service;

    public interface IUserService {
    public String getUser(String username) throws Exception;
    }

    UserService.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    package com.leezy.hystrix.service.impl;

    import org.springframework.stereotype.Component;

    import com.leezy.hystrix.service.IUserService;
    import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;

    @Component
    public class UserService implements IUserService{

    // 降级处理
    @Override
    @HystrixCommand(fallbackMethod="defaultUser")
    public String getUser(String username) throws Exception {
    if (username.equals("spring")) {
    return "This is real user.";
    } else {
    throw new Exception();
    }
    }

    public String defaultUser(String username) {
    return "The User does not exist in the system...Test!";
    }
    }
    bootstrap.yml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    server:
    port: 8888
    spring:
    application:
    name: hystrix-client-service
    eureka:
    client:
    serviceUrl:
    defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8761}/eureka/
    instance:
    prefer-ip-address: true
    打开浏览器访问:http://localhost:8888/getUser?username=springhttp://localhost:8888/getUser?username=testERROR

Hystrix Dashboard

Hystrix Dashboard仪表盘是根据系统一段时间内发生的请求情况来展示的可视化面板。
Hystrix的指标需要端口进行支撑,所以需要增加actuator依赖。
pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

HystrixDashboardApplication.java

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrixDashboard
public class HystrixDashboardApplication {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardApplication.class, args);
}
}

上面是单个实例的Hystrix Dashboard,整个系统和集群的情况下并不是特别有用。Turbine就是聚合所有相关Hystrix.stream 流的方案。

网关治理组件 Zuul

Zuul is the front door for all requests from devices and web sites to the backend of the Netflix streaming application. Zuul是对内部的微服务提供可配置的,对外URL到服务的映射关系,基于JVM的后端路由器。
pom.xml

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

启动类ZuulServerApplication.java

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}

bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
application:
name: zuul-server
server:
port: 5555
eureka:
client:
serviceUrl:
defaultZone: http://${eureka.host:127.0.0.1}:${eureka.port:8888}/eureka/
instance:
prefer-ip-address: true
zuul:
routes:
client-a:
path: /client/**
serviceId: client-a

最后五行的代码表示,Zuul组件的端口为portA,则将/client开头的URL映射搭配client-a这个服务中去,即实际访问portB。
/** 匹配任意数量的路径和字符
/* 匹配任意数量的字符
/? 匹配单个字符

Spring Cloud Zuul Filter链

(1) Filter的类型
(2) Filter的执行顺序
(3) Filter的执行条件
(4) Filter的执行效果
Zuul有四种不同生命周期的Filter,分别是:
pre Filter 按照规则路由到下级服务之前执行。比如鉴权、限流等
route Filter 路由动作的执行者(Apache HttpClient或Netflix Ribbon构建和发送原始Http请求的地方)
post Fliter 在源服务返回结果或者异常信息发生后执行的,对返回信息做一些处理
error Filter 在整个生命周期内如果发生异常,则会进入error Filter

Spring Cloud Zuul 权限集成
OAuth2.0 + JWT(JSON Web Token)

动态路由 Dynamic Routing

有两种解决方案:
(1) Spring Cloud Config + Bus、动态刷新配置文件。
(2) 重写Zuul的配置读取方式