在现有Fabric网络上设置和运行Caliper性能测试 官方操作文档中文版

   日期:2020-10-08     浏览:112    评论:0    
核心提示:这里写目录标题1.概述步骤1-创建Caliper工作区步骤2-构建网络配置文件创建模板网络配置文件填充模板文件1.概述本教程将带您完成在预先存在的Fabric网络上使用Caliper对智能合约进行性能测试。要完成本教程,您需要安装NodeJS。为此,我们建议使用nvm。本教程基于官方Hyperledger Fabric文档中提供的资源。假设一个由两个组织和一个单独排序者组成的网络,具有资产转移javascript智能合约,假定已经建立并准备好进行性能测试。下面的命令列表是一个最简单的快速步骤指南,

在现有Fabric网络上设置和运行Caliper性能基准测试

  • 1.概述
  • 2.步骤1-创建Caliper工作区
  • 3.步骤2-构建网络配置文件
    • 创建模板网络配置文件
    • 填充模板文件
    • 完整的网络配置文件
  • 4.步骤3-构建测试工作负载模块
    • 创建模板工作负载模块
    • 填充模板文件
    • 完整工作负载模块
  • 5.步骤4-构建基准配置文件
    • 创建模板基准配置文件
    • 填充模板文件
  • 6.第5步-运行Caliper基准测试

此文档是官方的操作文档, 实战请参考 这篇博客。

1.概述

本教程将带您完成在预先存在的Fabric网络上使用Caliper对智能合约进行性能测试。

要完成本教程,您需要安装NodeJS。为此,我们建议使用nvm。

本教程基于官方Hyperledger Fabric文档中提供的资源。假设一个由两个组织和一个单独排序者组成的网络,具有asset-transfer-basic的javascript智能合约,假定已经建立并准备好进行性能测试。

下面的命令列表是一个最简单的快速步骤指南,用于启动和运行所需的Fabric网络。我们在显式级别使用可用的Hyperledger Fabric资源。要了解并排除创建测试网络期间发生的情况,请参阅上面链接的Fabric文档!

# 克隆Hyperledger Fabric samples repo的固定版本
git clone https://github.com/hyperledger/fabric-samples.git
cd fabric-samples
git checkout 22393b629bcac7f7807cc6998aa44e06ecc77426

# 安装Fabric工具并将其添加到PATH
curl -sSL https://bit.ly/2ysbOFE | bash -s -- 2.2.0 1.4.8 -s
export PATH=$PATH:$(pwd)/bin

# 创建并初始化网络
cd test-network
./network.sh up createChannel
./network.sh deployCC -ccn basic -ccl javascript

2.步骤1-创建Caliper工作区

fabric-samples目录的同一级别创建一个名为caliper-workspace的文件夹,然后在caliper-workspace文件夹中,分别创建三个名为networksbenchmarksworkload的文件夹

Caliper的安装和使用将基于当地的npm安装。在caliper-workspace目录中,使用以下终端命令安装caliper CLI:

npm install --only=prod @hyperledger/caliper-cli@0.4.0

使用以下终端命令绑定SDK:

npx caliper bind --caliper-bind-sut fabric:2.1

有关Caliper安装和捆绑的更多信息,请参见相关文件页。
Caliper需要两个配置文件:

  • 网络配置文件,它描述被测网络并提供要使用的测试标识。
  • 基准文件,它定义了要通过一组有序的测试循环来完成的性能测试,每个测试轮都指定了一个工作负载模块和一系列选项,以在一个时间间隔内驱动工作负载。

现在我们将用Caliper所需的资源填充这些文件夹。

3.步骤2-构建网络配置文件

网络配置文件是Caliper工作人员创建到现有Fabric网络的连接所需的文件,以便他们可以提交交易。它类似于Fabric公共连接配置文件,并添加了其他必需字段。文件可以是YAML或JSON格式,本教程显示JSON格式。

创建模板网络配置文件

在“networks”文件夹下创建一个名为networkConfig.json包括以下内容:

