systemd 管理 Flask 服务实践

背景配置

一个用于启动 flask 服务的 sh 脚本方式,start.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# shellcheck disable=SC1090
source ~/tools/miniforge3/etc/profile.d/conda.sh
conda activate vltenvLinux
which python
python --version
flask --version
gunicorn --version
export $(grep -v '^#' .env | xargs)

TIMESTAMP=$(date '+%Y_%m_%d_%H_%M_%S')
ENABLE_SCHEDULER=true gunicorn -w 4 -b 0.0.0.0:5001 app:app \
--access-logfile "logs/gunicorn_access_${TIMESTAMP}.log" \
--error-logfile "logs/gunicorn_error_${TIMESTAMP}.log" \
--log-level info \
--timeout 120 \
--keep-alive 5 \
--max-requests 1000 \
--max-requests-jitter 100 \
--preload

启动方式:nohup ./start.sh >nohup.log 2>&1 &

转为 systemd 管理

step1,创建 systemd 服务文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# /home/autotest/vlt/vlt_track
(vltenvLinux) [autotest@dgvxl2905 vlt_track]$ cat vlt_track_app.service
[Unit]
Description=VLT TRACK Flask Application with Gunicorn
After=network.target

[Service]
Type=notify
User=autotest
WorkingDirectory=/home/autotest/vlt/vlt_track

Environment="PATH=/home/autotest/tools/miniforge3/envs/vltenvLinux/bin:/usr/local/bin:/usr/bin:/bin"
EnvironmentFile=/home/autotest/vlt/vlt_track/.env
Environment="ENABLE_SCHEDULER=true"

ExecStart=/home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn \
-w 4 \
-b 0.0.0.0:5000 \
app:app \
--access-logfile /home/autotest/vlt/vlt_track/logs/gunicorn_access.log \
--error-logfile /home/autotest/vlt/vlt_track/logs/gunicorn_error.log \
--log-level info \
--timeout 120 \
--keep-alive 5 \
--max-requests 1000 \
--max-requests-jitter 100 \
--preload

# 优雅重启(用于日志轮转)
ExecReload=/bin/kill -USR1 $MAINPID

Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
1
2
mkdir -p /home/autotest/vlt/vlt_track/logs
sudo ln -s /home/autotest/vlt/vlt_track/vlt_track_app.service /etc/systemd/system/vlt_track_app.service

step2,创建 logrotate 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(base) [root@dgvxl2905 vlt_track]# cd /etc/logrotate.d/
(base) [root@dgvxl2905 logrotate.d]# cat vlt_track
/home/autotest/vlt/vlt_track/logs/gunicorn_*.log {
su autotest autotest
daily
rotate 30
compress
delaycompress
notifempty
missingok
dateext
dateformat _%Y%m%d
create 0644 autotest autotest
sharedscripts
postrotate
/bin/systemctl reload vlt_track_app.service > /dev/null 2>&1 || true
endscript
}

测试 logrotate 配置

1
2
3
4
5
6
7
8
9
10
11
(base) [root@dgvxl2905 logrotate.d]# logrotate -d /etc/logrotate.d/vlt_track
reading config file /etc/logrotate.d/vlt_track
Allocating hash table for state file, size 15360 B

Handling 1 logs

rotating pattern: /home/autotest/vlt/vlt_track/logs/gunicorn_*.log after 1 days (30 rotations)
empty log files are not rotated, old logs are removed
switching euid to 5991 and egid to 5991
considering log /home/autotest/vlt/vlt_track/logs/gunicorn_access_2025_08_06_11_21_11.log
log does not need rotating (log has been already rotated)considering log /home/autotest/vlt/vlt_track/logs/gunicorn_access_2025_08_06_11_48_12.log

step3,启动服务

1
2
3
systemctl daemon-reload
systemctl start vlt_track_app
systemctl status vlt_track_app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(base) [root@dgvxl2905 vlt_track]# systemctl status vlt_track_app
● vlt_track_app.service - VLT TRACK Flask Application with Gunicorn
Loaded: loaded (/home/autotest/vlt/vlt_track/vlt_track_app.service; linked; vendor preset: disabled)
Active: active (running) since Fri 2025-10-17 10:17:41 CST; 2s ago
Main PID: 6208 (gunicorn)
Status: "Gunicorn arbiter booted"
Tasks: 8
Memory: 54.3M
CGroup: /system.slice/vlt_track_app.service
├─6208 /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/python /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn -w 4 -b 0.0.0.0:...
├─6213 /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/python /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn -w 4 -b 0.0.0.0:...
├─6228 /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/python /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn -w 4 -b 0.0.0.0:...
├─6229 /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/python /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn -w 4 -b 0.0.0.0:...
└─6230 /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/python /home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn -w 4 -b 0.0.0.0:...

