手写一个健康心跳探针监控平台

先创建一个maven项目,分别创建一个客户端探针module和一个服务端module,项目结构参考如下:

5c933accf28f6152043a335020f3dfd3.png

监控平台 - 客户端源码

1、先定义一个客户端探针类,将需要上报和统计的客户端信息通过类属性定义出来:

// 客户端探针类
public class Probe implements Serializable {
    private static final long serialVersionUID = 98765432124352L;
    // 客户端上报的服务名称
    private String serverName;
    // 客户端上报的ip地址
    private String ip;
    private String id;
    // 客户端首次上报的时间
    private long createTime;
    // 客户端当前上报的时间
    private long lastUpdateTime;
    
    // 其它信息自行按需添加
    
    // ....getter setter
}

2、定义一个基于套接字socket上报健康的类:

/**
 * 发送健康类
 */
public class SendHeartbeat {


    // 探针
    private final Probe probe;


    public SendHeartbeat(String serverName, String ip) {
        //实例化探针对象,一个应用对于一个探针
        this.probe = new Probe();
        probe.setServerName(serverName);
        probe.setIp(ip);
        probe.setId(SnowflakeIdUtils.getInstance().nextId() + "");
        //实例化ReportHeartbeat时,记录探针客户端首次创建时间
        probe.setCreateTime(System.currentTimeMillis());
    }


    //上报心跳
    public void send() {
        OutputStream out = null;
        Socket socket = null;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream os = null;
        try {
            //HEARTBEAT_SERVER_IP = 上报监控平台服务端 IP,HEARTBEAT_SERVER_PORT = 上报监控平台服务端 端口
            socket = new Socket(Config.HEARTBEAT_SERVER_IP, Config.HEARTBEAT_SERVER_PORT);
            os = new ObjectOutputStream(bos);
            os.writeObject(probe);
            os.flush();
            byte[] b = bos.toByteArray();
            out = socket.getOutputStream();
            out.write(b);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭socket
            this.closeSocket(socket, out, bos, os);
        }
    }


    //关闭socket
    private void closeSocket(Socket socket, OutputStream out, ByteArrayOutputStream bos, ObjectOutputStream os) {
        // ...忽略关闭socket资源代码
    }
}

3、定义一个基于while循环定时上报健康类:

/**
 * 定时执行上报
 */
public class TimerWork {


    //健康上报类,一个客户端实例化一个上报类
    private SendHeartbeat sendHeartbeat;


    public TimerWork(String serverName) {
        if (serverName == null || "".equals(serverName)) {
            System.out.println("请传递服务名称参数");
            return;
        }
        String ip = IpAddress.getIp();
        if (ip == null || "".equals(ip)) {
            System.out.println("ip没有找到");
            return;
        }
        sendHeartbeat = new SendHeartbeat(serverName, ip);
        while (true) {
            try {
                sendHeartbeat.send();
                //每隔30s发送一次心跳
                Thread.sleep(Config.SEND_HEARTBEAT_TIME);
            } catch (Exception ignored) {
            }
        }
    }
}

4、创建一个Java Agent代理类(要把这个项目打成一个jar包,例如agent-client.jar文件):

