本記事は Building a High-Performance Elevation API の日本語訳です。


ColorfulClouds Technology(彩云科技、Pinyin では “cai yun”)では、より高い時空間分解能の気象データを提供することに注力しています。これまで、標高データの解像度制限により、登山やクロスカントリーなどのアウトドア活動を行うユーザーが、彩云のデータと実際の体感に大きな差が生じることがありました。この問題は、標高変化が激しい山岳地帯や高原地域で特に顕著でした。

この問題を解決するため、2023 年末に標高データの刷新プロジェクトを開始し、空間分解能 30 メートルの標高データを用いて、より正確な気象情報を提供することを目標としました。

本記事では、新しいデータシステムの実装プロセスを紹介し、実践中に直面した課題、検討内容、および解決策を共有します。

従来の標高データ

2024 年初頭まで、当社が使用していた標高データは SRTM(Shuttle Radar Topography Mission)データセット に由来していました。このデータは PNG 形式で保存されており、リクエストされた地点の緯度経度から PNG のピクセル位置を算出し、RGB 値から標高を計算していました。以下は、横断山脈の標高データのサンプル画像です。

横断山脈の標高データスクリーンショット(出典: SRTM by NASA/JPL-Caltech)

横断山脈の標高データスクリーンショット(出典: SRTM by NASA/JPL-Caltech)

内部処理後、このデータは空間分解能が 5 キロメートルに低減され、垂直分解能は約 35 メートルとなっていました。中国地域のデータを主に扱っていたため、これらをメモリに直接ロードして計算を行うことができました。データ取得の遅延は通常ナノ秒単位であり、システムのボトルネックにはなりませんでした。

新バージョンの標高データ

ソース

海外で公開されている ASTER データセット を直接ダウンロードし、空間分解能 30 メートル、垂直分解能 1 メートルの GeoTIFF 形式で保存されているものを利用しました。データ量が大きく、ある程度の前処理が必要であったため、K8S 上で利用しやすいようにデータを直接 JuiceFS 分散ファイルシステム に書き込みました。

ストレージおよびアクセス

当初、GeoTIFF 形式のデータを .npy 形式 に変換し、Python の mmap を使って読み込む方式を試みました。

しかし、実運用で以下の課題が判明しました。

  1. グローバルデータは約 500GB ありますが、当社の実際の業務対象は中国地域(約 30GB)です。K8S 上のサービスはディスクマウントに依存して起動する必要がありました。
  2. 高並列シナリオでは、Python + mmap 方式では期待したスループットを確保できませんでした。

課題 1 に対する対策

JuiceFS 分散ファイルシステムの利用を試みました。以前の利用経験から、機械式ハードディスク並みの読み書き速度が得られ、K8S 上へのさまざまなデプロイ方法が提供されています。テスト環境では、低トラフィック時はかろうじて許容可能なスループットを達成できましたが、高トラフィック時にはファイル読み込み待ちが継続し、Python サービスがフリーズする状況が発生しました。そこで、中国地域のデータは遅延の低いローカル SSD に配置することにしました。海外の高精度データを必要とする特定の顧客向けには、海外データを JuiceFS 上に残す形とし、現在のトラフィック負荷では許容可能と判断しています。

また、サービス起動時のデータファイル準備の問題を解消するため、サービスをステートフルに設定し、以前の SSD を継続利用できるようにしました。さらに、データをダウンロードするための init コンテナ を追加し、増分ダウンロードに対応させることで、サービス全体の起動時間を非常に短くしています。

課題 2 に対する対策

Python + mmap 方式の高並列利用ではスループットが不足するため、Go 言語で標高データの読み込み処理を実装しました。元の 2 次元マトリクスを 1 次元の int16 配列に変換し、特定の順序でバイナリファイルに格納します。コード内では、緯度・経度と空間分解能からファイル内オフセットを算出し、os.File.ReadAt で該当位置を読み取ります:

1
2
3
4
5
6
7
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 による最適化を検討していますが、Linux カーネル 5.1 以降で利用可能な機能であるものの、K8S ノードでは特権モードを有効化する必要があり、現状では受け入れがたい状況です。したがって、格子点補間計算の最適化に注力しました。内部クエリ時に複数の大きなスライスが生成され、メモリ割り当てで大きなオーバーヘッドが発生していたため、sync.Pool を用いてスライスを再利用することで、パフォーマンスが大幅に向上しました。

現状、サービスは 1 コア+ 0.5GB メモリ構成で 1500 QPS の負荷下において、P99 レイテンシは約 500 マイクロ秒です。現行のビジネス要件を考慮すると、このレイテンシは許容範囲内と判断しています。

以下は社内システムで標高データを参照するための Web インターフェースの一例です。

場所画面イメージ
玉龍雪山-藍月谷景区(Jade Dragon Snow Mountain-Blue Moon Valley)
南岳高山気象観測所(実際の標高約 1266m)
エベレスト(実際の標高 8848.86m)
ギザのピラミッド(実際の標高約 138m)
D-Day:オマハビーチ上陸直前の高地

データの信頼性を検証するため、2023 年の社内麗江(リィージャン)でのチームビルディング時の軌跡データを使用し、軌跡の標高データと当社の標高データを比較しました。以下の図は、軌跡データと標高データの比較結果であり、両者の差異が非常に小さいことが示されています。

標高データ軌跡データ

結論

彩云科技では以前、NumPy と mmap 技術に強く依存していたため、現在では既に多くの問題が解消されていることに気づけていませんでした。より効率的なプログラミング言語と安価なメモリおよび SSD を組み合わせることで、ほぼ同等かそれ以上のパフォーマンスを実現しつつ、サービス運用コストを大幅に削減できます。

もちろん、問題ごとに適切な分析が必要です。たとえば、標高データ自体は比較的シンプルで、各地点に対して 1 つのデータしかないため、Go 言語や Rust で実装することも容易です。