一。技术背景
由于升级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 {
@PostConstruct
private 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);
}
这里有个重要的逻辑
HeartbeatSender sender = HeartbeatSenderProvider.getHeartbeatSender();
此次调用触发 HeartbeatSenderProvider 静态方法触发加载
static {
resolveInstance();
}
private static void resolveInstance() {
//spi加载类 并初始化
HeartbeatSender resolved = SpiLoader.of(HeartbeatSender.class).loadHighestPriorityInstance();
if (resolved == null) {
RecordLog.warn("[HeartbeatSenderProvider] WARN: No existing HeartbeatSender found");
} else {
heartbeatSender = resolved;
RecordLog.info("[HeartbeatSenderProvider] HeartbeatSender activated: {}", resolved.getClass()
.getCanonicalName());
}
}
/**
* 调用的方法
* Get resolved {@link HeartbeatSender} instance.
*
* @return resolved {@code HeartbeatSender} instance
*/
public static HeartbeatSender getHeartbeatSender() {
return heartbeatSender;
}
其中在 SpiLoader.of(HeartbeatSender.class).loadHighestPriorityInstance(); 方法中创建了实例 调用了SimpleHttpHeartbeatSender 构造方法
public SimpleHttpHeartbeatSender() {
// Retrieve the list of default addresses.
//获取控制台server地址列表
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;
}
在构造方法中获取连接配置中的地址列表
TransportConfig.getConsoleServerList();
//com.alibaba.csp.sentinel.transport.config.TransportConfig#getConsoleServerList
public static List<Endpoint> getConsoleServerList() {
//从sentinel配置中加载配置
String config = SentinelConfig.getConfig(CONSOLE_SERVER);
List<Endpoint> list = new ArrayList<Endpoint>();
if (StringUtil.isBlank(config)) {
return list;
}
//省略部分逻辑
return list;
}
SentinelConfig.getConfig(CONSOLE_SERVER);
获取配置的具体细节
public static String getConfig(String key) {
AssertUtil.notNull(key, "key cannot be null");
return props.get(key);
}
//此方法调用前会触发 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();
}
}
//我们来看核心的加载配置的方法 loadProps();
private static void loadProps() {
//又调用了SentinelConfigLoader的类型的获取属性方法
Properties properties = SentinelConfigLoader.getProperties();
for (Object key : properties.keySet()) {
setConfig((String) key, (String) properties.get(key));
}
}
我们再来看下 SentinelConfigLoader.getProperties(); 具体的实现
public static Properties getProperties() {
return properties;
}
//此方法的调用又会触发 SentinelConfigLoader的静态构造
static {
try {
load();
} catch (Throwable t) {
RecordLog.warn("[SentinelConfigLoader] Failed to initialize configuration items", t);
}
}
//核心load方法
private static void load() {
// Order: system property -> system env -> default file (classpath:sentinel.properties) -> legacy path
String 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);
}
}
}
核心逻辑主要有两步
- 从sentinel配置文件中加载 配置不为空则放入 properties 中
- 从系统的配置中获取 并 放入properties 如果已经存在则覆盖
这步完成后 就给 SimpleHttpHeartbeatSender 的 addressList 属性就行赋值操作
从这里就可以看到 这里加载的配置如果被提前触发 那么在自动装配类设置的 控制台地址等参数 也无法生效
具体再来的调度逻辑在scheduleHeartbeatTask中方法 每5秒发送一次心跳
private void scheduleHeartbeatTask(/*@NonNull*/
final HeartbeatSender sender,
/*@Valid*/ long interval) {
pool.scheduleAtFixedRate(new Runnable() {
@Override
public 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 从上面可以看到 配置是来自于SentinelConfigLoader类的properties 属性中。在这属性初始化时,如果系统中没有设置控制台地址等配置,那么后续也是无法获取到的。
看到这里就大概明白了,正常的情况,在自动配置类SentinelAutoConfiguration的init方法中
设置控制台地址等配置 然后开启饥饿加载情况下 初始化所有的 InitFunc 实现类 其中的心跳发送 也会在这里初始化,然后在进行后续流程。最终是加载 SentinelConfigLoader类的properties
因为我们在SentinelAutoConfiguration的init 那么SentinelConfigLoader 的properties属性中也会存在 控制台地址等配置。
到这里 我们就怀疑SentinelConfigLoader的properties 被提前加载了 我们在断点观察
可以看到 当自动配置类SentinelAutoConfiguration的init方法还没执行到时,发现我们自己实现的sentinelConfig 配置中 引起了 源码中·sentinelConfig的构造方法已经被调用 同时 这个调用又会触发SentinelConfigLoader 的静态构造调用 当我们定义的sentinelConfig 类执行完成后 才去执行的 SentinelAutoConfiguration的init方法
所以说,后续的心跳发送执行器,无法获取到控制台地址等配置, 就导致应用无法注册到dashboard上
至于为什么导致这个加载顺序问题 已经向官方提交了 issue 寻求帮助
https://github.com/alibaba/Sentinel/issues/3201
https://github.com/alibaba/spring-cloud-alibaba/issues/3425
四。解决办法
- 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
流程都一致 就是注册顺序上 有点不同
先初始化SentinelAutoConfiguration 的init
然后在初始化我们自己定义的sentinelConfig
这种就可以注册到dashboard上