{ 
    "version" : "1.0",
    "name": "Caliper test",
    "caliper" : { 
        "blockchain": "fabric"
    },
    "clients" : { 
    },
    "channels" : { 
    },
    "organizations" : { 
    },
    "peers" : { 
    }
}

版本:正在使用的配置文件的版本。只接受“1.0”。
名称:配置的名称,在本例中为“Caliper测试”。
Caliper:指示对目标的SUT进行Caliper,可能包含本教程中不需要的其他开始/结束命令。在本教程中,我们以Fabric网络为目标。
客户端:列出要在性能测试中使用的标识
通道:描述可用的Hyperledger Fabric通道、它们的状态以及在这些通道上部署的智能合约
组织:要在基准测试中使用的超级账本Fabric组织
节点:要在基准测试中使用的Hyperledger Fabric节点

填充模板文件

在测试网络教程之后,将生成一个公共连接配置文件(CCP);我们将使用此文件帮助填充Caliper网络配置文件的特定于Hyperledger Fabric的元素。模板将为所创建的Hyperledger Fabric网络提供一些唯一的证书,因此您将注意到后续阶段中显示的内容的差异;您必须使用为与您希望目标的测试网络进行交互而创建的证书。

在本例中,我们将使用Org1进行连接。要查找生成的JSON或yamlccp文件,请查看fabric-samples -> test-network -> organizations -> peerOrganizations -> org1.example.com。 我们假设使用json CCP connection-org1.json

组织和同行

使用CCP的内容进行填充:

将CCP的organizationspeers元素复制到Caliper网络配置文件中。

organizations.Org1对象,删除certificateAuthorities列表。这样做有两个原因:

i) 我们不会使用证书颁发机构注册或注册
ii)向CCP提交的变更与配置文件中的验证Caliper不兼容。

Caliper网络配置文件现在应该类似于:

{ 
    "version" : "1.0",
    "name": "Caliper test",
    "caliper" : { 
        "blockchain": "fabric"
    },
    "clients" : { 
    },
    "channels" : { 
    },
    "organizations": { 
        "Org1": { 
            "mspid": "Org1MSP",
            "peers": [
                "peer0.org1.example.com"
            ]
        }
    },
    "peers": { 
        "peer0.org1.example.com": { 
            "url": "grpcs://localhost:7051",
            "tlsCACerts": { 
                "pem": "-----BEGIN CERTIFICATE-----<UNIQUE CONTENT>-----END CERTIFICATE-----\n"
            },
            "grpcOptions": { 
                "ssl-target-name-override": "peer0.org1.example.com",
                "hostnameOverride": "peer0.org1.example.com"
            }
        }
    }
}

客户端

指定执行基准测试时Caliper要使用的标识。身份必须有效,这意味着它们必须为Fabric网络所知,并且具有相应的加密材料以供使用。标识在clients节中列出。这里我们使用单一身份Admin@org1.example.com,我们在其中嵌套来自CCP的client对象,以指示标识所属的组织,并提供基本的连接超时信息。

"clients": { 
    "Admin@org1.example.com": { 
        "client": { 
            "organization": "Org1",
            "connection": { 
                "timeout": { 
                    "peer": { 
                        "endorser": "300"
                    }
                }
            }
        }
    }
}

在client对象下,添加一个名为credentialStore的属性,在该属性下添加一个名为path的属性,该属性具有一个字符串变量,该变量指向工作区中名为/tmp/org1的临时文件。另外,在credentialStore属性下添加一个名为cryptoStore的属性,并在该属性下添加另一个指向上面相同临时文件/tmp/org1的路径属性。

以下是应该添加到client的内容:

"credentialStore": { 
    "path": "/tmp/org1",
    "cryptoStore": { 
        "path": "/tmp/org1"
    }
}

client对象下添加一个名为clientPrivateKey的属性,在此属性下添加一个名为path的属性,该属性具有一个字符串变量,该变量指向标识的私钥。请注意,提供的路径是相对于工作区的。在本例中,私钥位于fabric-samples -> test-network -> organizations -> peerOrganizations -> org1.example.com -> users -> Admin@org1.example.com -> msp -> keystore -> priv_sk

