使用 CouchDB 作为状态数据库

状态数据库选项

状态数据库选项包括 LevelDB 和 CouchDB 。LevelDB 是默认嵌入在 Peer 程序中的键-值数据库。 CouchDB 是一个可选的额外扩展的状态数据库。就像 LevelDB 键-值存储一样,CouchDB 可以存储任何在链码中建模的二进制数据(CouchDB 附件功能在内部用于非 JSON 二进制数据)。但是作为一个 JSON 文档存储数据库,当链码值(比如,资产)以 JSON 数据建模时,CouchDB 额外支持链码数据的富查询。

LevelDB 和 CouchDB 都支持链码操作,比如获取或者设置一个键(资产),以及基于键查询。键可以按范围查询,以复合键建模就可以支持针对多个参数的等效查询。比如一个复合键 owner,asset_id 就可以查询一个实体的所有资产。这些基于键的查询可以用来在账本上做只读查询,同样可以用于在交易中更新账本

如果你以 JSON 建模资产并使用 CouchDB,你就可以在链码中使用 CouchDB JSON 来使用富查询。这种方式对于理解账本上存了什么内容是非常好的。这些类型查询的提案响应对于客户端应用程序是很有用的,但是对于提交到排序服务的交易就不是太有价值了。事实上,无法保证在链码执行和提交的时候富查询的结果不被改变,因此富查询不适合用于账本更新的交易中,除非你的应用程序可以保证查询的结果不会被改变,并且在子交易中可以很好的处理潜在的变化。例如,如果你想以富查询的方式查询 Alice 所有的资产,然后转移给 Bob,在链码执行和提交期间可能会有其他交易向 Alice 发送新的资产,这时你就是是去这些“幻像”。

CouchDB 以独立的进程和 peer 一起运行,因此需要一些额外的配置来设置、管理和操作。也许你想以 LevelDB 开始,但是当你需要富查询的时候再更换到 CouchDB。以 JSON 的方式建模脸链码资产数据是一个好的方式,这样你就可以在需要富查询的时候进行切换了。

注解

CouchDB JSON 文档只能包含合法的 UTF-8 字符串并且不能以下划线开头(“_”)。无论你使用 CouchDB 还是 LevelDB 都不要在键中使用 U+0000 (空字节)。

CouchDB JSON 文档中不能使用一下值作为顶字段的名字。这些名字为内部保留字段。 - 任何以下划线开头的字段,“_” - ~version

从链码中使用 CouchDB

链码查询

链码 API 中大部分方法在 LevelDB 或者 CouchDB 状态数据库中都可用,例如 GetStatePutStateGetStateByRangeGetStateByPartialCompositeKey。另外当你使用 CouchDB 作为状态数据库并且在链码中以 JSON 建模资产的时候,你可以使用 GetQueryResult 通过向 CouchDB 发送查询字符串的方式使用富查询。查询字符串请参考 CouchDB JSON 查询语法

marbles02 示例 演示了如何从链码中使用 CouchDB 查询。它包含了一个 queryMarblesByOwner() 方法,通过向链码传递所有者 id 来演示如何通过参数查询。它还使用 JSON 查询语法在状态数据中查询符合 “docType” 的弹珠的所有者 id:

{"selector":{"docType":"marble","owner":<OWNER_ID>}}

CouchDB 分页

Fabric 支持对富查询和范围查询结果的分页。API 支持范围查询和富查询使用页大小和书签进行分页。要支持高效的分页,必须使用 Fabric 的分页 API。特别地,CouchDB 不支持 limit 关键字,分页是由 Fabric 来管理并隐式地按照 pageSize 的设置进行分页。

如果是通过查询 API (GetStateByRangeWithPagination()GetStateByPartialCompositeKeyWithPagination()、和 GetQueryResultWithPagination())来指定 pageSize 的,返回给链码的结果(以 pageSize 为范围)会带有一个书签。该书签会返回给调用链码的客户端,客户端可以根据这个书签来查询结果的下一“页”。

分页 API 只能用于只读交易中,查询结果旨在支持客户端分页的需求。对于需要读和写的交易,请使用不带分页的链码查询 API。在链码中,您通过迭代的方式来获取你想要的深度。

无论是否使用了分页 API,所有链码查询都受限于 core.yaml 中的 totalQueryLimit (默认 100000)。这是链码将要迭代并返回给客户端最多的结果数量,以防意外或者恶意地长时间查询。

注解

无论链码中是否使用了分页,节点都会根据 core.yaml 中的 ``internalQueryLimit``(默认 1000) 来查询 CouchDB。 这样就保证了在执行链码的时候有合理大小的结果在节点和 CouchDB 之间,以及链码和客户端之间传播。

使用 CouchDB 教程中有一个使用分页的示例。

CouchDB 索引

CouchDB 中的索引用来提升 JSON 查询的效率以及按顺序的 JSON 查询。索引可以在 /META-INF/statedb/couchdb/indexes 文件夹中和链码打包在一起。每一个索引文件必须定义在一个扩展名为 *.json 的文本文件中,文件内容符合 CouchDB 索引 JSON 语法 。例如,要想支持上边提到的弹珠查询,提供了一个 docTypeowner 字段的简单索引文件:

{"index":{"fields":["docType","owner"]},"ddoc":"indexOwnerDoc", "name":"indexOwner","type":"json"}

索引文件可以在 这里 找到。

