Mongodb Mapreduce 初窺

http://cloud.csdn.net/a/20110322/294395.html

作者聲明:本文是學習Mongodb過程中的副產品,因爲接觸時間並不長,難免有理解上的偏差,希望藉此文與感興趣的朋友討論切磋,呵呵。

去年年底,開始接觸並學習Mapreduce模型。因爲工作上的關係,最近開始研究Mongodb,其中對其新特性(2010年四月)reduce模型實現產生的興趣,因爲特別留意了一下。當然網上關於該方面的內容並不是很多,且多爲EN文,所以我想有必要將學習使用過程中的一些問題作一下記錄並加以整理,因爲就有了此文。

廢話不多說了,開始正文吧!

目前支持Mongodb的C#客戶端應該就是Samuel Corder 開源的這個項目了,鏈接:http://github.com/samus/mongodb-csharp

其中在它的源碼包中的MongoDB.Net-Tests目錄下有對TestMapReduce和TestMapReduceBuilder相應測試用例,因爲我本地沒安裝NUnit,所以接下來的內容我是在一個新建的web項目中直接Copy其中的部分代碼做的測試(注:有關Mapreduce模型的內容請查閱相關資料)。

首先我們要先加載測試數據,這裏我們以DNT中的在線用戶列表的(結構)作爲依據,批量倒入10條記錄,代碼如下:

  1. Mongo db = new Mongo("Servers=10.0.4.66:27017;ConnectTimeout=300000;ConnectionLifetime=300000;MinimumPoolSize=25;MaximumPoolSize=25;Pooled=true");           
  2.  
  3. db.Connect();   
  4.  
  5. Database test = db.GetDatabase("test");  
  6. IMongoCollection things = test["things"];  
  7.  
  8. for (int i = 1; i <= 10;i++)  
  9.           {      
  10.               Document record = new Document();  
  11.               record["_id"] = i;                 
  12.               record["userid"] = i;     
  13.               record["ip"] = "10.0.7." + i;  
  14.               record["username"] = "用戶" + i;  
  15.               record["nickname"] = "用戶" + i;  
  16.               record["password"] = "";  
  17.               record["groupid"] = i;//下面將就該字段使用MAPREDUCE方式進行分組統計  
  18.               record["olimg"] = "";  
  19.               record["adminid"] = 0;  
  20.               record["invisible"] = 0;  
  21.               record["action"] = 0;  
  22.               record["lastactivity"] = 1;  
  23.               record["lastposttime"] = DateTime.Now.ToString();  
  24.               record["lastpostpmtime"] = DateTime.Now.ToString();  
  25.               record["lastsearchtime"] = DateTime.Now.ToString();  
  26.               record["lastupdatetime"] = "1212313221231231213321";  
  27.               record["forumid"] = 0;  
  28.               record["forumname"] = "";  
  29.               record["titleid"] = 0;  
  30.               record["title"] = "";  
  31.               record["verifycode"] = "";  
  32.               record["newpms"] = 0;  
  33.               record["newnotices"] = 0;  
  34.               things.Insert(record);               
  35.  
  36.           }   
  37.  
  38.       db.Disconnect();  

假定目前我們有這樣一個需求,就是找出該表中用戶組(groupid)字段爲5的用戶數,當然這裏我們不會使用普通的查詢方法,而是使用MAPREDUCE方式,其工作過程分爲兩個階段:map階段和reduce階段。每個階段都有鍵/值對作爲輸入和輸出,並且它們的類型可由程序員選擇。下面是其實現方式:

首先是map方法:

  1. string mapfunction = "function() {  if(this.groupid==5) {emit({groupid : 5}, 1);} }"

然後是reduce方法:

  1. string reducefunction = "function(key, current ){" +  
  2.                                  "   var count = 0;" +  
  3.                                  "   for(var i in current) {" +  
  4.                                  "       count+=current[i];" +  
  5.                                  "   }" +  
  6.                                  "   return { groupcount : count };" +  //注意這裏的返回方式  
  7.                                "};";  

最後我們使用下面代碼實現對上面MAP,REDUCE的相應代碼綁定和MapReduce類的聲明:

  1. MapReduce mr = mrcol.MapReduce();  
  2.     mr.Map = new Code(mapfunction);  
  3.     mr.Reduce = new Code(reducefunction4);  
  4.     mr.Execute();  
  5.     foreach (Document doc in mr.Documents)  
  6.     {  
  7.            int groupCount = Convert.ToInt32(doc["value"]);  
  8.     }  
  9.  
  10.     mr.Dispose(); 

運行上面代碼,顯示結果如下: 

當前上面監視窗口中的"id:"{"groupid":5},即是mapfunction中的定義,當然如果要統計所有用戶組(10個用戶組)中各自的用戶數,只把將mapfunction改寫成:

string mapfunction = "function() { emit(this.groupid, 1); }";

這樣,它就會按當前用戶所屬的groupid來作爲鍵(確保不重複),凡是同一組的用戶就作爲輸出進行發送(emit),emit可以理解爲調用reduce方法,這裏參數爲1[即累加1操作])。

目前我在網上打到mongodb示例基本上都是圍繞分組統計功能展開的。

