Swoole基于IPC通信的跨进程连接池

2021年5月21日 517点热度 0人点赞 0条评论
图片

导读

池化技术的核心思想是空间换时间,使用预先创建好的对象来减少频繁创建对象的性能开销,降低对象的使用成本,其一直是高并发系统设计必不可少的利器,但在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

为了解决Task子进程、UserProcess的同步阻塞问题,又引入了Socket通信,不再使用swoole封装的原生进程间通信方式,改用Socket通信,其通信方式为UnixSocket,进程间100万次读写只需要1.2秒就可以完成。部分源码如图8所示,每次进程间通信都会有一个句柄fd,把fd通过swoole_event_add方法添加到epoll事件监听,底层会自动将该sock设置为非阻塞模式,从而实现进程间通信异步非阻塞。这样最核心的问题就解决了,再看其它选项,Task子进程业务耦合度非常高,Task子进程内需要完成很多其它任务,包括心跳上报、业务日志、统计打点等等,不能将连接池这种核心的功能也放到Task子进程里处理.我们需要UserProcess创建一个独立的进程,专门负责维护所有连接池(mysql、wredis、wtable、scf、http、tcp等),不受其它进程干扰,同时由于改用Socket通信,调用方不再受语言限制,只需要建立socket通信就可以调用连接池,因此FPM也能用该连接池。

上线有益效果

图片

图9

如图9所示,通过数据对比,可以看出CPU使用率得到了显著降低,使用率由17%降低到12%,接口平均耗时由12.31ms降低到10.09ms,而且目前线上服务只在一个接口中使用了跨进程wredis连接池,相信随着接口中跨进程连接池的普及及其他种类连接池(mysql、wtable、scf、http等)的陆续实践,CPU使用率和服务响应时间还会继续降低,收益将更加明显。

作者简介:
李旭东:58同城后端工程师。2014年毕业,工作7年,先后就职于百度、58,负责php\go\C++相关的后端开发工作。

推荐阅读

Flutter代码覆盖率研究

Swift Hook新思路--虚函数表

58信息安全—营销反作弊业务的算法实践

58同城商业生态与智能发展中心反作弊测试平台建设

图片

36240Swoole基于IPC通信的跨进程连接池

这个人很懒,什么都没留下

文章评论