1. 谈谈你对微服务的理解?它有什么优缺点?
微服务是一种架构风格,通过将大型的单体应用划分为比较小的服务单元,从而降低整个系统的复杂度。
优点:
- 服务部署更灵活:每个应用都可以是一个独立的项目,可以独立部署。不依赖其他服务,耦合性降低。
- 技术更新灵活:在大型单体应用中,技术要进行更新往往非常困难。而为服务可以根据业务特点,灵活选择技术栈。
- 应用的性能得到提高:大型单体应用中,往往启动就会成为一个很大的难关。而采用微服务之后,整个系统的性能是能够得到提高的。
- 更容易组合专门的团队:在单体应用中,团队成员往往需要对系统的各个部分都要有深入的了解,门槛很高。而采用微服务之后,可以给每个微服务组件专门的团队。
- 代码复用:很多底层服务可以以 REST API 的方式对外提供统一的服务,所有基础服务可以在整个微服务系统中通用。
缺点:
- 服务调用的复杂性提高了,容易出现很多问题,如:网络问题、容错问题、负载问题、高并发问题等。
- 产生分布式事务。
- 测试的难度提升了。
- 运维难度提升了:对部署、监控、警告等要求变得非常困难。
2. SpringCloud 和 SpringCloudAlibaba 都有哪些组件?都解决了什么问题?
SpringCloud 提供了构建微服务系统所需的一组通用开发模式以及一系列快速实现这些开发模式的工具。
通常所说的 SpringCloud 是指 SpringCloud Netflix。它和 SpringCloudAlibaba 都是 SpringCloud 这一系列开发模式的具体实现。
SpringCloud NetFlix组件有:
注册中心:Eureka
配置中心:SpringCloud Config
熔断器:Hytrix
负载均衡:Ribbon
网关:Zuul、gateway
服务调用:Feign
SpringCloud Alibaba:
注册中心:Nacos
配置中心:Nacos
熔断器:Sentinel
负载均衡:Ribbon
网关:gateway
服务调用:Open Feign、Dubbo RPC
分布式事务:Seata
3. 怎么拆分微服务?怎样设计出高内聚、低耦合的微服务?有没有了解过 DDD 领域驱动设计?什么是中台?中台和微服务是什么关系?
拆分微服务的时候,为了尽量保证微服务的稳定,会有一些基本的准则:
- 微服务之间尽量不用有业务交叉。
- 微服务之间只能通过接口进行服务调用,不能绕过接口直接访问对方的数据。
- 高内聚,低耦合。
高内聚低耦合是一种从上而下指导微服务设计的方法。实现高内聚低耦合的工具主要有同步的接口调用和异步的事件驱动两种方式。
什么是 DDD:在2004年,由 Eric Evans 提出,DDD 是面对软件复杂之道提出来的。Dmain-Driven-Design。
Martin Fowler - 贫血模型,简单的 POJO 只包含对象属性,没有属性的变化, 会造成贫血失忆症。充血模型不仅包含属性,还包含所有属性的变化。
大泥团:不利于微服务的拆分。大泥团拆分出来的微服务依然是大泥团结构,当这个业务服务逐渐复杂。这个泥团又会膨胀成大泥团。
DDD 是一种方法论,没有一个稳定的技术框架。DDD要求领域跟技术无关、跟存储介质无关,跟通信无关。
中台就是将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用的组件。
中台可以分为三类:业务中台、数据中台、技术中台。大数据杀熟——数据中台。电商——收银中台、支付风控中台。
中台跟DDD结合:DDD会通过限界上下文将系统拆分成一个一个的领域,而这种限界上下文天生就成了中台直接的逻辑屏障。
DDD在技术与资源调度方面都能够给中台建设提供不错的指导。
4. 你的项目中是怎么保证微服务敏捷开发的?微服务的链路追踪、持续集成、AB发布要怎么做?
敏捷开发:目的就是为了提高团队的交付效率,快速迭代,快速试错。
每个月固定发布新版本、以分支的形式保存到代码仓库中。快速入职。任务面板、站会。团队人员灵活流动、同时形成各个专家代表。开发环境、测试环境、预生产环境、生产环境。文档优先。
链路追踪:
- 基于日志:形成全局事务ID,落地到日志文件。通过 ELK 收集日志,形成报表。
- 基于MQ:往往需要架构支持。经过流式计算形成可视化结果。
持续集成:maven -> build -> shell ; jenkins
AB发布:
- 蓝绿发布、红黑发布:新老版本同时存在,优点是部署结构简单,运维方便,升级过程操作简单,周期短;缺点是资源冗余,需要部署两套生产环境,新版本故障影响范围大。
- A/B 测试发布:A/B 测试基于用户请求的元信息将流量路由到新版本,这是一种基于请求内容匹配的灰度发布策略。用户信息可以保存在请求 Header和 Cookie。优点是可以对特定的请求或者用户提供新版本服务,新版本故障影响范围小。缺点是仍然存在资源冗余,因为无法准确评估请求容量,发布周期长。
- 灰度发布、金丝雀发布:金丝雀发布的思想则是将少量的请求引流到新版本上,因此部署新版本服务只需极小数的机器。验证新版本符合预期后,逐步调整流量权重比例,使得流量慢慢从老版本迁移至新版本,期间可以根据设置的流量比例,对新版本服务进行扩容,同时对老版本服务进行缩容,使得底层资源得到最大化利用。优点是按比例将流量无差别地导向新版本,新版本故障影响范围小,发布期间逐步对新版本扩容,同时对老版本缩容,资源利用率高;缺点是流量无差别地导向新版本,可能会影响重要用户的体验;发布周期长。
5. Spring Cloud有哪些常用组件,作用是什么?
SpringCloud NetFlix组件有:
注册中心:Eureka
配置中心:SpringCloud Config
服务熔断:Hytrix
负载均衡:Ribbon
网关:Zuul、gateway
服务调用:Feign/OpenFeign
SpringCloud Alibaba:
注册中心:Nacos
配置中心:Nacos
熔断器:Sentinel
负载均衡:Ribbon
网关:gateway
服务调用:OpenFeign/Dubbo RPC
分布式事务:Seata
6. Spring Cloud 和 Dubbo 有哪些区别?
Spring Cloud 是一个微服务框架,提供了微服务领域中的很多功能组件。Dubbo 是一个 RPC 调用框架,核心是解决服务调用间的问题。Spring Cloud 是一个大而全的框架,Dubbo 侧重于服务调用,所以 Dubbo提供的功能没有 Spring Cloud 全面,但是 Dubbo 的服务调用性能比 Spring Cloud 高。Spring Cloud 和 Dubbo不是对立的,可以结合起来一起使用。
7. 什么是服务雪崩?什么是服务限流?
- 当服务 A 调用服务 B,服务 B 调用服务 C。此时大量请求突然请求服务 A,假如服务A本身能抗住这些请求,但是服务C扛不住,导致服务C请求堆积,从而导致服务B请求堆积,从而服务A不可用,这就是服务雪崩。解决方式就是服务降级和服务熔断。
- 服务限流是指在高并发请求下,为保护系统对服务请求数量进行限制,从而防止系统不被大量请求压垮。在秒杀中,限流是非常重要的。
8. 什么是服务熔断?什么是服务降级?区别是什么?
- 服务熔断是指当服务A调用某个服务B不可用时,上游服务A为保证自己不受影响,从而不再调用服务B,直接返回一个结果,减轻服务A和服务B的压力,直到服务B恢复。
- 服务降级是指当发现系统压力过载时,可以通过关闭某个服务或限流某个服务来减轻系统压力,这就是降级。
相同点:
- 都是为了防止系统奔溃
- 都让用户体系到某些功能暂不可用。
不同点:熔断是下游服务故障触发的,降级是为了降低系统负载。
9. SOA、分布式、微服务之间有什么关系和区别?
- 分布式架构师指将单体架构中的各个部分拆分,然后部署不同的集群或进程中去。SOA 和微服务基本上都是分布式架构。
- SOA 是一种面向服务的架构,系统的所有服务都注册在总线上。当调用服务时,从总线上查找服务信息,然后调用。
- 微服务是一种更彻底的面向服务的架构,将系统中各个功能个体抽成一个个小的应用程序,基本保持一个应用对应一个服务的架构。
10. Hystrix 是什么?有什么作用?
Hystrix 是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix 能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。
Hystrix 提供服务熔断和服务降级的功能。
熔断机制是应对雪崩效应的一种链路保护机制,当链路的某个微服务出错不可用或者响应时间太长时,会进行服务的降级,进而熔断该节点微服务的调用,快速返回错误的响应信息,当检测到该节点微服务调用响应正常后,恢复调用链路。
服务降级是当发生程序运行异常、超时、服务熔断、线程池/信号量打满等导致服务降级时,不让客户端等待,并立即返回一个友好的提示。
Hystrix 通过信号量或线程池来实现服务降级的。
信号量模式:信号量模式控制单个服务提供者执行并发度,比如单个CommondKey下正在请求数为N,若N小于maxConcurrentRequests,则继续执行;若大于等于maxConcurrentRequests,则直接拒绝,进入降级逻辑。信号量模式使用请求线程本身执行,没有线程上下文切换,开销较小,但超时机制失效。
线程池模式:线程池模式控制单个服务提供者执行并发度,代码上都会先走获取信号量,只是使用默认信号量,全部请求可通过,然后实际调用线程池逻辑。线程池模式下,比如单个CommondKey下正在请求数为N,若N小于maximumPoolSize,会先从 Hystrix 管理的线程池里面获得一个线程,然后将参数传递给任务线程去执行真正调用,如果并发请求数多于线程池线程个数,就有任务需要进入队列排队,但排队队列也有上限,如果排队队列也满,则进去降级逻辑。线程池模式可以支持异步调用,支持超时调用,存在线程切换,开销大。
11. 什么是Ribbon?它的作用是什么?
- Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现,通过Spring Cloud的封装,可以让我们轻松地将面向服务的Rest模板请求自动转换成客户端负载均衡的服务调用。
- Ribbon只具有负载均衡的能力,并不具有发送请求的能力。所以,需要配合服务通信组件,如:RestTemplate。
实现原理:Ribbon从注册中心中获取服务列表,然后通过获取到的服务列表,采用负载均衡算法(Ribbon默认采用的是轮训方式),利用通信框架(RestTemplate或Feign等)进行服务调用。
12. 什么是openfeign?它的作用是什么?
openfeign
是spring cloud
在feign
的基础上支持了spring mvc
的注解,如@RequesMapping
、@GetMapping
、@PostMapping
等。openfeign
还实现与Ribbon
的整合。
服务提供者只需要提供 API 接口,而不需要像 dubbo
那样需要强制使用 implem ents
实现接口,即使用 fegin
不要求服务提供者在 Controller
使用 implements
关键字实现接口。
Feign底层实现原理
openfeign
通过包扫描将所有被 @FeignClient
注解注释的接口扫描出来,并为每个接口注册一个FeignClientFactoryBean<T>
实例。FeignClientFactoryBean<T>
是一个FactoryBean<T>
,当Spring调用FeignClientFactoryBean<T>
的getObject
方法时,openfeign
返回一个Feign生成的动态代理对象,拦截接口的方法执行。
feign
会为代理的接口的每个方法Method
都生成一个MethodHandler
。
当为接口上的@FeignClient
注解的url属性配置服务提供者的url时,其实就是不与Ribbon整合,此时由SynchronousMethodHandler实现接口方法进行远程同步调用。
当接口上的@FeignClient
注解的url属性不配置时,且会走负载均衡逻辑,也就是需要与Ribbon整合使用。这时候不再是使用默认的Client(Default)调用接口,而是使用LoadBalancerFeignClient
调用接口,由LoadBalancerFeignClient
实现与Ribbon的整合。
13. Nacos面试题
-
为什么要将服务注册到nacos?
为了更好的查找这些服务。 -
Nacos服务是如何判定服务实例的状态?
通过发送心跳包,5秒发送一次,如果15秒没有回应,则说明服务出现了问题,
如果30秒后没有回应,则说明服务已经停止。 -
服务消费方是如何调用服务提供方的服务的?
通过创建RestTemplate对象来实现。 -
Nacos中的负载均衡底层是如何实现的?
通过Ribbon实现,Ribbon中定义了一些负载均衡算法。然后基于这些算法从服务
实例中获取一个实例为消费方提供服务。 -
Ribbon 是什么?Ribbon 可以解决什么问题?
Ribbon是Netflix公司提供的负载均衡客户端。
Ribbon可以基于负载均衡策略进行服务调用,所有策略都会实现IRule接口。 -
Ribbon 内置的负载策略都有哪些?
8种,可以通过查看IRule接口的实现类进行查看 -
@LoadBalanced的作用是什么?
描述RestTemplate对象,用于告诉Spring框架,在使用RestTempalte进行服务调用时,这个调用过程会被一个拦截器进行拦截,然后在拦截器内部。启动负载均衡策略。 -
项目中为什么要定义bootstrap.yml文件?
此文件被读取的优先级比较高,可以在服务启动时读取配置中心的数据
-
Nacos配置中心宕机了,我们的服务还可以读取到配置信息吗?
可以从内存,客户端获取配置中心的配置信息以后,会将配置信息在本地存储一份
-
微服务应用中我们的客户端如何从配置中心获取信息?
我们的服务一般会先从内存中读取配置信息,同时我们的微服务还可以定时向nacos配置中心发请求拉取(pull)更新的配置信息。
-
微服务应用中客户端如何感知配置中心的数据变化?
1.4.x版本以后nacos客户端会基于长轮询机制从nacos获取配置信息,所谓的长轮询就是没有配置更新时,会在nacos服务端的队列进行等待。
-
Nacos配置中的管理模型是怎样的?
namespace,group,service/data-id
14. Springboot启动流程
在启动类上加上 @SpringBootApplication
注解,这个注解主要包含:
// spring配置注解
@SpringBootConfiguration
// spring自动专配注解
@EnableAutoConfiguration
// spring 扫描包注解
@ComponentScan
Springboot 通过 run 方法启动,其大致流程如下:
-
初始化SpringApplication,配置基本的环境变量、资源、构造器、监听器,初始化阶段的主要作用是为运行SpringApplication实例对象启动环境变量准备以及进行必要的资源构造器的初始化动作。如项目运行环境:web应用还是普通的java程序。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.sources = new LinkedHashSet(); this.bannerMode = Mode.CONSOLE; this.logStartupInfo = true; this.addCommandLineProperties = true; this.addConversionService = true; this.headless = true; this.registerShutdownHook = true; this.additionalProfiles = Collections.emptySet(); this.isCustomEnvironment = false; this.lazyInitialization = false; this.applicationContextFactory = ApplicationContextFactory.DEFAULT; this.applicationStartup = ApplicationStartup.DEFAULT; this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet(Arrays.asList(primarySources)); // 设置运行环境 this.webApplicationType = WebApplicationType.deduceFromClasspath(); this.bootstrapRegistryInitializers = this.getBootstrapRegistryInitializersFromSpringFactories(); // 设置初始化 this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class)); // 设置监听 this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = this.deduceMainApplicationClass(); }
-
运行 SpringApplication,SpringBoot正式启动加载过程,包括启动流程监控模块、配置环境加载模块、ApplicationContext容器上下文环境加载模块。refreshContext方法刷新应用上下文并进行自动化配置模块加载,通过 SpringFactoriesLoader加载META-INF/spring.factories文件的配置,实现自动配置核心功能。运行SpringApplication的主要代码如下:
public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); DefaultBootstrapContext bootstrapContext = this.createBootstrapContext(); ConfigurableApplicationContext context = null; this.configureHeadlessProperty(); /** * 启动监听 * 创建应用的监听器SpringApplicationRunListeners并开始监听,监控模块通过调用 * getSpringFactoriesInstances私有协议从META-INF/spring.factories文件中 * 取得SpringApplicationRunListener监听器实例。 * 当前事件监听器SpringApplicationRunListener中只有一个EventPublishingRunlistener广播事件监听器, * 它的starting方法会封装成SpringApplicationEvent事件广播出去,被SpringApplication中配置的listener监听。 * 这一步骤执行完成后也会同时通知SpringBoot其他模块目前监听初始化已经完成,可以开始执行启动方案了。 */ SpringApplicationRunListeners listeners = this.getRunListeners(args); listeners.starting(bootstrapContext, this.mainApplicationClass); try { /** * ConfigurableEnviroment 配置环境模块和监听 * 创建配置环境,创建应用程序的环境信息。 * 如果是Web程序,创建StandardServletEnvironment;否则创建StandardEnviroment; * 加载属性配置文件,将配置文件加入监听器对象中(SpringApplicationRunListeners)。 * 通过configPropertySource方法设置properties配置文件,通过执行configProfies方法设置profiles; * 配置监听,发布environmentPrepared事件,及调用ApplicationListener#onApplicationEvent方法, * 通知SpringBoot应用的environment已经准备完成。 */ ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments); this.configureIgnoreBeanInfo(environment); Banner printedBanner = this.printBanner(environment); /** * ConfigurableApplicationContext 配置应用上下文 *(1) 配置Spring容器应用上下文对象, * 它的作用是创建Run方法的返回对象ConfigurableApplicationContext(应用配置上下文), * 此类主要继承了Application、LifeCycle、Closeable接口, * 而ApplicationContext是Spring框架中负责Bean注入容器的主要载体, * 负责bean加载、配置管理、维护bean之间依赖关系及Bean生命周期管理。 *(2) 配置基本属性,对应prepareContext方法将listener、environment、banner、applicationArguments * 等重要组件与Spring容器上下文对象关联。借助SpringFactoriesLoader查找可用的 * ApplciationContextInitailizer, 它的initialize方法会对创建好的ApplicationContext进行初始化, * 然后它会调用SpringApplicationRunListener#contextPrepared方法, * 此时SpringBoot应用的ApplicationContext已经准备就绪,为刷新应用上下文准备好了容器。 *(3) 刷新应用上下文,对应源码中的refreshContext(context)方法, * 将通过工程模式产生应用上下文中所需的bean。实现spring-boot-starter-*(mybatis、redis等)自动化配置的关键, * 包括spring.factories的加载、bean的实例化等核心工作。 * 然后调用SpringApplicationRunListener#finish方法告诉SprignBoot应用程序, * 容器已经完成ApplicationContext装载。 */ context = this.createApplicationContext(); context.setApplicationStartup(this.applicationStartup); this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); this.refreshContext(context); this.afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch); } listeners.started(context); this.callRunners(context, applicationArguments); } catch (Throwable var10) { this.handleRunFailure(context, var10, listeners); throw new IllegalStateException(var10); } try { listeners.running(context); return context; } catch (Throwable var9) { this.handleRunFailure(context, var9, (SpringApplicationRunListeners)null); throw new IllegalStateException(var9); } }
SpringBoot应用程序的启动流程主要包括初始化SpringApplication和运行SpringApplication两个过程。其中初始化SpringApplication包括配置基本的环境变量、资源、构造器和监听器,为运行SpringApplciation实例对象作准备;而运行SpringApplication实例为应用程序正式启动加载过程,包括SpringApplicationRunListeners 引用启动监控模块、ConfigrableEnvironment配置环境模块和监听及ConfigrableApplicationContext配置应用上下文。当完成刷新应用的上下文和调用SpringApplicationRunListener#contextPrepared方法后表示SpringBoot应用程序已经启动完成。
-
初始化 SpringApplication
配置基本的环境变量、资源、构造器和监听器,为运行SpringApplciation实例对象作准备。
-
运行 SpringApplication
启动监控模块
ConfigrableEnvironment 配置环境和监听
配置应用上下文 ConfigrableApplicationContext
-
启动完成
当完成刷新应用的上下文和调用SpringApplicationRunListener#contextPrepared方法后表示SpringBoot应用程序已经启动完成
15. Spring 中 Bean的 加载过程
IOC容器就像是一个工厂,里面有很多流水线生产出一个个产品(bean)。bean的加载流程大概分为:
-
容器启动阶段
spring启动时通过 AnnotatedBeanDefinitionReader 的 doRegisterBean 方法读取注解元信息封装成 BeanDefinition,然后通过BeanDefinationRegistry 注册到 BeanFactory 的 beanDefinitionMap 中。如果是xml文件配置bean元信息的话,需要BeanFactoryPostProcessor把占位符替换为相应的数据。
-
bean加载阶段
通过 beanName先从缓存中加载实例,缓存中没有则通过父BeanFactory加载,然后把BeanDefinition转换合并成RootBeanDefinition,初始化依赖bean。默认是单例bean,进入单例bean的创建方法、先将原生 Bean 实例装饰成 BeanWrapper,然后检查 Aware 相关接口以及BeanPostProcessor前置处理、初始化方法、后置处理、使用、销毁前的方法、销毁。