Spring Boot Admin 集成诊断利器 Arthas 实践
简介: Arthas 是 Alibaba 开源的 Java 诊断工具,具有实时查看系统的运行状况;查看函数调用参数、返回值和异常;在线热更新代码;秒解决类冲突问题;定位类加载路径;生成热点;通过网页诊断线上应用。如今在各大厂都有广泛应用,也延伸出很多产品。
前言
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")
@RestController
public class ArthasController {
@Autowired
private 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">Reference
Guide</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 License
2.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 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 新加 Jscopy 过来的 jspopper-1.14.6.min.jsweb-console.jsxterm.cssxterm.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 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,到下面是代码。
@Component
public class EnvironmentChangeListener implements ApplicationListener<EnvironmentChangeEvent> {
@Autowired
private Environment env;
@Autowired
private Map<String, String> arthasConfigMap;
@Autowired
private ArthasProperties arthasProperties;
@Autowired
private ApplicationContext applicationContext;
@Override
public 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 插件的方式集成如上集成仅供参考,请根据自己企业的情况来集成。
作者:阿提说说
本文为阿里云原创内容,未经允许不得转载
Spring Boot Admin 集成诊断利器 Arthas 实践的更多相关文章
- Spring Boot Admin简介及实践
问题 在若干年前的单体应用时代,我们可以相对轻松地对整个业务项目进行健康检查.指标监控.配置管理等等项目治理.如今随着微服务的发展,我们将大型单体应用按业务模型进行划分,以此形成众多小而自治的微服务, ...
- spring-boot-plus集成Spring Boot Admin管理和监控应用(十一)
spring-boot-plus集成Spring Boot Admin管理和监控应用 spring boot admin Spring Boot Admin用来管理和监控Spring Boot应用程序 ...
- Spring Boot Admin最佳实践
本文不进行Spring Boot Admin入门知识点说明 在Spring Boot Actuator中提供很多像health.metrics等实时监控接口,可以方便我们随时跟踪服务的性能指标.Spr ...
- spring boot admin项目的集成和开发
Spring Boot Admin是一个Github上的一个开源项目,它在Spring Boot Actuator的基础上提供简洁的可视化WEB UI,是用来管理 Spring Boot 应用程序的一 ...
- spring-boot-plus集成Spring Boot Admin管理和监控应用
Spring Boot Admin Spring Boot Admin用来管理和监控Spring Boot应用程序 应用程序向我们的Spring Boot Admin Client注册(通过HTTP) ...
- Spring Boot Admin 的使用 2
http://blog.csdn.net/kinginblue/article/details/52132113 ******************************************* ...
- Spring Boot admin 2.0 详解
一.什么是Spring Boot Admin ? Spring Boot Admin是一个开源社区项目,用于管理和监控SpringBoot应用程序. 应用程序作为Spring Boot Admin C ...
- Spring boot admin 节点状态一直为DOWN的排查
项目中需要监控各个微服务节点的健康状态,找到了spring boot admin这个全家桶监控工具,它其实是Vue.js美化过的Spring Boot Actuator,官方的解释是: codecen ...
- SpringCloud(8)微服务监控Spring Boot Admin
1.简介 Spring Boot Admin 是一个管理和监控Spring Boot 应用程序的开源软件.Spring Boot Admin 分为 Server 端和 Client 端,Spring ...
- Spring Boot Admin 2.1.0 全攻略
转载请标明出处: https://www.fangzhipeng.com 本文出自方志朋的博客 Spring Boot Admin简介 Spring Boot Admin是一个开源社区项目,用于管理和 ...
随机推荐
- System.out.print重定向到文件实例
该代码可以实现让System.out.print输出内容不再打印到控制台,而是输出到指定的文件中 <strong><span style="font-size:24px;& ...
- JS(简单数据类型、数据类型转换)
一. 数据类型简介 1.1 为什么需要数据类型 在计算机中,不同的数据所需占用的存储空间是不同的,为了便于把数据分成所需内存大小不同的数据,充分利用存储空间,于是定义了不同的数据类型.简单来说,数据类 ...
- 优化您的部署:Docker 镜像最佳实践
介绍 在快速发展的软件开发和部署领域,Docker 已成为容器化的强大工具,为打包.分发和运行应用程序提供了一种标准化的高效方式.Docker 镜像在这一过程中发挥着至关重要的作用,是容器化应用程序的 ...
- 【Leetcode】53. 最大子数组和
题目(链接) 给你一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和. 子数组是数组中的一个连续部分. 示例 1: 输入:nums = [-2,1,-3, ...
- 记录-JS 基础知识大全
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 1.通过javascript向文档中输出文本 document是javascript的内置对象,代表浏览器的文档部分 document.w ...
- 一文讲透Java核心技术之高可扩展利器SPI
大家好,我是冰河~~ SPI的概念 JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制 SPI的使用场景 Java是一种面向对象语言,虽然Java8开始支持函数式编程和Strea ...
- C#词云生成器
GitHub - sdcb/Sdcb.WordCloud: Generate WordCloud image from .NET/.NET Core 效果 根据mask生成
- Python---json数据格式详解
什么是JSON JSON是一种取代XML的数据结构,和xml相比,它更小巧但描述能力却不差,由于它的小巧所以网络传输数据将减少更多流量从而加快速度. 那么,JSON到底是什么? JSON就是一串字符串 ...
- 下载标准国民经济行业分类与代码GB/T 4754-2011,存入mysql数据库
戳链接下载:https://download.csdn.net/download/weixin_45556024/34913490 或关注公众号[靠谱杨阅读人生]回复[行业]获取. 整理不易,资源fu ...
- #线段树#洛谷 4681 [THUSC2015]平方运算
题目 给定一个数列 \(a\),维护以下两种操作 区间取平方 \(a[i]=a[i]^2\bmod p\) 区间和(不取模) \(p\) 为给定的小于 \(10^4\) 的数,\(n\leq 10^5 ...