【第六章:Java开发岗:SpringCould篇】

发布于:2023-01-23 ⋅ 阅读:(610) ⋅ 点赞:(0)

点击:【第一章:Java开发岗:基础篇

HashMap、Synchronized、ThreadLocal、AQS、线程池、JVM内存模型、内存屏障、class文件结构、类加载 机制、双亲委派、垃圾回收算法、垃圾回收器、空间分配担保策略、安全点、JIT技术、可达性分析、强软弱虚引用、gc的过程、三色标记、跨代引用、 逃逸分析、 内存泄漏与溢出、JVM线上调优经验。

点击:【第二章:Java开发岗:MySQL篇

隔离级别、ACID底层实现原理、 一致性非锁定读(MVCC的原理)、BufferPool缓存机制、filesort过程、 离散读、ICP优化、全文检索、 行锁、表锁、间隙锁、死锁、主键自增长实现原理、索引数据结构、SQL优化、索引失效的几种情况、聚集索引、辅助索引、覆盖索引、联合索引、redo log、bin log、undolog、分布式事务、SQL的执行流程、重做日志刷盘策略、有MySQL调优、分库分表、主从复制、读写分离、高可用。

点击:【第三章:Java开发岗:Redis篇

多路复用模式、单线程模型、简单字符串、链表、字典、跳跃表、压缩列表、encoding属性编码、持久化、布隆过滤器、分布式寻址算法、过期策略、内存淘汰策略 、Redis与数据库的数据一致性、Redis分布式锁、热点数据缓存、哨兵模式、集群模式、多级缓存架构、并发竞争、主从架构、集群架构及高可用、缓存雪崩、 缓存穿透、缓存失效。

点击:【第四章:Java开发岗:MQ篇

RabbitMQ、RockerMQ、Kafka 三种消息中间件出现的消息可靠投递、消息丢失、消息顺序性、消息延迟、过期失效、消息队列满了、消息高可用等问题的解决方案。RabbitMQ的工作模式,RocketMQ的消息类型,Kafka消费模式、主题/分区/日志、核心总控制器以及它的选举机制、Partition副本选举Leader机制、消费者消费消息的offset记录机制、消费者Rebalance机制、Rebalance分区分配策略、Rebalance过程、 producer发布消息机制、HW与LEO、日志分段存储、十亿消息数据线上环境规划、JVM参数设置。

点击:【第五章:Java开发岗:Spring篇

SpringBean生命周期、Spring循环依赖、Spring容器启动执行流程、Spring事务底层实现原理、Spring IOC容器加载过程、Spring AOP底层实现原理、Spring的自动装配、Spring Boot自动装配、SpringMVC执行流程。

点击【第六章:Java开发岗:SpringCould篇

微服务构建、客户端负载均衡、服务治理、服务容错保护、声明式服务调用、API网关服务、分布式配置中心、消息总线、消息驱动、分布式服务追踪。

【第七章:Java开发岗:待定中】

待定中,大家可以评论区留言,目前市场的行情,高频的一些面试题,适合的我会收录进去。


系列文章:文章以35k为备战面试背景,薪资参考坐标:上海;每个地方,每个时间段薪资待遇都不一样,文章仅做面试参考,具体能否谈到35k取决于面试表现、平时的积累、市场行情、机遇。
提示:系列文章还未全部完成,后续的文章,会慢慢补充进去的。

这里总结一下35k的Java开发岗需要掌握的面试题,帮助大家快速复习,突破面试瓶颈。本章主讲SpringCould知识点,知识点有:微服务构建、客户端负载均衡、服务治理、服务容错保护、声明式服务调用、API网关服务、分布式配置中心、消息总线、消息驱动、分布式服务追踪。大致估算可以讲三小时左右,作为备战面试的Spring相关知识点还是很不错的。35k薪资参考的坐标:上海,参考时间:2022年8月。

微服务构建

微服务架构设计原则

