MongoDB徵文|MongoDB技術 - 從0到1+

前言

偶然機會看到mongo中文社區辦了場徵文活動,覺得挺有意思的,雖說自己還在成爲大佬的路上,但參與一下未嘗不可。於是就有了這篇文章。
活動已規定了選題框架,我思索了小會兒,覺得從0到1+ 挺切合選題一,即聊聊如何由淺到深地學習MongoDB的核心技術。爲什麼是1+而不是1呢?因爲我覺得0代表起點、原點,1代表走了一段路程,1+ 則代表在這方向上不斷地走下去。畢竟學無止境嘛~

怎麼聊這事呢?我本人是專職於database領域的,接觸MongoDB卻也不久,但有幸接觸到要與內核打交道的項目,所以研究了一段時間,也略有所得,所以此篇文章中也必然會談到學習MongoDB我們可以怎麼做起。既然從0開始,也必然少不了談Mongo的基本概念原理,由於它是分佈式數據庫,也得談談分佈式技術的常見原理。這一趟下來,夠有談的了。當然單篇文章確實無法說盡全貌,我們不妨儘量地從high-level的角度來認識和理解根本的抽象性、原理性的東西,這樣其實不管學什麼數據庫,我想會受益良多。

入門使用

毫無疑問,最有用的文檔來自官方 https://docs.mongodb.com/ ,相當豐富,夠學一段時間了。但是若只是盯着文檔看幾周,估計用處不大,很容易忘。這種文檔就適合邊看邊學,邊看邊做。

有一個網站db rank,專門給各類database排名,mongodb一直很靠前,說明自有其魅力。簡單說,mongo是基於文檔的NoSQL數據庫系統,注意這裏的文檔可不是指word,excel那種概念的文檔,專指JSON文檔,{ k1: v1, k2: v2, … } 這個樣子的。我們都知道,數據庫可以在不同層面上進行分類,方便人去理解和對比。除了文檔型,還有kv, column family, graph等。那還有沒有和mongo同處文檔這一類別的呢? 當然有,我以前接觸過couchbase,Erlang寫的文檔型db,如果說要做性能比較,我倒覺得它們倆挺合適,雖然大家喜歡談mysql跟mongo比較。

在這裏還是推薦一本書《MongoDB in action》,書能幫我們系統地講清概念,起到官方文檔無法有的作用。書雖着重於較老版本 3.0,但我想與目前的4.2 在使用上也不會相差太大,等熟悉了使用和基本原理再換別的看。

在這裏就不對各種命令使用做解析了,這個大家看文檔看書都能get到。談談一些有體會的點。

schema-free

schema-free說的是沒有schema或者schema很寬鬆,相對於關係型的relational schema。對database新手是不是對schema的概念也較模糊呢?我不知道中文是啥,很多詞彙感覺就是英文表達順口些。我摘抄了一個定義[1]:

A database schema is the skeleton structure that represents the logical view of the entire database. It defines how the data is organized and how the relations among them are associated. It formulates all the constraints that are to be applied on the data.

它是一個骨架,數據庫的骨架,定義了它的邏輯視圖,即從外面看它長什麼樣子。包括數據是怎麼組織的,是如何關聯的,有什麼樣的約束等。因此它是一種descriptive detail,應該說在db設計階段就得有,幫助開發者建立mental view

爲什麼要在這裏着重說schema ? 因爲確實mongo schema長得不一樣,對習慣於關係型世界的人來講看着有點彆扭。其實也可以說它是schema-less,就好像沒有schema似的,很隨意,想怎麼加field,加attribute都行,這樣的能力很顯然非常適用於不斷動態發展的業務。初期你不知道你業務有哪些字段,你只希望在未來有需求時想加就加,此時mongo在這個層面上非常契合。然而,關係型schema若提供這樣的能力代價就有些大了,相比於mongo.

適用場景

談到這一點,恐怕做數據庫的經常要回答的問題。一個用戶過來問他的業務能不能接mongodb,你該如何回答他?他問得很簡單,可你答得卻不能很簡單,至少你得多問幾個問題,因爲他沒有交待清楚。交待哪些呢?

  • 是什麼樣的業務?數據量多大?
  • 讀寫比例如何?讀寫QPS多大?
  • 讀寫有何特徵,比如夜晚是低峯?
  • 訪問模式(access pattern) ?
  • and so on

