做文化遗产数字化这一行的人,几乎都会在某一天撞上同一堵墙:扫描仪那头一切顺利,几万张高精度图像安安稳稳躺在存储里,可一旦要把它们搬到浏览器上给人看,问题就全冒出来了。一张八万乘四万像素的绢本设色扫描件,不压缩的话体积能到几个GB,直接丢给浏览器,前端解码先崩,后端带宽后崩。我参与过的几个馆藏数字化展示项目,最后卡住的地方基本都不是"图扫得够不够清",而是"Nginx 这一层怎么把高分辨率图像又快又稳地喂给用户"。这篇就聊这个:Nginx 在文化遗产数字化在线展示里该扮演什么角色、哪些配置是真有用的、哪些是看起来很美实际添乱的,以及那些只有真正跑过一遍才知道的细节。不管你是刚接手一个数字展陈项目的后端,还是正在做古籍、拓片、壁画类高精度图像的在线浏览功能,下面这些内容应该都能直接拿去对照。
1. 先把账算明白:一张馆藏扫描件为什么能压垮一台服务器
在聊任何配置之前,我习惯先把数字摆在桌面上。很多项目失败的根源不是技术选型错,而是从第一步就没算清楚自己面对的是什么量级的东西。高分辨率图像的"高"到底高在哪,很多人只有一个模糊感觉,真要落到带宽、内存、磁盘 IO 上就说不清了。
1.1 从 8 亿像素说起:单图的体积、内存与解码开销
拿一张常见的馆藏平面扫描件举例:物理尺寸大约 1.2 米乘 0.8 米,按 600 DPI 扫描,得到约 28000×19000 像素,也就是 5.3 亿像素左右。若是小幅面的精细物件,比如一页手札或者一方印章的印面,为了看清纸张纤维和墨色层次,往往要上到 1200 DPI,像素总量反而可能更高。
这个量级意味着什么?未压缩的 RGB 位图,每像素 3 字节,5.3 亿像素就是大约 1.5 GB 的原始数据。存成高质量 JPEG,体积能压到 100~300 MB,听起来还能接受——但关键在于浏览器要把它完整解码进内存才能显示。解码后的位图在内存里同样是 1.5 GB 级别,移动端浏览器几乎必然会崩,桌面端稍好一点也是卡到没法交互。
我见过最典型的一个错误做法,是把整张大图直接挂在页面上,靠 CSS 缩放显示成"缩略图效果"。表面上看缩略图很小,实际上浏览器下载的是完整文件、解码的是完整位图,一个页面放十张,用户什么都没干就先吃掉十几个 GB 内存。这种问题在开发机上永远测不出来,因为你本地网络快、机器内存大,一上真实网络和低配设备,立刻原形毕露。
所以第一步的结论很明确:高分辨率图像不能以"整图"的形式进入浏览链路,必须切分。这个认知决定了后面所有的架构选择。
1.2 瓦片金字塔:把"下载一张图"变成"取需要的几百块"
瓦片化(Tiling)的思路其实很朴素:把一张大图按层级切成小块,第 0 层是整图的极小缩略,第 1 层是它的一半尺寸,依次往下,直到原始分辨率。每一层再切成 256×256 或 512×512 的小方块。用户打开页面时,前端只加载当前视口、当前缩放级别需要的那几十块瓦片。
这么做的好处是多重的,我按重要程度排一下:
- 首屏体积骤降。整图 200 MB,首屏可能只需要 40 块 512×512 的瓦片,合计不到 1 MB,用户几乎瞬间看到画面。
- 按需加载。用户不放大,深层瓦片永远不下载,带宽花在真正被看的地方。
- 内存可控。前端只持有视口范围内的位图,滚动时淘汰旧瓦片,内存占用稳定在几十 MB。
- 缓存友好。瓦片是天然的静态资源,URL 稳定、内容不变,缓存命中率极高。
主流格式有两类:一类是 DZI(Deep Zoom Image)配合 OpenSeadragon 这类前端库,目录结构固定;另一类是 IIIF Image API,通过 URL 参数动态请求任意区域和尺寸,灵活性更高但服务端实现更重。前者适合"图已经全部预切好、只读展示"的场景,后者适合"需要任意裁切、多机构共享图像"的场景。我个人的经验是:如果项目定位是单一展陈、图像不对外复用,老老实实用预切瓦片加 Nginx 直接静态分发,是最省事也最快的方案;IIIF 那套动态裁切,除非真有跨机构调用的需求,否则引入的复杂度往往不值。
1.3 一张带宽估算表,决定你要不要提前上 CDN
算完单图再算总量,这一步特别容易被跳过。下面这张表是我在项目启动阶段必做的估算,参数可以按自己实际情况替换:
| 项目 | 假设值 | 说明 |
|---|---|---|
| 单图瓦片总数 | 约 12000 块 | 600 DPI 全分辨率,512 像素瓦片 |
| 单块瓦片平均体积 | 45 KB | JPEG q85,内容复杂度中等 |
| 单图完整浏览产生流量 | 约 8~15 MB | 用户不会看完全部层级 |
| 日活浏览人次 | 3000 | 展陈页面的常见量级 |
| 峰值并发 | 200 | 集中在推送后的几小时 |
| 日流量估算 | 约 40 GB | 3000 × 13 MB |
| 峰值带宽需求 | 约 60 Mbps | 200 并发 × 平均 300 Kbps |
这张表的价值在于:它直接告诉你需不需要 CDN。日流量 40 GB、峰值 60 Mbps,一台配置尚可的机器配合 Nginx 的静态分发能力,扛下来毫无压力。但如果碰上大促式的集中推广,或者图像数量上万、分辨率再翻一倍,峰值带宽会瞬间冲到几百 Mbps 甚至 Gbps 级别,这时候单机出口就是硬瓶颈。
我的判断标准比较粗暴:峰值带宽需求超过单机出口的 60%,就该考虑 CDN 或者多节点。留 40% 余量是因为突发流量从不打招呼,而且 Nginx 处理请求本身也要消耗资源,不能把出口跑满。
2. Nginx 在瓦片链路里到底该站哪个位置
瓦片化方案确定之后,Nginx 的定位其实很清晰:它就是那个把磁盘上的瓦片文件高效吐给浏览器的角色。听起来简单,但真正要调好,得先搞清楚目录结构、文件数量、访问模式这三件事,因为 Nginx 的每一项优化都对应着其中某一环。
2.1 目录结构决定 location 写法:三种常见布局的对比
瓦片的落盘方式直接影响 Nginx 配置的复杂度和文件系统的表现。我见过三种主流布局,各有取舍。
第一种是按层级平铺,路径形如/tiles/{imageId}/{level}/{x}_{y}.jpg。这是 DZI 的标准结构,优点是直观、便于人工排查,缺点是深层级的目录下文件数会爆炸。一张 5 亿像素的图,最深层级可能有七八千个文件堆在同一个目录里,ext4 对单目录文件数没有硬限制,但readdir之类的操作会变慢,备份和 rsync 也难受。
第二种是按 x 坐标再分一级子目录,形如/tiles/{imageId}/{level}/{x}/{y}.jpg。这样单目录文件数被压到几百个,文件系统压力小很多。代价是 URL 多了一层,前端拼路径时要多拼一段。我目前更倾向这一种,尤其图像数量多、总量上百万瓦片的时候,差别很明显。
第三种是哈希散列,把 imageId 做一层哈希前缀,形如/tiles/{h1}/{h2}/{imageId}/...。这适合图像数量极大(十万级以上)的图库,避免顶层目录也堆太多子目录。缺点是路径不可读,排查问题时得先算哈希。
对应的 Nginx 配置,核心就是一条location加alias或者root。这里有个特别容易出错的点:alias和root的语义完全不同,root是把 URI 整个拼到路径后面,alias是替换掉 location 匹配的部分。我踩过最蠢的一次坑是把alias写成了root,结果所有瓦片都 404,排查了半小时才反应过来路径被重复拼接了。
# 推荐写法:alias 精准替换 location /tiles/ { alias /data/tiles/; try_files $uri =404; expires 30d; add_header Cache-Control "public, max-age=2592000, immutable"; }try_files $uri =404;这一句看着不起眼,但它避免了 Nginx 在找不到文件时去做目录索引尝试,直接返回 404,省掉一次系统调用。瓦片请求量大,这种小优化累积起来可观。
2.2 sendfile、AIO、open_file_cache:三个开关的实测收益与误用
这三个参数在老资料里被反复提及,但很多文章只给结论不给条件,导致照抄之后没效果甚至出问题。
sendfile on;的作用是让内核直接把文件数据从页缓存送到 socket,绕过用户态的内存拷贝。对静态文件分发来说这是标配,收益稳定,没有副作用,直接开。配合tcp_nopush on;还能让响应头和文件内容合并发送,减少 TCP 包数量;tcp_nodelay on;则在长连接下减少小包延迟。这三件套一起上,静态分发场景基本不需要犹豫。
aio on;就微妙一些。它把文件的读操作交给异步 IO 处理,理论上能减少 worker 进程的阻塞。但异步 IO 在 Linux 上需要directio配合才有意义,而directio又会绕过页缓存——这一点在高并发小文件场景下是灾难。瓦片正是典型的小文件,大量重复访问同一批热瓦片,页缓存命中率极高,绕过去等于自废武功。
我实测过一组数据,同一批 50 万块瓦片,200 并发压测:
| 配置组合 | QPS | 平均延迟 | 磁盘读 |
|---|---|---|---|
| sendfile + open_file_cache | 9800 | 18 ms | 首次高,后续接近 0 |
| sendfile + aio + directio | 6100 | 31 ms | 持续高 |
| 仅 sendfile | 7400 | 24 ms | 中等 |
结论很清楚:瓦片这种小文件高重复访问的场景,不要开 aio 和 directio,反而会拖慢。aio更适合大文件、低重复访问的场景,比如原始存档图下载。
open_file_cache是这里最值得开的。它缓存文件描述符和元信息,避免每次请求都做open()、stat()这些系统调用。对小文件海量请求的场景,收益非常明显:
open_file_cache max=200000 inactive=60s; open_file_cache_valid 30s; open_file_cache_min_uses 2; open_file_cache_errors on;参数的含义要说清楚:max是缓存条目上限,按你的热瓦片总量估,200000 能覆盖大部分中等规模图库;inactive是多久没被访问就淘汰;valid是多久重新校验一次元信息;min_uses是访问几次才进缓存,设 2 可以避免一次性扫描的冷文件污染缓存。这几个值不要照抄,max设太小等于没开,设太大浪费内存,每条目大约占几百字节。
2.3 小文件高并发的真凶:inode 缓存与文件句柄
有一个问题我遇到不止一次:服务器 CPU 和带宽都很空闲,但瓦片加载就是慢,top里%wa(IO 等待)很高。排查半天,问题出在文件句柄或者 inode 缓存上。
Nginx 的worker_rlimit_nofile和worker_connections必须匹配。默认值往往只有 1024,200 并发乘以每连接若干请求,很容易打满。表现是accept() failed (24: Too many open files)这种日志刷屏,请求被拒绝。
worker_rlimit_nofile 100000; events { worker_connections 16384; multi_accept on; use epoll; }另外是文件系统的 inode 缓存。瓦片数量上百万时,首次访问会大量触发dentry和inode分配,内存吃紧的机器上会频繁回收,形成抖动。解决办法一是保证机器内存足够(经验值是缓存元信息需要的内存约为文件总数的 200 字节/个,百万文件约 200 MB,不算多),二是用vmtouch之类的工具把热点目录的元信息预热进缓存。
还有个更隐蔽的坑:如果瓦片存储挂在网络文件系统上,open_file_cache的效果会大打折扣,因为元信息校验的往返延迟被放大了。这种情况我一般建议要么把热瓦片同步到本地 SSD,要么直接上对象存储配合 CDN,别在中间层硬扛。
3. 压缩与图片格式:不同材质扫描件的差异化策略
数字化对象千差万别,设色绢本、碑刻拓片、线描手稿,它们对压缩的敏感点完全不同。统一用一套压缩参数,结果不是体积浪费就是画质翻车。
3.1 设色绢本、碑刻拓片、线描稿:三类的画质敏感点不同
设色绢本的特点是色彩层次丰富但整体对比度不高,大面积渐变区域多。这类图最怕的是块效应,JPEG 质量系数低于 82 左右,平涂的绢地就会出现明显的方块噪点,尤其是在低层级的小瓦片上被放大显示时特别扎眼。
碑刻拓片几乎全是黑白,但细节密度极高,石花的纹理、笔画的边缘都是高频信息。这类图的 JPEG 压缩反而更吃亏,因为高频细节正是 DCT 变换最容易丢掉的部分。我一般会把质量提到 88~90,或者干脆考虑无损的 PNG 加预压缩——当然体积会大好几倍,得权衡。
线描手稿介于两者之间,大面积留白加细线条。留白区域的压缩效率极高,但线条边缘一旦出现振铃效应就很明显。有个小技巧是用 4:4:4 的色度采样而不是默认的 4:2:0,虽然体积增加约 15%,但线条边缘的清晰度提升肉眼可辨。
提示:不要用同一套参数处理全部素材。按材质分类打标,不同类别走不同的压缩流水线,这是最省事也最有效的优化。
3.2 WebP、AVIF、JPEG 的实测对比表
现代格式确实有优势,但优势大小取决于内容。我抽了 200 张瓦片做过对比,内容是混合的设色画和拓片:
| 格式 | 平均体积 | 相对 JPEG 节省 | 编码耗时 | 兼容性 |
|---|---|---|---|---|
| JPEG q85 | 48 KB | 基准 | 快 | 全兼容 |
| WebP q85 | 34 KB | 约 29% | 中 | 现代浏览器全支持 |
| AVIF q60 | 27 KB | 约 44% | 慢(约 8 倍) | 较新浏览器支持 |
| WebP 无损 | 112 KB | 反向增加 | 慢 | 现代浏览器全支持 |
结论是 WebP 的性价比最高,编码速度可接受,兼容性已经不是问题,节省近三成带宽。AVIF 更省,但编码慢,适合离线预切瓦片的场景——反正是一次性的,慢点无所谓,只要最终体积小。在线动态裁切的场景就不建议 AVIF 了,编码延迟会把响应时间拉到用户能感知的程度。
实际落地时我一般这么做:预切瓦片阶段同时生成 JPEG 和 WebP 两套,用 Nginx 做内容协商,通过Accept头判断浏览器是否支持 WebP:
map $http_accept $webp_suffix { default ""; "~*image/webp" ".webp"; } location /tiles/ { alias /data/tiles/; try_files $uri$webp_suffix $uri =404; }这里要留个心眼:try_files的匹配顺序和变量拼接容易写错,$uri已经带扩展名,拼接后缀会得到xxx.jpg.webp,所以文件命名规则要统一成"原扩展名 + .webp"。这个细节不写清楚,同事接手时一定会踩。
3.3 Brotli 与 gzip:只对文本有效,但配置文件本身要预压缩
这里要澄清一个常见误解:图片瓦片已经没有文本冗余,再上 gzip/brotli 基本没有收益,反而浪费 CPU。我见过配置里对所有类型都开gzip的,对 JPEG 来说纯属白忙,压缩率接近 1,只是多消耗 CPU。
真正需要压缩的是瓦片之外的东西:前端 JS、CSS、瓦片索引的 JSON 元数据、IIIF 的 info.json 响应。这些文本资源用 brotli 预压缩,收益很大,而且可以静态化:
location ~* \.(js|css|json|svg|xml)$ { root /data/web; brotli_static on; gzip_static on; expires 7d; }brotli_static on要求构建时提前生成.br文件,这个用构建脚本批量处理即可。静态预压缩的好处是零运行时 CPU 开销,只多占一点磁盘。对于访问量大的站点,这个选择很划算。
另外提一句瓦片索引文件的设计。如果前端需要知道某张图有哪些层级、每层多少块,别让前端自己去猜,服务端生成一个精简的 JSON 索引,压缩后往往只有几 KB,一次请求就搞定。这个文件应该长缓存,因为图像切好之后基本不会变。
4. 缓存头、版本化与"改了图看不到"的经典事故
缓存配置是瓦片分发里最容易出事故的地方,因为它出问题时表现两极:要么用户每次访问都重新下载全套瓦片,带宽白白烧掉;要么图像更新了,用户死活看不到新版本,然后怀疑你根本没改。
4.1 Cache-Control 的分层设计
我一般把资源分成四档,每档一个缓存策略:
| 资源类型 | 缓存时长 | 说明 |
|---|---|---|
| 瓦片图片 | 30 天 + immutable | 内容本质不可变 |
| 图像索引 JSON | 1 小时 | 可能有新增层级 |
| 前端 JS/CSS | 7 天 | 靠文件名指纹失效 |
| 展陈页 HTML | 不缓存 | 随时可能调整文案 |
immutable这个指令值得单独说。它告诉浏览器"在这个有效期内,连验证请求都不要发",直接命中本地缓存。对瓦片这种确定不变的文件,加上它能省掉大量 304 往返。但前提是你必须有版本化机制,否则改了图用户永远看不到。
4.2 路径版本化与内容指纹
版本化有两种做法。一种是路径带版本号,/tiles/v3/{imageId}/...,整体切换,简单粗暴,适合批量更新。另一种是内容指纹,文件名里带哈希片段,适合精细控制。我倾向路径版本号,因为瓦片数量太多,逐个算指纹成本高,而分批更新本来就是常态。
切换版本时有个必须注意的操作:新旧版本要并存一段时间,至少一个最长缓存周期。否则用户本地还缓存着旧路径的瓦片,新路径又还没生成,中间就会出现空白。我吃过这个亏,一次紧急重扫后直接覆盖了目录,结果一周内陆续有用户反馈"某几张图显示不全",全是本地缓存作祟。
# 新旧版本并存,旧版本只读 location /tiles/v2/ { alias /data/tiles_v2/; expires 30d; add_header Cache-Control "public, max-age=2592000, immutable"; } location /tiles/v3/ { alias /data/tiles_v3/; expires 30d; add_header Cache-Control "public, max-age=2592000, immutable"; }4.3 前端 memory cache 与浏览器后退缓存
后端配置对了,前端还有一层坑。浏览器的 memory cache 和 bfcache(后退前进缓存)行为不受服务端控制,有时候会出现"用户点开大图再返回,瓦片也没重新请求,但画面是错的"这种诡异现象。
这类问题的根源通常在前端状态管理,而不在 Nginx。我一般的建议是:图像切换时给瓦片容器一个明确的 key 变化,强制销毁重建;同时在 URL 上体现当前图像 ID,让后退按钮有明确语义。这些是前端的事,但作为服务端要理解——你返回的缓存头只有建议权,浏览器完全可能根据自己的策略决定不请求,所以不要在服务端做"依赖每次请求"的埋点统计,那数据一定不准。
5. 带宽防护:防盗链、限流与降级的组合拳
高分辨率图像的带宽成本是实打实的,一旦被第三方站点直接引用,或者被脚本大规模抓取,流量费能在一夜之间翻几倍。防护手段要分层做,单靠一招都不够。
5.1 secure_link 签名链路的落地
最基础的防护是 Referer 校验,但 Referer 可以被伪造、也可以被浏览器策略屏蔽,只能挡君子。真正有效的是签名 URL:服务端生成带过期时间和签名的地址,Nginx 用secure_link模块验证。
location /tiles/ { secure_link $arg_sign,$arg_expires; secure_link_md5 "$secure_link_expires$uri$remote_addr secret_key"; if ($secure_link = "") { return 403; } if ($secure_link = "0") { return 410; } alias /data/tiles/; }这套东西的原理是:生成端用相同规则算 MD5,客户端拿到带sign和expires参数的 URL,Nginx 重新算一遍比对。过期时间限制链接有效期,把$remote_addr拼进去还能绑定客户端 IP,防止链接被转发。
注意:绑定 IP 在移动网络下会出问题,用户切换基站 IP 就变了,链接立刻失效。我建议只对高价值、高分辨率的深层瓦片绑定 IP,缩略层级放宽,否则用户体验会很差。
签名的密钥管理也要认真对待,不要硬编码在配置文件里提交到版本库。用环境变量注入,配置文件里引用,这样换密钥不用改配置。
5.2 limit_req / limit_conn 的参数怎么定
限流参数最忌讳拍脑袋。设太松没效果,设太紧正常用户被误伤。我的做法是先观察真实分布:正常人浏览一张图,瓦片请求是突发性的,放大瞬间可能并发请求几十块,然后停顿几秒,再请求下一批。
基于这个模式,limit_req的burst要给足,否则用户一放大就被 503:
limit_req_zone $binary_remote_addr zone=tile_req:20m rate=30r/s; limit_conn_zone $binary_remote_addr zone=tile_conn:20m; location /tiles/ { limit_req zone=tile_req burst=120 nodelay; limit_conn zone=tile_conn 60; # ... }rate=30r/s对正常浏览足够宽松,burst=120容纳放大瞬间的突发,nodelay让突发请求立即处理而不是排队(排队反而会造成延迟累积)。limit_conn 60限制单 IP 同时连接数,挡住那种一条连接慢慢拉的脚本。
这里有个经验:限流阈值一定要结合 access.log 的真实分布来定,别凭感觉。用awk统计一下单 IP 的每秒请求数分布,看看 99 分位是多少,阈值设在 99 分位之上一点,既能挡住异常又不误伤。
5.3 带宽打满时的降级开关设计
总会有那么一刻,流量超出预期,出口跑满,所有用户都变慢。这时候需要有手段快速降级,而不是干等。
我一般会预置几个开关,用 Nginx 的变量配合map实现,改一个标志位就能全局生效:
| 降级级别 | 措施 | 影响 |
|---|---|---|
| 一级 | 关闭深层级瓦片(超过 level N 的返回 404) | 用户无法看到最高清晰度 |
| 二级 | 强制返回 WebP 低质量版本 | 画质下降,体积减半 |
| 三级 | 只允许缩略层级,禁止放大 | 浏览体验受限但不至于不可用 |
实现上可以用map从 Redis 或者一个本地文件读取开关状态,配合lua或者auth_request更灵活。如果不想引入额外组件,最简单的做法是维护一份"禁用层级"的配置,reload一下即可,秒级生效且不影响现有连接。
降级策略要提前和业务方对齐。我经历过一次没对齐的情况:流量告警后运维直接关掉了深层瓦片,结果当天正在做一场面向研究者的高清细节讲解活动,参与者全是冲最高清晰度来的,投诉电话打爆。技术上的合理选择,在业务上可能是灾难。
6. 色彩、DPI 与前端约定的坑
这一节聊的是技术之外但同样致命的东西。图像数字化领域有一套自己的行业约定,跟 Web 开发的默认假设经常冲突,不提前对齐就会反复返工。
6.1 ICC 与色彩空间在传输链路里的丢失
专业扫描件通常带 ICC 色彩配置文件,比如 Adobe RGB 或者某个特定的设备特性文件。问题在于:浏览器对图片内嵌 ICC 的支持是不一致的,而且很多图像处理工具在切瓦片时会默认丢弃 ICC 信息。
结果就是同一个文件,在设计软件里看和浏览器里看颜色不一样,负责校对的研究人员会认为你"把颜色搞错了"。我在一个项目里被这个问题纠缠了很久,最后定的方案是:在切瓦片之前统一转换到 sRGB,转换过程用高质量的重采样算法,保留一份原始文件作为存档版本。sRGB 是 Web 的事实标准,虽然色域窄一些,但至少所有人看到的是一致的。
这个取舍要提前和内容方沟通清楚。对普通浏览展示,sRGB 完全够用;如果有严格的色彩比对需求,那就不该走浏览器这条路,应该提供原始文件下载配合专业软件查看。
6.2 瓦片坐标系、DPR 与"放大就糊"
瓦片坐标系和 CSS 像素之间的换算,是前端和后端最容易产生误解的地方。后端按图像原始像素切瓦片,前端按 CSS 像素布局,中间隔着设备像素比(DPR)。
常见问题是:前端在高 DPR 屏幕上按 1:1 请求瓦片,结果每块瓦片被拉伸到 2 倍显示,用户看到一片模糊。正确的做法是前端根据 DPR 选择更高层级的瓦片,或者在请求时告知服务端需要更高分辨率。
如果用的是预切瓦片,前端可以直接选择 level+1 的瓦片来补偿 DPR。这需要瓦片层级足够深——切瓦片时最深层一定要保留原始分辨率,不要为了省空间提前截断。我见过为了节省磁盘把最深层降采样一半的项目,后期想支持高分屏时发现原始数据已经丢了,只能重新扫描。
另一个细节是瓦片的对齐偏移。图像尺寸往往不是 256 或 512 的整数倍,最后一行一列的瓦片会有空白区域。这些空白应该填充成什么颜色,前后端要约定一致,否则拼图边缘会出现细线。
6.3 从 access.log 反推用户行为
日志不只是排错用的,它能告诉你很多产品层面的信息。我习惯定期从瓦片访问日志里挖几个指标:
- 哪些图像被放大到最深层级。这个数据非常有用,它说明哪些内容真正有价值,值得投入更高扫描精度。
- 用户在哪个层级停止。如果绝大多数人在第 5 层就不再放大,那第 8 到 12 层的瓦片可能是过度投入。
- 404 的瓦片请求分布。集中出现的 404 通常意味着切瓦片时出了问题,或者某些图像的索引与实际文件不一致。
# 统计各图像的深层瓦片请求量 awk -F'"' '{print $2}' access.log \ | grep -oP '/tiles/[^/]+/1[0-2]/' \ | sort | uniq -c | sort -rn | head -20这类分析不用做得太复杂,每周跑一次,看看趋势就够。数据驱动的决策比拍脑袋靠谱得多,尤其在做数字化优先级排序的时候。
日志本身也要注意磁盘占用。瓦片请求量大,日志增长极快,我一般开启按天轮转加压缩保留 30 天,同时在 Nginx 里关掉不必要的字段(比如$request_body对静态资源毫无意义,纯属浪费)。
最后分享一个我们团队现在每次上线前必做的自检动作:挑三张不同材质的代表图,用真实移动设备在弱网下(限速到 3G 级别)完整走一遍浏览流程,从首屏加载、缩小、放大到最深层级、再返回。这个十几分钟的流程,能暴露出压测工具测不出来的绝大多数问题——缓存策略失效、瓦片缺失、DPR 处理错误、签名过期,基本都会在这一步现形。压测工具的并发模型太理想化,真实的浏览器行为比它刁钻得多,而用户不会给你第二次机会。