TimescaleDBに1兆件の気象データをロードする
(aliramadhan.me)- 世界中の過去の気象変化を高速に分析するには、ERA5再解析データをPostgreSQL/TimescaleDBに投入する必要があり、選択した変数とグリッド基準で約7,540億行規模になる
- ERA5は1940年からの時間単位の解像度と0.25度の緯度経度グリッドを提供するが、NetCDF構造では1地点の長期時系列抽出でも20〜30分かかることがある
- 単一行の
insertは毎秒約3千行のため、全体のロードに約8年が必要で、複数行insertでもpsycopg3基準で毎秒2.5万〜3万行で約10か月の水準 - PostgreSQLの
copyとpsycopg3のcursor.copy()はCSVの準備有無によって差があり、オーバーヘッド込みで約毎秒10万行から、並列化と設定調整でさらに引き上げられる - データフレームから直接投入する場合はpsycopg3でhypertableへ直接copy、CSVがすでにある場合は
timescaledb-parallel-copyが適しており、このシステムでは12〜16ワーカーが並列化の適正点に見える
世界規模の気象データウェアハウスを作る
- 目標は、世界中の過去の気象データをクエリし、すでに発生している気候変動のシグナルを分析できるデータウェアハウスを作ること
- 分析例では、ジャカルタが実際に暑くなったのか、嵐が頻繁になったのか、チリ全体がより暖かくなったのか、雲が増えたのか、地域別の変化がどのような形なのか、などを扱う
- 高速なグローバル分析を行うにはデータウェアハウスのクエリが高速である必要があり、第一段階は大量データをデータベースにロードすること
- PostgreSQLを基盤として使い、時間ベースのクエリ高速化にはTimescaleDB、将来の地理空間クエリ高速化にはPostGISが有望な選択肢
ERA5再解析データと7,540億行規模
- 実際の気象観測値の代わりにERA5 climate reanalysisデータを使用する
- 過去の観測値は特定の地域や過去の時点では疎である場合がある
- ERA5は観測値に合うよう制約された気候モデルの出力であり、気象・気候研究で広く使われている
- ERA5は地球全体を0.25度解像度で覆い、1940年から時間単位の解像度で提供されている
- 時間スナップショットは変数あたり727,080個
- グリッド点は1,038,240個で、経度1,440個と両極を含む緯度721個で構成される
- 時間と位置でインデックスすると、変数あたり753,836,544,000行、約7,540億行になる
- ロード対象の変数は、気温、東西・南北10m風速、全雲量、降水量、積雪量
- テーブルには
time、location_id、latitude、longitudeと各気象変数カラムを置くlocation_idと緯度経度カラムを併置した理由は、後続のクエリとインデックスのベンチマークのため
NetCDFファイル構造が遅くなるポイント
- ERA5はNetCDFファイルとして配布され、通常は1日・1か月・1年単位のファイルにデータが入っている
- 時間基準のチャンク構造は、特定時点のデータを参照するには高速で簡単
- 1地点の長期時系列のように時間パターンを見るには多くのファイルを読む必要があり、遅くなる
- 例として、ある位置の気温時系列を取り出すのに20〜30分かかる
- 複雑な地理空間クエリ、特に時間軸を含むクエリは遅く、実行も難しい
- xarray、dask、Pangeoのようなツールで高速化できるが、それでも遅いプロセスとして残る
insert: 単一行から複数行まで
- 最も単純な方法は
insertで1行ずつ入れること - 単一行
insertには複数のコストが伴う- PostgreSQLが文をパースし、テーブル・カラム名を検証し、実行計画を立てる
- データ整合性のためにロックが必要になる場合がある
- WAL(write-ahead logging)のためにデータをバッファに記録する
- 実際のテーブルのディスク領域にデータを挿入する
- トランザクションのコミット時に変更が永続化される
- Pythonで単一行挿入のために3つの方法をベンチマークした
- pandasの
df.to_sql()にchunksize=1を使用 - psycopg3のパラメータ化クエリを使用
- SQLAlchemyのパラメータ化クエリを使用
- pandasの
- 単一行挿入の結果はpsycopg3がわずかにリードし、SQLAlchemyが最も遅い
- TimescaleDB hypertableは通常のPostgreSQLテーブルよりわずかに遅い
- 最高性能は毎秒約3千行で、全データを入れるのに約8年かかる水準
- 複数行
insertは1つの文に複数行を入れ、ネットワーク往復、パース、計画コストを減らす- psycopg3が毎秒2.5万〜3万行で最も速い
- pandasは辞書を使って挿入する構造がタプルより遅い場合がある
- SQLAlchemyにはセッション管理やSQL式の抽象化といった追加オーバーヘッドがあり得る
- それでも全体のロードには約0.8年、ほぼ10か月が必要
copy: PostgreSQLの大量ロード経路
- PostgreSQLの
copyは、CSVまたはバイナリファイルから行を読み込んで入れる大量ロード機能 - 大量ロードを前提にパース、計画、WAL使用を最適化するため、複数行
insertより速い - 2つの経路を比較した
- NetCDFデータをCSVとして保存した後、
copyでロード - CSVファイルを作らず、psycopg3の
cursor.copy()でPostgreSQLへ直接ストリーミング
- NetCDFデータをCSVとして保存した後、
- CSVファイルがすでに準備されていれば、
copyは毎秒約40万行に近い挿入速度を出す - CSVファイル書き込みやタプル生成のオーバーヘッドを含めると、
copyとpsycopg3はいずれも約毎秒10万行水準で、psycopg3がやや速い - この速度でも全データのロードには約3か月が必要
持続ロード率と並列copy
- 多数の行を入れるときは、ディスク書き込み、WALとテーブル挿入のI/O競合、autovacuum、checkpointのようなボトルネックが生じる可能性がある
- 約7億7,200万行を744バッチで入れた実験では、単一ワーカー基準で大きな速度低下は見られない
copy csvは頻繁な低下が現れ、変動により弱い- psycopg3は総じてより速い
- 通常テーブルとhypertableの差は大きくない
- joblibで複数の
copyジョブまたはpsycopg3 cursorを並列実行した - 単一テーブルへの挿入は並列化がよく効く作業ではなく、性能はおおむね16ワーカー以降で頭打ちになる
pg_bulkloadとtimescaledb-parallel-copy
- PostgreSQLの
copy以外に、pg_bulkloadとtimescaledb-parallel-copyをベンチマークした - pg_bulkloadはデフォルト設定ではより速く見えるが、デフォルトではshared buffersを迂回し、WALロギングをスキップするため、クラッシュ後にデータ復旧できない可能性がある
fsyncを切った同一条件では、複数ワーカーを使うtimescaledb-parallel-copyがpg_bulkloadより優れている- timescaledb-parallel-copyはワーカー数を指定して並列挿入できる
- 初期性能は良いが、このシステムでは1億行より前にボトルネックに達し、挿入率が急落した後、波のように回復する
- 持続ロード率は通常テーブルで毎秒約60万〜70万行、hypertableで毎秒約30万行水準
- pg_bulkloadはワーカー数指定はないが、
writer=parallelオプションで読み込み、パース、書き込みに複数スレッドを使用する
PostgreSQL設定調整と耐久性のトレードオフ
- 追加性能はPostgreSQLの非耐久設定を調整することで得られる
- 主な設定は、
fsyncを切ってディスクflushを避け、full_page_writesを切ってpartial page write対策を避けること - これらの設定は、クラッシュ時にデータベースの整合性を危険にさらす可能性がある
- unlogged tableはWALを生成しないため書き込みが速いが、クラッシュリカバリ時に切り詰められる可能性がある
- その後、通常のlogged tableへ変換する必要があり、この過程は遅く、単一スレッドで進む場合がある
- hypertableはunloggedにできないため、hypertableが必要なら追加の変換・移行が必要
最終選択: hypertableへ直接入れる
- 目標がhypertableなら、通常テーブルに入れてからhypertableへ変換する方式より、hypertableへ直接入れる方式のほうが速い
- 約7億7,200万行をpsycopg3の
copyと16ワーカーで入れた簡単なテストでは、hypertable直接挿入は通常テーブル挿入後の変換より時間が少なく済んだ- この場合、hypertable直接挿入はおおよそ80%の時間で済む
- 変換・移行プロセスは速くなく、単一スレッドのように見える
- 推奨方式は次のとおり
- データフレームから直接ロードするなら、psycopg3でhypertableへ直接
copy - CSVファイルがすでにあるならtimescaledb-parallel-copyを使用
- 並列化は、このシステムでは12〜16ワーカーが適正点に見える
- データフレームから直接ロードするなら、psycopg3でhypertableへ直接
全ベンチマークの結論と所要時間
- 保護設定をオンにした単一ワーカー基準では、オーバーヘッド込みの持続ロード率の上限は、このハードウェアで約毎秒14万行に見える
- 複数ワーカーを使うと、psycopg3の
copycursorで保護設定を維持したまま約毎秒25万行まで持続ロード率を高められる - 挿入プロセスの並列化効率は大きくなく、4〜16ワーカーが適正範囲に見える
- リスクを取って
fsyncを切ると、psycopg3で約毎秒46.2万行を維持できる - pg_bulkloadはデフォルトで
fsyncを無効化するため、使用時は注意が必要 - 約毎秒46.2万行を維持できれば、約7,540億行のロードに約20日かかる
コードとベンチマーク環境
- ERA5のダウンロード、テーブル作成、挿入・copy、ベンチマーク、グラフ生成のコードはtimescaledb-insert-benchmarksにある
- 各ベンチマークごとに新しいDockerコンテナを起動し、一貫した環境を維持する
- Dockerコンテナ間でストレージは保持しない
- NetCDFとCSVファイルはHDDから読む
- データベースはNVMe SSDに保存する
- ハードウェア構成
- CPU: 2x 12-core Intel Xeon Silver 4214
- RAM: 16x 16 GiB Samsung ECC DDR4 2666 MT/s
- SSD: Intel 2 TB NVMe
- HDD: Seagate Exos X16 14TB 7200 RPM
- ソフトウェア構成
- Ubuntu 20.04, Linux kernel 5.15
- PostgreSQL 15.5
- TimescaleDB 2.13.0
- pg_bulkload 3.1.20
- PostgreSQL設定は、
timescaledb-tuneが250.57GBメモリと48 CPU基準で推奨した値を使用し、ベンチマークではWALサイズを別途調整した
2件のコメント
GN+のコメントが丁寧語ですね..?
Hacker Newsのコメント
仕事で地理空間分析をかなりやってきたが、地理空間データは座標参照系(CRS)と可視化の投影法を正確に理解する必要があり、思ったよりずっと繊細だ
メタデータをデータと一緒にきちんと運べるインフラがないと、CRSのせいで常に不安になる
AWSの関連機能、Postgres/PostGIS、Spark/Databricks、Snowflake、Trino、ArcGISを一通り使ってきたが、大規模な地理空間処理にはGoogle BigQueryが圧倒的に良いと思う
巨大なm6a EC2上のPostGISで数時間かかり、コストも高かったクエリが、BigQueryの無料枠では5秒もかからなかった
FEMAの公開データを使ったが、SnowflakeとAWSのサービスはgeometry列が最大バイトサイズを超えて詰まり、Sparkには地理空間データ型もなく、オープンソース拡張も物足りなかった
オンプレミスなら事情は違うかもしれないが、20TB基準でもBigQueryのストレージ費用は月100ドル未満の可能性が高いので、あえて自前運用したいとは思わない
初期にH3インデックスを使い、中間データセットがすべてメモリに載っていたから可能だった
構成は128GB Mac Studio + Asahi Linux + mmap parquetファイル + DuckDBで、Airflowも動かし、Nixで開発者ビルドの高速化やデータチームのAirflowジョブ実行にも使っている
GCPは無料または安いときは良いが、使用量が増えていなくても、ある時点でより大きな請求書に驚かされることがある
Apache Sedonaと比較したことがあるのか、具体的に何が足りなかったのか気になる
Apache Sedonaの作者たちが設立したWherobotsで働いているので、フィードバックを聞きたい
https://sedona.apache.org/latest/
https://wherobots.com/
非常に幅の広い列が多くのツールを壊すという点には同意するが、他の列指向Postgres系なら問題なく対応できそうに思う
BigQueryと直接競合するRedshiftを使っていないようなのは意外で、Redshiftの
super列はBigQueryの上限よりもさらに大きく設定できるPostGISに勝つのが難しいということを、人々が何度も苦労して悟るのをよく見る
Trino/PrestoとSparkがこの領域で停滞していることも、とりわけ示唆的だ
Postgresでは大きなテーブルの一部をキャッシュしており、BigQueryでは最初から作り直していたにもかかわらずそうだった
自前運用できるツールとしてはPostgresのほうが好きだが、性能が一桁倍以上良くなるのは反論しにくい
本当に素晴らしい記事だ
TimescaleでDevRelを担当しているが、コミュニティからこういうよく書かれた記事が出てくるのを見るのはうれしい
hypertableのほうが遅かった理由の一つは、ほぼ間違いなくtimestamp列にデフォルトでインデックスを作るからだと思う
通常のテーブルにはインデックスがなかったので、その分速かった可能性がある
create_hypertableでcreate_default_indexes=>falseを使えばインデックス作成をスキップできるし、データを入れる前にインデックスを削除してもよい最終的にはそのインデックスは必要になるだろうが、このような一括ロードではロード後にまとめて作るほうがよい
それと、並列性の高い構成でデータを読み出すHDDがどれくらい耐えられるのかも気になる
create_default_indexes=>falseと、hypertableがデフォルトで時間インデックスを作ることを知らなかったので、この点を説明する注記を追加する時間インデックスなしで挿入してから手動でインデックスを作るベンチマークも試してみたい
ワーカー32個でもHDDは大丈夫そうだった
btopでディスク使用量を見たところ、Postgresが載っているSSDのほうがHDDよりボトルネックに近く見えたので、データをHDDからSSDへ移すより、Postgres用SSDをより速いものに替えるほうが良い投資だと結論づけた
なぜわざわざそうするのか分からない
ERA5を含むほとんどの気象・気候データセットは、規則的な緯度・経度グリッド上に高度に構造化されている
特定地点の時系列だけを取り出すとしても、こうしたデータセットの強みは内在する時空間構造と文脈にあり、点の時系列だけを抽出する目的でないなら、その構造を完全に壊すのはあまり筋が通らない
それどころか点の時系列だけを取り出す場合でも、たとえば海の真ん中の表面温度の時系列のようなものはほとんど使い道がないので、データをかなり思い切って減らしたくなるはず
ERA5のようなデータセットの研究・運用での利用の多くは、Google Public DatasetsのARCO-ERA5のように元の構造を保持したクラウド最適化レプリカを使うほうが適しているように見える
こうしたバージョンは元の構造を保ちつつ、クラウドストレージ上で大規模な並列アクセスに合うようにチャンクを分割している
自分の経歴で見てきたほぼすべてのケースでは、Zarrベースで一般的なチャンク分割をしたアーカイブだけでも、関心を持たれそうな用途の大半には十分高速だった
https://cloud.google.com/storage/docs/public-datasets/era5
ただしrabernatが言っていたように、クラウド上のレプリカから長い時系列を取り出すのも遅い
最終的には、1940〜1980年のチリの夏季気温の99パーセンタイルを計算するような複雑な時空間クエリもやりたい
クラウドレプリカのほうが速い可能性があることは疑っていないが、予算$0とは相いれない
空間クエリには最適化されているが、時系列クエリには最適化されていない
たった今ベンチマークを回したところ、ある1地点の単一変数の時系列を取得するのに20分かかった
想定される利用パターンが時系列なら、時系列に最適化したチャンク分割が必要だということをよく示している
研究室によっては、すでに公開済みのアルゴリズムとデータが絡んだRDBMSベースのパイプラインを持っていて、誰も再実装したがらず、そのための費用もない
私たちが得られた最良の改善は、古いMySQLからPostgres + PostGISへ移行する程度だった
Timescaleも役に立ったと思う
プライバシー、クラスターへのアクセス、予算といった理由で、ローカルで動かさなければならない事情もあった
良い記事
ここで抜けているのは、気象データをリレーショナルデータベースへ移して何が得られるのかについての分析だと思う
動機はクエリ速度を上げることだが、ベースラインが何なのかが必要
XarrayとZarrのメンテナーであり、https://earthmover.io/ の創業者として、この技術領域にはかなり詳しいが、Zarrでデータを適切にチャンク分割すれば、サーバーレスソリューション + オブジェクトストレージだけで、気象データの時系列クエリを1秒未満のレイテンシで処理できる
記事で述べられている30分よりはるかに速い
この記事に出てくるデータロードの難しさを考えると、RDBMSの道へ進む前に、そうした解決策を真剣に評価する価値がある
その前にAPIを付ければクエリのようにもできる
RDBMSアプローチを十分に正当化していなかったという点もその通り
Postgres + Timescaleを選んだ主な理由は、それらを学びたかったことと、ERA5データで遊ぶのが一番面白そうに見えたこと
気象データが挑戦になるほど大きいという魅力もあったように思う
根拠はないが、十分にチューニングしてインデックスを張ったTimescaleDB + PostGISが、1940〜1980年のチリの夏季気温の99パーセンタイルのような複雑な時空間クエリで役に立つのかは気になる
この場合、複数のZarrチャンクを読む必要があるかもしれないため
こうした統計をキャッシュする別テーブルを持つアイデアも気に入っているが、Zarrでもそれほど難しくはない
次はクエリとインデックスをベンチマークする予定なので、もっと分かると思う
大きなチャンクは広い領域の可視化や大きなクエリに向いていて、小さなチャンクは点ベースまたは時系列クエリに向いているという根本的な緊張関係があるように思う
こうした大規模な地理空間データセットを、異なるチャンク分割版として別々に維持することは可能だが、コスト効率はあまり良くない
両方の利点を得るために
kerchunkが使われるという話を聞いたことがあるが、そうするとデータ圧縮の選択肢を失うように思えるし、複雑さもかなり増すと思う複数のユースケースの間で、どうバランスを取るのがよいのか気になる
ここで参照されているPostgres の大量ロードに関するドキュメントに貢献したことがありますが、さまざまな手法をよく概観した記事です。
OpenStreetMap データベースのロードを速くするために、こうした作業はかなりやってきました。最後の公開アップデートの発表は https://www.youtube.com/watch?v=BCMnu7xay2Y にあります。
その後、ハードウェアの進歩、PG15 の GIS 改善、osm2pgsql による middle-way-node-index-id-shift 手法の採用のおかげで、planet セットのロード時間は4時間未満まで下がりました。
著者への提案としては、一部の実験では pg_bulkload と COPY を通じて間接的に WAL 書き込みをなくしています。
Craig Ringer がリンク先の SO 投稿を書いた当時は文書化されていませんでしたが、設定でWAL をそのまま無効にできます。
もちろんクラッシュすれば処理中のテーブルは失われますし、作業に数週間かかるならそういうことも起こり得ます。
ただし時系列データは、ロード構造をうまく組めば最後のチャンクだけを失うようにするのは難しくありません。
大量ロードでは WAL データは実質的に不要です。
クラッシュしたら、ロード済みデータの右端を整理して再開すればよいのです。
WAL やその他のオーバーヘッドを切るために使っている
postgresql.confの設定は次のとおりです。wal_level = minimalmax_wal_senders = 0synchronous_commit = offfsync = offfull_page_writes = offautovacuum = offcheckpoint_timeout = 60min最後に、大きなチャンクでロードする際に vacuum 作業を減らすには、通常は上記のように autovacuum を切り、現在ロード中の日付パーティションの後ろで定期的に
VACUUM FREEZEを実行します。通常の PG の話で、新しいトランザクションが書き込まれたものの、まだ全員には見えていない中間段階をデータベースが気にする処理の一部を省けるようになります。
特にチャンク単位のロードでは WAL データが大量ロードに必須ではないと専門家から聞けてうれしいです。
UPS はまだありませんが、停電なしでデータロードにかかる約20日を乗り切れることを願っています。最悪の場合でも、そのまま続きからやればよさそうです。
OP が著者なら、4年ほど前に自分が行った似た実験も興味深いかもしれません。
同じデータセット、同じ対象、似た目標でした。
https://rdrn.me/optimising-sql/
調査の流れは似ていますが、Timescale ではなく通常の Postgres を使いました。私の構成では、データがすでにメモリ上にあるという前提で、バイナリデータの直接コピーにより COPY より約3倍速くなりました。
バイナリ COPY を試さなかった理由を脚注に残しましたが、基本的には他の人が性能に失望したと言っていたためです。
それでも自分で試してみるべきだと思います。
https://aliramadhan.me/2024/03/31/trillion-rows.html#fn:copy-binary-note
余白注釈を入れた形式もよかったです。
numpy の構造化配列を Postgres バイナリとして書き出す関数を提供してくれて助かりました。以前はこれを突き止められませんでした。
「格子状の気象データにリレーショナルデータベースは向いているのか? 分からないが、やってみれば分かるだろう。」
この姿勢が好きです。
他の主流の技術記事にある「実はですね」的なスタイルとは正反対で、好感が持てます。
その道のりを追う間、読者を最後まで引きつけておくところもよいです。
かなり検索しましたが、自分のユースケースに対する決定的な答えは見つからなかったので、自分でベンチマークを回してみることにしました。
興味深い記事です。
「継続的に毎秒約462k挿入なら、約7540億行に約20日かかるが、悪くないようだ。この記事を書くのにかかった時間より短い。」という部分が笑えます。
私もより長く深いブログ記事を書く方向に傾いているので、思った以上に多くの労力がかかるという点に共感します。
SQL クエリから直接、時系列チャートや複数のチャートを描きたいなら、qStudio は無料の SQL IDE で、TimescaleDB を含む複数の DB で動作します。
https://www.timestored.com/qstudio/database/timescale
免責: 私が作ったツールです。
Timeplus 対応を追加できるのではないかと思います。
Timeplus は ClickHouse ベースのストリーミングファーストなデータベースで、コア DB エンジンである Timeplus Proton はオープンソースです。
qStudio もオープンソースで Java 製なので、新しい RDBMS 対応には JDBC ドライバが必要そうです。
その通りなら、Timeplus Proton には ClickHouse ドライバをベースにストリーミング用途向けの修正を加えたオープンソースの JDBC ドライバがあります。
https://www.timeplus.com/
https://github.com/timeplus-io/proton
https://github.com/timeseries/qstudio
https://github.com/timeplus-io/proton-java-driver
まだデータ挿入をしただけですが、近いうちにクエリとプロットを試す予定です。
私も ERA5再解析データを使っており、高速な時系列が必要です
データは選択した期間ごとに積み上がった
[lat, lon]グリッド、たとえば[1か月分の時間別データ, lat, lon]の形で届くため、20年以上が欲しい場合は巨大な行列転置問題になります私のやり方は、各 netCDF ファイルをダウンロードし、転置してから
[lat, lon, hour]で構成された巨大な 3D HDF ファイルに入れるというものです私のワークステーションでは、1つの変数の1年分を作るのに約30分かかりますが、その後で単一の
(lat, lon)位置を取得するのはミリ秒単位です最初に苦労して、長期的に得をする方法です
単純ですが、私はデータベースの専門家ではなく、ただの気候学者です
ここでコメントしている rabernat と open-meteo も似た方法を使っており、高速だと見ているようです