當然就其傳參和返回值都可以使用類似元組的方式,記得上面的“emit({groupid : 5}, 1)”代碼嗎?返回值這裏也可以使用下面的方式:

  1. string reducefunction = "function(key, current ){" +  
  2.                                  "   var count = 0;" +  
  3.                                  "   for(var i in current) {" +  
  4.                                  "       count+=current[i];" +  
  5.                                  "   }" +  
  6.                                  "   return { groupcount : count };" +  //注意這裏的返回方式  
  7.                                "};";  

返回類型變了,取值的方式也要發生變成:

  1. int groupCount = int.Parse(((Document)doc["value"])["groupcount"].ToString());  

當然,上面的MapReduce 類的聲明使用方式過於拘謹,下面使用鏈式調用的方式:

  1. using (MapReduceBuilder mrb = mrcol.MapReduceBuilder().Map(mapfunction).Reduce(reducefunction))   
  2. {  
  3.         using (MapReduce mr = mrb.Execute())  
  4.         {  
  5.                    foreach (Document doc in mr.Documents)  
  6.                    {  
  7.                        int groupCount = int.Parse(((Document)doc["value"])["groupcount"].ToString());  
  8.                    }  
  9.         }  

返回的結果與之前的一樣,呵呵。

另外,mongodb還支持更加複雜的數據結構,比如官司方給的下面這個數據結構示例:

  1. mrcol.Insert(new Document().Append("_id", 1).Append("tags", new String[]{"dog", "cat"}));  
  2. mrcol.Insert(new Document().Append("_id", 2).Append("tags", new String[]{"dog"}));  
  3. mrcol.Insert(new Document().Append("_id", 3).Append("tags", new String[]{"mouse", "cat", "dog"}));  
  4. mrcol.Insert(new Document().Append("_id", 4).Append("tags", new String[]{}));   

可以看出tags字段(這裏暫且這麼說,呵呵),就是一個字符串數組,而下面的mapreduce方法將會統計裏面單詞dog,cat,mouse的出現次數:

  1. string mapfunction = "function(){\n" +  
  2.                            "   this.tags.forEach(\n" +  
  3.                            "       function(z){\n" +  
  4.                            "           emit( z , { count : 1 } );\n" +  
  5.                            "       });\n" +  
  6.                            "};";  
  7. string reducefunction = "function( key , values ){\n" +  
  8.                                "    var total = 0;\n" +  
  9.                                "    for ( var i=0; i<values.length; i++ )\n" +  
  10.                                "        total += values[i].count;\n" +  
  11.                                "    return { count : total };\n" +  
  12.                                "};";  

對於如何對(含)日期型數據的鍵進行分組統計,下面的這個鏈接中有詳細說明(統計每天用戶的訪問量):

Counting Unique Items with Map-Reduce

下面這個鏈接就是官方給出示例的文檔鏈接頁面,其中包括更加複雜的mapreduce示例:

http://www.mongodb.org/display/DOCS/MapReduce

當然目前對於Mapreduce模式,Mongodb使用一個單獨的進程來跑的,這主要是因爲JavaScript 引擎的限制。目前開發團隊正在設計解決這一問題。原文:

As of right now, MapReduce jobs on a single mongod process are single threaded. This is due to a design limitation in current JavaScript engines. We are looking into alternatives to solve this issue, but for now if you want to parallelize your MapReduce jobs, you will need to either use sharding or do the aggregation client-side in your code.

另外就是到現在對於MONGODB那一端是如果把輸入數據劃分成等長的小數據發送到MapReduce(Hadoop把這一操作稱爲input split,即輸入切片),因爲這一點對於併發運行的作業進行負載平衡很重要,而在 Hadoop中一個理想的切片大小往往是一個HDFS塊的大小,默認是64 MB(Hadoop權威指南(中文版))。

除了上面所提到了,在MONGODB的mapreduce模型中,還支持map輸出的臨時結果集的持久化,而這一特色還在文檔中專門作了如下說明:

Note on Permanent Collections

Even when a permanent collection name is specified, a temporary collection name will be used during processing. At map/reduce completion, the temporary collection will be renamed to the permanent name atomically. Thus, one can perform a map/reduce job periodically with the same target collection name without worrying about a temporary state of incomplete data. This is very useful when generating statistical output collections on a regular basis.

而如果想要持久化該臨時集合,只要將mapreduce實例的Keptemp屬性設爲true,同時使用Out屬性(方法)指定輸出的集合名稱即可。

當然就目前我測試時結果來看,在單臺機器上做這種模型測試就效率上是得不嘗失的(執行週期太長),特別是數據量特別大(比如3000w以上),所以應用(或運行)場景的選擇很重要。

上面所說的示例比較簡單,都是在單一reduce任務中的執行場景,如下圖:

singlereduce

而實際的生產環境要比上圖複雜許多,比如多reduce任務情況,在Hadoop中,如果運行多個reduce任務,map任務會對其輸出進行分區,爲每個reduce任務創建一個分區(partition)。每個分區包含許多鍵(及其關聯的值),但每個鍵的記錄都在同一個分區中。分區可以通過用戶定義的partitioner來控制。如下圖:

鑑於目前網上mongodb相關文檔內容並不多,所以這裏暫不多做討論了。


發佈了151 篇原創文章 · 獲贊 5 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章