🔥 实现方式
@HystrixCommand注解实现线程池隔离,通过配置服务名称,接口名称,线程池,以及回退方法,基于注解就可以对接口实现服务隔离。
// 线程池隔离 @HystrixCommand(groupKey = "productServiceSinglePool", // 服务名称,相同名称使用同一个线程池 commandKey = "selectProductById", // 接口名称,默认为方法名 threadPoolKey = "productServiceSinglePool", // 线程池名称,相同名称使用同一个线程池 commandProperties = { // 超时时间,默认1000ms @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") }, threadPoolProperties = { // 线程池大小 @HystrixProperty(name = "coreSize", value = "10"), // 等待队列长度(最大队列长度,默认值-1) @HystrixProperty(name = "maxQueueSize", value = "100"), // 线程存活时间,默认1min @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"), // 超出等待队列阈值执行拒绝策略 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100") }, fallbackMethod = "selectProductByIdFallBack" ) @Override public Product selectProductById(Integer id) { System.out.println(Thread.currentThread().getName()); return productClient.selectProductById(id); } private Product selectProductByIdFallBack(Integer id) { return new Product(888, "未知商品", 0, 0d); } // 线程池隔离 @HystrixCommand(groupKey = "productServiceListPool", // 服务名称,相同名称使用同一个线程池 commandKey = "selectByIds", // 接口名称,默认为方法名 threadPoolKey = "productServiceListPool", // 线程池名称,相同名称使用同一个线程池 commandProperties = { // 超时时间,默认1000ms @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "5000") }, threadPoolProperties = { // 线程池大小 @HystrixProperty(name = "coreSize", value = "5"), // 等待队列长度(最大队列长度,默认值-1) @HystrixProperty(name = "maxQueueSize", value = "100"), // 线程存活时间,默认1min @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"), // 超出等待队列阈值执行拒绝策略 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "100") }, fallbackMethod = "selectByIdsFallback" ) @Override public List<Product> selectByIds(List<Integer> ids) { System.out.println(Thread.currentThread().getName()); return productClient.selectPhoneList(ids); } private List<Product> selectByIdsFallback(List<Integer> ids) { System.out.println("==call method selectByIdsFallback=="); return Arrays.asList(new Product(999, "未知商品", 0, 0d)); }
📝 服务熔断
服务熔断一般是指软件系统中,由于某些原因使得服务出现了过载现象,为了防止造成整个系统故障,从而采用的一种保护措施,所以很多地方也把熔断称为过载保护。
🔥 实现方式
使用@HystrixProperty注解,通过配置请求数阈值、错误百分比阈值、快照时间窗口,基于注解就可以对方法实现服务熔断。
// 服务熔断 @HystrixCommand( commandProperties = { // 请求数阈值:在快照时间窗口内,必须满足请求阈值数才有资格熔断。打开断路器的最少请求数,默认20个请求。 //意味着在时间窗口内,如果调用次数少于20次,即使所有的请求都超时或者失败,断路器都不会打开 @HystrixProperty(name = HystrixPropertiesManager. CIRCUIT_BREAKER_REQUEST_VOLUME_THRESHOLD, value = "10"), // 错误百分比阈值:当请求总数在快照内超过了阈值,且有一半的请求失败,这时断路器将会打开。默认50% @HystrixProperty(name = HystrixPropertiesManager. CIRCUIT_BREAKER_ERROR_THRESHOLD_PERCENTAGE, value = "50"), // 快照时间窗口:断路器开启时需要统计一些请求和错误数据,统计的时间范围就是快照时间窗口,默认5秒 @HystrixProperty(name = HystrixPropertiesManager. CIRCUIT_BREAKER_SLEEP_WINDOW_IN_MILLISECONDS, value = "5000") }, fallbackMethod = "selectProductByIdFallBack" ) @Override public Product selectProductById(Integer id) { System.out.println(Thread.currentThread().getName()+ LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)); if (id == 1) { throw new RuntimeException("模拟查询ID为1导致异常"); } return productClient.selectProductById(id); }
📝 服务降级
开启条件
- 方法抛出HystrixBadRequestException异常
- 方法调用超时
- 熔断器开启拦截调用
- 线程池、队列、信号量跑满
🔥 方法服务降级
// 服务降级 @HystrixCommand(fallbackMethod = "selectProductByIdFallBack") @Override public Product selectProductById(Integer id) { System.out.println(Thread.currentThread().getName()+ LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME)); if (id == 1) { throw new RuntimeException("模拟查询ID为1导致异常"); } return productClient.selectProductById(id); }
类全局服务降级
在类中添加注解@DefaultProperties(defaultFallback = "selectProductByIdFallback")
在需要降级的方法上添加注解@HystrixCommand
创建一个全局fallbakc方法
public Product selectProductByIdFallback(){ return new Product(999, "undefined", 0, 0d); }
📝 Feign中使用断路器
当我们方法很多时,要是分别编写一个fallback估计也是崩溃的,虽然可以使用一个通用的fallback,但未进行特殊设置下,也是无法知道具体是哪个方法发生熔断的。
而对于Feign,我们可以使用一种更加优雅的形式进行。我们可以指定@FeignClient注解的fallback属性,或者是fallbackFactory属性,后者可以获取异常信息的。Feign是自带断路器的,在D版本的Spring Cloud中,它没有默认打开。
需要在配置文件中配置打开它,在配置文件加以下代码:
feign.hystrix.enabled=true
需要在FeignClient的SchedualServiceHi接口的注解中加上fallback的指定类就行了
@FeignClient(value = "service-hi",fallback = SchedualServiceHiHystric.class) public interface SchedualServiceHi { @RequestMapping(value = "/hi",method = RequestMethod.GET) String sayHiFromClientOne(@RequestParam(value = "name") String name); }
SchedualServiceHiHystric需要实现SchedualServiceHi 接口,并注入到Ioc容器中
@Component public class SchedualServiceHiHystric implements SchedualServiceHi { @Override public String sayHiFromClientOne(String name) { return "sorry "+name; } }
servcie-feign工程,浏览器打开http://localhost:8765/hi?name=forezp,注意此时service-hi工程没有启动,网页显示:
sorry forezp
打开service-hi工程,再次访问,浏览器显示:
hi forezp,i am from port:8762
这证明断路器起到作用了。
📝 服务监控
除了实现服务容错之外,Hystrix还提供了近乎实时的监控功能,将服务执行结果、运行指标、请求数量、成功数量等这些状态通过Actuator进行收集,然后访问/actuator/hystrix.stream即可看到实时的监控数据。
🔥 实现方式
🔖添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
🔖添加配置
management: endpoints: web: exposure: include: hystrix.stream
🔖启动类
添加@EnableHystrix注解
🔖访问
http://localhost:9090/actuator/hystrix.stream
🔖查看数据
📝 监控中心
Hystrix提供的一套可视化系统,Hystrix-Dashboard,可以非常友好的看到当前环境中服务运行的状态。Hystrix-Dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix-Dashboard我们可以直观地看到各Hystrix Command的请求响应时间,请求成功率等数据。
🔥 实现过程
🔖添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
🔖启动类添加注解@EnableHystrixDashboard
// 开启数据监控 @EnableHystrixDashboard // 开启熔断器 @EnableHystrix // 开启缓存注解 @EnableCaching @EnableFeignClients @SpringBootApplication public class ServiceConsumerApplication { public static void main( String[] args ) { SpringApplication.run(ServiceConsumerApplication.class); } }
访问:http://localhost:9090/hystrix,控制中心界面如下:
📝 聚合监控中心
🔥 实现过程
Turbine是聚合服务器发送事件流数据的一个工具,dashboard只能监控单个节点,实际生产环境中都为集群,因此可以通过Turbine来监控集群服务。
新建一个聚合监控项目,添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-turbine</artifactId> </dependency>
添加配置文件
server: port: 8181 spring: application: name: eureka-turbine eureka: instance: prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${server.port} client: service-url: defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/ turbine: # 聚合监控 app-config: service-consumer,service-provider # 监控的服务列表 cluster-name-expression: "'default'" # 指定集群名称
启动类添加注解
@EnableTurbine @EnableHystrix @EnableHystrixDashboard @SpringBootApplication public class App { public static void main( String[] args ) { SpringApplication.run(App.class); } }
访问http://localhost:8181/hystrix
📝 Hystrix工作流程
- 构造一个HystrixCommand或者HystrixObservableCommand对象
- 执行command命令
- 结果是否缓存
- 熔断器是否打开
- 线程池、队列、信号量是否打满
- 执行对应的构造方法或者run方法
- 计算熔断器状态开启还是关闭
- 获取fallback返回
- 返回成功响应
声明式服务调用
比如一个订单服务知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,建立连接、构造请求、接着发送请求过去、解析响应等等。
使用Feign组件直接就是用注解定义一个 FeignClient接口,然后调用那个接口就可以了。人家Feign Client会在底层根据你的注解,跟你指定的服务建立连接、构造请求、发起靕求、获取响应、解析响应,等等。这一系列脏活累活,人家Feign全给你干了。
对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理,接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心,Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址,最后针对这个地址,发起请求、解析响应。
🍊 API网关服务
请求拦截、服务分发、统一的降级、限流、认证授权、安全
关于业务网关,市场上也有蛮多的技术。一些大的公司一般选择定制化开发。但是从开发语言,可维护性上出发,能选的只有getway和zuul,但是zuul使用的阻塞IO,损失性能极大,虽然新版本有支持,但是spring并没有很好的支持升级后的zuul。gatway是spring做出来的,性能也比较好,支持长连接。
Spring Cloud Gateway明确区分了Router和Filter,位于请求接入:作为所有API接口服务请求的接入点
比如可以基于Header、Path、Host、Query自由路由。
gateway的组成
- 路由 : 网关的基本模块,有ID,目标URI,一组断言和一组过滤器组成
- 断言:就是访问该请求的访问规则,可以用来匹配来自http请求的任何内容,例如headers或者参数
- 过滤器:这个就是我们平时说的过滤器,用来过滤一些请求的,也可以自定义过滤器,但是要实现两个接口,ordered和globalfilter。
🍊 分布式配置中心
config配置中心
- 在分布式系统中,由于服务数量巨多,为了方便服务配置文件统一管理,实时更新,需要分布式配置中心组件。
- 支持配置服务放在配置服务的内存中(即本地),也支持放在远程Git仓库中。config 组件中,分两个角色,一是config server,二是config client。config-client可以从config-server获取配置属性。
🍊 消息总线
Bus数据总线:将分布式的节点用轻量的消息代理连接起来。它可以用于广播配置文件的更改或者服务之间的通讯,也可以用于监控。
应用场景:实现通知微服务架构的配置文件的更改。去代码仓库将foo的值改为“foo version 4”,即改变配置文件foo的值。如果是传统的做法,需要重启服务,才能达到配置文件的更新。我们只需要发送post请求:http://localhost:8881/bus/refresh,会发现config-client会重现肚脐配置文件,重新读取配置文件。
案例:当git文件更改的时候,通过pc端用post 向端口为8882的config-client发送请求/bus/refresh/;此时8882端口会发送一个消息,由消息总线向其他服务传递,从而使整个微服务集群都达到更新配置文件。
🍊 消息驱动
SpringCloud Stream:SpringBoot应用要直接与消息中间件进行信息交互的时候,由于各消息中间件构建的初衷不同,它们的实现细节上会有较大的差异性,通过定义绑定器作为中间层,完美地实现了应用程序与消息中间件细节之间的隔离。Stream对消息中间件的进一步封装,可以做到代码层面对中间件的无感知,甚至于动态的切换中间件(rabbitmq切换为kafka),使得微服务开发的高度解耦,服务可以关注更多自己的业务流程。
- 通过定义绑定器Binder作为中间层,实现了应用程序与消息中间件细节之间的隔离。
- Binder可以生成Binding,Binding用来绑定消息容器的生产者和消费者,它有两种类型,INPUT和OUTPUT,INPUT对应于消费者,OUTPUT对应于生产者。
设计思想:Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ就是Exchange,在Kakfa中就是Topic。
🎉 基础组件
- Binder: 很方便的连接中间件,屏蔽差异
- Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel对队列进行配置
- Source和Sink: 简单的可理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。
🍊 分布式服务追踪
微服务架构上通过业务来划分服务的,通过REST调用,对外暴露的一个接口,可能需要很多个服务协同才能完成这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败。随着业务的不断扩张,服务之间互相调用会越来越复杂。一个 HTTP 请求会调用多个不同的微服务来处理返回最后的结果,在这个调用过程中,可能会因为某个服务出现网络延迟过高或发送错误导致请求失败,所以需要对服务追踪分析,提供一个可视化页面便于排查问题所在。
Sleuth 整合 Zipkin,可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的 REST API 接口来辅助查询跟踪数据以实现对分布式系统的监控程序,从而及时发现系统中出现的延迟过高问题。除了面向开发的 API 接口之外,它还提供了方便的 UI 组件来帮助我们直观地搜索跟踪信息和分析请求链路明细,比如可以查询某段时间内各用户请求的处理时间等。
Skywalking是本土开源的基于字节码注入的调用链路分析以及应用监控分析工具,特点是支持多种插件,UI功能较强,接入端无代码侵入。
CAT是由国内美团点评开源的,基于Java语言开发,目前提供Java、C/C++、Node.js、Python、Go等语言的客户端,监控数据会全量统计,国内很多公司在用,例如美团点评、携程、拼多多等,CAT跟下边要介绍的Zipkin都需要在应用程序中埋点,对代码侵入性强。
性能对比:skywalking探针对吞吐量影响最小,zipkin对吞吐量影响适中,pinpoint的探针对吞吐量影响最大。对于内存和cpu的使用,都差不多,相差在10%之内。