环境
- 管理端:soul-admin v2.2.1
- 客户端:soul-spring-boot-starter-client-springmvc v2.2.1
- 网关:v2.2.1 websocket连接
- 插件:divide
- 单机+多机
- 网段有两个
- springboot项目
问题叙述和处理
- 在开发中的某一天发现有时候项目注册不到soul上去,检查发现数据库连接问题于是换了个数据库
- 数据库发现问题有二
- 放数据库的机子ip访问慢
- 数据库连接满了

- 数据库发现问题有二
更改了数据库时顺便更换了部署 soul-admin的服务器,于是新的问题出现了
由于是双ip段,导致soul-admin轮询是ping不同注册上去的ip,源码追踪发现的问题和解决方法
- 轮询 UpstreamCheckService 155行 check方法 检查服务健康状况
UpstreamCheckUtils 类 68行使用socket做的ip端口的有效验证
- 发现检查时使用的 host = 注册上去的ip
socket.connect(new InetSocketAddress(host, port));
- 发现检查时使用的 host = 注册上去的ip
同时发现注册的方法入参(org.dromara.soul.client.springmvc.config.SoulSpringMvcConfig)中有 host
- 方法一:于是注册时强制指定了 soul-admin 服务器能ping通的ip
- 方法二:linux配置两个不同网段的ip
- 上述问题处理完之后遇到了大麻烦,我也不清楚是配置问题还是其他,总之三个项目就其中一个老是出现这个问题,描述如下socket 连接超时造成了异常下线
socket 连接超时造成了异常下线
- 在org.dromara.soul.admin.service.impl.UpstreamCheckService中的轮询
final boolean pass = UpstreamCheckUtils.checkUrl(divideUpstream.getUpstreamUrl());上下输出ping服务的开始结束时间,发现了问题- 当15:51:19进行轮询时,15:51:20时项目进行了注册,同时在15:51:19 socket 验证时发生了延迟,导致15:51:21后才验证完,此时服务已经注册,这边才验证玩,并且验证结果为false,从而进行了下线操作
- 15:51:19 项目还没启动socket时访问不到的

