欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring cloud基础教程

程序员文章站 2022-06-12 16:11:01
...

Srping cloud


基础知识

在进行Spring Cloud 的具体内容介绍之前, 我们先通过本章学习一些关于微服务架构以及Spring Cloud 的基础知识。对Spring Cloud 能够解决的具体问题有一个大致的了解,以帮助我们更好地理解后续章节对各个组件的介绍。

什么是微服务架构

“微服务”一词源于Martin Fowler 的名为Microservices 的博文, 可以在他的官方博客上找到:http://martinfowler.com/articles/microservices.html。

简单地说, 微服务是系统架构上的一种设计风格,它的主旨是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP的RESTful API进行通信协作。被拆分成的每一个小型服务都围绕着系统中的某一项或一些耦合度较高的业务功能进行构建,并且每个服务都维护着自身的数据存储、业务开发、自动化测试案例以及独立部署机制。由千有了轻量级的通信协作基础, 所以这些微服务可以使用不同的语言来编写。

与单体系统的区别

在以往传统的企业系统架构中,我们针对一个复杂的业务需求通常使用对象或业务类型来构建一个单体项目。在项目中我们通常将需求分为三个主要部分: 数据库、服务端处理、前端展现。在业务发展初期,由于所有的业务逻辑在一个应用中, 开发、测试、部署都还比较容易且方便。但是,随着企业的发展, 系统为了应对不同的业务需求会不断为该单体项目增加不同的业务模块; 同时随着移动端设备的进步,前端展现模块已经不仅仅局限于Web的形式,这对千系统后端向前端的支持需要更多的接口模块。单体应用由千面对的业务需求更为宽泛,不断扩大的需求会使得单体应用变得越来越腕肿。单体应用的问题就逐渐凸显出来, 由于单体系统部署在一个进程内,往往我们修改了一个很小的功能, 为了部署上线会影响其他功能的运行。并且, 单体应用中的这些功能模块的使用场景、并发量、消耗的资源类型都各有不同, 对于资源的利用又互相影响, 这样使得我们对各个业务模块的系统容量很难给出较为准确的评估。所以, 单体系统在初期虽然可以非常方便地进行开发和使用, 但是随着系统的发展, 维护成本会变得越来越大, 且难以控制。

为了解决单体系统变得庞大脯肿之后产生的难以维护的问题, 微服务架构诞生了并被大家所关注。我们将系统中的不同功能模块拆分成多个不同的服务,这些服务都能够独立部署和扩展。由于每个服务都运行在自己的进程内, 在部署上有稳固的边界, 这样每个服务的更新都不会影响其他服务的运行。同时, 由千是独立部署的, 我们可以更准确地为每个服务评估性能容量, 通过配合服务间的协作流程也可以更容易地发现系统的瓶颈位置,以及给出较为准确的系统级性能容量评估。

如何实施微服务

在实施微服务之前, 我们必须要知道, 微服务虽然有非常多吸引人的优点, 但是也因为服务的拆分引发了诸多原本在单体应用中没有的问题。

  • 运维的新挑战:在微服务架构中, 运维人员需要维护的进程数量会大大增加。有条不紊地将这些进程编排和组织起来不是一件容易的事, 传统的运维人员往往很难适应这样的改变。我们需要运维人员有更多的技能来应对这样的挑战,运维过程需要更多的自动化, 这就要求运维人员具备一定的开发能力来编排运维过程并让它们能自动运行起来。
  • 接口的一致性:虽然我们拆分了服务, 但是业务逻辑上的依赖并不会消除, 只是从单体应用中的代码依赖变为了服务间的通信依赖。而当我们对原有接口进行了一些修改, 那么交互方也需要协调这样的改变来进行发布, 以保证接口的正确调用。我们需要更完善的接口和版本管理, 或是严格地遵循开闭原则。
  • 分布式的复杂性:由于拆分后的各个微服务都是独立部署并运行在各自的进程内,它们只能通过通信来进行协作, 所以分布式环境的问题都将是微服务架构系统设计时需要考虑的重要因素,比如网络延迟、分布式事务、异步消息等。

尽管微服务架构有很多缺点和问题, 但是其实现的敏捷开发和自动化部署等优点依然被广大优秀架构师和开发者所青眯,所以解决这些问题就是这几年诸多架构大师努力的目标。

微服务架构的九大特性

在架构师对于一个大型系统架构的设计与实施的过程中, 面对环境、资源、团队等各种因素的影响, 几乎不会出现完全相同的架构设计。对于微服务架构而言更是如此, 由于并没有一个标准或正式的定义, 每位架构师都根据自身理解与实际情况来进行设计, 并在发展的过程中不断演化与完善。经过多年的发展, Martin Fowler 在Microservices 一文中,提炼出了微服务架构的九大特性, 用于指导大家设计架构。

服务组件化

组件, 是一个可以独立更换和升级的单元。就像PC 中的CPU、内存、显卡、硬盘一样, 独立且可以更换升级而不影响其他单元。

在微服务架构中, 需要我们对服务进行组件化分解。服务, 是一种进程外的组件, 它通过HTTP 等通信协议进行协作,而不是像传统组件那样以嵌入的方式协同工作。每一个服务都独立开发、部署, 可以有效避免一个服务的修改引起整个系统的重新部署。

打一个不恰当的比喻, 如果我们的PC 组件以服务的方式构建, 那么只维护主板和一些必要外设之后, 计算能力通过一组外部服务实现, 我们只需要告诉PC 从哪个地址来获得计算能力, 通过服务定义的计算接口来实现我们使用过程中的计算需求, 从而实现CPU组件的服务化。这样原本复杂的PC 服务得到了轻量化的实现, 我们甚至只需要更换服务
地址就能升级PC 的计算能力。

按业务组织团队

当决定如何划分微服务时, 通常也意味着我们要开始对团队进行重新规划与组织。按以往的方式, 我们往往会从技术的层面将团队划分为多个,比如DBA团队、运维团队、后端团队、前端团队、设计师团队等。若我们继续按这种方式组织团队来实施微服务架构开发, 当有一个服务出现问题需要更改时, 可能是一个非常简单的变动, 比如对人物描述增加一个字段, 这需要从数据存储开始考虑一直到设计和前端, 虽然大家的修改都非常小,但这会引起跨团队的时间耗费和预算审批。

在实施微服务架构时, 需要采用不同的团队分割方法。由于每一个微服务都是针对特定业务的宽栈或是全栈实现, 既要负责数据的持久化存储, 又要负责用户的接口定义等各种跨专业领域的职能。因此,面对大型项目的时候, 对于微服务团队的拆分更加建议按业务线的方式进行拆分, 一方面可以有效减少服务内部修改所产生的内耗; 另一方面, 团队边界可以变得更为清晰。

做“ 产品” 的态度

在实施微服务架构的团队中, 每个小团队都应该以做产品的方式, 对其产品的整个生命周期负责。而不是以项目的模式,以完成开发与交付并将成果交接给维护者为最终目标。

开发团队通过了解服务在具体生产环境中的情况, 可以增加他们对具体业务的理解,比如, 很多时候, 一些业务中发生的特殊或异常情况, 很可能产品经理都并不知晓, 但细心的开发者很容易通过生产环境发现这些特殊的潜在问题或需求。

所以, 我们需要用做“产品”的态度来对待每一个微服务, 持续关注服务的运作情况,并不断分析以帮助用户来改善业务功能。

智能端点与哑管道

在单体应用中,组件间直接通过函数调用的方式进行交互协作。而在微服务架构中,由于服务不在一个进程中, 组件间的通信模式发生了改变, 若仅仅将原本在进程内的方法调用改成RPC 方式的调用,会导致微服务之间产生烦琐的通信, 使得系统表现更为糟糕,所以, 我们需要更粗粒度的通信协议。

在微服务架构中, 通常会使用以下两种服务调用方式:

  • 第一种, 使用HTTP 的RESTful API 或轻量级的消息发送协议, 实现信息传递与服务调用的触发。
  • 第二种, 通过在轻量级消息总线上传递消息, 类似RabbitMQ 等一些提供可靠异步交换的中间件。

在极度强调性能的情况下, 有些团队会使用二进制的消息发送协议, 例如protobuf。即使是这样, 这些系统仍然会呈现出“ 智能瑞点和哑管道” 的特点, 这是为了在易读性与高效性之间取得平衡。当然大多数Web 应用或企业系统并不需要在这两者间做出选择, 能够荻得易读性已经是一个极大的胜利了。

一Martin Fowler

去中心化治理

当我们采用集中化的架构治理方案时, 通常在技术平台上都会制定统一的标准, 但是每一种技术平台都有其短板, 这会导致在碰到短板时, 不得不花费大力气去解决, 并且可能因为其底层原因解决得不是很好, 最终成为系统的瓶颈。

在实施微服务架构时, 通过采用轻量级的契约定义接口, 使得我们对于服务本身的具体技术平台不再那么敏感,这样整个微服务架构系统中的各个组件就能针对其不同的业务特点选择不同的技术平台, 终千不会出现杀鸡用牛刀或是杀牛用指甲钳的尴尬处境了。

不是每一个问题都是钉子, 不是每一个解决方案都是锤子。

去中心化管理数据

我们在实施微服务架构时, 都希望让每一个服务来管理其自有的数据库, 这就是数据管理的去中心化。

在去中心化过程中, 我们除了将原数据库中的存储内容拆分到新的同平台的其他数据库实例中之外(如把原本存储在MySQL 中的表拆分后,存储到多个不同的MySQL 实例中),也可以将一些具有特殊结构或业务特性的数据存储到一些其他技术的数据库实例中(如把日志信息存储到MongoDB 中或把用户登录信息存储到Redis 中)。

虽然数据管理的去中心化可以让数据管理更加细致化, 通过采用更合适的技术可让数据存储和性能达到最优。但是, 由于数据存储于不同的数据库实例中后, 数据一致性也成为微服务架构中亟待解决的问题之一。分布式事务本身的实现难度就非常大, 所以在微服务架构中, 我们更强调在各服务之间进行“ 无事务” 的调用, 而对于数据一致性, 只要求数据在最后的处理状态是一致的即可;若在过程中发现错误, 通过补偿机制来进行处理,使得错误数据能够达到最终的一致性。

基础设施自动化

近年来云计算服务与容器化技术的不断成熟, 运维基础设施的工作变得越来越容易。但是,当我们实施微服务架构时,数据库、应用程序的个头虽然都变小了, 但是因为拆分的原因, 数量成倍增长。这使得运维人员需要关注的内容也成倍增长, 并且操作性任务也会成倍增长, 这些问题若没有得到妥善解决, 必将成为运维人员的噩梦。

所以,在微服务架构中, 务必从一开始就构建起“待续交付”平台来支撑整个实施过程, 该平台需要两大内容, 缺一不可。

  • 自动化测试:每次部署前的强心剂, 尽可能地获得对正在运行的软件的信心。
  • 自动化部署:解放烦琐枯燥的重复操作以及对多环境的配置管理。

容错设计

在单体应用中,一般不存在单个组件故障而其他部件还在运行的情况, 通常是一挂全挂。而在微服务架构中, 由于服务都运行在独立的进程中, 所以存在部分服务出现故障,而其他服务正常运行的情况。比如,当正常运作的服务B调用到故障服务A时, 因故障服务A 没有返回, 线程挂起开始等待, 直到超时才能释放, 而此时若触发服务B 调用服务A的请求来自服务C, 而服务C 频繁调用服务B 时, 由千其依赖服务A, 大量线程被挂起等待, 最后导致服务A也不能正常服务, 这时就会出现故障的荽延。

所以, 在微服务架构中,快速检测出故障源并尽可能地自动恢复服务是必须被设计和考虑的。通常, 我们都希望在每个服务中实现监控和日志记录的组件, 比如服务状态、断路器状态、吞吐量、网络延迟等关键数据的仪表盘等。

演进式设计

通过上面的几点特征, 我们已经能够体会到, 要实施一个完美的微服务架构, 需要考虑的设计与成本并不小, 对于没有足够经验的团队来说, 甚至要比单体应用付出更多的代价。

所以, 在很多情况下, 架构师都会以演进的方式进行系统的构建。在初期, 以单体系统的方式来设计和实施, 一方面系统体量初期并不会很大, 构建和维护成本都不高。另一方面,初期的核心业务在后期通常也不会发生巨大的改变。随着系统的发展或者业务的需要, 架构师会将一些经常变动或是有一定时间效应的内容进行微服务处理, 并逐渐将原来在单体系统中多变的模块逐步拆分出来, 而稳定不太变化的模块就形成一个核心微服务存在于整个架构之中。

为什么选择Spring Cloud

近几年很多入对于微服务架构的热情非常高, 但是回头看“微服务” 被提及也有很多年了。无数的架构师和开发者在实际项目中实践该设计理念并为此付出了诸多努力, 同时
也分享了他们在微服务架构中针对不同应用场景出现的各种问题的各种解决方案和开源框架, 其中也不乏国内互联网企业的杰出贡献。

  • 服务注册:阿里巴巴开源的Dubbo和当当网在其基础上扩展的DubboX、Netflix的
    Eureka、Apache的Consul等。
  • 分布式配置管理:百度的Disconf、Netflix的Archaius、360的QConf、SpringCloud
    的Config、淘宝的Diamond等。
  • 批量任务:当当网的Elastic-Job、Linkedln的Azkaban、SpringCloud的Task等。
  • 服务跟踪:京东的Hydra、SpringCloud的Sleuth、Twitter的Zipkin等。

上面列举了一些在实施微服务架构初期, 就需要被我们考虑进去的问题,以及针对这些间题的开源解决方案。可以看到国内、国外的技术公司都在贡献着他们的智慧。我们搜索微服务架构的实施方案时会发现,几乎大部分的分享主要以理论或是一个粗轮廓框架为主, 整合了来自不同公司或组织的诸多开源框架, 并加入针对自身业务的一些优化, 所以
找不到一个完全相同的架构方案。

