一、现象截图
一大早收到ELK的邮件提醒,让我来看看,又是哪个妖怪在作孽?
二、问题定位
2.1 SocketTimeoutException:connect timed out
经验告诉我,这个问题一般是第三方平台的问题,大部分原因是发起Http请求,但是请求超时导致,很多HTTP framework(如本文中涉及的Hutool工具中的HttpUtil,底层是HttpURLConnection)本身有超时机制的,实现超时,就是在应用层代码里启动一个Timer,如果Timer超时,则手动取消请求。比如用户现在网络环境较差,当客户端发起一个请求时,通信层开始请求与服务器建立连接(包括在重试),如果在5S之内还没有连接到服务器,那么就会判定为超时。Http请求超时一般与我们的代码无关。
2.2 ICardServiceImpl的代码第122行出错了
啊?ICardServiceImpl是对接第三方平台,用于积分充值的功能,是组内大神写的代码,按理是不会出问题,而且已经上线了快1年了都,最近又没有变更,所以,第一直觉,故障的原因是因为外部变更导致的。
122行,那不就是这段代码吗?也就是下面这段方法中第三行中的代码逻辑
private void recharge2ICard(String workNo, Integer amount) {
String icardPoint = String.format("%d.%02d", amount / 100, amount % 100);
ICardRechargeReq req = new ICardRechargeReq(null, workNo, null, iCardRechargeKey, icardPoint);
String respStr = HttpUtil.post(iCardRechargeUrl, JSONUtil.toJsonStr(req), 30 * 1000);
ICardRechargeResp resp = JSONUtil.toBean(respStr, ICardRechargeResp.class);
Assert.isTrue(resp.getHead().get(0).checkMac(iCardRechargeKey), "充值失败:一卡通返回信息校验失败!");
String retCode = resp.getHead().get(0).getRcode();
Assert.isTrue(Objects.equals(ICardReturnCode.OK.getCode(), retCode), "充值失败:" + ICardReturnCode.getMsg(retCode));
}
这段逻辑中会超时,而且超时时间设置的是,底层逻辑如下,即在30000毫秒(30 秒)无响应,将提示超时,上面的报错提示,就是这一段它在超时后提示的异常。
这段代码中存在以下问题
(1)错误提示不友好,超时了没有给反馈结果给前端。用户体验不好
(2)错误日志Assert.isTrue的方式可读性不高,而且容易逻辑判断出错
(3)没有考虑到返回不是json的情况,下面这个错误也是频发的。
三、问题深入分析
经过咨询运维同事还有网络同事之后,他们用了一堆很专业的网络命令,比如ping、tracepath命令在我们的服务器层面去排查,发现确实有问题,最后发现了是线路欠费断了导致???什么?没有监控的吗?这种欠费的低级错误也会犯??算了,外部原因不深究了,那就来一波网络排查指令语法知识干货科普吧~
3.1 tracepath指令
指令作用:
追踪数据到达目标主机的路由信息,同时还能够发现MTU值。
它跟踪路径到目的地,沿着这条路径发现MTU。它使用UDP端口或一些随机端口。
它类似于Traceroute命令,只是不需要超级用户特权,并且没有花哨的选项。
它用于检测网络延迟,但是,它不需要 root 权限,并且默认安装在 Ubuntu 中。
它跟踪到指定目的地的路由并识别其中的每一跳。
如果您的网络较弱,它会识别出网络较弱的点。
指令适用范围:
RedHat、RHEL、Ubuntu、CentOS、SUSE、openSUSE、Fedora。
指令语法:
tracepath <destination>
tracepath [ -n] [ -l pktlen] destination [ port]
指令选项列表:
选项 | 说明 |
---|---|
-p | 后面跟设置要使用的初始目标端口 |
-n | 不查看主机名字,以数字形式只显示 IP 地址。 |
-4 | 仅使用IPv4 |
-6 | 仅使用IPv6 |
-b | 同时显示 IP 地址和主机名 |
-l | 设置初始化的数据包长度,IPv4 默认为 65535,IPv6 默认为 128000 |
-m | 后面跟<max_hops> 设置最大 TTL 值,默认为 30 |
-V | 打印版本并退出 |
输出解答
核心:后面看不到地址,是超时,就代表断了
1? | [LOCALHOST] | pmtu 1500 |
---|---|---|
第一列 | 第二列 | 行的其余部分 |
显示探针的TTL,后面是冒号。通常TTL的值是从网络中得到的,但有时回复并不包含必要的信息,我们不得不猜测它。在这种情况下,数字后面跟着?。 | 显示网络跳,对探测作出答复。如果探测未发送到网络,则为路由器地址或者[localhost]地址。 | 显示了有关到达相关工作跳的路径的各种信息。作为规则,它包含RTT的值。此外,它可以显示路径MTU,当它改变。如果路径是不对称的,或者探测在到达指定跳之前完成,则显示前向和后向跳数之间的差异。这一信息不可靠。F.E.第三行显示1的不对称性,这是因为第一次TTL为2的探针在第一跳时由于路径MTU发现而被拒绝。 |
最后一行总结了到达目的地的所有路径的信息,显示了检测到的路径MTU、到达目的地的跳数以及我们对从目的地到我们的跳数的猜测,这在路径不对称时可能有所不同。
四、优化与重构现有代码逻辑
既然发现了问题,就应该着手修改问题了,刚好趁着这个契机,重构这一段代码,然后让测试同学重新测试下,嘿嘿。告别不好的用户体验,以及再见了无用的告警!
4.1 核心逻辑1:重构充值到一卡通的逻辑
private Pair<Boolean, String> recharge2ICard(String workNo, Integer amount) {
Pair<Boolean, String> handleResult;
String iCardPoint = String.format("%d.%02d", amount / 100, amount % 100);
ICardRechargeReq req = new ICardRechargeReq(null, workNo, null, iCardRechargeKey, iCardPoint);
String respStr;
try {
respStr = HttpUtil.post(iCardRechargeUrl, JSONUtil.toJsonStr(req), 30 * 1000);
} catch (Exception e) {
log.error("充值失败! 无法链接一卡通提供的URL = 【{}】,请检查网络配置!", iCardRechargeUrl);
handleResult = new Pair<>(Boolean.FALSE, "充值失败! 无法链接一卡通提供的URL!请检查网络配置!");
return handleResult;
}
log.info("充值信息:user={},amount={},respStr={}", workNo, amount, respStr);
if (StrUtil.isBlank(respStr)) {
log.error("充值失败! respStr是空的 respStr= 【{}】,可能原因是网络阻塞导致!", respStr);
handleResult = new Pair<>(Boolean.FALSE, respStr);
return handleResult;
}
ICardRechargeResp resp = JSONUtil.toBean(respStr, ICardRechargeResp.class);
if (!resp.getHead().get(0).checkMac(iCardRechargeKey)) {
String err = "充值失败:一卡通返回信息校验失败!";
handleResult = new Pair<>(Boolean.FALSE, err);
return handleResult;
}
String retCode = resp.getHead().get(0).getRcode();
if (!ObjectUtil.equals(ICardReturnCode.OK.getCode(), retCode)) {
String err = "充值失败:" + ICardReturnCode.getMsg(retCode);
handleResult = new Pair<>(Boolean.FALSE, err);
return handleResult;
}
return new Pair<>(Boolean.TRUE, null);
}
4.2 核心逻辑2:服务调用逻辑
Pair<Boolean, String> handleResult = recharge2ICard(userInfo.getWorkNo(), body.getAmount());
if (Boolean.FALSE.equals(handleResult.getKey())) {
return R.failed("充值到一卡通失败,请联系客服处理!");
}