SpringCloud

SpringCloud

Spring Cloud 介绍

架构图

image


image

组件

  • Eureka

    服务注册与发现,用于服务管理。服务提供方将己方调用地址注册到服务注册中心,让服务调用方能够方便地找到自己;服务调用方从服务注册中心找到自己需要调用的服务的地址。

  • Zuul

    网关路由,提供路由转发、请求过滤、限流降级等功能。服务网关是服务调用的唯一入口,可以在这个组件中实现用户鉴权、动态路由、灰度发布、A/B测试、负载限流等功能。

  • Ribbon

    基于客户端的负载均衡。服务提供方一般以多实例的形式提供服务,负载均衡功能能够让服务调用方连接到合适的服务节点。并且,服务节点选择的过程对服务调用方来说是透明的。

  • Config

    配置中心,分布式配置管理。将本地化的配置信息(Properties、XML、YAML等形式)注册到配置中心,实现程序包在开发、测试、生产环境中的无差别性,方便程序包的迁移,也是无状态特性。

  • Feign

    Web调用客户端,能够简化HTTP接口的调用。

  • Hystrix

    熔断降级,防止服务雪崩。

  • Sleuth

    服务链路追踪。记录完成一次请求的先后衔接和调用关系,并将这种串行或并行的调用关系展示出来。在系统出错时,可以方便地找到出错点。

  • Admin

    健康管理。

其他

  • SQA

    对单体应用的改进:引入SOA(Service-Oriented Architecture)面向服务架构,拆分系统,用服务的流程化来实现业务的灵活性。服务间需要某些方法进行连接,面向接口等,它是一种设计方法,其中包含多个服务, 服务之间通过相互依赖最终提供一系列的功能。一个服务 通常以独立的形式存在于操作系统进程中。各个服务之间 通过网络调用。但是还是需要用些方法来进行服务组合,有可能还是个单体应用。

Eureka

Eureka来源于古希腊词汇,意为“发现了”。Eureka是一个RESTful风格的服务,是一个用于服务发现和注册的基础组件,是搭建Spring Cloud微服务的前提之一,它屏蔽了Server和client的交互细节,使得开发者将精力放到业务上。

Eureka分为两部分,Server端和Client端。

Server

是一个公共服务,为Client提供服务注册和发现的功能,维护注册到自身的Client的相关信息,同时提供接口给Client获取注册表中其他服务的信息,使得动态变化的Client能够进行服务间的相互调用。

  • 功能
  1. 服务注册表

    记录各个微服务信息,例如服务名称,ip,端口等。注册表提供 查询API(查询可用的微服务实例)和管理API(用于服务的注册和注销)。

  2. 服务注册与发现

    注册:将微服务信息注册到注册中心。
    发现:查询可用微服务列表及其网络地址。

  3. 服务检查

    定时检测已注册的服务,如发现某实例长时间无法访问,就从注册表中移除。

  • 服务端依赖
1
@EnableEurekaServer
1
2
3
4
5
<!--  Eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

Client

将自己的服务信息通过一定的方式注册到Server上,并在正常范围内维护自己信息一致性,方便其他服务发现自己,同时可以通过Server获取到自己依赖的其他服务信息,完成服务调用,还内置了负载均衡器,用来进行基本的负载均衡。

  • 功能
  1. 注册

    每个微服务启动时,将自己的网络地址等信息注册到注册中心,注册中心会存储(内存中)这些信息。

  2. 获取服务注册表

    服务消费者从注册中心,查询服务提供者的网络地址,并使用该地址调用服务提供者,为了避免每次都查注册表信息,所以client会定时去server拉取注册表信息到缓存到client本地。

  3. 心跳

    各个微服务与注册中心通过某种机制(心跳)通信,若注册中心长时间和服务间没有通信,就会注销该实例。

  4. 调用

    实际的服务调用,通过注册表,解析服务名和具体地址的对应关系,找到具体服务的地址,进行实际调用。

  • 客户端依赖
1
@EnableEurekaClient
1
2
3
4
5
<!--  Eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Eureka原理

  • Register

    服务注册

    想要参与服务注册发现的实例首先需要向Eureka服务器注册信息注册在第一次心跳发生时提交

  • Renew

    续租,心跳

    Eureka客户端需要每30秒发送一次心跳来续租,更新通知Eureka服务器实例仍然是活动的,总共发送3次。如果服务器在90秒内没有看到更新,它将从其注册表中删除实例。

  • Fetch Registry

    Eureka客户端从服务器获取注册表信息并将其缓存在本地。之后,客户端使用这些信息来查找其他服务。通过获取上一个获取周期和当前获取周期之间的增量更新,可以定期(每30秒)更新此信息。节点信息在服务器中保存的时间更长(大约3分钟),因此获取节点信息时可能会再次返回相同的实例。Eureka客户端自动处理重复的信息。在获得增量之后,Eureka客户机通过比较服务器返回的实例计数来与服务器协调信息,如果由于某种原因信息不匹配,则再次获取整个注册表信息。

  • Cancel

    Eureka客户端在关闭时向Eureka服务器发送取消请求。这将从服务器的实例注册表中删除实例,从而有效地将实例从通信量中取出。

  • ime Lag

    同步时间延迟

    来自Eureka客户端的所有操作可能需要一段时间才能反映到Eureka服务器上,然后反映到其他Eureka客户端上。这是因为eureka服务器上的有效负载缓存,它会定期刷新以反映新信息。Eureka客户端还定期地获取增量。因此,更改传播到所有Eureka客户端可能需要2分钟。

  • Communication mechanism

    通讯机制

    Http协议下的Rest请求,默认情况下Eureka使用Jersey和Jackson以及JSON完成节点间的通讯。

配置单机服务高可用

  1. 引入Server依赖
1
2
3
4
5
<!--  Eureka-server -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
  1. 启用 @EnableEurekaServer
1
@EnableEurekaServer
  1. 本地配置hosts文件映射
1
2
127.0.0.1 peer1
127.0.0.1 peer2

Windows: C:/Windows/System32/drivers/etc/hosts

  1. 高可用配置
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
spring:
application:
name: eureka-server
---
server:
port: 8761
spring:
config:
activate:
on-profile: peer1
eureka:
instance:
hostname: peer1
client:
serviceUrl:
defaultZone: http://peer2:8762/eureka/
---
server:
port: 8762
spring:
config:
activate:
on-profile: peer2
eureka:
instance:
hostname: peer2
client:
serviceUrl:
defaultZone: http://peer1:8761/eureka/