一般来说,在设计微服务体系结构的时候,遵循业务边界的概念,按照业务进行拆分、同时隐藏实现细节、把内容组件化模块化、可伸缩性可扩展性要求较高、并且可以实现隔离应用故障,避免整体系统不可用、要求独立部署,持续交付。

对于用户激增、并发量较高、数据量较大还得考虑:

  • 水平复制因素,就是单个服务多运行几个实例。
  • 数据分区因素,按照用户区域进行数据分区,比如北京、上海、广州等多建几个集群。
  • 并发请求因素,如果产品数量多,用户群体多的情况下,使用多级缓存处理高并发,数据量小,数据访问很高,适合存储热点数据的,可以用Nignx缓存、数据量一般,访问量大,比较热门的数据,比如首页这种的,可以使用本地缓存、数据量很大,访问量一般,比如一般的商品这种,可以使用Redis缓存。产品数量不多,用户群体大,可以考虑将页面静态化,把页面放到CDN里面,图片放到云存储里面。

微服务构建技术选型

一般来说,利用Spring Boot快速构建应用,利用Spring Cloud Alibaba Nacos实现动态服务发现、服务配置管理、服务及流量管理,利用Open feign实现与其他系统进行交互,利用Hystrix 实现熔断和错误处理,利用Ribbon实现客户端负载均衡,利用 Nginx 实现服务端负载均衡,利用 Gateway管理外部系统访问、利用Spring Security Oauth2作为权限框架进行请求校验,权限拦截、利用Seata作为分布式事务组件、利用Zipkin/Skywalking作为链路追踪、利用Sentinel作为服务降级、使用arthas/VM作为Java诊断工具、引入swagger作为在线文档、使用Redis作为分布式缓存、使用MySQL作为关系型数据库、使用MongoDB作为非关系型数据库、使用ElasticSearch作为全文搜索、有大数据的情况下使用Spark或者炎凰数仓进行读时建模。

前后端分离架构

对于一些老的项目或者特定业务的项目可能还是没有分离前后端,不过目前主流基本都是前后端分离,前后端交互更清晰,就剩下了接口模型,后端的接口更简洁明了,更容易维护,前端多渠道集成场景更容易。

后端采用统一的数据模型,支持多个前端,比如:H5前端、PC前端、安卓前端、IOS前端。

对于请求方式,比如GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源,当然也需要根据自己项目的实际情况出发,请求方式尽量统一起来。另外需要考虑后期接口参数是否会新增,如果后期参数不确定,尽量使用POST,方便后期扩展参数。

对于返回响应的实体类,后端响应统一起来,不能每个后端都用自己的响应实体类,这样前端会炸的。另外对于后端抛出来的错也需要拦截封装,给到前端一个友好的响应,而不是直接抛出去,这样前端那边的感知不友好。

对于前端请求,一般会携带几个校验参数放到请求头中,比如:App私钥(前后端约定好)、时间戳、Token令牌、校验码等等,后端一般会在网关服务或者权限服务里面,将更新数据的请求,比如POST,PUT,DELETE请求方式,对这些请求进行校验,请求头里App私钥、时间戳、Token令牌通过某种运算或者算法计算出结果,最后通过加密的方式加密这个结果,作为最终的校验码,对比前后端的校验码是否一致,判断这个请求是否合法、请求的Token是否过期/失效。

项目部署安全方面

对于项目系统部署安全方面,建议改私有网络+堡垒机+密码复杂度,建立锁机机制使用第三方知名云厂商托管数据库,降低运维复杂度,将项目部署在VPC私网内,使用cloudwatch Log+lambda+Network Firewall服务,检查mysql连续登录失败的IP次数,触发lambda执行脚本更新Network Firewall规则,禁用该IP访问。出于安全考虑,服务器只允许通过堡垒机进行运维。在没有提供安全访问策略表的情况下,除了被堡垒机访问之外,所有虚拟机无法访问任何主机,也无法被任何主机访问。
项目架构

客户端负载均衡

负载均衡(服务端/客户端)

