ljfrocky

ljfrocky 查看完整档案

广州编辑  |  填写毕业院校酷狗计算机科技有限公司  |  PHP开发工程师 编辑填写个人主网站
编辑

90后程序猿,PHPer

个人动态

ljfrocky 发布了文章 · 11月18日

PHP进程卡死和MySQL超时时间的设置方法

前言

最近线上一台服务器的nginx总是会有一部分请求(不是所有请求)报upstream timed out (110: Connection timed out) while connecting to upstream的错误,看起来像是后端的phpcgi进程出问题了,但如果phpcgi进程有问题,不是应该所有请求都会报错才对么,于是展开排查。

排查原因

在我们服务器上,PHP是使用9006端口进行监听的,执行netstat -an | grep 9006命令查看相关连接的网络状态,看到有一部分连接处于CLOSE_WAIT状态:

tcp        0      0 10.0.0.188:9006          10.0.0.52:37316         CLOSE_WAIT  
tcp        0      0 10.0.0.188:9006          10.0.0.52:37292         CLOSE_WAIT  
tcp        0      0 10.0.0.188:9006          10.0.0.52:37300         CLOSE_WAIT  
tcp        0      0 10.0.0.188:9006          10.0.0.52:37302         CLOSE_WAIT  
tcp        0      0 10.0.0.188:9006          10.0.0.52:37234         CLOSE_WAIT

奇怪的是,这些连接一直停留在CLOSE_WAIT的状态,不会变化,所以应该就是这些连接导致PHP进程被卡死,无法处理新的请求,从而导致部分请求报Connection timed out错误的。

根据TCP连接的四次挥手过程,CLOSE_WAIT状态是连接被关闭方才会出现的。nginx调用PHP,PHP响应太慢导致nginx超时,继而nginx主动关闭跟PHP的TCP连接,因此PHP进程是被关闭方,这个没啥问题。但PHP进程在收到nginx的关闭请求后,应该也跟着关闭才对,但实际没有,而是停留在了CLOSE_WAIT状态,说明PHP被某些东西卡住了,没办法关闭连接。

于是使用strace -p PHP进程ID查看PHP卡在了什么地方,等了一两分钟,strace命令什么东西都没输出,说明PHP进程是卡在了某个系统调用:
image

再使用lsof -np PHP进程ID命令查看进程打开了哪些文件资源,发现有一个状态为ESTABLISHED的mysql的连接:
image.png

猜想会不会是mysql导致PHP卡死呢,于是根据显示的连接端口号10465,到mysql服务器上查看这个端口的连接情况,发现居然没有这个端口的TCP连接。这就神奇了,这里我只能猜测是:PHP成功跟mysql服务器建立TCP连接,但由于丢包或者防火墙拦截等奇怪的原因,PHP没有收到mysql的greeting packet,于是导致PHP一直在这里空等。后面mysql服务器主动断开TCP连接,但发送的FIN包也被拦截了,导致收不到PHP的ACK回应,于是mysql继续释放了这个连接。于是就出现了这个连接在PHP端是ESTABLISHED状态,但在mysql端却不存在这个连接的情况。

确认和模拟测试

为了确认到底是不是mysql连接卡死了PHP,使用gdb -p PHP进程ID进行调试,上面lsof命令显示mysql的连接对应的文件描述符是5u,gdb里输入命令call close(5)强制把这个连接关闭,关闭后PHP进程的CLOSE_WAIT状态马上消失了,证明确实是这个连接卡住了PHP。

接着我尝试模拟“成功建立TCP连接,但收不到mysql greeting packet”的这种情景,看PHP会不会卡死,测试代码如下:

$mysql = new mysqli();
$mysql->real_connect('45.113.192.102', 'root', 'xxx', 'xxx', 80);

45.113.192.102是百度的一台web服务器,跟80端口肯定是能建立TCP连接的,但它不是mysql服务器,所以建立连接后,也肯定不会发送greeting packet给PHP,正符合我要测试的场景。执行这段代码后,PHP确实会卡住在real_connect处,而且不会超时,跟线上情况一模一样。

解决方法

那怎样才能针对这种场景设置一个超时时间呢,在这里我走了弯路,曾尝试使用set_time_limitdefault_socket_timeoutMYSQLI_OPT_CONNECT_TIMEOUT等参数来设置超时时间,但均没有生效,一度使我十分苦恼。后面才意识到这种场景的超时是属于读写超时,不是连接超时,因此使用MYSQLI_OPT_CONNECT_TIMEOUT来设置是无效的,这参数控制的是连接超时。而set_time_limit控制的仅是PHP脚本自身的执行时间,不包括系统调用、数据库操作所消耗的时间,因此也不会生效,这一点在PHP文档里也有说明:
image.png

下面是正确答案,有两种方法:
1)修改mysqlnd.net_read_timeout配置项
php.ini里添加一个mysqlnd.net_read_timeout的配置项即可,例如mysqlnd.net_read_timeout = 60就代表将mysql的读写超时时间设置为60秒

mysqlnd.net_read_timeout配置项在PHP 7.2(含)之后的版本,可以在代码里通过ini_set函数来设置,旧的版本只能在php.ini里设置

2)通过MYSQLI_OPT_READ_TIMEOUT常量来配置,这个常量在PHP 7.2(含)版本才添加,因此需要PHP版本大于等于7.2,代码示例:

$mysql = new mysqli();
$mysql->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5); // 设置连接超时未5秒
$mysql->options(MYSQLI_OPT_READ_TIMEOUT, 60); // 设置读写超时为60秒
$mysql->real_connect('45.113.192.102', 'root', 'xxx', 'xxx', 80);

这种方法相比第一种影响范围更小,它只影响当前连接,而第一种因为是通过修改配置文件实现的,因此它会影响所有使用同一配置文件的其它PHP程序。

设置好读写超时后,重新执行测试代码,也确实没有卡死了,60秒后就会超时报错。

总结

针对mysql连接,一个完整的超时设置,应该同时设置连接超时和读写超时,如果仅仅设置了连接超时,那么在一些特殊情景下,PHP进程是有可能会被卡死的