peer1 跟 peer2 节点的applicationName(spring.application.name)一定要一样,否则就是两个 Server 服务了,也做不了集群

  • 集群

image

  • 非集群

image

  1. 使用IDEA配置多节点启动

image

  1. 分别启动两个节点

image

自我保护机制

机制

自我保护机制默认开启,服务器端容错的一种方式,即短时间心跳不到达仍不剔除服务列表里的节点。

Eureka在CAP理论当中是属于AP,也就说当产生网络分区时,Eureka保证系统的可用性,但不保证系统里面数据的一致性。

默认情况下,Eureka Server在一定时间内,没有接收到某个微服务心跳,会将某个微服务注销(90S)。但是当网络故障时,微服务与Server之间无法正常通信,上述行为就非常危险,因为微服务正常,不应该注销。

Eureka Server通过自我保护模式来解决整个问题,当Server在短时间内丢失过多客户端时,那么Server会进入自我保护模式,会保护注册表中的微服务不被注销掉。当网络故障恢复后,退出自我保护模式。

思想:宁可保留健康的和不健康的,也不盲目注销任何健康的服务。

自我保护触发

客户端每分钟续约数量小于客户端总数的85%时会触发保护机制

例子:服务实例数:10个,期望每分钟续约数:10 * 2=20,期望阈值:20 * 0.85=17,自我保护少于17时 触发。

关闭自我保护机制

1
2
3
eureka:
server:
enable-self-preservation: false

清理时间

默认60秒

1
2
3
eureka: 
server:
eviction-interval-timer-in-ms: 3000

Actuator

Server包已经包含了,在Client包里面添加即可

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

监控应用

访问地址:

http://localhost:8761/actuator

监控的默认端点

Spring Boot 2.0 的Actuator只暴露了health和info端点,提供的监控信息无法满足我们的需求。

开启所有端点

‘*’ 代表所有节点都加载

1
2
3
4
5
management:
endpoints:
web:
exposure:
include: *

开启远程关闭功能

1
2
3
4
management:
endpoint:
shutdown:
enabled: true

需要使用Post方式请求端点

健康检查

由于server和client通过心跳保持服务状态,而只有状态为UP的服务才能被访问。看eureka界面中的status。比如心跳一直正常,服务一直UP,但是此服务DB连不上了,无法正常提供服务。

此时,我们需要将微服务的健康状态也同步到server。只需要启动eureka的健康检查就行。这样微服务就会将自己的健康状态同步到eureka。

开启手动控制

1
2
3
4
eureka:
client:
healthcheck:
enabled: true

改变健康状态的Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class HealthStatusService implements HealthIndicator {

private Boolean status = true;

public void setStatus(Boolean status) {
this.status = status;
}

@Override
public Health health() {
// TODO Auto-generated method stub
if(status)
return new Health.Builder().up().build();
return new Health.Builder().down().build();
}

public String getStatus() {
// TODO Auto-generated method stub
return this.status.toString();
}
}

测试用的Controller

1
2
3
4
5
6
@GetMapping("/health")
public String health(@RequestParam("status") Boolean status) {

healthStatusSrv.setStatus(status);
return healthStatusSrv.getStatus();
}

这样我们可以直接通过 /health 接口实现服务的上下线

安全访问

开启Eureka安全连接

  • Maven依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • yml配置
1
2
spring.security.user.name=SLy
spring.security.user.password=123456

如果启动提示:

1
Root name 'timestamp' does not match expected ('instance') for type [simple]

需要手动关闭防止跨域攻击

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{

@Override
protected void configure(HttpSecurity http) throws Exception {
// TODO Auto-generated method stub
http.csrf().disable();
super.configure(http);
}
}

则此时配置Client的注册地址需要添加用户名以及密码

1
2
3
4
eureka:
client:
serviceUrl:
defaultZone: http://SLy:123456@peer2:8762/eureka/

Ribbon

Ribbon是Netflix开发的客户端负载均衡器,为Ribbon配置服务提供者地址列表后,Ribbon就可以基于某种负载均衡策略算法,自动地帮助服务消费者去请求 提供者。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。我们也可以实现自定义负载均衡算法。

Ribbon作为Spring Cloud的负载均衡机制的实现:

  1. Ribbon可以单独使用,作为一个独立的负载均衡组件。只是需要我们手动配置 服务地址列表。
  2. Ribbon与Eureka配合使用时,Ribbon可自动从Eureka Server获取服务提供者地址列表(DiscoveryClient),并基于负载均衡算法,请求其中一个服务提供者实例。
  3. Ribbon与OpenFeign和RestTemplate进行无缝对接,让二者具有负载均衡的能力。OpenFeign默认集成了Ribbon。
1
2
3
4
5
@Bean
@LoadBalanced
RestTemplate getRestTemplate() {
return new RestTemplate();
}

负载均衡理论

当系统面临大量的用户访问,负载过高的时候,通常会增加服务器数量来进行横向扩展(集群),多个服务器的负载需要均衡,以免出现服务器负载不均衡,部分服务器负载较大,部分服务器负载较小的情况。通过负载均衡,使得集群中服务器的负载保持在稳定高效的状态,从而提高整个系统的处理能力。

image

  • 服务端负载均衡

    在客户端和服务端中间使用代理,nginx。在服务端负载均衡中,客户端节点只知道单一服务代理的地址,服务代理则知道所有服务端的地址。

  • 客户端负载均衡

    根据自己的情况做负载,Ribbon。在客户端负载均衡中,所有的客户端节点都有一份自己要访问的服务端地址列表,这些列表统统都是从服务注册中心获取的。

    在Spring Cloud中我们如果想要使用客户端负载均衡,方法很简单,使用@LoadBalanced注解即可,这样客户端在发起请求的时候会根据负载均衡策略从服务端列表中选择一个服务端,向该服务端发起网络请求,从而实现负载均衡。

客户端负载均衡和服务端负载均衡最大的区别在于服务端地址列表的存储位置,以及负载算法在哪里。

负载均衡算法

