一。技术背景
由于升级jdk17的需要 我们将项目中的 spring cloud spring cloud alibaba 以及springboot进行了升级 各版本如下
spring cloud 2021.0.5
spring cloud alibaba 2021.0.5.0
spring boot 2.6.13
二。问题表现
当启动项目服务后,服务无法注册到 sentinel-dashboard
三。错误排查
- 首先检查sentinel-dashboard 启动状态 启动成功并且可以正常访问且不存在网络问题
- 环境配置检查
<!-- 依赖检查 无误 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
配置检查
#配置也正常
spring.cloud.sentinel.transport.dashboard=localhost:8080
spring.cloud.sentinel.transport.port=8719
- 第三步源码追踪
接下来开始漫长源码分析步骤
然后点击 spring.cloud.sentinel.transport.dashboard 这条配置 跳转 com.alibaba.cloud.sentinel.SentinelProperties.Transport#setDashboard
然后点击 getDashboard() 方法查看在哪里调用 最后来到了 com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration#init
在如下代码中
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(name = "spring.cloud.sentinel.enabled", matchIfMissing = true)
@EnableConfigurationProperties(SentinelProperties.class)
public class SentinelAutoConfiguration {@PostConstructprivate void init() {///省略部分逻辑 if (StringUtils.isEmpty(System.getProperty(TransportConfig.SERVER_PORT))&& StringUtils.isNotBlank(properties.getTransport().getPort())) {System.setProperty(TransportConfig.SERVER_PORT,properties.getTransport().getPort());}if (StringUtils.isEmpty(System.getProperty(TransportConfig.CONSOLE_SERVER))&& StringUtils.isNotBlank(properties.getTransport().getDashboard())) {System.setProperty(TransportConfig.CONSOLE_SERVER,properties.getTransport().getDashboard());}}
}
断点时 发现配置成功被设置到 系统的属性配置中
接下来再来看 心跳发送具体类 HeartbeatSenderInitFunc
com.alibaba.csp.sentinel.transport.init.HeartbeatSenderInitFunc#init
@Override
public void init() {HeartbeatSender sender = HeartbeatSenderProvider.getHeartbeatSender();if (sender == null) {RecordLog.warn("[HeartbeatSenderInitFunc] WARN: No HeartbeatSender loaded");return;}initSchedulerIfNeeded();long interval = retrieveInterval(sender);setIntervalIfNotExists(interval);//定时调度发送心跳scheduleHeartbeatTask(sender, interval);
}
具体得调度逻辑 每5秒发送一次心跳
private void scheduleHeartbeatTask(/*@NonNull*/ final HeartbeatSender sender, /*@Valid*/ long interval) {pool.scheduleAtFixedRate(new Runnable() {@Overridepublic void run() {try {sender.sendHeartbeat();} catch (Throwable e) {RecordLog.warn("[HeartbeatSender] Send heartbeat error", e);}}}, 5000, interval, TimeUnit.MILLISECONDS);RecordLog.info("[HeartbeatSenderInit] HeartbeatSender started: "+ sender.getClass().getCanonicalName());
}
我们再来看下具体的实现
sender.sendHeartbeat();
实现类只有一个 SimpleHttpHeartbeatSender
com.alibaba.csp.sentinel.transport.heartbeat.SimpleHttpHeartbeatSender#sendHeartbeat
@Override
public boolean sendHeartbeat() throws Exception { if (TransportConfig.getRuntimePort() <= 0) { RecordLog.info("[SimpleHttpHeartbeatSender] Command server port not initialized, won't send heartbeat"return false; } Endpoint addrInfo = getAvailableAddress(); if (addrInfo == null) { return false; } SimpleHttpRequest request = new SimpleHttpRequest(addrInfo, TransportConfig.getHeartbeatApiPath()); request.setParams(heartBeat.generateCurrentMessage()); try { SimpleHttpResponse response = httpClient.post(request); if (response.getStatusCode() == OK_STATUS) { return true; } else if (clientErrorCode(response.getStatusCode()) || serverErrorCode(response.getStatusCode())) { RecordLog.warn("[SimpleHttpHeartbeatSender] Failed to send heartbeat to " + addrInfo + ", http status code: " + response.getStatusCode()); } } catch (Exception e) { RecordLog.warn("[SimpleHttpHeartbeatSender] Failed to send heartbeat to " + addrInfo, e); } return false;
}
以上就是发送心跳的逻辑
核心逻辑
- 获取有效的链接
- 创建连接发送心跳请求
- 响应以及异常处理
但是在断点过程中 有效的链接列表居然是空的 这就是导致代码无法继续向下
然后我们继续围绕这个点进行排查
获取有效的地址列表方法如下
private Endpoint getAvailableAddress() {if (addressList == null || addressList.isEmpty()) {return null;}if (currentAddressIdx < 0) {currentAddressIdx = 0;}int index = currentAddressIdx % addressList.size();return addressList.get(index);
}
发现使用的成员变量 addressList 我们再来找下这个值的赋值操作
赋值操作只有一个地方 在SimpleHttpHeartbeatSender的构造方法中
public SimpleHttpHeartbeatSender() {// Retrieve the list of default addresses.List<Endpoint> newAddrs = TransportConfig.getConsoleServerList();if (newAddrs.isEmpty()) {RecordLog.warn("[SimpleHttpHeartbeatSender] Dashboard server address not configured or not available");} else {RecordLog.info("[SimpleHttpHeartbeatSender] Default console address list retrieved: {}", newAddrs);}this.addressList = newAddrs;
}
com.alibaba.csp.sentinel.transport.config.TransportConfig#getConsoleServerList
public static List<Endpoint> getConsoleServerList() {String config = SentinelConfig.getConfig(CONSOLE_SERVER);List<Endpoint> list = new ArrayList<Endpoint>();if (StringUtil.isBlank(config)) {return list;}//。。。。。省略部分return list;
}
com.alibaba.csp.sentinel.config.SentinelConfig#getConfig(java.lang.String)
public static String getConfig(String key) {AssertUtil.notNull(key, "key cannot be null");return props.get(key);
}
我们再来看下 props的初始化
在SentinelConfig的静态构造中
static {try {initialize();//加载配置loadProps();resolveAppName();resolveAppType();RecordLog.info("[SentinelConfig] Application type resolved: {}", appType);} catch (Throwable ex) {RecordLog.warn("[SentinelConfig] Failed to initialize", ex);ex.printStackTrace();}
}
com.alibaba.csp.sentinel.config.SentinelConfig#loadProps
private static void loadProps() {///从配置加载中获取配置Properties properties = SentinelConfigLoader.getProperties();for (Object key : properties.keySet()) {setConfig((String) key, (String) properties.get(key));}
}
从SentinelConfigLoader 获取到配置
public static Properties getProperties() {return properties;
}
而这个properties的初始化是在SentinelConfigLoader 静态构造方法中
static {try {load();} catch (Throwable t) {RecordLog.warn("[SentinelConfigLoader] Failed to initialize configuration items", t);}
}
private static void load() {// Order: system property -> system env -> default file (classpath:sentinel.properties) -> legacy pathString fileName = System.getProperty(SENTINEL_CONFIG_PROPERTY_KEY);if (StringUtil.isBlank(fileName)) {fileName = System.getenv(SENTINEL_CONFIG_ENV_KEY);if (StringUtil.isBlank(fileName)) {fileName = DEFAULT_SENTINEL_CONFIG_FILE;}}Properties p = ConfigUtil.loadProperties(fileName);if (p != null && !p.isEmpty()) {RecordLog.info("[SentinelConfigLoader] Loading Sentinel config from {}", fileName);properties.putAll(p);}for (Map.Entry<Object, Object> entry : new CopyOnWriteArraySet<>(System.getProperties().entrySet())) {String configKey = entry.getKey().toString();String newConfigValue = entry.getValue().toString();String oldConfigValue = properties.getProperty(configKey);properties.put(configKey, newConfigValue);if (oldConfigValue != null) {RecordLog.info("[SentinelConfigLoader] JVM parameter overrides {}: {} -> {}",configKey, oldConfigValue, newConfigValue);}}
}
看到这里就大概明白了,因为这是静态代码话 肯定优先初始化,而我们的地址在项目启动bean加载中才会设置到System的Properties里
所以获取的 地址一直是空的。 也无法注册到sentinel-dashboard上
四。解决办法
- idea 启动类 增加参数 指定dashboard server地址 以及应用名称
-Dcsp.sentinel.dashboard.server=localhost:8080 -Dcsp.sentinel.app.name=gateway-service
- 启动类设置系统变量
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayServiceApplication {public static void main(String[] args) {System.setProperty("csp.sentinel.dashboard.server","localhost:8080");System.setProperty("csp.sentinel.app.name","gateway-service");SpringApplication.run(GatewayServiceApplication.class, args);}}
五。后续分析旧的版本的依赖对应的实现方式
旧的依赖版本为
springboot 2.3.12.RELEASE
spring cloud Hoxton.SR12
spring cloud alibaba 2.2.9.RELEASE