Apache worker 在运行单个 PHP Web 应用程序的 Windows Server 上卡住了进程

Apache workers get stuck in process on Windows Server running a single PHP webapplication

提问人:Derenir 提问时间:11/14/2023 最后编辑:Derenir 更新时间:11/16/2023 访问量:72

问:

自 2017 年以来,我有一个 Web 应用程序在最新的 WAMP 堆栈中运行。它为大约 200-300 名用户提供服务,作为内部后台应用程序。当前 WAMP 堆栈:两个运行 Windows Server 2016 的 VM,在应用服务器上:Apache 2.4.58、PHP 8.2.12,在数据库服务器上:MySQL 8.0.33

直到大约半年前,它才运行没有任何重大问题。
用户体验的主要症状是在尝试加载任何页面后浏览器中出现白屏,并且选项卡卡在“加载状态”中。它发生在随机用户身上,而不是一直发生。我无法确定它发生的频率或与哪个用户发生的任何模式。从浏览器中删除 PHP 会话 cookie 后,它将返回正常操作。所有用户都使用 Chrome(公司政策)。
在服务器端,我可以看到用户的请求“卡在”mod_status页面上。如果他们尝试在删除 cookie 之前刷新站点,则可以将多个工作人员置于“卡住”状态。
我所说的“卡住”是指,工作人员的“M - 操作模式”处于“W - 发送回复”(至少在 http/1.1 协议中)并且“SS - 自最近请求开始以来的秒数”远高于配置的超时。将协议更改为 http/2 后,工作人员卡在“C - 关闭连接”中,值为高“SS”。

Multiple workers "stuck" in "W" state - using http/1.1 protocol
多个工作线程“卡”在“W”状态 - 使用 http/1.1 协议

Single worker "stuck" in "C" state - using http/2 protocol
单个工作线程“卡”在“C”状态 - 使用 http/2 协议

我试图尽可能地重新配置Apache,以下是相关部分:

# Core config
ThreadLimit 512
ThreadsPerChild 512
ThreadStackSize 8388608
MaxRequestsPerChild 0
KeepAlive On
KeepAliveTimeout 5
MaxKeepAliveRequests 500
TimeOut 60
ProxyTimeout 60
RequestReadTimeout handshake=0 header=10-40,MinRate=500 body=20,MinRate=500
# Http2 config
Protocols h2 http/1.1
H2Direct On
H2MinWorkers 64
H2MaxWorkers 512
H2MaxSessionStreams 512
H2StreamMaxMemSize 1048576
H2WindowSize 1048576
H2MaxWorkerIdleSeconds 10
H2StreamTimeout 60

在Apache配置中的更改不起作用之后,对http2协议的更改也不起作用,并且问题似乎与PHP会话处理有关,我也尝试重新配置它。以下是当前的 PHP 会话配置:

[Session]
session.save_handler = files
session.save_path = "c:/tmp"
session.use_strict_mode = 1
session.use_cookies = 1
session.cookie_secure = 0
session.use_only_cookies = 1
session.name = PHPSESSID
session.auto_start = 0
session.cookie_lifetime = 14500
session.cookie_path = /
session.cookie_domain =
session.cookie_httponly = 1
session.serialize_handler = php
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 14500
session.referer_check =
session.cache_limiter = nocache
session.cache_expire = 180
session.use_trans_sid = 0
session.hash_function = sha512
session.hash_bits_per_character = 5

我试图在我的应用程序中重写会话处理程序。以下是会话处理类的相关部分:

<?php
//  Framework Session handler
class Session {
    //  Generic PHP session token
    private $_session_id;
    //  Number of seconds after the session id is needs to be regenerated
    private $_session_keepalive = 300;

    //  Error logging handler
    private $_error = null;

    
    
    /**
     *  @Function: public __construct($config);
     *  @Task: Default constructor for Framework session handler
     *  @Params:
     *      array $config: associative configuration array for construction
     *          Default: array() - Empty array
     *          Note: It can contain any of the class' configurable variables
     *              without the first underscore as key
     *  @Returns:
     *      Session
    **/
    public function __construct($config = array()) {
        //  Setting config
        foreach(get_object_vars($this) as $key => $var) {
            if($key != '_session_id' && isset($config[mb_substr($key,1)]))
                $this->$key = $config[mb_substr($key,1)]; 
            
        }
        
        // Make sure use_strict_mode is enabled.
        // use_strict_mode is mandatory for security reasons.
        ini_set('session.use_strict_mode', 1);
        ini_set('session.cookie_secure', 1);
        //  Start the session
        $this->start();
        
        //  Create error logging handler
        $this->_error = new Error_Logging();
        
    }
    
    
    /**
     *  @Function: public __destruct();
     *  @Task: Destructor for Framework Session handler
     *  @Params: None
     *  @Returns: void
    **/
    public function __destruct() {
        //  Destroy variables
        foreach(get_object_vars($this) as $key => $var)
            unset($this->$key);

        //  Releases the session file from write lock
        session_write_close();
        
    }



