写给小白看的Spring Cloud入门教程

10,364 阅读20分钟

公司目前用的是Spring Cloud ,感觉自己对Spring Cloud了解的还不够系统,于是打算系统的整理一下自己对Spring Cloud的理解。边学边实践,我自己做了个Spring Cloud 的案例,考虑到访问GitHub比较慢,我就放码云上了。地址是 gitee.com/cxk6013/spr… 里面我都打了比较详细的注释,看完不妨自己尝试做一下。

微服务架构简介

当软件在慢慢的变复杂,开发和运维成本也在随之上升,这也是在软件开发发展过程中,改变或升级架构的一个原因,降低软件的复杂度,而降低软件复杂度的一种通用方式就是拆,将一个软件拆成若干个模块,这样复杂度就被均摊了,好钢也能用在刀刃上,什么意思呢? 以服务端为例,所有模块的请求量不是相等的,有的模块的访问量必然会很高,以电商为例,支付服务或者说是模块在平时访问量是相对平稳的,但是在双十一这一天,访问量就会急速的上升,这个时候企业常常就会对支付服务进行扩容,多给支付服务一些资源,假若不拆,这些服务整体被打包在一起的话,我给的资源就无法只被支付服务所享用。

微服务架构成为新常态的原因就是很好的降低了软件的复杂度,能够充分的利用资源,提升开发速度。将服务拆的足够泾渭分明,在某个服务访问量上升的时候,我就可以只给这个服务扩容,这就是充分的利用资源,除此之外,也能做到快速集成,我在重新集成这个服务的时候,就不会影响到其他服务,也就是解耦合。但这并不代表微服务是完美无缺的,管理和运维微服务 相对于过去就变得有些复杂起来了,这也是Docker和K8s流行的原因。

总结一下,这里讨论的微服务指代的是微服务架构,粗略的讲,我们可以这么理解微服务架构, 根据业务拆分软件模块,每个模块单独运行,每个模块本身是单体。

再议分布式和集群

"分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像是单个相关系统"。 《分布式系统原理和范型》 分布式系统是一组计算机,通过网络相互连接传递消息与通信后并协调它们的行为而形成的系统。组件之间彼此进行交互以实现一个共同的目标。 《维基百科》

《且谈软件架构》一文中我已经讨论过分布式和集群了,但是还不充分,因为技术在一直不断的朝前走,资源的利用率在不断的提高。上述的分布式强调的是一组计算机,是的,鸡蛋不能都放在一个篮子里,我之前的想法是假如我有一台计算机,资源比较丰富,使用Docker完成部署,是不是也算一种分布式,并不能算,因为假如这台计算机因为某些原因不能提供服务了怎么办? 如果将进程当做一个鸡蛋的话,计算机就是装鸡蛋的篮子,然后篮子坏掉了。我想分布式应该是在强调一组计算机应该就是在强调这种容灾能力吧,努力将损失降到最低。

那么如何在是分布式系统的情况下,某台服务器不能再提供服务之后,我们的系统还能正常提供服务呢?就好像这台计算机没有坏掉呢? 那就是集群,就是相同的程序找了几台服务器又部署了几次。一台坏掉,我还有另一个。你要是都坏掉? 应该不会这么点背吧。

总结一下:

  • 如果若干个程序如果部署在若干台不同的计算机上,它们通过网络相互协作最终完成一个服务,那么这些程序组成的系统就可以称为分布式系统。
  • 如果是相同的程序,在本机部署了多次,有了Docker后这很轻松,这就叫集中式集群。
  • 如果是在不同的计算机上部署了多次,我们可以称之为分布式集群。

都是集群,都提高了程序的访问量,分布式集群相对于集中式集群,多了一个容灾能力,但是也引入了新的问题。 这就是我们下文要讨论的CAP定理。

你需要知道的CAP定理

CAP原则又称CAP定理,指的是在一个分布式系统中,一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance)。CAP 原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。

