【Spring Cloud】简介

近期计划将个人项目进行微服务化,在比较了两大主流微服务框架:Spring Cloud和Dubbo后,鉴于Spring家族强大的生态与Spring Cloud的迅速发展,最终选择了Spring Cloud作为微服务整治的大框架。工欲善其事,必先利其器,那么我们先来大体了解一下Spring Cloud究竟为何物。

Spring Cloud概述

Spring Cloud provides tools for developers to quickly build some of the common patterns in distributed systems (e.g. configuration management, service discovery, circuit breakers, intelligent routing, micro-proxy, control bus, one-time tokens, global locks, leadership election, distributed sessions, cluster state). Coordination of distributed systems leads to boiler plate patterns, and using Spring Cloud developers can quickly stand up services and applications that implement those patterns. They will work well in any distributed environment, including the developer’s own laptop, bare metal data centres, and managed platforms such as Cloud Foundry.

摘自官网的介绍,总体来说,Spring Cloud是快速构建分布式系统的一站式解决方案,它为开发人员提供了一系列简单、易使用的开发工具,比如:配置管理、服务发现、断路器、智能路由、微代理、控制总线等。Spring Cloud是基于Spring Boot应用之上,开发者可以使用简单的注解或配置即可使用Spring Cloud的强大功能。

服务注册与发现——Eureka

一般来说,Spring Cloud最常使用的服务中心就是Spring Cloud Netflix提供的Eureka组件,它可以提供一个声明式配置的Eureka Server作为服务注册中心,也可以提供Eureka Client实例作为服务的提供者。当然还有其他可以使用的服务中心,如Zookeeper和Consul。

Eureka Server

它的使用非常简单,在spring boot启动类上添加@EnableEurekaServer注解即可定义一个服务注册中心。

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

同时,在配置文件application.yml中做简单的配置即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 8761 #服务端口

spring:
application:
name: eurka-server #服务应用名称

eureka:
instance:
hostname: localhost
client:
registerWithEureka: false #是否将自己注册到Eureka Server,默认为true
fetchRegistry: false #是否从Eureka Server获取注册信息,默认为true
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #服务注册的 URL

在默认情况下,Eureka Server也是一个Eureka Client,若不配置eureka.client.registerWithEureka项,它会将自己注册为一个Client;每个Eureka Client会从Server获取服务注册表信息,并将其缓存在本地,Client使用这个信息查找其他的服务,所以通过eureka.client.fetchRegistry:false的配置来表明该服务是一个Server。

启动工程后浏览器访问http://localhost:8671即可看到服务注册中心的页面。

Eureka Client

客户端通常是服务提供者或服务消费者,当它向Eureka Server注册时,它会提供自身的元数据(meta-data),比如IP地址、端口、运行状态指示符URL和主页等。

Eureka Client默认情况下会每隔30秒向Server发送一次心跳来续约,也就是表明自己还在运行状态,没有出现异常问题。如果Eureka Server超过90秒没有收到Client等续约,那么它就会将该客户端实例从注册表中删除。

上文简单提到过Client会从Server获取注册表信息,使用这个信息来查找其他服务。该注册表信息会每隔30秒更新一次,Client每次获取到的注册信息可能与它的缓存信息不同,如果由于某种原因导致注册表信息不能及时匹配,Client会重新获取整个注册表信息。这个注册表本质上是一个ConcurrentHashMap,而且是直接存储在内存中,所以对于注册表的操作都是发生在内存中,而且在Eureka Server端对于注册表存在多级缓存机制,这也保证了在高并发的情况下服务的响应速度。

使用Eureka Client也非常简单。

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

配置文件中只需要配置服务名和注册中心的地址即可。

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8762

spring:
application:
name: service-hi #服务名称

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

负载均衡客户端——Ribbon

Ribbon也是Netflix下的一个组件,是一个提供了负载均衡功能的客户端。如果一个服务启动了多个实例,那么可以通过Ribbon提供的负载均衡策略将请求发送给不同的服务实例。

Ribbon作为服务的消费者,本质上也是一个Eureka客户端,同样需要向服务中心进行注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@EnableEurekaClient
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceRibbonApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceRibbonApplication.class, args);
}

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

不同的是Ribbon会向应用注入一个RestTemplate的Bean,并开启负载均衡功能。

Spring Cloud的服务调用是基于REST的方式,RestTemplate是Spring提供的用于访问Rest服务的客户端,在我们的例子中,使用restTemplate.getForObject方法发送HTTP请求。

1
2
3
4
5
6
7
8
9
@Service
public class HelloService {
@Autowired
RestTemplate restTemplate;

public String hiService(String name) {
return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}
}

Ribbon常见的负载均衡策略有:随机(Random)、轮询(RoundRobin)、一致性哈希(ConsistentHash)、哈希(Hash)、加权(Weighted)。

其中,RoundRobin是负载均衡器使用的默认负载均衡策略。

断路器——Hystrix