public class Agent {
    //执行main方法前,会执行该方法,并且可以传递参数
    //只需要在启动JVM参数增加:"-javaagent:D:\Projects\agent-client.jar=这里传参,可以传很多信息参数,服务名称就是通过这里传入"
    public static void premain(String serviceName, Instrumentation instrumentation) {
        try {
            //自定义类和lib加载器,构建运行时环境
            AgentClassLoader.loadClass(instrumentation);
            //通过创建一条异步后台线程定时上报健康信息
            new Thread(() -> new TimerWork(serviceName)).start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

监控平台 - 服务端源码

1、定义一个客户端探针上报健康信息存储接口,支持增删改查:

public interface HeartbeatRepository {
    void add(Probe server);
    void update(Probe server);
    Map<String, List<Probe>> list();
}

2、定义一个定义一个客户端探针上报健康信息存储实现类,这里直接存储在内存,数据量大的客户选择储存到mysql或elasticsearch:

public class HeartbeatMemoryRepository implements HeartbeatRepository {
    // 探针数据源
    private final Map<String, List<Probe>> probeHolder = new ConcurrentHashMap<>();


    @Override
    public void add(Probe probe) {
        List<Probe> list = probeHolder.computeIfAbsent(probe.getIp(), k -> new ArrayList<>());
        list.add(probe);
    }


    @Override
    public synchronized void update(Probe probe) {
        if (probe == null) {
            return;
        }
        List<Probe> probes = probeHolder.computeIfAbsent(probe.getIp(), k -> new ArrayList<>());
        boolean found = false;
        for (Probe oldProbe : probes) {
            if (oldProbe.equals(oldProbe)) {
                found = true;
                oldProbe.setLastUpdateTime(System.currentTimeMillis());
                break;
            }
        }
        if (!found) {
            if (probe.getCreateTime() <= 0){
                probe.setCreateTime(System.currentTimeMillis());
            }
            probe.setLastUpdateTime(System.currentTimeMillis());
            probes.add(probe);
        }
    }


    @Override
    public Map<String, List<Probe>> list() {
        long currentTime = System.currentTimeMillis();
        for (Map.Entry<String, List<Probe>> entry : probeHolder.entrySet()) {
            //每次在查询的时候触发数据清理,将超过心跳上报间隔时间 * 2倍数 的还未更新过的探针清理出去
            entry.getValue().removeIf(next -> (currentTime - next.getLastUpdateTime()) > (Config.SEND_HEARTBEAT_TIME + Config.SEND_HEARTBEAT_DOWN_LINE_TIME));
        }
        return this.probeHolder;
    }
}

3、定义一个基于套接字socket接收探针健康类:

/**
 * 接收健康类
 */
public class ReceivedHeartbeat implements Runnable {
    //套接字
    private Socket socket;
    public ReceivedHeartbeat(Socket socket) {
        this.socket = socket;
    }


    @Override
    public void run() {
        InputStream is = null;
        PrintWriter out = null;
        ObjectInputStream ois = null;
        try {
            is = this.socket.getInputStream();
            ois = new ObjectInputStream(is);
            Probe server = (Probe) ois.readObject();


            out = new PrintWriter(this.socket.getOutputStream(), true);
            HeartbeatRepository heartbeatRepository = HeartbeatRepositoryFactory.of();
            heartbeatRepository.update(server);
            out.println("ok");
        } catch (Exception ignored) {
        } finally {
            //关闭套接字
            this.closeSocket(is, out, ois);
        }
    }
    
    private void closeSocket(InputStream is, PrintWriter out, ObjectInputStream ois){
        // ...忽略关闭socket资源代码
    }
}

4、定义一个通用线程池:

public class ThreadPoolHolder {


    //核心数
    public static final int nThreads = Runtime.getRuntime().availableProcessors();
    public static long startTime = System.currentTimeMillis();
    //主线程池
    public static final ExecutorService bootstrapExecutor = Executors.newFixedThreadPool(2);
    //任务线程池
    public static final ExecutorService taskExecutor = new ThreadPoolExecutor(
            nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<>(100),
            new ThreadPoolExecutor.DiscardPolicy());
}

5、定义一个接收健康信息服务线程任务工作类:

public class StartReceivedHeartbeatTask implements Runnable {


    private Integer port;
    public StartReceivedHeartbeatTask(Integer port) {
        if (port != null) {
            this.port = port;
        } else {
            this.port = Config.HEARTBEAT_SERVER_PORT;
        }
    }


    @Override
    public void run() {
        ServerSocket serverSocket = null;
        Socket socket = null;
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("The Heartbeat Server is start in port:" + port);
            while (true) {
                socket = serverSocket.accept();
                ThreadPoolHolder.taskExecutor.submit(new ReceivedHeartbeat(socket));
            }
        } catch (Exception ignored) {
            //ignored
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ignored) {
                    //ignored
                }
            }
        }
    }
}

监控平台 - 服务端 - 监控控制台源码

1、定义一个html显示的页面index.html,这里使用freemark模板引擎渲染页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <title>${title}</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">


    <style type="text/css">
html {
    font-family: sans-serif;
    -ms-text-size-adjust: 100%;
    -webkit-text-size-adjust: 100%;
}


body {
    margin: 10px;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}


td,th {
    padding: 0;
}


.pure-table {
    border-collapse: collapse;
    border-spacing: 0;
    empty-cells: show;
    border: 1px solid #cbcbcb;
}


.pure-table caption {
    color: #000;
    font: italic 85%/1 arial,sans-serif;
    padding: 1em 0;
    text-align: center;
}


.pure-table td,.pure-table th {
    border-left: 1px solid #cbcbcb;
    border-width: 0 0 0 1px;
    font-size: inherit;
    margin: 0;
    overflow: visible;
    padding: .5em 1em;
}


.pure-table thead {
    background-color: #e0e0e0;
    color: #000;
    text-align: left;
    vertical-align: bottom;
}


.pure-table td {
    background-color: transparent;
}
</style>
</head>
<body>
<h1>控制台启动时间:${consoleServerStartTime}</h1>
<h3>启动服务应用总数:${serverCount}   服务器节点总数:${ipCount}  应用实例总数:${allCount}</h3>
<table class="pure-table">
    <thead>
    <tr>
        <th>序号</th>
        <th>应用名称</th>
        <th>所在服务器节点IP</th>
        <th>创建时间</th>
        <th>最后心跳时间</th>
    </tr>
    </thead>
    <tbody>
    <#list allServer as server>
    <tr>
        <td>${server_index}</td>
        <td>${server.serverName}</td>
        <td>${server.ip}</td>
        <td>${server.createTime?number_to_datetime?string("yyyy-MM-dd HH:mm:ss")!}</td>
        <td>${server.lastUpdateTime?number_to_datetime?string("yyyy-MM-dd HH:mm:ss")!}</td>
    </tr>
    </#list>
    </tbody>