client应添加到的对象是:

"clientPrivateKey": { 
    "path": "../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/priv_sk"
}

另外,在client对象下添加另一个名为clientSignedCert的属性,并在该属性下添加一个名为path的属性,该属性具有一个字符串变量,该变量指向标识的签名证书。再次注意,提供的路径是相对于工作区的。在本例中,它位于fabric-samples -> test-network -> organizations -> peerOrganizations -> org1.example.com -> users -> Admin@org1.example.com -> msp -> signedcerts -> admin@org1.example.com-cert.pem

client应添加到的对象是:

"clientSignedCert": { 
    "path": "../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem"
}

clients对象现在应该如下所示:

	"clients": { 
	    "Admin@org1.example.com": { 
	        "client": { 
	            "organization": "Org1",
	            "connection": { 
	                "timeout": { 
	                    "peer": { 
	                        "endorser": "300"
	                    }
	                }
	            },
	            "credentialStore": { 
	                "path": "tmp/hfc-kvs/org1",
	                "cryptoStore": { 
	                    "path": "tmp/hfc-kvs/org1"
	                }
	            },
	            "clientPrivateKey": { 
	                "path": "../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/priv_sk"
	            },
	            "clientSignedCert": { 
	                "path": "../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem"
	            }
	        }
	    }
	}

通道对象

在创建与Fabric网络的连接时,Caliper的织物连接器需要帮助。必须提供一个channels对象,该对象列出可能与之交互的智能合约。

将由test-network创建的已知通道mychannel添加到Caliper网络配置文件的channels部分,并赋予它一个布尔值true创建的属性。我们必须将此通道上可用的智能合约列为名为contracts的数组中的对象。每个智能合约对象都有两个属性,idversionid指定合同ID;在本例中,它是basic。版本是特定的合同版本;在本例中是1.0.0。将其作为数组中的一个对象添加,以使Caliper网络配置文件中的结果通道对象变为:

    "channels": { 
        "mychannel": { 
            "created" : true,
            "contracts": [
                { 
                    "id":"basic",
                    "version":"1.0.0"
                }
            ]
        }
    }

完整的网络配置文件

Caliper网络配置文件现在应该完全填充。花点时间检查并确保证书和密钥的路径是正确的,这是很有用的。

{ 
    "version" : "1.0",
    "name": "Caliper test",
    "caliper" : { 
        "blockchain": "fabric"
    },
    "clients": { 
        "Admin@org1.example.com": { 
            "client": { 
                "credentialStore": { 
                    "path": "/tmp/org1",
                    "cryptoStore": { 
                        "path": "/tmp/org1"
                    }
                },
                "organization": "Org1",
                "clientPrivateKey": { 
                    "path": "../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/priv_sk"
                },
                "clientSignedCert": { 
                    "path": "../fabric-samples/test-network/organizations/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem"
                },
                "connection": { 
                    "timeout": { 
                        "peer": { 
                            "endorser": "300"
                        }
                    }
                }

            }
        }
    },
    "channels": { 
        "mychannel": { 
            "created" : true,
            "contracts": [
                { 
                    "id":"basic",
                    "version":"1.0.0"
                }
            ]
        }
    },
    "organizations":{ 
        "Org1": { 
            "mspid": "Org1MSP",
            "peers": [
                "peer0.org1.example.com"
            ]
        }
    },
    "peers": { 
        "peer0.org1.example.com": { 
            "url": "grpcs://localhost:7051",
            "tlsCACerts": { 
                "pem": "-----BEGIN CERTIFICATE-----\n<UNIQUE CONTENT>\n-----END CERTIFICATE-----\n"
            },
            "grpcOptions": { 
                "ssl-target-name-override": "peer0.org1.example.com",
                "hostnameOverride": "peer0.org1.example.com"
            }
        }
    }
}

4.步骤3-构建测试工作负载模块

