查看: 278|回复: 0

Python大日志文件过滤:逐行、mmap与多进程对

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在处理 GB 级应用日志时,常见需求是从中快速提取 ERROR 或特定 trace_id。原文以 2GB 左右日志为案例,对比 Python 的逐行流式读取、mmap 内存映射和多进程分块并行三种过滤方案,并给出实测耗时与内存数据。下面按代码实现、边界处理和性能结果重新整理。

一、逐行流式读取:baseline

最直接的写法是 open() 配合 for line in f,逐行扫描,把包含关键字的行写入目标文件。逻辑简单可靠,适合日志规模不大或对速度不敏感的场景。示例:
  1. def filter_stream(log_path, keyword, out_path):
  2.     with open(log_path, 'r', encoding='utf-8', errors='ignore') as fin, open(out_path, 'w') as fout:
  3.         for line in fin:
  4.             if keyword in line:
  5.                 fout.write(line)
复制代码

每次迭代都会在 Python 层做字符串解码和关键字匹配。原文实测 2GB 日志耗时约 45 秒,内存占用约 20MB,主要来自缓冲。问题是文件越大,Python 层循环和匹配开销越明显。

二、mmap + 内存视图:减少系统调用和 Python 循环

优化思路是用 mmap 把文件映射到虚拟内存,避免频繁 read 系统调用;再通过 bytes.find() 定位关键字,并配合 memoryview 切片输出匹配行。核心示例:
  1. import mmap
  2. def filter_mmap(log_path, keyword, out_path):
  3.     keyword_b = keyword.encode()
  4.     with open(log_path, 'rb') as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm, open(out_path, 'wb') as fout:
  5.         start = 0
  6.         while True:
  7.             idx = mm.find(keyword_b, start)
  8.             if idx == -1:
  9.                 break
  10.             # 找到行首和行尾
  11.             line_start = mm.rfind(b'\n', 0, idx) + 1
  12.             line_end = mm.find(b'\n', idx)
  13.             if line_end == -1:
  14.                 line_end = mm.size()
  15.             fout.write(mm[line_start:line_end])
  16.             fout.write(b'\n')
  17.             start = line_end + 1
复制代码

关键点是 mmap.find 由 C 实现,速度远高于 Python 层循环。需要注意边界情况,例如关键字跨行、最后一行没有换行符等。原文实测 2GB 日志耗时 12.8 秒,内存 45MB,内存开销主要来自映射。

三、多进程分块并行:利用多核的终极优化

当单机多核时,可以按字节偏移把文件均分成 N 块,每个进程独立处理一块,最后合并结果。为了避免切块时截断行,需要查找块首的换行符进行对齐。原文给出简化示例:
  1. import os, multiprocessing as mp
  2. def worker(chunk_path, start, end, keyword, out_path):
  3.     with open(chunk_path, 'rb') as f:
  4.         f.seek(start)
  5.         # 对齐到行首(略)
  6.         data = f.read(end - start)
  7.         lines = data.split(b'\n')
  8.         matched = [line for line in lines if keyword in line]
  9.         with open(out_path, 'ab') as fout:
  10.             for line in matched:
  11.                 fout.write(line + b'\n')
  12. def parallel_filter(log_path, keyword, num_workers=4):
  13.     size = os.path.getsize(log_path)
  14.     chunk_size = size // num_workers
  15.     # 创建任务列表(略)
复制代码

实际应用时要处理块边界,可用 seek 定位到下一个换行符。并行方案在 8 核机器上可把 2GB 日志处理时间从 45 秒降到 8 秒。原文还给出并行 mmap(4 进程)耗时 8.1 秒、内存峰值 180MB 的数据。进程数需要控制,否则可能耗尽内存。

四、性能验证与方案选择

原文测试环境为 8 核 CPU、16GB 内存、2.1GB 日志、约 1200 万行。对比结果如下:

流式读取:45.2 秒,内存 18MB
mmap 方案:12.8 秒,内存 45MB(映射开销)
并行 mmap(4 进程):8.1 秒,内存 180MB(峰值)

验证方法:使用 time 命令测耗时,用 /usr/bin/time -v 查看最大常驻内存。选择建议:单机日志过滤优先考虑 mmap,性价比最高;如果追求极致速度且内存充足,可以上多进程;最终应结合日志大小和机器规格决定。并行时尤其要限制进程数,避免内存耗尽。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-20 12:07 , Processed in 0.068341 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部