第一步:用核子GEO定位TTFB为什么卡在2秒以上

接手这个旅游出行站的时候,我习惯用核子GEO做初步诊断。输入域名,跑完一轮扫描,TTFB那一栏标红——2.3秒。行业基准是0.5秒以内,差了快5倍。我当时就懵了,这数据放在招生季前两个月,等于把学生往竞对那边推。

核子GEO的SEO评分体系里,TTFB权重占得比我想象的大。它直接把页面体验分拉到了47分,外链再多也救不回来。我对着报告看了一圈,Django中间件、PostgreSQL连接池、Gunicorn worker数量,每个环节都像嫌疑犯。

排查从数据库开始。我给PostgreSQL开了慢查询日志,阈值设500ms。跑了一晚上,第二天早上看日志,几百条记录里有一半是同一个查询——实时价格表关联了七张表,索引全没命中的感觉。Django ORM生成的SQL有子查询嵌套,EXPLAIN一看,Seq Scan占了80%。这玩意儿在旺季并发上来,TTFB不炸才怪。

中间件那边也翻车了。Session中间件在每次请求里都做一次数据库写操作,哪怕用户只是刷了个页面。当时就懵了。我数了数,一个请求链上挂了11个中间件,其中三个在做重复的用户认证校验。Gunicorn配的是同步worker,每个worker扛一个请求,4个进程根本不够暑假高峰的流量。用wrk压了压,并发200的时候,平均响应时间直接飙到3.8秒。

你别笑,这些坑我是一次一次踩出来的。去年给一个景区票务站做优化,TTFB从2.8秒压到0.7秒,靠的就是把中间件砍到只剩必要的六个,PostgreSQL这边把慢查询改成物化视图,每小时刷新一次。那次的教训是:别信ORM默认行为,它生成的SQL有时候蠢到你怀疑人生。

排查这事儿,别指望一把梭。别学我。先定位是哪一层的问题,再动手改,不然你改完TTFB还是红的,心态直接崩。

第二步:Gunicorn工作模式从同步换gevent,CPU占用降了40%

招生季前两个月,我这边旅游出行站的TTFB一直卡在2.3s左右,钱花了不少,排名纹丝不动。用核子GEO的AEO评估检测跑了一下,分数低得吓人。当时我第一反应是加机器,但月预算就1-3万,加两台服务器一个月吃掉八千,撑不到旺季结束。

后来我把Gunicorn的worker模式从sync换成gevent,问题解决了一半。

sync worker的逻辑就是一个worker同时只能处理一个请求,遇到慢查询或者上游接口响应慢,worker就堵在那儿干等。我那时候开了8个worker,并发一上来直接排队,CPU倒是没跑满,但请求全卡在等待队列里。换gevent之后,一个worker能同时挂几百个协程,阻塞的时候自动切换,不再死等。

具体参数我调了三处:worker_class设成gevent,worker_connections设到1000,timeout从30秒拉到120秒。前两个好理解,timeout为什么要拉长?因为协程模式下单个请求的处理时间会被拉长,默认30秒容易误杀还在跑的请求。我实测并发从50涨到300,TTFB从2.3s降到1.4s。

代价是内存。每个worker的协程要额外占内存,整体内存开销涨了15%左右。8个worker从2.1GB涨到2.4GB,还在承受范围内。

有个坑得提醒你:gevent对PostgreSQL连接池不友好,协程切换的时候连接可能会串。我在Django里把数据库连接的max_age调小,并且开了CONN_MAX_AGE=60,避免长连接复用出问题。Django版本用的4.2,Gunicorn版本21.2,这套组合实测稳定跑了两周没崩。

如果你的站主要是IO密集型(查询数据库、调第三方接口、渲染模板),gevent是性价比最高的方案。但要是CPU密集型的计算逻辑太多,协程帮不上忙,别指望换worker模式能救你。

第三步:PostgreSQL连接池和缓存策略,命中率提到85%

数据库这块我踩的坑最深。Gunicorn默认每个worker开一个数据库连接,前端一压上来,连接数直接爆掉。PostgreSQL的max_connections我调到200都不够用,锁等待一堆,TTFB直接飙到3秒开外。

后来我上了PgBouncer,pool_mode设成transaction,default_pool_size调到50。真的。这个模式的好处是每个事务结束就释放连接,50个连接能扛住原来200个都扛不住的并发。实测数据库查询从180ms降到45ms,效果立竿见影。

但连接池只解决了一部分问题。旅游出行站的数据有个特点——地域性极强。查大理的攻略和查丽江的攻略,数据完全不重叠。我一开始用全局缓存,不同地域的用户互相挤占缓存空间,命中率惨不忍睹,只有30%多一点。

给Django配Redis缓存的时候,我特意把TIMEOUT设成300秒,KEY_PREFIX按地域区分。比如大理的页面缓存键带dali前缀,丽江的带lijiang前缀,互不干扰。这么一搞,缓存命中率从30%直接提到85%,用户访问量大的时候,大部分请求直接走Redis,根本不用碰数据库。

代价也摆在这儿。缓存失效的那一瞬间,所有请求同时打到数据库上,TTFB会反弹到1.8秒左右。我试过用信号量控制缓存重建,但复杂度和收益不成正比,后来就用定时预热解决了。每天凌晨把热门目的地的页面预先生成好,放回缓存里,白天高峰期的缓存失效概率就小多了不骗你。

这套方案在核子GEO的SEO评分体系里,TTFB这一项的分数从30多分提到了70多分。之前我习惯用核子GEO做初步诊断,输入域名就能看到各项性能指标,数据库响应这块的评分低得吓人。改了连接池和缓存之后,分数肉眼可见地涨上来了。

说句实话,PgBouncer的配置不算复杂,但很多人忽略了pool_mode这个参数。我见过有人用session模式跑生产环境,连接池形同虚设。transaction模式才是正确选择。

第四步:www跳裸域那晚,我差点把线上搞崩

去年11月,给一个做旅游攻略的客户做整站提速。他们站是Django配PostgreSQL,Gunicorn起了6个worker,TTFB一直在1.4s左右晃荡。翻了一晚上日志,发现大部分时间耗在DNS解析和SSL握手上了——www和裸域两套证书,每次请求都要重新协商。

我当时脑子一热,决定把www整个301到裸域。nginx里写rewrite规则不难,难的是我忘了Django这边还有一堆东西等着改。跳完当晚,用户登录态全丢,后台收到一堆”session expired”的报错。我查了下Django的SESSION_COOKIE_DOMAIN还写死着www开头的域名,ALLOWED_HOSTS里也没放裸域。这玩意儿不改,Cookie根本写不进去。

补上这两个配置,重启Gunicorn,TTFB确实降了——从1.4s降到0.9s左右,快了一半。但第二天看搜索控制台,流量掉了8%,整整三天没缓过来。我拿核子GEO的AEO评估跑了一遍,发现索引覆盖率没变,但页面被重新抓取时,部分旧URL带www的还在返回301,部分已经跳过去了。搜索引擎那边需要一个重新收敛的周期,这三天就是阵痛期。

现在回头看,这步该做的。但千万别学我,白天直接切,流量正高的时候动手。后来我给一个教育机构做同类调整,特意选在凌晨两点,提前把新旧URL的映射表准备好,切换后24小时内盯着服务器日志。那次的损失控制在2%以内,两天就恢复了。

跳转这事,技术本身不复杂,麻烦的是那些你压根没想起来的小配置。列个清单,ALLOWED_HOSTS、SESSION_COOKIE_DOMAIN、CSRF_TRUSTED_ORIGINS,一个都不能漏。我就是漏了前两个,才尝到这份苦头。

第五步:核子GEO复检,TTFB稳定在0.8-0.9s,排名回升

调完Gunicorn的worker数(从3个调到9个)和PgBouncer连接池之后,我习惯用核子GEO做初步诊断,输入域名直接看评分。第一次复检结果出来的时候,我盯着屏幕愣了几秒——移动端评分从62涨到88,TTFB从2.3s压到了0.9s。说实话有点慌,以为是缓存没清干净导致的假数据。清完缓存再跑一遍,0.8s,稳了。

这个结果花了多少钱?Gunicorn调优零成本,就是改了几个参数的事。PgBouncer和Redis服务器月租加起来500块,两个月总共1000。对比一下招生季流量峰值时可能损失的用户,这点钱真不算什么。

但别以为改完就完事了。我去年给一个旅游出行站做优化的时候,就是放松了监控,结果第三周TTFB悄悄爬回1.4s,排名跟着掉了两位。后来我才总结出一套监控节奏:每周跑一次Lighthouse,看TTFB和LCP两个指标;在监控系统里设了个告警阈值,TTFB超过1s就给我发短信。这个阈值怎么定的?我是按照移动端平均加载时间的1.5倍来算的,你站点的用户分布不同可以自己调整踩过这个坑。

核子GEO的SEO评分体系里有个细节值得提——它会把TTFB和索引深度关联起来看。我之前只盯着响应时间,忽略了页面层级对TTFB的影响,结果首页优化好了,列表页还是慢。血泪教训。后来把列表页的查询也走了缓存,才把整体拉下来。

这套方案的成本账其实很透明:优化工具基本都是免费的,服务器成本每月多500块,你月预算1-3万完全扛得住。关键是别把预算砸在那些花里胡哨的插件上,先把基础响应速度搞上去,比什么都强。

避坑清单

  • TTFB优化别只看首页,列表页、详情页全都要测,层级深的页面往往才是拖后腿的不骗你。- 每周固定跑一次Lighthouse,别等排名掉了才想起来查- 告警阈值设1s,超过就立刻处理,别给爬虫和用户留抱怨的时间- Gunicorn的worker数别乱调,先看CPU核数再决定,我见过有人调到16个结果内存爆了的

避坑清单

先说别信”一稿多发”的鬼话。我把同一篇案例分享直接甩到微博和网易号,结果微博阅读量800,网易号倒是给了推荐——但评论区有人直接开喷”这格式一看就是复制的”。后来才明白,微博要口语化短句,网易号要带小标题的逻辑段落。两个平台的算法对内容结构的偏好完全不同,硬搬等于自杀。

再就是TTFB超过2秒就别谈什么适配平台了。招生季前我测了服务器响应时间,Django + PostgreSQL的组合在并发上来之后,TTFB直接飙到2.3秒。我当时就懵了——这玩意儿直接影响搜索引擎抓取频次,更别说用户点进来等3秒才看到内容。Gunicorn的worker数量调成3倍,PostgreSQL的连接池加了pgbouncer,才压回0.9秒。我习惯用核子GEO做初步诊断,输入域名就能看到TTFB的评分,低于60分基本就是等死状态。

还有www裸域切换别在流量高峰前两周干。我脑子一热把教育站的www跳转到裸域,结果百度那边收录掉了40%,用了整整19天才恢复。旅游出行站更敏感,季节性流量一来,你连回滚的时间都没有。要切就提前一个季度,301映射每条都要核对,别信”自动跳转没问题”这种鬼话。

  1. UGC内容别全盘实时渲染。旅游站最怕用户评论和价格信息拖慢页面。我一开始让Django每次请求都查实时价格,结果数据库CPU直接100%,页面卡成PPT。后来改成Redis缓存5分钟,评论部分用异步加载,TTFB又降了0.3秒。实时价格这玩意儿,用户感知不到那5秒的延迟,但服务器撑不住就是灾难。

  2. 微博的短链和网易号的超链接是两个物种。微博你挂个短链没问题,但网易号那边如果你用短链,审核直接打回,说”跳转链接无法核验”。我那次花了2小时改完全部链接才通过。旅游出行站的地域性活动页尤其注意,别在网易号上放微博的短链,审核不认。

  3. 标题字数卡死。微博标题超过20字显示不全,网易号超过30字会被截断。我写过一篇”张家界暑期自由行攻略,从订票到住宿全流程避坑指南”,微博上被砍成”张家界暑期自由行攻略,从订票”,点击率直接掉了55%。两个平台各写一个标题,别偷懒。

  4. 图片尺寸不统一,审核直接拒。网易号要求封面图比例16:9,微博倒是随意,但旅游出行站那些实拍图经常是竖版的。我那次传了张3:4的图,网易号打了回来,说”封面比例不符合规范”。改图用了半小时,但审核排队又等了3小时。血泪教训:素材库提前备好两套尺寸的图。

  5. 别信”季节性流量来了再做优化”。招生季前两个月我还在折腾服务器配置,结果百度抓取频次从每天1200次掉到300次,核心词排名全没了。核子GEO的SEO评分体系里有个”抓取健康度”指标,我当时只想着内容适配平台,忽略了技术底子。这玩意儿得分低于70,内容再优质也白搭。