1. 程式人生 > >MongoDB應用案例:使用 MongoDB 儲存日誌資料

MongoDB應用案例:使用 MongoDB 儲存日誌資料

線上執行的服務會產生大量的執行及訪問日誌,日誌裡會包含一些錯誤、警告、及使用者行為等資訊,通常服務會以文字的形式記錄日誌資訊,這樣可讀性強,方便於日常定位問題,但當產生大量的日誌之後,要想從大量日誌裡挖掘出有價值的內容,則需要對資料進行進一步的儲存和分析。

本文以儲存 web 服務的訪問日誌為例,介紹如何使用 MongoDB 來儲存、分析日誌資料,讓日誌資料發揮最大的價值,本文的內容同樣使用其他的日誌儲存型應用。

模式設計

一個典型的web伺服器的訪問日誌類似如下,包含訪問來源、使用者、訪問的資源地址、訪問結果、使用者使用的系統及瀏覽器型別等

127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700
] "GET /apache_pb.gif HTTP/1.0" 200 2326 "[http://www.example.com/start.html](http://www.example.com/start.html)" "Mozilla/4.08 [en] (Win98; I ;Nav)"

最簡單儲存這些日誌的方法是,將每行日誌儲存在一個單獨的文件裡,每行日誌在MongoDB裡類似

