目录

    Phoenix监控平台技术解析(十六):代理端整体架构与角色定位


    Phoenix监控平台技术解析(十六):代理端整体架构与角色定位

    前十五篇,我们从全景架构一路拆到了客户端 SDK 的每一根神经末梢——HTTP/WebSocket 双通道、加解密体系、心跳机制、JVM 采集、线程池监控、Spring Boot Starter 自动配置、SpringMVC Integrator 适配,以及业务告警 API。客户端的故事讲完了,但监控数据的旅程才刚过半。从这一篇开始,我们进入 Phoenix 的第三大模块——代理端(phoenix-agent)。如果说客户端是散落在各个应用进程里的「感知神经」,那代理端就是部署在服务器上的「区域大脑」——它既能自己采集基础设施信息,又能充当客户端与服务端之间的「中转枢纽」。


    一、为什么需要代理端?

    在第一篇全景概览中,我们知道 Phoenix 的四端分离架构包含客户端、代理端、服务端和 UI 端。客户端负责采集 Java 应用内的运行时数据(心跳、JVM、线程池),服务端负责数据持久化和告警触发。乍看之下,客户端直连服务端就能完成整个闭环——那代理端的存在价值是什么?

    答案藏在三个现实场景中:

    场景一:操作系统级的信息没人采集。 客户端作为一个嵌入到业务应用中的 SDK,它能拿到 JVM 内部的数据,但 CPU 利用率、内存使用、磁盘 IO、网卡流量这些操作系统级的指标,并不是每个 Java 应用都应该关心的——尤其当同一台服务器上跑了多个应用时,让每个应用都去采集一遍服务器信息既浪费资源又产生冗余。代理端作为「驻守在服务器上的哨兵」,只需要部署一个实例,就能统一采集该服务器的所有硬件信息。

    场景二:Docker 和网络设备需要专门的采集能力。 Docker 容器的状态、镜像列表、资源统计、实时事件,需要通过 Docker API 交互;交换机和路由器的信息,需要通过 SNMP 协议采集。这些能力显然不应该塞进轻量级的客户端 SDK 里——它们需要一个独立的、拥有更丰富技术栈的运行载体。

    场景三:网络壁垒需要突破。 在很多企业环境中,内网应用无法直接访问监控服务端——中间隔着防火墙、跳板机、网络分区。代理端可以部署在跳板机上,充当「数据中继站」:内网客户端将监控数据发送给代理端,代理端再转发给服务端。一道网络壁垒,就这样被优雅地绕过了。

    用一句话概括代理端的核心价值:它是基础设施的信息采集者,也是网络拓扑中的数据中转站。


    二、双重身份:采集者 + 中转站

    理解代理端的关键,在于认清它的「双重身份」。

    2.1 身份一:基础设施的信息采集者

    代理端部署在目标服务器上,利用各类技术栈采集基础设施信息:

    采集对象 技术手段 采集内容
    服务器硬件 oshi / Sigar CPU、内存、磁盘、网卡、进程、负载、GPU、电池、传感器
    Docker 服务 docker-java 系统概况、容器列表、镜像列表、实时事件、资源统计(CPU/内存/网络IO/磁盘IO)
    网络设备 snmp4j 设备在线状态、系统信息、接口信息

    这些数据会被定时采集、封装成数据包,经过加密后发送到服务端持久化。

    2.2 身份二:客户端的数据中转站

    代理端对外暴露了一组 REST 接口,接收来自客户端的各类监控数据包,然后「原封不动」地转发给服务端。这组接口包括:

    接口类别 端点 作用
    心跳包 /heartbeat/accept-heartbeat-package 接收客户端心跳并转发
    告警包 /alarm/accept-alarm-package 接收客户端告警并转发
    异常包 /exception/accept-exception-package 接收客户端异常信息并转发
    JVM 信息包 /jvm/accept-jvm-package 接收客户端 JVM 信息并转发
    下线包 /offline/accept-offline-package 接收客户端下线通知并转发
    命令包 /command/accept-command-package 接收并执行命令(如 Docker 操作)

    从客户端的视角看,代理端和服务端是「无差别」的——客户端配置文件中的 monitoring.comm.http.url 既可以指向服务端,也可以指向代理端,接口规范完全一致。这种透明中转的设计,让部署拓扑的调整不需要修改任何客户端代码。

    2.3 一个不容忽视的事实:代理端自己也是客户端

    打开 phoenix-agentpom.xml,你会发现一个有趣的依赖:

    <!-- springboot项目集成监控依赖,使之拥有监控功能 -->
    <dependency>
        <groupId>com.gitee.pifeng</groupId>
        <artifactId>phoenix-client-spring-boot-starter</artifactId>
    </dependency>
    

    代理端引入了 phoenix-client-spring-boot-starter——这意味着代理端自身也是一个被监控的应用实例。它会向服务端发送心跳、上报 JVM 信息、采集自己所在服务器的硬件数据。「监控者自身也被监控」——这种设计确保了代理端不会成为监控盲区。


    三、工程结构:按职责分包

    代理端的源码组织清晰地映射了它的「双重身份」。先看整体包结构:

    com.gitee.pifeng.monitoring.agent
    ├── AgentApplication.java                    # SpringBoot 启动入口
    ├── config/                                  # 配置类
    │   ├── RestTemplateConfig.java              #   HTTP 连接池配置
    │   ├── ThreadPoolConfig.java                #   Docker 采集线程池配置
    │   ├── WebSocketConfig.java                 #   WebSocket 端点配置
    │   ├── Knife4jConfig.java                   #   API 文档配置
    │   ├── MonitoringAgentDevConfig.java         #   开发环境监控配置
    │   └── MonitoringAgentProdConfig.java        #   生产环境监控配置
    ├── constant/
    │   └── UrlConstants.java                    # 服务端 URL 常量(30+ 个端点地址)
    ├── core/
    │   ├── AgentPackageConstructor.java         # 代理端数据包构造器
    │   └── MethodExecuteHandler.java            # 方法执行助手(核心调度器)
    ├── business/
    │   ├── client/                              # 面向客户端的接口层
    │   │   ├── controller/                      #   18 个 REST 控制器
    │   │   └── service/                         #   10 个服务接口 + 实现
    │   ├── server/                              # 面向服务端的通信层
    │   │   └── service/                         #   12 个服务接口 + 实现
    │   └── agent/                               # 代理端自身的采集层
    │       ├── collector/                       #   采集器(DockerCollector)
    │       ├── core/docker/                     #   Docker 中央控制器 + 事件回调
    │       └── thread/                          #   采集线程(DockerThread)
    └── util/docker/                             # Docker 工具类
        ├── DockerClientUtils.java               #   连接管理
        ├── DockerContainerUtils.java            #   容器操作
        ├── DockerImageUtils.java                #   镜像操作
        ├── DockerInfoUtils.java                 #   系统信息
        └── DockerStatsUtils.java                #   资源统计
    

    三个关键的 business 子包,精确对应了三个职责域:

    • business.client:对「下游」——接收客户端发来的数据包
    • business.server:对「上游」——把数据包转发给服务端
    • business.agent:对「自身」——代理端自己的采集任务

    这种分包方式的好处在于,当你想了解「代理端是怎么接收客户端心跳的」,直奔 business.client.controller.HeartbeatController;想了解「心跳包是怎么转发给服务端的」,转向 business.server.service.IHeartbeatService;想了解「Docker 信息是怎么采集的」,翻开 business.agent.collector.DockerCollector。职责边界一目了然。


    四、启动入口:AgentApplication

    @Slf4j
    @Indexed
    @EnableRetry
    @SpringBootApplication
    @ComponentScan(nameGenerator = UniqueBeanNameGenerator.class)
    public class AgentApplication extends CustomizationUndertowWebServerFactoryCustomizer {
    
        public static void main(String[] args) {
            // 计时器
            TimeInterval timer = DateUtil.timer();
            SpringApplication.run(AgentApplication.class, args);
            // 时间差(毫秒)
            String betweenDay = timer.intervalPretty();
            log.info("监控代理端启动耗时:{}", betweenDay);
        }
    }
    

    短短十几行,暗藏四个设计决策:

    1. @EnableRetry——重试能力。 代理端的核心操作是「转发数据到服务端」,网络请求天然不可靠。@EnableRetry 开启 Spring Retry 支持,让服务层方法可以通过 @Retryable 注解获得自动重试能力。在分布式场景中,这是对抗瞬时故障的基础设施。

    2. CustomizationUndertowWebServerFactoryCustomizer——Undertow 定制。 代理端选择 Undertow 而非 Tomcat 作为嵌入式 Web 服务器。Undertow 以轻量、高性能著称,非常适合代理端这种「高吞吐转发」的场景。继承这个定制器可以对 Undertow 做细粒度调优。

    3. UniqueBeanNameGenerator——Bean 命名策略。 代理端同时依赖了 phoenix-common-webphoenix-client-spring-boot-starter 两大模块,不同模块中可能存在同名的 Bean 定义。使用全限定类名作为 Bean ID,从根本上杜绝了命名冲突。

    4. @Indexed——加速组件扫描。 @Indexed 注解配合 spring-context-indexer 在编译期生成组件索引文件,避免运行时的 classpath 扫描。对于包含大量组件的代理端而言,这能显著缩短启动时间。


    五、数据中转:从接收到转发

    代理端最核心的职责之一是「透明中转」——接收客户端的数据包,然后转发给服务端。这个流程涉及三层:Controller → Client Service → Server Service。

    5.1 Controller 层:18 个入口

    代理端总共有 18 个 REST 控制器,覆盖了所有类型的数据交互。按功能可以分为两大类:

    第一类:数据包转发型。 接收客户端的数据包,通过对应的 Service 转发给服务端。

    @Deprecated
    @PostMapping("/accept-alarm-package")
    public BaseResponsePackage acceptAlarmPackage(@RequestBody AlarmPackage alarmPackage) {
        return this.alarmService.dealAlarmPackage(alarmPackage);
    }
    

    AlarmController 为例——接收告警包,交给 IAlarmService 处理。注意 @Deprecated 注解,这并不意味着接口废弃,而是标记该接口是「遗留的 HTTP 接口」。在新版本中,客户端已经通过 WebSocket 通道发送这些数据包,HTTP 接口保留只是为了向后兼容。

    第二类:请求代理型。 接收客户端的请求,转发给服务端处理后,把服务端的响应原样返回。

    @PostMapping("/test-monitor-db")
    public BaseResponsePackage testMonitorDb(@RequestBody BaseRequestPackage baseRequestPackage)
            throws NetException {
        return this.baseRequestPackageService.dealBaseRequestPackage(
                baseRequestPackage, UrlConstants.TEST_MONITOR_DB_URL);
    }
    

    DbController 为例——客户端请求测试数据库连通性,代理端并不自己执行测试,而是把请求原封不动地转发给服务端,服务端执行完测试后返回结果,代理端再传递给客户端。这类接口涵盖了数据库连通性测试、网络连通性测试、HTTP 连通性测试、MySQL/Oracle 会话管理、Redis/Mongo 信息查询等。

    5.2 MethodExecuteHandler:核心调度器

    MethodExecuteHandler 是代理端数据转发的核心枢纽。它通过 InvokerHolder(命令执行器管理器)查找注册的服务实例,调用对应的方法将数据包发往服务端:

    public static BaseResponsePackage sendAlarmPackage2Server(AlarmPackage alarmPackage) {
        // 通过命令执行器管理器,获取指定的命令执行器
        Invoker invoker = InvokerHolder.getInvoker(IAlarmService.class, "sendAlarmPackage");
        // 执行命令,返回执行结果
        return execute(invoker, alarmPackage);
    }
    

    为每一种数据包类型(心跳、告警、异常、服务器、JVM、Docker、网络设备、命令、下线、基础请求),MethodExecuteHandler 都提供了一个专门的 sendXxxPackage2Server() 方法。这种设计虽然看起来有些「手工路由」的味道,但胜在类型安全——每个方法的参数和返回值都是明确的 DTO 类型,编译期就能发现类型错误。

    execute() 方法是所有转发操作的统一出口:

    public static BaseResponsePackage execute(Invoker invoker, Object... objects) {
        BaseResponsePackage responsePackage;
        try {
            assert invoker != null;
            Object object = invoker.invoke(objects);
            responsePackage = (BaseResponsePackage) object;
        } catch (Exception e) {
            Result result = Result.builder().isSuccess(false).msg(e.getMessage()).build();
            responsePackage = AGENT_PACKAGE_CONSTRUCTOR.structureBaseResponsePackage(result);
        }
        return responsePackage;
    }
    

    任何转发失败都不会向上抛出异常——而是构造一个 isSuccess = false 的响应包返回。这和客户端 Monitor.sendAlarm() 的设计理念一脉相承:监控链路上的故障不应该向调用方传播

    5.3 UrlConstants:30+ 个端点地址

    UrlConstants 类定义了代理端与服务端之间所有的通信端点地址,根 URI 从监控配置中动态读取:

    private static final String ROOT_URI = ConfigLoader.getMonitoringProperties()
            .getComm().getHttp().getUrl();
    
    public static final String HEARTBEAT_URL = ROOT_URI + "/heartbeat/accept-heartbeat-package";
    public static final String ALARM_URL = ROOT_URI + "/alarm/accept-alarm-package";
    public static final String SERVER_URL = ROOT_URI + "/server/accept-server-package";
    // ... 30+ 个端点
    

    这意味着代理端和服务端之间的接口路径是完全对称的。代理端的 /alarm/accept-alarm-package 接收客户端的告警包,然后转发到服务端的 /alarm/accept-alarm-package——路径一模一样,只是主机和端口不同。这种对称设计极大地降低了理解和维护成本。


    六、信息采集:Docker 收集器的四步编排

    代理端「自主采集」的能力主要集中在 Docker 信息采集上(服务器信息和网络设备信息的采集依赖客户端 SDK 的内置能力,因为代理端本身就是一个客户端实例)。Docker 采集的启动流程是一个精心设计的四步编排。

    6.1 DockerCollector:CommandLineRunner 的巧妙运用

    @Component
    public class DockerCollector implements CommandLineRunner {
    
        @Override
        public void run(String... args) {
            MonitoringDockerInfoProperties dockerInfoProperties =
                    this.monitoringProperties.getDockerInfo();
            boolean dockerInfoEnable = dockerInfoProperties.getEnable();
            if (dockerInfoEnable) {
                // 第一步:获取 Docker 客户端
                DockerClient dockerClient = getDockerClient(dockerInfoProperties);
                // 第二步:初始化 Docker 中央控制器(单例)
                DockerCentralController dockerCentralController =
                        DockerCentralController.getInstance().init(dockerClient);
                // 第三步:启动事件监听回调
                startAllDockerCallback(dockerCentralController);
                // 第四步:定时采集 Docker 信息
                long rate = this.monitoringProperties.getDockerInfo().getRate();
                this.dockerMonitorScheduledThreadPoolExecutor
                        .scheduleAtFixedRate(new DockerThread(), 30, rate, TimeUnit.SECONDS);
            }
        }
    }
    

    CommandLineRunner 是 Spring Boot 提供的启动钩子——应用上下文完全初始化后才会执行。这比 @PostConstruct 更晚、更安全,确保所有 Bean(包括线程池、配置属性)都已经准备就绪。

    四步流程的设计逻辑非常清晰:先建连接,再初始化控制器,然后启动事件监听,最后开启定时采集。 其中前置检查 dockerInfoEnable 确保了当 Docker 采集被关闭时(默认关闭),整个流程不会浪费任何资源。

    6.2 DockerCentralController:枚举单例模式

    public class DockerCentralController {
    
        private DockerClient dockerClient;
    
        private DockerCentralController() {}
    
        private enum Singleton {
            INSTANCE;
            private final DockerCentralController instance;
            Singleton() { instance = new DockerCentralController(); }
            private DockerCentralController getInstance() { return instance; }
        }
    
        public static DockerCentralController getInstance() {
            return Singleton.INSTANCE.getInstance();
        }
    
        public DockerCentralController init(DockerClient dockerClient) {
            this.dockerClient = dockerClient;
            return this;
        }
    }
    

    DockerCentralController 采用枚举单例模式——这是 Java 中最安全的单例实现方式:天然线程安全、防止反射攻击、自动支持序列化。它作为 Docker 操作的「总控中心」,统一管理 Docker 客户端实例,提供三大能力:

    1. 信息采集getDockerInfo() 一次性获取系统概况、容器列表、镜像列表和资源统计
    2. 事件监听startDockerEventCallback() 注册异步事件回调
    3. 命令执行executeDockerCommand() 处理来自 UI 端的容器/镜像操作指令

    6.3 DockerEventCallback:实时事件流

    public class DockerEventCallback implements ResultCallback<Event> {
    
        @Override
        public void onNext(Event event) {
            // 解析事件类型、动作、参与者信息
            EventDomain eventDomain = new EventDomain();
            eventDomain.setEventType(type.getValue());
            eventDomain.setEventAction(action);
            // ...
            // 立即发送给服务端
            this.sendDockerEvent(eventDomain);
        }
    
        @Override
        public void onComplete() {
            log.warn("docker事件监听终止!");
            // 自动重新启动事件监听
            DockerCentralController.getInstance().startDockerEventCallback(this);
            log.warn("重新启动docker事件监听!");
        }
    }
    

    DockerEventCallback 实现了 docker-java 的 ResultCallback<Event> 接口,以异步回调的方式实时接收 Docker 事件(容器启停、镜像拉取、网络创建等)。两个设计亮点值得注意:

    即时转发:事件不做缓存,onNext() 中每收到一个事件就立即封装为 DockerPackage 发送给服务端。这保证了 Docker 事件在 UI 端的「近实时」展示。

    自动重连onComplete() 被调用意味着事件流断开了——可能是 Docker 守护进程重启,也可能是网络中断。回调在此处自动重新注册自身,实现了「断开即重连」的弹性设计。

    6.4 DockerThread:定时采集

    public class DockerThread implements Runnable {
    
        @Override
        public void run() {
            try {
                DockerCentralController controller = DockerCentralController.getInstance();
                Docker docker = controller.getDockerInfo();
                MethodExecuteHandler.send(docker, Docker.class);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }
    

    DockerThread 是定时采集的执行体,由 DockerCollectorscheduleAtFixedRate 的方式调度。每次执行时:

    1. 通过 DockerCentralController 获取完整的 Docker 信息(系统概况 + 容器 + 镜像 + 资源统计)
    2. 通过 MethodExecuteHandler.send() 将信息封装为 DockerPackage 并发送给服务端

    注意这里使用的是 scheduleAtFixedRate(固定速率)而非 scheduleWithFixedDelay(固定延迟)——与心跳机制的选择不同。为什么?因为 Docker 信息的「时间点」很重要:容器的 CPU 和内存统计需要在固定间隔的时间点上采样,才能计算出准确的速率值。如果采用固定延迟,采样间隔会因每次采集耗时不同而产生偏移,影响速率计算的精度。


    七、配置体系:双环境 + 灵活开关

    7.1 monitoring.properties:采集行为的「遥控器」

    代理端的采集行为完全由 monitoring-dev.properties(开发环境)和 monitoring-prod.properties(生产环境)控制:

    # 实例端点类型:标识自己是 agent
    monitoring.instance.endpoint=agent
    # 实例名称
    monitoring.instance.name=phoenix-agent
    
    # 心跳频率(秒),不可低于30秒
    monitoring.heartbeat.rate=30
    
    # 服务器信息采集
    monitoring.server-info.enable=true
    monitoring.server-info.rate=30
    
    # JVM 信息采集
    monitoring.jvm-info.enable=true
    monitoring.jvm-info.rate=60
    
    # Docker 信息采集
    monitoring.docker-info.enable=false
    monitoring.docker-info.rate=60
    monitoring.docker-info.host=unix:///var/run/docker.sock
    
    # 网络设备信息采集
    monitoring.network-device-info.enable=false
    monitoring.network-device-info.rate=300
    
    # 加密算法
    monitoring.secure.encryption-algorithm-type=aes
    

    几个值得关注的配置:

    monitoring.instance.endpoint=agent——这个配置告诉服务端:「我是代理端,不是普通客户端。」服务端会据此在 UI 上以不同的标识展示代理端实例。

    Docker 采集默认关闭——monitoring.docker-info.enable=false。这是因为不是每台服务器都安装了 Docker,强制启用会导致启动报错。只有在确认目标服务器运行了 Docker 服务后,才需要在配置中开启。

    网络设备采集频率较高——300 秒(5 分钟)一次,远低于服务器信息的 30 秒。这是因为 SNMP 协议的交互成本较高,而且网络设备的状态变化通常比较缓慢,不需要高频轮询。

    7.2 application.yml:Web 服务配置

    server:
      servlet:
        context-path: /phoenix-agent
      shutdown: graceful
    
    spring:
      lifecycle:
        timeout-per-shutdown-phase: 30s
    

    代理端的 context-path 为 /phoenix-agent,默认端口 12000。graceful 优雅停机配合 30 秒的缓冲期,确保代理端在关闭时能完成正在处理的请求、关闭 Docker 连接、发送下线通知——不会「硬杀」导致数据丢失。

    7.3 RestTemplateConfig:高性能 HTTP 连接池

    // 设置整个连接池的最大连接数
    manager.setMaxTotal(300);
    // 每个路由的最大连接数
    manager.setDefaultMaxPerRoute(200);
    // 在从连接池获取连接时,连接不活跃多长时间后需要进行一次验证
    manager.setValidateAfterInactivity(30 * 1000);
    

    代理端作为数据中转站,同时处理多个客户端的转发请求,对 HTTP 连接池的要求远高于普通应用。300 个最大连接数、200 个单路由连接数、连接重试 3 次——这套配置能支撑中等规模的并发转发场景。如果你的监控规模更大(比如上百个客户端同时通过一个代理端转发),可以进一步调大这些参数。

    7.4 ThreadPoolConfig:IO 密集型线程池

    @Bean(name = "dockerMonitorScheduledThreadPoolExecutor", destroyMethod = "shutdown")
    public MonitoredScheduledThreadPoolExecutor dockerMonitorScheduledThreadPoolExecutor() {
        return new MonitoredScheduledThreadPoolExecutor(
                (int) (ProcessorsUtils.getAvailableProcessors() / (1 - 0.8)),
                // ...
        );
    }
    

    Docker 采集线程池的线程数公式为 Ncpu / (1 - 0.8),即 CPU 核心数的 5 倍。这是 IO 密集型任务的经典配置——Docker API 调用大部分时间在等待网络响应,CPU 实际占用率很低,需要更多线程来提高吞吐。同时,线程池本身是 MonitoredScheduledThreadPoolExecutor,自身的运行指标(活跃线程数、队列大小等)也会被纳入 Phoenix 的线程池监控体系。


    八、数据流全景:一个 Docker 事件的旅程

    把以上所有组件串起来,看一个 Docker 容器启动事件是如何从发生到最终展示在 UI 上的:

    Docker 守护进程                    代理端                           服务端
          │                            │                              │
          │  容器 start 事件            │                              │
          │  ───────────────────▶      │                              │
          │                     DockerEventCallback.onNext()          │
          │                     ├── 解析事件类型、动作、时间             │
          │                     ├── 构建 EventDomain                  │
          │                     └── sendDockerEvent()                 │
          │                            │                              │
          │                     MethodExecuteHandler.send()            │
          │                     ├── AgentPackageConstructor            │
          │                     │   └── structureDockerPackage()       │
          │                     ├── sendDockerPackage2Server()         │
          │                     │   └── InvokerHolder → IDockerService │
          │                     └── HTTP POST (加密)  ───────────▶    │
          │                            │                              │
          │                            │               解密 → 反序列化 │
          │                            │               DockerServiceImpl
          │                            │               └── 持久化到    │
          │                            │                  MONITOR_DOCKER_EVENT
          │                            │                              │
          │                            │                     UI 端查询展示
    

    整个链路清晰可追踪:Docker 事件 → 回调捕获 → 数据包构造 → 加密转发 → 服务端入库 → UI 展示。每一步都有明确的代码对应,每一层都有异常捕获和日志记录。


    九、代理端 vs 服务端:API 对称性

    仔细对比代理端和服务端的 Controller 接口,你会发现一个有趣的设计原则——API 路径完全对称

    功能 代理端端点 服务端端点
    接收心跳包 /phoenix-agent/heartbeat/accept-heartbeat-package /phoenix-server/heartbeat/accept-heartbeat-package
    接收告警包 /phoenix-agent/alarm/accept-alarm-package /phoenix-server/alarm/accept-alarm-package
    测试数据库 /phoenix-agent/db/test-monitor-db /phoenix-server/db/test-monitor-db
    测试 HTTP /phoenix-agent/http/test-monitor-http /phoenix-server/http/test-monitor-http

    除了 context-path 不同(/phoenix-agent vs /phoenix-server),路径后缀完全一致。这不是巧合,而是刻意为之——客户端只需要修改一个配置项(服务地址),就能在「直连服务端」和「经代理端中转」之间无缝切换。对客户端来说,代理端和服务端是「可替换」的。

    这种对称性的背后,是 IBaseRequestPackageService 的统一抽象:

    public BaseResponsePackage dealBaseRequestPackage(
            BaseRequestPackage baseRequestPackage, String url) {
        return MethodExecuteHandler.sendBaseRequestPackage2Server(baseRequestPackage, url);
    }
    

    代理端的 Service 实现极其简洁——接到请求,直接委托 MethodExecuteHandler 转发,不做任何业务处理。它只是一个「透明管道」。


    十、小结

    这一篇我们从宏观到微观,全面剖析了 Phoenix 代理端的整体架构与角色定位。回顾几个核心要点:

    1. 双重身份:代理端既是「基础设施的信息采集者」(Docker、服务器、网络设备),又是「客户端的数据中转站」(透明转发所有类型的监控数据包)。

    2. 自我监控:代理端通过引入 phoenix-client-spring-boot-starter,让自身也成为一个被监控的应用实例——心跳、JVM、线程池信息一个不落。

    3. 三层分包business.client(面向客户端)、business.server(面向服务端)、business.agent(自身采集)——职责边界清晰,代码导航直觉化。

    4. Docker 采集四步编排:建立连接 → 初始化单例控制器 → 启动事件回调 → 开启定时采集。DockerEventCallback 的自动重连和 DockerCentralController 的枚举单例是两个值得学习的设计。

    5. API 对称性:代理端和服务端的接口路径完全对称,客户端可以在「直连」和「中转」之间无缝切换,实现了部署拓扑与业务代码的彻底解耦。

    6. 核心调度器 MethodExecuteHandler:作为数据转发的枢纽,通过 InvokerHolder 实现服务方法的动态查找和调用,异常兜底确保转发失败不会向调用方传播。

    代理端的本质,是在客户端与服务端之间搭建了一座桥——既拓展了监控能力的边界(从 JVM 内延伸到操作系统和容器),又突破了网络拓扑的限制(打通内外网)。从下一篇开始,我们将深入代理端最「重」的采集能力——服务器信息采集,看看 oshi 和 Sigar 这两套方案各自的实现细节与取舍。敬请期待。


    项目地址
    https://gitcode.com/monitoring-platform/phoenix
    https://gitee.com/monitoring-platform/phoenix
    https://github.com/709343767/phoenix

    欢迎关注微信公众号获取更多技术干货
    微信公众号·披锋斩棘

    end
  1. 作者: 锋哥 (联系作者)
  2. 发表时间: 2026-04-10 16:09
  3. 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)
  4. 转载声明:如果是转载博主转载的文章,请附上原文链接
  5. 公众号转载:请在文末添加作者公众号二维码(公众号二维码见右边,欢迎关注)
  6. 评论

    站长头像 知录

    你一句春不晚,我就到了真江南!

    文章0
    浏览0

    文章分类

    标签云