OpenWebUI调优

大量性能提升、流畅度优化

当我们的用户量增加到一定的程度时(比如当我的实时在线用户超过100人时),即便服务器的硬件配置再高,也会变得非常卡顿,这是因为OpenWebUI本身机制上有很多坑,接下来我们在GPT-5的帮助下逐步优化,整体流畅度可以提升10倍以上。

首先,OpenWebUI默认是单线程运行的,这也是导致卡顿的罪魁祸首,最大的问题根源,我们可以通过下面这个命令查看进程数:

# 1) 用宿主机的 ps 能力查看容器内进程
docker top open-webui -eo pid,comm,args | sed 's/\x00/ /g' | egrep -i "uvicorn|gunicorn|open-webui|python" || docker top open-webui

# 2) 粗略计数(看到几行 uvicorn/gunicorn 通常就是几个 worker)
docker top open-webui | egrep -i "uvicorn|gunicorn" | wc -l

对此,我们需要进行多进程配置,此时我们需要修改compose文件:

services:
  postgre:
    image: docker.1panel.live/library/postgres@sha256:4d89c904835259bc58876520e56267ca07a4ebd6a027f7814bbbf91b50d685be
    container_name: postgre
    restart: always
    environment:
      - POSTGRES_USER=st
      - POSTGRES_PASSWORD=STshentong
      - POSTGRES_DB=openwebui
    volumes:
      - ./postgres_data:/var/lib/postgresql/data
    network_mode: host #端口5432

  open-webui:
    image: ghcr.nju.edu.cn/ovinc-cn/openwebui:latest
    container_name: open-webui
    volumes:
      - ./open-webui:/app/backend/data
    restart: always
    environment:
      - DATABASE_URL=postgresql://st:STshentong@localhost:5432/openwebui # 连接到 openwebui 数据库
      - UVICORN_WORKERS=6
      - WEBSOCKET_MANAGER=redis
      - ENABLE_WEBSOCKET_SUPPORT=True
      - WEBSOCKET_REDIS_URL=redis://localhost:6379
      - REDIS_URL=redis://localhost:6379
      - WEBUI_NAME=ChatST
      - AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST=1
      - AIOHTTP_CLIENT_TIMEOUT_OPENAI_MODEL_LIST=1
      - USER_AGENT=${USER_AGENT:-Mozilla/5.0 (compatible; OpenWebUI/1.0; +https://github.com/open-webui)}
      - WEBUI_SECRET_KEY=85fafa5e-0992-4d9b-a84c-6679646040f3
      - LICENSE_KEY=enterprise
      - ORGANIZATION_NAME=ST-STUDIO
      - CUSTOM_NAME=ChatST
    build:
      args:
        USER_AGENT: $USER_AGENT
    network_mode: host #端口8080
    ulimits:
      nofile:
        soft: 1048576
        hard: 1048576
    depends_on:
      - postgre
      - redis

  redis:
    image: docker.1panel.live/library/redis:latest
    container_name: redis
    restart: always
    network_mode: host #端口6379
    ulimits:
      nofile:
        soft: 1048576
        hard: 1048576

  tika:
    image: docker.1panel.live/apache/tika:latest-full
    container_name: tika
    network_mode: host #端口9998
    restart: always

注意,在这份compose文件中,相较于我们之前的版本有很多的修改项,首先我们将数据库的版本号固定了,采用SHA256的形式写死在文件中,这样可以避免PostgreSQL版本升级造成的变更;然后我们在环境变量中配置了进程数为6个,这可以大幅提升并发性能,大幅改善多用户场景卡顿问题(具体来说,进程数建议设置为服务器核心数或略少于核心数即可,比如我在8H的服务器上使用6进程);另外需要特别注意的是,要开启多进程必须要确保ws的管理器是redis,这里我们也在环境变量中显式指定了;然后我们还显式指定了ulimit文件描述符nofile限制为1048576,虽然默认值大概率就是这个,但是写死更好一些。

修改compose文件后需要重建一下发生修改的容器:

docker compose up -d --force-recreate open-webui redis

然后我们同样可以通过上面的指令去查看多workers配置是否生效。然后也可以验证一下我们的ulimit设置有没有生效:

docker exec -it open-webui sh -lc 'ulimit -n; grep -i "open files" /proc/1/limits'
docker exec -it redis sh -lc 'ulimit -n; grep -i "open files" /proc/1/limits'
docker exec -it redis sh -lc 'redis-cli CONFIG GET maxclients'

这样我们在docker层面和项目本身的优化就差不多了,主要还是进程数的提升。接下来我们就要进行数据库层面的优化了。数据库层面将有很多的优化项,首先我们查询一下数据库的当前参数:

# 进入容器执行一组常用视图(不会改配置)
docker exec -it postgre bash -lc '
psql -U st -d openwebui <<SQL
SELECT version();
SELECT name, setting FROM pg_settings
 WHERE name IN (
  '\''shared_buffers'\'','\''effective_cache_size'\'','\''work_mem'\'','\''maintenance_work_mem'\'',
  '\''max_wal_size'\'','\''checkpoint_timeout'\'','\''checkpoint_completion_target'\'',
  '\''wal_compression'\'','\''jit'\'','\''random_page_cost'\'','\''effective_io_concurrency'\'',
  '\''max_connections'\'','\''synchronous_commit'\''
 ) ORDER BY name;

-- 看连接与等待(是否有锁/连接等待)
SELECT state, wait_event_type, wait_event, count(*) 
FROM pg_stat_activity WHERE datname='\''openwebui'\'' GROUP BY 1,2,3 ORDER BY 4 DESC;

-- 死元组(表膨胀的信号)
SELECT relname, n_dead_tup
FROM pg_stat_user_tables ORDER BY n_dead_tup DESC LIMIT 15;

-- 检查检查点频率
SELECT * FROM pg_stat_bgwriter;
SQL'

通过这个命令查看当前配置,然后我们可以一步到位修改参数(请注意下面这套参数是基于8GB的物理内存的服务器,如果内存大/小,需要按照比例进行修改):

# 1) 打开观察面板(强烈建议先做)——需重启一次
docker exec -it postgre bash -lc "
echo \"
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000
pg_stat_statements.track = all
\" >> /var/lib/postgresql/data/postgresql.auto.conf"
docker compose restart postgre
docker exec -it postgre bash -lc "psql -U st -d openwebui -c 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements;'"

# 2) 一组通用的、风险低的参数(除 * 标注外都支持 reload)
docker exec -it postgre bash -lc '
psql -U st -d openwebui <<SQL
-- * 需要重启:shared_buffers
ALTER SYSTEM SET shared_buffers = '\''1GB'\'';
-- 预估 OS+PG 可用缓存(通常取 shared_buffers 的 3x)
ALTER SYSTEM SET effective_cache_size = '\''3GB'\'';

-- 排序/哈希的工作内存(并发高别贪大)
ALTER SYSTEM SET work_mem = '\''32MB'\'';
-- VACUUM/重建索引用
ALTER SYSTEM SET maintenance_work_mem = '\''512MB'\'';

-- WAL/检查点(更平滑)
ALTER SYSTEM SET max_wal_size = '\''4GB'\'';
ALTER SYSTEM SET min_wal_size = '\''1GB'\'';
ALTER SYSTEM SET checkpoint_timeout = '\''15min'\'';
ALTER SYSTEM SET checkpoint_completion_target = 0.9;
ALTER SYSTEM SET wal_compression = on;

-- SSD 默认:更积极的并行/预取与成本估计
ALTER SYSTEM SET random_page_cost = 1.2;
ALTER SYSTEM SET effective_io_concurrency = 200;

-- JIT 对短小 OLTP 常常负收益
ALTER SYSTEM SET jit = off;

-- 可选:把慢 SQL 门槛设为 500ms,便于排查
ALTER SYSTEM SET log_min_duration_statement = '\''500ms'\'';

SELECT pg_reload_conf();
SQL'

# 3) 重启以让 shared_buffers 生效
docker compose restart postgre

# 4) 验证
docker exec -it postgre bash -lc "psql -U st -d openwebui -c \"SELECT name, setting FROM pg_settings WHERE name IN ('shared_buffers','effective_cache_size','work_mem','maintenance_work_mem','max_wal_size','checkpoint_timeout','wal_compression','jit','random_page_cost','effective_io_concurrency');\""

修改后我们可以使用下面的指令验证一下关键项的修改是否到位:

# 看 shared_buffers 是否到 1GB(这条最直观)
docker exec -it postgre bash -lc "psql -U st -d openwebui -c 'SHOW shared_buffers;'"

# 同时核实来源(来自哪个文件/是否 pending_restart)
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"SELECT name, setting, source, sourcefile FROM pg_settings WHERE name IN ('shared_buffers','shared_preload_libraries');\""

# 确认 pg_stat_statements 已预加载且可用
docker exec -it postgre bash -lc \
\"psql -U st -d openwebui -c \\\"SHOW shared_preload_libraries; SELECT extname, extversion FROM pg_extension WHERE extname='pg_stat_statements'; SELECT count(*)>0 AS can_query FROM pg_stat_statements;\\\" \"

也可以使用下面的指令查询有没有什么修改需要等待重启才能完成,若有,重启数据库容器即可:

docker exec -it postgre bash -lc "psql -U st -d openwebui -c \"SELECT name, pending_restart FROM pg_settings WHERE pending_restart;\""

然后这时有一个注意事项,当我们修改了这些参数后,系统盘的占用会快速增加,这主要是因为我们开启了大量的日志记录用于分析,他会写入大量的日志文件(前提是当前有大量用户正在使用),这时我们需要修改PostgreSQL的日志存储逻辑:

docker exec -it postgre bash -lc '
psql -U st -d openwebui <<SQL
ALTER SYSTEM SET logging_collector = on;                  -- 需重启
ALTER SYSTEM SET log_destination   = '\''csvlog'\'';      -- 写 CSV,便于分析
ALTER SYSTEM SET log_directory     = '\''log'\'';         -- 相对 PGDATA,即 /www/postgres_data/log
ALTER SYSTEM SET log_filename      = '\''postgresql-%Y-%m-%d_%H%M%S.csv'\'';
ALTER SYSTEM SET log_rotation_age  = '\''1h'\'';
ALTER SYSTEM SET log_rotation_size = '\''200MB'\'';
ALTER SYSTEM SET log_truncate_on_rotation = on;
-- 如果不是在排查期,慢日志阈值建议先提到 1s,避免量太大
ALTER SYSTEM SET log_min_duration_statement = '\''1s'\'';
SELECT pg_reload_conf();
SQL'
# 启用 logging_collector 需要重启
docker restart postgre

将日志存放在Postgre容器的数据卷内,也就是我们的数据盘中(前提是你的服务器分系统盘和数据盘),并且写了上限为200MB滚动记录。有了日志记录之后,我们就可以在业务跑一段时间之后来分析慢查询了,通过查看SQL查询最慢的几条记录来进行针对性的优化:

docker exec -it postgre bash -lc \
"PAGER=cat psql -U st -d openwebui -P pager=off -c \"
SELECT
  round((total_exec_time/1000)::numeric,1) AS total_s,
  calls,
  round(mean_exec_time::numeric,1)         AS mean_ms,
  rows,
  query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 20;\""

这个命令可以查出最慢的20条,如果需要进一步筛选也可:

docker exec -it postgre bash -lc \
"PAGER=cat psql -U st -d openwebui -P pager=off -c \"
SELECT
  calls,
  round(mean_exec_time::numeric,1) AS mean_ms,
  round((total_exec_time/1000)::numeric,1) AS total_s,
  rows,
  query
FROM pg_stat_statements
WHERE calls >= 100
ORDER BY mean_exec_time DESC
LIMIT 20;\""

然后我们将查询的结果发给AI让他帮我们撰写索引创建命令即可,创建了相应的索引后慢查询的速度会有显著提升;此外,我们还可以开启lz4压缩来减少大json的体积,先看是否支持lz4压缩,理论上Postgre 14以上的版本默认都支持:

docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"SET default_toast_compression='lz4'; SHOW default_toast_compression;\""

然后开启lz4压缩:

# 写入 postgresql.auto.conf
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"ALTER SYSTEM SET default_toast_compression='lz4';\""

# 让配置生效(这个参数支持 reload,不必重启)
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"SELECT pg_reload_conf();\""

# 验证
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"SHOW default_toast_compression;\""

将大字段存储策略确保为EXTENDED:

docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"ALTER TABLE chat ALTER COLUMN chat SET STORAGE EXTENDED;\""

这里给出在我的生产环境中有效的提速索引创建命令:

# 1) “未分组(folder_id IS NULL)+ 用户 + 归档筛选 + 时间倒序”
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \
\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chat_user_arch_upd_folder_null
   ON chat (user_id, archived, updated_at DESC)
   WHERE folder_id IS NULL;\""

# 2) “指定文件夹 + 用户 + 归档筛选 + 时间倒序”
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \
\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chat_folder_user_arch_upd
   ON chat (folder_id, user_id, archived, updated_at DESC);\""

docker exec -it postgre bash -lc "psql -U st -d openwebui -c 'ANALYZE chat;'"

docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \
\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chat_user_pinned_arch_upd
   ON chat (user_id, pinned, archived, updated_at DESC);\""
   
# A) chat:归档 + 时间阈值(覆盖你的 DELETE 与按归档列出的列表)
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \
\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chat_arch_upd
   ON chat (archived, updated_at);\""

# B) credit_log:按时间清理/查询
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \
\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_credit_log_created_at
   ON credit_log (created_at);\""

# 建完跑统计
docker exec -it postgre bash -lc "psql -U st -d openwebui -c 'ANALYZE chat; ANALYZE credit_log;'"

# 启用扩展(一次性)
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"CREATE EXTENSION IF NOT EXISTS pg_trgm;\""

# 给标题建 trigram 索引(大小写不敏感检索)
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \
\"CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_chat_title_trgm
   ON chat USING gin (lower(title) gin_trgm_ops);\""

docker exec -it postgre bash -lc "psql -U st -d openwebui -c 'ANALYZE chat;'"

让chat表维护更积极:

docker exec -it postgre bash -lc \
"psql -U st -d openwebui -c \"
ALTER TABLE chat SET (
  autovacuum_vacuum_scale_factor = 0.05,
  autovacuum_vacuum_threshold    = 1000,
  autovacuum_analyze_scale_factor= 0.02,
  autovacuum_analyze_threshold   = 500
);\""

运营自检:

# 1) 索引是否真的被用到(看扫描次数)
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -P pager=off -c \"
SELECT relname AS table, indexrelname AS index, idx_scan
FROM pg_stat_user_indexes
WHERE relname IN ('chat','credit_log')
ORDER BY idx_scan DESC;\""

# 2) 表膨胀/死元组(观察 chat 是否被及时清理)
docker exec -it postgre bash -lc \
"psql -U st -d openwebui -P pager=off -c \"
SELECT relname, n_live_tup, n_dead_tup, vacuum_count, autovacuum_vacuum_count
FROM pg_stat_user_tables
WHERE relname IN ('chat','credit_log')
ORDER BY n_dead_tup DESC;\""

# 3) 慢 SQL Top20(已在用)

最后,因为我们对数据库启用了日志记录,时间久了可能会占用很多空间,因此对之前的清理脚本也进行了一些优化:

#!/bin/bash
# 安全开关(出错直接退出)
set -Eeuo pipefail

# Docker 容器 / 数据库信息
CONTAINER_NAME="postgre"
PG_USER="st"
DATABASE_NAME="openwebui"

# 受保护用户
PROTECTED_USERS="'wyk','wwf','syx'"

# PostgreSQL 日志清理配置(log_directory 我们之前设在 /www/postgres_data/log)
LOG_DIR="/www/postgres_data/log"
LOG_RETENTION_DAYS=7   # 保留 7 天

echo "清理未激活且 45 天未登录的用户..."
docker exec -i "${CONTAINER_NAME}" \
  psql -U "${PG_USER}" -d "${DATABASE_NAME}" <<EOSQL
DELETE FROM "user"
WHERE role = 'pending'
  AND last_active_at < EXTRACT(EPOCH FROM NOW()) - (45 * 24 * 60 * 60)
  AND name NOT IN (${PROTECTED_USERS});
EOSQL

echo "清理 90 天前未归档的聊天记录..."
docker exec -i "${CONTAINER_NAME}" \
  psql -U "${PG_USER}" -d "${DATABASE_NAME}" \
  -c "DELETE FROM \"chat\" 
      WHERE updated_at <= EXTRACT(EPOCH FROM DATE_TRUNC('day', NOW() - INTERVAL '90 day'))::INTEGER
        AND archived = false;"

echo "清理 60 天前的文件上传记录..."
docker exec -i "${CONTAINER_NAME}" \
  psql -U "${PG_USER}" -d "${DATABASE_NAME}" \
  -c "DELETE FROM \"file\" 
      WHERE updated_at < EXTRACT(EPOCH FROM NOW()) - (60 * 24 * 60 * 60);"

echo "清理 7 天前的 credit_log 记录..."
docker exec -i "${CONTAINER_NAME}" \
  psql -U "${PG_USER}" -d "${DATABASE_NAME}" \
  -c "DELETE FROM \"credit_log\" 
      WHERE created_at < EXTRACT(EPOCH FROM NOW()) - (7 * 24 * 60 * 60);"

echo "对受影响的表执行 VACUUM (ANALYZE)..."
docker exec -i "${CONTAINER_NAME}" \
  psql -U "${PG_USER}" -d "${DATABASE_NAME}" \
  -v ON_ERROR_STOP=1 -c \
  "VACUUM (ANALYZE) \"user\"; VACUUM (ANALYZE) chat; VACUUM (ANALYZE) \"file\"; VACUUM (ANALYZE) credit_log;"

echo "数据库清理完成。"

echo "清理超过 ${LOG_RETENTION_DAYS} 天的 PostgreSQL CSV 日志(目录:${LOG_DIR})..."
if [ -d "${LOG_DIR}" ]; then
  BEFORE_SIZE=$(du -sh "${LOG_DIR}" | awk '{print $1}')
  # 只删我们配置生成的 CSV 日志
  find "${LOG_DIR}" -type f -name "postgresql-*.csv" -mtime +${LOG_RETENTION_DAYS} -print -delete
  AFTER_SIZE=$(du -sh "${LOG_DIR}" | awk '{print $1}')
  echo "PG 日志:清理前 ${BEFORE_SIZE} -> 清理后 ${AFTER_SIZE}"
else
  echo "警告:日志目录 ${LOG_DIR} 不存在,跳过 PG 日志清理。"
fi

echo "清理超过 7 天未修改的向量数据库目录..."
find /www/open-webui/vector_db -mindepth 1 -type d -mtime +7 -exec rm -r {} +

echo "所有清理任务已完成。"

最后更新于