fabric 2.0 外部构建及运行链码新功能探索

作者 tinywell 日期 2020-03-12
fabric 2.0 外部构建及运行链码新功能探索

fabric 2.0 在链码这块的改动非常大,除了之前分享过的新的链码生命周期之外,另一个比较大的更新是支持链码的外部构建和启动以及链码服务的外部运行。

在 2.0 以前,链码的整个生命周期完完全全由 peer 服务掌控,链码必须以源码打包的形式安装到 peer 节点,然后由 peer 调用 docker api 构建镜像并启动容器。这对运维工作并不是特别友好,比如有的企业需要对所有容器进行统一管理或者由指定平台运行,原有方式就会很困难。在 2.0 中,新增支持外部构建和启动,即能够自定义链码的构建和启动,不完全由 peer 掌控。在这个基础上,还支持链码的外部运行,这让链码的管理方式更加灵活。

链码的外部构建和启动

在 2.0 中外部构建和启动功能是基于 Heroku 的 Buildpacks 实现的。buildpack 可以将源码等资料转化为一个可运行的实体,它包含一系列的脚本或者小程序。通常它包含如下几个部分:

**1.bin/detect**:用于前置检测和判断,最终判定当前链码是否用这个构建器来构建并启动。比如检测当前链码的语言类型,是否适合这个构建器来构建;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh

CHAINCODE_METADATA_DIR="$2"

set -euo pipefail

# use jq to extract the chaincode type from metadata.json and exit with
# success if the chaincode type is golang
# 判断目录下有没有metadata.json文件 有就exit 0
if [ "$(cat "$CHAINCODE_METADATA_DIR/metadata.json" | sed -e 's/[{}]/''/g' | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/'type'\042/){print $(i+1)}}}' | tr -d '"')" = "external" ]; then
exit 0
fi

exit 1

**2.bin/build**:用来执行构建动作,将链码相关的原始资料转化为一个可运行的实体,比如用 go build 将一个 go编写的链码编译为一个可执行程序;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh

CHAINCODE_SOURCE_DIR="$1"
CHAINCODE_METADATA_DIR="$2"
BUILD_OUTPUT_DIR="$3"

set -euo pipefail

#external chaincodes expect connection.json file in the chaincode package
if [ ! -f "$CHAINCODE_SOURCE_DIR/connection.json" ]; then
>&2 echo "$CHAINCODE_SOURCE_DIR/connection.json not found"
exit 1
fi

#simply copy the endpoint information to specified output location
cp $CHAINCODE_SOURCE_DIR/connection.json $BUILD_OUTPUT_DIR/connection.json

if [ -d "$CHAINCODE_SOURCE_DIR/metadata" ]; then
cp -a $CHAINCODE_SOURCE_DIR/metadata $BUILD_OUTPUT_DIR/metadata
fi

exit 0

**3.bin/release**(可选):将链码相关的元数据提供给 peer,目前有两种类型的数据是 peer 可以从 这个步骤的产出中消费的:1. couchDB 的索引数据,一般放在和源码一起打包的 META-INF 文件夹里;2. 连接到外部链码服务相关的连接信息 connection.json;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/bin/sh

set -euo pipefail

BLD="$1"
RELEASE="$2"

if [ -d "$BLD/metadata" ]; then
cp -a "$BLD/metadata/"* "$RELEASE/"
fi

#external chaincodes expect artifacts to be placed under "$RELEASE"/chaincode/server
if [ -f $BLD/connection.json ]; then
mkdir -p "$RELEASE"/chaincode/server
cp $BLD/connection.json "$RELEASE"/chaincode/server

#if tls_required is true, copy TLS files (using above example, the fully qualified path for these fils would be "$RELEASE"/chaincode/server/tls)

exit 0
fi

exit 1

**4.bin/run**(可选):将链码启动起来。

如何让 peer 使用外部构建器和启动器呢?通过修改 core.yaml 中的相关配置:

1
2
3
4
5
6
chaincode:
externalBuilders:
- name: my-golang-builder
path: /builders/golang
- name: noop-builder
path: /builders/binary

上面的示例中配置了两个外部构建器,my-golang-buildernoop-builder,它们的脚本/程序分别放在 peer 的 /builders/golang/builders/golang路径下,并以 bin/文件夹包裹。

当 peer 服务检测到有外部构建器配置时,每次安装链码的时候就会依次调用配置的外部构建器的 bin/detect,逐个尝试直到成功为止,如果所有的外部构建器都失败了,则使用原始的链码构建方式。

链码的外部运行

有了对外部链码构建和启动机制的支持,现在 fabric 可以支持直接在外部运行链码服务,完全独立于 peer 服务,这样链码的运行形式完全“自主可控”。

如果将链码在外部运行,理论上我们不需要把链码安装到 peer 节点了,但是我们需要 peer 能够知道这个外部链码服务的存在并能够与之建立链接,所以我们还是要准备包含了这个外部链码相关信息的链码包并利用外部构建器和运行器在安装的过程中提供给 peer,只是不再需要链码的源码了。2.0 中链码包都是标准的 .tar.gz 格式,我们的链码包mycc.tar.gz需要包含以下文件:

1
2
3
4
5
mycc.tar.gz
|\_metadata.json --- 1
\_code.tar.gz
|\_metadata/ --- 2
\_connection.json --- 3
  1. metadata.json:定义链码基本信息,它与正常用 peer lifecycle package 生成的链码包中的 metadada.json 的区别是不用配置 path 项,另外 type 项需要定义一个特殊的用于 bin/detect 识别的类型;

    1
    {"path":"","type":"external","label":"mycc"}
  2. metadata:couchDB 索引,如果没有可以不包含这个目录(不要用空目录);

  3. connection.json:定义链接外部链码服务的信息,bin/buildbin/release 将这个文件逐步提供给 peer,它包含以下要素:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "address": "host.docker.internal:9999", // 外部链码服务地址
    "dial_timeout": "10s",
    "tls_required": false, // 这里必须是 js 的 bool 类型,官网示例中用了双引号,变成字符串,会导致出错,下面同理
    "client_auth_required": false,
    "client_key": "-----BEGIN EC PRIVATE KEY----- ... -----END EC PRIVATE KEY-----",
    "client_cert": "-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----",
    "root_cert": "-----BEGIN CERTIFICATE---- ... -----END CERTIFICATE-----"
    }

    通过 tar 命令完成打包:

    1
    2
    tar czvf code.tar.gz connection.json
    tar czvf mycc.tar.gz code.tar.gz metadata.json

