如何将Arthas集成进Spring Boot监控平台中
这篇文章将为大家详细讲解有关如何将Arthas集成进Spring Boot监控平台中,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。
前言
Arthas 是 Alibaba 开源的 Java 诊断工具,具有实时查看系统的运行状况;查看函数调用参数、返回值和异常;在线热更新代码;秒解决类冲突问题;定位类加载路径;生成热点;通过网页诊断线上应用。如今在各大厂都有广泛应用,也延伸出很多产品。
SpringBoot Admin
为了方便,SpringBoot Admin 简称为 SBA(版本:1.5.x)。
1.5 版本的 SBA 如果要开发插件比较麻烦,需要下载 SBA 的源码包,再按照 Spring-boot-admin-server-ui-hystrix的形式 Copy 一份,由于 JS 使用的是 Angular,本人尝试了很久,虽然掌握了如何开发插件,奈何不会 Angular,遂放弃????
版本:2.x 2.x 版本的 SBA 插件开发,官网有介绍如何开发,JS 使用 Vue,方便很多,由于我们项目还在使用 1.5,所以并没有使用该版本,请读者自行尝试。
不能使用 SBA 的插件进行集成,那还有什么办法呢?????
SBA 集成
鄙人的办法是将 Arthas 的相关文件直接 Copy 到 Admin 服务中,这些文件都来自 Arthas-all 项目 Tunnel-server。
admin 目录结构
1. Arthas 目录
该包下存放的是所有 Arthas 的 Java 文件。
Endpoint 包下的文件可以都注释掉,没多大用。
ArthasController 这个文件是我自己新建的,用来获取所有注册到 Arthas 的客户端,这在后面是有用的。
其他文件直接 Copy 过来就行。
@RequestMapping("/api/arthas")@RestControllerpublic class ArthasController { @Autowired private TunnelServer tunnelServer; @RequestMapping(value = "/clients", method = RequestMethod.GET) public SetgetClients() { Map agentInfoMap = tunnelServer.getAgentInfoMap(); return agentInfoMap.keySet(); }}
spring-boot-admin-server-ui
该文件建在 Resources.META-INF 下,Admin 会在启动的时候加载该目录下的文件。
2. Resources 目录
index.html 覆盖 SBA 原来的首页,在其中添加一个 Arthas 导航
Spring Boot Admin
Arthas.html
新建页面,用于显示 Arthas 控制台页面。
这个文件中有两个隐藏文本域,这两个用于连接 Arthas 服务端,在页面加载的时候会自动将 Admin 的 Url 赋值给 Ip。
Spring Boot Admin
Arthas.js 存储页面控制的 js
var registerApplications = null;var applications = null;$(document).ready(function () { reloadRegisterApplications(); reloadApplications();});/** * 获取注册的arthas客户端 */function reloadRegisterApplications() { var result = reqSync("/api/arthas/clients", "get"); registerApplications = result; initSelect("#selectServer", registerApplications, "");}/** * 获取注册的应用 */function reloadApplications() { applications = reqSync("/api/applications", "get"); console.log(applications)}/** * 初始化下拉选择框 */function initSelect(uiSelect, list, key) { $(uiSelect).html(''); var server; for (var i = 0; i < list.length; i++) { server = list[i].toLowerCase().split("@"); if ("phantom-admin" === server[0]) continue; $(uiSelect).append(""); }}/** * 重置配置文件 */function release() { var currentServer = $("#selectServer").text(); for (var i = 0; i < applications.length; i++) { serverId = applications[i].id; serverName = applications[i].name.toLowerCase(); console.log(serverId + "/" + serverName); if (currentServer === serverName) { var result = reqSync("/api/applications/" +serverId+ "/env/reset", "post"); alert("env reset success"); } }}function reqSync(url, method) { var result = null; $.ajax({ url: url, type: method, async: false, //使用同步的方式,true为异步方式 headers: { 'Content-Type': 'application/json;charset=utf8;', }, success: function (data) { // console.log(data); result = data; }, error: function (data) { console.log("error"); } }); return result;}
Web-console.js
修改了连接部分代码,参考一下。
var ws;var xterm;/**有修改**/$(function () { var url = _window.location.href; var ip = getUrlParam('ip'); var port = getUrlParam('port'); var agentId = getUrlParam('agentId'); if (ip != '' && ip != null) { $('#ip').val(ip); } else { $('#ip').val_(window.location.hostname); } if (port != '' && port != null) { $('#port').val(port); } if (agentId != '' && agentId != null) { $('#selectServer').val(agentId); } // startConnect(true);});/** get params in url **/function getUrlParam (name, url) { if (!url) url = _window.location.href; name = name.replace(/[\[\]]/g, '\\$&'); var regex = new RegExp('[?&]' + name + '(=([^]*)|&|#|$)'), results = regex.exec(url); if (!results) return null; if (!results[2]) return ''; return decodeURIComponent(results[2].replace(/\+/g, ' '));}function getCharSize () { var tempDiv = $('').attr({'role': 'listitem'}); var tempSpan = $('').html('qwertyuiopasdfghjklzxcvbnm'); tempDiv.append(tempSpan); $("html body").append(tempDiv); var size = { width: tempSpan.outerWidth() / 26, height: tempSpan.outerHeight(), left: tempDiv.outerWidth() - tempSpan.outerWidth(), top: tempDiv.outerHeight() - tempSpan.outerHeight(), }; tempDiv.remove(); return size;}function getWindowSize () { var e = window; var a = 'inner'; if (!('innerWidth' in window )) { a = 'client'; e = document.documentElement || document.body; } var terminalDiv = document.getElementById("terminal-card"); var terminalDivRect = terminalDiv.getBoundingClientRect(); return { width: terminalDivRect.width, height: e[a + 'Height'] - terminalDivRect.top };}function getTerminalSize () { var charSize = getCharSize(); var windowSize = getWindowSize(); console.log('charsize'); console.log(charSize); console.log('windowSize'); console.log(windowSize); return { cols: Math.floor((windowSize.width - charSize.left) / 10), rows: Math.floor((windowSize.height - charSize.top) / 17) };}/** init websocket **/function initWs (ip, port, agentId) { var protocol= location.protocol === 'https:' ? 'wss://' : 'ws://'; var path = protocol + ip + ':' + port + '/ws?method=connectArthas&id=' + agentId; ws = new WebSocket(path);}/** init xterm **/function initXterm (cols, rows) { xterm = new Terminal({ cols: cols, rows: rows, screenReaderMode: true, rendererType: 'canvas', convertEol: true });}/** 有修改 begin connect **/function startConnect (silent) { var ip = $('#ip').val(); var port = $('#port').val(); var agentId = $('#selectServer').val(); if (ip == '' || port == '') { alert('Ip or port can not be empty'); return; } if (agentId == '') { if (silent) { return; } alert('AgentId can not be empty'); return; } if (ws != null) { alert('Already connected'); return; } // init webSocket initWs(ip, port, agentId); ws.onerror = function () { ws.close(); ws = null; !silent && alert('Connect error'); }; ws.onclose = function (message) { if (message.code === 2000) { alert(message.reason); } }; ws.onopen = function () { console.log('open'); $('#fullSc').show(); var terminalSize = getTerminalSize() console.log('terminalSize') console.log(terminalSize) // init xterm initXterm(terminalSize.cols, terminalSize.rows) ws.onmessage = function (event) { if (event.type === 'message') { var data = event.data; xterm.write(data); } }; xterm.open(document.getElementById('terminal')); xterm.on('data', function (data) { ws.send(JSON.stringify({action: 'read', data: data})) }); ws.send(JSON.stringify({action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows})); window.setInterval(function () { if (ws != null && ws.readyState === 1) { ws.send(JSON.stringify({action: 'read', data: ""})); } }, 30000); }}function disconnect () { try { ws.close(); ws.onmessage = null; ws.onclose = null; ws = null; xterm.destroy(); $('#fullSc').hide(); alert('Connection was closed successfully!'); } catch (e) { alert('No connection, please start connect first.'); }}/** full screen show **/function xtermFullScreen () { var ele = document.getElementById('terminal-card'); requestFullScreen(ele);}function requestFullScreen (element) { var requestMethod = element.requestFullScreen || element.webkitRequestFullScreen || element.mozRequestFullScreen || element.msRequestFullScreen; if (requestMethod) { requestMethod.call(element); } else if (typeof window.ActiveXObject !== "undefined") { var wscript = new ActiveXObject("WScript.Shell"); if (wscript !== null) { wscript.SendKeys("{F11}"); } }}
其他文件
jquery-3.3.1.min.js 新加 Js
copy 过来的 js
popper-1.14.6.min.js
web-console.js
xterm.css
xterm.js
bootstrap.yml
# arthas端口arthas: server: port: 9898
这样子,admin 端的配置完成了。
客户端配置
在配置中心加入配置
#arthas服务端域名arthas.tunnel-server = ws://admin域名/ws#客户端id,应用名@随机值,js会截取前面的应用名arthas.agent-id = ${spring.application.name}@${random.value}#arthas开关,可以在需要调式的时候开启,不需要的时候关闭spring.arthas.enabled = false
需要自动 Attach 的应用中引入 Arthas-spring-boot-starter 需要对 Starter 进行部分修改,要将注册 Arthas 的部分移除,下面是修改后的文件。
这里是将修改后的文件重新打包成 Jar 包,上传到私服,但有些应用会有无法加载 ArthasConfigMap 的情况,可以将这两个文件单独放到项目的公共包中。
@EnableConfigurationProperties({ ArthasProperties.class })public class ArthasConfiguration { private static final Logger logger = LoggerFactory.getLogger(ArthasConfiguration.class); @ConfigurationProperties(prefix = "arthas") @ConditionalOnMissingBean @Bean public HashMaparthasConfigMap() { return new HashMap (); }}
@ConfigurationProperties(prefix = "arthas")public class ArthasProperties { private String ip; private int telnetPort; private int httpPort; private String tunnelServer; private String agentId; /** * report executed command */ private String statUrl; /** * session timeout seconds */ private long sessionTimeout; private String home; /** * when arthas agent init error will throw exception by default. */ private boolean slientInit = false; public String getHome() { return home; } public void setHome(String home) { this.home = home; } public boolean isSlientInit() { return slientInit; } public void setSlientInit(boolean slientInit) { this.slientInit = slientInit; } public String getIp() { return ip; } public void setIp(String ip) { this.ip = ip; } public int getTelnetPort() { return telnetPort; } public void setTelnetPort(int telnetPort) { this.telnetPort = telnetPort; } public int getHttpPort() { return httpPort; } public void setHttpPort(int httpPort) { this.httpPort = httpPort; } public String getTunnelServer() { return tunnelServer; } public void setTunnelServer(String tunnelServer) { this.tunnelServer = tunnelServer; } public String getAgentId() { return agentId; } public void setAgentId(String agentId) { this.agentId = agentId; } public String getStatUrl() { return statUrl; } public void setStatUrl(String statUrl) { this.statUrl = statUrl; } public long getSessionTimeout() { return sessionTimeout; } public void setSessionTimeout(long sessionTimeout) { this.sessionTimeout = sessionTimeout; }}
实现开关效果
为了实现开关效果,还需要一个文件用来监听配置文件的改变。
我这里使用的是在 SBA 中改变环境变量,对应服务监听到变量改变,当监听 spring.arthas.enabled 为 true 的时候,注册 Arthas,到下面是代码。
@Componentpublic class EnvironmentChangeListener implements ApplicationListener{ @Autowired private Environment env; @Autowired private Map arthasConfigMap; @Autowired private ArthasProperties arthasProperties; @Autowired private ApplicationContext applicationContext; @Override public void onApplicationEvent(EnvironmentChangeEvent event) { Set keys = event.getKeys(); for (String key : keys) { if ("spring.arthas.enabled".equals(key)) { if ("true".equals(env.getProperty(key))) { registerArthas(); } } } } private void registerArthas() { DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); String bean = "arthasAgent"; if (defaultListableBeanFactory.containsBean(bean)) { ((ArthasAgent)defaultListableBeanFactory.getBean(bean)).init(); return; } defaultListableBeanFactory.registerSingleton(bean, arthasAgentInit()); } private ArthasAgent arthasAgentInit() { arthasConfigMap = StringUtils.removeDashKey(arthasConfigMap); // 给配置全加上前缀 Map mapWithPrefix = new HashMap (arthasConfigMap.size()); for (Map.Entry entry : arthasConfigMap.entrySet()) { mapWithPrefix.put("arthas." + entry.getKey(), entry.getValue()); } final ArthasAgent arthasAgent = new ArthasAgent(mapWithPrefix, arthasProperties.getHome(), arthasProperties.isSlientInit(), null); arthasAgent.init(); return arthasAgent; }}
结束
到此可以愉快的在 SBA 中调式应用了,看看最后的页面。
调式流程
流程如下:
开启 Arthas
在 Select Application 中选择应用
Connect 连接应用
DisConnect 断开应用
Release 释放配置文件
一些缺陷:
使用 jar 包的方式引入应用,具有一定的侵略性,如果 Arthas 无法启动,会导致应用也无法启动。
如果使用 Docker,需要适当调整 JVM 内存,防止开启 Arthas、调试的时候,内存炸了。
没有使用 SBA 插件的方式集成如上集成仅供参考,请根据自己企业的情况来集成。
关于"如何将Arthas集成进Spring Boot监控平台中"这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,使各位可以学到更多知识,如果觉得文章不错,请把它分享出去让更多的人看到。