前面我们介绍了一些关于微服务的理念以及特性, 分析了实施微服务的优点和缺点,而这些缺点通常就是这些框架出现的源头,大家都是为了解决或弥补业务拆分后所引出的诸多词题来设计出这些解决方案。而当我们作为一个新手, 准备实施微服务架构时, 为了避免踩前辈们踩过的坑, 我们不得不在这些核心问题上做出选择, 而选择又是如此之多,这必然会导致在做技术选型的初期, 需要花费巨大的调研、分析与实验精力。
Spring Cloud的出现,可以说是对微服务架构的巨大支持和强有力的技术后盾。它不像我们之前所列举的框架那样, 只是解决微服务中的某一个问题, 而是一个解决微服务架构实施的综合性解决框架, 它整合了诸多被广泛实践和证明过的框架作为实施的基础部件,又在该体系基础上创建了一些非常优秀的边缘组件。

打个不太恰当的比喻:我们自己对各个问题选择框架来实施微服务架构就像在DIY电脑一样, 我们对各环节的选择*度很高, 但是最终结果很有可能因为一条内存质量不行就点不亮了, 总是让人不怎么放心。当然, 如果你是一名高手, 这些自然都不是问题, 然而千军易得、良将难求。而使用Spring Cloud来实施就像直接购买品牌机一样, 在Spring社区的整合之下, 做了大量的兼容性测试, 保证了其拥有更好的稳定性, 如果要在Spring Cloud架构下使用非原装组件时, 就需要对其基础有足够的了解。

Spring Cloud也许对很多已经实施微服务并自成体系的团队不具备足够的吸引力,但是对于还未实施微服务或是未成体系的团队, 这必将是一个非常有吸引力的框架选择。不论其项目的发展目标, 还是Spring的强大背景, 亦或其极高的社区活跃度, 都是未来企业架构师必须了解和接触的重要框架, 有一天成为微服务架构的标准解决方案也并非不可能。

Spring Cloud简介

Spring Cloud是一个基千Spring Boot实现的微服务架构开发工具。它为微服务架构中涉及的配置管理、服务注册、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。

Spring Cloud包含了多个子项目(针对分布式系统中涉及的多个不同开源产品,还可能会新增), 如下所述。

  • Spring Cloud Config: 配置管理工具, 支持使用Git存储配置内容, 可以使用它实现应用配置的外部化存储, 并支持客户端配置信息刷新、加密/解密配置内容等。
  • Spring CloudN etflix: 核心组件,对多个Netflix OSS开源套件进行整合。
    • Eureka: 服务注册组件, 包含服务注册中心、服务注册与发现机制的实现。
    • Hystrix: 容错管理组件,实现断路器模式, 帮助服务依赖中出现的延迟和为故障提供强大的容错能力。
    • Ribbon: 客户端负载均衡的服务调用组件。
    • Feign: 基于伈bbon 和Hystrix 的声明式服务调用组件。
    • Zuul: 网关组件, 提供智能路由、访问过滤等功能。
    • Archaius: 外部化配置组件。
  • Spring Cloud Bus: 事件、消息总线, 用于传播集群中的状态变化或事件, 以触发后续的处理, 比如用来动态刷新配置等。
  • Spring Cloud Cluster: 针对ZooKeeperRedisHazelcastConsul 的选举算法和通用状态模式的实现。
  • Spring Cloud Cloudfoundry: 与Pivotal Cloudfoundry 的整合支持。
  • Spring Cloud Consul: 服务发现与配置管理工具。
  • Spring Cloud Stream: 通过Redis、Rabbit 或者Kafka 实现的消费微服务, 可以通过
    简单的声明式模型来发送和接收消息。
  • Spring Cloud A WS: 用千简化整合Amazon Web Service 的组件。
  • Spring Cloud Security: 安全工具包, 提供在Zuul 代理中对0Auth2 客户端请求的中
    继器。
  • Spring Cloud Sleuth: Spring Cloud 应用的分布式跟踪实现, 可以完美整合Zip虹n。
  • Spring Cloud ZooKeeper: 基于ZooKeeper 的服务发现与配置管理组件。
  • Spring Cloud Starters: Spring Cloud 的基础组件, 它是基于Spring Boot 风格项目的
    基础依赖模块。
  • Spring Cloud CLI: 用于在Groovy 中快速创建Spring Cloud 应用的Spring Boot CLI
    插件。

本教程将对其中一些较为常用的组件进行介绍、分析, 并演示其使用方法。

版本说明

当我们通过搜索引擎查找一些Spring Cloud 的文章或示例时, 往往可以在依赖中看到很多不同的版本名字, 比如Angel.SR6、Brix ton.SR5 等, 为什么Spring Cloud 没有像其他Spring 的项目使用类似l.x.x 的版本命名规则呢?这些版本之间又有什么区别呢?在学习之
初,非常有必要弄清楚这些版本的意义和内容, 这样才能在我们使用Spring Cloud 时, 指导我们选择更为合适的版本进行架构与开发。

版本名与版本号

由于Spring Cloud 不像Spring 社区其他一些项目那样相对独立, 它是一个拥有诸多子项目的大型综合项目, 可以说是对微服务架构解决方案的综合套件组合, 其包含的各个子项目也都独立进行着内容更新与迭代,各自都维护着自己的发布版本号。因此每一个Spring Cloud 的版本都会包含多个不同版本的子项目, 为了管理每个版本的子项目清单, 避免Spring Cloud的版本号与其子项目的版本号相混淆,没有采用版本号的方式,而是通过命名的方式。

这些版本的名字采用了伦敦地铁站的名字, 根据字母表的顺序来对应版本时间顺序,比如最早的Release版本为Angel, 第二个Release版本为Brixton……

经过上面的解释, 不难猜出, 之前所提到的AngelS.SR6、BrixtonS.SR5中的SR6、SR5就是版本号了。
当一个版本的Spring Cloud项目的发布内容积累到临界点或者一个严重bug解决可用后, 就会发布一个"service releases"版本,简称SRX版本, 其中X是一个递增的数字,
所以Brixton.SR5就是Brixton的第5个Release版本。

使用spring cloud实现微服务

Spring Cloud Eureka 是Spring Cloud Netflix 微服务套件中的一部分,它基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务注册功能。Spring Cloud 通过为Eureka 增加了Spring Boot 风格的自动化配置,我们只需通过简单引入依赖和注解配置就能让Spring Boot 构建的微服务应用轻松地与Eureka 服务注册体系进行整合。

在本章中, 我们将学习下面这些核心内容, 并构建起用于服务注册的基础设施。

  • 构建服务注册中心
  • 服务注册与服务发现
  • Eureka 的基础架构
  • Eureka 的服务***制
  • Eureka 的配置服务

在一开始,我们需要先了解微服务中的参与者,他们分别是:服务提供者服务消费者服务注册中心

服务提供者与服务消费者

