less で十分、が崩れる瞬間 — コマンドライン職人芸の限界線4本

技術解説

G を押して、90秒。

less は軽い道具のはずでした。では、その90秒のあいだ、何が読まれていたのでしょう。

答えは道具の出来ではなく、「行の境界」というたった一点にあります。

この連載では毎回、汎用ツールが止まる場所を書いてきました。今回はその汎用ツールのなかでも、いちばん信頼されている4つ——less、Vim、パイプ職人芸、grep——を正面から扱います。どれも壊れてはいません。むしろよくできています。それでも、あるサイズを越えたところで同じ壁に当たる。その壁が「どこにあるか」を、線として引いておくのが今回の目的です。

先に結論: UwView は開いた瞬間から末尾までスクロールでき、索引はバックグラウンドで作られて完成すると行番号が付きます。Pro は索引と圧縮を保存するので、一度開いた47.73GBのログは2回目以降0.02〜0.07秒で行番号付きのまま開き直せます(特定環境での実測。環境により異なります。詳細は記事末尾)

本記事の所要時間・転送速度は、断りのあるものを除きすべて環境依存です。数値は考え方を示すための計算例・測定例であり、あなたの環境で同じ値になることを保証するものではありません。


1. less で足りていたはずが — 行番号ジャンプと日本語検索の壁

状況

less huge.log は一瞬で開きます。ここまでは期待どおりです。

崩れるのは次の操作からです。G を押して末尾へ飛ぼうとすると止まる。-N を付けて行番号を出そうとすると、もっと止まる。12000000g で目的の行へ飛ぼうとすると、また止まる。そして日本語のログで /エラー と打つと、何もヒットしません

less で十分」は、正しい判断だったはずでした。実際、less は数MBのファイルなら他のどんな道具より軽い。

なぜ起きるか

less が速いのは「読まないから」で、遅くなるのは「読まないと答えられない操作をしたから」です。

less はファイルを行のかたまりとして扱いますが、開いた時点で知っているのは「先頭から1画面ぶん」だけです。ファイルの中身は改行文字がどこにあるかで区切られていて、その位置は実際にバイトを読むまで分かりません。「1200万行目の先頭バイトはどこか」という問いにシークだけで答える方法は存在しません。だから:

  • G(末尾へ)= ファイル末尾までのバイトを読んで改行を数える
  • -N(行番号表示)= 表示中の行が何行目かを知るために、そこまでの改行を数える
  • 12000000g(行指定ジャンプ)= 先頭から12000000個目の改行を探す

3つとも、やっていることは同じ「全部読んで改行を数える」です。50GBのファイルなら50GBを読みます。90秒はそのための時間で、less が非効率なのではありません。

日本語の壁は別の理由です。less文字コードの変換をしません。表示できるかどうかを判定するだけです(LESSCHARSET はその判定の設定であって、変換の指定ではありません)。端末が UTF-8 で、ファイルが CP932 で書かれていれば、あなたが /エラー と打った UTF-8 のバイト列は、ファイル中の CP932 のバイト列と一致しません。「無い」のではなく「別のバイト列として在る」——第3回第11回で扱った話が、ここではページャの操作として現れます。

汎用ツールでの対処と限界

定石はあります。

less -n huge.log            # 行番号の計算をやめる → G が軽くなる(行番号は出ない)
LESSCHARSET=utf-8 less huge.log
iconv -f CP932 -t UTF-8 huge.log | less    # 変換してから流す
tail -c 200M huge.log | less               # 末尾だけ切り出して見る

1本目は効きます。ただし取引です。-n は「行番号を計算しない代わりに速い」なので、速さと行番号は同時に手に入りません。障害報告に「12,034,551行目」と書きたい仕事では、結局 -N に戻ることになります。

3本目が、いちばん静かに高くつきます。iconv | less にした瞬間、less が受け取るのはシークできない標準入力です。それでも less は上へ戻れます——なぜなら、流れてきたぶんを一時ファイルに書き出しているからです。つまり /tmp に50GBの第二のコピーが育ちます。しかも先へ進むには、そこまでのバイトを全部通す必要があるので、G は「変換しながら全部読む」になります。原本の行番号との対応は保たれます(変換しても行数は変わりません)が、その行番号を less 側で表示させるコストは元のままです。

4本目の tail -c は実務でよく使う逃げ道ですが、切り出した瞬間、それは原本ではなくなります。先頭が行の途中から始まって文字化けすることがあり、行番号は 1 から振り直されます。

限界を3つにまとめます。ひとつめ、「末尾を見る」「行番号を出す」「行を指定して飛ぶ」の3つが、どれも全読みを要求すること。ふたつめ、その全読みの結果が、less を閉じた瞬間に捨てられること——次に開けば、また90秒です。みっつめ、文字コードの不一致を、変換を挟まずに解決する手段がないこと。そして変換を挟むと、シークとディスク容量の両方を失います。


2. :e huge.log が固まる — Vim側でできる設定、できないこと

状況

vim huge.log と打って、Enter を押して、それきり返ってきません。Ctrl-C も効かない時間帯があります。しばらくして、swap file の警告か、メモリ不足の悲鳴が出ます。

less より Vim を選ぶ理由は、たいてい「検索して、その場で前後を読んで、必要なら印を付けたいから」です。目的は編集ではなく調査であることのほうが多い。

なぜ起きるか

Vim のバッファは「ファイル全体を行の配列として持つ」設計だからです。

これは Vim の欠陥ではなく、エディタとしての正しい設計です。任意の行を挿入・削除・置換できるためには、行の並びを構造として保持している必要があります。だから :e は原則としてファイルを全部読みます。そして読みながら、次のものを作ります。

  • swapfile(.swp: クラッシュ復旧用に、ディスクへもう1コピー
  • undo 履歴: undofile が有効なら、さらにディスクへ
  • syntax highlighting の状態: 構文強調は「この行がコメントの中かどうか」を判定するために前の行を遡ります。長い行・巨大ファイルで急激に重くなるのはここです
  • 折り畳み(fold)の計算: foldmethod=syntax などは全体を舐めます

つまり :e huge.log は、「50GBを読む」+「50GB前後をディスクに書く」+「全行に構文解析をかける」を同時に始めます。90秒どころでは済みません。

汎用ツールでの対処と限界

Vim 側でできることは、実はかなりあります。

vim -u NONE -N huge.log     # vimrc・プラグインを読まずに起動
:syntax off
:set noswapfile
:set noundofile
:set nofoldenable
:set lazyredraw
:set synmaxcol=200          " 長すぎる行の強調をあきらめる
:set viminfo=

LargeFile 系のプラグインがやっているのも、要するにこの自動適用です。効果は本物で、体感は明確に変わります。

それでも減らないものが1つあります。読み込むバイト数そのものです。

上の設定はすべて「読んだあとの処理」を軽くするもので、:e が全部読むという前提には手を触れていません。Vim には部分読み込み(必要な範囲だけをメモリに載せ、残りはディスクに置いたまま扱う)の仕組みがありません。だから、RAM 16GB のマシンで 48GB のファイルを Vim で開くのは、設定の問題ではなく原理の問題です。

そこで実務では切り出します。

sed -n '12000000,12000500p' huge.log > slice.txt && vim slice.txt
awk 'NR>=12000000 && NR<=12000500 {print NR": "$0}' huge.log > slice.txt   # 行番号を残す版

限界は3つ。ひとつめ、切り出す範囲を、切り出す前に決めなければならないこと。500行では足りないと分かるのは開いたあとで、そのたびに sed からやり直し——そして sed -n は目的の行に着くまでファイルを頭から読みます。ふたつめ、素直に切り出すと原本の行番号が消えること(2本目のように NR を前置きすれば残せますが、こんどはその行がテキストとして汚れるので、そのまま報告書へ貼れません)。みっつめ、slice.txt が増えていくこと。調査が終わるころ、/tmp に脈絡のない切れ端が20個並んでいます。どれがどの範囲だったかは、たいてい思い出せません。

なお、これは「Vim を使うな」という話ではありません。編集する仕事なら Vim が正解です。問題は、調査したいだけのときにも Vim を開いてしまい、編集用の全機能ぶんのコストを払っていることです。


3. パイプ職人芸とビューア — 境界線はどこにあるか

状況

tailheadsedawksortuniq を繋いだ1行で、たいていのことは片づきます。実際、片づいてきました。

引っかかるのは、決まってこういう場面です。「何を探せばいいのか、まだ分からない」。エラーメッセージは分かっている。でも、そのエラーが出る条件が分からない。だから grep に何を渡せばいいかが決まらない。

とりあえず head -100 して眺め、awk で列を切り、sort | uniq -c で分布を見て、また戻る。この往復が10回、20回と続きます。

なぜ起きるか

パイプは「問いを先に決めろ」と要求する道具だからです。

awk 'NR>=a && NR<=b' を書くには a と b を知っていなければなりません。grep 'X' を書くには X を知っていなければなりません。ところが調査の前半は、a も b も X も、見ながら決める時間です。問いが決まっていない段階で問いを要求されるので、噛み合いません。

もうひとつ、パイプの構造そのものに由来する性質があります。

  • 中間の状態が残らない: grep A | grep B | grep C を流したあと、Aで何件、Bで何件だったかは残っていません。効いた条件がどれか分からないまま、最終の件数だけを見ることになります
  • 2段目以降で行番号が消える: grep -n が数えるのは「そこへ流れてきたブロックの中の番号」です。原本の座標ではありません
  • やり直しは常に最初から: 条件を1文字変えると、パイプは原本を頭からもう一度読みます

逆に、パイプにしかない強みもはっきりしています。同じ問いを繰り返すのが得意なことです。一度書いた1行は、cron に載り、CI に載り、ssh 越しにそのまま動きます。ログが遠いサーバの中にあって手元へ持ってこられないとき、パイプは唯一の選択肢です(この制約は第4回で扱った「どこに置くか」の話とも地続きです)。

汎用ツールでの対処と限界

境界線を1行で書くなら、こうなります。

答えの「形」が決まっているならパイプ。答えの形をまだ探しているならビューア。

具体的には、こう分かれます。

仕事の性質 向いているもの
列を切って計算する・定型の加工をかける パイプ
毎日・毎回・自動で回す パイプ
リモートのサーバの中で完結させる パイプ
何を探すか探索している ビューア
絞り込んだ結果の中身の傾向を数える どちらでもよい(後述)
ヒット行の前後を読んで判断する ビューア
条件を10回変えながら絞り込む ビューア
見つけた行を原本の座標つきで引用する ビューア

実務では両方使います。よくある流れは「パイプで範囲を絞る → ビューアで読む」です。ただしこの流れには落とし穴があって、「絞る」ために結局1回は全読みしていることに気づきにくい。しかも絞った結果を別ファイルに落とすと、第9回に書いたとおり、そこはもう原本ではありません。

限界を2つに絞ります。ひとつめ、探索フェーズの往復回数ぶん、全読みが積み上がること。1回90秒でも、20往復すれば30分です。ふたつめ、その往復の履歴が端末を閉じると消えること。翌朝、どの条件まで試したかを思い出すところから再開になります。

正直に書いておくと、これは「パイプをビューアで置き換えよう」という話ではありません。対話が要らない仕事は、パイプが正解です。置き換わるのは、対話が要る前半だけです。


4. grep が遅いのではない — I/O律速という天井

状況

grep が遅い。だから rg(ripgrep)に替えてみた。ほとんど変わらなかった。

あるいは逆に、同じコマンドを2回続けて流したら、2回目だけ異様に速かった。何かを間違えた気がして、もう一度測り直した。

なぜ起きるか

遅さの原因が CPU ではなくディスクにあるからです。

固定文字列の検索は、現代の実装では非常に速い部類の処理です。Boyer-Moore 系のアルゴリズムに SIMD 命令が乗り、実質的にメモリ帯域に近い速度で走ります。つまり CPU 側は、ディスクが届けてくるバイトを待っている

読むだけで何秒かかるかは、単純な割り算で下限が出ます。

媒体 おおまかな連続読み出し速度 50GB を読むだけで(理論下限)
HDD 100〜200 MB/s 約 4〜8 分
SATA SSD 約 500 MB/s 約 100 秒
NVMe SSD 2〜7 GB/s 約 7〜25 秒

この表は各媒体の一般的な連続読み出し性能から計算した理論上の下限で、実測ではありません。実際にはファイルシステム、断片化、暗号化、他プロセスの負荷、ネットワーク越し(NFS/SMB)かどうかで大きく変わります。

HDD 上の50GBを検索するとき、どんなに賢い検索プログラムを持ってきても4分より速くはなりませんgreprg に替えても差が出ないのは、当然です。差が出るのは CPU 律速の条件——複雑な正規表現、大量の小さいファイル(rg は並列に走ります)、UTF-8 の正規化を伴う照合——のときだけです。

2回目が速かった話も同じ理屈です。1回目でカーネルのページキャッシュに載ったぶんは、2回目はディスクではなく RAM から読まれます。RAM に載る量までなら劇的に速い。だからベンチマークの数字は、「キャッシュが温まっているかどうか」を書かなければ意味がありません。この連載で実測値に必ず条件を付けているのは、ここが理由です。

汎用ツールでの対処と限界

まず、どちらが律速かを測ります。

time grep -c 'ERROR' huge.log
# real 4m12s / user 0m21s / sys 0m38s  → CPU はほぼ遊んでいる = I/O律速

# その環境の連続読み出しの上限を測る(キャッシュを避けて読む)
dd if=huge.log of=/dev/null bs=1M count=20000 iflag=direct

# 実行中に I/O 待ちを見る
iostat -x 1        # %util が 100 に張り付いていれば飽和
vmstat 1           # wa(I/O待ち)が大きければ同じ

real に対して user + sys が極端に小さければ、CPU は待っています。そこで検索プログラムを替えるのは、渋滞している道路で車を買い替えるようなものです。

I/O 律速と分かったあとにできることは、原理的に3つしかありません。

  1. 速い媒体に置く(HDD → SSD。効果は確実ですが、置き場所を選べないことも多い)
  2. 並列に読む(複数ファイルに分かれていれば効きます。1本の巨大ファイルでは、媒体次第で頭打ちになります)
  3. 読むバイト数を減らす

3つめが本命です。そして「読むバイト数を減らす」には、さらに2つの道があります。ひとつは索引——どこに何があるかを先に記録しておき、次回は全部読まない。もうひとつは圧縮——I/O が律速なら、圧縮された小さいバイト列を読んで CPU で展開するほうが、生を読むより速くなる場合があります(CPU が余っているのだから、そちらに仕事を回す)。第8回の「圧縮したまま検索する」が理にかなっているのは、この計算があるからです。

限界はひとつだけ、しかし重いものです。greprg も、走査した結果を次回に持ち越しません。 4分かけて読み終えた50GBの構造は、コマンドが終了した瞬間に消えます。条件を変えてもう一度打てば、また4分です。ページキャッシュが助けてくれるのは RAM に載るぶんまでで、50GB は載りません。


4つに共通していたもの

道具 何をした瞬間に崩れるか 直接の原因 汎用ツール側の対処 残る問題
less G-N・行指定ジャンプ・日本語検索 行の境界は読まないと分からない -niconv を挟む 速さと行番号が両立しない。変換でシークと容量を失う
Vim :e した瞬間 バッファがファイル全体を前提にしている syntax offnoswapfile・切り出し 読むバイト数は減らない。切り出すと原本の座標が消える
パイプ 探索フェーズに入った瞬間 問いを先に決めることを要求する tee で中間を保存 往復ごとに全読み。段ごとの件数と履歴が残らない
grep ファイルが RAM を越えた瞬間 I/O 律速。CPU は待っている 媒体を替える・並列化 走査結果が次回に持ち越されない

4つの道具はまったく別の設計思想で作られていて、崩れ方も違います。それでも、いちばん右の列が同じ形をしているのは、4つとも「毎回ファイル全体を読み直す」という前提の上に立っているからです。

小さいファイルなら、この前提はコストゼロです。0.1秒の全読みは、無いのと同じです。だから設計としては正しい。壊れるのは、その0.1秒が90秒になったときだけです。操作は何ひとつ難しくなっていません。難しくなっていないのに、割に合わなくなる。この連載でずっと同じことを書いていますが、今回のように道具そのものは正しい場合、原因が見えにくいぶんだけ厄介です。「自分の使い方が悪いのでは」と思って、設定を探しに行ってしまう。

必要なものは3つです。

  • 一度読んだ「行の境界」を捨てないless の90秒も、Vim の :e も、パイプの往復も、毎回同じ改行を数え直しています。1回目に作って保存すれば、2回目からは数え直す必要がありません
  • 読むバイト数を減らす — I/O 律速の下では、これだけが本質的な高速化です。圧縮したまま検索できれば、読む量そのものが減ります
  • 原本の座標を保ったまま、絞り込みを重ねる — 切り出しも iconvsed -n も、通した瞬間に原本ではなくなります。報告書に「何行目」と書く仕事では、これが最後に効いてきます

冒頭の90秒に戻ります。あれは less の遅さではありませんでした。改行を数え直していた時間です。そして数え終えた結果は、q を押した瞬間に捨てられました。


使っている道具

私が開発している UwView(無料)は、巨大なテキストを開いた瞬間から表示・スクロール・検索できるビューアです。ファイル全体をメモリへ読み込まないので、RAM より大きいファイルでも開けます。索引はバックグラウンドで作られ、完成すると行番号が付きます(多くのビューアは索引が終わるまで先頭しか見えません)。分割も切り出しもしないので、原本を1本のまま・無改変のまま扱えます。第1章の G にあたる末尾への移動も、開いた直後からできます。

  • 文字コードは開いたまま切り替える: UTF-8 / Shift-JIS(CP932) / EUC-JP / UTF-16 を自動判定し、外れたときは索引を作り直さずにその場で切り替えられます。第1章の「iconv を挟むとシークと容量を失う」が起きません
  • 編集機能を持たない: Vim の swapfile・undo・構文解析にあたるコストが、そもそも発生しません。第2章の設定リストは不要です(編集そのものが要る仕事には、別ライセンスの Edit Upgrade があります)
  • 前後の行数は後から変える: 何行の文脈が要るかを、開く前に決める必要がありません。第2章の「500行では足りないと分かってから sed をやり直す」が消えます(無料版は±1行、可変の±N は Pro)
  • 検索結果は独立したポップアップに分離され、そこから原本の行へ飛べます(フィルタPopupの記事)。第3章の「往復のたびに端末へ戻る」の代わりになります

そして UwView Pro が持っているのが、上の3つの必要条件そのものです。

  • 索引と圧縮を保存する: 一度開いたファイルは、2回目以降行番号付きのまま瞬時に開き直せます(47.73GBのテキストで実測0.02〜0.07秒。特定環境での測定例で、環境により異なります)。第1章の「q を押した瞬間に捨てられる90秒」と、第4章の「走査結果が持ち越されない」が、同じ1つの機能で解けます
  • 圧縮キャッシュ経由で検索する: 第4章の計算どおり、I/O が律速なら読むバイト数を減らすのが本質的な高速化です。約1/9で保管しつつ検索できるので、ディスクと待ち時間の両方に効きます(圧縮保管と検索の両立
  • 多段階検索(ドリルダウン): 絞り込んだ結果をさらに別の語で絞れます。タブに 語(件数) が並ぶので、第3章の「段ごとに何件減ったかが残らない」が解決します。2段目以降は前段の範囲だけが対象なので待ちがなく、条件を戻すのは前のタブをクリックするだけ。原本の行番号は最後まで保持されます多段階検索の記事
  • 翌日そこから再開できる: タブと条件が残るので、第3章の「端末を閉じると履歴が消える」が起きません(アーカイブ×セッション復元の実務フロー

第3章の表で「パイプ側」に置いた仕事のうち、頻度の集計だけは境界をまたいでいます。Pro の多段階検索には v1.5.0 で「集計」が入っていて、grep ... | grep -oE '...' | sort | uniq -c | sort -rn にあたる操作——ヒット行に正規表現を当て、第1キャプチャ群をキーに頻度順のランキングを出す——を、絞り込んだ段の上でそのまま実行できます。件数・種類数・所要時間が出て、CSV にも落とせます(uniq -c もGUIへ)。第3章で「段ごとに何件減ったかが残らない」と書いた不満の、もう一段先です。

ただしできるのは頻度ランキングまでで、合計・平均などの統計もグラフ描画もありません。そこは分析ツールの領分です。

そのうえで正直に書いておくと、UwView はシェルの代わりにはなりません。任意の列を切って計算すること、複数ファイルを突き合わせること、cron や CI で定期実行することはしません。リモートのサーバの中で完結させたいときは、パイプが唯一の答えです。第3章の境界線は集計のぶんだけ動きましたが、大枠はそのまま生きていて、この道具が担うのは右側の列——探索・文脈・絞り込み・引用——が中心です。定型の加工と自動化は、今日も awk が正しい。

もうひとつ正直に。初回の完全な索引作成には物理時間がかかります。 第4章の I/O 律速は UwView にも同じようにかかるので、1回目に50GBを読む時間そのものが消えるわけではありません。違うのは、その結果を保存して2回目以降に使うかどうかです。1回しか開かないファイルなら、less で十分という判断は、今日も正しいままです。崩れるのは、同じファイルを何度も開き直す日からです。

そして、ストレージを専有している巨大ログを圧縮して保管し、さらに高速に検索したいなら UwView Pro をどうぞ。永続索引・圧縮キャッシュ検索・約1/9保管で、開き直しも検索も一段速くなります(全OS対応・買い切り/月額プランあり)。

リンク

  • 第1回: 巨大ファイルに沈む4つの定番ツールと、その先: https://uvp.y42u.net/blog/uwview-ps01-huge-file-tool-limits/
  • 第3回: 文字コード4つの罠と切り分け手順: https://uvp.y42u.net/blog/uwview-ps03-japanese-encoding-traps/
  • 第4回: 消す・残す・圧縮するの判断基準: https://uvp.y42u.net/blog/uwview-ps04-log-retention-decision/
  • 第8回: 圧縮保管と検索の両立: https://uvp.y42u.net/blog/uwview-ps08-compressed-archive-search/
  • 第9回: 構造化データを生のまま読む4場面: https://uvp.y42u.net/blog/uwview-ps09-read-raw-structured-data/
  • 第10回: 深夜2時の自分を助ける4つの仕込み: https://uvp.y42u.net/blog/uwview-ps10-oncall-night-preparation/
  • 第11回: レガシー文字コードを2026年に読む: https://uvp.y42u.net/blog/uwview-ps11-legacy-encoding-euc-utf16/
  • 第14回: 攻撃の痕跡は生ログにある: https://uvp.y42u.net/blog/uwview-ps14-attack-traces-raw-logs/
  • 3GB/10GB/48GB × HDD/SSD/内蔵SSD の9マス比較: https://uvp.y42u.net/blog/uwview-pro-benchmark-3sizes/
  • klogg と正直に比べてみた(Ver1.1.1・完全版): https://uvp.y42u.net/blog/uwview-klogg-feature-comparison-v111/
  • 絞り込んだ先からさらに絞り込む: https://uvp.y42u.net/blog/uvp-drilldown-search/
  • uniq -c もGUIへ(多段階検索の「集計」): https://uvp.y42u.net/blog/uvp-tally-uniq-c-drilldown/
  • 検索結果を独立ウィンドウに分離した理由: https://uvp.y42u.net/blog/uwview-filter-popup-jump-save-context/
  • アーカイブ×セッション復元の実務フロー: https://uvp.y42u.net/blog/uwview-archive-session-restore-workflow/
  • ソースコード(GitHub): https://github.com/amru195704/UwView

開発者より: アプリ・Kindle本・オープンソースの一覧は GitHub: amru195704 にまとめています。


お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。less・Vim・grepawk 等の挙動は、実装(GNU/BSD/busybox)・バージョン・ビルドオプション・ロケール設定により異なります。オプション名や効果は必ず手元の man で確認してください。記事中の転送速度と所要時間は、断りのあるものを除き一般的な性能値からの計算例であり、実測値ではありません。実測と明記した数値も特定の1環境での測定例であり、同じ結果を保証するものではありません。ディスクの種類・ファイルシステム・断片化・暗号化・ページキャッシュの状態・同時に動いている他のプロセスにより、結果は大きく変わります。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。

タイトルとURLをコピーしました