要做风速查询服务,一开始我在 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 是 一个或少数几个大文件,读写时只和这几个文件打交道;
  • 打开速度、可靠性、备份和迁移都比较简单;
  • 对我这种“离线写一次,在线一直读”的模式尤其合适。

要做的事情,本质上是:

  1. 离线预处理 → 把散乱的原始数据整理成一个整洁的年度 NetCDF 文件;
  2. 在线服务 → 启动时仅打开一次这个文件,后面都把它当内存数据来用。

在这种模式里,底层是 NetCDF 还是别的,其实只是“启动需要多少秒”的差别

Zarr 真正擅长的场景

Zarr 的设计适合的场景:

  • 云对象存储(S3 / GCS / OSS);
  • 特别巨大的多维数据集(几十 TB);
  • 多机 Dask 并行计算。

它把每个 chunk 存成单独的对象,对云端“随机小块读取”的效果非常好,也可以在 HPC 上加速某些访问模式。

那么,代价是什么呢?

  • 本地文件系统上会出现成千上万的小文件,对运维和备份都不太友好;
  • 对于“单机 + 1 年数据 + 内存常驻”的场景,它能带来的好处非常有限,复杂度却实实在在提升了。

所以,如果没打算上云、也没计划变成几十 TB 的数据湖,可以先把 Zarr 从工具箱里暂时拿掉,等“体量够大”再引入。

推荐架构:NetCDF + 内存常驻 Dataset

下面这套可以直接落地的方案,基本覆盖了我现在的需求。

离线预处理 / 归档流程

  1. 收集原始数据(ERA5、MERRA2 等官方 NetCDF 文件)。

  2. 用 xarray 做一次“清洗 + 归并”:

    • 统一时间轴,确保每小时一个点;
    • 裁剪到中国范围;
    • 只保留我需要的风速变量(例如 u10v10 或已计算好的 wind_speed);
    • 如果以后还要支持插值,可以提前算好规则网格。
  3. 输出一个年度文件,例如:

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 的后台框架里,可以这样设计:

  1. 应用启动时只打开一次 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()
    
  2. 在 FastAPI 的 startup 事件中调用 load_dataset(),保证服务真正开始对外提供接口时,数据已经在内存里了。

4.3 请求处理流程

对每个查询请求:

  1. 把传入的经纬度映射到网格索引(最近邻或双线性插值);
  2. 在内存 Dataset 上切出时间序列;
  3. 返回 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 年逐小时;
  • 变量变多(风速、风向、温度、气压、辐射……);
  • 想做分布式计算、或者把数据放在对象存储共享给多个服务。

可以考虑:

  1. 继续保留 NetCDF 作为原始归档格式(方便下载、共享、互操作);
  2. 用 xarray + Dask + rechunker,把这些 NetCDF 转成为分析优化过的 Zarr 数据立方体
  3. 在云端或多机环境下,从 Zarr 数据立方体读取数据做大规模分析。

这时候,Zarr 的“按 chunk 存储 + 适配对象存储”的优势才会完全显现出来。