ERP

如何让 Odoo 或 ERPNext 重新变快:实用性能排查指南

August 24, 2026

每一套 ERP 系统在上线第一天都很快。六个月后,加上四十个自定义字段,销售团队就开始抱怨 Sales Order 列表视图要加载八秒钟,还有人已经悄悄开始用一份 Excel 表格"临时顶一下,等系统修好再说"。这是我们最常收到的支持工单之一,而且几乎从来不是单一原因造成的——通常是三四个小问题叠加在一起。

本文将同时针对 Odoo 和 ERPNext(Frappe)介绍诊断思路与具体修复方法,因为尽管两者技术栈不同,但它们的性能失效模式却出奇地相似:两者都是构建在关系型数据库之上的 Python ORM,都运行在数量有限的 worker 进程池后面,而且都容易在同样的三个地方变慢——数据库、worker 层,以及叠加在上面的定制开发。

一、先诊断,再调优

第一反应往往是"加内存"或"加 worker 数量",但请先克制这个冲动。给一个因为缺少索引而变慢的系统增加资源,只会让你更快地撞上同一堵墙。

先弄清楚时间究竟消耗在哪里:

  • 是某一个页面慢,还是全站都慢? 单个报表慢通常指向某条查询或某段脚本;全站范围的变慢则更可能是基础设施问题——worker、数据库连接或磁盘 I/O。
  • 是某个用户慢,还是所有用户都慢? 某个用户的个人仪表盘特别庞大,或者某个保存的过滤器返回了 5 万行数据,这和周一早上 9 点所有人同时变慢,是完全不同的两类问题。
  • Odoo: 使用 --log-level=debug_sql(或在开发者模式下使用内置的 Performance/Query Count 功能)查看每次请求的查询数量与耗时。一个触发 200 条以上查询的视图,问题出在定制开发上,而不是基础设施。
  • ERPNext/Frappe: 使用内置的 Recorder 工具(执行 bench --site [site] set-config recorder 1,然后在界面的 Developer Settings → Recorder 中查看)记录每次请求的 SQL 查询数量与耗时。MariaDB 的慢查询日志(slow_query_log = 1,long_query_time = 1)可以捕捉到其余部分。

如果在动基础设施之前只能做一件事,那就是:找出最慢的五条查询或请求,并逐一追溯到具体的 DocType/模型、报表或脚本。

二、数据库层

两套系统几乎把所有的性能压力都放在数据库上——Odoo 使用 PostgreSQL,ERPNext/Frappe 默认使用 MariaDB(Frappe 从 v14 起也支持 PostgreSQL)。数据库是实际瓶颈的概率,比大多数人想象的要高得多。

自定义字段缺少索引。 这是我们见过导致 ERP 变慢最常见的单一原因。任何用于过滤条件、列表视图列或报表的自定义 Link/Select 字段都需要索引——而自定义字段不会像内置字段那样自动获得索引。

  • Odoo:在字段定义中设置 index=True,或针对已有数据通过 SQL/迁移脚本补加。
  • Frappe:仅设置 "in_list_view": 1 并不会为字段建立索引,需要在 DocField 中设置 "search_index": 1,或使用 bench --site [site] add-index 手动添加。

统计信息过期与数据膨胀。 PostgreSQL 需要定期执行 VACUUM ANALYZE(通常由 autovacuum 自动处理,但要确认它确实在运行,并且没有在 stock_moveaccount_move_line 这类高写入表上出现滞后)。MariaDB 的表也会受益于对高频更新表定期执行 ANALYZE TABLEOPTIMIZE TABLE——尤其是 Frappe 的 Version 表和 Comment 表,它们往往会膨胀到很大,却很少被清理。

连接数耗尽。 Odoo 的每个 worker、Frappe 的每个 gunicorn worker 都会持有一个数据库连接。在配置一般的数据库实例上,负载升高时很容易触及 max_connections 上限,表现为间歇性超时,而不是均匀的变慢。连接池(Odoo/PostgreSQL 场景下的 PgBouncer)对于并发用户数超过个位数的部署来说是标准做法。

在事务型数据库上运行重度读取报表。 一份扫描过去一年 Sales Invoice Item 数据的定时报表,会占用销售团队此刻正需要的资源。如果有经常性的重报表或 BI 仪表盘,应该让它们读取只读副本(read replica),而不是主库。

三、Worker 层

Odoo 和 Frappe 都是运行在固定数量 worker 进程池之后的 WSGI 应用——这个进程池的大小,是基础设施层面最重要的杠杆,也往往是配置最容易出错的地方。

Odoo 的 worker。 标准公式是 workers = (CPU 核心数 × 2) + 1,并预留约 20% 的 worker 给定时任务使用(max_cron_threads)。每个 worker 都是独立进程,拥有各自的内存占用(--limit-memory-soft / --limit-memory-hard 用于防止失控请求),如果每个 worker 分配的 RAM 不足,worker 会在请求执行到一半时被强制终止并重启——表现为随机性的变慢或偶发的 502 错误。大批量导入、生成 PDF 这类长耗时请求,应该走单独的 longpolling/队列机制,而不是占用常规 worker。

Frappe/ERPNext 的 worker。 bench 会运行一个用于处理 Web 请求的 gunicorn 进程池,同时为后台任务运行独立的 RQ(Redis Queue)worker,分为 shortdefaultlong 三个队列。一个常见的失败模式是:有人把重型任务(批量发送邮件、大规模数据导入、批量生成 PDF)提交到了 default 队列,结果它后面排队的所有其他后台任务都被拖慢。应该把耗时长的任务明确路由到 long 队列,并确保为每个队列配置了与后台任务量相匹配的足够 worker 进程数——这在 Procfile/supervisor.conf 中设置,不会自动扩容。

