最近在做一个风速查询服务,里面绕不过去的一个问题,就是数据格式到底选谁: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 的优点很直接:

  • 它通常是一个或少数几个大文件;
  • 打开、复制、备份、迁移都很直观;
  • 不需要额外处理一大堆碎小文件;
  • 对“离线写一次,在线反复读”的模式非常友好。

从工程实现的角度看,这件事其实可以简化成两步:

  1. 离线预处理:把原始的 ERA5、MERRA2 或其他数据整理成一个干净、规则的年度 NetCDF 文件;
  2. 在线服务:服务启动时把这个文件打开,必要的话直接加载进内存,之后所有请求都在内存里完成。

在这种模式下,底层格式是什么,很多时候并不会决定系统成败。它更像是在影响:启动要花几秒、运维是不是省心、数据迁移是否简单。

而在这些方面,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 问题,而变成了普通的数组索引问题。两者的体验差别会非常明显。

请求处理流程

对每个查询请求:

  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 存储 + 适配对象存储”的优势才会完全显现出来。