在基准测试期间,工作负载模块与部署的智能合约进行交互。workload模块从Caliper-core扩展了Caliper类WorkloadModuleBase。工作负载模块提供三个覆盖:

  • initializeWorkloadModule-用于初始化基准测试所需的任何项
  • submitTransaction-用于在基准的监视阶段与智能合约方法交互
  • cleanupWorkloadModule—用于在完成基准测试后进行清理

有关更多信息,请参阅左侧菜单上有关工作负载配置的特定文档。

我们将推动的工作负载旨在对世界状态数据库中现有资产的查询进行基准测试。因此,我们将使用workload模块中提供的所有三个阶段:

  • initializeWorkloadModule-创建可在submitTransaction阶段查询的资产
  • submitTransaction-查询在initializeWorkloadModule阶段创建的资产
  • cleanuWorkloadModule-用于移除在initializeWorkloadModule阶段创建的资产,以便可以重复基准测试

创建模板工作负载模块

workload文件夹中,创建一个名为readAsset.js的文件,其中包含以下内容:

'use strict';

const {  WorkloadModuleBase } = require('@hyperledger/caliper-core');

class MyWorkload extends WorkloadModuleBase { 
    constructor() { 
        super();
    }
    
    async initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext) { 
        await super.initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext);
    }
    
    async submitTransaction() { 
        // NOOP
    }
    
    async cleanupWorkloadModule() { 
        // NOOP
    }
}

function createWorkloadModule() { 
    return new MyWorkload();
}

module.exports.createWorkloadModule = createWorkloadModule;

填充模板文件

填充此文件时,我们引用已部署智能合约中的可用方法资产转让可在以下位置找到的文件:fabric-samples -> asset-transfer-basic -> chaincode-javascript -> lib -> assetTransfer.js

填充initializeWorkloadModule

此方法用于在基准完成时准备主submitTransaction方法所需的任何项。

要创建的资产数量将给出如下roundArguments.assets。我们通过填充arguments对象(定义交易体)和使用Caliper API sendRequests(需要了解以下知识)来创建资产:

  • contractId,要使用的智能合约的名称,它存在于Caliper网络配置文件中
  • contractFunction,智能合约中要调用的特定函数
  • contractArguments,传递给智能合约函数的参数
  • invokerIdentity,Caliper网络配置文件中存在的要使用的标识
  • readOnly,如果是否执行查询操作

方法应该如下所示:

    async initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext) { 
        await super.initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext);

        for (let i=0; i<this.roundArguments.assets; i++) { 
            const assetID = `${ this.workerIndex}_${ i}`;
            console.log(`Worker ${ this.workerIndex}: Creating asset ${ assetID}`);
            const request = { 
                contractId: this.roundArguments.contractId,
                contractFunction: 'CreateAsset',
                invokerIdentity: 'Admin@org1.example.com',
                contractArguments: [assetID,'blue','20','penguin','500'],
                readOnly: false
            };

            await this.sutAdapter.sendRequests(request);
        }
    }

在上面的示例中,将创建具有相同参数的不同资产(blue,20,penguin,500)。将上述内容与智能合约方法本身相比较,可以明显看出,契约参数与方法参数之间存在1:1的映射关系。

填充submitTransaction

此方法在基准测试阶段重复运行。我们将通过查询在initializeWorkloadModule方法中创建的资产来评估ReadAsset智能合约方法。

首先,为要查询的资产创建一个字符串标识,该标识由worker索引和一个介于0和已创建资产数量之间的随机整数串联而成。

然后等待对sendRequests的调用,传递一个对象,该对象包含:从round参数传入的contractId集;设置为ReadAssetcontractFunction;设置为invokerIdentityadmin@org1.example.com;和chaincodeArguments设置为一个数组,其中包含要在此运行中查询的资产。