默认实现:

  • ZoneAvoidanceRule(区域权衡策略)

    复合判断Server所在区域的性能和Server的可用性,轮询选择服务器。

其他规则:

  • BestAvailableRule(最低并发策略)

    会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务。逐个找服务,如果断路器打开,则忽略。

  • RoundRobinRule(轮询策略)

    以简单轮询选择一个服务器。按顺序循环选择一个server。

  • RandomRule(随机策略)

    随机选择一个服务器。

  • AvailabilityFilteringRule(可用过滤策略)

    会先过滤掉多次访问故障而处于断路器跳闸状态的服务和过滤并发的连接数量超过阀值得服务,然后对剩余的服务列表安装轮询策略进行访问。

  • WeightedResponseTimeRule(响应时间加权策略

    据平均响应时间计算所有的服务的权重,响应时间越快服务权重越大,容易被选中的概率就越高。刚启动时,如果统计信息不中,则使用RoundRobinRule(轮询)策略,等统计的信息足够了会自动的切换到WeightedResponseTimeRule。响应时间长,权重低,被选择的概率低。反之,同样道理。此策略综合了各种因素(网络,磁盘,IO等),这些因素直接影响响应时间。

  • RetryRule(重试策略)

    先按照RoundRobinRule(轮询)的策略获取服务,如果获取的服务失败则在指定的时间会进行重试,进行获取可用的服务。如多次获取某个服务失败,就不会再次获取该服务。主要是在一个时间段内,如果选择一个服务不成功,就继续找可用的服务,直到超时。

切换负载均衡策略

  • 注解方式
1
2
3
4
5
6
@Bean
public IRule myRule() {
// return new RoundRobinRule();
// return new RandomRule();
return new RetryRule();
}
  • 配置文件

针对服务定ribbon策略:

1
provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

给所有服务定ribbon策略:

1
ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

脱离Eureka

Ribbon可以和服务注册中心Eureka一起工作,从服务注册中心获取服务端的地址信息,也可以在配置文件中使用listOfServers字段来设置服务端地址(脱离Eureka,直连Server)。

1
2
ribbon.eureka.enabled=false
ribbon.listOfServers=localhost:80,localhost:81

RestTemplate

RESTful

RESTful网络请求是指RESTful风格的网络请求,其中REST是Resource Representational State Transfer的缩写,直接翻译即“资源表现层状态转移”。

Resource代表互联网资源。所谓“资源”是网络上的一个实体,或者说网上的一个具体信息。它可以是一段文本、一首歌曲、一种服务,可以使用一个URI指向它,每种“资源”对应一个URI。

Representational是“表现层”意思。“资源”是一种消息实体,它可以有多种外在的表现形式,我们把“资源”具体呈现出来的形式叫作它的“表现层”。比如说文本可以用TXT格式进行表现,也可以使用XML格式、JSON格式和二进制格式;视频可以用MP4格式表现,也可以用AVI格式表现。URI只代表资源的实体,不代表它的形式。它的具体表现形式,应该由HTTP请求的头信息Accept和Content-Type字段指定,这两个字段是对“表现层”的描述。

State Transfer是指“状态转移”。客户端访问服务的过程中必然涉及数据和状态的转化。如果客户端想要操作服务端资源,必须通过某种手段,让服务器端资源发生“状态转移”。而这种转化是建立在表现层之上的,所以被称为“表现层状态转移”。客户端通过使用HTTP协议中的四个动词来实现上述操作,它们分别是:获取资源的GET、新建或更新资源的POST、更新资源的PUT和删除资源的DELETE。

RestTemplate

RestTemplate是Spring提供的同步HTTP网络客户端接口,它可以简化客户端与HTTP服务器之间的交互,并且它强制使用RESTful风格。它会处理HTTP连接和关闭,只需要使用者提供服务器的地址(URL)和模板参数。

注入与使用

1
2
3
4
5
@Bean
@LoadBalanced // 开启负载均衡
RestTemplate restTemplate() {
return new RestTemplate();
}
1
2
String url ="http://provider/getHi";
String respStr = restTemplate.getForObject(url, String.class);

请求

Get请求

  • getForObject

返回的是一个实体对象。

  • getForEntity

getForEntity方法的返回值是一个ResponseEntity,ResponseEntity是Spring对HTTP请求响应的封装,包括了几个重要的元素,如响应码、contentType、contentLength、响应消息体等。

1
<200,Hi,[Content-Type:"text/plain;charset=UTF-8", Content-Length:"8", Date:"Fri, 10 Apr 2020 09:58:44 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>

返回Map

Provider

1
2
3
4
5
6
@GetMapping("/getMap")
public Map<String, String> getMap() {
HashMap<String, String> map = new HashMap<>();
map.put("name", "SLy");
return map;
}

Consumer

1
2
3
String url ="http://provider/getMap";
ResponseEntity<Map> entity = restTemplate.getForEntity(url, Map.class);
System.out.println("respStr: " + entity.getBody());

返回Object

Provider

1
2
3
4
5
@GetMapping("/getObj")
public Person getObj() {
Person person = new Person(123, "SLy");
return person;
}

Consumer

1
2
ResponseEntity<Person> entity = restTemplate.getForEntity(url, Person.class);
System.out.println("respStr: " + ToStringBuilder.reflectionToString(entity.getBody()));

传参调用

Provider

1
2
3
4
5
@GetMapping("/getObjParam")
public Person getObjParam(String name) {
Person person = new Person(123, name);
return person;
}

Consumer - 占位符

1
2
String url ="http://provider/getObjParam?name={1}";
ResponseEntity<Person> entity = restTemplate.getForEntity(url, Person.class, "SLy");

Consumer - Map

1
2
3
String url ="http://provider/getObjParam?name={name}";
Map<String, String> map = Collections.singletonMap("name", " SLy");
ResponseEntity<Person> entity = restTemplate.getForEntity(url, Person.class, map);

Consumer - 返回对象

1
2
3
String url ="http://provider/getObjParam?name={name}";
Map<String, String> map = Collections.singletonMap("name", " SLy");
Person person = restTemplate.getForObject(url, Person.class, map);

Post请求

  • postForEntity
  • postForObject
  • postForLocation

传参调用

Provider

1
2
3
4
5
@PostMapping("/postParam")
public Person postParam(@RequestBody String name) {
Person person = new Person(123, name);
return person;
}

Consumer

1
2
3
String url ="http://provider/postParam";
Map<String, String> map = Collections.singletonMap("name", " SLy");
ResponseEntity<Person> entity = restTemplate.postForEntity(url, map, Person.class);

重定向

Provider

需要设置头信息,不然返回的是null

1
2
3
4
5
@PostMapping("/postParam")
public URI postParam(@RequestBody Person person, HttpServletResponse response) throws Exception {
URI uri = new URI("https://www.baidu.com/s?wd="+person.getName());
response.addHeader("Location", uri.toString());
}

Consumer

1
2
3
4
String url ="http://provider/postParam";
Map<String, String> map = Collections.singletonMap("name", " memeda");
URI location = restTemplate.postForLocation(url, map, Person.class);
System.out.println(location);

exchange

可以自定义http请求的头信息,同时保护get和post方法

拦截器

需要实现ClientHttpRequestInterceptor接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {

System.out.println("拦截啦!!!");
System.out.println(request.getURI());

ClientHttpResponse response = execution.execute(request, body);

System.out.println(response.getHeaders());
return response;
}

添加到resttemplate中

1
2
3
4
5
6
7
@Bean
@LoadBalanced
RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new LoggingClientHttpRequestInterceptor());
return restTemplate;
}

