前言
开发调试阶段,运行 flask 的方式多直接使用 app.run(),但 flask 内置的 wsgi server 的性能并不高。对于生产环境,一般使用 gunicorn。如果老项目并不需要多高的性能,而且用了很多单进程内的共享变量,使用 gunicorn 会影响不同会话间的通信,那么也可以试试直接用 gevent。
在 docker 流行之前,生产环境部署 flask 项目多使用 virtualenv + gunicorn + supervisor。docker 流行之后,部署方式就换成了 gunicorn + docker。如果没有容器编排服务,后端服务前面一般还会有个 nginx 做代理。如果使用 kubernetes,一般会使用 service + ingress(或 istio 等)。
运行方式
flask 内置 wsgi server
开发阶段一般使用这种运行方式。
# main.py
from flask import flask
from time import sleep
app = flask(__name__)
@app.get("/test")
def get_test():
sleep(0.1)
return "ok"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=10000)
运行:
python main.py
gevent
使用 gevent 运行 flask,需要先安装 gevent
python -m pip install -u gevent
代码需要稍作修改。
需要注意 monkey.patch_all() 一定要写在入口代码文件的最开头部分,这样 monkey patch 才能生效。
# main.py
from gevent import monkey
monkey.patch_all()
import time
from flask import flask
from gevent.pywsgi import wsgiserver
app = flask(__name__)
@app.get("/test")
def get_test():
time.sleep(0.1)
return "ok"
if __name__ == "__main__":
server = wsgiserver(("0.0.0.0", 10000), app)
server.serve_forever()
运行
python main.py
gunicorn + gevent
如果现有项目大量使用单进程内的内存级共享变量,贸然使用 gunicorn 多 worker 模式可能会导致数据访问不一致的问题。
同样需要先安装依赖。
python -m pip install -u gunicorn gevent
不同于单独使用 gevent,这种方式不需要修改代码,gunicorn 会自动注入 gevent 的 monkey patch。
gunicorn 可以在命令行配置启动参数,但个人一般习惯在 gunicorn 的配置文件内配置启动参数,这样可以动态设置一些配置,而且可以修改日志格式。
gunicorn.conf.py 的配置示例如下:
# gunicorn 配置文件
from pathlib import path
from multiprocessing import cpu_count
import gunicorn.glogging
from datetime import datetime
class customlogger(gunicorn.glogging.logger):
def atoms(self, resp, req, environ, request_time):
"""
重写 atoms 方法来自定义日志占位符
"""
# 获取默认的所有占位符数据
atoms = super().atoms(resp, req, environ, request_time)
# 自定义 't' (时间戳) 的格式
now = datetime.now().astimezone()
atoms['t'] = now.isoformat(timespec="seconds")
return atoms
# 预加载应用代码
preload_app = true
# 工作进程数量:通常是 cpu 核心数的 2 倍加 1
# workers = int(cpu_count() * 2 + 1)
workers = 4
# 使用 gevent 异步 worker 类型,适合 i/o 密集型应用
# 注意:gevent worker 不使用 threads 参数,而是使用协程进行并发处理
worker_class = "gevent"
# 每个 gevent worker 可处理的最大并发连接数
worker_connections = 2000
# 绑定地址和端口
bind = "127.0.0.1:10001"
# 进程名称
proc_name = "flask-dev"
# pid 文件路径
pidfile = str(path(__file__).parent / "tmp" / "gunicorn.pid")
logger_class = customlogger
access_log_format = (
'{"@timestamp": "%(t)s", '
'"remote_addr": "%(h)s", '
'"protocol": "%(h)s", '
'"host": "%({host}i)s", '
'"request_method": "%(m)s", '
'"request_path": "%(u)s", '
'"status_code": %(s)s, '
'"response_length": %(b)s, '
'"referer": "%(f)s", '
'"user_agent": "%(a)s", '
'"x_tracking_id": "%({x-tracking-id}i)s", '
'"request_time": %(l)s}'
)
# 访问日志路径
accesslog = str(path(__file__).parent / "logs" / "access.log")
# 错误日志路径
errorlog = str(path(__file__).parent / "logs" / "error.log")
# 日志级别
loglevel = "debug"
运行。gunicorn 的默认配置文件名就是 gunicorn.conf.py,如果文件名不同,可以使用 -c 参数来指定。
gunicorn main:app
传统进程管理:实现自动启动
在传统服务器部署时,常见的进程守护方式有:
- 配置 crontab + shell 脚本。定时检查进程在不在,不在就启动。
- 配置 supervisor。
- 配置 systemd。
由于 supervisor 需要单独安装,而本着能用自带工具就用自带工具、能少装就少装的原则,个人一般不会使用 supervisor,因此本文不会涉及如何使用 supervisor。
在服务器部署时,一般也会为项目单独创建 python 虚拟环境。
# 使用 python 内置的 venv,在当前目录创建 python 虚拟环境目录 .venv python3 -m venv .venv source .venv/bin/activate python -m pip install -r ./requirements.txt # 如果使用uv, 直接uv sync 即可
crontab + shell 脚本 (不推荐生产环境)
刚入行的时候对 systemd 不熟悉,经常用 crontab + shell 脚本来守护进程,现在想想这种方式并不合适,比较考验 shell 脚本的编写水平,需要考虑方方面面
- 首先要确保用户级 crontab 启用,有些生产环境会禁用用户级的 crontab,而且也不允许随便配置系统级的 crontab。
- crontab 是分钟级的,服务停止时间可能要一分钟。
- 如果有控制台日志,需要手动处理日志重定向,还有日志文件轮转问题。
- 如果 ulimit 不高,还得控制 ulimit。
- 经常出现僵尸进程,shell 脚本来要写一堆状态检查的逻辑。
如果只需要简单用用,也可以提供个示例
#!/bin/bash
# 环境配置
export flask_env="production"
export database_url="postgresql://user:pass@localhost:5432/mydb"
export redis_url="redis://localhost:6379/0"
script_dir=$(cd $(dirname $0) && pwd)
app_name="gunicorn" # 实际进程名是 gunicorn,不是 flask app
wsgi_module="wsgi:app" # 替换 wsgi 入口
socket_path="${script_dir}/myapp.sock" # unix socket 路径(避免 /run 重启丢失)
log_file="${script_dir}/app.log"
pid_file="${script_dir}/gunicorn.pid" # 用 pid 文件控制
# 进程检测
is_running() {
if [ -f "$pid_file" ]; then
pid=$(cat "$pid_file")
if ps -p "$pid" > /dev/null 2>&1 && grep -q "gunicorn.*${wsgi_module}" /proc/"$pid"/cmdline 2>/dev/null; then
echo "gunicorn (pid: $pid) is running"
return 0
else
rm -f "$pid_file" # 清理失效 pid
echo "stale pid file found, cleaned up"
return 1
fi
else
# 备用检测:通过 socket 文件 + 进程名
if [ -s "$socket_path" ] && pgrep -f "gunicorn.*${wsgi_module}" > /dev/null 2>&1; then
echo "gunicorn is running (detected by socket)"
return 0
fi
echo "gunicorn is not running"
return 1
fi
}
# 启动应用
start_app() {
is_running
if [ $? -eq 0 ]; then
echo "already running, skip start"
return 0
fi
echo "starting gunicorn at $(date)"
echo "socket: $socket_path"
echo "log: $log_file"
# 确保 socket 目录存在
mkdir -p "$(dirname "$socket_path")"
# 启动命令(关键:不加 --daemon,用 nohup 托管)
cd "$script_dir" || exit 1
# 生成 pid 文件
nohup "$script_dir/venv/bin/gunicorn" \
--workers 3 \
--bind "unix:$socket_path" \
--pid "$pid_file" \
--access-logfile "$log_file" \
--error-logfile "$log_file" \
--log-level info \
"$wsgi_module" > /dev/null 2>&1 &
# 等待启动完成
sleep 2
if is_running; then
echo "✓ start success (pid: $(cat "$pid_file" 2>/dev/null))"
return 0
else
echo "✗ start failed, check $log_file"
return 1
fi
}
# 停止应用
stop_app() {
is_running
if [ $? -eq 1 ]; then
echo "not running, skip stop"
return 0
fi
pid=$(cat "$pid_file" 2>/dev/null)
echo "stopping gunicorn (pid: $pid) gracefully..."
# 先发 sigterm(优雅停止)
kill -15 "$pid" 2>/dev/null || true
sleep 5
# 检查是否还在运行
if ps -p "$pid" > /dev/null 2>&1; then
echo "still running after 5s, force killing..."
kill -9 "$pid" 2>/dev/null || true
sleep 2
fi
# 清理残留
rm -f "$pid_file" "$socket_path"
echo "✓ stopped"
}
# 重启应用
restart_app() {
echo "restarting gunicorn..."
stop_app
sleep 1
start_app
}
# 入口函数
main() {
# 检查 gunicorn 是否存在
if [ ! -f "$script_dir/venv/bin/gunicorn" ]; then
echo "error: gunicorn not found at $script_dir/venv/bin/gunicorn"
echo "hint: did you activate virtualenv? (source venv/bin/activate)"
exit 1
fi
local action=${1:-start} # 默认动作:start
case "$action" in
start)
start_app
;;
stop)
stop_app
;;
restart)
restart_app
;;
status)
is_running
;;
cron-check)
# 专为 crontab 设计:只检查+重启,不输出干扰日志
if ! is_running > /dev/null 2>&1; then
echo "[$(date '+%f %t')] cron: gunicorn down, auto-restarting..." >> "$log_file"
start_app >> "$log_file" 2>&1
fi
;;
*)
echo "usage: $0 {start|stop|restart|status|cron-check}"
echo " cron-check: silent mode for crontab (logs to app.log only)"
exit 1
;;
esac
}
main "$@"
手动运行测试
bash app_ctl.sh start
配置 crontab
# 编辑当前用户 crontab crontab -e # 添加以下行(每分钟检查一次) * * * * * /opt/myflaskapp/app_ctl.sh cron-check >/dev/null 2>&1
配置logrotate
# /etc/logrotate.d/myflaskapp
/opt/myflaskapp/app.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
copytruncate # 避免 gunicorn 丢失文件句柄
}
systemd (推荐生产环境使用)
- 创建 systemd 服务文件
sudo vim /etc/systemd/system/myflaskapp.service
- 示例如下
[unit]
description=gunicorn instance for flask app
after=network.target
[service]
user=www-data
group=www-data
workingdirectory=/path/to/your/app
environment="path=/path/to/venv/bin"
execstart=/path/to/venv/bin/gunicorn \
--workers 4 \
--bind unix:/run/myapp.sock \
--access-logfile - \
--error-logfile - \
wsgi:app
# 禁止添加 --daemon!systemd 需直接监控主进程
restart=on-failure # 仅异常退出时重启(非0状态码、被信号杀死等)
restartsec=5s # 重启前等待5秒
startlimitinterval=60s # 60秒内
startlimitburst=5 # 最多重启5次,防雪崩
timeoutstopsec=30 # 停止时等待30秒(优雅关闭)
# 安全加固
privatetmp=true
nonewprivileges=true
protectsystem=strict
readwritepaths=/run /var/log/myapp
[install]
wantedby=multi-user.target
- 设置开机自启并启动服务
sudo systemctl daemon-reload sudo systemctl enable myflaskapp # 开机自启 sudo systemctl start myflaskapp
可以试试用kill -9停止后端服务进程,观察能否被重新拉起。
注意,
kill -15算是正常停止,不算异常退出。
docker 部署方案
- dockerfile。python 项目通常不需要多阶段构建,单阶段即可。
from python:3.11-slim-bookworm
# 安全加固
## 创建非 root 用户(避免使用 nobody,权限太受限)
run useradd -m -u 1000 appuser && \
# 安装运行时必需的系统库(非编译工具)
apt-get update && apt-get install -y --no-install-recommends \
libgomp1 \
libpq5 \
libsqlite3-0 \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get autoremove -y \
&& apt-get clean
# python 优化
env pythonunbuffered=1 \
pythondontwritebytecode=1 \
pip_no_cache_dir=1 \
pip_disable_pip_version_check=1
workdir /app
# 利用 docker 层缓存:先复制 requirements
copy requirements.txt .
run pip install --no-cache-dir --prefer-binary -r requirements.txt \
# 清理 pip 缓存(虽然 --no-cache-dir 已禁用,但保险起见)
&& rm -rf /root/.cache
# 应用代码
copy --chown=appuser:appuser . .
# 使用非root用户运行
user appuser
# 启动
expose 8000
cmd ["gunicorn", "--config", "config/gunicorn.conf.py", "wsgi:app"]
- 编写 docker-compose.yaml
services:
web:
image: myflaskapp:latest
container_name: flask_web
# 端口映射
## 如果 nginx 也使用 docker 部署,而且使用同一个网络配置,则可以不做端口映射
ports:
- "8000:8000"
# 环境变量
environment:
- flask_env=production
- database_url=postgresql://user:pass@db:5432/mydb
- redis_url=redis://redis:6379/0
# 健康检查
healthcheck:
test: ["cmd", "curl", "-f", "http://localhost:8000/health"]
interval: 30s # 每 30 秒检查一次
timeout: 5s # 超时 5 秒
start_period: 15s # 启动后 15 秒开始检查(给应用初始化时间)
retries: 3 # 失败重试 3 次后标记 unhealthy
# 自动重启策略
restart: unless-stopped # always / on-failure / unless-stopped
# 资源限制
deploy:
resources:
limits:
cpus: '2' # 最多 2 个 cpu
memory: 1g # 最多 1gb 内存
reservations:
cpus: '0.5' # 保留 0.5 个 cpu
memory: 256m # 保留 256mb 内存
# ulimit 限制(防资源滥用)
ulimits:
nproc: 65535 # 最大进程数
nofile:
soft: 65535 # 打开文件数软限制
hard: 65535 # 打开文件数硬限制
core: 0 # 禁止 core dump
# 安全加固
security_opt:
- no-new-privileges:true # 禁止提权
# 只读文件系统(除 /tmp 外)
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=100m
# 卷挂载(日志、临时文件)
volumes:
- ./logs:/app/logs:rw
# - ./static:/app/static:ro # 静态文件(可选)
# 网络
networks:
- app-network
# 网络配置
networks:
app-network:
driver: bridge
# 卷配置
volumes:
db_data:
driver: local
redis_data:
driver: local
kubernetes 部署方案
deployment
apiversion: apps/v1
kind: deployment
metadata:
name: flask-app
namespace: default
labels:
app: flask-app
tier: backend
spec:
replicas: 3
selector:
matchlabels:
app: flask-app
template:
metadata:
labels:
app: flask-app
tier: backend
spec:
securitycontext:
runasnonroot: true # 禁止 root 运行
runasuser: 1000 # 使用非 root 用户
runasgroup: 1000
fsgroup: 1000
seccompprofile:
type: runtimedefault # 启用 seccomp 安全策略
containers:
- name: flask-app
image: myregistry.com/myflaskapp:1.0.0
imagepullpolicy: ifnotpresent # 生产环境建议用 always
ports:
- name: http
containerport: 8000
protocol: tcp
env:
- name: flask_env
value: "production"
- name: database_url
valuefrom:
secretkeyref:
name: flask-app-secrets
key: database-url
- name: redis_url
valuefrom:
secretkeyref:
name: flask-app-secrets
key: redis-url
- name: secret_key
valuefrom:
secretkeyref:
name: flask-app-secrets
key: secret-key
resources:
requests:
memory: "256mi"
cpu: "100m"
limits:
memory: "512mi" # 超过会 oom kill
cpu: "500m"
livenessprobe:
httpget:
path: /health
port: 8000
scheme: http
initialdelayseconds: 30 # 启动后 30 秒开始检查
periodseconds: 10 # 每 10 秒检查一次
timeoutseconds: 3 # 超时 3 秒
successthreshold: 1
failurethreshold: 3 # 失败 3 次后重启容器
readinessprobe:
httpget:
path: /health
port: 8000
scheme: http
initialdelayseconds: 10 # 启动后 10 秒开始检查
periodseconds: 5 # 每 5 秒检查一次
timeoutseconds: 2
successthreshold: 1
failurethreshold: 3 # 失败 3 次后从 service 移除
startupprobe:
httpget:
path: /health
port: 8000
scheme: http
failurethreshold: 30 # 最多重试 30 次
periodseconds: 5 # 每 5 秒一次,共 150 秒容忍慢启动
timeoutseconds: 3
securitycontext:
allowprivilegeescalation: false # 禁止提权
readonlyrootfilesystem: true # 根文件系统只读
capabilities:
drop:
- all # 删除所有 linux capabilities
privileged: false
volumemounts:
- name: tmp-volume
mountpath: /tmp
- name: config-volume
mountpath: /app/config
readonly: true
imagepullsecrets:
- name: registry-secret # 如果使用私有镜像仓库
affinity:
podantiaffinity:
preferredduringschedulingignoredduringexecution:
- weight: 100
podaffinityterm:
labelselector:
matchexpressions:
- key: app
operator: in
values:
- flask-app
topologykey: kubernetes.io/hostname # 避免所有 pod 调度到同一节点
volumes:
- name: tmp-volume
emptydir:
medium: memory # 使用内存卷,更快
sizelimit: 100mi
- name: config-volume
configmap:
name: flask-app-config
service
apiversion: v1
kind: service
metadata:
name: flask-app-service
namespace: default
labels:
app: flask-app
tier: backend
spec:
type: clusterip
selector:
app: flask-app
ports:
- name: http
port: 80 # service 端口
targetport: 8000 # pod 端口
protocol: tcp
ingress-nginx
apiversion: networking.k8s.io/v1
kind: ingress
metadata:
name: flask-app-ingress
namespace: default
annotations:
# ==================== nginx 配置 ====================
kubernetes.io/ingress.class: "nginx"
# 启用 https 重定向
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# 限流(每秒 10 个请求,突发 20)
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "2"
# 客户端真实 ip
nginx.ingress.kubernetes.io/enable-real-ip: "true"
nginx.ingress.kubernetes.io/proxy-real-ip-cidr: "0.0.0.0/0"
# 连接超时
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
nginx.ingress.kubernetes.io/proxy-send-timeout: "60"
nginx.ingress.kubernetes.io/proxy-read-timeout: "60"
# 缓冲区大小
nginx.ingress.kubernetes.io/proxy-buffering: "on"
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
# gzip 压缩
nginx.ingress.kubernetes.io/enable-gzip: "true"
nginx.ingress.kubernetes.io/gzip-level: "6"
nginx.ingress.kubernetes.io/gzip-min-length: "1024"
nginx.ingress.kubernetes.io/gzip-types: "text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript"
# 安全头
nginx.ingress.kubernetes.io/configuration-snippet: |
add_header x-frame-options "sameorigin" always;
add_header x-content-type-options "nosniff" always;
add_header x-xss-protection "1; mode=block" always;
add_header referrer-policy "strict-origin-when-cross-origin" always;
# 认证
# nginx.ingress.kubernetes.io/auth-type: basic
# nginx.ingress.kubernetes.io/auth-secret: flask-app-basic-auth
# nginx.ingress.kubernetes.io/auth-realm: "authentication required"
# 自定义错误页面
# nginx.ingress.kubernetes.io/custom-http-errors: "404,500,502,503,504"
# nginx.ingress.kubernetes.io/default-backend: custom-error-pages
# 重写目标
# nginx.ingress.kubernetes.io/rewrite-target: /$1
# waf(如果安装了 modsecurity)
# nginx.ingress.kubernetes.io/enable-modsecurity: "true"
# nginx.ingress.kubernetes.io/modsecurity-snippet: |
# secruleengine on
# secrequestbodyaccess on
spec:
tls:
- hosts:
- flask.example.com
secretname: flask-app-tls-secret # tls 证书 secret
rules:
- host: flask.example.com
http:
paths:
- path: /
pathtype: prefix
backend:
service:
name: flask-app-service
port:
number: 80到此这篇关于flask常见应用部署方案详解的文章就介绍到这了,更多相关flask常见应用部署方案内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论