巨大なログファイルを集計・分析する際、いきなり本番データを使うのは少し怖い。そこで、本番と同じフォーマットのダミーデータを大量生成できるスクリプトを作成した。
今回は、その実装過程で遭遇した並列処理・メモリ管理・Jupyter Notebook 特有の問題と、その対処方法をメモとして残しておく。
生成結果
今回の設定では、3日分で約7.2億件のアクセスログを生成した。
- レコード数:約7.2億件
- ファイルサイズ:約32GB
- 生成時間:約31分
- 平均件数:約1,000万件/時間
実行環境は以下の通り。
- Windows 11
- Ryzen 7 5825U(8コア)
- メモリ 16GB
- NVMe SSD 512GB
手元の M5 MacBook Pro の方が速そうだが、今回は職場に置いてきていたため、自宅の Windows マシンで実施した。そろそろ自宅環境も強化したいところだが、最近のPC価格はなかなか厳しい。
Pythonでダミーアクセスログを生成する
最初は特に工夫せず実装していたが、7億件規模になると生成だけで3時間近くかかりそうだった。また、CPU・メモリ・ディスクI/Oを無駄に消費し、その間ほかの作業がしづらい。
そこで、
- CPUはマルチプロセスで活用する
- メモリ使用量を抑える
- ディスクI/Oの無駄を減らす
という方針で改善を進めた。
要件
生成するログの仕様は以下の通り。
カラム
- timestamp
- method
- path
- status_code
- response_time_ms
ログ特性
- 12:00〜13:00をピークとするアクセス分布
- 4xx / 5xx エラー率は約0.01%
- ごく低確率で異常値(スパイク)を混在
- 1時間あたり平均10万件〜1,000万件規模
- 1日1ファイルのCSVを指定日数分出力
第1段階:素直な実装
最初は時間帯ごとにログを生成し、CSVへ追記していく単純な実装にした。
def generate_day_log(date, rng, output_dir): for hour in range(24): count = int(round(AVG_REQUESTS_PER_HOUR * hourly_weights[hour])) df = generate_hour_rows(day_start + timedelta(hours=hour), count, rng) df.to_csv( filename, mode="a" if hour > 0 else "w", header=(hour == 0), index=False )
行生成自体は NumPy / pandas によるベクトル化を利用しているため、Python の for ループで1件ずつ生成するよりは十分高速だった。
1時間あたり数万〜数十万件程度なら、この方式でも快適に動作する。
レスポンスタイムやステータスコードは、それぞれ異なる分布を持たせている。
# 正常レスポンス vals = rng.lognormal(mean=2.9, sigma=0.6, size=n_success) # 5xx vals = rng.uniform(500, 5000, size=n_server) # スパイク vals = rng.uniform(lo, hi, size=n_spike)
また、スパイクはエラー率とは独立した確率で発生させるようにした。
そのため、
- status=200 だが異常に遅いケース
- status=500 かつ異常に遅いケース
の両方が混在する。
第2段階:ProcessPoolExecutorによる並列化
リクエスト数が増えると生成時間も比例して増える。
そこで8コアCPUを活用するため、時間帯単位で並列処理を導入した。
concurrent.futures.ProcessPoolExecutor
ここで注意した点は2つある。
1. 乱数の再現性
並列処理では各ワーカーが独立した乱数生成器を持つ必要がある。
単純に同じシードを使うと、実行順序によって結果が変わってしまうため、
seed = base_seed + day_index * 24 + hour
のように、日付と時間帯から決定的にシードを生成した。
これにより、
- 並列度が変わっても
- 実行順序が変わっても
常に同じログを再生成できる。
2. ファイル内の順序
各時間帯は並列実行されるため、完了順序はバラバラになる。
そのため一度結果を集め、
futures = {
executor.submit(_hour_task, t): i
for i, t in enumerate(tasks)
}
results = [None] * 24
for future in as_completed(futures):
hour = futures[future]
results[hour] = future.result()
day_df = pd.concat(results, ignore_index=True)
時間帯順に並べ直してからCSVへ出力した。
この時点では問題なく動作していた。
第3段階:Jupyter Notebook対応
実行環境を Jupyter Notebook に移したところ、別の問題に遭遇した。
ProcessPoolExecutor は、ワーカー関数を pickle 化して子プロセスへ送る必要がある。
しかし Notebook のセル内で定義した関数は、子プロセスから import できず失敗することがある。
特に Windows や macOS の spawn 方式では発生しやすい。
対処方法はシンプルだった。
ロジック本体を独立したモジュールへ切り出す。
# Notebook import log_generator_lib as lg lg.AVG_REQUESTS_PER_HOUR = 10_000 lg.main()
これにより、Notebook上でも安定して並列実行できるようになった。
第4段階:「リソースを使っているのに遅い」問題
AVG_REQUESTS_PER_HOUR を1,000万に引き上げると、CPU・メモリ・ディスクI/Oはフル稼働しているのに、思ったほど速度が出ない。
これは、
「リソース使用率が高い」
「効率的に動いている」
とは限らない典型例だった。
実際にはメモリ不足によりページングやスワップが発生していた可能性が高い。
原因
問題は以下の2点だった。
1. ワーカーが巨大なDataFrameを返していた
各ワーカーは最大1,800万行を一括生成し、
future.result()
で親プロセスへ返していた。
2. メインプロセスが全結果を保持していた
さらに24時間分のDataFrameを保持し、
pd.concat()
で結合していた。
つまり、
- DataFrame生成
- pickle化
- プロセス間転送
- concat
を巨大データで繰り返していたことになる。
16GBメモリではかなり厳しい。
実証:OOMで落ちる
仮説を検証するため、
df = generate_hour_rows(
datetime(2026, 6, 1, 12),
18_000_000,
rng
)
を実行してみた。
空きメモリ約3.6GBの環境では、
Killed (exit code 137)
で強制終了。
OOM Killer による停止だった。
設計そのものに無理があったと言える。
第5段階:チャンク処理への変更
そこで設計を見直した。
1. チャンク単位で生成
1時間分を一括生成せず、
CHUNK_SIZE = 500_000
単位で生成して即座に書き出す。
while remaining > 0: n = min(CHUNK_SIZE, remaining) df = generate_hour_rows( hour_start, n, rng ) df.to_csv(...)
2. ワーカーが直接ファイルへ出力
各ワーカーはDataFrameを返さない。
代わりに担当ファイルへ直接書き込む。
メインプロセスが受け取るのは件数だけ。
これにより、
- pickle
- プロセス間通信
- 巨大オブジェクト転送
を完全に排除できた。
3. 一時ファイルを結合
最後に各時間帯のCSVを結合する。
with open(final_filename, "wb") as out_f: ...
2つ目以降はヘッダー行だけスキップする。
この方式では完全な時刻順は保証されないが、時間帯ブロック順は維持される。
今回はその点を許容した。
検証結果
同じ1,800万行を新方式で実行した結果は以下の通り。
- ピークメモリ使用量:約354MB
- 実行時間:約52秒
旧方式は同条件でOOMにより停止していたため、大幅な改善となった。
メモリ使用量は1桁以上削減できている。
教訓
今回得られた教訓は次の通り。
- リソース使用率が高いことと効率的な処理は別問題
- メモリ不足によるスワップはCPU・ディスクI/O使用率だけでは見抜きにくい
- ProcessPoolExecutor ではプロセス間通信コストも考慮する必要がある
- Jupyter Notebook での並列処理はモジュール分離が安全
- 大規模バッチ処理ではチャンク処理が非常に有効
特に、
「巨大オブジェクトを作ってから処理する」
のではなく、
「小さく作ってすぐ捨てる」
という発想は、多くのデータ処理で応用できると思う。
まとめ
今回は「ダミーログを生成するだけ」の小さなスクリプトだったが、改善を重ねていく中で、
- 並列処理
- メモリ管理
- プロセス間通信
- Jupyter Notebook固有の制約
といった、実務でも頻繁に遭遇するテーマを一通り経験できた。
特に印象的だったのは、
「CPUもディスクもフル稼働しているのに遅い」
という現象の正体が、CPU性能ではなくメモリ設計だったことだ。
大量データを扱う処理では、アルゴリズムだけでなくデータの持ち方や受け渡し方も性能に大きく影響する。
今回のスクリプトは、そのことを改めて実感する良い題材になった。






