Chainlink LOOP 插件体系全解析:基于 go-plugin 与 gRPC 的进程外插件运行时
【免费下载链接】chainlinknode of the decentralized oracle network, bridging on and off-chain computation项目地址: https://gitcode.com/GitHub_Trending/ch/chainlink
LOOP(Local-Out-Of-Process)插件是 Chainlink 节点的一种备选运行时形态:将原本内嵌在节点进程中的 relayer、OCR 报告插件等系统拆分为独立进程,通过 HashiCorp go-plugin 协议与 gRPC 与主节点通信。本文以仓库 plugins/README.md 为主线,结合plugins/目录的源码、plugins/chainlink.Dockerfile 与 core/web/loop_registry.go 等实现,系统讲解 LOOP 插件的原理、启用方式、运行时环境变量、超时与 Prometheus 监控两大前置条件,以及基于 loopinstall 的构建安装流程,帮助读者从"配置能跑"深入到"源码可读"。
LOOP 插件是什么:从单进程到多进程的运行时演进
LOOP 是Local-Out-Of-Process的缩写,指一种备选的节点运行时:部分系统不再作为 goroutine 运行在 Chainlink 节点主进程内,而是运行在独立进程中,通过 github.com/hashicorp/go-plugin 协议插件化接入,并使用gRPC进行通信。
当前仓库中主要有两类 LOOP 插件:
- Relayer 插件:负责与具体区块链网络对接的中继层组件;
- Median 产品插件:实现 OCR2 Median 报告逻辑的产品级插件。
plugins/目录下保留了package main形式的可执行入口,见 plugins/cmd,可通过make install-<plugin>构建安装。README 同时明确指出:Solana 与 Starknet 插件已迁移至各自独立仓库,本模块内所有插件最终都会迁出,因此plugins/是一个处于过渡期的模块。
从源码看,本地插件入口 plugins/cmd/chainlink-medianpoc/main.go 清晰展示了 go-plugin 与 gRPC 的接入方式:
func main() { s := loop.MustNewStartedServer(loggerName) defer s.Stop() p := medianpoc.NewPlugin(s.Logger) defer s.Logger.ErrorIfFn(p.Close, "Failed to close") s.MustRegister(p) stop := make(chan struct{}) defer close(stop) plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: reportingplugins.ReportingPluginHandshakeConfig(), Plugins: map[string]plugin.Plugin{ reportingplugins.PluginServiceName: &reportingplugins.GRPCService[types.MedianProvider]{ PluginServer: p, BrokerConfig: loop.BrokerConfig{ Logger: s.Logger, StopCh: stop, GRPCOpts: s.GRPCOpts, }, }, }, GRPCServer: s.GRPCOpts.NewServer, }) }其业务实现位于 plugins/medianpoc/plugin.go,medianpoc.Plugin内嵌了loop.Plugin与reportingplugins.MedianProviderServer,并实现了NewValidationService等接口。这套代码是理解"进程内插件如何变成进程外服务"的最小完整示例。
节点如何拉起插件:CmdConfig 与 LoopRegistry 的协作
插件能否被节点"拉起",取决于节点侧如何构造子进程命令。三个关键源码文件构成这条链路:
1.plugins/cmd.go—— 定义待执行的命令
type CmdConfig struct { ID string // unique string used by the node to track the LOOP. typically supplied by the loop logger name Cmd string // string value of executable to exec Env []string // environment variables as described in [exec.Cmd.Env] }NewCmdFactory的作用是"保证 loop registry 与待执行命令之间的同步":先调用注册函数拿到*RegisteredLoop,再返回一个闭包,闭包每次执行时都会用exec.Command构造子进程,并把RegisteredLoop.EnvCfg.AsCmdEnv()生成的运行环境变量注入其中。
2.plugins/registrar.go—— 统一的注册入口
RegistrarConfig接口定义了RegisterLOOP(config CmdConfig) (func() *exec.Cmd, loop.GRPCOpts, error)与UnregisterLOOP(ID string),NewRegistrarConfig接收 gRPC 选项、注册函数与注销函数,把"注册"与"生成 exec.Cmd"两个动作绑定在一起。
3.plugins/loop_registry.go—— 插件注册表与端口分配
LoopRegistry是插件的全局登记处,职责包括:
- 分配 Prometheus 端口:
Register(id string)通过freeport.Take(1)为每个插件申请一个空闲端口,作为该插件 HTTP 指标服务的端口;重复注册同一 ID 会返回ErrExists;Unregister时通过freeport.Return归还端口; - 向插件注入节点配置:在
Register内组装loop.EnvConfig,把节点侧配置通过环境变量通道传给插件,包括数据库连接(URL、超时、连接池上限)、Mercury 传输器参数、Pyroscope 性能剖析、Tracing 链路追踪、Telemetry 遥测以及 Metering 计量等配置; - 提供并发安全的查询接口:
List()按插件名排序返回全部注册项,Get(id)按 ID 查找,供后续 HTTP 路由使用。
plugins/env.go中的ParseEnvFile则负责把环境变量文件解析为key=value切片,供命令工厂注入。
如何启用 LOOP 插件:Dockerfile 与运行环境变量
plugins/chainlink.Dockerfile 在常规的 core/chainlink.Dockerfile 基础之上扩展,把插件二进制一并打入镜像,并通过设置如下环境变量开启 LOOP 支持:
| 环境变量 | 对应二进制 | 说明 |
|---|---|---|
CL_SOLANA_CMD | chainlink-solana | Solana relayer 插件(二进制由外部仓库构建) |
CL_STARKNET_CMD | chainlink-starknet | Starknet relayer 插件(README 文档提及,对应仓库已独立) |
CL_MEDIAN_CMD | chainlink-feeds | Median 产品插件 |
CL_EVM_CMD | chainlink-evm | EVM 插件(Dockerfile 标注为实验性) |
CL_MERCURY_CMD | chainlink-mercury | Mercury/data-streams 插件(实验性) |
当前版本的 Dockerfile 中实际设置了ENV CL_MEDIAN_CMD=chainlink-feeds、ENV CL_SOLANA_CMD=${CL_SOLANA_CMD}(默认为chainlink-solana),以及实验性的CL_EVM_CMD、CL_MERCURY_CMD。
关闭插件的方式:取消(unset)对应的环境变量即可,节点会回退到原始的进程内(in-process)运行时。也就是说,LOOP 是"可切换"的运行时选项,而非强制改造。
镜像构建采用多阶段流水线(该 Dockerfile 内有详细注释):
deps-base:仅下载 Go 依赖,避免源码变更导致缓存失效;deps:拷贝完整源码树;build-delve:安装 Delve 调试器(供 debug 阶段使用);build-remote-plugins:只基于plugins.public.yaml/plugins.private.yaml/plugins.testing.yaml清单,通过go tool loopinstall编译远程插件(典型源码改动可跳过约 160 秒的远程插件构建);build-local-plugins:基于./plugins/cmd/...编译本地插件(当前即 chainlink-medianpoc);build-chainlink:编译 Chainlink 主节点二进制(CL_IS_PROD_BUILD=false时走install-chainlink-dev);final:Ubuntu 24.04 运行镜像,暴露 6688 端口,入口为chainlink local node,并带/health健康检查;debug:叠加 Delve 的调试镜像。
此外构建时支持两个开关:CL_INSTALL_PRIVATE_PLUGINS与CL_INSTALL_TESTING_PLUGINS,置为true时才会构建私有/测试插件,且需要提供GITHUB_TOKEN(见 GNUmakefile 中docker目标的校验逻辑)。
前置条件一:gRPC 超时必须真实,不能为 0
LOOP 插件间通信基于 gRPC,而 gRPC 调用始终携带context.Context,要求配置现实的超时值。README 明确警告:占位/伪值(例如MaxDurationQuery = 0)无法正常工作,必须更新为真实值。
针对 Solana 等已部署合约不便重新配置的场景,可通过环境变量CL_MIN_OCR2_MAX_DURATION_QUERY为 libocr 的LocalConfig.MinOCR2MaxDurationQuery设置新的下限;若未设置,默认值为100ms。
仓库源码印证了这一行为,见 core/services/ocr2/validate/config.go:
const defaultMinOCR2MaxDurationQuery = 100 * time.Millisecond var getMinOCR2MaxDurationQuery = sync.OnceValues(func() (time.Duration, error) { str := env.MinOCR2MaxDurationQuery.Get() if ... { return defaultMinOCR2MaxDurationQuery, nil } ... })环境变量定义位于 core/config/env/env.go 的MinOCR2MaxDurationQuery = Var("CL_MIN_OCR2_MAX_DURATION_QUERY")。作为参照,CCIP 的 OCR 插件在 core/capabilities/ccip/oraclecreator/plugin.go 中将MinOCR2MaxDurationQuery设置为1 * time.Second,并在 plugin_test.go 中予以断言——这从侧面说明 100ms 只是下限,实际取值应结合业务延迟要求。
前置条件二:动态插件的 Prometheus 监控
LOOP 插件是动态的(运行时才注册、端口由freeport动态分配),因此必须动态监控。节点的做法是:使用插件发现机制(Plugin discovery)动态决定监控哪些插件,并把外部 Prometheus 的抓取请求路由到各插件,而不直接暴露插件端口。
两个核心端点
/discovery:HTTP Service Discovery 端点。Prometheus 服务器轮询该 URL,节点根据"当前正在运行的插件"动态返回目标列表。响应采用 Prometheus 的targetgroup.Group结构,节点自身指标也会一并加入(见下文源码)。/plugins/<name>/metrics:节点充当极薄的中间层(thin middleware),把 Prometheus 的抓取请求转发到对应插件的/metrics端点。插件通过上述 discovery 机制被发现后,Prometheus 会在抓取间隔内持续调用该目标。
最小 Prometheus 配置改动
README 给出的最小改动是在 scrape 配置中加入一个 HTTP 服务发现:
- job_name: 'chainlink_node' ... + http_sd_configs: + - url: "http://127.0.0.1:6688/discovery" + refresh_interval: 30s即让 Prometheus 每 30 秒刷新一次节点上的服务发现列表(详见 Prometheus 官方http_sd_config文档)。
源码级实现:core/web/loop_registry.go
监控端点的实现位于 core/web/loop_registry.go:
discoveryHandler先加入节点自身的/metrics目标,再遍历registry.List(),为每个注册插件生成目标组,并打上__meta_plugin_name标签(常量LabelMetaPluginName),最终以 JSON 返回;pluginMetricHandler根据路径参数name查registry.Get(pluginName),找到后把请求转发到http://<loopHostName>:<PrometheusPort>/metrics,其中PrometheusPort正是LoopRegistry.Register时分配的端口;- 除指标外还提供了 pprof 转发端点:
pluginPPROFHandler会把/plugins/<name>/debug/pprof/*转发到插件的debug/pprof端口(profile、gc、seconds 等查询参数均透传); initHostNames说明两个 hostname 的来源:CL_PROMETHEUS_DISCOVERY_HOSTNAME(外部 Prometheus 可访问的地址,未设置时回退到os.Hostname())与CL_LOOPP_HOSTNAME(节点与插件间内部通信地址,默认localhost)。
路由注册见 core/web/router.go:
r.GET("/discovery", ginHandlerFromHTTP(loopRegistry.discoveryHandler)) r.GET("/plugins/:name/metrics", loopRegistry.pluginMetricHandler) r.GET("/plugins/:name/debug/pprof/*profile", loopRegistry.pluginPPROFHandler) r.POST("/plugins/:name/debug/pprof/symbol", loopRegistry.pluginPPROFPOSTSymbolHandler)这种"服务发现 + 反向代理转发"的设计既满足了插件动态增减的监控需求,又避免把插件内部端口暴露到外部网络。
构建与安装插件:Makefile 与 loopinstall 清单
仓库通过 GNUmakefile 提供一组插件安装目标:
| 目标 | 用途 |
|---|---|
make install-loopinstall | 安装loopinstall工具(go install github.com/smartcontractkit/chainlink-common/pkg/loop/cmd/loopinstall) |
make install-plugins-local | 编译本地插件,即./plugins/cmd/chainlink-medianpoc(带-ldflags="-s"去除调试信息) |
make install-plugins-public | 依据plugins/plugins.public.yaml通过go tool loopinstall --concurrency 5安装公共插件 |
make install-plugins-private | 依据plugins/plugins.private.yaml安装私有插件(需要GOPRIVATE与 git 认证) |
make install-plugins-testing | 依据plugins/plugins.testing.yaml安装仅测试用插件 |
make install-plugins | 等价于install-plugins-local install-plugins-public |
当设置了CL_LOOPINSTALL_OUTPUT_DIR时,loopinstall 还会产出public.json/private.json/testing.json安装工件清单,供 Dockerfile 的copy_loopinstall_libs.sh收集动态链接库(如 Cosmos 的libwasmvm.so)。
公共插件清单 plugins/plugins.public.yaml 展示了 loopinstall 的清单格式:defaults.goflags统一附加-ldflags=-s以减小二进制体积,plugins下每个条目声明moduleURI(外部模块地址)、gitRef(精确版本)、installPath(相对入口)以及可选的libs(需要拷入容器/usr/lib的动态库)。清单覆盖:
- 链相关 relayer:
aptos、sui、cosmos、solana、starknet、stellar、ton、tron、evm; - 产品插件:
feeds(Median,对应CL_MEDIAN_CMD)、streams(Mercury); - Capability 类:
capability-cron、capability-consensus、capability-workflowevent(默认enabled: false)、capability-httpaction、capability-httptrigger、capability-evm/solana/aptos/stellar; - 机密计算:
confidential-http、confidential-workflows。
这也解释了 README 中"Solana & Starknet 已迁移到各自仓库"的含义:它们的代码已迁出本模块,但通过moduleURI+gitRef的方式仍以远程插件形态参与构建。
小结
LOOP 插件把 Chainlink 节点的运行时从"单进程内嵌"演进为"多进程、可插拔、gRPC 通信"的架构。启用时只需通过 plugins/chainlink.Dockerfile 构建镜像并设置CL_*_CMD系列环境变量(取消即回退进程内运行时);落地前必须满足两个前置条件:为 gRPC 配置真实超时(可通过CL_MIN_OCR2_MAX_DURATION_QUERY调整下限,默认 100ms),以及通过/discovery+/plugins/<name>/metrics完成动态监控。进一步理解底层机制时,可依次阅读 plugins/loop_registry.go(端口分配与配置注入)、plugins/cmd.go(子进程构造)与 core/web/loop_registry.go(发现与转发),即可从"会配置"进阶到"懂原理"。
【免费下载链接】chainlinknode of the decentralized oracle network, bridging on and off-chain computation项目地址: https://gitcode.com/GitHub_Trending/ch/chainlink
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考