</table>
</body>
</html>

2、定义一个freemark模板引擎初始化类:

public class FreemarkerTemplateEngine {


    private static Configuration cfg;


    private static String templateRootPath = "/templates/";


    static {
        cfg = new Configuration(Configuration.VERSION_2_3_28);
        //指定模板文件根路径
        cfg.setClassForTemplateLoading(FreemarkerTemplateEngine.class, templateRootPath);
        cfg.setDefaultEncoding("UTF-8");
        //设置错误的显示方式
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        //不要在FreeMarker中记录它会抛出的异常
        cfg.setLogTemplateExceptions(false);
        //将模板处理期间抛出的未经检查的异常包装到TemplateException -s中
        cfg.setWrapUncheckedExceptions(true);
    }


    /**
     * 生成方法
     *
     * @param templateRelativePath 模板文件路径
     * @param templateData         模板数据
     * @return 输出流
     * @throws Exception
     */
    public static byte[] doGenerator(String templateRelativePath, Map<String, Object> templateData) {


        Writer out = null;
        try {
            Template template = cfg.getTemplate(templateRelativePath);
            out = new StringWriter();
            template.process(templateData, out);
            out.flush();
            String str = out.toString();
            return str.getBytes();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {


            try {
                if (out != null) {
                    out.close();
                }
            } catch (Exception ignored) {


            }
        }
    }
}

3、定义一个控制台线程任务类:

public class StartHeartbeatConsoleTask implements Runnable {


    private Integer port;


    public StartHeartbeatConsoleTask(Integer port) {
        if (port != null) {
            this.port = port;
        } else {
            this.port = Config.HEARTBEAT_CONSOLE_PORT;
        }
    }


    @Override
    public void run() {


        if (port == null) {
            port = 9001;
        }
        ServerSocket serverSocket = null;
        Socket socket = null;
        try {
            serverSocket = new ServerSocket(port);
            System.out.println("The Heartbeat Console is start in port:" + port);
            while (true) {
                socket = serverSocket.accept();
                ThreadPoolHolder.taskExecutor.submit(new HeartbeatConsoleTask(socket));
            }
        } catch (Exception ignored) {
            //ignored
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException ignored) {
                    //ignored
                }
            }
        }
    }
}

4、最后定义一个服务端总的引导启动类,同时启动控制台和监控接收服务任务线程:

public class Boostrap {


    //记录控制台启动时间
    public static long startTime = System.currentTimeMillis();


    public Boostrap(String arguments) {
        //解析端口
        Integer serverPort = null;
        Integer consolePort = null;
        if (arguments != null && !"".equals(arguments)) {
            String[] ports = arguments.split(",");
            try {
                serverPort = Integer.parseInt(ports[0]);
                consolePort = Integer.parseInt(ports[1]);
            } catch (Exception ignored) {


            }
        }


        //启动接收心跳健康信息服务
        ThreadPoolHolder.bootstrapExecutor.submit(new StartReceivedHeartbeatTask(serverPort));
        //启动监控控制台服务
        ThreadPoolHolder.bootstrapExecutor.submit(new StartHeartbeatConsoleTask(consolePort));
    }


}

5、创建一个Java Agent代理类(要把这个项目打成一个jar包,例如agent-server.jar文件):

public class Agent {
    //执行main方法前,会执行该参数
    //"-javaagent:D:\Projects\agent-server.jar=这里传参,可以传很多信息参数,服务端口就是通过这里逗号分隔方式传入"
    public static void premain(String arguments, Instrumentation instrumentation) {
        try {
            AgentClassLoader.loadClass(instrumentation);
            new Boostrap(arguments);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

监控平台 - 接入

1、将客户端和服务端module打成jar包;

2、在需要接入的客户端启动JVM参数中加入:

"-javaagent/root/agent-client.jar=这里传参,可以传很多信息参数,服务名称就是通过这里传入"

3、在服务端(服务端含控制台+健康接收存储)启动JVM参数中加入:

"-javaagent/root/agent-server.jar=这里传参,可以传很多信息参数,服务端口就是通过这里逗号分隔方式传入"

监控平台 - 控制台页面效果

1、启动服务端,启动后会看到如下端口日志,主要提取控制台页面的端9550:

6bca164fbcaf79d6d71d8fcca882de4d.png

2、启动客户端:

5beccef8bb2cd48c13b5cdf75b413f10.png

3、浏览器访问控制台地址:http://localhost:9550/,提示需要httpbasic登陆,输入账号:lazy ,密码:lazy123 即可登入:

29bc2b1025909ea70db6ee7a8d4cf0b7.png

4、登陆成功后可以看到监控平台页面效果,页面做的有点粗糙,大家可以按需自行优化页面显示的样式:

ae4c2e74b514f558fbc58491c8405836.png

5、可以尝试多启动几个应用,可以看到列表会不断追加行显示;

完整源码获取可关注【Java软件编程之家】后台回复:lazy-agent 关键字即可。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值