初探SpringCloud

如期而至,关于什么是 SpringCloud 这里不多说,之前的笔记已经把 SpringCloud 生态的主要技术都做了解释,但是这里对笔记进行补充一下,关于微服务、微服务架构,以及 SpringBoot 与 Cloud、Dubbo 之间微妙的关系做个简单的解释~
然后就是实践部分,先搞一个简单的 Demo 出来试试~

微服务:一种架构风格,重点在个体,拆分为单个的模块,具体的一个服务,仅关注自己负责的,比如现在流行使用 SpringBoot 来构建。
微服务架构:重点在整体,关注各个微服务之间的关系,如何治理好这些微服务,她要求各个微服务独立部署,能拥有“独立的空间(例如独立的数据库)”,现在流行使用 SpringCloud 提供的一站式解决方案构建。
微服务目前业内还没有一个准确的概念,上面的是我瞎说的 o( ̄▽ ̄)ゞ)) ̄▽ ̄)o
说到微服务架构就必须要分布式了,其中涉及的还有服务注册发现中心、服务调用方式(轻量级网络协议交互,REST、RPC)、服务监控、断路器、服务网关、分布式配置、服务跟踪、服务总线、数据流、批量任务等等。

可以看出,SpringCloud 作为全局的服务治理框架,它依赖于 SpringBoot,而与 Dubbo 的最显著区别就是 SpringCloud 使用 REST;Dubbo 使用 RPC。
使用 REST 更加灵活,并且语言无关,但是没有 RPC 的效率高,同时 RPC 也存在一些自身的问题。

当前由于 RPC 协议,注册中心元数据不匹配等问题,在面临微服务基础架构选型时,Dubbo 和 SpringCloud 只能二选一,所以才会出现两者的比较。
Dubbo 负责人表示之后会积极寻求适配到 SpringCloud 生态

如果 Dubbo 不停更 5 年的话,说不定又是另一番景象呢。

微服务搭建

这个不是重点,但是确实前提条件,所以需要先用 SpringBoot 搭出至少两个微服务,一个做服务提供,一个做服务消费,然后在这个基础上加 SpringCloud。
关于 SpringCloud 的生态圈涉及的技术太多了,看了不少视频和资料,大部分都是对主要的几个技术来做介绍,实际上也大部分都是用这些技术,其他的也就不多说了,感兴趣的可以去官方或者中文网逛逛,挺全的。

Eureka

使用 Eureka 来实现服务的注册与发现,介绍之前说过了不多说,它分为客户端和服务端,一般会新建一个项目(微服务)作为服务端,这里就需要加入 SpringCloud 的依赖管理来负责做版本仲裁,然后也需要加入 EureKa 服务端的依赖,注意是以结尾的。

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

然后在启动类使用注解开启功能,这个是 SpringCloud 的通用套路,先加依赖,然后在启动类添加 @EnableXXX 开启相关配置。

1
2
3
4
5
6
7
8
// 声明这是一个Eureka服务
@EnableEurekaServer
@SpringBootApplication
public class EurekaServer {
public static void main(String[] args) {
SpringApplication.run(EurekaServer.class, args);
}
}

最后,配置一下相关参数就可以启动测试了:

1
2
3
4
5
6
7
8
9
10
11
12
server:
# 服务端口
port: 6868
eureka:
client:
# 是否将自己注册到 Eureka 服务中,本身就是,所以无需注册
registerWithEureka: false
# 是否从Eureka中获取注册信息
fetchRegistry: false
# Eureka客户端与Eureka服务端进行交互的地址
serviceUrl:
defaultZone: http://127.0.0.1:${server.port}/eureka/

访问一下就可以看到相关的系统信息了,下面就可以把之前创建的微服务服务方注册到 Eureka 中了,如果某个微服务确定没有其他的微服务依赖它,那可以不用注册进来;
方法和之前的套路一样,加入 SpringCloud 的依赖管理,加入 Eureka 的依赖(可以是客户端也可以是服务端,推荐客户端),然后关键的地方就是配置文件的修改了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
server:
port: 8081
spring:
application:
# 指定服务名,非常重要
name: microService-item

eureka:
client:
# 是否将自己注册到 Eureka 服务中,默认为 true
registerWithEureka: true
# 是否从Eureka中获取注册信息,默认为true
fetchRegistry: true
# Eureka 客户端与 Eureka 服务端进行交互的地址
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
# 将自己的 ip 地址注册到 Eureka 服务中
instance:
prefer-ip-address: true
# 可以手动指定地址,可以可以通过表达式来获取
# ${spring.application.name}:${server.port}
ip-address: 127.0.0.1

最后,在主启动类上加入 @EnableDiscoveryClient 注解,表名这是个客户端即可。
另一个作为消费端的微服务也是一样,唯一不同的是配置文件里就不需要将自己注册到 Eureka 服务中了,也不需要设置了 instance 了。
具体使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class ItemService {
@Autowired
private RestTemplate restTemplate;

@Autowired
private DiscoveryClient discoveryClient;

public Item queryItemById(Long id) {
String serviceId = "microService-item";
List<ServiceInstance> instances = this.discoveryClient.getInstances(serviceId);
if(instances.isEmpty()){
return null;
}
// 为了演示,在这里只获取一个实例
ServiceInstance serviceInstance = instances.get(0);
String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
return this.restTemplate.getForObject("http://" + url + "/item/" + id, Item.class);
}
}

如果导入的是服务端依赖,某些版本的 SpringCloud 会响应 XML 格式的数据,而我们希望它是 JSON,破坏了 SpringMVC 的默认配置,可以在 eureka server 的依赖中排除 jackson-dataformat-xml。
对于 eureka 来说,这两个微服务都属于客户端,所以还是建议只导客户端依赖就好。
另外,你还可以开启 Eureka 的身份认证,需要导入相应的依赖,一旦开启,需要在客户端配置好凭证。

搭建集群

Eureka 的集群非常好搭建,为了避免单点故障,集群是很有必要的,只要启动多个 Eureka 服务并且让这些服务之间彼此进行注册即可实现。

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
server:
port: 6868
spring:
application:
name: microService-eureka

eureka:
client:
# 是否将自己注册到 Eureka 服务中,这次选择 true
registerWithEureka: true
# 是否从Eureka中获取注册信息
fetchRegistry: true
# Eureka 客户端与 Eureka 服务端进行交互的地址,选择另一台 Eureka 服务端
serviceUrl:
defaultZone: http://loli:[email protected]:6869/eureka/


############## 第二台服务端 ################
server:
port: 6869
spring:
application:
name: microService-eureka
eureka:
client:
# 是否将自己注册到 Eureka 服务中,这次选择 true
registerWithEureka: true
# 是否从Eureka中获取注册信息
fetchRegistry: true
# Eureka 客户端与 Eureka 服务端进行交互的地址,选择另一台 Eureka 服务端
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
security:
basic:
# 开启基于 HTTP basic 的认证
enable: true
user:
name: loli
password: pwd

他们的 defaultZone 互相指向对方,通过端口来区分,而微服务名字都是保持一致的,这样服务端的集群就搭建好了,而客户端注册的时候需要同时向这两台来注册,地址之间使用逗号分割。

自我保护机制

之前说过,Eureka 和 ZK 的一个区别,ZK 是按照 CP 原则来构建的,而 Eureka 是 AP 来做的。
默认情况下,如果 Eureka Server 在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server 将会移除该实例。但是当网络分区故障发生时,微服务与 Eureka Server 之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。
当 Eureka Server 节点在短时间内丢失过多客户端时(可能发生了网络分区故障),那么这个节点就会进入自我保护模式。一旦进入该模式,Eureka Server 就会保护服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。当网络故障恢复后,该 Eureka Server 节点会自动退出自我保护模式。
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,但是在保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败,对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。
我们在单机测试的时候很容易满足心跳失败比例在 15 分钟之内低于 85%,这个时候就会触发 Eureka 的保护机制,一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,此时我们可以使用eureka.server.enable-self-preservation=false来关闭保护机制,这样可以确保注册中心中不可用的实例被及时的剔除(不推荐)。

常用配置

