Timbo Site

write something


使用HAProxy支撑SSL/TLS高并发连接

我们在8月份在中国区服务器上做了一些架构变更,如下是我们的一些经验

背景

去年9月初的一个凌晨,阿里云中国区负载均衡器内部进行调整,大量连接重连将我们的Broker集群打垮了。即使重启Broker集群也会被大量新建的连接打垮,竭力恢复花了2个小时。

去年10月中旬,我们更换中间件BrokerSSL/TLS证书时重启了集群,同样因为大量连接重连,Broker集群只要一启动就会被迅速打垮。有了不久之前的经验,恢复只花了半个小时。

这两次故障均影响到了大部分中国用户。

为什么会这样?

用户通过TCP连接到我们的服务器进行数据交换,为了保护用户的数据安全,我们要求用户使用Secure TCP进行连接。

启用Secure TCP会让连接建立时进行协议握手、消息传递时参与加解密。当连接数十分巨大,即使仅仅建立连接就会消耗大量的服务器CPU资源。

分析了出现问题时的各个监控指标,新建连接的速度应该远远超过Broker集群能处理的量,Broker集群CPU持续打满,连接数无法上涨,过了一段时间后Broker集群开始出现错误,无法再处理新的连接,再过一段时间后,Broker打了crash dump文件后结束了自身进程。分析crash dump之后,我们认为Broker集群在处理SSL/TLS连接时性能缓慢,导致自身任务堆积,引起进程崩溃。

SSL/TLS是什么?

启用Secure TCP时,实际上是启用了SSL/TLS。

SSL(Secure Sockets Layer)和其继任者TLS(Transport Layer Security)是能保证互联网通信时信息安全与完整的安全协议。虽然SSL已被TLS接替,但平时也会说SSL证书TLS证书SSL/TLS证书SSL连接TLS连接SSL/TLS连接

SSL/TLS证书则是一个数字文件,包含公钥和私钥、网站的识别信息和域名,还有一些可选的信息。如果证书经过公开信任的证书颁发机构(Trusted Certificate Authority, Trusted CA)签发,则该证书进行数字签名的内容会被用户使用的客户端或浏览器所信任。

SSL/TLS连接如何进行的?

客户端向服务端进行SSL/TLS连接时,会先进行TCP握手

    +----------+                     +----------+
    |          |        SYN          |          |
    |          +-------------------->|          |
    |          |      SYN ACK        |          |
    |  Client  |<--------------------+  Server  |
    |          |        ACK          |          |
    |          +-------------------->|          |
    +----------+                     +----------+

接着进行正式SSL/TLS连接

  1. 客户端指定TLS协议和加密方式
  2. 服务端如果支持客户端指定的TLS协议和加密方式,则返回数字证书,证书里包含公钥
  3. 客户端验证数字证书是否有效,如访问域名是否匹配,证书是否在有效期内
  4. 客户端生成Key,使用公钥加密发给服务端,并使用该Key算出对称密钥,使用对称密钥加密一条Finished消息送往服务器
  5. 服务端使用该Key算出对称密钥,并使用对称密钥尝试解出Finished消息,如果成功,服务端使用对称密钥加密一条Finished消息返回给客户端
  6. 客户端与服务端可以开始进行加密通信

流程图如下:

             +-------------------+                                                +-------------------+              
             |                   |  ClientHello                                   |                   |              
             |                   |    ProtocolVersion: TLSv1.3                    |                   |              
             |                   |    CipherSuites: [TLS_RSA_WITH_RC4_128_SHA]    |                   |              
             |                   +------------------------------------------------>                   |              
             |                   |                                                |                   |              
             |                   |  ServerHello                                   |                   |              
             |                   |    ProtocolVersion: TLSv1.3                    |                   |              
             |                   |    CipherSuite: TLS_RSA_WITH_RC4_128_SHA       |                   |              
             |                   |  Certificate                                   |                   |              
             |                   |    PublicKey: 7ba327f8aed3eea7a7fa96581d0      |                   |              
             |                   |  ServerHelloDone                               |                   |              
 Verify      |                   <------------------------------------------------+                   |              
 Certificate |                   |                                                |                   |              
       +-----+                   |  ClientKeyExchange                             |                   |              
       |     |       Client      |    PreMasterSecret: c3ac534f4d919a0e92b966795e |       Server      |              
       |     |                   |  ChangeCipherSpec Finished                     |                   |              
       +----->                   |    21c124d28a548559f0f8abd4b                   |                   |              
             |                   +------------------------------------------------>                   |              
             |                   |                                                |                   |               
             |                   |         ChangeCipherSpec Finished              |                   |              
             |                   |           c49fe482d338760807c3c278f            |                   |              
             |                   <------------------------------------------------+                   |              
             |                   |                                                |                   |              
             |                   |         Uz5/CSEem2+fpiV8th9yNYAOxE             |                   |              
             |                   |         s8UvFDftnL62rm8BFtV8A084b+             |                   |              
             |                   |         R9dbU8Mnnkq2/YPRQmCAMO8Ak+             |                   |              
             |                   |         tMLjLcR2EytPGoRTwqJ06y9ijl             |                   |              
             |                   +------------------------------------------------>                   |              
             |                   |                                                |                   |              
             +-------------------+                                                +-------------------+              

我们的Broker集群并不擅长处理短时间大规模SSL/TLS连接,阿里云中国区的服务故障大部分来自于此。

只有阿里云中国区上会有这样的问题?

目前是的,其他服务区使用的负载均衡器让我们免于这样的烦恼:AWS无论是在很早之前的Classic Load Balancer或是进化的Network Load Balancer都支持在负载均衡器上挂载证书提供Secure TCP,腾讯云也提供在负载均衡上配置TCP SSL监听器

一些服务区上遇到过频繁的故障,但得益于这些服务区上负载均衡器提供的SSL终止能力,Broker能很快恢复回来。

SSL终止?有什么用?

SSL终止SSL Termination这一术语的英文直译,上述提到的负载均衡器处理所有TCP层上的加密和解密,让信息以明文的方式传递给Broker,这一能力被称为SSL终止,因为涉及到装载证书通信上的通信卸载,也有人称之为SSL卸载(SSL Offloading)。

SSL终止有很多好处,比如可以在负载均衡器上配置和更改证书,管理更方便;服务器不用承担加密解密的任务,有更多的CPU时间专注于计算。

                +-------------+         +-------------+
                |             |         |             |
    Secure TCP  |    Load     |   TCP   |             |
   ------------>|  Balancer   +-------->|   Broker    |
                |             |         |             |
                +-------------+         +-------------+

因此我们计划引入HAProxy来做SSL终止,虽然这样做会破坏全球数据中心架构的统一,但在遇到云上服务故障和每年更换证书时能活下去,我们开始对HAProxy的性能进行测试和无数实验,越过了无数障碍。

遇到的这些障碍都有啥?

比如单IP连接数限制。

我们计划将HAProxy前置在Broker之前,在负载均衡器之后,架构图会变成这样:

             +----------+             +-ECS------+         +-ECS------+
  Secure TCP |          |             |          |         |          |
  Passthrough|   Load   | Secure TCP  |          |   TCP   |          |
  ---------->| Balancer +------------>| HAProxy  +-------->|  Broker  |
             |          |             |          |         |          |
             +----------+             +----------+         +----------+

HAProxy将一个Secure TCP连接进行SSL终止之后,向Broker发起一个新的TCP连接,就会占用ECS实例上的一个端口(Port),单IP的端口范围在0-65535之间,理论上最多只能发起65535个连接。HAProxy部署在ECS实例上,就会受这个限制。

使用钞能力花钱购买更多的ECS实例就可以轻松突破65535连接的限制,假设单个Broker要支撑60万连接,就需要前置10台搭载HAProxy的ECS实例,那有N个Broker,就需要10N台ECS实例。即使不考虑机器成本,维护成本也会从原来的N变为10N,出现故障的几率也会变高。

对他使用虚拟IP吧!既然单个网卡上的IP会受这样的限制,那在单个网卡下新建多个虚拟IP,使用虚拟IP进行转发,应该可以绕开这个限制。

我们新建了一个新网段用于放置新的虚拟IP,开好安全组,做好路由,架构图如下:

                +-ECS------------------+                     
                | HAProxy              |         +-ECS-------------+
                |                      |         | Broker          |
    Secure TCP  | eth0: 172.0.0.1      |   TCP   |                 |
  ------------->|  |                   +-------->| eth0: 172.0.1.1 |
                |  +-vip0: 192.168.0.1 |         |                 |
                |  |                   |         +-----------------+
                |  +-vip1: 192.168.0.2 |                     
                +----------------------+                     

