首頁  >  文章  >  後端開發  >  沙發GO! — 使用 Go 編寫的查詢伺服器增強 CouchDB

沙發GO! — 使用 Go 編寫的查詢伺服器增強 CouchDB

PHPz
PHPz原創
2024-07-19 12:38:41569瀏覽

CouchGO! — Enhancing CouchDB with Query Server Written in Go

在過去的一個月裡,我一直在積極從事與 CouchDB 相關的概念驗證項目,探索其功能並為未來的任務做準備。在此期間,我多次閱讀了 CouchDB 文檔,以確保我了解一切是如何運作的。在閱讀文件時,我發現了這樣的說法:儘管 CouchDB 附帶了用 JavaScript 編寫的預設查詢伺服器,但建立自訂實作相對簡單,並且自訂解決方案已經存在。

我做了一些快速研究,發現了用 Python、Ruby 或 Clojure 寫的實作。由於整個實作看起來並不太長,因此我決定透過嘗試編寫自己的自訂查詢伺服器來嘗試 CouchDB。為此,我選擇 Go 作為語言。我之前對這種語言沒有太多的經驗,除了在 Helm 圖表中使用 Go 模板之外,但我想嘗試一些新的東西,並認為這個專案將是一個很好的機會。

了解查詢伺服器

開始工作之前,我再次回顧了 CouchDB 文檔,以了解查詢伺服器的實際工作原理。根據文檔,查詢伺服器的高級概述非常簡單:

查詢伺服器是一個外部進程,它透過 stdio 介面透過 JSON 協定與 CouchDB 進行通信,並處理所有設計函數呼叫 [...]。

CouchDB 傳送到查詢伺服器的命令結構可以表示為[, ] 或["ddoc", , [, ] 或["ddoc", , [, ] funcname>], [, , …]] 設計文件。

所以基本上,我要做的就是編寫一個能夠從 STDIO 解析此類 JSON、執行預期操作並傳回文件中指定的回應的應用程式。 Go 程式碼中涉及大量類型轉換來處理各種命令。有關每個命令的具體詳細資訊可以在文件的查詢伺服器協定部分找到。

我在這裡遇到的一個問題是查詢伺服器應該能夠解釋和執行設計文件中提供的任意程式碼。知道 Go 是一種編譯語言,我預計會在這一點上陷入困境。值得慶幸的是,我很快就找到了 Yeagi 包,它能夠輕鬆解釋 Go 程式碼。它允許創建沙箱並控制對可以在解釋程式碼中導入的包的存取。就我而言,我決定僅公開我的名為 couchgo 的包,但也可以輕鬆添加其他標準包。

介紹 CouchGO!

作為我工作的成果,開發了一個名為 CouchGO! 的應用程式!出現了。雖然它遵循查詢伺服器協議,但它不是 JavaScript 版本的一對一重新實現,因為它有自己的方法來處理設計文件功能。

例如,在CouchGO!中,沒有像emit這樣的輔助函數。要發出值,您只需從映射函數傳回它們即可。此外,設計文件中的每個函數都遵循相同的模式:它只有一個參數,該參數是一個包含特定於函數的屬性的對象,並且應該只返回一個值作為結果。該值不必是原始值;根據函數的不同,它可能是一個物件、一個映射,甚至是一個錯誤。

要開始使用 CouchGO!,您只需從我的 GitHub 儲存庫下載可執行二進位文件,將其放置在 CouchDB 實例中的某個位置,然後新增一個允許 CouchDB 啟動 CouchGO! 的環境變數!過程。

例如,如果您將 couchgo 執行檔放入 /opt/couchdb/bin 目錄中,則需要新增下列環境變數以使其能夠運作​​。

export COUCHDB_QUERY_SERVER_GO="/opt/couchdb/bin/couchgo"

使用 CouchGO 編寫函數!

為了快速了解如何使用 CouchGO! 寫函數,讓我們來探索以下函數介面:

func Func(args couchgo.FuncInput) couchgo.FuncOutput { ... }

CouchGO 中的每個功能!將遵循此模式,其中 Func 被替換為適當的函數名稱。目前,CouchGO!支援以下函數類型:

  • 地圖
  • 減少
  • 過濾器
  • 更新
  • 驗證 (validate_doc_update)

讓我們檢查一個範例設計文檔,該文檔指定具有 map 和 reduce 函數以及 validate_doc_update 函數的視圖。此外,我們需要指定我們使用 Go 作為語言。

