最近在做一个风速查询服务,里面绕不过去的一个问题,就是数据格式到底选谁:NetCDF 还是 Zarr?
这两个数据格式,在地学和气象数据圈里都不陌生。Zarr 这几年很热,云原生、并行友好,也很适合拿大规模多维数据做分析;NetCDF 则是老牌选手,生态成熟,和 xarray 的关系也非常紧密,尤其适合本地磁盘上的科学数据管理。
一开始我也有点摇摆。毕竟从“技术趋势”上看,Zarr 的话题度更高,听起来也更先进。但后来我把自己的实际约束一条条摊开,事情就没那么复杂了。
我面对的并不是什么通用气象大数据平台,也不是一个面向云端的多租户分析系统。说白了,就是一个很具体、很务实的工程问题:在单机、本地盘、只读查询、按点返回整年风速序列的前提下,到底选哪种方案更稳、更省事,也更不容易把系统复杂度搞得失控?
想清楚这一点以后,答案其实就开始变得明朗了。
场景到底是什么样?
先说明我自己的需求场景。
我的场景大概是这样:
- 部署环境:一台,或者少量几台服务器;数据放在本地磁盘,不是 S3,也不是 OSS。
- 数据内容:全国范围的一年逐小时风速数据,形态上很像 ERA5、MERRA2 这类栅格气象资料。
- 查询方式:给定任意经纬度,或者给定某个网格点索引,返回这一点整年的小时级风速时间序列,大约 8760 个值。
- 服务模式:数据是提前离线准备好的,线上只负责查,只读,不写。
换句话说,我要做的是一个“按点取整年时序”的查询接口,而不是一个面向各种分析任务的通用数据平台。
这个前提非常重要。因为一旦把问题限定清楚,很多原本看起来很宏大的技术争论,立刻就会落回到工程现实里。
关于数据量的考虑
粗略估一下:
- 时间维度:一年逐小时,大约 8760。
- 空间维度:如果按 0.25° 分辨率,中国范围大约是几万级网格点。
- 变量数量:假设只存一个变量,比如风速。
- 数据类型:
float32。
这么算下来,一个年度文件通常也就是几百 MB 到 1~2 GB这个量级。
这个体量对现代服务器来说,其实并不夸张。只要机器有 32~64 GB 内存,在服务启动时把整年的 Dataset 读进内存,完全是可行的。
而一旦整年的数据已经常驻内存,后续每次查询做的事情就很简单了:
- 找到最近的格点,或者算出插值位置;
- 在 time 这一维上切出一个一维序列;
- 把结果序列化后返回。
到了这一步,系统的瓶颈往往已经不在文件格式本身,而更可能出现在:
- 坐标映射;
- JSON 序列化;
- 网络传输;
- 接口层的整体吞吐。
也正因为如此,后面的判断会明显偏向一个更朴素、但更合适的方案:NetCDF + 内存常驻。
为什么我更倾向于 NetCDF?
原因很简单:它和 xarray 的契合度实在太高了。
如果你的数据处理链路本身就是基于 xarray,那 NetCDF 几乎可以算是天生一对。xarray 的数据模型本来就和 NetCDF 一脉相承。在 xarray 里看到的维度、坐标、变量、属性,这套东西和 NetCDF 的组织方式高度一致。很多时候,甚至会有这样一种感觉:xarray 操作的就是一种更现代、更 Python 化的 NetCDF 抽象层。
所以在本地文件系统上,NetCDF 的优点很直接:
- 它通常是一个或少数几个大文件;
- 打开、复制、备份、迁移都很直观;
- 不需要额外处理一大堆碎小文件;
- 对“离线写一次,在线反复读”的模式非常友好。
从工程实现的角度看,这件事其实可以简化成两步:
- 离线预处理:把原始的 ERA5、MERRA2 或其他数据整理成一个干净、规则的年度 NetCDF 文件;
- 在线服务:服务启动时把这个文件打开,必要的话直接加载进内存,之后所有请求都在内存里完成。
在这种模式下,底层格式是什么,很多时候并不会决定系统成败。它更像是在影响:启动要花几秒、运维是不是省心、数据迁移是否简单。
而在这些方面,NetCDF 的表现已经足够稳了。
Zarr 擅长什么场景?
并非说 Zarr 不好,恰恰相反,Zarr 很强,只是它最强的地方,不在我这个场景里。
Zarr 真正擅长的,通常是这些情况:
- 数据放在云对象存储里,比如 S3、GCS、OSS;
- 数据集特别大,已经到了几十 TB 甚至更大的量级;
- 需要结合 Dask 做分布式、多机并行分析;
- 需要频繁按 chunk 随机读取不同区域、不同变量、不同时间片。
它把 chunk 存成独立对象,这在对象存储环境里很有优势。因为你不需要把整个大文件拖下来,只取你要的那一小块就行。
问题在于,我并没有使用对象存储,也不是分布式分析。
如果把 Zarr 放到本地文件系统里用,往往意味着:
- 目录里会出现大量小文件;
- 备份、迁移、打包、同步时更容易变得麻烦;
- 运维上没有大文件来得省心;
- 而对“单机 + 一年数据 + 启动后常驻内存”的场景,它能提供的额外收益又很有限。
从这一点来讲,Zarr 并不能很好的解决我眼前遇到的问题。
更适合落地的方案:NetCDF + 内存常驻 Dataset
如果把目标定义成“快速做出一个稳定、可维护、响应够快的按点查询服务”,那我现在最认可的方案其实很简单:
离线阶段用 xarray 整理数据,产出年度 NetCDF;在线阶段在服务启动时把 Dataset 打开并加载进内存,查询直接走内存索引。
离线预处理 / 归档流程
第一步,先把原始数据收齐。比如 ERA5、MERRA2 之类的官方 NetCDF 文件。
然后用 xarray 做一次预处理,通常包括这些动作:
- 统一时间轴,确保每小时一个点;
- 裁剪到中国范围;
- 只保留真正需要的变量;
- 如果后面要做插值,也可以提前把网格处理成更适合服务侧读取的形式。
代码大概像这样:
import xarray as xr
ds = xr.open_mfdataset("raw/*.nc", combine="by_coords")
ds_china = ds.sel(
lat=slice(3, 54), # 按实际范围调整
lon=slice(73, 136),
)
# 可在这里顺便算风速、重采样等
# ds_china["wind_speed"] = ...
ds_china.to_netcdf("china_hourly_ws_2024.nc")
这样就把原始、分散、不适合直接在线服务的数据,整理成一个规则、清爽、可直接使用的年度归档文件(xarray 官方也建议,这类数据持久化时,默认用 NetCDF 是最稳妥的)。
服务启动流程
在 FastAPI 的后台框架里,我是这样设计的:服务启动时只打开一次 Dataset,如果内存允许,就直接 load() 到内存里。后续请求全都复用这一份已经准备好的数据。
import xarray as xr
DS_WS = None
def load_dataset():
global DS_WS
DS_WS = xr.open_dataset(
"china_hourly_ws_2024.nc",
engine="h5netcdf", # 官方文档建议,本地盘上往往会比 netcdf4 更快一点:contentReference[oaicite:4]{index=4}
)
# 如果总大小在可接受范围内,直接加载到内存
DS_WS.load()
然后在 FastAPI 的启动阶段调用 load_dataset()。这样服务真正开始接请求时,底层数据已经就位了。
只要数据已经在内存里,后面大多数请求就不再是 I/O 问题,而变成了普通的数组索引问题。两者的体验差别会非常明显。
请求处理流程
对每个查询请求:
- 把传入的经纬度映射到网格索引(最近邻或双线性插值);
- 在内存 Dataset 上切出时间序列;
- 返回 JSON、CSV 或二进制结果。
代码类似:
def query_timeseries(lat, lon):
# 1. 找最近格点(简单做法示意)
i_lat = int(round((lat - LAT_MIN) / LAT_RES))
i_lon = int(round((lon - LON_MIN) / LON_RES))
# 2. 在 Dataset 中切数据(假设变量名为 'wind_speed')
ts = DS_WS["wind_speed"].isel(lat=i_lat, lon=i_lon)
# 3. 返回 time, value 序列
return {
"time": ts["time"].values.astype("datetime64[s]").tolist(),
"wind_speed": ts.values.tolist(),
}
由于 DS_WS 已经在内存里,这个函数几乎不做 I/O,只是数组索引和序列化,性能非常可观。
性能与扩展
内存占用与 QPS
- 如果一个年度 Dataset 是 1–2 GB,服务启动时
ds.load()一次就好; - 服务运行过程中,尽量 不要再重新打开文件,所有读操作都走内存 Dataset;
-
QPS 提升可以从:
- 减少不必要的坐标转换;
- 输出使用紧凑编码(例如压缩的二进制或轻量浮点数组)等方向入手。
未来要扩容怎么办?
如果将来变成:
- 多年叠加,比如 10 年逐小时;
- 变量变多(风速、风向、温度、气压、辐射……);
- 想做分布式计算、或者把数据放在对象存储共享给多个服务。
可以考虑:
- 继续保留 NetCDF 作为原始归档格式(方便下载、共享、互操作);
- 用 xarray + Dask + rechunker,把这些 NetCDF 转成为分析优化过的 Zarr 数据立方体;
- 在云端或多机环境下,从 Zarr 数据立方体读取数据做大规模分析。
这时候,Zarr 的“按 chunk 存储 + 适配对象存储”的优势才会完全显现出来。