- 当15:51:19进行轮询时,15:51:20时项目进行了注册,同时在15:51:19 socket 验证时发生了延迟,导致15:51:21后才验证完,此时服务已经注册,这边才验证玩,并且验证结果为false,从而进行了下线操作
尝试处理
在
org.dromara.soul.common.utils.checkUrl方法中加入了一个重连- 导致加剧了这种情况 - 废弃
```java
/**
- Check url boolean. *
- @param url the url
- @return the boolean */ public static boolean checkUrl(final String url) { if (!checkUrl2(url)) { return checkUrl2(url); } return true;
}
- 导致加剧了这种情况 - 废弃
```java
/**
/**
* Check url boolean.** @param url the url* @return the boolean*/private static boolean checkUrl2(final String url) {if (StringUtils.isBlank(url)) {return false;}if (checkIP(url)) {String[] hostPort;if (url.startsWith(HTTP)) {final String[] http = StringUtils.split(url, "\\/\\/");hostPort = StringUtils.split(http[1], Constants.COLONS);} else {hostPort = StringUtils.split(url, Constants.COLONS);}return isHostConnector(hostPort[0], Integer.parseInt(hostPort[1]));} else {return isHostReachable(url);}}
- 发现 socket连接没有设置超时- 测试发现连接异常时默认2050毫秒左右才抛出异常断(测试2次)- 正常连接时在0~11毫秒之间- 手动设置100~500毫秒试试(测试3次)- 异常抛出稳定在了100~毫秒```javaprivate static boolean isHostConnector(final String host, final int port) {Long startTime = System.currentTimeMillis();try (Socket socket = new Socket()) {//socket.connect(new InetSocketAddress(host, port));socket.connect(new InetSocketAddress(host, port),100);} catch (IOException e) {Long endTime = System.currentTimeMillis();System.out.println("sockt连接时间"+(endTime-startTime)+"毫秒");return false;}Long endTime = System.currentTimeMillis();System.out.println("sockt连接时间"+(endTime-startTime)+"毫秒");return true;}/*----------------------- 默认超时测试 -----------------------*/ping项目开始:2021-02-19 10:32:10sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:32:10ping项目开始:2021-02-19 10:32:20sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:32:20ping项目开始:2021-02-19 10:32:30sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:32:30ping项目开始:2021-02-19 10:32:40sockt连接时间2042毫秒ping项目结束 -> false:2021-02-19 10:32:422021-02-19 10:32:42.895 ERROR 25448 --- [upstream-task-1] o.d.s.a.s.impl.UpstreamCheckService : check the url=192.168.0.51:9001 is failping项目开始:2021-02-19 10:33:13sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:33:13ping项目开始:2021-02-19 10:33:23sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:33:23ping项目开始:2021-02-19 10:33:33sockt连接时间11毫秒ping项目结束 -> true:2021-02-19 10:33:33ping项目开始:2021-02-19 10:33:43sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:33:43ping项目开始:2021-02-19 10:33:53sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:33:53ping项目开始:2021-02-19 10:34:03sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:34:03ping项目开始:2021-02-19 10:34:13sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:34:13ping项目开始:2021-02-19 10:34:23sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:34:23ping项目开始:2021-02-19 10:34:33sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:34:33ping项目开始:2021-02-19 10:34:43sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:34:43ping项目开始:2021-02-19 10:34:53sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:34:53ping项目开始:2021-02-19 10:35:03sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:35:03ping项目开始:2021-02-19 10:35:13sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:35:13ping项目开始:2021-02-19 10:35:23sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:35:23ping项目开始:2021-02-19 10:35:33sockt连接时间2毫秒ping项目结束 -> true:2021-02-19 10:35:33ping项目开始:2021-02-19 10:35:43sockt连接时间2039毫秒ping项目结束 -> false:2021-02-19 10:35:452021-02-19 10:35:45.334 ERROR 25448 --- [upstream-task-2] o.d.s.a.s.impl.UpstreamCheckService : check the url=192.168.0.51:9001 is fail/*----------------------- 设置100~500毫秒超时测试 -----------------------*/ping项目开始:2021-02-19 10:46:29sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:46:29ping项目开始:2021-02-19 10:46:39sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:46:39ping项目开始:2021-02-19 10:46:49sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:46:49ping项目开始:2021-02-19 10:46:59sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:46:59ping项目开始:2021-02-19 10:47:09sockt连接时间111毫秒ping项目结束 -> false:2021-02-19 10:47:092021-02-19 10:47:09.993 ERROR 12728 --- [upstream-task-3] o.d.s.a.s.impl.UpstreamCheckService : check the url=192.168.0.51:9001 is fail2021-02-19 10:47:15.002 WARN 12728 --- [upstream-task-3] com.zaxxer.hikari.pool.PoolBase : HikariPool-1 - Failed to validate connection com.mysql.jdbc.JDBC4Connection@6546761b (No operations allowed after connection closed.). Possibly consider using a shorter maxLifetime value.ping项目开始:2021-02-19 10:47:25sockt连接时间112毫秒ping项目结束 -> false:2021-02-19 10:47:252021-02-19 10:47:25.431 ERROR 12728 --- [upstream-task-3] o.d.s.a.s.impl.UpstreamCheckService : check the url=192.168.0.51:9001 is failping项目开始:2021-02-19 10:47:55sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:47:55ping项目开始:2021-02-19 10:48:05sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:48:05ping项目开始:2021-02-19 10:48:15sockt连接时间112毫秒ping项目结束 -> false:2021-02-19 10:48:152021-02-19 10:48:15.819 ERROR 12728 --- [upstream-task-3] o.d.s.a.s.impl.UpstreamCheckService : check the url=192.168.0.51:9001 is failping项目开始:2021-02-19 10:48:36sockt连接时间1毫秒ping项目结束 -> true:2021-02-19 10:48:36ping项目开始:2021-02-19 10:48:46sockt连接时间0毫秒ping项目结束 -> true:2021-02-19 10:48:46ping项目开始:2021-02-19 10:48:56sockt连接时间112毫秒ping项目结束 -> false:2021-02-19 10:48:562021-02-19 10:48:56.180 ERROR 12728 --- [upstream-task-2] o.d.s.a.s.impl.UpstreamCheckService : check the url=192.168.0.51:9001 is fail
- 同时测试上述 **轮询** 出现的异常下线问题 (测试方法疯狂重启)- 问题并没有改善,但是发现注册成功后一秒,轮询发生了检查失败,猜想可能是项目完成启动
2021-02-19 10:56:57 |INFO |org.dromara.soul.client.common.utils.RegisterUtils:48ping项目开始:2021-02-19 10:56:58sockt连接时间112毫秒ping项目结束 -> false:2021-02-19 10:56:58
- 启动时自动注册的实现方法为
org.dromara.soul.client.springmvc.init.ContextRegisterListener- 调用注册接口 -> 保存数据 -> 发布事件 -> 触发DataChangedEventDispatcher -> 将注册信息写进缓存 && 发送webSocket消息
- 注册后发布事件
- `org.dromara.soul.admin.service.impl.handlerSpringMvcSelector`
测试注册流程
测试数据
数据说明
- 注册时间 = org.dromara.soul.client.common.utils.RegisterUtils:48 打印
- 轮询时间 = final boolean pass = UpstreamCheckUtils.checkUrl(divideUpstream.getUpstreamUrl());之前的打印
- 项目启动时间 = org.apache.coyote.http11.Http11NioProtocol:173 打印 Starting ProtocolHandler [“http-nio-9001”]
- socket监控项目启动成功时间 = demo项目监控,用作跟项目启动时间的对比和 | 序号 | 注册时间 | 发起轮询时间 | 项目启动成功时间 | socket监控项目启动成功时间 | 是否注册成功 | | —- | —- | —- | —- | —- | —- | | 1 | 2021-02-20 13:06:43 | 2021-02-20 13:06:51 | 2021-02-20 13:06:46 | 2021-02-20 13:06:46.038 | true | | 2 | 2021-02-20 13:28:17 | 2021-02-20 13:28:23 | 2021-02-20 13:28:20 | 2021-02-20 13:28:20.530 | true | | 3 | 2021-02-20 13:32:43 | 2021-02-20 13:32:54 | 2021-02-20 13:32:46 | 2021-02-20 13:32:46.489 | true | | 4 | 2021-02-20 13:34:21 | 2021-02-20 13:34:24 | 2021-02-20 13:34:24 | 遗漏 | false | | 5 | 2021-02-20 13:35:40 | 2021-02-20 13:35:44 | 2021-02-20 13:35:43 | 2021-02-20 13:35:43.936 | false | | 6 | 2021-02-20 13:37:59 | 2021-02-20 13:38:04 | 2021-02-20 13:38:02 | 2021-02-20 13:38:02.802 | true | | 7 | 2021-02-20 13:41:41 | 2021-02-20 13:41:45 | 2021-02-20 13:41:44 | 2021-02-20 13:41:44.301 | false | | 8 | 2021-02-20 13:43:29 | 2021-02-20 13:43:32 | 2021-02-20 13:43:32 | 2021-02-20 13:43:32.526 | true | | 9 | 2021-02-20 13:44:59 | 2021-02-20 13:45:05 | 2021-02-20 13:45:02 | 2021-02-20 13:45:02.745 | true | | 10 | 2021-02-20 13:46:35 | 2021-02-20 13:46:32 | 2021-02-20 13:46:35 | 2021-02-20 13:46:35.623 | false |
数据说话
- 成功率:6/10 , 失败率:4/10
- 根据轮询时间跟启动时间得出异常下线的原因如下
- 轮询时间刚刚好在启动成功0-1秒 (由于没有毫秒对比所以是近似值且小于1s)
- 轮询的时间早于启动成功的时间 | 发起轮询时间 | 项目启动成功时间 | 相差时间/s | 是否注册成功 | | —- | —- | —- | —- | | 13:06:51 | 13:06:46 | 5.00 | true | | 13:28:23 | 13:28:20 | 3.00 | true | | 13:32:54 | 13:32:46 | 8.00 | true | | 13:34:24 | 13:34:24 | 0.00 | false | | 13:35:44 | 13:35:43 | 1.00 | false | | 13:38:04 | 13:38:02 | 2.00 | true | | 13:41:45 | 13:41:44 | 1.00 | false | | 13:43:32 | 13:43:32 | 0.00 | true | | 13:45:05 | 13:45:02 | 3.00 | true | | 13:46:32 | 13:46:35 | -3.00 | false |
猜想
可能是由于
ContextRegisterListener注册时跟轮询重合了,或者轮询出现延迟导致下线通知在注册之后才发送,导致被异常下线尝试修改
尝试注册后延迟写进缓存,既
// publish change event.Thread.sleep(2000);upstreamCheckService.submit(contextPath, addDivideUpstream);
- 据测试 submit 方法不是异步执行,所以问题并未得到解决
- debug看看 ContextRegisterListener
- Spring 中的事件监听的实现
- ContextClosedEvent(容器关闭时)
- ContextRefreshedEvent(容器刷新时)
- ContextStartedEvent(容器启动时)
- ContextStoppedEvent(容器停止时)
- 将ContextRegisterListener 的实现从 ApplicationListener 改成了 ApplicationRunner
- 每次注册都在启动彻底完成之后了,好像暂时解决上述的异常下线(测试10次)
- Spring 中的事件监听的实现
- debug看看 ContextRegisterListener
最终解决
- 将ContextRegisterListener 的实现从 ApplicationListener 改成了 ApplicationRunner 保证注册每次都在项目启动后执行
- 每次注册都在启动彻底完成之后了,此后无异常。
- 参考:https://jishuin.proginn.com/p/763bfbd23539
关于PR
- https://github.com/dromara/soul/pull/1114
- 作者回复:no, we also support springmvc, not only springboot
- 也是这个是要考虑全局影响
- 如果有小伙伴遇到了一样的问题。
- 根据上面的方法修改后install
