网络层容错:TCP连接池与心跳检测、自动重连机制、多线路冗余
网络层容错,说白了就是保证你的交易指令能发出去,行情数据能收回来。我见过太多系统因为网络抖动直接崩掉,尤其是做多品种交易时,几十个交易所同时连,任何一个掉线都可能引发连锁反应。
这一章我们重点聊三个东西:TCP连接池、心跳检测与自动重连、多线路冗余。这三者配合好了,你的系统网络层才算真正硬起来。
TCP连接池:别让每次交易都重新握手
很多新手喜欢每次发单前新建一个TCP连接,发完就关。你想想看,一次TCP三次握手至少一个RTT(往返时延),在毫秒级的交易场景里,这简直是灾难。
我个人习惯是用连接池来管理。说白了就是预先创建一批连接,放在池子里,用的时候拿一条,用完归还。这样避免了频繁创建销毁的开销。
核心要点:连接池大小要动态调整。太小了排队,太大了浪费资源。
我在项目中遇到过一个问题:某个交易所的网关只允许每个IP同时建立10条连接。我们一开始设了20条,结果一半连不上。后来改成根据交易所限制动态配置,才稳定下来。
下面是一个简单的连接池实现思路:
class TcpConnectionPool:
def __init__(self, host, port, min_size=5, max_size=20):
self.host = host
self.port = port
self.min_size = min_size
self.max_size = max_size
self._pool = []
self._init_pool()
def _init_pool(self):
for _ in range(self.min_size):
conn = self._create_connection()
self._pool.append(conn)
def get_connection(self):
# 从池中取连接,如果池空且未达上限则新建
if self._pool:
return self._pool.pop()
if len(self._pool) < self.max_size:
return self._create_connection()
# 等待或超时处理
return self._wait_for_connection()
def return_connection(self, conn):
# 检查连接是否健康,健康则归还
if self._is_healthy(conn):
self._pool.append(conn)
else:
conn.close()
self._pool.append(self._create_connection())
避坑指南:我曾经犯过一个错——连接池里的连接长时间不用会超时断开。后来我加了个定时心跳,每隔30秒对空闲连接做一次保活检测。
心跳检测:别等断开了才发现
TCP本身有Keep-Alive机制,但默认时间太长(Linux默认7200秒)。在交易系统里,等两个小时才发现连接断了?黄花菜都凉了。
我建议自己做应用层心跳。每1-2秒发一个心跳包,如果连续3次没收到回复,就判定连接断开。
为什么是3次?因为网络偶尔会有瞬断,一次丢包不代表连接真断了。3次是个比较合理的阈值,既不会误判,也不会反应太慢。
class HeartbeatMonitor:
def __init__(self, interval=1.5, timeout_count=3):
self.interval = interval # 心跳间隔(秒)
self.timeout_count = timeout_count # 超时次数阈值
self._missed = 0
self._running = False
def start(self, connection):
self._running = True
while self._running:
if not self._send_heartbeat(connection):
self._missed += 1
if self._missed >= self.timeout_count:
# 触发重连
self._on_timeout(connection)
break
else:
self._missed = 0
time.sleep(self.interval)
def _send_heartbeat(self, conn):
try:
conn.send(b'ping')
return True
except:
return False
注意:心跳包不能太大,否则占用带宽。我一般用4字节的固定标识,比如0x00FF。另外,心跳和业务数据要共用同一个连接,别单独开一条心跳连接——那样检测不出业务连接的问题。
自动重连机制:断了要能自己爬起来
自动重连不是简单的「断了就重连」。你想想看,如果交易所网关正在重启,你每秒重连一次,不仅连不上,还会加重对方负载。
我常用的策略是指数退避:第一次重连等1秒,第二次等2秒,第三次等4秒,以此类推,直到一个上限(比如60秒)。这样既不会疯狂重连,也不会等太久。
另外,重连时要重置连接池。我见过一个系统,重连后还拿着旧连接池里的连接去发数据,结果全部超时。
class AutoReconnect:
def __init__(self, max_retry=10, base_delay=1, max_delay=60):
self.max_retry = max_retry
self.base_delay = base_delay
self.max_delay = max_delay
self._retry_count = 0
def reconnect(self, pool):
while self._retry_count < self.max_retry:
delay = min(self.base_delay * (2 ** self._retry_count), self.max_delay)
time.sleep(delay)
try:
pool.reset() # 重置连接池
pool.connect()
self._retry_count = 0
return True
except:
self._retry_count += 1
return False
个人经验:重连成功后,不要立即恢复全量交易。先发一个查询类请求,确认连接状态正常,再逐步恢复。我曾经因为重连后直接发单,结果连接虽然恢复了但会话已过期,导致订单状态丢失。
多线路冗余:电信+联通+移动
单线路最大的问题是什么?运营商故障。我记得有一次电信骨干网出问题,整个华东地区的交易都受影响。如果你只有电信线路,那就只能干瞪眼。
我建议至少接入三条线路:电信、联通、移动。每条线路独立接入,互不依赖。
具体怎么做?
- 主备模式:默认走电信,电信断了切联通,联通断了切移动。切换时间控制在200ms以内。
- 负载均衡:三条线路同时用,按权重分配流量。比如电信50%,联通30%,移动20%。
- 智能路由:根据目标交易所的IP归属,自动选择最优线路。比如连上交所的服务器,优先走电信。
下面是一个多线路切换的流程图:
实际部署时,每条线路要独立接入不同的物理设备。我见过有人把三条线路都接在同一台交换机上,结果交换机挂了,三条线路全废。嗯,这种低级错误要避免。
核心原则:物理隔离 + 逻辑独立 + 自动切换。三条线路从物理设备到网络出口都要分开,切换逻辑要自动化,不能靠人工。
综合配置示例
下面是一个完整的配置示例,把连接池、心跳、重连、多线路整合在一起:
{
"network": {
"connection_pool": {
"min_size": 5,
"max_size": 20,
"idle_timeout": 30,
"health_check_interval": 10
},
"heartbeat": {
"interval": 1.5,
"timeout_count": 3,
"packet_size": 4
},
"reconnect": {
"max_retry": 10,
"base_delay": 1,
"max_delay": 60,
"reset_pool_on_reconnect": true
},
"lines": [
{
"name": "电信",
"weight": 50,
"priority": 1,
"gateway": "192.168.1.1",
"interface": "eth0"
},
{
"name": "联通",
"weight": 30,
"priority": 2,
"gateway": "192.168.2.1",
"interface": "eth1"
},
{
"name": "移动",
"weight": 20,
"priority": 3,
"gateway": "192.168.3.1",
"interface": "eth2"
}
],
"switch_policy": "auto_failover",
"switch_threshold_ms": 200
}
}
最后说一句:网络层容错不是一劳永逸的。我每个月都会做一次故障演练,手动拔掉一条线路的网线,看看系统能不能自动切换。你想想看,如果真出问题了才发现切换逻辑有bug,那损失就大了。
公众号:蓝海资料掘金营,微信deep3321