那一年,小白剛從學校畢業,學的是計算機專業。最開始他也不清楚本身想要一份怎樣的工做,只知道本身先找個互聯網公司乾乾技術再說。javascript
有一天,小白來到一家剛成立不久的小創業公司參見面試。公司雖小,但團隊倒是華麗麗的。兩位創始人都是MIT的MBA,Co-CEO。他們號稱,公司的運營、財務、市場以及銷售人員,都是從大公司高薪挖過來的。此外,他們還告訴小白,公司另有一位政府背景深厚但不肯透露姓名的神祕股東加盟。html
咱們手頭有百萬美金的風險投資,團隊也基本到位。如今萬事俱備,就差一位程序員了。java
小白日常在學校裏就很喜歡閱讀那些創業成功的勵志故事,對於能拿到風險投資的人特別崇拜。git
大家要作的是什麼產品?小白問道。程序員
這個涉及到咱們的創意,暫時保密。但能夠告訴你的是,咱們要作一款偉大的產品,它將顛覆整個互聯網行業。兩位CEO神祕地回答。github
隨後,他們又補充道,面試
咱們計劃在兩年內上市。編程
小白聽完不由心潮澎湃,隨後就入職了這家公司。網絡
公司已經有了一個能運行的網站系統,小白天天的工做就是維護這個網站。工做並不忙,日常只是讀讀代碼,改改bug。架構
忽然有一天,其中一位CEO讓小白統計一下網站的數據,好比日活躍(Dayliy Active Users)、周活躍(Weekly Active Users)、月活躍(Monthly Active Users),說是要給投資人看。
小白想了想,他手頭現有的一份基礎數據,就是訪問日誌(Access Log)。訪問日誌是天天一個文件,每一個文件裏面每一行的數據格式以下:
[時間] [用戶ID] [操做名稱] [其它參數...]複製代碼
比方說要統計日活躍,就要把一個文件(對應一天)裏出現一樣用戶ID的行進行去重,去重後的文件行數就是日活躍數據了。周活躍和月活躍與此相似,只是要分別在一週和一個月內進行去重。
小白那時只會寫Java程序,因此他寫了個Java程序來進行統計:
逐行讀取一個文件的內容,同時內存裏維護一個HashSet用於作去重判斷。每讀一行,解析出用戶ID,判斷它在HashSet裏是否存在。若是不存在,就把這個用戶ID插入HashSet;若是存在,則忽略這一行,繼續讀下一行。一個文件處理完以後,HashSet裏面存放的數據個數就是那一天對應的日活躍數據。
一樣,統計周活躍和月活躍只須要讓這個程序分別讀取7天和30天的文件進行處理。
今後之後,兩位CEO時不時地來找小白統計各類數據。小白知道,他們最近頻繁地出入於投資圈的各類會議和聚會,大概是想給公司進行第二輪融資。
每次看到數據以後,他們都一臉難以置信的表情。沒有統計錯誤吧?咱們就這麼點兒用戶嗎?
小白竟無言以對。
轉眼間一年時間過去了。小白髮現,他們與上市目標的距離跟一年前一樣遙遠。更糟糕的是,公司以前融到的錢已經花得差很少了,而第二輪融資又遲遲沒有結果,小白上個月的工資也被拖欠着沒發。因而,他果斷辭職。
截止到小白辭職的那一天,公司的日活躍數據也沒有超過四位數。
小白的第二份工做,是一家作手機App的公司。
這家公司的技術總監,自稱老王。在面試的時候,老王據說小白作過數據統計,二話沒說把他招了進來。
小白進了公司才知道,CEO之前是作財務出身,很是重視數據,天天他本身提出的各類大大小小的數據統計需求就不下十幾項。
小白天天忙着寫各類統計程序,處理各類數據格式,常常加班到晚上十一二點。並且,更令他沮喪的是,不少統計需求都是一次性的,他寫的統計程序大部分也是隻運行過一次,之後就扔到一邊再也用不到了。
有一天,他正在加班統計數據。老王走過來,發現他用的是Java語言,感到很驚訝。老王跟小白一塊兒分析後指出,大部分數據需求呢,其實均可以從訪問日誌統計出來。而處理文本文件的日誌數據,用Shell腳本會比較方便。
因而,小白猛學了一陣Shell編程。他發現,用一些Shell命令來統計一些數據,好比日活躍,變得很是簡單。以某一天的訪問日誌文件"access.log"爲例,它每行的格式以下:
[時間] [用戶ID] [操做名稱] [其它參數...]複製代碼
只用一行命令就統計出了日活躍:
cat access.log | awk '{print $2}' | sort | uniq | wc -l複製代碼
這行命令,使用awk把access.log中的第二列(也就是用戶ID)過濾出來,而後進行排序,這樣就使得相同的用戶ID挨在了一塊兒。再通過uniq命令處理,對相鄰行進行去重,就獲得了獨立用戶ID。最後用wc命令算出一共有多少行,就是日活躍。
寫了一些Shell腳本以後,小白慢慢發現,使用一些簡單的命令就能夠很快速地對文件數據集合進行並、交、差運算。
假設a和b是兩個文件,裏面每一行看做一個數據元素,且每一行都各不相同。
那麼,計算a和b的數據並集,使用下面的命令:
cat a b | sort | uniq > a_b.union複製代碼
交集:
cat a b | sort | uniq -d > a_b.intersect複製代碼
這裏uniq命令的-d參數表示:只打印相鄰重複的行。
計算a和b的差集稍微複雜一點:
cat a_b.union b | sort | uniq -u > a_b.diff複製代碼
這裏利用了a和b的並集結果a_b.union,將它與b一塊兒進行排序以後,利用uniq的-u參數把相鄰沒有重複的行打印出來,就獲得了a和b的差集。
小白髮現,不少數據統計均可以用集合的並、交、差運算來完成。
首先,把天天的訪問日誌加工一下,就獲得了一個由獨立用戶ID組成的集合文件(每行一個用戶ID,不重複):
cat access.log | awk '{print $2}' | sort | uniq > access.log.uniq複製代碼
好比,要計算周活躍,就先收集7天的獨立用戶集合:
把7個集合求並集就獲得周活躍:
cat access.log.uniq.[1-7] | sort | uniq | wc -l複製代碼
一樣,要計算月活躍就對30天的獨立用戶集合求並集。
再好比,計算用戶留存率(Retention),則須要用到交集。先從某一天的日誌文件中把新註冊的用戶集合分離出來,以它爲基礎:
再好比,相似這樣的統計需求:「在過去某段時間內執行過某個操做的用戶,他們N天以後又執行了另外某個操做的比率」,或者計算用戶深刻到某個層級的頁面留存率,基本均可以經過並集和交集來計算。而相似「使用了某個業務但沒有使用另外某個業務的用戶」的統計,則涉及到差集的計算。
在掌握了Shell腳本處理數據的一些技巧以後,小白又深刻學習了awk編程,今後他作起數據統計的任務來,愈加地輕鬆自如。而公司的CEO和產品團隊,天天仔細分析這些數據以後,有針對性地對產品進行調整,也取得了不錯的成績。
隨着用戶的增多和業務的發展,訪問日誌愈來愈大,從幾百MB到1個GB,再到幾十個GB,上百GB。統計腳本執行的時間也愈來愈長,不少統計要跑幾個小時,甚至以天來計。之前那種靈活的數據統計需求,再也作不到「立等可取」了。並且,更糟糕的是,單臺機器的內存已經捉襟見肘。機器雖然內存已經配置到了很大,但仍是常常出現嚴重的swap,之前的腳本眼看就要「跑不動」了。
爲了加快統計腳本執行速度,小白打算找到一個辦法可以讓數據統計腳本在多臺機器上並行執行,並使用較小的內存就能運行。他左思右想,終於想到了一個樸素但有效的辦法。
仍是以計算日活躍爲例。他先把某一天的日誌文件從頭到尾順序掃描一遍,獲得10個用戶ID文件。對於日誌文件中出現的每一個用戶ID,他經過計算用戶ID的哈希值,來決定把這個用戶ID寫入這10個文件中的哪個。因爲是順序處理,這一步執行所須要的內存並不大,並且速度也比較快。
而後,他把獲得的10個文件拷貝到不一樣的機器上,分別進行排序、去重,並計算各自的獨立用戶數。因爲10個文件中的用戶ID相互之間沒有交集,因此最後把計算出來的10個獨立用戶數直接加起來,就獲得了這一天的日活躍數據。
依靠這種方法,小白把須要處理的數據規模下降到了原來的1/10。他發現,無論原始數據文件多麼大,他只要在第一步掃描處理文件的時候選擇拆分的文件數多一些,總能把統計問題解決掉。可是,他也看到了這種方法的一些缺點:
特別是最後這個問題,讓小白非常苦惱。看起來每次統計過程相似,彷佛都在重複勞動,但每次又都有些不同的地方。好比,是根據什麼規則進行文件拆分?拆分到多少份?拆分後的數據文件又是怎麼處理?哪些機器空閒可以執行這些處理?都要根據具體的統計需求和計算過程來定。
整個過程無法自動化。小白雖然手下招了兩個實習生來分擔他的工做,但涉及到這種較大數據量的統計問題時,他仍是不放心交給他們來作。
因而,小白在思考,如何才能設計出一套通用的數據計算框架,讓每一個會寫腳本的人都能分佈式地運行他們的腳本呢?
這一思考就是三年。在這期間,他無數次地感受到本身已經很是接近於那個問題背後的本質了,但每次都沒法達到融會貫通的那個突破點。
而與此同時,公司的業務發展也進入了瓶頸期。小白逐漸認識到,在原有的業務基礎上進行精耕細做的微小改進,當然能帶來必定程度的提高,但終究沒法造就巨大的價值突破。這猶如他正在思考的問題,他須要換一個視野來從新審視。
正在這時,另外一家處於高速增加期的互聯網公司要挖他過去。再三考慮以後,他選擇了一個恰當的時機提交了辭職信,告別了他的第二份工做。
小白在新公司入職之後,被分配到數據架構組。他的任務正是他一直想實現的那個目標:設計一套通用的分佈式的數據計算框架。這一次,他面臨的是動輒幾個T的大數據。
小白作了無數次調研,自學了不少知識,最後,他從Lisp以及其它一些函數式語言的map和reduce原語中得到了靈感。他從新設計了整個數據處理過程,以下圖:
上面的數據處理過程,通常狀況下使用者只須要關心Map(3)和Reduce(5)兩個過程,即重寫Mapper和Reducer。所以,小白把這個數據處理系統稱爲MapReduce。
仍是以統計日活躍爲例,使用者須要重寫的Mapper和Reducer代碼以下:
public class MyMapper extends Mapper<Object, Text, Text, Text> {
private final static Text empty = new Text("");
private Text userId = new Text();
public void map(Object key, Text value, Context context ) throws IOException, InterruptedException {
//value格式: [時間] [用戶ID] [操做名稱] [其它參數...]
StringTokenizer itr = new StringTokenizer(value.toString());
//先跳過第一個字段
if (itr.hasMoreTokens()) itr.nextToken();
if (itr.hasMoreTokens()) {
//找到用戶ID字段
userId.set(itr.nextToken());
//輸出用戶ID
context.write(userId, empty);
}
}
}
public class MyReducer extends Reducer<Text,Text,Text,Text> {
private final static Text empty = new Text("");
public void reduce(Text key, Iterable<Text> values, Context context ) throws IOException, InterruptedException {
//key就是用戶ID
//重複的用戶ID只輸出一個, 去重
context.write(key, empty);
}
}複製代碼
假設配置了r
個Reducer,那麼通過上面的代碼執行完畢以後,會獲得r
個輸出文件。其中每一個文件由不重複的用戶ID組成,且不一樣文件之間不存在交集。所以,這些輸出文件就記錄了全部日活躍用戶,它們的行數累加,就獲得了日活躍數。
設計出MapReduce的概念以後,小白髮現,這是一個頗有效的抽象。它不只能完成日常的數據統計任務,它還有更普遍的一些用途,下面是幾個例子:
故事以外的說明:
首先,本文出現的故事情節純屬虛構,但裏面出現的技術和思考是真實的。本文在嘗試用一個先後貫穿的故事主線來講明數據統計以及MapReduce的設計思路,重點在於思惟的先後連貫,而不在於細節的面面俱到。所以,有不少重要的技術細節是本文沒有涵蓋的,但讀者們可能須要注意。好比:
(完)
其它精選文章: