Swoole基于IPC通信的跨进程连接池
导读
池化技术的核心思想是空间换时间,使用预先创建好的对象来减少频繁创建对象的性能开销,降低对象的使用成本,其一直是高并发系统设计必不可少的利器,但在php语言中,我们却很少提到这个技术,原因何在?让我们来一探究竟,捡起这把利剑,披荆斩棘。
高并发系统设计
池化技术的核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用成本。初始化的时候,先建立一些资源。根据相应的策略,使用时拿出来,不用时候再放入池中,减少使用过程中重复的创建和销毁,以达到性能提升的目的。线程、内存、数据库的连接对象都是资源,在程序中,当我们创建一个线程或者在堆上申请一块内存的时候都涉及到很多的系统调用,也是非常消耗CPU的。如果我们的程序需要很多类似的工作线程或者需要频繁地申请释放小块内存,在没有对这方面进行优化的情况下,这部分代码很可能会成为影响我们整个程序性能的瓶颈。
图1
现有技术及其缺陷
进程内连接池:
在一个进程内创建拥有一定数量的长连接,让用完的连接不会关闭,重复使用。
缺陷:
① 每个进程都要维护一套连接池,一个master进程如果创建N个work进程,那么相应的就要维护N个连接池,而且在work进程销毁再重建 时,需要重新初始化连接池,造成资源的浪费。
② 随着连接池种类(mysql、wredis、wtable、scf、http、tcp等)的增加,需要维护的连接池也以n * N数量级的方式增加,资源浪费会越来越严重。
③ 对于FPM这种引擎,进程内连接池没有太大的意义,因为每次请求都会初始化一次连接池,用完后立马销毁释放,失去了连接池重复使用长连接的初衷。
PHP如何池化
关于PHP-FPM
图2
如图2所示,在 PHP-FPM 模式下,一个请求的生命周期注定只有 1 次,也就是说,从 FPM 请求到解析 PHP 脚本,到 FPM 的 Zend 虚拟机分配资源执行,再到最后的处理结束PHP-FPM会回收这次请求的所有资源,这种方式一是为了让开发不需要关心资源的回收处理,所以我们可能没怎么关心过网络的关闭、文件描述符的关闭等等,二是为了减少内存溢出的情况,如果在这种模式下,我们实现了连接池,也意味着请求结束,连接池消失,做了一次无用功而已。
短连接VS长连接
图3(来源网络)
图4
在老版php版scf客户端的源码中,我们可以看到客户端与服务方建立通信的方式都是短连接,然后我们可以通过命令行查看线上机器连接状态为TIME_WAIT(此状态含义就不做过多赘述)的数量高达35160个,这就意味35160个临时端口被占用,我们知道一台机器的临时端口数最多为65535,假如临时端口被大量占用直至占满,后果大家可以想象,因此减少临时端口的占用,对一个高并发的系统十分有必要。
解决方案是什么?将scf客户端与服务端的通信方式由短连接改为长连接,然后我们就可以得到预想的结果,TIME_WAIT的连接数由35160降到1392 (如图5所示),临时端口占用量得到大幅降低,效果明显,长连接的使用让我们的系统受益匪浅,那我们能不能让长连接得到更极致的使用?当然可以!!!
图5
跨进程连接池
通过对scf客户端短连接->长连接的改造,举一反三,我们能否把单个work进程中用到长连接的地方,建立一个公共连接池,这样的话,长连接也省去了创建、释放的资源消耗,即短连接->长连接->连接池,到这一步我想大家应该都能很容易的实现。我们能否再跨一步,让所有的work进程都共用一个连接池,即短连接->长连接->连接池->跨进程连接池(如图6),这样话,在服务启动的时候,我们可能只需要初始化几个长连接就能提供给所有调用方使用,与服务端进行通信,这样可以把临时端口数降低到最低,同时通过复用TCP连接来减少创建和释放时间也是最优的。
图6
Swoole的IPC通信
关于Swoole
图7(引用swoole官方文档)
Swoole进程间的关系可以理解为 Reactor 就是 nginx,Worker 就是 PHP-FPM。Reactor 线程异步并行地处理网络请求,然后再转发给 Worker 进程中去处理。Reactor 和 Worker 间通过 unixSocket 进行通信。
IPC(Inter-Process Communication):同一台主机上两个进程间通信。目前的方式主要有管道、消息队列、信号、共享内存、套接字(socket)。
Swoole 下使用了 2 种方式 Unix Socket 和 sysvmsg(消息队列)。
Unix Socket:套接字的 API (socket,bind,listen,connect,read,write,close 等)和TCP/IP 不同的是不需要指定 ip 和 port,而是通过一个文件名来表示 (例如 FPM 和 Nginx 之间的 /tmp/php-fcgi.sock)。
Unix Socket是 Linux 内核实现的全内存通信,无任何 IO 消耗。
Sysvmsg:即 Linux 提供的消息队列,这种 IPC 方式通过一个文件名来作为 key 进行通讯,这种方式非常的不灵活,实际项目使用的并不多。防止丢数据,如果整个服务都挂掉,再次启动队列中的消息也在。外部投递数据。
基于IPC通信的跨进程
针对现有技术的缺陷,要解决的技术难点就是如何保证N个work进程公用一个连接池,这样就能做到资源的合理利用,N个work进程只需要维护一套连接池即可。N个work进程公用一个连接池,就涉及到进程间通信的问题,进程间通信目前有同步阻塞、同步非阻塞、异步阻塞、异步非阻塞四种方式,很明显为了支持高并发的需求,我们只能选择异步非阻塞方式,如何实现进程间通信的异步非阻塞就是本次方案的技术难点。
目前swoole能够实现进程间通信的方式共三种Task子进程、UserProcess用户自定义进程、ProcessPool进程池,如表1所示,我们从以下6个方面,分析它们之间的优缺点:
表1
Task子进程 |
UserProcess |
ProcessPool |
|
归属进程 |
Manager进程 |
Manager进程 |
独立进程 |
通信方式 |
timewait() |
Recv() |
unixsocket |
是否阻塞 |
同步阻塞 |
同步阻塞 |
异步非阻塞 |
使用方法 |
简单 |
复杂 |
简单 |
耦合度 |
高 |
低 |
低 |
进程数 |
1 |
1 |
N |
通过上面表1的数据,可以看到Task子进程、UserProcess是同步阻塞模式,可以直接PASS,高并发时会严重影响接口的响应时间;再看ProcessPool连接池,它的缺点也是显而易见,需要创建N个进程,这与我们的初衷是相反的,本来就是为了把N个进程的连接池优化成1个,现在又变成N个,也得PASS,这样看,三种方式都不能满足需求。
图8
上线有益效果
图9
如图9所示,通过数据对比,可以看出CPU使用率得到了显著降低,使用率由17%降低到12%,接口平均耗时由12.31ms降低到10.09ms,而且目前线上服务只在一个接口中使用了跨进程wredis连接池,相信随着接口中跨进程连接池的普及及其他种类连接池(mysql、wtable、scf、http等)的陆续实践,CPU使用率和服务响应时间还会继续降低,收益将更加明显。