这里再以 properties 为例:

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
#================================服务端==============================
#应用名称
spring.application.name=eureka-server-v1
#应用端口
server.port=7000
#=======eureka中心配置=======
#主机名
eureka.instance.hostname=localhost
# 注册时显示ip
#eureka.instance.prefer-ip-address=true
#是否注册为服务
eureka.client.register-with-eureka=false
#是否检索服务
eureka.client.fetch-registry=false
#eureka默认空间的地址
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/
#关闭自我保护(生产时打开该选项)
eureka.server.enable-self-preservation=false
#扫描失效服务的间隔时间(缺省为60*1000ms)
eureka.server.eviction-interval-timer-in-ms=5000

#================================客户端==============================
#端口号
server.port=8081
#服务名
spring.application.name=produce-service-v1
#=======eureka配置========
#注册到eureka中心,获取到配置服务
eureka.client.service-url.defaultZone=http://localhost:7000/eureka/
#设置实例的ID为ip:port
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port}
#========续约配置=========
# 心跳时间,即服务续约间隔时间(缺省为30s)
eureka.instance.lease-renewal-interval-in-seconds=5
# 发呆时间,即服务续约到期时间(缺省为90s)
eureka.instance.lease-expiration-duration-in-seconds=10
# 开启健康检查(依赖spring-boot-starter-actuator)
eureka.client.healthcheck.enabled=true

这些应该够用了吧…..

Ribbon

使用 Ribbon 实现客户端负载均衡,说到负载均衡,可以简单分为两类:

  • 集中式
    消费方和服务方中间使用独立的 LB 设施,例如 F5、nginx 这类就是。
  • 进程内
    一般集成到消费方,Ribbon 就是如此。

使用前的老一套不说了,导入依赖(eureka-server 中已经包含了 Ribbon 的依赖),在主启动类使用 @RibbonClient (简单使用可以不加)进行配置工具类。
然后,在 Config 创建 RestTemplate 对象上设置 @LoadBalanced 注解就表示已经启用负载均衡啦!

开启后,在执行请求前会经过 org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor 这个拦截器,并且通过 org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient 的时候会根据 serverId 查找服务地址,然后在去做真正的请求;
所以 RestTemplate 请求的 URL 可以直接使用服务名,而不需要手动获取地址了。

Spring Cloud Ribbon 虽然只是一个工具类框架,它不像服务注册中心、 配置中心、 API 网关那样需要独立部署, 但是它几乎存在于每一个Spring Cloud 构建的微服务和基础设施中。 因为微服务间的调用,API 网关的请求转发等内容实际上都是通过Ribbon 来实现的,包括后续我们将要介绍的 Feign, 它也是基于 Ribbon实现的工具。
SpringCloud 服务调用的方式一般就是两种:

  • Ribbon + RestTemplate
  • Feign

自定义

Ribbon 自带了七中负载均衡的算法。默认轮询,当想选择其他算法时,在配置类里使用 @Bean 声明需要的官方提供的 IRule 其他实现类即可,这是 SpringBoot 自动配置的知识了。
也正是因为这个原因,如果我们想自己实现负载均衡算法,除了需要在启动类使用 @RibbonClient 注解指定服务名和负载均衡算法具体实现类(需要在 @Configuration 下)外,还要求这个类不能在包扫描范围内
首先,可以自定义负载均衡规则,可以在配置文件里设置也可以使用注解:

1
2
3
microService-consumer:  
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

在启动类上设置:

1
2
3
4
5
@SpringBootApplication
@EnableEurekaClient
// 在启动该微服务的时候就能去加载我们的自定义 Ribbon 配置类,从而使配置生效
@RibbonClient(name="MICROSERVICECLOUD-DEPT",configuration=MySelfRule.class)
public class Main{}

然后就是实现自己的负载均衡算法:

1
2
3
4
5
6
7
8
9
10
@Configuration
public class MySelfRule{
@Bean
public IRule myRule(){
// 随机使用 RoundRobinRule();
return new TestLB();
}
}

public class TestLB extends AbstractLoadBalancerRule{}

一般来说,默认的轮询就已经够用了。

Feign