Feign

OpenFeign是Netflix 开发的声明式、模板化的HTTP请求客户端。可以更加便捷、优雅地调用Http Api。

OpenFeign会根据带有注解的函数信息构建出网络请求的模板,在发送网络请求之前,OpenFeign会将函数的参数值设置到这些请求模板中。feign主要是构建微服务消费端。只要使用OpenFeign提供的注解修饰定义网络请求的接口类,就可以使用该接口的实例发送RESTful的网络请求。还可以集成Ribbon和Hystrix,提供负载均衡和断路器。

英文表意为“假装,伪装,变形”, 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。Feign 封装 了HTTP 调用流程,面向接口编程。

Feign和OpenFeign的关系

Feign本身不支持Spring MVC的注解,它有一套自己的注解。

OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

调用原理图

image

Feign使用

  1. pom.xml
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 添加接口,注解

一般一个服务提供者,写一个interface。

此处由于结合了eureka,所以name是虚拟主机名,默认服务名(此时的name作用就是创建负载均衡器),请求时会将它解析成注册表中的服务。

1
2
3
4
5
@FeignClient(name = "service-valuation")
public interface ServiceForecast {
@RequestMapping(value = "/forecast/single", method = RequestMethod.POST)
public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest);
}

不结合eureka,就是自定义一个client名字。就用url属性指定 服务器列表。url=“http://ip:port/”

1
2
3
4
5
@FeignClient(name = "service-valuation-without-eureka", url = "http://localhost:8060", configuration = FeignAuthConfiguration.class)
public interface ServiceForecastWithoutEureka {
@RequestMapping(value = "/forecast/single", method = RequestMethod.POST)
public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest);
}
  1. 启动类
1
@EnableFeignClients // 就像是一个开关,只有使用了该注解,OpenFeign相关的组件和配置机制才会生效。
  1. 调用

调用此方法:会向service-valuation服务的接口:/forecast/single 发送请求

1
2
3
4
5
6
7
8
@Autowired
private ServiceForecast serviceForecast;

@PostMapping("/forecast")
public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest) {
ResponseResult<ForecastResponse> result = serviceForecast.forecast(forecastRequest);
return ResponseResult.success(result.getData());
}

Get和Post

Feign默认所有带参数的请求都是Post,想要使用指定的提交方式需引入依赖

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

并指明提交方式

1
2
3
4
@GetMapping("/findById")


@RequestMapping(value = "/register", method = RequestMethod.POST)

带参请求

1
2
3
4
5
6
7
8
9
10
11
12
@FeignClient(name = "user-provider")
public interface ConsumerApi {

@GetMapping("/findById")
public Map findById(@RequestParam("id") Integer id);

@GetMapping("/getMap")
Map<Integer, String> getMap(@RequestParam Map<String, Object> map);

@PostMapping("/register")
public Map<String, String> register(@RequestBody User user);
}

超时设置

Feign默认支持Ribbon,Ribbon的重试机制和Feign的重试机制有冲突,所以源码中默认关闭Feign的重试机制,使用Ribbon的重试机制。

1
2
3
4
#连接超时时间(ms)
ribbon.ConnectTimeout=1000
#业务逻辑超时时间(ms)
ribbon.ReadTimeout=6000

重试机制

使用Ribbon重试机制,请求失败后,每间隔6秒会重新尝试

1
2
3
4
5
6
#同一台实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetries=1
#重试负载均衡其他的实例最大重试次数,不包括首次调用
ribbon.MaxAutoRetriesNextServer=1
#是否所有操作都重试
ribbon.OkToRetryOnAllOperations=false

自定义Feign配置

Java代码定义

feign的默认配置类是:org.springframework.cloud.openfeign.FeignClientsConfiguration。默认定义了feign使用的编码器,解码器等。

允许使用@FeignClient的configuration的属性自定义Feign配置。自定义的配置优先级高于上面的FeignClientsConfiguration。

通过权限的例子,学习feign的自定义配置。服务提供者。上述例子开放service-valuation的权限后,访问。

  1. pom.xml
1
2
3
4
5
<!-- 安全认证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. WebSecurityConfig
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf
http.csrf().disable();
// 表示所有的访问都必须认证,认证处理后才可以正常进行
http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated();
// 所有的rest服务一定要设置为无状态,以提升操作效率和性能
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
  1. yml
1
2
3
4
5
spring: 
security:
user:
name: root
password: root

继续Feign原来访问,报错401。

  1. 自定义配置

有两种方式:自定义配置类和拦截器

  • 自定义配置类
1
2
3
4
5
6
public class FeignAuthConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("root", "root");
}
}

在Feign上加配置

1
@FeignClient(name = "service-valuation", configuration = FeignAuthConfiguration.class)

继续feign原来访问,访问成功。

如果在配置类上添加了@Configuration注解,并且该类在@ComponentScan所扫描的包中,那么该类中的配置信息就会被所有的@FeignClient共享。最佳实践是:不指定@Configuration注解(或者指定configuration,用注解忽略),而是手动:@FeignClient(name = “service-valuation”, configuration = FeignAuthConfiguration.class)

  • 拦截器