在微服务架构中,为了保证服务的高可用性,单个服务通常会集群部署。但是可能由于网络或自身原因,这个服务不能保证100%的可用性。一个业务流程往往需要调用多个微服务,假设其中的一个微服务出现问题,假设我们有100个线程,那么调用这个服务就会造成线程阻塞,此时在高并发场景下若有大量的请求涌入,容器的线程资源将很快就被消耗完,导致服务瘫痪。这必然会影响整个微服务架构,对系统造成严重影响,这就是服务故障的“雪崩”效应。为了解决这个问题,业界提出了断路器模型——Hystrix,负责服务的隔离、熔断和降级。

Hystrix会将线程资源按照服务进行隔离,比如100个线程资源,它会分配30个处理对服务A的请求、30个处理对 服务B的请求等等。如果处理A请求的线程都用光了且在一定时间内没有响应,那么Hystrix就认为服务A出现了故障,Hystrix就会调用一个fallback方法,这就是降级,然后将fallback方法的返回值返回,这就是熔断。

所谓的断路器实际上就是一种错误容忍机制,它是针对客户端的,为客户端微服务添加一个fallback方法,当前微服务出现故障的时候,就会自动调用这个方法,并将结果返回给调用方,这会有效避免故障对整个微服务系统的影响。

pom文件中引入对应的依赖即可使用。

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

在客户端的方法上加上@HystrixCommand(fallbackMethod = "hiError")注解,fallbackMethod参数指定fallback方法,这个方法返回相同类型的结果即可。例子如下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class HelloService {
@Autowired
RestTemplate restTemplate;

@HystrixCommand(fallbackMethod = "hiError")
public String hiService(String name) {
return restTemplate.getForObject("http://SERVICE-HI/hi?name="+name,String.class);
}

public String hiError(String name){
return "hi," + name + ",sorry, error!";
}
}

HTTP客户端——Feign

我们使用Ribbon作为客户端请求服务时是使用RestTemplate发送请求,这种方法需要每次构造URL并返回值,这种写法似乎有些繁琐。那么接下来我们就介绍另一种客户端——Feign。

Feign是Netflix开发的一个声明式、模版化的伪HTTP客户端,它可以使微服务之间的调用变得简单。Feign帮助我们定义和实现依赖服务接口的定义,在Spring Cloud Feign的实现下,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,简化了在使用Ribbon时自行封装服务调用客户端的开发量。Feign具备可插拔的注解支持,支持Feign注解、JAX-RS注解和Spring MVC的注解。

Feign默认集成了Ribbon和Hystrix,所以也具有负载均衡和熔断的功能。

pom文件中引入openfeign的依赖

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

程序的启动类加上@EnableFeignClients注解开启Feign的功能。

定义一个Feign接口,并声明要调用的服务名。对于每个方法,可以声明要调用的这个服务的具体接口。

1
2
3
4
5
@FeignClient(value = "service-hi")
public interface SchedualServiceHi {
@RequestMapping(value = "/hi", method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam(value = "name") String name);
}

那么Feign为什么能够简化服务的调用呢?我们应该明白,调用服务最终一定通过完整的http请求完成的,也就是说Feign帮助我们完成了构建http请求的任务,不需要我们使用Ribbon一样手动构建。Feign背后的关键机制在于使用了动态代理。

  • 对于某个添加了@FeignClient注解的接口,Feign就会对这个接口创建一个动态代理
  • 如果有请求调用这个接口,那么本质上就是调用这个动态代理
  • Feign会根据@RequestMapping的信息在动态代理中构建出完成的请求地址,然后针对这个地址发出请求。

路由网关——Zuul

在一个业务功能完整的微服务体系中,存在大量的单一微服务,在高并发的环境下,每个微服务往往都会部署多个实例,这些微服务和实例都具有唯一的URL地址。客户端通过HTTP的方式访问服务,那么在客户端和服务之间必须存在一个统一的出入口,这就是我们一般说的API Gateway(API网关)所扮演的角色。有了API网关,各个API服务提供团队可以专注于自己的业务逻辑处理,而API网关则负责安全、流量、路由等问题。

而Zuul就是Spring Cloud全家桶中的微服务API网关,所有从设备或网站来的请求都会经过Zuul到达后端的Netflix应用程序,它所实现的功能如下:

  • 认证和安全 识别每个需要认证的资源,拒绝不符合要求的请求。
  • 性能监测 在服务边界追踪并统计数据,提供精确的生产视图。
  • 动态路由 根据需要将请求动态路由到后端集群。
  • 压力测试 逐渐增加对集群的流量以了解其性能。
  • 负载卸载 预先为每种类型的请求分配容量,当请求超过容量时自动丢弃。
  • 静态资源处理 直接在边界返回某些响应。

总的来说,Zuul最核心的功能就是路由过滤。下面我们具体来看一下。

Zuul的路由功能

一句话概括Zuul的路由转发功能就是:把服务调用方的请求映射到对应的微服务实例。

那么它的路由功能怎么实现呢?

首先服务的启动类要加上@EnableZuulProxy注解,表示这个服务使用Zuul作为API网关。

