Nginx多进程高并发、低时延在滴滴缓存代理中的应用

redis非常但是不支持事务,mysql支持事务,但是不支持集群 腾讯在上面

PhxSQL is a high-availability and strong-consistency MySQL cluster built on Paxos and Percona

nginx非常厉害只支持https连接,但是Twemproxy支持连接tcp方式连接reids,有人在

上面进行结合 佩服这些人

这里特此记录下来,如何把单进程扩展到多进程??

1. 开发背景

现有开源缓存代理中间件有 Twemproxy、Codis 等,

其中 Twemproxy 为单进程单线程模型,只支持 Memcache 单机版和 Redis 单机版。Twemproxy 和 Codis 都不支持集群版功能。

由于 Twemproxy 无法利用多核特性,因此性能低下,

短连接 QPS 大约为 3W,长连接 QPS 大约为 13W;

Codis 起几十个线程,短连接 QPS 不超过 10 万;同时某些场景这些开源软件时延抖动厉害。

为了适应公有云平台上业务方的高并发需求,

因此决定借助于 Twemproxy 来做二次开发,把 Nginx 的高性能、高可靠、高并发机制引入到 Twemproxy 中,

通过 master+ 多 worker 进程来实现七层转发功能。

2. Twemproxy

2.1 Twemproxy 简介

Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和更新的 Redis 协议。它全部用 C 写成,使用 Apache 2.0 License 授权。支持以下特性:

速度快 轻量级 维护持久的服务器连接 启用请求和响应的管道 支持代理到多个后端缓存服务器 !!!! 同时支持多个服务器池 多个服务器自动分享数据!!!! 可同时连接后端多个缓存集群 !!!! 实现了完整的 Memcached Ascii 和 Redis 协议. 服务器池配置简单,通过一个 YAML 文件即可

一致性 Hash

详细的监控统计信息

支持 Linux,* BSD,OS X and Solaris ( SmartOS )

支持设置 HashTag

连接复用,内存复用,提高效率

2.2 滴云 Memcache 缓存集群拓扑结构

代理是集群

如上图所示,实际应用中业务程序通过轮询不同的 Twemproxy 来提高 QPS,

同时实现负载均衡。

旁白:Twemproxy必须是多个。

2.3 推特原生 Twemproxy 瓶颈

如今 Twemproxy 凭借其高性能的优势, 在很多互联网公司得到了广泛的应用,已经占据了不可动摇的地位,

然而在实际的生产环境中,Twemproxy 存在以下缺陷:(前方高能)

单进程单线程,无法充分发挥服务器多核 CPU 的性能。当 Twemproxy QPS 短连接达到 8000 后,消耗 CPU 超过 70%,时延陡增。大流量下造成 IO 阻塞,无法处理更多请求,QPS上不去,业务时延飙升。维护成本高,如果想要充分发挥服务器的所有资源包括 CPU、 网络 IO 等,就必须建立多个 Twemproxy 实例,维护成本高。扩容、升级不便。

原生 Twemproxy 进程呈现了下图现象:

一个人干活,多个人围观。多核服务器只有一个 CPU 在工作,资源没有得到充分利用。

3. Nginx

Nginx 是俄罗斯软件工程师 Igor Sysoev 开发的免费开源 Web 服务器软件,聚焦于高性能,高并发和低内存消耗问题,成为业界公认的高性能服务器,并逐渐成为业内主流的 Web 服务器。

Nginx 主要特点有:

完全借助 Epoll 机制实现异步操作,避免阻塞。(epoll解决不了)重复利用现有服务器的多核资源。充分利用 CPU 亲和性(affinity),把每个进程与固定 CPU 绑定在一起,给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性,减少进程调度开销。请求响应快。支持模块化开发,扩展性好。Master+ 多 worker 进程方式,确保 worker 进程可靠工作。当 worker 进程出错时,可以快速拉起新的 worker 子进程来提供服务。内存池、连接池等细节设计保障低内存消耗。热部署支持,master 与 worker 进程分离设计模式,使其具有热部署功能。升级方便,升级过程不会对业务造成任何伤害。

Nginx 多进程提供服务过程如下图所示:

4. Nginx Master+Worker 多进程机制在 Twemproxy 中的应用

4.1 为什么选择 Nginx 多进程机制做为参考?

Twemproxy 和 Nginx 都属于网络 IO 密集型应用,都属于七层转发应用,时延要求较高,应用场景基本相同。

Nginx 充分利用了多核 CPU 资源,性能好,时延低。

多进程模式

4.2 Master-Worker 多进程机制原理

Master-worker 进程机制采用一个 master 进程来管理多个 worker 进程。每一个 worker 进程都是繁忙的,它们在真正地提供服务,master 进程则很“清闲”,只负责监控管理worker进程。

master 进程包含:接收来自外界的信号;向各 worker 进程发送信号;监控 worker 进程的运行状态;当 worker 进程退出后 ( 异常情况下 ),会自动重新启动新的 worker 进程。Worker 进程负责处理客户端的网络请求,多个 worker 进程同时处理来自客户端的不同请求,worker 进程数可配置。

4.3 多进程关键性能问题点

Master-worker 多进程模式需要解决的问题主要有:

Linux 内核低版本 ( 2.6 以下版本 )“惊群”问题。Linux 内核低版本 ( 2.6 以下版本)负载均衡问题。Linux 内核高版本 ( 3.9 以上版本 ) 新特性如何利用。如何确保进程见高可靠通信。如何减少 worker 进程在不同 CPU 切换的开销。Master 进程如何汇总各个工作进程的监控数据。Worker 进程异常,如何快速恢复。

4.3.1 Linux 内核低版本关键技术问题

由于 Linux 低内核版本缺陷,因此存在“惊群”、负载不均问题,解决办法完全依赖应用层代码保障。

4.3.1.1 如何解决“惊群”问题

当客户端发起连接后,所有的 worker 子进程都监听着同一个端口,内核协议栈在检测到客户端连接后,会激活所有休眠的 worker 子进程,最终只会有一个子进程成功建立新连接,其他子进程都会 accept 失败。

Accept 失败的子进程是不应该被内核唤醒的,因为它们被唤醒的操作是多余的,占用本不应该被占用的系统资源,引起不必要的进程上下文切换,增加了系统开销,同时也影响了客户端连接的时延。

“惊群” 问题是多个子进程同时监听同一个端口引起的,因此解决的方法是同一时刻只让一个子进程监听服务器端口,这样新连接事件只会唤醒唯一正在监听端口的子进程。

“惊群”问题通过非阻塞的 accept 锁来实现进程互斥 accept(),其原理是:

在 worker 进程主循环中非阻塞 tryLock 获取 accept 锁,

如果 tryLock 成功,则此进程把监听端口对应的 fd 通过 epoll_ctl() 加入到本进程自由的 Epoll 事件集;

如果 tryLock 失败,则把监听 fd 从本进程对应的 Epoll 事件集中清除。

Nginx 实现了两套互斥锁:基于原子操作和信号量实现的互斥锁、基于文件锁封装的互斥锁。考虑到锁的平台可移植性和通用性,改造 Twemproxy 选择时,选择文件锁实现。

如果获取 accept 锁成功的进程占用锁时间过长,那么其他空闲进程在这段时间内无法获取到锁,从而无法接受新的连接。最终造成客户端连接相应时间变长,QPS 低,同时引起负载严重不均衡。

为了解决该问题,选择通过 post 事件队列方式来提高性能,tryLock 获取到 accept 锁成功的进程,其工作流程如下:

tryLock 获取 accept 锁成功。// 抢锁 if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) { return; // 不同进程如何抢同一个锁 文件锁 } 通过 epoll_wait 获取所有的事件信息,把监听到的所有 accept 事件信息加入 accept_post 列表,把已有连接触发的读写事件信息加入 read_write_post 列表。 /*接下来,epoll要开始wait事件, ngx_process_events的具体实现是对应到epoll模块中的ngx_epoll_process_events函数 这里之后会详细讲解的哦 */ (void) ngx_process_events(cycle, timer, flags); // 分发事件 执行 accept_post 列表中的所有事件。 /* ngx_posted_accept_events是一个事件队列,暂存epoll从监听套接口wait到的accept事件。 前文提到的NGX_POST_EVENTS标志被使用后,会将所有的accept事件暂存到这个队列 */ if (ngx_posted_accept_events) { ngx_event_process_posted(cycle, &ngx_posted_accept_events); } Unlock 锁。//所有accept事件处理完之后,如果持有锁的话,就释放掉。 if (ngx_accept_mutex_held) { ngx_shmtx_unlock(&ngx_accept_mutex); } 执行 read_write_post 列表中的事件。 /* // 处理事件 处理普通事件(连接上获得的读写事件), 因为每个事件都有自己的handler方法, */ if (ngx_posted_events) { if (ngx_threaded) { ngx_wakeup_worker_thread(cycle); } else { ngx_event_process_posted(cycle, &ngx_posted_events); } }

Worker 进程主循环工作流程图如下:

从上图可以看出,worker 进程借助 Epoll 来实现网络异步收发,客户端连接 Twemproxy 的时候,worker 进程循环检测客户端的各种网络事件和后端 Memcached 的网络事件,并进行相应的处理。

Twemproxy 各个进程整体网络 I/O 处理过程图如下:

4.3.1.2 如何解决“负载均衡“问题

在多个子进程争抢处理同一个新连接事件时,一定只有一个 worker 子进程最终会成功建立连接,随后,它会一直处理这个连接直到连接关闭。

最差的情况:

这样,如果有的子进程“运气”很好,它们抢着建立并处理了大部分连接,其他子进程就只能处理少量连接,这对多核 CPU 架构下的应用很不利。

理想情况下,每个子进程应该是平等的,每个 worker 子进程应该大致平均的处理客户端连接请求。如果 worker 子进程负载不均衡,必然影响整体服务的性能。

Nginx 通过连接阈值机制来实现负载均衡,

其原理如下:

每个进程都有各自的最大连接数阈值 max_threshold 和当前连接阈值数 local_threshold,以及当前连接数阈值,进程每接收一个新的连接,local_threshold 加 1,连接断开后,local_threashold 减 1。

如果 local_threshold 超过 max_threshold,则不去获取 accept 锁,把 accept 机会留给其他进程,同时把 local_threshold 减 1,

这样下次就有机会获取 accept 锁接收客户端连接了。

在实际业务应用中,有的业务采用长连接和 Twemproxy 建立连接,连接数最大可能就几百连接,如果设置 max_threshold 阈值过大,多个连接同时压到 Twemproxy,很容易引起所有连接被同一个进程获取从而造成不均衡。

为了尽量减少负载不均衡,在实际应用中,新增了 epoll_wait 超时时间配置选项,把该超时时间设短,这样减少空闲进程在 epoll_wait 上的等待事件,从而可以更快相应客户端连接,并有效避免负载不均衡。

4.3.2 Linux 内核高版本 TCP REUSEPORT 特性如何利用

4.3.2.1 什么是 Reuseport?

Reuseport 是一种套接字复用机制,它允许你将多个套接字 Bind 在同一个 IP 地址/端口对上,这样一来,就可以建立多个服务来接受到同一个端口的连接。

4.3.2.2 支持 Reuseport 和不支持 Reuseport 的区别

如果 Linux 内核版本小于 3.9,则不支持 Reuseport ( 注:部分 Centos 发行版在低版本中已经打了 Reuseport patch,所以部分 Linux 低版本发行版本也支持该特性)。

不支持该特性的内核,一个 IP+port 组合,只能被监听 Bind 一次。这样在多核环境下,往往只能有一个线程(或者进程)是 listener,也就是同一时刻只能由一个进程或者线程做 accept 处理,在高并发情况下,往往这就是性能瓶颈。其网络模型如下:

在 Linux kernel 3.9 带来了 Reuseport 特性,它可以解决上面的问题,其网络模型如下:

Reuseport 是支持多个进程或者线程绑定到同一端口,提高服务器程序的吞吐性能,其优点体

现在如下几个方面:

允许多个套接字 bind()/listen() 同一个 TCP/UDP 端口。每一个线程拥有自己的服务器套接字。在服务器套接字上没有了锁的竞争,因为每个进程一个服务器套接字。内核层面实现负载均衡。安全层面,监听同一个端口的套接字只能位于同一个用户下面。

4.3.3 Master 进程和 Worker 进程如何通信?

由于 master 进程需要实时获取 worker 进程的工作状态,并实时汇总 worker 进程的各种统计信息,所以选择一种可靠的进程间通信方式必不可少。

在 Twemproxy 改造过程中,直接参考 Nginx 的信号量机制和 channel 机制 ( 依靠 socketpair ) 来实现父子进程见通信。

Master 进程通过信号量机制来检测子进程是否异常,从而快速直接的反应出来;

此外,借助 socketpair,封装出 channel 接口来完成父子进程见异步通信,master 进程依靠该机制来统计子进程的各种统计信息并汇总,

通过获取来自 master 的汇总信息来判断整个 Twemproxy 中间件的稳定性、可靠性。

配置下发过程:

主进程接收实时配置信息,然后通过 channel 机制发送给所有的 worker 进程,各个 worker 进程收到配置信息后应答给工作进程。流程如下:

获取监控信息流程和配置下发流程基本相同,master 进程收到各个工作进程的应答后,

由 master 进程做统一汇总,然后发送给客户端。

4.3.4 如何减少 Worker 进程在不同 CPU 切换的开销

CPU 亲和性(affinity) 就是进程要在某个给定的 CPU 上尽量长时间地运行而不被迁移到其他处理器的倾向性。

Linux 内核进程调度器天生就具有被称为软 CPU 亲和性(affinity) 的特性,这意味着进程通常不会在处理器之间频繁迁移。这种状态正是我们希望的,因为进程迁移的频率小就意味着产生的负载小。具体参考 sched_setaffinity 函数。

旁白:简单来说就是利用linux内核提供给用户的API,强行将进程或者线程绑定到某一个指定的cpu核运行。

4.3.5 Worker 进程异常如何减少对业务的影响?

在实际线上环境中,经常出现这样的情况:某个多线程服务跑几个月后,因为未知原因进程挂了,最终造成整个服务都会不可用。

master+ 多 worker 的多进程模型就体现了它的优势,代码有隐藏的并且不容易触发的 Bug。某个时候如果某个请求触发了这个 Bug,则处理这个请求的 worker 进程会报错误退出。

但是其他 worker 进程不会受到任何的影响,也就是说如果一个改造后的 Twemproxy 起了 20 个 worker 进程。

某个时候一个隐藏 Bug 会被某个请求触发,则只有处理该请求的进程段错误异常,其他

19 个进程才不会受到任何影响。该隐藏 Bug 触发后影响面仅为 5%,

如果是多线程模型,则影响面会是 100%。(只要一个线程挂,整个进程就推出。)如果某个 worker 进程挂了,master 父进程会感知到这个信号,然后重新拉起一个 worker 进程,实现瞬间无感知”拉起”恢复。(数据可能找不到)

以下为模拟触发异常段错误流程:

手工kill work进程

如上图所示,杀掉 31420 worker 进程后,master 进程会立马在拉起一个 31451 工作进程,实现了快速恢复。

多进程异常,自动 “拉起” 功能源码,可以参考如下 Demo:https://github.com/y123456yz/reading-code-of-nginx-1.9.2/blob/master/nginx-1.9.2/src/demo.c

5. 网络优化

5.1 网卡多队列

在实际上线后,发现软中断过高,几乎大部分都集中在一个或者几个 CPU 上,严重影响客户端连接和数据转发,QPS 上不去,时延抖动厉害。

RSS(Receive Side Scaling)是网卡的硬件特性,实现了多队列,可以将不同的流分发到不同的 CPU 上。支持 RSS 的网卡,通过多队列技术,每个队列对应一个中断号,通过对每个中断的绑定,可以实现网卡中断在 CPU 多核上的分配,最终达到负载均衡的作用。

1. 5.2 可怕的 40ms

原生 Twemproxy 在线上跑得过程中,发现时延波动很大,抓包发现其中部分数据包应答出现了 40ms 左右的时延,拉高了整体时延抓包如下 ( 借助 tcprstat 工具 ):

解决办法如下:

在 recv 系统调用后,调用一次 setsockopt 函数,设置 TCP_QUICKACK。代码修改如下:

允许小包发送

6. Twemproxy 改造前后性能对比(时延、QPS 对比)

6.1 线上真实流量时延对比

6.1.1 改造前线上 Twemproxy 集群时延

线上集群完全采用开源 Twemproxy 做代理,架构如下:

未改造前线上 Twemproxy+Memcache 集群,QPS=5000~6000,长连接,客户端时延分布如下图所示:

长连接

在 Twemproxy 机器上使用 tcprstat 监控到的网卡时延如下:

从上面两个图可以看出,采用原生 Twemproxy 时延高,同时抖动厉害。

6.1.2 参照 Nginx 改造后的 Twemproxy 时延

线上集群一个 Twemproxy 采用官方原生 Twemproxy,另一个为改造后的 Twemproxy,

其中改造后的 Twemproxy 配置 worker 进程数为 1,保持和原生开源 Twemproxy 进程数一致,

架构如下:

Twemproxy 配置 worker 进程数为 1

替换线上集群两个代理中的一个后 ( 影响 50% 流量),长连接、QPS=5000~6000、客户端埋点监控时延分布如下:

替换两个 proxy 中的一个后,使用 tcprstat 在代理集群上面查看两个代理的时延分布如下:

原生 Twemproxy 节点机器上的时延分布:

另一个改造后的 Twemproxy 节点机器上的时延分布:

总结:

替换线上两个 proxy 中的一个后,客户端时间降低了一倍,如果线上集群两个代理都替换为改造后的 Twemproxy,客户端监控时延预计会再降低一倍,总体时延降低 3 倍左右。

此外,从监控可以看出,改造后的 Twemproxy 时延更低,更加稳定,无任何波动。

6.2 参考 Nginx 多进程改造后的 Twemproxy 线下压测结果 ( 开启 Reuseport 功能 )

监听同一个端口,数据长度 100 字节,压测结果如下:

– Linux 内核版本:Linux-3.10

– 物理机机型: M10 ( 48 CPU )

多进程监听同一个端口,数据长度 150 字节,压测结果如下:

– Linux内核版本:Linux-3.10

– 物理机机型: TS60 ( 24 CPU )

7. 总结

7.1 多进程、多线程机制选择

选择参照 Nginx 多进程机制,而不选择多线程实现原因主要有:

多进程机制无锁操作,实现更容易。多进程的代理,整个 worker 进程无任何锁操作,性能更好。如果是多线程方式,如果代码出现 Bug 段错误,则整个进程挂掉,整个服务不可用。而如果是多进程方式,因为 Bug 触发某个 worker 进程段错误异常,其他工作进程不会受到如何影响,20 个 worker 进程,如果触发异常,同一时刻只有 1/20 的流量受到影响。而如果是多线程模式,则 100% 的流量会受到影响。worker 进程异常退出后,master 进程立马感知拉起一个新进程提供服务,可靠性更高。配置热加载、程序热升级功能实现更加容易。

7.2 参照 Nginx 改造后的 Twemproxy 特性

支持 Nginx 几乎所有的优秀特性,同时也根据自己实际情况新增加了自有特性:

master+ 多 worker 进程机制适配所有 Linux 内核版本,内核低版本惊群问题避免支持quic_ack 支持reuser_port 适配支持worker 进程异常,master 进程自动拉起功能支持90%、95%、98%、100% 平均时延统计功能支持Memcache 单机版、集群版支持Redis 单机版、集群版支持二进制协议、文本协议同时支持Redis、Memcache 集群在线扩容、缩容、数据迁移支持,扩缩容、数据迁移过程对业务无任何影响。多租户支持,一个代理可以接多个 Memcache、Redis 集群,并支持混部。mget、gets、sets 等批量处理命令优化处理慢响应日志记录功能支持内存参数实时修改支持详细的集群监控统计功能CPU 亲缘性自添加内存配置动态实时修改

7.3后期计划

添加如下功能:

配置文件热加载支持代码热升级功能支持

7.4 长远规划展望

抽象出一款类似 Nginx 的高性能代理软件,Nginx 支持 HTTP 协议,

我们的支持 TCP 协议代理,覆盖 Nginx 所有功能,包括前面提到的所有功能,同时支持模块化开发。这样,很多的 TCP 协议代理就无需关心网络架构底层实现,只需要根据需要开发对应的协议解析模块,和自己关心的统计、审计等功能功能,降低开发成本。

现有开源的中间件,很大一部分都是 TCP 的,有自己的私有 TCP 协议,把这个抽象出来,开发成本会更低。