0 参考资料

0.1 引用资料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
[1] http://www.linuxvirtualserver.org/
[2] https://github.com/alibaba/LVS
[3] http://kb.linuxvirtualserver.org/wiki/IPVS_FULLNAT_and_SYNPROXY
[4] https://wearesocial.com/blog/2018/01/global-digital-report-2018?utm_content=buffer98aa2&utm_medium=social&utm_source =twitter.com&utm_campaign=buffer
[5] http://www.cac.gov.cn/2018-08/20/c_1123296882.htm
[6] https://tools.ietf.org/html/rfc5798
[7] https://tools.ietf.org/html/rfc2991
[8] https://tools.ietf.org/html/rfc1701
[9] https://www.quagga.net/docs/quagga.pdf
[10] http://wiki.nginx.org/CommandLine#Upgrading_To_a_New_Binary_On_The_Fly
[11] https://tools.ietf.org/html/rfc4291
[12] https://en.wikipedia.org/wiki/Border_Gateway_Protocol
[13] https://github.com/F-Stack/f-stack
[14] Jacobson, V. (1988) Congestion Avoidance and Control. ACM SIGCOMM Computer Communication Review, 18, 314-329.
[15] https://github.com/intel/asynch_mode_nginx
[16] http://tengine.taobao.org/document_cn/ngx_http_ssl_asynchronous_mode_cn.html
[17] https://github.com/intel/QAT_Engine
[18] https://github.com/skywind3000/kcp
[19] https://github.com/caddyserver/caddy
[20] https://github.com/lucas-clemente/quic-go
# 基于HTTP3.0项目
[21] https://github.com/litespeedtech/openlitespeed
# CloudFlare开源的Nginx Module,支持QUIC
[22] https://github.com/cloudflare/quiche
[23] /${GOROOT}/src/net/udpsock.go
[24] lucas-clemente/quic-go/packet_handler_map.go
[25] 阿里巴巴开源的 LVS 版本支持FullNAT和SYNPROXY功能,链接地址https://github.com/alibaba/LVS。我们可以看到它是基于 Linux 2.6.32 的内核实现的,并且近年并没有更新。
[26] 出自 Google Maglev 的论文:https://static.googleusercontent.com/ media/research.
google.com/en//pubs/archive/44824.pdf。
[27] 来自 mTCP 的论文:https://www.usenix.org/system/files/conference/nsdi14/
nsdi14-paper-jeong.pdf。
[28] https://queue.acm.org/detail.cfm?ref=rss&id=2927301
[29] https://github.com/iqiyi/dpvs
[30] https://github.com/fastos/fastsocket
[31] 来自开源项目 https://github.com/wg/wrk
[32] 来自开源项目 https://github.com/F-Stack/f-stack
[33] Maglev 性能数据来源https://static.googleusercontent.com/media/research.google. com/en//pubs/archive/44824.pdf
[34] 火焰图绘制工具 https://github.com/brendangregg/FlameGraph
[35] Linux Perf工具 https://perf.wiki.kernel.org/index.php/Tutorial#Introduction

3 负载均衡功能

3.1 负载均衡器的网络结构



多级负载均衡

3.1.1 使用SNAT集群提供外网访问


内部用户访问有两种模式:

  1. 直接路由方式:内部用户和SNAT服务器在同一个IP地址段,内部用户将该VIP地址设置为外网路由的下一跳
  2. 隧道方式:内部用户和SNAT服务器不在同一个IP地址段,内部用户与SNAT服务器的VIP地址建立隧道,并将隧道指定为外网访问的出口设备。

3.1.2 使用SNAT隧道服务无外网出口的IDC

  1. 内部用户向管理员申请外网访问权限
  2. 管理员向SNAT内网网关设置该内部用户的源路由规则,允许该内部用户通过网关访问SNAT隧道服务
  3. 内部用户发起外网访问请求,并将请求发送到其IDC内部的SNAT网关
  4. SNAT网关将内部用户请求通过GRE隧道发送到SNAT服务器上
  5. SNAT服务器将内部用户请求数据包解除GRE隧道封装,并把源地址替换为外网地址,通过防火墙把它传递到外部网络
  6. 外部网络的目标服务器处理完内部请求后,把响应数据通过外网IDC防火墙传送给SNAT服务器
  7. SNAT服务器将响应数据包的目标地址替换成发起请求的内部用户的内网地址,把响应数据通过GRE隧道封装后发送到内网IDC的网关设备。
  8. 内网网关解除GRE隧道封装,把响应数据传送给内部用户