使用 Feign 实现声明式的 REST 调用,是为了简化 RestTemplate 的使用,让我们的代码更优雅,添加依赖就不多说了,主启动类加上 @EnableFeignClients 注解,这也是加在客户端(消费端)的。
然后声明一个接口,然后可以像写 SpringMVC 哪样来定义这个接口的方法啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明这是一个Feign客户端,并且指明服务 id
@FeignClient(value = "microService-item")
public interface ItemFeignClient {
// 这里定义了类似于SpringMVC用法的方法,就可以进行RESTful的调用了
@RequestMapping(value = "/item/{id}", method = RequestMethod.GET)
public Item queryItemById(@PathVariable("id") Long id);

// 多参数构造-1
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get1(@RequestParam("id") Long id, @RequestParam("username") String username);
// 多参数构造-2
@RequestMapping(value = "/get", method = RequestMethod.GET)
public User get2(@RequestParam Map<String, Object> map);
}

Feign 使用,简单说:创建一个接口(使用 @FeignClient 标注),在上面使用 SpringMVC 注解即可;相当于封装了 RestClient。
只需要定义接口就可以完成调用,很显然是用了动态代理,在 FeignClient 中的定义方法以及使用了 SpringMVC 的注解,Feign 就会根据注解中的内容生成对应的 URL,然后基于 Ribbon 的负载均衡去调用 REST 服务

Hystrix

使用 Hystrix 的熔断机制保护应用,在微服务架构中通常会有多个服务层调用,基础服务的故障可能会导致级联故障,进而造成整个系统不可用的情况,这种现象被称为服务雪崩效应。
它是在服务端(服务提供方)使用的技术,使用起来非常简单,除了必要的依赖、在主启动类使用 @EnableHystrix 注解开启功能,只需要在 Service 的方法上加上 @HystrixCommand(fallbackMethod = "fallbackMethod") 注解就可以了,其中 fallbackMethod 是具有类似方法签名的备用方法,当此方法的调用不可用时就会走这个备用方法。
但是这里会有一个问题,如果一个核心方法对应一个备用方法,很容易就会造成方法膨胀,耦合性还很高,这样也太不优雅了,所以可以使用 AOP 的思想来解决这个问题嘛~其实这样可以做服务降级,配合 Feign 来使用:

1
2
3
4
5
6
7
// 该接口下哪个方法抛异常,会调 fallbackFactory
@FeignClient(value = "MICROSERVICECLOUD-DEPT",fallbackFactory=MyFallbackFactory.class)
public interface DeptClientService{}


@Component // 不要忘记添加
public class MyFallbackFactory implements FallbackFactory<DeptClientService>{}

这样就可以达到容错的目的了,说到这里,不得不提涉及的两个概念,服务降级和服务熔断。

  • 服务熔断
    主逻辑因短期内多次失败(也有可能是由于自我保护机制),而被暂时性的忽略,不再尝试使用,这种叫熔断。

  • 服务降级:
    主逻辑失败采用备用逻辑的过程叫做降级(也就是服务降级发生在服务熔断之后)。
    当整体资源快消耗殆尽的时候(例如内存、CPU等),将某些服务临时关掉一大部分以释放资源(一般留下一个来维持运行返回给用户友好的提示),减轻主模块的压力,待资源恢复可用再开启。

看上去熔断和降级是非常相似的,都是调用失败后调用备用方法;但是他们的着重点是不同的。
不管是服务降级还是熔断,他们的目的都是为了保证程序的健壮性,对于一些非核心服务,如果出现大量的异常,可以通过技术手段,对服务进行降级并提供有损服务,保证服务的柔性可用,避免引起雪崩效应。
开启熔断之后,如何实现自动恢复?
每隔一段时间,会释放一个请求到服务端进行探测,如果后端服务已经恢复,则自动恢复。

此外,Hystrix 还具有服务监控的功能,它提供了准实时的调用监控 HystrixDashboard,是可视化界面,可以进行实时监测。
这个监控使用非常简单,新建一个微服务,加入相关依赖,在启动类上加上 @EnableHystrixDashboard 就可以用啦!

SpringCloudZuul

因为之前笔记有,介绍咱们还是略过,简单理解为它相当于是个过滤器或者拦截器就好了,继续新建一个微服务,导入相关依赖,在主启动类加入 @EnableZuulProxy 注解。
服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供 REST API 的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能,将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。
从上面的介绍可以看出,它还需要 Eureka 等依赖加强功能,让后面的微服务专心做自己的事情。

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
server:
port: 6677
spring:
application:
name: microservice-api-gateway