1
2
3
4
5
6
7
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// TODO Auto-generated method stub
template.header("Authorization", "Basic cm9vdDpyb290");
}
}
1
2
3
4
5
6
feign:
client:
config:
service-valuation:
request-interceptors:
- com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor

属性定义

接上面例子,此例子和上面例子实现的功能一样。记得两者取一个即可。说明用属性而不是用属性中的configuration。

  1. 定义拦截器
1
2
3
4
5
6
7
public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// TODO Auto-generated method stub
template.header("Authorization", "Basic cm9vdDpyb290");
}
}
  1. yml
1
2
3
4
5
6
feign:
client:
config:
service-valuation:
request-interceptors:
- com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor

再次访问,测试Ok。

扩展

指定服务名称配置:

1
2
3
4
5
6
7
feign:
client:
config:
service-valuation:
connect-timeout: 5000
read-timeout: 5000
logger-level: full

通用配置:

1
2
3
4
5
6
7
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
logger-level: full

属性配置比Java代码优先级高。也可通过配置设置java代码优先级高。

1
2
3
feign:
client:
default-to-properties: false

feign在方法上可以设置:@RequestMapping,@ResponseBody。

方法中的参数可以设置:@RequestBody等等,Spring MVC中的注解。

推荐使用yml配置方式,在yml中按代码提示键,可以看到所有配置。

Hystrix

Spring Cloud 用的是 Hystrix,是一个容错组件。

Hystrix实现了 超时机制和断路器模式。

