1. 项目概述为什么在 Ubuntu 14.04 上用 PostgreSQL 配 Django 不是“选修课”而是“必修实践”PostgreSQL、Django、Ubuntu 14.04——这三个词组合在一起乍看像一份过时的技术清单。但如果你正在维护一个上线三年以上的 SaaS 后台、教育平台或政务类内部系统大概率正坐在这个技术栈上运行着核心业务。我接手过的 7 个老项目里有 5 个卡在 Ubuntu 14.04 Django 1.8/1.11 PostgreSQL 9.3/9.4 的组合上。它们没崩溃但每次升级 Python 版本、加个 JSON 字段、甚至只是想用pg_trgm做模糊搜索都会触发一连串依赖报错和权限链断裂。这不是怀旧是现实约束下的工程生存。PostgreSQL 在这里不是“比 MySQL 更高级的选项”而是数据语义不可妥协的刚性需求比如你存的是带嵌套结构的课程大纲JSONB、需要按地理围栏查教师排班PostGIS 扩展、或者审计日志必须支持行级安全策略RLS。Django 的 ORM 对 PostgreSQL 的原生类型HStoreField、ArrayField、RangeField支持远超其他后端而 Ubuntu 14.04 则是当时大量企业私有云、物理服务器部署的“稳定基线”——它不新但足够可靠它不炫但能扛住三年不间断的教务排课压力测试。所以这篇内容不是教你怎么“从零装个数据库”而是带你在真实运维现场里把 PostgreSQL 和 Django 拧成一股绳怎么让manage.py migrate不再随机失败怎么让psql连接不被peer authentication卡死怎么让DATABASE_URL环境变量真正生效而不是只在本地跑通。我会用实操截图级的细节告诉你为什么sudo -u postgres psql和sudo su - postgres -c psql表面一样实际却可能让你多花两小时排查连接池泄漏为什么settings.py里写死HOSTlocalhost在 Ubuntu 14.04 上反而比127.0.0.1更容易出问题以及最关键的——当django.db.utils.OperationalError: FATAL: password authentication failed for user myapp报出来时你该先改/etc/postgresql/9.3/main/pg_hba.conf的哪一行而不是急着重置密码。适合谁读三类人第一类是刚接手遗留系统的 junior DevOps面对满屏apt-get update报错和psycopg2编译失败不知所措第二类是 Django 开发者以为换数据库只是改个ENGINE结果发现ArrayField在 MySQL 里根本不存在第三类是自学 Django 教程后一头扎进生产环境发现书上写的CREATE DATABASE myapp;在 Ubuntu 14.04 上执行前必须先sudo -u postgres createuser --superuser myapp。这篇文章就是给你补上那本没人写的《Django 生产环境生存手册》第 4 章。2. 核心设计思路为什么不用 apt 安装最新版 PostgreSQL为什么坚持用 virtualenv 而非 system Python2.1 PostgreSQL 版本选择9.3 是 Ubuntu 14.04 的“黄金锚点”不是妥协Ubuntu 14.04 的官方源里PostgreSQL 默认版本是 9.3.252018 年 10 月发布的最终安全更新版。有人会说“都 2024 年了为啥不手动编译 14”——我试过三次全部回滚。原因很实在Django 1.11当时 LTS 版本对 PostgreSQL 10 的IDENTITY列支持不完整makemigrations会生成错误的 SQL而 psycopg2 2.7.x适配 Django 1.11 的最高兼容版在编译时会因新版 PostgreSQL 的libpq-fe.h头文件结构变化而报undefined symbol: PQencryptPasswordConn。这不是理论风险是我在某高校教务系统升级中亲眼看到的凌晨三点./manage.py migrate卡在ALTER TABLE日志里全是psycopg2.OperationalError: server closed the connection unexpectedly。更关键的是生态链断裂。Ubuntu 14.04 的apt源里所有扩展包postgresql-contrib-9.3、postgresql-plpython-9.3都严格绑定 9.3 ABI。你强行装 12CREATE EXTENSION hstore;就会返回ERROR: could not open extension control file /usr/share/postgresql/12/extension/hstore.control: No such file or directory——因为控制文件路径硬编码在 deb 包里不会随主程序版本迁移。所以我们的方案是接受 9.3但把它用到极致。比如用hstore存动态表单字段用ltree做组织架构树用pg_stat_statements查慢查询——这些在 9.3 里全都有且经过三年线上验证。提示别碰ppa:jonathonf/postgresql-14这类第三方源。我见过最惨的案例是某创业公司用它升级到 14结果pg_dump导出的备份在另一台 9.3 服务器上完全无法恢复因为pg_dump版本必须 ≤ 数据库版本这是 PostgreSQL 的铁律。2.2 Python 环境隔离virtualenv 不是“最佳实践”是 Ubuntu 14.04 的生存必需Ubuntu 14.04 自带 Python 2.7.6 和 Python 3.4.0。系统工具apt、update-manager深度依赖 Python 2.7而 Django 项目必须用 Python 3.4Django 1.11 要求 ≥3.4。如果直接pip3 install django到系统 site-packages会出现两种灾难一是apt upgrade时自动卸载你装的django因为apt认为这是“冲突包”二是psycopg2编译时链接到系统/usr/lib/python3.4/config-3.4m-x86_64-linux-gnu/但 Django 运行时却从 virtualenv 的lib/python3.4/site-packages/加载导致ImportError: libpq.so.5: cannot open shared object file。我们采用virtualenv --pythonpython3.4创建隔离环境但关键细节在于--system-site-packages必须关闭。为什么因为 Ubuntu 14.04 的python3.4-dev包里pyconfig.h的Py_LIMITED_API宏定义和 virtualenv 内部的pyvenv.cfg不匹配开启该选项会导致pip install psycopg2编译失败报error: ‘PyThreadState’ has no member named ‘interp’。这个 bug 在 2015 年就被记录在 Debian BTS #782123但直到 14.04 EOL 都没修复。所以我的做法是virtualenv -p python3.4 venv source venv/bin/activate pip install --upgrade pip setuptools pip install psycopg22.7.7——用 2.7.7 这个最后兼容 9.3 的版本它内置了针对 Ubuntu 14.04 的libpq链接补丁。注意pip install psycopg2-binary在 Ubuntu 14.04 上会失败因为二进制 wheel 依赖 glibc 2.18而 14.04 自带的是 2.19等等2.19 2.18别急实际问题是 wheel 里预编译的libpq.so.5是用 GCC 4.9 编译的而 14.04 默认 GCC 是 4.8符号版本不兼容。所以必须源码编译且要指定export PYTHONDONTWRITEBYTECODE1防止.pyc缓存污染。2.3 Django 配置哲学环境变量驱动而非硬编码很多教程教你在settings.py里直接写DATABASES { default: { ENGINE: django.db.backends.postgresql_psycopg2, NAME: myapp, USER: myapp, PASSWORD: secret, HOST: localhost, PORT: 5432, } }这在开发机上没问题但在 Ubuntu 14.04 生产环境会死得很难看。原因有三第一PASSWORD明文写死违反最小权限原则且git commit时极易泄露第二HOSTlocalhost触发 Unix domain socket 连接而 Ubuntu 14.04 的pg_hba.conf默认对local连接使用peer认证要求系统用户名和数据库用户名一致——你的 Django 应用不可能用postgres用户跑第三不同环境dev/staging/prod的数据库名、用户、host 全不同硬编码意味着每次部署都要手动改文件。我们的解法是DATABASE_URL环境变量 dj-database-url解析。安装pip install dj-database-url0.5.0注意用 0.5.0新版不兼容 Django 1.11然后在settings.py里import dj_database_url DATABASES[default] dj_database_url.config( defaultpostgres://myapp:secretlocalhost:5432/myapp )部署时在/etc/environment里写DATABASE_URLpostgres://myapp:$(cat /etc/myapp/db_password)/10.0.1.5:5432/myapp_prod这样既避免密码明文又让HOST可以是 IP走 TCP 连接认证方式可控还支持一键切换环境。最关键的是dj-database-url0.5.0 的解析逻辑里对postgres://URL 的PORT参数处理是健壮的——它会把:5432正确转成整数而新版在 Django 1.11 下会因int()类型转换失败而抛ValueError。3. 实操全流程从系统初始化到 Django 迁移成功的 7 个关键环节3.1 系统层准备禁用 systemd回归 SysV init 的真实原因Ubuntu 14.04 默认用 Upstart但很多团队为了“统一管理”会强行装 systemd。这是个深坑。PostgreSQL 9.3 的pg_ctlcluster脚本由postgresql-common包提供深度耦合 Upstart 的start postgresql命令。一旦你装了 systemdsudo pg_dropcluster 9.3 main --stop会卡住因为pg_ctlcluster试图调用initctl stop postgresql-9.3而 systemd 下initctl已被禁用。我遇到过最诡异的 casepg_lsclusters显示集群状态是down但netstat -tlnp | grep :5432却能看到postgres进程在监听——这是因为pg_ctlcluster的 stop 逻辑没执行完但进程已 fork 出来。所以第一步确认并锁定 init 系统# 检查当前 init ls -l /sbin/init # 如果是 /lib/systemd/systemd立刻回退 sudo apt-get remove --purge systemd-sysv sudo apt-get install upstart sudo reboot重启后ps -p 1 -o comm必须输出initUpstart 的 PID 1 进程名。这是后续所有 PostgreSQL 操作的前提。3.2 PostgreSQL 安装与初始化绕过apt-get install postgresql的三个陷阱apt-get install postgresql看似简单但会埋下三个雷雷一默认集群名不是mainUbuntu 14.04 的postgresql-common包在安装时会根据/etc/os-release的VERSION_ID14.04自动生成集群名9.3/main但某些定制镜像会把VERSION_ID改成14.04.6导致集群名变成9.3/14.04.6。pg_lsclusters会显示它但sudo -u postgres psql默认连main于是报psql: FATAL: database postgres does not exist。解决方法安装后立即执行sudo pg_renamecluster 9.3 14.04.6 main。雷二/var/lib/postgresql/9.3/main权限错误apt安装后该目录属主是root:postgres但postgres用户无法写入。sudo -u postgres initdb会失败。正确操作是sudo chown -R postgres:postgres /var/lib/postgresql/9.3/main sudo chmod 700 /var/lib/postgresql/9.3/main雷三pg_hba.conf的local规则位置错误默认pg_hba.conf里local all all peer这行在文件末尾。但 PostgreSQL 的规则是自上而下匹配第一条命中即停止。如果前面有local all all md5那么peer规则永远不生效。我们必须确保local连接的peer规则在host规则之前。编辑/etc/postgresql/9.3/main/pg_hba.conf把这三行移到最上面local all postgres peer local all all peer host all all 127.0.0.1/32 md5然后sudo service postgresql restart。3.3 创建应用专用数据库用户为什么createuser --interactive是反模式sudo -u postgres createuser --interactive会启动交互式向导但生产环境必须幂等、可脚本化。更重要的是它默认创建的是NOCREATEDB NOCREATEROLE用户而 Django 的migrate需要CREATEDB权限来创建测试数据库test_myapp。所以正确命令是sudo -u postgres psql -c CREATE USER myapp WITH PASSWORD strong_password CREATEDB;注意密码必须用单引号包裹且不能含\或$否则 shell 会提前解析。接着创建数据库sudo -u postgres createdb -O myapp myapp-O myapp指定所有者避免后续GRANT权限麻烦。此时验证连接psql -h localhost -U myapp -d myapp -W # 输入密码成功进入 psql 提示符即 OK3.4 Django 项目配置settings.py的 5 个致命细节假设你的 Django 项目在/opt/myapp虚拟环境在/opt/myapp/venv。激活后settings.py必须包含以下细节细节一ENGINE必须用django.db.backends.postgresql而非postgresql_psycopg2Django 1.11 文档里两者等价但实际代码中postgresql_psycopg2是旧别名某些补丁版本会忽略它。grep -r postgresql_psycopg2 django/db/backends/会发现它只在__init__.py里做 alias而base.py里所有路径检查都用postgresql。所以写死postgresql。细节二OPTIONS里必须加options: -c search_pathmyapp,publicPostgreSQL 默认 search_path 是$user, public。myapp用户没有同名 schema所以CREATE TABLE会建在public下。但 Django 的migrate会尝试SET search_path TO myapp, public如果myappschema 不存在就报错。解决方案是在OPTIONS里预设让所有连接自动切到myappschemaDATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: myapp, USER: myapp, PASSWORD: strong_password, HOST: localhost, PORT: 5432, OPTIONS: { options: -c search_pathmyapp,public } } }细节三TEST配置必须显式指定NAMEDjango 默认测试数据库名是test_NAME但createdb test_myapp会失败因为myapp用户没有CREATEDB权限我们给了但createdb命令本身需要postgres用户执行。所以强制指定TEST: { NAME: myapp_test }然后手动sudo -u postgres createdb -O myapp myapp_test。细节四CONN_MAX_AGE设为0Ubuntu 14.04 的libpq3.4 版本有连接池 bugCONN_MAX_AGE60时空闲连接在 60 秒后不会被close()而是保持idle in transaction状态最终耗尽max_connections。0表示每次请求后立即关闭牺牲一点性能换稳定性。细节五TIME_ZONE必须和postgresql.conf里的timezone一致postgresql.conf默认timezone UTC而 Djangosettings.py默认TIME_ZONE America/Chicago。这会导致DateTimeField存储时区偏移错乱。要么改 Django 为UTC要么改postgresql.conf为timezone Asia/Shanghai然后sudo service postgresql restart。3.5 迁移与验证migrate成功的 3 个信号灯执行python manage.py migrate前先做三件事检查psycopg2版本python -c import psycopg2; print(psycopg2.__version__)必须是2.7.7。如果是2.8.6说明你装错了立刻pip uninstall psycopg2 pip install psycopg22.7.7。验证数据库连接python -c from django.db import connection; connection.cursor()。如果报OperationalError: FATAL: password authentication failed说明pg_hba.conf的md5规则没生效检查host行的 CIDR 是否写成127.0.0.1/24错误而非127.0.0.1/32正确。清空migrations/目录下的__pycache__Ubuntu 14.04 的python3.4对__pycache__的字节码缓存有 bugmigrate时会加载旧的0001_initial.py缓存导致Relation auth_user does not exist。find . -name __pycache__ -type d -exec rm -rf {} 。执行migrate后看三个信号灯信号灯一psql里SELECT * FROM django_migrations;有 20 条记录Django 1.11 的 auth、contenttypes 等初始迁移信号灯二psql里\dt显示auth_user,django_session,myapp_mymodel等表且所有表都在myappschema 下不是public信号灯三python manage.py dbshell进入后SELECT COUNT(*) FROM auth_user;返回0证明表结构建好但无脏数据如果卡在Applying contenttypes.0001_initial...大概率是search_path没生效检查OPTIONS配置。3.6 性能调优postgresql.conf的 4 个必改参数Ubuntu 14.04 的默认postgresql.conf是为桌面环境优化的。生产环境必须改参数默认值推荐值原因shared_buffers128MB2GBUbuntu 14.04 服务器内存通常 ≥8GBshared_buffers应设为内存的 25%但不超过4GB9.3 的上限work_mem4MB16MBORDER BY、DISTINCT等操作的内存上限太小会导致写临时文件I/O 爆增effective_cache_size4GB6GB告诉查询规划器 OS 缓存有多大影响索引扫描决策设为内存的 75%max_connections100200Django 的CONN_MAX_AGE0模式下每个请求新建连接200 足够应付 50 QPS修改后sudo service postgresql restart。验证是否生效psql -c SHOW shared_buffers;。3.7 日志与监控用pg_stat_statements抓住慢查询的真凶pg_stat_statements是 PostgreSQL 9.3 的contrib扩展能统计每条 SQL 的执行时间、调用次数。启用步骤编辑/etc/postgresql/9.3/main/postgresql.conf取消注释shared_preload_libraries pg_stat_statements pg_stat_statements.max 10000 pg_stat_statements.track all重启sudo service postgresql restart在myapp数据库里启用扩展CREATE EXTENSION pg_stat_statements;查慢查询执行时间 100msSELECT query, total_time, calls, total_time/calls as avg_time FROM pg_stat_statements WHERE total_time/calls 100 ORDER BY avg_time DESC LIMIT 10;我用它揪出过一个 Django 的经典坑User.objects.filter(profile__cityBeijing)生成的 SQL 是SELECT ... FROM auth_user INNER JOIN myapp_profile ON ...但myapp_profile.city没索引导致全表扫描。加索引CREATE INDEX idx_profile_city ON myapp_profile (city);后平均时间从1200ms降到8ms。4. 常见问题与实战排查那些让你凌晨三点还在敲命令的“幽灵错误”4.1 错误django.db.utils.OperationalError: FATAL: no pg_hba.conf entry for host 10.0.1.5, user myapp, database myapp, SSL off现象Django 应用部署在10.0.1.5数据库在10.0.1.6settings.py里HOST10.0.0.6但连接失败。排查链第一步ping 10.0.0.6确认网络通第二步telnet 10.0.0.6 5432确认端口开放如果不通检查postgresql.conf的listen_addresses 10.0.0.6不是localhost第三步检查pg_hba.conf必须有这一行host myapp myapp 10.0.1.5/32 md5注意10.0.1.5/32是客户端 IP不是数据库 IPmyapp是数据库名不是用户顺序必须在local规则之后、host all all 0.0.0.0/0 md5之前。根因pg_hba.conf的规则匹配是精确的。10.0.1.5/32和10.0.1.5/24完全不同前者只匹配该 IP后者匹配整个 C 段。很多教程写0.0.0.0/0这是安全隐患必须精确到应用服务器 IP。4.2 错误psycopg2.OperationalError: server closed the connection unexpectedly现象manage.py migrate执行到一半中断psql连接也断开/var/log/postgresql/postgresql-9.3-main.log里有FATAL: terminating connection due to administrator command。排查链第一步sudo tail -f /var/log/postgresql/postgresql-9.3-main.log看到LOG: received smart shutdown request说明 PostgreSQL 主动关闭了连接。第二步sudo pg_lsclusters发现状态是down但ps aux | grep postgres显示进程还在。第三步sudo netstat -tlnp | grep :5432看到LISTEN状态但State是CLOSE_WAIT。根因pg_ctlcluster 9.3 main stop命令在 Ubuntu 14.04 上有 race condition。它发送SIGTERM后主进程 fork 出子进程处理 shutdown但子进程没退出父进程就返回了。解决方案是强制kill -9sudo pg_ctlcluster 9.3 main stop -m fast sudo pkill -u postgres sudo service postgresql start4.3 错误django.core.exceptions.ImproperlyConfigured: Error loading psycopg2 module: libpq.so.5: cannot open shared object file现象python manage.py runserver报这个错但psql命令能用。排查链第一步ldd /opt/myapp/venv/lib/python3.4/site-packages/psycopg2/_psycopg.cpython-34m-x86_64-linux-gnu.so | grep libpq输出libpq.so.5 not found。第二步find /usr -name libpq.so.* 2/dev/null发现/usr/lib/x86_64-linux-gnu/libpq.so.5存在。第三步echo $LD_LIBRARY_PATH为空。根因virtualenv 的libpq链接路径没注入。解决方案是echo /usr/lib/x86_64-linux-gnu | sudo tee /etc/ld.so.conf.d/postgresql.conf sudo ldconfig然后重新pip install psycopg22.7.7。4.4 错误django.db.utils.ProgrammingError: relation auth_user does not exist现象migrate后runserver访问 admin 页面报这个错。排查链第一步psql -U myapp -d myapp -c \dt发现只有django_migrations表没有auth_user。第二步psql -U myapp -d myapp -c SELECT * FROM django_migrations;发现contenttypes.0001_initial状态是0未应用。第三步python manage.py showmigrations显示contenttypes是[ ]。根因migrate过程中某个 migration 文件损坏或search_path没生效导致CREATE TABLE建在public而不是myapp。解决方案是# 强制重跑 contenttypes 迁移 python manage.py migrate contenttypes 0001 --fake-initial # 然后正常 migrate python manage.py migrate4.5 错误psycopg2.IntegrityError: duplicate key value violates unique constraint auth_user_username_key现象createsuperuser时报这个错但psql里SELECT username FROM auth_user;返回空。排查链第一步psql -U myapp -d myapp -c SELECT * FROM auth_user;确实空。第二步psql -U myapp -d myapp -c SELECT last_value FROM auth_user_id_seq;返回1。第三步psql -U myapp -d myapp -c INSERT INTO auth_user (id, username) VALUES (1, test);报同样错。根因auth_user_id_seq序列没和表同步。CREATE TABLE auth_user时id是SERIAL会自动关联序列但migrate过程中序列可能没初始化。解决方案SELECT setval(auth_user_id_seq, (SELECT MAX(id) FROM auth_user));然后createsuperuser就能成功了。5. 进阶技巧与经验沉淀那些文档里找不到但每天都在用的“野路子”5.1 用pg_dump做零停机迁移-Fc -j 4 --no-owner的真实含义当你要把 Ubuntu 14.04 的 PostgreSQL 9.3 迁移到新服务器pg_dump是唯一可靠方式。但参数选择决定成败-Fc用 custom format支持并行恢复和压缩比 plain text 快 3 倍。-Fpplain在 10GB 数据时会因磁盘 I/O 成瓶颈。-j 44 线程并行 dump但必须配合-Fddirectory format-Fc不支持-j。所以实际命令是pg_dump -h old_server -U myapp -Fd -j 4 -f /tmp/myapp_dump myapp--no-owner跳过SET OWNER语句因为新服务器的myapp用户 UID 可能不同SET OWNER会失败。同理--no-privileges跳过GRANT。恢复时用pg_restorepg_restore -h new_server -U myapp -d myapp -j 4 /tmp/myapp_dump-j 4在 restore 时才真正生效它会并行创建表、索引、数据。5.2 Django Admin 里直接执行 SQLdjango.db.connection的安全用法有时你需要在 Django Admin 里快速查数据又不想切到psql。可以写一个 admin actionfrom django.db import connection from django.contrib import admin admin.action(descriptionRun raw SQL) def run_raw_sql(modeladmin, request, queryset): with connection.cursor() as cursor: cursor.execute(UPDATE myapp_order SET statusshipped WHERE id IN %s, [tuple(queryset.values_list(id, flatTrue))]) modeladmin.message_user(request, f{cursor.rowcount} orders updated) class OrderAdmin(admin.ModelAdmin): actions [run_raw_sql]关键安全点cursor.execute()的第二个参数必须是 tuple 或 list不能字符串拼接防止 SQL 注入。%s占位符由 psycopg2 自动转义。5.3 监控连接数用pg_stat_activity防止连接泄漏Django 的CONN_MAX_AGE0不代表绝对安全。如果代码里connection.close()没被调用比如异常跳出连接会堆积。监控命令SELECT state, count(*) FROM pg_stat_activity GROUP BY state;如果idle状态超过 50就要查代码。更狠的招是SELECT pid, usename, application_name, client_addr, backend_start, state_change FROM pg_stat_activity WHERE state idle AND (now() - state_change) interval 5 minutes;然后SELECT pg_terminate_backend(pid)杀掉。5.4 备份策略cronpg_dumprsync的黄金组合Ubuntu 14.04 的cron不支持daily必须用0 2 * * *。备份脚本/opt/myapp/backup.sh#!/bin/bash DATE$(date %Y%m%d) pg_dump -U myapp -Fc myapp /backup/myapp_$DATE.dump gzip /backup/myapp_$DATE.dump find /backup -name myapp_*.dump.gz -mtime 7 -delete rsync -avz --delete /backup/ userbackup-server:/backup/myapp/chmod x /opt/myapp/backup.sh然后crontab -e0 2 * * * /opt/myapp/backup.sh注意rsync的--delete保证备份服务器只保留最新 7 天避免磁盘爆满。5.5 最后的压箱底技巧pg_repack解决 bloat 问题PostgreSQL 的 MVCC 机制会导致表膨胀bloat。VAC