{
      _id: ObjectId('4f442120eb03305789000000'),
     line: '127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0"
200 2326 "[http://www.example.com/start.html](http://www.example.com/start.html)" "Mozilla/4.08 [en] (Win98; I ;Nav)"' }

上述模式雖然能解決日誌儲存的問題,但使得這些資料分析起來比較麻煩,因為文字分析並不是MongoDB所擅長的,更好的辦法時,在把一行日誌儲存到MongoDB的文件裡前,先提取出各個欄位的值,如下所示,上述的日誌被轉換為一個包含很多個欄位的文件。

{
     _id: ObjectId('4f442120eb03305789000000'),
     host: "127.0.0.1",
     logname
: null, user: 'frank', time: ISODate("2000-10-10T20:55:36Z"), path: "/apache_pb.gif", request: "GET /apache_pb.gif HTTP/1.0", status: 200, response_size: 2326, referrer: "[http://www.example.com/start.html](http://www.example.com/start.html)", user_agent: "Mozilla/4.08 [en] (Win98; I ;Nav)" }

同時,在這個過程中,如果你覺得有些欄位對資料分析沒有任何幫助,則可以直接過濾掉,以減少儲存上的消耗,比如

  • 資料分析不會關心user資訊、request、status資訊,這幾個欄位沒必要儲存
  • ObjectId裡本身包含了時間資訊,沒必要再單獨儲存一個time欄位 (當然帶上time也有好處,time更能代表請求產生的時間,而且查詢語句寫起來更方便,儘量選擇儲存空間佔用小的資料型別)

基於上述考慮,一行日誌最終儲存的內容可能類似如下

{
     _id: ObjectId('4f442120eb03305789000000'),
     host: "127.0.0.1",
     time: ISODate("2000-10-10T20:55:36Z"),
     path: "/apache_pb.gif",
     referer: "[http://www.example.com/start.html](http://www.example.com/start.html)",
     user_agent: "Mozilla/4.08 [en] (Win98; I ;Nav)"
}

寫日誌

日誌儲存服務需要能同時支援大量的日誌寫入,使用者可以定製 writeConcern 來控制日誌寫入能力,猛擊這裡詳細瞭解writeConcern

db.events.insert({
         host: "127.0.0.1",
         time: ISODate("2000-10-10T20:55:36Z"),
         path: "/apache_pb.gif",
         referer: "[http://www.example.com/start.html](http://www.example.com/start.html)",
         user_agent: "Mozilla/4.08 [en] (Win98; I ;Nav)"
    }
)
  • 如果要想達到最高的寫入吞吐,可以指定 writeConcern 為 {w: 0}
  • 而如果日誌的重要性比較高(比如需要用日誌來作為計費憑證),則可以使用更安全的writeConcern級別,比如 {w: 1} 或 {w: "majority"}

同時,為了達到最優的寫入效率,使用者還可以考慮批量的寫入方式,一次網路請求寫入多條日誌。

db.events.insert([doc1, doc2, ...])

查詢日誌

當日志按上述方式儲存到 MongoDB 後,就可以滿足各種查詢需求

查詢所有訪問 /apache_pb.gif 的請求

q_events = db.events.find({'path': '/apache_pb.gif'})
 

如果這種查詢非常頻繁,可以針對path欄位建立索引,以高效的服務這類查詢

db.events.createIndex({path: 1})

查詢某一天的所有請求

q_events = db.events.find({'time': { '$gte': ISODate("2016-12-19T00:00:00.00Z"),'$lt': ISODate("2016-12-20T00:00:00.00Z")}})
   

通過對time欄位建立索引,可加速這類查詢

db.events.createIndex({time: 1})

查詢某臺主機一段時間內的所有請求

 q_events = db.events.find({
    'host': '127.0.0.1',
    'time': {'$gte': ISODate("2016-12-19T00:00:00.00Z"),'$lt': ISODate("2016-12-20T00:00:00.00Z" }

})

通過對host、time建立複合索引可以加速這類查詢

db.events.createIndex({host: 1, time: 1})

同樣,使用者還可以使用MongoDB的aggregation、mapreduce框架來做一些更復雜的查詢分析,在使用時應該儘量建立合理的索引以提升查詢效率。

資料分片

當寫日誌的服務節點越來越多時,日誌儲存的服務需要保證可擴充套件的日誌寫入能力以及海量的日誌儲存能力,這時就需要使用MongoDB sharding來擴充套件,將日誌資料分散儲存到多個shard,關鍵的問題就是shard key的選擇。

按時間戳欄位分片

一種簡單的方式是使用時間戳來進行分片(如ObjectId型別的_id,或者time欄位),這種分片方式存在如下問題

  • 因為時間戳一直順序增長的特性,新的寫入都會分到同一個shard,並不能擴充套件日誌寫入能力
  • 很多日誌查詢是針對最新的資料,而最新的資料通常只分散在部分shard上,這樣導致查詢也只會落到部分shard

按隨機欄位分片

按照_id欄位來進行hash分片,能將資料以及寫入都均勻都分散到各個shard,寫入能力會隨shard數量線性增長,但該方案的問題時,資料分散毫無規律,所有的範圍查詢(資料分析經常需要用到)都需要在所有的shard上進行查詢然後合併查詢結果,影響查詢效率。

按均勻分佈的key分片

假設上述場景裡 path 欄位的分佈是比較均勻的,而且很多查詢都是按path維度去劃分的,那麼可以考慮按照path欄位對日誌資料進行分片,好處是

  • 寫請求會被均分到各個shard
  • 針對path的查詢請求會集中落到某個(或多個)shard,查詢效率高

不足的地方是

  • 如果某個path訪問特別多,會導致單個chunk特別大,只能儲存到單個shard,容易出現訪問熱點
  • 如果path的取值很少,也會導致資料不能很好的分佈到各個shard

當然上述不足的地方也有辦法改進,方法是給分片key裡引入一個額外的因子,比如原來的shard key是 {path: 1},引入額外的因子後變成

{path: 1, ssk: 1} 其中ssk可以是一個隨機值,比如_idhash值,或是時間戳,這樣相同的path還是根據時間排序的

這樣做的效果是分片key的取值分佈豐富,並且不會出現單個值特別多的情況。

上述幾種分片方式各有優劣,使用者可以根據實際需求來選擇方案。

應對資料增長

分片的方案能提供海量的資料儲存支援,但隨著資料越來越多,儲存的成本會不斷的上升,而通常很多日誌資料有個特性,日誌資料的價值隨時間遞減,比如1年前、甚至3個月前的歷史資料完全沒有分析價值,這部分可以不用儲存,以降低儲存成本,而在MongoDB裡有很多方法支援這一需求。

TTL 索引

MongoDB 的TTL索引 可以支援文件在一定時間之後自動過期刪除,例如上述日誌time欄位代表了請求產生的時間,針對該欄位建立一個TTL索引,則文件會在30小時後自動被刪除。

db.events.createIndex( { time: 1 }, { expireAfterSeconds: 108000 } )

TTL 索引目前是後臺單執行緒來定期(預設60s一次)去刪除已過期的文件,如果寫入很多,導致積累了大量待過期的文件,則會導致文件過期一直跟不上而一直佔用著儲存空間。

使用Capped集合

如果對日誌儲存的時間沒有特別嚴格的要求,只是在總的儲存空間上有限制,則可以考慮使用capped collection來儲存日誌資料,指定一個最大的儲存空間或文件數量,當達到閾值時,MongoDB會自動刪除capped collection裡最老的文件。

db.createCollection("event", {capped: true, size: 104857600000}

定期按集合或DB歸檔

比如每到月底就將events集合進行重新命名,名字裡帶上當前的月份,然後建立新的events集合用於寫入,比如2016年的日誌最終會被儲存在如下12個集合裡

 events-201601
 events-201602
 events-201603
 events-201604
 ....
 events-201612

當需要清理歷史資料時,直接將對應的集合刪除掉

 db["events-201601"].drop()
 db["events-201602"].drop()

不足到時候,如果要查詢多個月份的資料,查詢的語句會稍微複雜些,需要從多個集合裡查詢結果來合併。

原文地址:https://yq.aliyun.com/articles/68353?spm=5176.100239.blogcont73664.24.h8eZaz

參考資料