HAProxy的设置中,放置如下配置:

frontend tcp_conn
    bind *:1234 ssl crt /certs/cert.pem
    mode tcp
    default_backend broker

backend broker
    mode tcp
    balance leastconn
    server broker1 172.0.1.1:1234 source 192.168.0.1 check inter 5000 rise 3
    server broker2 172.0.1.1:1234 source 192.168.0.2 check inter 5000 rise 3

重新加载配置之后,HAProxy做了几次健康检查,认定broker1broker2无法连接。

我们在HAProxy机器上检查向Broker机器的连接,是可达的:

> telnet 172.0.1.1 1234
Trying 172.0.1.1...
Connected to 172.0.1.1.
Escape character is '^]'.

而使用虚拟IP去连接,则无法连接:

> telnet -s 192.168.0.1 172.0.1.1 1234
Trying 172.0.1.1...
telnet: connect to address 172.0.1.1: Connection refused
telnet: Unable to connect to remote host

我们花了很长时间去调查,用iptables进行数据包跟踪,进行了很多试验,发现这是Linux内核保证安全的功能:反向路径过滤(Reserve Path Filtering),检查数据包的源地址是否可达。

反向路径过滤功能打开,机器在收到一个数据包时,会先检查收到的数据包的源地址是否可以通过它进来的接口到达。如果该数据包可以通过进来的网卡进行路由,那么机器会接受该数据包。反之,则机器就会丢掉这个数据包。说得稍微细一点,如果eth0网卡有incoming数据包,反向路径过滤模块会将数据包源地址和目的地址调转,即src IP -> dst IP调转为dst IP -> src IP,接着在路由表中查找dst IP -> src IP的路由,如果出口为eth0,则该数据包能通过,否则丢弃。

一些Linux操作系统默认会打开这个功能,检查操作系统中含有rp_filter的相关内核参数即可看到。我们的Broker能直接被外部客户端访问,通过伪造源地址可以很轻易的对服务器进行DDoS攻击,为了安全,这个保护功能我们不会关掉。

一番折腾过后,我们认定虚拟IP不能满足需求,只能用真实IP来解决问题了。

如何在阿里云上用真实IP呢?

在ECS上建立的虚拟IP无法被阿里云的负载均衡、路由器识别,自然不能简单将流量在阿里云上进行路由。阿里云ECS上有弹性网卡的概念,可用来进行更精细的网络管理。当我们在ECS上追加多个弹性网卡时,可以在负载均衡中将流量打入加载多个弹性网卡并安装了HAProxy的ECS实例上,架构图如下:

                                  +-ECS------------------+                           
                                  | HAProxy              |                           
                                  +-----------------+    |                           
      +----------+            +-->+ eth0: 172.0.0.1 +----+--+      +-ECS-----+       
      |          |            |   +-----------------+    |  |      |         |       
      |   Load   | Secure TCP |   +-----------------+    |  | TCP  | Broker  |       
----->+ Balancer |------------+-->+ eth1: 172.0.0.2 +----+--+----->|         |       
      |          |            |   +-----------------+    |  |      |         |       
      +----------+            |   +-----------------+    |  |      |         |       
                              +-->+ eth2: 172.0.0.3 +----+--+      |         |       
                                  +-----------------+    |         +---------+       
                                  |....                  |                           
                                  +----------------------+                          

要用单个机器支撑60万并发连接,至少需要10张弹性网卡。而为这台机器添加第9张弹性网卡时,系统提示当前机器规格ecs.c7a.4xlarge最多只能支持8张弹性网卡。

8张弹性网卡,算下来也有52万并发连接,虽离60万有些距离但是并不是很遥远,配置好了弹性网卡,将负载均衡配置到这台机器上,我们开始进行测试。

我们使用3台机器进行压测,每秒新建1000个连接,将并发抬到13万左右时,HAProxy无法再接受新连接,压测机器开始抛出大量连接拒绝错误。

又怎么了?

小编我们也很好奇,跳到机器上检查系统错误日志,全是如下错误:

.....
kernel: nf_conntrack: table full, dropping packet
kernel: nf_conntrack: table full, dropping packet
kernel: nf_conntrack: table full, dropping packet
kernel: nf_conntrack: table full, dropping packet
kernel: nf_conntrack: table full, dropping packet
......

nf全称为netfilter,是Linux系统中的包过滤框架(Packet Filtering Framework),上文提到的我们用于跟踪流量包的iptables工具,就是基于流量包经过netfilter上触发的hook来完成工作的。

问题简单描述为:netfilter模块中conntrack table默认最大限制为262144,而在HAProxy机器上每建立1个连接,会有1个入和1个出,在conntrack table上会消耗2个entry,压测至13万左右时已把conntrack table耗尽。

RedHat Knowledgebase的指引,我们修改系统模块参数,将conntrack table的上限抬高,同时修改hashsize至合适机器配置的参数,可以继续进行测试了。

应该没问题了吧?

压测至15万时又压不动了,压测机器开始抛出大量连接拒绝错误,而HAProxy机器很平静。

我们检查了HAProxy机器,没有任何报错,无论是CPU、内存、网络指标都非常平稳,这表明大量的连接无法达到HAProxy机器上。检查负载均衡的指标,最大连接数还没有达到负载均衡的上限,而新建连接数反复在3000左右摆动,正好是设计的压测新建连接的数量。那现在问题不在负载均衡上,也不是HAProxy机器内操作系统或软件的限制,而是HAProxy机器这个框体的限制。

上文提到的机器规格ecs.c7a.4xlarge参数表中,最大连接数为30万,而在阿里云控制台中也可看到连接数已达上限。

虽然机器连接数已达上限,但是CPU、内存均没有超过10%,为了网络连接数而升级机器,这和我们的初衷背道而驰。

最接近我们需求的阿里云ECS实例为g7ne实例,如ecs.g7ne.2xlarge,提供最大175万连接,但最多只能接驳6张弹性网卡,理论上限为393210(65535*6),近40万连接。

それからどうしたの

我们将HAProxy和Broker部署在了一起。不需要弹性网卡,不需要升级实例,有多少连接就是多少。但虚拟IP还是需要的,架构图如下:

                                     +-ECS-------------------------------------+
                                     |                                         |
                                     | HAProxy                                 |
                                     | +-----------------+                     |
                                 +---->| eth0: 172.0.0.1 |                     |
                                 |   | +-+---------------+                     |
                                 |   |   | +-------------------+               |
           +------------+        |   |   +-+ vip0: 192.168.0.1 +-------+       |
           |            |        |   |   | +-------------------+       |       |
Secure TCP |            | Secure |   |   | +-------------------+       |       |
Passthrough|    Load    |  TCP   |   |   +-+ vip1: 192.168.0.2 +-------+       |
---------->|  Balancer  |--------+   |   | +-------------------+       |       |
           |            |            |   | +-------------------+       |       |
           |            |            |   +-+ vip2: 192.168.0.3 +-------│       |
           +------------+            |   | +-------------------+       |       |
                                     |   | +-------------------+       │       |
                                     |   +-+ ....              +       |       |
                                     |     +-------------------+       V       |
                                     |                          +-----------+  |
                                     |                          |           |  |
                                     |                          |  Broker   |  |
                                     |                          |           |  |
                                     |                          +-----------+  |
                                     +-----------------------------------------+

只需要在Broker机器上搭建好HAProxy,开好虚拟IP,调整好系统、内核参数,把负载均衡指向新端口,静等连接rebalance就好。

HAProxy没有让我们失望,它将最繁重的工作接了过来,节省了大量的CPU时间和内存,原实例支持最大60万连接,意味着单Broker连接极限能力从15万台增至60万台。

cpu usage

memory usage

反复横跳三周,HAProxy治好了我们的精神内耗

这三周里有过失望有过气馁,虽然经常相互说这不行那不行能力不行素质还很差,但都在拼命思考找解决问题的方法,每一条路我们都走通了,只因各因素放弃,并没有失败,我觉得我们每个人都非常光荣。

实际上,这次架构改进收益最大的是我。我会在很长一段时间内不需要对服务器进行扩容。

最重要的是不需要在10月份花几个晚上熬夜替换证书,还要担心无法拉起集群而遭用户投诉,写检讨。这点上又让我贯彻了身为运维职责时的理念:不加班。