方法应该如下所示:

    async submitTransaction() { 
        const randomId = Math.floor(Math.random()*this.roundArguments.assets);
        const myArgs = { 
            contractId: this.roundArguments.contractId,
            contractFunction: 'ReadAsset',
            invokerIdentity: 'Admin@org1.example.com',
            contractArguments: [`${ this.workerIndex}_${ randomId}`],
            readOnly: true
        };

        await this.sutAdapter.sendRequests(myArgs);
    } 

填充cleanupWorkloadModule

此函数用于在测试后进行清理,因为它通过使用智能合约函数DeleteAsset删除在initializeWorkloadModule函数中创建的资产。该实现类似于initializeWorkloadModule中的实现。注意可以重构initializeWorkloadModulecleanupWorkloadModule,以使用执行创建/删除操作的通用方法,这将留给感兴趣的读者。

   async cleanupWorkloadModule() { 
        for (let i=0; i<this.roundArguments.assets; i++) { 
            const assetID = `${ this.workerIndex}_${ i}`;
            console.log(`Worker ${ this.workerIndex}: Deleting asset ${ assetID}`);
            const request = { 
                contractId: this.roundArguments.contractId,
                contractFunction: 'DeleteAsset',
                invokerIdentity: 'Admin@org1.example.com',
                contractArguments: [assetID],
                readOnly: false
            };

            await this.sutAdapter.sendRequests(request);
        }
    }

完整工作负载模块

现在应该完全填充测试回调文件:

'use strict';

const {  WorkloadModuleBase } = require('@hyperledger/caliper-core');

class MyWorkload extends WorkloadModuleBase { 
    constructor() { 
        super();
    }
    
    async initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext) { 
        await super.initializeWorkloadModule(workerIndex, totalWorkers, roundIndex, roundArguments, sutAdapter, sutContext);

        for (let i=0; i<this.roundArguments.assets; i++) { 
            const assetID = `${ this.workerIndex}_${ i}`;
            console.log(`Worker ${ this.workerIndex}: Creating asset ${ assetID}`);
            const request = { 
                contractId: this.roundArguments.contractId,
                contractFunction: 'CreateAsset',
                invokerIdentity: 'Admin@org1.example.com',
                contractArguments: [assetID,'blue','20','penguin','500'],
                readOnly: false
            };

            await this.sutAdapter.sendRequests(request);
        }
    }
    
    async submitTransaction() { 
        const randomId = Math.floor(Math.random()*this.roundArguments.assets);
        const myArgs = { 
            contractId: this.roundArguments.contractId,
            contractFunction: 'ReadAsset',
            invokerIdentity: 'Admin@org1.example.com',
            contractArguments: [`${ this.workerIndex}_${ randomId}`],
            readOnly: true
        };

        await this.sutAdapter.sendRequests(myArgs);
    }
    
    async cleanupWorkloadModule() { 
        for (let i=0; i<this.roundArguments.assets; i++) { 
            const assetID = `${ this.workerIndex}_${ i}`;
            console.log(`Worker ${ this.workerIndex}: Deleting asset ${ assetID}`);
            const request = { 
                contractId: this.roundArguments.contractId,
                contractFunction: 'DeleteAsset',
                invokerIdentity: 'Admin@org1.example.com',
                contractArguments: [assetID],
                readOnly: false
            };

            await this.sutAdapter.sendRequests(request);
        }
    }
}

function createWorkloadModule() { 
    return new MyWorkload();
}

module.exports.createWorkloadModule = createWorkloadModule;

5.步骤4-构建基准配置文件

基准配置文件定义基准轮次并引用定义的工作负载模块。它将指定在生成负载时要使用的测试工作者的数量、测试轮的数量、每轮的持续时间、在每轮期间应用于交易负载的速率控制以及与监视器相关的选项。本教程不会使用任何可用的监视器或交易观察程序;有关这些详细信息,请参阅文档。

基准配置文件可能以yaml或json格式提供:这里我们将使用yaml格式。请注意,yaml文件区分大小写,所有标签都是小写。

基准配置文件有一个必需的节:

test:

创建模板基准配置文件

benchmarks文件夹下创建一个名为myAssetBenchmark.yaml包括以下内容:

