如何在香港服务器上实现图片存储分层架构,避免电商网站大流量访问导致商品图加载卡顿?

大促预热夜里两点,我在香港葵涌机房的冷通道里,背靠着第 3 排机柜蹲着看 IOPS 曲线。监控面板上一条紫色的线忽上忽下,像心电图一样不规律。商品详情页刚换了新模版,首屏大图从 1 张变成了 3 张,平均每个用户要拉 9~12 个变体图。营销一推送,CDN 命中掉到 60% 出头、源站直连飙升、NVMe 抖成马赛克。
运维群里有人说“要不要先关新模版?”我回了句:“别,给我 30 分钟,把热层再顶上去。”接下来就是这篇文章里你会看到的一切:我在香港机房如何把图片架构做成多层分级,从骨子里解决大流量下的商品图加载卡顿。
1. 架构目标与分层思路(L0–L3)
目标:首屏图片 P95 < 150ms(香港/华南区域),P95 < 300ms(全国),在峰值 QPS 5–8k 时保持稳定;CDN 回源带宽 ≤ 20% 链路上限;源站避免“读放大”与“抖动”。
分层设计(由外到内)
L0:CDN 边缘
多厂混用(海外/内地加速分线),回源到香港边缘网关(支持 HTTP/2、TLS1.3、0-RTT 可选)。
缓存策略:变体 URL 强缓存、源图弱缓存;命中失败再回源 L1。
L1:边缘图像网关(Nginx + imgproxy)· 热层缓存(NVMe)
负责格式自适应(AVIF/WEBP/JPEG)、尺寸变体生成与NVMe 盘上代理缓存;
LRU + 背景更新 + 失败回源。
热层目标命中率 ≥ 90%(大促 ≥ 85%)。
L2:对象存储·温层(MinIO 分布式 + HDD,LVMcache 加速)
存放原图与热门变体(按生命周期策略);
HDD 池 + NVMe LVMcache(writethrough/写回按风险切换)。
L3:冷层
MinIO 的分层策略(ILM)把低频对象迁移到EC(纠删码)更高冗余的盘组或远端副本(同城/跨机房 DR)。
关键点:热在最边缘,冷在对象层,回源始终“可预期”。图片变体“就地生成”后写回对象层,下一次边缘直接命中。
2. 硬件与网络选型(香港机房)
均以 CentOS 7(最小化安装)为基线。网络 10GbE 为主,机房内链路 MTU 9000,公网出口 MTU 1500。
| 角色 | 机型/CPU | 内存 | 存储 | 网卡 | 备注 |
|---|---|---|---|---|---|
| L1 边缘网关 ×3 | Intel Xeon Silver 4310(或 AMD 7313) | 64–128GB | NVMe Gen4 2TB×4(RAID10) | 2×10GbE | Nginx + imgproxy;本地热缓存 |
| L2 MinIO 节点 ×4 | AMD EPYC 7313P | 128GB | HDD 16TB×12 + NVMe 1.92TB×2(LVMcache) | 2×10GbE | 分布式对象存储(EC 8+4) |
| 负载均衡(可选) | 任意支持 BGP 的 x86 | 32GB | SATADOM/SSD | 2×10GbE | LVS/HAProxy/Keepalived |
| 监控/日志 | 任意 | 32GB | SSD 1TB | 2×10GbE | Prometheus + Loki/Grafana |
3. 系统与内核调优(关键摘录)
/etc/sysctl.d/99-tuning.conf:
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.ipv4.tcp_max_syn_backlog = 262144
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_congestion_control = bbr
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
fs.file-max = 10485760
vm.dirty_background_ratio = 5
vm.dirty_ratio = 20
/etc/security/limits.d/99-nofile.conf:
* soft nofile 1048576
* hard nofile 1048576
NVMe I/O 调度(边缘热层):
echo none > /sys/block/nvme0n1/queue/scheduler
echo 0 > /sys/block/nvme0n1/queue/iosched/low_latency 2>/dev/null || true
4. 对象存储(L2)部署:MinIO + LVMcache(CentOS 7)
4.1 LVMcache:用 NVMe 加速 HDD 池
以一台 L2 节点为例,HDD /dev/sd[b-m],NVMe /dev/nvme0n1:
# 1) PV/VG
pvcreate /dev/sd{b..m} /dev/nvme0n1
vgcreate vg_obj /dev/sd{b..m} /dev/nvme0n1
# 2) HDD 主卷(温层)
lvcreate -L 24T -n lv_obj vg_obj /dev/sd{b..m}
mkfs.xfs /dev/vg_obj/lv_obj
mkdir -p /data/obj
echo "/dev/vg_obj/lv_obj /data/obj xfs defaults,noatime 0 0" >> /etc/fstab
mount -a
# 3) NVMe 建 cachepool
lvcreate -L 1.8T -n lv_cachepool vg_obj /dev/nvme0n1
lvconvert --type cache-pool vg_obj/lv_cachepool
# 4) 绑定缓存(写通以稳为先,后期评估改写回)
lvconvert --type cache --cachepool vg_obj/lv_cachepool --cachemode writethrough vg_obj/lv_obj
# 查看
lvs -a -o +devices,cache_policy,cache_mode
经验:大促切前先用 writethrough 保守运行,确认停机恢复流程后再评估 writeback,否则掉电/内核崩溃后 cache 元数据恢复会让你在机房“多坐一会儿”。
4.2 MinIO(分布式,EC 8+4)
四节点,每节点 12 块 16TB HDD,共 48 盘。以 /data/obj/d{1..12} 作为导出目录:
# 用户与目录
useradd -r -s /sbin/nologin minio
chown -R minio:minio /data/obj
# systemd service
cat >/etc/systemd/system/minio.service <<'EOF'
[Unit]
Description=MinIO Object Storage
After=network.target
[Service]
User=minio
Group=minio
ExecStart=/usr/local/bin/minio server --address ":9000" --console-address ":9001" \
http://minio{1...4}/data/obj/d{1...12}
EnvironmentFile=-/etc/minio.env
LimitNOFILE=1048576
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# 环境变量(访问密钥等)
cat >/etc/minio.env <<'EOF'
MINIO_ROOT_USER=imgroot
MINIO_ROOT_PASSWORD=<强口令>
MINIO_STORAGE_CLASS_STANDARD=EC:8
MINIO_PROMETHEUS_AUTH_TYPE=public
EOF
systemctl daemon-reload
systemctl enable --now minio
创建桶与策略(在任一节点):
mc alias set local http://127.0.0.1:9000 imgroot <强口令>
mc mb local/images
mc ilm rule add --transition-days 30 --storage-class EC:12 local/images # 30 天转冷层
mc anonymous set download local/images # 仅公开读(按需调整)
5. 边缘图像网关(L1):Nginx + imgproxy(自适应与热缓存)
5.1 部署 imgproxy(S3/MinIO 后端)
docker run -d --name imgproxy --restart=always -p 127.0.0.1:8080:8080 \
-e IMGPROXY_S3_REGION=us-east-1 \
-e IMGPROXY_S3_ENDPOINT=http://minio-vip:9000 \
-e IMGPROXY_S3_ACCESS_KEY_ID=imgroot \
-e IMGPROXY_S3_SECRET_ACCESS_KEY=<强口令> \
-e IMGPROXY_USE_S3=true \
-e IMGPROXY_ENABLE_AVIF=true \
-e IMGPROXY_ENABLE_WEBP=true \
-e IMGPROXY_QUALITY=82 \
-e IMGPROXY_AUTO_ROTATE=true \
-e IMGPROXY_MAX_SRC_RESOLUTION=80 \
-e IMGPROXY_READ_TIMEOUT=5s \
-e IMGPROXY_WRITE_TIMEOUT=5s \
darthsim/imgproxy:v3
坑点:若 MinIO 证书非公信,IMGPROXY_S3_SECURE=false 或导入 CA,避免握手失败导致 5xx 抖动。
5.2 Nginx(CentOS 7)安装与核心配置
yum install -y epel-release
yum install -y nginx
缓存目录与共享内存区:
mkdir -p /data/cache/img
chown -R nginx:nginx /data/cache
cat >/etc/nginx/conf.d/cache.conf <<'EOF'
proxy_cache_path /data/cache/img levels=1:2 keys_zone=imgcache:20g
max_size=1400g inactive=14d use_temp_path=off;
map $http_accept $imgfmt {
"~*image/avif" "avif";
"~*image/webp" "webp";
default "jpeg";
}
EOF
站点配置(变体强缓存、失败回源、后台更新):
server {
listen 80 reuseport;
server_name img.example.com;
# 热层缓存控制
proxy_cache imgcache;
proxy_cache_key "$scheme://$host$uri?sz=$arg_sz&fmt=$imgfmt";
proxy_cache_valid 200 30d;
proxy_cache_valid 404 1m;
proxy_cache_lock on;
proxy_cache_min_uses 1;
proxy_cache_background_update on;
proxy_ignore_headers Set-Cookie Expires Cache-Control;
# 错误与陈旧内容
proxy_next_upstream error timeout http_500 http_502 http_503 http_504;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
# 大图分片(避免长连接占用)
slice 1m;
proxy_set_header Range $slice_range;
# 源:本机 imgproxy
location /i/ {
set $bucket "images";
# 约定:/i/<object-key>?sz=WxH
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 6s;
proxy_connect_timeout 2s;
proxy_send_timeout 6s;
# 生成变体
proxy_pass http://127.0.0.1:8080/insecure/rs:fit:{$arg_sz}/f:$imgfmt/plain/s3://$bucket$uri;
# 强缓存给 CDN/浏览器
add_header Cache-Control "public, max-age=31536000, immutable";
}
# 健康检查
location = /healthz { return 200 "ok\n"; }
access_log /var/log/nginx/img_access.log main;
}
小技巧:proxy_cache_key 把 尺寸 与 目标格式嵌入 key,避免“变体串味”。同时开启 slice 能有效提升高 RTT 环境下的大图传输效率。
6. 域名与 CDN 策略(L0)
- 域名 img.example.com 指向 CDN(多厂混用),源站设置为香港 L1 VIP。
- 强缓存变体:/i/<key>?sz=WxH,Cache-Control: immutable, max-age=31536000。
- API/原图弱缓存:max-age=300, stale-while-revalidate=600。
- CDN 回源超时 ≤ 3s,最小源连数限制,防止“惊群式”穿透。
- 国内线路优先选 直连/专线回源香港(CN2/GIA),避免绕行引起的 TTFB 波动。
7. 预热与回源保护
7.1 预热脚本(常用 SKU 变体)
#!/bin/bash
# warmup.sh
set -e
LIST=/srv/top_skus.txt # 每日导出 Top N SKU -> object key
SIZES=("400x400" "800x800" "1200x1200")
for key in $(cat $LIST); do
for sz in "${SIZES[@]}"; do
curl -s -o /dev/null "https://img.example.com/i/${key}?sz=${sz}"
done
done
Crontab(凌晨 3 点):
0 3 * * * /bin/bash /srv/warmup.sh
7.2 回源保护
- Nginx proxy_cache_lock on:同一 key 的首个 MISS 获取锁,其他请求等待缓存写入,避免放大。
- MinIO 限速与并发:MINIO_API_REQUESTS_MAX=2000(按节点能力)+ nginx upstream keepalive 控制连接重用。
8. 生命周期与冷层策略(L3)
- 30 天未访问 → 转至 EC:12 冷盘组(上文 mc ilm 示例)。
- 对于法务/溯源需要的图片,打上 LegalHold 标签,跳过 ILM。
- 可选:异地只读副本(另一个香港/新加坡机房),通过 MinIO Site Replication 开启双活/容灾。
9. 基准与压测(实测样例)
环境:香港本地压测 + 华南外网压测(电信/联通/移动各 1 条)。工具:wrk(静态变体),vegeta(混合 URL)。
| 指标 | 改造前(单层 NAS 源图) | 改造后(分层架构) |
|---|---|---|
| CDN 命中率 | 62% | 91% |
| L1 热层命中率 | — | 93%(大促 88–90%) |
| 首屏 3 图 P95(华南) | 380ms | 120ms |
| 峰值回源带宽(至 L1) | 4.5 Gbps | 1.2 Gbps |
| 峰值回源带宽(至 L2) | 3.2 Gbps | 0.4 Gbps |
| L2 盘阵平均读延迟 | 16–20ms | 4–6ms(NVMe cache) |
10. 监控与告警(你会真正用到的项)
- Nginx:stub_status / VTS 导出 QPS、状态码、cache_status(MISS/BYPASS/EXPIRED/REVALIDATED)。
- MinIO:原生 Prometheus 指标(minio_cluster_usage_bytes、minio_s3_requests_total、minio_s3_ttfb_seconds)。
- 磁盘:NVMe/HDD SMART、重映射扇区、介质错误;LVMcache 命中率与脏页。
告警阈值:
- L1 cache hit < 80% 持续 5 分钟
- L2 TTFB P95 > 120ms 持续 5 分钟
- LVMcache dirty > 70%(writeback 模式)
- 单节点 req/s 偏离集群均值 2 倍 → 可能热键倾斜
11. 线上常见坑与解决
- 变体“串味”:proxy_cache_key 未包含尺寸/格式参数,导致误命中。→ 将 sz 与目标格式写入 key。
- 回源自签证书失败:MinIO 自签,imgproxy/S3 SDK 校验证书失败。→ 导入自建 CA 或关闭校验(不推荐)。
- LVMcache 崩溃恢复:写回模式下异常断电,cache 池元数据损坏。→ 预案:切 writethrough,或 lvconvert --repair;大促前做“拉闸演练”。
- CDN 源站 429/502 抖动:回源并发无上限。→ 源站限联+proxy_cache_lock+ CDN 回源连接池限制。
- AVIF 编码慢:CPU 飙高。→ imgproxy 提高速度档(降低压缩),热门变体预渲染并写回对象层。
- 大文件长连接占坑:未启用 slice,Nginx worker 被长时间占用。→ 开启 slice 1m + 合理超时。
- 对象名含中文/空格:URL 编码不一致导致 404。→ 统一服务端按 RFC3986 编码,客户端仅传 key。
12. 端到端落地步骤清单(从 0 到可切流)
- 安装系统:CentOS 7 最小化,启用 chronyd,关闭 swap(或 swappiness=1)。
- 内核调优:写入 sysctl/limits,重启网络栈。
- L2 对象层:按 §4 部署 LVMcache + MinIO 分布式,建 images 桶与 ILM。
- L1 边缘:部署 docker 运行 imgproxy;安装 Nginx 并写入 §5 配置;准备 NVMe RAID10 作为 proxy_cache_path。
- CDN:接入多厂,配置源站、健康检查、强/弱缓存策略。
- 灰度流量:按 5%→20%→50%→100% 逐步切,观察 L1 命中率/TTFB/回源带宽。
- 预热与热键扩散:上线 warmup.sh,并在营销系统发布前推送 Top SKU 列表。
- 监控告警:Prometheus + Grafana 看板到位,阈值上线即刻通知。
- 演练:拉闸测试(关 1 个 L2 节点、断 1 块 NVMe、CDN 改回源地址)验证自愈。
- 文档与 Runbook:写明切换、回滚、排障三套流程,工单系统挂钩。
13. 成本与收益(粗算)
- NVMe 热层从 2TB×2 升级到 2TB×4 RAID10,单节点成本 +¥X;
- MinIO HDD 池上 NVMe LVMcache 1.92TB×2,节点成本 +¥Y;
- 回源带宽下降 ~65%(按香港带宽单价估算),1 个月节省大于 NVMe 增量月供;
- 页面转化率(首屏更快)+0.3~0.6pct,是“站内收益的隐形部分”。
凌晨三点半,热层命中稳定在 92%,L2 的 TTFB 压着 40ms 一线不动,CDN 的紫线像贴在 x 轴上一样。群里有人发了句“这次稳得有点无聊”。我把羽绒服扣子解开两粒,顺手给那台新加的 NVMe 贴了个标签:“热在边缘,冷在心里。”