打开浏览器,输入域名,敲下回车,页面出来——这句轻描淡写的话背后,其实就是整个全栈部署体系的缩影。我最近刚把一个全栈项目从零部署上线,部署完顺手写了篇笔记,越写越觉得整个过程像极了带用户做一次城市观光:DNS 是查地图,CDN 是接驳快线,Nginx 是景区大门,后端应用是核心展馆,数据库和缓存是后勤仓库,浏览器渲染则是游客最终看到的风景。这篇文章就把这条链路一截一截拆开看,搞懂了它,你就搞懂了全栈部署中最常被忽略的部分:用户请求到底经过了哪些服务,每一层又该怎么配置、怎么排查。
1. 出发之前:从域名到 IP,DNS 是整个旅程的地图
1.1 用户敲下回车,第一件事不是发请求,而是“查地图”
用户在浏览器地址栏输入https://example.com然后回车,浏览器做的第一件事并不是建立连接,而是先搞清楚这个域名对应的服务器 IP 是什么。这一步专业叫法是 DNS 解析,放在城市观光的比喻里,就是游客在出发前打开地图查景点位置。
整个解析过程是一级一级向上问的,查完就缓存,不会每次都从根节点开始。实际顺序大致是这样的:
- 浏览器缓存:Chrome、Firefox 这些浏览器自己在内存里保留一份域名到 IP 的映射,命中就直接发请求。
- 操作系统缓存:如果浏览器没命中,就查系统 hosts 文件和系统 DNS 缓存。这个可以在命令行用
ipconfig /flushdns(Windows)或者sudo dscacheutil -flushcache(macOS)清掉。 - 路由器缓存:再没命中,请求会到家用路由器或公司网关,由它继续转发。
- ISP 的本地 DNS 服务器:这是最常见的递归 DNS,运营商替你去问全球的根服务器。
- 根域名服务器、顶级域名服务器、权威域名服务器:一层层往下问,最后拿到
example.com的权威 NS 记录,再由权威服务器返回真正的 A 记录或 CNAME 记录。
从部署者视角看,最需要关心的其实是最后一段:你的域名解析记录配置得对不对、TTL 设置得合不合理。我见过太多线上事故,就是因为改了解析记录,但 TTL 设成了 24 小时,导致用户长时间访问到旧 IP。做全栈部署时,A 记录、CNAME、AAAA 这几类要分清楚:
- A 记录:域名直接指向 IPv4 地址,比如
api.example.com → 123.45.67.89。 - CNAME:域名指向另一个域名,典型场景就是接 CDN,把
www.example.comCNAME 到 CDN 厂商给你分配的域名上。好处是 CDN 节点 IP 变了,你不用跟着改。 - AAAA 记录:IPv6 地址的映射,现在国内云厂商基本都支持,看业务需求配不配置。
1.2 防止迷路:TTL 与缓存策略,改解析前必须想清楚
TTL 是 DNS 记录的生存时间,单位是秒。它本质上是告诉各级缓存“这条记录你可以存多久”。TTL 越大,DNS 查询越快,因为大家都有缓存;但缺点是变更解析记录后生效慢。TTL 太小,DNS 查询频率会升高,不过对普通中小站点来说压力可以忽略。
我个人习惯是:平时把主域名的 TTL 设为 600 秒(10 分钟),需要变更 IP 前 24~48 小时先降成 60 秒,等变更完成后观察一段时间再调回去。这样既保证大部分用户能快速命中缓存,又给变更留了足够的缓冲期。
还要顺手验证一下解析到底对不对。在部署机器上我一般用这两条命令:
# 查看最终解析到的 IP dig +short example.com # 用指定 DNS 服务器查询,判断是否被本地缓存污染 nslookup example.com 8.8.8.8如果你发现dig出来的 IP 和你预期不一致,先别慌,大概率是本地 DNS 缓存或运营商 DNS 缓存的问题,让用户清缓存或者等 TTL 过期即可。如果用的是云厂商的 DNS 解析服务,也可以在控制台观察解析量曲线,确认缓存命中情况。
注意:接 CDN 时,尽量不要把域名直接用 A 记录指到源站 IP,否则就绕过了 CDN 节点,流量全部回源,既享受不到 CDN 缓存,还会暴露源站 IP,被攻击时连兜底都没有。
2. 城市大门:CDN、负载均衡、反向代理,谁在用户前面挡着
2.1 数据包跨过网络之后,第一站是“城市大门”
拿到 IP 地址之后,浏览器会发起 TCP 连接,如果是 HTTPS,还要先完成 TLS 握手。在这个过程中,数据包经过层层路由转发,最终抵达你的服务器——但注意,大多数线上全栈部署里,用户请求直接打到源站服务器的比例其实很低,前面通常还站着几道门:CDN、负载均衡器和反向代理。
这三者的职责经常被混着说,实际分工是这样的:
- CDN 主要管“静态资源就近分发”和“基础网络加速”,比如图片、CSS、JS、字体这些不经常变化的内容,CDN 会缓存在离用户最近的边缘节点上。
- 负载均衡器主要管“流量分发”,把大量用户请求分摊到多台后端服务器上,避免单台机器被打爆。有云厂商的 LB,也有自建的 LVS、HAProxy。
- 反向代理则是在应用层做路由、限流、缓存、HTTPS 卸载等事情,最常见的就是 Nginx。
在全栈部署里,大多数中小项目不会真的把三样都独立架起来,常见做法是:前面挂 CDN,CDN 回源到一台 Nginx(这台 Nginx 同时充当反代和负载均衡),Nginx 再把动态请求转发给后端的应用服务,静态资源由 Nginx 直接返回或回源到 CDN。
2.2 Nginx 作为反向代理:细节决定体验
Nginx 的配置看起来简单,真正坑人的都在细节里。我先给一段最基础的配置,然后逐个解释关键点。
upstream backend { server 127.0.0.1:8080 max_fails=3 fail_timeout=10s; server 127.0.0.1:8081 max_fails=3 fail_timeout=10s; } server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/nginx/ssl/example.com.pem; ssl_certificate_key /etc/nginx/ssl/example.com.key; gzip on; gzip_types text/plain text/css application/json application/javascript application/xml; gzip_min_length 1024; location /api/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 30s; } location /static/ { alias /srv/www/static/; expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; } location / { proxy_pass http://backend; } }这里有三个点几乎是每次部署都会踩的:
第一个是proxy_http_version 1.1。默认 Nginx 向上游发的是 HTTP/1.0,而 HTTP/1.0 不支持 keep-alive,于是每次请求都要新建后端连接。如果后端是 Node.js 或 Spring Boot,连接创建的开销会累积得相当可观。把这行加上,并配合后端的 keep-alive 配置,长连接建立后性能立刻就上来了。
第二个是X-Forwarded-Proto。后端如果做 HTTPS 重定向或者生成某些绝对链接,需要知道用户原始请求是 HTTP 还是 HTTPS。不传这个头会导致明明页面是 HTTPS,应用却生成了 HTTP 链接,浏览器直接拦截。
第三个是超时时间。proxy_read_timeout指的是 Nginx 等待后端响应的最长时间,不是整个请求的最长时间。后端如果有个接口在批量导出数据,需要跑 1 分钟,而你这里设了 30 秒,那请求就会被 Nginx 掐断,用户收到 504。这类超时一定要按业务接口的最长耗时来调,不要天真地以为设个 60 秒就万事大吉,因为有些慢查询或者第三方接口调用,60 秒都不一定够。
2.3 负载均衡的健康检查:别让“哑巴”服务器拖垮全局
很多人以为负载均衡就是把请求随机分发到几台服务器上,其实没那么简单。如果某个后端服务已经挂了或者卡死了,负载均衡器还傻乎乎地把请求发过去,就会出现时不时的 502/504,时好时坏,特别难排查。
所以配置 upstream 的时候一定要加上健康检查机制。Nginx 开源版自带的健康检查比较基础,主要靠max_fails和fail_timeout这两个参数:
max_fails=3:允许连续失败 3 次。fail_timeout=10s:在 10 秒内失败达到 3 次,就标记该节点不可用,接下来的 10 秒内不再往这个节点分发请求。
这只是被动健康检查,也就是等请求真的失败了才摘除节点。如果要主动探测,比如每 5 秒检查一次/health接口是否返回 200,就需要用 Nginx Plus 或者换用 OpenResty、云负载均衡。我的经验是:后端应用无论如何都要实现一个/health接口,并且这个接口不能只是简单返回 200,最好能顺带检查一下数据库连接池、Redis 是否正常。这样无论是 Nginx 健康检查还是容器探针,都能拿到真实的服务健康状态。
3. 核心展馆:后端应用服务器如何处理用户请求
3.1 从 Nginx 到应用服务:路由、中间件、控制器在干什么
Nginx 把请求转发到后端的应用服务后,后端开始真正处理业务。这里不管你是用 Node.js 的 Express/NestJS、Python 的 Django/FastAPI,还是 Java 的 Spring Boot,处理流程本质都一样:请求先进中间件,再做路由匹配,然后进控制器,最后调用业务逻辑和数据库。
在全栈部署的语境里,这一层最常被忽视的是应用服务的生命周期管理和环境配置。很多人本地跑没问题,一上服务器就各种报错,十有八九是因为环境变量、配置文件和运行环境不一致。比如本地开发时数据库地址是localhost:3306,服务器上变成了内网 IP;本地静态文件路径和部署机上不一致;依赖版本号没锁定,导致构建结果和本地不一样。
解决这类问题的最佳实践是用环境变量统一管理配置,不要写死在代码里。拿 Node.js 项目举例,我习惯用.env文件配合dotenv工具,服务器部署时再通过容器编排或 CI/CD 平台注入真实的环境变量。关键的一点是:.env文件绝对不能提交到 git 仓库,里面可能含数据库密码、密钥等敏感信息。
3.2 有状态还是无状态:Session 和 JWT 的部署考量
后端处理请求时,还需要回答一个问题:用户是谁?这个状态信息存哪里,直接决定全栈部署的架构复杂度。
传统做法是 Session,服务端存一份用户会话数据,客户端只持有一个 sessionId。部署到多台机器时,问题就来了:用户请求第一次打到 A 机器,会话存在 A 上,下一次请求被负载均衡分到 B 机器,B 上没有这个会话,用户就被判定为未登录。
解决办法有三个:
- 会话粘滞(sticky session):负载均衡保证同一个 IP 的请求总是发给同一台后端,配置简单,但不够优雅,有机器下线时会话还是会失效。
- Session 存中间件:比如把 Session 数据存到 Redis,所有后端从同一个 Redis 读,这样任何一台机器都能处理任意用户的请求,这是现阶段最主流的方案。
- 无状态化:用 JWT 之类的 token,把用户信息加密后直接发给客户端,后端不存任何状态,每次请求拿 token 校验。好处是天然支持水平扩展,坏处是 token 吊销不方便。
我个人在中小全栈项目里,推荐“Redis 存 Session 或加解密 token 二选一”,不要用 sticky session 凑合。因为全栈部署一旦上了规模,迟早要做水平扩展,到时候再改状态存储方式,付出的成本比现在大得多。
3.3 容器化部署:Docker Compose 是最合适的上手姿势
到了部署应用服务环节,容器化已经算标配了。我个人的经验是,对中小型全栈项目,Docker Compose 完全够用,没必要一上来就上 Kubernetes。Kubernetes 解决的是几十上百个服务的编排调度问题,为一个单体应用加 Nginx 加数据库,Compose 足够轻量、足够快。
下面是一份比较典型的全栈部署docker-compose.yml骨架:
version: "3.8" services: app: build: ./backend restart: always environment: - NODE_ENV=production - DB_HOST=mysql - DB_PORT=3306 - DB_USER=app_user - DB_PASSWORD=change_me - REDIS_HOST=redis - REDIS_PORT=6379 depends_on: - mysql - redis expose: - "8080" nginx: image: nginx:stable-alpine restart: always ports: - "80:80" - "443:443" volumes: - ./nginx/conf.d:/etc/nginx/conf.d - ./ssl:/etc/nginx/ssl - ./frontend_dist:/usr/share/nginx/html depends_on: - app mysql: image: mysql:8.0 restart: always environment: - MYSQL_ROOT_PASSWORD=change_me_root - MYSQL_DATABASE=app_db - MYSQL_USER=app_user - MYSQL_PASSWORD=change_me volumes: - mysql_data:/var/lib/mysql redis: image: redis:7-alpine restart: always command: redis-server --appendonly yes --requirepass change_me_redis volumes: mysql_data:有几个细节要特别提醒:
depends_on只保证容器启动顺序,不保证服务可用。MySQL 容器启动了不代表 MySQL 已经能接受连接,所以应用代码里要做重试机制,或者用healthcheck来控制。- 数据库密码、Redis 密码这种敏感信息,在真实生产环境里要从环境变量或密钥管理服务里读,不要直接写在 compose 文件里。我现在搭个人项目图省事,也会先用 environment 顶着,但心里清楚这只能用于测试环境。
- 前端构建产物通过 volume 挂载进 Nginx,或者直接构建进镜像都行。前者上线简单,后者环境一致性更好,个人项目前者更快。
4. 城市后勤:数据库、缓存与存储的精细配合
4.1 数据库连接池:为什么不能每次现连
请求走到业务逻辑层,开始读写数据库。如果应用是直接用数据库驱动每次现连数据库,那流量稍微上来一点,数据库就会先撑不住。因为建立数据库连接需要 TCP 握手、认证、分配资源,这是高代价操作,且数据库端连接数是有限的。
MySQL 默认的最大连接数是 151,如果应用没有连接池,每个并发请求占用一个连接,高峰期几百个请求打过来,连接数直接打满,后续请求全部排队或报错。连接池的作用就好比城市里的共享单车调度站:一批连接被提前建立并持续复用,应用用完归还,而不是用完就销毁。
无论是 Java 的 HikariCP、Python 的 SQLAlchemy 连接池,还是 Node.js 的mysql2/promise配连接池,配置要点都差不多:
- 初始连接数不要太大,5~10 足够。
- 最大连接数要结合数据库上限和应用并发量来算。比如数据库上限 151,应用要留一些给管理操作,那连接池最大可以设到 100。
- 连接空闲超时(idleTimeout)不要太长,MySQL 默认
wait_timeout是 8 小时,连接池空闲超时如果设得比它还长,可能拿到的是已断开的死连接。
一个我在部署中踩过的典型坑是:应用连接池最大连接数设置得比数据库最大连接数还大,数据库直接拒绝新连接,应用启动后没跑几分钟就开始抛Too many connections的错。遇到这种问题,第一反应应该是去查连接池配置,而不是盲目调大数据库的max_connections。调大数据库连接数只是缓解症状,真正的问题是没有限制应用侧的连接占用。
4.2 缓存层:Redis 不是装了就行,得知道什么时候用
缓存的作用是减少数据库压力。在典型的全栈部署里,缓存常见的应用场景有:
- 热点数据缓存:比如首页 banner、配置信息、商品详情,这些读多写少的接口,可以先查 Redis,没命中再查数据库并回填。
- 接口级缓存:把整个响应结果缓存,适合数据一致性要求不高的接口。
- 分布式锁:多个应用实例并发处理同一订单时,通过 Redis 做互斥。
- 会话存储:前面提到的 Session 放 Redis。
但缓存也会带来三个经典问题,部署前最好心里有数:
- 缓存穿透:查询一个绝对不存在的数据,缓存里没有,数据库里也没有,每次请求都打到数据库。解决思路是布隆过滤器,或者把空值也缓存一段时间。
- 缓存击穿:某个热点 key 突然失效,瞬间大量请求直接打到数据库。解决思路是热点数据不设过期时间,或加互斥锁回源。
- 缓存雪崩:大面积 key 同时失效,数据库压力瞬间飙升。解决思路是过期时间加随机抖动。
4.3 主从复制与读写分离:中小项目也能做的数据库高可用
数据库单点是全栈部署里最大的隐患。很多开发者把项目部署上线后,默认 MySQL 只在台机器上跑,数据没有备份。一旦磁盘损坏或误删数据,整个项目就回到解放前。
最划算的提升可用性的方案,是配一主一从。主库负责写入,从库负责读取,通过 MySQL 自带的 Replication(复制)机制同步数据。应用层面配置两个数据源,读写分离。读写分离在 ORM 层面大多有现成方案,比如 Django 配DATABASE_ROUTERS、Spring 配两个数据源,Node.js 的 Sequelize 也可以用replication配置。
配置主从时,最容易被忽略的有两点:一是必须用独立账号做同步,不要用 root 账号;二是要监控从库的复制延迟。主从复制延迟会导致用户写入后读不到数据,这在用户体验上非常致命。部署好后,用SHOW SLAVE STATUS\G查看Seconds_Behind_Master,这个值如果持续增长,就得检查网络带宽或主库写入压力了。
5. 最后一公里:响应返回与浏览器渲染
5.1 服务器是怎么把页面“递”给用户的
后端处理完请求,返回给浏览器的是一份 HTTP 响应,包含状态码、响应头、响应体三部分。很多人写接口时只关心响应体,忽略了响应头,但响应头恰恰是全栈部署中直接影响用户体验和性能的部分。
几个常见的响应头,每个部署者都应该认识:
Content-Type:告诉浏览器返回的内容类型。text/html、application/json、image/png各不一样,设置错了浏览器会把内容当纯文本显示。Cache-Control:控制浏览器和 CDN 怎么缓存。对于静态资源,我会设置public, max-age=604800, immutable;对于接口,通常设置no-store,避免缓存导致数据不更新。ETag:给响应内容打一个指纹,浏览器再次请求时带上If-None-Match,服务器判断内容没变就返回 304,告诉浏览器继续用本地缓存。这个机制能大幅减少不必要的响应体传输。Content-Security-Policy:浏览器端安全策略,限制页面能加载哪些外部资源,能有效防 XSS。
在 Nginx 里配置这些响应头很容易,但要记住:如果前面挂了 CDN,CDN 会依据源站的Cache-Control决定边缘节点缓存多久。所以一定要先理清哪些资源是“应该被缓存的”,哪些是“绝对不能被缓存的”,千万不要图省事全部设成max-age=3600,接口被 CDN 缓存后,用户看到的永远是旧数据。
5.2 浏览器渲染管线:为什么会有白屏
浏览器拿到 HTML 之后,会开始解析并构建页面,这个过程叫渲染管线。大致分五步:
- 解析 HTML,构建 DOM 树。
- 解析 CSS,构建 CSSOM 树。
- 把 DOM 和 CSSOM 合并成渲染树。
- 计算每个节点的位置和尺寸,这叫布局或 reflow。
- 把渲染树绘制到屏幕上,这叫 paint。
在实际部署中,这段渲染管线对应到三个常见指标,也是我在前端部署时最关注的三个:
- FCP(First Contentful Paint):页面首次绘制出内容的时间。
- LCP(Largest Contentful Paint):页面最大内容绘制完成的时间,代表用户看到主要内容的速度。
- CLS(Cumulative Layout Shift):页面元素是否跳动,在用户体验上影响很大。
如果部署上线后发现 FCP 和 LCP 指标不好,先不要急着甩锅给后端接口慢,按这个顺序排查:
- 检查静态资源是否被压缩。Nginx 开启 gzip 或 Brotli 后,JS/CSS 体积能缩小 60% 以上。
- 检查静态资源是否走了 CDN。用户离源站上千公里,下载一个 2MB 的 JS 体验会很差。
- 检查是否有同步加载的大体积 JS。最好用
defer或async,避免阻塞 DOM 解析。 - 检查图片是否做了尺寸压缩和懒加载。一个原本 2MB 的 PNG 压成 WebP,可能只有 200KB。
5.3 静态资源优化:Nginx 和 CDN 的常见设置
针对前端构建出来的静态资源,我习惯在 Nginx 里做这么几件事:
location /static/ { alias /srv/www/static/; gzip_static on; expires 7d; add_header Cache-Control "public, max-age=604800, immutable"; }gzip_static on:如果构建时已经生成了.gz文件,Nginx 直接返回压缩后的文件,省去每次动态压缩。Vite、Webpack 构建时可以通过插件预压缩。expires 7d配合immutable:因为前端构建产物文件名通常带 hash(比如app.a1b2c3.js),内容变了文件名就会变,所以可以放心让浏览器和 CDN 缓存很久。- 如果发现线上更新后用户还是加载旧版本,不要随意把
Cache-Control改成no-cache,那个副作用太大了。正确的做法是让入口 HTML 不缓存(no-cache),带 hash 的 JS/CSS 长缓存。这也是前端部署“版本更新”的黄金法则。
6. 全景串联:一次完整的“城市观光”路线
把前面所有环节串起来,一次完整的用户访问过程是这样的:
- 用户在浏览器输入
https://example.com,浏览器查本地 DNS 缓存。 - 缓存未命中,递归查询 DNS,最终从权威服务器拿到 IP,这个 IP 是 CDN 节点的 IP。
- 浏览器向 CDN 节点发起 HTTPS 请求,TLS 握手。
- CDN 节点判断请求内容是否可缓存。如果命中了缓存,直接返回;如果没命中,回源到源站的 Nginx。
- Nginx 先判断是不是静态资源,是的话直接返回;动态请求转发给后端应用服务。
- 应用服务读取 Redis 缓存,未命中再查数据库,拼装响应返回给 Nginx。
- Nginx 把响应返回给 CDN 节点,CDN 根据响应头决定是否缓存。
- 浏览器收到 HTML,解析渲染,再发起若干静态资源请求,这些请求又走一遍 CDN。
- 页面完成绘制,用户看到内容。
对应到全栈部署的视角,这张表基本覆盖了主要环节:
| 环节 | 负责组件 | 部署时最关键的配置 | 常见故障 |
|---|---|---|---|
| 域名解析 | DNS 服务商 | A/CNAME 记录、TTL | 解析不生效、指向旧 IP |
| 网络加速 | CDN | 缓存规则、回源配置 | 缓存不更新、回源失败 |
| 入口网关 | Nginx / LB | SSL 证书、超时、转发头 | 502/504、证书过期 |
| 应用服务 | Node.js / Java / Python 等 | 环境变量、连接池、容器化 | 环境不一致、内存泄漏 |
| 状态与缓存 | Redis | 持久化、密码、淘汰策略 | 缓存穿透、雪崩 |
| 数据存储 | MySQL | 连接数、主从复制、备份 | 连接打满、主从延迟 |
| 浏览器端 | 前端构建产物 | 静态资源压缩、缓存头 | 白屏、加载慢 |
如果是从零搭一个全栈项目,我建议的最小方案是:一台云服务器 + Docker Compose(应用 + Nginx + MySQL + Redis)+ 一个域名 + 免费的 HTTPS 证书。这套组合足以跑起来一个完整的全栈项目,也足够覆盖上面链路里的每一个环节,是性价比最高的全栈部署练手方式。
7. 常见问题与排查技巧实录
7.1 访问不了、时好时坏、白屏,分别怎么查
线上部署之后遇到问题,最忌讳的是没有章法地乱试。我按现象整理了一份排查清单,每次按照这个顺序走,基本都能定位到问题。
现象一:域名打不开,浏览器提示找不到服务器
先用dig +short example.com看解析结果。如果返回空,说明域名解析有问题;如果返回的 IP 和预期不一致,可能是缓存了旧记录。然后curl -I https://example.com看能不能拿到 HTTP 响应头。如果 curl 能通但浏览器不行,检查下是不是本地 hosts 文件被改过,或者浏览器插件拦了。
现象二:页面时好时坏,刷新一下能开、再刷新就 502
这大概率是负载均衡的后端实例不稳定。先curl -I http://后端IP:端口/health直接测每台后端,找出哪台挂了或响应超时。同时看 Nginx 的 error.log,观察是否有upstream timed out或no live upstreams的报错。处理方式是修好故障实例,同时确认max_fails和fail_timeout的配置是合理的。
现象三:页面白屏,但是接口请求都正常
接口正常说明后端没大问题,问题多半在前端打包资源或渲染报错。打开浏览器 DevTools,看 Console 有没有 JS 报错,看 Network 里静态资源是否返回 200。如果 JS/CSS 请求返回 403 或 404,检查 Nginx 的alias路径是否正确,以及前端构建产物是否真的挂载到了对应目录。
现象四:页面能开但特别慢
分场景看:如果是首屏慢,大概率是静态资源没压缩或没走 CDN;如果是接口慢,优先查数据库慢查询日志和后端应用日志,看时间消耗在哪一层。也可以用curl -w "@curl-format.txt" https://example.com/api/xxx看耗时分段,重点看time_starttransfer(首字节时间)和time_total(总耗时)的差距,差距大说明响应体传输慢,差距小说明服务端处理慢。
7.2 几个容易“教做人”的全栈部署细节
最后分享几个我反复踩、后来又反复提醒别人的细节。
第一,HTTPS 证书一定要配自动续期。官方 Let's Encrypt 的证书有效期是 90 天,不要手动续,用 certbot 的定时任务或 acme.sh 做自动续期。我见过太多站点因为我忘记续期证书,用户访问时浏览器直接大红页,整个站看起来像是被劫持了。
第二,服务器时间一定要同步。印象最深的一次,是排查了半天接口签名一直失败,最后发现是服务器时间比真实时间慢了 5 分钟。全栈项目里如果涉及 JWT、HTTPS 证书、日志分析,时间不对会引发各种诡异问题。用timedatectl set-ntp true或者安装 chrony 同步时间,属于部署完必须立刻做的事。
第三,日志一定要做好滚动和大小控制。不设日志轮转的话,半年后一个app.log可能轻松顶几十 GB,把磁盘塞满。不管用什么框架,都要配上按天切分和最大大小控制。排查问题时,日志就是唯一能还原现场的证据,没有日志什么都白搭。
第四,发布窗口期内优先保证“可回滚”。全栈部署不是把代码推上去就完事,做任何变更前先确认有可回滚的镜像或版本包。我现在的习惯是,每次部署前先把当前生产环境打一个快照或镜像标签,万一新版本出问题,几分钟内能切回旧版本。这种事前准备加事后验证的习惯,比任何高深的架构设计都管用。