Oct 17 10:17:40 dgvxl2905 systemd[1]: Starting VLT TRACK Flask Application with Gunicorn...
Oct 17 10:17:41 dgvxl2905 gunicorn[6208]: 2025-10-17 10:17:41.336 | INFO | utils.api_scanner:start_scheduler:297 - API扫描-定时任务已启动
Oct 17 10:17:41 dgvxl2905 gunicorn[6208]: 2025-10-17 10:17:41.336 | INFO | utils.api_scanner:start_scheduler:298 - API扫描-定时任务时间: 1点0分
Oct 17 10:17:41 dgvxl2905 gunicorn[6208]: 2025-10-17 10:17:41.336 | INFO | utils.api_scanner:init_api_scanner_scheduler:564 - API扫描-定时任务已…PID: 6208)
Oct 17 10:17:41 dgvxl2905 gunicorn[6208]: 2025-10-17 10:17:41.338 | INFO | utils.statistics_collector:start_scheduler:37 - 统计数据-定时任务已启…PID: 6208)
Oct 17 10:17:41 dgvxl2905 gunicorn[6208]: 2025-10-17 10:17:41.338 | INFO | utils.statistics_collector:init_statistics_collector_scheduler:70 - …(PID: 6208)
Oct 17 10:17:41 dgvxl2905 systemd[1]: Started VLT TRACK Flask Application with Gunicorn.
Hint: Some lines were ellipsized, use -l to show in full.

注意点

1. 路径相关

常见错误

1
2
3
4
5
6
7
# 不要使用 ~ 波浪号
WorkingDirectory=~/vlt/vlt_track
Environment="PATH=~/tools/bin:$PATH"
# 不要使用相对路径
ExecStart=./start.sh
# 不要使用环境变量展开
WorkingDirectory=$HOME/app

正确做法

1
2
3
4
# 必须使用绝对路径
WorkingDirectory=/home/autotest/vlt/vlt_track
Environment="PATH=/home/autotest/tools/miniforge3/envs/vltenvLinux/bin:/usr/bin:/bin"
ExecStart=/home/autotest/vlt/vlt_track/start.sh

2. 环境变量

常见错误

1
2
3
4
5
6
# 不能在一行中使用 shell 变量引用
Environment="PATH=$CONDA_HOME/bin:$PATH"
# 不能导出多个变量在一行
Environment="VAR1=value1 VAR2=value2"
# 引号使用错误
Environment=PATH="/usr/bin:/bin"

正确做法

1
2
3
4
5
6
7
8
9
10
# 每个变量单独一行
Environment="PATH=/home/autotest/tools/miniforge3/envs/vltenvLinux/bin:/usr/bin:/bin"
Environment="ENABLE_SCHEDULER=true"
Environment="FLASK_ENV=production"

# 或使用 EnvironmentFile 读取 .env 文件
EnvironmentFile=/home/autotest/vlt/vlt_track/.env

# 引号在外面
Environment="PATH=/usr/bin:/bin"

3. Type 类型选择

1
2
3
4
5
6
7
8
9
10
11
12
13
# simple - 最常见,ExecStart 进程就是主进程
Type=simple

# forking - 进程会 fork 后台运行(如传统 daemon)
Type=forking
PIDFile=/var/run/myapp.pid

# notify - 进程通过 sd_notify 通知 systemd(gunicorn 支持)
Type=notify

# oneshot - 执行完就退出的任务
Type=oneshot
RemainAfterExit=yes

注意

  • gunicorn 使用 Type=notify 需要安装 systemd 支持
  • 如果 gunicorn 没有 systemd 支持,用 Type=simple 代替

4. 用户权限

1
2
3
4
5
6
7
8
9
10
# 指定运行用户
User=autotest
Group=autotest

# 如果需要提权(不推荐)
# User=root

# 安全限制
NoNewPrivileges=true
PrivateTmp=true

注意

  • 确保用户对 WorkingDirectory 和日志目录有权限
  • 确保用户对可执行文件有执行权限

5. 重启策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 总是重启(推荐生产环境)
Restart=always

# 仅非正常退出时重启
Restart=on-failure

# 不重启
Restart=no

# 重启间隔(防止频繁重启)
RestartSec=10

# 启动失败后重试次数限制
StartLimitBurst=5
StartLimitIntervalSec=60

6. 服务文件位置

1
2
3
4
5
6
7
8
9
10
# 不推荐:使用软链接可能导致权限问题(这里我觉得,用软连接,方便维护和同步)
sudo ln -s /home/autotest/vlt/vlt_track/vlt_track_app.service /etc/systemd/system/

