Spring Boot Admin 集成诊断利器 Arthas 实践
作者 | 阿提说说
来源|阿里巴巴云原生公众号
前言
Arthas 是 Alibaba 开源的 Java 诊断工具,具有实时查看系统的运行状况;查看函数调用参数、返回值和异常;在线热更新代码;秒解决类冲突问题;定位类加载路径;生成热点;通过网页诊断线上应用。如今在各大厂都有广泛应用,也延伸出很多产品。
这里将介绍如何将 Arthas 集成进 Spring Boot 监控平台中。
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 {@Autowiredprivate TunnelServer tunnelServer;@RequestMapping(value = "/clients", method = RequestMethod.GET)public Set<String> getClients() {Map<String, AgentInfo> agentInfoMap = tunnelServer.getAgentInfoMap();return agentInfoMap.keySet();}}
spring-boot-admin-server-ui
该文件建在 Resources.META-INF 下,Admin 会在启动的时候加载该目录下的文件。
2. Resources 目录
index.html 覆盖 SBA 原来的首页,在其中添加一个 Arthas 导航
<!DOCTYPE html><html class="no-js"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>Spring Boot Admin</title><meta name="description" content=""><meta name="viewport" content="width=device-width"><link rel="shortcut icon" type="image/x-icon" href="img/favicon.png"/><link rel="stylesheet" type="text/css" href="core.css"/><link rel="stylesheet" type="text/css" href="all-modules.css"/></head><body><header class="navbar header--navbar desktop-only"><div class="navbar-inner"><div class="container-fluid"><div class="spring-logo--container"><a class="spring-logo" href="#"><span></span></a></div><div class="spring-logo--container"><a class="spring-boot-logo" href="#"><span></span></a></div><ul class="nav pull-right"><!--增加Arthas导航--><li class="navbar-link ng-scope"><a class="ng-binding" href="arthas/arthas.html">Arthas</a></li><li ng-repeat="view in mainViews" class="navbar-link" ng-class="{active: $state.includes(view.state)}"><a ui-sref="{{view.state}}" ng-bind-html="view.title"></a></li></ul></div></div></header><div ui-view></div><footer class="footer"><ul class="inline"><li><a href="https://codecentric.github.io/spring-boot-admin/@project.version@" target="_blank">ReferenceGuide</a></li><li>-</li><li><a href="https://github.com/codecentric/spring-boot-admin" target="_blank">Sources</a></li><li>-</li><li>Code licensed under <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank">Apache License2.0</a></li></ul></footer><script src="dependencies.js" type="text/javascript"></script><script type="text/javascript">sbaModules = [];</script><script src="core.js" type="text/javascript"></script><script src="all-modules.js" type="text/javascript"></script><script type="text/javascript">angular.element(document).ready(function () {angular.bootstrap(document, sbaModules.slice(0), {strictDi: true});});</script></body></html>
Arthas.html
新建页面,用于显示 Arthas 控制台页面。
这个文件中有两个隐藏文本域,这两个用于连接 Arthas 服务端,在页面加载的时候会自动将 Admin 的 Url 赋值给 Ip。
<input type="hidden" id="ip" name="ip" value="127.0.0.1"><input type="hidden" id="port" name="port" value="19898">
<!DOCTYPE html><html class="no-js"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>Spring Boot Admin</title><meta name="description" content=""><meta name="viewport" content="width=device-width"><link rel="shortcut icon" type="image/x-icon" href="../img/favicon.png"/><link rel="stylesheet" type="text/css" href="../core.css"/><link rel="stylesheet" type="text/css" href="../all-modules.css"/><script src="js/jquery-3.3.1.min.js"></script><script src="js/popper-1.14.6.min.js"></script><script src="js/xterm.js"></script><script src="js/web-console.js"></script><script src="js/arthas.js"></script><link href="js/xterm.css" rel="stylesheet" /><script type="text/javascript">window.addEventListener('resize', function () {var terminalSize = getTerminalSize();ws.send(JSON.stringify({ action: 'resize', cols: terminalSize.cols, rows: terminalSize.rows }));xterm.resize(terminalSize.cols, terminalSize.rows);});</script></head><body><header class="navbar header--navbar desktop-only"><div class="navbar-inner"><div class="container-fluid"><div class="spring-logo--container"><a class="spring-logo" href="#"><span></span></a></div><div class="spring-logo--container"><a class="spring-boot-logo" href="#"><span></span></a></div><ul class="nav pull-right"><li class="navbar-link ng-scope"><a class="ng-binding" href="arthas.html">Arthas</a></li><li class="navbar-link ng-scope"><a class="ng-binding" href="../">Applications</a></li><li class="navbar-link ng-scope"><a class="ng-binding" href="../#/turbine">Turbine</a></li><li class="navbar-link ng-scope"><a class="ng-binding" href="../#/events">Journal</a></li><li class="navbar-link ng-scope"><a class="ng-binding" href="../#/about">About</a></li><li class="navbar-link ng-scope"><a class="ng-binding" href="../#/logout"><i class="fa fa-2x fa-sign-out" aria-hidden="true"></i></a></li></ul></div></div></header><div ui-view><div class="container-fluid"><form class="form-inline"><input type="hidden" id="ip" name="ip" value="127.0.0.1"><input type="hidden" id="port" name="port" value="19898">Select Application:<select id="selectServer"></select><button class="btn" onclick="startConnect()" type="button"><i class="fa fa-connectdevelop"></i> Connect</button><button class="btn" onclick="disconnect()" type="button"><i class="fa fa-search-minus"></i> Disconnect</button><button class="btn" onclick="release()" type="button"><i class="fa fa-search-minus"></i> Release</button></form><div id="terminal-card"><div id="terminal"></div></div></div></div></body></html>
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("<option value=" + list[i].toLowerCase() + ">" + server[0] + "</option>");}}/*** 重置配置文件*/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 = $('<div />').attr({'role': 'listitem'});var tempSpan = $('<div />').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 webSocketinitWs(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 xterminitXterm(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@Beanpublic HashMap<String, String> arthasConfigMap() {return new HashMap<String, String>();}}
@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<EnvironmentChangeEvent> {@Autowiredprivate Environment env;@Autowiredprivate Map<String, String> arthasConfigMap;@Autowiredprivate ArthasProperties arthasProperties;@Autowiredprivate ApplicationContext applicationContext;@Overridepublic void onApplicationEvent(EnvironmentChangeEvent event) {Set<String> 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<String, String> mapWithPrefix = new HashMap<String, String>(arthasConfigMap.size());for (Map.Entry<String, String> 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 有奖征文正在进行中!
为了让更多开发者开始用上 Arthas 这个 Java 诊断神器,Arthas 社区联合 JetBrains 推出 Arthas 有奖征文活动:聊聊这些年你和 Arthas 之间的那些事儿。活动仍在火热进行中,[点击即可参与](http://alibabacloud.mikecrm.com/9khcRrs),欢迎大家踊跃投稿,参与即有可能获奖!
版权声明: 本文为 InfoQ 作者【阿里巴巴云原生】的原创文章。
原文链接:【http://xie.infoq.cn/article/4c4c8c3539ad898c6a4ab3a9d】。文章转载请联系作者。
阿里巴巴云原生
阿里巴巴云原生 2019.05.21 加入
还未添加个人简介











评论