前一段时间,我们在迁移Unicorn服务到Docker集群中。并且在前端使用ELB作为反向代理。这样利用ELB高可用的特点,实现Docker集群任何一个节点挂掉时,即时屏蔽该节点的同时不会影响到服务的整体可用性。当然也可以采用DNS中设置多IP地址的A记录的方式,不过DNS的更新生效时间受终端缓存影响较大,比较不可控。当然,为什么采用ELB并不是我讨论的重点,而采用ELB直接和Unicorn通信的问题才是本文要说的问题。

当我们上线集群和ELB运行一段时间后发现,ELB经常会检测到后端OutOfService,导致终端无法访问服务。在ELB日志里体现出来就是众多的5xx错误。而进入到节点中直接访问对应端口的Unicorn服务,则会很长时间才返回(并不会超时或者错误)。

经过一大圈的排查,最后问题锁定到Unicorn和ELB的兼容性问题。最后还是Unicorn和ELB的官方文档给了我们最有力的证据。

在Unicorn的官方文档中有一段设计准则,说的是Unicorn如何符合Unix哲学,如何balabala。简单说就一句话:老子不负责高并发,只负责高并发业务。听起来很拗口是不。那我们具体来说说:

  1. Unicorn认为一个完整的高效率的WEB服务器,包含两部分工作:一是尽可能多的勾搭客户端实现高并发,一是尽可能快的处理单个客户端的业务实现高效率。
  2. 同时他觉得客户端都是矮穷挫,发请求收响应什么的网络IO操作太慢,配不上高大上的Unicorn。
  3. 所以应该找个傻逼,干着和一群群客户保持连接的脏活累活。把收集客户端的请求(HTTP Request),火速的通过内网或者呈送给Unicorn。等待Unicorn处理完后接住结果(HTTP Response),然后慢慢的发回给客户端。
  4. 而自己只需要秒读取请求数据,然后超牛逼超快的处理完就可以了。

这样的设计准则,就造成了在服务器并发处理中,Unicorn不会负责或者不擅长负责一个重要的东西——客户端连接池。

而再来看看另外一个主角ELB。这货就是Unicorn所需要的傻逼,负责保持客户端的连接池。然而这并不是Unicorn所推荐的满足他几大要求的傻逼,因为ELB干着一件让Unicorn不爽的事情——保持后端连接。

根据ELB的文档,ELB主机会尝试和被代理的后端应用建立持久化连接。一方面可以简单的用来作为健康检查。另一方面在有客户端请求的时候,能够直接复用这个链接,从而更快速的处理请求。

当Unicorn用在集群上的时候,通常我们会配备多个节点。多个节点都会和ELB勾兑起自己的“持久化链接”。而这些链接最后都会落实到Unicorn身上。当节点数足够多的时候,就会导致光是ELB的“持久化链接”数量,就达到了Unicorn的上限。

按理说如果只是链接多,Unicorn几刷子处理完也就罢了。关键是ELB的目的并不是Unicorn期待的“火速呈上请求并拿走响应断开链接”,而是一直保持着链接不放。这样就基本榨干了Unicorn有限的连接池。造成正常的请求,甚至部分ELB的健康检查请求排队,直到Unicorn受不了此前的请求逐个断开才能处理。

所以,总结起来,还是Unicorn自己“设计准则”里的一句话比较管用——符合他们xxxx需求的前端连接池管理方案,只有Nginx这个脑残。Nginx既可以怒怼海量的客户端,所以区区几个ELB的持久话链接不在话下。同时,Nginx只会开链接——发请求——等响应——断链接,不会对背后的Unicorn造成什么压力。

最后我们的解决方案就是,把这一堆Unicorn容器和一个Nginx容器(以Nginx的并发处理能力,一个足矣)加入到同一个Overlay网络。对外由Nginx去Public端口,接收请求。对内,Nginx再把请求交由Unicorn处理。然后Nginx外层,再直接连客户端或者ELB都是极好的。