Neo4j【從無到有從有到無】【N6】Graph數據庫內部

目錄

1.本機圖處理

2.本機圖存儲

3.程序化API

3.1.內核API(Kernel API)

3.2.核心API(Core API)

3.3.遍歷框架(Traversal Framework)

4.非功能特性(Nonfunctional Characteristics)

4.1.事務處理(Transactions)

4.2.可恢復性(Recoverability)

4.3.可用性(Availability)

4.4.規模(Scale)

4.4.1.Capacity

4.4.2.Latency

4.4.3.Throughput

5.摘要


在本章中,我們將深入瞭解並討論圖形數據庫的實現,展示它們與其他存儲和查詢複雜的,可變結構的,緊密連接的數據的方式有何不同。 儘管確實沒有單個通用體系結構模式存在,即使在圖形數據庫之間也不存在,但是本章介紹了可以期望在圖形數據庫中找到的最常見的體系結構模式和組件。

由於多種原因,我們將使用Neo4j圖形數據庫說明本章中的討論。 Neo4j是一個具有本機處理功能以及本機圖存儲的圖數據庫(有關本機圖處理和存儲的討論,請參見第1章)。 除了在撰寫本文時成爲最常用的圖形數據庫外,它還具有開源的透明性優勢,這使冒險的讀者可以更輕鬆地更深入地研究代碼。 最後,這是一個作者很瞭解的數據庫。

1.本機圖處理

在本書中,我們多次討論了屬性圖模型。 至此,您應該熟悉其通過命名和定向關係連接的節點的概念,其中節點和關係都充當屬性的容器。 儘管模型本身在圖形數據庫實現之間合理地一致,但是有許多方法可以在數據庫引擎的主內存中編碼和表示圖形。 在許多不同的引擎體系結構中,我們說圖數據庫如果具有稱爲無索引鄰接的屬性,則具有本機處理功能。

利用無索引鄰接的數據庫引擎是其中每個節點都維護對其相鄰節點的直接引用的引擎。 因此,每個節點都充當附近其他節點的微索引,這比使用全局索引便宜得多。 這意味着查詢時間與圖形的總大小無關,而是與搜索的圖形量成正比。

相反,非本機圖數據庫引擎使用(全局)索引將節點鏈接在一起,如圖6-1所示。 這些索引爲每個遍歷增加了一個間接層,從而導致更大的計算成本。 支持本機圖處理的人認爲,無索引的鄰接對於快速,有效地遍歷圖至關重要。

要了解爲什麼本機圖處理比基於重索引的圖更高效,請考慮以下內容:根據實現的不同,索引查找的算法複雜度可能爲O(log n),而查找直接關係的開銷可能爲O(1)。 爲了遍歷m個步長的網絡,索引方法的成本爲O(m log n),與使用無索引鄰接的實現的O(m)成本相比顯得微不足道。

圖6-1顯示了圖處理的非本機方法如何工作。 要查找愛麗絲的朋友,我們首先要執行索引查找,費用爲O(log n)。 對於偶爾或淺淺的查找,這可能是可以接受的,但是當我們反轉遍歷的方向時,它很快變得昂貴。 如果我們不是要找到愛麗絲的朋友,而是要找出誰是愛麗絲的朋友,而必須執行多個索引查找,則對可能與愛麗絲成爲朋友的每個節點執行一次索引查找。 這使成本更加繁重。 找出誰是愛麗絲的朋友是O(log n)成本,而找出誰是愛麗絲的朋友是O(m log n)。

無索引的鄰接導致低成本的“加入”

使用無索引鄰接,可以有效地預先計算雙向聯接並將其作爲關係存儲在數據庫中。 相反,當使用索引來模擬記錄之間的連接時,數據庫中沒有實際存儲的關係。 由此產生兩個問題:

首先,在算法上使用全局索引查找通常比遍歷物理關係要昂貴得多。 索引通常在時間上花費O(log(n)),而至少在Neo4j中,遍歷關係在時間上是O(1) 。 從理論上講,即使n的值適中,對數成本也可能比恆定時間貴很多倍。 在實踐中,由於圖形及其全局索引競爭諸如緩存和 I/O 之類的資源(例如,當索引和圖形數據之間發生頁面爭用時),其性能甚至會更差。

其次,當我們嘗試從構造索引的方向“相反(opposite)”的方向進行遍歷時,使用索引來模擬連接變得很成問題。 現在,我們面臨着爲每個遍歷方案創建反向查找索引的選擇,或者我們必須通過原始索引(這是O(n)操作)執行蠻力搜索。 鑑於這種情況下的算法性能不佳,像這樣的聯接太昂貴了,以至於無法用於在線系統。

索引查找可用於小型網絡(例如圖6-1中的網絡),但對於大型圖的查詢而言代價太高。 具有本機圖處理功能的圖數據庫不使用索引查找來履行查詢時的關係角色,而是使用無索引的鄰接關係來確保高性能遍歷。圖6-2顯示了關係如何消除了對索引查找的需求。