我们先来解释P,因为P看起来比较高端的样子。上文我们讨论过分布式系统强调不同的计算机,不同的程序部署在若干台不同的计算机上,通过网络来相互协作最终完成一个服务。我们可以认为组成分布式系统的这些计算机当做一个节点。

分区容错就是在说节点之间的通信可能会失败,当我们只有一台数据库的时候,而某天分布式系统的某个节点因为网络故障无法访问到数据库,那么这个时候分区就是无法容错的。

为了提升分区容错性,这个时候我们又引入了一个数据库,这样分区容错性就进一步的提高了,但是这个时候又出现了新的问题,即如何保持两个数据库的数据的一致性,要保证一致的话,完成写操作的充分必要条件就是等待全部节点写入成功。

每次写操作等待所有节点写入完成,又会引入降低可用性,因为向所有节点写入数据,可能会写入失败或者写入时间过长,因为你无法保证网络是绝对可靠的。

总结: 为了提升分区容错性,我们将相同的服务多次部署,每个服务相同,部署的越多,分区容错性越高,然后这些服务之间的数据同步就是一个新的问题,也就是一致性。如果我们硬要保持强一致性,那么可用性就会降低。

从上面的讨论我们可以得出,CAP是无法兼得的,我们只能选择其二,我们可以选择CP,AP。但是请注意不是要了AP就不要C了,我们可以放松对一致性的要求。

在上面的讨论中C是强一致性,即每个节点的数据都是最新版本,我们可以放松对一致性的要求,也就是降级,一致性也有不同的级别:

  • 弱一致性: 每个节点的数据可以不全是最新版本
  • 最终一致性: 放宽对时间的要求,经过一段时间,各个节点的数据才是全是最新的。

举例: 写操作不等着全部节点写入完成就返回响应,等待一些时间完成各个节点的同步。

对可用性的要求,对于一个满足可用性的系统,每一个非故障节点必须对每一个请求作出响应。我们一般衡量一个系统的可用性的时候,都是通过通过停机时间来计算的:

参考的博文:

Spring Cloud与Spring Boot

上文我们提到了微服务架构,根据业务来拆分软件模块,我们用Spring boot来构建服务,用Spring Cloud来治理服务。 这个治理是什么意思呢?我们来思考一下,软件拆分模块,各模块独立运行之后引入的问题:

  • 怎么解决服务之间的相互调用
  • 假如在服务之间的相互调用中,某个服务因为网络或其他原因导致很久没有返回结果,应该怎么处理
  • 各个模块的配置如何统一的管理
  • 如何屏蔽我的真实访问路径,就是在我的真实访问路径包装一层,类似于虚拟手机号

Spring Boot 开发 Spring Cloud三部曲

Spring Boot的定位

之前因为找资料加了一些群,会看到一些人会问,现在学Spring框架的技术,应该怎么学起,有人就直接说,不要学Spring,Spring MVC了,直接从Spring Boot开始学吧,说Spring Spring MVC都已经落后了。这里我们理一下Spring boot和Spring MVC Spring 之间的关系哈。

因为我们在开发Spring 、 SpringMVC的时候配置是比较多的,做过SSM整合的都懂(SSM指 Spring、SpringMVC、MyBatis),某些程度上我们可以认为这些配置是固定的,就是谁注入谁之类的,那么Spring Boot的大致思想就是我帮你把这些配置都配了,加速你的开发。除此之外,一些关键的依赖之间的版本不是也比较难搞吗? Spring Boot将一些固定的依赖做到了一起,也就是starter,这就进一步的加速了开发。

但是Spring Boot的底层还是Spring,SpringMVC。不存在谁比谁先进的问题,只不过用Spring Boot开发更快而已,Spring Boot是一个稍微偏大的主题,这里限于篇幅,不做过多介绍,有兴趣的同学可以下去查查资料。

Spring Boot 和Spring Cloud的关系

