使用caliper对fisco-bcos进行压力测试
通过Caliper进行压力测试程序
注意:官网给出的测试案例会出现错误,我会给出相应的解决方案,本文以centos系统为例进行测试
1. 环境要求
第一步:配置基本环境
- 部署Caliper的计算机需要安装有以下软件:python 2.7、make、g++、gcc及git。
- 操作系统满足以下要求:centos>=7
第二步:安装NodeJs
NodeJS 版本建议 8 (LTS), 9, 或 10 (LTS)。
安装步骤:
# 安装nvm
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash
# 若出现因网络问题导致长时间下载失败,可尝试以下命令
curl -o- https://gitee.com/mirrors/nvm/raw/v0.33.2/install.sh | bash
# 加载nvm配置
source ~/.$(basename $SHELL)rc
# 安装Node.js 8
nvm install 8
# 使用Node.js 8
nvm use 8
第三步:部署Docker
版本要求:>= 18.06.01
安装步骤:
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum makecache fast
sudo yum install docker-ce
service docker start
ps -A | grep docker
systemctl enable docker
第四步:安装Docker Compose
版本要求:>= 1.22.0
安装步骤:
sudo curl -L “https://github.com/docker/compose/releases/download/1.24.0/docker-compose-(uname − s ) − (uname -s)-(uname −s)−(uname -m)” -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
2. Caliper部署
第一步:部署
Caliper提供了方便易用的命令行界面工具caliper-cli
具体步骤
1. 建立一个工作目录
mkdir benchmarks && cd benchmarks
2. 对npm项目进行初始化
npm init -y
3. 安装caliper-cli
npm install --only=prod @hyperledger/caliper-cli@0.2.0
4. 验证caliper-cli安装成功
npx caliper --version
5. 如安装成功,则会打印相应的版本信息,如下所示:
第二步:绑定
由于Caliper采用了轻量级的部署方式,因此需要显式的绑定步骤指定要测试的平台及适配器版本,caliper-cli会自动进行相应依赖项的安装。
使用如下方式绑定fisco-bcos
npx caliper bind --caliper-bind-sut fisco-bcos --caliper-bind-sdk latest
注意:在部署的时候可能会出现如下问题,
针对这个问题,去http://ping.chinaz.com/找海外节点信息即可
按照如下步骤进行配置域名
针对这个问题
第三步:快速体验fisco-bcos的基准测试
为方便测试人员快速上手,FISCO BCOS已经为Caliper提供了一组预定义的测试样例,测试对象涵盖HelloWorld合约、Solidity版转账合约及预编译版转账合约。同时在测试样例中,Caliper测试脚本会使用docker在本地自动部署及运行4个互连的节点组成的链,因此测试人员无需手工搭链及编写测试用例便可直接运行这些测试样例。
具体的步骤如下
1. 在工作目录下下载预定义测试用例
# 拉取gitee代码
git clone https://gitee.com/vita-dounai/caliper-benchmarks.git
2. 执行HelloWorld合约测试
npx caliper benchmark run --caliper-workspace caliper-benchmarks --caliper-benchconfig benchmarks/samples/fisco-bcos/helloworld/config.yaml --caliper-networkconfig networks/fisco-bcos/4nodes1group/fisco-bcos.json
################################测试结果如下#################################
3. 执行Solidity版转账合约测试
npx caliper benchmark run --caliper-workspace caliper-benchmarks --caliper-benchconfig benchmarks/samples/fisco-bcos/transfer/solidity/config.yaml --caliper-networkconfig networks/fisco-bcos/4nodes1group/fisco-bcos.json
################################测试结果如下#################################
4.执行预编译版转账合约测试
npx caliper benchmark run --caliper-workspace caliper-benchmarks --caliper-benchconfig benchmarks/samples/fisco-bcos/transfer/precompiled/config.yaml --caliper-networkconfig networks/fisco-bcos/4nodes1group/fisco-bcos.json
################################测试结果如下#################################
3. 需要注意的事项
由于FISCO BCOS对于caliper0.2.0版本的适配存在部分不兼容情况,需要修改代码后方可正常运行。
这里给出需要修改后的文件(注意:如果没有出现问题就不用修改)
fiscoBcos.js文件如下:
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const {
BlockchainInterface,
CaliperUtils
} = require('@hyperledger/caliper-core');
const installSmartContractImpl = require('./installSmartContract');
const invokeSmartContractImpl = require('./invokeSmartContract');
const generateRawTransactionImpl = require('./generateRawTransactions');
const sendRawTransactionImpl = require('./sendRawTransactions');
const Color = require('./common').Color;
const commLogger = CaliperUtils.getLogger('fiscoBcos.js');
/**
* Implements {BlockchainInterface} for a FISCO BCOS backend.
*/
class FiscoBcos extends BlockchainInterface {
/**
* Create a new instance of the {FISCO BCOS} class.
* @param {string} config_path The absolute path of the FISCO BCOS network configuration file.
* @param {string} workspace_root The absolute path to the root location for the application configuration files.
*/
constructor(config_path, workspace_root) {
super(config_path);
this.bcType = 'fisco-bcos';
this.workspaceRoot = workspace_root;
this.fiscoBcosSettings = CaliperUtils.parseYaml(this.configPath)['fisco-bcos'];
if (this.fiscoBcosSettings.network && this.fiscoBcosSettings.network.authentication) {
for (let k in this.fiscoBcosSettings.network.authentication) {
this.fiscoBcosSettings.network.authentication[k] = CaliperUtils.resolvePath(this.fiscoBcosSettings.network.authentication[k], workspace_root);
}
}
}
/**
* Initialize the {FISCO BCOS} object.
* @async
* @return {Promise<object>} The promise for the result of the execution.
*/
async init() {
return Promise.resolve();
}
/**
* Deploy the smart contract specified in the network configuration file to all nodes.
* @async
*/
async installSmartContract() {
const fiscoBcosSettings = this.fiscoBcosSettings;
try {
await installSmartContractImpl.run(fiscoBcosSettings, this.workspaceRoot);
} catch (error) {
commLogger.error(Color.error(`FISCO BCOS smart contract install failed: ${(error.stack ? error.stack : error)}`));
throw error;
}
}
/**
* Get a context for subsequent operations
* 'engine' attribute of returned context object must be reserved for benchmark engine to extend the context
* engine = {
* submitCallback: callback which must be called once new transaction(s) is submitted, it receives a number argument which tells how many transactions are submitted
* }
* @param {String} name name of the context
* @param {Object} args adapter specific arguments
* @param {Integer} clientIdx the client index
* @return {Promise<object>} The promise for the result of the execution.
*/
async getContext(name, args, clientIdx) {
return Promise.resolve();
}
/**
* Release a context as well as related resources
* @param {Object} context adapter specific object
* @return {Promise<object>} The promise for the result of the execution.
*/
async releaseContext(context) {
return Promise.resolve();
}
/**
* Invoke the given smart contract according to the specified options. Multiple transactions will be generated according to the length of args.
* @param {object} context The FISCO BCOS context returned by {getContext}.
* @param {string} contractID The name of the smart contract.
* @param {string} contractVer The version of the smart contract.
* @param {Array} args Array of JSON formatted arguments for transaction(s). Each element contains arguments (including the function name) passing to the smart contract. JSON attribute named transaction_type is used by default to specify the function name. If the attribute does not exist, the first attribute will be used as the function name.
* @param {number} timeout The timeout to set for the execution in seconds.
* @return {Promise<object>} The promise for the result of the execution.
*/
async invokeSmartContract(context, contractID, contractVer, args, timeout) {
let promises = [];
try {
args.forEach((arg) => {
let fcn = null;
let fcArgs = [];
for (let key in arg) {
if (key === 'transaction_type') {
fcn = arg[key].toString();
} else {
fcArgs.push(arg[key].toString());
}
}
promises.push(invokeSmartContractImpl.run(context, this.fiscoBcosSettings, contractID, fcn, fcArgs, this.workspaceRoot));
});
return await Promise.all(promises);
} catch (error) {
commLogger.error(Color.error(`FISCO BCOS smart contract invoke failed: ${(error.stack ? error.stack : JSON.stringify(error))}`));
throw error;
}
}
/**
* Query state from the ledger
* @param {Object} context The FISCO BCOS context returned by {getContext}
* @param {String} contractID Identity of the contract
* @param {String} contractVer Version of the contract
* @param {String} key lookup key
* @param {String} fcn The smart contract query function name
* @return {Promise<object>} The result of the query.
*/
async queryState(context, contractID, contractVer, key, fcn) {
try {
return invokeSmartContractImpl.run(context, this.fiscoBcosSettings, contractID, fcn, key, this.workspaceRoot, true);
} catch (error) {
commLogger.error(Color.error(`FISCO BCOS smart contract query failed: ${(error.stack ? error.stack : error)}`));
throw error;
}
}
/**
* Generate an raw transaction and store in local file
* @param {Object} context The FISCO BCOS context returned by {getContext}
* @param {String} contractID Identity of the contract
* @param {Object} arg Arguments of the transaction
* @param {String} file File path which will be used to store then transaction
* @return {TaskStatus} Indicates whether the transaction is written to the file successfully or not
*/
async generateRawTransaction(context, contractID, arg, file) {
return generateRawTransactionImpl.run(this.fiscoBcosSettings, this.workspaceRoot, context, contractID, arg, file);
}
/**
* Send raw transactions
* @param {Object} context The FISCO BCOS context returned by {getContext}
* @param {Array} transactions List of raw transactions
* @return {Promise} The promise for the result of the execution
*/
async sendRawTransaction(context, transactions) {
return sendRawTransactionImpl.run(this.fiscoBcosSettings, context, transactions);
}
}
module.exports = FiscoBcos;
channelPromise.js如下所示:
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const tls = require('tls');
const fs = require('fs');
const net = require('net');
const uuidv4 = require('uuid/v4');
const events = require('events');
/**
* NetworkError exception class thrown in socket connection
*/
class NetworkError extends Error {
/**
*
* @param {String} msg exception message
*/
constructor(msg) {
super(msg);
this.name = 'NetworkError';
}
}
let emitters = new Map();
let buffers = new Map();
let sockets = new Map();
let lastBytesRead = new Map();
/**
* Parse response returned by node
* @param {Buffer} response Node's response
*/
function parseResponse(response) {
let seq = response.slice(6, 38).toString();
let result = JSON.parse(response.slice(42).toString());
let emitter = emitters.get(seq);
if(!emitter) {
//Stale message receieved
return;
}
emitter = emitter.emitter;
if (emitter) {
let readOnly = Object.getOwnPropertyDescriptor(emitter, 'readOnly').value;
if (readOnly) {
if (result.error || result.result !== undefined ) {
emitter.emit('gotresult', result);
}
} else {
if (result.error || result.status || (result.result && result.result.status)) {
emitter.emit('gotresult', result);
} else {
if (!result.result) {
throw new NetworkError(`unknown message receieved, seq=${seq}, data=${response.toString()}`);
}
}
}
} else {
throw new NetworkError(`unknown owner message receieved, seq=${seq}, data=${response.toString()}`);
}
}
/**
* Create a new TLS socket
* @param {String} ip IP of channel server
* @param {Number} port Port of channel server
* @param {Object} authentication A JSON object contains certificate file path, private key file path and CA file path
* @return {TLSSocket} A new TLS socket
*/
function createNewSocket(ip, port, authentication) {
let secureContextOptions = {
key: fs.readFileSync(authentication.key),
cert: fs.readFileSync(authentication.cert),
ca: fs.readFileSync(authentication.ca),
ecdhCurve: 'secp256k1',
};
let secureContext = tls.createSecureContext(secureContextOptions);
let socket = new net.Socket();
socket.connect(port, ip);
let clientOptions = {
rejectUnauthorized: false,
secureContext: secureContext,
socket: socket
};
let tlsSocket = tls.connect(clientOptions);
tlsSocket.on('error', function (error) {
throw new Error(error);
});
let socketID = `${ip}:${port}`;
lastBytesRead.set(socketID, 0);
tlsSocket.on('data', function (data) {
let response = null;
if (data instanceof Buffer) {
response = data;
}
else {
response = Buffer.from(data, 'ascii');
}
if (!buffers.has(socketID)) {
// First time to read data from this socket
let expectedLength = null;
if (tlsSocket.bytesRead - lastBytesRead.get(socketID) >= 4) {
expectedLength = response.readUIntBE(0, 4);
}
if (!expectedLength || tlsSocket.bytesRead < lastBytesRead.get(socketID) + expectedLength) {
buffers.set(socketID, {
expectedLength: expectedLength,
buffer: response
});
} else {
lastBytesRead.set(socketID, lastBytesRead.get(socketID) + expectedLength);
parseResponse(response);
buffers.delete(socketID);
}
} else {
// Multiple reading
let cache = buffers.get(socketID);
cache.buffer = Buffer.concat([cache.buffer, response]);
if (!cache.expectedLength && tlsSocket.bytesRead - lastBytesRead.get(socketID) >= 4) {
cache.expectedLength = cache.buffer.readUIntBE(0, 4);
}
if (cache.expectedLength && tlsSocket.bytesRead - lastBytesRead.get(socketID) >= cache.expectedLength) {
lastBytesRead.set(socketID, lastBytesRead.get(socketID) + cache.expectedLength);
parseResponse(buffers.get(socketID).buffer);
buffers.delete(socketID);
}
}
});
return tlsSocket;
}
/**
* Prepare the data which will be sent to channel server
* @param {String} data JSON string of load
* @return {Object} UUID and packaged data
*/
function packageData(data) {
const headerLength = 4 + 2 + 32 + 4;
let length = Buffer.alloc(4);
length.writeUInt32BE(headerLength + data.length);
let type = Buffer.alloc(2);
type.writeUInt16BE(0x12);
let uuid = uuidv4();
uuid = uuid.replace(/-/g, '');
let seq = Buffer.from(uuid, 'ascii');
let result = Buffer.alloc(4);
result.writeInt32BE(0);
let msg = Buffer.from(data, 'ascii');
return {
'uuid': uuid,
'packagedData': Buffer.concat([length, type, seq, result, msg])
};
}
/**
* Clear context when a message got response or timeout
* @param {String} uuid The ID of an `channelPromise`request
*/
function clearContext(uuid) {
clearTimeout(emitters.get(uuid).timer);
emitters.delete(uuid);
buffers.delete(uuid);
}
/**
* Return channel promise for a request
* @param {Object} node A JSON object which contains IP and port configuration of channel server
* @param {Object} authentication A JSON object contains certificate file path, private key file path and CA file path
* @param {String} data JSON string of load
* @param {Number} timeout Timeout to wait response
* @param {Boolean} readOnly Is this request read-only?
* @return {Promise} a promise which will be resolved when the request is satisfied
*/
function channelPromise(node, authentication, data, timeout, readOnly = false) {
let ip = node.ip;
let port = node.channelPort;
let connectionID = `${ip}${port}`;
if (!sockets.has(connectionID)) {
let newSocket = createNewSocket(ip, port, authentication);
newSocket.unref();
sockets.set(connectionID, newSocket);
}
let tlsSocket = sockets.get(connectionID);
let dataPackage = packageData(JSON.stringify(data));
let uuid = dataPackage.uuid;
tlsSocket.socketID = uuid;
let packagedData = dataPackage.packagedData;
let channelPromise = new Promise(async (resolve, reject) => {
let eventEmitter = new events.EventEmitter();
Object.defineProperty(eventEmitter, 'readOnly', {
value: readOnly,
writable: false,
configurable: false,
enumerable: false
});
eventEmitter.on('gotresult', (result) => {
clearContext(uuid);
if (result.error) {
reject(result);
} else {
resolve(result);
}
return; // This `return` is not necessary, but it may can avoid future trap
});
eventEmitter.on('timeout', () => {
clearContext(uuid);
reject({ 'error': 'timeout' });
return; // This `return` is not necessary, but it may can avoid future trap
});
emitters.set(uuid, {
emitter: eventEmitter,
timer: setTimeout(() => {
eventEmitter.emit('timeout');
}, timeout)
});
tlsSocket.write(packagedData);
});
return channelPromise;
}
module.exports = channelPromise;
web3sync.js如下所示:
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const uuidv4 = require('uuid/v4');
const utils = require('./utils');
const Transaction = require('./transactionObject').Transaction;
/**
* Generate a random number via UUID
* @return {Number} random number
*/
function genRandomID() {
let uuid = uuidv4();
uuid = '0x' + uuid.replace(/-/g, '');
return uuid;
}
/**
* Sign a transaction with private key and callback
* @param {String} txData transaction data
* @param {Buffer} privKey private key
* @param {callback} callback callback function
* @return {String} signed transaction data
*/
function signTransaction(txData, privKey, callback) {
let tx = new Transaction(txData);
let privateKey = Buffer.from(privKey, 'hex');
tx.sign(privateKey);
// Build a serialized hex version of the tx
let serializedTx = '0x' + tx.serialize().toString('hex');
if (callback !== null) {
callback(serializedTx);
} else {
return serializedTx;
}
}
/**
* get transaction data
* @param {String} func function name
* @param {Array} params params
* @return {String} transaction data
*/
function getTxData(func, params) {
let r = /^\w+\((.*)\)$/g.exec(func);
let types = [];
if (r[1]) {
types = r[1].split(',');
}
return utils.encodeTxData(func, types, params);
}
/**
* get signed transaction data
* @param {Number} groupId ID of the group where this transaction will be sent to
* @param {Buffer} account user account
* @param {Buffer} privateKey private key
* @param {Buffer} to target address
* @param {String} func function name
* @param {Array} params params
* @param {Number} blockLimit block limit
* @return {String} signed transaction data
*/
function getSignTx(groupId, account, privateKey, to, func, params, blockLimit) {
let txData = getTxData(func, params);
let postdata = {
data: txData,
from: account,
to: to,
gas: 1000000,
randomid: genRandomID(),
blockLimit: blockLimit,
chainId: 1,
groupId: groupId,
extraData: '0x0'
};
return signTransaction(postdata, privateKey, null);
}
/**
* get signed deploy tx
* @param {Number} groupId ID of the group where this transaction will be sent to
* @param {Buffer} account user account
* @param {Buffer} privateKey private key
* @param {Buffer} bin contract bin
* @param {Number} blockLimit block limit
* @return {String} signed deploy transaction data
*/
function getSignDeployTx(groupId, account, privateKey, bin, blockLimit) {
let txData = bin.indexOf('0x') === 0 ? bin : ('0x' + bin);
let postdata = {
data: txData,
from: account,
to: null,
gas: 1000000,
randomid: genRandomID(),
blockLimit: blockLimit,
chainId: 1,
groupId: groupId,
extraData: '0x0'
};
return signTransaction(postdata, privateKey, null);
}
module.exports.getSignDeployTx = getSignDeployTx;
module.exports.signTransaction = signTransaction;
module.exports.getSignTx = getSignTx;
module.exports.getTxData = getTxData;
当出现以下问题时,非常抱歉,这是一个愚蠢的bug,指定secp256k1依赖包时版本限制没写对,导致在绑定时时自动安装了4.0版本的secp256k1包,但是最新的4.0的包API全部变了,导致执行出错。
有一个临时的解决方案,进入node_modules/@hyperledger/caliper-fisco-bcos目录,编辑该目录下的package.json文件,在"dependencies"中添加一项"secp256k1": “^3.8.0”,随后在该目录下执行npm i,更新完成后测试程序就能启动了。