在链码的 META-INF/statedb/couchdb/indexes 文件夹下的所有索引都会打包到链码中以便部署。当链码在 Peer 节点上安装和在通道上实例化后,索引会自动部署在 Peer 节点的通道和链码指定的状态数据库中(如果它被配置为使用 CoucbDB)。如果你先在 Peer 节点上安装了链码,之后才会在通道上实例化链码,索引会在链码 实例化 的时候部署。如果链码已经在通道上实例化了,你之后又在 Peer 节点上安装了链码,索引会在链码 安装 的时候部署。

部署之后,调用链码查询的时候会自动使用索引。CouchDB 会根据查询的字段选择使用哪个索引。或者,在查询选择器中通过 use_index 关键字指定要使用的索引。

安装的不同版本的链码可能会有相同版本的索引。要更改索引,需要使用相同的索引名称但是不同的索引定义。在安装或者实例化完成的时候,索引就会重新被部署到 Peer 节点的状态数据库了。

如果你已经有了大量的数据,然后才安装或者初始化链码,在安装或初始化的过程中索引的创建可能会花费一些时间。在索引创建的过程中请不要调用来嘛查询状态数据库。在交易的过程中,区块提交到账本后索引会自动更新。

CouchDB 配置

通过在 stateDatabase 状态选项中将 goleveldb 切换为 CouchDB 可以启用 CouchDB 状态数据库。另外配置 couchDBAddress 来指向 Peer 节点所使用的 CouchDB。如果 CouchDB 设置了用户名和密码,也需要在配置中指定。其他的配置选项在 couchDBConfig 部分也都有相关说明。重启 Peer 节点就可以使 core.yaml 文件立马生效。

你也可以使用环境变量来覆盖 core.yaml 中的值,例如 CORE_LEDGER_STATE_STATEDATABASECORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS

下边是 core.yaml 中的 stateDatabase 部分:

state:
  # stateDatabase - options are "goleveldb", "CouchDB"
  # goleveldb - default state database stored in goleveldb.
  # CouchDB - store state database in CouchDB
  stateDatabase: goleveldb
  # Limit on the number of records to return per query
  totalQueryLimit: 10000
  couchDBConfig:
     # It is recommended to run CouchDB on the same server as the peer, and
     # not map the CouchDB container port to a server port in docker-compose.
     # Otherwise proper security must be provided on the connection between
     # CouchDB client (on the peer) and server.
     couchDBAddress: couchdb:5984
     # This username must have read and write authority on CouchDB
     username:
     # The password is recommended to pass as an environment variable
     # during start up (e.g. LEDGER_COUCHDBCONFIG_PASSWORD).
     # If it is stored here, the file must be access control protected
     # to prevent unintended users from discovering the password.
     password:
     # Number of retries for CouchDB errors
     maxRetries: 3
     # Number of retries for CouchDB errors during peer startup
     maxRetriesOnStartup: 10
     # CouchDB request timeout (unit: duration, e.g. 20s)
     requestTimeout: 35s
     # Limit on the number of records per each CouchDB query
     # Note that chaincode queries are only bound by totalQueryLimit.
     # Internally the chaincode may execute multiple CouchDB queries,
     # each of size internalQueryLimit.
     internalQueryLimit: 1000
     # Limit on the number of records per CouchDB bulk update batch
     maxBatchUpdateSize: 1000
     # Warm indexes after every N blocks.
     # This option warms any indexes that have been
     # deployed to CouchDB after every N blocks.
     # A value of 1 will warm indexes after every block commit,
     # to ensure fast selector queries.
     # Increasing the value may improve write efficiency of peer and CouchDB,
     # but may degrade query response time.
     warmIndexesAfterNBlocks: 1

Hyperledger Fabric 提供的 CouchDB docker 镜像可以通过 Docker Compose 脚本来定义 COUCHDB_USERCOUCHDB_PASSWORD 环境变量,从而设置 CouchDB 管理员的用户名和密码。

如果没有使用 Fabric 提供的 docker 镜像安装 CouchDB,必须编辑 local.ini 文件 来设置管理员的用户名和密码。

Docker Compose 脚本只能在创建容器的时候设置用户名和密码。在容器创建之后,必须使用 local.ini 文件来修改用户名和密码。

注解

每次 Peer 节点启动的时候都会读取 CouchDB 节点的选项。

查询练习

避免对将导致扫描整个 CouchDB 数据库的;链码查询。全长数据库扫描将导致较长的响应时间,并将降低您的网络性能。您可以采取以下一些步骤来避免长时间查询:

  • 使用 JSON 查询:

    • 确保在链码包中创建了索引。
    • 不要使用 $or$in$regex 之类会扫描整个数据库的操作。
  • 对于范围查询、复合键查询和 JSON 查询:

    • 使用分页查询(就像 v1.3 中所说),不要使用一个大的查询结果。
  • 如果在您的应用中想创建一个仪表盘(dashboard)或者聚合数据,您可以将区块链数据复制到链下的数据库中,通过链下数据库来查询或分析区块链数据,以此来优化数据存储,并防止网络性能的降低或交易的终端。要实现这个功能,可以通过区块或链码事件将交易数据写入链下数据库或者分析引擎。对于每一个接收到的区块,区块监听应用将遍历区块中的每一个交易并根据每一个有效交易的 读写集 中的键值对构建一个数据存储。文档 基于通道的节点事件服务 提供了可重放事件,以确保下游数据存储的完整性。