    /**
     *  @Function: private start()
     *  @Task: Starts the PHP session
     *  @Params: none
     *  @Returns: none
    **/
    private function start() {
        session_start();

        //  Store the session id
        $this->_session_id = session_id();
        
        //  Set CreatedOn if not set
        if(!$this->exists('CreatedOn'))
            $this->set('CreatedOn', date('Y-m-d H:i:s'));

        //  Do not allow the use of old session id
        $time_limit = strtotime(' - ' . $this->_session_keepalive . ' seconds');
        if(!empty($this->get('DeletedOn', '')) && strtotime($this->get('DeletedOn', '')) <= $time_limit) {
            session_destroy();
            session_start();
            $this->set('CreatedOn', date('Y-m-d H:i:s'));

            //  Store the new session id
            $this->_session_id = session_id();

        }

        //  Regenerate the session when older than required
        if(strtotime($this->get('CreatedOn', '')) <= $time_limit) {
            $this->regenerate();

        } 

    }

    /**
     *  @Function: private regenerate()
     *  @Task: Regenerates the current PHP session
     *  @Params: none
     *  @Returns: none
    **/
    public function regenerate() {
        //  Call session_create_id() while session is active to 
        //  make sure collision free.
        if(session_status() != PHP_SESSION_ACTIVE) {
            $this->start();

        }

        //  Get all session data to restore
        $old_session_data = $this->get_all();
        //  Create a new non-conflicting session id
        $this->_session_id = session_create_id();
        //  Set deleted timestamp.
        //  Session data must not be deleted immediately for reasons.
        $this->set('DeletedOn', date('Y-m-d H:i:s'));
        //  Finish session
        session_write_close();

        //  Set new custom session ID
        session_id($this->_session_id);
        //  Start with custom session ID
        $this->start();

        //  Restore the session data except CreatedOn and DeletedOn
        if(isset($old_session_data['CreatedOn']))
            unset($old_session_data['CreatedOn']);
        if(isset($old_session_data['DeletedOn']))
            unset($old_session_data['DeletedOn']);
        if(!empty($old_session_data))
            $this->set_multi($old_session_data);

    }

    
    /**
     *  @Function: public set($key, $val);
     *  @Task: Set Session variable
     *  @Params:
     *      mixed key: Key of the session array variable
     *      mixed val: Value of the session variable
     *  @Returns:
     *      bool
    **/
    public function set($key, $val) {
        //  Return variable
        $response = false;
        
        try{
            //  check if session is started
            if(empty(session_id()))
                throw new Exception('Session Error [0001]: Session is not started.');
            
            //  check if key is not null
            if(empty($key))
                throw new Exception('Session Error [0002]: Session key cannot be empty.');
            
            //  set session key
            $this->write($key, $val);
            $response = true;
            
        }
        //  Handle errors
        catch(Exception $e) {
            $this->_error->log($e->getMessage());
            
        }
        
        return $response;
        
    }
    
    
    /**
     *  @Function: public get($key);
     *  @Task: Get session variable
     *  @Params:
     *      mixed key: Key of the session array variable
     *      mixed default: Default value if result is empty
     *  @Returns:
     *      bool/mixed
    **/
    public function get($key, $default = '') {
        //  Return variable
        $response = false;
        
        try{
            //  check if session is started
            if(empty(session_id()))
                throw new Exception('Session Error [0001]: Session is not started.');
            
            //  check if key is not null
            if(empty($key))
                throw new Exception('Session Error [0002]: Session key cannot be empty.');
            
            //  get session key if exists, else false
            $response = $this->read($key, $default);
            
        }
        //  Handle errors
        catch(Exception $e) {
            $this->_error->log($e->getMessage());
            
        }
        
        return $response;
        
    }

