zoukankan      html  css  js  c++  java
  • 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 = {
             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 {
             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 中调式应用了,看看最后的页面。

    • 调式流程

    流程如下:

    1. 开启 Arthas
    2. 在 Select Application 中选择应用
    3. Connect 连接应用
    4. DisConnect 断开应用
    5. Release 释放配置文件

    一些缺陷:

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

    作者:阿提说说

    原文链接 

    本文为阿里云原创内容,未经允许不得转载

  • 相关阅读:
    hdu 1754 I Hate It(线段树水题)
    hdu 1166敌兵布阵(线段树入门题)
    多校1007 Naive Operations
    51NOD 1277 字符串中的最大值(KMP)
    括号匹配
    Visual Studio中定义OVERFLOW不能用
    数据结构第二章小节
    关键字new与malloc函数
    构造函数不能为虚/重载函数总结
    预处理之宏定义
  • 原文地址:https://www.cnblogs.com/yunqishequ/p/14391919.html
Copyright © 2011-2022 走看看