Socket/实时通信层。 Frappe 的实时更新和 Odoo 的 longpolling/bus 都依赖 Redis 以及一个专用进程(Frappe socketio 对应的 node,Odoo 的 longpolling worker)。一旦这个进程出问题,用户看到的现象通常是"页面像卡住了一样",而不是明确的报错信息。

四、正确分离的缓存架构

两套系统都会把 Redis 用于多种用途——通常包括缓存、后台任务队列,以及实时通知的 pub/sub——一个常见的配置错误,是把这三者全部指向同一个 Redis 实例/数据库编号,且不设内存上限。在内存压力下发生的缓存驱逐,可能会连带清掉排队中的后台任务。应该为缓存和队列使用不同的 Redis 数据库(或不同实例),并单独为缓存实例设置合理的 maxmemory 与淘汰策略。

对于 ERPNext,请确认 site_config.json 中的 redis_cacheredis_queueredis_socketio 确实指向了不同的数据库编号——很多自建部署仍停留在 bench 的默认配置上,这在单站点开发环境中没问题,但对于有真实负载的生产环境来说并不合适。

五、规模扩大之前不会显现的定制化反模式

这一类问题最容易让团队措手不及,因为在只有十条测试数据时一切看起来都很正常,而一旦数据量达到五万条,就会彻底崩溃。

  • 服务端脚本/自定义脚本中的 N+1 查询。 在循环中逐行调用 frappe.get_doc()self.env['res.partner'].browse(),而不是批量获取,在 10 行数据时毫无影响,在 10,000 行数据时则是灾难。应始终先用 frappe.get_all() / search_read() 批量取数,再进入循环。
  • 每次按键或每次字段变更都向服务器发起查询的客户端脚本,尤其是在显示大量行的列表视图上——每一次都是一趟网络往返。
  • 范围过大的列表视图/报表查询:明明只显示三列,却拉取了整张宽表 DocType/模型的所有字段;或者提前加载了几乎从不展开查看的子表行。
  • 需要逐行评估的权限与共享规则。 用户权限和记录规则功能强大,但如果一条规则要在 10 万行的列表上逐行执行子查询,速度自然会和听起来一样慢。
  • 在请求周期内同步执行的打印格式与 PDF 生成。 打印单张发票没问题,但"批量打印 200 张发票"这种操作若同步执行就会很痛苦,应该改为后台任务执行。

以上所有问题的解决方式都是一样的:先做性能剖析(见第一部分),找到具体的脚本或查询,再针对性修复——而不是"给它换个更大的服务器,指望问题自己消失"。

六、Filestore 与附件

两套系统默认都把附件存储在应用服务器的本地磁盘上。这在规模较小时没有问题,但迟早会遇到瓶颈:应用服务器上的本地磁盘无法水平扩展,备份会变慢,而在部分网络存储方案下,filestore 的 I/O 延迟会直接拖慢每一份带附件预览的文档。对于超出单机部署规模的场景,应将附件迁移到对象存储(兼容 S3 的方案)——Odoo(通过 filestore 插件)和 Frappe(原生支持 S3 备份/附件)都对此提供原生支持。

七、基础设施规模规划与水平扩展

一旦数据库索引配置正确、定制开发经过性能剖析并清理干净,关于基础设施规模的讨论就会简单得多:

  • 磁盘的重要性往往被低估。 在机械硬盘或 IOPS 配置不足的云存储上运行数据库,会成为你之前所有优化工作的瓶颈。对于超出小团队规模的生产级 ERP 数据库而言,配备足够 IOPS 的 SSD 存储并非可选项。
  • 水平扩展需要共享状态。 增加第二台应用服务器,意味着 filestore 以及 session/缓存层都需要共享(对象存储 + 共享 Redis),否则用户会因为落在不同节点上而看到不一致的行为。
  • 静态资源应该放在 CDN 之后,至少也要有一层激进缓存的反向代理。 让 nginx 直接提供 /assets 内容,而不是把每一次 JS/CSS 请求都代理到应用 worker 上处理。

诊断清单

  1. 复现变慢的场景,并明确:是单个页面还是所有页面变慢,是单个用户还是所有用户受影响。
  2. 开启查询性能剖析(Odoo 的 debug_sql、Frappe 的 Recorder),找出最慢的五个请求。
  3. 检查同一时间段内 MariaDB/PostgreSQL 的慢查询日志。
  4. 针对每一条慢查询:WHERE 子句中使用的自定义字段是否缺少索引?
  5. 检查 worker/队列健康状况:Odoo 的 worker 是否因内存不足被杀掉?Frappe 的某个后台队列是否已经积压?
  6. 检查 Redis:缓存是否在与任务队列争抢内存?
  7. 检查涉及慢路径的自定义脚本中是否存在 N+1 模式。
  8. 只有完成以上第 1–7 步之后,才考虑增加 CPU/内存/worker 数量。

大多数 ERP 变慢问题都不需要升级硬件就能解决。数据库层和定制开发层是绝大多数真实工单的成因——基础设施规模调整通常是最后的那 5%,而不是应该最先动手的地方。


来源:

Ready to talk about your project?

Share goals and constraints. We'll assemble architects and engineers to move fast with you.

Get in touch