一般我们所说的负载均衡通常都是服务器端负载均衡,服务器端负载均衡又分为两种,一种是硬件负载均衡,还有一种是软件负载均衡。

硬件负载均衡主要通过在服务器节点之前安装专门用于负载均衡的设备,常见的如:F5。

软件负载均衡则主要是在服务器上安装一些具有负载均衡功能的软件来完成请求分发进而实现负载均衡,常见的如:LVS 、 Nginx 。

微服务为负载均衡的实现提供了另外一种思路:把负载均衡的功能以库的方式集成到服务的消费方,不再是由一台指定的负载均衡设备集中提供。这种方案称为软负载均衡客户端负载均衡。常见的如:Spring Cloud中的 Ribbon。

当我们将Ribbon和Eureka一起使用时,Ribbon会到Eureka注册中心去获取服务端列表,然后进行轮询访问以到达负载均衡的作用,客户端负载均衡也需要心跳机制去维护服务端清单的有效性,当然这个过程需要配合服务注册中心一起完成。

实现Ribbon

在 pom.xml 文件中引入依赖

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.6.RELEASE</version>
	</parent>
 
	<properties>
		<spring-cloud.version>Finchley.SR2</spring-cloud.version>
	</properties>
 
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<!-- Eureka-Client 依赖 -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
		</dependency>
	</dependencies>
 
	<dependencyManagement>
		<dependencies>
			<!-- SpringCloud 版本控制依赖 -->
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>${spring-cloud.version}</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

然后在启动类里面向Spring容器中注入一个带有@LoadBalanced注解的RestTemplate Bean

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
 
@SpringBootApplication
@EnableEurekaClient
public class MessageCenterApplication {
 
	@Bean
	@LoadBalanced
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}
	public static void main(String[] args) {
		new SpringApplicationBuilder(MessageCenterApplication.class).web(WebApplicationType.SERVLET).run(args);
	}
}

调用那些需要做负载均衡的服务时,用上面注入的RestTemplate Bean进行调用就可以了

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
 
@RestController
@RequestMapping("/api/v1/center")
public class MessageCenterController {
 
	@Autowired
	private RestTemplate restTemplate;
 
	@GetMapping("/msg/get")
	public Object getMsg() {
 
		String msg = restTemplate.getForObject("http://message-service/api/v1/msg/get", String.class);
		return msg;
 
	}
}

在application.yml配置文件里添加好配置,比如:

spring:
  application:
    name: message-service
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

启动的时候,Run As --> Run Configurations->VM arguments,分别使用 8771、8772、8773 三个端口各启动一个MessageApplication应用。

-Dserver.port=8771
-Dserver.port=8772
-Dserver.port=8773

三个服务启动完成后,浏览器输入:http://localhost:8761/
在这里插入图片描述
应用启动之后,连续三次请求地址 http://localhost:8781/api/v1/center/msg/get
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

Ribbon负载均衡策略

  • RoundRobinRule: 轮询策略,Ribbon以轮询的方式选择服务器,这个是默认值。启动的服务会被循环访问。

  • RandomRule: 随机策略,也就是说Ribbon会随机从服务器列表中选择一个进行访问。

  • BestAvailableRule: 最大可用策略,先过滤出故障服务器后,选择一个当前并发请求数最小的。

  • WeightedResponseTimeRule: 带有加权的轮询策略,对各个服务器响应时间进行加权处理,然后在采用轮询的方式来获取相应的服务器。

  • AvailabilityFilteringRule: 可用过滤策略,先过滤出故障的或并发请求大于阈值的一部分服务实例,然后再以线性轮询的方式从过滤后的实例清单中选出一个。

  • ZoneAvoidanceRule: 区域感知策略,先使用主过滤条件(区域负载器,选择最优区域)对所有实例过滤并返回过滤后的实例清单,依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤,判断最小过滤数(默认1)和最小过滤百分比(默认0),最后对满足条件的服务器则使用RoundRobinRule(轮询方式)选择一个服务器实例。

例如,message-service的负载均衡策略设置为随机访问RandomRule,application.yml配置如下

message-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

脱离Eureka使用Ribbon

如果你不用Eureka,也可以继续使用Ribbon和Feign。
假设不使用Eureka,通过将ribbon.eureka.enabled 属性设置为 false, 可以在Ribbon中禁用Eureka,用@RibbonClient声明了一个"stores"服务,这个时候Ribbon Client 默认会引用一个配置好的服务列表,你可以在application.yml进行配置:

ribbon:
  eureka:
   enabled: false
stores:
  ribbon:
    listOfServers: example.com,google.com

服务治理

为什么需要服务治理

公司的系统是由几百个微服务构成的,每一个微服务又有多个实例,服务数量较多,服务之间的相互依赖成网状,所以微服务系统需要服务注册中心来统一管理微服务实例,方便查看每一个微服务实例的健康状态。

服务治理的解决方案

  • 服务注册:服务提供者主动自报家门

服务提供者将自己的服务信息(比如服务名、IP地址等)告诉服务注册中心。

  • 服务发现:服务消费者拉取注册数据

当服务消费者需要消费另外一个服务时,服务注册中心需要告诉服务消费者它所要消费服务的实例信息(如服务名、IP地址等)。

  • 心跳检测、服务续约和服务剔除

服务注册中心会检查注册的服务是否可用,通常一个服务实例注册后,会定时向服务注册中心提供“心跳”,以表明自己还处于可用的状态。如果一个服务实例停止向服务注册中心提供心跳一段时间后,服务注册中心会认为这个服务实例不可用,会把这个服务实例从服务注册列表中剔除。如果这个被剔除掉的服务实例过一段时间后继续向注册中心提供心跳,那么服务注册中心会把这个服务实例重新加入服务注册中心的列表中。

  • 服务下线:服务提供者主动发起下线

服务治理的技术选型

服务治理的技术选型

Eureka

Eureka Client:负责将这个服务的信息注册到Eureka Server中
Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号

自我保护机制产生的原因

Eureka的自我保护特性主要用于减少在网络分区或者不稳定状况下的不一致性问题,默认情况下,如果Server在一定时间内没有接收到某个服务实例的心跳(默认周期为30秒),Server将会注销该实例。如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,启动自我保护机制。

自我保护机制

  • Eureka不再从注册表中移除因为长时间没有收到心跳而过期的服务。
  • Eureka仍然能够接受新服务注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用。
  • 当网络稳定时,当前实例新注册的信息会被同步到其它节点中。

在什么环境下开启自我保护机制

本地环境

建议关闭自我保护机制。因为在本地开发环境中,EurekaServer端相对来说重启频率不高,但是在EurekaClient端,可能改动代码之后需要重启,频率相对来说比较高;那么EurekaClient端重启之后就不会及时去向EurekaServer端发送心跳包,EurekaServer端就会认为是网络延迟或者其他原因,不会剔除服务,这样的话就会影响开发效率。

生产环境

建议开启自我保护机制。因为生产环境不会频繁重启服务器,并且EurekaClient端与EurekaServer端存在网络延迟的几率较高,所以需要开启自我保护机制避免误删服务。

Eureka Server端:配置关闭自我保护,并按需配置Eureka Server清理无效节点的时间间隔。

eureka.server.enable-self-preservation	# 设为false,关闭自我保护
eureka.server.eviction-interval-timer-in-ms # 清理间隔(单位毫秒,默认是60*1000

Eureka Client端:配置开启健康检查,并按需配置续约更新时间和到期时间

eureka.instance.lease-renewal-interval-in-seconds	# 续约更新时间间隔(默认30秒)
eureka.instance.lease-expiration-duration-in-seconds # 续约到期时间(默认90秒)

公司client 的配置:

eureka:
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${spring.client.ipAdress}:${server.port}
    lease-expiration-duration-in-seconds: 30 #服务过期时间配置,超过这个时间没有接收到心跳EurekaServer就会将这个实例剔除
    lease-renewal-interval-in-seconds: 10 #服务刷新时间配置,每隔这个时间会主动心跳一次

