在过去的一个月里,我一直在积极从事与 CouchDB 相关的概念验证项目,探索其功能并为未来的任务做准备。在此期间,我多次阅读了 CouchDB 文档,以确保我了解一切是如何工作的。在阅读文档时,我发现了这样的说法:尽管 CouchDB 附带了用 JavaScript 编写的默认查询服务器,但创建自定义实现相对简单,并且自定义解决方案已经存在。
我做了一些快速研究,发现了用 Python、Ruby 或 Clojure 编写的实现。由于整个实现看起来并不太长,因此我决定通过尝试编写自己的自定义查询服务器来尝试 CouchDB。为此,我选择 Go 作为语言。除了在 Helm 图表中使用 Go 模板之外,我之前对这种语言没有太多经验,但我想尝试一些新东西,并认为这个项目将是一个很好的机会。
在开始工作之前,我再次回顾了 CouchDB 文档,以了解查询服务器的实际工作原理。根据文档,查询服务器的高级概述非常简单:
查询服务器是一个外部进程,它通过 stdio 接口通过 JSON 协议与 CouchDB 进行通信,并处理所有设计函数调用 [...]。
CouchDB 发送到查询服务器的命令结构可以表示为 [
所以基本上,我要做的就是编写一个能够从 STDIO 解析此类 JSON、执行预期操作并返回文档中指定的响应的应用程序。 Go 代码中涉及大量类型转换来处理各种命令。有关每个命令的具体详细信息可以在文档的查询服务器协议部分找到。
我在这里遇到的一个问题是查询服务器应该能够解释和执行设计文档中提供的任意代码。知道 Go 是一种编译语言,我预计会在这一点上陷入困境。值得庆幸的是,我很快就找到了 Yeagi 包,它能够轻松解释 Go 代码。它允许创建沙箱并控制对可以在解释代码中导入的包的访问。就我而言,我决定仅公开我的名为 couchgo 的包,但也可以轻松添加其他标准包。
作为我工作的成果,开发了一个名为 CouchGO! 的应用程序!出现了。尽管它遵循查询服务器协议,但它不是 JavaScript 版本的一对一重新实现,因为它有自己的方法来处理设计文档功能。
例如,在CouchGO!中,没有像emit这样的辅助函数。要发出值,您只需从映射函数返回它们即可。此外,设计文档中的每个函数都遵循相同的模式:它只有一个参数,该参数是一个包含特定于函数的属性的对象,并且应该只返回一个值作为结果。该值不必是原始值;根据函数的不同,它可能是一个对象、一个映射,甚至是一个错误。
要开始使用 CouchGO!,您只需从我的 GitHub 存储库下载可执行二进制文件,将其放置在 CouchDB 实例中的某个位置,然后添加一个允许 CouchDB 启动 CouchGO! 的环境变量!过程。
例如,如果将 couchgo 可执行文件放入 /opt/couchdb/bin 目录中,则需要添加以下环境变量以使其能够工作。
export COUCHDB_QUERY_SERVER_GO="/opt/couchdb/bin/couchgo"
为了快速了解如何使用 CouchGO! 编写函数,让我们探索以下函数接口:
func Func(args couchgo.FuncInput) couchgo.FuncOutput { ... }
CouchGO! 中的每个功能!将遵循此模式,其中 Func 被替换为适当的函数名称。目前,CouchGO!支持以下函数类型:
让我们检查一个示例设计文档,该文档指定具有 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" }
现在,我们从map函数开始分解各个函数:
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 }
在CouchGO!中,没有emit函数;相反,您返回一个键值元组切片,其中键和值可以是任何类型。文档对象并不像 JavaScript 中那样直接传递给函数;而是直接传递给函数。相反,它被包裹在一个对象中。文档本身只是各种值的哈希图。
接下来我们来看看reduce函数:
func Reduce(args couchgo.ReduceInput) couchgo.ReduceOutput { out := 0.0 for _, value := range args.Values { out = value.(float64) } return out }
与JavaScript类似,CouchGO中的reduce函数!接受键、值和 rereduce 参数,所有这些都包装在一个对象中。此函数应返回表示归约运算结果的任何类型的单个值。
最后我们看一下Validate函数,它对应的validate_doc_update属性:
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 }
在此函数中,我们接收新文档、旧文档、用户上下文和安全对象等参数,所有这些参数都包装到作为函数参数传递的一个对象中。在这里,我们需要验证文档是否可以更新,如果不能更新则返回错误。与 JavaScript 版本类似,我们可以返回两种类型的错误:ForbiddenError 或 UnauthorizedError。如果文档可以更新,我们应该返回nil。
有关更详细的示例,可以在我的 GitHub 存储库中找到。需要注意的一件重要事情是函数名称不是任意的;它们应该始终匹配它们所代表的函数类型,例如 Map、Reduce、Filter 等。
尽管编写自己的查询服务器是一种非常有趣的体验,但如果我不将其与现有解决方案进行比较,那就没有多大意义。因此,我在 Docker 容器中准备了一些简单的测试,以检查 CouchGO 的速度有多快!能:
我使用专用 shell 脚本将预期数量的文档植入数据库,并测量响应时间或区分 Docker 容器的时间戳日志。实现的详细信息可以在我的 GitHub 存储库中找到。结果如下表所示。
测试 | CouchGO! | CouchJS | 促进 |
---|---|---|---|
索引 | 141.713s | 421.529s | 2.97x |
减少 | 7672ms | 15642ms | 2.04x |
过滤 | 28.928s | 80.594s | 2.79x |
更新中 | 7.742s | 9.661s | 1.25x |
正如您所看到的,JavaScript 实现的提升是显着的:索引的速度几乎是原来的三倍,reduce 和过滤函数的速度是原来的两倍多。对于更新函数来说,提升相对较小,但仍然比 JavaScript 更快。
正如文档作者所承诺的那样,遵循查询服务器协议时编写自定义查询服务器并不那么困难。尽管 CouchGO!一般来说,缺少一些已弃用的函数,即使在开发的早期阶段,它也比 JavaScript 版本提供了显着的提升。我相信还有很大的改进空间。
如果您需要将本文中的所有代码集中到一个地方,您可以在我的 GitHub 存储库中找到它。
感谢您阅读本文。我很想听听您对此解决方案的想法。您会将其与 CouchDB 实例一起使用吗?或者您可能已经使用了一些定制的查询服务器?我很高兴在评论中听到它。
不要忘记查看我的其他文章,以获取更多提示、见解以及本系列的其他部分。快乐黑客!
免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。
Copyright© 2022 湘ICP备2022001581号-3