我有一个自2017年以来运行在一个大部分更新的WAMP堆栈上的Web应用程序。它作为一个内部后台应用程序为大约200-300个用户提供服务。当前的WAMP堆栈:两个运行Windows Server 2016的虚拟机,应用服务器上运行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”值很高。
在Apache配置更改无效之后,HTTP2协议的更改也无效,问题似乎与PHP会话处理有关,我尝试重新配置它。以下是当前的PHP会话配置:
我试图在我的应用程序中重新编写会话处理程序。以下是会话处理类的相关部分:
我不知道还能做什么,或者我哪里搞砸了。非常感谢任何帮助。 如果有人需要更多信息,请随时提问!
它在大约半年前之前没有遇到任何重大问题。
用户遇到的主要症状是在尝试加载任何页面后,浏览器中出现白屏,并且选项卡处于“加载状态”。这种情况发生在随机用户身上,并不是每次都发生。我无法确定它发生的频率或与哪个用户有关的任何模式。在从浏览器中删除PHP会话cookie之后,它恢复到正常操作。所有用户都使用Chrome(公司政策)。
在服务器端,我可以看到用户的请求在mod_status页面上“卡住”。如果他们在删除cookie之前尝试刷新站点,他们可以将多个工作进程保持在“卡住”状态。
所谓“卡住”,我指的是工作进程的“M-操作模式”处于“W-发送回复”(至少在http/1.1协议中),而“SS-自最近请求开始以来的秒数”远高于配置的超时时间。将协议更改为http/2后,工作进程将卡在“C-关闭连接”状态,并且“SS”值很高。
我尽力重新配置了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];
}
}
我不知道还能做什么,或者我哪里搞砸了。非常感谢任何帮助。 如果有人需要更多信息,请随时提问!