test:
    name: basic-contract-benchmark
    description: A test benchmark
    workers:
    rounds:

test:包含基准测试信息的根级块。
name:测试的名称,在本例中为“基本合同基准”。
description:对基准的描述,在本例中为“测试基准”。
workers:一组键,用于定义后续基准中使用的worker(独立的worker客户端实例)的数量。
rounds:将按顺序进行的不同测试回合的数组。轮次可用于对不同的智能合约方法进行基准测试,或以不同的方式对同一方法进行基准测试。

填充模板文件

现在,我们将填充模板文件以指定工作人员的数量以及使用我们创建的工作负载模块的测试回合。

填充workers

我们将使用两个单独的工人,这是通过工人规范完成的:

  type: local
  number: 2

填充rounds

每个round块包含以下内容:

  • label - 用于回合的唯一标题标签。
  • description - 正在运行的回合的描述。
  • txDuration - 测试持续时间的规范,以秒为单位
  • rateControl - 一种速率控制类型,带有选项。
  • workload - 要使用的工作负载模块,带有要传递给模块的参数。传递的所有参数都可以作为roundArguments在workload模块中使用。

我们将指定一个标记为readAsset的基准轮,并使用description Query asset benchmark运行30秒,使用一个fixed-load速率控制器来保持2的恒定交易压力。此外,我们将提供一个工作负载通过规范我们的readAsset.js工作负载文件,我们将传递参数{assets:10,compactId:asset-transfer-basic}

以上是通过round规范来实现的:

    - label: readAsset
      description: Read asset benchmark
      txDuration: 30
      rateControl: 
        type: fixed-load
        opts:
          transactionLoad: 2
      workload:
        module: workload/readAsset.js
        arguments:
          assets: 10
          contractId: basic

完整的基准配置文件

现在应该完全填充基准配置文件:

test:
    name: basic-contract-benchmark
    description: test benchmark
    workers:
      type: local
      number: 2
    rounds:
      - label: readAsset
        description: Read asset benchmark
        txDuration: 30
        rateControl: 
          type: fixed-load
          opts:
            transactionLoad: 2
        workload:
          module: workload/readAsset.js
          arguments:
            assets: 10
            contractId: basic

6.第5步-运行Caliper基准测试

我们现在可以使用上面的配置文件和测试模块来运行性能基准测试了。性能基准测试将使用Caliper CLI运行,需要提供一个指向工作区的路径,以及指向网络配置文件和基准配置文件的工作区相对路径。这些信息分别与标记--caliper workspace--caliper-networkconfig--caliper benchconfig一起提供。

由于智能合约已经安装和实例化,Caliper只需要执行测试阶段。这是通过使用--caliper-flow-only-test标志来指定的。

由于目标网络已启用发现功能,我们可以通过使用标志--capiler Fabric gateway enabled--capiler Fabric gateway discovery来使用Hyperledger Fabric网关。

运行命令

确保您位于Caliper工作区目录中。

在终端中,运行以下Caliper CLI命令:

npx caliper launch manager --caliper-workspace ./ --caliper-networkconfig networks/networkConfig.json --caliper-benchconfig benchmarks/myAssetBenchmark.yaml --caliper-flow-only-test --caliper-fabric-gateway-enabled --caliper-fabric-gateway-discovery

基准测试结果

结果报告将详细说明每轮基准测试的以下项目:

  • Name—基准配置文件中的圆形名称
  • Succ/Fail-成功/失败的交易数
  • Send Rate-Caliper发出交易的速率
  • Latency(max/min/avg)-与发出交易和接收响应之间的时间(以秒为单位)有关的统计信息
  • Throughput—每秒处理的平均交易数

您已经成功地为智能合约运行基准性能测试。您可以通过改变基准测试参数来重复测试,以及添加资源监视器。有关全套选项,请参阅Caliper文档。

参考自官方文档
如有侵权,请联系作者删除,谢谢!
If there is infringement, please contact the author to delete, thank you!

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服