现在我的服务器使用apache转发用户的请求。然而,最近在转发websocket时,客户端经常出现pending,即一直无法连接成功(握手成功,但是响应一直无法下来)。看了一下请求要转发到的目标服务器日志,发现请求连接没有过来,也就是apache没有转发。但是pending是握手成功后状态,不可能跟apache连接失败。在服务器抓包和监控网络连接,发现请求能够正常到达apache(也就是握手成功),但是apache就是无法把请求转发给目标服务器(无法创建连接跟目标服务器通信)!
这时检查了目标服务器的连接状态,发现大量close_wait连接。而这些连接都是apache发起的(apache转发请求的连接)。说到这里,大家需要对tcp状态机有清晰地认识,同时明白转发的机制,否则不知道这里描述的内容。由于篇幅有限,我就不补充这部分知识。
为了进一步验证问题,我直接建立了一个tcp服务器,检查收到的数据。首先,客户端发送请求到apache后,apache就会创建转发连接连接到目标服务器。这里说一点,apache只有在完全收到客户端数据后,才会创建转发连接,也就是,apache的转发是基于解析模式,而不是单纯的隧道模式,毕竟apache会添加系列自定义头部数据,不得不解析客户端数据。如果你要实验这个步骤,记得客户端要正确发送http数据。然后,客户端主动断开连接,跟apache建立的连接立即被回收,然而转发连接依然处于established!当目标服务器断开连接后,转发连接变成close_wait,之后就一直这样!经过以上验证,问题已经非常明显:apache的转发连接无法释放!
由于转发连接是apache转发模块的功能,而无法释放的问题,明显是逻辑异常(真有人经常写出不释放连接的代码),很难通过配置改变(跟time_wait不是一个类型问题)。因为我也写过tunnel,所以明白维护这两个连接的问题。我在设计连接释放时,特别对两个连接同时关闭做了特殊处理,否则也会出现这个问题。那如何解决呢?
可能我的apache版本比较低(2.4.6),所以存在这个问题。大家要是遇到这个问题,可以升级看看,或者换其它转发服务器,例如ngnix。由于我的服务器需求较小,所以我自己选择了以下折中的处理方案。
一方面,每天午夜清理全部apache连接任务(除了监听任务),来彻底释放全部请求。apache启动后默认缓存一定数量的连接任务。随之请求增加,任务数不断增加直到最大值。所以,手动释放这些任务不会影响apache功能(当然还在通信的连接被迫断开,但是这种网络不稳定比手机省电优化还要友好吧,至少可以重连),这些任务会在清零后重新缓存。另一方面,在核心目标服务器增加清理close_wait的任务。如果你的服务器请求比较多,建议换成定时清理close_wait任务。
这里补充一个问题,目前有的目标服务器存在连接释放问题,这个是目标服务器问题,需要目标服务器修复。如果这个问题没有修复,会导致转发连接一直处于established。特别是我这次使用的websocket组件,看了它的源码后,发现它除了被动结束,即收到close帧才会关闭连接,否则会一直挂着!
另外,我在测试时发现apache一个奇葩的功能。由于我每次客户端中断,都会导致转发连接堆积。在目标服务器没有释放这些连接时,它们的状态都会是established。如果转发连接数量达到最大值,客户端连接apache后,apache无法再创建转发连接,但是它的数据直接给了最后的转发连接。这个问题是我自己写的tcp服务器才能看到,不然还不知道有这种骚操作。虽然从http长连接规范来说,这是合理的,毕竟每条长连接都是可以复用的,只是服务器如何分发转发请求而已。要是这个规范跟目标服务器没有释放碰到一起,就会出现有的客户端不断发送请求没问题,但是有的客户端一直无法建立连接!这个就是抢占请求的现象,即最初发起请求的客户端一直可以连接,新的客户端就无法连接。注意,这时客户端也是支持http长连接的,否则它也是无法建立(不过请求数据还是会到达转发连接)。这个想象在过去还真的出现过,那时因为没有去分析数据分发过程,只是处理close_wait问题,结果忘了这种奇葩的现象。