• php-fpm与fastcgi、php-cgi之间的关系及源码解析


    前言

    关于FastCGI、php-cgi、php-fpm的区别是什么,各自有什么用途,以及相互间的关系是什么,查阅相关资料,可谓是众说纷纭,莫衷一是:

    说法一:fastcgi是一个协议,php-fpm实现了这个协议;
    说法二:php-fpm是FASTCGI进程的管理器,用来管理fastcgi进程的;
    说法三:php-fpm是php内核的一个补丁;
    说法四:修改了php.ini配置文件后,没办法平滑重启,所以就诞生了php-fpm;
    说法五:php-cgi是php自带的FASTCGI管理器;

    一、什么是 CGI

    CGI是干嘛的?通俗的讲,CGI是为了保证Web Server传递过来的数据是标准格式的,方便CGI程序的编写者

    Web Server(比如说Nginx)只是内容的分发者。比如,如果请求/index.html,那么Web Server会去文件系统中找到这个文件,发送给浏览器,这里分发的是静态数据。好了,如果现在请求的是/index.php,根据配置文件,Nginx知道这个不是静态文件,需要去找PHP解析器来处理,那么他会把这个请求简单处理后交给PHP解析器。Nginx会传哪些数据给PHP解析器呢?url要有吧,查询字符串也得有吧,POST数据也要有,HTTP header不能少吧,好的,CGI就是规定要传哪些数据、以什么样的格式传递给后方处理这个请求的协议。

    当Web Server收到/index.php这个请求后,会启动对应的CGI程序,这里就是PHP的解析器。接下来PHP解析器会解析php.ini文件,初始化执行环境,然后处理请求,再以规定CGI规定的格式返回处理后的结果,退出进程。web server再把结果返回给浏览器。

    二、FastCGI又是什么呢?

    CGI是个协议,跟进程什么的没关系。Fastcgi是CGI的升级版,一种语言无关的协议,FastCGI是用来提高CGI程序性能的(从字面意思来能好理解)

    标准的CGI对每个请求都会执行这些步骤,所以处理每个请求的时间会比较长。这明显不合理嘛!那么FastCGI是怎么做的呢?首先,FastCGI会先启一个master,解析配置文件,初始化执行环境,然后再启动多个worker。当请求过来时,master会传递给一个worker,然后立即可以接受下一个请求。这样就避免了重复的劳动,效率自然是高。而且当worker不够用时,master可以根据配置预先启动几个worker等着;当然空闲worker太多时,也会停掉一些,这样就提高了性能,也节约了资源。这就是"FastCGI"的对进程的管理(先姑且这么说)

    三、php-fpm是什么?

    是一个实现了FastCGI(协议)的程序

    我们知道web服务器与PHP应用之间通过SAPI接口进行交互数据。PHP提供了多种SAPI接口,例如 apache2hander、fastcgi、cli等等。

    大家都知道,PHP的解释器是php-cgi。php-cgi只是个CGI程序,他自己本身只能解析请求,返回结果,不会管理进程,所以就出现了一些能够调度php-cgi进程的程序,比如说由lighthttpd分离出来的spawn-fcgi。而php-fpm也是这么个东西。

    php-fpm的管理对象是php-cgi,但不能说php-fpm是FastCGI进程的管理器,因为前面说了FastCGI是个协议,似乎没有这么个进程存在,就算存在php-fpm也管理不了他(至少目前是)。他负责管理一个进程池,来处理来自Web服务器的请求。

    php-fpm是一种master(主)/worker(子)多进程架构,与nginx设计风格有点类似。master进程主要负责CGI及PHP环境初始化、事件监听、子进程状态等等,worker进程负责处理php请求。

    php-fpm是PHP内核的一个补丁? 以前是正确的,因为最开始的时候php-fpm没有包含在PHP内核里面,要使用这个功能,需要找到与源码版本相同的php-fpm对内核打补丁,然后再编译。后来PHP(5.3以后)内核集成了php-fpm,编译时加上–enalbe-fpm这个参数即可。

    对于php.ini文件的修改,php-cgi进程是没办法平滑重启的,有了php-fpm后,就把平滑重启成为了一种可能,php-fpm对此的处理机制是新的worker用新的配置,已经存在的worker处理完手上的活就可以歇着了,通过这种机制来平滑过度的。

    四、php-fpm源码解析

    4.1 进程管理模式

    PHP-FPM由1个master进程和N个worker进程组成。其中,Worker进程由master进程fork而来。

    PHP-FPM有3种worker进程管理模式。

    1. Static:静态模式,启动时分配固定的worker进程。初始化调用fpm_children_make(wp,0,0,1)函数fork出pm.max_children数量的worker进程,后续不再动态增减worker进程数量
    2. Dynamic:动态模式,启动时分配固定的进程。伴随着请求数增加,在设定的浮动范围调整worker进程。初始化时调用fpm_children_make(wp,0,0,1)函数fork出pm.start_servers数量的worker进程,然后由每隔1秒触发的心跳事件fpm_pctl_perform_idle_server_maintenance()来维护空闲woker进程数量:空闲worker进程数量若多于pm.max_spare_servers则kill进程,若少于pm.min_spare_servers则fork进程。
    3. Ondemand:按需分配,当收到用户请求时fork worker进程。初始化时不生成worker进程,但注册事件ondemand_event监听listening_socket。当listen_socket收到request,先检查是否存在已生成的空闲的worker进程,若存在就使用这个空闲进程,否则fork一个新的进程。每隔1秒触发的心跳事件fpm_pctl_perform_idle_server_maintenance()会kill掉空闲时间超过pm.process_idle_timeout的worker进程

    4.2 PHP-FPM 运行原理

    master进程
    master进程工作流程分为4个阶段,如下图:
    在这里插入图片描述

    1. cgi初始化阶段:分别调用fcgi_init()sapi_startup()函数,注册进程信号以及初始化sapi_globals全局变量。
    2. php环境初始化阶段:由cgi_sapi_module.startup 触发。实际调用php_cgi_startup函数,而php_cgi_startup内部又调用php_module_startup执行。 php_module_startup主要功能:a).加载和解析php配置;b).加载php模块并记入函数符号表(function_table);c).加载zend扩展 ; d).设置禁用函数和类库配置;e).注册回收内存方法;
    3. php-fpm初始化阶段:执行fpm_init()函数。负责解析php-fpm.conf文件配置,获取进程相关参数(允许进程打开的最大文件数等),初始化进程池及事件模型等操作。
    4. php-fpm运行阶段:执行fpm_run() 函数,运行后主进程发生阻塞。该阶段分为两部分:fork子进程 和 循环事件。fork子进程部分交由fpm_children_create_initial函数处理( 注:ondemand模式在fpm_pctl_on_socket_accept函数创建)。循环事件部分通过fpm_event_loop函数处理,其内部是一个死循环,负责事件的收集工作。

    worker进程
    worker进程分为 接收客户端请求、处理请求、请求结束三个阶段。
    在这里插入图片描述

    1. 接收客户端请求:执行fcgi_accept_request函数,其内部通过调用accept 函数获取客户端请求。
    //请求锁
    FCGI_LOCK(req->listen_socket);
    req->fd = accept(listen_socket, (struct sockaddr *)&sa, &len);
    //释放锁
    FCGI_UNLOCK(req->listen_socket);12345
    

    从上面的代码,可以注意到accept之前有一个请求锁的操作,这么设计是为了避免请求出现“惊群”的现象。当然,这是一个可选的选项,可以取消该功能。

    1. 处理请求阶段:首先,分别调用fpm_request_info、php_request_startup获取请求内容及注册全局变量($_GET、$_POST、$_SERVER、$_ENV、$_FILES);然后根据请求信息调用php_fopen_primary_script访问脚本文件;最后交给php_execute_script执行。php_execute_script内部调用zend_execute_scripts方法将脚本交给zend引擎处理。
    2. 请求结束阶段:执行php_request_shutdown函数。此时 回调register_shutdown_function注册的函数及__destruct()方法,发送响应内容、释放内存等操作。

    总结
    php-fpm采用master/worker架构设计, master进程负责CGI、PHP公共环境的初始化及事件监听操作。worker进程负责请求的处理功能。在worker进程处理请求时,无需再次初始化PHP运行环境,这也是php-fpm性能优异的原因之一。

    4.3 标准IO

    FastCGI的典型流程如下:

    (1) web server(例如nginx或apache)接受到一个请求。然后,web server通过unix域socket或TCP socket连接到FastCGI应用。

    (2) FastCGI应用可以选择接受或拒绝这个连接。如果接受了连接,FastCGI应用会试图从stream中读取到一个packet

    (3) Web server发送的第一个packet是BEGIN_REQUEST packet。BEGIN_REQUEST packet包含一个独一无二的request ID。所有该request的后续packet都被这个ID标记。

    Unix系统中,标准输入的文件描述符是0,标准输出的文件描述符是1,标准错误输出的文件描述符是2,宏定义如下:

    #define STDIN_FILENO  0
    #define STDOUT_FILENO  1
    #define STDERR_FILENO  2
    

    PHP-FPM重定向了这三个标准IO。

    在master进程中,STDIN_FILENO(0)和STDOUT_FILENO(1)均重定向到”/dev/null”,STDERR_FILENO(2)重定向到error_log。

    在worker进程中,STDIN_FILENO(0)重定向到listening_socket。如果catch_workers_output为no的话,STDOUT_FILENO(1)和STDERR_FILENO(2)均重定向到”/dev/null”。否则,STDOUT_FILENO(1)重定向到1个pipe的写端,而这个pipe的fd读端保存于master进程child链表对应的child节点结构的fd_stdout元素上。同样的,STDERR_FILENO(2)也重定向到1个pipe的写端,而这个pipe的读端fd保存于master进程child链表对应的child节点结构的fd_stderr元素上。以上两个位于master进程的pipe读端由master进程的reactor进行监听。

    4.4 进程间通信模型

    PHP-FPM中的进程间通信主要分为

    4.4.1 Master进程和worker进程之间的通信

    前面讲过master进程和worker进程间有两条pipe。Worker进程向STDOUT_FILE或STDERR_FILENO中写信息,master进程收到信息后写入log。Master进程用reactor监听两个pipe

    4.4.2 Web server与worker进程之间的通信

    Worker进程阻塞在accept(listening socket)监听web server

    4.4.3 Web server与master进程之间的通信

    当pm模式是ondemand时,master进程会在reactor注册listening_socket的监听事件。当有request到来,master进程将生成一个worker进程
    在这里插入图片描述
    PHP-FPM采用的进程模型是进程池。Worker进程继承由master进程socket(),bind(),listen()的socket fd并直接阻塞在accept()上。当有一个request到来,进程池中的一个worker进程接受request。当这个worker进程完成执行,就会返回进程池等待新的request。这事实上是leader/follower模式。在leader/follower模式中,仅有leader阻塞等待,其他进程都在sleep。同样的,在FPM中,由于linux内核解决了accept()的惊群问题,新request同样只会唤醒一个worker进程。在这里,leader的继任是由linux内核决定的(当然,你也可以用mutex守卫accept代码段来确保leader只有一位)。

    Worker进程处理所有IO和逻辑。Master进程负责worker进程的生成和销毁。Master进程的Reactor注册了三个可读事件和四个定时器事件。当pm是ondemand时,额外注册一个可读事件。三个可读事件分别是1个信号事件,2个pipe事件。

    Fpm_event_s 结构:

    struct fpm_event_s {
        int fd;
        struct timeval timeout;
        struct timeval frequency;
        void (*callback)(struct fpm_event_s *, short, void *);
        void *arg;
        int flags;
        int index;  
        short which;
    };
    

    Flags代表该事件的类型。FPM中flags的值有三种:

    FPM_EV_READ : 可读事件
    FPM_EV_PERSIST : 心跳事件
    FPM_EV_READ | FPM_EV_EDGE : 边缘触发的可读事件

    Which代表该事件位于哪个事件队列。其值有两种:

    FPM_EV_READ : 位于可读事件队列
    FPM_EV_TIMEOUT : 位于定时器事件队列

    事件队列的结构是双向链表

    typedef struct fpm_event_queue_s {
        struct fpm_event_queue_s *prev;
        struct fpm_event_queue_s *next;
        struct fpm_event_s *ev;
    } fpm_event_queue;
    
    static struct fpm_event_queue_s *fpm_event_queue_timer = NULL;
    static struct fpm_event_queue_s *fpm_event_queue_fd = NULL;  
    

    fpm_event_queue_timer是定时器事件队列,fpm_event_queue_fd是可读事件队列。定时器事件队列并没有采用最小堆,红黑树或事件轮等结构,因为这个队列非常小,没有必要使用这些复杂结构。但是如果把定时器事件队列改为升序链表,对性能应该会有提升。

    Fd和index仅在可读事件中使用。fd表示被监听的文件描述符。Index的值与使用哪个IO复用API有关。在epoll和select中,index的值等于fd的值。在poll中,index是该fd在描述符集fds[]中位置的下标。在心跳事件中,fd == -1,index == -1。

    Struct timeval timeout和struct timeval frequency仅在心跳事件中使用。frequency表示每隔多少时间触发一次心跳事件,Timeout表示下一次触发心跳事件的时刻,通常由now与frequency相加而得。在可读事件中,这两个结构不设置。

    Signal_fd_event事件
    先来看fpm中的信号处理。

    int fpm_signals_init_main() {
        struct sigaction act;
        /* create socketpair*/
        if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {
            zlog(ZLOG_SYSERROR, "failed to init signals: socketpair()");
            return -1;
        }
    
        /*将两个socket设为NONBLOCK*/
        if (0 > fd_set_blocked(sp[0], 0) || 0 > fd_set_blocked(sp[1], 0)) {
            zlog(ZLOG_SYSERROR, "failed to init signals: fd_set_blocked()");
            return -1;
        }
        /*如果程序成功地运行完毕,则自动关闭这两个fd*/
        if (0 > fcntl(sp[0], F_SETFD, FD_CLOEXEC) || 0 > fcntl(sp[1], F_SETFD, FD_CLOEXEC)) {
          zlog(ZLOG_SYSERROR, "falied to init signals: fcntl(F_SETFD, FD_CLOEXEC)");
          return -1;
        }
      
        memset(&act, 0, sizeof(act));
        /* 将信号处理函数设为sig_handler*/
        act.sa_handler = sig_handler;
        /* 将所有信号加入信号集*/
        sigfillset(&act.sa_mask);
      
        /* 更改指定信号的action */
        if (0 > sigaction(SIGTERM,  &act, 0) ||
            0 > sigaction(SIGINT,   &act, 0) ||
            0 > sigaction(SIGUSR1,  &act, 0) ||
            0 > sigaction(SIGUSR2,  &act, 0) ||
            0 > sigaction(SIGCHLD,  &act, 0) ||
            0 > sigaction(SIGQUIT,  &act, 0)) {
          zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()");
          return -1;
        }
        return 0;
    }
    

    master进程注册了SIGTERM,SIGINT,SIGUSR1,SIGUSR2,SIGCHLD,SIGQUIT,并创建了socketpair sp[]。当收到这些信号时,master进程将向sp[1]中写一个代表该信号的字符。

    [SIGTERM] = ‘T’,
    [SIGINT] = ‘I’,
    [SIGUSR1] = ‘1’,
    [SIGUSR2] = ‘2’,
    [SIGQUIT] = ‘Q’,
    [SIGCHLD] = ‘C’

    Sp[0]就是Signal_fd_event事件监听的fd。该事件的回调函数对不同的信号(从sp[0]读到的代表信号的字符)做出不同的反应。

    信号SIGCHLD:调用fpm_children_bury();该函数调用waitpid()分析子进程的status,根据情况决定是否重启子进程。

    信号SIGINT , SIGTERM:调用fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);把fpm的状态改为terminating

    信号SIGQUIT:调用fpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET); 把fpm的状态改为finishing

    信号SIGUSR1:调用fpm_stdio_open_error_log(1)重启error log file,调用fpm_log_open(1)重启access log file 并重启所有子进程

    信号SIGUSR2:调用fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET); 把fpm的状态改为reloading

    子进程的信号处理

    子进程关闭了socketpair,并把SIGTERM,SIGINT,SIGUSR1,SIGUSR2,SIGHLD重新设为默认动作,把SIGQUIT设为soft quit。

    int fpm_signals_init_child() {
        struct sigaction act, act_dfl;
        memset(&act, 0, sizeof(act));
        memset(&act_dfl, 0, sizeof(act_dfl));
        act.sa_handler = &sig_soft_quit;
        // 当system call或library function阻塞时一个信号到来。系统默认会返回错误并设置errno为EINTR.这里设为自动重启
    
        act.sa_flags |= SA_RESTART;
        act_dfl.sa_handler = SIG_DFL;
    
        close(sp[0]);
        close(sp[1]);
    
        if (0 > sigaction(SIGTERM,  &act_dfl,  0) ||
        0 > sigaction(SIGINT,   &act_dfl,  0) ||
        0 > sigaction(SIGUSR1,  &act_dfl,  0) ||
        0 > sigaction(SIGUSR2,  &act_dfl,  0) ||
        0 > sigaction(SIGCHLD,  &act_dfl,  0) ||
        0 > sigaction(SIGQUIT,  &act, 0)) {
            zlog(ZLOG_SYSERROR, "failed to init child signals: sigaction()");
            return -1;
        }
        zend_signal_init();
        return 0;
    }
    

    其一是可读事件队列。其二是定时器(timer)队列。

  • 相关阅读:
    lodash chunk
    lodash.slice
    ⚡ vue3 全家桶体验
    构建一个简约博皮的过程
    [译] 制作 Vue 3 的过程
    ⚠ | 不要再使用 markdown 主题了!
    win 常用命令
    2020年了,别再重复学习原型了
    删除 linux 导致原来的 win10 进不去
    手写一个文章目录插件
  • 原文地址:https://www.cnblogs.com/daozhangblog/p/12446326.html
Copyright © 2020-2023  润新知