一、背景
我开发了一个 Flask 数据采集控制台(工银理财在售产品采集),数据源是工银理财官网,前端页面每 1.2 秒轮询一次 /api/status 展示采集进度。应用有两个"特殊体质":
- 任务状态全部在内存里 (后台线程采集 + 全局变量记录进度),所以必须单进程常驻,任何形式的进程回收/重启都会导致采集任务状态丢失;
-
- 代码本身没有任何改动空间(部署目标就是"不改代码")。
部署目标:Windows Server + IIS,把python web_app.py跑起来的 5022 端口,正式变成 IIS 托管的站点。
- 代码本身没有任何改动空间(部署目标就是"不改代码")。
二、方案选型:为什么是 IIS + wfastcgi
一开始考虑了两个方案:
| 方案 | 说明 | 结论 |
|---|---|---|
| A:IIS + wfastcgi | 微软官方推荐的 Python 托管方式,FastCGI 协议,应用代码零改动 | ✅ 选用 |
| B:宝塔面板反向代理 | 本机是 Windows 版宝塔,官方未集成 Python 项目管理器,反向代理还依赖 IIS URL Rewrite 模块(未安装) | ❌ 仅作备选 |
核心结论:Windows 上部署 Flask,IIS + wfastcgi 是最正统的路线;宝塔的 Python 支持主要在 Linux 版,Windows 版基本指望不上。
三、部署步骤全记录
1. 环境与工具
- Python 3.11.5(
D:\Programs\Python\Python311\python.exe,PyCharm 同款解释器) -
- IIS(本机已装,有多个既有站点)
-
- 全程用管理员 PowerShell +
appcmd.exe(C:\Windows\System32\inetsrv\appcmd.exe)
- 全程用管理员 PowerShell +
注意:本机 PowerShell 是 5.1 老版本,不支持
&&串联命令 ,一直用;。另外appcmd传中文参数值会丢字(后面有专门一坑),涉及中文的配置我后来都绕开了。
2. 安装 wfastcgi
powershell
D:\Programs\Python\Python311\python.exe -m pip install wfastcgi
安装后 wfastcgi.py 落在 Lib\site-packages\wfastcgi.py(版本 3.0.0)。
官方推荐
python -m wfastcgi enable一键注册,但我执行时挂起超时了(它内部还会去改 IIS 配置),于是放弃,改为手动用 appcmd 注册,可控性反而更好。
3. 注册 FastCGI 应用(坑 1:必须 fullPath + arguments 一起注册)
powershell
appcmd set config -section:system.webServer/fastCgi "/+[fullPath='D:\Programs\Python\Python311\python.exe',arguments='D:\Programs\Python\Python311\Lib\site-packages\wfastcgi.py',signalBeforeTerminateSeconds='30']" /commit:apphost
# 防回收三件套(采集任务常驻必需)
appcmd set config -section:system.webServer/fastCgi "/[fullPath='D:\Programs\Python\Python311\python.exe'].instanceMaxRequests:10000000" /commit:apphost
appcmd set config -section:system.webServer/fastCgi "/[fullPath='D:\Programs\Python\Python311\python.exe'].activityTimeout:600" /commit:apphost
appcmd set config -section:system.webServer/fastCgi "/[fullPath='D:\Programs\Python\Python311\python.exe'].requestTimeout:600" /commit:apphost
坑 1 详解 :只注册 fullPath 不带 arguments 时,站点启动报 0x80070585(无法在 fastCGI 配置中找到 handler scriptProcessor)。因为 wfastcgi 的 handler 匹配规则是 fullPath + arguments 必须同时匹配 ,漏掉 arguments 就等于没注册上。另外 appcmd 的 instanceMaxRequests 不接受 0(最小 1),10000000(1e7)等效"永不回收"。
4. 编写 web.config
放在项目根目录(应用物理路径下):
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<handlers>
<add name="Python FastCGI" path="*" verb="*" modules="FastCgiModule" resourceType="Unspecified"
scriptProcessor="D:\Programs\Python\Python311\python.exe|D:\Programs\Python\Python311\Lib\site-packages\wfastcgi.py" />
</handlers>
<appSettings>
<add key="WSGI_HANDLER" value="web_app.app" />
<add key="PYTHONPATH" value="D:\产品\MIS\FinancialProduct\src\icbc_wm" />
<add key="WSGI_LOG" value="D:\产品\MIS\FinancialProduct\src\icbc_wm\wfastcgi.log" />
</appSettings>
</system.webServer>
</configuration>
```
三个 appSettings 的含义:
- `WSGI_HANDLER`:WSGI 应用入口(`模块名.app`);
- - `PYTHONPATH`:项目目录,让 `import web_app` 能找到代码;
- - `WSGI_LOG`:**排障神器**,wfastcgi 会把全生命周期和 Python 异常 traceback 写进这个日志文件(后面排障全靠它)。
### 5. 创建 IIS 站点(坑 2:绑定字符串必须三段式)
```powershell
appcmd add site /name:icbc_wm /physicalPath:"D:\产品\MIS\FinancialProduct\src\icbc_wm" /bindings:http/*:5022
创建成功但启动时报 0x80070057 参数不正确。折腾半天发现根因:IIS 绑定字符串必须是 协议://IP:端口:主机名 三段式,末尾的冒号不能省,正确写法:
powershell
appcmd set site icbc_wm /bindings:http/*:5022:
6. 专用应用池(防回收核心配置)
powershell
appcmd add apppool /name:icbc_wm
appcmd set apppool icbc_wm /managedRuntimeVersion:"" # 无托管代码
appcmd set apppool icbc_wm /processModel.idleTimeout:00:00:00 # 空闲不回收
appcmd set apppool icbc_wm /recycling.periodicRestart.time:00:00:00 # 定时不回收
appcmd set app icbc_wm/ /applicationPool:icbc_wm
采集任务在后台线程里跑、进度存内存,任何回收都是灾难,所以:空闲超时 0、定期回收 0、instanceMaxRequests 拉到 1e7。
7. 防火墙放行端口
powershell
New-NetFirewallRule -DisplayName "icbc_wm 5022" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 5022
8. 权限修复(坑 3:应用池身份读不了 Python)
启动后日志报 0x8007010b 目录无效、FastCGI 进程意外退出。排查发现:D:\Programs\Python\Python311 的 ACL 只有 Administrators/SYSTEM,应用池身份(IIS APPPOOL\icbc_wm)根本读不了 python.exe。授权:
powershell
icacls "D:\Programs\Python\Python311" /grant "IIS APPPOOL\icbc_wm:(OI)(CI)RX" /T
icacls "D:\产品\MIS\FinancialProduct\src\icbc_wm" /grant "IIS APPPOOL\icbc_wm:(OI)(CI)M" /T
(Python 目录 10 万+文件,/T 递归跑了两分钟,属正常。)
9. 中文物理路径乱码(坑 4:最隐蔽的一坑)
权限修好后进程能启动了,但还是 500。这次用 WSGI_LOG 抓到了真相:
FileNotFoundError: [WinError 3] 系统找不到指定的路径。: 'D:\\²úÆ·\\MIS\\FinancialProduct\\src\\icbc_wm\\'
"产品"被 IIS 转成了 ²úÆ· !这是 GBK 编码被按 Latin-1 解码的经典乱码------IIS 在把物理路径作为 APPL_PHYSICAL_PATH 传给 FastCGI 进程时,对非 ASCII 路径会编码损坏 ,wfastcgi 里 os.chdir(physical_path) 直接 FileNotFoundError。
解法:目录联接(junction)方案------建一个纯 ASCII 路径的联接指向真实目录,站点物理路径用 ASCII 路径,代码零拷贝:
powershell
cmd /c mklink /J "D:\wwwroot\icbc_wm" "D:\产品\MIS\FinancialProduct\src\icbc_wm"
appcmd set vdir "icbc_wm/" /physicalPath:"D:\wwwroot\icbc_wm"
改完后 chdir 正常,进程顺利走到 import flask。
10. 依赖版本兼容(坑 5:Flask 1.1.2 与 Jinja2 3.1.6 不兼容)
日志显示下一个错误:
ImportError: cannot import name 'escape' from 'jinja2'
Flask 1.1.2 还在用 from jinja2 import escape 这种旧式导入,而环境里的 Jinja2 已经被升到 3.1.6(顶层 escape 被移除了)。验证一下:python -c "import flask" 直接复现同样报错------连手动 python web_app.py 都跑不起来了(之前能跑是因为旧进程没重启过)。
修复思路:这台机器的 Python 是共享环境(还装着 Flask-SocketIO、flask-cors 等,可能是别的项目在用),全局升级 Flask 风险大 。所以创建独立虚拟环境,只给这个应用用:
powershell
# 1. 创建 venv
D:\Programs\Python\Python311\python.exe -m venv D:\wwwroot\icbc_wm_venv
# 2. 安装兼容组合(Flask 2.x 对旧代码完全兼容)
D:\wwwroot\icbc_wm_venv\Scripts\python.exe -m pip install flask==2.3.3 pymysql requests wfastcgi -i https://pypi.tuna.tsinghua.edu.cn/simple
# 3. 重新注册 FastCGI 到 venv 的 python
appcmd set config -section:system.webServer/fastCgi "/+[fullPath='D:\wwwroot\icbc_wm_venv\Scripts\python.exe',arguments='D:\wwwroot\icbc_wm_venv\Lib\site-packages\wfastcgi.py',signalBeforeTerminateSeconds='30']" /commit:apphost
# 4. venv 目录同样授权给应用池
icacls "D:\wwwroot\icbc_wm_venv" /grant "IIS APPPOOL\icbc_wm:(OI)(CI)RX" /T
# 5. web.config 的 scriptProcessor 改为 venv 路径
心得 :部署环境里如果历史包袱(旧 Flask、混合装的包)已经没法理清,venv 隔离是成本最低、最不容易误伤其他项目的方案。
四、验收结果
| 测试项 | 结果 |
|---|---|
首页 GET / |
✅ 200,页面正常渲染(含"工银理财"标题) |
GET /api/status |
✅ 200,{"running":false,...} 正常 JSON |
GET /api/products |
✅ 200,返回 1656 只产品真实数据(主表采集时间 2026-09-14) |
| 回收应用池后再测 | ✅ 依然全部 200,配置持久生效 |
| 站点/应用池/防火墙 | ✅ 全部 Started / 已放行 |
五、排障方法论总结(心得)
这次部署最值钱的不是步骤,而是排障顺序:
- 先抓日志,别猜 。wfastcgi 的
WSGI_LOG环境变量是唯一能拿到 Python 侧完整 traceback 的通道。我前几个 500 全靠猜,效率极低;把WSGI_LOG加上后,每个错误都是一次定位。 -
- 区分"环境问题"和"IIS 问题" 。用同一个解释器手动
python -c "import flask"复现,就能判断报错跟 Web 环境无关、是包组合本身坏了------这直接决定了修复方向。
- 区分"环境问题"和"IIS 问题" 。用同一个解释器手动
-
- 中文是 Windows 部署的头号敌人。站点路径、命令行参数、环境变量值,任何带中文的地方都可能在不同环节被编码搞坏。能用 ASCII 就用 ASCII(junction、venv 都是这个思路)。
-
- 最小改动原则。共享 Python 环境别乱动,venv 隔离;系统配置改前先备份(我备份了 applicationHost.config)。
-
- 回收验证不可省。部署完回收一次应用池再测,确认配置是"持久生效"而不是"碰巧活着"。
六、上线后维护要点
- 改代码:直接改原目录
D:\产品\MIS\FinancialProduct\src\icbc_wm\(junction 自动同步),然后appcmd recycle apppool icbc_wm即可; -
wfastcgi.log会持续增长,定期清理;
-
- 应用池/站点配置都在 applicationHost.config,有备份
.bak_icbc。
- 应用池/站点配置都在 applicationHost.config,有备份
最终效果 :http://127.0.0.1:5022/ 由 IIS 托管,开机即用,无需手动 python web_app.py,采集任务单进程常驻不回收。希望这篇实录能帮到在 Windows 上部署 Flask 的同学们。