[TOC]
一站式速学 SpringCloudAlibaba 体系自学课程,希望帮助更多想学的童鞋快速掌握 SCA 体系。
模块 | 微服务工程名 | 端口 | 功能描述 |
---|---|---|---|
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、解决了什么问题?
2、如何完整的阐述涉及到的组件?
1、什么是最佳实践?
2、实践的又是什么?
相关网址:
Eureka Server 启动
引入包
添加应用启动类、添加Eureka服务端注解、设置Tomcat绑定端口、设置Eureka地址
设置 Tomcat 启动端口为 6000(为什么不行?)
Eureka Provider 工程
引入包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加应用启动类、添加Eureka客户端注解、设置Tomcat绑定端口、设置Eureka地址
Eureka Consumer 工程
引入包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加应用启动类、添加Eureka客户端注解、设置Tomcat绑定端口、设置Eureka地址
怎么进行负载均衡呢?
Nacos Server 启动
Nacos Provider 工程
引入包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
添加应用启动类、添加控制器、添加服务发现注解、设置Tomcat绑定端口、设置Nacos地址
Nacos Consumer 工程
引入包
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
添加启动类、控制器、RestTemplate、远程调用方法、设置Tomcat绑定端口、设置Nacos地址
版本说明: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 引用
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
nacos-discovery-consumer 关键 pom 引用
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
发起远程调用
怎么快速熟悉注册流程?
异常堆栈能给什么启示?
注册流程:1.x 版本、2.x 版本
怎么理解订阅关键环节?
如何巧妙创造订阅异常?
订阅流程
心跳和剔除分别解决了什么问题?
强调这里讨论 1.0 版本非长连接的机制
心跳(客户端)
剔除(服务端)
UDP 推送
官方文档:https://nacos.io/zh-cn/docs/quick-start-spring-cloud.html
留个醒,重点关注,port 是不能随便修改的,一旦修改的话,虽然内存里面会直接变成最新的配置,但是端口提供的服务还是原来的旧端口
重点关注放在 bootstrap.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
如何模拟实时拉取效果?
根据配置返回内容反推源头
源码关键节点
堆栈流程
C(Consistency)A(Avaliability)P(Partition Tolerance)是什么?
为什么一定得保证 P ?
CP 应用案例(价格)
AP 应用案例(点赞)
Nacos、Eureka、Zookeeper 典型模式
新节点同步机制: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 都会处理一部分应该由自己负责的数据
public boolean responsible(String responsibleTag) {
final List<String> 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 端的所有数据
Ribbon 是什么?
Ribbon
openfeign-provider 9040
ribbon-consumer 9043
核心流程讲解
源码流程跟踪
扫描流程: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.cloud.openfeign.FeignClientsRegistrar#registerClientConfiguration
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
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
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
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
入口:org.springframework.cloud.openfeign.FeignClientFactoryBean#feign
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 的项目会慢一点点的核心原因
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<String, C> 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.<String, Object>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;
}
Zuul -> Gateway
官网地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-starter
路径拦截:
怎么做到负载均衡?怎么探索源码找到负载均衡?
思考寻找调试突破口
分析异常日志可以抓取哪些重要信息
在 ExceptionHandlingWebHandler 中添加断点
一边调试一边分析堆栈日志
整体架构
Sentinel 控制台
应用联动 Sentinel 限流
官网文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:9999
联动 Sentinel 限流
联动 Sentinel 降级
官方文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
动态数据源支持
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
配置文件内容怎么写呢?
利弊
怎么寻找持久化突破口?
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:9999
尝试断点,dashboard 都拿来在哪些地方用?
com.alibaba.cloud.sentinel.SentinelProperties.Transport#getDashboard
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
// earlier initialize
if (properties.isEager()) {
InitExecutor.doInit();
}
断点 + 代码分析
com.alibaba.csp.sentinel.init.InitExecutor#doInit 添加断点后,发现没进来,为什么?
触发一次 Http 请求调用后,发现进入了 com.alibaba.csp.sentinel.init.InitExecutor#doInit 断点
循环逻辑代码块分析
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:开启服务端层面的线程
socketReference = serverSocket;
executor.submit(new ServerThread(serverSocket));
success = true;
port = serverSocket.getLocalPort();
关键代码 2:从 ServerSocket 的 accept 方法收数据
socket = this.serverSocket.accept();
setSocketSoTimeout(socket);
HttpEventTask eventTask = new HttpEventTask(socket);
bizExecutor.submit(eventTask);
数据源处理核心代码
if (FLOW_RULE_TYPE.equalsIgnoreCase(type)) {
List<FlowRule> flowRules = JSONArray.parseArray(data, FlowRule.class);
FlowRuleManager.loadRules(flowRules);
if (!writeToDataSource(getFlowDataSource(), flowRules)) {
result = WRITE_DS_FAILURE_MSG;
}
return CommandResponse.ofSuccess(result);
}
分析如何扩展数据源
if (FLOW_RULE_TYPE.equalsIgnoreCase(type)) {
List<FlowRule> 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.datasource.WritableDataSource 实现类呢?
如何找到限流降级的入口?
曾经无数次练习过的 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
// NodeSelectorSlot -> ClusterBuilderSlot -> LogSlot -> StatisticSlot ->
// AuthoritySlot -> SystemSlot -> FlowSlot -> DefaultCircuitBreakerSlot -> DegradeSlot
从官网了解一些限流降级模块的作用:https://sentinelguard.io/zh-cn/docs/basic-implementation.html
过滤器链触发调用:
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);
}
分水岭的由来
@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
void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();
if (authorityRules == null) {
return;
}
Set<AuthorityRule> 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
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
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider,
ResourceWrapper resource,
Context context,
DefaultNode node,
int count, boolean prioritized) throws BlockException {
if (ruleProvider == null || resource == null) {
return;
}
Collection<FlowRule> 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
void performChecking(Context context, ResourceWrapper r) throws BlockException {
// 如果没有降级规则的话,那么就直接返回,不需要做任何降级处理
List<CircuitBreaker> 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());
}
}
}
我们要搭建的工程架构是怎样的?
这里停顿一下,思考下,应该怎么正确的添加 pom 引用?
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
工程需要哪些模块?
需要怎么填写 yml 配置文件内容?
借鉴性参考:https://cn.dubbo.apache.org/zh-cn/overview/quickstart/java/spring-boot/
服务发布,到底发布了什么东西?
可以从哪些环节挖掘突破口?
在提供方这边,怎么写代码提供方服务?
**总结:**https://static001.geekbang.org/resource/image/2c/cd/2c4c58169731a6092afbdfa59aafcfcd.jpg
巧妙利用对比思维摸出订阅流程
**总结:**https://static001.geekbang.org/resource/image/45/b5/45f6c0b365efc1f587f36336e95467b5.jpg
泛化调用:接口类名、方法名、方法参数类名、业务请求参数
如何针对这些入参设计服务端接口?
简单方式
代理方式
**总结:**https://static001.geekbang.org/resource/image/99/c6/9939c9abb9879cd4c65c2yy7c11430c6.jpg
Socket 发送数据 《----》 ServerSocket 接收数据
固定长度:发送 100 个字节
分隔符
定长+变长:特殊前缀(读取多少长度)+读取指定长度
**总结:**https://static001.geekbang.org/resource/image/28/fb/28b10a1005ab622c8865e6693a17f8fb.jpg
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。