回想一下,在通用圖形數據庫中,可以非常便宜地在任一方向(從頭到尾,或從頭到尾)遍歷關係。 如圖6-2所示,要使用圖表查找愛麗絲的朋友,我們只需遵循她的外向朋友關係,每個關係的成本爲O(1) 。 要查找誰是愛麗絲的朋友,我們只需跟蹤所有愛麗絲傳入的FRIEND關係到他們的來源,同樣,每次花費O(1) 。

考慮到這些成本,很明顯,至少在理論上,圖遍歷可能非常有效。 但是,只有在爲此目的設計的架構支持它們的情況下,此類高性能遍歷才成爲現實。

2.本機圖存儲

如果無索引的鄰接關係是高性能遍歷,查詢和寫入的關鍵,那麼圖形數據庫設計的一個關鍵方面就是圖形的存儲方式。 高效的本機圖形存儲格式支持任意圖形算法的快速遍歷,這是使用圖形的重要原因。 爲了說明起見,我們將使用Neo4j數據庫作爲圖形數據庫的結構示例。

首先,讓我們通過圖6-3所示的Neo4j的高級體系結構將我們的討論背景化。 接下來,我們將自下而上地進行研究,從磁盤上的文件,編程API到Cypher查詢語言。 在此過程中,我們將討論Neo4j的性能和可靠性特徵,以及使Neo4j成爲高性能,可靠的圖形數據庫的設計決策。

Neo4j將圖形數據存儲在許多不同的存儲文件中。 每個存儲文件都包含圖形特定部分的數據(例如,有單獨的存儲用於節點,關係,標籤和屬性)。 存儲職責的劃分(尤其是圖形結構與屬性數據的分離)有助於進行高效的圖形遍歷,即使這意味着用戶對其圖形的看法與磁盤上的實際記錄在結構上也不相同。 讓我們開始研究物理存儲,方法是查看節點的結構和磁盤上的關係,如圖6-4所示。

節點存儲文件存儲節點記錄。 用戶級圖中創建的每個節點最終都位於節點存儲中,該節點存儲的物理文件爲neostore.nodestore.db。 像大多數Neo4j存儲文件一樣,節點存儲是固定大小的記錄存儲,其中每個記錄的長度爲9個字節。 固定大小的記錄可對存儲文件中的節點進行快速查找。 如果我們有一個ID爲100的節點,那麼我們知道它的記錄從文件開始900個字節。 根據這種格式,數據庫可以直接計算記錄的位置,而費用爲O(1),而不是執行搜索,而費用爲O(log n)。

節點記錄的第一個字節是使用中(in-use)標誌。 這可以告訴數據庫該記錄當前是用於存儲節點,還是可以代表新節點進行回收(Neo4j的.id文件跟蹤未使用的記錄)。 接下來的四個字節表示連接到該節點的第一個關係的ID,接下來的四個字節表示該節點的第一個屬性的ID。 標籤的五個字節指向該節點的標籤存儲(可以在標籤相對較少的地方內聯標籤)。 最後一個字節多餘的部分用於標誌。 一個這樣的標誌用於標識密集連接的節點,其餘空間保留供將來使用。 節點記錄非常輕巧:實際上只是指向關係,標籤和屬性列表的少數指針。

相應地,關係存儲在關係存儲文件neo store.relationshipstore.db中。 與節點存儲一樣,關係存儲也由固定大小的記錄組成。 每個關係記錄都包含關係開始和結束時節點的ID,指向關係類型的指針(存儲在關係類型存儲中),指向每個開始和結束的下一個和上一個關係記錄的指針節點和一個標誌,指示當前記錄是否是通常稱爲關係鏈的第一條記錄。

節點和關係存儲僅與圖的結構有關,而與圖的屬性數據無關。 這兩個存儲都使用固定大小的記錄,因此只要指定了文件ID,就可以快速計算出商店文件中任何單個記錄的位置。 這些關鍵的設計決策突顯了Neo4j對高性能遍歷的承諾。

在圖6-5中,我們看到了各種存儲文件如何在磁盤上進行交互。兩個節點記錄中的每個記錄都包含一個指向該節點的第一個屬性和關係鏈中的第一個關係的指針。要讀取節點的屬性,我們遵循從第一個屬性的指針開始的單鏈列表結構。爲了找到節點的關係,我們遵循該節點的關係指針指向其第一個關係(在此示例中爲LIKES關係)。然後,從此處開始,跟蹤該特定節點的關係的雙向鏈接列表(即開始節點雙向鏈接列表或結束節點雙向鏈接列表),直到找到我們感興趣的關係爲止。記錄我們想要的關係,我們可以使用與節點屬性相同的單鏈列表結構來讀取該關係的屬性(如果有),或者我們可以使用關係的開始檢查關係所連接的兩個節點的節點記錄節點和終端節點ID。這些ID乘以節點記錄大小,即可得出節點存儲文件中每個節點的立即偏移量。

關係存儲中的雙鏈接列表

最初,如果關係存儲結構看起來有些複雜,請不要擔心。 它不像節點存儲或屬性存儲那麼簡單。

將關係記錄視爲“屬於”兩個節點(關係的開始節點和結束節點)會很有幫助。 顯然,我們不想存儲兩個關係記錄,因爲那樣會很浪費。 但是,同樣清楚的是,關係記錄應該以某種方式既屬於起始節點又屬於終止節點。

這就是爲什麼有兩個雙向鏈接列表的指針(又稱記錄ID)的原因。 一個是從起始節點可見的關係列表。 另一個是從終端節點可見的關係列表。 每個列表都被雙重鏈接,簡單地使我們能夠在任一方向上快速迭代該列表,並有效地插入和刪除關係。

選擇遵循不同的關係涉及對關係的鏈接列表進行迭代,直到找到合適的候選對象(例如,匹配正確的類型或具有一些匹配的屬性值)。 建立起適當的關係後,我們便重新開始業務,將ID乘以記錄大小,然後追逐指針。

使用固定大小的記錄和類似指針的記錄ID,可以通過在數據結構周圍跟蹤指針來簡單地實現遍歷,這可以以很高的速度執行。 爲了遍歷從一個節點到另一個節點的特定關係,數據庫執行了幾次廉價的ID計算(這些計算比搜索全局索引便宜得多,因爲如果要僞造非圖本機數據庫中的圖,則必須這樣做):

  1. 從給定的節點記錄中,通過計算其在關係存儲中的偏移量,即通過將其ID乘以固定的關係記錄大小,來在關係鏈中找到第一條記錄。 這使我們直接到達關係存儲中的正確記錄。
  2. 從關係記錄中,在第二個節點字段中查找以找到第二個節點的ID。 將該ID乘以節點記錄大小即可在存儲中找到正確的節點記錄。

如果我們希望將遍歷限制爲與特定類型的關係,則可以在關係類型存儲區中添加查找。 同樣,這是ID與記錄大小的簡單乘積,以在關係存儲中找到適當的關係類型記錄的偏移量。 同樣,如果我們選擇按標籤進行約束,則會引用標籤存儲。

除了包含圖結構的節點和關係存儲外,我們還有屬性存儲文件,這些文件將用戶的數據保存在鍵值對中。 回想一下,Neo4j是一個屬性圖數據庫,它允許將屬性(name-value對)附加到節點和關係。 因此,屬性存儲是從節點和關係記錄中引用的。

屬性存儲中的記錄,實際上,存儲在neostore.propertys tore.db文件中。與節點和關係存儲一樣,屬性記錄的大小是固定的。每個屬性記錄都包含四個屬性塊屬性鏈中下一個屬性的ID(請記住,與關係鏈中使用的雙鏈表相比,屬性在磁盤上作爲單鏈表保存)。每個屬性佔用一個到四個屬性塊-一個屬性記錄最多可以容納四個屬性。屬性記錄保存屬性類型(Neo4j允許任何原始JVM類型,字符串以及JVM基本類型的數組)以及指向屬性索引文件的指針(neostore.propertystore.db.index)名稱已存儲。對於每個屬性的值,記錄都包含指向動態存儲記錄的指針或內聯值。動態存儲允許存儲較大的屬性值。動態存儲有兩個:動態字符串存儲(neostore.propertystore.db.strings)和動態數組存儲(neostore.propertystore.db.arrays)。動態記錄包括固定大小記錄的鏈接列表;因此,一個非常大的字符串或一個大數組可能會佔用多個動態記錄。

內聯和優屬性存儲的利用

Neo4j支持存儲優化,從而將某些屬性直接內聯到屬性存儲文件(neostore.propertystore.db)。 當可以對屬性數據進行編碼以使其適合記錄的四個屬性塊中的一個或多個時,就會發生這種情況。 實際上,這意味着可以將諸如電話號碼和郵政編碼之類的數據直接內聯到屬性存儲文件中,而不是將其推送到動態存儲中。 由於只需要單個文件訪問,因此減少了I / O操作並提高了吞吐量。

除了內聯某些兼容的屬性值外,Neo4j還維護屬性名稱上的空間規則。 例如,在社交圖中,可能會有許多具有諸如first_name和last_name之類的屬性的節點。 如果將每個屬性名稱逐字寫到磁盤上,那將很浪費,因此通過屬性索引文件從屬性存儲中間接引用屬性名稱。 屬性索引允許具有相同名稱的所有屬性共享一條記錄,因此對於重複圖形(這是一種非常常見的用例),Neo4j節省了大量空間和I/O。

有效的存儲佈局只是圖片的一半。 儘管已針對快速遍歷對存儲文件進行了優化,但是硬件方面的考慮仍然會對性能產生重大影響。 近年來,內存容量顯着增加。 儘管如此,非常大的圖形仍將超出我們將其完全保存在主內存中的能力。 硬盤的毫秒級搜索時間約爲個位數,儘管按照人類的標準,這是快速的,但是在計算方面卻非常緩慢。 固態磁盤(SSD)更好(因爲沒有大的尋星等待磁盤旋轉),但是CPU和磁盤之間的路徑仍然比通向L2高速緩存或主內存的路徑更潛伏,理想情況下,我們希望在圖表上進行操作。

