PHP-FPM中进程管理的实现

背景

fpm是一个php进程管理工具,是php sapi的一种官方实现,盗用一张《PHP7底层设计与源码实现》中的图片,
在这里插入图片描述
如果学习php,那基本上就是web后端,那么就离不开fpm,了解fpm的重要性可想而知,而fpm中的进程管理也是fpm能在高并发环境中让php有一席之地的原因。

1.fpm的启动过程

先来一张流程图,这个是fpm启动时调用的一些函数,其中1,2,3,4是公有的(也就是fork之前父子进程都会存在的一些东西)5是fork完成之后只有父进程才会有的,while accept循环是只有子进程才会有的,这里的父进程叫做master,子进程叫做worker
在这里插入图片描述

这里 fpm 的进程管理(pm)有三种方式
dynamic(根据请求数动态增加)
ondemand(有请求才增加,空闲一段时间自动杀死)
static(一直是固定的请求数
源码里

int fpm_children_create_initial(struct fpm_worker_pool_s *wp)

负责初试化进程(dynamic是start_servers, ondemand 是 0 static是pm.max_children(这里是5))

2.fpm中用到的unix知识

1.信号

linux 中定义了数十种信号,而kill就是专门的发送信号的工具(不仅是杀掉,还有别的,但很多信号的默认行为就是杀掉自己),用户可以通过Ctrl+C,Ctrl+\发送或kill命令,操作系统在遇到子进程死掉会发送父进程SIGCHID,有段错误(内存访问)发送SIGSEVG等。
操作系统中的信号处理如下图,注意信号产生后handler里还有可能发生信号, 所以一般都可以看到信号处理函数中

int old_errno = errno;
// 不安全的操作
errno = old_errno

在这里插入图片描述

2.管道

维基百科上的介绍 这里注意fpm中的管道并没有像介绍里那样用在父子进程间通信,而是master接受信号(比如子进程死亡SIGCHILD)后写入管道内(sp[0]),然后通过epoll事件监听读管道是否(sp[1])是否有信号产生,放到管道里避免了这种”并发”问题 为此我编写了一段代码来解释这个处理 参考 gitlist
其中29行创建了一个管道(为啥没用pipe?fpm就这么用的)之后地下的whille循环就相当于那个event_loop,源码
fpm 中 fpm_init 初始化了这个管道(fpm_signals_init_main 相当于29行) ,并且设置了信号回调为写入这个管道(相当于18行,33行),并在 5 中fpm_event_loop绑定读取管道的句柄为的事件为 fpm_got_signal(相当于39-40行)。

3.epoll

select poll 和 eopll 都是io多路复用的技术,作用都是当系统帮助用户监听句柄,有内容了就告诉用户,否则就将cpu占用交给别的进程(该进程就是io等待状态),区别就是epoll更为强大,支持水平触发和边缘触发,和poll一样监听数量没有限制但效率更高(当句柄特别多时)。
这里fpm 非常有心的为那些不支持epoll的系统提供了 poll,select,kqueue的实现(在sapi/fpm/fpm/events中),这个事件系统也支持了定时任务(1s一次,通过epoll超时实现),所有需要执行的定时任务都在fpm_events.c:fpm_event_queue_timer 中。
下面是fpm_events.c:fpm_event_loop(int err) 中的具体代码

    while(1) {
    	// 省略了很多代码
        ret = module->wait(fpm_event_queue_fd, timeout); // epoll_wait
        /* trigger timers */
        // 省略了很多代码
        q = fpm_event_queue_timer;
        while (q) {
            struct fpm_event_queue_s *next = q->next;
            fpm_clock_get(&now);
            if (q->ev) {
                if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) { //是否到点了
                    struct fpm_event_s *ev = q->ev;
                    if (ev->flags & FPM_EV_PERSIST) {
                        // 常驻任务要设置下一次的触发事件 比如fpm_pctl_perform_idle_server_maintenance_heartbeat
                        fpm_event_set_timeout(ev, now);
                    } else {
                        // 临时任务要从链表中移除
                        // 省略了很多代码
                    }
                    // 触发定时任务的事件
                    fpm_event_fire(ev);
                    // 省略了很多代码
                }
            }
            q = next;
        }
        // 省略了很多代码
    }

