Categories
程式開發

當小內存遇上大量數據,你該怎麼解決這個問題?


當小內存遇上大量數據,你該怎麼解決這個問題? 1

當你寫了一個處理數據的軟件,它可能在小樣本文件上運行地很好,但一旦加載大量真實數據後,這個軟件就會崩潰。

問題在於你沒有足夠的內存——如果你有16GB的RAM,你就無法一次載入100GB大小的文件。載入這麼大的文件時,操作系統在某個時刻就會耗盡內存,不能分配存儲單元,你的程序也就會崩潰。

所以,你該怎樣防止這類情況發生?你可以啟動一個大數據集群——你所需要做的是:

  • 搞到一個計算機集群。
  • 花一周時間搭建這個集群。
  • 大部分情況下,你需要學習一套全新的API,重寫你所有的代碼。

這個代價可能很昂貴,會令人沮喪;幸運的是,大部分情況下,我們不必這樣做。

你需要一個簡單而容易的解決方案:在單機上處理你的數據,最小化環境搭建開銷,盡可能利用你正在使用的代碼庫。實際上,大部分情況你都可以做到這樣,只要使用一些方法即可,有時候這些方法被稱為“核外計算”(out-of-core computation)。

本文將介紹如下內容:

  • 你究竟為什麼需要RAM。
  • 處理無法放入內存的數據最簡單的方法:花些錢。
  • 處理大量數據的三種基本軟件方法:壓縮、分塊、索引。

之後的文章將會展示如何把這些方法應用到諸如NumPyPandas這樣的庫中。

你究竟為什麼需要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。但是,如果這個方案無法實現或者仍然不夠用時,你就需要使用壓縮、分塊或者索引來解決。

這些方法也出現在其他許多不同的軟件包和工具中。即使是大數據系統也是建立在這些方法之上的:例如使用多個計算機來處理分塊數據。

在接下來的文章裡,我會給你展示如何使用具體的庫和工具(NumPy、Pandas,或者用ZIP工具壓縮文件)來應用這些方法。如果在我寫完這些文章時,你就想閱讀到它們,請在下方表格里註冊我的通訊簡報(newsletter)。

原文鏈接:

When your data doesn’t fit in memory: the basic techniques