爲了減輕機械/電子大容量存儲設備的性能特徵,許多圖形數據庫使用內存中緩存來提供對圖形的概率低延遲訪問。從Neo4j的2.2,一個off-heap緩存用於提供這種性能提升。

從Neo4j 2.2開始,Neo4j使用LRU-K頁面緩存。 頁面高速緩存是一種LRU-K頁面化高速緩存,這意味着該高速緩存將每個存儲區劃分爲多個離散區域,然後每個存儲文件保留固定數量的區域。 頁面是根據最不常用的(LFU)緩存策略從緩存中逐出的,這受頁面受歡迎程度的影響。 也就是說,優先頁面會從緩存中撤出,而不是流行頁面,即使最近沒有訪問過流行頁面也是如此。 此策略可確保在統計上優化使用緩存資源。

3.程序化API

儘管文件系統和緩存基礎結構本身很吸引人,但是開發人員很少直接與它們進行交互。 相反,開發人員通過查詢語言來操縱圖形數據庫,查詢語言可以是命令性的也可以是聲明性的。 本書中的示例使用Cypher查詢語言,這是Neo4j固有的聲明性查詢語言,因爲它是一種易於學習和使用的語言。 但是,還存在其他API,根據我們在做什麼,我們可能需要對不同的問題進行優先級排序。 着手新項目時,務必要了解API的選擇及其功能。 如果本章沒有其他內容,則可以將這些API視爲堆棧,如圖6-6所示:在頂部,我們重視表達性和聲明性編程; 在底部,我們獎勵精度,命令式樣式以及(最低層)“裸機(bare metal)”性能。

我們在第3章中詳細討論了Cypher。在以下各節中,我們將從底部到頂部逐步介紹其餘的API。 此API導覽旨在進行說明。 並非所有的圖形數據庫都具有相同數量的層,也不一定具有行爲和交互方式完全相同的層。 每個API都有其優點和缺點,您應該對其進行調查,以便做出明智的決定。

3.1.內核API(Kernel API)

API堆棧的最低層是內核的事務事件處理程序。 這些允許用戶代碼在事務流過內核時偵聽事務,此後基於事務的數據內容和生命週期階段做出反應(或不做出反應)。

內核事務事件處理程序

事務事件處理程序的典型用例是防止記錄的物理刪除。 可以將處理程序設置爲攔截節點的刪除,然後簡單地將該節點標記爲邏輯刪除(或者以更復雜的方式,通過創建帶有時間戳的歸檔關係將節點“回滾(back in time)”)。

3.2.核心API(Core API)

Neo4j的Core API是命令性Java API,它向用戶提供節點,關係,屬性和標籤的圖基元。 當用於讀取時,會延遲評估API,這意味着僅當調用代碼要求下一個節點時才遍歷關係。 只要API調用者可以使用它,就可以從圖形中檢索數據,並且調用者可以選擇隨時終止遍歷。 對於寫操作,Core API提供了事務管理功能,以確保原子,一致,隔離和持久的持久性。

在以下代碼中,我們看到從Neo4j教程借來的代碼片段,在其中我們嘗試從Doctor Who宇宙中找到人類伴侶:

// Index lookup for the node representing the Doctor is omitted for brevity
Iterable<Relationship> relationships =
        doctor.getRelationships( Direction.INCOMING, COMPANION_OF );
for ( Relationship rel : relationships )
{
    Node companionNode = rel.getStartNode();
    if ( companionNode.hasRelationship( Direction.OUTGOING, IS_A ) )
    {
        Relationship singleRelationship = companionNode
            .getSingleRelationship( IS_A, Direction.OUTGOING );
        Node endNode = singleRelationship.getEndNode();
        if ( endNode.equals( human ) )
        {
            // Found one!
        }
    }
}

這段代碼非常重要:我們只需要在Doctor的同伴之間循環,並檢查是否有任何同伴節點與代表人類物種的節點之間具有IS_A關係。 如果同伴節點連接到人類物種節點,我們將對其進行處理。

因爲它是命令性API,所以Core API要求我們對其進行微調以使其成爲基礎圖結構。 這可以非常快。 但是,與此同時,這意味着我們最終將特定領域結構的知識應用到我們的代碼中。 與更高級別的API(尤其是Cypher)相比,需要更多的代碼才能實現相同的目標。 儘管如此,Core API和底層記錄存儲之間的親和力顯而易見,Core API相對忠實地向用戶代碼公開了存儲和緩存級別使用的結構。

3.3.遍歷框架(Traversal Framework)

遍歷框架是聲明性Java API。 它使用戶可以指定一組約束,這些約束限制了允許遍歷的圖形的各個部分。 我們可以指定要遵循的關係類型和方向(有效地指定關係過濾器); 我們可以指出我們要遍歷是廣度優先還是深度優先; 並且我們可以指定一個用戶定義的路徑評估器,該評估器由遇到的每個節點觸發。 在遍歷的每個步驟中,該評估器確定下一步如何進行遍歷。 以下代碼段顯示了運行中的遍歷API:

Traversal.description()
    .relationships( DoctorWhoRelationships.PLAYED, Direction.INCOMING )
    .breadthFirst()
    .evaluator( new Evaluator()
{
    public Evaluation evaluate( Path path )
    {
        if ( path.endNode().hasRelationship(
                            DoctorWhoRelationships.REGENERATED_TO ) )
        {
            return Evaluation.INCLUDE_AND_CONTINUE;
        }
        else
        {
            return Evaluation.EXCLUDE_AND_CONTINUE;
        }
    }
} );

通過此摘要,可以清楚地看到遍歷框架的主要聲明性。 Relationships()方法聲明只能在INCOMING方向上遍歷PLAYED關係。 此後,我們聲明遍歷應該以breadthFirst()方式執行,這意味着它將遍歷所有最近的鄰居,然後再進行進一步的遍歷。

遍歷框架在導航圖結構方面是聲明性的。 但是,對於我們的Evaluator實施,我們將使用命令式Core API。 也就是說,在給定當前節點的路徑的情況下,我們使用Core API來確定是否有必要進一步遍歷該圖(我們也可以使用Core API從評估程序內部修改該圖)。 同樣,數據庫內部的本地圖結構在此處靠近表面氣泡,而節點,關係和屬性的圖基元在API中佔據中心位置。

核心API,遍歷框架還是Cypher?(Core API, Traversal Framework, or Cypher?)

給定這幾種查詢圖形的不同方法,我們應該選擇哪一種?

核心API(Core API)允許開發人員微調其查詢,以使其與基礎圖形具有高度的親和力。 編寫良好的Core API查詢通常比任何其他方法都快。 缺點是此類查詢可能很冗長,需要開發人員付出大量努力。 此外,它們與基礎圖的高度親和性使它們緊密耦合到其結構。 當圖結構改變時,它們經常會斷裂。 Cypher對結構變化的容忍度更高-諸如可變長度路徑之類的東西有助於減輕變化和變化。

遍歷框架(Traversal Framework)的耦合性比Core API寬鬆(因爲它允許開發人員聲明信息目標),而且冗長得多,因此,使用遍歷框架(Traversal Framework)編寫的查詢通常比使用核心API(Core API)編寫的查詢所需的開發工作更少。但是,由於它是通用框架,因此遍歷框架的性能往往不如編寫良好的Core API查詢好。

如果我們發現自己處於使用Core API或Traversal Framework進行編碼的異常情況下(從而避免使用Cypher及其提供的功能),那是因爲我們正在研究一種邊緣情況,我們需要精心設計無法使用高效表達的算法,密碼的模式匹配。 在Core API和Traversal Framework之間進行選擇是要確定Traversal Framework的較高抽象/較低耦合是否足夠,或者實際上是否需要Core API的接近金屬(metal)/較高耦合纔是必要的。 根據我們的性能要求正確實施算法。

到此,我們以本機Neo4j API爲例對圖形編程API進行了簡要調查。 我們已經瞭解了這些API如何反映Neo4j堆棧較低層中使用的結構,以及這種對齊方式如何允許慣用且快速的圖形遍歷。

但是,數據庫的快速運行還不夠。 它也必須是可靠的。 這使我們對圖數據庫的非功能特性進行了討論。

4.非功能特性(Nonfunctional Characteristics)

至此,我們已經瞭解了構建本地圖形數據庫的含義,並瞭解瞭如何使用Neo4j作爲示例來實現其中某些圖形本地功能。 但是要被認爲是可靠的,任何數據存儲技術都必須在某種程度上保證所存儲數據的持久性和可訪問性。

傳統上評估關係數據庫的一種常用方法是每秒可處理的事務數。 在關係世界中,假定這些事務維護ACID屬性(即使存在故障),以使數據一致且可恢復。 對於大批量的不間斷處理和管理,關係數據庫有望擴展,以便許多實例可用於處理查詢和更新,而單個實例的丟失不會過度影響整個羣集的運行。

至少在高層次上,圖數據庫也是如此。 他們需要保證一致性,從崩潰中正常恢復,並防止數據損壞。 此外,他們需要擴展以提供高可用性並擴展性能。在以下部分中,我們將探討這些要求中的每一個對於圖形數據庫體系結構的意義。 再一次,我們將通過研究Neo4j的體系結構來擴展某些方面,以提供具體示例。 應該指出的是,並非所有圖形數據庫都是完全ACID的。 因此,重要的是要了解所選數據庫的事務模型的細節。 Neo4j的ACID事務性顯示了可以從圖形數據庫獲得的相當高的可靠性水平,這是我們習慣從企業級關係數據庫管理系統獲得的水平。

4.1.事務處理(Transactions)

