NetCDF vs Zarr:风速时间序列服务的现实选择
要做风速查询服务,一开始我在 NetCDF 和 Zarr 之间犹豫过:Zarr 很「新潮」, 云原生、并行友好,被很多地学和能源项目用来做大规模分析;NetCDF 则是 xarray 官方推荐的老牌格式, 和它的数据模型一一对应,尤其适合本地盘上的气象数据集。
但把自己的约束条件摆在桌面上——单台服务器、本地磁盘、只做全国范围一年逐小时风速、接口只读且每次只是取某个点一年的时间序列。 我发现,这不是一场“通用格式之争”,而是一个非常具体的工程选择题:在这样的现实需求下,哪种方案既能跑得稳,又不把系统复杂度搞炸?
场景什么样?
先把需求说清楚:
- 部署环境:一台(或几台)服务器,数据存在本地磁盘,不是 S3、不是 OSS。
- 数据内容:全中国范围,一年逐小时风速,类似 ERA5 / MERRA2 那种栅格数据。
- 查询方式:给定任意经纬度(或网格点索引),返回这一点整年的小时级风速时间序列(8760 点左右)。
- 特点:数据是离线准备好的,在线服务 只读、不写。
一句话总结: 我要做的是一个「按点取整年时序」的查询服务,而不是一个通用的气象大数据平台。
在这个前提下,我们再来看 NetCDF 和 Zarr 的取舍。
数据有多大?
粗略估一下量级:
- 时间维度:一年逐小时 ≈ 8760。
-
空间维度:
- 例如 0.25° 分辨率,中国范围大约几万网格点;
- 只存一个变量(风速),用
float32。
整体下来,一个年度文件通常是 几百 MB 到 1–2 GB 这个级别——对一台有 32–64 GB 内存的服务器来说,完全可以在启动时整体读进内存。
一旦整年的 Dataset 进了内存:
-
每次查询只是:
- 找到最近格点 / 插值位置;
- 在
time这一维上切出一个 1D 序列返回。
-
真正的瓶颈,很可能是序列化、网络带宽,而不是底层文件格式。
这也是为什么下面的结论会偏向 NetCDF + 内存常驻。
本地盘 + 单机服务:为什么更推荐 NetCDF?
xarray 的“亲儿子”
xarray 官方文档说得很直接:推荐的存储方式就是 netCDF,xarray 的数据模型本身就是从 netCDF 演化来的。
在本地文件系统上:
- NetCDF 是 一个或少数几个大文件,读写时只和这几个文件打交道;
- 打开速度、可靠性、备份和迁移都比较简单;
- 对我这种“离线写一次,在线一直读”的模式尤其合适。
要做的事情,本质上是:
- 离线预处理 → 把散乱的原始数据整理成一个整洁的年度 NetCDF 文件;
- 在线服务 → 启动时仅打开一次这个文件,后面都把它当内存数据来用。
在这种模式里,底层是 NetCDF 还是别的,其实只是“启动需要多少秒”的差别。
Zarr 真正擅长的场景
Zarr 的设计适合的场景:
- 云对象存储(S3 / GCS / OSS);
- 特别巨大的多维数据集(几十 TB);
- 多机 Dask 并行计算。
它把每个 chunk 存成单独的对象,对云端“随机小块读取”的效果非常好,也可以在 HPC 上加速某些访问模式。
那么,代价是什么呢?
- 本地文件系统上会出现成千上万的小文件,对运维和备份都不太友好;
- 对于“单机 + 1 年数据 + 内存常驻”的场景,它能带来的好处非常有限,复杂度却实实在在提升了。
所以,如果没打算上云、也没计划变成几十 TB 的数据湖,可以先把 Zarr 从工具箱里暂时拿掉,等“体量够大”再引入。
推荐架构:NetCDF + 内存常驻 Dataset
下面这套可以直接落地的方案,基本覆盖了我现在的需求。
离线预处理 / 归档流程
-
收集原始数据(ERA5、MERRA2 等官方 NetCDF 文件)。
-
用 xarray 做一次“清洗 + 归并”:
- 统一时间轴,确保每小时一个点;
- 裁剪到中国范围;
- 只保留我需要的风速变量(例如
u10、v10或已计算好的wind_speed); - 如果以后还要支持插值,可以提前算好规则网格。
-
输出一个年度文件,例如:
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
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 的
startup事件中调用load_dataset(),保证服务真正开始对外提供接口时,数据已经在内存里了。
4.3 请求处理流程
对每个查询请求:
- 把传入的经纬度映射到网格索引(最近邻或双线性插值);
- 在内存 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 存储 + 适配对象存储”的优势才会完全显现出来。