之后通过 peer lifecycle 系列命令进行链码的安装部署。

1
2
3
peer lifecycle chaincode install mycc.tar.gz
peer lifecycle chaincode approveformyorg
peer lifecycle chaincode commit

研究学习的话,可以使用 fabric-sample,稍微修改下 first-network 相关的启动脚本和运行脚本即可。当我们配置了外部构建器和启动器(上一节中的示例脚本),则在安装时将安装包中相关的资源提供给 peer,用于连接外部链码服务。

接下来,我们要准备用于外部运行的链码服务了。在 2.0 中,fabric 将源码进行了切分,用于链码开发的 shim 包已经切分出去为一个单独的项目 github.com/hyperledger/fabric-chaincode-go/shim,另外用于 grpc 的基础数据结构 proto 包也切分为单独的项目 github.com/hyperledger/fabric-protos-goshim 包中新增了一个 ChaincodeServer 结构,专门用于外部链码服务的开发,链码核心部分与原来几乎一样。我们用 fabric-sample 中的 abstore 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {

server := &shim.ChaincodeServer{
CCID: "mycc:f3e1f72620f7daa511759190823afd1cc57c606f40a1e99ef68e48fe655bdfce",
Address: "localhost:9999",
CC: new(ABstore),
TLSProps: shim.TLSProperties{
Disabled: true,
},
}
err := server.Start()
if err != nil {
fmt.Printf("Error starting Simple chaincode: %s", err)
}
}

所需参数很简单:CCID是指安装链码后生成的链码标识,peer lifecycle chaincode install 命令会返回这个信息,或者通过 peer lifecycle chaincode queryinstalled 查询得到。所以这个参数最好是可配置,然后在安装链码之后才能得到,如果你是用 fabric-sample 的脚本运行的话,在安装和执行动作之间留出足够的时间去配置这个参数,或者你可以拆成两个脚本分别执行。Address是链码服务监听地址。CC是链码实例。TLSProps是 TLS 相关参数。

之后这个服务就可以用任何你喜欢的方式进行部署。比如我们直接 go run,然后fabric 网络中调用合约:

1
2
3
4
5
6
7
8
9
10
➜  chaincdoe go run chaincode.go 
ABstore Init
Aval = 100, Bval = 100
ABstore Invoke
Query Response:{"Name":"a","Amount":"100"}
ABstore Invoke
Aval = 90, Bval = 110
ABstore Invoke
Query Response:{"Name":"a","Amount":"90"}

by the way,由于这种外部运行的链码和 peer 完全解绑,它们不在必须是一对一的关系。是的,你甚至可以多个 peer 都连接同一个链码服务来执行交易,又或者部署一个链码服务集群~

参考链接