Nebula Graph 原始碼解讀系列|客戶端的通訊祕密——fbthrift

NebulaGraph發表於2022-02-23

Nebula Graph 原始碼解讀系列|客戶端的通訊祕密——fbthrift

概述

Nebula Clients 給使用者提供了多種程式語言的 API 用於和 Nebula Graph 互動,並且對服務端返回的資料結構進行了重新封裝,便於使用者使用。

目前 Nebula Clients 支援的語言有 C++、Java、Python、Golang 和 Rust。

通訊框架

Nebula Clients 使用了 fbthrift https://github.com/facebook/fbthrift 作為服務端和客戶端之間的 RPC 通訊框架,實現了跨語言的互動。

fbthrift 提供了三方面的功能:

  1. 生成程式碼:fbthrift 可將不同語言序列化成資料結構
  2. 序列化:將生成的資料結構序列化
  3. 通訊互動:在客戶端、服務端之間傳輸訊息,收到不同語言的客戶端的請求時,呼叫相應的服務端函式

例子

這裡以 Golang 客戶端為例,展示 fbthrift 在 Nebula Graph 中的應用。

  1. Vertex 結構在服務端的定義:

    struct Vertex {
     Value vid;
     std::vector<Tag> tags;
    
     Vertex() = default;
    };
  2. 首先, 在 src/interface/common.thrift 中定義一些資料結構:
struct Tag {
        1: binary name,
        // List of <prop_name, prop_value>
        2: map<binary, Value> (cpp.template = "std::unordered_map") props,
} (cpp.type = "nebula::Tag")

struct Vertex {
        1: Value     vid,
        2: list<Tag> tags,
} (cpp.type = "nebula::Vertex")

在這裡我們定義了一個 Vertex 的結構,其中 (cpp.type = "nebula::Vertex") 標註出了這個結構對應了服務端的 nebula::Vertex

  1. fbthrift 會自動為我們生成 Golang 的資料結構:

    // Attributes:
    //  - Vid
    //  - Tags
    type Vertex struct {
     Vid *Value `thrift:"vid,1" db:"vid" json:"vid"`
     Tags []*Tag `thrift:"tags,2" db:"tags" json:"tags"`
    }
    
    func NewVertex() *Vertex {
     return &Vertex{}
    }
    
    ...
    
    func (p *Vertex) Read(iprot thrift.Protocol) error { // 反序列化
     ...
    }
    
    func (p *Vertex) Write(oprot thrift.Protocol) error { // 序列化
     ...
    }
  2. MATCH (v:Person) WHERE id(v) == "ABC" RETURN v 這條語句中:客戶端向服務端請求了一個頂點(nebula::Vertex),服務端找到這個頂點後會進行序列化,通過 RPC 通訊框架的 transport 傳送到客戶端,在客戶端收到這份資料時,會進行反序列化,生成對應客戶端中定義的資料結構(type Vertex struct)。

客戶端模組

在這個章節會以 nebula-go 為例,介紹客戶端的各個模組和其主要介面。

  1. 配置類 Configs,提供全域性的配置選項。

    type PoolConfig struct {
     // 設定超時時間,0 代表不超時,單位 ms。預設是 0
     TimeOut time.Duration
     // 每個連線最大空閒時間,當連線超過該時間沒有被使用將會被斷開和刪除,0 表示永久 idle,連線不會關閉。預設是 0
     IdleTime time.Duration
     // max_connection_pool_size: 設定最大連線池連線數量,預設 10
     MaxConnPoolSize int
     // 最小空閒連線數,預設 0
     MinConnPoolSize int
    }
  2. 客戶端會話 Session,提供使用者直接呼叫的介面。

    //管理 Session 特有的資訊
    type Session struct {
     // 用於執行命令的時候的身份校驗或者訊息重試
     sessionID  int64
     // 當前持有的連線
     connection *connection
     // 當前使用的連線池
     connPool   *ConnectionPool
     // 日誌工具
     log        Logger
     // 用於儲存當前 Session 所用的時區
     timezoneInfo
    }
  3. 介面定義有以下

     // 執行 nGQL,返回的資料型別為 ResultSet,該介面是非執行緒安全的。
     func (session *Session) Execute(stmt string) (*ResultSet, error) {...}
     // 重新為當前 Session 從連線池中獲取連線
     func (session *Session) reConnect() error {...}
     // 做 signout,釋放 Session ID,歸還 connection 到 pool
     func (session *Session) Release() {
  4. 連線池 ConnectionPool,管理所有的連線,主要介面有以下

    // 建立新的連線池, 並用輸入的服務地址完成初始化
    func NewConnectionPool(addresses []HostAddress, conf PoolConfig, log Logger) (*ConnectionPool, error) {...}
    // 驗證並獲取 Session 例項
    func (pool *ConnectionPool) GetSession(username, password string) (*Session, error) {...}
  5. 連線 Connection,封裝 thrift 的網路,提供以下介面

    // 和指定的 ip 和埠的建立連線
    func (cn *connection) open(hostAddress HostAddress, timeout time.Duration) error {...}
    // 驗證使用者名稱和密碼
    func (cn *connection) authenticate(username, password string) (*graph.AuthResponse, error) {
    // 執行 query
    func (cn *connection) execute(sessionID int64, stmt string) (*graph.ExecutionResponse, error) {...}
    // 通過 SessionId 為 0 傳送 "YIELD 1" 來判斷連線是否是可用的
    func (cn *connection) ping() bool {...}
    // 向 graphd 釋放 sessionId
    func (cn *connection) signOut(sessionID int64) error {...}
    // 斷開連線
    func (cn *connection) close() {...}
  6. 負載均衡 LoadBalance,在連線池裡面使用該模組

    • 策略:輪詢策略

模組互動解析

模組互動圖

  1. 連線池

    • 初始化:

      • 在使用時使用者需要先建立並初始化一個連線池 ConnectionPool,連線池會在初始化時會對使用者指定的 Nebula 服務所在地址建立連線 Connection,如果在用叢集部署方式部署了多個 Graph 服務,連線池會採用輪詢的策略來平衡負載,對每個地址建立近乎等量的連線。
    • 管理連線:

      • 連線池內維護了兩個佇列,空閒連線佇列 idleConnectionQueue 和使用中的連線佇列 idleConnectionQueue,連線池會定期檢測過期空閒的連線並將其關閉。這兩個佇列在增刪元素的時候會通過讀寫鎖來確保多執行緒執行的正確性。
      • 當 Session 向連線池請求連線時,會檢查空閒連線佇列中是否有可用的連線,如果有則直接返回給 Session 供使用者使用;如果沒有可用連線並且當前的總連線數沒有超過配置中限定的最大連線數,則新建一個連線給 Session;如果已經到達了最大連線數的限制,返回錯誤。
    • 一般只有在程式退出時才需要關閉連線池, 在關閉時池中所有的連線都會被斷開。
  2. 客戶端會話

    • 客戶端會話 Session 通過連線池生成,使用者需要提供使用者密碼進行校驗,在校驗成功後使用者會獲得一個 Session 例項,並通過 Session 中的連線與服務端進行通訊。最常用的介面是 execute(),如果在執行時發生錯誤,客戶端會檢查錯誤的型別,如果是網路原因則會自動重連並嘗試再次執行語句。
    • 需要注意,一個 Session 不支援被多個執行緒同時使用,正確的方式是用多個執行緒申請多個 Session,每個執行緒使用一個 Session。
    • Session 被釋放時,其持有的連線會被放回到連線池的空閒連線佇列中,以便於之後被其他 Session 複用。
  3. 連線

    • 每個連線例項都是等價的,可以被任意 Session 持有,這樣設計的目的是這些連線可以被不同的 Session 複用,減少反覆開關 Transport 的開銷。
    • 連線會將客戶端的請求傳送到服務端並將其結果返回給 Session。
  4. 使用者使用示例

    // Initialize connection pool
    pool, err := nebula.NewConnectionPool(hostList, testPoolConfig, log)
    if err != nil {
     log.Fatal(fmt.Sprintf("Fail to initialize the connection pool, host: %s, port: %d, %s", address, port, err.Error()))
    }
    // Close all connections in the pool when program exits
    defer pool.Close()
    
    // Create session
    session, err := pool.GetSession(username, password)
    if err != nil {
     log.Fatal(fmt.Sprintf("Fail to create a new session from connection pool, username: %s, password: %s, %s",
         username, password, err.Error()))
    }
    // Release session and return connection back to connection pool when program exits
    defer session.Release()
    
    // Excute a query
    resultSet, err := session.Execute(query)
    if err != nil {
     fmt.Print(err.Error())
    }

返回資料結構

客戶端對部分複雜的服務端返回的查詢結果進行了封裝並新增了介面,以便於使用者使用。

查詢結果基本型別封裝後的型別
Null
Bool
Int64
Double
String
TimeTimeWrapper
Date
DateTimeDateTimeWrapper
List
Set
Map
VertexNode
EdgeRelationship
PathPathWrraper
DateSetResultSet
-Record(用於ResultSet 的行操作)

對於 nebula::Value,在客戶端會被包裝成 ValueWrapper,並通過介面轉換成其他結構。(i.g. node = ValueWrapper.asNode())

資料結構的解析

對於語句 MATCH p= (v:player{name:"Tim Duncan"})-[]->(v2) RETURN p,返回結果為:

+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| p                                                                                                                                                                                                                         |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| <("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"})<-[:teammate@0 {end_year: 2016, start_year: 2002}]-("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"})> |
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
Got 1 rows (time spent 11550/12009 us)

我們可以看到返回的結果包含了一行,型別是一條路徑. 此時如果需要取得路徑終點(v2)的屬性,可以通過如下操作實現:

// Excute a query
resultSet, _ := session.Execute("MATCH p= (v:player{name:"\"Tim Duncan"\"})-[]->(v2) RETURN p")

// 獲取結果的第一行, 第一行的 index 為0
record, err := resultSet.GetRowValuesByIndex(0)
if err != nil {
    t.Fatalf(err.Error())
}

// 從第一行中取第一列那個 cell 的值
// 此時 valInCol0 的型別為 ValueWrapper 
valInCol0, err := record.GetValueByIndex(0)

// 將 ValueWrapper 轉化成 PathWrapper 物件
pathWrap, err = valInCol0.AsPath()

// 通過 PathWrapper 的 GetEndNode() 介面直接得到終點
node, err = pathWrap.GetEndNode()

// 通過 node 的 Properties() 得到所有屬性
// props 的型別為 map[string]*ValueWrapper
props, err = node.Properties()

客戶端地址

各語言客戶端 GitHub 地址:

推薦閱讀

《開源分散式圖資料庫Nebula Graph完全指南》,又名:Nebula 小書,裡面詳細記錄了圖資料庫以及圖資料庫 Nebula Graph 的知識點以及具體的用法,閱讀傳送門:https://docs.nebula-graph.com.cn/site/pdf/NebulaGraph-book.pdf

交流圖資料庫技術?加入 Nebula 交流群請先填寫下你的 Nebula 名片,Nebula 小助手會拉你進群~~

相關文章