1. 从“一条命令部署”到“一个集群规划”:为什么非要把Fabric搬上K8s
先说点实在话。我写这个系列以来,前几篇一直在聊docker-compose单机部署Fabric,通常一条docker-compose up -d就能把orderer、peer、ca、couchdb全拉起来,本地跑个Demo,写个链码测试,特别顺手。但后来遇到一个现实需求:要把这套网络放到三台物理机上,组织之间彼此独立,还要求某一个节点挂了以后能自动重启、自动迁移。当时我就知道,compose这条路顶不住。单机部署时,所有容器共享同一个Docker daemon,一旦宿主机内存告急或者磁盘满了,整个网络一起遭殃,更别提动态扩容peer节点这种操作。所以我把目光放到了Kubernetes。
K8s对我来说不是新东西,之前部署过一些无状态Web服务,但把Fabric这种带账本、带证书、带状态数据库的传统中间件搬上去,完全不是一个量级。你需要考虑的不只是镜像怎么跑,还要考虑持久化存储、服务发现、配置管理、安全敏感文件挂载、Pod调度策略。这篇文章就把我完整的思路、关键的YAML设计、以及我一星期里踩过的坑都梳理出来。适合谁看?已经用docker-compose跑通Fabric、想往生产环境或大规模测试环境迁移的开发者,或者对K8s有点基础但没碰过有状态应用的运维同学。我会尽量把每个决策背后的理由讲清楚,这样你抄的时候不是盲抄,而是真的知道自己在干什么。
先给出一个核心结论:Fabric部署在K8s上的本质,是把原来“一组容器”的编排思维,转换成“一组对象”的声明式管理。你不再关心容器具体落在哪台机器上,而是告诉K8s“我要什么”,让它去调度、重启、扩容。听起来很美好,但Fabric给了一个大麻烦:它是重度依赖身份和账本的状态型应用。所以你需要特别谨慎地处理Volume和证书。
2. 组件拆解与K8s资源映射:先想清楚要部署什么
2.1 Fabric网络里到底有哪些进程
Fabric网络不是单个程序,而是一组服务通过gRPC和 gossip 协议互相通信。最小可用的网络至少包含:
- CA节点:给组织和用户签发证书,如果是测试环境,可以用 cryptogen 提前生成证书,甚至不部署CA。
- Orderer节点:对交易排序,出区块,是整个网络的“共识中心”。
- Peer节点:维护账本,执行链码,Peer里还带一个状态数据库,常用的是CouchDB或LevelDB。
- CouchDB:如果要支持富查询,CouchDB作为Peer的附属状态库,需要单独部署。
然后是CLI工具,比如 fabric-tools 镜像里的peer命令、osnadmin命令,这些不是常驻进程,而是我们用来执行创建通道、安装链码等操作的一次性任务。在K8s里,这类工具可以用Job或者临时Pod来跑。
我在部署时,选择把CA也放进K8s。这样证书轮换和重新注册身份会方便很多。但如果你的场景只是为了做功能验证,直接用cryptogen生成静态证书,完全没必要再维护一个CA。这点后面会展开。
2.2 K8s对象与Fabric组件的对应关系
从Docker Compose转到Kubernetes,第一步就是把原来docker-compose.yml里每个服务拆成对应的K8s工作负载资源。我的做法是:
| Fabric组件 | K8s资源 | 说明 |
|---|---|---|
| orderer | StatefulSet + Headless Service | 有序、稳定网络标识,需要持久化账本 |
| peer | StatefulSet + Headless Service | 状态数据库依赖PVC,peer身份需要稳定标识 |
| CouchDB | StatefulSet + Service | 需要持久化数据文件,同时为Peer提供内部访问 |
| CA | Deployment + Service | CA其实可以无状态,证书数据用PVC即可,Deployment够用 |
| CLI | Job | 创建通道、安装链码等操作完成后退出 |
| 创世区块/通道配置 | ConfigMap + Secret | 通过volume挂载到Pod的指定路径 |
| 组织证书 | Secret | 以subPath方式挂载,避免只读权限问题 |
这里有个关键决策:Orderer和Peer我都用了StatefulSet而不是Deployment。原因是Fabric节点必须有稳定的网络标识。Orderer的TLS证书里面通常包含域名,Pod重启后如果IP变了、域名变了,和现有网络里的其他节点对不上,直接连接失败。StatefulSet会为每个副本创建稳定的DNS名称,比如orderer0-orderer-svc的格式,虽然和Fabric官方配置里那种orderer0.example.com看着不一样,但我们可以通过Fabric的配置项和HostAlias去适配。
Deployment虽然也能挂PVC,但它创建的Pod名称是随机后缀,Service固定却能通过Label选择Pod,不过Pod自身的hostname不稳定。Peer证书里的CN名称通常和org名字相关,不会直接依赖hostname,但要保证gossip发现地址和外部地址配置齐全。为了省心,状态型组件统一上StatefulSet。
2.3 从docker-compose到K8s的三个关键差异
第一个差异是网络标识。compose模式下,容器通过服务名互相访问,比如peer0.org1.example.com这个服务名在集群内部可以解析。到了K8s,你要么用StatefulSet的DNS格式:orderer0-orderer-svc.default.svc.cluster.local,要么给Service指定externalName。Fabric内部配置用的是hostname,需要你把所有hostname和K8s DNS对应起来。我建议直接在configtx.yaml的Profile里,把Orderer的地址写成orderer0-orderer-svc,把Peer的地址写成peer0-peer-svc,这样通过Service代理,不需要改Fabric代码。
第二个差异是持久化。compose模式用命名volume,存在本机;K8s里你要定义PVC和StorageClass。很多初学者会忽略Fabric账本和CouchDB的持久化,结果Pod一重建,数据全没了。我的原则是:所有带账本数据、状态数据、证书CA数据的组件,必须挂独立的PVC。而且不建议多个组件共用一个PVC,因为Fabric的文件权限很敏感,CouchDB的data目录如果被Orderer写入,整个库可能起不来。
第三个差异是配置管理。compose模式可以直接通过环境变量覆盖Fabric配置,写起来很简单。K8s里环境变量虽然也能配置,但涉及证书、MSP路径、bootstrap区块等多文件内容,最稳妥的方式是用ConfigMap和Secret挂载成文件。特别要注意:Fabric容器通常以普通用户运行,Secret挂载的默认权限是0644,有时目录没有执行权限,你需要设置defaultMode为0755,否则容器里读取MSP目录会报permission denied。
3. 材料准备:证书、创世区块、通道配置
3.1 Cryptogen和Configtxgen还是老一套
在写K8s YAML之前,必须先准备好Fabric网络的身份材料和排序配置。这一步和docker-compose模式差别不大,依然用cryptogen生成组织身份,用configtxgen生成创世区块、通道事务和锚点更新事务。我以两组织、一个排序节点的最小拓扑为例:
- 两个组织:org1、org2。
- 每个组织一个peer节点。
- 一个Raft orderer节点。
cryptogen生成后的crypto-config目录结构大概是这样:
crypto-config/ ordererOrganizations/ example.com/ orderers/ users/ msp/ peerOrganizations/ org1.example.com/ peers/peer0.org1.example.com/ users/ msp/ org2.example.com/ ...这些文件之后要全部塞进K8s。我的做法是打包成tar后base64,再生成Secret。如果你用的是私有仓库,也可以打成镜像推上去,但不建议。证书属于敏感信息,放Secret是正路。
configtxgen生成的东西分三类:
- genesis.block:orderer启动时读取的系统链区块。
- channel.tx:创建应用通道时的输入。
- anchor peer更新事务。
这些文件是网络启动的基础。channel.tx创建通道时用一次,之后就不再需要。创世区块会被所有orderer挂载,如果网络里有多个orderer,每个orderer都需要同样的一份genesis.block。在K8s里我把它放到ConfigMap里,注意block文件是二进制,ConfigMap的data字段只能放字符串,所以我是先base64编码存成Secret,或者直接用irl路径挂载ConfigMap时指向二进制文件也可以,但更标准的还是Secret。为了操作方便,我统一用Secret承载所有敏感配置文件,其中genesis.block和channel.tx也走Secret,设置defaultMode: 0755。
3.2 把敏感文件变成K8s的Secret和ConfigMap
K8s官方推荐用Secret管理敏感数据,ConfigMap管理非敏感配置。但在实操中,整份crypto-config目录里的MSP文件都是敏感内容,所以我把整个crypto-config目录做成一个Secret,需要挂载的配置文件也单独做成Secret。
你可以在本地先执行:
kubectl create secret generic fabric-crypto --from-file=crypto-config kubectl create secret generic fabric-genesis --from-file=genesis.block --from-file=channel.tx这个命令会把整个crypto-config目录序列化到Secret。然后,在StatefulSet的volume定义里挂载:
volumes: - name: crypto-volume secret: secretName: fabric-crypto defaultMode: 0755容器内路径直接指向Fabric期望的路径。例如Orderer需要看/etc/hyperledger/fabric/msp和/etc/hyperledger/fabric/tls,Peer需要看/etc/hyperledger/fabric/msp、/etc/hyperledger/fabric/tls、/etc/hyperledger/fabric/users等。Secret挂载是只读的,这对Fabric是好事,因为身份文件本来就不该被篡改。
这里有一个经常被忽略的点:Secret挂载后,如果你更新了Secret(比如证书轮换),K8s会自动把所有使用该Secret的Pod里的文件更新,但不会重启Pod。Fabric进程可不会感知文件更新,必须手动滚动重启关联的StatefulSet。我在证书轮换实验里踩过坑,改了证书后Orderer继续使用旧的TLS会话,连接一直报错,最后重建Pod才解决。所以记住:K8s的配置热更新对Fabric并不友好,Fabric的很多配置是在进程启动时一次性加载的。
3.3 目录规划与命名空间
我建议为整个区块链网络建一个独立命名空间,比如fabric,这样网络内部的服务可以通过orderer0-orderer-svc.fabric.svc.cluster.local互相访问,逻辑也清晰。
我把K8s上的部署清单放在一个k8s目录下,按组件分文件:
k8s/ namespace.yaml pvc-orderer0.yaml pvc-peer0-org1.yaml pvc-peer0-org2.yaml orderer.yaml peer0-org1.yaml peer0-org2.yaml couchdb.yaml cli.yaml每个组件文件里包含工作负载、Service、PVC等配套资源。这样后期扩容时,只需要新增peer1-org1.yaml,复制一份改几个参数即可。
4. 一步步落地:核心工作负载YAML解析
4.1 Orderer部署
Orderer是整个网络最先启动的组件,因为它要提供排序服务。我的Orderer部署清单包含一个StatefulSet、一个Headless Service、一个PVC。
先看StatefulSet片段:
apiVersion: apps/v1 kind: StatefulSet metadata: name: orderer0 namespace: fabric spec: serviceName: orderer-svc replicas: 1 selector: matchLabels: app: orderer template: metadata: labels: app: orderer component: orderer0 spec: containers: - name: orderer image: hyperledger/fabric-orderer:2.4 command: ["orderer"] env: - name: FABRIC_CFG_PATH value: /etc/hyperledger/fabric - name: ORDERER_GENERAL_LISTENADDRESS value: 0.0.0.0 - name: ORDERER_GENERAL_LISTENPORT value: "7050" - name: ORDERER_GENERAL_LOCALMSPID value: OrdererMSP - name: ORDERER_GENERAL_TLS_ENABLED value: "true" - name: ORDERER_GENERAL_TLS_PRIVATEKEY value: /etc/hyperledger/fabric/tls/server.key - name: ORDERER_GENERAL_TLS_CERTIFICATE value: /etc/hyperledger/fabric/tls/server.crt - name: ORDERER_GENERAL_TLS_ROOTCAS value: /etc/hyperledger/fabric/tls/ca.crt - name: ORDERER_GENERAL_BOOTSTRAPMETHOD value: file - name: ORDERER_GENERAL_BOOTSTRAPFILE value: /etc/hyperledger/fabric/genesis.block volumeMounts: - name: data mountPath: /var/hyperledger/production/orderer - name: crypto mountPath: /etc/hyperledger/fabric/msp - name: genesis mountPath: /etc/hyperledger/fabric/genesis.block subPath: genesis.block volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 10Gi这里有个细节:bootstrap虽然写的是文件,但是Fabric还支持通过ORDERER_GENERAL_BOOTSTRAPMETHOD=none然后从系统通道加入。测试环境用file最简单。
PersistentVolume我用的动态provisioning,也就是集群有默认StorageClass,这里的volumeClaimTemplates会自动创建PVC。如果你的集群没有动态存储,就需要提前创建PV,并且volumeClaimTemplates里指定storageClassName为已有类名。
需要注意,Orderer启动后会修改data目录里的文件,这个PVC必须ReadWriteOnce(RWO)就行,因为只有一个副本。但如果你部署多副本Orderer,每个副本要有独立PVC,不能共享。
Headless Service 配置很简单:
apiVersion: v1 kind: Service metadata: name: orderer-svc namespace: fabric spec: clusterIP: None selector: app: orderer ports: - port: 7050 targetPort: 7050为什么用Headless Service?因为StatefulSet的Pod需要通过稳定的DNS名称来解析和互相访问。Headless Service 不给Pod做负载均衡,而是直接暴露每个Pod的DNS,格式为podname.servicename.namespace.svc.cluster.local。在这个例子里就是orderer0.orderer-svc.fabric.svc.cluster.local。这个地址可以被其他组织引用。
4.2 Peer和CouchDB部署
Peer的部署和Orderer类似,但多了一个核心的附带数据库。如果你用CouchDB做状态库,必须单独部署CouchDB的StatefulSet,然后在Peer的环境变量里指定CORE_LEDGER_STATE_STATEDATABASE=CouchDB,并配置CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb-peer0-org1-svc:5984。
每个Peer可以带一个独立的CouchDB实例,也可以多个Peer共享一个CouchDB。生产环境建议一个Peer对应一个CouchDB,避免I/O争抢。我这里为每个Peer单独部署了一个CouchDB,CouchDB的PVC用ReadWriteOnce,Pod分配独立存储。
CouchDB部署片段:
apiVersion: apps/v1 kind: StatefulSet metadata: name: couchdb-peer0-org1 namespace: fabric spec: serviceName: couchdb-peer0-org1-svc replicas: 1 selector: matchLabels: app: couchdb peer: peer0-org1 template: metadata: labels: app: couchdb peer: peer0-org1 spec: containers: - name: couchdb image: hyperledger/fabric-couchdb:0.4.22 ports: - containerPort: 5984 env: - name: COUCHDB_USER value: admin - name: COUCHDB_PASSWORD value: adminpw volumeMounts: - name: data mountPath: /opt/couchdb/data volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 5Gi注意,这里的用户名密码不是随便写的,Peer连接CouchDB时也必须在环境变量里填相同的值。我会把用户名密码通过Secret注入到两个组件里。测试环境图省事直接明文了,生产环境不要学我。
Peer本身的环境变量更复杂一些,你需要设置:
- name: CORE_PEER_ID value: peer0.org1.example.com - name: CORE_PEER_ADDRESS value: peer0-org1-svc:7051 - name: CORE_PEER_GOSSIP_BOOTSTRAP value: peer0-org2-svc:7051 - name: CORE_PEER_GOSSIP_EXTERNALENDPOINT value: peer0-org1-svc:7051 - name: CORE_PEER_LOCALMSPID value: Org1MSP - name: CORE_PEER_TLS_ENABLED value: "true" - name: CORE_PEER_TLS_ROOTCERT_FILE value: /etc/hyperledger/fabric/tls/ca.crt这里最让人头疼的是gossip。Fabric的gossip协议要求Peer之间能够通过域名解析到彼此,如果你的组织只有1个peer,那bootstrap可以不用填,但如果为了高可用,每个组织至少2个peer,必须配置CORE_PEER_GOSSIP_BOOTSTRAP指向同组织的另一个peer地址。在K8s中,同组织的peer2可以直接用peer1-org1-svc这种Service名。
还有一个常见坑:CORE_PEER_ADDRESS是给外部客户端用的地址,如果你把Peer暴露到集群外,需要设置为外部可路由的地址;如果只限集群内访问,就填Service DNS或Headless DNS。我踩过的坑是,创建通道时CLI容器通过Service访问peer,然后peer内部的gossip地址又指向另一个解析不了的域名,导致锚点更新失败。最好的做法是让所有内部通信通过peer0-org1-svc.fabric.svc.cluster.local,然后给这些Service统一起清晰的名称。
4.3 Service与Headless Service的配合
我在Orderer和Peer上用了Headless Service,目的是稳定DNS。但对于客户端访问,比如从CLI容器执行peer chaincode invoke,或者外部SDK连接,直接用普通ClusterIP Service更合适,因为它会做负载均衡。两种Service可以并存:一个Headless Service给节点间互访,一个普通Service给客户端。
例如Peer的一个普通Service:
apiVersion: v1 kind: Service metadata: name: peer0-org1-client namespace: fabric spec: selector: app: peer peer: peer0-org1 ports: - name: grpc port: 7051 targetPort: 7051 - name: events port: 7053 targetPort: 7053Peer的StatefulSet也提供了稳定的Pod DNS,但普通Service用ClusterIP,客户端访问时走的是虚拟IP。如果SDK需要连接指定Peer的锚点,直接用peer0-org1-svc就够了。两种方式都能通,我建议文档里统一用普通Service名,因为更容易迁移到跨命名空间访问。
4.4 创建通道并安装链码
网络启动后,创建通道这些操作无法靠K8s声明式完成,必须跑一次CLI Job。我写了一个cli.yaml,用Job执行一系列命令:
apiVersion: batch/v1 kind: Job metadata: name: fabric-cli namespace: fabric spec: template: spec: restartPolicy: Never containers: - name: cli image: hyperledger/fabric-tools:2.4 command: ["/bin/bash", "-c"] args: - | sleep 10 peer channel create -o orderer-svc:7050 \ -c mychannel -f /etc/hyperledger/fabric/config/channel.tx \ --tls --cafile /etc/hyperledger/fabric/msp/tls/ca.crt peer channel join -b mychannel.block peer chaincode install -n mycc -v 1.0 -p github.com/chaincode/sample peer chaincode instantiate -o orderer-svc:7050 \ -C mychannel -n mycc -v 1.0 -c '{"Args":["init"]}' \ --tls --cafile /etc/hyperledger/fabric/msp/tls/ca.crt volumeMounts: - name: crypto mountPath: /etc/hyperledger/fabric/crypto - name: config mountPath: /etc/hyperledger/fabric/config volumes: - name: crypto secret: secretName: fabric-crypto - name: config secret: secretName: fabric-genesis这里有个点:CLI容器里执行peer命令时,需要指定环境变量CORE_PEER_MSPCONFIGPATH指向当前组织管理员的MSP目录,比如/etc/hyperledger/fabric/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp,否则会报身份错误。Job执行完成后,可以通过kubectl logs job/fabric-cli查看日志。如果失败了,直接kubectl delete job fabric-cli然后修改参数重新apply。
5. 踩坑实录:七天里我遇到的问题
5.1 Pod反复重启,日志显示文件找不到
这个最常见。特别是Orderer启动时报open /etc/hyperledger/fabric/msp/config.yaml: no such file or directory。原因大多是Secret挂载的路径不对,或者MSP目录结构不对。你需要确认crypto-config里每个组织的msp目录下都有config.yaml文件。cryptogen生成时可能不会默认包含config.yaml,你要手动在对应组织的msp目录下创建:
NodeOUs: Enable: true ClientOUIdentifier: Certificate: cacerts/ca.example.com-cert.pem OrganizationalUnitIdentifier: client PeerOUIdentifier: Certificate: cacerts/ca.example.com-cert.pem OrganizationalUnitIdentifier: peer AdminOUIdentifier: Certificate: cacerts/ca.example.com-cert.pem OrganizationalUnitIdentifier: admin OrdererOUIdentifier: Certificate: cacerts/ca.example.com-cert.pem OrganizationalUnitIdentifier: orderer如果你的Secret是从crypto-config整个目录生成的,并且Pod里只挂载了msp子目录,K8s的Secret挂载实际上是把整个Secret虚拟成目录。比如你挂载mountPath: /etc/hyperledger/fabric/crypto,那Pod里看到的是crypto-config的所有内容,后面找msp路径时要包含crypto-config前缀。我最后为了省事,把每个组件需要的文件分开用subPath挂到准确的路径,绕过了路径混乱问题。
5.2 通道创建时proposal失败
执行peer channel create时经常报Error: failed to deliver orderer response: rpc error: code = Unknown desc = access denied。这个要么是orderer的MSP配置不对,要么是channel.tx里的组织定义和orderer不一致。我在K8s环境遇到一个更隐蔽的问题:orderer通过gRPC返回的信息里携带的地址,和我K8s Service的地址不一致。因为orderer配置里有一个外部访问地址,默认会监听并广播127.0.0.1:7050,这样客户端连接的时候,orderer把自己的地址告诉客户端,客户端再连接就失败。需要设置环境变量:
- name: ORDERER_GENERAL_CLUSTER_CLIENTCERTIFICATE ... - name: ORDERER_GENERAL_CLUSTER_CLIENTPRIVATEKEY ... - name: ORDERER_GENERAL_CLUSTER_ROOTCAS ... - name: ORDERER_GENERAL_CLUSTER_LISTENADDRESS value: 0.0.0.0 - name: ORDERER_GENERAL_CLUSTER_LISTENPORT value: "7050"或者确认ORDERER_GENERAL_LISTENADDRESS设置为0.0.0.0,不要只监听回环地址。如果是多节点Raft集群,还要检查ORDERER_GENERAL_CLUSTER_ROOTCAS,不然节点之间协商失败。
5.3 CouchDB连接闪断
Peer日志里出现CouchDB connection error或者unable to connect to couchdb。这种问题分两种:一种是CouchDB本身启动失败,看日志发现权限错误,因为CouchDB镜像会以couchdb用户运行,挂载PVC目录的属主不对。解决办法是给PVC使用支持动态创建且权限正确的StorageClass,或者在CouchDB容器启动命令里执行chown。
另一种是网络问题。Peer的CouchDB连接串写的是couchdb-peer0-org1-svc:5984,但在K8s里解析这个Service需要Peer和CouchDB在同一个命名空间。如果你把CouchDB放在另一个命名空间,要写全限定域名。我还遇到过CouchDB的Service端口写错,5984被映射成6984,导致一直超时。所以排查时要先kubectl exec进Peer容器里,用curl检查CouchDB地址是否能通。这个在容器网络问题排查中最有效。
5.4 K8s DNS解析名没对上
Fabric的Peer证书里包含了域名,比如peer0.org1.example.com。当Peer启动后,gossip会通过这个域名去连接自己或其他人。如果K8s里没有这个DNS记录,gossip就会失败,锚点更新也失败。解决办法有几种:
- 给Service配置
externalName为证书里的域名,但只能做别名,不能随意映射到pod IP。 - 用Headless Service的StatefulSet Pod DNS,然后把证书里的域名改成与Pod DNS一致。
- 用HostAlias把证书域名映射到Service ClusterIP。
我在两组织多节点环境里,干脆重新生成了证书,把组织域名改成与K8snamespace结合起来,生成如peer0-org1-svc这样的域名,然后在configtx.yaml和所有MSP配置里都统一用这个域名,这样证书、gossip、K8s DNS三者就能完美对齐。这需要一开始规划好,否则后面改证书会让你抓狂。
5.5 状态文件丢失,账本回滚
这个问题发生在用Deployment部署Peer时。我为了贪图方便,一开始用Deployment绑定PVC,结果每次滚动更新后,旧的PVC还被上一个Pod占用,新的Pod一直等待挂载,最终导致账本数据没写入新PVC,Peer启动时发现账本为空。后来我改用volumeClaimTemplates的StatefulSet,每个Pod都有独立PVC,Pod重建后依然绑定同一个PVC,账本数据就不会丢。
还有一点:Fabric的账本目录是/var/hyperledger/production/ledger,如果你只挂载了Peer的整个数据目录,没有分开挂载,那没问题。如果你把CouchDB也放在同一个PVC下,会带来CouchDB文件锁冲突。所以再次强调:所有组件独立PVC,不要图省事。
6. 运维与扩展思考:氨基化的K8s部署并不是终点
把Fabric跑在K8s上,只是第一步。真正投入生产,还有一堆事情要做。
第一是配置管理。我上面的部署用了大量环境变量,K8s里可以用ConfigMap管理,但要注意ConfigMap更新后Pod不会自动重启。我们写了一个简单脚本,在每次更换配置后执行kubectl rollout restart statefulset完成滚动重启。更正式的做法是使用配置热加载工具,但Fabric进程不支持,所以只能重启。
第二是监控。Fabric本身暴露了Prometheus metrics接口,K8s集群里可以配置ServiceMonitor来抓取。Orderer和Peer的端口分别是8443和9443之类,需要在环境变量里开启metrics provider。如果你不想引入Prometheus整套方案,最简单的是给Pod加annotations: prometheus.io/scrape: "true",然后用现成的Prometheus服务发现。
第三是伸缩。Peer组织要增加节点,最简单是复制一份StatefulSet,改名称、PVC、Service,然后在新Peer上执行peer channel join。这需要新Peer拿到创世区块或者使用peer channel fetch从Orderer获取。但前提是名字、证书、gossip配置都要配好。我们为了自动化,还写了一段shell脚本,根据传入的组织名和节点序号自动生成YAML并apply,但这里就不展开了。
第四是备份和恢复。账本和CouchDB的持久化数据都在PVC里,需要定期备份。K8s没有内置的备份机制,我们用的是存储快照功能。如果是本地存储,你得自己写脚本把PVC里的数据拷贝出来。别依赖K8s滚动更新时的自动保护,那只是基础能力。
最后说一点个人经验:从这个项目得到的最大收获,并不是“Fabric能跑在K8s上”这个结论,而是我重新理解了一个道理——K8s编排的是资源生命周期,但像Fabric这样的分布式系统,真正要求的是稳定身份和稳定数据。所有调优手段,其实都在围绕这两点做文章。那些看着花哨的自动扩缩容、优雅停机,在身份和数据面前都得往后排。
如果你现在也正准备做类似迁移,我的建议是先用docker-compose搭一套可用的Fabric网络,搞清楚证书和通道配置怎么流转,再开始写K8s YAML。不要一开始就在K8s里折腾,否则根本分不清是Fabric的问题还是K8s的问题。而且我强烈建议把整套部署文件用Git管理,每次变更都记录清楚,后期排查的时候,你一定会感谢自己当时记下的这些commit记录。
这个方向后续我还在继续探索,比如把CA完全容器化、用Operator管理Fabric生命周期、链码的升级流程如何在不中断网络的情况下平滑完成。等踩出新的坑,我再写一篇。今天这篇的内容足够你搭出一个能跑的K8s版Fabric网络了,动手的时候遇到具体报错,再回来对照着排查。