Spring Cloud系列的组件都是用Spring boot来构建的,所以开发起来同样很快。 但是一旦出了错,如果你对Spring Spring MVC掌握的不深,你就很懵逼了。

Spring Boot也是一个庞大的项目,下面包含了许多starter(不懂这些starter的可以再去做下功课,不懂下面的你照样也可以看懂) Spring Cloud是一个庞大的项目,下面包含了各个组件。 所以我们一般在父工程中统一管理Spring Boot和Spring Cloud的版本。

Spring Boot开发的常规操作

  • 引入依赖,我们一般用maven做
  • 写配置文件
  • 在启动类上打上注解

本文介绍的Spring Cloud组件

这里选取的是Spring Cloud Netflix家族系列:

  • Eureka 注册中心 主管服务的发现与相互调用
  • Feign 远程调用者
  • Ribbon 负载均衡器
  • zuul 网关
  • config 配置中心
  • Hystrix 熔断器

注册中心 Eureka

假如我现在有A、B、C三个服务,那我此时A服务希望调用B服务,我是直接通过显式的URL来调用B吗? 肯定不是,我们事实上可以从网络通信去找到对应的设计思路,想象一下过去的电话,假设A要和B打电话,也不是从A家里面架一条电话线到B家,假设A朋友比较多,A家的电话线就装不下了,这仅仅是两家的通信,假设我们不断的引入新用户,这种从需要相互通信的用户之间直接接电话线的思想,很快会让我们的线路变得难以管理,像下面这样:

哪天D搬家了,这个线路也得随时跟着D走,迁移成本太高了,事实上我们要接电话线和网线都是从自家引一条网线,接入电信运营商的网络,中间会有若干中间层,通过这些中间层来实现通信,服务调用也是对应的设计思路,A服务直接通过显式的URL(IP+端口+接口名: 192.168.3.1: 8080/buy/)调用B服务,哪天B服务换个地址,你A服务也要跟着动,这就是注册中心的用处之一:

这也就是Eureka注册中心的用处,注册中心上保存的是服务名,已注册的服务可以通过Eureka来获取其他服务的服务名,通过服务名来完成远程调用,这样某个服务换了IP也不用在意,你名字不变就行。 Eureka 分成server 和 client ,注册中心是server , client需要向注册中心的服务。 通过Spring Cloud和Spring Boot来构建微服务非常简单,通常分为三步:

  • 引入依赖 (我默认你已经学习过Maven了, 如果没学习过,请参看我的这篇博客: Maven学习笔记)
  <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
  • 写配置文件
server:
  port: 9991
eureka:
  client:
    service-url:
      defaultZone: http://localhost:${server.port}/eureka/
    register-with-eureka: false 
    fetch-registry: false

defaultZone是注册中心的地址,register-with-eureka是否让Eureka自己注册自己。 fetch-registry Eureka是否自己获取其他服务的地址。

  • 启动类上打上对应的注解
EnableEurekaServer // 该注解标识该服务是服务注册中心。
SpringBootApplication
public class MicroEureka {
    public static void main(String[] args) {
        SpringApplication.run(MicroEureka.class,args);
    }
}

