Bitmart下单失败,DNS与HTTP KeepAlive问题研究

自从上次写完读后感已经过去半年,很快2023就要结束了。说好的重启写作计划结果其实今年总共写了不到5篇内容,中间还跨了大半年。 根本原因还是手头的币圈交易项目发展的好,毕竟能赚钱的东西谁会有那么大动力写什么分享呢。这次想到要更新博客完全是因为cloudflare通知我这个域名要过期了,所以趁这次把这个单独写博客的域名迁移到我另一个独立域名的时机,也顺便记录一次排查技术问题的过程。

情况分析

目前我的交易系统对接了Bitmart的现货盘,在运作的过程中发现有一些时间下单会无法成功,于是开始了这次的调查。 Bitmart下单接口返回由Cloudflare相应的错误:504 Gateway Time-out

最开始的判断

遇事不决,重启试试,好了。于是猜测是DNS有更新,而我的下单程序使用了旧的IP,服务已经不存在所以相应504 gateway timeout。 也在Bitmart的API群询问了support,对方回答是系统刚好在升级,这也强化了我的印象毕竟现在都是云服务了,后台服务资源动态分配,ip有变更很正常。

优化DNS查询

本身我是通过自建的bind9做本地DNS缓存优化查询速度,既然是DNS缓存的问题那定期做个清理应该可以解决。同时还研究了一番如何能够在出现问题的时候强制刷新bind9的dns,未果。 下一次仍然遇到相同的问题,于是猜测是程序本身缓存了dns,最终使用了非常暴力的方法,用crontab定时重启程序解决:

55 * * * * sudo docker restart bitmart_order_server

但是问题本身仍然有一些奇怪的地方,我有两台服务器,其中一台用作下单部署在东京实现与交易所的快速连接,另一台在洛杉矶利用更好的CPU资源做价格计算、订单下载和pnl统计。理论上两台服务器都需要访问Bitmart的API,却只有其中一台每次对方一升级就遇到504。百思不得其解。

更深入的调查

终于在下一次遇到相同问题的时候,我专门在重启前和重启后分别通过bind9查询了Bitmart API的地址,猛然发现其实IP并没有变更,一直都是cloudflare提供的两个接入点。

api-cloud.bitmart.com.  297 IN  A 104.18.16.176
api-cloud.bitmart.com.  297 IN  A 104.18.17.176

重新仔细研究了这个问题,发现两个最重要的差异点:

  1. DNS没有变化,对应的ip理论上应该可以提供服务;
  2. 重启程序可以解决,说明重建连接即可修复;反推:未重启时有问题是因为连接被复用。

进一步推断问题应该是cloudflare端在API后台更新部署调整内网ip之后,原有的连接由于某种策略,仍然保持请求到已经停止的服务上。

go的连接复用逻辑

// By default, Transport caches connections for future re-use. 64 这是go transport 里的介绍,go的transport自动实现了Keep-Alive和HTTP1.1的连接复用,在上一个请求的Body正确读取并Close之后,连接会被保持并用于下一次的请求。 根据default transport的初始化参数可以看到:

var DefaultTransport RoundTripper = &Transport{
44 	Proxy: ProxyFromEnvironment,
45 	DialContext: defaultTransportDialContext(&net.Dialer{
46 		Timeout:   30 * time.Second,
47 		KeepAlive: 30 * time.Second,
48 	}),
49 	ForceAttemptHTTP2:     true,
50 	MaxIdleConns:          100,
51 	IdleConnTimeout:       90 * time.Second,
52 	TLSHandshakeTimeout:   10 * time.Second,
53 	ExpectContinueTimeout: 1 * time.Second,
54 }

KeepAlive的时间是30s,也就是说,在30s内如果有另一个新的请求,就会复用原有的连接。

那么只有一台机器出问题的原因也找到了,下单机器出问题是因为隔几秒就会访问API下单,所以连接一直被复用;数据机没问题则是因为每隔一分钟以上才会访问历史订单列表,所以每次都会发起新的连接。

终极优化方法

既然如此那么我在收到Cloudflare响应5xx的情况下,主动断开并创建新的连接,应该就可以解决这个问题。 由于我用的是goresty库,那么对应的代码就可以实现:

		if httprsp.StatusCode() > 500 {
			log.Printf("[Bitmart] reset the connection since code:%d", httprsp.StatusCode())
			if exist_transport, err := bmt.RestyClient.Transport(); err == nil {
				bmt.RestyClient.SetTransport(exist_transport.Clone())
			}
		}