zuul:
ignored-services: "*"
prefix: /atguigu
routes:
# 名字可以随意起
item-service:
# 配置请求 URL 的请求规则
path: /item-service/**
# 真正的微服务地址,可以使用 url ,也可以指定 Eureka 注册中心中的服务 id
# url: http://127.0.0.1:8081
serviceId: microservice-item

eureka:
client:
# 是否注册自己,默认为true
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
instance:
# 将自己的ip地址注册到Eureka服务中
prefer-ip-address: true

info:
app.name: microcloud
company.name: www.bfchengnuo.com
build.artifactId: $project.artifactId$
build.version: $project.version$

同时,Zuul 还支持正则匹配、拦截器、路由前缀等功能,由于是入门,这里就不多说了。

SpringCloudConfig

使用 SpringCloudConfig 统一管理微服务的配置,可以让我们统一管理配置文件,以及实时同步更新,并不需要重新启动应用程序,默认使用 Git 存储配置文件内容。
同样,它也分为客户端和服务端,服务端可以新建一个微服务,加入相应的依赖,在主启动类加上 @EnableConfigServer 注解就可以使用了,当然还是要写一点配置的。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 6688
spring:
application:
name: microservice-config-server
cloud:
config:
server:
git:
uri: http://172.16.55.138:10080/bfchengnuo/config-server.git
#username: loli
#password: 123456

还是推荐使用 SSH 密钥认证的方式,这样就可以通过 SpringCloudConfig 直接访问 Git 上的配置文件,同时它支持 properties 和 yml 的互相转换,通过请求地址的后缀实现。
客户端的使用也是类似,导入没有 server 后缀的依赖,另外为了避免地址的硬编码,可以将服务端使用 @EnableDiscoveryClient 也注册到 Eureka 中,然后在客户端使用服务名来访问。
需要新建配置文件:bootstrap.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
config:
# 对应的配置服务中的应用名称
name: microservice
# uri: http://127.0.0.1:6869/
# 对应配置服务中的 {profile}
profile: dev
label: master
discovery:
# 启用发现服务功能
enabled: true
# 指定服务名称
service-id: microservice-config-server

因为 bootstrap.yml 优先与 application.yml 加载(先读取了配置才能启动啊),所以把发现服务配置在 bootstrap 里。
然后你可以使用 @Value 来注入配置,就是和配置文件在本地是一样使用。
为了能够让配置自动更新,还需要为 Config Client 添加 refresh 支持,就是导入一个 spring-boot-starter-actuator 依赖,然后在配置类对应的实体类上加上 @RefreshScope 注解(测试可以临时把 actuator 安全认证关掉 management.security.enabled)。
然后就可以使用 post 请求 /refresh 地址来更新配置内容了。
更新后还需要手动访问下这个地址未免太麻烦了,所以,可以借助 git 的 webhook(web 钩子)实现自动更新。

SpringCloudBus

消息总线 Spring Cloud Bus 也是很重要的,例如它可以更优雅的完成自动更新配置文件,简单的你可以理解为它就是个消息的交换机,所有的微服务模块都监听它,所以可以实现配置、缓存等的更新。
以 RabbitMQ 为例,就先在 ConfigServer 中来加入吧,导入 spring-cloud-starter-bus-amqp 依赖,在 application.yml 添加 rabbitmq 的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
eureka:
client:
serviceUrl:
defaultZone: http://127.0.0.1:6868/eureka/
spring:
cloud:
config:
name: microservice
# uri: http://127.0.0.1:6869/
profile: dev
label: master
discovery:
# 启用发现服务功能
enabled: true
service-id: microservice-config-server
# RabbitMQ 相关的配置
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest

然后,会自动注册一个 /bus/refresh 的请求,这个请求就是由消息总线来处理的,那么我们可以设置当配置更新后 web 钩子向统一配置服务器发送 /bus/refresh 请求,然后配置服务器会将消息发送到 springCloudBus 的交换机,由于其他微服务的队列也绑定到交换机,所以也就都获取到了更新的通知,然后去 ConfigServer 获取最新的数据。
需要注意,其他的微服务(客户端)这个 bus 配置是要写在 bootstrap.yml 中的,保证优先加载。

参考

https://www.jianshu.com/p/3463571febc2

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~