- 后端
- 低代码
- AI 应用
- 人工智能
- AI Agent
- 认证鉴权
- RAG
【免费下载链接】erupt
🚀 通用数据管理框架,VORM 对象视图模型,注解驱动低代码开发
erupt-cloud-server 是 Erupt 分布式部署架构中的控制平面(Control Plane):它统一注册管理一组erupt-cloud-node工作节点、向节点下发配置、并把 API 与 UI 请求代理到正确的节点,同时提供一个用于节点健康监控与管理的管理界面。读完本文,你将掌握该模块的完整配置项(erupt.cloud-server.*)、节点注册/心跳/摘除的生命周期、基于 Redis 的节点状态存储、请求代理与故障转移的实现细节,以及如何与节点端erupt.cloud-node配置配对完成一次真实的多节点部署。
模块定位:erupt-cloud 三件套
Erupt 的分布式能力位于 erupt-cloud 模块下,由三个子模块构成。erupt-cloud-server 的定位可以概括为:
Control plane for Erupt distributed deployments. Manages a fleet of
erupt-cloud-nodeworkers: registers nodes, distributes configuration, and proxies API and UI requests to the appropriate node. Provides an admin UI for node health monitoring and management. Built on top oferupt-adminwith Ant Design UI.
结合 pom.xml 的依赖声明可以印证这句话:模块依赖 erupt-cloud-common(server 与 node 共用的 DTO、常量与工具类),以provided作用域引用erupt-upms(用户与权限)和erupt-security(API 防护)——这正是 erupt-admin 打包的两大能力;同时依赖erupt-tpl与erupt-tpl-ui.ant-design,说明管理界面基于模板引擎 + Ant Design 组件渲染,并引入freemarker用于节点信息/节点日志等行内模板页。
三个子模块的分工:
| 子模块 | 角色 | 说明 |
|---|---|---|
| erupt-cloud-common | 共享库 | 通用 DTO(NodeInfo)、REST 路径常量(CloudRestApiConst)、请求头常量(CloudCommonConst),不直接用于业务工程 |
| erupt-cloud-node | 工作节点 | 启动后向 server 注册,接收被代理的请求并在本地执行;使用 Spring AOP 透明拦截请求 |
| erupt-cloud-server(本文) | 控制平面 | 节点注册、配置下发、API/UI 代理、节点健康监控管理界面 |
server 端的自动装配入口是 EruptCloudServerAutoConfiguration:它通过@ComponentScan+@EruptScan+@EntityScan扫描自身包下的组件与实体,并以EruptModule身份向 Erupt 注册模块信息(erupt-cloud-server/ "Erupt Cloud Microservice control center"),同时声明了节点管理的整套菜单(见后文"管理界面"一节)。
服务端配置项:erupt.cloud-server.*
所有服务端可调参数集中在 EruptCloudServerProp,通过@ConfigurationProperties("erupt.cloud-server")绑定。完整参数与默认值如下:
| 配置项 | 类型 | 默认值 | 含义 |
|---|---|---|---|
erupt.cloud-server.nodeExpireTime | Integer | 600000(10 分钟,毫秒) | 节点在 Redis 中的缓存存活时长,即注册信息 TTL |
erupt.cloud-server.nodeSurviveCheckTime | Integer | 120000(120 秒,毫秒) | 节点存活巡检任务的执行周期 |
erupt.cloud-server.cloudNameSpace | String | erupt-cloud: | 节点注册信息在 Redis 中的 Key 命名空间 |
erupt.cloud-server.validateAccessToken | boolean | true | 节点注册/配置拉取时是否校验 Access Token |
erupt.cloud-server.nodeRequestTimeout | Integer | 30000(30 秒,毫秒) | server 向节点转发请求的读超时,作用于共享的RestClient |
配置示例(放入 server 端工程的application.yml):
erupt: cloud-server: nodeExpireTime: 600000 # Redis 中节点注册信息的 TTL(毫秒) nodeSurviveCheckTime: 120000 # 存活巡检周期(毫秒) cloudNameSpace: "erupt-cloud:" validateAccessToken: true # 是否校验节点的 Access Token nodeRequestTimeout: 30000 # 转发到节点的读超时(毫秒)各参数的底层作用位置:
nodeExpireTime在 NodeManager.putNode 中作为RedisTemplate.opsForValue().set(...)的 TTL 写入;nodeSurviveCheckTime是 NodeWorker 定时任务的调度间隔;nodeRequestTimeout在 EruptCloudServerAutoConfiguration#nodeRestClient 中构建 server → node 的共享RestClient时作为读超时生效;cloudNameSpace与固定前缀node:拼接出每个节点的缓存 Key(见下节);validateAccessToken在 EruptNodeMicroservice.findNodeByAppName 中决定是否比对 Token,注册与配置拉取路径都会走到这里。
节点注册与心跳:一个实例如何进入集群
节点状态的核心数据结构是 MetaNode,它继承自 common 模块的 NodeInfo,在共享字段(instanceId唯一实例 ID、version版本、nodeName节点名、accessToken访问令牌、nodeAddress上报地址数组、eruptModules本节点承载的模块列表、erupts本节点承载的 erupt 列表、resources可选模块贡献的命名资源)之外,扩展了:
registerTime:本次服务注册时间;locations(Set<String>):该节点当前所有可用实例地址的集合——一个nodeName可以对应多个实例,这是多实例负载均衡与故障转移的基础。
注册接口
节点端 EruptNodeTask 启动后以守护线程方式进入注册循环:每heartbeatTime(节点端默认 15 秒)构造一次NodeInfo并POST到 server 的注册接口。server 侧的落点链路如下:
- 入口 EruptMicroserviceController.registerNode,路径为
CloudRestApiConst.REGISTER_NODE,即POST /erupt-cloud-api/register-node; - 先经 EruptNodeMicroservice.findNodeByAppName 用
nodeName在数据库CloudNode表查出节点档案,若validateAccessToken=true则比对accessToken,不匹配直接抛错——也就是说节点必须先在管理界面里建好档案(拿到 Token)才允许注册; - 检查
CloudNode.status,被禁用的节点会被拒绝注册(返回 500 并提示 "prohibiting the registration"); - 若主应用类上标注了
@EruptCloudServer(见"扩展点"一节),回调其Proxy.registerNode(metaNode, request); - 对
metaNode.getNodeAddress()中每一个地址执行健康探测(CloudServerUtil.nodeHealth):GET{location}/node-health,要求 2xx 且响应体与nodeName一致,任一地址不通则整体注册失败; - 最后调用 EruptNodeMicroservice.registerNode:与 Redis 中已存在的
locations合并、追加本次上报的地址、刷新registerTime并putNode写入 Redis。
节点端上报的地址默认由本机回环 IPv4 地址 +server.port+context-path自动拼装(协议前缀由erupt.cloud-node.schema控制,默认http);在多层代理等复杂网络环境下可通过erupt.cloud-node.hostAddress显式指定对外可达地址,这一设计直接体现在 EruptNodeTask.run 的分支逻辑中。
由于心跳会周期性重放注册流程并刷新 Redis TTL,nodeExpireTime(默认 10 分钟)相当于"心跳丢失后节点被自然淘汰"的兜底窗口。
优雅下线
与注册对称的是摘除接口POST /erupt-cloud-api/remove-instance-node(EruptMicroserviceController.removeInstanceNode),参数为nodeName、accessToken与可选的locations(逗号分隔)。节点端在 EruptNodeTask.destroy 中按"两阶段"完成下线:
- 精确摘除:把本实例注册的地址列表上报给 server,EruptNodeMicroservice.removeInstance 调用
nodeManager.removeNodeInstance逐个删除locations中的条目,使 server 立即停止向该实例路由,不必等待存活巡检;若未上报地址则退化为触发一次NodeWorker.run()全量健康巡检; - 排空(drain):节点端等待已受理的请求处理完毕,等待上限为
erupt.cloud-node.drainTimeout(默认 30 秒),期间通过 EruptNodeInterceptor.inFlight() 的在途计数轮询;超时仍有在途请求会打印告警。
基于 Redis 的节点状态存储
NodeManager 是节点状态的读写中枢,其设计要点:
- Key 结构:
{cloudNameSpace}node:{nodeName},即默认erupt-cloud:node:order-service; - 值序列化:复用应用注入的
RedisTemplate连接工厂,但单独构建一个以 Gson JSON 序列化MetaNode的模板(setRedisTemplate)。注释中说明这样做的收益:给NodeInfo增加字段不会使旧缓存失效;遇到旧二进制格式(JDK 序列化)的历史条目时解析失败按"不存在"处理,等下次心跳重写即可; - 写语义:
putNode时若locations为空集则直接删除该节点缓存(putNode),保证"无实例"的节点不会残留在路由表里; - 实例级摘除:
removeNodeInstance只移除指定实例地址(null 安全,重复摘除无副作用);evictInstance是其"尽力而为"版本,吞掉异常——请求路径上发现死实例时先摘掉,后续由存活巡检兜底对齐; - 全量视图:
findAllNodes以数据库CloudNode表登记过的节点名反查 Redis(multiGet),因此"管理界面可见、但当前离线"的节点会表现为无缓存,不会出现在全量列表里。
多实例选择:轮询 + 故障转移候选序
pickLocations实现了节点内多实例的选择策略(NodeManager.pickLocations):
- 无实例时抛出
EruptWebApiRuntimeException('{nodeName}' has no available instance); - 单实例直接返回;
- 多实例时用一个共享的
AtomicInteger游标做轮询(round-robin):返回的列表中第一个元素是本次主选,其余元素按轮转顺序作为故障转移候选,所有实例都保留在候选序列中。
对应的单元测试 NodeManagerTest 锁定了三组行为:单实例恒等返回;三实例时每次调用"头部轮转、成员不丢、整圈回到原点";空集合抛异常。
健康巡检与死实例摘除
NodeWorker 在@PostConstruct中启动单线程定时调度,按nodeSurviveCheckTime(默认 120 秒)执行一轮巡检:遍历findAllNodes(),对每个实例地址调用 CloudServerUtil.retryableNodeHealth(nodeName, location, 2, 200)——最多重试 2 次、每次间隔 200 毫秒;仍有存活实例变化的节点会回写putNode刷新缓存(同时续期 TTL)。
健康探测本身使用一个独立的 1 秒超时客户端(源码注释:"Health probes must answer fast; a slow instance counts as down",健康探测必须快速应答,慢实例按宕机计),探测目标正是节点端 NodeController 暴露的GET /node-health——该接口直接返回eruptNodeProp.getNodeName(),server 侧用"响应体 == nodeName"来确认探到的确实是目标节点而非挂在同地址的其他服务。
故障转移的安全性边界
转发请求时并非"失败就重试"。EruptCloudServerInterceptor.httpProxy 按pickLocations的顺序逐个尝试,只有当异常被 CloudServerUtil.isConnectFailure 判定为"连接从未到达节点应用"时才向下一实例故障转移,并顺手evictInstance摘除死实例。isConnectFailure沿 cause 链解包,仅识别ConnectException、UnknownHostException、NoRouteToHostException、HttpConnectTimeoutException四类;读超时(SocketTimeoutException)或应用级 500 都不算——因为请求可能已被节点处理,重试会重复写入。
CloudServerUtilTest 把这条安全边界固化成了断言:三类连接异常及其嵌套包装形式判定为可转移,而SocketTimeoutException("read timed out")与业务异常判定为不可转移。这是理解"为什么某些故障会重试、某些不会"的直接证据。
请求代理:浏览器流量如何到达正确的节点
erupt-cloud-server 的核心职责之一是"proxies API and UI requests to the appropriate node"。实现落在 EruptCloudServerInterceptor,它拦截/erupt-api/**(EruptRestPath.ERUPT_API),只在目标 handler 标注了@EruptRouter时介入。
路由键:nodeName.eruptName
代理的路由依据是@EruptRouter(verifyType = ERUPT)接口上携带的 erupt 名。请求头erupt(或 URL 参数_erupt)取值若包含点号,按最后一个点切分(preHandle):
order-service.xyz.erupt.sample.model.Customer ───────────────────────────────────────── nodeName: order-service eruptName: xyz.erupt.sample.model.Customer代理前的鉴权在 server 侧完成:token 与在线会话(SessionKey.TOKEN_OLINE)校验失败返回 401,父级菜单权限getEruptMenuByValue(authErupt)校验失败返回 403;随后把用户、erupt、token 写入MetaContext/NodeContext并进入proxyAndRespond。
两个值得注意的旁路:
@EruptRouter.cloudProxy()默认为true;对"档案数据在 server 本地"的接口(如记录评论,节点既没有该模块也不认识 server 的用户),把cloudProxy置为false即可让 server 自己应答,节点名仅作为寻址依据。该语义完整写在 EruptRouter.cloudProxy 的注释中;- 节点承载的模板页走
/erupt-api/tpl/路径,此时没有 erupt 名可路由,改由显式的_node查询参数指定节点,路径原样透传(preHandle 中的 tpl 分支),并按 tpl 路径做菜单权限校验。
转发细节
httpProxy 的关键处理:
- 请求头:整体拷贝后剔除逐跳/成帧头(
Host、Origin、Content-Length、Connection、Transfer-Encoding、Expect、Upgrade、Accept-Encoding);其中Origin被有意剔除——节点端 EruptNodeInterceptor.preHandle 会拒绝携带Origin的请求(即浏览器直连节点,节点 API 只允许 server 转发),而剔除Accept-Encoding使节点以 identity 编码返回,方便透传; - 注入的交互头:
Access-Token(节点档案中的 Token)、token(当前用户会话)、erupt(节点本地的 erupt 简单名)、user(Base64 编码的MetaUserJSON);钻取来源头DRILL_SOURCE_ERUPT会剥掉nodeName.前缀还原为节点本地名; - 请求体:普通 body 一次性读入字节数组(这样故障转移到下一个实例时可以重放,而不必重读一次性的 servlet 输入流);
multipart/form-data则被逐 part 重编码,由 Spring 重新生成 boundary; - 响应回写(proxyAndRespond):透传
Content-Type与Content-Disposition;非 200 状态缓冲小体积 body、记录错误日志并原样回传;/erupt-api/build/{erupt}的构建响应需要缓冲反序列化为EruptBuildModel,由 eruptBuildProcess 把主模型、drill 链接、operation/tab/combine 子模型的 erupt 名统一补回nodeName.前缀,使前端在"节点聚合视图"里看到的仍是全限定名;其余响应(数据、Excel、文件、大体积 JSON)一律StreamUtils.copy流式透传,不做全量缓冲。
节点端收到转发请求后,由 EruptCloutNodeInterceptor 复核erupt头在本节点真实存在(不存在返回 404),再由 EruptNodeInterceptor 校验Access-Token并从token/user/erupt头重建MetaContext——于是节点本地完整地重跑自己的校验、权限与 DataProxy 管线。
进程内路由:CloudNodeRouter
浏览器请求走的是上面的 HTTP 拦截器短路;但 AI/MCP 工具、工作流等进程内调用者走的是 Erupt 数据管线而非原始 HTTP。CloudNodeRouter 在@PostConstruct中注册为EruptRemoteRouter,以nodeName.eruptName形式的点分名识别远程 erupt(isRemote),并提供:
resolveErupt/resolveEruptView:解析远程 erupt 的模型/视图(视图按需懒加载 schema,注释明确"Only the schema is fetched (lazily); data is never cached",只取结构、从不缓存数据);remoteEruptNames:汇总所有节点的 erupt 全限定名列表;tableQuery、findById、insert、update、delete:把 Erupt 标准数据 API 转发到节点(路径模板中的/{erupt}占位符替换为节点本地简单名)。
其exchange方法与拦截器共用同一套请求头方案、pickLocations轮询与"仅连接失败才转移"的策略,并在转移时同样evictInstance,保证进程内路径与浏览器路径的一致性。
管理界面:节点档案、配置下发与权限
节点档案模型
管理数据由两个实体承载,均通过注解驱动生成低代码界面:
CloudNode(表e_cloud_node,菜单名 "Node Config"):
| 字段 | 说明 |
|---|---|
nodeName | 节点名,唯一索引,注册寻址的主键 |
name | 友好名称 |
cloudNodeGroup | 所属节点组(REFERENCE_TREE引用,形成组 → 节点树) |
status | 启用/禁用,禁用后节点注册被拒绝 |
eruptNum/eruptModuleNum/instanceNum/version | 瞬态字段,列表查询时由 CloudNodeProcess.afterFetch 从 Redis 的MetaNode实时填充(模型数、模块数、实例数、版本),即"节点健康监控"的数据来源;节点离线时显示-,取数异常时版本列渲染红色错误提示 |
duty | 值守人,标签来源于用户列表(fetchTags) |
config | 节点配置,JSON 代码编辑器,注释明确"Config can be read by child nodes"(子节点可读取) |
accessToken | 访问令牌;新建节点时若留空,beforeAdd 自动生成 16 位大写随机码;列表展示时脱敏为"前 3 位 + ****** + 后 3 位",完整值需通过 "View Token" 行操作查看 |
remark | 描述(HTML 渲染) |
行操作(定义在 CloudNode 的 @Erupt 上):
- View Token:微前端模板页
/tpl/node-info.ftl(MICRO_FRONTEND嵌入),受cloud_access_token_permission权限控制; - Node Log:抽屉式日志页
/tpl/erupt-log.html(底部抽屉、高度 80vh),仅在节点版本可用(item.version !== '-')时显示,受erupt_cloud_node_log权限控制;日志经 EruptNodeController.eruptNodeLog 代理到节点端/erupt-api/tool/log接口取回; - 实例移除:管理端也可主动摘除某实例,入口为 EruptNodeController.removeInstance(
GET /erupt-cloud-api/remove-instance/{nodeName}?instance=...,受cloud_node_manager_permission控制)。
CloudNodeGroup(表e_cloud_node_group,菜单名 "Group Config",树形菜单且默认隐藏):code(唯一)、name、config(组级 JSON 配置)、remark。
菜单整体结构由 EruptCloudServerAutoConfiguration.initMenus 声明:根菜单$NodeManager("Micronode Management")下挂组树(隐藏)与节点列表,按钮权限对应 CloudServerConst 中的三个权限码cloud_access_token_permission、cloud_node_manager_permission、erupt_cloud_node_log。
配置下发
server 上维护的两级 JSON 配置通过 EruptServerApi 的两个只读接口下发,节点端 ServerRemoteService 拉取:
| 接口 | 鉴权 | 返回 |
|---|---|---|
GET /erupt-cloud-api/node-config/{nodeName} | 请求头Access-Token与档案一致 | CloudNode.config |
GET /erupt-cloud-api/node-group-config/{nodeName} | 同上 | 节点所属CloudNodeGroup.config |
同文件还暴露两个协作接口:GET /erupt-cloud-api/erupt-power?eruptName=&nodeName=供节点侧查询"某用户在nodeName.eruptName上的增删改查/导入/导出/详情权限位"(eruptPower);GET /erupt-cloud-api/userinfo/{nodeName}返回当前 server 侧登录用户的简要信息,供节点端重建用户上下文(注意其verifyType = LOGIN,即只验登录)。节点侧 ServerRemoteService.getMenuCodePermission 也会回到 server 校验菜单码权限——权限的最终裁定权始终在 server。
扩展点:@EruptCloudServer
CloudServerUtil.findEruptCloudServerAnnotation 会在注册/摘除两个时机检查主应用类上是否标注了 @EruptCloudServer:该注解的value指定一个实现了EruptCloudServer.Proxy的 Bean,Proxy 提供registerNode(metaNode, request)与removeNode(nodeName, request)两个默认空实现钩子,供部署方在节点加入/离开时执行自己的联动逻辑(如同步到其他注册中心)。
此外,distribute 包中的 DistributeAbstract(init/distributePut/distributeRemove)抽象出了节点状态变更的分布式分发骨架,是从"单 server + Redis"向多 server 部署演进的预留扩展面;从源码结构看,当前默认实现即直接使用 Redis 缓存,DistributeAbstract作为扩展抽象存在。
实战配套:节点端erupt.cloud-node.*配置
要让一个 Spring Boot 应用成为可被本模块管理的节点,需要在节点端配置 EruptNodeProp(前缀erupt.cloud-node):
| 配置项 | 默认值 | 说明 |
|---|---|---|
erupt.cloud-node.enableRegister | true | 是否启用节点注册 |
erupt.cloud-node.nodeName | 无 | 节点名,必须与 server 档案中的nodeName一致(建议填 Java 工程名) |
erupt.cloud-node.accessToken | 无 | 在 server 管理界面生成/查看的访问令牌 |
erupt.cloud-node.serverAddresses | 无 | server 地址数组,支持多 server;多地址时getBalanceAddress轮询取用 |
erupt.cloud-node.schema | http | 自动注册地址的协议前缀 |
erupt.cloud-node.hostAddress | 无 | 显式指定本实例对外可达地址(多层代理场景必配) |
erupt.cloud-node.heartbeatTime | 15000 | 心跳(注册刷新)周期,毫秒;连接失败时按半周期重试 |
erupt.cloud-node.drainTimeout | 30000 | 优雅下线时等待在途请求排空的超时,毫秒 |
配置示例:
erupt: cloud-node: nodeName: order-service accessToken: A1B2C3D4E5F6G7H8 # 在 server 管理界面"View Token"获取 serverAddresses: - http://cloud-server:8080 # hostAddress: # 多层代理时显式指定 # - http://order-1.internal:8081节点启动日志会打印节点名、server 地址与版本横幅,注册失败按heartbeatTime/2间隔重试并计数,成功恢复时输出绿色 "Connection success" 提示(EruptNodeTask.run)。
小结
erupt-cloud-server 用一个相当克制的技术组合完成了控制平面的全部职责:数据库(CloudNode/CloudNodeGroup档案)承载节点身份与配置,Redis 承载带 TTL 的运行时实例视图,单线程定时巡检负责最终一致性,拦截器 + 共享RestClient负责请求转发与安全的故障转移。它的几个设计取舍——心跳刷新 TTL 而非依赖主动注销、"仅连接级失败才转移"的保守重试、build 响应重写 erupt 前缀而数据响应流式透传、节点 API 拒绝浏览器直连——都直接体现在 NodeManager、EruptCloudServerInterceptor 与其配套测试(NodeManagerTest、CloudServerUtilTest)中,值得在多实例、多节点的管理系统代理层设计中参考。
- 后端
- 低代码
- AI 应用
- 人工智能
- AI Agent
- 认证鉴权
- RAG
【免费下载链接】erupt
🚀 通用数据管理框架,VORM 对象视图模型,注解驱动低代码开发
相关推荐
ERUPT代理模式:AOP面向切面编程深度解析
ERUPT代理模式:AOP面向切面编程深度解析 引言:为什么需要代理模式? 在现代企业级应用开发中,我们经常面临这样的挑战:如何在 不修改核心业务逻辑 的情况下
后端低代码AI 应用人工智能AI Agent认证鉴权RAGErupt erupt-ai-claw 深度解析:把自然语言变成 Erupt 管理操作的内置 Agent
Erupt erupt ai claw 深度解析:把自然语言变成 Erupt 管理操作的内置 Agent erupt ai claw 是 Erupt 框架中的一
后端低代码AI 应用人工智能AI Agent认证鉴权RAGERUPT反射机制:动态类加载与调用深度解析
ERUPT反射机制:动态类加载与调用深度解析 引言:现代低代码框架的反射核心 在当今快速发展的软件开发领域,低代码(Low Code)开发平台正成为企业数字化转
后端低代码AI 应用人工智能AI Agent认证鉴权RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考