1. 为什么单台Discourse服务器在用户量破万后必然卡死——从架构基因说起Discourse不是传统PHP论坛那种“一个Apache进程扛所有请求”的老派架构它的底层是Ruby on Rails Ember.js全栈框架但真正让它区别于其他社区系统的是它对实时性、一致性与高并发的原生设计诉求。我最早在2016年接手一个日活8000的本地技术社区时就踩过这个坑当时用一台16核32G的云服务器单机部署Discourse前两个月一切平稳第三个月开始凌晨2点定时备份一跑整个站点响应延迟直接飙到8秒以上用户发帖失败率超过35%。运维日志里反复出现PG::ConnectionBad: connection is closed和Redis::TimeoutError: Connection timed out——这不是配置没调好而是单点架构的物理天花板到了。Discourse的请求链路天然具备“三重IO密集”特征数据库层每个帖子、回复、点赞、私信都触发PostgreSQL的ACID事务且大量使用JSONB字段做动态结构存储缓存层Redis不仅承担Session和Cache还负责Pub/Sub广播如新消息实时推送、Rate Limiting防刷、Job Queue后台任务调度应用层Sidekiq后台作业队列依赖Redis而Web进程又需频繁读写Redis获取用户状态、未读消息计数等高频数据。这三者一旦全部压在一台机器上CPU、内存、网络带宽、磁盘IOPS会形成多米诺骨牌式争抢。更关键的是Discourse的Session默认存储在Redis中而它的CSRF Token校验、登录态续期、WebSocket连接绑定都强依赖Redis响应速度——只要Redis慢100ms整个Web请求就卡住。这不是“加个SSD就能解决”的问题而是架构层面的耦合瓶颈。所以当标题里出现“Scale a Discourse Deployment with a Load Balancer and Managed Database Cluster”时它说的不是“怎么让Discourse跑得更快”而是“如何把原本紧耦合的三件套WebDBCache拆成可独立伸缩的三个服务单元”。Load Balancer不是锦上添花的装饰品它是解耦的第一道手术刀Managed Database Cluster也不是为了省事而是把PostgreSQL最脆弱的主从同步、故障切换、连接池管理这些脏活交给专业团队而Redis必须独立出来否则你永远无法真正实现水平扩展。我后来复盘那次故障发现一个反直觉的事实当时我们给服务器加到了32核64GPostgreSQL的shared_buffers调到了16GBRedis maxmemory设为24GB结果性能反而下降了——因为Linux内核在超大内存下进行页回收page reclaim的开销剧增加上NUMA节点跨区访问延迟实际可用资源反而缩水。真正的扩容路径从来不是堆硬件而是拆服务。这也是为什么Discourse官方文档开篇就强调“Discourse is designed to run on multiple servers — not one.”提示如果你正在评估是否需要拆分记住这个硬指标——当你的PostgreSQLpg_stat_database中xact_commit每秒超过1200次且blks_read持续高于blks_hit的3倍时单机DB已进入高危区当RedisINFO stats中expired_keys每秒超过500evicted_keys非零说明缓存压力已不可控。2. Load Balancer不是插上网线就完事——Discourse对会话亲和性与健康检查的硬性要求很多团队第一次尝试加负载均衡器时习惯性地把它当成“流量分发器”配置完轮询策略就上线。结果第二天就发现用户登录后刷新页面直接掉回登录页上传图片失败率飙升甚至同一个用户在不同节点看到的未读消息数都不一致。这不是Discourse Bug是你没读懂它的会话模型。Discourse的Session机制有三个关键事实Session数据不存Cookie而存在Redis中——Cookie里只存一个加密签名的Session ID_discourse_session真实数据全在Redis的session:前缀Key里CSRF Token与Session强绑定——每次表单提交都需校验Token而Token生成逻辑依赖Session中的_csrf_token字段WebSocket连接必须与处理该连接的Web进程保持长驻——Discourse用ActionCable实现消息推送而ActionCable的连接生命周期由单个Puma Worker进程管理。这意味着如果负载均衡器把用户第一次请求打到Node A生成Session并写入Redis第二次请求却打到Node BNode B虽然能从Redis读到Session但它的CSRF Token校验会失败——因为Token是Node A用本地密钥生成的Node B没有相同密钥上下文。更糟的是WebSocket握手成功后后续Ping/Pong帧若被转发到其他节点连接会立即中断。所以Discourse对Load Balancer提出两个不可妥协的要求必须启用Sticky Session会话亲和性且亲和性依据必须是_discourse_sessionCookie值而非IP哈希因用户可能走NAT或代理健康检查端点必须用Discourse原生接口不能用HTTP 200兜底。我实测过AWS ALB、Nginx Plus、HAProxy三种方案它们的配置差异极大负载均衡器类型Sticky Session配置要点健康检查正确路径常见误配陷阱AWS ALBTarget Group中启用StickinessCookie名称填_discourse_sessionTTL设为3600秒匹配Discourse默认Session过期时间HTTP GET /healthz返回200且Body含{status:ok}误用/作为健康检查Discourse首页会触发大量DB查询导致健康检查超时误判Nginx Plusip_hash完全无效必须用hash $cookie__discourse_session consistent;且需在upstream块中声明sticky cookie _discourse_session expires1h domain.yourdomain.com path/;location /healthz { return 200 {status:ok}; add_header Content-Type application/json; }忘记在server块中添加proxy_cookie_path / /; HttpOnly; Secure;导致Cookie Secure标志缺失HTTPS下Session失效HAProxycookie SESSIONID prefix indirect nocache无效必须用cookie _discourse_session insert indirect nocache并在backend中为每个server指定cookie node_idoption httpchk GET /healthzhttp-check expect status 200未配置timeout check 5s默认10s超时导致节点频繁被踢出集群特别提醒一个血泪教训Discourse的/healthz端点默认只检查Redis连接不检查PostgreSQL。我在一次生产环境升级中误将DB连接池参数max_connections从100调到50结果所有节点健康检查都通过Redis正常但实际用户请求90%失败DB连接池耗尽。后来我们在/healthz后面加了自定义检查强制验证DB连通性# config/initializers/health_check.rb Rails.application.config.after_initialize do if Rails.env.production? Rails.application.routes.draw do get /healthz health_checks#show end end end# app/controllers/health_checks_controller.rb class HealthChecksController ApplicationController skip_before_action :verify_authenticity_token, only: [:show] skip_before_action :authenticate_user!, only: [:show] def show redis_ok begin Redis.current.ping PONG rescue e Rails.logger.error Redis health check failed: #{e.message} false end db_ok begin ActiveRecord::Base.connection.execute(SELECT 1).first[0] 1 rescue e Rails.logger.error PostgreSQL health check failed: #{e.message} false end if redis_ok db_ok render json: { status: ok, timestamp: Time.now.iso8601 }, status: :ok else render json: { status: unhealthy, redis: redis_ok, db: db_ok }, status: :service_unavailable end end end注意Discourse 3.2版本已内置/healthz增强版但默认仍不检查DB。上述代码是生产环境经受住日均200万次健康检查的稳定方案务必在上线前用ab -n 1000 -c 100 https://your-site.com/healthz压测验证。3. Managed Database Cluster不是“托管”那么简单——PostgreSQL分片、连接池与只读副本的实战取舍当Discourse用户量突破5万PostgreSQL单实例的瓶颈就不再是CPU或内存而是连接数与锁竞争。Discourse默认配置中Puma启动8个Worker每个Worker最多32个线程理论上单台Web服务器就能发起256个DB连接。而一个中等活跃的Discourse站点光是后台Sidekiq Job、Rake任务、Admin后台查询就常驻占用50连接。此时若再叠加用户高峰PostgreSQL的max_connections默认100瞬间打满新连接排队等待整个站点进入“假死”状态。Managed Database Cluster如AWS RDS PostgreSQL Multi-AZ、Google Cloud SQL for PostgreSQL、阿里云RDS PostgreSQL高可用版表面看是帮你省去了主从搭建、故障切换的运维工作但Discourse对它的使用方式决定了你能否真正获得弹性。3.1 连接池是第一道生死线Discourse本身不内置连接池它依赖PostgreSQL客户端库pg gem的连接管理。但pg gem的连接复用机制非常原始每个线程独占一个连接用完即关。这意味着8个Puma Worker × 32线程 256个连接需求而RDS默认最大连接数通常只有{DBInstanceClassMemory/12582880}如db.t3.large约1200看似充裕。但问题在于Discourse的后台Job如发送邮件、生成摘要和Web请求共享同一连接池当大量邮件Job并发执行时Web请求连接会被饿死。解决方案只能是引入外部连接池——PgBouncer。但Managed DB服务对PgBouncer的支持千差万别AWS RDS不支持直接安装PgBouncer必须在应用层前加EC2实例部署PgBouncer或使用RDS Proxy仅支持PostgreSQL 10.7且不支持Discourse所需的transaction池模式Google Cloud SQL原生支持Cloud SQL Auth Proxy但Auth Proxy不提供连接池功能仍需额外部署PgBouncer阿里云RDS提供“数据库代理”功能支持transaction和session两种池化模式且自动处理SSL证书透传是目前对Discourse最友好的方案。我最终在生产环境采用阿里云RDS代理配置如下池化模式transactionDiscourse所有SQL都是短事务此模式复用率最高最小连接数32保障低峰期连接不被回收最大连接数512按Web服务器总线程数×2预留空闲超时300秒避免长连接占用资源。配置生效后RDS监控面板中的DatabaseConnections曲线从锯齿状频繁创建销毁变为平滑直线平均连接数稳定在80左右峰值不超过120。3.2 只读副本不是“读写分离”的银弹Discourse的代码中99%的数据库操作都是读写混合的。比如一个简单的“查看帖子”请求会触发查询帖子主体SELECT * FROM posts WHERE id ?查询作者信息SELECT * FROM users WHERE id ?查询该用户对该帖子的点赞状态SELECT * FROM post_actions WHERE user_id ? AND post_id ? AND post_action_type_id 2更新帖子的浏览计数UPDATE posts SET views views 1 WHERE id ?。这四个操作必须在同一个数据库事务中完成否则会出现“看到帖子但看不到自己已点赞”的数据不一致。因此Discourse官方明确不支持读写分离——它的database.yml中只允许配置一个primary数据库所有操作都走这里。那Managed DB的只读副本有什么用答案是专供后台离线分析与报表生成。我们在RDS集群中配置了一个专用只读副本IP地址单独配置然后在Discourse后台的Site Settings中设置enable_data_explorer为true并在data_explorer插件中指定只读副本的连接串。这样管理员运行SQL报表时所有查询都路由到只读副本完全不影响主库性能。3.3 分片Discourse官方至今不支持搜索热词里有“postgresql和mysql区别”、“postgresql分片”但必须清醒认识Discourse是一个单租户single-tenant应用它的数据模型天然不适合水平分片。所有用户、帖子、分类都存在同一套表结构中靠user_id、topic_id等外键关联。强行分片需要修改Discourse核心代码代价远超收益。真正可行的扩展路径是垂直拆分把附件存储S3、日志ELK、搜索Elasticsearch彻底剥离冷热分离用PostgreSQL的PARTITION BY RANGE按created_at对posts表分区将3年前的旧帖自动归档到只读表空间物化视图加速为高频统计报表如“各版块发帖量TOP10”创建物化视图并用REFRESH MATERIALIZED VIEW CONCURRENTLY每小时更新。我们实施冷热分离后posts主表大小从280GB降至42GBVACUUM耗时从47分钟缩短至3分钟这是比任何分片都实在的性能提升。4. Redis不是“装上就行”的缓存——Discourse对持久化、集群与连接管理的严苛规范Discourse对Redis的依赖程度远超一般Web应用。它不只是缓存HTML片段而是把Redis当作“内存数据库消息总线分布式锁会话中心”四合一的核心组件。这也是为什么Discourse官方文档反复强调“Redis must be available and responsive at all times. If Redis goes down, Discourse will stop working entirely.”4.1 持久化策略必须关闭RDB仅用AOFDiscourse的Redis使用场景决定了它对数据一致性的要求极高Session数据必须100%不丢失否则用户登录态消失Pub/Sub消息如新消息通知必须可靠投递不能因Redis重启丢失Rate Limiting计数必须精确否则刷屏攻击无法拦截。RDB快照save 900 1的问题在于它是周期性全量保存两次快照之间产生的数据全部丢失。而Discourse的Session过期时间默认是1小时若RDB配置为save 3600 1意味着最坏情况下1小时内所有新登录用户Session全丢。AOFAppend Only File则记录每个写命令配合appendfsync everysec每秒刷盘可保证最多丢失1秒数据且AOF重写BGREWRITEAOF过程不影响服务。我们在生产环境强制禁用RDB# redis.conf save # 彻底禁用RDB appendonly yes appendfilename appendonly.aof appendfsync everysec no-appendfsync-on-rewrite yes auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb注意AOF文件会持续增长必须配置auto-aof-rewrite。我们观察到当AOF文件超过2GB时重写耗时超过90秒期间Redis主线程会阻塞。因此将auto-aof-rewrite-min-size设为64MB确保重写更频繁但更轻量。4.2 Redis ClusterDiscourse官方不兼容搜索热词里有“redis集群”、“redis分布式锁”但Discourse的Redis客户端redis-rbgem使用的是单连接模式不支持Redis Cluster的哈希槽hash slot自动路由。当你把Discourse指向Redis Cluster的任意节点时它会报错MOVED 12345 10.0.1.5:6379——因为Discourse发出的GET session:abc123命令被重定向而客户端不会自动跟随重定向。唯一可行的高可用方案是Redis Sentinel。它提供一个虚拟IPVIP或域名客户端连接这个VIPSentinel自动将请求路由到当前Master。Discourse的redis.yml配置极其简单# config/redis.yml production: url: redis://:your_passwordredis-sentinel.example.com:26379/0 sentinels: - host: sentinel1.example.com port: 26379 - host: sentinel2.example.com port: 26379 - host: sentinel3.example.com port: 26379 role: master关键点在于url中的host必须是Sentinel的VIP而sentinels列表是真实的Sentinel节点地址。Discourse会先连VIP获取Master地址再建立实际连接。4.3 连接泄漏是隐形杀手——Sidekiq与Puma的协同配置Discourse的后台Job由Sidekiq驱动而Sidekiq默认为每个Worker进程创建独立的Redis连接。若Puma配置8个WorkerSidekiq配置25个Concurrency则Redis连接数 8 × 25 200再加上Web进程的8×32256总计近500连接。而Redis默认maxclients是10000看似安全但连接数过多会导致内存碎片和TCP端口耗尽。我们的解决方案是让Sidekiq复用Puma的Redis连接池。在config/sidekiq.yml中--- :concurrency: 25 :queues: - default - mailers - critical :redis: :url: redis://:your_passwordredis-sentinel.example.com:26379/0 :pool: 16 # 关键限制Sidekiq最多用16个连接 :pool_timeout: 5同时在Puma配置中将Redis连接池大小设为32# config/puma.rb on_worker_boot do require redis Redis.current Redis.new( url: redis://:your_passwordredis-sentinel.example.com:26379/0, pool_size: 32, pool_timeout: 5 ) end这样Web进程最多32连接Sidekiq最多16连接总计48连接资源利用率提升5倍以上。我们用redis-cli CLIENT LIST | grep addr | wc -l监控连接数稳定在52含4个管理连接再无波动。5. 从0到1部署实录NginxPumaRDSRedis Sentinel的完整配置清单理论讲完现在给你一份可直接复制粘贴的生产环境部署清单。这套方案已在3个日活10万的Discourse站点稳定运行18个月所有配置均经过压测验证。5.1 网络拓扑与安全组规则首先明确各组件网络位置Web服务器集群2台EC2c5.4xlarge16核32G部署Discourse应用Load BalancerAWS ALB公网IP监听443端口Managed DatabaseAWS RDS PostgreSQL 14.5Multi-AZdb.m5.2xlarge8核32GRedis Sentinel集群3台t3.xlarge4核16GEC2部署Redis 7.0.12 Sentinel对象存储AWS S3用于附件、头像、备份。安全组Security Group必须严格限制ALB安全组入站开放443出站全放行Web服务器安全组入站仅允许ALB安全组访问443出站仅允许RDS安全组5432、Redis Sentinel安全组26379、S3 VPC EndpointRDS安全组入站仅允许Web服务器安全组访问5432Redis Sentinel安全组入站仅允许Web服务器安全组访问26379且Sentinel节点间26379端口互通。提示绝对禁止将RDS或Redis暴露在公网所有连接必须走内网VPC。我们曾因RDS安全组误配导致数据库被暴力破解扫描虽未失窃但CPU被打满引发雪崩。5.2 Discourse核心配置文件详解containers/app.ymlDocker Compose配置# 以下为关键配置节选非完整文件 env: # 数据库连接 DISCOURSE_DB_HOST: your-rds-cluster.cluster-xxxxxx.us-east-1.rds.amazonaws.com DISCOURSE_DB_PORT: 5432 DISCOURSE_DB_NAME: discourse DISCOURSE_DB_USERNAME: discourse DISCOURSE_DB_PASSWORD: your_strong_password # Redis连接指向Sentinel VIP DISCOURSE_REDIS_HOST: redis-sentinel.internal DISCOURSE_REDIS_PORT: 26379 DISCOURSE_REDIS_PASSWORD: your_redis_password DISCOURSE_REDIS_DATABASE: 0 # 外部服务 DISCOURSE_SMTP_ADDRESS: email-smtp.us-east-1.amazonaws.com DISCOURSE_S3_REGION: us-east-1 DISCOURSE_S3_ACCESS_KEY_ID: YOUR_S3_KEY DISCOURSE_S3_SECRET_ACCESS_KEY: YOUR_S3_SECRET DISCOURSE_S3_BUCKET: discourse-attachments # 性能调优 PUMA_THREADS: 8:32 # 最小8线程最大32线程 PUMA_WORKERS: 8 # 启动8个Puma Worker UNICORN_WORKERS: 0 # 禁用Unicorn只用Puma volumes: - volume: host: /var/discourse/shared/standalone guest: /shared - volume: host: /var/discourse/shared/standalone/log/var-log guest: /var/log hooks: after_code: - exec: cd: $home/plugins cmd: - git clone https://github.com/discourse/docker_manager.git - git clone https://github.com/mozilla/discourse-saml.git after_web_config: - replace: filename: /etc/nginx/conf.d/discourse.conf from: /gzip_types.*;/g to: | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xmlrss text/javascript application/vnd.apijson; # 强制开启Brotli压缩需Nginx编译时加入--with-http_brotli_module brotli on; brotli_comp_level 6; brotli_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xmlrss text/javascript application/vnd.apijson; run: - exec: echo Starting with up to $WEB_CONCURRENCY processes - exec: /bin/bash -c cd /var/www/discourse bundle exec rake assets:precompileconfig/database.yml手动覆盖启用连接池production: adapter: postgresql encoding: unicode database: discourse pool: 32 # 每个Puma Worker最多32个连接 username: discourse password: your_strong_password host: your-rds-cluster.cluster-xxxxxx.us-east-1.rds.amazonaws.com port: 5432 # 关键启用连接池中间件 variables: statement_timeout: 10000 # RDS Proxy或PgBouncer连接串若使用 # host: your-rds-proxy.proxy-xxxxxx.us-east-1.rds.amazonaws.comconfig/redis.yml启用Sentinelproduction: url: redis://:your_redis_passwordredis-sentinel.internal:26379/0 sentinels: - host: redis-sentinel-1.internal port: 26379 - host: redis-sentinel-2.internal port: 26379 - host: redis-sentinel-3.internal port: 26379 role: master # 连接池大小必须小于Puma线程数 pool_size: 32 pool_timeout: 55.3 验证与压测脚本部署完成后必须执行三重验证第一重基础连通性# 在Web服务器上执行 # 1. 测试DB连通 psql hostyour-rds-cluster.cluster-xxxxxx.us-east-1.rds.amazonaws.com dbnamediscourse userdiscourse passwordyour_password -c SELECT version(); # 2. 测试Redis Sentinel连通 redis-cli -h redis-sentinel.internal -p 26379 -a your_redis_password ping # 3. 测试Discourse内部健康检查 curl -k https://localhost/healthz第二重模拟真实流量我们用k6编写了Discourse专属压测脚本模拟用户登录→浏览首页→点击热门帖子→发帖→退出全流程import http from k6/http; import { sleep, check } from k6; export const options { vus: 200, // 虚拟用户数 duration: 5m, thresholds: { http_req_failed: [rate0.01], // 请求失败率低于1% http_req_duration: [p951000], // 95%请求耗时低于1秒 } }; export default function () { const baseUrl https://your-discourse-site.com; // 1. 访问首页 let res http.get(${baseUrl}/); check(res, { homepage status: (r) r.status 200 }); // 2. 获取CSRF Token const csrfToken res.headers[X-CSRF-Token]; // 3. 模拟登录需提前准备测试账号 const loginPayload JSON.stringify({ login: testuser, password: testpass, remember: true }); res http.post(${baseUrl}/session, loginPayload, { headers: { Content-Type: application/json, X-CSRF-Token: csrfToken } }); check(res, { login status: (r) r.status 200 }); sleep(1); // 4. 访问热门帖子 res http.get(${baseUrl}/t/12345); // 替换为真实帖子ID check(res, { topic view status: (r) r.status 200 }); sleep(2); }第三重故障注入测试手动停止一台Redis Sentinel节点验证自动故障转移在RDS控制台手动触发主备切换验证Discourse无感知恢复临时关闭一台Web服务器验证ALB自动剔除并重新分配流量。所有测试必须在业务低峰期进行并全程监控New Relic APM中的Web Transaction Time、Database Query Time、Redis Command Time三项核心指标。我们设定的SLA红线是P95延迟 800ms错误率 0.5%这正是这套架构交付的结果。6. 我踩过的五个最痛的坑以及为什么90%的团队会重复踩部署Discourse高可用集群不是按文档敲几行命令就完事而是要和无数个隐藏的“默认行为”搏斗。以下是我在三年运维中亲手踩过、修复过、被报警电话半夜叫醒过的五个致命坑每一个都价值至少20小时的排查时间。6.1 坑一Discourse的upload_s3_region必须与S3 Bucket区域严格一致Discourse的S3上传配置中DISCOURSE_S3_REGION参数看似只是指定API端点但它实际影响SDK的签名算法。我们最初将Bucket建在us-west-2却在app.yml中误配为us-east-1。结果现象是小文件5MB上传成功大文件5MB分片上传时第二片开始报错The request signature we calculated does not match the signature you provided.错误日志里反复出现Aws::S3::Errors::SignatureDoesNotMatch。原因在于AWS S3的V4签名算法中Region是签名字符串的组成部分。当SDK以为请求发往us-east-1却实际发到us-west-2签名自然失效。修复方法不是改SDK而是强制让S3客户端使用正确的Endpoint# containers/app.yml env: DISCOURSE_S3_REGION: us-west-2 DISCOURSE_S3_ENDPOINT: https://s3-us-west-2.amazonaws.com # 必须显式指定教训永远不要相信“自动推断”。S3的区域、Endpoint、Bucket Name三者必须在配置中显式对齐缺一不可。6.2 坑二PostgreSQL的timezone必须设为UTC否则定时任务全乱Discourse的后台Job如每日摘要、周报生成全部基于Time.current计算而Time.current依赖PostgreSQL的timezone设置。我们某次RDS升级后timezone参数被重置为US/Eastern。结果所有定时任务提前5小时执行导致凌晨3点就给用户发“早安日报”投诉电话打爆。验证方法连接RDS执行SHOW timezone;必须返回UTC。永久修复-- 在RDS参数组中修改 ALTER DATABASE discourse SET timezone TO UTC; -- 并在Discourse的database.yml中添加 # production: # variables: # timezone: UTC6.3 坑三Nginx的client_max_body_size必须大于100M否则大附件上传失败Discourse允许用户上传最大100MB的附件视频、PDF等但Nginx默认client_max_body_size是1M。现象是上传进度条走到99%卡住浏览器控制台报net::ERR_CONNECTION_RESETNginx error.log里全是client intended to send too large body。修复只需一行# /etc/nginx/conf.d/discourse.conf http { client_max_body_size 101m; # 必须大于Discourse配置的100MB }6.4 坑四Redis的maxmemory-policy必须设为allkeys-lru不能用volatile-lruDiscourse的Session、Cache、Job Queue都存Redis且大部分Key没有设置过期时间TTL。比如Session Keysession:abc123的过期由Discourse应用层控制Redis里只是永不过期。若maxmemory-policy设为volatile-lru只淘汰有TTL的Key当内存满时Redis会拒绝写入新KeyDiscourse直接报Redis::CommandError: OOM command not allowed when used memory maxmemory。正确策略是allkeys-lru让Redis在内存不足时公平淘汰所有Key中最久未使用的。6.5 坑五ALB的Idle Timeout必须大于Puma的worker_timeoutDiscourse的WebSocket连接是长连接ALB默认Idle Timeout是60秒。而Puma的worker_timeout默认是60秒。当用户挂起浏览器标签页超过60秒ALB会主动断开连接但Puma Worker并不知情继续向已关闭的Socket写数据触发Broken pipe错误最终导致整个Puma Worker崩溃。解决方案ALBIdle Timeout设为3600秒1小时Pumaworker_timeout设为3600秒DiscourseSite Setting中websocket_timeout设为3600秒。三者必须严格相等否则必出问题。最后分享一个个人体会Discourse的高可用不是“配置正确就能跑”而是“配置正确监控到位预案清晰”三位一体。我们现在的告警体系中有7个Discourse专属指标Redis连接数80%、PostgreSQL连接数90%、ALB HTTP 5xx错误率0.1%、Sidekiq队列积压1000、Puma Worker崩溃次数/小时3、S3上传失败率1%、健康检查连续失败3次。每个告警都绑定Runbook值班工程师5分钟内就能定位根因。这才是真正可落地的高可用。