一般說來,業務是應該給出這些信息,我們纔好判斷到底合不合適。畢竟市場上有這麼多db呢,如果每個db都適用於所有場合,那整這麼多db幹嘛?大一統不香嘛

可有些時候比如在公有云環境下,業務上下文可能還是祕密,不想告訴你,或者說他也不太清楚/不好預估,此時能不能接呢?不好說,最好還是在測試環境裏跑跑workload。前幾天看了TiDB前段時間分享的KeyViz,我有了點想法,如果有童鞋對這種 observability 工具有興趣,可以一起討論下

據《MongoDB in action》裏所說,以下場景可接入mongo:

  • web app
    這個範圍太廣了。不過確實mongodb在web應用裏應用挺多,web應用特點本要求擴展性高,靈活豐富的查詢 ,動態地添加字段等
  • 敏捷開發
    這裏主要強調的是由於沒有固定的schema的優勢使得很適用敏捷開發方法論
  • 分析型和logging
    capped collection適合於放日誌數據 ,至於分析型我倒見不多,莫非還可比dedicated OLAP db更好?
  • caching
  • 可變schema

更多use case場景可參見[9]

腳本搭建

個人建議呢,自己寫一個搭建集羣的腳本,方便一鍵生成,非常方便,而不要每次一個命令一個命令地敲。我的腳本供參考[7]

後面我會談到在快速創建定製化集羣的基礎上用gdb調試內核

分佈式概念及原理

這一塊領域太大了!
Mongodb屬於分佈式數據庫,相比於單機數據庫,節點之間有了網絡距離,於是乎,各種不靠譜的事就會發生了(google一下"分佈式系統裏常見的8個fallacies")。我根據mongo實際情況講點皮毛 _^^_ 更多有趣的資料務必參考DDIA[6],這個是迄今最通俗的版本

非常簡單地用自己的語言從 背景爲什麼需要它, Mongo怎麼做的 三方面來談談,談到的詞彙建議讀者多多google

共識協議(Consensus)

背景

簡單理解就是要多方達到一致。稍微接觸這塊的都知道raft,這個Stanford 教授  John Ousterhout 和其博士生 Diego Ongaro 弄出來的,已經在多種分佈式數據庫上應用瞭如TiDB, PolarDB. 

當然業界還有其它協議,Lamport的Paxos(被應用到Chubby),Zookeeper的ZAB,MongoDB的pv1

爲什麼需要它

簡單來看,當多個節點共同來做決定時,如果你說你的,我說我的,還怎麼決定啊?就像一羣人在房間開會,七嘴八舌,就是沒統一,最後這會只能白開了。同理在分佈式系統中,我們需有一套規則讓各節點對事件以及結果達成一致,這樣才能正常運轉。這其實與現實世界模型是很吻合的

Mongo怎麼做的

mongo用的是mongodb pv1 , 是一種類raft 協議,不過它進行了豐富的擴展,如rs.conf()中就可配置各節點的priority, hidden, vote等屬性,有非常大的靈活性; 增加了PreVote, DryRun等動作等。詳細細節讀者可參考相關文檔

隔離級別/一致性(ACID/Consistency/CAP)

背景

這幾個概念有相似性,就放在一起了。貌似分佈式系統裏我們一般不談ACID,這是在單機關係型數據庫常用的詞彙,且這裏面的C 與分佈式系統所說的Consistency不是一回事!

CAP是Brewer 92年就提出的詞彙了,很多論文現在都不推薦使用這個詞彙,因爲它很有歧義;
在衆多論文裏,還有與一致性很多的詞彙,如
- causal consistency,因果一致性,Mongo中有
- linearizability,線性一致性,針對於single object的,始終讀到最新的數據
- serializability,串行化,強調多個事務操作多個object的,在關係型db屬最強的隔離級別
- strict serializability, linearizability + serializability,在google spanner中有提到
- sequence consistency: 順序一致性,比linearizability弱點,比如x86 CPU默認一致性是它,我們常在C++ Memory Model裏見到`std::memory_order_seq`

