diff --git a/_includes/arrow-result-transfer-series-japanese.md b/_includes/arrow-result-transfer-series-japanese.md new file mode 100644 index 000000000000..8918e30bdad6 --- /dev/null +++ b/_includes/arrow-result-transfer-series-japanese.md @@ -0,0 +1,23 @@ + + +シリーズの記事: + +1. [Apache Arrowフォーマットはどのようにクエリー結果の転送を高速にしているのか]({% link _posts/2025-01-10-arrow-result-transfer-japanese.md %}) +1. [データは自由になりたい:Apache Arrowで高速データ交換]({% link _posts/2025-03-10-data-wants-to-be-free-japanese.md %}) diff --git a/_includes/top.html b/_includes/top.html index f31f984f8d22..6e35df6c713d 100644 --- a/_includes/top.html +++ b/_includes/top.html @@ -1,5 +1,5 @@ - + diff --git a/_posts/2025-01-10-arrow-result-transfer-japanese.md b/_posts/2025-01-10-arrow-result-transfer-japanese.md index b9722a9eb683..3545b297c93e 100644 --- a/_posts/2025-01-10-arrow-result-transfer-japanese.md +++ b/_posts/2025-01-10-arrow-result-transfer-japanese.md @@ -35,6 +35,8 @@ limitations under the License. _この記事はデータベースとクエリーエンジン間のデータ交換フォーマットとしてなぜArrowが使われているのかという謎を解くシリーズの最初の記事です。_ +{% include arrow-result-transfer-series-japanese.md %} + 「どうしてこんなに時間がかかるの?」 diff --git a/_posts/2025-02-28-data-wants-to-be-free.md b/_posts/2025-02-28-data-wants-to-be-free.md index 0d64be85ef00..f75a41eaa2df 100644 --- a/_posts/2025-02-28-data-wants-to-be-free.md +++ b/_posts/2025-02-28-data-wants-to-be-free.md @@ -9,6 +9,9 @@ image: path: /img/arrow-result-transfer/part-1-share-image.png height: 1200 width: 705 +translations: + - language: 日本語 + post_id: 2025-03-10-data-wants-to-be-free-japanese --- + + + +_この記事はデータベースとクエリーエンジン間のデータ交換フォーマットとしてなぜArrowが使われているのかという謎を解くシリーズの2記事目です。_ + +{% include arrow-result-transfer-series-japanese.md %} + +データ技術者として、データが「人質に取られている」とよく感じます。 +データをもらってもすぐに使うことはできません。使えるようになるまでに時間がかかります。 +非効率的でやっかいなCSVファイルを整理する時間だったり、 +型落ちのクエリエンジンが数GBのデータに苦労するのを待つ時間だったり、 +データがソケットを介して受信するのを待つ時間をだったり。 +今回はこの三番目の問題に注目します。 +マルチギガビットネットワークの時代に、そもそもこの問題がまだ起こっているのはどうしてでしょうか? +間違いなく、この問題はまだ起こっています。 +[Mark RaasveldtとHannes Mühleisenの2017年の論文](https://doi.org/10.14778/3115404.3115408)[^freepdf]では、いくつかシステムは10秒しかかからないはずのデータセットの送受信に**10分**以上かかっていると指摘しています[^ten]。 + +[^freepdf]: [VLDB](https://www.vldb.org/pvldb/vol10/p1022-muehleisen.pdf)から論文を無料でダウンロードできます。 +[^ten]: 論文のFigure 1では、ベースラインのnetcatは10秒でCSVファイルを送信し、HiveとMongoDBは600秒以上かかっていること示しています。もちろん、CSVは解析されていないので、この比較は完全に平等ではありません。しかし、問題の規模を把握できます。 + +どうして必要な時間より60倍以上も長い時間がかかるのでしょうか? +[この前に論じていた通り、ツールはデータシリアライズのオーバーヘッドに悩まされています。]({% link _posts/2025-01-10-arrow-result-transfer.md %}) +しかし、この問題はArrowで解消できます。 +それではもっと具体的な話をしましょう。データシリアライズフォーマットの影響を示すために、PostgreSQLとArrowが同じデータをどうやってエンコードするのかを比較しましょう。 +その後、Arrow HTTPやArrow FlightなどのArrowベースのプロトコルを作る色々な方法を説明し、各方法の使い方も説明します。 + +## PostgreSQL対Arrow:データシリアライズ + +[PostgreSQLのバイナリーフォーマット](https://www.postgresql.jp/document/current/html/sql-copy.html#id-1.9.3.55.9.4)と[Arrow IPC](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc)を同じデータセットに比較します。 +この比較で、Arrowは(後知恵のおかげで)前任者より適切のトレードオフを行うのを証明します。 + +PostgreSQLでクエリを実行すると、クライアント(すなわちドライバ)はPostgreSQLの通信プロトコルでクエリを送り、結果を受けます。 +そのプロトコルの内に、結果セットはPostgreSQLのバイナリーフォーマットでエンコードされています[^textbinary]。 + +[^textbinary]: テキストフォーマットもあります。クライアントはそのフォーマットをほとんど使っています。この記事でテキストフォーマットを論じません。 + +まず、テーブルを定義し、テーブルにデータを入力します。 + +``` +postgres=# CREATE TABLE demo (id BIGINT, val TEXT, val2 BIGINT); +CREATE TABLE +postgres=# INSERT INTO demo VALUES (1, 'foo', 64), (2, 'a longer string', 128), (3, 'yet another string', 10); +INSERT 0 3 +``` + +それでCOPYコマンドでPostgreSQLから生バイナリーデータをファイルにダンプします。 + +``` +postgres=# COPY demo TO '/tmp/demo.bin' WITH BINARY; +COPY 3 +``` + +そして[文書](https://www.postgresql.jp/document/current/html/sql-copy.html#id-1.9.3.55.9.4)の通り、データの実際のバイトに注釈を付けます。 + +
00000000: 50 47 43 4f 50 59 0a ff  PGCOPY..  COPYの署名、フラグフィールド、
+00000008: 0d 0a 00 00 00 00 00 00  ........  ヘッダ拡張領域長
+00000010: 00 00 00 00 03 00 00 00  ........  次の行の値の数
+00000018: 08 00 00 00 00 00 00 00  ........  次の値長
+00000020: 01 00 00 00 03 66 6f 6f  .....foo  
+00000028: 00 00 00 08 00 00 00 00  ........
+00000030: 00 00 00 40 00 03 00 00  ...@....
+00000038: 00 08 00 00 00 00 00 00  ........
+00000040: 00 02 00 00 00 0f 61 20  ......a 
+00000048: 6c 6f 6e 67 65 72 20 73  longer s
+00000050: 74 72 69 6e 67 00 00 00  tring...
+00000058: 08 00 00 00 00 00 00 00  ........
+00000060: 80 00 03 00 00 00 08 00  ........
+00000068: 00 00 00 00 00 00 03 00  ........
+00000070: 00 00 12 79 65 74 20 61  ...yet a
+00000078: 6e 6f 74 68 65 72 20 73  nother s
+00000080: 74 72 69 6e 67 00 00 00  tring...
+00000088: 08 00 00 00 00 00 00 00  ........
+00000090: 0a ff ff                 ...       ストリームの終わり
+ +正直なところ、PostgreSQLのバイナリーフォーマットは一見すると結構わかりやすくてコンパクトです。 +このフォーマットは連続したフィールドだけです。 +各フィールドの前に、値長が置かれています。 +しかし、もっとよく見てみると、問題が明らかになります。 +**PostgreSQLのバイナリーフォーマットはオーバーヘッドが行と列の数に比例します。** + +* 各行の前に、行内の値の数(2バイト)が置かれています。*しかし、データは表形式ですから、値の数はもうわかっています。それに、値の数は変わりません!* +* 各行内の値の前に、フィールド長(4バイト)が置かれています。(NULLの場合、−1です。)*しかし、ほとんどのデータ型では値が固定長だし、データ型をわかっているし、データ型は変わらないし、フィールド長は大概もうわかっています!* +* すべての値はビッグエンディアンです。*しかし、現代の機器はほとんどリトルエンディアンですから、エンディアン交換が必要です。* + +例えば、一つのint32の列の場合に、各行に4バイトのデータと6バイトのオーバーヘッドがあります。 +つまり、**60%は無駄にされています!**[^1] +列が増えれば増えるほど、オーバーヘッドの比率が減ります。 +(しかし、行が増えればオーバーヘッドが変わりません。) +極限において、50%オーバーヘッドに近づきます。 +エンディアン交換は高価な操作ではありませんが、それでも必要です。 +もちろん、PostgreSQLは称賛に値するところもあります。 +バイナリーフォーマットは安価で解析しやすいです。 +[他のフォーマット](https://protobuf.dev/programming-guides/encoding/)は「varint」エンコードなどの技術を使っています。 +こういう技術は結構高価です。 + +Arrowはどうでしょうか? +[ADBC](https://arrow.apache.org/adbc/current/driver/postgresql.html)でPostgreSQLテーブルを読み込み、そして前の通りにデータに注釈を付けます。 + +```console +>>> import adbc_driver_postgresql.dbapi +>>> import pyarrow.ipc +>>> conn = adbc_driver_postgresql.dbapi.connect("...") +>>> cur = conn.cursor() +>>> cur.execute("SELECT * FROM demo") +>>> data = cur.fetchallarrow() +>>> writer = pyarrow.ipc.new_stream("demo.arrows", data.schema) +>>> writer.write_table(data) +>>> writer.close() +``` + +
00000000: ff ff ff ff d8 00 00 00  ........  IPCメッセージ長
+00000008: 10 00 00 00 00 00 0a 00  ........  IPCスキーマ
+⋮         (208バイト)
+000000e0: ff ff ff ff f8 00 00 00  ........  IPCメッセージ長
+000000e8: 14 00 00 00 00 00 00 00  ........  IPCレコードバッチ
+⋮         (240バイト)
+000001e0: 01 00 00 00 00 00 00 00  ........  1つ目の列のデータ
+000001e8: 02 00 00 00 00 00 00 00  ........
+000001f0: 03 00 00 00 00 00 00 00  ........
+000001f8: 00 00 00 00 03 00 00 00  ........  文字列のオフセット
+00000200: 12 00 00 00 24 00 00 00  ....$...
+00000208: 66 6f 6f 61 20 6c 6f 6e  fooa lon  2つ目の列のデータ
+00000210: 67 65 72 20 73 74 72 69  ger stri
+00000218: 6e 67 79 65 74 20 61 6e  ngyet an
+00000220: 6f 74 68 65 72 20 73 74  other st
+00000228: 72 69 6e 67 00 00 00 00  ring....  アラインメントのためのパッディング
+00000230: 40 00 00 00 00 00 00 00  @.......  3つ目の列のデータ
+00000238: 80 00 00 00 00 00 00 00  ........
+00000240: 0a 00 00 00 00 00 00 00  ........
+00000248: ff ff ff ff 00 00 00 00  ........  IPCストリームの終わり
+ +一見すると、Arrowは結構わかりにくいです。 +データセットに全然関係なさそうなヘッダーもあるし、 +まるで領域を占有するためにだけそうで謎のパッディングもあるし。 +しかし大事なのは、**オーバーヘッドが固定です**。 +1行でも1億行でも、オーバーヘッドが変わりません。 +それに、PostgreSQLと違って**値ごとの解析は必要ありません**。 + +Instead of putting lengths of values everywhere, Arrow groups values of the same column (and hence same type) together, so it just needs the length of the buffer[^header]. Overhead isn't added where it isn't otherwise needed. Strings still have a length per value. Nullability is instead stored in a bitmap, which is omitted if there aren’t any NULL values (as it is here). Because of that, more rows of data doesn’t increase the overhead; instead, the more data you have, the less you pay. + +[^header]: That's what's being stored in that ginormous header (among other things)—the lengths of all the buffers. + +Even the header isn’t actually the disadvantage it looks like. The header contains the schema, which makes the data stream self-describing. With PostgreSQL, you need to get the schema from somewhere else. So we aren’t making an apples-to-apples comparison in the first place: PostgreSQL still has to transfer the schema, it’s just not part of the “binary format” that we’re looking at here[^binaryheader]. + +[^binaryheader]: And conversely, the PGCOPY header is specific to the COPY command we executed to get a bulk response. + +There’s actually another problem with PostgreSQL: alignment. The 2 byte field count at the start of every row means all the 8 byte integers after it are unaligned. And that requires extra effort to handle properly (e.g. explicit unaligned load idioms), lest you suffer [undefined behavior](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p7), a performance penalty, or even a runtime error. Arrow, on the other hand, strategically adds some padding to keep data aligned, and lets you use little-endian or big-endian byte order depending on your data. And Arrow doesn’t apply expensive encodings to the data that require further parsing. So generally, **you can use Arrow data as-is without having to parse every value**. + +That’s the benefit of Arrow being a standardized data format. By using Arrow for serialization, data coming off the wire is already in Arrow format, and can furthermore be directly passed on to [DuckDB](https://duckdb.org), [pandas](https://pandas.pydata.org), [polars](https://pola.rs), [cuDF](https://docs.rapids.ai/api/cudf/stable/), [DataFusion](https://datafusion.apache.org), or any number of systems. Meanwhile, even if the PostgreSQL format addressed these problems—adding padding to align fields, using little-endian or making endianness configurable, trimming the overhead—you’d still end up having to convert the data to another format (probably Arrow) to use downstream. + +Even if you really did want to use the PostgreSQL binary format everywhere[^3], the documentation rather unfortunately points you to the C source code as the documentation. Arrow, on the other hand, has a [specification](https://github.com/apache/arrow/tree/main/format), [documentation](https://arrow.apache.org/docs/format/Columnar.html), and multiple [implementations](https://arrow.apache.org/docs/#implementations) (including third-party ones) across a dozen languages for you to pick up and use in your own applications. + +Now, we don’t mean to pick on PostgreSQL here. Obviously, PostgreSQL is a full-featured database with a storied history, a different set of goals and constraints than Arrow, and many happy users. Arrow isn’t trying to compete in that space. But their domains do intersect. PostgreSQL’s wire protocol has [become a de facto standard](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html), with even brand new products like Google’s AlloyDB using it, and so its design affects many projects[^4]. In fact, AlloyDB is a great example of a shiny new columnar query engine being locked behind a row-oriented client protocol from the 90s. So [Amdahl’s law](https://en.wikipedia.org/wiki/Amdahl's_law) rears its head again—optimizing the “front” and “back” of your data pipeline doesn’t matter when the middle is what's holding you back. + +## 矢筒Arrowプロジェクトたち + +So if Arrow is so great, how can we actually use it to build our own protocols? Luckily, Arrow comes with a variety of building blocks for different situations. + +* We just talked about [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc) before. Where Arrow is the in-memory format defining how arrays of data are laid out, er, in memory, Arrow IPC defines how to serialize and deserialize Arrow data so it can be sent somewhere else—whether that means being written to a file, to a socket, into a shared buffer, or otherwise. Arrow IPC organizes data as a sequence of messages, making it easy to stream over your favorite transport, like WebSockets. +* [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http) is “just” streaming Arrow IPC over HTTP. The Arrow community is working on standardizing it, so that different clients agree on how exactly to do this. There’s examples of clients and servers across several languages, how to use HTTP Range requests, using multipart/mixed requests to send combined JSON and Arrow responses, and more. While not a full protocol in and of itself, it’ll fit right in when building REST APIs. +* [**Disassociated IPC**](https://arrow.apache.org/docs/format/DissociatedIPC.html) combines Arrow IPC with advanced network transports like [UCX](https://openucx.org/) and [libfabric](https://ofiwg.github.io/libfabric/). For those who require the absolute best performance and have the specialized hardware to boot, this allows you to send Arrow data at full throttle, taking advantage of scatter-gather, Infiniband, and more. +* [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html) is a fully defined protocol for accessing relational databases. Think of it as an alternative to the full PostgreSQL wire protocol: it defines how to connect to a database, execute queries, fetch results, view the catalog, and so on. For database developers, Flight SQL provides a fully Arrow-native protocol with clients for several programming languages and drivers for ADBC, JDBC, and ODBC—all of which you don’t have to build yourself. +* And finally, [**ADBC**](https://arrow.apache.org/docs/format/ADBC.html) actually isn’t a protocol. Instead, it’s an API abstraction layer for working with databases (like JDBC and ODBC—bet you didn’t see that coming), that’s Arrow-native and doesn’t require transposing or converting columnar data back and forth. ADBC gives you a single API to access data from multiple databases, whether they use Flight SQL or something else under the hood, and if a conversion is absolutely necessary, ADBC handles the details so that you don’t have to build out a dozen connectors on your own. + +要するに、 + +* If you’re *using* a database or other data system, you want [**ADBC**](https://arrow.apache.org/adbc/). +* If you’re *building* a database, you want [**Arrow Flight SQL**](https://arrow.apache.org/docs/format/FlightSql.html). +* If you’re working with specialized networking hardware (you’ll know if you are—that stuff doesn’t come cheap), you want the [**Disassociated IPC Protocol**](https://arrow.apache.org/docs/format/DissociatedIPC.html). +* If you’re *designing* a REST-ish API, you want [**Arrow HTTP**](https://github.com/apache/arrow-experiments/tree/main/http). +* And otherwise, you can roll-your-own with [**Arrow IPC**](https://arrow.apache.org/docs/format/Columnar.html#serialization-and-interprocess-communication-ipc). + +![A flowchart of the decision points.]({{ site.baseurl }}/assets/data_wants_to_be_free/flowchart.png){:class="img-responsive" width="100%"} + +## まとめ + +既存のクライアントプロトコルは効率が悪いです。 +Arrowの方はより良い効率が可能になり、昔のデザイン上の落とし穴も回避できます。 +それにArrowには色々な標準を使えば、データAPIを簡単に作れます。 +例えば、Arrow IPC、Arrow HTTP、ADBCなどを使えます。 +プロトコルの内にArrowを使えば、皆にデータやりとりをもっと高速で簡単になるメリットがあります。 +そしてデータを低速の悪くて低速のインタフェースに人質に取られないようになります。 + +--- + +[^1]: もちろん、完全に無駄にされていません。NULLかでないかもデータとされています。しかし一貫しているために長、パッディング、ビットマップなどをオーバーヘッドとして数えます。 + +[^2]: それに、データAnd if your data really benefits from heavy compression, you can always use something like Apache Parquet, which implements lots of fancy encodings to save space and can still be decoded to Arrow data reasonably quickly. + +[^3]: [そういう人実際にいます…](https://datastation.multiprocess.io/blog/2022-02-08-the-world-of-postgresql-wire-compatibility.html) + +[^4]: [We have some experience with the PostgreSQL wire protocol, too.](https://github.com/apache/arrow-adbc/blob/ed18b8b221af23c7b32312411da10f6532eb3488/c/driver/postgresql/copy/reader.h)