【系统架构理论】一篇文章搞掂:微服务架构
本文篇幅较长,建议合理利用右上角目录进行查看(如果没有目录请刷新)。
本文基于《Spring 微服务实战》一书进行总结和扩展,大家也可以自行研读此书。
一、Spring、云计算、微服务简介
1.1、什么是微服务
单体架构:
-
- 在微服务概念逐步形成前,大部分Web应用都是基于单体架构风格进行构建。
- 单体架构风格往往是几个团队维护一份共同代码,生成一个单一程序进行运行。
-
- 问题:当应用规模增长,各个团队间的沟通和合作成本也会增大;而当其中一个团队修改代码时,整个应用程序需要重新构建、测试、部署。
微服务:
-
- 微服务的概念在2014年左右在软件开发社区中蔓延
- 一个小的、松耦合的分布式服务;分解和分离应用程序的功能,并使他们彼此独立
微服务架构特征
-
- 逻辑业务分解为明确定义了职责范围的细粒度组件,组件互相协调提供解决方案
- 每个组件有其职责领域,并能完全独立部署
- 微服务应该对业务领域的单个部件负责
- 一个微服务可以跨多个应用程序复用
- 微服务通信基于一些基本原则,采用HTTP或JSON等轻量级通信协议作,在服务的消费者和提供者间进行数据交换
- 微服务小、独立、分布式的特性,使组织拥有明确责任领域的小型开发团队;同样是开发一个应用程序,这些团队各自只需要负责自己做的服务。
1.2、Spring和微服务的关系
- Spring的地位:Java应用的标准开发框架。因为Spring提供了依赖注入的功能,使程序中类之间的关系可以外部管理,为开发企业级应用提供了一个轻量级的解决方案,使其成为了Java程序的一种必然使用的开发框架。
- Spring的更新:Spring团队是一个与时俱进的团队,不断地改造自己的代码迎合时代需求;为相应微服务架构的需求,Spring团队开发出了2个项目:Spring Boot和Spring Cloud
- Spring Boot:对Spring架构理念重新思考的结果:以默认配置的方式简化了原来Spring大量的手工配置。
- Spring Cloud:一个封装了多个流行的云管理微服务框架的公共框架。
1.3、为什么要改变应用架构和选择微服务架构
当前局势:
互联网连接了社会各个部分,连接了全球客户群,带来了全球竞争
-
- 复杂度上升:如今的应用程序不仅要访问自己的数据,更要和互联网和外部服务提供商进行交互和通信。
- 客户要求更快的交付:为了应对瞬息万变的市场,现在的客户追求快速的变化,希望能在几周或者几天发布新功能,而无需等待整个产品发布。
- 性能和伸缩性:全球性的应用程序无法预测事务量的变化,希望能使应用程序跨多个服务器在事务高峰时进行扩大,在高峰后又能进行收缩。
- 高可用性:客户希望不能因为某个部分的故障导致整个系统的崩溃
微服务架构提供的解决方案:
将单体的应用程序,分解为互相独立构建和部署的小型服务,带来的好处是:
-
- 高灵活性:将结构的服务进行组合和重新安排,可快速交付新功能;一个代码单元越小,更容易修改代码,也更容易测试。
- 高弹性:解耦的服务使故障不会蔓延到整个应用程序,导致程序中断。
- 可伸缩性:解耦的服务可以轻松地跨多个服务器进行水平分布,从而适当地对功能/服务进行伸缩。如果是单体应用程序,即使只有小部分遇到瓶颈,都要整个程序进行扩展;而微服务的扩展是局部的,实现成本更低。
1.4、什么是云
现在“云”的概念已经被过度使用。
云:
云是网络、互联网的一种比喻说法。
云计算:
将数据的运算,放到互联网中去。
云计算的3种基本模式:
-
- IaaS(Infrastructure as a Service)基础设施即服务:直接使用互联网中的硬件,如租用硬盘,租用内存,租用服务器;例如我在阿里云上租用了一个服务器,在里面安装自己的数据库,IIS,然后搭建Web应用程序。
- Paas(Platform as a Service)平台即服务:互联网商提供一个完整的软件研发和部署平台,我无需购买硬件和软件,直接在该平台上开发和部署我的应用。
- SaaS(Software as a Service)软件即服务:互联网商提供一套已经研发好,部署好的软件,我直接通过Web来使用他的软件。
新型的模式
-
- Faas(Functions as a Service)函数即服务:将代码块交给互联网商,该代码会在互联网商的计算设施上计算,对外暴露成一种服务,我们直接调用该服务即可。
- Caas(Container as a Service)容器即服务:互联网商提供一些虚拟容器(如Docker),我们把程序放到容器中启动即可。
1.5、为什么选择云+微服务
微服务架构核心概念是:把每一个服务打包和部署为独立的制品。
部署方式的可选项:
-
- 物理服务器:虽然可以,但是物理服务器无法快速提升容量;且在多个物理服务器间水平伸缩微服务成本非常高。
- 虚拟机镜像:基于IaaS,可以在租用的云虚拟机中快速部署和启动服务的多个实例。
- 虚拟容器:基于IaaS,不是直接把服务部署到云虚拟机,而是将Docker容器(或其他容器)部署到虚拟机,再把服务放到虚拟容器中运行。这样可以把单个虚拟机隔离成共享相同虚拟机镜像的一系列独立进程。
云的优势:
-
- 快速:开发人员可以在几分钟内快速启动新的虚拟机和容器。
- 高水平伸缩性:服务容量需求下降,可以关闭虚拟服务器,减少费用;需求上升,可以快速添加服务器和服务实例。
- 高弹性:例如一台微服务遇到问题,可以启动新的服务实例,让程序继续运行,给时间开发团队解决问题。
IaaS+Docker:
我们采用IaaS+Docker作为部署环境
为什么不是基于PaaS:IaaS与供应商无关,PaaS要考虑基于供应商提供的微服务抽象,所以考虑IaaS有更大灵活性,可以跨供应商移植。
1.6、微服务架构的内容
编写单个微服务的代码很简单,但是要开发、运行、支持一个健壮的微服务应用
需要考虑几个主题:
-
- 大小适当:如何正确划分微服务大小,允许快速更改应用程序,降低整个应用程序中断的风险?
- 位置透明:微服务应用程序中,多个服务实例可以快速启动和关闭,如何管理服务调用的物理细节?
- 有弹性:如何绕过失败的服务,确保采用“快速失败”来保护微服务消费者和应用程序的整体完整性?
- 可重复:如何确保提供的每个新服务实例与生产环境中的所有其他服务实例具有相同的配置和代码库?
- 可伸缩:如何使用一部处理和事件,最小化服务之间的直接依赖关系,确保可以优雅地扩展微服务?
从基于模式的方法来考虑解决这些主题(详细内容不是太重点,暂时不讲解这些模式):
-
- 核心微服务开发模式
- 微服务路由模式
- 微服务客户端弹性模式
- 微服务安全模式
- 微服务日志记录和跟踪模式
- 微服务构建和部署模式
1.7、总结与实例
互联网的发展与变化,要求现在的应用程序在高复杂性下仍然能保持快速交付、高弹性、高可用、高伸缩性,微服务架构就是为了实现这个要求所出现的系统架构。
微服务架构,把系统的需求,分割成若干个可以独立运行的应用,并作为服务暴露出来;然后通过服务的注册与发现,使这些服务之间可以互相协作,完成系统任务。
一个微服务框架的例子:Cloud-Admin,开源框架,https://gitee.com/minull/ace-security
架构图:
这里需要补充这个架构图的意义
二、第一步:实现服务注册与发现
服务发现:在分布式计算架构中,我们要调用某台计算机提供的服务时,必须知道这台机器的物理地址,这个称为服务发现。
微服务架构与服务发现:
- 由于应用被拆分为多个微服务,我们可以将这些微服务实例数量进行水平伸缩,并通过服务发现让这些服务参与运算;服务消费者不需要知道服务的真正地址,所以我们可以在服务池中添加和移除服务实例。而单体架构,只能考虑购买更大型、更好的硬件来扩大服务。
- 当某些服务变得不健康或者不可用时,我们可以移除这些服务,服务发现引擎可以绕过这些服务继续运行,使移除服务造成的影响最小化。
我们下面将通过Spring Cloud和Netflix技术,来实现这一个功能。
2.1、一种传统的方式服务位置解析模型
DNS和负载均衡器的传统服务位置解析模型:
缺点:
- 单点故障——虽然负载均衡器可以实现高可用,但这是整个基础设施的单点故障。如果负载均衡器出现故障,那么依赖它的每个应用程序都会出现故障。尽管可以使负载平衡器高度可用,但负载均衡器往往是应用程序基础设施中的集中式阻塞点。
- 有限的水平可伸缩性——在服务集中到单个负载均衡器集群的情况下,跨多个服务器水平伸缩负载均衡基础设施的能力有限。许多商业负载均衡器受两件事情的限制:冗余模型和许可证成本。第一,大多数商业负载均衡器使用热插拔模型实现冗余,因此只能使用单个服务器来处理负载,而辅助负载均衡器仅在主负载均衡器中断的情况下,才能进行故障切换。这种架构本质上受到硬件的限制。第二,商业负载均衡器具有有限数量的许可证,它面向固定容量模型而不是更可变的模型。
- 静态管理——大多数传统的负载均衡器不是为快速注册和注销服务设计的。它们使用集中式数据库来存储规则的路由,添加新路由的唯一方法通常是通过供应商的专有API(Application Programming Interface,应用程序编程接口)来进行添加。
- 复杂——由于负载均衡器充当服务的代理,它必须将服务消费者的请求映射到物理服务。这个翻译层通常会为服务基础设施增加一层复杂度,因为开发人员必须手动定义和部署服务的映射规则。在传统的负载均衡器方案中,新服务实例的注册是手动完成的,而不是在新服务实例启动时完成的。
2.2、适合云的服务发现架构
基于云的微服务环境希望实现的服务发现架构有如下特点:
- 高可用——服务发现需要能够支持“热”集群环境,在服务发现集群中可以跨多个节点共享服务查找。如果一个节点变得不可用,集群中的其他节点应该能够接管工作。
- 点对点——服务发现集群中的每个节点共享服务实例的状态。
- 负载均衡——服务发现需要在所有服务实例之间动态地对请求进行负载均衡,以确保服务调用分布在由它管理的所有服务实例上。在许多方面,服务发现取代了许多早期Web应用程序实现中使用的更静态的、手动管理的负载均衡器。
- 有弹性——服务发现的客户端应该在本地“缓存”服务信息。本地缓存允许服务发现功能逐步降级,这样,如果服务发现服务变得不可用,应用程序仍然可以基于本地缓存中维护的信息来运行和定位服务。
- 容错——服务发现需要检测出服务实例什么时候是不健康的,并从可以接收客户端请求的可用服务列表中移除该实例。服务发现应该在没有人为干预的情况下,对这些故障进行检测,并采取行动。
2.2.1、服务发现架构的共同流程
- 服务注册——当服务实例启动时,它们将通过一个或多个服务发现实例来注册它们可以访问的物理位置、路径和端口。虽然每个服务实例都具有唯一的IP地址和端口,但是每个服务实例都将以相同的服务ID进行注册。服务ID是唯一标识一组相同服务实例的键。
- 服务地址的客户端查找——客户端通过服务发现代理获取服务IP地址。
- 信息共享——服务发现节点之间共享服务实例的健康信息。
- 健康监测——服务向服务发现代理发送心跳包。
2.2.2、增加客户端负载均衡
上面的模型中,每次调用注册的微服务实例时,服务发现引擎就会被调用。这种方法很脆弱,因为服务客户端完全依赖于服务发现引擎来查找和调用服务。
增加客户端负载均衡的服务发现模型:
在这个模型中,当服务消费者需要调用一个服务时:
- 它将联系服务发现服务,获取它请求的所有服务实例,然后在服务消费者的机器上本地缓存数据。
- 每当客户端需要调用该服务时,服务消费者将从缓存中查找该服务的位置信息。通常,客户端缓存将使用简单的负载均衡算法,如“轮询”负载均衡算法,以确保服务调用分布在多个服务实例之间。
- 然后,客户端将定期与服务发现服务进行联系,并刷新服务实例的缓存。客户端缓存最终是一致的,但是始终存在这样的风险:在客户端联系服务发现实例以进行刷新和调用时,调用可能会被定向到不健康的服务实例上。
- 如果在调用服务的过程中,服务调用失败,那么本地的服务发现缓存失效,服务发现客户端将尝试从服务发现代理刷新数据。
2.3、使用Spring Cloud和Netflix实现服务发现架构
使用Spring Cloud和Netflix实现的服务发现架构模型:
-
- 随着服务的启动,许可证和组织服务将通过Eureka服务进行注册。这个注册过程将告诉Eureka每个服务实例的物理位置和端口号,以及正在启动的服务的服务ID。
- 当许可证服务调用组织服务时,许可证服务将使用Netflix Ribbon库来提供客户端负载均衡。Ribbon将联系Eureka服务去检索服务位置信息,然后在本地进行缓存。
- Netflix Ribbon库将定期对Eureka服务进行ping操作,并刷新服务位置的本地缓存。
- 任何新的组织服务实例现在都将在本地对许可证服务可见,而任何不健康实例都将从本地缓存中移除。
2.4、代码实现服务发现架构
2.4.1、构建Eureka服务
Eureka服务的作用是:
提供服务的注册中心。
主POM文件:
添加Spring Cloud的版本管理,添加Spring Boot的Starter依赖和Test依赖。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>xxx</groupId> <artifactId>xxx</artifactId> <packaging>pom</packaging> <version>1.0-SNAPSHOT</version> <modules> <module>base-eureka</module> </modules> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.M8</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
Eureka模块POM文件:
Eureka服务作为项目的一个模块进行添加,并在POM文件中添加Eureka服务器依赖以及Spring Cloud Starter依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>XXX</artifactId> <groupId>XXX</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>base-eureka</artifactId> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter</artifactId> </dependency> </dependencies> </project>
创建Eureka服务程序引导类:
通过@EnableEurekaServer注解,启动Eureka服务器
@SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); } }
添加配置文件:
通过application.yml文件对Eureka服务器进行配置
spring: application: name: eureka server: port: 8761 #启动端口 eureka: client: registerWithEureka: false #false:不作为一个客户端注册到注册中心 fetchRegistry: false #为true时,可以启动,但报异常:Cannot execute request on any known server
启动服务器:
根据配置的端口,通过引导类启动程序后,访问如http://localhost:8761/,即可看到如下画面,说明Eureka服务器启动成功
2.4.2、把RESTful服务注册到Eureka注册服务中去
模块名为:testrest
testrest模块POM文件:
添加Spring Boot Web依赖和Eureka客户端依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>XXX</artifactId> <groupId>XXX</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>base-testrest</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> </project>
testrest服务程序引导类:
@SpringBootApplication public class TestRestApplication { public static void main(String[] args) { SpringApplication.run(TestRestApplication.class, args); } }
testrest控制器:
@RestController @RequestMapping("/test") public class TestController { @RequestMapping(value = "/{id}", method = RequestMethod.GET) public String selectById(@PathVariable String id) { return id; } }
配置文件:
server: port: 8801 #启动端口 spring: application: name: base-testrest #将使用Eureka注册的服务的逻辑名称 eureka: instance: preferIpAddress: true #注册服务的IP,而不是服务器名称 client: registerWithEureka: true #向Eureka注册服务 fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/ #Eureka服务的位置
启动客户端:
先启动Eureka服务器,然后再启动testrest程序引导类,再访问Eureka服务器界面,如http://localhost:8761/,即可看见此服务成功注册到Eureka服务器中了
2.4.3、创建另外一个RESTful服务,对上面2中已注册的服务进行发现和消费
对注册到Eureka的服务进行发现和消费,有几种方式:
- Spring DiscoveryClient
- 启用了RestTemplate的Spring DiscoveryClient
- Netflix Feign客户端
这里仅介绍Feign客户端
模块名为:testrestconsume
testrestconsume模块POM文件:
添加Spring Boot Web依赖、Eureka客户端依赖及Feign依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>XXX</artifactId> <groupId>XXX</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>base-testrestconsume</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies> </project>
testrestconsume服务程序引到类:
添加注解@EnableFeignClients
@SpringBootApplication @EnableFeignClients public class TestRestConsumeApplication { public static void main(String[] args) { SpringApplication.run(TestRestConsumeApplication.class, args); } }
创建接口发现和消费微服务:
一个微服务的多个实例,使用其spring.application.name进行标识;
Feign客户端利用接口和@FeignClient客户端,使用name标识,可以直接调用注册到Eureka中的服务。
@FeignClient("base-testrest") public interface ITestConsumeService { @RequestMapping(value="/test/{id}",method = RequestMethod.GET) public String selectById(@PathVariable("id") String id); }
控制器调用接口:
Feign客户端接口,直接通过@Autowired自动装配即可使用
@RestController @RequestMapping("/testconsume") public class TestConsumeController { @Autowired private ITestConsumeService testConsumeService; @RequestMapping(value = "/{id}", method = RequestMethod.GET) public String selectById(@PathVariable String id) { return testConsumeService.selectById(id); } }
配置文件:
主要需要指明Eureka服务的地址
server: port: 8802 #启动端口 spring: application: name: base-testrestconsume #将使用Eureka注册的服务的逻辑名称 eureka: instance: preferIpAddress: true #注册服务的IP,而不是服务器名称 client: serviceUrl: #拉取注册表的本地副本 defaultZone: http://${EUREKA_HOST:localhost}:${EUREKA_PORT:8761}/eureka/ #Eureka服务的位置
三、第二步:为微服务提供认证和授权
通常一个系统的服务不会完全暴露给所有人使用,而是根据用户的身份、权力来决定是否允许其使用。
我们下面将通过Spring Cloud Security、Spring Security Oauth2和Spring Security Jwt技术,来实现这一个功能。
3.1、OAuth2框架
OAuth2是一个基于令牌的安全验证和授权框架,它将安全性分解为4个部分:
- 受保护资源——开发人员想要保护的资源(如一个微服务),确保只有已通过验证并且具有适当授权的用户才能访问它。
- 资源所有者——资源所有者定义哪些应用程序可以调用其服务,哪些用户可以访问该服务,以及他们可以使用该服务完成哪些事情。资源所有者注册的每个应用程序都将获得一个应用程序名称,该应用程序名称与应用程序**一起标识应用程序。应用程序名称和**的组合是在验证OAuth2令牌时传递的凭据的一部分。
- 应用程序——直接和用户解除的应用程序。通常用户访问应用程序,然后应用程序再调用各个微服务。
- OAuth2验证服务器——OAuth2验证服务器是应用程序和正在使用的服务之间的中间人。OAuth2验证服务器允许用户对自己进行验证,而不必将用户凭据传递给由应用程序代表用户调用的每个服务。
OAuth2运作流程:
- 1、用户访问应用程序,并提交其身份凭证(如帐号密码);
- 2、应用程序把身份凭证传给OAuth2验证服务器;
- 3、OAuth2验证服务器对其身份进行认证,认证通过则返回一个令牌给应用程序;
- 4、应用程序调用服务时,把令牌传给受保护服务;
- 5、受保护服务把令牌传给OAuth2验证服务器,验证令牌对应的用户,是否拥有将要访问资源的权力;
- 6、验证通过,则可以使用对应资源。
OAuth2规范具有以下4种类型的授权,这里仅讨论密码的方式:
- 密码(password);
- 客户端凭据(client credential);
- 授权码(authorization code);
- 隐式(implicit)。
3.2、建立OAuth2验证服务器
主POM文件:
添加Spring Boot Web依赖、Spring Cloud Security依赖及Spring Security Oauth2依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>XXX</artifactId> <groupId>XXX</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>base-authentication</artifactId> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> </dependencies> </project>
程序引导类:
添加验证服务器注解@EnableAuthorizationServer表明这个是验证服务器,添加资源服务器注解@EnableResourceServer表明这个程序提供的资源受到OAuth2保护
@SpringBootApplication @EnableResourceServer @EnableAuthorizationServer public class AuthenticationApplication { public static void main(String[] args) { SpringApplication.run(AuthenticationApplication.class, args); } }
OAuch2配置类:
配置一个AuthorizationServerConfigurerAdapter类,用于配置验证服务注册哪些应用程序
@Configuration public class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; // 因为当前security版本,密码需要以{0}XXXX的方式,增加密码的编码方式在花括号内进行传输 // 所以如果想直接传XXXX的密码,需要加这段代码 @Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("eagleeye")// 允许访问的客户端 .secret("thisissecret")// 密码 .authorizedGrantTypes(// 允许的授权类型 "refresh_token", "password", "client_credentials") .scopes("webclient", "mobileclient");// 引用程序作用域 } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } }
WebSecurity配置类:
配置一个WebSecurityConfigurerAdapter类,用于配置系统有哪些用户,分别是什么角色
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override @Bean public UserDetailsService userDetailsServiceBean() throws Exception { return super.userDetailsServiceBean(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("john.carnell") .password("password1") .roles("USER") .and() .withUser("william.woodward") .password("password2") .roles("USER", "ADMIN"); } }
配置文件:
主要设置端口号和增加了一个url路径前缀
spring:
application:
name: authenticationservice
server:
servlet:
context-path: /auth
port: 8901
令牌验证端点:
创建一个REST端点,用于令牌验证
@RestController public class TokenController { @RequestMapping(value = {"/user"}, produces = "application/json") public Map<String, Object> user(OAuth2Authentication user) { Map<String, Object> userInfo = new HashMap<>(); userInfo.put( "user", user.getUserAuthentication().getPrincipal()); userInfo.put( "authorities", AuthorityUtils.authorityListToSet( user.getUserAuthentication().getAuthorities())); return userInfo; } }
运行:
启动程序后,使用POST方式,可以获取到用户对应令牌
然后使用令牌访问令牌验证端点,获取令牌信息
3.3、设置获取微服务资源需要认证和授权
要对其它微服务进行保护,只需要在微服务上进行一些设置即可;然后访问微服务资源的时候就会根据要求进行令牌验证。
主POM文件:
添加Spring Cloud Security依赖及Spring Security Oauth2依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency>
主程序引导类:
添加资源服务器注解@EnableResourceServer表明这个程序提供的资源受到OAuth2保护
@EnableResourceServer @SpringBootApplication public class TestRestApplication { public static void main(String[] args) { SpringApplication.run(TestRestApplication.class, args); } }
配置文件:
配置OAuth2验证服务器令牌验证服务的地址
security:
oauth2:
resource:
userInfoUri: http://localhost:8901/auth/user
配置ResourceServer配置类:
配置一个ResourceServerConfigurerAdapter配置类,用于定义哪些资源需要什么角色、什么权限才能访问
@Configuration public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception{ // 只要用户通过认证即可访问 http.authorizeRequests().anyRequest().authenticated(); //用户需要有相关角色和权限才能访问 // http // .authorizeRequests() // .antMatchers(HttpMethod.DELETE, "/v1/organizations/**") // .hasRole("ADMIN") // .anyRequest() // .authenticated(); } }
运行:
把测试的微服务以及OAuth2验证服务器都运行起来
访问测试的微服务
当没有令牌时,会提示:
当令牌不正确的时候,会提示:
我们访问验证服务器,传入相关数据,获取一个新的token,并填入上面的Headers-Authorization中,才能正确访问资源
3.4、使用JSON Web Token
OAuth2是一个基于令牌的验证框架,但它并没有为如何定义其规范中的令牌提供任何标准。
为了矫正OAuth2令牌标准的缺陷,一个名为JSON Web Token(JWT)的新标准脱颖而出。
JWT是因特网工程任务组(Internet Engineering Task Force,IETF)提出的开放标准(RFC-7519),旨在为OAuth2令牌提供标准结构。
JWT令牌具有如下特点:
- 小巧——JWT令牌编码为Base64,可以通过URL、HTTP首部或HTTP POST参数轻松传递。
- 密码签名——JWT令牌由颁发它的验证服务器签名。这意味着可以保证令牌没有被篡改。
- 自包含——由于JWT令牌是密码签名的,接收该服务的微服务可以保证令牌的内容是有效的,因此,不需要调用验证服务来确认令牌的内容,因为令牌的签名可以被接收微服务确认,并且内容(如令牌和用户信息的过期时间)可以被接收微服务检查。
- 可扩展——当验证服务生成一个令牌时,它可以在令牌被密封之前在令牌中放置额外的信息。接收服务可以解密令牌净荷,并从它里面检索额外的上下文。
3.4.1、修改配置验证服务器使用JWT
主POM文件:
原来已添加Spring Cloud Security依赖、Spring Security Oauth2依赖,再添加Spring Security JTW依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency>
JWTOAuch2配置类:
去掉原来的OAuch2Config配置类,配置一个新的AuthorizationServerConfigurerAdapter类,里面设置使用JWT作为令牌标准,用于配置验证服务注册哪些应用程序
@Component @Configuration public class ServiceConfig { @Value("${signing.key}") private String jwtSigningKey=""; public String getJwtSigningKey() { return jwtSigningKey; } }
@Configuration public class JWTTokenStoreConfig { @Autowired private ServiceConfig serviceConfig; @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } @Bean @Primary // @Primary注解用于告诉Spring,如果有多个特定类型的bean(在本例中是DefaultTokenService),那么就使用被@Primary标注的bean类型进行自动注入 public DefaultTokenServices tokenServices() { // 用于从出示给服务的令牌中读取数据 DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); return defaultTokenServices; } @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() {// 在JWT和OAuth2服务器之间充当翻译 JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(serviceConfig.getJwtSigningKey());// 定义将用于签署令牌的签名** return converter; } @Bean public TokenEnhancer jwtTokenEnhancer() { return new JWTTokenEnhancer(); } }
public class JWTTokenEnhancer implements TokenEnhancer { // @Autowired // private OrgUserRepository orgUserRepo; // // private String getOrgId(String userName){ // UserOrganization orgUser = orgUserRepo.findByUserName( userName ); // return orgUser.getOrganizationId(); // } @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> additionalInfo = new HashMap<>(); // String orgId = getOrgId(authentication.getName()); String orgId = "id"; additionalInfo.put("organizationId", orgId); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; } }
@Configuration public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private TokenStore tokenStore; @Autowired private DefaultTokenServices tokenServices; @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired private TokenEnhancer jwtTokenEnhancer; // 因为当前security版本,密码需要以{0}XXXX的方式,增加密码的编码方式在花括号内进行传输 // 所以如果想直接传XXXX的密码,需要加这段代码 @Bean public static NoOpPasswordEncoder passwordEncoder() { return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter)); endpoints.tokenStore(tokenStore) // 注入令牌存储 .accessTokenConverter(jwtAccessTokenConverter) // 这是钩子,用于告诉Spring Security OAuth2代码使用JWT .tokenEnhancer(tokenEnhancerChain) // 注入Token扩展器 .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("eagleeye") .secret("thisissecret") .authorizedGrantTypes("refresh_token", "password", "client_credentials") .scopes("webclient", "mobileclient"); } }
其它地方无需变更。
运行:
再次运行获取token的服务,会得到JWT形式的token:
3.4.2、配置微服务使用JWT进行验证
此时,即使不修改微服务,也可以通过验证;因为验证服务器的JWT是基于OAuth2的,所以支持客户端使用OAuth2进行验证。
当然,为了使用一些JWT的特性,例如自包含等,我们需要配置微服务使用JWT。
主POM文件:
原来已添加Spring Cloud Security依赖、Spring Security Oauth2依赖,再添加Spring Security JTW依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-security</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2 --> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-jwt --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> </dependency>
创建令牌存储配置类:
告诉微服务使用JWT作为令牌,并需要设置**和验证服务端对应
@Configuration public class JWTTokenStoreConfig { //JWT @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } //JWT @Bean @Primary public DefaultTokenServices tokenServices() { DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); return defaultTokenServices; } //JWT @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey("123456"); return converter; } }
创建JWTRestTemplate Bean:
因为许可证服务调用组织服务,所以需要确保OAuth2令牌被传播。这项工作通常是通过OAuth2RestTemplate类完成的,但是OAuth2RestTemplate类并不传播基于JWT的令牌。为了确保许可证服务能够做到这一点,需要添加一个自定义的RestTemplate bean来完成这个注入。
@Component public class UserContext { public static final String CORRELATION_ID = "tmx-correlation-id"; public static final String AUTH_TOKEN = "Authorization"; public static final String USER_ID = "tmx-user-id"; public static final String ORG_ID = "tmx-org-id"; private static final ThreadLocal<String> correlationId= new ThreadLocal<String>(); private static final ThreadLocal<String> authToken= new ThreadLocal<String>(); private static final ThreadLocal<String> userId = new ThreadLocal<String>(); private static final ThreadLocal<String> orgId = new ThreadLocal<String>(); public static String getCorrelationId() { return correlationId.get(); } public static void setCorrelationId(String cid) {correlationId.set(cid);} public static String getAuthToken() { return authToken.get(); } public static void setAuthToken(String aToken) {authToken.set(aToken);} public static String getUserId() { return userId.get(); } public static void setUserId(String aUser) {userId.set(aUser);} public static String getOrgId() { return orgId.get(); } public static void setOrgId(String aOrg) {orgId.set(aOrg);} }
@Component public class UserContextFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(UserContextFilter.class); @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; logger.debug("I am entering the licensing service id with auth token: ", httpServletRequest.getHeader("Authorization")); UserContextHolder.getContext().setCorrelationId(httpServletRequest.getHeader(UserContext.CORRELATION_ID)); UserContextHolder.getContext().setUserId(httpServletRequest.getHeader(UserContext.USER_ID)); UserContextHolder.getContext().setAuthToken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN)); UserContextHolder.getContext().setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID)); filterChain.doFilter(httpServletRequest, servletResponse); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }
public class UserContextHolder { private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>(); public static final UserContext getContext(){ UserContext context = userContext.get(); if (context == null) { context = createEmptyContext(); userContext.set(context); } return userContext.get(); } public static final void setContext(UserContext context) { Assert.notNull(context, "Only non-null UserContext instances are permitted"); userContext.set(context); } public static final UserContext createEmptyContext(){ return new UserContext(); } }
public class UserContextInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept( HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpHeaders headers = request.getHeaders(); headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId()); headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken()); return execution.execute(request, body); } }
@Configuration public class JWTRestTemplateConfig { @Primary @Bean public RestTemplate getCustomRestTemplate() { RestTemplate template = new RestTemplate(); List interceptors = template.getInterceptors(); if (interceptors == null) { template.setInterceptors(Collections.singletonList(new UserContextInterceptor())); } else { interceptors.add(new UserContextInterceptor()); template.setInterceptors(interceptors); } return template; } }
运行:
运行方式和OAuth2没有区别,有一个有意思的区别是:
获取到token后,把验证服务器关掉,再使用token去访问微服务,仍然能通过,这是因为JWT是自包含的,并不需要在每个服务联系验证服务器再验证。
四、第三步:使用Spring Cloud和Zuul进行服务路由
需求:在微服务架构这种分布式架构中,需要确保多个服务调用的关键行为正常运作,如安全、日志记录、用户跟踪等。
问题:如果把这些工作分布在各个微服务中实现,有时会忘记,有时需要修改则所有微服务都要修改,显然不现实。
方案:
将服务的这些横切关注点抽象成一个独立的,作为所有微服务调用的过滤器和路由器的服务。这个横切关注点称为服务网关(service gateway)。
客户端不在直接调用服务,而是由服务网管作为单个策略执行点(Policy Enforcement Point,PEP),所有调用通过服务网关进行路由,然后被路由到目的地。
4.1、什么是服务网关
使用服务网关前:难以实现安全性、日志等横切关注点
使用服务网关后:客户端调用服务网关,所有服务的调用交给服务网关进行
这样,微服务架构的横切关注点可以放在服务网关实现,如:
-
- 静态路由——服务网关将所有的服务调用放置在单个URL和API路由的后面。这简化了开发,因为开发人员只需要知道所有服务的一个服务端点就可以了。
- 动态路由——服务网关可以检查传入的服务请求,根据来自传入请求的数据和服务调用者的身份执行智能路由。例如,可能会将参与测试版程序的客户的所有调用路由到特定服务集群的服务,这些服务运行的是不同版本的代码,而不是其他人使用的非测试版程序的代码。
- 验证和授权——由于所有服务调用都经过服务网关进行路由,所以服务网关是检查服务调用者是否已经进行了验证并被授权进行服务调用的自然场所。
- 度量数据收集和日志记录——当服务调用通过服务网关时,可以使用服务网关来收集数据和日志信息,还可以使用服务网关确保在用户请求上提供关键信息以确保日志统一。这并不意味着不应该从单个服务中收集度量数据,而是通过服务网关可以集中收集许多基本度量数据,如服务调用次数和服务响应时间。
4.2、Spring Cloud和Netflix Zuul简介
Spring Cloud集成了Netflix开源项目Zuul。
Zuul提供了许多功能,具体包括以下几个:
- 将应用程序中的所有服务的路由映射到一个URL——Zuul不局限于一个URL。在Zuul中,开发人员可以定义多个路由条目,使路由映射非常细粒度(每个服务端点都有自己的路由映射)。然而,Zuul最常见的用例是构建一个单一的入口点,所有服务客户端调用都将经过这个入口点。
- 构建可以对通过网关的请求进行检查和操作的过滤器——这些过滤器允许开发人员在代码中注入策略执行点,以一致的方式对所有服务调用执行大量操作。
使用Zuul的步骤:
- 1、建立一个Spring Boot项目,并配置Zuul的Maven依赖项:
注意!在Spring Cloud的新版本中,依赖名改成了spring-cloud-starter-netflix-zuul
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> </dependency>
- 2、使用Spring Cloud注解修改这个Spring Boot项目,将其声明为Zuul服务:
@SpringBootApplication @EnableZuulProxy ⇽--- 使服务成为一个Zuul服务器 public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
@EnableZuulProxy与@EnableZuulServer
@EnableZuulServer:使用此注解将创建一个Zuul服务器,它不会加载任何Zuul反向代理过滤器,也不会使用Netflix Eureka进行服务发现(我们将很快进入Zuul和Eureka集成的主题)。
本文只会使用@EnableZuulProxy注解。
- 3、配置Zuul以便Eureka进行通信(可选):
Zuul将自动使用Eureka来通过服务ID查找服务,然后使用Netflix Ribbon对来自Zuul的请求进行客户端负载均衡。
配置src/main/resources/application.yml文件,与Eureka通信
eureka: instance: preferIpAddress: true client: registerWithEureka: true fetchRegistry: true serviceUrl: defaultZone: http://localhost:8761/eureka/
4.3、在Zuul中配置路由
Zuul的核心是一个反向代理中间服务器,负责捕获客户端的请求,然后代表客户端调用远程资源。
在微服务架构的情况下,Zuul(反向代理)从客户端接收微服务调用并将其转发给下游服务。服务客户端认为它只与Zuul通信。
Zuul要与下游服务进行沟通,Zuul必须知道如何将进来的调用映射到下游路由。Zuul有几种机制来做到这一点,包括:
-
4.3.1、通过服务发现自动映射路由;
Zuul不需要配置,默认自动使用正在调用的服务的Eureka服务ID,并将其映射到下游服务实例。例如,如果要调用organizationservice并通过Zuul使用自动路由,则可以使用以下URL作为端点,让客户端调用Zuul服务实例:
http://localhost:5555/organizationservice/v1/organizations/e254f8c-c442-4ebe-
使用带有Eureka的Zuul的优点在于,开发人员不仅可以拥有一个可以发出调用的单个端点,有了Eureka,开发人员还可以添加和删除服务的实例,而无须修改Zuul。例如,可以向Eureka添加新服务,Zuul将自动路由到该服务,因为Zuul会与Eureka进行通信,了解实际服务端点的位置。
Zuul服务器上的/routes端点可以查看服务中所有映射的列表。如http://localhost:5555/routes
通过zuul注册的服务的映射展示在从/route调用返回的JSON体的左边,路由映射到的实际Eureka服务ID展示在其右边。
-
4.3.2、使用服务发现手动映射路由;
Zuul允许开发人员更细粒度地明确定义路由映射,而不是单纯依赖服务的Eureka服务ID创建的自动路由。
可以通过在zuulsvr/src/main/resources/application.yml中手动定义路由映射。
zuul: routes: organizationservice: /organization/**
通过添加上述配置,现在我们就可以通过访问/organization/v1/organizations/ {organization-id}路由来访问组织服务了。
查看route的结果
此时出现2条服务条目,一条是根据Eureka自动映射的,一条是我们在配置文件中手动映射的。
可以通过application.yml文件添加一个额外的Zuul参数ignored-services来排除Eureka自动映射的服务.
以下代码片段展示了如何使用ignored-services属性从Zuul完成的自动映射中排除Eureka服务ID organizationservice。
zuul: ignored-services: 'organizationservice' routes: organizationservice: /organization/**
ignored-services属性允许开发人员定义想要从注册中排除的Eureka服务ID的列表,该列表以逗号进行分隔。
添加前缀标记
zuul:
ignored-services: '*' ⇽--- ignored-services被设置为*,以排除所有基于Eureka服务ID的路由的注册
prefix: /api ⇽--- 所有已定义的服务都将添加前缀/api
routes:
organizationservice: /organization/** ⇽--- organizationservice和licensingservice分别映射到organization和licensing
licensingservice: /licensing/**
现在,则需要通过/api/organization/v1/organization/ {organization-id}来访问网关接口了
-
4.3.3、使用静态URL手动映射路由。
Zuul可以用来路由那些不受Eureka管理的服务。在这种情况下,可以建立Zuul直接路由到一个静态定义的URL。
zuul:
routes:
licensestatic: ⇽--- Zuul用于在内部识别服务的关键字
path: /licensestatic/** ⇽--- 许可证服务的静态路由
url: http://licenseservice-static:8081 ⇽--- 已建立许可证服务的静态实例,它将被直接调用,而不是由Zuul通过Eureka调用
现在,licensestatic端点不再使用Eureka,而是直接将请求路由到http://licenseservice-static:8081端点。
这里存在一个问题,那就是通过绕过Eureka,只有一条路径可以用来指向请求。
幸运的是,开发人员可以手动配置Zuul来禁用Ribbon与Eureka集成,然后列出Ribbon将进行负载均衡的各个服务实例。
zuul: routes: licensestatic: path: /licensestatic/** serviceId: licensestatic ⇽--- 定义一个服务ID,该服务ID将用于在Ribbon中查找服务 ribbon: eureka: enabled: false ⇽--- 在Ribbon中禁用Eureka支持 licensestatic: ribbon: listOfServers: http://licenseservice-static1:8081, http://licenseservice-static2:8082 ⇽--- 指定请求会路由到的服务器列表
但是如果禁用了Ribbon与Eureka集成,Zuul无法通过Ribbon来缓存服务的查找,那么每次调用都会调用Eureka。
解决这些问题,可以通过对非JVM应用程序建立单独的Zuul服务器来处理这些路由。例如通过Spring Cloud Sidecar使用Eureka实例注册非JVM服务,然后通过Zuul进行代理。(这里不介绍Spring Cloud Sidecar)
-
4.3.4、动态重新加载路由配置
动态重新加载路由的功能允许在不回收Zuul服务器的情况下更改路由的映射。
Zuul公开了基于POST的端点路由/refresh
,其作用是让Zuul重新加载路由配置。在访问完refresh
端点之后,如果访问/routes
端点,就会看到路由被刷新了。
-
4.3.5、Zuul和服务超时
Zuul使用Netflix的Hystrix和Ribbon库,来帮助防止长时间运行的服务调用影响服务网关的性能。
在默认情况下,对于任何需要用超过1 s的时间(这是Hystrix默认值)来处理请求的调用,Zuul将终止并返回一个HTTP 500错误。
可以使用hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds
属性来为所有通过Zuul运行的服务设置Hystrix超时。
zuul.prefix: /api
zuul.routes.organizationservice: /organization/**
zuul.routes.licensingservice: /licensing/**
zuul.debug.request: true
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 2500
为特定服务设置Hystrix超时,可以使用需要覆盖超时的服务的Eureka服务ID名称来替换属性的default
部分。
hystrix.command.licensingservice.execution.isolation.thread.timeoutInMilliseconds:3000
最后,读者需要知晓另外一个超时属性。虽然已经覆盖了Hystrix的超时,Netflix Ribbon同样会超时任何超过5 s的调用。尽管我强烈建议读者重新审视调用时间超过5 s的调用的设计,但读者可以通过设置属性servicename.ribbon.ReadTimeout
来覆盖Ribbon超时。
hystrix.command.licensingservice.execution.isolation.thread.timeoutInMilliseconds: 7000
licensingservice.ribbon.ReadTimeout: 7000
4.4、Zuul的真正威力:过滤器
通过Zuul网关代理确实简化了服务调用,但是Zuul的真正威力在于可以为所有流经网关的服务调用编写自定义逻辑。
例如:安全性、日志记录和对所有服务的跟踪。
Zuul实现这个功能的方式是:过滤器。
Zuul支持这3种类型过滤器:
- 前置过滤器——前置过滤器在Zuul将实际请求发送到目的地之前被调用。前置过滤器通常执行确保服务具有一致的消息格式(例如,关键的HTTP首部是否设置妥当)的任务,或者充当看门人,确保调用该服务的用户已通过验证(他们的身份与他们声称的一致)和授权(他们可以做他们请求做的)。
- 后置过滤器——后置过滤器在目标服务被调用并将响应发送回客户端后被调用。通常后置过滤器会用来记录从目标服务返回的响应、处理错误或审核对敏感信息的响应。
- 路由过滤器——路由过滤器用于在调用目标服务之前拦截调用。通常使用路由过滤器来确定是否需要进行某些级别的动态路由。例如,本章的后面将使用路由级别的过滤器,该过滤器将在同一服务的两个不同版本之间进行路由,以便将一小部分的服务调用路由到服务的新版本,而不是路由到现有的服务。这样就能够在不让每个人都使用新服务的情况下,让少量的用户体验新功能。
Zuul的运作过程:
- (1)在请求进入Zuul网关时,Zuul调用所有在Zuul网关中定义的前置过滤器。前置过滤器可以在HTTP请求到达实际服务之前对HTTP请求进行检查和修改。前置过滤器不能将用户重定向到不同的端点或服务。
- (2)在针对Zuul的传入请求执行前置过滤器之后,Zuul将执行已定义的路由过滤器。路由过滤器可以更改服务所指向的目的地。
- (3)路由过滤器可以将服务调用重定向到Zuul服务器被配置的发送路由以外的位置。但Zuul路由过滤器不会执行HTTP重定向,而是会终止传入的HTTP请求,然后代表原始调用者调用路由。这意味着路由过滤器必须完全负责动态路由的调用,并且不能执行HTTP重定向。
- (4)如果路由过滤器没有动态地将调用者重定向到新路由,Zuul服务器将发送到最初的目标服务的路由。
- (5)目标服务被调用后,Zuul后置过滤器将被调用。后置过滤器可以检查和修改来自被调用服务的响应。
五、第四步:客户端弹性模式
分布式系统的传统弹性实现:集群关键服务器、服务间的负载均衡以及将基础设施分离到多个位置
传统弹性实现的缺点:只能解决致命的问题,当服务器奔溃,服务无法调用时,应用程序才会绕过它;而当服务变得缓慢或者性能不佳时,传统方式则无法绕过它
传统弹性实现的危害:可能只是一个服务的问题,如执行缓慢等,导致线程池被占用完,或数据库连接池被占用完,结果最终导致整个服务器资源被耗尽,导致服务器崩溃;甚至可能从一个服务器,蔓延往上游服务器去蔓延,导致整个生态系统崩溃。
解决方案:客户端弹性模式
5.1、什么是客户端弹性模式
客户端:是指调用远程服务或远程资源的应用程序
客户端弹性模式:当调用的远程服务或远程资源,出现缓慢或其他问题时,客户端执行“快速失败”,保证自己的线程池和数据库连接不被占用,防止远程服务或资源的问题向上游传播
四种模式:
- 客户端负载均衡模式
在前面的服务发现中介绍到,客户端从服务发现代理(如Netflix Eureka)查找服务的所有实例,然后缓存服务实例的物理位置。
每当服务消费者需要调用该服务实例时,客户端负载均衡器将从它维护的服务位置池返回一个位置。
因为客户端负载均衡器位于服务客户端和服务消费者之间,所以负载均衡器可以检测服务实例是否抛出错误或表现不佳。
如果客户端负载均衡器检测到问题,它可以从可用服务位置池中移除该服务实例,并防止将来的服务调用访问该服务实例。
使用Netflix的Ribbon库提供的开箱即用的功能,不需要额外的配置即可实现。
- 断路器模式
断路器模式是模仿电路断路器的客户端弹性模式。
当远程服务被调用时,断路器将监视这个调用。
如果调用时间太长,断路器将会介入并中断调用。
此外,断路器将监视所有对远程资源的调用,如果对某一个远程资源的调用失败次数足够多,那么断路器实现就会出现并采取快速失败,阻止将来调用失败的远程资源。
- 后备模式
后备模式中,当远程服务调用失败时,服务消费者将执行替代代码路径,并尝试通过其他方式执行操作,而不是生成一个异常。
这通常涉及从另一数据源查找数据或将用户的请求进行排队以供将来处理。
用户的调用结果不会显示为提示问题的异常,但用户可能会被告知,他们的请求要在晚些时候被满足。
例如,假设我们有一个电子商务网站,它可以监控用户的行为,并尝试向用户推荐其他可以购买的产品。
通常来说,可以调用微服务来对用户过去的行为进行分析,并返回针对特定用户的推荐列表。
但是,如果这个偏好服务失败,那么后备策略可能是检索一个更通用的偏好列表,该列表基于所有用户的购买记录分析得出,并且更为普遍。
这些更通用的偏好列表数据可能来自完全不同的服务和数据源。
- 舱壁模式
舱壁模式是建立在造船的概念基础上的。
通过使用舱壁模式,可以把远程资源的调用分到线程池中,并降低一个缓慢的远程资源调用拖垮整个应用程序的风险。
线程池充当服务的“舱壁”。
每个远程资源都是隔离的,并分配给线程池。
如果一个服务响应缓慢,那么这种服务调用的线程池就会饱和并停止处理请求,而对其他服务的服务调用则不会变得饱和,因为它们被分配给了其他线程池。
5.2、客户端弹性的重要性
一个实现客户端弹性的例子
第一种场景:
- 愉快路径,断路器将维护一个定时器,如果在定时器的时间用完之前完成对远程服务的调用,那么一切都非常顺利,服务B可以继续工作。
第二种场景:
- 在部分降级的场景中,服务B将通过断路器调用服务C。
- 这一次服务C运行缓慢,在断路器维护的线程上的定时器超时之前无法完成对远程服务的调用,断路器就会切断对远程服务的连接。
- 然后,服务B将从发出的调用中得到一个错误,但是服务B不会占用资源(也就是自己的线程池或连接池)来等待服务C完成调用。
第三种场景:
- 如果对服务C的调用被断路器超时中断,断路器将开始跟踪已发生故障的数量。
- 如果在一定时间内在服务C上发生了足够多的错误,那么断路器就会电路“跳闸”,并且在不调用服务C的情况下,就判定所有对服务C的调用将会失败。
- 电路跳闸将会导致如下3种结果。
- 服务B现在立即知道服务C有问题,而不必等待断路器超时。
- 服务B现在可以选择要么彻底失败,要么执行替代代码(后备)来采取行动。
- 服务C将获得一个恢复的机会,因为在断路器跳闸后,服务B不会调用它。这使得服务C有了喘息的空间,并有助于防止出现服务降级时发生的级联死亡。
- 最后,断路器会让少量的请求调用直达一个降级的服务,如果这些调用连续多次成功,断路器就会自动复位。
总结断路器模式提供的关键能力:
- 快速失败:
当远程服务处于降级状态时,应用程序将会快速失败,并防止通常会拖垮整个应用程序的资源耗尽问题的出现。在大多数中断情况下,最好是部分服务关闭而不是完全关闭。
- 优雅地失败:
通过超时和快速失败,断路器模式使应用程序开发人员有能力优雅地失败,或寻求替代机制来执行用户的意图。
例如,如果用户尝试从一个数据源检索数据,并且该数据源正在经历服务降级,那么应用程序开发人员可以尝试从其他地方检索该数据。
- 无缝恢复:
有了断路器模式作为中介,断路器可以定期检查所请求的资源是否重新上线,并在没有人为干预的情况下重新允许对该资源进行访问。
对于有几百个服务的系统,这种自恢复能力很重要,而不是靠人为手工去恢复这些服务的状态。
5.3、Hystrix
构建断路器模式、后备模式和舱壁模式的实现需要对线程和线程管理有深入的理解,正确地做到这一点很困难。
我们现在可以借助Spring Cloud和Netflix的Hystrix库,轻松实现这些模式。
1、引入依赖和使用注解开启断路器模式
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency>
@EnableCircuitBreaker public class Application { }
断路器我们介绍2种类型:包装数据库调用、包装服务调用
六、使用Spring Cloud Steam的事件驱动架构
基于消息,实现微服务间异步通信
6.1、为什么使用消息传递、EDA和微服务
假设许可证服务需要调用组织服务获取组织信息,而组织信息是较少修改的。
如果每次获取组织信息,都调取组织服务对应的API,那么网络开销较大。
一个可行方案是对组织信息进行缓存。
缓存方案实施时有以下3个核心要求:
(1)缓存的组织数据应该在许可证服务所有实例之间保持一致——代表不能在许可证服务本地缓存数据。
(2)不能将组织数据缓存在许可证服务的容器的内存中——许可证服务的运行时容器通常受到大小限制,并且可以使用不同的访问模式来对数据进行访问。本地缓存可能会带来复杂性,因为必须保证本地缓存与集群中的所有其他服务同步。
(3)在更新或删除一个组织记录时,开发人员希望许可证服务能够识别出组织服务中出现了状态更改——许可证服务应该使该组织的所有缓存数据失效,并将它从缓存中删除。
2种实现方法
6.1.1、使用同步请求——响应方式来传递状态变化
1、用户调用许可证服务,查询许可证数据
2、许可证服务中需要组织信息,先检查Redis缓存中是否有
3、如果没有,则调用组织服务获取,并写入缓存中保存
4、用户调用组织服务可以更新组织数据
5、组织数据更新后,组织服务应该更新缓存中数据,可以通过调用许可证端点或直接与缓存联系。
问题:
1、服务之间紧耦合
许可证服务始终依赖于组织服务来检索数据。
如果通过调用许可证端点来更新缓存,则令组织服务又依赖于许可证服务。
如果组织服务直接联系Redis,也不合理,因为你直接与另外一个服务的数据库进行通信,是不正确的,一个是权限问题,另一个不清楚规则会破坏许可证服务的数据格式。
2、服务之间脆弱性
如果许可证服务出现问题,会影响甚至拖垮组织服务;而Redis出现问题,则会影响2个服务。
3、缺乏灵活性
如果有新的服务对组织服务的变化感兴趣,则需要修改组织服务,从而需要重新生成构建代码部署代码;而且整个网络互相依赖,容易出现一个故障点拖垮整个网络。
6.1.2、使用消息传递在服务之间传达状态更改
加入消息传递方式与上面一种方法的差别在于组织变更时如何
使用消息传递方式将会在许可证服务和组织服务之间注入队列。该队列不会用于从组织服务中读取数据,而是由组织服务用于在组织服务管理的组织数据内发生状态更改时发布消息。图8-2演示了这种方法。
在图8-2所示的模型中,每次组织数据发生变化,组织服务都发布一条消息到队列中。许可证服务正在监视消息队列,并在消息进入时将相应的组织记录从Redis缓存中清除。当涉及传达状态时,消息队列充当许可证服务和组织服务之间的中介。这种方法提供了以下4个好处:
松耦合;
耐久性;
可伸缩性;
灵活性。
1.松耦合
微服务应用程序可以由数十个小型的分布式服务组成,这些服务彼此交互,并对彼此管理的数据感兴趣。正如在前面提到的同步设计中所看到的,同步HTTP响应在许可证服务和组织服务之间产生一个强依赖关系。尽管我们不能完全消除这些依赖关系,但是通过仅公开直接管理服务所拥有的数据的端点,我们可以尝试最小化依赖关系。消息传递的方法允许开发人员解耦两个服务,因为在涉及传达状态更改时,两个服务都不知道彼此。当组织服务需要发布状态更改时,它会将消息写入队列,而许可证服务只知道它得到一条消息,却不知道谁发布了这条消息。
2.耐久性
队列的存在让开发人员可以保证,即使服务的消费者已经关闭,也可以发送消息。即使许可证服务不可用,组织服务也可以继续发布消息。消息将存储在队列中,并将一直保存到许可证服务可用。另一方面,通过将缓存和队列方法结合在一起,如果组织服务关闭,许可证服务可以优雅地降级,因为至少有部分组织数据将位于其缓存中。有时候,旧数据比没有数据好。
3.可伸缩性
因为消息存储在队列中,所以消息发送者不必等待来自消息消费者的响应,它们可以继续工作。同样地,如果一个消息消费者没有足够的能力处理从消息队列中读取的消息,那么启动更多消息消费者,并让它们处理从队列中读取的消息则是一项非常简单的任务。这种可伸缩性方法适用于微服务模型,因为我通过本书强调的其中一件事情就是,启动微服务的新实例应该是很简单的,让这些追加的微服务处理持有消息的消息队列亦是如此。这就是水平伸缩的一个示例。从队列中读取消息的传统伸缩机制涉及增加消息消费者可以同时处理的线程数。遗憾的是,这种方法最终会受消息消费者可用的CPU数量的限制。微服务模型则没有这样的限制,因为它是通过增加托管消费消息的服务的机器数量来进行扩大的。
4.灵活性
消息的发送者不知道谁将会消费它。这意味着开发人员可以轻松添加新的消息消费者(和新功能),而不影响原始发送服务。这是一个非常强大的概念,因为可以在不必触及现有服务的情况下,将新功能添加到应用程序。新的代码可以监听正在发布的事件,并相应地对它们做出反应。
6.1.3、消息传递架构的缺点
与任何架构模型一样,基于消息传递的架构也有折中。基于消息传递的架构可能是复杂的,需要开发团队密切关注一些关键的事情,包括:
消息处理语义;
消息可见性;
消息编排。
1.消息处理语义
开发人员不仅需要了解如何发布和消费消息
面对有序消息,开发人员需要考虑如何处理,没有按顺序处理会出现什么情况,而不是每条消息独立地使用
还要考虑消息抛出异常或错误时,对当前消息的处理(重试or失败?)以及对未来消息的处理
2.消息可见性
考虑使用关联ID等,跟踪Web服务调用以及消息的发布,实现对用户事务的跟踪
3.消息编排
基于消息传递的应用程序很难按照顺序进行业务逻辑推理,用户事务可能也在不同时间不按顺序执行,调试基于消息的应用程序会设计多个不同服务的日志。
6.2、Spring Cloud Stream
简介
Spring Cloud通过Spring Cloud Stream项目,轻松地将消息传递集成到基于Spring的微服务中。
Spring Cloud Stream是一个由注解驱动的框架,它允许开发人员在Spring应用程序中轻松地构建消息发布者和消费者。
Spring Cloud Stream对消息传递平台进行了抽象,实现消息发布和消费是通过平台无关的Spring接口实现的,而平台的具体实现细节(如使用Kafka还是RabbitMQ),则排除在应用程序之外。
架构
随着Spring Cloud中消息的发布和消费,有4个组件涉及发布消息和消费消息,它们是:
发射器(source);
通道(channel);
绑定器(binder);
接收器(sink)。
1.发射器
当一个服务准备发布消息时,它将使用一个发射器发布消息。发射器是一个Spring注解接口,它接收一个普通Java对象(POJO),该对象代表要发布的消息。发射器接收消息,然后序列化它(默认的序列化是JSON)并将消息发布到通道。
2.通道
通道是对队列的一个抽象,它将在消息生产者发布消息或消息消费者消费消息后保留该消息。通道名称始终与目标队列名称相关联。然而,队列名称永远不会直接公开给代码,相反,通道名称会在代码中使用。这意味着开发人员可以通过更改应用程序的配置而不是应用程序的代码来切换通道读取或写入的队列。
3.绑定器
绑定器是Spring Cloud Stream框架的一部分,它是与特定消息平台对话的Spring代码。Spring Cloud Stream框架的绑定器部分允许开发人员处理消息,而不必依赖于特定于平台的库和API来发布和消费消息。
4.接收器
在Spring Cloud Stream中,服务通过一个接收器从队列中接收消息。接收器监听传入消息的通道,并将消息反序列化为POJO。从这里开始,消息就可以按照Spring服务的业务逻辑来进行处理。
6.3、编写简单的消息发布者和消费者
目的:A服务发布消息,B服务打印到窗口
6.3.1、编写消息发布者
我们首先修改组织服务,以便每次添加、更新或删除组织数据时,组织服务将向Kafka主题(topic)发布一条消息,指示组织更改事件已经发生。
二、使用Spring Cloud构建微服务
从3个关键角色的视角解析:
- 架构师:负责大局,了解应用程序如何分解为单个微服务,以及微服务如何交互以交付解决方案。
- 软件开发人员:编写代码并交付微服务。
- DevOps工程师:负责服务部署和管理,保障每个环境一致性和可重复性。
2.1、架构师:设计微服务架构
架构师负责提供解决问题的工作模型,提供脚手架供开发人员构建代码,使应用程序所有部件组合在一起。
关键任务:
- 分解业务问题
- 建立服务粒度
- 定义服务接口
2.1.1、分解业务问题
!!!暂时不是重点,这里需要补充
三、使用Spring Cloud配置服务器控制配置
因为微服务架构中,各个服务是独立开发独立部署的,配置文件不可能像单体架构一样,给每一个服务代码中放置一份配置文件来控制。
这里提出了一种方案:通过Spring Cloud配置服务器,将配置作为一个微服务部署起来,其它微服务通过访问这个配置服务器来获取配置,从而实现统一管理。
3.1、构建一个基于文件系统的Spring Cloud配置服务器
目的:
编写对应的配置文件,放置在Spring Cloud配置服务的代码下;其它微服务访问获取配置时,读取这些文件,并返回给服务的消费者。
1、创建Maven工程
为项目加上resources文件夹
下一步,添加排除的文件类型
2、加入POM文件
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.4.RELEASE</version> </parent> <groupId>com.ltmicro</groupId> <artifactId>test01</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>test01</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
主要代码解析:
- parent:spring-boot-starter-parent,使用Spring Boot
- dependencyManagement-spring-cloud-dependencies:设定使用的Spring Cloud的版本
- dependency-spring-cloud-config-server、spring-cloud-starter-config:引入Spring Cloud的服务器和配置组件,版本自动根据上面的来
- 注意,Spring Boot和Spring Cloud是相对独立的2个项目,他们之间的版本兼容性请查看官方文档
3、添加几个配置文件
4、配置Spring Cloud Config启动类
使用注解:
@SpringBootApplication:表明Spring Boot应用程序(Spring Cloud是Spring Boot应用)
@EnableConfigServer:表明是Spring Cloud Config服务
@SpringBootApplication @EnableConfigServer public class ConfigServiceApplication { public static void main(String[] args) { SpringApplication.run(ConfigServiceApplication.class, args); } }
5、 配置程序
在src/main/resources文件夹下,添加一个application.yml文件(也可以使用properties文件,语法不同)
server: port: 8888 spring: profiles: active: native cloud: config: server: native: searchLocations: classpath:config/,classpath:config/licensingservice # searchLocations: c:/Louis/Projects/JavaDefaultWorkPlace/test01/src/main/resources/config/licensingservice, # c:/Louis/Projects/JavaDefaultWorkPlace/test01/src/main/resources/config/organizationservice # searchLocations: file:///Louis/Projects/JavaDefaultWorkPlace/test01/src/main/resources/config/licensingservice, # file:///Louis/Projects/JavaDefaultWorkPlace/test01/src/main/resources/config/organizationservice
主要设置了这个程序的端口,以native(本地)的方式运行,然后配置了对应查找配置的目录(包含了3种写法)
6、运行程序
我这里使用的是Maven 的spring-boot:run命令运行,也可以直接项目右键运行
访问以下地址均可以得到对应配置
- http://localhost:8888/licensingservice/default
- http://localhost:8888/licensingservice/dev
- http://localhost:8888/licensingservice/prod
3.2、将配置服务改造为基于Git
3.1中,如果部署这个配置服务,要求部署环境必须有对应文件系统,这个有时候很不方便,例如需要用容器来部署。
所以Spring Cloud Config支持其他后端存储库,如Git等,我们这里介绍如何基于Git来构建这个配置服务。
1、Git的使用
Git是一个开源分布式版本管理系统,我们可以使用一些厂商提供的Git服务,如GitHub、Gitee(码云)
下面我们使用码云来实现这个配置服务。
在码云中创建一个项目,然后创建文件夹,放入我们在3.1中设计好的配置文件
2、现在我们可以删掉项目中resources文件夹下的配置文件
3、修改application.yml文件
注意访问码云地址的时候,uri应该填项目地址下面这个
server: port: 8888 spring: cloud: config: server: git: uri: https://gitee.com/Louisyzh/LTMicro.git searchPaths: /** username: 你的码云帐号 password: 你的码云密码
4、同3.1运行程序,能得到相同的结果
3.3、客户端(其它微服务)获取配置
我们的配置服务器经过上面已经搭建好了,是一个Spring Boot程序,打包成jar或者war包即可放到生产环境中运行。
下面介绍客户端(其它微服务)如果获取这些配置
1、重新创建一个Maven项目,并添加resources文件夹
(步骤同上,略)
2、修改POM文件
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.4.4.RELEASE</version> </parent> <groupId>com.ltmicro</groupId> <artifactId>test01</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>test01</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Camden.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-client</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
主要代码解析:
- dependency-spring-boot-starter-web:使用Spring Boot的Web依赖
- dependency-spring-cloud-config-client:使用Spring Cloud Config的客户端依赖
3、配置application.yml文件
spring:
application:
name: licensingservice
profiles:
active: default
cloud:
config:
uri: http://localhost:8888
- spring.application.name:应用程序名称,必须直接映射到Spring Cloud配置服务器中的目录名称,即服务器上必须有此名的一个目录。
- spring.profiles.active:表明程序运行哪个profile,对应就会读取配置服务器中对应的profile的配置。
- spring.cloud.config.uri:Spring Cloud Config服务器的地址。
4、构建一些测试代码
配置好后,程序运行时,相当于把从服务器得到的配置,作为自己的配置文件进行初始化。
我们这里通过一个控制器[email protected]注解读取配置属性来进行测试
创建一个组件用来读取配置属性
@Component public class ServiceConfig { @Value("${example.property}") private String exampleProperty; public String getExampleProperty() { return exampleProperty; } }
创建一个控制器用来显示属性
@RestController public class LicenseServiceController { @Autowired private ServiceConfig serviceConfig; @RequestMapping(value = "/config", method = RequestMethod.GET) public String getLicenses() { return serviceConfig.getExampleProperty(); } }
5、运行程序
运行程序,访问对应地址http://localhost:8080/config,即可显示出获取到的属性。
3.4、配置文件的加密解密(待补充)
如果我们使用像Gitee、GitHub这样的第三方存储库存放我们的配置文件,如果敏感的信息都明文存放,会容易暴露。
所以这里提供一个对敏感信息进行加密解密的方案。
布式调用架构、SOA架构上面演进的一种架构,也不是一种完全创新的架构。
是为了适应互联网行业需求而出现的一种架构。
包括词汇、工作方式、开发方式的演进。
目前还没有很标准的一种定义,这里介绍只是一种实现方式。
架构演进:服务的时代背景:
- 互联网行业的快速发展:为快不破,快速套现,占领用户
- 敏捷方法论的深入人心:短迭代,还是为了实现快
- 容器虚拟化等DevOps技术的成熟化:持续交付,为快提供支持
- 传统分布式技术面临的挑战:大规模服务化需求,服务粒度较小,数量较多
讨论集中式架构:
集中式单块(Monolithic)架构:如MVC
优势
-
- 易于开发/测试/部署
- 具有一定程度的水平伸缩性,如放多块上去,做一些分流、负载均衡等,都是可以的
劣势
-
- 维护成本与业务复杂度:当业务变复杂,东西都放在单块里面,就不方便了,维护增加了成本
- 团队规模与结构
- 交互周期与管理
- 可扩展性
讨论SOA架构:
主要问题:
-
- SOA只是一个思想,只提供指导思想和原则,难以达成共识
- 服务的粒度/依赖关系/通信协议等如何确定:除非使用第三方框架
- 服务实例数量相对有限,难以应对大规模服务化场景:这个只是相对微服务而言
- 企业级实施方案,需要自顶向下推行
- 交互方式偏重于功能性团队模式:需要不同功能的部门都齐备且合作和谐
互联网公司的需求:
应对大规模服务化需求:(具体的含义,或者实际的示例是什么?需要补充)
-
- 如何确定服务拆分粒度
- 如何降低服务之间依赖
- 如果尽量降低技术/架构之间差异化
- 如果让团队Owning服务
- 如何进行快速的服务开发和交付
微服务概述:
定义:(来自Martin Fowler)
- 微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间互相协调和配合,为用户提供最终价值
- 每个服务运行在其独立的进程中,服务于服务器间采用轻量级通信机制相互沟通(通常是基于HTTP的RESTful API)
- 每个服务都围绕着业务进行构建,并且能够被独立部署到生产/类生产环境
- 尽量避免同一的、集中式的服务管理机制,对具体的一个服务而已,应该根据业务上下文,选择合适的语言、工具进行构建
特性:(“微”的含义)
- 业务独立
- 团队自主
- 技术无关轻量级通信
- 交付独立性:
- 进程隔离
服务间并不需要组合成组建去发布,而是每个服务都是独立的;同时降低服务之间的耦合。
微服务与SOA的区别和联系:
架构师演化:
快速演进过程中的架构师:
- 关注服务之间的交互,而不是服务内部实现细节
- 做出符合团队目标的技术选择,提供代码模版
- 考虑系统允许多少可变性,并能够快速适应变化
- 完成>完美
- 建设团队
我们的思路:
- 从核心原理到基本实现
- 从开发到运维
- 从技术体系到案例
- 技术体系:Spring Boot、Spring Cloud
- 案例:建模、开发、测试、部署
- 穿插相关模式和架构风格
微服务架构
实现策略:
- 采用松散服务体系
- 围绕业务组件团队
- 融合技术多样性
- 确保业务数据独立
- 基本设施自动化
采用松散服务体系
- 独立开发/部署/维护
- 技术无关的集成接口
围绕业务组件团队
建立Feature Team
融合技术多样性
因为面向服务,可能遇到很多系统
使用http这些技术无关的技术进行连接
确保业务数据独立
使用服务集成而不是数据集成(微服务中一个很有用的特点)
基础设施自动化(DevOps)
- 服务部署
- 健康诊断
- 错误回滚
- 日志分析
- 服务治理
实施问题:
- 分布式系统固有的复杂性
- 性能
- 可靠性
- 异步
- 数据一致性
- 部署自动化与运维成本
- 配置
- 监控和报警
- 日志收集
- 部署流水线
- 组织架构
- 康威定律与企业文化
- 开发/运维角色变换
- 开发承担整个生命周期
- 运维尽早开来服务部署
- 服务间的依赖管理
- 开发进程同步和管理
- 服务通信与集成
- 服务依赖测试
切入点:
一切从零开始:
- 明确服务建模和服务集成的方法
- 寻找一个构建微服务的基本框架
- 探索面向发布的全流程微服务实现方案
- 技术体系
- 工具
- 策略
第三节:
服务建模:
微服务中的如何实现松耦合
独立部署单个服务并不需要修改其它服务
尽可能使用轻量级通信方式进行服务集成
微服务中的高内聚
只改一个地方就可以发布
尽可能明确领域边界
不应该是技术驱动建模,应该业务驱动建模
利用上下文划分界限
服务集成:
RPC,Messaging分别代表了2中风格的通信方式
API网关,一个统一入口
Spring Boot入门
Spring 的一个子项目
简介:
基本示例:
Spring Boot消息传递
点对点,发布订阅
这2种方式都比较常见,所以基本的第三方消息中间件都支持这2种模型
消息传递的协议、规范
1、JMS(Java Message Service)协议
实现:ActiveMQ
Spring组件:spring-jms 只要实现了JMS的组件,都可以通过Spring的spring-jms抽象来操作
2、AMQP(Advanced Message Queuing Protocol)协议
实现:RabbitMQ
Spring组件:同上,有spring-rabbit
3、以上2个协议是业界标准;其它协议,如kafka,自定的规范,自为体系
JMS规范(选修课里面讲到)
AMQP规范:
2个示例
讲解了2个组件在Sping BOot中的简单实用
Spring Boot部署与Docker
SPring Boot部署
Docker云部署
部署建模:把部署提升到建模等级
有点类似Maven的仓库,例如把一个jar包作为镜像放到注册中心中,那么其他镜像都可以访问到他
群关键服务器、服务间的负载均衡以及将基础设施分离到多个位置的