數十年來,事務一直是可靠的計算系統的基礎。儘管許多NOSQL存儲區都不進行事務處理,部分原因是未經驗證的假設認爲事務處理系統的伸縮性較差,但是事務仍然是當代圖形數據庫(包括Neo4j)中可靠性的基本抽象。 (關於事務限制可伸縮性的說法是有道理的,因爲在病理情況下,分佈式的兩階段提交可能會顯示不可用性問題,但總的來說,其效果遠不如通常所設想的那樣。)

Neo4j中的事務在語義上與傳統的數據庫事務相同。寫入發生在事務上下文中,爲了保持一致性,對事務中涉及的任何節點和關係都採用了寫鎖。 事務成功完成後,更改將刷新到磁盤以提高持久性,並釋放寫鎖定。 這些動作保持了事務的原子性保證。 如果事務由於某種原因失敗,則將丟棄寫入並釋放寫入鎖,從而將圖形保持在其先前的一致狀態。

如果兩個或多個事務試圖同時更改相同的圖形元素,Neo4j將檢測到潛在的死鎖情況,並對事務進行序列化。單個事務上下文中的寫入將對其他事務不可見,從而保持隔離。

Neo4j中如何實現事務

Neo4j中的事務實現在概念上很簡單。 每個事務都表示爲一個內存中對象,其狀態表示對數據庫的寫入。 鎖定管理器支持該對象,該管理器在創建,更新和刪除節點和關係時將寫鎖定應用於節點和關係。 在事務回滾時,將丟棄事務對象並釋放寫鎖,而在成功完成時,將事務提交到磁盤。

在Neo4j中將數據提交到磁盤使用預先寫入日誌,從而將更改作爲可操作條目附加到活動事務日誌中。 在事務提交時(假定對準備階段的響應是肯定的),提交條目將被寫入日誌。 這將導致日誌刷新到磁盤,從而使更改具有持久性。一旦發生磁盤刷新,更改將應用於圖本身。 將所有更改應用於圖之後,將釋放與事務關聯的所有寫鎖。

提交事務後,即使故障導致非病理性故障,系統也處於保證更改在數據庫中的狀態。 正如我們現在所看到的,這在可恢復性和持續提供服務方面具有實質性優勢。

4.2.可恢復性(Recoverability)

數據庫與任何其他軟件系統都沒有什麼不同,因爲它們易於在實現,運行的硬件以及硬件的電源,散熱和連通性方面受到錯誤的影響。 儘管勤奮的工程師試圖將所有這些故障的可能性降到最低,但在某些時候數據庫不可避免地會崩潰—儘管兩次故障之間的平均時間確實應該很長。

在設計良好的系統中,數據庫服務器崩潰雖然很煩人,但不影響可用性,儘管它可能會影響吞吐量。 並且,當發生故障的服務器恢復運行時,無論崩潰的性質或時機如何,它都不得向其用戶提供損壞的數據。

當從不正常的關機中恢復時,Neo4j可能是由故障甚至是過度的操作員引起的,都會檢入最近活動的事務日誌,並根據存儲,重放,找到的所有事務。 這些事務中的某些事務可能已經應用於存儲,但是由於重播是冪等操作,因此最終結果是相同的:恢復後,存儲將與失敗之前成功提交的所有事務保持一致。

在單個數據庫實例的情況下,僅需進行本地恢復。 但是,通常,我們在羣集中運行數據庫(稍後將討論),以確保代表客戶端應用程序的高可用性。 幸運的是,羣集爲恢復實例提供了更多好處。 如前所述,實例不僅會與失敗之前成功提交的所有事務保持一致,而且還可以迅速趕上羣集中的其他實例,從而與失敗之後成功提交的所有事務保持一致。 也就是說,本地恢復完成後,副本服務器可以向羣集的其他成員(通常是主服務器)詢問任何新的事務。 然後,它可以通過事務重播將這些較新的事務應用於其自己的數據集。

可恢復性處理數據庫在發生故障後立即進行設置的能力。 除了可恢復性之外,一個良好的數據庫還需要高度可用才能滿足數據密集型應用程序日益複雜的需求。

4.3.可用性(Availability)

Neo4j的事務和恢復功能除了本身有價值之外,還具有其高可用性特徵。 數據庫能夠在崩潰後識別並在必要時修復實例,這意味着無需人工干預即可快速恢復數據。 當然,更多的活動實例可以提高數據庫處理查詢的整體可用性。

在典型的生產場景中,通常需要單獨的斷開連接的數據庫實例。 通常,我們對數據庫實例進行集羣以實現高可用性。 Neo4j使用主從集羣配置來確保圖形的完整副本存儲在每臺計算機上。 寫入會定期從主機複製到從機。 在任何時候,主服務器和一些從服務器都將擁有該圖的最新副本,而其他從服務器將迎頭趕上(通常,它們僅落後毫秒)。

對於寫操作,具有讀取從屬關係的經典寫主是一種流行的拓撲。 通過這種設置,所有數據庫寫操作都針對主服務器,而讀操作則針對從屬服務器。 這爲寫入提供了漸進的可伸縮性(最大達到單個主軸的容量),但爲讀取提供了近乎線性的可伸縮性(考慮到管理集羣的適度開銷)。