# 推荐:直接复制
sudo cp vlt_track_app.service /etc/systemd/system/

# 或者用户服务(不需要 sudo)
mkdir -p ~/.config/systemd/user/
cp vlt_track_app.service ~/.config/systemd/user/
systemctl --user enable vlt_track_app

注意

  • 软链接的源文件如果权限不对,systemd 可能拒绝加载
  • 系统服务放在 /etc/systemd/system/
  • 用户服务放在 ~/.config/systemd/user/

7. 文件权限

1
2
3
4
5
6
7
8
# service 文件权限
sudo chmod 644 /etc/systemd/system/vlt_track_app.service

# 不要给执行权限
# ❌ chmod 755 vlt_track_app.service

# 执行脚本需要执行权限
chmod +x /home/autotest/vlt/vlt_track/start.sh

8. ExecStart 命令

常见错误

1
2
3
4
5
6
7
8
9
# 不能使用 shell 管道、重定向
ExecStart=/usr/bin/app > /var/log/app.log 2>&1
# 不能使用 &&、||(除非用 shell 包装)
ExecStart=/usr/bin/app && echo "started"
# 不能使用环境变量
ExecStart=$HOME/bin/app
# 反斜杠续行后不能有空格
ExecStart=/usr/bin/gunicorn \
-w 4 \

正确做法

1
2
3
4
5
6
7
8
9
10
11
# 简单命令
ExecStart=/home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn app:app

# 带参数(使用反斜杠续行)
ExecStart=/home/autotest/tools/miniforge3/envs/vltenvLinux/bin/gunicorn \
-w 4 \
-b 0.0.0.0:5000 \
app:app

# 需要 shell 特性时,用脚本包装
ExecStart=/bin/bash /home/autotest/vlt/vlt_track/start.sh

9. 日志管理

1
2
3
4
5
6
7
8
9
10
# 标准输出/错误输出到 journald
StandardOutput=journal
StandardError=journal

# 或输出到文件(需要配合 Type=simple)
StandardOutput=append:/var/log/myapp.log
StandardError=append:/var/log/myapp_error.log

# 或者完全禁用
StandardOutput=null

10. 修改后的操作流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 修改 service 文件后,必须 reload
sudo systemctl daemon-reload

# 2. 重启服务使配置生效
sudo systemctl restart vlt_track_app

# 3. 检查状态
sudo systemctl status vlt_track_app

# 4. 查看完整日志(-l 显示完整行)
sudo systemctl status vlt_track_app -l

# 5. 实时查看日志
sudo journalctl -u vlt_track_app -f

# 6. 查看启动失败原因
sudo journalctl -u vlt_track_app -n 50 --no-pager

11. 依赖关系

1
2
3
4
5
6
7
8
9
10
11
12
[Unit]
# 在网络就绪后启动
After=network.target

# 在数据库服务后启动
After=postgresql.service
Requires=postgresql.service

# 依赖关系说明:
# After - 启动顺序(但不强制依赖)
# Requires - 强依赖(依赖服务停止,本服务也停止)
# Wants - 弱依赖(依赖服务失败,本服务仍启动)

12. 常见排错

1
2
3
4
5
6
7
8
9
10
11
# 检查 service 文件语法
systemd-analyze verify /etc/systemd/system/vlt_track_app.service

# 查看服务依赖树
systemctl list-dependencies vlt_track_app

# 查看服务启动耗时
systemd-analyze blame

# 查看某个服务的启动时间
systemd-analyze critical-chain vlt_track_app

13. 配置文件名称规范

1
2
3
4
5
6
7
# 推荐命名
vlt_track_app.service
my-web-app.service

# 避免使用
vlt_track.service (与其他配置中的引用要保持一致)
vlt-track-app.service (虽然可以,但下划线更清晰)

14. EnvironmentFile 注意事项

1
2
3
4
5
6
7
8
9
10
# .env 文件格式
# 正确
DATABASE_URL=postgresql://localhost/db
ENABLE_SCHEDULER=true

# 错误(不要 export)
export DATABASE_URL=postgresql://localhost/db

# 错误(不要引号,除非值本身包含空格)
DATABASE_URL="postgresql://localhost/db"

核心原则

  1. 绝对路径优先 - 避免任何路径歧义
  2. 权限最小化 - 用普通用户运行,除非必须 root
  3. 日志要完善 - 便于排查问题
  4. 重启策略 - 生产环境务必配置自动重启
  5. 修改后 reload - daemon-reload 是必须的
  6. 服务名一致性 - service 文件名和引用要匹配