Hystrix是Netflix开源的一个类库,用于隔离远程系统、服务或者第三方库,防止级联失败,从而提升系统的可用性与容错性。主要有以下几点功能:

  • 为系统提供保护机制

    在依赖的服务出现高延迟或失败时,为系统提供保护和控制。

  • 防止雪崩

  • 包裹请求

    使用HystrixCommand(或HystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中运行。

  • 跳闸机制

    当某服务失败率达到一定的阈值时,Hystrix可以自动跳闸,停止请求该服务一段时间。

  • 资源隔离

    Hystrix为每个请求都的依赖都维护了一个小型线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速失败判定。防止级联失败。

  • 快速失败

    Fail Fast。同时能快速恢复。侧重点是:(不去真正的请求服务,发生异常再返回),而是直接失败。

  • 监控

    Hystrix可以实时监控运行指标和配置的变化,提供近实时的监控、报警、运维控制。

  • 回退机制

    fallback,当请求失败、超时、被拒绝,或当断路器被打开时,执行回退逻辑。回退逻辑我们自定义,提供优雅的服务降级。

  • 自我修复

    断路器打开一段时间后,会自动进入“半开”状态,可以进行打开,关闭,半开状态的转换。

熔断

在分布式系统下,微服务之间不可避免地会发生相互调用,但每个系统都无法百分之百保证自身运行不出问题。在服务调用中,很可能面临依赖服务失效的问题(网络延时,服务异常,负载过大无法及时响应)。因此需要一个组件,能提供强大的容错能力,为服务间调用提供保护和控制。

我们的目的:当我自身 依赖的服务不可用时,服务自身不会被拖垮。防止微服务级联异常。

本质:就是隔离坏的服务,不让坏服务拖垮其他服务(调用坏服务的服务)。

舱壁模式

舱壁模式(Bulkhead)隔离了每个工作负载或服务的关键资源,如连接池、内存和CPU,硬盘。每个工作单元都有独立的 连接池,内存,CPU。

使用舱壁避免了单个服务消耗掉所有资源,从而导致其他服务出现故障的场景。

这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。

据说泰坦尼克原因:泰坦尼克号上有16个防水舱,设计可以保障如果只有4个舱进水,密闭和隔离可以阻止水继续进入下一个防水舱,从而保证船的基本浮力。但是当时冰山从侧面划破了船体,从而导致有5个防水舱同时进水,而为了建造豪华的头等舱大厅,也就是电影里杰克和罗斯约会的地方,5号舱的顶部并未达到密闭所需要的高度,水就一层层进入了船体,隔离的失败导致了泰坦尼克的沉没。

给我们的思路:可以对每个请求设置,单独的连接池,配置连接数,不要影响 别的请求。就像一个一个的防水舱。

雪崩效应

每个服务 发出一个HTTP请求都会在服务中开启一个新线程。而下游服务挂了或者网络不可达,通常线程会阻塞住,直到Timeout。如果并发量多一点,这些阻塞的线程就会占用大量的资源,很有可能把自己本身这个微服务所在的机器资源耗尽,导致自己也挂掉。

如果服务提供者响应非常缓慢,那么服务消费者调用此提供者就会一直等待,直到提供者响应或超时。在高并发场景下,此种情况,如果不做任何处理,就会导致服务消费者的资源耗竭甚至整个系统的崩溃。一层一层的崩溃,导致所有的系统崩溃。

雪崩

由基础服务故障导致级联故障的现象。描述的是:提供者不可用导致消费者不可用,并将不可用逐渐放大的过程。像滚雪球一样,不可用的服务越来越多。影响越来越恶劣。

雪崩三个流程:

  1. 服务提供者不可用。
  2. 重试会导致网络流量加大,更影响服务提供者。
  3. 导致服务调用者不可用,由于服务调用者 一直等待返回,一直占用系统资源。

服务不可用原因:

  • 服务器宕机
  • 网络故障
  • 宕机
  • 程序异常
  • 负载过大,导致服务提供者响应慢
  • 缓存击穿导致服务超负荷运行

总之:基础服务故障导致级联故障就是雪崩。

容错机制

  1. 网络请求设置超时

必须为网络请求设置超时。一般的调用一般在几十毫秒内响应。如果服务不可用,或者网络有问题,那么响应时间会变很长。长到几十秒。

每一次调用,对应一个线程或进程,如果响应时间长,那么线程就长时间得不到释放,而线程对应着系统资源,包括CPU,内存,得不到释放的线程越多,资源被消耗的越多,最终导致系统崩溃。

因此必须设置超时时间,让资源尽快释放。

  1. 使用断路器模式

想一下家里的保险丝,跳闸。如果家里有短路或者大功率电器使用,超过电路负载时,就会跳闸,如果不跳闸,电路烧毁,波及到其他家庭,导致其他家庭也不可用。通过跳闸保护电路安全,当短路问题,或者大功率问题被解决,在合闸。

自己家里电路,不影响整个小区每家每户的电路。

断路器

如果对某个微服务请求有大量超时(说明该服务不可用),再让新的请求访问该服务就没有意义,只会无谓的消耗资源。例如设置了超时时间1s,如果短时间内有大量的请求无法在1s内响应,就没有必要去请求依赖的服务了。

  1. 断路器是对容易导致错误的操作的代理。这种代理能统计一段时间内的失败次数,并依据次数决定是正常请求依赖的服务还是直接返回。
  2. 断路器可以实现快速失败,如果它在一段时间内检测到许多类似的错误(超时),就会在之后的一段时间,强迫对该服务的调用快速失败,即不再请求所调用的服务。这样对于消费者就无须再浪费CPU去等待长时间的超时。
  3. 断路器也可自动诊断依赖的服务是否恢复正常。如果发现依赖的服务已经恢复正常,那么就会恢复请求该服务。通过重置时间来决定断路器的重新闭合。

这样就实现了微服务的“自我修复”:当依赖的服务不可用时,打开断路器,让服务快速失败,从而防止雪崩。当依赖的服务恢复正常时,又恢复请求。

断路器状态转换的逻辑:

  • 关闭状态

    正常情况下,断路器关闭,可以正常请求依赖的服务。

  • 打开状态

    当一段时间内,请求失败率达到一定阈值,断路器就会打开。服务请求不会去请求依赖的服务。调用方直接返回。不发生真正的调用。重置时间过后,进入半开模式。

  • 半开状态

    断路器打开一段时间后,会自动进入“半开模式”,此时,断路器允许一个服务请求访问依赖的服务。如果此请求成功(或者成功达到一定比例),则关闭断路器,恢复正常访问。否则,则继续保持打开状态。

断路器的打开,能保证服务调用者在调用异常服务时,快速返回结果,避免大量的同步等待,减少服务调用者的资源消耗。并且断路器能在打开一段时间后继续侦测请求执行结果,判断断路器是否能关闭,恢复服务的正常调用。

降级

为了在整体资源不够的时候,适当放弃部分服务,将主要的资源投放到核心服务中,待渡过难关之后,再重启已关闭的服务,保证了系统核心服务的稳定。当服务停掉后,自动进入fallback替换主方法。

用fallback方法代替主方法执行并返回结果,对失败的服务进行降级。当调用服务失败次数在一段时间内超过了断路器的阈值时,断路器将打开,不再进行真正的调用,而是快速失败,直接执行fallback逻辑。服务降级保护了服务调用者的逻辑。

熔断和降级

  • 共同点

    1. 为了防止系统崩溃,保证主要功能的可用性和可靠性。
    2. 用户体验到某些功能不能用。
  • 不同点

    1. 熔断由下级故障触发,主动惹祸。
    2. 降级由调用方从负荷角度触发,无辜被抛弃。

隔离

image

两种隔离策略:线程隔离和信号量隔离,即“THREAD”和“SEMAPHORE”,系统默认为“THREAD”。

@HystrixCommand注释修饰一个服务时,HystrixCommand的运行逻辑有可能是在该请求的主线程上一并执行,也有可能是单独起一个线程来执行,这取决于我们如何设置Hystrix线程的隔离策略。execution.isolation.strategy属性就是用来设置HystrixCommand.run()执行的隔离策略的。

Hystrix中默认并且推荐使用线程隔离(THREAD),一般来说,只有当调用负载异常高时(例如每个实例每秒调用数百次)才需要信号量隔离,因为这种场景下使用THREAD开销会比较高。信号量隔离一般仅适用于非网络调用的隔离。

  • THREAD(线程隔离)

使用该方式,HystrixCommand将会在单独的线程上执行,并发请求受线程池中线程数量的限制。不同服务通过使用不同线程池,彼此间将不受影响,达到隔离效果。

此种隔离方式:将调用服务线程与服务访问的执行线程分割开来,调用线程能够空出来去做其他工作,而不至于因为服务调用的执行,阻塞过长时间。

Hystrix将使用独立的线程池对应每一个服务提供者,用于隔离和限制这些服务。于是某个服务提供者的高延迟或者资源受限只会发生在该服务提供者对应的线程池中。

  • SEMAPHORE(信号量隔离)

其实就是个计数器,使用该方式,HystrixCommand将会在调用线程上执行,通过信号量限制单个服务提供者的并发量,开销相对较小(因为不用那么多线程池),并发请求受到信号量个数的限制。 线程隔离会带来线程开销,有些场景(比如无网络请求场景)可能会因为用开销换隔离得不偿失,为此hystrix提供了信号量隔离,当服务的并发数大于信号量阈值时将进入fallback。

取舍

线程池和信号量都支持熔断和限流。相比线程池,信号量不需要线程切换,因此避免了不必要的开销。但是信号量不支持异步,也不支持超时,也就是说当所请求的服务不可用时,信号量会控制超过限制的请求立即返回,但是已经持有信号量的线程只能等待服务响应或从超时中返回,即可能出现长时间等待。线程池模式下,当超过指定时间未响应的服务,Hystrix会通过响应中断的方式通知线程立即结束并返回。

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
# 隔离策略,默认是Thread, 可选Thread|Semaphore
# thread 通过线程数量来限制并发请求数,可以提供额外的保护,但有一定的延迟。一般用于网络调用
# semaphore 通过semaphore count来限制并发请求数,适用于无网络的高并发请求
hystrix.command.default.execution.isolation.strategy
# 命令执行超时时间,默认1000ms
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
# 执行是否启用超时,默认启用true
hystrix.command.default.execution.timeout.enabled
# 发生超时是是否中断,默认true
hystrix.command.default.execution.isolation.thread.interruptOnTimeout
# 最大并发请求数,默认10,该参数当使用ExecutionIsolationStrategy.SEMAPHORE策略时才有效。如果达到最大并发请求数,请求会被拒绝。理论上选择semaphore size的原则和选择thread size一致,但选用semaphore时每次执行的单元要比较小且执行速度快(ms级别),否则的话应该用thread。
# semaphore应该占整个容器(tomcat)的线程池的一小部分。
hystrix.command.default.execution.isolation.semaphore.maxConcurrentRequests

Hystrix使用

Hystrix独立使用脱离Spring Cloud

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
public class HystrixTest extends HystrixCommand {

protected HystrixTest(HystrixCommandGroupKey group) {
super(group);
// TODO Auto-generated constructor stub
}

public static void main(String[] args) {

// HystrixTest hystrixTest = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext"));
/**
* execute():以同步阻塞方式执行run()。以demo为例,调用execute()后,
* hystrix先创建一个新线程运行run(),
* 接着调用程序要在execute()调用处一直阻塞着,直到run()运行完成
*/
// System.out.println("result:" + hystrixTest.execute());

/**
* queue():以异步非阻塞方式执行run()。以demo为例,
* 一调用queue()就直接返回一个Future对象,
* 同时hystrix创建一个新线程运行run(),
* 调用程序通过Future.get()拿到run()的返回结果,
* 而Future.get()是阻塞执行的
*/
Future<String> futureResult = new HystrixTest(HystrixCommandGroupKey.Factory.asKey("ext")).queue();
String result = "";
try {
result = futureResult.get();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println("程序结果:"+ result);
}

@Override
protected Object run() throws Exception {
// TODO Auto-generated method stub
System.out.println("执行逻辑");
int i = 1/0;
return "ok";
}

@Override
protected Object getFallback() {
// TODO Auto-generated method stub
return "getFallbackgetFallback";
}
}

结合RestTemplate

  1. pom.xml

服务消费端引入

1
2
3
4
5
<!-- 引入hystrix依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  1. 启动类
1
@EnableCircuitBreaker
  1. 调用方法
1
2
3
4
5
6
7
8
9
10
11
12
13
@HystrixCommand(fallbackMethod = "back")
public String alive() {
// 自动处理URL
RestTemplate restTemplate = new RestTemplate();
String url ="http://user-provider/User/alive";
String object = restTemplate.getForObject(url, String.class);
return object;
}


public String back() {
return "请求失败~bbb...";
}

结合Feign

  1. pom.xml

服务消费端引入

1
2
3
4
5
<!-- 引入hystrix依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
  1. yml

feign自带Hystrix,但是默认没有打开,首先打开Hystrix。(从Spring Cloud Dalston开始,feign的Hystrix 默认关闭,如果要用feign,必须开启)

1
2
3
feign:
hystrix:
enabled: true
  1. 注解添加FeignClient
1
2
3
4
5
6
7
8
9
@FeignClient(name = "user-provider", fallback = AliveBack.class)
public interface ConsumerApi {

@RequestMapping(value = "/User/alive", method = RequestMethod.GET)
public String alive();

@RequestMapping(value = "/User/getById", method = RequestMethod.GET)
public String getById(Integer id);
}
  1. 实现接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class AliveBack implements ConsumerApi {

@Override
public String alive() {
// TODO Auto-generated method stub
return "error";
}

@Override
public String getById(Integer id) {
// TODO Auto-generated method stub
return "error";
}
}
  1. 启动类
1
2
@EnableFeignClients
@EnableCircuitBreaker

结合FallbackFactory

可以检查具体错误

1
@FeignClient(name = "user-provider", fallbackFactory = WebError.class)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Component
public class WebError implements FallbackFactory<ConsumerApi> {

@Override
public ConsumerApi create(Throwable cause) {
return new ConsumerApi() {

@Override
public String alive() {
// TODO Auto-generated method stub
System.out.println(cause.getLocalizedMessage());
cause.printStackTrace();
return ToStringBuilder.reflectionToString(cause);
}

@Override
public String getById(Integer id) {
// TODO Auto-generated method stub
return "error";
}
};
}
}

可视化

  1. pom.xml
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. 启动类
1
@EnableHystrixDashboard
  1. 访问

Zuul

网关是介于客户端(外部调用方比如app,h5)和微服务的中间层。

Zuul默认集成了:Ribbon和Hystrix。

Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。

  • 分发请求

    是所有微服务入口,进行分发。

  • 身份认证与安全

    识别合法的请求,拦截不合法的请求。

  • 监控

    在入口处监控,更全面。

  • 动态路由

    动态将请求分发到不同的后端集群。

  • 压力测试

    可以逐渐增加对后端服务的流量,进行测试。

  • 负载均衡

    也是用Ribbon。

  • 限流

    比如我每秒只要1000次,10001次就不让访问了。

Zuul使用

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

<!--zuul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
  1. 启动类

该注解声明这是一个Zuul代理,该代理使用Ribbon来定位注册到Eureka server上的微服务,同时整合了Hystrix,实现了容错。

1
@EnableZuulProxy
  1. yml

普通配置,端口,应用名,eureka地址。即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server:
port: 9000

spring:
application:
name: zuul-server

#注册中心
eureka:
client:
#设置服务注册中心的URL
service-url:
defaultZone: http://euk1.com:7001/eureka/
instance:
hostname: localhost
instance-id: online-taxi-zuul
  1. 访问

地址:http://网关ip:网关端口/服务名/微服务路径

结论:网关会将服务名转换成具体服务的ip和端口,实际进行访问。

注意:此处的ip和端口是 网关的ip和端口。

负载均衡

启动两个Consumer,轮询访问上面地址,会看到返回结果中,端口一直轮询在变。说明负载均衡生效了,默认是轮询。

1
consumer.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule

路由端点

作用:调试的时候,看网关请求的地址,以及映射是否正确。网关请求有误时,可以通过此处排查错误。

1
2
3
4
5
6
7
8
9
10
11
12
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
##默认是never
show-details: ALWAYS
enabled: true
routes:
enabled: true

配置指定微服务的访问路径

  1. 通过服务名配置(虚拟主机名)
1
2
3
zuul:
routes:
consumer: /xx/**
  1. 自定义命名配置
1
2
3
4
5
uul:
routes:
consumer-zuul-name: # 此处名字随便取
path: /xx/**
url: http://localhost:9002/
  1. 自定义下的负载均衡

基于2,恢复Ribbon+Hystrix

1
2
3
4
5
6
7
8
9
10
11
12
zuul:
routes:
consumer-zuul-name: # 此处名字随便取
path: /xx/**
service-id: no-eureka-api-driver # 指定serviceId

no-eureka-api-driver:
ribbon:
listOfServers: localhost:9003,localhost:9002
ribbon:
eureka:
enabled: false

高可用

Zuul作为普通的服务。对外访问。前面加一层(Nginx+keepalived)

Config

单体应用,配置写在配置文件中,没有什么大问题。如果要切换环境 可以切换不同的profile(2种方式),但在微服务中:

  1. 微服务比较多。成百上千,配置很多,需要集中管理。
  2. 管理不同环境的配置。
  3. 需要动态调整配置参数,更改配置不停服。

分布式配置中心包括3个部分:

  1. 存放配置的地方:git,本地文件等。
  2. Config Server。从 1 读取配置。
  3. Config Client。是 Config Server 的客户端消费配置。

Config使用

环境部署之前,将所需的配置信息推送到配置仓库。

启动配置中心服务端,将配置仓库的配置信息拉取到服务端,配置服务端对外提供RESTful接口。

启动配置客户端,客户端根据 spring.cloud.config 配置的信息去服务器拉取相应的配置。

Git

创建4个配置文件,上传到Git

config-client-dev.yml

config-client-test.yml

config-client-prod.yml

config.yml

1
env: dev

Config Server

  1. pom.xml
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. yml
1
2
3
4
5
6
7
8
9
10
spring: 
cloud:
config:
lable: master
server:
git:
uri: https://github.com/xxx/xxx.git
username:
password:
timeout: 15 #默认是秒,因为git慢
  1. 启动类
1
@EnableConfigServer
  1. 访问

http://localhost:6001/config-client-dev.yml

http://localhost:6001/config-client-dev.properties

http://localhost:6001/config-client-dev.json

获取配置规则:根据前缀匹配

/{name}-{profiles}.properties

/{name}-{profiles}.yml

/{name}-{profiles}.json

/{label}/{name}-{profiles}.yml

name:服务名称
profile:环境名称,开发、测试、生产:dev test prod
lable:仓库分支、默认master分支

匹配原则:从前缀开始。

Config Client

  1. pom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 配置中心客户端:config-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>

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

<!-- eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  1. application.yml
1
2
server:
port: 8011
  1. bootstrap.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
#应用名称,配置文件名,此时:config-client-dev.yml
spring:
application:
name: config-client
cloud:
config:
discovery:
enabled: true
service-id: config-server # config server 的服务id
# 环境
profile: dev
# 分支
label: master
  1. 代码
1
2
@Value("${env}")
private String env;

Sleuth

如果能跟踪每个请求,中间请求经过哪些微服务,请求耗时,网络延迟,业务逻辑耗时等。我们就能更好地分析系统瓶颈、解决系统问题。因此链路跟踪很重要。

Sleuth是Spring Cloud的分布式跟踪解决方案。

  • span(跨度)

    基本工作单元。一次链路调用,创建一个span,

    span用一个64位id唯一标识。包括:id,描述,时间戳事件,spanId,span父id。

    span被启动和停止时,记录了时间信息,初始化span叫:root span,它的span id和trace id相等。

  • trace(跟踪)

    一组共享“root span”的span组成的树状结构称为trace,trace也有一个64位ID,trace中所有span共享一个trace id。类似于一颗 span 树。

  • annotation(标签)

    annotation用来记录事件的存在,其中,核心annotation用来定义请求的开始和结束。

    • CS(Client Send客户端发起请求)

      客户端发起请求描述了span开始。

    • SR(Server Received服务端接到请求)

      服务端获得请求并准备处理它。SR-CS=网络延迟。

    • SS(Server Send服务器端处理完成,并将结果发送给客户端)

      表示服务器完成请求处理,响应客户端时。SS-SR=服务器处理请求的时间。

    • CR(Client Received 客户端接受服务端信息)

      span结束的标识。客户端接收到服务器的响应。CR-CS=客户端发出请求到服务器响应的总时间。

Sleuth使用

单独使用

  1. pom.xml

每个需要监控的系统

1
2
3
4
5
<!-- 引入sleuth依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
  1. 启动,看日志
1
2
3
[api-driver,1a409c98e7a3cdbf,1a409c98e7a3cdbf,true] 

[服务名称,traceId(一条请求调用链中 唯一ID),spanID(基本的工作单元,获取数据等),是否让zipkin收集和展示此信息]

结合zipkin

zipkin是twitter开源的分布式跟踪系统。原理收集系统的时序数据,从而追踪微服务架构中系统延时等问题。还有一个友好的界面。

由4个部分组成:

Collector、Storage、Restful API、Web UI组成。

原理:

sleuth收集跟踪信息通过http请求发送给zipkin server,zipkin将跟踪信息存储,以及提供RESTful API接口,zipkin ui通过调用api进行数据展示。默认内存存储,可以用mysql,ES等存储。

操作步骤:

  1. pom.xml

每个需要监听的服务的pom中添加

1
2
3
4
5
<!-- zipkin -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
  1. yml

每个需要监听的服务yml中添加

1
2
3
4
5
6
7
spring:
#zipkin
zipkin:
base-url: http://localhost:9411/
sleuth:
sampler:
rate: 1 #采样比例1
  1. 启动zipkin
1
2
3
4
5
6
7
jar包下载:curl -sSL https://zipkin.io/quickstart.sh | bash -s

java -jar zipkin.jar

或者docker:

docker run -d -p 9411:9411 openzipkin/zipkin

访问zipkin:http://localhost:9411/zipkin/

Admin

Admin 使用

  • 服务端
  1. pom.xml
1
2
3
4
5
6
7
8
9
10
<!-- Admin 服务 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<!-- Admin 界面 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
</dependency>
  1. yml
1
2
server:
port: 6060
  1. 启动类
1
@EnableAdminServer
  • 客户端
  1. pom.xml
1
2
3
4
5
6
7
8
9
10
<!-- Admin 服务 -->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  1. yml
1
2
3
4
5
6
7
8
9
10
11
12
13
spring
boot
admin
client
url: http://localhost:8080
management:
endpoints:
web:
exposure:
include: "*" #yml加双引号,properties不用加
health:
show-details: ALWAYS ##默认是never
enabled: true
  1. 访问服务端

http://localhost:6060/