其次在配置文件中对请求路径和微服务名称做一个映射。这样凡是以/api-a/开头的请求都会被转发给service-ribbon微服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8769
spring:
application:
name: service-zuul
zuul:
routes:
api-a:
path: /api-a/**
serviceId: service-ribbon

Zuul的过滤功能

所有的请求都要经过Zuul,那么自然可以通过添加过滤器对请求进行过滤,这样就能实现限流、灰度发布、权限控制等功能。

Zuul Filter有以下几个特征:

  • Type:表示该过滤器等类型,在Zuul中定义了四种不同生命周期的过滤器类型,它们表示在路由的不同阶段进行过滤功能,有:pre(路由之前)、routing(路由之时)、post(路由之后)和error(发送错误调用)。
  • Execution Order:表示相同Type的Filter的执行顺序
  • Criteria:执行条件,可以写判断逻辑,是否要过滤
  • Action:执行体

Zuul提供了动态读取、编译和执行Filter的框架。各个Filter之间没有直接联系,但是都通过RequestContext共享一些状态数据。

尽管Zuul支持任何基于JVM的语言,但是过滤器目前是用Groovy编写的。每个过滤器的源代码被写入到Zuul服务器上的一组指定的目录中,这些目录将被定期轮询检查是否更新。Zuul会读取已更新的过滤器,动态编译到正在运行的服务器中,并在后续请求中调用。

四种Filter Type:

  • PRE Filter:在请求路由到目标之前执行。一般用于请求认证、负载均衡和日志记录。
  • ROUTING Filter:处理目标请求。这里使用Apache HttpClient或Netflix Ribbon构造对目标的HTTP请求。
  • POST Filter:在目标请求返回后执行。一般会在此步骤添加响应头、收集统计和性能数据等。
  • ERROR Filter:整个流程某块出错时执行。

例子:

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
@Component
public class MyFilter extends ZuulFilter {
private static Logger log = LoggerFactory.getLogger(MyFilter.class);

/**
* 返回一个字符串代表过滤器的类型
*
* @return
*/
@Override
public String filterType() {
return "pre";
}

/**
* 过滤器的执行顺序
*
* @return
*/
@Override
public int filterOrder() {
return 0;
}

/**
* 执行过滤的判断逻辑
*
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}

/**
* 过滤器的具体逻辑
*
* @return
*/
@Override
public Object run(){
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
log.info(String.format("%s >>> %s", request.getMethod(), request.getRequestURL().toString()));
Object accessToken = request.getParameter("token");
if(accessToken == null) {
log.warn("token is empty");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("token is empty");
}catch (Exception e){
e.printStackTrace();
}
return null;
}
log.info("ok");
return null;
}
}

分布式配置中心——Spring Cloud Config

在分布式系统中,由于服务和实例的数量巨多,为了方便众多服务的配置文件统一管理,实时更新,所以需要分布式配置中心组件。在Spring Cloud中,使用了分布式配置中心组件Spring Cloud Config,它为分布式系统中的外部化配置提供服务器和客户端支持。使用Config服务器,可以在中心位置管理所有环境中应用程序的属性配置。

也就是说Spring Cloud Config可以将所有微服务的配置文件放到统一的地方进行管理(Git或SVN)。

在Spring Cloud Config组件中,有两个角色,config server和config client。它们的功能比较简单,可以用下图简单说明:

config server从远程拉取配置信息,可以做如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: config-server
cloud:
config:
server:
git:
uri: https://github.com/forezp/SpringcloudConfig/
search-paths: respo
label: master

server:
port: 8888

而confg client会从config server获取所需要的配置项,只需要做简单的配置

1
2
3
4
5
6
7
8
9
10
11
spring:
application:
name: config-client
cloud:
config:
label: master # 指明远程仓库的分支
profile: dev # 指定配置文件环境,dev、test和pro(开发、测试和生产环境)
uri: http://localhost:8888/ # 指明配置服务中心的地址

server:
port: 8881

就可以像我们平常使用本地配置文件的配置项一样进行开发。

1
2
3
4
5
6
@Value("${foo}")
String foo;
@RequestMapping(value = "/hi")
public String hi(){
return foo;
}

消息总线——Spring Cloud Bus

Spring Cloud Bus将分布式的节点用轻量级的消息代理连接起来。它可以用于广播配置文件的更改或者服务之间的配置,也可以用于监控。比如承接上一节的config,可以使用消息总线实现通知微服务架构的配置文件的更改。

比如当git文件更改时,通过POST请求向端口为8882的config client发送/bus/refresh请求,此时8882端口会发送一个消息,由消息总线向其他服务传递,从而使整个微服务集群都达到更新配置文件的目的。

网关——Spring Cloud Gateway

Spring Gateway是官方推出的第二代网关,用来取代Netflix Zuul,并且几乎包含了Zuul所有的功能。Spring Cloud Gateway主要有两个重要的组成部分:predicatefilter

上面的图来自官网,描述了Gateway的工作机制。Gateway Handler Mapping接收外部请求并匹配一个路由,有点类似Spring MVC的Handler Mapping,这是通过predicate(断言)实现的;把匹配的请求发送给Gateway Web Handler,然后发给一个Filter chain,这些filter可以在代理请求发送之前或响应之前分别发挥作用。

0%