在彩云科技,我们始终致力于为用户提供更高时空分辨率的气象数据。在过去这些年中,我们始终面临一个挑战:由于高程数据分辨率的限制,徒步、越野等户外活动爱好者用户经常遇到彩云提供的数据与实际感受有着明显偏差,这种情况在海拔变化剧烈的山地和高原地区尤为突出。

为解决这一问题,我们于 2023 年末着手启动了高程数据的升级工作,目标是基于空间分辨率 30 米的高程数据为用户提供更精准的气象信息。

本文旨在介绍这套新数据的实现过程,并分享我们在实践中遇到的挑战、探索以及解决方案。

以往的高程数据

2024 年初之前,我们使用的高程数据源自 SRTM(Shuttle Radar Topography Mission)数据集。这些数据以 PNG 文件格式存储,我们通过计算请求点的经纬度来确定 PNG 中的像素位置,并根据 RGB 值来计算高程。以下是一张示例图片,展示横断山脉的高程数据:

横断山脉高程数据截图, 来源: SRTM by NASA/JPL-Caltech

这些数据经过内部处理后空间分辨率降到了 5 公里,垂直分辨率大约为 35 米。由于我们主要关注中国区域的数据,因此可以直接将这些数据加载到内存中进行计算。数据获取的延迟通常在纳秒级别,因此并不构成系统的瓶颈。

新版本高程数据

来源

我们直接下载了国外公开的 ASTER 数据集,该数据集以空间分辨率 30 米,垂直分辨率 1 米的 GeoTIFF 格式存储。由于数据的体积比较大并且需要进行一些预处理,我们直接将数据写入了 JuiceFS 分布式文件系统,方便在 K8S 中使用。

存储与访问

最初,我们将数据从 GeoTIFF 格式转换为 .npy格式,以便在 Python 中使用 mmap 方式 读取数据。

然而,我们发现这种方式在实际使用中存在问题:

  1. 全球数据约为 500GB,但实际业务主要在中国区域,数据量约为 30GB。在 K8S 中,服务需要依赖磁盘挂载才能启动。
  2. 在高并发场景下,使用 Python+mmap 的方式无法达到预期的吞吐量。

针对第一个问题,我们尝试使用 JuiceFS 分布式文件系统来解决。根据以往的使用经验,它可以获得与机械硬盘相当的读写速度,并提供了多种方式将其部署到 K8S 中。在测试中,发现在低流量情况下,服务可以获得勉强可接受的吞吐量。但在高流量情况下,进程会一直等待文件读取结果,导致 Python 服务出现卡死的情况。因此,我们做出了取舍,将中国区域的数据放在固态硬盘上,因为本地盘的延迟比网络 IO 低得多。但对于特定需要海外高精度海拔数据的客户,仍将海外数据放在 JuiceFS 上,考虑到目前的流量压力,这个方案是可接受的。同时,为了解决服务启动时数据文件的准备问题,将服务配置为有状态服务,以便可以继续复用之前的固态硬盘,并增加了一个初始化容器用于下载数据。初始化过程支持增量下载,因此服务的总启动时间非常短。

针对第二个问题,我们尝试使用 Go 语言来实现高程数据的读取。将原始二维矩阵处理成一维的 int16 数组,按照从特定的顺序存储到二进制文件中。在代码中根据经纬度和数据的空间分辨率计算出在文件中的偏移量,然后使用 os.File.ReadAt 来读取特定位置的数据:

data := make([]byte, dataSize)
_, err = f.ReadAt(data, offset)
if err != nil {
   return 0, err
}

value := binary.LittleEndian.Uint16(data)

可以看到,这种方法大大降低了文件 IO 的压力。通过使用 Pyroscope 对服务进行性能分析,我们发现性能消耗的主要部分是文件读取的 os.File.ReadAt 和格点插值计算的环节。对于前者,我们预期通过 io_uring 来优化,io_uring 是 Linux 内核 5.1 版本引入的一种新的异步 IO 方式。虽然目前 K8S 节点支持了 io_uring 机制,但是需要打开特权模式,暂时是不可接受的。因此,我们将优化的重点放在格点插值计算上。内部查询的时候会生成多个尺寸很大的 slice,导致内存分配的性能损耗很大。通过使用 sync.Pool 来复用 slice,性能得到了极大改善。

目前,服务配置为 1C+0.5GB 内存,在 1500 QPS 的压力下,访问 P99 约为 500 微秒。结合公司当前的业务场景,这个延迟水平是可以接受的。

下面的截图展示内部系统用于查询高程数据的 Web 界面:

地点界面截图
玉龙雪山-蓝月谷景区
南岳高山气象站(实际海拔约 1266 米)
珠穆朗玛峰(实际海拔 8848.86 米)
吉萨金字塔(实际海拔约 138 米)
D-Day 美军在 Omaha 海滩登陆时临近高地

为了验证数据的可靠性,我们使用了 2023 年公司在丽江团建时往返玉龙雪山的轨迹数据,将轨迹的海拔数据与高程数据进行对比。下图展示了轨迹数据与高程数据的对比结果,可以看到两者的差异非常小。

海拔数据轨迹数据

结语

彩云天气过去对 NumPy 和 mmap 的技术路线依赖较高1,导致没有及时发现很多以前的问题在现在已经不存在了。通过使用更高效的编程语言结合更廉价的内存和固态硬盘,完全可以获得更好或几乎一致的性能,并显著降低服务的运行成本。

当然,还需要具体问题具体分析。例如,高程数据本身比较简单,每个点上只有一个数据,因此使用 Go 语言实现也很容易,甚至可以使用 Rust 来实现。