前阵子帮某业务组处理一个跟文件拆分有关的需求他们要从生产环境导出一张月度明细CSV 足足 12GB。拿去做 Excel 透视Excel 直接罢工想一次性导入数仓跑了大半个下午还在几亿行里打转更糟的是有人直接双击想在电脑里预览笔记本风扇当场起飞。折腾到最后所有人的共识其实特别朴素——先把文件按规矩拆开分而治之。这篇文章想把“拆文件”这件事彻底说清楚。它包含两部分一部分是方法论比如什么时候按大小拆、什么时候按时间拆、拆完怎么命名、怎么摆目录另一部分是工具和命令比如 Linux 下的split、日志场景里的logrotate以及用 Python 快速定制的拆分脚本。无论你是做运维、做数据开发还是写业务代码时顺手处理大文件看完应该都能直接照着干。另外这篇不会只讲成功路径。拆分看着简单真出事时往往出在三个地方拆完的数据能不能正确合回去、拆出来的每个分片能不能独立被消费、以及三天后别人拿到这些文件还认不认识它们属于谁。这几个问题下面都会展开。1. 拆文件之前先把这几个问题想明白1.1 不同的痛点对应不同的拆分维度我见过太多“为了拆而拆”的方案。有人拿到大文件第一反应就是按大小咔咔切成几块结果下游要按天消费发现每块文件里日期全都混在一起也有人一上来就按时间切结果单日数据太大某个分片照样几 GB处理端还是吃不消。所以动手之前请先回答一个最基础的问题你到底在解决谁的痛点常见的目标大概有四类。第一类是传输限制邮件附件、网盘上传、FTP 工具都有单文件大小上限这时候你按字节大小拆最直接合并时顺序拼接即可。第二类是工具算力限制Excel 单表有行数上限内存分析脚本处理几十 GB 会崩这时候必须按行数拆保证每个分片都能被现有工具独立打开。第三类是并行消费你有四台机器、八个 worker想把一个大任务摊开跑这时候既要考虑行数也要考虑数据是否均匀甚至要按某个业务键分桶。第四类是生命周期管理日志要保留 30 天、离线数据要按月归档这种场景必须按时间切因为你要保证“删除某段数据”这个动作能按目录或文件名轻松完成。把这四类摆在一起你会发现拆分维度不是拍脑袋选的它取决于数据将来怎么被使用。给下游 worker 用的文件不能拆得连一行完整记录都没有给归档系统用的文件不能拆得让“按天删”变成痴人说梦。1.2 三种主流拆法按大小、按行数、按时间按大小、按行数、按时间是文件拆分里最基础的三个维度。我把它们的适用场景和典型代价整理成了一张表每次做方案前我都会先过一遍这张表拆分维度优点缺点典型场景按大小字节分片体积均匀和传输通道限额天然匹配文本文件可能切断记录分片不可独立解析大文件传输、备份分发、压缩包分卷按行数每条记录完整分片可独立消费实际字节大小可能差异很大行内字段不齐时更复杂CSV、日志、数据库导出后的并行处理按时间文件边界与业务周期对齐方便清理归档高峰时段单个分片仍然可能很大日志滚动、按日/月分区的数据同步除了这三种还有一个容易被忽略的思路叫“按业务键分桶”。比如你想按用户维度拆分交易明细可以把用户 ID 做哈希映射到 N 个桶里。这样同一个用户的所有记录一定落在同一个分片里下游做关联、做去重都方便。这个方案在分布式数据同步里很常见这里先不展开但你要知道除了横着切、竖着切还有“按归属切”这条路。1.3 先想好“怎么合”再动手拆我做文件拆分这么多年最想强调的一条经验是任何一个拆分方案都必须同时定义合并协议。哪怕你笃定这些分片永远不会合并也要知道分片之间是什么关系否则后续会被反复问“这些文件能不能拼回去”。对顺序敏感的数据比如增量日志、交易明细你在拆分的时候就得把全局顺序信息放进去要么靠文件名里的序号要么靠每行数据里的时间戳。对顺序不敏感的数据比如视频、普通压缩包每个分片独立合并只是简单的顺序拼接。如果分片会分发到不同地点、最后还要汇总更要在拆之前统一 schema、编码方式和字段顺序不要等到分片落地了再后悔。拆和合必须对称。用split拆的就该用cat按相同顺序合用脚本按行数拆的合回去的行顺序必须和原文件一致。很多“拆完合不回去”的事故根源都是拆的时候对排序规则想得太少。2. 大小与行数拆分一条 split 命令能解决的别写脚本2.1 最常用的 split 参数组合Linux 自带的split命令是处理“按大小拆、按行数拆”最趁手的工具。绝大多数场景一行命令就搞定完全不需要自己造轮子。先看三个最常用的组合# 按字节大小拆每个分片 100MB split -b 100M big_export.bin part_ # 按行数拆每个分片 20 万行 split -l 200000 big_export.csv chunk_ # 数字后缀 固定三位保证排序稳定 split -d -a 3 -l 200000 big_export.csv data_这里有几个参数需要认真理解。-b表示按字节大小-l表示按行数这个不用多说。真正关键的是后缀规则split默认生成的是aa、ab、ac这种字母后缀而且在某些环境里字母后缀的排序规则和你的直觉并不一致。所以我强烈建议任何时候都用-d指定数字后缀再用-a 3固定后缀位数得到data_000、data_001这样的结果。位数要提前估算如果分片数可能超过 1000就把-a改成4或者更大别等到生成了data_1000才意识到后缀宽度不够。还有一个 GNU 扩展参数叫--additional-suffix可以给每个分片自动补充扩展名比如split -d -a 3 -l 200000 big.csv data_ --additional-suffix.csv生成data_000.csv。不过要注意macOS 上自带的 BSD 版本split并不支持这个参数如果你要写跨平台的脚本要么放弃这个参数要么改用 Python。2.2 多行一拆时表头文件怎么处理这是新手最容易忽略的问题。split -l会把第一行也就是表头当成普通行处理结果第一个分片有表头后面所有分片都没有表头。下游把这些分片分别导入数据库时字段名对不上整批数据全乱。我常用的处理办法是把表头单独拎出来再进行先拆后补# 1. 把表头单独存下来 head -n 1 big_export.csv header.csv # 2. 从第二行开始按行拆分不保留表头 tail -n 2 big_export.csv | split -d -a 3 -l 200000 - data_ # 3. 给每个分片补上表头 for f in data_000 data_001 data_002; do cat header.csv $f ${f}.csv done注意这一步生成的文件扩展名可能是残缺的因为split默认不给后缀。实际操作时我会在循环里统一处理成.csv再删掉中间文件避免目录里出现一堆没有扩展名的数据文件。还有一个更隐蔽的坑如果 CSV 字段本身包含换行符也就是被引号包裹的多行字段那么“按物理行拆”会把一条逻辑记录拦腰劈开。这种情况就不能再用split -l了得用 Python 的csv模块按逻辑记录去拆或者用一个能感知引号状态的数据处理框架。给一个小脚本骨架import csv import sys src big_export.csv chunk_size 200000 with open(src, r, encodingutf-8, newline) as f: reader csv.reader(f) header next(reader) chunk [header] idx 0 for row in reader: chunk.append(row) if len(chunk) chunk_size: with open(fpart_{idx:04d}.csv, w, encodingutf-8, newline) as out: writer csv.writer(out) writer.writerows(chunk) chunk [header] idx 1 if len(chunk) 1: with open(fpart_{idx:04d}.csv, w, encodingutf-8, newline) as out: writer csv.writer(out) writer.writerows(chunk)2.3 二进制文件拆分后如何无损还原二进制文件没有“行”的概念所以只能按字节拆。这个时候数字后缀的排序问题就不再是锦上添花而是必须项。举个例子把一个 8GB 的备份包拆成 4GB 的分片split -d -a 3 -b 4G backup.tar.gz segment_ cat segment_000 segment_001 restore.tar.gz md5sum backup.tar.gz restore.tar.gz还原时要用cat严格按照segment_000、segment_001的顺序拼接任何顺序错误都会导致文件损坏。而且注意千万不能用cat segment_*一锅端除非你能百分之百确定这个前缀只有这一批文件。我见过有人把两个批次的备份都命名为segment_前缀合并时直接混到一起最后压缩包损坏数据差点找不回来。还原之后一定要用md5sum或sha256sum做校验不要只比较文件大小。大小相同不代表字节一致这种低级错误在文件传输场景里太常见了。3. 时间维度拆文件日志滚动里的门道3.1 为什么日志一定要“主动滚”而不是等它爆如果说按行拆分主要解决“处理不动”的问题那么按时间拆分主要解决“清理不掉”的问题。日志文件如果只写不切会一路膨胀到磁盘爆掉。到那时候你面对的不是优雅的归档而是一台写不进任何新数据的机器以及一个几 GB 到几十 GB、连tail都卡半天的巨型日志。而且日志还有一个特殊性它天生就是按时间顺序追加的。所以对日志做时间维度的切割几乎不会破坏任何业务逻辑反而天然形成“按天归档、按天可查”的结构。业界普遍的做法是“边写边滚”也就是在服务运行期间就按天或按大小自动切换文件而不是等磁盘满了再手动处理。自己写 cron 脚本盯文件大小、到阈值就复制加清空这种做法我不是很推荐。它容易出三个问题复制大文件占用大量 IO影响业务写入清空时机和写入进程冲突可能丢数据脚本里往往没有压缩策略和保留周期时间一长磁盘照样崩。所以多数服务器场景直接上系统自带的logrotate更省心。3.2 可以照抄的 logrotate 配置块logrotate是绝大多数 Linux 发行版都自带的日志滚动工具由cron或systemd timer定期触发。下面这个配置块是我在项目里用得最多的模板/data/app/logs/app.log { daily rotate 30 compress delaycompress dateext dateformat -%Y%m%d missingok notifempty copytruncate }每个配置项背后都有原因。daily表示每天滚动一次实际执行时机取决于系统里logrotate被调度的时刻通常在凌晨。rotate 30表示只保留最近 30 个滚动文件更早的自动删除这就是“保留周期”的落地方式。compress会把滚动后的旧日志压缩成.gz显著省磁盘。delaycompress表示延迟一个周期再压缩这个非常有用刚被滚出去的旧文件可能还被进程占用着而且压缩是吃 IO 的操作放到下一次轮转时做对业务影响更小。dateext和dateformat让滚动后的文件名变成app.log-20250115而不是app.log.1、app.log.2。数字后缀一旦轮转次数超过一位排序就会乱而且看到app.log.27你根本不知道这是哪天的日志。missingok表示日志文件不存在时不报错notifempty表示文件为空就不滚动这两个都是避免产生大量无意义操作和空文件的。3.3 copytruncate 与 create 的取舍以及丢数据风险logrotate的滚动方式有两种理解它们的区别直接从根上决定了你会不会丢日志。默认的create模式是这样工作的先把app.log重命名为app.log-20250115再新建一个空的app.log。看起来没问题但前提是写日志的进程能感知到日志文件被换走了并主动重新打开新文件。很多服务靠接收 SIGHUP 信号来重新打开日志例如某些 Web 服务器和 Java 日志框架。如果服务不支持重载那么重命名之后它持有的文件描述符仍然指向旧文件后续所有日志都会继续写进那个“历史文件”你以为在切割实际上新日志全跑到了老文件里。copytruncate模式则不同它先把当前文件复制一份作为历史文件再把原文件清空。原文件的 inode 没有变写日志的进程持有的文件描述符仍然有效所以不需要服务做任何配合。但它有一个绕不开的缺陷复制和清空之间存在一个时间窗口这个窗口里写入的日志既不在复制出去的副本里也可能在清空操作中被丢掉。流量越大窗口内丢的越多。我的经验是这样如果服务能够重新打开日志文件优先用create如果服务做不到只能copytruncate同时接受少量丢失的现实。如果你的日志属于强审计要求、一条都不能丢那就别在文件层面纠结了让应用把日志写到标准输出交给容器或统一采集系统去处理。无论选哪种还有一个容易踩的坑滚动策略改了之后文件名规则就变了。下游的采集、监控、分析脚本如果还在用旧模式匹配app.log.*新生成带日期的文件可能根本匹配不上导致“看似有日志其实没人接”。文件名约定和下游消费必须一起改。4. 拆分产物的命名与目录约定我踩过的最贵一坑4.1 文件名里的信息排布与零填充逻辑聊完命令和工具该聊聊比工具更重要的东西命名规范。这一节是我最有感触的部分因为我在这上面真金白银地踩过一个巨坑。有一年做历史数据迁移我把每个月导出的文件命名为affair_202501_0.csv、affair_202501_1.csv一直排到affair_202501_10.csv。结果下游同事用通配符合并时系统按字典序展开顺序变成了_0、_1、_10、_2、_3……数据全部错位。查了一下午才发现是排序问题。从那以后我定下死规矩序号必须零填充等宽宁可多写几位也绝不让排序产生歧义。一份合格的拆分文件命名至少包含四个要素数据主题、时间、序号、格式。正例是user_events_20250115_0001.csv反例是第1批用户事件(最终版).csv。空格、中文、括号在 shell 和脚本里全都要转义而“最终版”这种词对机器没有任何可读信息。时间格式统一用20250115这种纯数字不要用01-15-2025、2025.01.15之类的变体目的很单纯让字典序等于时间序。序号固定四位比如0001位数要比你预估的最大分片数至少多一位。顺手再推荐一个低成本高回报的习惯拆完之后在文件旁边生成一个.md5校验文件。文件只要跨机器、跨网络校验和就能告诉你它在路上有没有被改过。4.2 按时间分区摆放目录的工程价值单个文件名再规范如果几百个文件全堆在一个目录里也是灾难。所以目录结构本身就是拆分方案的一部分。我见过两种最常见也最好用的模式。第一种是普通文件系统的层级目录比如data/2025/01/15/user_events_0001.csv或者logs/2025/01/15/app.log。这么做的直接好处是删除过期数据变成“删目录”归档压缩变成“按目录打包”排查问题时可以直接跳到某一天的目录而不用在几千个文件里翻。第二种是数据仓库和大数据平台里更常见的分区列模式比如data/dt2025-01-15/hour10/data_0001.csv。这种keyvalue结构的好处是下游引擎能自动识别分区列做分区裁剪查询只扫必要目录。即使你不用大数据平台只是把 CSV 按日归档我也建议在目录上显式标出分区字段因为它是机器可读的比纯数字层级更不易错。还有一个隐藏的效率问题单个目录下文件数量超过几千个之后ls、通配符展开、对象存储列举都会明显变慢。按时间拆目录不只是为了整洁是实打实的性能需求。我自己处理日均上亿行的数据时通常按天甚至按小时拆目录保证每个目录下文件数在一个可控量级。4.3 多任务并发拆分时的防冲突设计很多拆分需求不是单机一个进程跑完的而是分布式环境里多个 worker 同时在拆。这种场景下命名和文件写入的冲突问题会被放大。首先是写文件隔离。每个 worker 必须独占一个前缀或一个目录绝对不能多个 worker 往同一个文件里追加。哪怕只是日志两个进程同时 append 同一个文件虽然单行小于管道缓冲时不会立刻崩但一旦交错就会出现半行垃圾数据。正确的做法是每个 worker 用自己的唯一前缀比如worker01_、worker02_或者各自的子目录。其次是原子性。拆分任务里下游通常会在目录里扫描新文件。如果 worker 把半成品文件直接写到正式目录下游一旦扫描到就会读到只有一半内容的文件。我习惯先写临时文件比如worker01_data.tmp全部写完后统一mv成正式名。mv在同一个文件系统里是原子操作下游扫描时要么看不到要么看到的就是完整文件不会读到切开一半的中间状态。再就是全局唯一性。分片文件的命名如果在整个作业空间里不唯一合并时就可能把两个批次的内容混在一起。固定前缀加 worker 编号加零填充序号比如job20250115_worker01_part_001.csv基本能杜绝这种冲突。5. 五个高频事故和一套兜底校验流程5.1 多字节字符被拦腰截断之后讲一个很典型的命令级事故。UTF-8 编码下一个中文字符占 3 个字节。split -b 100M只认字节边界完全不知道它切到的地方是不是字符边界。如果你只是要把分片合并回去字节切法本身是可逆的cat回去就完全一致但如果每个分片要被单独处理比如单独导入数据库、单独打开预览你就会遇到最后一行乱码或者编码错误。这里有一条铁律需要独立消费的文本文件绝对不要用-b去切用-l按行切。如果业务上必须按大小切而且每个分片都要独立可用那就用能感知字符流的语言去处理或者干脆先把文本压缩再对压缩包按字节切——压缩包没有编码问题怎么切都行。5.2 合并顺序错误与通配符陷阱通配符展开和排序规则是一个很隐蔽的陷阱。cat data_*看起来人畜无害但 Shell 通配符默认按字典序展开。如果序号不等宽data_2就会排在data_10前面合并出来的文件顺序全错。这个问题我在 4.1 里已经吃了大亏这里的建议是两条文件名序号必须零填充等宽合并时不要赌通配符的排序用明确范围或者先sort再拼接。另外注意分片前缀冲突。假如两批不同的拆分作业都叫segment_同时出现在同一个目录里你执行cat segment_*的时候两批数据就混在一起了。加作业编号、加时间戳、加批次号目的就是让前缀全局唯一。文件拆分这种操作任何歧义最终都会变成数据事故。5.3 拆分完必跑的六项检查拆分不是执行完命令就结束了。按我现在的习惯任何一次批量拆分之后都会跑一套固定校验相当于给文件拆分加一道质检流水线。可能是环境原因也可能是人为疏忽但校验能兜住绝大多数低级错误。编号检查项方法通过标准1分片数与计划一致统计分片文件数量和每个文件的行数与预算值完全相等2逻辑内容不丢源文件行数和各分片行数之和严格比对表头只算一次严格相等3合并可逆把分片按正确顺序拼接后做md5sum比对校验和一致4每个分片可独立读取用目标工具逐个打开或解析无报错、无乱码5文件结尾完整检查最后一个字符是换行符无截断行检查通过6命名可稳定排序按文件名排序后与业务顺序比对字典序即业务序其中第 2 项和第 3 项是最重要的一个保证“内容没少”一个保证“拼得回去”。后面四项是给下游的消费方兜底。做数据分发的人最怕的就是拆出来的东西“看着挺多其实各有残缺”。现在我自己做任何批量拆分都会先花十分钟把三件事确认掉拆分维度选对没有、合并协议定义清楚没有、校验清单能不能自动化跑。这三件事想明白了后面的一堆命令和脚本都只是执行细节。至于那些“先拆了再说”的方案我这些年看到的结局基本都是在某个凌晨接到数据对不上的电话。