11

SpringBoot Admin集成诊断利器Arthas实践

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU2MTY2MjE4OQ%3D%3D&%3Bmid=2247483988&%3Bidx=1&%3Bsn=cfb6ae739e7d67322aa6267ae5ad38ac
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

Arthas 是 Alibaba开源的Java诊断工具,具有实时查看系统的运行状况,查看函数调用参数、返回值和异常,在线热更新代码,秒解决类冲突问题、定位类加载路径,生成热点图,通过网页诊断线上应用。  如今在各大厂都有广泛应用,也延伸出很多产品。

这里将介绍如何将Arthas集成进SpringBoot监控平台中。

SpringBoot Admin

为了方便SpringBoot Admin 简称为SBA

版本:1.5.x

1.5版本的SBA如果要开发插件比较麻烦,需要下载SBA的源码包,再按照spring-boot-admin-server-ui-hystrix的形式copy一份,由于JS使用的是Angular,本人放弃了

VjYzuqY.png!mobile

版本:2.x 2.x版本的SBA插件开发,官网有介绍如何开发,JS使用Vue,方便很多,由于我们项目还在使用1.5,所以并没有使用该版本,请读者自行尝试

不能使用SBA的插件进行集成,那还有什么办法呢?:sweat_smile:

SBA 集成

鄙人的办法是将Arthas的相关文件直接copy到admin服务中

263EfeV.png!mobile

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会在启动的时候加载该目录下的文件

R3QbI3y.png!mobile
  • index.html 覆盖SBA原来的首页,在其中添加一个导航,首页会是这样

<!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控制台页面

<!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;
}
  • 其他文件 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 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中调试应用了,看看最后的页面

VnA7FnJ.png!mobile
  • 调试流程

    开启Arthas FZv2ymY.png!mobile

在Select Application中选择应用

Connect 连接应用

DisConnect 断开应用

Release 释放配置文件

这种集成方式有一些缺陷:

  • 使用jar包的方式引入应用,具有一定的侵略性,如果arthas无法启动,会导致应用也无法启动

  • 如果使用docker,需要适当调整JVM内存,防止开启arthas的时候,内存炸了

  • 没有使用SBA插件的方式集成

  • 如上集成仅供参考,还是需要根据自己企业的情况来做

作者:阿提说说,绿城理想生活Java工程师

那些年,用Arthas排查过的问题

欢迎大家在“ 原文链接 ”里投稿,有丰富的奖品等你噢。

  • 凡提交满足投稿要求文章的同学,将获得 Arthas Most Valuable User 福袋一份(礼品随机),包含淘公仔、Arthas 贴纸、阿里云 T 恤、JetBrains 周边礼包;

  • 第一期最受欢迎的 top3 文章,获得天猫精灵一台;

  • 年度 top 20 文章,将有机会获得 cherry 键盘及 JetBrains 提供的包括 Coupon 等周边礼包 。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK