alibaba / arthas

Alibaba Java Diagnostic Tool Arthas/Alibaba Java诊断利器Arthas
https://arthas.aliyun.com/
Apache License 2.0
35.63k stars 7.49k forks source link

SpringBoot Admin集成Arthas实践 #1601

Closed jujunchen closed 2 years ago

jujunchen commented 3 years ago

前言

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,本人尝试了很久,虽然掌握了如何开发插件,奈何不会Angular,遂放弃💀

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

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

SBA 集成

鄙人的办法是将Arthas的相关文件直接copy到admin服务中,这些文件都来自arthas-all项目tunnel-server

image

arthas目录

该包下存放的是所有arthas的Java文件

spring-boot-admin-server-ui
该文件建在resources.META-INF下,admin会在启动的时候加载该目录下的文件

image

resources目录

<!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>
<!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>

/**

/**

/**

/**

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   
修改了连接部分代码,参考一下
```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}");
        }
    }
}

这样子,admin端的配置完成了

客户端配置

}

```java
@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中调式应用了,看看最后的页面

![image](https://user-images.githubusercontent.com/29937421/180287395-2f1f26d8-9e28-4845-bcee-8b445b14d796.png)

- 调式流程  
开启Arthas
![image](https://user-images.githubusercontent.com/29937421/180287443-ce2de869-8ce0-4d90-937f-455b5c1eb138.png)

在Select Application中选择应用  
Connect 连接应用  
DisConnect 断开应用  
Release 释放配置文件

一些缺陷:
- 使用jar包的方式引入应用,具有一定的侵略性,如果arthas无法启动,会导致应用也无法启动
- 如果使用docker,需要适当调整JVM内存,防止开启arthas、调试的时候,内存炸了
- 没有使用SBA插件的方式集成
- 如上集成仅供参考,请根据自己企业的情况来集成
xingtaoshi commented 3 years ago

代码共享一下如何?

jujunchen commented 3 years ago

代码共享一下如何?

代码都贴上来了

zxdposter commented 3 years ago

@jujunchen 是否兼容最新的spring boot admin 2?

jujunchen commented 3 years ago

@jujunchen 是否兼容最新的spring boot admin 2?

兼容

katana8023 commented 3 years ago

你好,源码有贴上来吗?没找到。。。

password36 commented 3 years ago

已经参考此文档实现了SBA 2.0的兼容,基于arthas3.4.5版本,生产运行几个月,多次解决生产问题。使用了JMX接口对目标进程附加的arthas客户端进行启动并在webconsole中关闭。剩余问题是希望新版本arthas客户端能开放关闭接口,现在通过srping-boot插件无法获取到arthas-agent的classloader以及bootstrap相关类实例句柄,没法关闭,只能在使用完成后在web console关闭目标进程的agent。

hengyunabc commented 3 years ago

已经参考此文档实现了SBA 2.0的兼容,基于arthas3.4.5版本,生产运行几个月,多次解决生产问题。使用了JMX接口对目标进程附加的arthas客户端进行启动并在webconsole中关闭。剩余问题是希望新版本arthas客户端能开放关闭接口,现在通过srping-boot插件无法获取到arthas-agent的classloader以及bootstrap相关类实例句柄,没法关闭,只能在使用完成后在web console关闭目标进程的agent。

欢迎投稿分享实践经验: https://github.com/alibaba/arthas/issues/1079

另外,建议让arthas长驻运行,而不是多次启动。

Xlinlin commented 3 years ago

有没有更详细的代码哇,前端这块按教程集成,没生效。sc admin 2.2.4

Xlinlin commented 3 years ago

/**

Xlinlin commented 3 years ago

各种404,感觉心态要蹦了: Refused to apply style from 'http://localhost:7000/extensions/core.css' because its MIME type ('application/json') is not a supported stylesheet MIME type, and strict MIME checking is enabled. index.html:1 Refused to apply style from 'http://localhost:7000/extensions/all-modules.css' because its MIME type ('application/json') is not a supported stylesheet MIME type, and strict MIME checking is enabled.

unclebryan719 commented 2 years ago

心态蹦了

spilledyear commented 2 years ago

已经参考此文档实现了SBA 2.0的兼容,基于arthas3.4.5版本,生产运行几个月,多次解决生产问题。使用了JMX接口对目标进程附加的arthas客户端进行启动并在webconsole中关闭。剩余问题是希望新版本arthas客户端能开放关闭接口,现在通过srping-boot插件无法获取到arthas-agent的classloader以及bootstrap相关类实例句柄,没法关闭,只能在使用完成后在web console关闭目标进程的agent。

这个额可以详细说一下吗? 通过JMX启动和关闭arthas

jujunchen commented 2 months ago

如果有问题,需要解答,可以@我,要源码包也行

wzh900521 commented 1 week ago

@jujunchen 您好, 可以提供下源码包么