我们将注册中心当做电信运行商就会得到注册中心的其他用处:

  • 服务注册: 相当于开号,服务的的提供者也就是被其他服务调用的服务,启动的时候会通过发送Rest请求的方式将自己注册到注册中心上,同时携带了自身的一些元数据信息(比如服务名,端口)。

  • 服务续约: 相当于缴费,电话费快耗尽了,电信运行商会催着你缴费,你如果不缴费就会停你机。也就是说,在注册完服务之后,服务提供者会维护一个心跳用来持续高速注册中心,我还活着。

  • 服务下线: 也就是销号,当服务要正常关闭的时候,它会向注册中心发送一个Rest请求给Eureka Server,告诉服务中心,这个号我不用了。

  • 获取服务 和 服务调用 : 就是相当于打电话,服务消费者可以通过注册中心得到已注册服务的名单,然后通过这些名单上的服务实例名,通过Feign和Ribbon来完成远程调用。

  • 失效剔除: 也就是停机,一段时间内你不缴费,一直欠费就会电信运行商就会停你机。默认每隔一段时间(默认为60秒) 将当前清单中超时(默认为90秒)没有续约的服务剔除出去。

  • 自我保护机制: 注册中心这么重要,我肯定不能只部署一个啊,要是这个崩了怎么办,所以我们就要搞分布式集群啊。但是我们知道,一旦涉及到分布式,我们就必然要去选择,Eureka选择的是AP,分区容错性必然存在,因为你总无法保证网络是绝对可靠的,A即为可用性,也就是说Eureka是不能保证强一致性的,为了保证A,Eureka引入了自我保护机制,自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的健壮、稳定的运行。

  • 自我保护机制是指如果在15分钟内超过85%的节点都没有正常的心跳,那么此时Eureka就认为此时节点到注册中心出现了网络故障,此时Eureka进入自我保护机制,可能会出现下面几种情况:

    • Eureka Server不再从注册列表中移除因为长时间没收到心跳而应该过期的服务,等待网络恢复稳定状态。
    • Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。
    • 当网络恢复到稳定状态时,新的注册信息会被同步到其它节点中。

同电信网络一样,你仅仅接入到电信网络还是不行的,你还需要通信的工具,电话或者计算机,才能和别人进行通信,那么服务之间的电话就是Feign和Ribbon

Eureka 注册中心参考的资料:

远程调用Feign 和 Ribbon

feign 完成服务之间的相互调用

按照Spring Boot的常规操作,我们首先还是引入依赖:

	<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-openfeign</artifactId>
         </dependency>
            <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>

然后写对应的配置,像上面一样:

Spring:
  application:
    name: cityclient
server:
  port: 8081
eureka:
  client:
    service-url:
      defaultZone: http://localhost:9991/eureka/

启动类上打上对应的注解:

SpringBootApplication
// 该服务是Eureka的客户端,仅限于Eureka
@EnableEurekaClient
// 注册中心有许多替代品,该注解无缝适应其他注册中心
@EnableDiscoveryClient
// 表明此服务要进行远程调用
@EnableFeignClients
public class MicroEurekaClient {
    public static void main(String[] args) {
        SpringApplication.run(MicroEurekaClient.class,args);
    }
}

我该怎么调用其他服务,本次样例中我准备调用的是city服务:

@FeignClient(value = "city") // 服务名
public interface CityClient {
	// 对应的url
    @GetMapping("cityTest")
     String select();
}

然后在对应的控制层注入即可。

Ribbon完成服务之间的相互调用

Ribbon是通过RestTemplate完成服务的相互调用,我们首先要将RestTemplate装入IOC容器中。 然后在对应的控制器调用即可:

   @Autowired
    private RestTemplate restTemplate;
    @GetMapping("testRibbon")
    public void  testRibbon(){
        String result = restTemplate.getForObject("http://city/cityTest", String.class);
        System.out.println(result);
    }

负载均衡 Ribbon

单个服务应付的请求是有效的,为了应付多出来的请求,我们就可以搞搞集群,也就是说相同的服务我再跑一个,此时相同的服务就有两个了,那请求过来,应该让哪个服务处理请求呢? 这也就是负载均衡算法和负载均衡器。 负载均衡有两种类型:

  • 客户端负载均衡 ribbon 服务清单在Eureka,客户端从Eureka拿到清单,在发起调用时,由Ribbon根据负载均衡算法决定调用哪个服务。
  • 服务端负载均衡 (Nginx) 相同的服务实例有两个,由Nginx决定此次的请求由哪个服务承接。 常用的几种负载均衡算法简介:
  • 轮询(Round Robin)法 也就是按请求顺序轮流分配,简单的说你可以理解为排队。
  • 随机法 随机抽取一名幸运的服务来响应请求。
  • 加权轮询 给每一台服务赋予权重,配置高的优先响应请求。

