ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Flask+ECharts 数据可视化看板:从开发到部署实战

Flask+ECharts 数据可视化看板:从开发到部署实战 Flask 是我这些年在需要快速搭一个 WEB 可视化界面时第一个会想到的工具。不是因为它最强而是因为它最不挡路。你不需要先去理解一堆约定、目录规范和配置体系几十行代码就能让一个页面跑起来把数据变成能看的图。这篇内容想聊的就是这件事从一台干净的机器开始怎么把一个带着图表、能筛选、能刷新、最后能挂到服务器上给别人访问的可视化界面做出来。整套东西适合有 Python 基础但没怎么碰过后端的人也适合做过前端、想自己接一套数据展示页面的开发者还适合那种手里有一堆数据、被要求下周给个看板的倒霉蛋。我会把每一步为什么这么做讲清楚包括那些文档里不写、只有真跑过一遍才知道的坑。1. 为什么我选 Flask 做可视化界面的底座1.1 先把可视化界面这件事拆开看很多人一上来就问用哪个框架其实这个问题问早了。可视化界面这个词听着挺专业落到实际工作里往往就三件事把散落在各处的事实数据收拢起来用一种人能一眼看懂的图形把它呈现出来再让看的人能点几下、筛一下、导出一下。数据源可能是数据库里的业务表可能是别人给的接口也可能是自己写爬虫跑下来的一批结果文件。呈现方式可能是折线图、柱状图、地图、桑基图也可能就是一堆带颜色的数字卡片。至于交互最低要求通常就是我选个时间范围图跟着变。把这三件事摆清楚之后技术选型就变得简单了。数据收拢需要一个能连数据库、能读文件、能定时任务的东西这是后端图形呈现需要一个浏览器能渲染的图表库这是前端两者之间的通道就是接口。Flask 在这个链条里扮演的是中间那个不添乱的调度员——它负责接收浏览器的请求去取数据整理成规范格式扔回去。它不强制你用什么数据库不强制你用什么模板引擎甚至不强制你用它的模板功能这一点对可视化场景特别重要因为可视化项目的数据来源往往很杂今天读 MySQL明天读 CSV后天要调同事的接口框架越轻你改起来越自由。还有一点容易忽略可视化界面通常不是那种需要支撑百万用户的核心系统它更多是内部工具、运营看板、实验监控、汇报材料。这类东西的特点是生命周期短、需求变更快、使用者就那几个人。用重框架去搭前期配置的时间可能比写业务逻辑还长而且需求一变改起来处处受制约。我用 Flask 做过好几个这样的看板从立项到能给人看基本都在一天以内这个速度是我一直留着它的主要原因。1.2 Flask 的能力边界在哪说清楚它擅长什么也得说清楚它不擅长什么不然容易踩坑。Flask 是微框架微的意思是核心只给你路由、请求响应对象、模板渲染这几样剩下的靠自己组装或者找插件。数据库 ORM 要自己装 SQLAlchemy表单验证要自己装 WTForms用户登录要自己装 Flask-Login后台管理要自己装 Flask-Admin 这类插件。听起来麻烦但对可视化项目来说反而是好事因为你通常用不到一半装多了只是增加学习成本和依赖风险。它的边界主要在两个地方。第一是并发能力Flask 自带的开发服务器是单线程的、带调试功能的玩具绝对不能拿去做生产环境这一点后面会专门讲部署用 Gunicorn 或者 uWSGI 挂上去就没问题。第二是它不解决前端问题页面长什么样、图怎么画、交互怎么做Flask 一概不管你得自己写 HTML、CSS、JavaScript或者请一个前端同事。如果你期待的是写几行 Python 就自动出一个漂亮界面那你要找的不是 Flask而是另一类工具。我的判断标准很粗暴如果这个界面要长期维护、要接复杂的权限体系、要和已有的业务系统深度耦合那我会认真考虑 Django如果只是要快速把数据画出来、能自己控制每一个细节、以后可能随时推倒重写Flask 就是最合适的那个。可视化看板绝大多数属于后者。1.3 四条常见路线的横向对比在真正动手之前我把常见的几种做法摆在一起对比过这张表我至今还留着每次有新需求都会拿出来看一眼。方案上手速度灵活度适合场景主要痛点Flask 模板 ECharts中等极高需要精细控制页面、长期维护的看板前端要自己写Dash极快中等纯 Python 背景、交互复杂的分析页前端定制受限于回调机制Streamlit极快低单人用的数据探索、演示页面结构难控、多人协作弱Django Admin中等中等已有 Django 项目、需要权限和后台前端自由度低、启动成本高Dash 和 Streamlit 这几年确实火写几行 Python 就能出图做数据探索非常爽。但它们的共同问题是你被框架的形状框住了。Dash 的回调机制在交互变复杂之后会变得难以维护一个页面上十几个组件互相触发代码很快就成一团Streamlit 每次交互都会重跑整个脚本页面布局的控制力很弱想做一个左右分栏、顶部固定筛选栏的布局得绕着它的模型走。Flask 的路线是反过来的前期你得多写一些 HTML 和 JavaScript但写完之后的每一寸页面都是你的。筛选条件放哪、图例怎么排、加载动画长什么样、点击柱子跳转到哪个页面全部可控。做可视化看板这件事视觉效果和交互手感占了很大比重我宁愿前期多花两个小时写前端也不愿意后期为了改一个布局跟框架的机制搏斗。1.4 我把这套组合用在了哪些真实场景说几个我自己做过的例子方便你判断这套东西的适用范围。第一个是爬虫数据的监控看板每天定时抓取的数据落到 SQLite 里页面上用折线图看趋势、用表格看明细、用关键词云看热点分布运营同事每天早上打开看一眼。第二个是服务器资源监控Python 脚本定时采集 CPU、内存、磁盘和几个关键进程的状态写进时序库前端用多折线图实时刷新出问题时能直接定位到是哪台机器。第三个是给业务部门做的订单分析页数据直接从业务库读支持按时间、地区、品类筛选图表联动导出 Excel。这三个项目的共同点是数据源明确、使用者有限、需求会变、不需要复杂的权限体系。它们全都用同一套技术栈做完了分别是 Flask Jinja2 模板 ECharts SQLite/MySQL Bootstrap。这套组合我用了很多次几乎没有翻过车所以下面的内容就围绕它展开。如果你手上的场景和这几个差不多那后面的内容可以基本照搬。2. 从零搭环境别小看这一步2.1 Python 版本和虚拟环境怎么处理第一步是确定 Python 版本。我现在的习惯是新项目一律用 3.10 或以上主要原因是新版本的类型标注支持更完整写接口的时候能给返回值加类型提示配合编辑器提示体验好很多而且 Flask 3.x 对 Python 3.8 以下已经不再支持了。如果你机器上装的是 3.7 甚至更老先升级不要试图在旧版本上凑合后面装依赖会一路报错。虚拟环境这一步经常被新手跳过然后就会遇到我明明装了 flask 为什么 import 报错这类问题。原因很简单你装到了全局环境里而你的项目用的是另一个解释器。我推荐用 Python 自带的 venv不需要额外装东西# 创建虚拟环境会生成一个 .venv 目录 python -m venv .venv # 激活Windows .venv\Scripts\activate # 激活macOS / Linux source .venv/bin/activate激活之后命令行前面会出现(.venv)的标记这时候再装包就只影响当前项目。这里有个实操细节在 PyCharm 里创建项目时它会问你要不要新建虚拟环境我一般选择新建然后把位置放在项目目录下的.venv这样项目文件夹拷到别的机器上也能重新建环境不会出现跑到别人电脑上就起不来的情况。VS Code 也是类似用CtrlShiftP调出命令面板选 Python 解释器指向.venv/bin/python就行选错了会出现终端能跑、编辑器报红线的怪现象。提示虚拟环境目录一定要加进.gitignore这东西是跟机器和系统绑定的提交上去只会给别人添麻烦。同理__pycache__、.idea、.vscode也一起忽略掉。2.2 依赖清单和安装顺序可视化项目的基础依赖其实很少。我先装核心的跑通之后再按需加pip install flask pip install pandaspandas 不是必须的但如果你的数据要从 CSV、Excel 或者数据库里来用它做清洗和聚合会省掉大量循环代码。等页面能跑起来再根据实际需要补下面这些pip install flask-sqlalchemy # 需要连数据库时 pip install flask-cors # 前后端分离部署、跨域时 pip install gunicorn # 部署到 Linux 服务器时 pip install python-dotenv # 用 .env 管理配置时装完之后一定要做一件事把当前环境的依赖导出成文件这样以后换机器或者部署到服务器上一条命令就能还原pip freeze requirements.txt这里我踩过一个不大的坑顺便分享一下。pip freeze会把环境里所有包都写进去包括你随手装的、跟项目无关的东西时间长了这份文件会变得又长又乱还可能出现版本冲突。更干净的做法是手动维护一份顶层依赖只写你直接用的那几个包不写间接依赖让 pip 自己去解析版本。项目小的时候两种做法差别不大项目一旦涉及部署和别人接手手动维护的价值就出来了。2.3 目录结构一开始就分清楚后面少受罪单人写小脚本的时候把代码全塞进一个app.py是最快的但可视化项目很快就会长出第二、第三个页面接口从三个变成十几个这时候单文件会变成灾难。我现在的标准结构是这样viz_dashboard/ ├── app/ │ ├── __init__.py # 应用工厂负责创建和配置 app │ ├── config.py # 配置集中管理 │ ├── views/ │ │ ├── __init__.py │ │ ├── main.py # 页面路由 │ │ └── api.py # 数据接口路由 │ ├── services/ │ │ └── data_service.py # 取数、清洗、聚合逻辑 │ ├── templates/ │ │ ├── base.html # 公共骨架 │ │ └── index.html # 首页 │ └── static/ │ ├── css/ │ ├── js/ │ └── lib/ # 本地托管的第三方库 ├── requirements.txt ├── wsgi.py # 生产环境入口 └── run.py # 本地开发入口为什么要分成 views 和 services 两层因为视图函数只应该关心接收什么参数、返回什么格式具体怎么查数据、怎么算指标属于业务逻辑放在 services 里。这样做的好处是当你要把数据源从 MySQL 换成接口时只需要改 services 里的实现视图函数一行都不用动。这个分离在项目早期看起来是多余的但等到你要换数据源、要写单元测试、要给别人交接的时候会庆幸当初分了层。2.4 配置集中管理别把密码写在代码里配置这件事我的原则是任何需要在不同环境本地、测试、生产变化的参数都必须抽出来。数据库连接串、密钥、调试开关、接口地址全部放进config.py用类的方式分环境import os from dotenv import load_dotenv load_dotenv() class BaseConfig: SECRET_KEY os.getenv(SECRET_KEY, dev-only-change-me) JSON_AS_ASCII False # 让接口直接返回中文不转义成 \uXXXX DATA_PAGE_SIZE 500 # 单次返回的最大数据点数 class DevConfig(BaseConfig): DEBUG True DATABASE_URI sqlite:///local.db class ProdConfig(BaseConfig): DEBUG False DATABASE_URI os.getenv(DATABASE_URI) CONFIG_MAP { dev: DevConfig, prod: ProdConfig, }JSON_AS_ASCII False这一行值得单独说。Flask 默认会把中文转成\u4e2d\u6587这种形式返回浏览器解析出来是中文没问题但你在浏览器网络面板里调试接口时会看得非常痛苦。关掉之后接口返回的就是可读的中文排查问题效率提升明显。注意 Flask 2.3 之后这个配置项改成了app.json.ensure_ascii False如果你用的是新版本按新写法来旧配置项会被忽略而且不报错很容易查半天查不出问题。注意SECRET_KEY绝对不能提交到代码仓库。我在.env里放真实值代码里只写os.getenv兜底同时把.env加进.gitignore。这是最基本的一条安全底线尤其是当你的可视化界面要放到公网上访问的时候。3. 后端数据是怎么流到前端的3.1 应用工厂和蓝图的搭配用法Flask 有两种组织方式一种是模块级直接创建app Flask(__name__)另一种是工厂函数。小项目用前者没问题但只要涉及配置切换和蓝图我就一律用工厂# app/__init__.py from flask import Flask from .config import CONFIG_MAP from .views.main import main_bp from .views.api import api_bp def create_app(env: str dev) - Flask: app Flask(__name__) app.config.from_object(CONFIG_MAP[env]) app.json.ensure_ascii False app.register_blueprint(main_bp) app.register_blueprint(api_bp, url_prefix/api) return app蓝图的作用是把路由分组。页面路由挂根路径接口路由统一挂/api前缀好处非常直观前端同事一看就知道哪些地址是接口Nginx 配置转发规则时也能按前缀精准匹配日志里按前缀统计请求量也方便。我在一个项目里没做这个分组页面和接口全混在根路径下后来加缓存策略时傻眼了——静态资源缓存、页面不缓存、接口短缓存三种策略要一个个路径去列改了整整一个下午。分组这件事做的时候花五分钟能省掉后面几小时。run.py里就三行负责本地开发启动# run.py from app import create_app app create_app(dev) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)host写0.0.0.0而不是默认的127.0.0.1是为了让同一局域网内的同事能用你的 IP 访问做完的东西直接甩个链接给人看比截图方便得多。用debugTrue启动后改代码会自动重启页面报错会显示交互式调试器开发阶段体验非常好。注意debugTrue绝对不能出现在生产环境。它会在出错页面暴露源码片段和变量值还能执行任意代码是实打实的安全漏洞。后面讲部署时会用 Gunicorn 彻底关掉它。3.2 接口返回格式定好规矩前后端都省事接口格式这件事最怕的是每个接口长得都不一样。第一个接口返回数组第二个返回对象第三个出错时返回纯文本前端就得写无数个判断。我现在的做法是强制统一成一个信封结构{ code: 0, msg: ok, data: { labels: [2024-01, 2024-02, 2024-03], series: [120, 180, 150] } }code为 0 表示成功非 0 表示业务层面的错误msg放人类可读的提示data里才是真正的数据。这样前端只需要写一个统一的处理函数判断code是否等于 0成功就取data失败就把msg弹出来。整个前端的数据层逻辑可以压缩到二十行以内。在 Flask 里我习惯封装几个辅助函数避免每个视图都手写jsonify# app/views/api.py from flask import Blueprint, jsonify, request from ..services.data_service import get_trend_data api_bp Blueprint(api, __name__) def ok(dataNone, msgok): return jsonify({code: 0, msg: msg, data: data}) def fail(msgerror, code1, status200): return jsonify({code: code, msg: msg, data: None}), status api_bp.get(/trend) def trend(): start request.args.get(start, ) end request.args.get(end, ) dim request.args.get(dim, day) if not start or not end: return fail(缺少时间范围参数) try: data get_trend_data(start, end, dim) except Exception as exc: return fail(f数据查询失败: {exc}, code1001) return ok(data)这段代码里有几个我自己坚持的习惯。参数校验放在最前面缺参数直接返回错误不要等到查库时才发现异常用try/except兜住返回结构化的错误信息而不是让 Flask 直接抛 500因为 500 页面是 HTML前端解析 JSON 会直接崩掉报错信息还很难看另外注意code和 HTTP 状态码是两回事业务错误我依然返回 HTTP 200只有真正的服务端异常才用 4xx、5xx这样前端的 AJAX 封装逻辑会简单很多。3.3 服务层怎么写才不容易乱服务层的职责很纯粹进去是参数出来是前端能直接用的结构。我一般会在这一层完成三件事取原始数据、做聚合或降采样、组装成图表需要的格式。# app/services/data_service.py import sqlite3 from datetime import datetime def get_trend_data(start: str, end: str, dim: str day) - dict: conn sqlite3.connect(local.db) conn.row_factory sqlite3.Row cur conn.cursor() sql SELECT stat_date, total_count FROM daily_stats WHERE stat_date BETWEEN ? AND ? ORDER BY stat_date ASC cur.execute(sql, (start, end)) rows cur.fetchall() conn.close() labels [r[stat_date] for r in rows] values [r[total_count] for r in rows] return { labels: labels, series: values, total: sum(values), avg: round(sum(values) / len(values), 2) if values else 0, }这里有两个值得说的点。第一是 SQL 用了参数化的?占位符不要用字符串拼接去构造查询条件。可视化看板的筛选条件大多来自用户的输入框字符串拼接等于直接给对方开了后门参数化是最省事也最有效的防线。第二是返回值里除了画图用的labels和series我还顺手算了total和avg一起返回因为页面上通常要在图的上方放几个汇总数字卡片。把这两个指标放在同一个接口里返回页面只需要发一次请求比分开请求两个接口要快也少一次数据不一致的风险。还有一个容易被忽略的问题单个用户传来的时间范围可能非常宽比如选了三年那返回的数据点可能上万个。前端渲染上万个点会明显卡顿尤其在低配电脑上。我的处理方式是在服务层做降采样超过一定数量就按周或按月聚合if len(labels) 2000: # 降采样按 N 个点合并成一个 step len(labels) // 1000 1 labels labels[::step] values [sum(values[i:istep]) for i in range(0, len(values), step)]这段逻辑不复杂但它把前端卡死这个体验问题挡在了服务端。降采样阈值和步长的选择要看具体业务我做过一个每秒一个点的采集数据最后是前端绘图用降采样、导出功能用完整数据两个接口分开各自服务不同的目的。4. 前端把图真正画出来4.1 页面骨架与模板继承前端的组织方式我选 Jinja2 模板而不是完全的静态 HTML。原因很简单模板继承能省掉大量重复代码。base.html里放公共的头部、导航、样式引用和公共脚本子页面只写自己那块内容!-- templates/base.html -- !DOCTYPE html html langzh-CN head meta charsetUTF-8 title{% block title %}数据看板{% endblock %}/title link relstylesheet href{{ url_for(static, filenamelib/bootstrap.min.css) }} link relstylesheet href{{ url_for(static, filenamecss/dashboard.css) }} /head body nav classtopbar span classbrand运营数据看板/span span classuser{{ current_user if current_user else 访客 }}/span /nav main classcontainer {% block content %}{% endblock %} /main script src{{ url_for(static, filenamelib/echarts.min.js) }}/script {% block scripts %}{% endblock %} /body /htmlurl_for(static, ...)这个写法看着比直接写/static/css/dashboard.css麻烦但它有个实实在在的好处静态资源的路径前缀是 Flask 管理的以后如果要把应用挂到子路径下比如部署在/dashboard/后面所有引用会自动跟着变不用全局搜索替换。这个细节在我给客户做二次部署时救过一次场。另外注意第三方库我是下载到本地static/lib/目录下托管的不是从 CDN 引用。这一点在开发机上感受不到差别但很多企业的内网环境访问不了外部网络或者访问很慢用 CDN 的页面会直接白屏图一个都画不出来。本地托管多占几百 KB 的磁盘换来的是任何环境都能跑我觉得很值。4.2 图表库选型为什么最后落在 ECharts图表库这个领域选择不少我前后试过 Chart.js、Highcharts、ECharts 和几个基于 D3 的封装。最后的结论是中文场景、要做看板、需要复杂交互的选 ECharts 基本不会错。理由有几条。配置项足够全。折线、柱状、饼图、散点、雷达、地图、热力图、桑基图、关系图常见的不常见的都有而且配置风格统一学会一个图表之后切换到另一个只是换个series.type的事情。文档是中文的示例站上每个配置项都能在线改参数实时看效果遇到不会写的配置去示例站搜一个接近的复制下来改改就行。这个效率比翻英文文档高太多。性能扛得住。我这个场景里有过一张 5000 个点的折线图配上缩放和框选ECharts 处理得很流畅换成某些轻量库就会明显掉帧。它内部做了 canvas 渲染和数据抽稀不用你操心。生态成熟。想要地图就加载地图 JSON想要导出图片它自带 toolbox想要主题就用官方的主题编辑器生成一份 JSON 挂上去。这些周边在赶工期的时候特别管用。当然它也不是没有缺点打包体积偏大一个完整的echarts.min.js压缩后有几个 MB如果只是要画个简单的柱状图用 Canvas 手写可能更划算。但可视化看板通常是内部使用不是面向公网的营销页体积这点代价完全可以接受。4.3 一个完整的图表接入流程我把从取数到渲染的完整流程写一遍这是整个项目里最核心的一段。页面上方放筛选栏中间放图表容器!-- templates/index.html -- {% extends base.html %} {% block content %} div classfilter-bar label开始日期 input typedate idstartDate/label label结束日期 input typedate idendDate/label label统计维度 select iddim option valueday按天/option option valueweek按周/option option valuemonth按月/option /select /label button idbtnQuery查询/button /div div classcard-row div classstat-cardspan idtotalNum-/spansmall总量/small/div div classstat-cardspan idavgNum-/spansmall日均/small/div /div div idtrendChart stylewidth:100%;height:480px;/div {% endblock %}对应的 JavaScript{% block scripts %} script const chart echarts.init(document.getElementById(trendChart)); function fmtDate(d) { return d.toISOString().slice(0, 10); } function setDefaultRange() { const end new Date(); const start new Date(); start.setDate(end.getDate() - 30); document.getElementById(startDate).value fmtDate(start); document.getElementById(endDate).value fmtDate(end); } async function loadTrend() { const start document.getElementById(startDate).value; const end document.getElementById(endDate).value; const dim document.getElementById(dim).value; chart.showLoading({ text: 加载中 }); try { const url /api/trend?start${encodeURIComponent(start)}end${encodeURIComponent(end)}dim${dim}; const res await fetch(url); const json await res.json(); if (json.code ! 0) { alert(json.msg); return; } const d json.data; document.getElementById(totalNum).textContent d.total; document.getElementById(avgNum).textContent d.avg; chart.setOption({ tooltip: { trigger: axis }, grid: { left: 50, right: 30, top: 40, bottom: 50 }, xAxis: { type: category, data: d.labels, boundaryGap: false }, yAxis: { type: value }, series: [{ name: 数量, type: line, smooth: true, areaStyle: { opacity: 0.15 }, data: d.series }] }); } catch (e) { console.error(e); alert(请求失败请检查网络或稍后重试); } finally { chart.hideLoading(); } } window.addEventListener(resize, () chart.resize()); document.getElementById(btnQuery).addEventListener(click, loadTrend); setDefaultRange(); loadTrend(); /script {% endblock %}这段代码里有几个细节是踩过坑之后加的。chart.resize()挂在窗口 resize 事件上否则窗口一缩放图表就保持原尺寸右边留一大块空白这是最常见的一个问题showLoading和hideLoading放在 try/finally 里避免请求异常时加载动画一直转着用户以为页面死了参数用encodeURIComponent包了一层防止日期或者维度值里出现特殊字符导致 URL 解析出错这个问题在参数值包含中文时必现。还有一个隐藏的坑new Date().toISOString()返回的是 UTC 时间如果用户的时区是东八区默认日期可能比实际早一天。严谨的做法是用toLocaleDateString(sv-SE)这种格式或者自己拼年月日。这个问题在月初和月末特别明显用户会发现默认查出来的数据少了一天然后来问你数据是不是错了。4.4 大屏适配与几个交互细节如果你的看板最终要投到会议室的大屏上适配这件事必须提前考虑。我一般用三段式断点宽度小于 768 走单列768 到 1440 走两列大于 1440 走三列用 CSS Grid 控制。图表容器的高度用clamp()做弹性字体用rem配合根元素字号动态调整比写死px灵活得多。.card-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px; margin-bottom: 20px; } .stat-card { background: #fff; border-radius: 8px; padding: 18px 22px; box-shadow: 0 2px 8px rgba(0,0,0,.06); } .stat-card span { font-size: clamp(20px, 2.2vw, 36px); font-weight: 600; }另一个常见需求是自动刷新。我见过不少代码直接用setInterval(loadTrend, 5000)就完事了问题有两个请求如果耗时超过 5 秒会出现请求堆积越堆越多用户切到别的标签页时还在后台疯狂请求白白浪费资源。我的写法是改成递归的setTimeout并且判断页面可见性let timer null; function scheduleNext() { timer setTimeout(async () { if (document.visibilityState visible) { await loadTrend(); } scheduleNext(); }, 30000); }这样上一次请求结束后才排下一次永远不会堆积页面隐藏时也跳过执行。这些细节做不做用户未必说得出来但体验上的差别是实打实的。5. 部署上线从本机到别人能访问5.1 为什么不能用 Flask 自带的服务器app.run()启动的那个服务器官方文档里写得很明白是给开发用的。它有三个致命问题单进程单线程稍微并发几个请求就排队性能很差跟专业的 WSGI 服务器差一个量级没有任何超时和错误隔离机制一个请求卡死可能影响其他所有请求。我做过测试本地开发服务器在几十个并发下就开始明显变慢而换成 Gunicorn 之后表现平稳得多。所以部署的第一步是把入口换成 WSGI 应用对象。新建一个wsgi.pyfrom app import create_app app create_app(prod)注意这里传的是prod对应前面配置里的生产配置DEBUG是关掉的。这个文件就是给 Gunicorn 用的它只暴露一个app对象不做任何启动动作一切都交给 WSGI 服务器管理。5.2 Gunicorn 参数怎么定在 Linux 服务器上装好依赖之后启动命令大致是这样gunicorn -w 4 -b 127.0.0.1:8000 \ --timeout 60 \ --access-logfile logs/access.log \ --error-logfile logs/error.log \ --daemon \ wsgi:app-w是工作进程数。可视化的场景一般是 IO 密集型查数据库、读文件、等接口返回进程大部分时间在等所以可以开得多一些我通常用2 * CPU核心数 1作为起点比如 4 核机器开 9 个进程。但这只是起点不是教条实际要看监控数据调如果 CPU 长期跑满说明进程不够或者是计算密集如果内存吃紧说明进程太多了每个进程都要单独占一份内存。--timeout 60是单个请求的最长处理时间超过就被杀掉。这个值默认是 30 秒但可视化项目里经常有全量统计的查询可能跑十几秒所以我一般调到 60。这里有个坑要提醒如果你的接口有超过 60 秒的重查询调 timeout 只是治标真正的解法是把查询改成异步任务页面先返回任务 ID轮询拿结果。我在一个千万级数据的项目里就是这么干的直接同步查询无论怎么调参数都不靠谱。--daemon让进程在后台跑配合日志文件用。不过我现在更倾向于用 systemd 或者 supervisor 来托管因为 daemon 模式下的进程管理比较原始服务器重启后不会自动拉起来出问题也不容易看状态。5.3 Nginx 前端的配置要点Gunicorn 监听的是本机端口对外需要 Nginx 做反向代理。这样做的好处是 Nginx 处理静态文件效率极高还能顺便做压缩、缓存、限流。配置大致是这样server { listen 80; server_name dashboard.example.com; client_max_body_size 20m; location /static/ { alias /srv/viz_dashboard/app/static/; expires 7d; add_header Cache-Control public; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 60s; } gzip on; gzip_types text/css application/javascript application/json; }静态文件这一段单独处理是部署后提升最明显的一步。Flask 直接返回静态文件时每个 CSS 和 JS 都要走 Python 进程并发一高就成了瓶颈交给 Nginx 之后响应时间和资源占用都会明显改善。另外expires 7d加了七天缓存回访用户的加载速度会快很多但要注意配合版本号使用否则你更新了 CSS 用户看到的还是旧的。我的做法是给静态文件加一个查询参数版本号比如dashboard.css?v20240601改了就改这个数字。如果你要在同一台机器上部署多个类似的 Web 项目用不同的server_name或者不同的location前缀分开就行静态资源的alias指向各自项目的目录互不干扰。这是 Nginx 最擅长的活一台 2 核 4G 的轻量服务器挂五六个小看板完全没问题。5.4 上线前必须过的几项检查部署这件事我踩过太多次坑后来整理成一份清单每次上线前对着走一遍。关掉DEBUG确认生产配置生效SECRET_KEY从环境变量读取不是默认值数据库连接信息不写死在代码里日志目录存在且可写否则启动直接失败时区统一服务器、数据库、Python 进程三者的时区要一致我在一个项目上被时区问题折磨了两天最后发现服务器是 UTC数据库存的是本地时间差值八小时静态文件路径权限正确Nginx 用户要能读到文件这个权限问题报错信息很不直观通常是 403容易怀疑到别的地方去。6. 常见问题与排查实录6.1 报错速查表现象大概率原因处理方式TemplateNotFound模板目录位置不对或文件名拼错确认模板放在app/templates/下检查大小写Address already in use5000 端口被占用lsof -i:5000找到进程杀掉或换端口页面能开但接口 404蓝图前缀写错或方法不匹配用flask routes打印所有路由对照检查图表区域空白容器宽高为 0 或初始化时机太早给容器明确 height初始化放在 DOM 就绪后接口返回中文乱码未关闭 ASCII 转义Flask 2.3 设置app.json.ensure_ascii False静态资源 404路径不对或 Nginx alias 少斜杠alias 路径末尾要带斜杠与 location 保持一致部署后首次访问很慢工作进程冷启动、连接池未预热加健康检查接口部署后先请求一次预热缓存不生效expires生效但文件名未变静态资源加版本号参数改版本号即刷新数据时间差 8 小时服务器与数据库时区不一致统一为同一时区或在代码里显式转换请求超时 502Gunicorn timeout 太短调大 timeout或改异步任务模式6.2 几个印象最深的坑第一个是模板缓存的问题。生产环境我开了模板缓存改完 HTML 重启 Gunicorn 之后发现页面没变化一度以为是部署脚本没生效折腾了半小时才想起来是浏览器缓存了 HTML。后来我在 Nginx 里专门给 HTML 加了Cache-Control: no-cache静态资源才走长缓存这样改页面立即生效改样式也不会影响加载速度。第二个是 ECharts 容器的初始化时机。我最初把echarts.init写在head里的脚本中结果图表一直不显示控制台也不报错。原因是脚本执行时容器div还没被解析出来init拿到的是一个空元素宽高都是 0。解决办法是把脚本放到页面底部或者用DOMContentLoaded包起来。这个问题很基础但我相信每个做过前端的人都至少踩过一次。第三个是数据量增长带来的性能陡降。项目上线三个月后数据从几千条涨到几十万条之前很快的接口开始变慢页面加载要十几秒。排查下来问题出在没有索引stat_date这一列在表里没建索引每次查询都是全表扫描。加索引之后从十几秒降到几十毫秒。这件事让我养成了一个习惯任何用来做筛选条件的字段建表时候就先想好要不要加索引不要等到出问题再补。第四个是前端定时刷新的内存泄漏。页面开一整天之后越来越卡最后崩掉。原因是每次切页面或者重新查询都新建了定时器但旧的没清理。解决办法是在创建新定时器前先clearTimeout用变量统一管理。这类问题在短时间测试里完全发现不了只有在长时间运行后才会暴露。6.3 调试技巧和日志习惯调试这块我有几个一直在用的方法。接口返回不对时先用浏览器的网络面板看原始响应确认是后端返回内容的问题还是前端处理的问题这一步能省掉大量瞎猜。后端逻辑不对时用 Python 的交互式调试器打断点比在代码里到处插print高效得多但记住调试完要清理干净我见过有人的print留到了生产环境日志里全是垃圾信息。日志方面我一般用标准库的logging按环境分级别开发环境 INFO生产环境 WARNING 起步同时把请求相关的关键信息记下来。import logging from logging.handlers import RotatingFileHandler def setup_logging(app): handler RotatingFileHandler(logs/app.log, maxBytes10*1024*1024, backupCount5) handler.setFormatter(logging.Formatter( %(asctime)s [%(levelname)s] %(message)s )) handler.setLevel(logging.INFO) app.logger.addHandler(handler)RotatingFileHandler会按大小切分日志文件避免一个日志文件无限增长把磁盘撑爆。这个配置我建议所有需要长期运行的服务都加上我吃过一次亏一个跑了大半年的看板日志文件涨到了十几个 G最后把服务器磁盘写满整个服务挂了才发现。加两行配置就能避免的事情真的没必要省。排查线上问题时我习惯先看三样东西接口的响应时间分布、错误日志的最后几十行、服务器的资源使用情况。这三样基本能覆盖绝大多数情况。响应时间突然变长一般是数据量涨了或者少了索引错误日志里频繁出现同一个异常那就是代码 bug资源使用异常那就是配置或者代码有循环、有泄漏。按这个顺序看通常十分钟内能定位到方向。最后分享一个我觉得挺有用的小习惯每个项目在本地跑通之后我会立刻写一份简短的部署记录把服务器信息、启动命令、Nginx 配置路径、数据库位置、注意事项全部记在一个DEPLOY.md里。当时觉得多余但每次过几个月回来改东西或者要把项目交接给别人的时候这份记录能省下大量回忆的时间。做可视化看板这件事本身不难难的是几个月后你还能记得当初为什么这么写。
返回列表