# SpringCloudAlibabaTutorial **Repository Path**: ylimhhmily/SpringCloudAlibabaTutorial ## Basic Information - **Project Name**: SpringCloudAlibabaTutorial - **Description**: 一站式速学 SpringCloudAlibaba 体系教程 - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 22 - **Forks**: 28 - **Created**: 2023-04-05 - **Last Updated**: 2025-06-11 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # SpringCloudAlibaba 学习课程 ## I、大纲 [TOC] ## II、简介 ```markdown 一站式速学 SpringCloudAlibaba 体系自学课程,希望帮助更多想学的童鞋快速掌握 SCA 体系。 ``` ## III、各微服务占用端口情况 | 模块 | 微服务工程名 | 端口 | 功能描述 | | -------------------------------------- | -------------------------------- | --------------------------------------- | :----------------- | | sc01-eureka | eureka-server | 8761 | Eureka 服务端 | | | eureka-provider | 9000 | Eureka 提供方 | | | eureka-consumer | 9001 | Eureka 消费方 | | sc02-nacos | nacos-server | 8848 | | | | nacos-provider | 9010 | Nacos 提供方 | | | nacos-consumer | 9011 | Nacos 消费方 | | sc03-cluster | nacos-cluster-provider | 9020
9021 | Nacos 提供方 | | | nacos-cluster-consumer | 9022
9023 | Nacos 消费方 | | | nginx | 8888 | Nginx 反向代理 | | | | | | | sca01-nacos-regdis | nacos-registry-provider | 9030 | Nacos 提供方 | | | nacos-discovery-consumer | 9031 | Nacos 消费方 | | sca02-nacos-config | nacos-dynamic-port | 9032 | Nacos 动态配置端口 | | | nacos-dynamic-env | 9033 | Nacos 动态环境变量 | | sca03-openfeign | openfeign-provider | 9040
9041
9042 | Openfeign 提供方 | | | ribbon-consumer | 9043 | Ribbon 消费方 | | | openfeign-consumer | 9044 | Openfeign 消费方 | | | degrade-consumer | 9045 | 降级消费方 | | sca04-gateway | gateway-server | 9050 | 网关服务端 | | | gateway-provider | 9051 | 网关提供方 | | | gateway-consumer | 9052 | 网关消费方 | | | gateway-filter-server | 9053 | 网关过滤器 | | sca05-sentinel | sentinel-manual-flow-demo | - | 手动限流 | | | sentinel-manual-degrade-demo | - | 手动降级 | | | sentinel-dashboard | 9999 | 限流控制台 | | | sentinel-auto-flowdeg-consumer | 9061 | 自动限流/降级消费方 | | | sentinel-flow-degrade-provider | 9062 | 限流/降级提供方 | | | sentinel-online-flowdeg-consumer | 9063 | 在线修改限流降级消费方 | | | sentinel-nacos-persist-consumer | 9064 | 限流规则持久化消费方 | | sca06-dubbo | zookeeper | - | Zookeeper 注册中心 | | | dubbo-facade | - | dubbo 接口模块 | | | dubbo-discovery-provider | 9070
29070 | dubbo 提供方 | | | dubbo-discovery-consumer | 9075
29075 | dubbo 消费方 | | | dubbo-check-consumer | 9076
29076 | dubbo 依赖检查消费方 | | | dubbo-loadbalance-consumer | 9076
29076

