javascript
Spring Boot Admin 集成诊断利器 Arthas 实践
作者 | 阿提說說
來源|阿里巴巴云原生公眾號
前言
Arthas 是 Alibaba 開源的 Java 診斷工具,具有實(shí)時(shí)查看系統(tǒng)的運(yùn)行狀況;查看函數(shù)調(diào)用參數(shù)、返回值和異常;在線熱更新代碼;秒解決類沖突問題;定位類加載路徑;生成熱點(diǎn);通過網(wǎng)頁診斷線上應(yīng)用。如今在各大廠都有廣泛應(yīng)用,也延伸出很多產(chǎn)品。
這里將介紹如何將 Arthas 集成進(jìn) Spring Boot 監(jiān)控平臺中。
SpringBoot Admin
為了方便,SpringBoot Admin 簡稱為 SBA(版本:1.5.x)。
1.5 版本的 SBA 如果要開發(fā)插件比較麻煩,需要下載 SBA 的源碼包,再按照 Spring-boot-admin-server-ui-hystrix的形式 Copy 一份,由于 JS 使用的是 Angular,本人嘗試了很久,雖然掌握了如何開發(fā)插件,奈何不會 Angular,遂放棄💀
版本:2.x 2.x 版本的 SBA 插件開發(fā),官網(wǎng)有介紹如何開發(fā),JS 使用 Vue,方便很多,由于我們項(xiàng)目還在使用 1.5,所以并沒有使用該版本,請讀者自行嘗試。
不能使用 SBA 的插件進(jìn)行集成,那還有什么辦法呢?😅
SBA 集成
鄙人的辦法是將 Arthas 的相關(guān)文件直接 Copy 到 Admin 服務(wù)中,這些文件都來自 Arthas-all 項(xiàng)目 Tunnel-server。
admin 目錄結(jié)構(gòu)
1. Arthas 目錄
該包下存放的是所有 Arthas 的 Java 文件。
- Endpoint 包下的文件可以都注釋掉,沒多大用。
- ArthasController 這個(gè)文件是我自己新建的,用來獲取所有注冊到 Arthas 的客戶端,這在后面是有用的。
- 其他文件直接 Copy 過來就行。
spring-boot-admin-server-ui
該文件建在 Resources.META-INF 下,Admin 會在啟動的時(shí)候加載該目錄下的文件。
2. Resources 目錄
- index.html 覆蓋 SBA 原來的首頁,在其中添加一個(gè) Arthas 導(dǎo)航
- Arthas.html
新建頁面,用于顯示 Arthas 控制臺頁面。
這個(gè)文件中有兩個(gè)隱藏文本域,這兩個(gè)用于連接 Arthas 服務(wù)端,在頁面加載的時(shí)候會自動將 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
- 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
這樣子,admin 端的配置完成了。
客戶端配置
- 在配置中心加入配置
- 需要自動 Attach 的應(yīng)用中引入 Arthas-spring-boot-starter 需要對 Starter 進(jìn)行部分修改,要將注冊 Arthas 的部分移除,下面是修改后的文件。
這里是將修改后的文件重新打包成 Jar 包,上傳到私服,但有些應(yīng)用會有無法加載 ArthasConfigMap 的情況,可以將這兩個(gè)文件單獨(dú)放到項(xiàng)目的公共包中。
@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;} }- 實(shí)現(xiàn)開關(guān)效果
為了實(shí)現(xiàn)開關(guān)效果,還需要一個(gè)文件用來監(jiān)聽配置文件的改變。
我這里使用的是在 SBA 中改變環(huán)境變量,對應(yīng)服務(wù)監(jiān)聽到變量改變,當(dāng)監(jiān)聽 spring.arthas.enabled 為 true 的時(shí)候,注冊 Arthas,到下面是代碼。
@Component public 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;} }結(jié)束
到此可以愉快的在 SBA 中調(diào)式應(yīng)用了,看看最后的頁面。
- 調(diào)式流程
流程如下:
一些缺陷:
- 使用 jar 包的方式引入應(yīng)用,具有一定的侵略性,如果 Arthas 無法啟動,會導(dǎo)致應(yīng)用也無法啟動。
- 如果使用 Docker,需要適當(dāng)調(diào)整 JVM 內(nèi)存,防止開啟 Arthas、調(diào)試的時(shí)候,內(nèi)存炸了。
- 沒有使用 SBA 插件的方式集成如上集成僅供參考,請根據(jù)自己企業(yè)的情況來集成。
Arthas 有獎(jiǎng)?wù)魑恼谶M(jìn)行中!
為了讓更多開發(fā)者開始用上 Arthas 這個(gè) Java 診斷神器,Arthas 社區(qū)聯(lián)合 JetBrains 推出?Arthas 有獎(jiǎng)?wù)魑幕顒?#xff1a;**聊聊這些年你和 Arthas 之間的那些事兒。**活動仍在火熱進(jìn)行中,點(diǎn)擊即可參與,歡迎大家踴躍投稿,參與即有可能獲獎(jiǎng)!
總結(jié)
以上是生活随笔為你收集整理的Spring Boot Admin 集成诊断利器 Arthas 实践的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RocketMQ-Spring 毕业两周
- 下一篇: OpenYurt:延伸原生 Kubern