使用微服务构建的是分布式系统,微服务之间通过网络进行通信。我们使用服务提供者与服务消费者来描述微服务之间的调用关系,下表解释了服务提供者与服务消费者。
名词 定义
服务提供者 服务的被调用方(即:为其他服务提供服务的服务
服务消费者 服务的调用方,即依赖其他服务的服务

服务注册中心

服务注册可以说是微服务架构中最为核心和基础的模块, 它主要用来实现各个微服务实例的自动化注册与发现。为什么我们在微服务架构中那么需要服务注册模块呢?微服务系统没有它会有什么不好的地方吗?

在最初开始构建微服务系统的时候可能服务并不多, 我们可以通过做一些静态配置来完成服务的调用。比如,有两个服务A 和B, 其中服务A 需要调用服务B 来完成一个业务操作时, 为了实现服务B 的高可用, 不论采用服务端负载均衡还是客户端负载均衡, 都需要手工维护服务B 的具体实例清单。但是随着业务的发展, 系统功能越来越复杂, 相应的微服务应用也不断增加, 我们的静态配置就会变得越来越难以维护。并且面对不断发展的业务, 我们的集群规模、服务的位置、服务的命名等都有可能发生变化, 如果还是通过手工维护的方式,那么极易发生错误或是命名冲突等问题。同时,对于这类静态内容的维护
也必将消耗大量的人力。

为了解决微服务架构中的服务实例维护问题, 产生了大量的服务注册框架和产品。这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。

使用服务注册中心后的架构如下图所示:

spring cloud基础教程

服务提供者、服务消费者、服务注册组件三者的关系大致如下:

  • 各微服务在启动时,将自己的网络地址等信息注册到服务注册组件中,服务注册组件会存储这些信息。
  • 服务消费者可以从服务注册组件中查询服务提供者的网络地址,并使用该地址调用服务提供者所提供的接口。
  • 各微服务与服务注册组件使用一定机制(例如心跳)通信,服务注册组件如长时间无法与某服务实例通信,就会注销该实例。
  • 微服务网络地址发生变更时,会重新注册到服务注册组件。使用这种方式,服务消费者就无需人工维护提供者的网络地址了。

综上,服务注册组件应该具备以下功能:

  • 服务注册表:是服务注册组件的核心,它用来记录各微服务的信息,例如微服务的名称、IP、端口等。服务注册表提供查询API和管理API,查询API用于查询可用的微服务实例,管理API用于服务的注册和注销。
  • 服务注册于服务发现:服务注册指的是微服务在启动时,将自己的信息注册到服务发现组件上的过程。服务发现是指查询可用微服务列表及其网络地址的机制。
  • 服务检查: 服务注册组件使用一定机制定时检测已注册的服务,如发现某实例长时间无法访问,就会从服务注册表中移除该实例。

综上,使用服务注册组件的好处显而易见。spring cloud提供了多种服务注册组件的支持,例如:Eureka,Consul,Zookeeper等,在本教材内我们均使用Eureka为例。

服务注册组件在市面上也可能叫做服务注册组件、服务发现组件、注册中心等名词。

Eureka简介

Eureka是Netflix开发的服务发现组件,本身是一个基于REST的服务。Spring Cloud将它集成在其子项目spring-cloud-netflix中,以实现Spring Cloud的服务发现功能。目前Eureka 项目相当活跃,代码更新相当频繁。

Region、Zone解析

Eureka的官方文档对regin、zone几乎没有提及,由于概念抽象,新手很难理解。因此,在分析Eureka原理之前,我们先来了解一下region、zone、Eureka集群三者的关系,如图:

spring cloud基础教程

region和zone(或者Availability Zone)均是AWS的概念。在非AWS环境下,我们可以简单地将region理解为Eureka集群,zone理解成机房。这样图4-2就很好理解了——一个Eureka集群被部署在了zone1机房和zone2机房中。

Eureka架构

Eureka架构图如下

spring cloud基础教程

这是来自Eureka官方的架构图,大致描述了Eureka集群的工作过程。图中包含的组件非常多,可能比较难以理解,我们用通俗易懂的语言解释一下:

  • Application Service 相当于服务提供者,Application Client相当于服务消费者;
  • Make Remote Call,可以简单理解为调用RESTful API;
  • us-east-1c、us-east-1d等都是zone,它们都属于us-east-1这个region;

由图可知,Eureka包含两个组件:Eureka ServerEureka Client,它们的作用如下:

  • Eureka Client是一个Java客户端,用于简化与Eureka Server的交互;
  • Eureka Server提供服务发现的能力,各个微服务启动时,会通过Eureka Client向Eureka Server进行注册自己的信息(例如网络信息),Eureka Server会存储该服务的信息;
  • 微服务启动后,会周期性地向Eureka Server发送心跳(默认周期为30秒)以续约自己的信息。如果Eureka Server在一定时间内没有接收到某个微服务节点的心跳,Eureka Server将会注销该微服务节点(默认90秒);
  • 每个Eureka Server同时也是Eureka Client,多个Eureka Server之间通过复制的方式完成服务注册表的同步;
  • Eureka Client会缓存Eureka Server中的信息。即使所有的Eureka Server节点都宕掉,服务消费者依然可以使用缓存中的信息找到服务提供者。

综上,Eureka通过心跳检测、健康检查和客户端缓存等机制,提高了系统的灵活性、可伸缩性和可用性。

创建一个Eureka Server

在Spring Cloud实现一个Eureka Server是一件非常简单的事情。下面我们来写一个Eureka Server 。

因为在spring cloud应用中会涉及到多个项目模块,接下来我们使用IDEA工具在一个windows下实现多个项目。

第一步 创建一个普通的java项目,存储在一个空的目录下,这里具体步骤省略。
第二步 在上一步创建的项目的基础上,新建一个Module
file -> new -> Module 打开如下窗口,选择Spring Initilizr ,点击next
spring cloud基础教程

注意,在我们的示例中采用目前最新的版本的spring cloud,其依赖的spring boot版本为2.0,对JDK的需求是必须JDK1.8或者1.9,不再支持1.7及以下版本。

填写Module的相关信息,主要是groupArtifact,然后点击next

spring cloud基础教程

选择Cloud Discover中的Eureka Server,点击next,然后点击finish,如下图所示。

spring cloud基础教程

注意,尽量不要改变Module的存储路径,直接将其放在第一步建立的普通java项目目录下。

建立好的Eureka Server目录结构如下图所示:

spring cloud基础教程

说明:pom.xml是本Module的maven配置文件,EurekaServerApplication.java是入口程序,EurekaServerApplicationTests.java是测试入口。
application.properties是spring cloud的属性配置文件,其也可以是application.yml,这里我们用application.yml

其实细心的我们已经发现了,spring cloud的项目结构和spring boot基本一样。区别是在pom.xml中增加了spring cloud的依赖管理以及spring cloud Eureka server依赖,具体如下:


<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>

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

配置启动类

在`EurekaServerApplication.java`入口类加上一个注解`@EnableEurekaServer`,声明这是一个Eureka Server。
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

编写配置文件application.yml

server:
  port: 8761
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false

这样就完成了一个简单的Eureka Server。简要说明一下application.yml中的配置项:
eureka.client.registerWithEureka :表示是否将自己注册到Eureka Server,默认为true。由于当前这个应用就是Eureka Server,故而设为false。
eureka.client.fetchRegistry:表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。

使用maven打包

在通过IDEA的Terminal,进入Eureka-Server目录,执行maven打包命令mvn clean package,打包成功后,再进入target目录,通过java -jar命令执行

spring cloud基础教程

出现如下所示信息时,表示服务注册中心已经启动成功。
spring cloud基础教程

打开浏览器,输入地址:http://localhost:8761访问,我们会发现此时还没有服务注册到Eureka上面,如下图:

spring cloud基础教程

该页面展示了Eureka的系统状态、当前注册到Eureka Server上的服务实例、一般信息、实例信息等。我们可以看到,当前还没有任何服务被注册到Eureka Server上。

创建一个服务提供者,提供服务

下面我们创建提供服务的客户端,并向服务注册中心注册自己。

创建服务提供者项目

首先,和创建Eureka Server应用基本一样,唯一不同的地方在于,在Cloud Discovery不再选择Eureka Server,而是选择Eureka Discover。命名为eureka-provider,在pom.xml中,变更的配置信息是去掉了server的依赖,而增加了client依赖:

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

注意:在spring cloud的Finchley版本之前的版本,添加Eureka client依赖的artifactId为spring-cloud-starter-eureka

修改程序启动类

在启动类加上一个注解@EnableDiscoveryClient,声明这是一个Eureka client。

@EnableDiscoveryClient
@SpringBootApplication
public class UserProviderApplication {

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

编写配置文件application.yml

server:
  port: 9000
spring:
  application:
    name: user-provider
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

通过spring.application.name属性,我们可以指定微服务的名称后续在调用的时候只需要使用该名称就可以进行服务的访问。eureka.client.serviceUrl.defaultZone属性对应服务注册中心的配置内容,指定服务注册中心的位置。为了在本机上测试区分服务提供方和服务注册中心,使用server.port属性设置不同的端口。

编写具体的服务逻辑

在spring cloud中,具体的服务表现为Rest服务,通过spring mvc实现,当然我们完全可以通过spring boot简化它,具体代码如下:

@RestController
public class HelloProviderController {

    @Value("${server.port}")
    private String port;

    @GetMapping("/hello")
    public String say() {
        return String.format("你好,我是一个服务提供者。我的对外的端口是:%s",port);
    }

}

运行程序服务提供者,注册服务

可以通过IDE工具直接运行入口程序,也可以通过maven打包之后运行,在控制台中输出如下内容:

spring cloud基础教程

且在eureka-server端的控制台上有日志消息如下

spring cloud基础教程

则表示服务提供者正确在服务注册中心注册了。现在我们再次通过http://localhost:8761来访问服务注册中心,如下图所示:
spring cloud基础教程

Eureka的自我保护模式

如果在Eureka Server的首页看到以下这段提示,则说明Eureka已经进入了保护模式。

EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.

保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。一旦进入保护模式,Eureka Server将会尝试保护其服务注册表中的信息,不再删除服务注册表中的数据(也就是不会注销任何微服务)。

如何解决Eureka Server不踢出已关停的节点的问题

在开发过程中,我们常常希望Eureka Server能够迅速有效地踢出已关停的节点,但是由于Eureka自我保护模式,以及心跳周期长的原因,常常会遇到Eureka Server不踢出已关停的节点的问题。解决方法如下:

(1) Eureka Server端:配置关闭自我保护,并按需配置Eureka Server清理无效节点的时间间隔

eureka.server.enable-self-preservation          # 设为false,关闭自我保护
eureka.server.eviction-interval-timer-in-ms     # 清理间隔(单位毫秒,默认是60*1000)

(2) Eureka Client端:配置开启健康检查,并按需配置续约更新时间和到期时间。

eureka.client.healthcheck.enabled           # 开启健康检查(需要spring-boot-starter-actuator依赖)
eureka.instance.lease-renewal-interval-in-seconds       # 续约更新时间间隔(默认30秒)
eureka.instance.lease-expiration-duration-in-seconds    # 续约到期时间(默认90秒)

示例:
服务器端配置:

eureka:
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 4000

客户端配置:

eureka:
  client:
    healthcheck:
      enabled: true
  instance:
    lease-expiration-duration-in-seconds: 30 
    lease-renewal-interval-in-seconds: 10

注意:
更改Eureka更新频率将打破服务器的自我保护功能,生产环境下不建议自定义这些配置。

创建一个服务消费者,消费服务

下面我们创建消费服务的客户端,从服务中心查询一个服务,并调用该服务。

创建服务消费者项目

服务消费者项目和本质上也是一个Eureka server的客户端,所以和创建服务提供者的方式一致。

修改启动类

在启动类加上一个注解@EnableDiscoveryClient,声明这是一个Eureka client。

@EnableDiscoveryClient
@SpringBootApplication
public class UserConsumerApplication {

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


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

在启动类中定义了一个RestTemplate的bean,并为它添加了@LoadBalanced注解,该注解具体含义后面介绍

编写配置文件application.yml

server:
  port: 8000
spring:
  application:
    name: user-consumer
eureka:
  client:
    serviceUrl:
      myZone: http://localhost:8761/eureka/

编写Controller,使用RestTemplate来消费服务


@RestController
public class HelloController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/call")
    public String call(){
       return restTemplate.getForEntity("http://user-provider/hello",String.class).getBody();
    }

}

说明:http://user-provider/hello 其中user-provider是服务提供者的应用名称,即spring.application.name,它不区分大小写,而hello为我们要消费的具体服务。
这个应用可以理解为服务消费者在服务注册中心通过应用名称user-provider找到具体提供服务的应用,然后再调用其具体的服务。

启动应用,测试

启动应用,通过http://localhost:8000/call 访问,结果如下:

spring cloud基础教程

高可用服务注册中心

在微服务架构这样的分布式环境中我们需要充分考虑发生故障的情况, 所以在生产环境中必须对各个组件进行高可用部署, 对于微服务如此,对于服务注册中心也一样。但是到本节为止,我们一直都在使用单节点的服务注册中心,这在生产环境中显然并不合适,我们需要构建高可用的服务注册中心以增强系统的可用性。EurekaS erver的设计一开始就考虑了高可用问题, 在Eureka的服务注册设计中, 所有节点即是服务提供方, 也是服务消费方, 服务注册中心也不例外。是否还记得在单节点的配置中, 我们设置过下面这两个参数, 让服务注册中心不注册自己:

eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false

Eureka Server的高可用实际上就是将自己作为服务向其他服务注册中心注册自己,这样就可以形成一组互相注册的服务注册中心, 以实现服务清单的互相同步,达到高可用的效果。下面我们就来尝试搭建高可用服务注册中心的集群。我们在前面的服务注册中心的基础之上进行扩展, 构建一个双节点的服务注册中心集群。

注意:若需要实现eureka server集群,需要将以上两个参数设置为true,否则将造成不可用的服务分片unavailable-replicas

  • 创建application-peerl.yml, 作为peerl服务中心的配置,并将serviceUri指向peer2:
spring:
  application:
    name: eureka-server  #服务名称,Eureka server集群的服务名称必须一致
server:
  port: 1111  # 端口号
eureka:
  instance:
    hostname: peer1  #主机名称
  client:
    serviceUrl:
      defaultZone: http://peer2:1112/eureka/  # 将自己作为一个微服务应用注册到另外一个Eureka server
    registerWithEureka: true   #必须设置为true
    fetchRegistry: true        #必须设置为true
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 4000
  • 创建application-peer2.yml,作为peer2服务中心的配置,并将serviceUrl指向peer1:
spring:
  application:
    name: eureka-server  #服务名称,Eureka server集群的服务名称必须一致
server:
  port: 1112  # 端口号
eureka:
  instance:
    hostname: peer2  #主机名称
  client:
    serviceUrl:
      defaultZone: http://peer1:1111/eureka/  # 将自己作为一个微服务应用注册到另外一个Eureka server
    registerWithEureka: true   #必须设置为true
    fetchRegistry: true        #必须设置为true
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 4000
  • 配置系统的hosts,Windows系统的hosts文件路径是:C:\Windows\System32\drivers\etc\hosts,Linux及Mac Os等系统的文件路径是/etc/hosts,在这个文件的最后添加:
127.0.0.1 peer1 
127.0.0.1 peer2
  • 通过spring.profiles.active属性来分别启动peer1和peer2

此时访问peer1的注册中心:http://localhost:1111/,如下图所示,我们可以看到registered-replicas中已经有peer2节点的eureka-server了。同样地,访问peer2的注册中心:http://localhost:1112/,能看到registered-replicas中已经有peer1节点,并且这些节点在可用分片(available-replicase)之中。我们也可以尝试关闭peer1,刷新http://localhost:1112/,可以看到peer1的节点变为了不可用分片(unavailable-replicas)。

spring cloud基础教程

服务注册与发现

在设置了多节点的服务注册中心之后,我们只需要简单的服务配置,就能将服务注册到Eureka Server集群中。我们以前面的user-provider为基础,修改application.yml配置文件:

server:
  port: 9000
spring:
  application:
    name: user-provider
eureka:
  client:
    healthcheck: true
    serviceUrl:
      deafultZone: http://peer1:1111/eureka/,http://peer2:1112/eureka/

上面的配置主要对eureka.client.serviceUrl.defaultZone属性做了改动,将注册中心指向了之前我们搭建的peer1与peer2,中间以英文逗号分隔。

下面,我们启动该服务,通过访问http://localhost:1111/和http://localhost:1112/,可以观察到user-provider同时被注册到了peer1和peer2上。若此时断开peer1,由于user-provider同时也向peer2注册,因此在peer2上其他服务依然能访问到compute-service,从而实现了高可用的服务注册中心。

说明:其实eureka.client.serviceUrl.defaultZone可以不用指定所有的Eureka Server节点,仅指peer1的话,Eureka Server集群会自动将该服务同步注册到peer2,但是不推荐这么做,因为这么做无法解决Eureka Server集群的单点故障。

深入理解

虽然上面我们以双节点作为例子,但是实际上因负载等原因,我们往往可能需要在生产环境构建多于两个的Eureka Server节点。那么对于如何配置serviceUrl来让集群中的服务进行同步,需要我们更深入的理解节点间的同步机制来做出决策。

Eureka Server的同步遵循着一个非常简单的原则:只要有一条边将节点连接,就可以进行信息传播与同步。什么意思呢?不妨我们通过下面的实验来看看会发生什么。

  • 场景一:假设我们有3个注册中心,我们将peer1、peer2、peer3各自都将serviceUrl指向另外两个节点。换言之,peer1、peer2、peer3是两两互相注册的。启动三个服务注册中心,并将user-provider的serviceUrl指向peer1并启动,可以获得如下图所示的集群效果。
    spring cloud基础教程

访问http://localhost:1112/,可以看到3个注册中心组成了集群,user-provider服务通过peer1同步给了与之互相注册的peer2和peer3。

通过上面的实验,我们可以得出下面的结论来指导我们搭建服务注册中心的高可用集群:
两两注册的方式可以实现集群中节点完全对等的效果,实现最高可用性集群,任何一台注册中心故障都不会影响服务的注册与发现

高可用的服务提供者

在上面的示例中,我们通过Eureka server集群的方式实现了高可用的服务注册中心,接下来我们需要实现高可用的服务提供者。

在生产环境下,单点的服务提供往往会存在性能不足、可用性不高等缺陷,在spring cloud中可以非常方便的实现高可用的服务提供者,即将某服务应用以集群的形式注册到服务注册中心,不用修改任何的user-provider的代码,仅仅通过下列方式启动多个user-provider的实例即可。

java -jar user-provider-0.0.1-SNAPSHOT.jar --server.port=2001
java -jar user-provider-0.0.1-SNAPSHOT.jar --server.port=2002
java -jar user-provider-0.0.1-SNAPSHOT.jar --server.port=2003

再次访问http://peer1:1111或者http://peer2:1112如图所示,可见注册了3个user-provider服务到Eureka server。

spring cloud基础教程

使用ribbon实现客户端负载均衡

在前面,我们部署了3个user-provider的实例,那么对于服务消费者来说,是如何将强求分摊到多个服务提供者身上呢?

Ribbon

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。它是一个基于HTTP和TCP的客户端负载均衡器。当为Ribbon配置服务提供者地址列表后,Ribbon就可以基于某种负载均衡算法,自动地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询、随机等。当然我们也可以为Ribbon实现自定义的负载均衡算法。

在spring cloud中,当ribbon与Eureka配合使用时,Ribbon可以自动从Eureka server获取服务提供者地址列表,并给予负载均衡算法,请求其中一个服务提供者实例。下图展示了Ribbon与Eureka配合使用时的大致架构。

spring cloud基础教程

Ribbon工作时分为两步:第一步先选择 Eureka Server, 它优先选择在同一个Zone且负载较少的Server;第二步再根据用户指定的策略,在从Server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略,例如轮询round robin、随机Random、根据响应时间加权等。

动手试一试

其实我们不用对服务消费者进行任何的变更,即可实现客户端基于Ribbon的负载均衡。核心原因在:

  1. spring-cloud-starter-netflix-eureka-client依赖中已经包含有spring-cloud-starter-netflix-eureka-Ribbon,所有不用再添加依赖
  2. 还记得前面在user-consumer项目的启动类中,定义的RestTemplate吗?我们为它增加了一个@LoadBalanced注解,即代表RestTemplate已经整合了Ribbon,无需我们再进行其他的变更了。

现在我们再次启动user-consumer,并多次访问http://localhost:3000/call,结果执行为以下一些数据

你好,我是一个服务提供者。我的对外的端口是:2000
你好,我是一个服务提供者。我的对外的端口是:2001
你好,我是一个服务提供者。我的对外的端口是:2002
你好,我是一个服务提供者。我的对外的端口是:2000
你好,我是一个服务提供者。我的对外的端口是:2001
你好,我是一个服务提供者。我的对外的端口是:2002
你好,我是一个服务提供者。我的对外的端口是:2000
...

可以看到,此时请求均匀分布在3个微服务节点上,说明实现了负载均衡。

在默认情况下,Ribbon和Eureka server集成后,所采用的负载均衡算法为轮询算法。但在生成环境,轮询算法并非适用于所有的场景,此时我们就需要修改其负载均衡策略。当然除此之外还有很多Ribbon配置可以修改,此处不做讲解。

服务容错保护: Spring Cloud Hystrix

在微服务架构中,我们将系统拆分成了一个个的服务单元,各单元间通过服务注册与订阅的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会出现因等待出现故障的依赖方响应而形成任务积压,最终导致自身服务的瘫痪。

举个例子,在一个电商网站中,我们可能会将系统拆分成,用户、订单、库存、积分、评论等一系列的服务单元。用户创建一个订单的时候,在调用订单服务创建订单的时候,会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因网络原因无法被访问到,导致创建订单服务的线程进入等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些等待线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。

在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就会因依赖关系形成故障蔓延,最终导致整个系统的瘫痪,这样的架构相较传统架构就更加的不稳定。为了解决这样的问题,因此产生了断路器模式。

通过前边的学习,服务注册中心、服务提供者和服务消费者都成功建立并运行起来,而且通过默认的配置RestTemplate@Loadbalanced注解开启了负载均衡。
在默认的情况下,负载均衡策略是线性轮询的方式,也就是说在客户端获取到的服务列表中依次交替,例如开启了三个服务server1、server2、server3,那么在线性轮询时,就会按这个顺序来调用。
我之前是开启了三个服务,一个端口是2000,2001和2002,那么在之前的这种情况下,如果我关闭其中一个服务,就比如这里关闭2001端口的服务,当再次访问的时候,每访问三次,就会有一次是如下的error page,直到我挂掉的这个服务被服务注册中心剔除前均会存在。

如果服务提供者响应非常缓慢,name消费者对提供者的请求就会被强制等待,知道提供者响应或超时。在高负载场景下,如果不做任何处理,此类问题可能会导致服务消费者的资源耗尽甚至整个系统的崩溃。例如,曾经发生过一个案例----某电子商务网站在一个黑色星期五发生过过载。过多的并发请求,导致用户支付的请求延迟很久都没有响应,在等待很长时间后最终失败。支付失败又导致用户重新刷新页面并再次尝试支付,进一步增加了服务器的负载,最终导致整个系统都崩溃了。

当依赖的服务不可用时,服务自身会不会被拖垮,这是我们在构建分布式应用时需要考虑的问题。

雪崩效应

微服务架构的应用系统通常包含多个服务层。微服务之间通过网络进行通信,从而支撑整个应用系统,因此,微服务之间难免存在依赖关系。我们知道任何微服务都并非100%可用,网络往往也很脆弱,因此难免有些请求会失败。

我们常把“基础服务故障”导致“级联故障”的现象称为雪崩效应。雪崩效应描述的是提供者不可用导致消费者不可用,并将不可用逐渐放大的过程。

如下图,A作为服务提供者(基础服务),B为A的服务消费者,C和D都是B的消费者。当A不可用引起B的不可用,并将不可用像滚雪球一样放大到C和D时,雪崩效应就形成了。

spring cloud基础教程

如何容错

要想防止雪崩效应,必须有一个强大的容错机制。该容错机制需事先以下两点:

  • 为网络请求设置超时
    必须为网络请求设置超时。正常情况下,一个远程调用一般在几十毫秒内就能得到响应了。如果依赖的服务不可用或者网络有问题,那么响应时间就会变得很长(几十秒)。
    通常情况下, 一次远程调用对应着一个线程/进程。如果响应太慢,这个线程/进程就得不到释放。而线程/进程又对应着系统资源,如果得不到释放的线程/进程越积越多,资源就会逐渐被耗尽,最终导致服务的不可用。因此,必须为每个网络请求设置超时,让资源尽快释放。
  • 使用断路器模式
    试想一下,如果家里没有断路器,当电流过载时(例如功率过大、短路等),电路不断开,电路就会升温,甚至可能烧断电路、引发火灾。使用断路器,电路一旦过载就会跳闸,从而可以保护电路的安全。在电路超载的问题被解决后,只须关闭断路器,电路就可以恢复正常。同理,如果对某个微服务的请求有大量超时(常常说明该微服务不可用),再去让新的请求访问该服务已经没有任何意义,只会无谓消耗资源。例如,设置了超时时间为1秒,如果短时间内有大量的请求无法在1秒内得到响应,就没有必要再去请求依赖的服务了。
    断路器可理解为对容易导致错误的操作的代理。这种代理能够统计一段时间内调用失败的次数,并决定是正常请求依赖的服务还是直接返回。
    断路器可以实现快速失败,如果它在一段时间内检测到许多类似的错误(例如超时),就会在之后的一段时间内,强迫对该服务的调用快速失败,即不再请求所依赖的服务。这样,应用程序就无须再浪费CPU时间去等待长时间的超时。
    断路器也可自动诊断依赖的服务是否已经恢复正常。如果发现依赖的服务已经恢复正常,那么就会恢复请求该服务。使用这种方式,就可以实现微服务的“自我修复”——当依赖的服务不正常时打开断路器时快速失败,从而防止雪崩效应;当发现依赖的服务恢复正常时,又会恢复请求。
    断路器状态转换的逻辑如下图所示,简单来说:
    • 正常情况下,断路器关闭,可正常请求依赖的服务。
    • 当一段时间内,请求失败率达到一定阔值(如错误率达到50%,或100次/分钟等),断路器就会打开。此时,不会再去请求依赖的服务。
    • 断路器打开一段时间后,会自动进入“半开”状态。此时,断路器可允许一个请求访问依赖的服务。如果该请求能够成功调用,则关闭断路器;否则继续保持打开状态。

spring cloud基础教程

Hystrix简介

hystrix是一个实现了超时机制和断路器模式的工具类库。

简介

hystrix是由Netflix开源的一个延迟和容错库,用于隔离访问远程系统、服务、或者第三方库,防止级联失败,从而提升系统的可用性与容错性。

hystrix主要通过以下几点实现容错和延迟:

包裹请求

使用hystrixCommand(或hystrixObservableCommand)包裹对依赖的调用逻辑,每个命令在独立线程中执行。这使用到了设计模式中的“命令模式”。

跳闸机制

当某服务的错误率超过一定阔值时,hystrix可以自动或手动跳闸,停止请求该服务一段时间。

资料隔离

hystrix为每个依赖都维护了一个小型的线程池(或信号量)。如果该线程池已满,发往该依赖的请求就会被立即拒绝,而不是排队等待,从而加速失败判定。

监控

hystrix可以近乎实时的监控运行指标和配置的变化,例如成功、失败、超时以及被拒绝的请求等

回退机制

当请求失败、超时、被拒绝,或当断路器打开时,执行回退逻辑。回退逻辑可由开发人员自行提供,例如返回一个缺省值。

自我修复

断路器打开一段时间后,会自动进入“半开”状态。

使用hystrix实现容错

第一步 : 在服务消费者的pom.xml中增加hystrix的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

注意,在springcloud的Finchley版本之前的starter是
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency>

第二步: 在服务消费者的启动类上加上注解@EnableHystrix

说明:

  1. @EnableHystrix 注解可以使用@EnableCircuitBreaker注解来替代,代码如下:

    @EnableCircuitBreaker
    @EnableDiscoveryClient
    @SpringBootApplication
    public class UserConsumerApplication {...}
  2. 在springboot中提供了@SpringCloudApplication来定义spring cloud应用,他整合了多个注解,主要包含服务发现和断路器这两个注解,代码如下:

    @SpringCloudApplication
    public class UserConsumerApplication{...}

第三步: 修改Controller


@RestController
public class HelloController {


    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/call")
    @HystrixCommand(fallbackMethod = "whenCallError")
    public String call(){
       return restTemplate.getForEntity("http://user-provider/hello",String.class).getBody();
    }
    
    public String whenCallError(){
        return "远程服务发生错误了,该功能暂时不可用.";
    }

}

代码中,call方法增加了注解@HystrixCommand(fallbackMethod = "whenCallError"),表示若远程微服务消费不成功,则执行fallbackMethod所指定的方法,这叫服务回退,也叫服务的降级

注意

fallbackMethod所指定的方法的返回类型必须是call方法的返回类型兼容。

第四步: 启动消费者应用,访问http://localhost:3000/call

发现在正常情况下未有任何影响,但是若服务提供者因某些原因无法正常被消费,比如服务提供者宕机不可访问时,直接响应的是
spring cloud基础教程

使用Feign实现声明式REST调用

我们在使用Spring Cloud 伈bbon 时, 通常都会利用它对RestTemplate 的请求拦截来实现对依赖服务的接口调用, 而RestTemplate 已经实现了对HTTP 请求的封装处理, 形成了一套模板化的调用方法。在之前的例子中,我们只是简单介绍了RestTemplate 调用的实现,但是在实际开发中,由于对服务依赖的调用可能不止于一处,往往一个接口会被多处调用,所以我们通常都会针对各个微服务自行封装一些客户端类来包装这些依赖服务的调用。这个时候我们会发现, 由于RestTemplate 的封装, 几乎每一个调用都是简单的模板化内容。综合上述这些情况, Spring Cloud Feign 在此基础上做了进一步封装, 由它来帮助我们定义和实现依赖服务接口的定义。在Spring Cloud Feign 的实现下, 我们只需创建一个接口并用注解的方式来配置它, 即可完成对服务提供方的接口绑定, 简化了在使用Spring Cloud伈bbon 时自行封装服务调用客户端的开发量。

Spring Cloud Feign是一套基于Netflix Feign实现的声明式服务调用客户端。它使得编写Web服务客户端变得更加简单。我们只需要通过创建接口并用注解来配置它就可完成对Web服务接口的绑定。它具备可插拔的注解支持,包括Feign注解、JAX-RS注解。它也支持可插拔的编码器和解码器。Spring Cloud Feign还扩展了对Spring MVC注解的支持,同时还整合了Ribbon和Eureka来提供均衡负载的HTTP客户端实现。

下面,我们通过一个例子来展现Feign如何方便的声明对eureka-client服务的定义和调用。

下面的例子,我们将利用之前构建的eureka-server作为服务注册中心、user-provider作为服务提供者作为基础。而基于Spring Cloud Ribbon实现的消费者,我们可以根据user-consumer实现的内容进行简单改在就能完成,具体步骤如下:

添加Feign依赖

user-consumer中添加Feign依赖:

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

Finchley之前版本,Fenign的artifactId为:<artifactId>spring-cloud-starter-feign</artifactId>

修改启动类

修改应用启动类。通过@EnableFeignClients注解开启扫描Spring Cloud Feign客户端的功能,同时取消掉RestTEmplate的bean定义,当然如果在你的应用中仍然想用的话也可以继续保留。

@EnableFeignClients
@SpringCloudApplication
public class UserConsumerApplication {

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

创建Feign的客户端接口定义。

创建一个Feign的客户端接口定义,使用@FeignClient注解来指定这个接口所要调用的服务名称,接口中定义的各个函数使用Spring MVC的注解就可以来绑定服务提供方的REST接口,比如下面就是绑定user-provider服务的/hello接口的例子:

@FeignClient(name = "user-provider")
public interface HelloClient {

    @GetMapping("/hello")
    String hello();

}

修改HelloController

修改Controller。通过定义的feign客户端来调用服务提供方的接口:


@RestController
public class HelloController {



    @Autowired
    private HelloClient helloClient;

    @GetMapping("/call")
    @HystrixCommand(fallbackMethod = "whenCallError")
    public String call(){
        return helloClient.hello();
    }

    public String whenCallError(){
        return "远程服务发生错误了,该功能暂时不可用.";
    }

}

可以看到通过Spring Cloud Feign来实现服务调用的方式更加简单了,通过@FeignClient定义的接口来统一的声明我们需要依赖的微服务接口。而在具体使用的时候就跟调用本地方法一点的进行调用即可。由于Feign是基于Ribbon实现的,所以它自带了客户端负载均衡功能,也可以通过Ribbon的IRule进行策略扩展。另外,Feign还整合的Hystrix来实现服务的容错保护,在Finchley版本中,Feign的Hystrix默认是打开的的。但是在在Dalston版本中,Feign的Hystrix默认是关闭的。

在完成了上面的代码编写之后,读者可以将eureka-server、user-provider、user-consumer都启动起来,然后访问http://localhost:3000/call ,来跟踪观察user-consumer服务是如何消费user-provider服务的/hello接口的,并且也可以通过启动多个user-provider服务来观察其负载均衡的效果。

示例:实现用户的完整RESTFul API

在前面的示例中,服务提供者都是提供的get方式的请求,并且未携带参数,但在实际应用场景中,这是完全不能满足业务需求的,接下来我们通过一个对用户进行CRUD的示例来看看如何实现其他的请求方法,以及参数的传递。

前置工作

因为在user-provider和user-consumer中,均需要使用到相同的POJO对象User,所以我们建立一个公共的maven模块,在这里面定义User.java,并在user-provider和user-consumer中进行引用。

其实不仅是User.java,在分布式微服务应用领域,有很多通用的java类,我们都可以将其从各个子模块中抽象出来,建立成一个单独的模块,然后在需要使用的地方引入即可。

  1. 新建一个空的maven项目,其pom.xml内容如下:
    ```XML
    <?xml version="1.0" encoding="UTF-8"?>

    4.0.0

     <groupId>com.dengcl</groupId>
     <artifactId>springcloud-base</artifactId>
     <version>1.0-SNAPSHOT</version>

    ```
  2. 新建java类User.java,其代码如下:
    ```JAVA
    package com.dengcl.springcloud.pojo;
    /**
    • Description:
    • User: tangbak
    • Date: 2018-03-15
    • Time: 16:07
      */
      public class User {
      private Long id;
      private String name;
      private Integer age;
      public User() {
      }
      public User(String name, Integer age) {
      this.name = name;
      this.age = age;
      }
      public Long getId() {
      return id;
      }
      public void setId(Long id) {
      this.id = id;
      }
      public String getName() {
      return name;
      }
      public void setName(String name) {
      this.name = name;
      }
      public Integer getAge() {
      return age;
      }
      public void setAge(Integer age) {
      this.age = age;
      }
      }
    ```
  3. 通过mvn install发布springcloudbase到本地maven资源库
  4. 在user-provider和user-comsumer中的'pom.xml'中添加依赖
<dependency>
 <groupId>com.dengcl</groupId>
 <artifactId>springcloud-base</artifactId>
 <version>1.0-SNAPSHOT</version>
</dependency>

服务提供者提供用户相关的服务

在user-provider中添加UserController.java,其代码如下:

package com.dengcl.userprovider.controller;
import com.dengcl.springcloud.pojo.User;
import org.springframework.web.bind.annotation.*;

import java.util.*;
@RestController
@RequestMapping(value="/users")     // 通过这里配置使下面的映射都在/users下
public class UserController {
 
    // 创建线程安全的Map 
    static Map<Long, User> users = Collections.synchronizedMap(new HashMap<Long, User>());

    @RequestMapping(value={""}, method=RequestMethod.GET)
    public List<User> getUserList() {
        List<User> r = new ArrayList<User>(users.values());
        return r;
    }

    @RequestMapping(value="", method= RequestMethod.POST)
    public String postUser(@RequestBody User user) {
        users.put(user.getId(), user);
        return "success";
    }

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public User getUser(@PathVariable Long id) {
        return users.get(id);
    }

    @RequestMapping(value="/{id}", method=RequestMethod.PUT)
    public String putUser(@PathVariable Long id, @RequestBody User user) {
        User u = users.get(id);
        u.setName(user.getName());
        u.setAge(user.getAge());
        users.put(id, u);
        return "success";
    }

    @RequestMapping(value="/{id}", method=RequestMethod.DELETE)
    public String deleteUser(@PathVariable Long id) {
        users.remove(id);
        return "success";
    }
}

在这个示例中,我们通过一个静态的线程安全的Map来存储用户信息,这种存储方式仅适用于服务提供者不采用集群的时候。当然完全能满足我们现在要实现的目标。

实现消费者功能

编写UserClient,代码如下:
@FeignClient("user-provider")
public interface UserClient {

    @GetMapping("/users")
    List<User> getUserList() ;

    @PostMapping("/users")
    public String postUser(@RequestBody User user) ;

    @GetMapping("/users/{id}")
    public User getUser(@PathVariable("id") Long id) ;

    @PutMapping("/users/{id}")
    public String putUser(@PathVariable("id") Long id, @RequestBody User user) ;

    @DeleteMapping("/users/{id}")
    public String deleteUser(@PathVariable("id") Long id) ;

}

特别注意:

  1. 虽然Feign兼容SpringMVC的注解,但是有一点特别要注意:FeignClient接口中,如果使用到@PathVariable ,必须指定其value,不能省略,必须指定。
  2. 若get请求方法的参数是复杂对象,请求不会成功,只要参数是复杂对象,即使指定了是GET方法,feign依然会以POST方法进行发送请求。
编写UserConsumerController,代码如下:

@RestController
@RequestMapping("/userconsumer")
public class UserConsumerController {

    @Autowired
    private UserClient userClient;


    @RequestMapping(value={""}, method= RequestMethod.GET)
    public List<User> getUserList() {
        return userClient.getUserList();
    }

    @RequestMapping(value="", method= RequestMethod.POST)
    public String postUser(@RequestBody User user) {
        return userClient.postUser(user);
    }

    @RequestMapping(value="/{id}", method=RequestMethod.GET)
    public User getUser(@PathVariable Long id) {
        return userClient.getUser(id);
    }

    @RequestMapping(value="/{id}", method=RequestMethod.PUT)
    public String putUser(@PathVariable Long id, @RequestBody User user) {
       return userClient.putUser(id,user);
    }

    @RequestMapping(value="/{id}", method=RequestMethod.DELETE)
    public String deleteUser(@PathVariable Long id) {
        return userClient.deleteUser(id);
    }
}

OK,接下来可以通过PostMan来测试这些接口是否正确了。

Feign整合Hystrix实现服务降级

在前面介绍Hystrix时,我们通过@HystrixCommand(fallbackMethod = "whenCallError")实现了服务的降级处理,但是如果用Feign客户端的话,那么又如何来实现服务降级呢?

定义Feign client接口的实现类

代码如下:


@Component
public class UserClientFallback implements UserClient {
    @Override
    public List<User> getUserList() {
        return new ArrayList<>();
    }
    @Override
    public String postUser(User user) {
        return "error";
    }
    @Override
    public User getUser(Long id) {
        User user = new User();
        user.setId(id);
        user.setName("unknown");
        return user;
    }
    @Override
    public String putUser(Long id, User user) {
        return "error";
    }
    @Override
    public String deleteUser(Long id) {
        return "error";
    }
}

注意这个类需要@Component注解把它加入到spring 容器。

修改Feign client接口

其实只需要修改这个接口的@FeignClient注解就可以了。具体如下:


@FeignClient(name = "user-provider",fallback = UserClientFallback.class)
public interface UserClient {

}

开启Hystrix

在Spring Cloud Feign中,除了引入了用于客户端负载均衡的Spring Cloud Ribbon之外,还引入了服务保护与容错的工具Hystrix。默认情况下,Spring Cloud Feign会为将所有Feign客户端的方法都封装到Hystrix命令中进行服务保护。

默认情况下,Spring Cloud Feign会为将所有Feign客户端的方法都封装到Hystrix命令中进行服务保护。这个说法在Finchley版本之前没有错,但是在Finchley版本中正好相反,在该版本中,Feign客户端的Hystrix熔断器是默认关闭的,需要我们手动开启。

开启的方式为在application.yml中增加:

###  开启全局的hystrix熔断器,在Finchley版本中是默认关闭的,其他版本默认打开
feign:
  hystrix:
    enabled: true

测试

现在我们关闭user-provider应用,然后测试user-consumer的User相关接口,发现熔断器已经工作。



服务网关

通过之前Spring Cloud中几个核心组件的介绍,我们已经可以构建一个简略的(不够完善)微服务架构了。比如下图所示:

spring cloud基础教程

我们使用Spring Cloud Netflix中的Eureka实现了服务注册中心以及服务注册与发现;而服务间通过Ribbon或Feign实现服务的消费以及均衡负载;通过Spring Cloud Config实现了应用多环境的外部化配置以及版本管理。为了使得服务集群更为健壮,使用Hystrix的融断机制来避免在微服务架构中个别服务出现异常时引起的故障蔓延。

在该架构中,我们的服务集群包含:内部服务Service A和Service B,他们都会注册与订阅服务至Eureka Server,而Open Service是一个对外的服务,通过均衡负载公开至服务调用方。本文我们把焦点聚集在对外服务这块,这样的实现是否合理,或者是否有更好的实现方式呢?

先来说说这样架构需要做的一些事儿以及存在的不足:

  • 首先,破坏了服务无状态特点。为了保证对外服务的安全性,我们需要实现对服务访问的权限控制,而开放服务的权限控制机制将会贯穿并污染整个开放服务的业务逻辑,这会带来的最直接问题是,破坏了服务集群中REST API无状态的特点。从具体开发和测试的角度来说,在工作中除了要考虑实际的业务逻辑之外,还需要额外可续对接口访问的控制处理。
  • 其次,无法直接复用既有接口。当我们需要对一个即有的集群内访问接口,实现外部服务访问时,我们不得不通过在原有接口上增加校验逻辑,或增加一个代理调用来实现权限控制,无法直接复用原有的接口。

面对类似上面的问题,我们要如何解决呢?下面进入正题:服务网关!

为了解决上面这些问题,我们需要将权限控制这样的东西从我们的服务单元中抽离出去,而最适合这些逻辑的地方就是处于对外访问最前端的地方,我们需要一个更强大一些的均衡负载器,它就是本文将来介绍的:服务网关。

服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

下面我们通过实例例子来使用一下Zuul来作为服务的路有功能。

准备工作

在构建服务网关之前,我们先准备一下网关内部的微服务,我们直接使用前几篇编写的内容:

  • eureka-server
  • user-provider
  • user-consumer

启动以上3个服务,其中eureka-server和user-provider以集群的方式启动,此处不再累述。

所有的准备工作就以就绪,下面我们来试试使用Spring Cloud Zuul来实现服务网关的功能。

构建服务网关

第一步:使用Spring Cloud Zuul来构建服务网关的基础步骤非常简单,我们可以直接使用IDEA工具的Spring Initilizr向导建立,其他步骤省略,仅展示选取zuul的截图如下:

spring cloud基础教程

第二步:确保pom.xml中有关于zuul和eureka client的依赖,因为zuul本身也将作为一个微服务注册到服务注册中心。

<!--ZUUL 依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!--eureka client依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

第三步:修改启动类,将启动类定义为spring cloud的启动类,同时开启zuul代理支持,即在启动类中使用两个注解:@SpringCloudApplication@EnableZuulProxy

第四步:修改配置文件application.yml,定义端口、应用名称、eureka server注册中心地址,内容如下:

spring:
  application:
    name: api-gateway
server:
  port: 10000
eureka:
  client:
    serviceUrl:
      defaultZone: http://peer1:1111/eureka/,http://peer2:1112/eureka/

到这里,一个基于Spring Cloud Zuul服务网关就已经构建完毕。启动该应用,一个默认的服务网关就构建完毕了。由于Spring Cloud Zuul在整合了Eureka之后,具备默认的服务路由功能,即:当我们这里构建的api-gateway应用启动并注册到eureka之后,服务网关会发现上面我们启动的两个服务user-provideruser-consumer,这时候Zuul就会创建两个路由规则。每个路由规则都包含两部分,一部分是外部请求的匹配规则,另一部分是路由的服务ID。针对当前示例的情况,Zuul会创建下面的两个路由规则:

  • 转发到user-provider服务的请求规则为:/user-provider/**
  • 转发到user-consumer服务的请求规则为:/user-consumer/**

最后,我们可以通过访问10000端口的服务网关来验证上述路由的正确性:

  • 比如访问:http://localhost:10000/user-consumer/call ,该请求将最终被路由到user-consumer的/call接口上。

通过上面的构建内容,我们已经为所有内部服务提供了一个统一的对外入口,同时对于服务的路由都是自动创建了,减少了传统方式大量的运维配置工作。

zuul配置

过滤器

通过前面的学习,我们已经能够实现请求的路由功能,所以我们的微服务应用提供的接口就可以通过统一的API网关入口被客户端访问到了。但是,每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都需要有一定的限制,系统并不会将所有的微服务接口都对它们开放。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会被毫无保留地转发到具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后的系统维护难度,因为同一个系统中的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式会使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。

在完成了剥离之后,有不少开发者会直接在微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了鉴权逻辑的分离,并没有在本质上将这部分不属于业余的逻辑拆分出原有的微服务应用,冗余的拦截器或过滤器依然会存在。

对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用的接口开发和测试复杂度也得到了相应的降低。

为了在API网关中实现对客户端请求的校验,我们将需要使用到Spring Cloud Zuul的另外一个核心功能:过滤器

Zuul允许开发者在API网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承ZuulFilter抽象类并实现它定义的四个抽象函数就可以完成对请求的拦截和过滤了。

过滤器的实现

比如下面的代码,我们定义了一个简单的Zuul过滤器,它实现了在请求被路由之前检查HttpServletRequest的请求头中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回401 Unauthorized错误。

@Component
public class AccessFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre";
    }
    @Override
    public int filterOrder() {
        return 0;
    }
    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("accessToken");
        if(StringUtils.isBlank(token)){
            //令zuul过滤该请求,不对其进行路由
            ctx.setSendZuulResponse(false);
            //设置了其返回的错误码
            ctx.setResponseStatusCode(HttpServletResponse.SC_UNAUTHORIZED);
        }

        return null;
    }
}

在上面实现的过滤器代码中,我们通过继承ZuulFilter抽象类并重写了下面的四个方法来实现自定义的过滤器。这四个方法分别定义了:

  • filterType:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,具体如下:
    • pre:可以在请求被路由之前调用。
    • routing:在路由请求时候被调用。
    • post:在routing和error过滤器之后被调用。
    • error:处理请求时发生错误时被调用。
  • filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。通过int值来定义过滤器的执行顺序,数值越小优先级越高。
  • shouldFilter:判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。
  • run:过滤器的具体逻辑。这里我们通过ctx.setSendZuulResponse(false)令zuul过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)对返回body内容进行编辑等。

在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的Bean才能启动该过滤器,此处直接在类上增加了注解@Component

在对api-gateway服务完成了上面的改造之后,重新启动它,并发起下面的请求,对上面定义的过滤器做一个验证:

利用postMan构建一个url为http://localhost:10000/user-consumer/call的get请求,不带请求头accessToken时的结果如下图,可见返回了401错误。
spring cloud基础教程

接下来请求头中带上accessToken的结果如下图,可见正确的路由到了user-consume的/call接口,并返回了结果。

spring cloud基础教程

到这里,对于Spring Cloud Zuul过滤器的基本功能就以介绍完毕。可以根据自己的需要在服务网关上定义一些与业务无关的通用逻辑实现对请求的过滤和拦截,比如:签名校验、权限校验、请求限流等功能。


消息总线-spring cloud bus

在微服务架构的系统中, 我们通常会使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接上来, 由于该主题中产生的消息会被所有实例监听和消费, 所以我们称它为消息总线。在总线上的各个实例都可以方便地广播一些需要让其他连接在该主题上的实例都知道的消息, 例如配置信息的变更或者其他一些管理操作等。

由于消息总线在微服务架构系统中被广泛使用, 所以它同配置中心一样, 几乎是微服务架构中的必备组件。Spring Cloud 作为微服务架构综合性的解决方案,对此自然也有自己的实现, 这就是本章我们将要具体介绍的Spring Cloud Bus。通过使用Spring Cloud Bus,可以非常容易地搭建起消息总线,同时实现了一些消息总线中的常用功能,比如,配合Spring Cloud Config 实现微服务应用配置信息的动态更新等。

在本章中, 我们将从消息代理的基础开始, 由浅入深地介绍如何使用Spring Cloud Bus构建微服务架构中的消息总线。

消息代理

消息代理(Message Broker) 是一种消息验证、传输、路由的架构模式。它在应用程序之间起到通信调度并最小化应用之间的依赖的作用, 使得应用程序可以高效地解耦通信过程。消息代理是一个中间件产品, 它的核心是一个消息的路由程序, 用来实现接收和分发消息,并根据设定好的消息处理流来转发给正确的应用。它包括独立的通信和消息传递协议, 能够实现组织内部和组织间的网络通信。设计代理的目的就是为了能够从应用程序中传入消息, 并执行一些特别的操作,下面这些是在企业应用中, 我们经常需要使用消息代理的场景:

  • 将消息路由到一个或多个目的地。
  • 消息转化为其他的表现方式。
  • 执行消息的聚集、消息的分解, 并将结果发送到它们的目的地, 然后重新组合响应
    返回给消息用户。
  • 调用Web服务来检索数据。
  • 响应事件或错误。
  • 使用发布-订阅模式来提供内容或基于主题的消息路由。

目前已经有非常多的开源产品可以供大家使用, 比如:

  • ActiveMQ
  • Kafka
  • RabbitMQ
  • RocketMQ
  • ...

当前版本的Spring Cloud Bus仅支待两款中间件产品: RabbitMQ和Kafka。在下面的章节中, 我们将介绍如何使用RabbitMQ与Spring Cloud Bus配合实现消息总线。

RabbitMQ实现消息总线

RabbitMQ是实现了高级消息队列协议CAMQP)的开源消息代理软件, 也称为面向消息的中间件。RabbitMQ服务器是用高性能、可伸缩而闻名的Erlang语言编写而成的, 其
集群和故障转移是构建在开放电信平台框架上的。

AMQP是Advanced Message Queuing Protocol的简称,它是一个面向消息中间件的开放式标准应用层协议。它定义了以下这些特性:

  • 消息方向
  • 消息队列
  • 消息路由(包括点到点和发布-订阅模式)
  • 可靠性
  • 安全性

AMQP要求消息的提供者和客户端接收者的行为要实现对不同供应商可以用相同的方式(比如SMTP、HTTP、FTP等)进行互相操作。在以往的中间件标准中, 主要还是建立在API级别, 比如JMS, 集中于通过不同的中间件实现来建立标准化的程序间的互操作性, 而不是在多个中间件产品间实现互操作性。

AMQP与JMS不同,JMS定义了一个API和一组消息收发必须实现的行为,而AMQP是一个线路级协议。线路级协议描述的是通过网络发送的数据传输格式。因此,任何符合该数据格式的消息发送和接收工具都能互相兼容和进行操作,这样就能轻易实现跨技木平台的架构方案。

RabbitMQ以AMQP协议实现, 所以它可以支持多种操作系统、多种编程语言, 几乎可以覆盖所有主流的企业级技术平台。在微服务架构消息中间件的选型中, 它是一个非常适合且优秀的选择。 因此, 在SpringCloudB us中包含了对Rabbit的自动化默认配置, 在下面的章节中, 我们将先从RabbitMQ的基础安装和使用开始, 循序渐进地学习如何与SprinCg loudB us进行整合实现消息总线。

RabbitMQ基本概念

在开始具体实践之前, 我们先介绍一些关于RabbitMQ的基本概念,.

  • Broker: 可以理解为消息队列服务器的实体, 它是一个中间件应用, 负责接收消息生产者的消息, 然后将消息发送至消息接收者或者其他的Broker
  • Exchange: 消息交换机, 是消息第一个到达的地方, 消息通过它指定的路由规则,分发到不同的消息队列中去。
  • Queue: 消息队列, 消息通过发送和路由之后最终到达的地方, 到达Queue的消息即进入逻辑上等待消费的状态。每个消息都会被发送到一个或多个队列。
  • Binding: 绑定, 它的作用就是把ExchangeQueue按照路由规则绑定起来, 也就是ExchangeQueue之间的虚拟连接。
  • Routing Key: 路由关键字,Exchange根据这个关键字进行消息投递。
  • Virtual host: 虚拟主机, 它是对Broker的虚拟划分, 将消费者、生产者和它们依赖的AMQP相关结构进行隔离,一般都是为了安全考虚。比如,我们可以在一个Broker中设置多个虚拟主机, 对不同用户进行权限的分离。
  • Connection: 连接, 代表生产者、消费者、Broker之间进行通信的物理网络。
  • Channel: 消息通道,用千连接生产者和消费者的逻辑结构。在客户端的每个连接里,可建立多个Channel, 每个Channel代表一个会话任务, 通过Channel可以隔离同一连接中的不同交互内容。
  • Producer: 消息生产者, 制造消息并发送消息的程序。
  • Consumer: 消息消费者, 接收消息并处理消息的程序。

消息投递到队列的整个过程大致如下:

  1. 客户端连接到消息队列服务器, 打开一个Channel
  2. 客户端声明一个Exchange, 并设置相关属性。
  3. 客户端声明一个Queue, 并设置相关属性。
  4. 客户端使用Routing Key, 在ExchangeQueue之间建立好绑定关系。
  5. 客户端投递消息到Exchange
  6. Exchange接收到消息后,根据消息的Key和已经设置的Binding,进行消息路由,将消息投递到一个或多个Queue里。

Exchange也有几种类型。

  1. Direct交换机:完全根据Key进行投递。比如,绑定时设置了Routing Key为abc,那么客户端提交的消息,只有设置了Key为abc 的才会被投递到队列。
  2. Topic交换机:对Key进行模式匹配后进行投递,可以使用符号#匹配一个或多个词,符号*匹配正好一个词。比如,abc.#匹配abc.def.ghi,abc.*只匹配abc.def 。
  3. Fanout交换机:不需要任何Key,它采取广播的模式,一个消息进来时,投递到与该交换机绑定的所有队列。

RabbitMQ支持消息的待久化,也就是将数据写在磁盘上。为了数据安全考虑,大多数情况下都会选择持久化。消息队列持久化包括3个部分:

  1. Exchange 持久化,在声明时指定durable => 1
  2. Queue 待久化,在声明时指定durable => 1
  3. 消息持久化,在投递时指定delivery_mode => 2 (1是非持久化)。

如果Exchange和Queue都是持久化的,那么它们之间的Binding也是持久化的。如果Exchange和Queue两者之间有一个是待久化的,一个是非持久化的,就不允许建立绑定。

windows下安装RabbitMQ

由于Rabbit MQ 是建立在强大的Erlang OTP平台上,因此我们需要先安装Erlang,然后在安装RabbitMQ.

  1. 安装Erlang, 通过官方下载页面http://www.erlang.org/downloads获取exe安装包, 直接打开并完成安装。
  2. 安装RabbitMQ,通过官方下载页面https://www.rabbitmq.com/download.html获取exe安装包。
  3. 下载完成后, 直接运行安装程序。
  4. RabbitMQServer安装完成之后,会自动注册为服务, 并以默认配置进行启动。
    spring cloud基础教程

在Windows的安装过程中, 有时候会碰到服务启动失败的情况, 通常都是由于windows用户名为中文, 导致默认的db和log目录访问出现问题。要解决该问题, 需要先卸载RabbitMQ Server, 然后设置环境变量RABBITMQ BASE 为一个不含中文的路径, 比如E:\server\rabbitmq。最后, 重新安装RabbitMQ即可。

Rabbit管理

我们可以直接通过访问配置文件进行管理, 也可以通过访问Web进行管理。下面将介绍如何通过Web进行管理。
在命令行执行rabbitmq-plugins enable rabbitmq management 命令, 开启Web管理插件, 这样就可以通过浏览器来进行管理了。

打开浏览器并访问http://localhost:15672/, 并使用默认用户guest登录,密码也为guest。可以看到如下图所示的管理页面:

spring cloud基础教程

从图中我们可以看到之前提到的一些基本概念, 比如Connections、Channels、Exchanges、Queues 等。可以点开各项看看都有些什么内容, 熟悉一下RabbitMQ Server 的服务端。

  • 单击Admin 选项卡, 如下图所示, 可以尝试创建一个名为dengcl的用户。

spring cloud基础教程

其中, Tags 标签是RabbitMQ 中的角色分类, 共有下面几种。

  • none: 不能访问management plugin。
  • management: 用户可以通过AMQP 做的任何事外加如下内容。
    • 列出自己可以通过AMQP 登入的virtual hosts。
    • 查看自己的virtual hosts 中的queues、exchanges 和bindings。
    • 查看和关闭自己的channels 和connections。
    • 查看有关自己的virtual hosts 的“ 全局” 统计信息, 包含其他用户在这些virtual hosts 中的活动。
  • policymaker: management 可以做的任何事外加如下内容。
    • 查看、创建和删除自己的virtual hosts 所属的policies 和parameters。
  • monitoring: management 可以做的任何事外加如下内容。
    • 列出所有virtual hosts, 包括它们不能登录的virtual hosts。
    • 查看其他用户的connections 和channels。
    • 查看节点级别的数据, 如clustering 和memory 的使用情况。
    • 查看真正的关于所有virtual hosts的全局的统计信息。
  • administrator: policymaker和monitoring可以做的任何事外加如下内容。
    • 创建和删除virtual hosts。
    • 查看、创建和删除users。
    • 查看、创建和删除permissions。
    • 关闭其他用户的connections。

快速入门

接下来,我们通过在Spring Boot应用中整合RabbitMQ, 实现一个简单的发送、接收消息的例子来对RabbitMQ有一个直观的感受和理解。

在SpringBoot中整合RabbitMQ是一件非常容易的事,因为之前我们已经介绍过Starter
POMs, 其中的AMQP模块就可以很好地支持RabbitMQ, 下面我们就来详细说说整合过程。

创建生产者

  • 新建一个SpringBoot工程, 命名为rabbitmq-sender
  • pom.xml中引入如下依赖内容, 其中spring-boot-starter-amqp用于支持RabbitMQ。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • application.properties中配置关于RabbitMQ的连接和用户信息,这里使用之前安装时创建的dengcl

#服务名称
spring.application.name=rabbitmq-sender

###############################
#### RabbitMQ服务器相关配置#####
###############################

#rabbitmq server地址,默认localhost
spring.rabbitmq.host=localhost
#rabbitmq server端口,默认5672
spring.rabbitmq.port=5672
#rabbitmq server用户名,默认guest
spring.rabbitmq.username=dengcl
#rabbitmq server密码,默认guest
spring.rabbitmq.password=123456
  • 创建消息生产者Sender。通过注入AmqpTemplate接口的实例来实现消息的发送AmqpTemplate 接口定义了一套针对AMQP协议的基础操作。在Spring Boot中会根据配置来注入其具体实现。在该生产者中,我们会产生一个字符串, 并发送到名为hello的队列中。
@Slf4j  ///
@Component
public class Sender {

    //队列名称
    public static final String QUEUE_NAME="hello";

    @Autowired
    private AmqpTemplate amqpTemplate;

    public void sendMsg(String msg){
        log.info("准备发送消息到RabbitMQ Server.消息是{}",msg);
        amqpTemplate.convertAndSend(QUEUE_NAME,msg);
    }

}
  • 创建单元测试类, 用来调用消息生产。
@RunWith(SpringRunner.class)
@SpringBootTest
public class RabbitmqSenderApplicationTests {

    public static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    @Autowired
    private Sender sender;

    @Test
    public void testSend(){
        sender.sendMsg("我发送了一个消息,现在的时间是:"+sdf.format(new Date()));
    }
}

创建消费者

  • 新建一个SpringBoot工程, 命名为rabbitmq-receiver
  • pom.xml中引入如下依赖内容, 其中spring-boot-starter-amqp用于支持RabbitMQ。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • application.properties中配置关于RabbitMQ的连接和用户信息,这里使用之前安装时创建的dengcl

#服务名称
spring.application.name=rabbitmq-receiver

###############################
#### RabbitMQ服务器相关配置#####
###############################

#rabbitmq server地址,默认localhost
spring.rabbitmq.host=localhost
#rabbitmq server端口,默认5672
spring.rabbitmq.port=5672
#rabbitmq server用户名,默认guest
spring.rabbitmq.username=dengcl
#rabbitmq server密码,默认guest
spring.rabbitmq.password=123456

以上步骤和创建消息发布者一致。

  • 创建消息消费者Receiver。通过@RabbitListener 注解定义该类对hello队列的监听, 并用@Rabb江Handler 注解来指定对消息的处理方法。所以,该消费者实现了对hello队列的消费, 消费操作为输出消息的字符串内容。
@Slf4j
@Component
@RabbitListener(queues = "hello")
public class Receiver {

    @RabbitHandler
    public void process(String msg){
        log.info("接收到RabbitMQ中的queue为hello的消息,消息的内容是:{}",msg);
    }
}

测试

  1. 通过启动类运行rabbitmq-receiver;从控制台中,我们可看到如下内容,程序创建了一个访问127.0.0.1:5672中dengcl的连接。

    2018-03-29 14:40:42.225  INFO 11444 --- [cTaskExecutor-1] o.s.a.r.c.CachingConnectionFactory       : Created new connection: rabbitConnectionFactory#13d73fa:0/aaa@qq.com [delegate=amqp://aaa@qq.com:5672/, localPort= 61925]
    
    同时,我们通过RabbitMQ 的控制面板,可以看到Connections 和Channels中包含当前连接的条目。
    spring cloud基础教程
    spring cloud基础教程
  2. 运行rabbitmq-sender的测试类。我们可以在控制台中看到下面的输出内容, 消息被发送到了RabbitMQ Server 的hello 队列中。

    2018-03-29 14:41:02.266  INFO 16316 --- [           main] com.dengcl.rabbitmqsender.mq.Sender      : 准备发送消息到RabbitMQ Server.消息是我发送了一个消息,现在的时间是:2018-03-29 14:41:02
  3. 切换到rabbitmq-receiver应用主类的控制台,我们可以看到类似如下的输出,消费者对hello 队列的监听程序执行了, 并输出了接收到的消息信息。

    2018-03-29 14:41:02.424  INFO 11444 --- [cTaskExecutor-1] com.dengcl.rabbitmqreceiver.mq.Receiver  : 接收到RabbitMQ中的queue为hello的消息,消息的内容是:我发送了一个消息,现在的时间是:2018-03-29 14:41:02
    

通过上面的示例,我们在Spring Boot 应用中引入spring-boot-starter-amqp模块, 进行简单配置就完成了对RabbitMQ 的消息生产和消费的开发内容。然而在实际应用中, 还有很多内容没有演示, 比如之前提到的一些概念: 交换机、路由关键字、绑定、虚拟主机等, 这里不做更多的讲解, 大家可以自行查阅RabbitMQ 的官方教程, 其中有更全面的讲解。 我们需要重点理解的是, 在整个生产消费过程中, 生产和消费是一个异步操作,这也是在分布式系统中要使用消息代理的重要原因,以此我们可以使用通信来解耦业务逻辑。在这个例子中, 可以进一步做一些测试, 比如,不运行消费者,先运行生产者, 此时可以看到在RabbitMQServer管理页面的Queues选项卡下多了一些待处理的消息, 这时我们再启动消费者, 它就会处理这些消息, 所以通过生产消费模式的异步操作, 系统间调用就没有同步调用需要那么高的实时性要求, 同时也更容易控制处理的吞吐量以保证系统的正常运行等。

整合spring cloud bus

在上一节中, 我们已经介绍了关于消息代理、AMQP以及RabbitMQ的基础知识和使用方法,并且在spring boot中应用。在下面的内容中, 我们开始具体介绍SpringCloud Bus的配置。

服务发现中心

直接使用前面章节的eureka-server作为服务发现中心。并启动服务发现中心。

消息生产者

  1. 创建消息生产者微服务项目microservice-msg-sender,并添加相关依赖。依赖信息如下:

    <!--eureka 客户端依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
    <!--spring bus rabbitmq依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    
    <!--为了使用@Slf4j注解-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
  2. 主类添加@EnableDiscoveryClient注解
  3. 配置application.properties,当然你也可以用application.yml,内容如下
    ```properties
    #应用端口号
    server.port=3000
    #服务名称
    spring.application.name=microservice-msg-sender
    #注册到服务注册中心
    eureka.client.service-url.defaultZone=http://localhost:1000/eureka/

    ###############################
    #### RabbitMQ服务器相关配置#####
    ###############################

    #rabbitmq server地址,默认localhost
    spring.rabbitmq.host=localhost
    #rabbitmq server端口,默认5672
    spring.rabbitmq.port=5672
    #rabbitmq server用户名,默认guest
    spring.rabbitmq.username=dengcl
    #rabbitmq server密码,默认guest
    spring.rabbitmq.password=123456

    ###绑定spring cloud bus的rabbitmq消息通道的exchange名称
    spring.cloud.stream.bindings.rabbitmq_channel_output.destination=exchangeName
    ```

  4. 创建spring cloud bus的通道配置接口RabbitSendChannel,内容如下:

    public interface RabbitSendChannel {
    
        String rabbitmqChannelName = "rabbitmq_channel_output";
    
        @Output(rabbitmqChannelName)
        MessageChannel output();
    }
    

    特别说明:
    接口中的rabbitmqChannelName的值和application.properties中定义的spring.cloud.stream.bindings.rabbitmq_channel_output.destination=exchangeNamerabbitmq_channel_output内容必须一致。

  5. 创建消息生产业务类RabbitMQSendService并通过@EnableBinding注解绑定通道接口。
    ```JAVA
    @Slf4j
    @EnableBinding(RabbitSendChannel.class)
    public class RabbitMQSendService {

     @Autowired
     private RabbitSendChannel rabbitSendChannel;
     //发送消息的业务
     public boolean sendMsg(String msg){
         log.info("准备发送消息到rabbitmq server,消息内容是:{}",msg);
         return rabbitSendChannel.output().send(MessageBuilder.withPayload(msg).build());
     }

    }

    ```
  6. 创建一个定时任务类SendMsgTask,定时发送消息,当然在实际应用中具体发送消息的事件需要根据业务来定义。
    ```JAVA
    @Slf4j
    @Component
    public class SendMsgTask {
    private final static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

     @Autowired
     private RabbitMQSendService sendService;
    
     //每隔5秒发送一条消息
     @Scheduled(fixedDelay = 5000L)
     public void process(){
         String msg = String.format("我是消息的生产者,我在[%s]生成了一条信息。",sdf.format(new Date()));
         log.info("SEND :{}",msg);
         sendService.sendMsg(msg);
     }

    }
    ``> **注意** > 应用主类中要增加注解@EnableScheduling`来开启定时任务支持。

  7. 启动应用主类,可以在控制台上看到以下信息:
    2018-03-29 15:41:38.585 INFO 16868 --- [ask-scheduler-4] c.d.m.mq.RabbitMQSendService : 准备发送消息到rabbitmq server,消息内容是:我是消息的生产者,我在[2018-03-29 15:41:38]生成了一条信息。 2018-03-29 15:41:43.587 INFO 16868 --- [ask-scheduler-4] c.d.m.task.SendMsgTask : SEND :我是消息的生产者,我在[2018-03-29 15:41:43]生成了一条信息。 2018-03-29 15:41:43.587 INFO 16868 --- [ask-scheduler-4] c.d.m.mq.RabbitMQSendService : 准备发送消息到rabbitmq server,消息内容是:我是消息的生产者,我在[2018-03-29 15:41:43]生成了一条信息。

此时在rabbitmq的控制面板可以看到:

[image_1c9ocl9odqu42qtrcs1h17lqn4j.png-119.5kB][31]

消息消费者

  1. 创建消息消费者微服务项目microservice-msg-receiver,并添加相关依赖。依赖信息如下:

    <!--eureka 客户端依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    
    <!--spring bus rabbitmq依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bus-amqp</artifactId>
    </dependency>
    
    <!--为了使用@Slf4j注解-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
  2. 主类添加@EnableDiscoveryClient注解

  3. 配置application.properties,当然你也可以用application.yml,内容如下
    ```properties
    server.port=4000
    #服务名称
    spring.application.name=microservice-msg-reveciver
    #注册到服务注册中心
    eureka.client.service-url.defaultZone=http://localhost:1000/eureka/

    ###############################
    #### RabbitMQ服务器相关配置#####
    ###############################

    #rabbitmq server地址,默认localhost
    spring.rabbitmq.host=localhost
    #rabbitmq server端口,默认5672
    spring.rabbitmq.port=5672
    #rabbitmq server用户名,默认guest
    spring.rabbitmq.username=dengcl
    #rabbitmq server密码,默认guest
    spring.rabbitmq.password=123456

    ###绑定spring cloud bus的rabbitmq消息通道的exchange名称
    spring.cloud.stream.bindings.rabbitmq_channel_input.destination=exchangeName
    ```

  4. 创建spring cloud bus的通道配置接口RabbitReceiverChannel,内容如下:

    public interface RabbitMQReciveChannel {
    
        String rabbitmqChannelName = "rabbitmq_channel_input";
    
        @Input(rabbitmqChannelName)
        SubscribableChannel input();
    }
    

    特别说明:

    1. 接口中的rabbitmqChannelName的值和application.properties中定义的spring.cloud.stream.bindings.rabbitmq_channel_input.destination=exchangeNamerabbitmq_channel_input内容必须一致。
    2. 与消息生产者的rabbitmqChannelName的值不能一样。
  5. 创建消息生产业务类RabbitMQSendService并通过@EnableBinding注解绑定通道接口。并且提供一个方法,通过@StreamListener注解监听指定通道的新的消息。

    @Slf4j
    @EnableBinding(RabbitMQReciveChannel.class)
    public class RabbitMQReciveService {
    
        @Autowired
        private RabbitMQReciveChannel reciveChannel;
    
        @StreamListener(RabbitMQReciveChannel.rabbitmqChannelName)
        public void reciveMsg(Message<String> msg){
            log.info("RECEIVER:{}",msg.getPayload());
        }
    
    }
    

    #### 测试结果

    1. 启动服务注册中心
    2. 启动消息消费者
    3. 启动消息生产者
    4. 查看结果。

其他说明

在这个示例中,仅仅是展示了spring cloud bus如何整合rabbitmq,更多关于rabbitmq的使用请参考rabbitmq的官方网站。

分布式配置中心

Spring Cloud Config是Spring Cloud团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密/解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。Spring Cloud Config实现了对服务端和客户端中环境变量和属性配置的抽象映射,所以它除了适用于Spring构建的应用程序之外,也可以在任何其他语言运行的应用程序中使用。由于Spring Cloud Config实现的配置中心默认采用Git来存储配置信息,所以使用Spring Cloud Config构建的配置服务器,天然就支持对微服务应用配置信息的版本管理,并且可以通过Git客户端工具来方便的管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如:SVN仓库、本地化文件系统。

快速入门

在本文中,我们将学习如何构建一个基于Git存储的分布式配置中心,并对客户端进行改造,并让其能够从配置中心获取配置信息并绑定到代码中的整个过程

准备配置仓库

准备一个git仓库,可以在码云或Github上创建都可以。仓库示例:https://gitee.com/dengcl/spring-cloud-config-server/

构建配置中心

通过Spring Cloud Config 构建一个分布式配置中心非常简单, 只需要以下三步:

  • 创建一个基础的Spring Boot 工程, 命名为config-server, 并在pom.xml 中引

入下面的依赖:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-config-server</artifactId>
</dependency>

<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>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>
  • 创建Spring Boot 的程序主类, 并添加@EnableConfigServer 注解, 开启SpringCloud Config 的服务端功能。
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(ConfigServerApplication.class, args);
   }
}
  • 在`application.properties中添加配置服务的基本信息以及Git 仓库的相关信息, 如下所示:

    spring.cloud.config.server.git.uri=https://gitee.com/dengcl/spring-cloud-config-server
    spring.cloud.config.server.git.username=dengcl
    spring.cloud.config.server.git.password=dcl745120
    
    spring.application.name=config-server
    
    server.port=7000

其中Git 的配置信息分别表示如下内容。

  • spring.cloud.config.server.git.uri: 配置Git 仓库位置。
  • spring.cloud.config.server.git.searchPaths: 配置仓库路径下的相对搜索位置, 可以配置多个。
  • spring.cloud.config.server.git.username: 访问Git 仓库的用户名。
  • spring.cloud.config.server.git.password: 访问Git 仓库的用户密码。

到这里, 使用一个通过Spring Cloud Config实现, 并使用Git 管理配置内容的分布式配置中心就完成了。我们可以将该应用先启动起来, 确保没有错误产生, 然后进入下面的学习内容。

配置规则详解

为了验证上面完成的分布式配置中心config-server, 根据Git 配置信息中指定的仓库位置, 在https://gitee.com/dengcl/spring-cloud-config-server下创建了/config_repo目录作为配置仓库, 并根据不同环境新建下面4个配置文件:

  • config-client.properties
  • config-client-dev.properties

  • config-client-test.properties

  • config-client-prod.properties

在这4个配置文件中均设置了一个username属性, 并为每个配置文件分别设置了不同的值, 如下所示:

  • username=dengcl
  • username=dengcl_dev
  • username=dengcl_test
  • username=dengcl_prod

为了测试版本控制,在该Git仓库的master 分支中,我们为username属性加入1.0 的后缀, 同时创建一个config-label-test 分支, 并将各配置文件中的值用2.0 作为后缀。
完成了这些准备工作之后, 我们就可以通过浏览器、POSTMAN或CURL等工具直接来访问我们的配置内容了。访问配置信息的URL与配置文件的映射关系如下所示:

  • /{application}/{profile}[/{label}]
  • /{application}-{profile}.yml
  • /{label}/{application}-{profile}.yml
  • /{application}-{profile}.properties
  • /{label}/{application}-{profile}.properties

上面的url会映射{application}-{profile}.properties对应的配置文件,其中{label}对应Git上不同的分支,默认为master。我们可以尝试构造不同的url来访问不同的配置内容,比如,要访问master分支,config-client应用的dev环境,就可以访问这个url:http://localhost:7000/config-client/dev/master,并获得如下返回:

{
    "name": "config-client",
    "profiles": [
        "dev"
    ],
    "label": "master",
    "version": "804509ea362b52de23ca06f627087301002aa6d0",
    "state": null,
    "propertySources": [
        {
            "name": "https://gitee.com/dengcl/spring-cloud-config-server/config_repo/config-client-dev.properties",
            "source": {
                "username": "dengcl_dev_1.0"
            }
        },
        {
            "name": "https://gitee.com/dengcl/spring-cloud-config-server/config_repo/config-client.properties",
            "source": {
                "username": "dengcl_1.0"
            }
        }
    ]
}

我们可以看到该Json中返回了应用名:config-client,环境名:dev,分支名:master,以及default环境和dev环境的配置内容。

同时, 我们可以看到config-server 的控制台中还输出了下面的内容,配置服务器在从Git 中获取配置信息后, 会存储一份在config-server 的文件系统中, 实质上config-server是通过git clone 命令将配置内容复制了一份在本地存储, 然后读取这些内容并返回给微服务应用进行加载。

2018-03-30 11:05:00.663  INFO 4884 --- [nio-7000-exec-5] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springaaa@qq.com7e88bbb: startup date [Fri Mar 30 11:05:00 CST 2018]; root of context hierarchy
2018-03-30 11:05:00.677  INFO 4884 --- [nio-7000-exec-5] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: file:/C:/Users/dengcl/AppData/Local/Temp/config-repo-8571982357257432495/config_repo/config-client-dev.properties
2018-03-30 11:05:00.677  INFO 4884 --- [nio-7000-exec-5] o.s.c.c.s.e.NativeEnvironmentRepository  : Adding property source: file:/C:/Users/dengcl/AppData/Local/Temp/config-repo-8571982357257432495/config_repo/config-client.properties
2018-03-30 11:05:00.677  INFO 4884 --- [nio-7000-exec-5] s.c.a.AnnotationConfigApplicationContext : Closing org.springaaa@qq.com7e88bbb: startup date [Fri Mar 30 11:05:00 CST 2018]; root of context hierarchy

config-server通过Git 在本地仓库暂存,可以有效防止当Git 仓库出现故障而引起无法加载配置信息的情况。我们可以通过断开网络, 再次发起http://localhost:7000/config-client/test/master请求,可以看到, config-server 提示无法从远程获取该分支内容的报错信息, 但是它依然会为该请求返回配置内容, 这些内容源于之前访问时存于config-server 本地文件系统中的配置内容。

构建客户端

在完成了上述验证之后,确定配置服务中心已经正常运作,下面我们尝试如何在微服务应用中获取上述的配置信息。

  • 创建一个Spring Boot应用,命名为config-client,并在pom.xml中引入下述依赖:
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
  • 创建bootstrap.yml配置,来指定获取配置文件的config-server-git位置,例如:
spring:
  application:
    name: config-client
  cloud:
    config:
      uri: http://localhost:7000/
      profile: default
      label: master

server:
  port: 8000

上述配置参数与Git中存储的配置文件中各个部分的对应关系如下:

  • spring.application.name:对应配置文件规则中的{application}部分
  • spring.cloud.config.profile:对应配置文件规则中的{profile}部分
  • spring.cloud.config.label:对应配置文件规则中的{label}部分
  • spring.cloud.config.uri:配置中心config-server的地址

这里需要格外注意:上面这些属性必须配置在bootstrap.yml中,当然也可以是bootstrap.properties,这样config-server中的配置信息才能被正确加载。

在完成了上面的代码编写之后,将config-serverconfig-client都启动起来,然后访问http://localhost:2001/info ,我们可以看到该端点将会返回从git仓库中获取的配置信息:

  • 创建测试用例
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConfigClientApplicationTests {
   @Value("${username}")
   private String username;

   @Test
   public void test(){
      System.err.println(username);
   }
}
  • 运行测试用例,可以看到正确的读取到配置中心的用户信息

配置中心的高可用

传统作法

通常在生产环境,Config Server与服务注册中心一样,我们也需要将其扩展为高可用的集群。在之前实现的config-server基础上来实现高可用非常简单,不需要我们为这些服务端做任何额外的配置,只需要遵守一个配置规则:将所有的Config Server都指向同一个Git仓库,这样所有的配置内容就通过统一的共享文件系统来维护,而客户端在指定Config Server位置时,只要配置Config Server外的均衡负载即可,就像如下图所示的结构:

spring cloud基础教程

注册为服务

虽然通过服务端负载均衡已经能够实现,但是作为架构内的配置管理,本身其实也是可以看作架构中的一个微服务。所以,另外一种方式更为简单的方法就是把config-server也注册为服务,这样所有客户端就能以服务的方式进行访问。通过这种方法,只需要启动多个指向同一Git仓库位置的config-server就能实现高可用了。

首先启动eureka实现的服务注册中心应用server-center

接下来实现配置管理服务端和客户端,配置过程非常简单,具体如下:

config-server配置
  • pom.xmldependencies节点中引入如下依赖,相比之前的config-server就加入了spring-cloud-starter-netflix-eureka-client,用来注册服务。
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
  • application.properties中配置参数eureka.client.serviceUrl.defaultZone以指定服务注册中心的位置,详细内容如下:
# 配置服务注册中心
eureka.client.serviceUrl.defaultZone=http://peer1:1111/eureka/
  • 在应用主类中,新增@EnableEurekaClient注解,用来将config-server注册到上面配置的服务注册中心上去。
@EnableEurekaClient
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {

   public static void main(String[] args) {
      SpringApplication.run(ConfigServerApplication.class, args);
   }
}
  • 启动该应用,并访问http://localhost:1111/,可以在Eureka Server的信息面板中看到config-server已经被注册了。
config-client配置
  • config-server一样,在pom.xmldependencies节点中新增spring-cloud-starter-netflix-eureka-client依赖,用来注册服务:
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • bootstrap.properties中,按如下配置:
spring.application.name=config-client
server.port=8000

eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

spring.cloud.config.discovery.enabled=true
spring.cloud.config.discovery.serviceId=config-server
spring.cloud.config.profile=dev

其中,通过eureka.client.serviceUrl.defaultZone参数指定服务注册中心,用于服务的注册与发现,再将spring.cloud.config.discovery.enabled参数设置为true,开启通过服务来访问Config Server的功能,最后利用spring.cloud.config.discovery.serviceId参数来指定Config Server注册的服务名。这里的spring.application.namespring.cloud.config.profile如之前通过URI的方式访问时候一样,用来定位Git中的资源。

  • 在应用主类中,增加@EnableEurekaClient注解,用来发现config-server服务,利用其来加载应用配置
@EnableEurekaClient
@SpringBootApplication
public class ConfigClientApplication {

   public static void main(String[] args) {
      SpringApplication.run(ConfigClientApplication.class, args);
   }
}
  • 创建一个TestController,其代码如下:
@RestController
public class TestController {


    @Value("${username}")
    private String userName;

    @GetMapping("/show")
    public String show(){
        return this.userName;
    }
}
  • 完成了上述配置之后,我们启动该客户端应用。若启动成功,访问http://localhost:1111/,可以在Eureka Server的信息面板中看到该应用已经被注册成功了。

  • 访问客户端应用提供的服务:http://localhost:8000/show,此时,我们会返回在Git仓库中config-client.properties文件配置的username属性内容dengcl_1.0

配置刷新

有时候,我们需要对配置内容做一些实时更新的场景,那么Spring Cloud Config是否可以实现呢?答案显然是可以的。下面,我们看看如何进行改造来实现配置内容的实时更新。

在改造程序之前,我们先将config-serverconfig-client都启动起来,并访问客户端提供的REST APIhttp://localhost:8000/show来获取配置信息,可以获得返回内容为:dengcl_dev_1.0。接着,我们可以尝试使用Git工具修改当前配置的内容,比如,将config-repo/config-client-dev.properties中的username的值从username=dengcl_dev_1.0修改为username=dengcl_dev_1.0,再访问http://localhost:8000/show,可以看到其返回内容还是username=dengcl_dev_1.0

下面,我们将在config-client端增加一些内容和操作以实现配置的刷新:

  • 在config-client的pom.xml中新增spring-boot-starter-actuator监控模块,其中包含了/actuator/refresh刷新API。
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 在config-client的bootstrap.properties中开启/actuator/refresh端点。
management.endpoints.web.exposure.include=refresh
  • 为需要在/actuator/refresh时需要刷新的bean添加注解@RefreshScope,在本例中需要被刷新的是TestController,因为我们需要在这里面注入配置中心的属性。
@RestController
@RefreshScope
public class TestController {


    @Value("${username}")
    private String userName;

    @GetMapping("/show")
    public String show(){
        return this.userName;
    }
}
  • 重新启动config-client,访问一次http://localhost:8000/show,可以看到当前的配置值
  • 修改Git仓库`config-repo/config-client-dev.properties文件中username的值
  • 再次访问一次http://localhost:8000/show,可以看到配置值没有改变
  • 通过POST请求发送到http://localhost:8000/actuator/refresh,我们可以看到返回内容如下,代表username参数的配置内容被更新了
[
    "config.client.version",
    "username"
]

除此之外,我们还可以通过git仓库的web hook来功能进行关联,当有Git提交变化时,就给对应的配置主机发送/actuator/refresh请求来实现配置信息的实时更新。但是,这种方式不仅对网络拓扑有需求,同时当我们的系统发展壮大之后,维护这样的刷新清单也将成为一个非常大的负担,而且很容易犯错,那么有什么办法可以解决这个复杂度呢?其实可以通过Spring Cloud Bus来实现以消息总线的方式进行通知配置信息的变化,完成集群上的自动化更新。

基于消息中间件实现配置参数自动刷新

在上一节的示例中,虽然我们已经能够通过/actuator/refresh接口和Git仓库的Web Hook来实现Git仓库中的内容修改触发应用程序的属性更新。但是, 若所有触发操作均需要我们手工去维护Web Hook中的应用配置的话, 随着系统的不断扩展, 会变得越来越难以维护, 而消息代理中间件是解决该问题最为合适的方案。是否还记得我们在介绍消息代理中的特点时提到过这样一个功能: 消息代理中间件可以将消息路由到一个或多个目的地。利用这个功能, 我们就能完美地解决该问题, 下面来说说SpringCloud Bus中的具体实现方案。

下面我们来具体动手尝试整个配置过程。

  • 准备工作: 这里我们不创建新的应用, 但需要用到前面已经实现的关于SpringCloudConfig的几个工程。

    • config-repo: 定义在Git仓库中的一个目录,其中存储了应用名为中config-client的多环境配置文件, 配置文件中有一个username参数。
    • eureka-server: 基于eureka实现的服务注册中心。
    • config-server: 配置中心服务器,配置了Git仓库, 并注册到了Eureka的服务端。
    • config-client: 通过Eureka发现ConfigServer的客户端, 应用名为config-client, 用来访问配置服务器以获取配置信息。该应用中提供了一个/show接口, 它会获取config-repo/config-client-dev.properties中的username属性并返回。
  • 扩展config-client应用

    • 修改pom.xml,添加spring-cloud-starter-bus-amqp模块依赖,注意spring-boot-starter-actuator也是必须的,用来添加提供刷新节点。
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>


    org.springframework.cloud
    spring-cloud-starter-bus-amqp

    ```

    • 在配置文件中增加关千RabbitMQ的连接和用户信息,同时打开bus-refresh端点。

          #配置rabbitmq连接信息
      spring.rabbitmq.host=localhost
      spring.rabbitmq.port=5672
      spring.rabbitmq.username=dengcl
      spring.rabbitmq.password=123456
      #打开/bus-refresh节点
      management.endpoints.web.exposure.include=bus-refresh
    • 启动config-server, 再启动两个config-client分别在不同的端口上, 比如8000、8001)。我们可以在config-client中的控制台中看到如下内容, 在启动时, 客户端程序多了一个/actuator/bus-refresh请求。

      2018-03-30 16:41:16.895  INFO 15376 --- [           main] s.b.a.e.w.s.WebMvcEndpointHandlerMapping : Mapped "{[/actuator/bus-refresh],methods=[POST]}" onto public java.lang.Object org.springframework.boot.actuate.endpoint.web
      .servlet.AbstractWebMvcEndpointHandlerMapping$OperationHandler.handle(javax.servlet.http.HttpServletRequest,java.util.Map<java.lang.String, java.lang.String>)
      
    • 先访问两个config-client/show请求, 会返回当前config-repo/config-client-dev.properties中的username属性。

    • 接着, 修改config-repo/config-client-dev.properties中的username属性值,
      并发送POST请求到config-client中的一个/actuator/bus-refresh

    • 最后, 再分别访问启动的两个config-client/show请求, 此时这两个请求都会返回最新的username属性。

      到这里, 我们已经能够通过SpringCloud Bus来实时更新总线上的属性配置了。并且可以有效的结合GIT仓库的web hook实现配置信息的全自动更新。

分布式服务跟踪.

通过之前学习,实际上我们已经能够通过使用它们搭建起一个基础的微服务架构系统来实现我们的业务需求了。但是,随着业务的发展,我们的系统规模也会变得越来越大,各微服务间的调用关系也变得越来越错综复杂。通常一个由客户端发起的请求在后端系统中会经过多个不同的微服务调用来协同产生最后的请求结果,在复杂的微服务架构系统中,几乎每一个前端请求都会形成一条复杂的分布式服务调用链路,在每条链路中任何一个依赖服务出现延迟过高或错误的时候都有可能引起请求最后的失败。这时候对于每个请求全链路调用的跟踪就变得越来越重要,通过实现对请求调用的跟踪可以帮助我们快速的发现错误根源以及监控分析每条请求链路上的性能瓶颈等好处。

针对上面所述的分布式服务跟踪问题,Spring Cloud Sleuth提供了一套完整的解决方案。在本章中,我们将详细介绍如何使用Spring Cloud Sleuth来为我们的微服务架构增加分布式服务跟踪的能力。