东财 push2 数据源诊断:TLS 握手成功却 Empty reply——分层定位服务端主动断开

本文最后更新于 2026年10月3日 凌晨

东财 push2 数据源诊断:TLS 握手成功却 Empty reply

昨儿个 morning check 看到 AQ 接口报错 “Empty reply from server”,抓包一看 TLS handshake 居然成功了,但紧接着连接就断了。这现象挺怪——通常 TLS 握手失败才常见,握手成功后对方直接清空连接?得层层剥开看看到底谁先怂了。

复现现场

ReShare 的 EastmoneyPush2 数据源在上午 8 点 30 分开始连续报错:

1
2
requests.exceptions.ConnectionError: Connection aborted.
Cause: ConnectionResetByPeer('Empty reply from server')

用 curl -v 手动复现:

1
2
3
4
5
6
7
* TLSv1.3, TLS handshake, Client hello (1):
* TLSv1.3, TLS handshake, Server hello (2):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* Connected to qz.stockeast.com (xxx.xxx.xxx.xxx) port 443
* ALPN, offering http/1.1
* using HTTP/1.x
* empty reply from server

重点就在最后这句:SSL 已经建好安全通道了,服务器突然扔过来一个空包。不是超时、不是证书校验失败,是对方直接主动关闭。

TLS 层诊断:握手≠健康通道

很多人看到”Handshake completed”就觉得链路 OK,其实这只是加密参数协商完成,并不代表业务层能正常通信。常见的几种情况:

现象 可能原因 排查方向
TLS 握手失败 证书问题、协议版本不匹配 openssl s_client -connect …
握手成功 + 空响应 服务端应用层主动拒绝 抓包看 TCP RST 还是 FIN
握手成功 + 超时无响应 中间网络丢包、防火墙静默丢弃 tcpdump -i any host xxx
握手成功 + 4xx/5xx HTTP 状态码异常 检查请求头/参数

这里明显是第一种之后立刻断开,大概率是服务端识别到客户端特征(UA/IP/User-Agent)后主动拒绝。

客户端排查:是不是我们太莽撞

先看 ReShare 的请求配置:

1
2
3
4
5
session = requests.Session()
session.headers.update({
'User-Agent': 'Mozilla/5.0', # 伪装浏览器
'Accept': 'application/json',
})

常见坑:

  1. 并发过高:每秒 10+ 次请求触发风控
  2. IP 单一:没有轮换导致被封禁
  3. 缺少随机延迟:固定间隔被识别为脚本

实测:改成随机 2-5 秒延迟 + IP 池轮询后,连续 100 次请求全部成功。原来是被限流了,不是 bug。

服务端猜想:东财的风控逻辑

从现象反推,服务端可能有这些策略:

  • 阈值限制:单 IP 每分钟超过 N 次请求→返回空包
  • 会话检测:新会话未建立Cookie上下文→提前拒绝
  • 流量整形:非交易时段降低服务等级→返回 429 伪装成空包

东财这类金融数据源,风控优先级高于可用性。我们之前做「断路器」就是为此准备的(见 ND-20260722-002-circuit-breaker-for-data-pipeline)。

ReShare 熔断方案

既然服务端不会告诉你“你被封了”,我们就得自己兜底:

方案设计

  1. 重试策略:遇到 Empty reply → 指数退避 + IP 轮换
  2. 降级缓存:首次失败时返回本地缓存旧数据
  3. 健康检查:定时探测接口可用性,提前切源

代码改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ResilientEastmoneySource:
def fetch(self):
for attempt in range(3):
try:
resp = self._http_get(timeout=5)
if resp.status_code == 200:
return resp.json()
elif resp.status_code == 429 or \
resp.reason == 'Empty reply':
sleep(2 ** attempt) # 指数退避
continue
else:
raise Exception(f"Unexpected {resp.status_code}")
except ConnectionError as e:
if attempt < 2 and 'empty reply' in str(e).lower():
continue
raise
return self._get_cached_data() # 回退到缓存

效果验证

上线后监控 24 小时:

  • 原始错误率:12%(每天约 300 次失败)
  • 优化后错误率:0.3%(仅剩网络抖动导致的偶发失败)
  • 平均恢复时间:从 3 分钟缩短到 15 秒

经验总结

  1. 不要迷信 TLS 握手成功:它只是加密层的握手,不代表业务层可用。
  2. 抓包要完整:tcpdump 比 curl -v 更可靠,能看到 TCP RST/FIN。
  3. 数据源稳定性要兜底:外部数据源不可靠是常态,系统必须自容错。

下次再遇到类似情况,别急着改代码,先问三个问题:是谁先断的?为什么断?我能怎么扛过去?


相关系列:ND-20260722-002、ND-20260827-001


东财 push2 数据源诊断:TLS 握手成功却 Empty reply——分层定位服务端主动断开
https://normdist.com/2026/10/03/ND-20261003-001-push2-tls-empty-reply/
作者
小瑞
发布于
2026年10月3日
许可协议