3.2 高可用性

  1. 使用Keepalived做健康检查

  2. 使用VRRP实现主备

  3. 使用ECMP实现集群化

    ECMP主要应用在路由策略上,比如,当某一个节点发现发往下一跳的多个路由都是最佳路径时,就会根据一定的策略将数据包分发到不同的下一跳,通常情况下,为了确保一个数据流都分发到同一个下一跳,这个策略一般是哈希算法。

  4. 使用网卡绑定扩展单网卡的流量

3.3 高可扩展性

  1. 使用DNS技术扩展负载均衡器
  2. 通过ECMP扩展负载均衡器


使用ECMP技术扩展负载均衡的方法通常用于IDC内部多个负载均衡的水平扩展。这种方法使用等价路由原理把业务的VIP通过多台负载均衡器发布到外网,每个服务只需要占一个IP地址,而且扩展方法简单、方便,所以被广泛应用于IDC内部负载均衡器和业务的部署。

3.4 使用BGP Anycast实现多个IDC负载均衡和机房灾备


使用Anycast技术可以实现把相同的业务VIP地址跨地域发布到不同的负载均衡集群上,不同用户的访问流量按照最优路径原则被路由到不同的负载均衡器上。
需要注意的是,用户服务之间的Anycast路由可能会因为网络拥塞或网络拓扑的变化而改变,所以无法保证同一个用户的数据始终发到同一个负载均衡集群上。因此Anycast主要用于基于无状态的UDP状态构建的服务,如DNS服务。另外考虑到路由的相对稳定性,对于基于TCP协议的短连接服务,Anycast也有一定的应用价值。

4 现有负载均衡器比较

4.1 四层负载均衡器

硬件常见的是F5,缺点成本高
软件实现:
LVS+Keepalived:配置性低、实施复杂
DPVS:基于DPDK,染过复杂的内核协议栈,采用轮询的方式收发数据包,避免了锁、内核中断、上下文切换、内核态和用户态数据复制产生的性能开销。
DPVS支持DR、NAT、Tunnel、FullNAT、SNAT共5种转发模式。支持IPv4协议和IPv6协议,增加了NAT64的转发功能,可以通过IPv6网络访问IPv4服务。

4.2 七层负载均衡器

硬件常见:F5
软件实现:
HAProxy:逐渐被Nginx取代
F-Stack:由腾讯开源的用户态协议栈,基于DPDK和FreeBSD来实现
Nginx:基于F-Stack实现的Nginx,可以充分利用F-Stack Kernel Bypass的优异性能。Nginx的多进程模型可以使每个Worker都在一个独立的协议栈上,因此可以使吞吐量达到很高的状态。

5 负载均衡与云计算

5.1 负载均衡与弹性计算

通过负载均衡实现弹性计算框架

可以通过K8S根据业务情况动态增加POD,然后通过负载均衡器把流量调度到新的POD。

5.2 跨区域调度与容灾

同城容灾由同城或临近区域内的两个数据中心组成:一个作为数据中心,负责日常生产,另一个作为灾备中心,负责在灾难发生后使应用系统正常运行。比如,网易云在负载均衡的基础上结合Redis和Kafka服务实现的跨可用区容灾架构:

生产和灾备站点通过数据复制实现同步,在正常运行时,只有生产站点在工作,当出现生产站点故障时,会通过负载均衡机制转至灾备站点。
该技术搭配DNS可以进行跨区域调度与容灾。跨区域容灾在主备中心距离较远时会用到,一般采用异步镜像进行,但会丢失少量的数据。
在大范围自然灾害出现的背景下,以同城加异地灾备的“两地三中心”的灾备模式随之出现,该方案同时具有高可用及灾备的能力。“两地”是指同城、异地;“三中心”是指生产中心、同城灾备中心、异地灾备中心。“两地三中心”灾备模式基本架构如下:

5.3 API Gateway

比较出色的是KONG

6 网络协议优化

6.1 TCP协议优化

TCP相关优化技术,在三次握手阶段,使用TFO(TCP Fast Open)机制,当它通过握手时SYN包中的TFO Cookie选项用来验证客户端是否有连接过本服务端。若验证成功,则服务器可以在客户端发送的三次握手及ACK包到达前就开始发送数据。若TFO Cookie校验失败,则丢弃TFO请求,将该SYN包视为普通的SYN包,完成三次握手。

在流量控制方面,最初TCP协议分配给接收窗口大小的字段为16位,及最多一次可以传输64KB,为了解决这个问题,TCP窗口缩放出现了,该机制将窗口大小扩展到12位。

6.2 TLS/HTTPS协议优化

6.2.1 Session ID及Session Ticket

使用Session ID和Session Ticket进行复用连接,较小握手造成的性能降低。

Nginx支持Session ID和Session Ticket。
开启Session ID:由于Nginx是多进程模型,每个进程都有独立的内存空间,所以需要配置保存Session的全局共享内存。另外需要设置Session的缓存老化时间,如下:

1
2
ssl_session_cache shared:SSL:10m; //全剧共享内存10MB
ssl_session_timeout 1440m; //缓存老化时间1440分钟

开启Session Ticket:Session Ticket需要通过一个Ticket Key开启,这个Key一般可以用OpenSSL来生成,如下:

1
2
ssl_session_tickets on;
ssl_session_ticket_key /usr/local/nginx/ssl_cert/session_ticket.key;

如下的集群架构很难达到Session复用的效果:

以上架构无法达到Session ID的复用,即使要达到Session Ticket复用,需要NginxA、NginxB、NginxC的Ticket key保持一致。

通过增加一个全局Session数据库和Ticket Key的变化库,但如果同步阻塞查找,那么网络T/O等待时间会导致Nginx的吞吐量大幅下降,所以需要采用异步查找的方式。

Nginx一般用 OpenSSL 来完成 TLS/SSL相关的握手。其中,OpenSSL 自1.10版本开始,便支持了异步操作,该异步操作是基于内部实现的Async job(协程)进行的当 TLS/SSL 握手采用异步模式进行时,就会调用 ASYNC_ start job,同时保留这个进程当前的堆栈信息,然后切换到进程去进行一些类似I/O操作。操作完毕后,用户需要通过原来的进程去主动查询 Async job 的状态。如果状态是 ASYNC_FINISHED,则切换到原来的堆栈,继续后面的操作。
在这里,我们通过 OpenSSL 这种异步特性实现了一套基于OpenSSL Async job 的全局 Session 远程查找 Nginx 模块,该模块实现的前提是 Nginx要支持 Async job。它的主要思想就是创建一个远程的集群共享数据库,存储TLS/SSL 的 Session 信息,并利用 OpenSSL提供的 SSL_CTX_sess_set()接口函数注册一些回调函数,包括新建、获取及删除 TLS/SSL Session。新建 Session 的操作一般需要将该 Session 在本地共享内存区存储一次后,再在远程的数据库内存储一次;获取Session 的操作需要先在本地共享内存区查找,如果找不到再去远程的集群共享数据库查找,删除操作实际上可以不用实现,本地内存区和远程集群共享数据库的过期机制可以使得Session在配置的过期时间后自行超时老化,上述远程数据库操作都要在 OpenSSL 的异步框架內实现,目前,我们还设有开源这个模块,更多关于 Nginx对于 OpenSSL的异步支持可以參考 Intel的 Nginx(见链接15)或淘宝的 Tengine(见链接16)。
通过共享数据库进行Session共享的架构:

6.2.2 False-Start

在客户端发送change_cipher_spec的同时,不用等待服务端响应change_cipher_spec,就去发送加密应用数据。
在Nginx上开启False-Start配置:

1
ssl_prefer_server_ciphers on; 

6.2.3 TLS1.3协议

  1. 引入新的密钥协商算法
  2. 实现0-RTT传输
  3. 废弃例如RSA加密算法,使用前向安全的DH算法进行握手
  4. 不再使用DSA证书
  5. 只有ClientHello和ServerHello报文是明文传输,其余所有报文都是加密传输,增强安全性

关于TLS1.3的协议优化,在其他笔记中有记录,本笔记不再记录。

6.2.4 硬件加速卡和计算分离

以Intel的QAT卡为代表,使用专门的硬件加速卡,听声加解密运算性能。
Intel的QAT支持异步的Nginx。
基于OpenSSL的Engine机制,Intel提供了一套QAT的加速卡软件包(见链接17)。

  • OpenSSL的Engine机制

它是一种可以为开发者提供自定义加解密接口的框架。开发者可以自己实现一套常用的加解密接口,注册到 OpenssL 中,然后编译成动态库,存放到OpenSSL 的指定目录中,再对OpensSL 的配置文件 openssl.cnf进行配置,指定 Engine。OpenSSL 在调用初始化函数过程中,会读取配置文件,根据配置文件指定的动态库查找注册的加解密接口函数,具体可以参考 OpenssL 官方给出的实例。这样,对于 OpenSSL 的上层应用来说,调用的接口是透明的,应用程序无需关心 OpenSSL 内部具体的加解密运算到底是 CPU 还是加速卡来负责。

