Ruby の標準 CSV パーサーは柔軟で使いやすい一方、パフォーマンス面における課題があります。
今回、1 ファイルあたり 100 万件以上のデータを含む多数の CSV ファイルを並列かつ高速に処理する必要があり、ETL パイプラインを構築して APM でトレースしたところ、2 つの明確なボトルネックが浮かび上がってきました。
データ加工過程におけるメモリの圧迫
データの加工処理は AWS Batch で行っており、CSV ファイルを読み込んだ後に整形・加工し、OpenSearch へ書き込む構成をとっていました。
この際、大量のレコードをメモリ上に保持したまま処理するため、コンテナのメモリ使用量が著しく増加し、OOM に近い状態となるケースも見られました。
この問題に関しては、エフェメラルストレージに加工したデータを一時ファイルとして分割保存しながら処理する方式を導入することで、無事に解決できました。
標準 CSV パーサーのパース速度
さらに深掘りしてみると、バッチ起動から完了時間までの大半が CSV ファイルの読み込みに集中していることがわかりました。
ビジネスロジックやデータベースへの書き込み部分よりも、読み込み処理に大きな時間が費やされていたのです。
詳しく調査したところ、Ruby 標準の CSV クラスは柔軟で堅牢な一方で、コード自体が全て Ruby で実装されているため、処理速度が遅くなりがちという根本的な特徴があることが分かりました。
Ruby 標準の CSV パーサーに関する「遅い」という指摘は、検索してみると複数の事例が見つかります。
本件ではサーバーレスアーキテクチャを採用しており、秒単位で処理時間を短縮できれば、コスト削減とスループット向上に直結するため、より高速な代替パーサーの導入を検討することにしました。
下表に、Ruby で提供される代表的な CSV パーサーのベンチマーク結果をまとめます (2025 年 7 月時点)。
条件は、「1 レコード 125 カラム × 100 万件 (1GB 超)」の CSV ファイルを each で 1 行ずつ読み込んで処理が完了するまでの時間を計測したものです。
ライブラリ | 実行速度 (秒) | 累計 DL 数 (rubygems.org) | 最終コミット | コメント |
---|---|---|---|---|
標準 CSV クラス | 80.55 | – | – | パーサーとしては堅牢だが極めて遅い |
rcsv | 18.95 | 1,846,148 | 2013/9 | C 拡張。安定性は高いが更新は停止中 |
osv | 9.91 | 10,197 | 2025/3 | Rust 製。最近登場した高性能パーサー |
fastcsv | 8.23 | 212,655 | 2025/6 | Ragel ベースの C 拡張。日本語未対応 |
fastest-csv | 5.82 | 336,340 | 2013/8 | C 拡張。最速レベルだが更新は停止中 |
結果、標準クラスで 80.55 秒かかる処理が fastest-csv
を使うことで 5.82 秒まで改善しました。これは実に 13倍以上の高速化 に相当します。
ちなみに Go で同様の処理を書いたところ 4.34 秒という結果になりました。Ruby の C 拡張でも十分に Go に迫るパフォーマンスを出せることが分かります。
Ruby の標準 CSV パーサーは堅牢で信頼性の高いライブラリであるものの、大量のデータを高速に処理する用途では明確なボトルネックとなります。
特にサーバーレス環境や大規模データパイプラインでは、C 拡張 や Rust 製パーサーの導入を検討してみるのも良いかと思います。
計測で使った Ruby 標準クラスと Go で書き直したコードを乗せておきます。
require 'csv'
i = 1
start_time = Time.now
CSV.foreach('./cur.csv') do |_row|
i += 1
end
end_time = Time.now
puts i
puts end_time - start_time
package main
import (
"encoding/csv"
"fmt"
"os"
"time"
)
func main() {
file, _ := os.Open("data.csv")
defer file.Close()
reader := csv.NewReader(file)
i := 1
start := time.Now()
for {
_, err := reader.Read()
if err != nil {
break
}
i++
}
end := time.Now()
fmt.Println(i)
fmt.Println(end.Sub(start).Seconds())
}
Views: 0