查看原文

赞 2 收藏 1 评论 0

ljfrocky 提出了问题 · 11月18日

PHP里mysqlnd.net_read_timeout和default_socket_timeout的区别?

在PHP里,如果想限制一个SQL语句的最长执行时间,可以通过配置php.ini里的mysqlnd.net_read_timeout的值来实现,经测试,配置mysqlnd.net_read_timeout = 10后,执行一个时间超过10秒的语句,PHP确实会超时报错。但如果配置的是default_socket_timeout = 10,PHP并不会报错,看起来mysql的读写超时并不受default_socket_timeout参数控制。

但是PHP文档里(链接)的描述是:

PHP, by default, sets a read timeout of 60s for streams. This is set via php.ini, default_socket_timeout. This default applies to all streams that set no other timeout value. mysqlnd does not set any other value and therefore connections of long running queries can be disconnected after default_socket_timeout seconds resulting in an error message 2006 - MySQL Server has gone away.

描述里说SQL语句执行时间超过default_socket_timeout的话,PHP会报错,文档描述跟实际测试情况不一样,令我比较迷惑。

问题:
1)mysql会不会受default_socket_timeout的影响?
2)mysqlnd.net_read_timeoutdefault_socket_timeout这2个配置项有什么关系和区别?

关注 1 回答 0

ljfrocky 回答了问题 · 11月16日

解决PHP连接mysql时卡死

找到解决方法了,这个场景的超时是属于读写超时,不是连接超时,所以MYSQLI_OPT_CONNECT_TIMEOUT配置的超时是不会生效的。读写超时需要通过php.ini里的mysqlnd.net_read_timeout参数来配置才能生效。

注:PHP 5.3(含) ~ PHP 7.1(含) 只能通过修改php.ini文件来调整 net_read_timeout 的值,PHP 7.2(含)之后的版本可以在代码里通过 ini_set 函数来调整,例如:ini_set('mysqlnd.net_read_timeout', 1)

关注 1 回答 1

ljfrocky 赞了文章 · 11月14日

面试官:select、poll、epoll有何区别?我:阿巴阿巴...

前言

很多朋友对 select、poll、epoll很头疼,因为搞不清三者的区别和内在逻辑,所以今天就来给大家详细说说,一家之言,不对之处还请指正。

大家可以进群973961276一起聊聊技术吹吹牛,每周都会有几次抽奖送专业书籍的活动,奖品不甚值钱,但也算个彩头。

缺乏项目实战经验和想跳槽涨薪或是自我提升的朋友看这里>>c/c++ 项目实战/后台服务器开发高级架构师


(1)、select==>时间复杂度O(n)

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)、poll==>时间复杂度O(n)

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)、epoll==>时间复杂度O(1)

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。  

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现

select:

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

1、 单个进程可监视的fd数量被限制,即能监听端口的大小有限。

      一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

2、 对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:

       当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。                   

2、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll:

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

epoll为什么要有EPOLLET触发模式?

如果采用EPOLLLT模式的话,系统中一旦有大量你不需要读写的就绪文件描述符,它们每次调用epoll_wait都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率.。而采用EPOLLET这种边沿触发模式的话,当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

epoll的优点:

1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;
即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

3、 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
select、poll、epoll 区别总结:

1、支持一个进程所能打开的最大连接数

select

单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32_32,同理64位机器上FD_SETSIZE为32_64),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

poll

poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的

epoll

虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接

2、FD剧增后带来的IO效率问题

select

因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

poll

同上

epoll

因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

select

内核需要将消息传递到用户空间,都需要内核拷贝动作

poll

同上

epoll

epoll通过内核和用户空间共享一块内存来实现的。

总结:

综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善 


  今天对这三种IO多路复用进行对比,参考网上和书上面的资料,整理如下:

1、select实现

select的调用过程如下所示:

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间。

总结:

select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

2 poll实现

  poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

3、epoll

  epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

  对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

总结:

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

查看原文

赞 8 收藏 3 评论 0

ljfrocky 提出了问题 · 11月13日

解决PHP连接mysql时卡死

前言

最近线上一台服务器偶尔会出现连接mysql的时候PHP进程卡死的情况,排查后发现,PHP当时和mysql之间的tcp连接已经建立,但怀疑tcp连接建立后,mysql没有发送greeting packet给PHP,然后就导致PHP一直在等待。

模拟测试

然后我写了下面一段测试代码,为了模拟上述的那种情况,数据库地址和端口是使用百度主页的地址和端口,然后在浏览器里访问运行,代码如下:

$mysql = new mysqli();
$mysql->options(MYSQLI_OPT_CONNECT_TIMEOUT, 5);
$mysql->real_connect('www.baidu.com', 'root', 'xxx', 'xxx', 80);

代码里已经有使用MYSQLI_OPT_CONNECT_TIMEOUT设置了连接超时时间为5秒,但并没有生效,运行后PHP还是卡死了,不会超时,跟线上环境的情况一模一样。请问有什么方法可以设置超时时间吗?

测试环境

windows 10
PHP 5.6

关注 1 回答 1

ljfrocky 收藏了文章 · 9月30日

PHP回顾之协程

转载请注明文章出处: https://tlanyan.me/php-review...

PHP回顾系列目录

PHP自5.5起引入了生成器(Generator),基于其可实现协程编程。本文先回顾生成器,然后过渡到协程编程。

yield与生成器

生成器

生成器是一种数据类型,实现了iterator接口。不能通过new得到生成器实例,也没有获取生成器实例的静态方法。得到生成器实例的唯一办法是调用生成器函数(包含yield关键字的函数)。调用生成器函数直接返回一个生成器对象,生成器运行时函数内的代码才开始执行。

先上代码直观感受一下yield与生成器:

# generator1.php
function foo() {
    exit('exit script when generator runs.');
    yield;
}

$gen = foo();
var_dump($gen);
$gen->current();

echo 'unreachable code!';

# 执行结果
object(Generator)#1 (0) {
}
exit script when generator runs.

foo函数包含yield关键字,变身为生成器函数。调用foo不会执行函数体中的任何代码,而是返回一个生成器实例。生成器运行后,foo函数内的代码执行,脚本结束。

如其名,生成器可以用来生成数据。只是其生成数据的方式与其他函数不一样:生成器通过yield返回数据,而非return; yield返回数据后,生成器函数不会销毁,只是暂停运行,未来可以从暂停处恢复运行;生成器运行一次,(只)返回一个数据,多次运行就返回多个数据;不调用生成器获取数据,生成器内的代码就躺着不动,所谓动次打次,说的就是生成器生成数据的样子。

生成器实现了迭代器接口,获取生成器数据可以用foreach循环或手工current/next/valid。如下代码演示数据生成和遍历:

# generator2.php
function foo() {
  # 返回键值对数据
  yield "key1" => "value1";
  $count = 0;
  while ($count < 5) {
    # 返回值,key自动生成
    yield $count;
    ++ $count;
  }
  # 不返回值,相当于返回null
  yield;
}

# 手动获取生成器数据
$gen = foo();
while ($gen->valid()) {
  fwrite(STDOUT, "key:{$gen->key()}, value:{$gen->current()}\n");
  $gen->next();
}

# foreach 遍历数据
fwrite(STDOUT, "\ndata from foreach\n");
foreach (foo() as $key => $value) {
    fwrite(STDOUT, "key:$key, value:$value\n");
}

yield

yield关键字是生成器的核心,其让普通函数异化(进化)为生成器函数。yield有“让出”的意思,程序执行到yield语句会暂停执行,让出CPU并将控制权返回到调用者,下次执行时从中断点继续执行。控制权返回到调用者时,yield语句可以携带值返回给调用方。generator2.php脚本演示了yield返回值的三种形式:

  1. yield $key => $value: 返回数据的key和value;
  2. yield $value: 返回数据,key由系统分配;
  3. yield: 返回null值,key由系统分配;

yield让函数可以随时暂停、继续执行,并返回数据给调用方。如果继续执行时需要外部数据,这个工作由生成器的send函数提供:出现在yield左边等号的变量会接收send传来的值。看一个常见的send函数使用样例:

function logger(string $filename) {
  $fd = fopen($filename, 'w+');
  while($msg = yield) {
    fwrite($fd, date('Y-m-d H:i:s') . ':' . $msg . PHP_EOL);
  }
  fclose($fd);
}

$logger = logger('log.txt');
$logger->send('program starts!');
// do some thing
$logger->send('program ends!');

send让生成器之间和外部有双向数据通信的能力:yield返回数据;send提供继续运行的支撑数据。由于send让生成器继续执行,这个行为与迭代器的next接口类似,next相当于send(null)

其他

  1. $string = yield $data;的表达式在PHP7前不合法,需要加括号:$string = (yield $data);
  2. PHP5生成器函数不能return值,PHP7后可以return值,并通过生成器的getReturn获取返回的值。详情参考返回值的RFC:https://wiki.php.net/rfc/gene...
  3. PHP7新增了yield from语法,实现了生成器委托,详情请参考其RFC: https://wiki.php.net/rfc/gene...
  4. 生成器是单向迭代器,开动后不能调用rewind

总结

相对于其他迭代器,生成器具有性能开销小、编码容易的特点。其作用主要体现在三个方面:

  1. 数据生成(生产者),通过yield返回数据;
  2. 数据消费(消费者),消费send传来的数据;
  3. 实现协程。

关于PHP中的生成器及基本用法,建议看看 2gua 大佬的博文:PHP之生成器,生动有趣且易懂。

协程编程

协程(coroutine)是随时可中断、恢复执行的子程序,yield关键字让函数拥有这种能力,所以可以用于协程编程。

进程、线程和协程

线程归属于进程,一个进程可有多个线程。进程是计算机分配资源的最小单位,线程是计算机调度执行的最小单位。进程和线程均由操作系统调度。

协程可以看成“用户态的线程”,需要用户程序实现调度。线程和进程由操作系统调度“抢占式”交替运行,协程主动让出CPU“协商式”交替运行。协程十分的轻量,协程切换不涉及线程切换,执行效率高,数目越多,越能体现协程的优势。

生成器和协程

生成器实现的协程属于无栈协程(stackless coroutine),即生成器函数只有函数帧,运行时附加到调用方的栈上执行。不同于功能强大的有栈协程(stackful coroutine),生成器暂停后无法控制程序走向,只能将控制权被动的归还调用者;生成器只能中断自身,不能中断整个协程。当然,生成器的好处便是效率高(暂停时只需保存程序计数器即可),实现简单。

协程编程

说到PHP中的协程编程,相信大部分人已经看过鸟哥转载(翻译)的这篇博文:在PHP中使用协程实现多任务调度。原文作者 nikic 是PHP的核心开发者,生成器功能的倡议者和实现人。想深入了解生成器及基于其的协程编程,nikic关于生成器的RFC和鸟哥网站上的文章必读。

nikic的文章,生成器部分好懂,看完后用yield写个xrange类似函数肯定毫无压力。为什么一进入协程,就有点懵逼呢?

懵逼评论

先看看基于生成器的协程工作方式:协程协作式工作,即协程之间通过主动让出CPU达到多任务交替运行(即并发多任务,但不是并行);一个生成器可看成一个协程,执行到yield语句,让出CPU控制权回到调用方,调用方继续执行其他协程或其他代码。

再来看鸟哥博客理解的难点何在。协程非常轻量,一个系统中可以同时存在成千上万个协程(生成器)。而操作系统不会对协程调度,安排协程执行的工作就落到开发者身上。部分人看不懂鸟哥文章的协程部分,是因为里面说协程编程少(写协程主要就是写生成器函数),而是花笔墨实现了一个协程的调度器(scheduler或者kernel):模拟了操作系统,对所有协程进行公平调度。PHP开发一般的思维是:我写了这些代码,PHP引擎会调用我这些代码得到预期结果。而协程编程不仅要写干活的代码,还要写指导这些代码什么时候干活的代码。没有很好的把握作者的思维,理解起来自然会难一些。需要自行调度,这是生成器协程相对于原生协程(async/await形式)的一个缺点。