在數據安全性方面,要有持久化的保證,一般常用技巧是定期做checkpoint,且有write-ahead log,這在WiredTiger引擎層有原生的支持;

爲什麼需要它

凡是有副本,有讀寫,就必然存在讀能否讀到最新數據的問題,這就屬於一致性的問題。有的業務要求必須讀到最新寫入的數據 ,此爲strong consistency,但有些業務不要求,那數據庫可以放開這種強約束,於是有最終一致性eventual consistency,即意味着給定一定的時間,最終各副本數據都會一樣的,這樣的實現比起強一致複雜度要低很多

Mongo怎麼做的

關於一致性,我得談談當初自己存在已久的誤解。原來mongo裏的quorom 不是我們常說的那種quorum !

以前深入瞭解過Cassandra,和其C++產品Scylladb,它們的原型是amazon 的Dynamo,論文裏談到quorum模型:當有N個節點,如果寫大多數,即 W > N/2,讀也大多數 > N/2,則讀一定可以讀到最新寫入的數據。然而mongo雖然也有majority的說法,但其內涵完全是另外一回事。

寫mongo時,客戶端只可能寫主,不可能寫從,這與leader-less 系統(無主系統,各節點都是對等的)就不一樣了,從是從主拉數據過去的; 主從節點都在維護着一個majority committed 的時間點,當寫已經到達大多數時,這個點就會向前推進; 

當客戶端指定 readConcern: majority 時,能不能讀成功,就看發起操作的時間點是不是在majority committed 時間點後面,如果是,則majority 讀就是成功的;
Mongo事務支持快照隔離,即事務可讀最近穩定的一個點,它可能是老數據,但是它與其它數據是一致的,這樣就避免了讀寫衝突;

複製和故障冗餘(Replication, Fault Tolerance)

背景

在分佈式系統中,複製是提高可用性的重要、常規手段。在複雜分佈式環境下,總有個別組件就會崩,卡住,不響應,此時爲了不影響用戶的請求,就需要將請求轉到正常的節點上,那數據就得有多份,要不然怎麼訪問先前訪問的數據呢?
故障冗餘是個經典概念了,分佈式裏的故障千奇百怪,軟件的,硬件的,人爲的; 在典型的單主系統裏,主節點要是沒有,就會影響用戶的讀寫,所以在前一個的主節點沒了的那很短的時刻就必須有新的主來替代它,完美的時候用戶根本感受不到切主。

爲什麼需要它

正如前面所說保證系統可用性,數據安全性

Mongo怎麼做的

Mongo是單主系統,寫只能寫主節點,因此它有選舉機制,靠的是前面的所說的類raft協議。這是保證故障冗餘; 
複製方面,從節點從主節點拉oplog,oplog就可理解爲raft裏的log,它反映了主節點的mutation,從節點將這在本地apply,就可達到與主節點一致的狀態
非常詳細的說明見官方源碼[12]

內核

個人接觸內核也沒多久,在此拋磚引玉

內核其實分Server層和Storage Engine層,由於Server接觸不完備,暫只講講引擎層的事兒

存儲引擎

這裏有一份由doxygen生成的文檔[11],值得一閱。

引擎層技術可謂是數據庫系統的核心技術,裏面涉及了數據庫的核心原理的實現。首先我們要明白,數據的組織可以是多種方式,究竟哪個方式好,在代碼未實現出來之前,恐怕還沒法說

很明顯這裏我們需要插拔的特性,數據庫層(也就是幹sql,cql,查詢優化,執行計劃等的)可以靈活接入多種存儲引擎,這樣最後誰好誰差,比一比就知道了。所以引擎層必須很獨立,提供最原始的接口供上層調用即可,這也是計算機分層思想在數據庫領域的完美體現。

MongoDB引擎從3.x開始就是WiredTiger了,官方似乎一直沒考慮把RocksDB兼容性的代碼放進去,所以MongoRocks是一個第三方的存在;當然還有一個in-memory引擎

WiredTiger

這裏簡稱爲WT[8]。WT最初是一家由大佬 Michael Cahill 創立的,某一年被MongoDB收購,從此一直是mongo默認的存儲引擎。我們可以在這兒[2]看到WT的基本介紹 ,挺豐富的,沒事可多查閱。

WT首先是一個kv存儲引擎,類別上與Rocksdb一致,不過名氣確實小很多,原因可能是比較小衆,貌似只有mongo用,且代碼看着確實不太易讀;
引擎索引實現是B tree,而不是B+ tree,這一點網上也有不少的討論,至於爲何用B tree,據我所知:

  1. mongo着重於提高point query性能,而非range query,這樣不像B+ tree那樣每次都得去葉子節點拿數據,平均來看,走更短的路徑
  2. 優化讀多寫少的場景
  3. 其他

WT API的使用

WT在mongo使用,其實基本的調用就那麼幾個:

  1. 創建連接conn
    wiredtiger_open(home, NULL, "create,cache_size=**, transaction_sync=**, checkpoint_sync=**,...", &conn)
    這在啓動時就需調用,生成一個指向db的WT_CONN,它作爲WiredTigerKVEngine的私有成員
  2. 創建session
    mongo裏的操作都有session上下文的,文檔裏的session,其實就對應引擎層的WT_SESSION ; 代碼裏爲了高效利用session,有個sessionCache供使用,不用每次都去open
    conn->open_session(conn, NULL, "isolation=**", &session)
  3. 創建表/索引
    當mongo層執行createCollection/createIndex時,即有:
    sesssion->create(session, "table::access", "key_format=S, value_format=S"))
  4. 在session上創建cursor
    session->open_cursor(session, "table:mytable", NULL,NULL, &cursor)
  5. 支持事務時,在session上開啓事務
    session->begin_transaction(session, "isolation=**, read_timestamp=**, sync=**,...")
  6. 用cursor set/get key/value
    用戶看到的json,mongo server層看到的BSON,其實在底層都轉成了(key, value) pair
    cursor->set_key(cursor, "key")
    cursor->set_value(cursor, "value")
    cursor->update(cursor);
  7. 提交/回滾事務
    session->commit_transaction(session, "commit_timestamp=**, durable_timestamp=**, sync=**,...")
    session->rollback_transaction(session, NULL);

對於以上步驟,幾點澄清:

  • WT API調用就像那種風格,特別明顯的是會有一參數char* config,裏面就用a=b這種格式來指定各種配置參數。雖說挺原始的做法
  • 有關時間戳的參數較爲複雜,需要深入文檔
  • 參數含義還是得參考[2]

時間戳機制

從官方文檔和視頻[14]中來看,從3.6開始引入 logical session,在WT 的update structure裏添加timestamp field等這些動作都是逐漸在爲支持事務、分佈式事務爲鋪路。

我爲熟悉MongoRocks對事務的支持接觸過WT的時間戳一些概念,目前還不能很系統地論述各個時間戳之間是如何運作的。這方面可多多參考[2] ,我不在此講了。

MongoRocks

聽名字想必也能猜得到是與rocksdb有關,想到它也很自然,既然底層接kv engine,rocksdb又是kv型,完全可接啊,正如MyRocks那樣。看源碼[3] stars也有300+,最初由 開發者 Igor Canadi 及其他實現了3.2, 3.4的MongoRocks版。項目被擱置一段時間,幾個月前 Igor Canadi 接受了wolfkdy 對MongoRocks 4.0 的MR[16],我在其中參與了相關PR提交如[4]

4.0 mongo-rocks 驅動層的實現主要集中於事務部分,正如 Igor 所說,3.6.x之後,mongo的內部事務跳躍性大,若正確實現4.0版需很大一部分精力[5]

MongoRocks 4.0 剛出不久,因此還需更多時間來穩定,比如之前由我發現的oplog讀取有空洞的問題[13],已被作者修復[15]。個人還是非常期待Rocksdb能接入到Mongo的,相信會有比WT更亮的點!在這方面個人應該會投入更多時間,期待有更多國內開發者加入!

內核gdb調試

大型代碼,如果用gdb單步着來學習肯定是不行的,單步只適用於調試bug的時候。我這裏談gdb調試用來幹嘛呢? get runtime path !

我一直認爲,拿到一份大型C++項目,除了肉眼盯着代碼看半天瞭解code flow之外,用gdb bt 更是一大利器 !在server端加一斷點,客戶端發一個命令過來,然後一bt ,立刻知道server 走的核心路徑,很方便 !

劃重點:請用 >= 8.x 版本的gdb。好處是bt自帶顏色顯示,看着比以前舒服多了。


以下聊聊一般怎麼用
首先啓動一副本集或分片集羣(取決於你關注哪個),對主進行以下設置:

cfg=rs.conf(); cfg.settings.heartbeatTimeoutSecs=3600; cfg.settings.electionTimeoutMillis=3600000; rs.reconfig(cfg)

這裏假設我們要調試主。爲了防止調試時,默認的時間內就failover了,所以增大heartbeat,election的超時,這樣主就一直仍是主(當然若想調試主從代碼的code,就不要這麼做了)

當我們想看看insert命令的請求路徑時,
隨便看看代碼,去搜索一下insert關鍵字,相信不難發現有CmdInsert這樣的字眼。再仔細一看,發現它繼承一個基類,它還有個run方法,有感覺的開發者其實這時就能猜一猜了:斷定server收到insert請求時,很可能run要被調用 !
於是乎可在run處加個斷點,或者我們在grep中發現了insertRecords的字眼,更能判定插入文檔時很可能走了這裏,於是有了這樣:
在這裏插入圖片描述
可以繼續enter,這個路徑從libc.so start_thread 到run,到insertRecords 很長的,這一段路徑夠我們分析是怎麼走的了

同樣,對於find, update,delete都是類似手段。

對於事務操作,可以去grep transaction字樣 ,也會發現可以被作爲斷點的函數,遇到begin_transaction, commit_transaction, rollback_transaction其實是很熟悉的函數名稱,適合加上斷點


結語

MongoDB技術淺談在此,這方面知識量非常龐大,確不是由一篇文章能道盡的。對我自己而言,其內涵本身是迷人的,因爲它是數據庫,它是分佈式系統,它還有許多毛病。儘管Mongo官方縮緊了協議,一些雲廠商沒法玩高版本了。但我想,只要它還是開源的,只要它代碼還是真的,對工程師而言這仍然是一件欣慰的事吧。由淺入深,從此刻開始!


=== 我不是廣告
騰訊雲CMongo團隊致力於打造業務精緻的mongodb雲服務,歡迎有興趣小夥伴加入,或者找我交流也行啊,哈哈.
Email: [email protected]

References

[1] https://www.tutorialspoint.com/dbms/dbms_data_schemas.htm
[2] http://source.wiredtiger.com/
[3] https://github.com/mongodb-partners/mongo-rocks
[4] https://github.com/mongodb-partners/mongo-rocks/pull/153
[5] https://github.com/mongodb-partners/mongo-rocks/issues/145
[6] Martin Kleppmann DDIA: Designing data-intensive Applications
[7] https://gitee.com/cshi/codes/dbzrmhvy4s87lnt60uc2f46
[8] https://github.com/wiredtiger/wiredtiger
[9] https://www.cnblogs.com/williamjie/p/10416294.html
[10] 4.0事務淺析:https://mongoing.com/archives/6102
[11] 存儲引擎API:https://mongodbsource.github.io/doxygen/index.html
[12] 源碼對複製的詳細解釋:https://github.com/mongodb/mongo/blob/d8caa7410ce2642d1c67f31c330f6d82ba384495/src/mongo/db/repl/README.md
[13] https://github.com/mongodb-partners/mongo-rocks/issues/154
[14] https://www.youtube.com/watch?v=mUbM29tB6d8
[15] https://github.com/wolfkdy/rocksdb/commit/5eb8a67f955b4035d6c034e00f1bb7c6bb6f47d4
[16] https://github.com/mongodb-partners/mongo-rocks/pull/149

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章