# Ubuntu服务器上Python脚本的持久化运行:超越nohup的生产级守护方案
在Ubuntu服务器上部署Python脚本时,很多开发者都曾遇到过这样的困境:SSH连接一断开,精心编写的脚本就戛然而止,数据采集中断,定时任务失效,整个自动化流程瞬间崩溃。这种场景对于需要长期运行的数据处理、API服务、监控脚本或机器学习模型来说,简直是灾难性的。
传统的解决方案是使用`nohup`命令,这确实是Linux系统管理员工具箱中的经典工具。但如果你真的在生产环境中依赖过`nohup`,可能会发现它远非完美——日志文件无限膨胀最终撑爆磁盘、进程意外退出后无人知晓、多个脚本同时运行时日志混杂难以排查。这些问题在简单的测试环境中或许可以忍受,但在真正的生产系统中,它们会成为运维人员的噩梦。
今天,我要分享的不仅仅是`nohup`的基础用法,而是一套完整的、经过实战检验的Python脚本守护方案。这套方案融合了系统级进程管理、智能日志轮转、错误监控和自动化恢复机制,能够确保你的Python脚本像真正的系统服务一样稳定运行。无论你是全栈开发者、运维工程师,还是数据科学家,掌握这些技能都能让你的服务器管理能力提升一个层次。
## 1. 理解进程守护的核心机制:为什么简单的nohup不够用
在深入具体操作之前,我们需要先理解Linux进程管理的基本原理。很多人知道`nohup`能让进程在后台运行,但很少有人真正明白它是如何工作的,以及它的局限性在哪里。
### 1.1 信号机制:SIGHUP与进程的生命周期
Linux系统通过信号来管理进程行为。当你关闭终端或断开SSH连接时,系统会向该会话的所有进程发送`SIGHUP`(挂断)信号。默认情况下,这个信号会导致进程终止。`nohup`的作用就是让进程忽略这个信号,从而在终端关闭后继续存活。
但这里有一个关键细节经常被忽略:`nOHUP`只处理`SIGHUP`信号,而进程可能因为其他原因终止,比如内存不足被系统杀死(`SIGKILL`)、程序自身崩溃、或者收到其他终止信号。单纯的`nohup`无法处理这些情况。
```bash
# 查看所有信号及其编号
kill -l
# 常见的进程终止信号:
# 1) SIGHUP - 终端挂断,nohup可以忽略
# 2) SIGINT - 键盘中断 (Ctrl+C)
# 3) SIGTERM - 终止信号 (kill命令默认)
# 4) SIGKILL - 强制终止 (kill -9)
# 5) SIGSEGV - 段错误 (程序访问非法内存)
```
### 1.2 会话、进程组与控制终端
要真正理解后台运行,需要了解Linux的会话管理模型:
- **会话(Session)**:一次用户登录创建一个会话,包含一个或多个进程组
- **进程组(Process Group)**:相关进程的集合,通常由一个shell命令及其子进程组成
- **控制终端(Controlling Terminal)**:会话关联的终端设备
当你通过SSH连接到服务器时,系统创建一个新的会话。在这个会话中启动的所有进程(包括shell和它启动的程序)都属于这个会话。会话首进程(通常是你的shell)负责管理整个会话。当SSH连接断开时,系统向会话首进程发送`SIGHUP`,然后会话首进程会向会话中的所有进程转发这个信号。
`nohup`的工作原理是让进程脱离这个信号传播链,但它不改变进程与会话的关系。这就是为什么在某些情况下,即使使用了`nohup`,进程仍然可能异常终止。
### 1.3 nohup的典型问题场景
在实际使用中,我遇到过不少`nohup`的坑:
1. **日志管理混乱**:默认输出到`nohup.out`,多个程序运行时互相覆盖
2. **磁盘空间危机**:长期运行的脚本产生大量日志,最终填满磁盘
3. **进程状态未知**:脚本是否在运行?有没有崩溃?需要手动检查
4. **资源泄漏风险**:僵尸进程积累,系统资源逐渐耗尽
5. **启动管理困难**:服务器重启后需要手动重新启动所有脚本
下面这个表格对比了简单`nohup`与生产级方案的关键差异:
| 特性维度 | 基础nohup方案 | 生产级守护方案 |
|---------|--------------|---------------|
| **进程监控** | 无自动监控,需手动检查 | 实时监控,异常自动重启 |
| **日志管理** | 单一文件,无限增长 | 轮转归档,按大小/时间分割 |
| **启动控制** | 手动执行命令 | 系统服务,开机自启 |
| **资源限制** | 无限制,可能耗尽系统资源 | CPU/内存限制,防止失控 |
| **多实例管理** | 困难,容易冲突 | 命名空间隔离,独立配置 |
| **状态查询** | 需要执行ps/grep命令 | 统一状态接口,一目了然 |
理解了这些底层原理和实际问题,我们就能更好地设计解决方案。接下来,我将从基础的`nohup`用法开始,逐步构建完整的生产级守护体系。
## 2. nohup的高级用法与实战技巧
虽然`nohup`有局限性,但它仍然是工具箱中的重要工具。关键在于如何正确、高效地使用它。下面这些技巧来自我多年的运维经验,能帮你避免很多常见陷阱。
### 2.1 正确的nohup命令格式与参数解析
大多数教程只教`nohup command &`这种基本形式,但在生产环境中,我们需要更精细的控制。让我们分解一个完整的命令:
```bash
# 生产环境推荐的完整格式
nohup /usr/bin/python3 /path/to/your_script.py \
> /var/log/app/script_output.log \
2>> /var/log/app/script_error.log \
</dev/null &
```
这个命令的每个部分都有其作用:
- `nohup`:忽略挂断信号
- `/usr/bin/python3`:使用绝对路径,避免环境变量问题
- `>/path/to/output.log`:将标准输出重定向到指定文件
- `2>>/path/to/error.log`:将标准错误追加到另一个文件(注意是`>>`不是`>`)
- `</dev/null`:断开标准输入,防止程序等待输入
- `&`:在后台运行
**特别注意**:错误日志使用`>>`(追加)而不是`>`(覆盖)非常重要。这样即使多次重启,错误历史也会保留,便于排查问题。
### 2.2 分离输出与错误日志的重要性
很多开发者将标准输出和错误输出都重定向到同一个文件,这在实际运维中是个坏习惯。原因如下:
1. **调试效率**:正常日志和错误日志混在一起,查找问题如同大海捞针
2. **监控便利**:错误监控系统通常只关注错误日志
3. **存储优化**:正常日志可能很大,但错误日志通常很小,可以分开管理
4. **权限控制**:可能希望不同角色访问不同类型的日志
我推荐的做法是为每个脚本创建独立的日志目录结构:
```bash
# 创建标准化的日志目录结构
sudo mkdir -p /var/log/your_app/{output,error,archive}
# 设置正确的权限
sudo chown -R $USER:$USER /var/log/your_app
sudo chmod -R 755 /var/log/your_app
# 使用分离的日志运行脚本
nohup python3 data_processor.py \
> /var/log/your_app/output/data_processor_$(date +%Y%m%d).log \
2>> /var/log/your_app/error/data_processor_$(date +%Y%m%d).log \
</dev/null &
```
这里我使用了`$(date +%Y%m%d)`来按日期分割日志,这是管理长期运行服务的关键技巧。否则,单个日志文件会无限增长,最终导致各种问题。
### 2.3 进程管理与状态检查技巧
启动脚本只是第一步,更重要的是如何管理运行中的进程。下面是一些实用命令:
```bash
# 1. 查找特定脚本的进程
# 使用pgrep比ps | grep更可靠
pgrep -f "data_processor.py"
# 2. 查看进程的详细信息
# 包括内存使用、CPU占用等
ps -fp $(pgrep -f "data_processor.py")
# 3. 查看进程打开的文件(包括日志文件)
# 这在确认日志是否正确写入时很有用
sudo lsof -p $(pgrep -f "data_processor.py")
# 4. 实时监控进程资源使用
top -p $(pgrep -f "data_processor.py")
# 5. 查看进程的系统调用(高级调试)
sudo strace -p $(pgrep -f "data_processor.py") -o /tmp/strace.log
```
对于需要管理多个脚本的情况,我建议创建一个简单的管理脚本:
```bash
#!/bin/bash
# manage_scripts.sh - 统一管理多个后台脚本
SCRIPTS=(
"data_collector.py:/var/log/app/data_collector"
"api_server.py:/var/log/app/api_server"
"model_trainer.py:/var/log/app/model_trainer"
)
case "$1" in
start)
for item in "${SCRIPTS[@]}"; do
IFS=':' read -r script log_prefix <<< "$item"
echo "启动 $script..."
nohup python3 "$script" \
> "${log_prefix}_output_$(date +%Y%m%d).log" \
2>> "${log_prefix}_error_$(date +%Y%m%d).log" \
</dev/null &
echo "PID: $!"
done
;;
stop)
for item in "${SCRIPTS[@]}"; do
IFS=':' read -r script _ <<< "$item"
pid=$(pgrep -f "$script")
if [ -n "$pid" ]; then
echo "停止 $script (PID: $pid)..."
kill -TERM "$pid"
fi
done
;;
status)
for item in "${SCRIPTS[@]}"; do
IFS=':' read -r script _ <<< "$item"
pid=$(pgrep -f "$script")
if [ -n "$pid" ]; then
echo "✓ $script 正在运行 (PID: $pid)"
else
echo "✗ $script 未运行"
fi
done
;;
*)
echo "用法: $0 {start|stop|status}"
exit 1
;;
esac
```
这个管理脚本提供了统一的启动、停止和状态检查接口,大大简化了多脚本管理的复杂度。你可以根据自己的需求扩展它,比如添加重启、日志轮转等功能。
## 3. 日志轮转:防止磁盘被撑爆的关键策略
日志管理是生产环境中最容易被忽视的环节。我曾经遇到过因为日志文件太大(超过100GB)导致系统无法正常写入,最终服务崩溃的情况。下面分享一套完整的日志轮转方案。
### 3.1 使用logrotate进行自动化日志管理
Ubuntu系统自带的`logrotate`工具是管理日志文件的瑞士军刀。它可以按时间、大小自动轮转日志,并压缩旧日志。下面是一个针对Python脚本日志的配置示例:
```bash
# /etc/logrotate.d/python_apps
/var/log/your_app/output/*.log
/var/log/your_app/error/*.log {
daily # 每天轮转一次
missingok # 如果日志文件不存在,不报错
rotate 30 # 保留30天的日志
compress # 压缩旧日志
delaycompress # 延迟压缩(方便查看最新日志)
notifempty # 如果日志为空,不轮转
create 644 $USER $USER # 创建新日志文件时的权限
sharedscripts # 所有文件轮转后执行一次脚本
postrotate
# 重新打开日志文件,确保程序能继续写入
# 这里需要向进程发送USR1信号(如果程序支持)
# 或者重启服务
for pid in $(pgrep -f "python3.*your_app"); do
kill -USR1 "$pid" 2>/dev/null || true
done
endscript
}
```
这个配置实现了:
- 每天自动轮转日志
- 保留最近30天的日志
- 自动压缩旧日志节省空间
- 轮转后通知相关进程
要使配置生效,可以手动测试:
```bash
# 测试配置是否正确
sudo logrotate -d /etc/logrotate.d/python_apps
# 强制立即执行轮转
sudo logrotate -f /etc/logrotate.d/python_apps
```
### 3.2 Python内置的日志轮转支持
除了系统级的`logrotate`,Python的`logging`模块也提供了强大的日志轮转功能。如果你的脚本是自己开发的,建议直接使用这些功能:
```python
# script_with_logging.py
import logging
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
import time
# 创建logger
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
# 方法1:按文件大小轮转(每个文件最大10MB,保留5个备份)
size_handler = RotatingFileHandler(
'/var/log/app/output.log',
maxBytes=10*1024*1024, # 10MB
backupCount=5
)
# 方法2:按时间轮转(每天午夜轮转,保留30天)
time_handler = TimedRotatingFileHandler(
'/var/log/app/output.log',
when='midnight',
interval=1,
backupCount=30
)
# 设置日志格式
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
size_handler.setFormatter(formatter)
time_handler.setFormatter(formatter)
# 添加处理器
logger.addHandler(size_handler)
# 示例日志输出
def main():
while True:
logger.info("程序正常运行中...")
try:
# 你的业务逻辑
pass
except Exception as e:
logger.error(f"发生错误: {e}", exc_info=True)
time.sleep(60)
if __name__ == "__main__":
main()
```
Python内置的日志处理器比系统级的`logrotate`更精确,因为它们知道何时安全地关闭当前日志文件并打开新文件。对于关键应用,我建议同时使用两种方法:Python内置轮转处理日常管理,`logrotate`作为后备方案处理异常情况。
### 3.3 日志监控与告警
日志轮转解决了存储问题,但更重要的是从日志中发现问题。以下是一些实用的日志监控技巧:
```bash
# 1. 实时监控错误日志
tail -f /var/log/your_app/error/*.log | grep -E "(ERROR|CRITICAL|Exception|Traceback)"
# 2. 统计错误频率(最近1小时)
find /var/log/your_app/error/ -name "*.log" -mmin -60 -exec grep -c "ERROR" {} \;
# 3. 创建简单的日志监控脚本
#!/bin/bash
# monitor_logs.sh
ERROR_LOG="/var/log/your_app/error/$(date +%Y%m%d).log"
ALERT_THRESHOLD=10 # 10分钟内10个错误就告警
# 检查最近10分钟的错误数量
error_count=$(tail -n 1000 "$ERROR_LOG" 2>/dev/null | \
grep -c "$(date -d '10 minutes ago' '+%Y-%m-%d %H:%M')")
if [ "$error_count" -ge "$ALERT_THRESHOLD" ]; then
# 发送告警(邮件、Slack、钉钉等)
echo "警告: 过去10分钟检测到 $error_count 个错误" | \
mail -s "应用错误告警" admin@example.com
# 或者调用Webhook
curl -X POST -H "Content-Type: application/json" \
-d "{\"text\":\"应用错误数异常: $error_count\"}" \
https://hooks.slack.com/services/your/webhook/url
fi
```
对于更复杂的监控需求,可以考虑使用专业的日志收集和分析工具,如ELK Stack(Elasticsearch, Logstash, Kibana)或Graylog。但对于大多数中小型应用,上述脚本已经足够。
## 4. 超越nohup:系统服务化与进程监控
虽然`nohup`简单易用,但对于生产环境的关键服务,我们需要更可靠的方案。将Python脚本转换为系统服务,可以获得开机自启、自动重启、资源限制等高级功能。
### 4.1 使用systemd创建可靠的服务
`systemd`是现代Linux系统的标准服务管理器。将Python脚本配置为systemd服务,可以获得企业级的可靠性。下面是一个完整的服务配置示例:
```ini
# /etc/systemd/system/data-processor.service
[Unit]
Description=数据处理器服务
After=network.target
Wants=network.target
[Service]
Type=simple
User=appuser # 指定运行用户,提高安全性
Group=appgroup
WorkingDirectory=/opt/your_app
# 重要:设置资源限制,防止脚本失控
LimitNOFILE=65535
LimitNPROC=4096
LimitCORE=infinity
# 环境变量
Environment="PYTHONPATH=/opt/your_app"
Environment="LOG_LEVEL=INFO"
# 执行命令
ExecStart=/usr/bin/python3 /opt/your_app/data_processor.py
# 重启策略:异常退出时自动重启
Restart=on-failure
RestartSec=10
StartLimitInterval=60
StartLimitBurst=3
# 标准输出和错误输出重定向
StandardOutput=journal
StandardError=journal
SyslogIdentifier=data-processor
# 安全加固
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/log/your_app /opt/your_app/data
[Install]
WantedBy=multi-user.target
```
这个配置文件做了很多重要的事情:
1. **指定运行用户**:不以root身份运行,提高安全性
2. **资源限制**:防止脚本消耗过多系统资源
3. **自动重启**:脚本崩溃后自动恢复
4. **日志集成**:使用系统日志(journald)统一管理
5. **安全加固**:限制服务的权限和访问路径
配置完成后,使用以下命令管理服务:
```bash
# 重新加载systemd配置
sudo systemctl daemon-reload
# 启动服务
sudo systemctl start data-processor
# 查看服务状态
sudo systemctl status data-processor
# 查看服务日志
sudo journalctl -u data-processor -f # 实时跟踪
sudo journalctl -u data-processor --since "2024-01-01" # 按时间筛选
# 设置开机自启
sudo systemctl enable data-processor
# 停止服务
sudo systemctl stop data-processor
```
### 4.2 使用Supervisor进行进程监控
如果你使用的是较旧的系统,或者需要更灵活的进程管理,`Supervisor`是一个优秀的选择。它专门为管理进程而设计,提供了Web界面和丰富的监控功能。
首先安装Supervisor:
```bash
# Ubuntu/Debian
sudo apt-get install supervisor
# CentOS/RHEL
sudo yum install supervisor
```
然后创建配置文件:
```ini
# /etc/supervisor/conf.d/data-processor.conf
[program:data-processor]
command=/usr/bin/python3 /opt/your_app/data_processor.py
directory=/opt/your_app
user=appuser
autostart=true
autorestart=true
startretries=3
stderr_logfile=/var/log/supervisor/data-processor.err.log
stdout_logfile=/var/log/supervisor/data-processor.out.log
environment=PYTHONPATH="/opt/your_app",LOG_LEVEL="INFO"
# 资源限制
priority=100
numprocs=1
process_name=%(program_name)s_%(process_num)02d
# 信号处理
stopsignal=TERM
stopwaitsecs=10
stopasgroup=true
killasgroup=true
```
管理Supervisor服务:
```bash
# 重新加载配置
sudo supervisorctl reread
sudo supervisorctl update
# 启动/停止/重启特定服务
sudo supervisorctl start data-processor
sudo supervisorctl stop data-processor
sudo supervisorctl restart data-processor
# 查看所有服务状态
sudo supervisorctl status
# 进入交互模式
sudo supervisorctl
# 查看服务日志
tail -f /var/log/supervisor/data-processor.out.log
```
Supervisor的Web界面(需要额外配置)提供了直观的管理界面,特别适合管理多个服务的场景。
### 4.3 容器化部署:更现代的解决方案
对于复杂的Python应用,容器化部署可能是更好的选择。Docker提供了完整的隔离环境,简化了依赖管理和部署流程。
```dockerfile
# Dockerfile
FROM python:3.9-slim
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY requirements.txt .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用代码
COPY . .
# 创建非root用户
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# 设置环境变量
ENV PYTHONUNBUFFERED=1
ENV LOG_LEVEL=INFO
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:8080/health')"
# 运行应用
CMD ["python", "data_processor.py"]
```
使用Docker Compose管理多服务:
```yaml
# docker-compose.yml
version: '3.8'
services:
data-processor:
build: .
container_name: data-processor
restart: unless-stopped
volumes:
- ./data:/app/data
- ./logs:/app/logs
environment:
- LOG_LEVEL=INFO
- DATABASE_URL=postgresql://user:pass@db:5432/app
depends_on:
- db
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: example
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
```
容器化部署的优势包括环境一致性、易于扩展和更好的资源隔离。对于微服务架构的应用,这是首选的部署方式。
## 5. 高级技巧与故障排除
即使有了完善的守护方案,实际运行中仍然可能遇到各种问题。下面分享一些高级技巧和常见问题的解决方法。
### 5.1 信号处理与优雅关闭
Python脚本需要正确处理信号,才能实现优雅关闭。这确保了在停止服务时,脚本能完成当前任务并清理资源。
```python
# graceful_shutdown.py
import signal
import sys
import time
import logging
logger = logging.getLogger(__name__)
# 全局标志,用于控制主循环
running = True
def signal_handler(signum, frame):
"""处理终止信号"""
global running
logger.info(f"收到信号 {signum},开始优雅关闭...")
running = False
def cleanup():
"""清理资源"""
logger.info("清理资源...")
# 关闭数据库连接、文件句柄等
time.sleep(2) # 模拟清理操作
logger.info("资源清理完成")
def main():
# 注册信号处理器
signal.signal(signal.SIGTERM, signal_handler) # kill命令默认信号
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
signal.signal(signal.SIGHUP, signal_handler) # 终端挂断
logger.info("服务启动...")
try:
while running:
# 主业务逻辑
logger.info("处理数据中...")
time.sleep(1)
# 检查点:每次循环检查是否应该退出
if not running:
break
except Exception as e:
logger.error(f"未处理的异常: {e}", exc_info=True)
finally:
cleanup()
logger.info("服务正常退出")
if __name__ == "__main__":
main()
```
这个脚本展示了如何:
1. 捕获终止信号并设置退出标志
2. 在主循环中定期检查退出标志
3. 在finally块中确保资源清理
对于使用`systemd`或`Supervisor`管理的服务,优雅关闭尤为重要,因为管理工具通常会先发送`SIGTERM`信号,等待一段时间后再发送`SIGKILL`。
### 5.2 内存泄漏检测与预防
长期运行的Python脚本容易发生内存泄漏。以下是一些检测和预防方法:
```python
# memory_monitor.py
import gc
import tracemalloc
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
class MemoryMonitor:
def __init__(self, interval=300):
"""初始化内存监控器
Args:
interval: 检查间隔(秒)
"""
self.interval = interval
self.last_check = datetime.now()
tracemalloc.start()
def check_memory(self):
"""检查内存使用情况"""
current_time = datetime.now()
if (current_time - self.last_check).seconds < self.interval:
return
# 获取当前内存快照
snapshot = tracemalloc.take_snapshot()
# 统计前10个内存占用最大的对象
top_stats = snapshot.statistics('lineno')[:10]
logger.info("内存使用统计:")
for stat in top_stats:
logger.info(f" {stat}")
# 强制垃圾回收(谨慎使用)
unreachable = gc.collect()
if unreachable > 0:
logger.warning(f"垃圾回收释放了 {unreachable} 个对象")
self.last_check = current_time
def track_object(self, obj, name):
"""跟踪特定对象的内存使用"""
# 使用weakref避免影响引用计数
import weakref
ref = weakref.ref(obj)
# 这里可以记录对象信息用于后续分析
return ref
# 在业务代码中使用
monitor = MemoryMonitor(interval=600) # 每10分钟检查一次
def process_data():
while True:
# 业务逻辑
data = fetch_data()
result = analyze_data(data)
store_result(result)
# 定期检查内存
monitor.check_memory()
# 清理临时变量
del data, result
```
除了代码层面的监控,还可以使用系统工具:
```bash
# 监控Python进程的内存使用
watch -n 5 "ps -p $(pgrep -f your_script.py) -o pid,ppid,pmem,pcpu,rss,vsz,cmd"
# 使用memory_profiler分析内存使用
# 首先安装:pip install memory_profiler
# 然后在代码中添加装饰器
# @profile
# def your_function():
# ...
# 使用valgrind进行更深入的分析(需要编译支持)
valgrind --tool=memcheck --leak-check=full python your_script.py
```
### 5.3 性能监控与优化
对于需要长期运行的服务,性能监控同样重要。以下是一个简单的性能监控实现:
```python
# performance_monitor.py
import time
import logging
from functools import wraps
from collections import defaultdict
from datetime import datetime, timedelta
logger = logging.getLogger(__name__)
class PerformanceMonitor:
def __init__(self):
self.metrics = defaultdict(list)
self.start_time = time.time()
def track(self, metric_name):
"""装饰器:跟踪函数执行时间"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
result = func(*args, **kwargs)
return result
finally:
duration = time.perf_counter() - start
self.record_metric(metric_name, duration)
return wrapper
return decorator
def record_metric(self, name, value):
"""记录指标"""
self.metrics[name].append({
'timestamp': datetime.now(),
'value': value
})
# 保留最近1小时的数据
cutoff = datetime.now() - timedelta(hours=1)
self.metrics[name] = [
m for m in self.metrics[name]
if m['timestamp'] > cutoff
]
def get_stats(self, metric_name):
"""获取统计信息"""
values = [m['value'] for m in self.metrics.get(metric_name, [])]
if not values:
return None
return {
'count': len(values),
'min': min(values),
'max': max(values),
'avg': sum(values) / len(values),
'p95': sorted(values)[int(len(values) * 0.95)],
'last_hour': len(values)
}
def report(self):
"""生成性能报告"""
uptime = time.time() - self.start_time
logger.info(f"服务运行时间: {uptime:.2f}秒")
for metric_name in self.metrics:
stats = self.get_stats(metric_name)
if stats:
logger.info(
f"指标 {metric_name}: "
f"平均={stats['avg']:.4f}s, "
f"P95={stats['p95']:.4f}s, "
f"最近1小时调用={stats['last_hour']}次"
)
# 使用示例
monitor = PerformanceMonitor()
@monitor.track('data_processing')
def process_data_batch(batch):
# 数据处理逻辑
time.sleep(0.1) # 模拟处理时间
return len(batch)
# 定期报告性能
def periodic_report():
while True:
time.sleep(300) # 每5分钟报告一次
monitor.report()
```
这个性能监控器可以帮助你:
1. 识别性能瓶颈
2. 监控服务质量
3. 为容量规划提供数据支持
4. 及时发现性能退化
### 5.4 常见问题与解决方案
在实际运维中,我遇到过各种各样的问题。下面是一些常见问题及其解决方案:
**问题1:脚本突然停止,没有错误日志**
可能原因和解决方案:
- **磁盘空间不足**:监控磁盘使用率,设置日志轮转
- **内存不足被OOM Killer终止**:检查系统日志`/var/log/kern.log`,优化内存使用
- **权限问题**:确保日志目录可写,使用正确的用户运行
**问题2:日志文件不更新**
可能原因和解决方案:
- **文件句柄泄漏**:使用`lsof -p <pid>`检查,确保正确关闭文件
- **缓冲区问题**:在Python中设置`flush=True`或使用`-u`参数运行Python
- **日志轮转后程序未重新打开文件**:实现信号处理重新打开日志文件
**问题3:CPU使用率异常高**
排查步骤:
```bash
# 1. 查看哪个线程占用CPU
top -H -p $(pgrep -f your_script.py)
# 2. 使用py-spy进行性能分析
pip install py-spy
py-spy top --pid $(pgrep -f your_script.py)
# 3. 生成火焰图
py-spy record -o profile.svg --pid $(pgrep -f your_script.py)
```
**问题4:脚本无法正常停止**
解决方案:
```bash
# 1. 先尝试优雅停止
kill -TERM $(pgrep -f your_script.py)
# 2. 等待一段时间(比如30秒)
sleep 30
# 3. 如果还在运行,强制停止
kill -KILL $(pgrep -f your_script.py)
# 4. 检查是否有子进程残留
pstree -p $(pgrep -f your_script.py)
```
**问题5:多脚本之间的协调**
当需要运行多个相关脚本时,协调它们很重要:
```python
# coordinator.py
import subprocess
import time
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
class ScriptCoordinator:
def __init__(self, scripts_config):
self.scripts = scripts_config
self.processes = {}
def start_all(self):
"""启动所有脚本"""
for name, config in self.scripts.items():
self.start_script(name, config)
def start_script(self, name, config):
"""启动单个脚本"""
log_dir = Path(config['log_dir'])
log_dir.mkdir(parents=True, exist_ok=True)
cmd = [
'nohup',
'python3', config['script_path'],
'>', str(log_dir / f"{name}_output.log"),
'2>>', str(log_dir / f"{name}_error.log"),
'</dev/null', '&'
]
logger.info(f"启动脚本 {name}: {' '.join(cmd)}")
process = subprocess.Popen(
' '.join(cmd),
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
self.processes[name] = process
logger.info(f"脚本 {name} 已启动,PID: {process.pid}")
def monitor(self):
"""监控所有脚本"""
while True:
for name, process in list(self.processes.items()):
retcode = process.poll()
if retcode is not None:
logger.error(f"脚本 {name} 已退出,返回码: {retcode}")
# 自动重启
self.restart_script(name)
time.sleep(60) # 每分钟检查一次
def restart_script(self, name):
"""重启脚本"""
if name in self.scripts:
logger.info(f"重启脚本 {name}")
self.stop_script(name)
time.sleep(2)
self.start_script(name, self.scripts[name])
def stop_script(self, name):
"""停止脚本"""
if name in self.processes:
process = self.processes[name]
process.terminate()
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
del self.processes[name]
def stop_all(self):
"""停止所有脚本"""
for name in list(self.processes.keys()):
self.stop_script(name)
# 配置示例
scripts_config = {
'data_collector': {
'script_path': '/opt/app/data_collector.py',
'log_dir': '/var/log/app/data_collector'
},
'data_processor': {
'script_path': '/opt/app/data_processor.py',
'log_dir': '/var/log/app/data_processor'
},
'api_server': {
'script_path': '/opt/app/api_server.py',
'log_dir': '/var/log/app/api_server'
}
}
if __name__ == "__main__":
coordinator = ScriptCoordinator(scripts_config)
coordinator.start_all()
try:
coordinator.monitor()
except KeyboardInterrupt:
coordinator.stop_all()
```
这个协调器提供了统一的管理界面,可以同时启动、监控和重启多个相关脚本,特别适合微服务架构的应用。
通过上述方案,你的Python脚本将获得生产级的可靠性和可维护性。从简单的`nohup`到完整的服务化部署,每一步都在增加系统的稳定性和运维的便利性。在实际项目中,我建议根据应用的重要性和团队的技术栈选择合适的方案。对于关键业务系统,投入时间建立完善的守护和监控机制,长远来看会节省大量的故障处理时间。