知道了协程是怎么回事,那么它能用来干什么?协程自行让出CPU来协作高效利用CPU,让出的时机当然应该是程序阻塞时。什么地方会让程序阻塞呢?用户态的代码鲜有阻塞,阻塞主要是系统调用。而系统调用的大头是IO,所以协程的主要应用场景在网络编程。为了让程序高性能、高并发,程序应该异步执行不能阻塞。既然异步执行,就需要通知和回调,写回调函数避免不了“回调地狱(callback hell)”的问题:代码可读性差,程序执行流程散落在层层回调函数中等。解决回调地狱的方式主要有两种:Promise和协程。协程能以同步的方式编写代码,在高性能网络编程(IO密集型)中是推荐的。

再回过头看PHP中的协程编程。PHP中基于生成器实现实现协程编程,优先推荐使用RecoilPHPAmp等协程框架。这些框架已经写好了调度器,在其上开发直接写生成器函数,内核会自动调度执行(想让一个函数以协程方式调度执行,在函数体内加上yield即可)。如果不想用yield方式进行协程编程,推荐swoole或其衍生框架,能做到类似golang的协程编程体验,又能享受PHP的开发效率。

如果想用原生态的做PHP协程编程,类似鸟哥博客中的调度器必不可少。调度器调度协程执行,协程中断后控制权又回到调度器中。所以调度器应该总是在主(事件)循环中,即CPU不在执行协程,就应当在执行调度器的代码。无协程运行时,调度器应当自我阻塞避免消耗CPU(鸟哥博客中使用了内置的select系统调用),等待事件到来再执行相应的协程。程序运行期间,除了调度器阻塞,协程在运行过程中不应该调用阻塞API。

总结

在协程编程中,yield的主要作用是将控制权转让,无需纠结于其返回值(基本上yield返回的值会在下次执行时直接send过来)。重点应当关注控制权转让的时机,以及协程的运作方式。

另外需要说明一点,协程和异步没有多大关系,还要看运行环境支撑。常规的PHP运行环境,即使用了promise/coroutine,也还是同步阻塞的。再牛逼的协程框架,sleep一下也不好使了。作为类比,即使JavaScript不使用promise/async这些技术,也是异步非阻塞的。

通过生成器和Promise,能实现类似于await的协程编程,相关代码在Github上很多,本文不再给出。

总结

本文先介绍了生成器的概念,重点是yield的用法及生成器的接口。协程部分则简要说了协程的原理,以及PHP协程编程中应当注意的事项。

感谢阅读,欢迎指正!

参考

  1. http://php.net/manual/zh/lang...
  2. http://php.net/manual/zh/clas...
  3. https://wiki.php.net/rfc/gene...
  4. https://wiki.php.net/rfc/gene...
  5. https://zhuanlan.zhihu.com/p/...
  6. http://www.laruence.com/2015/...
  7. https://medium.com/async-php/...
  8. https://blog.kghost.info/2011...
查看原文

ljfrocky 收藏了文章 · 9月27日

PHP yield 高级用法——同步编码,异步执行

开篇

刚开始接触PHPyield 的时候,感觉,yield 是什么黑科技,百度一下:yield——协程,生成器。很多文章都在讲 IteratorGenerater, 蛤~,这东西是 PHP 迭代器的一个补充。再翻几页,就是Go 协程。我出于好奇点开看了下Go 协程, 里面都是 并发线程管道通讯这类字眼,wc,nb, 这tm才是黑科技啊,再回来看PHP,分分钟想转 Go

你能得到

通过这篇文章,你能学会如何写一个简单的socket server,PHP yield 的综合应用,以及协程版的Workerman。

四部曲

yield 语法加入 PHP

yield语法是在版本5.5加入PHP的,配合迭代器使用,功能上就是 流程控制 代码,和gotoreturn 类似。

