386 lines
8.6 KiB
Markdown
386 lines
8.6 KiB
Markdown
|
|
# PHP 连接泄漏问题修复
|
|||
|
|
|
|||
|
|
## 问题描述
|
|||
|
|
|
|||
|
|
Python 后端调用 PHP 后端接口时出现超时错误:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
HTTPConnectionPool(host='192.168.1.164', port=30100): Read timed out. (read timeout=5)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 问题根源
|
|||
|
|
|
|||
|
|
通过 `netstat -ano | findstr :30100` 检查发现:
|
|||
|
|
- PHP 服务(PID 23736 和 1416)有 30+ 个 `CLOSE_WAIT` 连接
|
|||
|
|
- `CLOSE_WAIT` 状态表示:客户端已关闭连接,但服务器端未关闭
|
|||
|
|
- 这是典型的**连接泄漏**问题
|
|||
|
|
|
|||
|
|
### 为什么会出现 CLOSE_WAIT?
|
|||
|
|
|
|||
|
|
1. **PHP 内置开发服务器的限制**
|
|||
|
|
- `php -S` 是单线程服务器,设计用于开发测试
|
|||
|
|
- 在处理大量并发请求时容易出现连接泄漏
|
|||
|
|
- 长时间运行会导致资源耗尽
|
|||
|
|
|
|||
|
|
2. **连接未正确关闭**
|
|||
|
|
- 客户端(Python)发送请求后关闭连接
|
|||
|
|
- 服务器端(PHP)没有正确关闭 socket
|
|||
|
|
- 连接进入 CLOSE_WAIT 状态并一直保持
|
|||
|
|
|
|||
|
|
3. **资源耗尽**
|
|||
|
|
- 大量 CLOSE_WAIT 连接占用系统资源
|
|||
|
|
- 导致新请求无法处理或响应缓慢
|
|||
|
|
- 最终导致超时错误
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 临时解决方案:重启 PHP 服务
|
|||
|
|
|
|||
|
|
### 方法 1:使用快速重启脚本(推荐)
|
|||
|
|
|
|||
|
|
双击运行 `restart_php_service.bat`:
|
|||
|
|
|
|||
|
|
```batch
|
|||
|
|
restart_php_service.bat
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这个脚本会:
|
|||
|
|
1. 检查当前 PHP 服务状态
|
|||
|
|
2. 停止所有 PHP 服务进程
|
|||
|
|
3. 等待端口释放
|
|||
|
|
4. 启动新的 PHP 服务
|
|||
|
|
|
|||
|
|
### 方法 2:手动重启
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1. 查看当前 PHP 进程
|
|||
|
|
netstat -ano | findstr :30100
|
|||
|
|
|
|||
|
|
# 2. 停止所有 PHP 进程(替换 PID)
|
|||
|
|
taskkill /F /PID 23736
|
|||
|
|
taskkill /F /PID 1416
|
|||
|
|
|
|||
|
|
# 3. 等待 2 秒
|
|||
|
|
|
|||
|
|
# 4. 启动新的 PHP 服务
|
|||
|
|
cd C:\Users\Administrator\Desktop\Project\AI_GirlFriend\xunifriend_RaeeC\public
|
|||
|
|
php -S 192.168.1.164:30100
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验证服务已重启
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 检查服务状态
|
|||
|
|
netstat -ano | findstr :30100
|
|||
|
|
|
|||
|
|
# 应该只看到 LISTENING 状态,没有 CLOSE_WAIT
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 监控连接状态
|
|||
|
|
|
|||
|
|
### 使用监控脚本
|
|||
|
|
|
|||
|
|
双击运行 `monitor_php_connections.bat`:
|
|||
|
|
|
|||
|
|
```batch
|
|||
|
|
monitor_php_connections.bat
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这个脚本会每 5 秒刷新一次,显示:
|
|||
|
|
- 所有连接状态
|
|||
|
|
- ESTABLISHED 连接数(正常活跃连接)
|
|||
|
|
- CLOSE_WAIT 连接数(连接泄漏)
|
|||
|
|
- TIME_WAIT 连接数(正常关闭中)
|
|||
|
|
|
|||
|
|
### 判断标准
|
|||
|
|
|
|||
|
|
- **正常**:CLOSE_WAIT < 10 个
|
|||
|
|
- **注意**:CLOSE_WAIT 10-20 个(需要关注)
|
|||
|
|
- **警告**:CLOSE_WAIT > 20 个(建议立即重启)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 长期解决方案
|
|||
|
|
|
|||
|
|
### 方案 1:使用 Nginx + PHP-FPM(推荐)
|
|||
|
|
|
|||
|
|
PHP 内置服务器不适合生产环境,建议使用 Nginx + PHP-FPM。
|
|||
|
|
|
|||
|
|
#### 安装步骤
|
|||
|
|
|
|||
|
|
1. **下载 Nginx for Windows**
|
|||
|
|
- 访问:https://nginx.org/en/download.html
|
|||
|
|
- 下载稳定版(Stable version)
|
|||
|
|
|
|||
|
|
2. **下载 PHP(非线程安全版本)**
|
|||
|
|
- 访问:https://windows.php.net/download/
|
|||
|
|
- 下载 NTS (Non Thread Safe) 版本
|
|||
|
|
|
|||
|
|
3. **配置 PHP-FPM**
|
|||
|
|
|
|||
|
|
创建 `php-cgi.bat`:
|
|||
|
|
|
|||
|
|
```batch
|
|||
|
|
@echo off
|
|||
|
|
cd C:\php
|
|||
|
|
php-cgi.exe -b 127.0.0.1:9000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **配置 Nginx**
|
|||
|
|
|
|||
|
|
编辑 `nginx.conf`:
|
|||
|
|
|
|||
|
|
```nginx
|
|||
|
|
server {
|
|||
|
|
listen 30100;
|
|||
|
|
server_name 192.168.1.164;
|
|||
|
|
root C:/Users/Administrator/Desktop/Project/AI_GirlFriend/xunifriend_RaeeC/public;
|
|||
|
|
index index.php index.html;
|
|||
|
|
|
|||
|
|
location / {
|
|||
|
|
try_files $uri $uri/ /index.php?$query_string;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
location ~ \.php$ {
|
|||
|
|
fastcgi_pass 127.0.0.1:9000;
|
|||
|
|
fastcgi_index index.php;
|
|||
|
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|||
|
|
include fastcgi_params;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
5. **启动服务**
|
|||
|
|
|
|||
|
|
```batch
|
|||
|
|
# 启动 PHP-FPM
|
|||
|
|
start php-cgi.bat
|
|||
|
|
|
|||
|
|
# 启动 Nginx
|
|||
|
|
cd C:\nginx
|
|||
|
|
start nginx.exe
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 优点
|
|||
|
|
|
|||
|
|
- 支持多进程,性能更好
|
|||
|
|
- 连接管理更稳定
|
|||
|
|
- 适合生产环境
|
|||
|
|
- 不会出现连接泄漏
|
|||
|
|
|
|||
|
|
### 方案 2:定期自动重启 PHP 服务
|
|||
|
|
|
|||
|
|
如果暂时无法切换到 Nginx,可以设置定时任务自动重启 PHP 服务。
|
|||
|
|
|
|||
|
|
#### 创建定时任务
|
|||
|
|
|
|||
|
|
1. 打开"任务计划程序"(Task Scheduler)
|
|||
|
|
2. 创建基本任务
|
|||
|
|
3. 设置触发器:每 4 小时
|
|||
|
|
4. 操作:启动程序 `restart_php_service.bat`
|
|||
|
|
|
|||
|
|
#### 或使用 Windows 计划任务命令
|
|||
|
|
|
|||
|
|
```batch
|
|||
|
|
schtasks /create /tn "重启PHP服务" /tr "C:\path\to\restart_php_service.bat" /sc hourly /mo 4
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 方案 3:优化 Python 请求代码
|
|||
|
|
|
|||
|
|
在 `lover/deps.py` 中优化 HTTP 请求:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import requests
|
|||
|
|
from requests.adapters import HTTPAdapter
|
|||
|
|
from urllib3.util.retry import Retry
|
|||
|
|
|
|||
|
|
# 创建带重试和连接池的 session
|
|||
|
|
def get_http_session():
|
|||
|
|
session = requests.Session()
|
|||
|
|
|
|||
|
|
# 配置重试策略
|
|||
|
|
retry = Retry(
|
|||
|
|
total=3,
|
|||
|
|
backoff_factor=0.3,
|
|||
|
|
status_forcelist=[500, 502, 503, 504]
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 配置连接池
|
|||
|
|
adapter = HTTPAdapter(
|
|||
|
|
max_retries=retry,
|
|||
|
|
pool_connections=10,
|
|||
|
|
pool_maxsize=20,
|
|||
|
|
pool_block=False
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
session.mount('http://', adapter)
|
|||
|
|
session.mount('https://', adapter)
|
|||
|
|
|
|||
|
|
return session
|
|||
|
|
|
|||
|
|
# 使用 session
|
|||
|
|
def _fetch_user_from_php(token: str) -> Optional[dict]:
|
|||
|
|
"""通过 PHP/FastAdmin 接口获取用户信息。"""
|
|||
|
|
import logging
|
|||
|
|
logger = logging.getLogger(__name__)
|
|||
|
|
|
|||
|
|
user_info_api = "http://192.168.1.164:30100/api/user_basic/get_user_basic"
|
|||
|
|
|
|||
|
|
logger.info(f"用户中心调试 - 调用接口: {user_info_api}")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
session = get_http_session()
|
|||
|
|
resp = session.get(
|
|||
|
|
user_info_api,
|
|||
|
|
headers={
|
|||
|
|
"token": token,
|
|||
|
|
"Connection": "close" # 明确关闭连接
|
|||
|
|
},
|
|||
|
|
timeout=10, # 增加超时时间
|
|||
|
|
)
|
|||
|
|
logger.info(f"用户中心调试 - 响应状态码: {resp.status_code}")
|
|||
|
|
|
|||
|
|
# 确保连接关闭
|
|||
|
|
resp.close()
|
|||
|
|
|
|||
|
|
except requests.exceptions.Timeout:
|
|||
|
|
logger.error(f"用户中心调试 - 请求超时")
|
|||
|
|
raise HTTPException(
|
|||
|
|
status_code=status.HTTP_504_GATEWAY_TIMEOUT,
|
|||
|
|
detail="用户中心接口超时",
|
|||
|
|
)
|
|||
|
|
except Exception as exc:
|
|||
|
|
logger.error(f"用户中心调试 - 请求异常: {exc}")
|
|||
|
|
raise HTTPException(
|
|||
|
|
status_code=status.HTTP_502_BAD_GATEWAY,
|
|||
|
|
detail="用户中心接口不可用",
|
|||
|
|
) from exc
|
|||
|
|
|
|||
|
|
# ... 其余代码
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 预防措施
|
|||
|
|
|
|||
|
|
### 1. 监控连接状态
|
|||
|
|
|
|||
|
|
定期运行 `monitor_php_connections.bat` 检查连接状态。
|
|||
|
|
|
|||
|
|
### 2. 设置告警
|
|||
|
|
|
|||
|
|
当 CLOSE_WAIT 连接数超过阈值时,发送告警通知。
|
|||
|
|
|
|||
|
|
### 3. 日志记录
|
|||
|
|
|
|||
|
|
在 Python 代码中记录每次 PHP 调用的耗时:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
import time
|
|||
|
|
|
|||
|
|
start_time = time.time()
|
|||
|
|
resp = requests.get(...)
|
|||
|
|
elapsed_time = time.time() - start_time
|
|||
|
|
|
|||
|
|
logger.info(f"PHP 接口调用耗时: {elapsed_time:.2f}秒")
|
|||
|
|
|
|||
|
|
if elapsed_time > 3:
|
|||
|
|
logger.warning(f"PHP 接口响应缓慢: {elapsed_time:.2f}秒")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 4. 健康检查
|
|||
|
|
|
|||
|
|
添加健康检查端点,定期检查 PHP 服务状态:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
@app.get("/health/php")
|
|||
|
|
async def check_php_health():
|
|||
|
|
try:
|
|||
|
|
resp = requests.get(
|
|||
|
|
"http://192.168.1.164:30100/api/health",
|
|||
|
|
timeout=2
|
|||
|
|
)
|
|||
|
|
return {
|
|||
|
|
"status": "healthy" if resp.status_code == 200 else "unhealthy",
|
|||
|
|
"response_time": resp.elapsed.total_seconds()
|
|||
|
|
}
|
|||
|
|
except:
|
|||
|
|
return {"status": "down"}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 常见问题
|
|||
|
|
|
|||
|
|
### Q1: 为什么会有两个 PHP 进程(PID 23736 和 1416)?
|
|||
|
|
|
|||
|
|
**A**: 可能是之前启动了多次 PHP 服务,导致有多个进程在监听同一端口。建议:
|
|||
|
|
1. 停止所有 PHP 进程
|
|||
|
|
2. 只启动一个 PHP 服务
|
|||
|
|
|
|||
|
|
### Q2: 重启后还是有 CLOSE_WAIT 怎么办?
|
|||
|
|
|
|||
|
|
**A**:
|
|||
|
|
1. 确认已停止所有旧的 PHP 进程
|
|||
|
|
2. 检查是否有其他程序占用端口
|
|||
|
|
3. 考虑更换端口或使用 Nginx
|
|||
|
|
|
|||
|
|
### Q3: 如何判断是 PHP 问题还是 Python 问题?
|
|||
|
|
|
|||
|
|
**A**:
|
|||
|
|
1. 使用 curl 直接测试 PHP 接口:
|
|||
|
|
```bash
|
|||
|
|
curl -X GET "http://192.168.1.164:30100/api/user_basic/get_user_basic" -H "token: YOUR_TOKEN"
|
|||
|
|
```
|
|||
|
|
2. 如果 curl 正常,说明是 Python 客户端问题
|
|||
|
|
3. 如果 curl 也慢,说明是 PHP 服务器问题
|
|||
|
|
|
|||
|
|
### Q4: 生产环境应该用什么?
|
|||
|
|
|
|||
|
|
**A**:
|
|||
|
|
- **不推荐**:`php -S`(仅用于开发)
|
|||
|
|
- **推荐**:Nginx + PHP-FPM
|
|||
|
|
- **备选**:Apache + mod_php
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 快速参考
|
|||
|
|
|
|||
|
|
### 检查连接状态
|
|||
|
|
```bash
|
|||
|
|
netstat -ano | findstr :30100
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 重启 PHP 服务
|
|||
|
|
```bash
|
|||
|
|
restart_php_service.bat
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 监控连接
|
|||
|
|
```bash
|
|||
|
|
monitor_php_connections.bat
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 停止所有服务
|
|||
|
|
```bash
|
|||
|
|
stop_all_services.bat
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 启动所有服务
|
|||
|
|
```bash
|
|||
|
|
start_all_services.bat
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 总结
|
|||
|
|
|
|||
|
|
1. **问题根源**:PHP 内置服务器连接泄漏
|
|||
|
|
2. **临时方案**:定期重启 PHP 服务
|
|||
|
|
3. **长期方案**:使用 Nginx + PHP-FPM
|
|||
|
|
4. **监控措施**:使用监控脚本定期检查
|
|||
|
|
|
|||
|
|
建议尽快切换到 Nginx + PHP-FPM,彻底解决连接泄漏问题!
|