其中epoll一个作用是主进程自己监听事件,另一个也监听了listen具柄,也就是不仅每个子进程的accept会收到连接,主进程中的epoll在收到连接后也会有相应的执行(比如如果进程数量到达max_children了就新搞个,代码可以查看)为此我也写了个代码实验,gitlist 里面主进程又新的请求后会打印一行字,但主进程不会尝试accept

4.共享内存

入门文章 在fpm_shm.c 中子进程 更新记分板(比如是否是idel状态,父进程在event_loop中定时统计idel进程数)

mem = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);

3.fpm中如何管理进程的

1.fpm 中如何实现子进程异常退出,通知父进程管理的

我们设置启动进程为2
我们把之前的188882 发送kill -9 后,fpm显示又重启了一个进程
18895
这里就是主进程收到SIGCHID,在event_loop中将进程重启的 参考 fpm_children.c:fpm_children_bury

2.fpm 中如何实现没有进程处理请求时创建新的进程的

对于ondemand是fpm master 在接收到epoll listen事件(代表有连接请求需要accept了)新搞一个进程

int fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */
{
    if (wp->config->pm == PM_STYLE_ONDEMAND) {
        wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s));
        if (!wp->ondemand_event) {
               zlog(ZLOG_ERROR, "[pool %s] unable to malloc the ondemand socket event", wp->config->name);
               // FIXME handle crash
               return 1;
        }
        memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s));
        fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ | FPM_EV_EDGE,         fpm_pctl_on_socket_accept, wp);
        wp->socket_event_set = 1;
        fpm_event_add(wp->ondemand_event, 0);
        return 1;
	}
	return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1);
}

对于dynamic则是在 fpm_pctl_perform_idle_server_maintenance_heartbeat 的定时任务中实现(这里相当于预先fork进程,而不是等请求来了才fork提高响应速度)

        if (idle < wp->config->pm_min_spare_servers) {
            // 省略很多代码
            /* compute the number of idle process to spawn */
            children_to_fork = MIN(wp->idle_spawn_rate, wp->config->pm_min_spare_servers - idle);


            /* get sure it won't exceed max_children */
            children_to_fork = MIN(children_to_fork, wp->config->pm_max_children - wp->running_children);
            wp->warn_max_children = 0;


            fpm_children_make(wp, 1, children_to_fork, 1);
            // 省略很多代码
            continue;
        }

3.fpm中如何实现进程空闲过多时杀死空闲进程的

跟第二条中的dynamic模式中的一样,只是在不同的判断分支里,fpm_process_ctl:fpm_pctl_perform_idle_server_maintenance_heartbeat 中注册的 event_loop中实现,将多余的闲置进程杀死

        if (idle > wp->config->pm_max_spare_servers && last_idle_child) {
            last_idle_child->idle_kill = 1;
            fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT);
            wp->idle_spawn_rate = 1;
            continue;
        }

4.fpm 中如何实现平滑重启的

父进程给所有当前子进程发送一个SIGKILL,每个子进程都注册了一个信号(接受SIGKILL时),将 fastcgi.c:in_shutdown 变量设置为1,此时处理完当前请求,但是不会继续accept 处理完进程死掉退出后内核给父进程发送一个SIGCHILD,之后父进程当所有子进程都死掉后,在调用exevp 重启 fpm_process_ctl.c:fpm_pctl_exec中实现的重启
在这里插入图片描述

总结

fpm中运用了各种进程间通信(IPC机制)保证了空闲进程数量,但是可以看到重启的时候进程是停止accept的一直到重启成功,如果正好当时正在处理慢的请求就会导致listen队列积压很多,当然也只有重启的时候可能有问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值