1
2
3
4
5
6
7
8
9
10
11
openssl_conf = openssl_def
[openssl_def]
engines = engine_section

[engine_section]
qat = qat_section

[dasync_section]
engine_id = qat
dynamic_path = /path/to/openssl/source/engines/qat.so
default_algorithms = RSA

这里的default_algorithms表示哪些算法会调用qat.so动态库提供的接口,上边的配置只分离了RSA算法。这是因为一些对称加密的算法调用很频繁,如果每次都经过系统调用去让加速卡计算,那么虽然CPU的负载低了,但是系统调用的成本变高了,在内核态和用户态之间过于频繁的切换并不是一件好事,实际上,分离哪些算法并没有一定的说法,在使用加速卡的场景下,最好可以根据实际情况进行实验,得到较优的经验值。
通过性能测试,在相同的硬件条件和测试环境下(Nginx服务器有32个逻辑CPU核心),利用QAT卡进行异步计算的Nginx服务器在HTTPS完全握手的场景下QPS达到2.5万,原生Nginx大约达到1.5万。
利用OpenSSL的命令接口测试QAT的RSA计算性能,一张卡的计算能力可以达到4万左右,发现QAT的性能没有充分利用,所以我们可以采用计算分离的方案,建立一个QAT集群,让多个服务器去远程调用QAT集群。
需要实现一个类似qat.so的OpenSSL异步Engine,如将Engine命名为Remote_Engine。其内部有一些加解密算法的实现,如RSA加解密的接口。这些加解密的借口都会把加解密的参数通过RPC远程传输给专门的计算集群,然后异步返回;计算集群计算完毕后返回响应,再继续执行之前的握手流程。上述异步式远程调用Remote_Engine进行加解密的流程需要在异步的Nginx基础上实现,远程调用的异步基于RPC的异步和OpenSSL的Async job框架实现。计算集群可以插入多张QAT卡,也可以是CPU空闲的集群。如下:

6.2.5 自动化数字证书管理

为了保证证书的安全性,可以构建证书管理平台,针对服务器证书私钥进行特殊的加密运算,并把密文保存在证书管理平台中,不涉密的文件明文保存。对业务服务器或七层负载均衡器进行改造,使其支持从证书管理平台远程下载证书保存在内存中,如下:

不用每次SSL握手都去证书管理平台下载,可以在启动服务器时远程获取证书参数,然后保存在本地内存,也可以在本地保存加密的证书文件,以便在远程访问证书管理平台失败时,可以读取本地缓存文件。
证书管理平台可以统一管理证书,在证书过期之前,可以更新证书管理平台上相应的证书,业务服务器或七层负载均衡器需要周期性获取远程证书。

6.3 HTTP协议优化和HTTP2.0

  1. 二进制帧
  2. 多路复用:可以在同一个连接中发送多个请求-响应消息
  3. 头部压缩
  4. 服务器推送:可以在客户端请求时,服务器主动推送一些客户端可能需要的资源。

6.4 基于UDP的传输协议优化

6.4.1 基于UDP的传输协议简介

  1. KCP
  2. uTP
  3. FASP
  4. SCTP
  5. UDT
  6. QUIC:内建安全性集成了TLS协议、避免前序包阻塞、改进的拥塞控制(默认使用TCP协议的Cubic拥塞控制算法,但做了很大改进,主要改进有支持可插拔、采用数据包序号递增来避免重传歧议,以及通过ACK包携带延时来精确计算RTT)、连接迁移

6.4.2 QUIC协议优化

  • QUIC协议在HTTP2.0上的改进

QUIC协议结合HTTP2.0、TLS及TCP协议的设计经验,在其传输方面进行了多路复用,以及流量控制的传输优化;在安全通信方面,为通信双方提供等效于TLS协议的安全机制;在可靠性方面,提供类似TCP协议的包重传、拥塞控制等特性保证传输的可靠性。

  • QUIC协议的实现特点
  1. QUIC协议的数据流
  2. 全TLS加密传输
  3. RTT快速会话恢复:QUIC协议在和客户端进行第一次连接时,QUIC协议仅需要1RTT即可建立安全可靠的连接,然后,客户端可以在本地缓存添加加密认证信息,后续可以实现0RTT连接建立
  4. 没有对头阻塞的多路复用
  • QUIC在负载均衡器中的使用

QUIC为每个连接指定一个64位的身份标识–连接ID,不再使用类似TCP的四元组,在负载均衡中通过连接ID作为一致性哈希键值。
如果TCP,负载均衡器可以把客户端信息插入到TCP的Options扩展字段中,将这部分信息透传到后端服务器。UDP中无类似的Options扩展字段,所以将客户端IP、端口等数据透传下沉到网络层IP报文的Options扩展字段中是一个可行的方案。
DPVS负载均衡技术实现在自定义私有协议及IP协议的Options扩展字段中插入客户端信息的数据,在技术上被称为UOA。

  • gQUIC完成度较高的开源项目有Caddy,基于quic-go库支持QUIC协议

  • 快速支持QUIC的方案,可以部署支持QUIC的反向代理

6.5 DNS协议优化

DNS解析流程主要包括5个步骤:

  1. 查找浏览器缓存
  2. 查找系统缓存
  3. 查找路由器缓存
  4. 查找ISP DNS缓存
  5. 迭代查询

优化思路:减少DNS请求数量和缩短DNS请求时间

优化方案:

  1. 保持TCP连接,降低DNS查找频率
  2. 浏览器、和计算机DNS缓存,防止DNS迭代查询
  3. 采用DNS预解析,降低用户等待时间,提升用户体验
  4. 部署域名服务注册consul集群,每一个域名对应的IP注册到consul集群,在客户端安装改进的dnsmasq,把要访问的域名添加到配置文件,这样客户端的dnsmasq进程定期到consul集群获取域名对应的IP地址,将其缓存到本地,当客户端访问域名是,dns解析请求会被dnsmasq进程劫持,直接返回域名对应的ip地址。这样DNS解析过程在本地就完成了,基本无延迟。

7 性能优化

7.1 内核成为瓶颈的原因

  1. 上下文切换

上下文切换包括用户态/内核态的切换、多进程/线程上下文的切换等。上下文有一定的开销,应该尽量避免频繁切换。可以选择将任务和CPU核心进行亲和性绑定。

  1. 资源共享与锁的使用
  2. 中断风暴

内核网卡驱动的收发包部分是通过硬件中断和下半部软中断实现的,通过NAPI接口实现“中断加轮询”的方式。
在高性能、高包转发率的特殊网络应用场景下,当网络io非常大时,在Linux中通过top命令就可以看到CPU被大量消耗在软中断及其处理函数上。

  1. 强大且复杂的网络协议栈
  2. 数据复制

内核态到用户态的数据复制

7.2 高性能四层负载均衡的关键技术

  1. Share-Nothing思想

Linux内核在CPU共享了大量数据,如全局UDP哈希表、Netfilter Hook表等,还有内核的accept队列。

  1. 避免上下文切换:亲和性设置
  2. 使用轮询而非中断
  3. 避免数据复制

通过kernel bypass避免内核态到用户态的数据复制。

7.4 使用DPDK加速四层负载均衡

7.4.1 高性能负载均衡架构


基于DPVS讲解,总体架构主要包括以下几点:

  1. Master/Worker模型
  2. 网卡队列/CPU绑定:让不同的内核处理不同的网卡队列流量,分摊工作量,实现并行处理和线性扩展。
  3. 关键数据Per-Core及无锁化:对于DPVS来说,connection表、邻居表、路由表等都是频繁修改或频繁查找的数据,需要进行Per-Core化。

在具体实现上,connection表(连接表)和ARP/route表并不相同。对于connection表,在高并发的情况下,不仅会被频繁地查找,还会被频繁地添加、删除。可以让每个CPU维护不同的连接表,不同的网络数据流(包含TCP或UDP两种协议的数据流)按照n元组被定向到不同的CPU内核,在此特定CPU内核上创建、查找、转发、销毁。同样的数据流(即n元组匹配)只会出现在某个CPU内核上,不会落到其他CPU上。如果要实现同一个数据流落到同一个CPU上,可以使用网卡的RSS实现,但是要实现同一个数据流在出口和入口两个方向都落在同一个CPU内核上就没那么容易了。
针对邻居表和路由表,每个CPU都会用到系统的“全局”数据。但这个不像connection表那样会频繁变化,可以采用跨CPU无锁同步的方式,通过跨CPU通信将表的变化同步到每个CPU内核上。

  1. 跨CPU无锁消息:虽然采用了数据Per-Core化,但以下场景中还是需要跨内核通信的。
    1. Master获取Worker的各种统计信息
    2. Master将路由、黑名单等配置同步到各个Worker
    3. Master将来自KNI接口的数据发送到Worker

可以使用DPDK提供的无锁rte_ring库,从底层保证通信是无锁的。

7.4.2 高性能负载均衡功能模块


DPVS总体架构从下至上包括:

  1. 网络设备层
  2. 轻量级IP协议栈
  3. 负载均衡层(IPVS)
  4. 基础功能模块
  5. 控制面