Nacos

Nacos的动态更新如何实现?什么是长轮询/如何实现长轮询?配置和服务器之间的配置变更整个方案如何实现?Nacos如何实现配置的变更和对比?

Nacos采用长轮训机制来实现数据变更的同步
在这里插入图片描述
Nacos是采用长轮训的方式向Nacos Server端发起配置更新查询的功能。所谓长轮训就是客户端发起一次轮训请求到服务端,当服务端配置没有任何变更的时候,这个连接一直打开,直到服务端有配置或者连接超时后返回。Nacos Client端需要获取服务端变更的配置,前提是要有一个比较,也就是拿客户端本地的配置信息和服务端的配置信息进行比较。

一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本地。在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较慢的问题。于是Nacos针对这个场景,做了两个方面的优化。

  • 减少网络通信的数据量,客户端把需要进行比较的配置进行分片,每一个分片大小是3000,也就是说,每次最多拿3000个配置去Nacos Server端进行比较。

  • 分阶段进行比较和更新,

    第一阶段,客户端把这3000个配置的key以及对应的value值的md5拼接成一个字符串,然后发送到Nacos Server端

    进行判断,服务端会逐个比较这些配置中md5不同的key,把存在更新的key返回给客户端。

    第二阶段,客户端拿到这些变更的key,循环逐个去调用服务单获取这些key 的value值。

这两个优化,核心目的是减少网络通信数据包的大小,把一次大的数据包通信拆分成了多次小的数据包通信。虽然会增加网络通信次数,但是对整体的性能有较大的提升。最后,再采用长连接这种方式,既减少了pull轮询次数,又利用了长连接的优势,很好的实现了配置的动态更新同步功能。

基于HTTP的长轮询简单实现

web客户端代码

//向后台长轮询消息
    function longPolling(){
        $.ajax({
            async : true,//异步
            url : 'longPollingAction!getMessages.action', 
            type : 'post',
            dataType : 'json',
            data :{},
            timeout : 30000,//超时时间设定30秒
            error : function(xhr, textStatus, thrownError) {
                longPolling();//发生异常错误后再次发起请求
            },
            success : function(response) {
                message = response.data.message;
                if(message!="timeout"){
                    broadcast();//收到消息后发布消息
                }
                longPolling();
            }
        });
    }

web服务器端代码

public class LongPollingAction extends BaseAction {
    private static final long serialVersionUID = 1L;
    private LongPollingService longPollingService;
    private static final long TIMEOUT = 20000;// 超时时间设置为20秒

    public String getMessages() {
        long requestTime = System.currentTimeMillis();
        result.clear();
        try {
            String msg = null;

            while ((System.currentTimeMillis() - requestTime) < TIMEOUT) {
                msg = longPollingService.getMessages();
                if (msg != null) {
                    break; // 跳出循环,返回数据
                } else {
                    Thread.sleep(1000);// 休眠1秒
                }
            }
            if (msg == null) {
                result.addData("message", "timeout");// 超时
            } else {
                result.addData("message", msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return SUCCESS;
    }
    
    public LongPollingService getLongPollingService() {
        return longPollingService;
    }

    public void setLongPollingService(LongPollingService longPollingService) {
        this.longPollingService = longPollingService;
    }

}

Nacos、Eureka与Zookeeper区别

相同点:

  • 都可以实现分布式注册中心框架

不同点:

  • Zookeeper采用CP保证数据的一致性的问题,原理是采用ZAB原子广播协议。当我们ZK领导者宕机或出现了故障,会自动重新实现选举新的领导角色,整个选举的过程中为了保证数据一致性的问题,整个微服务无法实现通讯,可运行的节点必须满足过半机制,整个zk才可以使用,要不然会奔溃。

  • Eureka采用AP设计理念架构注册中心,相互注册完全去中心化,也就是没有主从之分,只要有一台Eureka节点存在整个微服务就可以实现通讯。Eureka中会定时向注册中心发送心跳,如果在短期内没有发送心跳,则就会直接剔除。会定时向注册中心定时拉去服务,如果不主动拉去服务,注册中心不会主动推送。

  • Nacos中注册中心会定时向消费者主动推送信息 ,这样就会保持数据的准时性。它会向注册中心发送心跳,但是它的频率要比Eureka快。Nacos从1.0版本选择Ap和CP混合形式实现注册中心,默认情况下采用Ap保证服务可用性,CP形式底层采用Raft协议保证数据的一致性问题。默认采用AP方式,当集群中存在非临时实例时,采用CP模式。选择Ap模式,在网络分区的的情况允许注册服务实例。选择CP模式,在网络分区的产生了抖动情况下不允许注册服务实例。

服务容错保护

服务雪崩

在微服务架构中,一个请求要调用多个服务是非常常见的。如客户端访问A服务,A服务访问B服务,B服务调用C服务,由于网络原因或者自身的原因,如果B服务或者C服务不能及时响应,A服务将处于阻塞状态,直到B服务C服务响应。此时若有大量的请求涌入,容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,造成连锁反应,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩效应”。

在高并发的情况下,一个服务的延迟可能导致所有服务器上的所有资源在数秒内饱和。比起服务故障,更糟糕的是这些应用程序还可能导致服务之间的延迟增加,导致整个系统出现更多级联故障。

造成服务雪崩的原因可以归结为以下三点:

  • 服务提供者不可用(硬件故障,程序bug,缓存击穿,用户大量请求等)
  • 重试加大流量(用户重试,代码逻辑重试)
  • 服务消费者不可用(同步等待造成的资源耗尽)

解决方案:

  • 服务隔离:限制调用分布式服务的资源,某一个调用的服务出现问题不会影响到其他服务调用
  • 服务熔断:牺牲局部服务,保全整体系统稳定性
  • 服务降级:服务熔断以后,客户端调用自己本地方法返回缺省值

所谓的容错处理其实就是捕获异常了,不让异常影响系统的正常运行,正如java中的try catch一样。在微服务调用中,自身异常可自行处理外,对于依赖的服务发生错误,或者调用异常,或者调用时间过长等原因时,为了避免长时间等待,造成系统资源耗尽, 一般上都会通过设置请求的超时时间,如http请求中的ConnectTimeout和ReadTimeout;而微服务提供了Hystrix熔断器,隔离问题服务,防止级联错误的发生。

Hystrix

Hystrix是一个实现了超时机制和断路器模式的工具类库,用于隔离访问远程系统、服务或第三方库,提升系统的可用性和容错性。

Hystrix容错机制:

  • 包裹请求:使用HystrixCommand包裹对依赖的调用逻辑,每个命令在独立线程中执行,这是用到了设计模式“命令模式”。
  • 跳闸机制:当某服务的错误率超过一定阈值时,Hystrix可以自动或手动跳闸,停止请求该服务一段时间。
  • 资源隔离:Hystrix为每个依赖都维护了一个小型的线程池,如果该线程池已满,发往该依赖的请求就被立即拒绝,而不是排队等候,从而加速判定失败。
  • 监控:Hystrix可以近乎实时的监控运行指标和配置的变化。如成功、失败、超时、被拒绝的请求等。
  • 回退机制:当请求失败、超时、被拒绝,或当断路器打开时,可以执行回退逻辑。
  • 自我修复:断路器打开一段时间后,会自动进入半开状态,断路器打开、关闭、半开的逻辑转换。

熔断器的工作原理

每个请求都会在 hystrix 超时之后返回 fallback,每个请求时间延迟就是近似 hystrix 的超时时间,假设是 5 秒,那么每个请求都要延迟 5 秒后才返回。当熔断器在 10 秒内发现请求总数超过 20,并且错误百分比超过 50%,此时熔断打开。

熔断打开之后,再有请求调用的时候,将不会调用主逻辑,而是直接调用降级逻辑,这个时候就会快速返回,而不是等待 5 秒才返回 fallback。通过断路器,实现了自动发现错误并将降级逻辑切为主逻辑,减少响应延迟。

当断路器打开,主逻辑被熔断后,hystrix 会启动一个休眠时间窗,在这个时间窗内,降级逻辑就是主逻辑;当休眠时间窗到期,断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求返回正常,那么断路器将闭合,主逻辑恢复,如果这次请求依然失败,断路器继续打开,休眠时间窗重新计时。

信号量隔离

请求并发大,耗时短,采用信号量隔离,因为这类服务的返回通常很快,不会占用线程太长时间,而且也减少了线程切换的开销。

每个请求线程通过计数信号进行限制,当信号量大于了最大请求数maxConcurrentRequest时,调用fallback接口快速返回。另外由于通过信号量计数器进行隔离,它只是个计数器,资源消耗小。

信号量的调用是同步的,每次调用都得阻塞调用方的线程,直到有结果才返回,这样就导致了无法对访问做超时处理,只能依靠协议超时,无法主动释放。

实现方式

@HystrixCommand注解实现线程池隔离,通过配置超时时间,信号量隔离,信号量最大并发,以及回退方法,基于注解就可以对方法实现服务隔离。

// 信号量隔离
@HystrixCommand(
        commandProperties = {
                // 超时时间,默认1000ms
                @HystrixProperty(name = HystrixPropertiesManager.
                	EXECUTION_ISOLATION_THREAD_TIMEOUT_IN_MILLISECONDS, value = "5000"),
                // 信号量隔离
                @HystrixProperty(name = HystrixPropertiesManager.
                	EXECUTION_ISOLATION_STRATEGY, value = "SEMAPHORE"),
                // 信号量最大并发
                @HystrixProperty(name = HystrixPropertiesManager.
                	EXECUTION_ISOLATION_SEMAPHORE_MAX_CONCURRENT_REQUESTS, value = "5")
        },
        fallbackMethod = "selectProductByIdFallBack"
)
@Override
public Product selectProductById(Integer id) {
    System.out.println(Thread.currentThread().getName());
    return productClient.selectProductById(id);
}

线程池隔离

请求并发大,耗时长,采用线程池隔离策略。这样可以保证大量的线程可用,不会由于服务原因一直处于阻塞或等待状态,快速失败返回。还有就是对依赖服务的网络请求涉及超时问题的都使用线程隔离。

优缺点

优点:

  • 使用线程池隔离可以安全隔离依赖的服务,减少所依赖的服务发生故障时的影响。比如A服务发生异常,导致请求大量超时,对应的线程池被打满,这时并不影响在其他线程池中的C、D服务的调用。
  • 当失败的服务再次变得可用时,线程池将清理并立即恢复,而不需要一个长时间的恢复。
  • 独立的线程池提高了并发性。

缺点:

  • 请求在线程池中执行,肯定会带来任务调度、排队个上下文切换带来的CPU开销。
  • 因为涉及到跨线程,那么就存在ThreadLocal数据传递的问题,比如在主线程初始化的ThreadLocal变量,在线程池中无法获取。
实现方式

@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工作流程

  1. 构造一个HystrixCommand或者HystrixObservableCommand对象
  2. 执行command命令
  3. 结果是否缓存
  4. 熔断器是否打开
  5. 线程池、队列、信号量是否打满
  6. 执行对应的构造方法或者run方法
  7. 计算熔断器状态开启还是关闭
  8. 获取fallback返回
  9. 返回成功响应
    Hystrix工作流程

声明式服务调用

比如一个订单服务知道库存服务、积分服务、仓库服务在哪里了,同时也监听着哪些端口号了。但是新问题又来了:难道订单服务要自己写一大堆代码,建立连接、构造请求、接着发送请求过去、解析响应等等。

使用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%之内。


网站公告

今日签到

点亮在社区的每一天
去签到