Ribbon默认采取轮询算法,可以自定义,实现起来也很简单: 继承AbstractLoadBalancerRule类重写choose方法即可。

熔断机制 Hystrix

我们解决完了服务的调用和负载均衡之后,很快会引入新的问题,在服务之间的相互调用中,某个服务因为某些原因不可用了或者响应时间过长怎么办?

就比如在双十一的时候,是让用户一直等着抢购结果,还是在一段时间内没抢购成功,就返回一个失效呢? 人是厌倦等待的,相信各位已经有了答案,就是返回一个失败结果。这是能够提升用户体验的。这就是熔断器Hystrix的作用,

怎么和Feign结合在一起呢? 我们通常用Feign完成服务调用,

// value 指定调用哪个服务
// fallback 指定由哪个类返回快速失败结果    
@FeignClient(value = "city", fallback = CityClientImpl.class) //
public interface CityClient {
	  // 客户端的地址
    @GetMapping("cityTest")
     String select();
}
@Component
public class CityClientImpl implements CityClient {
    @Override
    public String select() {
        return "调用失败";
    }
}

Hystrix 仪表盘

Hystrix仪表盘,就像汽车的仪表盘一样,实时显示Hystrix的实时运行状态,通过Hystrix仪表盘我们可以看到Hystrix的各项指标信息,从而快速的发现系统中存在的问题。

启动时的页面:

之后的页面: )

路由网关 zuul

到现在我们的系统结构已经基本成型了,拥有下面这样的结构, 但是我们的系统还有以下问题:

  • 服务端向外部暴露的URL是真实的,外部通过这些URL来发起攻击。为了保护服务的安全,我们可以像滴滴一样,司机在接到订单式拿到的客户的电话是虚拟的,保护客户的安全,同样的我们暴露给外部的URL也可以是虚拟的。
  • 安全校验、登陆校验冗余: 为了保证系统安全,微服务的各个模块都会做一定的校验,但是微服务是独立的,我们不得不在每个微服务模块都做一套校验。显然这是冗余的。
  • 服务实例和负载均衡严重耦合: 外层的负载均衡器Nginx需要维护所有的服务清单,也就是ip+端口+服务名。哪天我的IP和服务划分发生了变化,我就需要去Nginx改。

由此引出API网关:,解决了以上问题。网关提供:

  • URL遮蔽,向外部暴露的是虚拟URL,网关会将虚拟的URL映射到真实的上
  • 统一校验,减少校验冗余
  • 接触服务实例和负载均衡的耦合,相当于服务实例和Nginx加了个中间层,Nginx不直接访问实例。

悄悄的说一句,Zuul也可以做负载均衡,那Nginx还要不要了? 网关也是要搞集群的,也要搞负载均衡,所以Nginx是可以搭配zuul使用的。

有了这层后,我们的架构就变成了这样:

配置中心 Spring Cloud config

目前为止我们的架构已经趋向完善,但是还有一点不完美的地方,微服务架构将我们的软件拆成一个一个的服务,每个服务都有着对应的配置文件,当服务变多,配置文件就变得难以管理,因为它分散在各个服务中,而且这些配置文件具备共性,那我们自然会有这样的需求,能否统一来管理微服务的配置文件呢? 这就是配置中心的用处,Spring Cloud为我们提供了 config组件,也就是配置中心。

config组件的配置中心的思想是这样的: 配置文件集中放在github、gitee等代码托管网站的仓库上,配置中心和这个仓库直连,每个服务在配置文件中指明配置中心和配置文件名,也就是向配置文件请求资源,然后由配置中心将对应的文件推给对应的服务。

总结

到目前为止Spring Cloud的组件之间竞争是十分激烈的,注册中心这块有Eureka和nacos,网关也有Spring Cloud官方也出了个gateway。但是思想还是没有变,拆分模块,,把握思想就好。

参考资料: