当你写了一个处理数据的软件,它可能在小样本文件上运行地很好,但一旦加载大量真实数据后,这个软件就会崩溃。
问题在于你没有足够的内存——如果你有 16GB 的 RAM ,你就无法一次载入 100GB 大小的文件。载入这么大的文件时,操作系统在某个时刻就会耗尽内存,不能分配存储单元,你的程序也就会崩溃。
所以,你该怎样防止这类情况发生?你可以启动一个大数据集群——你所需要做的是:
搞到一个计算机集群。
花一周时间搭建这个集群。
大部分情况下,你需要学习一套全新的 API,重写你所有的代码。
这个代价可能很昂贵,会令人沮丧;幸运的是,大部分情况下,我们不必这样做。
你需要一个简单而容易的解决方案:在单机上处理你的数据,最小化环境搭建开销,尽可能利用你正在使用的代码库。实际上,大部分情况你都可以做到这样,只要使用一些方法即可,有时候这些方法被称为“核外计算”(out-of-core computation)。
本文将介绍如下内容:
你究竟为什么需要 RAM。
处理无法放入内存的数据最简单的方法:花些钱。
处理大量数据的三种基本软件方法:压缩、分块、索引。
之后的文章将会展示如何把这些方法应用到诸如 NumPy 和 Pandas 这样的库中。
你究竟为什么需要 RAM?
在我们开始解释解决方案前,我们要弄清楚该问题是如何产生的。我们的计算机内存(RAM)能让你读写数据,但是你的硬盘也可以读写数据——那为什么计算机还需要 RAM 呢?硬盘比 RAM 更便宜,所以它通常大到能够容纳下你的所有数据,那为什么你的代码不能直接从硬盘读写数据呢?
理论上讲,这也行得通的。但是,即使是最现代化且速度很快的 SSD 硬盘也比 RAM 慢太多:
从 SSD 上读取数据:大约 1.6 万纳秒
从 RAM 上读取数据:大于 100 纳秒
如果你想要实现快速计算,数据就只能放在 RAM 中,否则你的代码运行时就会慢上 150 倍。
资金方面的解决方案:购买更多的 RAM
没有足够 RAM 时的最简单解决方案就是花钱来解决。你要么购买一台计算机,或者租一台云端的虚拟机(VM:Virtual Machine,这会比大多数笔记本电脑贵得多)。2019 年 11 月,我稍微调研了一下,在价格方面做了一些比较,发现你可以这样:
购买一台 Thinkpad M720 Tower,它有 6 个核和 64GB RAM,价格是 1074 美金。
租用一台云端的 VM,它有 64 个核和 432GB RAM,价格是每小时 3.62 美金。
这只是我稍微调研后发现的数字,再继续调研下去,你会发现更好的方案。
如果花钱购买硬件可以把你的数据读入 RAM,这通常就会是一个最经济的解决方案:毕竟你的时间相当宝贵。但是,有时候,这还不够解决这个问题。
例如,如果你要运行许多数据处理任务,在一段时期内,云计算可能是一个自然能想到的解决方案,但也是一个昂贵的解决方案。曾经在一个工作中,我的软件项目需要的计算开销几乎快用完了我们产品所有的预期收入,包括支付我薪水所需的至关重要的那部分收入。
如果购买 / 租用更多的 RAM 不足以满足需求或者根本行不通时,下一步就应该考虑如何通过修改软件来减少内存使用了。
技巧#1:压缩
压缩意味着用一种不同的表达形式表示你的数据,这种形式能占用更少内存。有两种方式来压缩:
无损压缩:存储的数据包含的信息和原始数据包含的信息完全相同。
有损压缩:存储的数据丢失了一些原始数据里的细节信息,但是这种信息丢失理想情况下不会对计算结果造成什么影响。
我想说明的是,我不是在谈论使用 ZIP 或者 gzip 工具来压缩文件,因为这些工具通常是对硬盘上的文件进行压缩的。为了处理 ZIP 压缩过的文件,你通常需要把这个文件载入内存中再进行解压缩为原始文件大小,所以这其实对内存节省没有什么帮助。
你需要的是内存中的数据压缩表示形式。
例如,比如说你的数据有两个值,一共也只会有两个值:“AVAILABLE”(代表可能取到的值)和“UNAVAILABLE”(代表不可能取到的值)。我们可以不必将其存为 10 个或更多字节的字符串,你可以将其存为一个布尔值,用 True 或者 False 表示,这样你就可以只用 1 个字节来表示了。你甚至可以继续压缩至 1 位来表示布尔值,这样就继续压缩到了 1 个字节时的 1/8 大小。
技巧#2:分块,每次只加载所有数据里的某一块
当你需要处理所有数据,而又无需把所有数据同时载入内存时,分块是很有用的。你可以把数据按块载入内存,每次计算一块的数据(或者正如我们要在今后一篇文章里想讨论的,可以多块并行处理)。
例如,比如说,你想搜索一本书里的最长单词。你可以一次性将所有数据载入内存:
largest_word = “”
for word in book.get_text().split():
if len(word) > len(largest_word):
largest_word = word
但是在我们的例子中,这本书太大而不能完全载入内存,这时候你就可以一页一页地载入这本书。
largest_word = “”
for page in book.iterpages():
for word in page.get_text().split():
if len(word) > len(largest_word):
largest_word = word
这样你使用的内存就大大减少了,因为你一次只需要把这本书的一页载入内存,而最后得到的结果仍然是正确的。
技巧#3:当你需要数据的一个子集时,索引会很有用
当你需要数据的一个子集时,索引会很有用,使用索引你可以在不同时刻加载数据的不同子集。
你也可以用分块解决这个问题:每次加载所有的数据,过滤掉你不想要的数据。但这会很慢,因为你加载了很多不相关的数据。
如果你只需要部分数据,不要使用分块,最好使用索引,它可以告诉你到哪里能找出你关心的那部分数据。
想象一下,你只想阅读书本中关于土豚的章节。如果你运用分块技术,你得载入整本书,一页一页的载入,每页地搜寻土豚——但这要花很长时间才能完成。
或者说,你可以直接阅读这本书的末尾部分,也就是书本的索引部分,然后找到“土豚”的索引项。它可能会告诉你在第 7、19 页以及 120-123 页可以读到相关内容。所以,现在你可以只读那几页,这样就快多了。
这样很有效,因为索引比整本书占用的空间要小很多,所以把索引载入内存找出相关内容所在就会更容易。
最简单的索引技巧
最简单也最常用的实现索引的方法就是在目录里给文件恰当命名:
mydata/
2019-Jan.csv
2019-Feb.csv
2019-Mar.csv
2019-Apr.csv
…
如果你想要 2019 年 3 月数据,你就只需要加载 2019-Mar.csv 这个文件——而不用加载 2 月、7 月或者其他任何月份的数据。
下一步:应用这些技巧
RAM 不够用,最简单的解决方法就是花钱买更多的 RAM。但是,如果这个方案无法实现或者仍然不够用时,你就需要使用压缩、分块或者索引来解决。
这些方法也出现在其他许多不同的软件包和工具中。即使是大数据系统也是建立在这些方法之上的:例如使用多个计算机来处理分块数据。