以下就是官方提供的 yield 小例子,通过执行结果,我们可分析当代码执行到 yield $i 时,他会进行 return $i, 待 echo "$value\n" 后, gotofor ($i = 1; $i <= 3; $i++) {, 对!PHP 的 yield 就是一个能出能进的语法。在z代码中七进七出,把 $i 平平安安得送了出来。

<?php
function gen_one_to_three() {
    for ($i = 1; $i <= 7; $i++) {
        //注意变量$i的值在不同的yield之间是保持传递的。
        yield $i;
    }
}

$generator = gen_one_to_three();
foreach ($generator as $value) {
    echo "$value\n";
}

// output
1
2
...
6
7

我们遇到了什么问题

写代码就是解决问题。我们来看看他们遇到了什么问题:php官方呢,需要言简意赅地把yield介绍给大家。一部分网友呢,需要在有限的资源内完成大文件操作。而我们的鸟哥。面对的一群对当下yield的教程停留于初级而不满意的phper,就以一个任务调度器作为例子,给大家讲了一种yield高级用法。

php.net:生成器语法,
PHP如何读取大文件
风雪之隅:在PHP中使用协程实现多任务调度.

提出问题,再用yield来解答,看到以上答案,我觉得呢,这PHP协程不过如此(Go协程相比 )。

有句话——一个好问题比答案更重要,目前广大网友还没有给yield提出更好,更困难的问题。

yield这个进进出出的语法,很多举例都是再让yield做迭代器啊,或者利用低内存读取超大文本的Excelcsv什么的,再高级就是用它实现一个简单的任务调度器,并且这个调度器,一看代码都差不多。

我来出道题

正如一个好的问题,比答案更有价值
  1. 用PHP实现一个 Socket Server,他能接收请求,并返回Server的时间。

好,这是第一个问题,铺垫。 官方答案

  1. 在原来的代码上,我们加个需求,该Socket Server 处理请求时,依赖其他 Socket Server,还需要有 Client 功能。也就是他能接收请求,向其它Server发起请求。

这是第二个问题,也是铺垫。

  1. 原来的Socket Server同一时间只能服务一个客户,希望能实现一个 非阻塞I/O Socket Server, 这个 Server 内有 Socket Client 功能,支持并发处理收到的请求,和主动发起的请求。要求不用多线程,多进程。

这个问题,还是铺垫,这几个问题很干,大家可以想一想,2,3题的答案,都放在一个脚本里了:nio_server.php

以上这段代码,我列举了一个具体的业务,就是用户请求购物车加购动作, 而购物车服务呢,又需要和 产品服务,库存服务,优惠服务 交互,来验证加购动作可行性。有同步,异步方式请求,并做对比。

后续还有很多代码,我都放gitee链接了。使用方法,见readme.md
  1. 最后一个问题:在PHP中,用同步写代码,程序呢异步执行?需要怎么调整代码。

.
.
.
.
.
.

提示:这个和 PHPyield 语法有关。

.
.
.
.
.
.

再提示:yield 语法特征是什么,进进出出!

看着我们的代码,同步, 异步,进进出出 你想到了什么?

.
.
.
.
.
.

看到代码,同步处理模式下,这三个函数checkInventorycheckProductcheckPromo 时,发起请求,并依次等待返回的结果,这三个函数执行后,再响应客户请求。

异步处理模式下,这三个函数发起请求完毕后,代码就跳出循环了,然后是在select()下的一个代码分支中接收请求, 并收集结果。每次收到结果后判断是否完成,完成则响应客户端。

那么能不能这样:在异步处理的流程中,当 Server 收到 自己发起的 client 有数据响应后,代码跳到 nio_server.php 的 247行呢,这样我们的收到请求校验相关的代码就能放到这里,编码能就是同步,容易理解。不然,client 的响应处理放在 280 行以后,不通过抓包,真的很难理解,执行了第 247 行代码后,紧接着是从 280 行开始的。

.
.
.
.
.
.
.
.

诶~这里是不是有 进进出出 那种感觉了~ 代码从 247 行出去,开始监听发出 Client 响应,收到返回数据,带着数据再回到 247 行,继续进行逻辑校验,综合结果后,再响应给客户端。

用yield来解决问题

基于 yield 实现的,同步编码,"异步"I/OSocket Server 就实现了。代码。

这里 "异步" 打了引号,大佬别扣这个字眼了。 该是非阻塞I/O

不等大家的答案了,先上我的结果代码吧,代码呢都放在这个目录下了。

gitee https://gitee.com/xupaul/PHP-generator-yield-Demo/tree/master/yield-socket

运行测试代码

clone 代码到本地后,需要拉起4个 command 命令程序:

拉起3个第三方服务

## 启动一个处理耗时2s的库存服务
$ php ./other_server.php 8081 inventory 2

## 启动一个处理耗时4s的产品服务
$ php ./other_server.php 8082 product 4

## 监听8083端口,处理一个请求 耗时6s的 promo 服务
$ php ./other_server.php 8083 promo 6

启动购物车服务

## 启动一个非阻塞购物车服务
$ php ./async_cart_server.php 

## 或者启动一个一般购物车服务
$ php ./cart_server.php 

发起用户请求

$ php ./user_client.php

运行结果呢如下,通过执行的时间日志,可得这三个请求是并发发起的,不是阻塞通讯。

在看我们的代码,三个函数,发起socket请求,没有设置callback,而是通过yield from 接收了三个socket的返回结果。

也就是达到了,同步编码,异步执行的效果。

运行结果

非阻塞模式

client 端日志:

通过以上 起始时间结束时间 ,就看到这三个请求耗时总共就6s,也就按照耗时最长的promo服务的耗时来的。也就是说三个第三方请求都是并发进行的。

cart server 端日志:

而 cart 打印的日志,可以看到三个请求一并发起,并一起等待结果返回。达到非阻塞并发请求的效果。

阻塞模式

client 端日志:

以上是阻塞方式请求,可以看到耗时 12s。也就是三个服务加起来的耗时。

cart server 端日志:

cart 服务,依次阻塞方式请求第三方服务,顺序执行完毕后,共耗时12s,当然如果第一个,获第二个服务报错的话,会提前结束这个检查。会节约一点时间。

工作原理

这里就是用到了 yield 的工作特点——进进出出,在发起非阻塞socket请求后,不是阻塞方式等待socket响应,而是使用yield跳出当前执行生成器,等待有socket响应后,在调用生成器的send方法回到发起socket请求的函数内,在 yield from Async::all() 接收数据响应数据搜集完毕后,返回。

当初我在写这篇文章时——NIO、BIO、AIO 与 PHP 实现, 用到很多回调函数,事件轮训,就为了高性能,写出来的代码可读性太差。那么有没有两全其美的办法,当我遇到协程后,我的脑子里就迸发出了这个灵感。

和Golang比一比

考虑到网速原因,我这就放上一个国内教程链接:Go 并发 教程

php的协程是真协程,而Go是披着协程外衣的轻量化线程(“协程”里,都玩上“锁”了,这就是线程)。

我个人偏爱,协程的,觉得线程的调度有一定随机性,因此需要锁机制来保证程序的正确,带来了额外开销。协程的调度(换入换出)交给了用户,保证了一段代码执行连续性(当然进程级上,还是会有换入换出的,除非是跨进程的资源访问,或者跨机器的资源访问,这时,就要用到分布式锁了,这里不展开讨论),同步编码,异步执行,只需要考虑那个哪个方法会有IO交互会协程跳出即可。

和NodeJS比划一下

Javascript 和 PHP 两个脚本语言有很多相似的地方,弱类型,动态对象,单线程,在Web领域生态丰富。不同的是,Javascript在浏览器端一开始就是异步的(如果js发起网络请求只能同步进行,那么你的网页渲染线程会卡住),例如AjaxsetTimeoutsetInterval,这些都是异步+回调的方式工作。

基于V8引擎而诞生的NodeJS,天生就是异步的,在提供高性能网络服务有很大的优势,不过它的IO编码范式么。。。刚开始是 回调——毁掉地狱,后来有了Promise——屏幕竖起来看,以及Generator——遇事不绝yield一下吧,到现在的Async/Await——语法糖?真香!

可以说JS的委员非常勤快,在异步编程范式的标准制定也做的很好(以前我尝试写NodeJS时,几个回调就直接把我劝退了),2009年诞生的NodeJS有点后来居上的意思。目前PHP只是赶上了协程,期待PHP的Async/Await语法糖的实现吧。

PHP yield 使用注意事项

一旦使用上 yield 后,就必须注意调用函数是,会得到函数结果,还是 生成器对象。PHP 不会自动帮你区别,需要你手动代码判断结果类型—— if ($re instanceof \Generator) {}, 如果你得到的是 生成器,但不希望去手动调用 current() 去执行它,那么在生成器前 使用 yield from 交给上游(框架)来解决。

爆改 Workerman

博客写到这,就开始手痒痒了,看到Workerman框架,我在基础上二开,使其能——同步编码,异步执行

代码已放到:PaulXu-cn/CoWorkerman.git

目前还是dev阶段,大家喜欢可以先 体验一波。

$ composer require paulxu-cn/co-workerman

一个简单的单线程 TCP Server

<?php
// file: ./examples/example2/coWorkermanServer.php , 详细代码见github
$worker = new CoWorker('tcp://0.0.0.0:8080');
// 设置fork一个子进程
$worker->count = 1;

$worker->onConnect = function (CoTcpConnection  $connection) {
    try {
        $conName = "{$connection->getRemoteIp()}:{$connection->getRemotePort()}";
        echo PHP_EOL . "New Connection, {$conName} \n";

        $re = yield from $connection->readAsync(1024);
        CoWorker::safeEcho('get request msg :' . $re . PHP_EOL );

        yield from CoTimer::sleepAsync(1000 * 2);

        $connection->send(json_encode(array('productId' => 12, 're' =>true)));

        CoWorker::safeEcho('Response to :' . $conName . PHP_EOL . PHP_EOL);
    } catch (ConnectionCloseException $e) {
        CoWorker::safeEcho('Connection closed, ' . $e->getMessage() . PHP_EOL);
    }
};

CoWorker::runAll();

这里设置fork 一个worker线程,处理逻辑中带有一个sleep()2s的操作,依然不影响他同时响应多个请求。

启动测试程序

## 启动CoWorker服务
$ php ./examples/example2/coWorkermanServer.php start

## 启动请求线程
$ php ./examples/example2/userClientFork.php

运行结果

WX20200619-212005@2x.png

绿色箭头——新的请求,红色箭头——响应请求

从结果上看到,这一个worker线程,在接收新的请求同时,还在回复之前的请求,各个连接交错运行。而我们的代码呢,看样子就是同步的,没有回调。

CoWorker购物车服务

好的,这里我们做几个简单的微服务模拟实际应用,这里模拟 用户请求端购物车服务库存服务产品服务。 模拟用户请求加购动作,购物车去分别请求 库存,产品 校验用户是否可以加购,并响应客户请求是否成功。

代码我就不贴了,太长了,麻烦移步 CoWorkerman/example/example5/coCartServer.php

运行命令

## 启动库存服务
$ php ./examples/example5/otherServerFork.php 8081 inventory 1
## 启动产品服务
$ php ./examples/example5/otherServerFork.php  8082 product 2
## 启动CoWorker 购物车服务
$ php ./examples/example5/coCartServer.php start
## 用户请求端
$ php ./examples/example5/userClientFork.php

运行结果

WX20200619-214858.png

黄色箭头——新的用户请求,蓝色箭头——购物车发起库存,产品检查请求,红色箭头——响应用户请求

从图中看到也是用1个线程服务多个连接,交错运行。

好的,那么PHP CoWorkerman 也能像 NodeJS 那样用 Async/Await 那样同步编码,异步运行了。

快来试试这个 CoWorkerman 吧:

$ composer require paulxu-cn/co-workerman

工作原理

先上图:

gaitubao_coworkerman图解.png

图的上部是Workerman 的工作泳道图,图下部是CoWorkerman的工作泳道图。

workerman内的worker进程遇到阻塞函数的处理方式时,会等待IO返回,如果这个时候,又有了新的请求,那么闲的worker会竞争到这个新的连接。

我在上图worker5中,描述了一个AsyncTCPConnection使用情况,woker内发起了一个非阻塞请求,并注册了回调函数,然后程序继续运行到结束。当异步请求响应时,就需要通过其他方式去响应(如自己再发起一个请求告知请求方)。

在下图中CoWorkerman,也是多个Worker竞争新的请求,当worker1收到一个新的请求,会产生一个生成器,生成器内发起异步请求,并注册响应回调,请求响应后,回到该生成器跳出(yield)的地方,继续执行代码。

发起异步请求,并注册回调函数,这些默认工作CoWorkerman框架内已做了,回调函数内工作是:收到数据,并发给 发起该请求的生成器。

这例子中,通过调用 Promise:all() 发起多个请求,并监听结果返回,待所有的响应返回再继续运行生成器

在程序yield跳出后,该worker就处于事件循环状态($event->loop()),也就是多路监听:请求端口,第三方客户端请求响应端口。这个时候如果:

  1. 有新的请求来,他和其他 worker 竞争新的请求,如果竞争到了,则该worker内又产生一个新的 生成器。
  2. 客户端有响应,则调用回调函数
  3. 客户端都响应了,继续运行 生成器程序。

从1中,我们可假设,如果就一个 Worker,那么该 Worker 可以在上一个请求未完成情况下,继续接受处理下一个请求。也就是 CoWorkerman 可以在单 Worker 下运行,并发处理多个请求。

当然,这里也有个前提,单 Worker 模式内不能运行阻塞函数,一旦阻塞,后续请求就会堵在网卡。所以,除非对自己的代码非常了解,如果用到第三方库,那么我还是建议你在多 Worker 模式下运行 CoWorkerman,阻塞时,还有其他Worker兜住新请求。

CoWorkerman 的意义

  1. 用同步的代码,发起异步请求,多个请求可并发,从IO串行等待,改为并行等待,减少无畏的等待时间。提高业务程序的效率同时,不降低代码可读性。
  2. 在一个线程内通过事件循环,尽可能处理多个请求,缓解了一个请求一个线程带来的频繁线程切换,从核心上提高运行效率。

CoWorkerman 生态位

适合处理纯Socket请求的应用,如Workerman Gateway,或者是 大前端 整合多个服务RPC结果, 综合后返给前三页这样的场景.

日志记录是每个程序最基本需求,由于写文件函数是阻塞的,建议用消息队列,或者redis队列,更或者跳过Logstash直接丢Elasticsearch.

CoWorkerman有他的局限性,也有他自己位置。

总结

好~PHP 协程编码到 网络异步编码就到此结束了,如果看到本文章有很多疑惑,欢迎留言提问,如果是 yield 语法不太记得,可以先读一读这个系列前几篇文章复习一下。

如果行,请三连。CoWorkerman 谢谢!

参考

查看原文

ljfrocky 赞了回答 · 8月6日

php链式调用如果在new实体之后马上调用方法

(new MyClass())->setData('1');

关注 2 回答 1

ljfrocky 赞了文章 · 5月22日

毫秒级从百亿大表任意维度筛选数据,是怎么做到的...

1、业务背景

随着闲鱼业务的发展,用户规模达到数亿级,用户维度的数据指标,达到上百个之多。如何从亿级别的数据中,快速筛选出符合期望的用户人群,进行精细化人群运营,是技术需要解决的问题。业界的很多方案常常需要分钟级甚至小时级才能生成查询结果。本文提供了一种解决大数据场景下的高效数据筛选、统计和分析方法,从亿级别数据中,任意组合查询条件,筛选需要的数据,做到毫秒级返回。

2、技术选型分析

从技术角度分析,我们这个业务场景有如下特点:

  1. 需要支持任意维度的组合(and/or)嵌套查询,且要求低延迟;
  2. 数据规模大,至少亿级别,且需要支持不断扩展;
  3. 单条数据指标维度多,至少上百,且需要支持不断增加;
    综合分析,这是一个典型的OLAP场景。

2.1 OLTP与OLAP

下面简单对比下OLTP和OLAP:

OLTPOLAP
定义联机事务处理联机分析处理
应用场景日常业务操作分析决策,报表统计
事务要求需要支持事务无需事务支持
常用数据操作读/写高并发查询为主,并发要求相对低
实时性要求要求不严格
DB大小MB-GBGB-TB
数据行数几万到几百万亿级别甚至几十亿上百亿
数据列数几十至上百列百级别至千级别

最常见的数据库,如MySql、Oracle等,都采用行式存储,比较适合OLTP。如果用MySql等行数据库来实现OLAP,一般都会碰到两个瓶颈:

  1. 数据量瓶颈:mysql比较适合的数据量级是百万级,再多的话,查询和写入性能会明显下降。因此,一般会采用分库分表的方式,把数据规模控制在百万级。
  2. 查询效率瓶颈:mysql对于常用的条件查询,需要单独建立索引或组合索引。非索引字段的查询需要扫描全表,性能下降明显。

综上分析,我们的应用场景,并不适合采用行存储数据库,因此我们重点考虑列存数据库。

2.2 行式存储与列式存储

下面简单对比一下行式存储与列式存储的特点:

行式存储列式存储
存储特点同一行数据一起存储同一列数据一起存储
读取优点一次性读取整行数据快捷读取单列数据快捷
读取缺点单列读取,也需要读取整行数据整行查询,需要重组数据
数据更新特点INSERT/UPDATE比较方便INSERT/UPDATE比较麻烦
索引需要单独对查询列建索引每一列都可以作为索引
压缩特点同一行数据差异大,压缩比率低一列数据由于相似性,压缩比率高

行存适合近线数据分析,比如要求查询表中某几条符合条件的记录的所有字段的场景。列存适合用于数据的统计分析。考虑如下场景:一个用于存放用户的表中有20个字段,而我们要统计用户年龄的平均值,如果是行存,则要全表扫描,遍历所有行。但如果是列存,数据库只要定位到年龄这一列,然后只扫描这一列的数据就可以得到所有的年龄,计算平均值,性能上相比行存理论上就会快20倍。
而在列存数据库中,比较常见的是HBase。HBase应用的核心设计重点是rowkey的设计,一般要把常用的筛选条件,组合设计到rowkey中,通过rowkey的get(单条记录)或者scan(范围)查询。因此HBase比较适合有限查询条件下的非结构化数据存储。而我们的场景,由于所有字段都需要作为筛选条件,所以本质上还是需要结构化存储,且要求查询低延迟,因此也无法使用HBase。
我们综合考虑集团内多款列式存储的DB产品(ADS/PostgreSQL/HBase/HybridDB),综合评估读写性能、稳定性、语法完备程度及开发和部署成本,我们选择了HybridDB for MySQL计算规格来构建人群圈选引擎。

2.3 HybridDB for MySQL计算规格介绍

HybridDB for MySQL计算规格对我们的这个场景而言,核心能力主要有:

  1. 任意维度智能组合索引(使用方无需单独自建索引)
  2. 百亿大表查询毫秒级响应
  3. MySql BI生态兼容,完备SQL支持
  4. 空间检索、全文检索、复杂数据类型(多值列、JSON)支持

那么,HybridDB for MySQL计算规格是如何做到大数据场景下的任意维度组合查询的毫秒级响应的呢?

  • 首先是HybridDB的高性能列式存储引擎,内置于存储的谓词计算能力,可以利用各种统计信息快速跳过数据块实现快速筛选;
  • 第二是HybridDB的智能索引技术,在大宽表上一键自动全索引并根据列索引智能组合出各种谓词条件进行过滤;
  • 第三是高性能MPP+DAG的融合计算引擎,兼顾高并发和高吞吐两种模式实现了基于pipeline的高性能向量计算,并且计算引擎和存储紧密配合,让计算更快;
  • 第四是HybridDB支持各种数据建模技术例如星型模型、雪花模型、聚集排序等,业务适度数据建模可以实现更好的性能指标。

综合来说,HybridDB for MySQL计算规格是以SQL为中心的多功能在线实时仓库系统,很适合我们的业务场景,因此我们在此之上构建了我们的人群圈选底层引擎。

3、业务实现

在搭建了人群圈选引擎之后,我们重点改造了我们的消息推送系统,作为人群精细化运营的一个重要落地点。

3.1 闲鱼消息推送简介

消息推送(PUSH)是信息触达用户最快捷的手段。闲鱼比较常用的PUSH方式,是先离线计算好PUSH人群、准备好对应PUSH文案,然后在第二天指定的时间推送。一般都是周期性的PUSH任务。但是临时性的、需要立刻发送、紧急的PUSH任务,就需要BI同学介入,每个PUSH任务平均约需要占用BI同学半天的开发时间,且操作上也比较麻烦。本次我们把人群圈选系统与原有的PUSH系统打通,极大地改善了此类PUSH的准备数据以及发送的效率,解放了开发资源。

3.2 系统架构

离线数据层:用户维度数据,分散在各个业务系统的离线表中。我们通过离线T+1定时任务,把数据汇总导入到实时计算层的用户大宽表中。

实时计算层:根据人群的筛选条件,从用户大宽表中,查询符合的用户数量和用户ID列表,为应用系统提供服务。

人群圈选前台系统:提供可视化的操作界面。运营同学选择筛选条件,保存为人群,用于分析或者发送PUSH。每一个人群,对应一个SQL存储。类似于:
select count(*) from user_big_table where column1> 1 and column2 in ('a','b') and ( column31=1 or column32=2)
同时,SQL可以支持任意字段的多层and/or嵌套组合。
用SQL保存人群的方式,当用户表中的数据变更时,可以随时执行SQL,获取最新的人群用户,来更新人群。

闲鱼PUSH系统:从人群圈选前台系统中获取人群对应的where条件,再从实时计算层,分页获取用户列表,给用户发送PUSH。在实现过程中,我们重点解决了分页查询的性能问题。

分页查询性能优化方案:
在分页时,当人群的规模很大(千万级别)时,页码越往后,查询的性能会有明显下降。因此,我们采用把人群数据增加行号、导出到MySql的方式,来提升性能。表结构如下:

批次号人群ID行号用户ID
100111123
100112234
100113345
100114456
  • 批次号:人群每导出一次,就新加一个批次号,批次号为时间戳,递增。
  • 行号:从1开始递增,每一个批次号对应的行号都是从1到N。

我们为"人群ID"+"批次号"+"行号"建组合索引,分页查询时,用索引查询的方式替换分页的方式,从而保证大页码时的查询效率。

另外,为此额外付出的导出数据的开销,得益于HybridDB强大的数据导出能力,数据量在万级别至百万级别,耗时在秒级至几十秒级别。综合权衡之后,采用了本方案。

4、PUSH系统改造收益

undefined

人群圈选系统为闲鱼精细化用户运营提供了强有力的底层能力支撑。同时,圈选人群,也可以应用到其他的业务场景,比如首页焦点图定投等需要分层用户运营的场景,为闲鱼业务提供了很大的优化空间。

本文实现了海量多维度数据中组合查询的秒级返回结果,是一种OLAP场景下的通用技术实现方案。同时介绍了用该技术方案改造原有业务系统的一个应用案例,取得了很好的业务结果,可供类似需求或场景的参考。

5、未来

人群圈选引擎中的用户数据,我们目前是T+1导入的。这是考虑到人群相关的指标,变化频率不是很快,且很多指标(比如用户标签)都是离线T+1计算的,因此T+1的数据更新频度是可以接受的。后续我们又基于HybridDB构建了更为强大的商品圈选引擎。闲鱼商品数据相比用户数据,变化更快。一方面用户随时会更新自己的商品,另一方面,由于闲鱼商品单库存(售出即下架)的特性,以及其他原因,商品状态会随时变更。因此我们的选品引擎,应该尽快感知到这些数据的变化,并在投放层面做出实时调整。我们基于HybridDB(存储)和实时计算引擎,构建了更为强大的“马赫”实时选品系统。



本文作者:闲鱼技术-才思

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

赞 54 收藏 39 评论 0

ljfrocky 关注了标签 · 2019-11-13

tidb

TiDB 是 PingCAP 公司设计的开源分布式 HTAP (Hybrid Transactional and Analytical Processing) 数据库,结合了传统的 RDBMS 和 NoSQL 的最佳特性。TiDB 兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用性。TiDB 的目标是为 OLTP (Online Transactional Processing) 和 OLAP (Online Analytical Processing) 场景提供一站式的解决方案。

TiDB 具备如下特性:

  • 高度兼容 MySQL

    大多数情况下,无需修改代码即可从 MySQL 轻松迁移至 TiDB,分库分表后的 MySQL 集群亦可通过 TiDB 工具进行实时迁移。

  • 水平弹性扩展

    通过简单地增加新节点即可实现 TiDB 的水平扩展,按需扩展吞吐或存储,轻松应对高并发、海量数据场景。

  • 分布式事务

    TiDB 100% 支持标准的 ACID 事务。

  • 真正金融级高可用

    相比于传统主从 (M-S) 复制方案,基于 Raft 的多数派选举协议可以提供金融级的 100% 数据强一致性保证,且在不丢失大多数副本的前提下,可以实现故障的自动恢复 (auto-failover),无需人工介入。

  • 一站式 HTAP 解决方案

    TiDB 作为典型的 OLTP 行存数据库,同时兼具强大的 OLAP 性能,配合 TiSpark,可提供一站式 HTAP 解决方案,一份存储同时处理 OLTP & OLAP,无需传统繁琐的 ETL 过程。

  • 云原生 SQL 数据库

    TiDB 是为云而设计的数据库,支持公有云、私有云和混合云,使部署、配置和维护变得十分简单。

TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过TiSpark 项目来完成。

TiDB 对业务没有任何侵入性,能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题,专注于业务开发,极大的提升研发的生产力。

https://pingcap.com/docs-cn/s...

关注 4

认证与成就

  • 获得 21 次点赞
  • 获得 29 枚徽章 获得 3 枚金徽章, 获得 9 枚银徽章, 获得 17 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-05-16
个人主页被 448 人浏览