
鏡像下載下傳、域名解析、時間同步請點選
阿裡巴巴開源鏡像站一、模式設計
一個典型的web伺服器的通路日志類似如下,包含通路來源、使用者、通路的資源位址、通路結果、使用者使用的系統及浏覽器類型等。
1. 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裡的存儲模式如下所示:
1. {
2. _id: ObjectId('4f442120eb03305789000000'),
3. 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)"'
4. }
上述模式雖然能解決日志存儲的問題,但這些資料分析起來比較麻煩,因為文本分析并不是MongoDB所擅長的,更好的辦法是把一行日志存儲到MongoDB的文檔裡前,先提取出各個字段的值。如下所示,上述的日志被轉換為一個包含很多個字段的文檔。
1. {
2. _id: ObjectId('4f442120eb03305789000000'),
3. host: "127.0.0.1",
4. logname: **null**,
5. **user**: 'frank',
6. time: ISODate("2000-10-10T20:55:36Z"),
7. path: "/apache_pb.gif",
8. request: "**GET** /apache_pb.gif HTTP/1.0",
9. status: 200,
10. response_size: 2326,
11. referrer: "[http://www.example.com/start.html](http://www.example.com/start.html)",
12. user_agent: "Mozilla/4.08 [en](Win98; I ;Nav)"
13. }
同時,在這個過程中,如果您覺得有些字段對資料分析沒有任何幫助,則可以直接過濾掉,以減少存儲上的消耗。比如資料分析不會關心user資訊、request、status資訊,這幾個字段沒必要存儲。ObjectId裡本身包含了時間資訊,沒必要再單獨存儲一個time字段 (當然帶上time也有好處,time更能代表請求産生的時間,而且查詢語句寫起來更友善,盡量選擇存儲空間占用小的資料類型)。基于上述考慮,上述日志最終存儲的内容可能類似如下所示:
1. {
2. _id: ObjectId('4f442120eb03305789000000'),
3. host: "127.0.0.1",
4. time: ISODate("2000-10-10T20:55:36Z"),
5. path: "/apache_pb.gif",
6. referer: "[http://www.example.com/start.html](http://www.example.com/start.html)",
7. user_agent: "Mozilla/4.08 [en](Win98; I ;Nav)"
8. }
二、寫日志
日志存儲服務需要能同時支援大量的日志寫入,使用者可以定制writeConcern來控制日志寫入能力,比如如下定制方式:
1. db.events.insert({
2. host: "127.0.0.1",
3. time: ISODate("2000-10-10T20:55:36Z"),
4. path: "/apache_pb.gif",
5. referer: "[http://www.example.com/start.html](http://www.example.com/start.html)",
6. user_agent: "Mozilla/4.08 [en](Win98; I ;Nav)"
7. }
8. )
說明:
- 如果要想達到最高的寫入吞吐,可以指定writeConcern為 {w: 0}。
- 如果日志的重要性比較高(比如需要用日志來作為計費憑證),則可以使用更安全的writeConcern級别,比如 {w: 1} 或 {w: “majority”}。
同時,為了達到最優的寫入效率,使用者還可以考慮批量的寫入方式,一次網絡請求寫入多條日志。格式如下所示:
db.events.insert([doc1, doc2, ...])
三、查詢日志
當日志按上述方式存儲到MongoDB後,就可以按照各種查詢需求查詢日志了。
1. 查詢所有通路/apache_pb.gif 的請求
q_events = db.events.find({'path': '/apache_pb.gif'})
如果這種查詢非常頻繁,可以針對path字段建立索引,提高查詢效率:
db.events.createIndex({path: 1})
2. 查詢某一天的所有請求
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})
3. 查詢某台主機一段時間内的所有請求
1. q_events = db.events.find({
2. 'host': '127.0.0.1',
3. 'time': {'$gte': ISODate("2016-12-19T00:00:00.00Z"),'$lt': ISODate("2016-12-20T00:00:00.00Z" }
4. })
同樣,使用者還可以使用MongoDB的aggregation、mapreduce架構來做一些更複雜的查詢分析,在使用時應該盡量建立合理的索引以提升查詢效率。
四、無資料分片
當寫日志的服務節點越來越多時,日志存儲的服務需要保證可擴充的日志寫入能力以及海量的日志存儲能力,這時就需要使用MongoDB sharding來擴充,将日志資料分散存儲到多個shard,關鍵的問題就是shard key的選擇。
1. 按時間戳字段分片
使用時間戳來進行分片(如ObjectId類型的_id,或者time字段),這種分片方式存在如下問題:
- 因為時間戳一直順序增長的特性,新的寫入都會分到同一個shard,并不能擴充日志寫入能力。
- 很多日志查詢是針對最新的資料,而最新的資料通常隻分散在部分shard上,這樣導緻查詢也隻會落到部分shard。
2. 按随機字段分片
按照_id字段來進行hash分片,能将資料以及寫入都均勻都分散到各個shard,寫入能力會随shard數量線性增長。但該方案的問題是,資料分散毫無規律。所有的範圍查詢(資料分析經常需要用到)都需要在所有的shard上進行查找然後合并查詢結果,影響查詢效率。
3. 按均勻分布的key分片
假設上述場景裡 path 字段的分布是比較均勻的,而且很多查詢都是按path次元去劃分的,那麼可以考慮按照path字段對日志資料進行分片,好處是:
- 寫請求會被均分到各個shard。
- 針對path的查詢請求會集中落到某個(或多個)shard,查詢效率高。
不足的地方是:
- 如果某個path通路特别多,會導緻單個chunk特别大,隻能存儲到單個shard,容易出現通路熱點。
- 如果path的取值很少,也會導緻資料不能很好的分布到各個shard。
當然上述不足的地方也有辦法改進,方法是給分片key裡引入一個額外的因子,例如原來的shard key是 {path: 1},引入額外的因子後變成:
{path: 1, ssk: 1}
其中ssk可以是一個随機值,例如:_id的hash值、時間戳,這樣相同的path還是根據時間排序的。
這樣做的效果是分片key的取值分布豐富,并且不會出現單個值特别多的情況。上述幾種分片方式各有優劣,使用者可以根據實際需求來選擇方案。
五、應對資料增長
分片的方案能提供海量的資料存儲支援,但随着資料越來越多,存儲的成本會不斷的上升。通常很多日志資料有個特性,日志資料的價值随時間遞減。比如1年前、甚至3個月前的曆史資料完全沒有分析價值,這部分可以不用存儲,以降低存儲成本,而在MongoDB裡有很多方法支援這一需求。
1. TTL 索引
MongoDB的TTL索引可以支援文檔在一定時間之後自動過期删除。例如上述日志time字段代表了請求産生的時間,針對該字段建立一個TTL索引,則文檔會在30小時後自動被删除。
db.events.createIndex( { time: 1 }, { expireAfterSeconds: 108000 } )
注意:TTL索引是目前背景用來定期(預設60s一次)删除單線程已過期文檔的。如果日志文檔被寫入很多,會積累大量待過期的文檔,那麼會導緻文檔過期一直跟不上而一直占用着存儲空間。
2. 使用Capped集合
如果對日志儲存的時間沒有特别嚴格的要求,隻是在總的存儲空間上有限制,則可以考慮使用capped collection來存儲日志資料。指定一個最大的存儲空間或文檔數量,當達到門檻值時,MongoDB會自動删除capped collection裡最老的文檔。
db.createCollection("event", {capped: true, size: 104857600000}
3. 定期按集合或DB歸檔
比如每到月底就将events集合進行重命名,名字裡帶上目前的月份,然後建立新的events集合用于寫入。比如2016年的日志最終會被存儲在如下12個集合裡:
1. events-201601
2. events-201602
3. events-201603
4. events-201604
5. ....
6. events-201612
當需要清理曆史資料時,直接将對應的集合删除掉:
1. db["events-201601"].drop()
2. db["events-201602"].drop()
不足:如果要查詢多個月份的資料,查詢的語句會稍微複雜些,需要從多個集合裡查詢結果來合并。
“ 提供全面,高效和穩定的系統鏡像、應用軟體下載下傳、域名解析和時間同步服務。”