{
  "_id": "_design/ddoc-go",
  "views": {
    "view": {
      "map": "func Map(args couchgo.MapInput) couchgo.MapOutput {\n\tout := couchgo.MapOutput{}\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 1})\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 2})\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 3})\n\t\n\treturn out\n}",
      "reduce": "func Reduce(args couchgo.ReduceInput) couchgo.ReduceOutput {\n\tout := 0.0\n\n\tfor _, value := range args.Values {\n\t\tout += value.(float64)\n\t}\n\n\treturn out\n}"
    }
  },
  "validate_doc_update": "func Validate(args couchgo.ValidateInput) couchgo.ValidateOutput {\n\tif args.NewDoc[\"type\"] == \"post\" {\n\t\tif args.NewDoc[\"title\"] == nil || args.NewDoc[\"content\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Title and content are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif args.NewDoc[\"type\"] == \"comment\" {\n\t\tif args.NewDoc[\"post\"] == nil || args.NewDoc[\"author\"] == nil || args.NewDoc[\"content\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Post, author, and content are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif args.NewDoc[\"type\"] == \"user\" {\n\t\tif args.NewDoc[\"username\"] == nil || args.NewDoc[\"email\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Username and email are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn couchgo.ForbiddenError{Message: \"Invalid document type\"}\n}",
  "language": "go"
}

現在,讓我們從地圖函數開始分解每個函數:

func Map(args couchgo.MapInput) couchgo.MapOutput {
  out := couchgo.MapOutput{}
  out = append(out, [2]interface{}{args.Doc["_id"], 1})
  out = append(out, [2]interface{}{args.Doc["_id"], 2})
  out = append(out, [2]interface{}{args.Doc["_id"], 3})

  return out
}

In CouchGO!, there is no emit function; instead, you return a slice of key-value tuples where both key and value can be of any type. The document object isn't directly passed to the function as in JavaScript; rather, it's wrapped in an object. The document itself is simply a hashmap of various values.

Next, let’s examine the reduce function:

func Reduce(args couchgo.ReduceInput) couchgo.ReduceOutput {
  out := 0.0
  for _, value := range args.Values {
    out += value.(float64)
  }
  return out
}

Similar to JavaScript, the reduce function in CouchGO! takes keys, values, and a rereduce parameter, all wrapped into a single object. This function should return a single value of any type that represents the result of the reduction operation.

Finally, let’s look at the Validate function, which corresponds to the validate_doc_update property:

func Validate(args couchgo.ValidateInput) couchgo.ValidateOutput {
  if args.NewDoc["type"] == "post" {
    if args.NewDoc["title"] == nil || args.NewDoc["content"] == nil {
      return couchgo.ForbiddenError{Message: "Title and content are required"}
    }

    return nil
  }

  if args.NewDoc["type"] == "comment" {
    if args.NewDoc["post"] == nil || args.NewDoc["author"] == nil || args.NewDoc["content"] == nil {
      return couchgo.ForbiddenError{Message: "Post, author, and content are required"}
    }

    return nil
  }

  return nil
}

In this function, we receive parameters such as the new document, old document, user context, and security object, all wrapped into one object passed as a function argument. Here, we’re expected to validate if the document can be updated and return an error if not. Similar to the JavaScript version, we can return two types of errors: ForbiddenError or UnauthorizedError. If the document can be updated, we should return nil.

For more detailed examples, they can be found in my GitHub repository. One important thing to note is that the function names are not arbitrary; they should always match the type of function they represent, such as Map, Reduce, Filter, etc.

CouchGO! Performance

Even though writing my own Query Server was a really fun experience, it wouldn’t make much sense if I didn’t compare it with existing solutions. So, I prepared a few simple tests in a Docker container to check how much faster CouchGO! can:

  • Index 100k documents (indexing in CouchDB means executing map functions from views)
  • Execute reduce function for 100k documents
  • Filter change feed for 100k documents
  • Perform update function for 1k requests

I seeded the database with the expected number of documents and measured response times or differentiated timestamp logs from the Docker container using dedicated shell scripts. The details of the implementation can be found in my GitHub repository. The results are presented in the table below.

Test CouchGO! CouchJS Boost
Indexing 141.713s 421.529s 2.97x
Reducing 7672ms 15642ms 2.04x
Filtering 28.928s 80.594s 2.79x
Updating 7.742s 9.661s 1.25x

As you can see, the boost over the JavaScript implementation is significant: almost three times faster in the case of indexing, more than twice as fast for reduce and filter functions. The boost is relatively small for update functions, but still faster than JavaScript.

Conclusion

As the author of the documentation promised, writing a custom Query Server wasn’t that hard when following the Query Server Protocol. Even though CouchGO! lacks a few deprecated functions in general, it provides a significant boost over the JavaScript version even at this early stage of development. I believe there is still plenty of room for improvements.

If you need all the code from this article in one place, you can find it in my GitHub repository.

Thank you for reading this article. I would love to hear your thoughts about this solution. Would you use it with your CouchDB instance, or maybe you already use some custom-made Query Server? I would appreciate hearing about it in the comments.

Don’t forget to check out my other articles for more tips, insights, and other parts of this series as they are created. Happy hacking!

以上是沙發GO! — 使用 Go 編寫的查詢伺服器增強 CouchDB的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn