SpringCloud微服务实战—翟永超 读书笔记
什么是微服务建构
- 微服务是系统架构上的一种风格的设计,将原本独立的系统拆分成多个小型服务。
- 小型的服务都运行在自己独立的进程当中,服务之间基于HTTP通过Restful api进行通信写作。
- 被拆分成的每个小型服务都围绕系统中的某一项或一些耦合度较高的业务功能进行构建。
- 每个服务维护自身的数据存储,业务开发,自动化测试,独立部署。
- 由于有了轻量级的通信协作,所以这些微服务可以用不同的语言编写。
微服务和单体系统的区别
- 传统的系统架构中,我们针对一个复杂的需求同参使用对象或业务类型来构建一个单体项目。
- 传统项目中我们通常将需求分为三个部分:数据库,服务端处理,前端展现。
- 业务发展初期,所有的逻辑在一个应该用中,开发测试,部署都比较容易。
- 随着业务的发展,会增加不同的业务模块;随着移动端设备的进步,前端不仅仅局限于web形式,这需要系统后端像前端支持需要更多的接口模块。
- 单体应用面对不断扩大的需求会使得单体应用变得越来越臃肿。
- 由于部署在一个进程中,修改 一个小功能,部署上线运行会影响其他功能的运行。
- 单体应用中消耗的资源类型也不同,对于业务模块系统容量也很难准确评估。
- 微服务通过不同模块拆分成多个不同服务,这些服务独立部署扩展。
- 每个微服务部署运行在自己的进程内,在部署上有稳固的边界,这样每个微服务都不会影响其他服务的运行。
- 由于独立部署,可以准确为每个服务评估性能容量,以及更容易发现系统的瓶颈位置。
如何实施微服务
微服务虽然有这么多优点,但是因为服务拆分引发了诸多原本在单体应用中没有的问题。
- 运维的新挑战:运维需要维护的进程数量会增加。编排和组织这些进程是一件不容易的事。
- 接口的一致性: 单体应用中的依赖变为服务间的通信依赖。当对原有的接口进行修改,交互方也需要协调改变,保证接口正确调用。所以我们需要更完善的接口和版本管理,或严格遵循开闭原则。
- 分布式的复杂性:服务间通过通信来协作,就需要考虑分布式环境问题,比如网络延迟,分布式事务,异步消息等。
虽然有这么多缺点和问题,但其实现的敏捷开发和自动化部署等优点被广大程序员所青睐。
微服务架构的九大特性
-
服务组件化:微服务架构中需要对服务进行组件化分解,避免服务的修改引起整个系统的重新部署
-
按业务组织团队:每一个微服务针对特定业务的全栈实现,既需要负责数据的持久化,又要负责用户的接口定义等各种跨专业领域的职能。因此建议按业务线的方式进行拆分。
-
做”产品“的态度:持续关注服务的运作情况, 并不断分析以帮助用户来改善业务功能。
-
智能端点与哑管道:
- 使用HTTP的RESTful API或轻量级的消息发送协议,实现消息传递与服务调用的触发。
- 通过在轻量级消息总线上传递消息,类似RabbitMQ等一些可靠的异步交换的中间件。
-
去中心化治理:不是每一个问题都是钉子, 不是每一个解决方案都是锤子
-
去中心化管理数据:让每一个服务来管理其自有的数据库, 这就是数据管理的去中心化
-
基础设施自动化:
-
自动化测试:每次部署前的强心剂, 尽可能地获得对正在运行的软件的信心。
-
自动化部署:解放烦琐枯燥的重复操作以及对多环境的配置管理.
-
-
容错设计: 通常,我们都希望在每个服务中实现监控和日志记录的组件, 比如服务状态、 断路器状态、 吞吐量 、网络延迟等关键数据的仪表盘等。
-
演进式设计:初期以单体应用来架构,随着业务的扩展将多变的模块拆分出来
为什么选中Spring Cloud
在Spring 社区的整合之下, 做了大量的兼容性测试, 保证了其拥有更好的稳定性, 如果要在Spring Cloud架构下使用非原装组件时, 就需要对其基础有足够的了解。
Spring Cloud简介
Spring Cloud 是基于Spring Boot实现的微服务架构开发工具。
为微服务架构中涉及的配置管理,服务治理,断路器,智能路由,微代理,控制总线,全局锁,决策竞选,分布式会话和集群状态管理等操作提供了一种简单的开发方式。
Spring Cloud包含了多个子项目:
-
Spring Cloud Config: 配置管理工具,支持使用Git存储配置内容,可以使用它实现应用配置的外部化存储,并支持客户端配置信息刷新、 加密/解密配置内容等。
-
Spring Cloud Netflix: 核心组件,对多个Netflix OSS开源套件进行整合。
Eureka: 服务治理组件,包括服务注册中心,服务注册与发现机制的实现。
Hystrix:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。
Ribbon:客户端负载均衡的服务调用组件。
Feign:基于Ribbon和Hystrix的声明式服务调用组件。
Zuul:网关 组件,提供只能路由,访问过滤等功能。
Archaius:外部化配置组件。
-
Spring Cloud Bus:事件,消息总线,用于传播集群中的状态变化或事件,以触发后续的处理,比如用来动态刷新配置等。
-
Spring Cloud Cluster:针对ZooKeeper,Redis,Hazelcast,Consul的选举算法和通用状态模式的实现。
-
Spring Cloud Cloudfoundry:与Pivotal Cloudfoundry的整合支持。
-
Spring Cloud Consul:服务发现于配置管理工具。
-
Spring Cloud Stream:通过Redis,Rabbit或者Kafka实现的消息微服务,可以通过简单的声明式模型来发送和接收消息。
-
Spring Cloud AWS:用于简化整合Amazon Web Service的组件。
-
Spring Cloud Security:安全工具包,提供Zuul代理中对OAuth2客户端请求的中继器。
-
Spring Cloud Sleuth:Spring Cloud应用的分布式跟踪实现,可以完美整合Zipkin.
-
Spring Cloud ZooKeeper:基于ZooKeeper的服务发现于配置管理组件。
-
Spring Cloud Starters:Spring Cloud的基础组件,它是基于Spring Boot风格项目的基础依赖模块。
-
Spring Cloud CLI:用于在Groovy中快速创建Spring Cloud应用的Spring Boot CLI插件
-
…
版本说明
当一个版本的Spring Cloud项目的发布内容积累到临界点或者一 个严重bug解决可用 后, 就会发布 一个"service releases"版本, 简称SRX版本, 其中 X是 一 个递增的数字, 所以Brixton.SRS就是Brixton的第5个Release版本。
微服务构建:Spring Boot
Spring Boot的宗旨是通过设计大量的自动化配置等方式来简化Spring原有样板化的配置。
Spring Boot还通过一系列StaiterPOMs的定义, 让我们整合 各项功能的时候, 不需要在 Maven的pom.xml中维护那些错综复杂的依赖关系, 而是通 过类似模块化的Starter模块定义来引用, 使得依赖管理工作变得更为简单。
Spring Boot融入了Docker技术,自身支持嵌入式的Tomcat,Jetty等容器。不在需要安装Tomcat,打成war包,现在只需要打成jar包,并通过java -jar命令直接运行就可以。
单元测试:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = HelloApplication.class)
@WebAppConfiguration
public class HelloApplicationTests (
private MockMvc mvc;
@Before
public void setOp() throws Exception {
mvc = MockMvcBuilders. standaloneSetup (new HelloController ()) . build() ;
}
@Test
public void hello() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON))
.andExpect(status() .isOk()) . andExpect (content() . string (equal To ("Hello World")));
}
}
代码解析:
- @RunWith(SpringJUnit4ClassRunner.class): 引入Spring对JUnit4的支持。
- @SpringApplicationConfiguration (classes = HelloApplication.class): 指定Spring Boot的启动类。
- @WebAppConfiguraton: 开启Web应用的配置, 用千模拟ServletContext.
- MockMvc对象: 用于模拟调用 Controller的接口发起请求, 在@Test定义的hello测试用例中, perform函数执行 一次请求调用, accept用于执行接收的数据类型,andExpect用于判断接口返回的期望值。
- @Before: JUnit中定义在测试用例@Test内容执行前预加载的内容, 这里用来初始化对HelloController的模拟
配置文件
-
src/main/resources目录是Spring Boot的配置目录。
-
Spring Boot的默认配置文件位置为:
src/main/resources/application.properties.
src/main/resources/application.yml.
server.port = 8888 #指定端口
spring.application.name = hello #指定应用名
server:
port: 8888
spring:
profiles:test
- 注意 YAML 目前还有—些不足,它无法通过@PropertySource 注解来加载 配置。 但是, YAML 将属性加载到内存中保存的时候是有序的, 所以当配置文件 中的信息需要具备顺序含义时, YAML 的配置方式比起 properties 配置文件更有优势。
自定义参数
book.name=张三
@Value("${book.name}")
private String name;
参数引用
book.name=zs
book.author=mh
book.desc=${book.author} is writing 《 ${book.name}
最后book.desc的值为:mh is writing 《 zs
使用随机数
#随机字符串
com.val=${random.value}
#随机int
com.number=${random.int}
# 随机long
com.bignumber=${random.long}
# 10以内的随机数
com.test1=${random.int(10)}
#10~20的随机数
com.test2=${random.int[10,20]}
该配置方式可以设置应用端口等场景,避免本地调用时出现端口冲突。
多环境配置
- application-dev.properties:开发环境
- application-test.properties:测试环境
- application-prod.properties:生产环境
至于具体哪个配置文件会被加载, 需要在 application.properties 文件中通过 spring.profiles.active属性来设置, 其 值 对应配置文件中的{profile}值。 如 spring.profiles.active= test就会加载application-test.properties配置文件内容。
监控与管理
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
服务治理:Spring Cloud Eureka
Eureka基于Netflix Eureka做了二次封装,主要负责完成微服务架构中的服务治理功能。
Spring Cloud为Eureka增加了Spring Boot风格的自动化配置,我们只需要引入依赖和注解就能让Spring Boot构建微服务应用轻松与Eureka服务治理体系进行整合。
服务治理
是微服务架构中最核心和最基础的模块,主要实现各个微服务的自动化注册和发现。
- 服务注册:其他服务单元向注册中心登记自己提供的服务,将主机与端口号,版本号,通信协议等一些附加信息告知注册中心。注册中心就会维护一个服务清单。服务注册中心还需要以心跳的方式去检测清单中的服务是否可用,如果不可用就会自动从服务清单中剔除,达到排除故障服务的效果
- 服务发现:服务间的调用不在需要通过指定具体的实例地址来实现,而是通过向服务名发起请求调用实现。所以服务调用方在调用服务提供方接口的时候,并不知道具体的服务实例位置。因此调用发需要向服务注册中心咨询服务,获取所有服务的实例清单,以实现对具体服务实例的访问。
Netflix Eureka
Spring Cloud Eureka,使用Netflix Eureka来实现服务注册与发现,他既包含了服务端组件,也包含了客户端组件,并且服务端与客户端均采用Java编写,所以Eureka主要适用于通过Java实现分布式系统,或是与JVM兼容语言构建的系统。但是,由于Eureka服务端的服务治理体提供了完备的RESTful API,所以它也支持将非Java语言构建的微服务应用纳入Eureka的服务治理体系中来。
Eureka 服务端,也称为服务注册中心。支持高可用配置,在Eureka集群模式中,如果集群中有分片出现故障时,那么Eureka就转入自我保护模式。允许在分片故障期间继续提供服务的发现和注册,当故障分片恢复运行时,集群中的其他分片就会把他们的状态再次同步回来。
Eureka客户端,主要处理服务的注册与发现。客户端服务通过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身。提供的服务并周期性的发送心跳来更新它的服务租约。也能从服务端查询当前注册的服务信息并把他们缓存到本地周期性地刷新服务状态。
搭建服务注册中心
-
搭建服务注册中心
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.4.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupid>org.springframework.cloud</groupid> <artifactid>spring-cloud-dependencies</artifactid> <version>Brixton.SRS</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
通过@EnableEurekaServer注解启动一个服务注册中心提供给其他应用进行对话。
@SpringBootApplication @EnableEurekaServer public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class); } }
默认情况下,服务注册中心也会将自己作为客户端来尝试注册自己,所以我们需要禁用他的客户端注册行为,需要在配置文件配置
server: port: 6868 eureka: client: register-with-eureka: false #是否将自己注册到eureka中,false:不注册 fetch-registry: false #是否从eureka中检索服务 service-url: defaultZone: http://mh.com:${server.port}/eureka/
-
注册服务提供者
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--测试包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Greenwich.SR1</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
@RestController public class HelloController { private final Logger logger = Logger.getLogger(getClass()); @Autowired private DiscoveryClient client; @RequestMapping(value = "/hello", method= RequestMethod.GET) public String index() { Service Instance instance = client.getLocalServiceinstance(); logger.info("/hello, host:" + instance.getHost() + " , service id:" + instance.getServiceid()); return "Hello World"; } }
在主类上加@EnableDiscoveryClient注解,激活Eureka中的DiscoveryClient实现自动化配置
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) // @EnableEurekaClient @EnableDiscoveryClient public class SearchAppliction { public static void main(String[] args) { SpringApplication.run(SearchAppliction.class, args); } }
最后在配置文件中配置:
server: port: 18085 eureka: client: service-url: defaultZone: http://127.0.0.1:6868/eureka
-
高可用注册中心
在Euuek的服务治理设计中,所有节点既是服务提供方,又是服务消费方,服务注册中心也一样。
Euureka Server的高可用实际上是将自己作为服务向其他服务注册中心注册自己,形成相互注册,实现服务清单同步,达到高可用的效果。
-
创建服务注册中心配置peer1
spring.application.name=eureka-server server.port=llll eureka.instance.hostname=peerl eureka.client.serviceUrl.defaultZone=http://peer2:1112/eureka/
-
创建服务注册中心配置peer2
spring.application.name=eureka-server server.port=llll eureka.instance.hostname=peer2 eureka.client.serviceUrl.defaultZone=http://peer1:1112/eureka/
-
peer1和peer2是在hosts文件中配置的dns
-
通过spring.profiles.active属性分别启动
java -jar eureka-server-1.0.0.jar --spring.profiles.active=peerl java -jar eureka-server-1.0.0.jar --spring.profiles.active=peer2
-
服务提供者需要修改一些配置
spring.application.name=hello-service eureka.client.serviceUrl.defaultZone=http://peerl:llll/eureka/,http://peer2:lll2/eureka/
如果不想使用主机名来定义注册中心地址也可以使用ip地址,但是需要该配置
eureka.instance.prefer-ip-address= true #该值默认为false。
-
-
服务发现与消费
服务发现的任务由Eureka的客户端完成,而服务消费的任务由Ribbon完成 。Ribbon是一个基 于HTTP和TCP的客户端负载均衡器,它可以在通过客户端中配ribbonServerList 服务端列表去轮询访问以达到均衡负载的作用。
-
先启动两个服务
java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port=8081 java -jar hello-service-0.0.1-SNAPSHOT.jar --server.port=8082
-
启动成功,在Eureka信息面板查看。
-
创建服务消费者,依赖省略
@EnableDiscoveryClient @SpringBootApplication public class ConsumerApplication { @Bean @LoadBalanced RestTemplate restTemplate () { return new RestTemplate (); } public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } }
-
-
配置文件,注意防止冲突
spring.application.name=ribbon-consumer server.port=9000 eureka.client.serviceUrl.defaultZone=http://localhost:llll/eureka/
@RestController public class ConsumerController { @Autowired RestTemplate restTemplate; @RequestMapping(value = "/ribbon-consumer", method = RequestMethod.GET) public String helloConsumer () { return restTemplate.getForEntity("http://HELLO-SERVICE/hello", String.class).getBody(); //HELLO-SERVICE是服务提供者在注册中心注册的名字 method = RequestMethod.GET) } }
- 最后启动,可以在Eureka信息面板中查看
Eureka详解
构建Eureka服务治理体系的三个核心:服务注册中心,服务提供者,服务消费者。
-
基础架构
-
服务注册中心:Eureka提供的服务端,提供服务注册与发现的功能。
-
服务提供者: 提供服务的应用,可以将自己注册到Eureka,以提供其他应用发现。
-
服务消费者:从服务注册中心获取服务列表,从而使消费者可以知道去何处调用所需的服务。
很多时候,客户端既是服务消费者又是服务提供者
-
-
服务治理机制
-
服务提供者
- 服务注册
“服务提供者” 在启动的时候会通过发送REST请求的方式将自己注册到EurekaServer 上, 同时带上了自身服务的一些元数据信息。Eureka Server接收到这个REST请求之后, 将元数据信息存储在一个双层结构Map中, 其中第一层的key是服务名, 第二层的key是 具体服务的实例名。
在服务注册时, 需要确认一下 eureka.cli ent.register-with-eureka=true 参数是否正确, 该值默认为true。 若设置为false将不会 启动注册操作。
-
服务同步
通过服务注册中心相互之间进行注册,实现服务注册中心之间的服务同步,两个服务提供者的服务信息就可以通过这两台服务注册中心的任意一台获取到
-
服务续约
服务提供者会维护一个心跳来持续告诉Eureka Server:“我还活着”,以防止Eureka Server的"剔除任务"将该服务从列表中排除出去,这种操作称为续约.
eureka.instance.lease-renewal-interval-in-seconds=30 #定义服务续约任务的调用间隔时间,默认30s. eureka.instance.lease-expiration-duration-in-seconds=90 #定义服务失效的时间,默认90s.
-
服务消费者
-
获取服务
启动服务消费者时,会发送REST请求给服务注册中心,来获取上面的服务清单,考虑性能,Eureka Server会维护一份只读的服务清单来返回给客户端,同时该缓存会每隔30s更新一次. 获取服务时消费者的基础,所以必须确保eureka.client.fetch-registry=true,默认为true.
若希望修改缓存清单的更新时间,可以通过eureka.client.registry-fetch-interval-seconds=30参数进行修改.默认30s
-
服务调用
获取服务清单后,通过服务名可以获得具体的服务信息,客户端可以根据自己的需要决定嗲用那个实例,在Ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡.
对于访问实例(服务提供者)的选择,Eureka中有Region和Zone的概念,一个Region中可以包含多个Zone.
-
服务下线
当服务实例进行正常关闭时,会触发服务下线的请求给Eureka Server,告诉服务注册中心"我要下线了".服务端接收到请求之后,将该服务状态设置为下线(DOWN),并把该下线事件传播出去.
-
-
服务注册中心
-
失效剔除
有的服务实例由于各种情况(内存溢出,网络故障等)不一定正常下线,而服务注册中心没有收到"服务下线"的请求.为了从服务列表中将这些无法提供服务的实例剔除,Eureka Server在启动的时候会创建一个定时任务,默认60s将清单中超时(90s)没有续约的服务剔除出去. -
自我保护
有些服务实例没有正常下线,所以Eureka信息面板就会有一个红色警告提示,该警告就是出发来了Eureka Server的自我保护机制.
EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THRTHRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRD JUST TO BE SAFE.
-
-
-
源码分析
对于服务注册中心,服务提供者,服务消费者这三个主要元素来所,后两者在整个运行机制中是大部分通信行为的主动发起者,而注册中心主要是处理请求的接收者.先从Eureka的客户端作为入口看看他是如何完成这些主动通信行为的.
在将普通的Spring boot应用注册到Eureka Server或是从Eureka Server中获取服务列表时,主要就做了两件事:
-
在应用启动类配置了@EnableDiscoveryClient注解
-
在配置文件中用eureka.client.serviceUrl.defaultZone参数指定了服务注册中心的位置.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(EnableDiscoveryClientimportSelector.class) public @interface EnableDiscoveryClient { }
它主要用来开启DiscoveryClient的实例.通过搜素得出如下图
DiscoveryClient是Spring Cloud的接口,定义了服务常用的方法,通过该接口可以有效的屏蔽服务治理的实现细节,所以Spring Cloud构建的微服务应用可以方便的切换不同的服务治理框架,而不改动程序代码,只需要另外添加一些针对服务治理框架的配置即可.
EurekaDiscoverClient是对该(DiscoveryClient)接口的实现,是对Eureka的封装.所以EurekaDiscoveryClient依赖了Netflix Eureka的com.netflix.discovery.EurekaClient接口EurekaClient继承了LookupService接口.他们都是Netflix开源包中的内容,主要定义了针对Eureka的发现服务的抽象方法,而真正实现发现服务的则是Netflix包中的com.netflix.discovery.DiscoveryClient类.
DiscoveryClient类:
这个类用于帮助与Eureka Server互相协作。 Eureka Client 负责下面的任务: -向Eureka Server 注册服务实例 -向Eureka Server 服务租约 - 当服务关闭期间,向Eureka Server取消租约 -查询Eureka Server中的服务实例列表 Eureka Client还需要配置一个Eureka Server的 URL列表。
Eureka Server的URL列表的配置.根据配置的属性名eureka.client.serviceUrl.defaultZone,通过serviceUrl可以找到该属性相关的加载属性.
public static Map<String, List<String>> getServiceUrlsMapFromConfig( EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { Map<String, List<String>> orderedUrls = new LinkedHashMap<> (); String region = getRegion(clientConfig); String [] availZones = clientConfig.getAvailabilityZones (clientConfig. getRegion ()); if (availZones == null || availZones.length == 0) { availZones = new String[l]; availZones[O] = DEFAULT_ZONE; } int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); String zone = availZones[myZoneOffset]; List<String> serviceUrls =clientConfig.getEurekaServerServiceUrls(zone); if (serviceUrls != null) { orderedUrls.put(zone, serviceUrls); } ....... return orderedUrls; }
-
Region,Zone
上面的函数可以发现,客户端一次加载两个内容,第一个是Region,第二个是Zone,从其加载逻辑上可以判断它们之间的关系:
- 通过getRegion函数,我们可以看到他从配置中读取了一个Region返回,所以一个微服务应用只可以属于一个Region,如果不特别配置,默认为default.若我们自己设置,可以通过eureka.client.region属性来定义.
public static String getRegion(EurekaClientConfig clientConfig) { String region = clientConfig.getRegion(); if (region == null) { region = DEFAULT_REGION; region = region.trim() .toLowerCase(); } return region; }
-
通过getAvailabilityZones函数,可以知道当我们没有特别为Region配置Zone的时候,将默认采用defaultZone,这也是我们之前配置参数eureka.client.serviceUrl.defaultZone的由来.若要为应用指定Zone,可以通过eureka.client.availability-zones属性来进行设置.从函数的return内容,可以知道Zone能够设置多个,并且通过都好分割来配置.所以Region和Zone是一对多的关系
public String[] getAvailabilityZones(String region) { String value = this.availabilityZones.get(region); if (value == null) { value = DEFAULT_ZONE; } return value.split(","); }
Region:可以代表一个Eureka Client;而Zone表示的是加载的Eureka Server的地址,可以有多个
-
serviceUrls
获取了Region和Zone的信息之后,才开始真正加载Eureka Server的具体地址,他根据传入的参数按一定的算法确定加载位于哪一个Zone配置的serviceUrls.int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); String zone = availZones[myZoneOffset]; List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
具体获取serviceUrls的实现,可以查看getEurekaServerServiceUrls函数的具体实现类EurekaClientConfigBean,该类是EurekaClientConfig和EurekaConstants接口的实现.用来加载配置文件中的内容.
public List<String> getEurekaServerServiceUrls(String myZone) { String serviceUrls = this.serviceUrl.get(myZone); if (serviceUrls == null I I serviceUrls. isEmpty ()) { serviceUrls = this.serviceUrl.get(DEFAULT_ZONE); } if (1 StringUtils.isEmpty(serviceUrls)) { final String[] serviceUrlsSplit =StringUtils.comrnaDelimitedListToStringArray(serviceUrls); List<String> eurekaServiceUrls = new ArrayList<>(serviceUrlsSplit.length); for (String eurekaServiceUrl : serviceUrlsSplit){ if (!endsWithSlash(eurekaServiceUrl)) { eurekaServiceUrl += "/"; } eurekaServiceUrls.add(eurekaServiceUrl); } return eurekaServiceUrls; } return new ArrayList<>(); }
当我们在微服务应用中使用Ribbon来实现服务调用时,对于Zone的设置可以在负载均衡时实现区域测特性:
Ribbon的默认策略会优先访问同客户端处于一个Zone中的服务端实例,只有当同一个Zone中没有可用服务端实例的时候才会范文其他Zone中的实例.所以通过Zone属性的定义,配合实际部署的物理结构,我们就可以有效的设计出对区域性故障的容错集群.
-
服务注册
查看DiscoveryClient类是如何实现"服务注册"行为的,通过查看它的构造类,可以找到它调用了下面这个函数:
private void initScheduledTasks () { if (clientConfig.shouldRegisterWithEureka()) { // Instanceinfo replicator instanceinfoReplicator = new InstanceinfoReplicatr(this,instanceinfo,clientConfig.getinstanceinfoReplicationintervalSeconds(),2); // burstSize ...... instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds()); } else { logger.info("Not registering with Eureka server per configuration"); } }
从上面的函数中,可以看到一个与服务注册相关的判段语句 if(clientConfig.shouldRegisterWithEureka()). 在该分支内,创建了一个InstanceInfoReplicator类的实例,它会执行一个定时任务,而这个定时任务的具体工作可以查看该类的run()函数.
public void run() { try { discoveryClient.refreshinstanceinfo(); Long dirtyTimestamp = instanceinfo.isDirtyWithTime(); if (dirtyTimestamp != null) { discoveryClient.register(); instanceinfo.unsetisDirty(dirtyTimestamp); } } catch (Throwable t) { logger.warn("There was a problem with the instance info replicator", t); } finally { Future next = scheduler.schedule(this, replicationintervalSeconds, TimeUnit.SECONDS); scheduledPeriodicRef.set(next); } }
discoveryClient.register();这一行,真正触发调用注册的地方就在这里.继续查看register()的实现内容
boolean register() throws Throwable { logger.info(PREFIX + appPathidentifier + ": registering service ... "}; EurekaHttpResponse<Void> httpResponse; try { httpResponse = eurekaTransport.registrationClient.register(instanceinfo}; } catch (Exception e) { logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e. getMessage (), e); throw e; } if (logger. isinfoEnabled ()) { logger. info (" {} - registration status: {} ", PREFIX + appPathidentifier, httpResponse.getStatusCode()); } return httpResponse.getStatusCode() == 204; }
注册 操作也是通过REST请求的方式进行的.同时,可以看到发起注册请求的时候,传入了一个com.netflix.appinfo.InstanceInfo对象,该对象就是注册时客户端给服务端的元数据.
-
服务获取与服务续约
来看DiscoveryClient的initScheeduledTasks函数,不难发现在其中还有两个定时任务,分别是"服务获取"和"服务续约":
private void initScheduledTasks () { if (clientConfig.shouldFetchRegistry()) { // registry cache refresh timer int registryFetchintervalSeconds =clientConfig.getRegistryFetchinterval.Seconds(); int expBackOffBound =clientConfig.getCacheRefreshExecutorExponential.BackOffBound(); scheduler.schedule( new TimedSupervisorTask( "cacheRefresh", scheduler, cacheRefreshExecutor, registryFetchintervalSeconds, TimeUnit.SECONDS, expBackOffBound, new CacheRefreshThread () registryFetchintervalSeconds, TimeUnit.SECONDS); ) if(clientConfig.should.RegisterWithEureka()) { int renewalintervalinSecs = instanceinfo. getLeaseinfo () . getRenewalintervalinSecs () ; int expBackOffBound = clientConfig. getHeartbeatExecutorExponentialBackOffBound(); logger.info("Starting heartbeat executor: "+ "renew interval is: " + renewalintervalinSecs); // Heartbeat timer scheduler.schedule( new TimedSupervisorTask( "heartbeat", scheduler, heartbeatExecutor, renewa1Interva1InSecs, TimeUnit.SECONDS, expBackOffBound, new HeartbeatThread () ), renewa1Interva1InSecs, TimeUnit.SECONDS); // Instanceinfo replicator .... } }
从源码中可以发现,"服务获取"任务相对于"服务续约"和"服务注册"任务更为独立,"服务续约"与"服务注册"在同一个if逻辑中,这个不难理解服务注册到Eureka Server后,自然需要一个心跳去续约,防止被剔除,所以它们肯定是成对出现的.从源码中,清楚的看到了之前所提到的,对于服务续约相关的时间控制参数:
eureka.instance.lease-renewal-interval-in-seconds=30 eureka.instance.lease-expiration-duration-in-seconds=90
而"服务获取"的逻辑在独立的一个if判断中,其判断依据就是我们之前所提到的eureka.client.fetche-registry=true参数,它默认为true,大部分情况下我们不需要关心.为了定期更新客户端的服务清单,以保证客户端能够访问确实健康的服务实例,"服务获取"的请求不会只限于服务启动,而是一个定时执行的任务,从源码中可以看到任务运行中的registryFetchIntervalSeconds参数对应的就是之前所提到 的eureka.client.registry-fetch-interval-seconds=30配置参数,它默认为30s.
继续向下深入,可以发现实现"服务获取"和"服务续约"的具体方法,其中"服务续约"的实现较为简单,直接以REST请求的方式进行续约:
boolean renew() { EurekaHttpResponse<Instanceinfo> httpResponse; try { httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instance Info. getAppName(), instanceInfo.getId(), instanceInfo, null); logger.debug("{} - Heartbeat status: {}", PREFIX + appPathidentifier, httpResponse.getStatusCode()); if (httpResponse.getStatusCode() == 404) { REREGISTER COUNTER.increment(); logger.info("{} - Re-registering apps/{}", PREFIX + appPathidentifier, instanceinfo.getAppName()); return register(); } return httpResponse.getStatusCode() == 200; } catch (Throwable e) { logger. error (" { } - was unable to send heartbeat!", PREFIX + appPathidentifier, e) ; return false; } }
而"服务获取"则复杂一些,会根据是否是第一次获取发起不同的Rest请求和相应的处理.具体的实现逻辑跟之前的类似.
-
服务注册中心处理
通过上面的源码可以看到所有的交互都是通过REST请求来发起的.Eureka Server对于各类REST请求的定义都位于com.netflix.eureka.resources包下. 以"服务注册"请求为例:
@POST @Consumes ({ "application/json", "application/xml"}) public Response addinstance(Instanceinfo info, @HeaderParam(PeerEurekaNode.HEADER—REPLICATION) String isReplication) { logger.debug ("Registering instance {} (replication= {})", info.getId (),isReplication); // validate that the instanceinfo contains all the necessary required fields ... // handle cases where clients may be registering with bad DataCenterinfo withmissing data DataCenterinfo dataCenterinfo = info.getDataCenterinfo(); if (dataCenterinfo instanceof Uniqueidentifier) { String dataCenterinfoid = ((Uniqueidentifier) dataCenterinfo) . getid(); if (isBlank(dataCenterinfoid)) { boolean experimental = "true".equalsignoreCase( serverConfig.getExperimental("registration.validation. dataCenterlnfoid")); if (experimental) { String entity= "DataCenterinfo of type " + dataCenterinfo. getClass ()+" must contain a valid id"; return Response.status(400).entity(entity).build(); } else if (dataCenterinfo instanceof Arnazonlnfo) { Arnazonlnfo amazoninfo = (Arnazonlnfo) dataCenterinfo; String effectiveld = amazoninfo.get(Arnazoninfo.MetaDataKey.instanceid); if (effectiveld == null) { amazoninfo.getMetadata() .put( Arnazoninfo.MetaDataKey.instanceid.getName(),info.getld ()); } else { logger.warn("Registering DataCenterinfo of type{} without an appropriate id", dataCenterinfo.getClass()); } } } registry.register(info, "true".equals(isReplication)); return Response. status (204) . build() ; // 204 to bebackwards compatible }
在对注册信息进行了一堆校验之后,会调用org.spirngframework.cloud.netflix.eureka.server.InstanceRegistry对象中的register(InstanceInfo info ,int leaseDreation, boolean isReplication)函数来进行服务注册:
public void register(Instanceinfo info, intleaseDuration, boolean isReplication){ if (log.isDebugEnabled()) { log. debug ("register " + info. getAppName () + ", vip " + info. getVIPAddress () + ", leaseDuration " + leaseDuration + ", isReplication " + isReplication); } this.ctxt.publishEvent(new EurekainstanceRegistered.Event(this,info,leaseDuration, isReplication)); super.register(info, leaseDuration, isReplication); }
在注册函数中,先调用publishEvent函数,将该新服务注册的事件传播出去,然后调用com.netflix.eureka.registry.AbstractInstanceRegistry父类中的注册实现,将InstanceInfo中的元数据信息存储在一个ConcurrentHashMap对象中.正如之前所说的,注册中心存储了两层Map结构,第一层的key存储服务名:InstanceInfo中的appName属性,第二层中的key存储实例名:InstanceInfo中instanceId属性.
服务端的请求和接收非常类似.
-
-
配置详解
Eureka客户端的配置主要分为一下两个方面:
- 服务注册相关的配置信息,包括服务注册中心的地址,服务获取的间隔时间,可用区域等。
- 服务实例相关的配置信息,包括服务实例的名称,IP地址,端口号,健康检查路径等。
Eureka服务短的更多类似于一个现成的产品,所以就不介绍了。
-
服务注册类配置
可以查看EurekaClientConfigBean源码。
-
指定注册中心
在配置文件中指定注册中心,主要是通过eureka.client.serviceUrl参数实现。它的配置值存储在HashMap类型中,并且设置有一组默认值,默认的key为defaultZone,value为http://localhost:8786/erreka/.
private Map<String, String> serviceUrl = new HashMap<> (); { this.serviceUrl.put(DEFAULT_ZONE, DEFAULT_URL); } public static丘nal String DEFAULT_URL = "http://localhost:8761" +DEFAULT_PREFIX + "/"; public static final String DEFAULT_ZONE = "defaultZone";
为了服务注册中心的安全考虑,很多时候我们都会为服务注册中心加入安全校验。在配置serviceUrl时,需要在vlue值的URL中加入相应的安全校验信息,比如http://:@localhost:1111/eureka.其中为安全校验信息的用户名为该用户的密码。
-
其他配置
下表时EurekaClientConfigBean中定义的常用配置参数以及对应说明和默认值,这些参数均已eureka.client为前缀
参数名 说明 默认值 enabled 启用Eureka客户端 true registryFetchIntervalSeconds 从Eureka服务端获取注册信息的间隔时间,单位/s 30 instanceInfoReplicationIntervalSeconds 更新实例信息的变化到Eureka服务间隔的时间 /s 30 initialInstanceInfoReplicationIntervalSeconds 初始化实例信息到Eureka服务端的间隔时间 /s 40 eurekaSserviceUrlPollIntervalSeconds 轮询Eureka服务端地址更改的间隔时间,/s ,当我们与Spring Cloud Config配合,动态刷新Eureka的serviceUrl地址时需要关注该参数 300 eurekaServerReadTimeoutSeconds 读取Eureka Server信息的超时时间,/s 8 eurekaServerConnectTimeoutSeconds 连接Eureka Server 超时时间 /s 5 eurekaServerTotalConnections 从Eureka客户端到所有Eureka服务端的连接总数 200 eurekaServerTotalConnectionsPerHost 从Eureka客户端到每个Eureka服务端主机的连接总数 50 eurekaConnectionIdleTimeoutSeconds Eureka服务端连接的空闲关闭时间,/s 30 hearbeatExecutorThreadPoolSize 心跳连接池的初始化线程数 2 heartbeatExecutoExponentialBackOffBound 心跳超时重试延迟时间的最大乘数值 10 cacheRefreshExecutorThreadPoolSize 缓存刷新线程池的初始化线程数 2 cacheRefreshExecutoExponentialBackOffBoound 缓存刷新重试延迟时间的最大乘数值 10 useDnsForFetchingServiceUrls 使用DNS来获取Eureka服务端的serviceUrl false registerwithEureka 是否要将自身的实例信息注册到Eureka服务端 true preferSameZoneEureka 是否偏好使用处于相同Zone的Eureka服务端 true filterOnlyUpInstances 获取实例时是否过滤,仅保留UP状态的实例 true fetchRegistry 是否从Eureka服务端获取注册信息 true
-
-
服务实例类配置
服务实例类的配置可以查看EurekaInstanceConfigBean的源码,这些信息都是以eureka.instance为前缀。
-
元数据
Eureka客户端向服务端发送注册请求时,用来描述自身服务信息的对象,其中包含一些标准化的元数据,比如服务名称,实例名称,实例IP,实例端口等重要信息。
-
实例名配置
实例名,即是InstanceInfo中的instanceId参数,它是区分同一服务中不同实例的唯一标准。在Netflix Eureka的原生实现中,实例名采用主机名作为默认值,这样的设置使得在同意主机上无法启动多个相同的服务实例。所以,在Spring Cloud Eureka的配置中针对同一主机启动多实例的情况,对实例名的默认命名做了更为合理的扩展,它采用了如下默认规则:
${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}
对于实例命名规则,可以通过eureka.instanc.instanceId参数进行配置。比如,在本地客户端负载均衡调试时,需要启动同一服务的多个实例,如果我们直接启动同一个应用必然会产生端口冲突。虽然可以在命令行中指定不同的server.port来启动,但是这样会略显麻烦。实际上,我们可以直接通过设置server.port=0或者使用随机数server.port=${random.int[10000,19999]}来让Tomcat启动的时候采用随机端口。但是这个时候我们会发现注册到Eureka Server的实例名都是相同的,这会使得只有一个服务实例能够正常提供服务。对于这个问题,我们可以通过设置实例名规则来轻松解决
eureka.instance.instanceId=${spring.application.name}:${random.int}
通过上面的配置,利用应用名加随机数的方式来区分不同的实例,从而实现在同主机上,不指定端口就能轻松启动多个实例的效果。
-
端点配置
在InstanceInfo中,可以看到一些URL的配置信息,比如homePageUrl,statusPageUrl,healthCheckUrl,它们分别代表了应用主页的URL,状态页的URL,健康检查的URL,状态页和健康检查的URL在Spring Cloud Eureka中默认使用spring-boot-actuator模块提供的/info端点和/health端点。为了服务正常运行,必须确保Eureka客户端的/health端点在发送元数据的时候,是一个能够被注册中心访问到的地址,否则服务注册中心不会根据应用的健康检查来更改状态(仅当开启了healthcheck功能时,以该端点信息作为健康检查标准)。而/info端点如果不正确的话,会导致在Eureka面板中单击服务实例时,无法访问到服务实例提供的信息接口。 一般不需要该这几个URL的配置,但特殊情况下,比如应用设置了context-path,这时,所有spring-boot-actuator模块的监控端点都会增加一个前缀。所以,我们就需要做类似如下的配置,为/info和/health端点也加上类似的前缀信息
management.context-path=/hello enreka.instance.statusPageUrlPath=${management.contex-path}/info eureka.instance.healthCheckUrlPath=${management.context-path}/health
有时候为了安全考虑,也有可能会修改/info 和/health端点的原始路径。这个时候也需要做一些特殊配置
endpoints.info.path=/appInfo endpoints.health.path=/checkHealth eureka.instance.statusPageUrlPath=/${endpoints.health.path}
上面的两个示例 使用了eureka.instance.statusPageUrlPath和healthCheckUrlPath参数,这两者都是使用相对路径来配置的,Eureka默认会以Http的方式来访问和暴露这些端点,因此当客户端以https的方式来暴露服务和监控端点时,相对路径就不行了,需要配置绝对路径
eureka.instance.statusPageUrl=https://${eureka.instance.hostname}/info eureka.instance.healthCheckUrl=https://${eureka.instance.hostname}/health eureka.instance.homePageUrl=https://${eureka.instance.hostname}/
-
健康检测
默认情况下Eureka各个服务实例的健康并不是通过spring-boot-actuator模块的/health端点来实现,而是依靠心跳来保持服务实例的存活。在Eureka的服务续约和剔除机制下,客户端的健康状态从注册到注册中心开始都会处于UP状态,除非心跳终止。但是服务注册中心只能保证服务的进程是否正常,但不能保证服务是否能正常提供服务。所以实际上服务消费这调用时可能并不一定能到达预期的效果。
在Spring Cloud Eureka中,可以简单配置以实现更加全面的健康状态维护。
- 在pom.xml中引入spring-boot-starter-actuator模块的依赖
- 在application.properties中增加参数配置eureka.client.healthcheck.enabled=true.
- 如果客户端的/health端点路径做了特殊处理,那就需要按照上边的示例进行配置
-
其他配置
参数 说明 默认值 preferIpAddress 是否优先使用IP地址作为主机名和标识 false leaseRenewalIntervalInSeconds Eureka客户端向服务端发送心跳的时间间隔/s 30 leaseExpirationDurationInSeconds Eureka服务端在收到最后一次心跳之后等待的时间上线,/s。超过该时间后服务端会将该服务实例从服务清单中剔除,从而禁止服务调用请求被发送到该实例上 90 nonSecurePort 非安全的通信端口号 80 securePort 安全的通信端口号 443 nonSecurePortEnabled 是否启用非安全的通信端口号 true securePortEnabled 是否启用安全的通信端口号 appname 服务名,默认取spring.application.name得的配置值,如果没有则为unknown hostname 主机名,不配置的时候将根据操作系统的主机名来获取 上面的配置前三个在需要的时候调整,其他都使用默认。
-
-
跨平台支持
Eureka的通信机制使用了Http的REST接口实现,也是Eureka同其他服务注册工具的一个关键不同点。由于HTTP的平台无关性,虽然Eureka Server通过Java实现,但是在其下的微服务应用并不限于使用Java来进行开发。
-
通信协议
默认情况下,Eureka使用Jersey和XStream配置JSON作为Server与Client之间的通信协议。Jersey是JAX-RS的参考实现,包含三个主要部分。
- 核心服务器:提供标准化注释和API,可以最直观的使用RESTfuWeb服务
- 核心客户端:API可以让REST服务轻松通信
- 集成:还集成了Spring Guice,Apache Abdera的库。
XStream是用来将对象序列 化成XML(JSON)或反序列化为对象的一个Java类库。
-
客户端负载均衡:Spring Cloud Ribbon
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,基于Netflix Ribbon实现。通过Spring Cloud的封装,将面向服务的Rest模板请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,但不像其他服务一样需要独立部署,微服务间的调用实际上都是通过Ribbon来实现的。
-
客户端负载均衡
负载均衡是对系统的高可用,网络压力的缓解和处理能力扩容的重要手段之一。通常说的负载均衡指的是服务端的负载均衡,其中分为硬件负载均衡和软件负载均衡。
硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如F5等;
软件负载均衡是通过在服务器上安装一些具有负载均衡功能或模块的软件来完成请求分发工作。
不论是硬件负载均衡设备还是负载均衡的软件模块都会维护下一个挂可用的服务端清单,通过心跳检测剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡的设备时,该设备按某种算法(线性轮询,权重负载,流量负载等)从维护的可用服务端清单中取出一台服务端的地址,然后进行转发。
客户端负载均衡和服务端负载均衡最大的不同点在于服务清单所存储的位置。在客户端负载均衡中,所有客户端节点都维护着自己要访问的服务清单,而这些服务清单都来自于服务注册中心。同服务端负载均衡的架构类似,在客户端负载均衡中也需要心跳来维护服务端清单的健康性,只是这个步骤需要服务注册中心配合完成。在Spring Cloud实现的微服务治理框架中,默认会创建针对各个服务治理框架的Ribbon自动化整合配置。
通过Spring Cloud Ribbon的封装,我们在微服务架构中使用客户端负载均衡调用非常简单,只需要如下两步:
- 服务提供者只需要启动多个服务实例并注册到一个注册中心或是多个相关的服务注册中心。
- 服务消费者直接通过调用被@LoadBalanced注解修饰过的RestTemplate来实现面向服务的接口调用。
这样,就可以将服务提供者的高可用以及服务消费者的负载均衡调用一起实现了。
-
RestTemplate详解
RestTemplate对象会使用 Ribbon的自动化配置,同时,通过配置@LoadBalanced还能够开启客户端负载均衡。
-
Get请求
第一种:getForEntity函数。该函数返回的是ResponseEntity,该对象是Spring对HTTP请求响应的封装,主要存储了HTTP的几个重要元素,比如HTTP请求状态码的枚举对象HttpStatus,在他的父类HttpEntity中还存储着HTTP请求的头信息对象HttpHeaders以及泛型类型的请求体对象。
'服务消费者:' @Bean @LoadBalanced RestTemplate restTemplate(){ return new RestTemplate(); } ----------------------------------------------- @Autowired private RestTemplate restTemplate; @RequestMapping("test") public String restTemplate1(){ ResponseEntity<String> world = restTemplate.getForEntity("http://GOODS/album/restTemplate?str={1}", String.class, "World"); return world.getBody(); } ========================================================== '服务提供者':Eureka中注册的服务名是goods @RestController @CrossOrigin //跨域注解 @RequestMapping("/album") public class AlbumController { @GetMapping("restTemplate") public String testRestTemplate(String str) { return "RestTemplate,"+str; } }
getForEntity函数提供了下边三个不同的重载实现:
-
getForEntity(String url,Class responseType,Object … urlVariables):
- url为请求的地址;
- responseType为请求响应体body的包装类型;
- urlVariables为url中的参数绑定;
GET请求的参数绑定通常使用url中的拼接方式,而更好的方法是在url中使用占位符并配合urlVariables参数实现GET请求的参数绑定,其中第三个参数会替换url中的占位符。需要注意的是,urlVariables参数是一个数组,所以它的顺序会对应url中占位符定义的数字顺序。
-
getForEntity(String url ,Class responseType,Map urlVariables):
-
使用该方法进行参数绑定时需要在占位符中指定Map中参数的key值,例如url定义为http://USER-SERVICE/user?name={name},在Map类型的urlVariables中,就需要put一个key为name的参数来绑定url中{name}占位符的值:
Map<String,String> params=new HashMap<>(); params.put("name","data"); ResponseEntity<String> forEntity = restTemplate.getForEntity("http://GOODS/user?name={name}",String.class, params); forEntity.getBody();//拿到name中的值
-
-
getForEntity(URI uri,Class responseType):
使用URI和urlVariables参数来指定访问地址和参数绑定。URI是JDKjava.net包下的一个类,他表示一个统一资源标识符引用。
UriComponents build =UriComponentsBuilder.fromUriString("http://USER-SERVICE/user?name={name}").build().expand("dodo").encode(); URI uri = build.toUri(); String body = restTemplate.getForEntity(uri, String.class).getBody();
第二种:getForObject函数,是对getForEntity进一步的封装,通过HttpMessageConverterExtractor对HTTP的请求响应体body内容进行对象转换,实现请求直接返回包装好的对象内容。
String forObject = restTemplate.getForObject(uri, String.class); //当body是一个User对象时,可以直接这样实现: User user = restTemplate.getForObject(uri, User.class);
当不需要关注请求响应除body外的其他内容时,该函数就非常好用。可以少一个Response中获取body的步骤。他与getForEntity函数类似,也提供了三种不同的重载实现。
-
getForObject(String url,Class responseType,Object … urlVariables):
- 与getForEntity方法类似,url指定访问地址,responseType只当返回类型,urlVariables是url中占位符对应的参数。
-
getForObject(String url,Class responseType,Map urlVariables):
- Map类型的urlVariables替代上面数组形式的urlVariables.
-
getForObject(URI uri,Class responseType):
- 使用URI对象替代之前的url和urlVariables参数使用
-
-
POST请求可以通过如下三个方法进行调整实现:
第一种: postForEntity函数。该方法同GET请求的getForEntity类似,在调用后返回ResponseEntity对象,其中T为请求响应的body类型。
User user=new User("zs",30); ResponseEntity<String> stringResponseEntity =restTemplate.postForEntity("http://USER-SERVICE/user", user,String.class); String body = stringResponseEntity.getBody();
postForEntity函数也实现了三种不同的重载方法:
- postForEntity(String url,Object request,Class responseType,Object… urlVariables);
- postForEntity(String url,Object request,Class responseType,Map uriVariables);
- postForEntity(URI uri,Object request,Class responseType);
大部分与getForEntity一致,request参数可以是一个普通对象,也可以是一个HttpEntity对象。如果是一个普通对象,而非HttpEntity对象的时候,RestTemplate会将请求对象转换为一个HttpEntity对象来处理,其中Object就是request的类型,request内容会被视作一个完整的body来处理;如果request是一个HttpEntity对象,就会被当作一个完整的HTTP请求对象来处理,这个request中不仅包含了body的内容,也包含了header的内容。
第二种:
postForObject函数。和getForObject的类型类似:
String s = restTemplate.postForObject("http://USER-SERVICE/user", user, String.class);
postForObject函数也实现了三种不同的重载方法:
- postForObject(String url,Object request,Class responseType,Object… uriVariables);
- postForObject(String url,Object request,Class requestType,Map uriVariables);
- postForObject(URI uri,Object request,Class responseType);
上边三个函数除了返回的对象类型不同,函数的传入参数均与postForEntity一致。
第三种:
postForLocation函数。该方法实现了以POST请求提交资源,并返回新资源的URI,User user=new User("didi",40); URI responseURI = restTemplate.postForLocation("http://USER-SERVICE/user",user);
postForLocation函数的三种重载方法:
-
postForLocation(String url,Object request,Object… urlVariables);
-
postForLocation(String url,Object request,Map urlVariables);
-
postForLocation(URI uri,Object request);
由于postForLocation函数会返回新资源的URI,该URI就相当于指定了返回类型,所以此方法实现的POST请求不需要像postForEntity和postForObject那样指定responseType。其他参数和上边用法相同。
-
PUT请求
put函数实现的三种重载方法:-
put(String url,Object request,Object… urlVariables);
-
put(String url,Object request,Map urlVariables);
-
put(URI uri,Object request);
put函数为void类型,所以没有返回内容,也就没有其他函数定义的responseType参数,除此之外的其他传入参数定义与用法与postForObject基本一致。
-
-
DELETE请求
delete函数的三种重载:-
delete(String url,Object… urlVariables);
-
delete(String url,Map urlVariables);
-
delete(URI url);
由于进行REST请求时,通常都将DELETE请求的唯一标识拼接在url中,所以DELETE请求也不需要request的body信息,其他参数和上面一样。
-
-
源码分析
探索Ribbon如何通过RestTemplate实现客户端的负载均衡的。从@LoadBalanced注解可以知道,该注解用来给RestTemplate做标记,以使用负载均衡的客户端(LoadBalancerClient)来配置它。
public interface ServiceInstanceChooser { org.springframework.cloud.client.ServiceInstance choose(java.lang.String serviceId); } ====================================================================== public interface LoadBalancerClient extends org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser { <T> T execute(java.lang.String serviceId, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest<T> request) throws java.io.IOException; <T> T execute(java.lang.String serviceId, org.springframework.cloud.client.ServiceInstance serviceInstance, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest<T> request) throws java.io.IOException; java.net.URI reconstructURI(org.springframework.cloud.client.ServiceInstance instance, java.net.URI original); }
可以通过抽象方法来了解客户端的负载均衡器中应具备的几种能力:
-
ServiceInstance choose(String serviceId):根据传入的服务名serviceId,从负载均衡器中挑选一个对应服务器的实例。
-
T execute(String serviceId,LoadBalancerRequest request) throws IOException:使用从负载均衡器中挑选出的服务器实例来执行请求内容。
-
URI reconstructURI(ServiceInstance instance,URI original):为系统构建一个合适的host:port形式的URI。在分布式系统中,我们使用逻辑上的服务名称作为host来构建URI(替代服务实例的host:port形式)进行请求,比如 http://myservice/path/to/service。在该操作的定义中,前者ServiceInstance对象是带有host和port的具体服务实例,而后者URI对象则是使用逻辑服务名定义为host的URI,而返回的URI内容则是通过ServiceInstance的服务实例详情拼接出的具体host:post形式的请求地址。
顺着LoadBalancerClient接口的所属包 org.springframework.cloud.client.loadbalancer,可以得出如下关系。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-08HTSE43-1582522296647)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-19 201158.png)]
LoadBalancerAutoConfiguration为实现客户端负载均衡器的自动化配置类。
@org.springframework.context.annotation.Configuration @org.springframework.boot.autoconfigure.condition.ConditionalOnClass({org.springframework.web.client.RestTemplate.class}) @org.springframework.boot.autoconfigure.condition.ConditionalOnBean({org.springframework.cloud.client.loadbalancer.LoadBalancerClient.class}) @org.springframework.boot.context.properties.EnableConfigurationProperties({org.springframework.cloud.client.loadbalancer.LoadBalancerRetryProperties.class}) public class LoadBalancerAutoConfiguration { @org.springframework.cloud.client.loadbalancer.LoadBalanced @org.springframework.beans.factory.annotation.Autowired(required = false) private java.util.List<org.springframework.web.client.RestTemplate>restTemplates; @Bean public SmartInitialzingSingLeton loadBalancedRestTemplateInitializer( final List<RestTemplateCustomizer> customizers){ return new SmartInitializingSingleton(){ @Override public void afterSingletonsInstantiated(){ for(RestTemplate restTemplate: LoadBalancerAutoConfiguration.this.restTemplates){ for(RestTemplateCustomizer customizer:customizers){ customizer.customize(restTemplate); } } } }; } @Bean @ConditionalOnMissingBean public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor){ @Override public void customize(RestTemplate restTemplate) { List<ClientHttpRequestInterceptor> list = new ArrayList<>{ restTemplate.getInterceptors()); list.add(loadBalancerInterceptor); restTemplate.setInterceptors(list); } }; } @Bean public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient){ return new LoadBalancerInterceptor(loadBalancerClient); } } }
从LoadBalancerAutoConfiguration 类头上的注解就可以知道,Ribbon实现的负载均衡自动化配置需要满足下面两个条件。
- @ConditionalOnClass(RestTemplate.class):RestTemplate类必须存在于当前工程的环境中。
- @ConditionalOnBean(LoadBalancerClient.class):在Spring 的Bean工程中必须有LoadBalancerClient的实现Bean.
在该自动化配置类中,主要做了下面三件事:
- 创建了一个LoadBalancerInterceptor的Bean,用于实现对客户端发起请求时进行拦截,以实现客户端负载均衡。
- 创建了一个RestTemplateCustomizer的Bean,用于给RestTemplate增加LoadBalanceerInterceptor拦截器。
- 维护了一个被@LoadBalanced 注解修饰的RestTemplate对象列表,并在这里进行初始化,通过调用RestTemplateCustomizer的实例来给需要客户端负载均衡的RestTemplate增加LoadBalancerInterceptor拦截器。
接下来,看一下LoadBalancerInterceptor拦截器是如何将一个普通的RestTemplate变成客户端负载均衡的:
public class LoadBalancerInterceptor implements ClientHttpRRequestInterceptor{ private LoadBalancerClient loadBalancer; public LoadBalancerInterceptor(LoadBalancerClient loadBalancer){ this.loadBalancer = loadBalancer; } @Override public ClientHttpResponse intercept(final HttpRequest request,final by[] body,final ClientHttpRequestExecution execution) throws IOException{ final URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); return this.loadBalancer.execute(serviceName,new LoadBalancerRequest<ClientHttpResponse>(){ @Override public ClientHttpResponse applly(final ServiceInstance instance) throws Exception{ HttpRequest serviceRequest = new ServiceRequestWrapper(request,instance); return execution.execute(serviceRequest,body); } }); } } private class ServiceRequestWrapper extends HttpRequestWrapper{ private final ServiceInstance instance; public ServiceRequestWrapper(HttpRequest request,ServiceInstance instance){ super(request); this.instance = instance; } @Override public URI getURI(){ UURI uri = LoadBalancerInterceptor.this.loadBalancer.reconstructURI(this.instance,getRequest().getURI()); return uri; } }
通过源码及之前的自动化配置类,可以看到拦截器中注入了LoadBalancerClient的实现。当一个被@LoadBalanced注解修饰的RestTemplate对象向外发起HTTP请求时,会被LoadBalancerInterceptor类的intercept函数所拦截。
由于在使用RestTemplate时采用了服务器名作为host,所以直接从HttpRequest的URI对象中通过getHost()就可以拿到服务名,然后调用execute函数去根据服务名来选择实例并发起实际的请求。
分析到这里,LoadBalancerClient还只是个抽象的负载均衡器接口,它对应的实现类LoadBalancerClient。在execute函数的实现中,第一步通过getServer根据传入的服务名serviceId去获取具体的服务实例。
通过getServer函数的实现源码,可以看到具体的服务实例并没有使用LoadBalancerClient 接口中的choose函数,而是使用了Netflix Ribbon自身的ILoadBalancer接口中定义的chooseServer函数。
ILoadBalancer接口的抽象方法:
- addServers: 向负载均衡器中维护的实例列表增加服务实例。
- chooseServer:通过某种策略,从负载均衡中挑选出一个具体的服务实例。
- markServerDown:用来通知标识负载均衡中某个具体实例已经体制服务,不然负载均衡器在下一次获取服务实例清单前都会认为服务实例均是正常服务的。
- getReachableServers:获取当前正常服务的实例列表。
- getAllServers:获取所有已知的服务实例列表,包括正常服务和停止服务的实例。
在整合Ribbon的时候Spring Cloud默认采用ZoneAware-LoadBalancer来实现负载均衡器。
通过分析可以知道,在用Ribbon实现负载均衡器的时候,实际使用的是Ribbon中定义的ILoadBalancer接口的实现,自动化配置采用ZoneAwareLoadBalancer的实例来实现客户端负载均衡。
-
-
负载均衡器
查看ILoadBalancer接口的实现类-
AbstractLoadBalancer
- ALL:所有服务实例;
- STATUS_UP:正常服务的实例;
- STATUS_NOT_UP:停止服务的实例;
- getServerList(ServerGroup serverGroup):定义了根据分组类型获取不同服务实例的列表。
- getLoadBalancerStats():定义了获取LoadBalancerStats对象的方法。
-
BaseLoadBalancer
该类中定义了有关负载均衡器相关的基础内容。 -
DynamicServerListLoadBalancer
继承BaseLoadBalancer类,它是对基础负载均衡器的扩展,实现了服务实例清单在运行期间的动态更新能力了;具备了对服务实例清单的过滤功能。
-
-
负载均衡策略
-
AbstractLoadBalancerRule
负载均衡的抽象类,该抽象类中定义了负载均衡器ILoadBalancer对象,该对象能在具体实现选择服务策略时,获取到一些负载均衡器中维护的信息作为分配依据,设计出一些算法来实现特定场景的高效策略。 -
RandomRule
实现了服务实例清单中随机选择一个服务实例的功能。通过把随机数当做实例列表的索引获取具体实例。 -
RoundRobinRule
按照线性轮询的方式依次选择每个服务实例的功能。 -
RetryRule
实现了一个具备重试机制的实例选择功能。 -
WeightedResponseTimeRule
该策略对RoundRobinRule的扩展,增加了根据实例的运行情况来计算权重,并根据权重来挑选实例,以达到更优的分配效果,主要有三个核心内容。- 定时任务
- 权重计算
- 实例选择
-
ClientConfigEnabledRoundRobinRule
该策略较为特殊,一般不用。实际上和RoundRobinRUle相同,使用线性轮询机制。 -
BestAvailableRule
该策略可选出最空闲的实例。 -
PredicateBasedRule
抽象策略,先通过工具类对一部分实例进行过滤,然后在以线性轮询方式从过滤后的实例清单中选出一个。 -
AvailabilityFilteringRule
该策略通过线性抽样的方式直接尝试寻找可用且比较空闲的实例来使用,优化了父类每次都要便利所有实例的开销。 -
ZoneAvoidanceRule
先过滤清单,在轮询。
-
-
-
配置详解
-
自动化配置
引入Spring Cloud Ribbon的依赖后,就能够自动化构建下面这些接口的实现。
- IClientConfig:Ribbon的客户端配置,默认采用DefaultClientConfigImpl实现
- IRule: Ribbon的负载均衡策略,默认采用ZoneAvoidanceRule实现,该策略能够在多区域环境下选出最佳区域的实例进行访问。
- IPing: Ribbon的实例检查策略,默认采用NoOpPing实现,该检查策略是一个特殊的实现,实际上它并不会检查实例是否可用,而是始终返回true,默认所有服务实例可用。
- ServerList: 服务实例清单的维护机制,默认采用ConfigurationBasedServerList实现。
- ServerListFilter: 服务实例清单过滤机制,默认采用ZonePreferenceServerListFilter实现,该策略能优先过滤出请求调用方处于同区域的服务实例。
- ILoadBalancer: 负载均衡器,默认采用ZoneAWareLoadBalancerer实现,它具备了区域感知的能力。
以上自动化配置内容仅在没有引入Eureka等服务治理框架时如此。
针对一些个性化配置,只需创建对应的实例覆盖默认配置即可
@Configuration public class MyRibbonConfiguration{ @Bean public IPing ribbonPing(IClientConfig config){ return new PingUrl(); } }
也可以使用@RibbonClient注解来实现更细粒度的客户端配置,比如下面的代码实现了为hello-service服务使用HelloServiceConfiguration中的配置。
@Configuration @RibbonClient(name="hello-service",configuration = HelloServiceConfiguration.cclass) public class RibbonConfiguration{ }
-
参数配置
两种:全局配置以及指定客户端配置。
- 全局配置:使用ribbon. =格式
- 客户端配置:采用.ribbon. = 格式。
-
与Eureka结合
同时引入Spring Cloud Ribbon和Spring Cloud Eureka依赖时,会触发Eureka中实现的对Ribbon的自动化配置。使用物理元数据进行负载均衡。
由于Spring Cloud Ribbon默认实现了区域亲和策略,所以可以通过Eureka实例的元数据配置实现区域化的实例配置方案。
也可以通过参数配置禁用Eureka对Ribbon服务实例的维护实现。
-
-
重试机制
Eureka(在服务治理强调可用性和可靠性),ZooKeeper(强调一致性,可靠性)最大的区别在于Eureka为了实现高可用,牺牲了一致性,在极端的情况下接受故障实例也不丢掉"健康"实例。
由于Eureka在可用性和一致性上的取舍,不论触发保护机制还是服务剔除延迟,引起服务调用故障实例的时候,希望能够增加容错,所以在实现服务调用时通常加入一些重试机制,这需要我们自己扩展完成。
服务容错保护:Spring Cloud Hystrix
由于每个单元在不同的进程中运行,依赖通过远程调用的方式执行,这样可能由于网络或服务自身问题出现调用故障或延迟,若此时请求不断增加,最后可能导致任务积压,最终导致服务自身瘫痪。
当某个服务单元发生故障之后,通过断路器的故障监控,像调用发返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免故障在分布式系统中的蔓延。
针对上述问题,Spring Cloud Hystrix实现了断路器,线程隔离等一系列服务保护功能。
Hystrix具备服务降级,服务熔断,线程和信号隔离,请求缓存,请求合并以及服务监控强大功能。
开始使用
-
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency>
-
在服务消费者的主类中使用**@EnableCircuitBreaker**注解开启断路器功能:
-
注意:还可以使用Spring Cloud应用中的@SpringCloudApplication注解来修饰应用主类:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker public @interface SpringCloudApplication { }
-
可以在服务调用的方法上增加@HystrixCommand注解指定回调方法
@Autowired private RestTemplate restTemplate; @HystrixCommand(fallbackMethod = "error") //添加容错调用的方法。 @RequestMapping("test") public String restTemplate1(){ ResponseEntity<String> world = restTemplate.getForEntity("http://GOODS/album/restTemplate?str={1}", String.class, "World"); return world.getBody(); } //某个服务停止或出现问题时调用的方法。 public String error(){ return "error"; }
Hystrix默认超时时间为2000ms
-
原理分析
-
工作流程
- 创建HystrixCommand或SystrixObservableCommand对象。用来表示对依赖服务的操作请求。
- HystrixCommand:用在依赖的服务返回单个操作结果的时候。
- HystrixObservableCommand:用在依赖的服务返回多个操作结果的时候。
- 创建HystrixCommand或SystrixObservableCommand对象。用来表示对依赖服务的操作请求。
-
依赖隔离
Docker通过“舱壁模式”实现进程隔离,使得容器与容器间互不影响。Hystrix则使用该模式实现线程池的隔离,为每个依赖服务创建一个独立的线程池,就算某个依赖服务出现延迟的情况,也只是对该依赖服务的调用者产生影响,而不会拖慢其他依赖服务。
- 通过隔离线程池,可以带来如下优势:
- 应用自身得到完全保护,不会受到不可控的依赖服务影响。即便给依赖服务分配的线程池被填满,也不会影响应用自身的其余部分。
- 可以有效降低接入新服务的风险。如果新服务接入后运行不稳定或存在问题,完全不会影响应用其他的请求。
- 当依赖的服务从失效恢复正常后,它的线程池也会被清理并且能够马上恢复健康的服务,相比之下,容器级别的清理恢复速度慢的多。
- 当依赖的服务出现配置错误的时候,线程池也会快速反映出此问题。同时,可以在不影响应用功能的情况下实时动态属性刷新来处理它。
- 线程池也可以监控依赖服务 性能指标信息的变化。
- 每个专有线程池都提供了内置的并发实现,可以利用它为同步的依赖服务构建异步访问。
Hystrix中除了使用线程池之外,还可以使用信号量来控制单个依赖服务的并发度,信号量的开销要比线程池小的多,但是它不能设置超时和实现异步访问。所以在依赖服务足够可靠的情况下才使用信号量。
- 通过隔离线程池,可以带来如下优势:
-
异常处理
- 异常传播
当需要在命令执行中抛出不触发降级的异常时来使用它。
@HystrixCommand(ignoreExceptions = {HystrixBadRequestException.class}) @RequestMapping("test") public String restTemplate1(){ ResponseEntity<String> world = restTemplate.getForEntity("http://GOODS/album/restTemplate?str={1}", String.class, "World"); return world.getBody(); }
当restTemplate1()方法抛出类型为BadRequestException的异常时,Hystrix会将它包装在HystrixBadRequestException中抛出,这样就不会触发后续的fallback逻辑。
-
异常获取
可以用getFallback()方法通过Throwable getExecutionException()方法来获取具体的异常,通过判断进入不同的处理逻辑。注解也同样实现获取异常,只需要在fallback实现的方法增加参数Throwable e对象的定义。
public String error(String id,Throwable e) { assert "get".equals(e.getMessage()); return "error"; }
- 异常传播
-
命令名称,分组以及线程池划分
通过设置命令组,Hystrix会根据组名来组织和统计命令的告警,仪表盘等信息。
Hystrix命令默认的线程划分是根据命令分组实现的,默认情况下,Hystrix会让相同组名的命令使用同一个线程,所以需要我们在创建Hystrix命令时为其指定命令组来实现默认的线程池划分。
当使用@HystrixCommand注解的时候,需要设置commandKey,groupKey以及threadPoolKey属性即可,它们分别表示命令名称,分组以及线程池划分,
@HystrixCommand(commandKey = "getUserByid", groupKey = "UserGroup", threadPoolKey = "getUserByidThread") public User getOserByid(Long id) { return restTemplate.getForObject("http://USER-SERVICE/users/{1}", User.class,id); }
-
请求缓存
在高并发场景下,Hystrix中提供了请求缓存的功能,我们可以方便的开启和使用请求缓存来优化系统,达到减轻高并发时的请求线程消耗,降低请求响应时间的效果。- 开启请求缓存功能
只需实现HystrixCommand或HystrixObservableCOmmand时,通过重载getCacheKey()方法来开启请求缓存
缓存的好处:
- 减少重复的请求 数,降低依赖服务的并发度。
- 同一用户请求的上下文中,相同依赖服务的返回数据始终保持一致。
- 请求缓存在run()和construct()执行之前生效,所以可以有效减少不必要的线程开销。
-
清理失效和缓存功能
HystrixRequestCache.clear() -
使用注解实现请求缓存
注解 描述 属性 @CacheResult 标记请求命令返回的结果应该被缓存,他必须与@HystrixCommand注解结合使用 cacheKeyMethod @CacheRemove 该注解用来让请求命令的缓存失效,失效的缓存根据定义的Key决定 commandKey,cacheKeyMethod @CacheKey 用来在请求命令的参数上标记,使其作为缓存的Key值,如果没有标注则会使用所有参数。如果同时是用来@CacheResult和@CacheRemove注解的cacheKeyMethod方法指定缓存Key的生成,那么该注解将不会起作用 value -
使用注解实现请求合并器
@HystrixCollapser(batchMethod = "findAll",collapserProperties={ @HystrixProperty(name="timerDelayInMilliseconds",value = "100") }) public User find(Long id){ return null; } @HystrixCommand public List<User> findAll(List<Long> ids){ return restTemplate.getForObject("http://USER-SERVICE/user?ids={1}",List.class,StringUtils.join(ids,",")); }
- 开启请求缓存功能
-
-
Hystrix仪表盘
声明式服务调用:Spring Cloud Feign
在RestTemplate的基础上做了进一个的封装,从而完成服务调用。
-
快速入门
-
pom.xml中引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> <version>1.4.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-feign</artifactId> <version>1.4.7.RELEASE</version> </dependency>
-
主类添加@EnableFeignClients注解开启Spring Cloud Feign支持 功能。
-
定义接口,通过@FeignClient注解指定服务名来绑定服务,然后使用Spring MVC的注解绑定具体该服务提供的REST接口。
@Component @FeignClient(value = "goods") //调用goods服务 @RequestMapping("/album") public interface SkuFeign { @GetMapping Result findAll(); } // 注意:服务名不区分大小写
-
创建消费者服务实现对Feign客户端的调用。使用@Autowired直接注入上面定义的SkuFeign实例,然后直接调用接口中的方法。
-
-
参数绑定
在接口中的方法定义参数和Controller中方法传参一样。 -
继承特性
可以观察到,接口中的方法基本都是从服务提供发的Controller中复制的。所以需要继承来解决这些复制操作。- 对服务提供者进行重构:先定义一个单独的模块在该模块中定义接口和需要的实体类,然后让服务提供者的pom.xml依赖那个模块,让Controller实现该接口的所有方法。
- 服务消费者:和服务提供者一样依赖那个单独的模块,然后在让该server层接口继续那个模块中的接口,并且添加@FeignClient注解来绑定服务。
- 最后在消费者的Controller中注入server层 实例,实现调用
优缺点:
优点就是将接口的定义从Controller中剥离,同时配合Maven私有仓库就能轻易实现接口定义的共享,实现在构建期的接口绑定,从而减少服务客户端的绑定配置。 缺点:由于接口 在构建期间就建立了依赖,那么接口变动就会对项目构建造成影响,所以开发评审期间严格遵守面向对象的开闭原则,尽可能的做好前后的版本兼容,防止牵一发而动全身的后果。
-
Ribbon配置
由于Spring Cloud Feign 的客户端负载均衡是通过Spring Cloud Ribbon实现的,所以我们可以直接通过配置Ribbon客户端的方式来自定义各个服务客户端调用的参数。 那么如何在使用Spirng Cloud Feign工程中使用Ribbon的配置呢?
-
全局配置
全局配置的方法非常简单,可以直接使用ribbon. = 的方式来设置ribbon的各项默认参数。比如修改默认的客户端调用超时时间:
ribbon.ConnectTimeout = 500 ribbon.ReadTimeout =5000
-
指定服务配置
针对各个服务客户端进行个性化配置的方式使用Spring Cloud Ribbon时的配置方式是一样的,都采用.ribbon.key=value 的格式进行设置。在使用@FeignClient注解的时候,会用该注解的name属性或value属性指定服务名,自动创建一个同名的Ribbon客户端。在使用@FeignClient(value=“GOODS”)来创建Feign客户端的时候,同时也创建了一个名为GOODS的Ribbon客户端。所以我们可以使用@FeignClient注解中的name或 value属性值来设置对应的Ribbon参数,比如:
GOODS.ribbon.ConnectTimeout=500 GOODS.ribbon.ReadTimeout=2000
-
重试机制
Ribbon的超时与Hystrix的超时是两个概念。需要让Hystrix的超时时间大于Ribbon的超时时间,否则Hystrix命令超时后,该命令直接熔断,重试机制就没有任何意义了。
-
-
Hystrix配置
在Spring Cloud Feign 中,除了引入了客户端的负载均衡的Spring Cloud Ribbon之外,还引入了服务保护与容错的工具Hystrix。默认情况下,Spring Cloud Feign会将所有Feign客户端封装到Hystrix的命令中进行服务保护。
接下来介绍Spring Cloud Feign在使用时配置Hystrix属性以及如何实现服务降级。
-
全局配置
对于Hystrix的全局配置同Spring Cloud Ribbon的全局配置一样,直接使用它的默认配置前缀hystrix.command.default就可以进行设置:比如设置超时时间hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
在对Hystrix进行配置之前,需要确认feign.hystrix.enabled参数设置为false,否则该参数设置会关闭Feign客户端的Hystrix支持。
对于Hystrix的超时时间控制也可以使用上面的配置来增加熔断时时间,或者使用hystrix.command.default.execution.timeout.enabled=false来关闭熔断功能。
-
禁用Hystrix
如果不想全局的关闭Hystrix支持,而只想针对某个服务客户端关闭Hystrix支持时,需要使用@Scope(“prototype”)注解为指定的客户端配置Feign.Builder实例。-
构建一个关闭Hystrix的配置类。
@Configuration public class DisableHystrixConfiguration{ @Bean @Scope("prototype") public Feign.Builder feignBuilder(){ return Feign.builder(); } }
通过@FeignClient注解的configuration属性引入配置类的运行时类。
-
-
指定命令配置
采用hystrix.command.作为前缀。默认采用Feign客户端中的方法名作为标识。比如对/hello接口的熔断超时时间配置:hystrix.command.hello.execution.isolation.thread.timeoutInMilliseconds=5000
-
服务降级配置
通过@HystrixCommand注解的fallback参数指定具体降级处理的方法。而Spring Cloud Feign提供了另一种方式,就是为Feign客户端的定义接口编写一个具体的接口实现类。
@Component public class HelloServiceFallback implements HelloService{ @Override public String hello(String name) { return "error"; } @Override public User hello(String name, Integer age) { return new User("未知",0); } @Override public String hello(User user) { return "error"; } }
在服务绑定接口的@FeignClient注解的fallback属性来指定对应的服务降级实现类。
@RequestMapping("/refactor") @FeignClient(name = "GOODS",fallback = HelloServiceFallback.class) public interface HelloService { @RequestMapping(value = "/hello4", method = RequestMethod.GET) String hello(@RequestParam("name") String name); @RequestMapping(value = "/hello5", method = RequestMethod.GET) User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age); @RequestMapping(value = "/hello6",method = RequestMethod.POST) String hello(@RequestBody User user); }
-
-
其他配置
-
请求压缩
Spring Cloud Feign支持请求与响应进行GZIP压缩,以减少通信过程中的性能损耗。feign.compression.request.enabled=true feign.compression.response.enabled=true
压缩更细致的配置
feign.compression.request.enabled=true feign.compression.request.mime-types=text/xml,application/json #请求类型 feign.compression.request.min-request-size=2048 #请求压缩的大小限制
-
日志配置
在配置文件中使用logging.level.的参数配置格式来开启指定Feign客户端的DEBUG日志,其中为客户端定义接口的完整路径。logging.level.com.didispace.web.HelloService=DEBUG
由于Feign客户端默认的Logger.Level对象定义为NONE级别,该级别不会记录任何Feign调用过程中的信息,可以在应用主类中直接加入Logger.Level的Bean创建,也可以通过配置类实现:
@Bean Logger.Level feignLoggerLevel(){ return Logger.Level.FULL; }
如果通过配置类来实现的,那么需要在@FeignClient的configuration属性中配置该配置类的运行时。
对于Feign的Logger级别主要有下面4类:
- NONE:不记录任何信息
- BASIC:仅记录请求方法,URL以及响应状态码和执行时间。
- HEADERS:除了记录BASIC级别的信息之外,还记录请求和响应的头信息。
- FULL:记录所有请求与响应的明细,包括头信息,请求体,元数据等。
-
API网关服务:Spring Cloud Zuul
需要Zuul网关的原因:
- 运维角度:需要手工维护那些路由规则与服务实例列表和对修改的信息保持一致性。
- 开发人员角度:对于每个微服务都需要一套验证签名。并且后期修改逻辑中的BUG不方便。
目前构建项目使用Zuul很少,一般会使用Spring Cloud Gateway来做网关或者Nginx;
使用Gateway作为网关时,解决跨域配置:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': #匹配所有请求
allowedOrigins: "*" #跨域处理,允许所有的域
allowedMethods: #支持的方法
- GET
- POST
- PUT
- DELETE
routes:
- id: xxx # 唯一标识
uri: http://localhost:8080 #用户请求需要路由到该服务(要路由的服务地址)
predicates: #路由规则
- Host: cloud.mh.com** #所有请求的域名规则配置,所有cloud.mh.com开头的请求将被路由
API网关就像整个微服务架构的门面,所有的外部访问都需要经过它来进行调度与过滤。
Spring Cloud中提供了基于NetflixZuul实现的API网关组件——Spring Cloud Zuul。
Spring Cloud Zuul通过与Eureka进行整合,将自身注册为Eureka服务治理下的应用,从Eureka中获取其他微服务实例的信息。Zuul默认会通过服务名作为ContextPath的方式来创建路由映射。
对于类似签名校验,登录校验在微服务架构中的冗余问题,单独剥离出来作为一个独立的服务存在。通过SpringCloudZuul提供的一套过滤机制,实现统一调用对微服务接口做前置过滤。
-
快速入门
-
构建网关
引入依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zuul</artifactId> <version>1.4.7.RELEASE</version> </dependency>
spring-cloud-starter-zuul不仅包含了Netflix Zuul的核心依赖zuul-ccore,还包含了下面这些网关服务需要的重要依赖。
- spring-cloud-starter-hystrix:实现对微服务转发时候的保护机制。
- spring-cloud-starter-ribbon:实现在网关服务进行路由转发时的客户端负载均衡以及请求重试。
- spring-boot-starter-actuator:提供常规的微服务管理端点。
应用主类:使用@EnableZuulProxy注解开启Zuul的API网关服务功能。
完成以上工作,Zuul实现的API网关就构建完毕了。
-
-
请求路由
组需要对路由服务增加一些相关路由规则的配置,就能实现传统的路由转发功能:
zuul.routes.api-a-url.path=/api-a-url/** zuul.routes.api-a-url.url=http://localhost:8080/
该配置定义了发往API网关服务的请求中,所有符合/api-a-url/**的规则的访问都将被路由转发到http://localhost:8080/地址上,也就是说,当我们访问http://localhost:5555/api-a-url/hello的时候,API网关服务会将改请求路由到http://localhost:8080/hello提供的微服务接口上。其中,配置属性zuul.routes.api-a-rul映射的路由名要相同。
-
面向服务的路由
Spring Cloud Zuul实现了与Spring Cloud Eureka的无缝整合,可以让路由的path映射到具体的某个服务,而具体的url则交给Eureka的服务发现机制去自动维护。
-
导入Eureka的依赖:spring-cloud-starter-eureka
-
在配置文件中指定Eureka注册中心的位置,并且配置服务路由。
zuul.routes.api-a.path=/api-a/** zuul.routes.api-a.serviceId=hello-service zuul.routes.api-b.apth=/api-b/** zuul.routes.api-b.serviceId=feign-consumer eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
在上面的配置中分别定义了api-a和api-b的路由来映射hello-service和feign-consumer两个微服务。另外,通过指定EurekaServer服务注册中心的位置,除了将自己注册成服务外,同时也让Zuul能够获取hello-service和feign-consumer服务的实例清单,以实现path映射服务,再从服务中挑选实例来进行请求抓案发的完整路由机制。
通过简单的path与serviceId映射组合,使得维护工作变得非常简单。着完全归功于Spring Cloud Eureka的服务发现机制。
-
-
请求过滤
只需要继承ZuulFilter抽象类并实现它定义的4个抽象函数就可以完成对请求的拦截和过滤了。下面代码定义了简单点的Zuul过滤器,它实现了请求被路由之前检查HttpServletRequest中是否有accessToken参数,若有就进行路由,若没有就拒绝访问,返回 401 Unauthorized错误。
public class AccessFilter extends ZuulFilter { private static Logger log= LoggerFactory.getLogger(AccessFilter.class); @Override public String filterType() { // 过滤器类型,他决定过滤器在请求的哪个生命周期中执行。这里定义为pre,代表会在请求被路由之前执行,routing:路由请求时被调用,post:在routing和error过滤器之后被调用,error:处理请求时发生错误时被调用。 return "pre"; } @Override public int filterOrder() { //过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来一次执行,数值越小优先级越高。 return 0; } @Override public boolean shouldFilter() { //判断该过滤器是否需要被执行。返回true,因此该过滤器对所有请求都会生效。实际运用中可以利用该函数指定过滤器的有效范围。 return true; } //过滤器的具体逻辑。通过ctx.setSendZuulResponse(false)让zuul过滤该请求,不对其路由,然后通过ctx.setResponseStatusCode(401)设置返回的错误吗 @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString()); String accessToken = request.getParameter("accessToken"); if (accessToken == null){ log.warn("access token is empty"); ctx.setSendZuulResponse(false); ctx.setResponseStatusCode(401); return null; } log.info("access token ok"); return null; } }
创建具体的Bean启动过滤器,在应用主类创建具体的Bean
@EnableZuulProxy @SpringCloudApplication public class Application { @Bean public AccessFilter accessFilter(){ return new AccessFilter(); } public static void main(String[] args) { new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args); } }
- 它作为系统的统一入口,屏蔽了系统内部各个微服务的细节。
- 它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发。
- 它可以实现接口权限与微服务业务逻辑的解耦。
- 通过服务网关中的过滤器,在各生命周期中去校验请求的内容,将原本在对外服务层做的校验迁移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。
-
-
路由详解
由于默认情况下所有Eureka上的服务都会被Zuul自动地创建映射关系来进行路由,这会让我们不希望对外开放地服务也可能被外部访问到。这个时候我们可以使用zuul.ignored-services参数来设置一个服务名匹配表达式来定义不自动创建路由地规则。 Zuul在自动创建服务路由地时候会根据该表达式来进行判断。如果服务名匹配表达式,那么Zuul将跳过改服务,不为其创建路由规则。比如设置zuul.ignored-services=*时,Zuul将对所有地服务都不自动创建路由规则。在这种情况下,我们就要在配置文件中逐个巍峨哇i需要路由地服务添加映射规则(可以使用path与serviceId组合地配置方式,也可以使用跟简洁地zuul.routes.=
配置方式),只有在配置文件中出现地映射规则会被创建路由,而从Eureka中获取地其他服务,Zuul将不会在为它们创建路由规则。 -
自定义路由规则
为了兼容外部不同版本地客户端程序,一般会采用开闭原则进行设计与开发。这使得系统在迭代过程中,有时候会需要我们为一组互相配合地微服务定义一个版本标识来方便管理它们地版本关系。默认情况下,Zuul自动为服务创建地路由表达式会采用服务名作为前缀,通常为不同版本地微服务应用生成以版本号作为路由前缀定义地路由规则。
实现步骤如下:
@Bean public PatternServiceRouteMapper serviceRouteMapper(){ return new PatternServiceRouteMapper( "(?<name>^.+)-(?<version>v.+$)","${version}/${name}" ); }
PatternServiceRouteMapper对象可以让开发者通过正则表达式来自定义服务与路由映射地生成关系。其中构造函数地第一个参数是用来匹配服务名称是否符合该自定义规则地表达式,第二个参数根据服务名中定义地内容转换出的路径表达式规则。
当在API网关中定义了PatternServiceeRouteMapper实现之后,只要符合第一个参数定义规则的服务名,都会优先使用该实现构建出的路径表达式,如果没有匹配上的服务则还是会使用默认的路由映射规则,即采用完整服务名作为前缀的路径表达式。
-
路径匹配
在Zuul中,路由匹配的路径表达式采用了Ant风格定义。Ant风格的路径表达式使用起来非常方便,他一共有下面三种通配符:
通配符 说明 ? 匹配任意单个字符 * 匹配任意数量的字符 ** 匹配任意数量的字符,支持多级目录 示例:
URL路径 说明 /user-service/? /user-service/a, /user-service/b /user-service/* /user-uservice/a, /user-uservice/abca /user-service/** /user-uservice/a/b -
忽略表达式
zuul.ignored-patterns可以用来设置不希望被API网关路由的URL表达式。 如果不希望/hello接口被路由,那么可以如下设置:
zuul.ignored-patterns=/**/hello/** zuul.routes.api-a..path=/api-a/** zuul.routes.api-a.serviceId=hello-service
如果访问http://localhost:8888/api-a/hello那么就不会路由。
该参数需要注意它的范围是对所有路由。所有在设置的时候需要全面考虑URL规则。
-
路由前缀
Zuul提供了zuul.prefix参数进行设置。比如希望网关上的路由规则都增加/api前缀,可以在配置文件中增加zuul.prefix=/api。对于代理前缀会默认从路由中移除 ,可以设置zuul.stripPrefix=false来关闭该移除代理前缀的动作,也可以通过zuul.routes..strip-prefix=true来对指定路由关闭移除代理前缀的动作。注意:使用路由前缀的时候可能有一些BUG,所以要避免表达式的起始字符串与zuul.prefix参数相同
-
本地跳转
只需要通过path与url的配置方式就能完成,通过url中使用forward来指定需要跳转的服务器资源路径。 例如:api-a路由实现了将符合/api-a/**规则的请求转发到http://localhost:8008/;而api-b路由则使用了本地跳转,实现了将符合/api-b/**规则的请求转发到API网关以/local为前缀的请求上,有API网关进行本地处理。比如API网关接收到请求/api-b/hello,它符合api-b地路由规则,所以该请求会被API网关转发到网关的/local/hello请求上进行本地处理。
zuul.routes.api-a.path=/api-a/** zuul.routes.api-a.url=http://localhost:8001/ zuul.routes.api-b.path=/api-b/** zuul.routes.api-b.url=forward:/local
-
Cookie与头信息
默认情况下,Spring Cloud Zuul在请求路由时,会过滤掉HTTP请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器。所以我们在开发Web项目时常用的Cookie在Spring Cloud Zuul网关中默认是不会传递的,如果我们使用了Spring Security,Shiro等安全框架构建的Web应用通过Spring Cloud Zuul构建网关进行路由时,由于Cookie信息无法传递,我们的Web应用将无法实现登录和鉴权。
为了解决这个问题,配置的方法有很多:
-
通过设置全局参数为空来覆盖默认值(不推荐,破坏了默认设置的用意):
zuul.sensitiveHeaders=
-
通过指定路由的参数来配置(推荐,仅对指定的web应用开启对敏感信息传递,影响范围小,不至于引起其他服务的信息泄露问题):
#方法一:对指定路由开启自定义敏感头 zuul.routes.<router>.customSensitiveHeaders=true #方法二:将指定路由的敏感头设置为空 zuul.routes.<router>.sensitiveHeaders=
-
重定向问题
低版本可能会报302状态码zuul.addHostHeader=true
-
-
Hystrix和Ribbon支持
spring-cloud-starter-zuul依赖本身包含了对spring-cloud-starter-hystrix和spring-cloud-starter-ribbon模块的依赖,所以Zuul天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。但使用path和url的映射关系来配置路由规则的时候,对于路由转发的请求不会采用HystrixCommand来包装,所以这类路由请求没有线程隔离和断路器的保护,并且不会有负载均衡的能力。因此使用zuul时尽量使用path和serviceId的组合来进行配置,这样不仅保证API网关的健壮和稳定,也能用到Ribbon的客户端负载均衡功能。
-
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:该参数用来设置API网关中路由转发请求的HystrixCommand执行超时时间,单位毫秒。当路由转发请求的命令执行时间超出该配置值之后,Hystrix会将该执行命令标记为Timeout并抛出异常,Zuul会对该异常进行处理并返回如下JSON信息给外部调用方。
{ "timestamp": 123456765432, "status": 500, "error": "Internal Server Error", "exception":"com.netflix.zuul.exception.ZuulException", "message":"TIMEOUT" }
-
ribbon.ConnectTimmeout:该参数用来设置路由转发请求的时候,创建请求连接的超时时间。当ribbon.ConnectTimeout的配置值小于hystrix。command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,若出现路哟请求出现连接超时,会自动进行重试路由请求,如果重试依然失败,Zuul会返回如下JSON信息给外部调用方。
{ "timestamp": 123456765432, "status": 500, "error": "Internal Server Error", "exception":"com.netflix.zuul.exception.ZuulException", "message":"NUMBEEROF_RETRIES_NEXTSERVER_EXCEEDED" }
-
ribbon.ReadTimeout:设置路由转发请求的超时时间。他的处理与ribbon.ConnectTimeout类似,只是它的超时是对请求连接建立之后的处理时间。当ribbon.ReadTimeout的配置值小于hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds配置值的时候,若路由请求的处理时间超过该配置值且依赖服务的请求还未响应的时候,会自动进行重试路由 请求。如果重试后依然没有获得请求响应,Zuul会返回NUMBEROF_RETRIES_ENXTSERVER_EXCEEDED错误。如果ribbon.ReadTimeout的配置大于hystrix.command.default.execution.islation.thread.timeoutInMilliseconds配置值,若路由请求的处理时间超过改配置值且依赖服务的请求还未响应时,不会进行重试路由请求,而是直接按请求命令超时处理,返回TIMEOUT的错误信息。
如果需要关闭重试机制,可配置如下:
zuul.retryable=false #全局关闭长重试机制 zuul.routes.<route>.retryable=false #指定路由关闭重试机制
-
-
-
过滤器详解
zuul包含了请求路由和过滤器两个功能。 路由功能负责将外部请求转发到具体的微服务示例上,是实现外部访问同一入口的基础;
过滤器负责对请求的处理过程进行干预,时实现请求校验,服务聚合等功能的基础。
在Spring Cloud Zuul中实现的过滤器必须包含4个基本特征:过滤类型,执行顺序,执行条件,具体操作。这些元素看起啦非常熟悉,实际上他就是ZuulFilter接口中定义的4个抽象方法。
-
请求生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TkARezgZ-1582522296648)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-22 124159.png)]
当外部HTTP请求到达API网关服务的时候,首先他会进入第一个阶段pre,在这里它会被pre类型的过滤器进行处理,该类型过滤器的主要目的时在进行请求路由之前做一些前置加工,比如请求的校验等。完成pre类型过滤后进入及二阶段routing,也就是请求转发阶段,请求被routing类型过滤器处理。具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例请求结果都返回之后,routing阶段完成,请求进入第三个阶段post。此时请求将会被post类型的过滤器处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,可以对处理结果进行一些加工或转换等内容。另外还有一个特殊的阶段error,该节点只有在上述三个阶段中发生异常时才会触发,但是它的最后流向还是post类型的过滤器,因为他需要通过post过滤器将最终结果返回给请求客户端。
-
核心过滤器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-juff6ZUQ-1582522296648)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-22 125319.png)]
-
异常处理
在run方法中使用try-catch处理,并且catch中向RequestContext设置ERROR_STATUS_CODE,error.message,error.exception的key和value.- error.status_code:错误编码;
- error.exception:Exception异常对象
- error.message:错误信息。
其中,error.status_code参数是SendErrorFilter过滤器用来判断是否需要执行的重要参数。
-
不足与优化
自定义过滤器中处理异常的两种基本解决方法:- 通过在各阶段的过滤器中增加try-catch块,实现过滤器内部的异常处理。
- 利用error类型过滤器的生命周期特性,集中处理pre,route,post阶段抛出的异常细信息。
-
自定义异常信息
错误信息实际不是Spring Cloud Zuul完成的,SendErrorFilter会根据请求的上下文保存错误的信息来组织一个forward到/error端点的请求来获取错误的响应,所以我们的扩展目标转移到/error端点的实现,可以看源码。如果不希望将exception熟悉返回给客户端,那就可以编写一个自定义的实现,可以基于DefaultErrorAttributes,然后重写getErrorAttributes方法,从原来的结果中将exception移除即可,实现如下
public class DidiErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes ( RequestAttributes requestAttributes, boolean includeStackTrace) { Map<String, Object> result = super.getErrorAttributes(requestAttributes, includeStackTrace); result.remove("exception"); return result; } }
最后为了让自定义的错误信息生效,创建实例来替代默认的实现:
@Bean public DefaultErrorAttributes errorAttributes(){ return new DidiErrorAttributes(); }
-
禁用过滤器
Zuul中特别提供了一个参数来禁用指定的过滤器:zuul.<SimpleClassName>.<filterType>.disable=true
:代表过滤器类名;
:代表过滤器类型;如pre;
很多时候可以禁用Spring Cloud Zuul中默认定义的核心过滤器。这样我们就可以抛开Spring Cloud Zuul自带的那套核心过滤器,实现一套更符合我们实际需求的处理机制。
-
动态加载
作为最外部的网关,它必须具备动态更新内部逻辑的能力,比如动态修改路由规则,动态添加/删除过滤器等。- 动态路由
实现Zuul动态路由,可以与Spring Cloud Config的动态刷新机制联系在一起。只需要API网关服务的配置文件通过Spring Cloud Config连接的Git仓库存储和管理,就能轻松实现动态刷新路由规则的功能。
重构API网关服务,该服务的配置从config-server中获取:
-
创建一个基础的Spring Boot工程,命名为api-gateway-dynamic-route.
-
在pom.xml中引入对zuul,eureka和config的依赖。
-
在/resource目录下创建配置文件bootstrap.properties,并在该文件中指定config-server和erureka-server的具体地址,以获取应用的配置文件和实现服务注册与发现。
spring application.name=api-gateway server.port=8888 spring.cloud.config.uri=http://localhost:7001/ eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
-
创建启动API网关的应用主类。这里需要使用@RefreshScope注解来将Zuul的配置内容动态化
@EnableZuulProxy @SpirngCloudAppliction public class Application{ public static void main(String[] args){ new SpringApplicationBuilder(Application.class).web(true).run(args) } @Bean @RefreshScope @ConfigurationProperties("zuul") public ZuulProperties zuulProperties(){ return new ZuulProperties(); } }
-
完成了相关编写后,还需要在Git仓库中添加网关的配置文件,取名为api-gateway.properties.在排位置文件中,为API网关服务预定义一下路由规则。例如:
zuul.routes.service-a.path=/service-a/** zuul.routes.service-a.serviceId=hello-service zuul.routes.service-b.path=/service-b/** zuul.routes.service-b.url=http://localhost:8001
对于Git仓库中的配置文件名完全取决于网关应用配置文件bootstrap.properties中spring.application.name属性的配置值。
-
修改Git仓库的配置文件中的映射信息,并推送到远程仓库;
-
向api-gateway-dynamic-route的/refresh接口发送post请求来刷新配置信息。当配置文件有修改的时候,该接口会返回被修改的属性名称。
不足的一点就是,Spring Cloud Config没有UI管理界面,我们不得不通过Git客户端来进行修改和配置。
- 动态路由
-
动态过滤器
API网关的另外一个功能——请求过滤器的动态加载。
请求路由通过配置文件就能实现,而请求过滤则是通过编码实现。所以对于实现请求过滤器器的动态加载,我们需要借助基于JVM实现的动态语言的帮助,比如Groovy.
下面,通过简单示例来演示如何构建一个具备动态加载Groovy过滤能力的API网关服务的详细步骤。
-
创建一个基础的Spring Boot工程,命名为api-gateway-dynamic-filter.
-
在pom.xml中引入对zuul,erureka和groovy的依赖。
-
在/resource目录下创建配置文件application.properties,并在该文件设置api网关服务应用名和端口号,以及指定eureka-server的具体地址,同时在配置一个用于测试的路由规则。
spring.application.name=api-gateway server.port=5555 eureka.cient.serviceUrl.defaultZone=http://localhost:1111/eureka zuul.routes.hello.path=/hello-service/** zuul.routes.hello.serviceId=hello-service
下面增加动态过滤器功能:
-
先自定义一些配置来动态加载过滤器的参数,并将它们的配置值加入到application.properties中
zuul.filter.root=filter #指定动态加载的过滤器存储路径 zuul.filter.interval=5 #动态加载的间隔时间 /s;
-
创建用来加载自定义属性的配置类,
@ConfigurationProperties("zuul.filter") @Data public class FilterConfiguration{ private String root; private Integer interval; }
-
创建启动类,并引FilterConfiguration配置,并创建动态加载过滤器的实例
@EnableZuulProxy @EnableConfigurationProperties({FilterConfiguration.class}) @SpringCloudApplication public class Application{ public static void main(String[] args){ new SpringApplicationBuilder(Application.class).web(true).run(args); } @Bean public FilterLocader filterLoader(FilterConfiguration filterConfiguration){ filterLoader.setCompiler(new GroovyCompiler()); try{ FilterFileManager.setFilenameFilter(new GroovyFileFilter()); FilterFilleManager.init( filterConfiguration.getInterval(), filterConfiguration.getRoot()+"/pre", filterConfiguration.getRoot()+"/post"; ); }catch(Exception e){ throw new RuntimeException(e); } return filterLoader; } }
根据上面的定义API网关每隔5s,从API网关服务所在的位置的filter/pre和filter/post目录下获取Groovy定义的过滤器,并对其进行编译和动态加载时使用。动态加载的间隔时间可以通过zuul.filter.interval参数来修改。过滤器实现类的根目录可以通过zuul.filter.root调整目录的位置来修改,但对于根目录的子目录这里是写死的,自己可以做进一步的扩展。
-
接下来可以启动所有服务。
-
在filter/pre目录下创建一个pre类型的过滤器,命名为PreFilter.groovy继承ZuulFilter
-
在filter/post目录下创建一个post类型的过滤器,命名为PostFilter.groovy。在过滤其中增加日志信息,查看控制台打印。
分布式配置中心:Spring Cloud Config
用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,分为服务端与客户端两个部分。
服务端:也称分布式配置中心,是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息,加密/解密信息等访问接口;
客户端:是微服务架构风格中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动时从配置中心获取和加载配置信息。
Spring Cloud Config实现了对服务端和客户端中环境变量和属性配置的抽象映射,所以它除了适用于Spring构建的应用程序之外,也可以在任何其他语言运行的应用程序中使用。由于Spring Cloud Config实现的 配置中心默认采用Git来存储配置信息,所以使用Spring Cloud Config构建的配置服务器,天然就支持对微服务应用配置信息的版本管理,并且可以通过Git客户端工具来方便的管理和访问配置内容。
-
快速入门
-
构建配置中心
-
创建一个基础Spring Boot工程,并在pom.xml中引入config-server依赖
-
创建主类,添加@EnableConfigServer注解,开启Spring Cloud Config的服务端功能。
-
在配置文件中添加配置服务的基本信息以及Git仓库的相关信息。
spring.application.name=config-server server.port=7001 # 配置Git仓库的位置 spirng.cloud.config.server.git.uri=http://git.oschina.net/didispace/SpringCloud-Learning/ # 配置仓库路径下的相对搜索位置,可以配置多个。 spirng.cloud.config.server.git.searchPaths=spring_cloud_in_action/config-repo #访问Git仓库的用户名 spring.cloud.config.server.git.username=username #访问Git仓库的用户密码 spring.cloud.config.server.git.password=password
-
-
配置规则详解
为了验证上面完成的分布式配置中心config-server,根据Git配置信息中指定的仓库位置,在Http://git.oschina.net/didispace/SpringCloud-Learning/spring_cloud_in_action/下创建一个config-repo目录作为配置仓库,并根据不同环境新建下面4个配置文件。
- didispace.properties
- didispace-dev.properties
- didispace-test.properties
- didispace-prod.properties
在这4个配置文件中均2设置了一个from属性,并为每个配置文件分别设置了不同的值,如下所示:
-
from=git-default-1.0
-
from=git-dev-1.0
-
from=git-test-1.0
-
from=git-prod-1.0
为了测试版本控制,在该Git仓库的master分支中,我们为from属性加入1.0后缀,同时创建一个config-label-test分支,并将各配置文件中的值用2.0作为后缀。
访问URL与配置文件的映射关系如下:
-
/{application}/{profile} [/{label}]
-
I {application}-{profile}. yml
-
/{label}/{application}-{profile}.yml
-
/{application}-{profile}.properties
-
/{label}/{application}-{profile}.properties
其中{label}对应Git上不同的分支,默认为master。比如访问http://localhost:7001/didispace/prod/config-label-test,并获得如下返回信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cOdi1wPy-1582522296649)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-22 185039.png)]
-
-
客户端配置映射
-
创建应用,并在pom中引入依赖,然后编写主类
-
创建bootstrap.properties配置,指定获取配置 文件config-server位置
spring.application.name=didispace #对应配置文件规则中的{application}部分 spring.cloud.config.profile=dev #对应配置文件规则中的{profile}部分 spring.cloud.label=master #对应配置文件中的{label}部分 spring.cloud.config.uri=http://localhost:7001/ #配置中心config-server的地址 server.port=7002
-
创建一个RESTful接口来返回配置中心的from属性,通过@Value("${from}")绑定配置服务中配置的from属性;
-
除了通过@Value注解绑定注入之外,也可以通过Environment对象来获取配置属性
-
启动config-client应用,进行访问即可。
-
-
服务端详解
-
基础架构
- **远程Git仓库:**用来存储配置文件的地方;
- **Config Server:**分布式配置中心,在该工程中指定所要连接的Git仓库位置以及账户,密码等连接信息。
- 本地Git仓库:在Config Server的文件系统中,每次客户端请求获取配置信息时,Config Server从Git仓库中获取最新配置到本地,然后在本地Git仓库中读取并返回。当远程仓库无法获取时,直接将本地内容返回。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMKvxKVL-1582522296649)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-22 200348.png)]
客户端应用从配置管理中获取配置信息遵从下面的执行流程:
- 应用启动时,根据bootstrap.properties中配置的应用名{application},环境名{profile},分支名{label},向Config Server请求获取配置信息。
- Config Server根据自己维护的Git仓库信息和客户端传递过来的配置定位信息查找配置信息。
- 通过git clone命令将找到的配置信息下载到Config Server的文件系统中。
- Config Server创建Spring的ApplicationContext实例,并从Git本地仓库中加载配置文件,最后将这些配置内容读取出来返回给客户端应用。
- 客户端应用在获得外部配置文件后加载到客户端的ApplicationContext实例,改配置内容的优先级高于客户端Jar包内部的配置内容,所以在jar包中重复的内容将不在被加载。
-
-
Git配置仓库
如果通过file://前缀为一个文件地址,那么它将以本地仓库的方式运行,这样就脱离了Git服务端来快速进行调试与开发,比如:spring.cloud.config.server.git.uri=file://${user.home}/config-repo
-
占位符配置URI
# {application}代表了应用名,所以Config Server会根据客户端的spring.application.name信息来填充{application} spring.cloud.config.server.git.uri=http://git.com/didispace/{application}
例如:
-
代码库:http://git.oschina.net/didispace/member-service;
-
配置库:http://git.oschina.net/didispace/member-service-config;
这时,就可以用spring.cloud.config.server.git.uri=http://git.oschina.net/didispace/{application}-config配置,来同时匹配多个不同服务的配置仓库。
-
-
配置多个仓库,可以使用逗号","分割。
-
-
SVN配置仓库
引入svnkit依赖,然后在配置文件中配置位置以及用户名密码。
-
本地文件系统
只需要设置spring.profiles.active=native,Config Server会默认从应用的src/main/resource目录下搜索配置文件。如果需要指定搜索配置文件的路径,可以通过spring.cloud.config.server.native.searchLocations属性指定具体的配置文件位置。 -
安全保护
由于配置中心存储的内容比较敏感,所以需要做一定的安全处理,结合spring security:-
引入依赖:spring-boot-starter-security
-
默认获得一个名为user用户名,在配置中心启动的时候,在日志中会打印该用户的随机密码。
-
一般会自己配置密码
security.user.name=user security.user.password=1234567
-
由于config-server设置了安全保护,所以这时客户端也需要配置安全信息来进行校验
spring.cloud.config.username=user spring.cloud.config.password=1234567
-
-
高可用配置
- 传统模式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DoQ5hRZ8-1582522296650)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-23 113128.png)]
- 服务模式:将Config Server作为普通微服务注册到Eureka服务治理中。
-
客户端详解
Config客户端启动的时候,默认会加载classpath下的配置信息,只有配置了spring.cloud.config.uri的时候,客户端才会获取远程配置信息,同时,该参数必须在bootstrap.properties,环境变量或其他优先级高于jar包的配置信息中,才能正确加载远程配置。 -
服务化配置中心
将Config Server注册到服务中心,并通过服务发现来访问Config Server并获取Git仓库中的配置信息。服务端配置
- 增加erureka依赖
- 在application.properties中指定服务注册中心的位置。
- 主类新增@EnableDiscoveryClient注解
- 启动应用,查看Eureka Server的信息看是否注册。
-
客户端配置
-
增加eureka依赖;
-
bootstrap.properties中增加配置:
# 定位Git中的资源 spring.application.name=didispace server.port=7002 # 指定注册中心位置 eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/ # 开启通过服务来访问Config Server的功能 spring.cloud.config.discovery.enabled=true # 指定Config Server注册的服务名 spring.cloud.config.discovery.serviceId=config-server spring.cloud.config.profile=dev
-
主类增加@EnableDiscoveryClient注解
-
创建Controller加载Git配置信息。
-
消息总线:Spring Cloud Bus
我们通常使用轻量级的消息代理来构建一个共用的消息主题让系统中所有微服务实例都连接上来,由于该主题中产生的消息会被所有实例监听和消费,所以我们称它为消息总线
-
消息代理
是一种消息验证,传输,路由的架构模式。在程序间起到通信调度并最小化应用间的依赖作用,可以实现通信过程解耦。它是一个中间件产品,核心是一个消息的路由程序,用来接收和分发消息,并根据设定好的消息处理流来转发给正确的应用。
常用场景:
- 将消息路由到一个或多个目的地。
- 消息转化为其他的表现形式。
- 执行消息的聚集,消息的分解,并将结果发送到它们的目的地,然后重新组合响应返回给消息用户。
- 调用Web服务来检索数据。
- 响应事件或错误。
- 使用发布-订阅模式来提供内容或基于主题的消息路由。
目前开源的产品:
- ActiveMQ
- Kafka
- RabbitMQ
- RocketMQ
- …
-
RabbitMQ实现消息总线
RabbitMQ是实现高级消息队列协议(AMQP)的开源消息代理软件,也称面向消息的中间件。也称为面向消息的中间件。RabbitMQ服务器是用高性能,可伸缩而闻名的Erlang语言编写而成的,其集群和故障转移是构建在开放电信平台框架上的。
RibbitMQ以AMQP协议实现,所以它可以支持多种操作系统,多种编程语言,几乎可以覆盖所有主流的企业级技术平台。Spring Cloud Bus中包含了对Rabbit的自动化默认配置。
-
基本概念
- **Broker:**消息队列服务器的实体,他是一个中间件应用,负责接收消息生产者的消息,然后将消息发送至消息接收者或其他的Broker.
- **Exhange:**消息交换机,是消息第一个到达的地方,消息通过它指定的路由规则,分发到不同的消息队列中去。
- **Queue:**消息队列,消息通过发送和路由之后最终到达的地方,到达Queue的消息即进入逻辑上等待消费的状态。每个消息都会被发送到一个或多个队列。
- **Binding:**绑定,他的作用就是把Exhange和Queue按照路由规则绑定起来,也就是Exchange和Queue之间的虚拟机连接。
- **Routing Key:**路由关键字,Exchange根据这个关键字进行消息投递。
- **Virtual host:**虚拟主机,它是对Broker的虚拟划分,将消费者,生产者和它们依赖的AMQP相关结构进行隔离,一般都是为了安全考虑。比如,可以在一个Broker中设置多个虚拟主机,对不同用户进行权限的分类。
- **Connection:**连接,代表生产者,消费者,Broker之间进行通信的物理网络。
- **Channel:**消息通道,用于连接生产者和消费者的逻辑结构。在客户端的每个连接里,可以建立多个Channel,每个Channel代表一个会话任务,通过Channel可以隔离同一个连接中的不同交互内容。
- **Producer:**消息生产者,制造消息并发送消息的程序。
- **Consumer:**消息消费者,接收消息并处理消息的程序。
消息投递到队列中的整个过程:
- 客户端连接到消息队列服务器,打开一个Channel.
- 客户端声明一个Exchange,并设置相关属性。
- 客户端声明一个Queue,并设置相关属性。
- 客户端使用Routing Key,在Exchange和Queue之间建立好绑定关系。
- 客户端投递消息到Exchange。
- Exchange接收到消息后根据消息的Key和已经设置的Binding,进行消息路由,将消息投递到一个或多个Queue中。
Exchange也有几种类型:
- Direct交换机:完全根据Key进行投递。
- **Topic交换机:**对Key进行模式匹配后进行投递,可以使用符号#匹配一个或多个词,符号*匹配正好一个词。
- **Fanout交换机:**不需要任何Key,他采取广播的模式,一个消息进来时,投递到与该叫户籍绑定的所有队列。
RabbitMQ支持消息的持久化,也就是将数据写在磁盘上。为了数据安全考虑,大多数情况下都会选择持久化。消息队列持久化包括3个部分:
- Exchange持久化,在声明时指定durable=>1。
- Queue持久化,在声明时指定durable=>1。
- 消息持久化,在投递时指定delivery_mode=>2(1是非持久化)。
如果Exchange和Queue都是持久化的,那么它们之间的Binding也是持久化的。如果Exchange和Queue两者之间有一个是持久化的,一个是非持久化的,就不允许建立绑定。
-
-
安装与使用
安装 后访问出现页面:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GYEZKeo3-1582522296650)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-23 224928.png)]
-
快速入门
- 引入依赖 spring-boot-starter-amqp
- 配置连接信息
- 创建生产者,注入AmqpTemplate接口实例
- 创建消费者
- 创建配置类,用来配置队列,交换机,路由等高级信息。
-
整合Spring Cloud Bus后的架构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aUGsGU82-1582522296651)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-23 220240.png)]Service A的三个实例会请求Config Server获取配置信息,Config Server根据应用配置的规则从Git仓库获取配置信息并返回。
此时,如果修改Service A的属性。首先Git管理工具会去仓库中修改对应的属性值,但是这个修改不会触发Service A实例的属性更新。我们向Service A的实例3发送Post请求,访问/bus/refresh接口。此时Service A的实例3就会将刷新请求发送到消息总线中,该消息事件会被Service A的实例1和实例2从总线中获取到,并重新从Config Server中获取它们的配置信息,从而实现配置信息的动态更新。
而从Git仓库中配置的修改到发起/bus/refresh的Post请求这一步可以通过Git仓库的Web Hook来自动触发。由于所有连接到消息总线上的应用都会接收到更新请求,所以在Web Hook中就不需要维护所有节点内容来进行更新。
-
指定刷新范围
Spring Cloud Bus 的/bus/refresh提供了一个destination参数,用来定位具体要刷新的应用程序。比如请求/bus/refresh?destination=customers:90000,此时总线上的各应用实例会根据destination属性的值来判断是否为自己的实例名,若符合才进行配置刷新。destination除了可以指定具体实例外,还可以用来定位具体的服务。定位服务的原理是通过使用Spring 的PathMatecher(路径匹配)来实现的,比如/bus/refresh?destination=customers:**,该请求会触发customers服务的所有实例进行刷新。
-
架构优化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-szqdMv3L-1582522296651)(E:\学习资料--------其他资料\笔记图片\批注 2020-02-24 090035.png)]- 在Config Server中也引入了Spring Cloud Bus,将配置服务也加入到消息总线中来。
- /bus/refresh请求不在发送到具体服务实例上,而是发送给Config Server,并通过destination参数来指定需要更新配置的服务或实例。
上面的改动,使得我们的服务不需要承担触发配置更新的职责,同时,对于Git的触发等配置都只需要针对Config Server即可,从而简化了集群维护工作。
-
RabbitMQ配置:
这里省略,可自行查看官网
-
KafKa实现消息总线
-
kafka简介
Kafka使用Scala实现,是基于消息发布-订阅模式实现的消息系统,主要设计目的:
- 消息持久化:以时间复杂度为0(1)的方式提消息持久化能力,机试对TB级以上的数据也能保证常数时间复杂度的访问性能。
- 高吞吐:在廉价的商用机器上也能支持单继每秒10万条以上的吞吐量。
- 分布式:支持消息分区以及分布式消费,并保证区分内的消息顺序。
- 跨平台:支持不同技术平台的客户端(如java,php,python等)
- 实时性:支持实时数据处理和离线数据处理。
- 伸缩性:支持水平扩展。
-
Kafka中涉及的一些基本概念
- **Broker:**kafka集群包含一个或多个服务器,这些服务器被称为Broker.
- **Topic:**逻辑上同RabbitMQ的Queue队列相似,每条发布到Kafka集群的消息都必须有一个Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个Broker上,但用户只需指定消息的Topic即可生产或消费数据而不必关系数据存于何处。)
- **Parition:**Partition是物理概念上的分区,为了提供系统吞吐率,在物理上每个Topic会分成一个或多个Partition,每个Partitio对应一个文件夹(存储对应分区的消息内容和索引文件)。
- Producer:消息生产者,负责生产消息并发送到Kafka Broker.
- **Consumer:**消息消费者,向Kafka Broker读取消息并处理的客户端。
- **Consumer Group 😗*每个Consumer属于一个特定的组(可为每个Consumer指定属于一个组,若不指定则属于默认组),组可以同来实现一条消息被组内多个成员消费等功能。
-
快速入门
Kafka的设计中依赖了ZooKeeper,在它的bin和config目录中可以看到ZooKeeper相关的内容。具体安装启动这里省略; -
整合Spring Cloud Bus
- 引入依赖:spring-cloud-starter-bus-kafka模块
-