微服务的特点:
Spring 最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。官网
SpringCloud 也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
Eureka
:服务治理组件,包含服务注册中心,服务注册与发现机制的实现。(服务治理,服务注册/发现)Zuul
:网关组件,提供智能路由,访问过滤功能Ribbon
:客户端负载均衡的服务调用组件(客户端负载)Feign
:服务调用,给予Ribbon和Hystrix的声明式服务调用组件 (声明式服务调用)Hystrix
:容错管理组件,实现断路器模式,帮助服务依赖中出现的延迟和为故障提供强大的容错能力。(熔断、断路器,容错)附图一张,看不懂没关系,回头再回顾
出处:写的不错
总的来说,Eureka
就是一个服务发现框架。写的不错
我们就可以来看关于 Eureka
的一些基础概念了,你会发现这东西理解起来怎么这么简单。
服务发现:其实就是一个“中介”,整个过程中有三个角色:服务提供者(出租房子的)、服务消费者(租客)、服务中介(房屋中介)。
服务提供者: 就是提供一些自己能够执行的一些服务给外界。
服务消费者: 就是需要使用一些服务的“用户”。
服务中介: 其实就是服务提供者和服务消费者之间的“桥梁”,服务提供者可以把自己注册到服务中介那里,而服务消费者如需要消费一些服务(使用一些功能)就可以在服务中介中寻找注册在服务中介的服务提供者。
服务注册 Register:
官方解释:当 Eureka
客户端向 Eureka Server
注册时,它提供自身的元数据,比如IP地址、端口,运行状况指示符URL,主页等。
结合中介理解:房东 (提供者 Eureka Client Provider
)在中介 (服务器 Eureka Server
) 那里登记房屋的信息,比如面积,价格,地段等等(元数据 metaData
)。
服务续约 Renew:
官方解释:Eureka
客户会每隔30秒(默认情况下)发送一次心跳来续约。 通过续约来告知 Eureka Server
该 Eureka
客户仍然存在,没有出现问题。 正常情况下,如果 Eureka Server
在90秒没有收到 Eureka
客户的续约,它会将实例从其注册表中删除。
结合中介理解:房东 (提供者 Eureka Client Provider
) 定期告诉中介 (服务器 Eureka Server
) 我的房子还租(续约) ,中介 (服务器Eureka Server
) 收到之后继续保留房屋的信息。
获取注册列表信息 Fetch Registries:
官方解释:Eureka
客户端从服务器获取注册表信息,并将其缓存在本地。客户端会使用该信息查找其他服务,从而进行远程调用。该注册列表信息定期(每30秒钟)更新一次。每次返回注册列表信息可能与 Eureka
客户端的缓存信息不同, Eureka
客户端自动处理。如果由于某种原因导致注册列表信息不能及时匹配,Eureka
客户端则会重新获取整个注册表信息。 Eureka
服务器缓存注册列表信息,整个注册表以及每个应用程序的信息进行了压缩,压缩内容和没有压缩的内容完全相同。Eureka
客户端和 Eureka
服务器可以使用JSON / XML格式进行通讯。在默认的情况下 Eureka
客户端使用压缩 JSON
格式来获取注册列表的信息。
结合中介理解:租客(消费者 Eureka Client Consumer
) 去中介 (服务器 Eureka Server
) 那里获取所有的房屋信息列表 (客户端列表 Eureka Client List
) ,而且租客为了获取最新的信息会定期向中介 (服务器 Eureka Server
) 那里获取并更新本地列表。
服务下线 Cancel:
官方解释:Eureka客户端在程序关闭时向Eureka服务器发送取消请求。 发送请求后,该客户端实例信息将从服务器的实例注册表中删除。该下线请求不会自动完成,它需要调用以下内容:DiscoveryManager.getInstance().shutdownComponent();
结合中介理解:房东 (提供者 Eureka Client Provider
) 告诉中介 (服务器 Eureka Server
) 我的房子不租了,中介之后就将注册的房屋信息从列表中剔除。
服务剔除 Eviction:
官方解释:在默认的情况下,当Eureka客户端连续90秒(3个续约周期)没有向Eureka服务器发送服务续约,即心跳,Eureka服务器会将该服务实例从服务注册列表删除,即服务剔除。
结合中介理解:房东(提供者 Eureka Client Provider
) 会定期联系 中介 (服务器 Eureka Server
) 告诉他我的房子还租(续约),如果中介 (服务器 Eureka Server
) 长时间没收到提供者的信息,那么中介会将他的房屋信息给下架(服务剔除)。
模拟一个服务调用的场景,搭建两个工程:leyou-service-provider(服务提供方)和 leyou-service-consumer(服务调用方)。方便后面学习微服务架构
服务提供方:使用 mybatis 操作数据库,实现对数据的增删改查;并对外提供rest接口服务。
服务消费方:使用 restTemplate 远程调用服务提供方的 rest 接口服务,获取数据。
什么是 RestTemplate?
RestTemplate
是Spring
提供的一个访问Http服务的客户端类,怎么说呢?就是微服务之间的调用是使用的 RestTemplate
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Finchley.SR2version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
server:
port: 10086 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:${server.port}/eureka
register-with-eureka: false # 把自己注册到eureka服务列表
fetch-registry: false # 拉取eureka服务信息
server:
enable-self-preservation: false # 关闭自我保护
eviction-interval-timer-in-ms: 5000 # 每隔5秒钟,进行一次服务列表的清理
@SpringBootApplication
@EnableEurekaServer // 声明当前springboot应用是一个eureka服务中心
public class LeyouEurekaApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouEurekaApplication.class, args);
}
}
注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。
修改 leyou-service-provider工程
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/leyou
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
application:
name: service-provider # 应用名称,注册到eureka后的服务名称
mybatis:
type-aliases-package: cn.leyou.service.pojo
eureka:
client:
service-url: # EurekaServer地址
defaultZone: http://127.0.0.1:10086/eureka
@EnableDiscoveryClient
来开启Eureka客户端功能@SpringBootApplication
@EnableDiscoveryClient
public class LeyouServiceProviderApplication {
public static void main(String[] args) {
SpringApplication.run(LeyouServiceApplication.class, args);
}
}
接下来我们修改 leyou-service-consumer,尝试从EurekaServer获取服务。
server:
port: 80
spring:
application:
name: service-consumer
eureka:
client:
service-url:
defaultZone: http://localhost:10086/eureka
@SpringBootApplication
@EnableDiscoveryClient // 开启Eureka客户端
public class ItcastServiceConsumerApplication {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient; // eureka客户端,可以获取到eureka中服务的信息
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
// 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合
List<ServiceInstance> instances = discoveryClient.getInstances("service-provider");
// 因为只有一个Service-provider。所以获取第一个实例
ServiceInstance instance = instances.get(0);
// 获取ip和端口信息,拼接成服务地址
String baseUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
Eureka 架构中的三个核心角色:
服务注册中心
Eureka的服务端应用,提供服务注册和发现功能,就是我们建立的这个注册 leyout-eureka。
服务提供者
提供服务的应用,可以是 SpringBoot 应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的leyou-service-provider
。
服务消费者
消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的 leyou-service-consumer
。
Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。
服务同步
多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。
集群搭建,实现思路
我们假设要运行两个EurekaServer的集群,端口分别为:10086和10087。在两个启动器上配置好端口,启动即可。
所谓的高可用注册中心,其实就是把 EurekaServer 自己也作为一个服务进行注册,这样多个EurekaServer之间就能互相发现对方,从而形成集群。
server:
port: 10086 # 端口
spring:
application:
name: eureka-server # 应用名称,会在Eureka中显示
eureka:
client:
service-url: # 配置其他Eureka服务的地址,而不是自己,比如10087
defaultZone: http://127.0.0.1:10087/eureka
因为EurekaServer不止一个,因此注册服务到集群的时候,service-url 参数需要变化:
eureka:
client:
service-url: # EurekaServer地址,多个地址以','隔开
defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
服务提供者要向 EurekaServer 注册服务,并且完成服务续约等工作。
服务注册
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true
参数是否正确,事实上默认就是 true 。如果值确实为 true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server 会把这些信息保存到一个双层Map结构中。
spring.application.name
属性locahost:service-provider:8081
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
eureka:
instance:
lease-expiration-duration-in-seconds: 10 # 10秒即过期
lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
lease-renewal-interval-in-seconds
:服务续约(renew)的间隔,默认为30秒
lease-expiration-duration-in-seconds
:服务失效时间,默认值90秒
默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
但是在开发时,这个值有点太长了,所以我们在开发阶段可以适当调小。
获取服务列表
当服务消费者启动时,会检测eureka.client.fetch-registry=true
参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
eureka:
client:
registry-fetch-interval-seconds: 5
服务下线
当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。
失效剔除
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生产环境不要修改。
自我保护
我们关停一个服务,就会在Eureka面板看到一条警告:
会触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)
在刚才的案例中,我们启动了一个 leyou-service-provider
,然后通过DiscoveryClient来获取服务实例信息,然后获取ip和端口来访问。
在实际环境中,我们往往会开启很多个 leyou-service-provider
的集群。此时我们获取的服务列表中就会有多个,到底该访问哪一个呢?
一般这种情况下我们就需要编写负载均衡算法,在多个实例列表中进行选择。不过Eureka中已经帮我们集成了负载均衡组件:Ribbon,简单修改代码即可使用。
什么是Ribbon?
Ribbon是一个基于HTTP 和 TCP 的客户端负载均衡工具,它基于Netflix Ribbon
实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续我们将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。
因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖,直接修改代码。
修改 leyou-service-consumer
的引导类,在 RestTemplate 的配置方法上添加@LoadBalanced
注解:
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
修改调用方式,不再手动获取 ip 和端口,而是直接通过服务名称调用:
@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
//@Autowired
//private DiscoveryClient discoveryClient; // 注入discoveryClient,通过该客户端获取服务列表
//原方法
/*
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
// 根据服务名称,获取服务实例。有可能是集群,所以是service实例集合
List instances = discoveryClient.getInstances("service-provider");
// 因为只有一个Service-provider。所以获取第一个实例
ServiceInstance instance = instances.get(0);
// 获取ip和端口信息,拼接成服务地址
String baseUrl = "http://" + instance.getHost() + ":" + instance.getPort() + "/user/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
return user;
}
*/
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
// 通过client获取服务提供方的服务列表,这里我们只有一个
// ServiceInstance instance = discoveryClient.getInstances("service-provider").get(0);
String baseUrl = "http://service-provider/user/" + id;
User user = this.restTemplate.getForObject(baseUrl, User.class);
return user;
}
}
为什么我们只输入了service名称就可以访问了呢?之前还要获取ip和端口。
显然有人帮我们根据service名称,获取到了服务实例的ip和端口。它就是LoadBalancerInterceptor
负载均衡,不管 Nginx
还是 Ribbon
都需要其算法的支持,如果我没记错的话 Nginx
使用的是 轮询和加权轮询算法。而在 Ribbon
中有更多的负载均衡调度算法,其默认是使用的 RoundRobinRule
轮询策略。
RoundRobinRule
:轮询策略。Ribbon
默认采用的策略。若经过一轮轮询没有找到可用的 provider
,其最多轮询 10 轮。若最终还没有找到,则返回 null
。RandomRule
: 随机策略,从所有可用的 provider
中随机选择一个。RetryRule
: 重试策略。先按照 RoundRobinRule
策略获取 provider
,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。我们是否可以修改负载均衡的策略呢?
我们注意一下 IRule 这个类:
SpringBoot也帮我们提供了修改负载均衡规则的配置入口,在leyou-service-consumer
的application.yml
中添加
格式是:{服务名称}.ribbon.NFLoadBalancerRuleClassName
,值就是IRule的实现类。
server:
port: 80
spring:
application:
name: service-consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
service-provider:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
当然,在 Ribbon
中你还可以自定义负载均衡算法,你只需要实现 IRule
接口,然后修改配置文件或者自定义 Java Config
类。
什么是 Hystrix?
在分布式环境中,不可避免地会有许多服务依赖项中的某些失败。Hystrix是一个库,可通过添加等待时间容限和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix通过隔离服务之间的访问点,停止服务之间的级联故障并提供后备选项来实现此目的,所有这些都可以提高系统的整体弹性。参考
Hystrix
就是一个能进行 熔断 和 降级 的库,通过使用它能提高整个系统的弹性。讲一个场景:
此时我们整个微服务系统是这样的。服务A调用了服务B,服务B再调用了服务C,但是因为某些原因,服务C顶不住了,这个时候大量请求会在服务C阻塞。
服务C阻塞了还好,毕竟只是一个系统崩溃了。但是请注意这个时候因为服务C不能返回响应,那么服务B调用服务C的的请求就会阻塞,同理服务B阻塞了,那么服务A也会阻塞崩溃。
请注意,为什么阻塞会崩溃。因为这些请求会消耗占用系统的线程、IO 等资源,消耗完你这个系统服务器不就崩了么。
这就叫 服务雪崩。
Hystix解决雪崩问题的手段有两个:
那么什么是 熔断和降级 呢?
所谓 熔断 就是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过 断路器 直接将此请求链路断开。
也就是我们上面服务B调用服务C在指定时间窗内,调用的失败率到达了一定的值,那么 Hystrix
则会自动将 服务B与C 之间的请求都断了,以免导致服务雪崩现象。
其实这里所讲的 熔断 就是指的 Hystrix
中的 断路器模式 ,你可以使用简单的 @HystrixCommand
注解来标注某个方法,这样 Hystrix
就会使用 断路器 来“包装”这个方法,每当调用时间超过指定时间时(默认为1000ms),断路器将会中断对这个方法的调用。
熔断的3个状态:
Hystrix 为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理,
服务降级:优先保证核心服务,而非核心服务不可用或弱可用。
以我的理解,降级是为了更好的用户体验,当一个方法调用异常时,通过执行另一种代码逻辑来给用户友好的回复。这也就对应着 Hystrix
的 后备处理 模式。你可以通过设置 fallbackMethod
来给一个方法设置备用的代码逻辑。比如这个时候有一个热点新闻出现了,我们会推荐给用户查看详情,然后用户会通过id去查询新闻的详情,但是因为这条新闻太火了(比如最近什么*易对吧),大量用户同时访问可能会导致系统崩溃,那么我们就进行 服务降级 ,一些请求会做一些降级处理比如当前人数太多请稍后查看等等。
用户的请求故障时,不会被阻塞,更不会无休止的等待或者看到系统崩溃,至少可以看到一个执行结果(例如返回友好的提示信息) 。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。
触发Hystix服务降级的情况:
leyou-service-consumer
的 pom.xml 中<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrixartifactId>
dependency>
@EnableCircuitBreaker
注解我们可以使用 @SpringCloudApplication 注解,他默认开启了三个注解
/**
* @author Spencer Gibb
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}
如下:
@SpringCloudApplication
public class ItcastServiceConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
我们改造 leyou-service-consumer
,当目标服务的调用出现故障,我们希望快速失败,给用户一个友好提示。因此需要提前编写好失败时的降级处理逻辑,要使用 HystixCommond
来完成:
@HystrixCommand(fallbackMethod = "queryByIdFallBack")
:用来声明一个降级逻辑的方法@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@ResponseBody
@HystrixCommand(fallbackMethod = "queryUserByIdFallBack")
public String queryUserById(@RequestParam("id") Long id) {
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
public String queryUserByIdFallBack(Long id){
return "请求繁忙,请稍后再试!";
}
}
要注意,因为熔断的降级逻辑方法必须跟正常逻辑方法保证:相同的参数列表和返回值声明。失败逻辑中返回User对象没有太大意义,一般会返回友好提示。所以我们把queryById的方法改造为返回String,反正也是Json数据。这样失败逻辑中返回一个错误说明,会比较方便。
我们可以把 Fallback配置加在类上,实现默认fallback:
@DefaultProperties(defaultFallback = "defaultFallBack")
:在类上指明统一的失败降级方法@HystrixCommand
:在方法上直接使用该注解,使用默认的降级方法。defaultFallback
:默认降级方法,不用任何参数,以匹配更多方法,但是返回值一定一致@Controller
@RequestMapping("consumer/user")
@DefaultProperties(defaultFallback = "fallBackMethod") // 指定一个类的全局熔断方法
public class UserController {
@Autowired
private RestTemplate restTemplate;
@GetMapping
@ResponseBody
@HystrixCommand // 标记该方法需要熔断
public String queryUserById(@RequestParam("id") Long id) {
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
/**
* 熔断方法
* 返回值要和被熔断的方法的返回值一致
* 熔断方法不需要参数
* @return
*/
public String fallBackMethod(){
return "请求繁忙,请稍后再试!";
}
}
在之前的案例中,请求在超过1秒后都会返回错误信息,这是因为Hystix的默认超时时长为1,我们可以通过配置修改这个值:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms
为了能够精确控制请求的成功或失败,我们在consumer的调用业务中加入一段逻辑:
@GetMapping("{id}")
@HystrixCommand
public String queryUserById(@PathVariable("id") Long id){
if(id == 1){
throw new RuntimeException("太忙了");
}
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
return user;
}
不过,默认的熔断触发要求较高,休眠时间窗较短,为了测试方便,我们可以通过配置修改熔断策略:
hystrix:
command:
default:
circuitBreaker:
requestVolumeThreshold: 10
sleepWindowInMilliseconds: 10000
errorThresholdPercentage: 50
进入正题
在前面的学习中,我们使用了Ribbon的负载均衡功能,大大简化了远程调用时的代码:
String user = this.restTemplate.getForObject("http://service-provider/user/" + id, String.class);
有没有更优雅的方式,来对这些代码再次优化呢?
聪明的小朋友肯定想到了,那就用 映射 呀,就像域名和IP地址的映射。我们可以将被调用的服务代码映射到消费者端,这样我们就可以 **“无缝开发” **啦。
OpenFeign
也是运行在消费者端的,使用 Ribbon
进行负载均衡,所以 OpenFeign
直接内置了 Ribbon
。leyou-service-consumer
工程<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
@EnableFeignClients
@SpringCloudApplication
@EnableFeignClients // 开启feign客户端
public class ItcastServiceConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ItcastServiceConsumerApplication.class, args);
}
}
删除RestTemplate:feign 已经自动集成了Ribbon负载均衡的RestTemplate。所以,此处不需要再注册RestTemplate。
@FeignClient(value = "service-provider") // 标注该类是一个feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryById(@PathVariable("id") Long id);
}
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称@Controller
@RequestMapping("consumer/user")
public class UserController {
@Autowired
private UserClient userClient;
@GetMapping
@ResponseBody
public User queryUserById(@RequestParam("id") Long id){
User user = this.userClient.queryUserById(id);
return user;
}
}
Feign 中本身已经集成了 Ribbon 依赖和自动配置,因此我们不需要额外引入依赖,也不需要再注册RestTemplate
对象。可以在配置文件中再额外配置
Feign 默认也有对 Hystrix 的集成:只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:
在 consumer工程添加配置内容:
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
但是,Feign中的Fallback配置不像hystrix中那样简单了。
首先,我们要定义一个类UserClientFallback,实现刚才编写的UserClient,作为fallback的处理类
@Component
public class UserClientFallback implements UserClient {
@Override
public User queryById(Long id) {
User user = new User();
user.setUserName("服务器繁忙,请稍后再试!");
return user;
}
}
然后在 UserFeignClient 中,指定刚才编写的实现类:
@FeignClient(value = "service-provider", fallback = UserClientFallback.class) // 标注该类是一个feign接口
public interface UserClient {
@GetMapping("user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
min-request-size: 2048 # 设置触发压缩的大小下限
response:
enabled: true # 开启响应压缩
前面讲过,通过logging.level.xx=debug
来设置日志级别。然而这个对 Fegin 客户端而言不会产生效果。因为@FeignClient
注解修改的客户端在被代理时,都会创建一个新的 Fegin.Logger
实例。我们需要额外指定这个日志的级别才可以。
logging:
level:
cn.leyou: debug
@Configuration
public class FeignLogConfiguration {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
这里指定的 Level 级别是FULL,Feign支持4种级别:
@FeignClient(value = "service-privider", fallback = UserFeignClientFallback.class, configuration = FeignConfig.class)
public interface UserFeignClient {
@GetMapping("/user/{id}")
User queryUserById(@PathVariable("id") Long id);
}
进入正题
Zuul 是从设备和 web 站点到 Netflix 流应用后端的所有请求的前门。作为边界服务应用,Zuul 是为了实现动态路由、监视、弹性和安全性而构建的。它还具有根据情况将请求路由到多个 Amazon Auto Scaling Groups(亚马逊自动缩放组,亚马逊的一种云计算方式) 的能力
不管是来自于客户端(PC或移动端)的请求,还是服务内部调用。一切对服务的请求都会经过Zuul这个网关,然后再由网关来实现 鉴权、动态路由等等操作。Zuul 就是我们服务的统一入口。
Zuul
中最关键的就是 路由和过滤器 了。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulDemoApplication.class, args);
}
}
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
eureka:
client:
registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
service-url:
defaultZone: http://127.0.0.1:10086/eureka
zuul:
routes:
service-provider: # 这里是路由id,随意写
path: /service-provider/** # 这里是映射路径
serviceId: service-provider # 指定服务名称
而大多数情况下,我们的
路由名称往往和服务名会写成一样的。因此Zuul就提供了一种简化的配置语法:zuul.routes.
比方说上面我们关于 service-provider 的配置可以简化为一条:
zuul:
routes:
service-provider: /service-provider/** # 这里是映射路径
默认的路由规则
在使用Zuul的过程中,上面讲述的规则已经大大的简化了配置项。但是当服务较多时,配置也是比较繁琐的。因此Zuul就指定了默认的路由规则:
service-provider
,则默认的映射路径就 是:/service-provider/**
也就是说,刚才的映射规则我们完全不配置也是OK的,不信就试试看。
我们通过zuul.prefix=/api
来指定了路由的前缀,这样在发起请求时,路径就要以/api
开头。
zuul:
routes:
service-provider: /service-provider/**
service-consumer: /service-consumer/**
prefix: /api # 添加路由前缀
拓展
**
代表匹配多级任意路径*
代表匹配一级任意路径zuul:
prefix: /api # 路由路径前缀
routes:
item-service: /item/** # 商品微服务的映射路径
search-service: /search/** #路由到搜索微服务
user-service: /user/** # 用户微服务
add-host-header: true #携带请求本身的head头信息
sensitive-headers: # 覆盖默认敏感头信息,设置为 null
ignore-services: "*" # 服务名屏蔽
ignore-patterns: **/auto/** # 路径屏蔽
add-host-header
:使zuul 转发时,附带自身的host**,解决host地址变化的问题**,当然使用 nginx 时,需要在nginx的配置文件中添加 proxy_set_header Host $host;
sensitive-headers
:Zuul内部有默认的过滤器,会对请求和响应头信息进行重组,过滤掉敏感的头信息,设置全局属性,可解决无法 拿到 cookie 问题Zuul 作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的,这样我们就能实现 限流,灰度发布,权限控制 等等。
ZuulFilter 是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:
public abstract ZuulFilter implements IZuulFilter{
abstract public String filterType();
abstract public int filterOrder();
boolean shouldFilter();// 来自IZuulFilter
Object run() throws ZuulException;// IZuulFilter
}
shouldFilter
:返回一个Boolean
值,判断该过滤器是否需要执行。返回true执行,返回false不执行。run
:过滤器的具体业务逻辑。filterType
:返回字符串,代表过滤器的类型。包含以下4种:
pre
:请求在被路由之前执行route
:在路由请求时调用post
:在route和errror过滤器之后调用error
:处理请求时发生错误调用filterOrder
:通过返回的 int 值来定义过滤器的执行顺序,数字越小优先级越高。这张是Zuul官网提供的请求生命周期图,清晰的表现了一个请求在各个过滤器的执行顺序。
异常流程:
场景非常多:
接下来,我们在Zuul编写拦截器,对用户的token进行校验,如果发现未登录,则进行拦截。
关注一下末尾的 filter ,并不是所有的路径我们都需要拦截,所以,我们需要在拦截时,配置一个白名单,如果在名单内,则不进行拦截。
server:
port: 10010
spring:
application:
name: leyou-gateway
eureka:
client:
registry-fetch-interval-seconds: 5
service-url:
defaultZone: http://127.0.0.1:10086/eureka
zuul:
prefix: /api # 路由路径前缀
routes:
item-service: /item/** # 商品微服务的映射路径
search-service: /search/** #路由到搜索微服务
user-service: /user/** # 用户微服务
auth-service: /auth/** # 授权中心微服务
cart-service: /cart/** # 购物车微服务
order-service: /order/** # 购物车微服务
add-host-header: true
sensitive-headers: # 覆盖默认敏感头信息
leyou:
jwt:
pubKeyPath: C:\\tmp\\rsa\\rsa.pub # 公钥地址
cookieName: LY_TOKEN # token
filter:
allowPaths:
- /api/auth
- /api/search
- /api/user/register
- /api/user/check
- /api/user/code
- /api/item
@Component
@ConfigurationProperties(prefix = "leyou.filter")
public class FilterProperties {
private List<String> allowPaths;
public List<String> getAllowPaths() {
return allowPaths;
}
public void setAllowPaths(List<String> allowPaths) {
this.allowPaths = allowPaths;
}
}
@Component
@ConfigurationProperties(prefix = "leyou.jwt")
public class JwtProperties {
private String pubKeyPath;// 公钥
private PublicKey publicKey; // 公钥
private String cookieName; //cookie
private static final Logger logger = LoggerFactory.getLogger(JwtProperties.class);
/**
* @PostContruct:在构造方法执行之后执行该方法
*/
@PostConstruct
public void init(){
try {
// 获取公钥和私钥
this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
} catch (Exception e) {
logger.error("初始化公钥和私钥失败!", e);
throw new RuntimeException();
}
}
//省略 getter与setter
}
重头:配置过滤器
继承 ZuulFilter,重写里面方法,忘了的话,可跳至 6.2.1. 章节回顾。
@Component
@EnableConfigurationProperties({JwtProperties.class, FilterProperties.class})
public class LoginFilter extends ZuulFilter {
@Autowired
private JwtProperties jwtProperties;
@Autowired
private FilterProperties filterProperties;
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 10;
}
@Override
public boolean shouldFilter() {
//获取context
RequestContext context = RequestContext.getCurrentContext();
//获取request对象
HttpServletRequest request = context.getRequest();
//获取请求路径
String url = request.getRequestURL().toString();
//获取白名单
List<String> allowPaths = this.filterProperties.getAllowPaths();
// 判断白名单
// 遍历允许访问的路径
for (String allowPath : allowPaths) {
if (StringUtils.contains(url,allowPath)){
return false;
}
}
return true;
}
@Override
public Object run() throws ZuulException {
//获取context
RequestContext context = RequestContext.getCurrentContext();
//获取request对象
HttpServletRequest request = context.getRequest();
//获取token
String token = CookieUtils.getCookieValue(request, this.jwtProperties.getCookieName());
/*if (StringUtils.isBlank(token)){
// 校验出现异常,返回403
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}*/
try {
//解析
JwtUtils.getInfoFromToken(token,this.jwtProperties.getPublicKey());
} catch (Exception e) {
e.printStackTrace();
// 校验出现异常,返回403
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
}
Zuul 中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 2000 # 设置hystrix的超时时间为6000ms
当我们的微服务系统开始慢慢地庞大起来,那么多 Consumer
、Provider
、Eureka Server
、Zuul
系统都会持有自己的配置,这个时候我们在项目运行的时候可能需要更改某些应用的配置,如果我们不进行配置的统一管理,我们只能去每个应用下一个一个寻找配置文件然后修改配置文件再重启应用。
那么有没有一种方法既能对配置文件统一地进行管理,又能在项目运行时动态修改配置文件呢?
Config 是什么?
Spring Cloud Config
为分布式系统中的外部化配置提供服务器和客户端支持。使用 Config
服务器,可以在中心位置管理所有环境中应用程序的外部属性。
简单来说,Spring Cloud Config
就是能将各个 应用/系统/模块 的配置文件存放到 统一的地方然后进行管理(Git 或者 SVN)。
当然这里你肯定还会有一个疑问,如果我在应用运行时去更改远程配置仓库(Git)中的对应配置文件,那么依赖于这个配置文件的已启动的应用会不会进行其相应配置的更改呢?
答案是不会的。
什么?那怎么进行动态修改配置文件呢?这不是出现了 配置漂移 吗?
一般我们会使用 Bus
消息总线 + Spring Cloud Config
进行配置的动态刷新。
用于将服务和服务实例与分布式消息系统链接在一起的事件总线。在集群中传播状态更改很有用(例如配置更改事件)。
你可以简单理解为 Spring Cloud Bus
的作用就是管理和广播分布式系统中的消息,也就是消息引擎系统中的广播模式。当然作为 消息总线 的 Spring Cloud Bus
可以做很多事而不仅仅是客户端的配置刷新功能。
而拥有了 Spring Cloud Bus
之后,我们只需要创建一个简单的请求,并且加上 @ResfreshScope
注解就能进行配置的动态修改了,下面我画了张图供你理解。
实现思路
这篇文章中我带大家初步了解了 Spring Cloud
的各个组件,他们有
这时候再来看看这张图试试吧!