spring cloud基础教程

Srping cloud


基础知识

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

什么是微服务架构

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

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

与单体系统的区别

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

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

如何实施微服务

在实施微服务以前, 咱们必需要知道, 微服务虽然有很是多吸引人的优势, 可是也由于服务的拆分引起了诸多本来在单体应用中没有的问题。spring

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

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

微服务架构的九大特性

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

服务组件化

组件, 是一个能够独立更换和升级的单元。就像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.SR六、Brix ton.SR5 等, 为何Spring Cloud 没有像其余Spring 的项目使用相似l.x.x 的版本命名规则呢?这些版本之间又有什么区别呢?在学习之
初,很是有必要弄清楚这些版本的意义和内容, 这样才能在咱们使用Spring Cloud 时, 指导咱们选择更为合适的版本进行架构与开发。

版本名与版本号

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

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

通过上面的解释, 不难猜出, 以前所提到的AngelS.SR六、BrixtonS.SR5中的SR六、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 的具体实例清单。可是随着业务的发展, 系统功能愈来愈复杂, 相应的微服务应用也不断增长, 咱们的静态配置就会变得愈来愈难以维护。而且面对不断发展的业务, 咱们的集群规模、服务的位置、服务的命名等都有可能发生变化, 若是仍是经过手工维护的方式,那么极易发生错误或是命名冲突等问题。同时,对于这类静态内容的维护
也必将消耗大量的人力。

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

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

mark

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

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

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

  • 服务注册表:是服务注册组件的核心,它用来记录各微服务的信息,例如微服务的名称、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集群三者的关系,如图:

image_1c91473tt5mn1ctfmv77ov1skq16.png-5.6kB

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

Eureka架构

Eureka架构图以下

image_1c9147u9f5ej68n5v3n3g19901j.png-45.4kB

这是来自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
image_1c914sdvu122k1us11gqs19961lk020.png-60.3kB

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

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

image_1c91528vi1gfp19fc2651k2biog30.png-40.9kB

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

image_1c9155hik7pb13841e2a1m851k6p3d.png-55kB

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

创建好的Eureka Server目录结构以下图所示:

image_1c915a6hf2bpogo1e7b1kfl1bkj4q.png-27.6kB

说明: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命令执行

image_1c9165gko12j4k59iov1ls2107j77.png-4.8kB

出现以下所示信息时,表示服务注册中心已经启动成功。
image_1c9166nvk1n6u1og3sjr487187e7k.png-11.7kB

打开浏览器,输入地址:http://localhost:8761访问,咱们会发现此时尚未服务注册到Eureka上面,以下图:

image_1c9169bitu0dbk0707ed1mub81.png-107.5kB

该页面展现了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打包以后运行,在控制台中输出以下内容:

image_1c934ctcs1api166u1m7a1887178sm.png-15kB

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

image_1c934hjfuhp2pa8i411jto1qrf2j.png-4.6kB

则表示服务提供者正确在服务注册中心注册了。如今咱们再次经过http://localhost:8761来访问服务注册中心,以下图所示:
image_1c934jm0s1tt9hnge48a2q3k33.png-102.7kB

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 访问,结果以下:

image_1c938gfh618be1hhruu31diiatm3g.png-9.4kB

高可用服务注册中心

在微服务架构这样的分布式环境中咱们须要充分考虑发生故障的状况, 因此在生产环境中必须对各个组件进行高可用部署, 对于微服务如此,对于服务注册中心也同样。可是到本节为止,咱们一直都在使用单节点的服务注册中心,这在生产环境中显然并不合适,咱们须要构建高可用的服务注册中心以加强系统的可用性。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)。

image_1c93l1a1o1gi7chisnf1ik1q8c54.png-121.8kB

服务注册与发现

在设置了多节点的服务注册中心以后,咱们只须要简单的服务配置,就能将服务注册到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个注册中心,咱们将peer一、peer二、peer3各自都将serviceUrl指向另外两个节点。换言之,peer一、peer二、peer3是两两互相注册的。启动三个服务注册中心,并将user-provider的serviceUrl指向peer1并启动,能够得到以下图所示的集群效果。
    image_1c93a5co9cdk16dcp011bp31rkc4n.png-20.1kB

访问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。

image_1c93lsq4kgp6vfd1v2e9km1net5h.png-45.6kB

使用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配合使用时的大体架构。

image_1c93mmthd1tip10ku1ngn8hvbhc5u.png-55.1kB

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注解开启了负载均衡。
在默认的状况下,负载均衡策略是线性轮询的方式,也就是说在客户端获取到的服务列表中依次交替,例如开启了三个服务server一、server二、server3,那么在线性轮询时,就会按这个顺序来调用。
我以前是开启了三个服务,一个端口是2000,2001和2002,那么在以前的这种状况下,若是我关闭其中一个服务,就好比这里关闭2001端口的服务,当再次访问的时候,每访问三次,就会有一次是以下的error page,直到我挂掉的这个服务被服务注册中心剔除前均会存在。

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

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

雪崩效应

微服务架构的应用系统一般包含多个服务层。微服务之间经过网络进行通讯,从而支撑整个应用系统,所以,微服务之间不免存在依赖关系。咱们知道任何微服务都并不是100%可用,网络每每也很脆弱,所以不免有些请求会失败。

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

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

image_1c93tti5r1mufe5r1fmj1g241c146o.png-106.7kB

如何容错

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

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

image_1c93vj469uqnl42h1p1vdt3j882.png-96.6kB

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

发如今正常状况下未有任何影响,可是若服务提供者因某些缘由没法正常被消费,好比服务提供者宕机不可访问时,直接响应的是
image_1c95khlsd17ss11rs14cvcco1r09.png-8.8kB

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


    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中几个核心组件的介绍,咱们已经能够构建一个简略的(不够完善)微服务架构了。好比下图所示:

image_1c986bjo7p8i14cua109egaja9.png-79.3kB

咱们使用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的截图以下:

image_1c98786oo1paf17toc16g241274m.png-58kB

第二步:确保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错误。
image_1c98dndneb72it37j3dapikp13.png-27kB

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

image_1c98dq4u9vkd3061sb4mljsu11g.png-31.4kB

到这里,对于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安装完成以后,会自动注册为服务, 并以默认配置进行启动。
    image_1c9o4lufh15pr166k1j5i47lvsn9.png-8.4kB

在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。能够看到以下图所示的管理页面:

image_1c9o4u3ek5md7o21p911i92fgj16.png-121.4kB

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

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

image_1c9o51uff1npp1sae11qrjbt17nj1j.png-95.2kB

其中, 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/SimpleConnection@6ddac9a1 [delegate=amqp://dengcl@127.0.0.1:5672/, localPort= 61925]
    同时,咱们经过RabbitMQ 的控制面板,能够看到Connections 和Channels中包含当前链接的条目。
    image_1c9o859501ka718pukhn12kb182o9.png-40.7kB
    image_1c9o85rfeec617d3192bhcn1942m.png-43.5kB
  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.springframework.context.annotation.AnnotationConfigApplicationContext@7e88bbb: 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.springframework.context.annotation.AnnotationConfigApplicationContext@7e88bbb: 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外的均衡负载便可,就像以下图所示的结构:

mark

注册为服务

虽然经过服务端负载均衡已经可以实现,可是做为架构内的配置管理,自己其实也是能够看做架构中的一个微服务。因此,另一种方式更为简单的方法就是把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来为咱们的微服务架构增长分布式服务跟踪的能力。

相关文章
相关标签/搜索