先创建一个maven项目,分别创建一个客户端探针module和一个服务端module,项目结构参考如下:
监控平台 - 客户端源码
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:
2、启动客户端:
3、浏览器访问控制台地址:http://localhost:9550/,提示需要httpbasic登陆,输入账号:lazy ,密码:lazy123 即可登入:
4、登陆成功后可以看到监控平台页面效果,页面做的有点粗糙,大家可以按需自行优化页面显示的样式:
5、可以尝试多启动几个应用,可以看到列表会不断追加行显示;
完整源码获取可关注【Java软件编程之家】后台回复:lazy-agent 关键字即可。