    /**
     *  @Function: public exists($key);
     *  @Task: Check if session variable exists
     *  @Params:
     *      mixed key: Key of the session array variable
     *  @Returns:
     *      bool
    **/
    public function exists($key) {
        //  Return variable
        $response = false;
        
        try{
            //  check if session is started
            if(empty(session_id()))
                throw new Exception('Session Error [0001]: Session is not started.');
            
            //  check if key is not null
            if(empty($key))
                throw new Exception('Session Error [0002]: Session key cannot be empty.');
            
            //  get if exists
            $response = isset($_SESSION[$key]);
            
        }
        //  Handle errors
        catch(Exception $e) {
            $this->_error->log($e->getMessage());
            
        }
        
        return $response;
        
    }
    
    
    /**
     *  @Function: public set_multi($params);
     *  @Task: Set multiple session variables
     *  @Params:
     *      array params: Associative array of key/val pairs to be set as session variables
     *  @Returns:
     *      bool
    **/
    public function set_multi($params) {
        //  Return variable
        $response = false;
        
        try{
            //  check if session is started
            if(empty(session_id()))
                throw new Exception('Session Error [0001]: Session is not started.');
            
            //  check if key is not null
            if(empty($params))
                throw new Exception('Session Error [0003]: Params array cannot be empty.');
            
            $res = array();
            foreach($params as $key => $val) {
                //  check if key is not null
                if(empty($key))
                    throw new Exception('Session Error [0002]: Session key cannot be empty.');
                
                //  set session key
                $this->write($key, $val);
                $res[] = true;
                
            }
            
            //  Check if all set succeded
            $response = count($params) == count($res);
            
        }
        //  Handle errors
        catch(Exception $e) {
            $this->_error->log($e->getMessage());
            
        }
        
        return $response;
        
    }

    
    /**
     *  @Function: public get_all();
     *  @Task: Get all session variables
     *  @Params: None
     *  @Returns:
     *      array
    **/
    public function get_all() {
        //  Return variable
        $response = false;
        
        try{
            //  check if session is started
            if(empty(session_id()))
                throw new Exception('Session Error [0001]: Session is not started.');
            
            $res = array();
            $keys = array_keys($_SESSION);
            foreach($keys as $key) {
                //  get session key
                $res[$key] = $this->read($key);
                
            }
            
            //  Check if all set succeded
            $response = $res;
            
        }
        //  Handle errors
        catch(Exception $e) {
            $this->_error->log($e->getMessage());
            
        }
        
        return $response;
    
    }
    
    /**
     *  @Function: private write($key, $val);
     *  @Task: write session variable 
     *  @Params:
     *      mixed key: key of the session variable to be stored
     *      mixed val: value of the session variable to be stored
     *  @Returns:
     *      void
    **/
    private function write($key, $val) {
        $_SESSION[$key] = $val;
        
    }
    
    
    /**
     *  @Function: private read($key, $default);
     *  @Task: get session variable 
     *  @Params:
     *      mixed key: key of the session variable to be retrieved
     *      mixed default: default value, if session not found
     *  @Returns:
     *      mixed
    **/
    private function read($key, $default = '') {
        if(!isset($_SESSION[$key]))
            return $default;
        else
            return $_SESSION[$key];
        
    }
    
}

我不知道我还能做什么,或者我在哪里搞砸了。任何帮助都非常感谢。 如果有人需要更多信息,请随时询问!

php apache 会话 配置 wamp

评论


答:

0赞 Derenir 11/27/2023 #1

到今天为止,问题似乎已经解决。但我不确定解决方案是否正确,所以如果有人有更多提示,欢迎。

线索是重复出现的错误,但起初我认为这是一个单独的问题,这就是为什么我没有包含在上面的问题描述中。MySQL server has gone away

首先,我在数据库处理程序类中编写了一个重新连接函数,并在MySQL服务器上执行任何查询之前每次调用它。以下是重新连接功能:

/**
    *   @Function: private reconnect();
    *   @Task: Reconnect to database if the connection has gone away
    *   @Params: None
    *   @Returns: void
**/
private function reconnect() {
    try {
        if($this->_db === null || !(@$this->_db->ping())) {
            if($this->_reconnect_count > $this->_reconnect_try_max) {
                throw new Exception('Database Error [0012]: MySqli connector could not reconnect.');

            }
            else {
                //  Count the reconnect trys
                $this->_reconnect_count++;

                //  Dump old connection
                unset($this->_db);
                //  Create MySQLi connector
                $this->_db = new mysqli($this->_host, $this->_user, $this->_pass, '', $this->_port);

                //  Check if MySQLi connection is established
                if($this->_db->connect_errno != 0) {
                    //  Wait before re-try
                    sleep($this->_reconnect_wait);
                    $this->reconnect();

                }
                else {
                    //  Reset the reconnect counter
                    $this->_reconnect_count = 0;

                }

            }

        }

    }
    catch(Exception $e) {
        //  Log the error
        $this->_error->log($e->getMessage());

        //  Terminate connection
        header('location: ' . get_url() . '/500.html');
        die(0);

    }

}

此方法检查数据库连接是否仍处于活动状态(使用函数),如果连接消失,则尝试每秒重新连接一次,最多一次。$mysqli->ping()_reconnect_try_max

但起初,它没有帮助,因为事实证明,该方法是抛出错误的方法,而不是按预期重新调整。
因此,在添加错误控制运算符 () 之后 - 如上面的代码所示 - 并且在调用 的任何时候,“Mysql 消失了”错误消失了,并且从那以后就没有“卡住”的 Apache 工作器了(截至撰写本文时,连续 7 天)。
ping()false@ping()$mysqli->ping()