Odoo Performance Tuning: PostgreSQL and Server Optimization

Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.

E
ECOSIRE Research and Development Team
|2026年3月19日7 分钟阅读1.4k 字数|

属于我们的Performance & Scalability系列

阅读完整指南

Odoo 性能调优:PostgreSQL 和服务器优化

缓慢的 Odoo 实例会导致生产力下降和用户沮丧,从而造成经济损失。好消息:大多数 Odoo 性能问题无需升级硬件即可解决。坏消息:诊断根本原因需要了解整个堆栈——Python、PostgreSQL、Nginx、Redis 和网络层。

本指南涵盖了 Odoo 19 Enterprise 的完整性能优化生命周期:识别瓶颈、调整 PostgreSQL、优化 Odoo 服务器设置、配置 Nginx 缓存以及根据用户数量和交易量调整基础设施规模。

要点

  • PostgreSQL 调优可带来最大的性能提升(典型安装中为 50-300%)
  • 共享缓冲区应设置为可用 RAM 的 25% 作为起点
  • Odoo 的 ORM 生成可以用 pg_stat_statements 捕获的 N+1 查询
  • 频繁过滤字段(company_id、state、date)的索引是强制性的
  • Nginx 代理缓存无需访问 Odoo 服务器即可提供静态资源
  • Worker配置直接影响并发用户容量
  • Redis会话缓存减少了身份验证的数据库负载
  • 必须针对高写入 Odoo 工作负载调整清理和分析计划

诊断性能瓶颈

在调整任何内容之前,先确定时间实际花在哪里。盲目优化浪费精力。

启用 Odoo 查询日志记录:

# odoo.conf
[options]
log_level = info
logfile = /var/log/odoo/odoo.log

# For SQL query logging (development/staging only):
log_handler = odoo.sql_db:DEBUG

启用 PostgreSQL 慢查询日志记录:

# /etc/postgresql/15/main/postgresql.conf
log_min_duration_statement = 1000  # Log queries taking > 1 second
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
log_checkpoints = on
log_connections = on
log_lock_waits = on

安装pg_stat_statements(最有价值的PostgreSQL扩展):

-- Enable the extension
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;

-- Find the top 20 slowest queries
SELECT
    round(total_exec_time::numeric, 2) AS total_ms,
    calls,
    round(mean_exec_time::numeric, 2) AS mean_ms,
    round((100 * total_exec_time / sum(total_exec_time) OVER ())::numeric, 2) AS pct,
    left(query, 100) AS query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;

-- Reset statistics after tuning to measure improvement
SELECT pg_stat_statements_reset();

在 Odoo 中识别 N+1 查询:

当 Odoo 加载记录列表,然后对每个记录进行一次查询以获取相关数据时,会发生 N+1 查询。在 pg_stat_statements 中查找类似这样的模式:

-- This query appearing 500 times in a single page load = N+1 problem
SELECT * FROM res_partner WHERE id = $1

修复方法是使用 Odoo 的 prefetch_ids 机制或将 select 添加到 ORM 查询中:

# Bad: triggers N+1 for partner on each order
for order in orders:
    print(order.partner_id.name)  # One query per order

# Good: prefetch partner data in one query
orders = self.env['sale.order'].search([...])
orders.mapped('partner_id')  # Forces prefetch
for order in orders:
    print(order.partner_id.name)  # No additional queries

PostgreSQL 配置调优

PostgreSQL 附带了保守的默认值,旨在在任何硬件上运行。对于 Odoo 生产服务器,必须调整这些默认值。

内存设置(对于 32 GB RAM 服务器):

# /etc/postgresql/15/main/postgresql.conf

# Shared buffers: 25% of RAM
shared_buffers = 8GB

# Work memory: per-operation memory for sorts/joins
# Start conservative, increase if you see disk sorts
work_mem = 64MB

# Maintenance work memory: for VACUUM, CREATE INDEX
maintenance_work_mem = 2GB

# Effective cache size: tells planner how much OS cache is available
# Set to 75% of total RAM
effective_cache_size = 24GB

# WAL settings for better write performance
wal_buffers = 256MB
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min
max_wal_size = 4GB
min_wal_size = 1GB

连接设置:

# Maximum connections (Odoo workers × 2 + headroom)
max_connections = 200

# For connection pooling with PgBouncer
# If using PgBouncer, reduce to 50-100

查询计划器设置:

# Enable parallel query execution
max_parallel_workers_per_gather = 4
max_parallel_workers = 8
max_worker_processes = 16

# SSD storage: random_page_cost should equal seq_page_cost
random_page_cost = 1.1  # Default is 4.0 (for spinning disk)
seq_page_cost = 1.0

# Increase statistics target for better query plans on Odoo's large tables
default_statistics_target = 200

针对高写入工作负载的 Autovacuum 调整:

Odoo 的库存、会计和消息传递模块会产生大量 INSERT/UPDATE 流量。默认 autovacuum 设置不够:

autovacuum = on
autovacuum_max_workers = 5
autovacuum_naptime = 30s
autovacuum_vacuum_threshold = 50
autovacuum_analyze_threshold = 50
autovacuum_vacuum_scale_factor = 0.01   # Vacuum when 1% of rows are dead
autovacuum_analyze_scale_factor = 0.005  # Analyze when 0.5% of rows change
autovacuum_vacuum_cost_delay = 2ms       # Reduce I/O throttling

关键数据库索引

缺少索引是 Odoo 性能问题的第二大常见原因,仅次于配置不当。 Odoo 为主键和一些外键创建索引,但许多常用过滤字段缺少索引。

使用 pg_missing_fk_indexes 视图检查丢失的索引:

-- Find foreign keys without indexes
SELECT
    tc.table_name,
    kcu.column_name,
    ccu.table_name AS foreign_table_name,
    pg_relation_size(tc.table_name::regclass) AS table_size
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
    ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
    ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND NOT EXISTS (
    SELECT 1 FROM pg_indexes pi
    WHERE pi.tablename = tc.table_name
    AND pi.indexdef LIKE '%' || kcu.column_name || '%'
)
ORDER BY table_size DESC;

Odoo 19 的基本索引:

-- Sale orders (most queried table)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sale_order_state
    ON sale_order(state);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sale_order_company_date
    ON sale_order(company_id, date_order DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sale_order_partner
    ON sale_order(partner_id) WHERE state != 'cancel';

-- Account moves (invoicing)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_account_move_state_type
    ON account_move(state, move_type);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_account_move_company_date
    ON account_move(company_id, invoice_date DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_account_move_partner
    ON account_move(partner_id) WHERE state = 'posted';

-- Account move lines (most queried for reconciliation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_account_move_line_account_reconcile
    ON account_move_line(account_id, reconciled, date);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_account_move_line_move_date
    ON account_move_line(move_id, date);

-- Stock moves (inventory)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_stock_move_state_product
    ON stock_move(state, product_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_stock_quant_product_location
    ON stock_quant(product_id, location_id);

-- Mail messages (can grow very large)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_mail_message_res_model_id
    ON mail_message(res_model, res_id);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_mail_message_date
    ON mail_message(date DESC);

-- IR rule performance (access control)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_ir_rule_model_groups
    ON ir_rule(model_id);

Odoo Worker 配置

Odoo 工作线程的数量决定了服务器可以处理的并发请求数。

工人计数公式:

Workers = (CPU_cores × 2) + 1
Memory per worker: 256MB - 512MB depending on workload

Example for 8 CPU cores, 32GB RAM:
Workers = (8 × 2) + 1 = 17
Memory check: 17 × 512MB = 8.5GB (well within 32GB)

odoo.conf 工作进程设置:

[options]
# Worker processes
workers = 17

# Limits to prevent runaway workers
limit_memory_hard = 2684354560  # 2.5GB hard limit (kills worker)
limit_memory_soft = 2147483648  # 2GB soft limit (triggers gc)
limit_time_cpu = 600            # CPU seconds per request
limit_time_real = 1200          # Wall clock seconds per request
limit_request = 8192            # Requests before worker restart

# Long polling (for live notifications)
longpolling_port = 8072

了解工人类型:

Odoo 使用两种类型的工作人员:

  1. HTTP工作人员workers配置):处理所有网络请求
  2. Cron工人(保留1个):在后台运行计划的操作

cron 工作线程始终运行,但不计入您的 HTTP 容量。确保即使在峰值负载下也至少有 1 个 cron 工作线程可用。


Nginx 性能配置

Nginx 位于 Odoo 前面,负责处理 TLS 终止、静态文件服务和可选的缓存。

高性能Nginx配置:

upstream odoo {
    server 127.0.0.1:8069 weight=1 fail_timeout=0;
}

upstream odoochat {
    server 127.0.0.1:8072 weight=1 fail_timeout=0;
}

# Cache zone for static assets
proxy_cache_path /var/cache/nginx/odoo
    levels=1:2
    keys_zone=odoo_cache:100m
    max_size=1g
    inactive=60m
    use_temp_path=off;

server {
    listen 443 ssl http2;
    server_name your-odoo.com;

    # Gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml application/xml+rss text/javascript;
    gzip_min_length 1000;
    gzip_comp_level 6;

    # Static file caching
    location /web/static/ {
        proxy_cache odoo_cache;
        proxy_cache_valid 200 7d;
        proxy_cache_use_stale error timeout updating
            http_500 http_502 http_503 http_504;
        add_header X-Cache-Status $upstream_cache_status;
        expires 7d;
        proxy_pass http://odoo;
    }

    # Long polling for live chat/notifications
    location /web/longpolling {
        proxy_pass http://odoochat;
        proxy_read_timeout 3600s;
        proxy_connect_timeout 300s;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Main application
    location / {
        proxy_pass http://odoo;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 720s;
        proxy_connect_timeout 300s;
        proxy_buffering on;
        proxy_buffer_size 128k;
        proxy_buffers 4 256k;
        proxy_busy_buffers_size 256k;

        # Security headers
        add_header X-Frame-Options DENY;
        add_header X-Content-Type-Options nosniff;
        add_header Referrer-Policy strict-origin-when-cross-origin;
    }
}

Redis 用于会话和缓存

Redis 显着降低了会话管理和 Odoo ORM 缓存的数据库负载。

安装和配置Redis:

# Install Redis
sudo apt install redis-server

# Configure Redis for Odoo (max 4GB memory, LRU eviction)
sudo nano /etc/redis/redis.conf
# redis.conf
maxmemory 4gb
maxmemory-policy allkeys-lru
save ""  # Disable persistence for pure cache
tcp-keepalive 300

配置 Odoo 使用 Redis:

# odoo.conf
[options]
# Redis for session storage
session_redis_host = 127.0.0.1
session_redis_port = 6379
session_redis_prefix = odoo_session_

# Redis for IR rules and ORM cache
cache_redis_host = 127.0.0.1
cache_redis_port = 6379

监控和持续绩效管理

设置pgBadger进行PostgreSQL日志分析:

# Install pgBadger
sudo apt install pgbadger

# Generate report from PostgreSQL logs
pgbadger /var/log/postgresql/postgresql-15-main.log \
    -o /var/www/html/pgbadger/index.html \
    --format html \
    --top 20

要监控的关键指标:

公制警告阈值临界阈值
页面加载时间> 2 秒> 5 秒
数据库查询时间> 平均 100 毫秒> 平均 500 毫秒
工人记忆> 限制的 80%> 极限值的 95%
PostgreSQL 连接> 最大值的 70%> 最大值的 90%
磁盘 IOPS> 80% 的配置> 95% 的配置
缓存命中率< 95%< 90%

常见问题

50 个并发 Odoo 用户的最低服务器规格是多少?

对于具有中等交易量的 50 个并发用户:8 个 vCPU、32 GB RAM、500 GB SSD(首选 NVMe)。 Odoo 的 PostgreSQL 数据库是主要的 I/O 瓶颈,因此快速存储比原始 CPU 速度更重要。配置 13 个工作线程(8×2-3 的预留空间)、8 GB 共享缓冲区,并确保您的数据库位于 SSD 卷上。

如何诊断我的 Odoo 慢是 Python 还是 PostgreSQL?

使用 Odoo 的内置分析器(开发人员模式下的设置 → 技术 → 性能 → 分析器)来记录缓慢的操作。火焰图将显示时间是花费在 Python 代码上还是等待 SQL 结果。如果 SQL 查询占主导地位,请重点关注 PostgreSQL 调优和索引。如果 Python 占主导地位,请查找是否缺少 @api.depends 缓存或自定义代码效率低下。

我应该使用 PgBouncer 进行连接池吗?

是的,适用于具有 30 多个 Odoo 工作人员或大量 API 流量的部署。事务模式池中的 PgBouncer 允许许多 Odoo 工作人员共享较小的实际 PostgreSQL 连接池,从而减少每个连接的开销。使用 PgBouncer 时,将 PostgreSQL 中的 max_connections 配置为 50-100,然后设置 PgBouncer 的 pool_size 以匹配您的 Odoo 工作线程数。

我应该多久在 Odoo 数据库上运行 VACUUM ANALYZE 一次?

如果配置正确,Autovacuum 会自动处理此问题。经过上述调整(激进的比例因子,更多的工作人员)后,autovacuum 应该在活动表上连续运行。运行 SELECT schemaname, tablename, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 20; 来验证表是否经常被清理。

Odoo 工作人员过多会产生什么影响?

每个 Odoo 工作线程至少消耗 256–512 MB RAM。太多的worker会导致内存耗尽,导致worker崩溃(limit_memory_hard),从而导致用户出现HTTP 500错误。此外,太多的 PostgreSQL 连接(workers × max_db_connections)可能会压垮数据库。从公式(CPU×2+1)开始,监视负载下的内存,并根据需要向下调整。


后续步骤

Odoo 性能调优是一个迭代过程。单个调整会话可以带来显着的收益,但持续的性能需要随着数据量的增长进行持续监控、定期索引分析和配置调整。

ECOSIRE 为企业部署提供 Odoo 性能审计,为您的特定工作负载、事务模式和基础设施确定影响最大的优化。我们的工程师已将 Odoo 安装从 10 个用户的中小型企业部署调整为 500 个用户的企业部署。

向 ECOSIRE 请求 Odoo 性能审计 →

分享您当前的服务器规格、用户数量以及您遇到的症状,我们的团队将确定根本原因并提供优先优化计划。

E

作者

ECOSIRE Research and Development Team

在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。

通过 WhatsApp 聊天