儘管帶有讀取從機的寫主機是經典的部署拓撲,但Neo4j還支持通過從機進行寫。 在這種情況下,客戶端首先將寫操作定向到的從屬服務器確保其與主服務器保持一致(“追趕”); 此後,將在兩個實例之間同步處理寫操作。 當我們要在兩個數據庫實例中具有持久性時,這很有用。 此外,由於它允許將寫入定向到任何實例,因此它提供了額外的部署靈活性。 然而,由於強制追趕階段,這以更高的寫入等待時間爲代價。 這並不意味着寫操作會在系統中分佈:所有寫操作仍必須在某個時刻通過主服務器

Neo4j中的其他複製選項

從Neo4j 1.8版開始,可以指定在認爲事務完成之前,以盡力而爲的方式將對主服務器的寫入複製到任意數量的副本中。 這提供了通過從站寫入實現的“至少兩個”持久性級別的替代方法。 有關更多詳細信息,請參見“複製”。

可用性的另一個方面是爭用資源。 爭奪對圖的特定部分的排他訪問(例如,用於寫入)的操作可能遭受足夠高的等待時間,以致顯得不可用。 我們已經在RDBMS中看到了粗粒度表級鎖定的類似爭用,即使邏輯上沒有爭用,寫操作也是潛在的。

慣用查詢的好處

一級方程式賽車手傑基·斯圖爾特(Jackie Stewart)曾說過,要駕駛好汽車,您不需要成爲一名工程師,但是您需要機械上的配合。 也就是說,最佳性能來自於駕駛員和汽車和諧地協作。

以幾乎相同的方式,當圖數據庫查詢被構造爲從一個或多個起點開始遍歷的慣用圖本地查詢時,它們被視爲機械同情數據庫。 對基礎結構(包括緩存和存儲訪問)進行了優化,以支持此類工作負載。

慣用查詢具有有益的副作用。 例如,由於緩存與慣用搜索保持一致,因此,本身是慣用的查詢往往比非慣用的查詢更好地利用緩存並運行得更快。 反過來,快速運行的查詢可以釋放數據庫以運行更多數據庫,這意味着從客戶端的角度來看,吞吐量更高,可用性更高,因爲等待的時間更少。

單項查詢(例如,選擇隨機節點/關係而不是遍歷的查詢)表現出相反的特徵:它們不尊重底層的緩存層,因此運行速度較慢,因爲需要更多的磁盤I / O。 由於查詢運行緩慢,因此數據庫每秒可以處理更少的查詢,這意味着從客戶端的角度來看,數據庫執行有用工作的可用性降低了。

無論使用哪種數據庫,瞭解底層存儲和緩存基礎結構都將幫助我們構建慣用的(因此機械上能相互配合)查詢,以最大限度地提高性能。

我們對可用性的最終觀察是,針對羣集範圍的複製進行擴展具有積極的影響,不僅在容錯方面,而且在響應能力方面。 因爲有許多計算機可用於給定的工作負載,所以查詢延遲很短,並且可以保持可用性。 但是,正如我們現在將要討論的,擴展本身比我們部署的服務器數量更加細微。

4.4.規模(Scale)

隨着數據量的增長,規模主題變得越來越重要。 實際上,事實證明,關係數據庫難以解決的大規模數據問題一直是NOSQL運動的主要動力。 從某種意義上說,圖形數據庫沒有什麼不同。 畢竟,它們還需要擴展以滿足現代應用程序的工作量需求。 但是規模並不是一個簡單的值,例如每秒的事務數。 而是我們跨多個軸衡量的彙總值。

對於圖數據庫,我們將把對比例的廣泛討論分解爲三個關鍵主題:

  1. 容量                Capacity (graph size)
  2. 潛在因素         Latency (response time)
  3. 讀寫吞吐量     Read and write throughput

4.4.1.Capacity

一些圖形數據庫供應商選擇避開圖形大小的任何上限,以換取性能和存儲成本。 Neo4j歷史上採用了某種獨特的方法,通過優化圖大小等於或低於用例第95個百分位數的情況,維護了一個“最佳點”,該點可實現更快的性能和更低的存儲量(並因此減少了內存佔用和IO-ops) 進行權衡的原因在於使用了固定的記錄大小和指針(如“本機圖形存儲”中所述),它在存儲內部大量使用。 在撰寫本文時,Neo4j的當前版本可以支持具有數百億個節點,關係和屬性的單個圖。 這樣就可以將社交網絡數據集的大小與Facebook的大小相提並論。

Neo4j團隊已公開表示打算在單個圖中支持100B + 節點/關係/屬性 (nodes/relationships/properties)作爲其路線圖的一部分。

數據集必須利用多少大小才能利用圖形數據庫必須提供的所有優勢? 答案是,比您想像的要小。 對於二級或三級查詢,在具有幾個個位數的千個節點的數據集上,性能優勢會顯現出來。 查詢的程度越高,變化量就越極端。 易於開發的好處當然與數據量無關,並且無論數據庫大小如何都可以使用。 作者已經看到有意義的生產應用程序,範圍從小到幾萬個節點,數十萬個關係到數十億個節點和關係。

4.4.2.Latency

圖形數據庫不會像傳統的關係數據庫一樣遭受延遲問題的困擾,在傳統的關係數據庫中,表和索引中的數據越多,聯接操作的時間就越長(這種簡單的事實是性能調優幾乎是關鍵的原因之一)始終是關係型DBA的首要問題)。對於圖形數據庫,大多數查詢都遵循一種模式,在該模式下,僅使用索引來查找一個或多個起始節點。然後,遍歷的其餘部分使用指針追蹤和模式匹配的組合來搜索數據存儲。這意味着,與關係數據庫不同,性能不取決於數據集的總大小,而僅取決於要查詢的數據。這導致性能時間幾乎是恆定的(即與結果集的大小有關),即使數據集的大小不斷增長(儘管正如我們在第3章中所討論的那樣,仍然需要調整性能)。圖形以適合查詢,即使我們處理的數據量較小也是如此。

4.4.3.Throughput

我們可能認爲圖形數據庫需要以與其他數據庫相同的方式進行縮放。 但這不是事實。 當我們查看IO密集型應用程序行爲時,我們看到單個複雜的業務操作通常會讀寫一組相關數據。 換句話說,應用程序對整個數據集中的邏輯子圖執行多項操作。 使用圖形數據庫,可以將多個操作彙總爲更大,更緊密的操作。 此外,對於圖形本機存儲,執行每個操作比等效的關係操作需要更少的計算量。 對於相同的結果,圖形可以通過減少工作量來進行縮放。

例如,假設有一個發佈場景,我們想要閱讀作者的最新文章。 在RDBMS中,我們通常通過以下方式來選擇作者的作品:根據匹配的作者ID將authors表與出版物表相連,然後按出版日期對出版物進行排序,並限制爲最新的出版物。 根據訂購操作的特性,可能是O(log(n))操作,這並不是很糟糕。

但是,如圖6-7所示,等效圖形操作爲O(1),這意味着性能穩定,與數據集大小無關。 使用圖表,我們只需遵循從作者到已發表文章列表(或樹)頂部的工作的稱爲WROTE的出站關係。 如果我們希望找到較舊的出版物,我們只需遵循PREV關係並通過鏈表進行迭代(或者通過樹進行遞歸)。寫操作更合適,因爲我們總是在列表的開頭(或樹的根)插入新的出版物,這是另一個固定時間的操作。 與RDBMS替代方案相比,這是有利的,特別是因爲它自然地保持了讀取的恆定時間性能。

當然,最苛刻的部署將使一臺計算機運行查詢的能力不堪重負,更具體地說,其I / O吞吐量將不堪重負。 發生這種情況時,使用Neo4j構建可水平擴展以實現高可用性和高讀取吞吐量的集羣非常簡單。 對於典型的圖形工作負載(讀取遠超過寫入),此解決方案體系結構可能是理想的選擇。

如果我們超出了集羣的容量,則可以通過在應用程序中建立分片邏輯來在整個數據庫實例中分佈圖。 分片涉及在應用程序級別使用綜合標識符來跨數據庫實例連接記錄。執行效果如何很大程度上取決於圖形的形狀。 一些圖形非常適合這一點。 例如,Mozilla將Neo4j圖形數據庫用作其下一代雲瀏覽器Pancake的一部分。 它存儲了大量獨立於終端用戶的小獨立圖,而不是擁有一個大圖。 這使得擴展非常容易。

當然,並不是所有的圖都有這樣方便的邊界。 如果我們的圖足夠大以至於需要分解,但是不存在自然界限,則我們使用的方法與使用MongoDB等NOSQL存儲的方法幾乎相同:我們創建綜合(synthetic)鍵,並通過 應用層使用這些鍵以及一些應用程序級別的解析算法。 與MongoDB方法的主要區別在於,本機圖數據庫將在您在數據庫實例內進行遍歷時隨時爲您提供性能提升,而在實例之間運行的遍歷部分將以與MongoDB大致相同的速度運行加入。 但是,總體性能應該明顯更快。

圖可擴展性的聖盃

大多數圖形數據庫的未來目標是能夠在多臺計算機之間劃分圖形,而無需應用程序級別的干預,以便可以水平縮放對圖形的讀寫訪問。 通常情況下,這是一個NP Hard問題,因此不切實際。

對於幼稚的問題,由於遍歷(慢速)網絡的計算機之間的圖形遍歷會導致意外的查詢時間,從而導致查詢時間無法預測。 相比之下,明智的實現可以理解特定域範圍內的最小切點,從而最大程度地減少了跨機器的遍歷。 儘管在撰寫本文時該地區正在進行令人興奮的研究工作,但尚無數據庫可證明地支持這種行爲。

5.摘要

在本章中,我們展示了屬性圖如何成爲實用數據建模的理想選擇。 我們已經探究了圖形數據庫的體系結構,特別參考了Neo4j的體系結構,並討論了圖形數據庫實現的非功能性特徵及其對可靠性的意義。

 

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