29071
29072 | dubbo 负载均衡消费方 | | | dubbo-broadcast-consumer | 9077
29077 | dubbo 广播调用消费方 | | | dubbo-cache-consumer | 9078
29078 | dubbo 缓存操作消费方 | | | dubbo-p2p-consumer | 9079
29079 | dubbo 点对点消费方 | | | dubbo-generic-consumer | 9080
29080 | dubbo 泛化调用消费方 | | |dubbo-jdk-spi|-|dubbo 和 jdk spi| | |dubbo-wrapper|-|dubbo wrapper 机制| ## 一、体系介绍 ### 1.1 组件介绍 1、解决了什么问题? 2、如何完整的阐述涉及到的组件? #### 1.1.1 Nacos Registry #### 1.1.2 Nacos Config #### 1.1.3 OpenFeign #### 1.1.4 Gateway #### 1.1.5 Dubbo #### 1.1.6 Sentinel #### 1.1.7 Seata image-20230504160927741 ### 1.2 SpringCloud 最佳实践 1、什么是最佳实践? 2、实践的又是什么? #### 1.2.1 Provider + Consumer + Eureka 服务搭建 - 相关网址: - SpringCloud 大致介绍:https://spring.io/projects/spring-cloud - SpringCloud 版本文档:https://spring.io/projects/spring-cloud#learn(如何选择?) - Eureka Server 启动 - Netflix:https://spring.io/projects/spring-cloud-netflix - image-20230504174146527 - 引入包 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` - 添加应用启动类、添加Eureka服务端注解、设置Tomcat绑定端口、设置Eureka地址 - 设置 Tomcat 启动端口为 6000(**为什么不行?**) - Eureka Provider 工程 - 引入包 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` - 添加应用启动类、添加Eureka客户端注解、设置Tomcat绑定端口、设置Eureka地址 - Eureka Consumer 工程 - 引入包 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` - 添加应用启动类、添加Eureka客户端注解、设置Tomcat绑定端口、设置Eureka地址 - 怎么进行负载均衡呢? #### 1.2.2 Provider + Consumer + Nacos 服务搭建 - Nacos Server 启动 - Nacos 的官方文档:https://nacos.io/zh-cn/docs/quick-start.html - Nacos 安装包下载:https://github.com/alibaba/nacos/releases/download/2.1.0/nacos-server-2.1.0.tar.gz - Nacos 源码包下载:https://github.com/alibaba/nacos/archive/refs/tags/2.1.0.tar.gz - Jar 包启动 & 源码启动 - image-20230504192851751 - Nacos Provider 工程 - 引入包 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` - 添加应用启动类、添加控制器、添加服务发现注解、设置Tomcat绑定端口、设置Nacos地址 - Nacos Consumer 工程 - 引入包 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` - 添加启动类、控制器、RestTemplate、远程调用方法、设置Tomcat绑定端口、设置Nacos地址 #### 1.2.3 Consumer + Provider + nginx 集群搭建 - Cluster Provider:多台节点 - Cluster Consumer:多台节点 - Nginx - 下载地址:http://nginx.org/ - 访问地址:http://localhost:8888/ - 负载均衡处理、start nginx.exe、nginx.exe -s quit、tasklist /fi "imagename eq nginx.exe" image-20230504192811712 ## 二、Nacos 注册与发现 ### 2.1 Nacos 工程案例 #### 2.1.1 Nacos 服务注册案例搭建 - 源码地址:https://github.com/alibaba/spring-cloud-alibaba - 版本说明:https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E - 本地启动 Nacos-Server 工程 - 搭建 SpringCloudAlibaba 骨架的最佳实践 - nacos-registry-provider 关键 pom 引用 - ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` image-20230504234730377 #### 2.1.2 Nacos 服务订阅案例搭建 - nacos-discovery-consumer 关键 pom 引用 - ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` - 发起远程调用 ### 2.2 Nacos 注册流程源码分析 #### 2.2.1 Nacos Client 发起注册流程 - 怎么快速熟悉注册流程? - 可以去看百度的一些介绍,通过别人的一些分析源码的思路来学习 - 直接去官网去搜索一些官方的资料,来学习一些流程 - 自己去琢磨,去研究,如果没有一套很好的方法体系的话,那么研究起来就会非常痛苦 - 异常堆栈能给什么启示? - 注册流程:1.x 版本、2.x 版本 - AbstractAutoServiceRegistration#onApplicationEvent - NacosServiceRegistry#register - NamingClientProxyDelegate#register - 1.x 为什么先注册心跳? - 2.x 基于长连接? #### 2.2.2 Nacos Server 处理注册流程 - V1:InstanceController - Service#serviceMap - Service#onDelete - V2:InstanceRequestHandler - serviceManager - singletonRepository - namespaceSingletonMaps - clientManager - publishers - Client - addServiceInstance - removeServiceInstance - 事件 - ClientEvent.ClientChangedEvent - ClientOperationEvent.ClientRegisterServiceEvent - MetadataEvent.InstanceMetadataEvent ### 2.3 Nacos 订阅流程源码分析 #### 2.3.1 Nacos Client 发起订阅流程 - 怎么理解订阅关键环节? - 如何巧妙创造订阅异常? - 第一步:启动 Nacos Server - 第二步:启动 Nacos Provider - 第三步:启动 Nacos Consumer - 第四步:关闭 Nacos Server - 第五步:向 Nacos Consumer 发起 HTTP 请求调用 - 订阅流程 - DynamicServerListLoadBalancer#updateListOfServers - NacosServerList#getServers - NamingClientProxyDelegate#subscribe - scheduleUpdateIfAbsent - 关注1.0与2.0分支分叉逻辑 #### 2.3.2 Nacos 心跳与剔除机制 - 心跳和剔除分别解决了什么问题? - 心跳:Client 与 Server 之间的一个连接情况,维持Client与Server之间的联系,防止许久不联系的话,担心被Server删除干掉 - 剔除:解决的是,Server 这边很久都没感知到 Client 是否活跃的情况,因此需要将这些许久非活跃的数据删除掉,侧重于Server端的逻辑 - 强调这里讨论 1.0 版本非长连接的机制 - 心跳(客户端) - 回顾注册环节提到的心跳代码片段 - NamingClientProxyDelegate#register - BeatTask#run - 剔除(服务端) - 思考应该在哪里开启剔除监测? - Service#init - ClientBeatCheckTask#run - deleteIp - UDP 推送 - getPushService().serviceChanged(service) ## 三、Nacos 配置中心 ### 3.1 Nacos Config 工程案例 #### 3.1.1 Nacos 动态更新端口案例搭建 - 官方文档:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html - 留个醒,重点关注,port 是不能随便修改的,一旦修改的话,虽然内存里面会直接变成最新的配置,但是端口提供的服务还是原来的旧端口 - image-20230507124215562 - image-20230507124305978 #### 3.1.2 Nacos 动态更新配置案例搭建 - 重点关注放在 bootstrap.yml - ```yml spring: cloud: nacos: config: extension-configs[0]: dataId: nacos-dynamic-user.properties refresh: true extension-configs[1]: dataId: nacos-dynamic-common.properties refresh: true extension-configs[2]: dataId: nacos-dynamic-global.properties refresh: true ``` - image-20230507124659138 ### 3.2 Nacos 配置中心源码分析 #### 3.2.1 Nacos 客户端配置更新机制 - 如何模拟实时拉取效果? - 根据配置返回内容反推源头 - 源码关键节点 - NacosConfigService - ClientWorker 构造方法 - ConfigRpcTransportClient - ClientWorker#executeConfigListen - 堆栈流程 - ![image-20230507151436544](README-IMAGES/image-20230507151436544.png) #### 3.2.2 Nacos 服务端配置更新机制 - 找准核心操作源头 URL - 精准定位到 ConfigController - 源码关键节点 - ConfigController#publishConfig - 发布 ConfigDataChangeEvent 事件 - 发布 LocalDataChangeEvent 事件 - LongPollingService 注册监听的回调 - DataChangeTask#run ### 3.3 Nacos 集群同步源码分析 #### 3.3.1 CP + AP 模式 - C(Consistency)A(Avaliability)P(Partition Tolerance)是什么? - image-20230507201030723 - 如果想解决分区容忍的现象:意味着,需要将 dataA 进行复制给到其他节点 - 如果想解决复制的问题,但是又会引发复制过程带来的可用性问题 - 既然可用性也受损的话,那么是不是可以直接去掉分区概念 - 为什么一定得保证 P ? - 既然去掉分区,那就意味着没有多节点的概念,从而意味着是单节点 - 单节点的好处就是,不用复制数据到其他节点,因为自己就是唯一,然后一致性得到了保证 - 但是如果,单节点发生的宕机,那是不是意味着可用性又受损了 - 如果想拥有可用性,那么又变相的反推需要多节点,那是不是又回到了分区的概念 - 因此呢,我们需要保证P,也就是需要对分区进行一定的容忍 - 而这个所谓的一定的容忍,可以是一段时间之内,也可以是一定数量之间,但是最终可以通过设计方案达到数据的最终一致 - CP 应用案例(价格) - 一致性(强) + 分区容忍新 - image-20230507200408490 - AP 应用案例(点赞) - 可用性 + 分区容忍性 - image-20230507201231395 - Nacos、Eureka、Zookeeper 典型模式 - Nacos:AP + CP - Eureka:AP - Zookeeper:CP #### 3.3.2 Distro 一致性协议 - 新节点同步机制:com.alibaba.nacos.core.distributed.distro.DistroProtocol#startLoadTask - 平等机制:com.alibaba.nacos.naming.web.DistroFilter - 路由转发机制:com.alibaba.nacos.naming.web.DistroFilter - 异步复制机制:com.alibaba.nacos.core.distributed.distro.DistroProtocol#sync(DistroKey, DataOperation, long) - 健康检查机制:com.alibaba.nacos.core.distributed.distro.DistroProtocol#startVerifyTask - 本地读机制:com.alibaba.nacos.naming.controllers.InstanceController#list - 最最关键的环节: - 每个 Nacos Server 都会处理一部分应该由自己负责的数据 - ```java public boolean responsible(String responsibleTag) { final List servers = healthyList; if (!switchDomain.isDistroEnabled() || EnvUtil.getStandaloneMode()) { return true; } if (CollectionUtils.isEmpty(servers)) { // means distro config is not ready yet return false; } // localAddress = 192.168.100.183:8848 String localAddress = EnvUtil.getLocalAddress(); // 获取起始索引值 int index = servers.indexOf(localAddress); // 获取结束索引值 int lastIndex = servers.lastIndexOf(localAddress); if (lastIndex < 0 || index < 0) { return true; } // distroHash(responsibleTag) 计算出来的值与 servers.size() 求余后的结果, // 若在 [index, lastIndex] 中则为当前节点负责的数据 int target = distroHash(responsibleTag) % servers.size(); return target >= index && target <= lastIndex; } ``` - 每个 Nacos Server 都会存储所有 Nacos Client 端的所有数据 #### 3.3.3 Raft 一致性协议 - image-20230507223521621 - V1 版本的选举与同步 - 选举 - RaftCore - 角色 LEADER、FOLLOWER、CANDIDATE - MasterElection - 同步 - HeartBeat - V2 版本的选举与同步 - 选举 - com.alibaba.nacos.naming.consistency.DelegateConsistencyServiceImpl - com.alibaba.nacos.naming.consistency.persistent.PersistentConsistencyServiceDelegateImpl#PersistentConsistencyServiceDelegateImpl - com.alibaba.nacos.naming.consistency.persistent.impl.PersistentServiceProcessor#afterConstruct - com.alibaba.nacos.core.distributed.raft.JRaftProtocol#addRequestProcessors - com.alibaba.nacos.core.distributed.raft.JRaftServer#createMultiRaftGroup - com.alipay.sofa.jraft.RaftGroupService#start(boolean) - com.alipay.sofa.jraft.RaftServiceFactory#createAndInitRaftNode - com.alipay.sofa.jraft.core.NodeImpl#init - com.alipay.sofa.jraft.core.NodeImpl#electSelf - com.alipay.sofa.jraft.core.NodeImpl#becomeLeader - 同步 - com.alipay.sofa.jraft.ReplicatorGroup#addReplicator(com.alipay.sofa.jraft.entity.PeerId) ## 四、OpenFeign 负载均衡 ### 4.1 OpenFeign 工程案例 #### 4.1.1 Ribbon 工程案例搭建 - Ribbon 是什么? - Netflix 的开源项目,主要来提供关于客户端的负载均衡的能力。 - Ribbon - Feign:Netflix,SpringCloud 的第一代 LB(负载均衡)客户端工具包 - OpenFeign:SpringCloud 自研,SpringCloud 的第二代LB(负载均衡)客户端工具包,扩展支持了 @RequestMapping、@GetMapping 等之类的注解的能力 - image-20230508224755712 - openfeign-provider 9040 - ribbon-consumer 9043 #### 4.1.2 OpenFeign 工程案例搭建 - 官网地址:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#netflix-feign-starter - 引包 - @EnableFeignClients - @FeignClient - openfeign-consumer 9044 #### 4.1.3 负载均衡与降级案例搭建 - 负载均衡:openfeign-provider 9040 9041 9042 - 消费降级: - https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker-fallback - image-20230509223817334 - 方式一:直接实现接口 - 方式二:实现 FallbackFactory - 工程搭建:degrade-consumer 9045 ### 4.2 OpenFeign 源码分析 #### 4.2.1 @FeignClient 注解扫描机制 - 核心流程讲解 - image-20230510222500806 - 源码流程跟踪 - 扫描流程:org.springframework.cloud.openfeign.EnableFeignClients - org.springframework.cloud.openfeign.FeignClientsRegistrar - org.springframework.cloud.openfeign.FeignClientsRegistrar#registerBeanDefinitions - org.springframework.cloud.openfeign.FeignClientsRegistrar#registerDefaultConfiguration - org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients - org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents - protected boolean isCandidateComponent(MetadataReader metadataReader) - protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) - scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); - org.springframework.cloud.openfeign.FeignClientsRegistrar#registerClientConfiguration - ```java registry.registerBeanDefinition( name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); ``` - org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClient - org.springframework.cloud.openfeign.FeignClientFactoryBean + factoryBean.getObject() - org.springframework.cloud.openfeign.FeignClientFactoryBean#getTarget - org.springframework.cloud.openfeign.HystrixTargeter#target - feign.ReflectiveFeign#newInstance - ```java public T newInstance(Target target) { Map nameToHandler = targetToHandlersByName.apply(target); Map methodToHandler = new LinkedHashMap(); List defaultMethodHandlers = new LinkedList(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } ``` - 调用流程:feign.ReflectiveFeign.FeignInvocationHandler#invoke - ```java public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); } catch (IllegalArgumentException e) { return false; } } else if ("hashCode".equals(method.getName())) { return hashCode(); } else if ("toString".equals(method.getName())) { return toString(); } return dispatch.get(method).invoke(args); } ``` - feign.SynchronousMethodHandler#invoke - com.alibaba.nacos.client.naming.NacosNamingService#selectInstances - 这就是 OpenFeign 首次发起远程调用时,最最最慢的核心原因 #### 4.2.2 Feign 的上下文隔离机制 - 入口:org.springframework.cloud.openfeign.FeignClientFactoryBean#feign - image-20230510230417128 - getTarget() 所在对象的参数信息来源? - 源码分析: - org.springframework.cloud.openfeign.FeignClientFactoryBean#get - org.springframework.cloud.context.named.NamedContextFactory#getInstance - org.springframework.cloud.context.named.NamedContextFactory#getContext - org.springframework.cloud.context.named.NamedContextFactory#createContext - 含有 OpenFeign 的项目,就是比没有 OpenFeign 的项目会慢一点点的核心原因 - ```java protected AnnotationConfigApplicationContext createContext(String name) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); if (this.configurations.containsKey(name)) { for (Class configuration : this.configurations.get(name) .getConfiguration()) { context.register(configuration); } } for (Map.Entry entry : this.configurations.entrySet()) { if (entry.getKey().startsWith("default.")) { for (Class configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } } context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType); context.getEnvironment().getPropertySources().addFirst(new MapPropertySource( this.propertySourceName, Collections.singletonMap(this.propertyName, name))); if (this.parent != null) { // Uses Environment from parent as well as beans context.setParent(this.parent); // jdk11 issue // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101 context.setClassLoader(this.parent.getClassLoader()); } context.setDisplayName(generateDisplayName(name)); // 重点在这里,创建了一个迷你的上下文对象,并且刷新了迷你上下文对象进行实例化操作 context.refresh(); return context; } ``` ## 五、Gateway 网关路由 ### 5.1 Gateway 路由案例搭建 - Zuul -> Gateway - 官网地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter - 路径断言:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-path-route-predicate-factory - 路径拦截: - 请求前的地址:http://localhost:9050/gateway/web/consumer/helloworld/geek - 请求前的地址:http://localhost:9050/gateway/web/provider/hello/geek - 转换后的地址:http://localhost:9052/helloworld/geek - uri: http://127.0.0.1:9052 - StripPrefix=3 - 怎么做到负载均衡?怎么探索源码找到负载均衡? - org.springframework.cloud.gateway.filter.LoadBalancerClientFilter - uri: lb://openfeign-provider ### 5.2 自定义拦截过滤 - 使用拦截 - 请求前的地址:http://localhost:9053/gateway/web/consumer/helloworld/geek - 请求前的地址:http://localhost:9053/gateway/web/provider/hello/geek - Header - Query - 查看对应的源码类:AbstractGatewayFilterFactory - 自定义拦截 - 局部拦截 - 继承类:AbstractGatewayFilterFactory - CostGatewayFilterFactory - Cost=ON/OFF - 借鉴:org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory - 全局拦截 - 继承类:GlobalFilter - LoggerGlobalFilter - LOG=ON - 借鉴:还是 org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory ### 5.3 转发重定向机制 - 思考寻找调试突破口 - **制造异常** - 在必经之路的过滤器中进行断点 - 分析异常日志可以抓取哪些重要信息 - image-20230513172610385 - 在 ExceptionHandlingWebHandler 中添加断点 - 一边调试一边分析堆栈日志 - 找到 Gateway 框架体系处理的入口:org.springframework.http.server.reactive.ReactorHttpHandlerAdapter - 构建网关上下文:org.springframework.web.server.adapter.HttpWebHandlerAdapter - 遍历 Mapping 获取真实处理请求的 Handler:org.springframework.web.reactive.DispatcherHandler - 构建过滤器链:org.springframework.cloud.gateway.handler.FilteringWebHandler - 过滤器必经之路:org.springframework.cloud.gateway.handler.FilteringWebHandler.DefaultGatewayFilterChain - 负载均衡过滤器:org.springframework.cloud.gateway.filter.LoadBalancerClientFilter - 利用 Netty 发送网络请求过滤器:org.springframework.cloud.gateway.filter.NettyRoutingFilter - 官网调用流程抽象总结:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/images/spring_cloud_gateway_diagram.png ## 六、Sentinel 限流降级 ### 6.1 Sentinel 工程案例 #### 6.1.1 Sentinel 手动设置限流/降级案例搭建 - 限流 - 官方文档:https://sentinelguard.io/zh-cn/docs/flow-control.html - 并发数:触发阈值直接抛弃 - QPS - 直接拒绝:触发阈值直接抛弃 - 冷启动:在一段时间内针对突发流量缓慢增长处理数量 - 匀速器:请求以均匀的速度通过; - 降级 - 官方文档:https://sentinelguard.io/zh-cn/docs/circuit-breaking.html - RT:统计时长内,请求总数大于预设请求数,且慢请求个数大于预设比例,则熔断拒绝一段时间 - 异常比例:统计时长内,请求总数大于预设请求数,且异常比例大于预比例,则熔断拒绝一段时间 - 异常数:统计时长内,请求总数大于预设请求数,且异常数大于预设数值,则熔断拒绝一段时间 #### 6.1.2 Sentinel 自动获取限流/降级案例搭建 - 整体架构 - Sentinel 控制台 - 官网文档:https://sentinelguard.io/zh-cn/docs/dashboard.html - 控制台相关包下载地址:https://github.com/alibaba/Sentinel/releases/tag/2.0.0-alpha - 可执行文件:https://github.com/alibaba/Sentinel/releases/download/2.0.0-alpha/sentinel-dashboard-2.0.0-alpha-preview.jar - 源码包1:https://github.com/alibaba/Sentinel/archive/refs/tags/2.0.0-alpha.zip - 源码包2:https://github.com/alibaba/Sentinel/archive/refs/tags/2.0.0-alpha.tar.gz - 启动控制台 - java -Dserver.port=9999 -Dcsp.sentinel.dashboard.server=localhost:9999 -Dproject.name=sentinel-dashboar -jar sentinel-dashboard-2.0.0-alpha-preview.jar - 打开控制台 - http://localhost:9999/ - 查看控制台 - 应用联动 Sentinel 限流 - image-20230514172558391 - 官网文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel - ```yaml spring: cloud: sentinel: transport: port: 8719 dashboard: localhost:9999 ``` - 联动 Sentinel 限流 - image-20230514095226718 - 联动 Sentinel 降级 - image-20230514095314904 ​ #### 6.1.3 Sentinel 接入 Nacos 案例搭建 - 官方文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel - image-20230514172503486 - 动态数据源支持 - ```properties spring.cloud.sentinel.datasource.flow.nacos.server-addr=localhost:8848 spring.cloud.sentinel.datasource.flow.nacos.data-id=sentinel-online-flowdeg-consumer-sentinel.properties spring.cloud.sentinel.datasource.flow.nacos.group-id=DEFAULT_GROUP spring.cloud.sentinel.datasource.flow.nacos.rule-type=flow ``` - 配置文件内容怎么写呢? - Sentinel 控制台保存一下 - F12 - CV大法 - 利弊 - 利:简单方便,仅需配置中心即可完美动态更新限流降级配置 - 弊:不够直观,不知道怎么设置限流降级,对开发人员要求高 ### 6.2 Sentinel 持久化操作 #### 6.2.1 探索 Sentinel 规则持久化机制 image-20230514172401543 - 怎么寻找持久化突破口? - ```yaml spring: cloud: sentinel: transport: port: 8719 dashboard: localhost:9999 ``` - 尝试断点,dashboard 都拿来在哪些地方用? - com.alibaba.cloud.sentinel.SentinelProperties.Transport#getDashboard - ```java if (StringUtils.isEmpty(System.getProperty(TransportConfig.CONSOLE_SERVER)) && StringUtils.hasText(properties.getTransport().getDashboard())) { System.setProperty(TransportConfig.CONSOLE_SERVER, properties.getTransport().getDashboard()); } ``` - com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration#init - ```java // earlier initialize if (properties.isEager()) { InitExecutor.doInit(); } ``` - 断点 + 代码分析 - com.alibaba.csp.sentinel.init.InitExecutor#doInit 添加断点后,发现没进来,为什么? - 触发一次 Http 请求调用后,发现进入了 com.alibaba.csp.sentinel.init.InitExecutor#doInit 断点 - 循环逻辑代码块分析 - ```java for (InitFunc initFunc : loader) { RecordLog.info("[InitExecutor] Found init func: " + initFunc.getClass().getCanonicalName()); insertSorted(initList, initFunc); } for (OrderWrapper w : initList) { w.func.init(); RecordLog.info(String.format("[InitExecutor] Executing %s with order %d", w.func.getClass().getCanonicalName(), w.order)); } ``` - 寻找到了 com.alibaba.csp.sentinel.transport.command.SimpleHttpCommandCenter#start 的相关收发数据逻辑 - 关键代码 1:开启服务端层面的线程 - ```java socketReference = serverSocket; executor.submit(new ServerThread(serverSocket)); success = true; port = serverSocket.getLocalPort(); ``` - 关键代码 2:从 ServerSocket 的 accept 方法收数据 - ```java socket = this.serverSocket.accept(); setSocketSoTimeout(socket); HttpEventTask eventTask = new HttpEventTask(socket); bizExecutor.submit(eventTask); ``` - 数据源处理核心代码 - ```java if (FLOW_RULE_TYPE.equalsIgnoreCase(type)) { List flowRules = JSONArray.parseArray(data, FlowRule.class); FlowRuleManager.loadRules(flowRules); if (!writeToDataSource(getFlowDataSource(), flowRules)) { result = WRITE_DS_FAILURE_MSG; } return CommandResponse.ofSuccess(result); } ``` #### 6.2.2 联动 Sentinel 与 Nacos 的持久化实现 image-20230514172401543 - 分析如何扩展数据源 - ```java if (FLOW_RULE_TYPE.equalsIgnoreCase(type)) { List flowRules = JSONArray.parseArray(data, FlowRule.class); FlowRuleManager.loadRules(flowRules); if (!writeToDataSource(getFlowDataSource(), flowRules)) { result = WRITE_DS_FAILURE_MSG; } return CommandResponse.ofSuccess(result); } ``` - 为什么 getFlowDataSource() 是 null 值? - 如何给 getFlowDataSource() 赋值呢? - 充分利用已有方法:com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry#registerFlowDataSource - 充分利用 Spring 的特性 - org.springframework.beans.factory.InitializingBean - @javax.annotation.PostConstruct - 接着又如何得到 com.alibaba.csp.sentinel.datasource.WritableDataSource 实现类呢? - 创建 com.alibaba.nacos.api.config.ConfigService 对象 - 调用 com.alibaba.nacos.api.config.ConfigService 进行发布数据 ### 6.3 核心 SphU.entry 源码分析 #### 6.3.1 ProcessorSlotChain 过滤器链流程 - 如何找到限流降级的入口? - 曾经无数次练习过的 Demo 入手:sca05-sentinel 内部的各个模块 - 找到 Sphu.entry 内的核心必经环节:CtSph#entryWithPriority - 创建上下文:com.alibaba.csp.sentinel.context.ContextUtil#trueEnter - 构建过滤器链:com.alibaba.csp.sentinel.CtSph#lookProcessChain - com.alibaba.csp.sentinel.slotchain.SlotChainProvider#newSlotChain - com.alibaba.csp.sentinel.slots.DefaultSlotChainBuilder#build - ```properties // NodeSelectorSlot -> ClusterBuilderSlot -> LogSlot -> StatisticSlot -> // AuthoritySlot -> SystemSlot -> FlowSlot -> DefaultCircuitBreakerSlot -> DegradeSlot ``` - 从官网了解一些限流降级模块的作用:https://sentinelguard.io/zh-cn/docs/basic-implementation.html - 过滤器链触发调用: - ```java Entry e = new CtEntry(resourceWrapper, chain, context); try { chain.entry(context, resourceWrapper, null, count, prioritized, args); } catch (BlockException e1) { e.exit(count, args); throw e1; } catch (Throwable e1) { // This should not happen, unless there are errors existing in Sentinel internal. RecordLog.info("Sentinel unexpected exception", e1); } ``` #### 6.3.2 StatisticSlot 核心处理逻辑分水岭 - 分水岭的由来 - ```java @Override public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { try { // 先触发后续的过滤器 fireEntry(context, resourceWrapper, node, count, prioritized, args); // 然后再做请求通过数累加、线程数累加等等 node.increaseThreadNum(); node.addPassRequest(count); // 省略部分代码 } catch (PriorityWaitException ex) { // 省略部分代码 } catch (BlockException e) { // 省略部分代码 throw e; } catch (Throwable e) { // 省略部分代码 throw e; } } ``` - 核心校验逻辑 - com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot - ```java void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException { Map> authorityRules = AuthorityRuleManager.getAuthorityRules(); if (authorityRules == null) { return; } Set rules = authorityRules.get(resource.getName()); if (rules == null) { return; } for (AuthorityRule rule : rules) { if (!AuthorityRuleChecker.passCheck(rule, context)) { // 校验不通过,抛出异常,并且将【来源 Origin】、【规则 rule】抛出去,便于识别哪条规则不通过了 throw new AuthorityException(context.getOrigin(), rule); } } } ``` - com.alibaba.csp.sentinel.slots.system.SystemSlot - ```java public static void checkSystem(ResourceWrapper resourceWrapper, int count) throws BlockException { // NPE 判断,如果为空,不做任何处理 if (resourceWrapper == null) { return; } // Ensure the checking switch is on. // 没有配置规则的话,那么就是处于 false 状态,并且直接返回,不做任何校验处理 if (!checkSystemStatus.get()) { return; } // for inbound traffic only // 并且该系统监控指标,只允许 IN 方向的控制,对于 OUT 方向的则不做任何校验处理 if (resourceWrapper.getEntryType() != EntryType.IN) { return; } // 接下来就是一系列的指标判断,纯粹的比大小判断 // total qps double currentQps = Constants.ENTRY_NODE.passQps(); if (currentQps + count > qps) { throw new SystemBlockException(resourceWrapper.getName(), "qps"); } // total thread int currentThread = Constants.ENTRY_NODE.curThreadNum(); if (currentThread > maxThread) { throw new SystemBlockException(resourceWrapper.getName(), "thread"); } double rt = Constants.ENTRY_NODE.avgRt(); if (rt > maxRt) { throw new SystemBlockException(resourceWrapper.getName(), "rt"); } // load. BBR algorithm. if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) { if (!checkBbr(currentThread)) { throw new SystemBlockException(resourceWrapper.getName(), "load"); } } // cpu usage if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) { throw new SystemBlockException(resourceWrapper.getName(), "cpu"); } } ``` - com.alibaba.csp.sentinel.slots.block.flow.FlowSlot - ```java public void checkFlow(Function> ruleProvider, ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized) throws BlockException { if (ruleProvider == null || resource == null) { return; } Collection rules = ruleProvider.apply(resource.getName()); if (rules != null) { for (FlowRule rule : rules) { // 具体的流控规则校验 if (!canPassCheck(rule, context, node, count, prioritized)) { // 校验失败的话,则抛出异常 throw new FlowException(rule.getLimitApp(), rule); } } } } ``` - com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot - ```java void performChecking(Context context, ResourceWrapper r) throws BlockException { // 如果没有降级规则的话,那么就直接返回,不需要做任何降级处理 List circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName()); if (circuitBreakers == null || circuitBreakers.isEmpty()) { return; } for (CircuitBreaker cb : circuitBreakers) { if (!cb.tryPass(context)) { throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule()); } } } ``` ## 七、Dubbo 服务调用框架 ### 7.1 Dubbo 工程案例 - 官方文档:https://cn.dubbo.apache.org/zh-cn/overview/home/ - 如何从Alibaba工程代码一路找到Dubbo文档:https://github.com/alibaba/spring-cloud-alibaba - 总体架构:https://cn.dubbo.apache.org/zh-cn/overview/core-features/service-discovery/ #### 7.1.1 Provider & Consumer 案例搭建 - 我们要搭建的工程架构是怎样的? - image-20230515231030935 - 这里停顿一下,思考下,应该怎么正确的添加 pom 引用? - ```xml com.alibaba.cloud spring-cloud-starter-dubbo com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` - 工程需要哪些模块? - 需要怎么填写 yml 配置文件内容? - 借鉴性参考:https://cn.dubbo.apache.org/zh-cn/overview/quickstart/java/spring-boot/ #### 7.1.2 依赖检查 & 负载均衡案例搭建 - 依赖检查 - check - 验证 check = false 后,提供方启动成功了,那么消费方是否还能调用提供方成功? - 第一步:启动消费方; - 第二步:消费方其实会阻塞等待 20s; - 第三步:当消费方在20s等待的过程中,立马启动提供方; - 第四步:待提供方启动成功后,消费方这边就等待20s睡眠完成,然后再次发起远程调用。 - 负载均衡 - 搭建多套提供方 - image-20230515235449701 - loadbalance = roundrobin - -Dserver.port=9070 -Ddubbo.protocol.port=29070 - -Dserver.port=9071 -Ddubbo.protocol.port=29071 - -Dserver.port=9072 -Ddubbo.protocol.port=29072 #### 7.1.3 广播调用 & 缓存操作案例搭建 - braodcast - image-20230517070758927 - cache - image-20230517070830955 #### 7.1.4 点对点调用案例搭建 - url = dubbo://IP地址:端口 - -Dcom.springcloudali.ms.facde.DemoService=dubbo://localhost:29070 #### 7.1.5 泛化调用案例搭建 - 模拟HTTP请求 - 协议:http vs dubbo - 端口:443 vs 20880 --》 从注册中心拿 - 路径:相对地址 vs 类名/方法名/方法入参类型名 - 请求体:requestBody vs reqArgs - 如何标识泛化形式调用 ### 7.2 Dubbo 源码分析 #### 7.2.1 JDK SPI 与 Dubbo SPI 机制 - SPI:Service Provider Interface,“服务” 提供 “接口” - image-20230518234936314 - JDK SPI:ServiceLoader.load - 使用 load 方法频率高,容易影响 IO 吞吐和内存消耗。 - 使用 load 方法想要获取指定实现类,需要自己进行遍历并编写各种比较代码。 - Dubbo SPI:ApplicationModel.defaultModel().getExtensionLoader - 增加缓存,来降低磁盘IO访问以及减少对象的生成。 - 使用Map的hash查找,来提升检索指定实现类的性能。 #### 7.2.2 Dubbo 服务发布流程 - 服务发布,到底发布了什么东西? - 将自身信息注册到注册中心 - 开启端口服务 - 可以从哪些环节挖掘突破口? - 在提供方这边,怎么写代码提供方服务? - ![image-20230520152246113](README-IMAGES/image-20230520152246113.png) - **总结:**https://static001.geekbang.org/resource/image/2c/cd/2c4c58169731a6092afbdfa59aafcfcd.jpg ![image-20230520080042792](README-IMAGES/image-20230520080042792.png) #### 7.2.3 Dubbo 服务订阅流程 - 巧妙利用对比思维摸出订阅流程 - 作用域 - @DubboService:作用提供方 - @DubboReference:作用在消费方 - 类的名称 - @DubboService:ServiceConfig - @DubboReference:ReferenceConfig - 辐射功能 - 提供方:注册自己、开启端口服务 - 消费方:注册自己、捞取订阅列表 - **总结:**https://static001.geekbang.org/resource/image/45/b5/45f6c0b365efc1f587f36336e95467b5.jpg ![image-20230520080308342](README-IMAGES/image-20230520080308342.png) #### 7.2.4 Dubbo Wrapper 机制 - 泛化调用:接口类名、方法名、方法参数类名、业务请求参数 - 如何针对这些入参设计服务端接口? - 简单方式 - 代理方式 - JDK 代理 - Cglib 代理 - 自定义代理 - Wrapper源码代理、测试代理效率 - **总结:**https://static001.geekbang.org/resource/image/99/c6/9939c9abb9879cd4c65c2yy7c11430c6.jpg ![image-20230520075853355](README-IMAGES/image-20230520075853355.png) #### 7.2.5 Dubbo 协议编解码 - **抽象:**https://static001.geekbang.org/resource/image/4a/ef/4ab9241ef2acf486fdfd3b2a13e005ef.jpg image-20230520080410005 - Socket 发送数据 《----》 ServerSocket 接收数据 - 固定长度:发送 100 个字节 - 分隔符 - 定长+变长:特殊前缀(读取多少长度)+读取指定长度 - **总结:**https://static001.geekbang.org/resource/image/28/fb/28b10a1005ab622c8865e6693a17f8fb.jpg ![image-20230520080448602](README-IMAGES/image-20230520080448602.png)