JMX
Java 平台提供了全面的 JVM 监控和管理措施。
在 Java SE 5 之前,虽然 JVM 提供了一些底层的 API,比如 JVMPI 和 JVMTI,但这些 API 都是面向 C 语言的,需要通过 JNI 等方式才能调用,想要监控 JVM 和系统资源非常不方便。
Java SE 5.0 版本引入了 JMX 技术(Java Management Extensions,Java 管理扩展),JMX 技术的前身是“JSR3:Java Management Extensions”,以及“JSR 160:JMX Remote API”。
JMX 是用于监控和管理 JVM 资源(包括应用程序、设备、服务和 JVM)的一组标准 API。
通过这些 API 接口,可以对外暴露 JVM 和宿主机的一些信息,甚至支持远程动态调整某些运行时参数。
JMX 技术让我们在 JDK 中开发自检程序成为可能,同时也提供了很多轻量级的 API 来监测 JVM 状态和运行中对象/线程状态,从而提高了 Java 语言自身的管理监测能力。
客户端使用 JMX 主要通过两种方式:
- 程序代码手动获取 MXBean;
- 通过网络远程获取 MBean。
从 JVM 运行时获取 GC 行为数据,最简单的办法是使用标准 JMX API 接口。JMX 也是获取 JVM 内部运行时状态信息 的标准 API。可以编写程序代码,通过 JMX API 来访问本程序所在的 JVM,也可以通过 JMX 客户端执行(远程)访问。MXBean 可用于监控和管理 JVM,每个 MXBean 都封装了一部分功能。
如果用通俗的话来讲,就是我们可以在 JVM 这个机构内部搞一个“政务信息公开系统”,这个东西就可以看做是 MBeanServer,然后系统默认有很多信息,比如 JVM 的基本信息、内存和 GC 的信息等,可以放到这个系统来公开。应用程序里的其他 Java 代码也可以自己定义一些 MBean,然后把这些自己想要公开的信息挂到这个系统里来。这个时候,就不管是本 JVM 内部,还是其他的 Java 应用程序,都可以访问到这个 MBeanServer 上的所有公开信息,也就是 MBean 的属性,甚至可以直接调用 MBean 提供的方法反过来影响系统。
获取当前 JVM 的 MXBean 信息
JDK 默认提供的 MXBean 相关类,主要位于 rt.jar 文件的 java.lang.management 包中。获取 JVM 中 MXBean 信息的代码示例如下:
public class MXBeanTest { public static void main(String[] args) { Map<String, Object> beansMap = loadMXBeanMap(); String jsonString = toJSON(beansMap); System.out.println(jsonString); } public static Map<String, Object> loadMXBeanMap() { // import java.lang.management.* // 1. 操作系统信息 OperatingSystemMXBean operatingSystemMXBean = ManagementFactory.getOperatingSystemMXBean(); // 2. 运行时 RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean(); // 3.1 JVM 内存信息 MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); // 3.2 JVM 内存池-列表 List<MemoryPoolMXBean> memoryPoolMXBeans = ManagementFactory.getMemoryPoolMXBeans(); // 3.3 内存管理器-列表 List<MemoryManagerMXBean> memoryManagerMXBeans = ManagementFactory.getMemoryManagerMXBeans(); // 4. class 加载统计信息 ClassLoadingMXBean classLoadingMXBean = ManagementFactory.getClassLoadingMXBean(); // 5. 编译统计信息 CompilationMXBean compilationMXBean = ManagementFactory.getCompilationMXBean(); // 6. 线程 ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 7. GC List<GarbageCollectorMXBean> garbageCollectorMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); // 8. 获取平台日志 MXBean PlatformLoggingMXBean platformLoggingMXBean = ManagementFactory.getPlatformMXBean(PlatformLoggingMXBean.class); // Map<String, Object> beansMap = new HashMap<String, Object>(); // beansMap.put("operatingSystemMXBean", operatingSystemMXBean); beansMap.put("runtimeMXBean", runtimeMXBean); beansMap.put("memoryMXBean", memoryMXBean); beansMap.put("memoryPoolMXBeans", memoryPoolMXBeans); beansMap.put("memoryManagerMXBeans", memoryManagerMXBeans); beansMap.put("classLoadingMXBean", classLoadingMXBean); beansMap.put("compilationMXBean", compilationMXBean); beansMap.put("threadMXBean", threadMXBean); beansMap.put("garbageCollectorMXBeans", garbageCollectorMXBeans); beansMap.put("platformLoggingMXBean", platformLoggingMXBean); return beansMap; } public static String toJSON(Object obj) { // MemoryPoolMXBean 这些未设置的属性序列化时会报错 SimplePropertyPreFilter filter = new SimplePropertyPreFilter(); filter.getExcludes().add("collectionUsageThreshold"); filter.getExcludes().add("collectionUsageThresholdCount"); filter.getExcludes().add("collectionUsageThresholdExceeded"); filter.getExcludes().add("usageThreshold"); filter.getExcludes().add("usageThresholdCount"); filter.getExcludes().add("usageThresholdExceeded"); // String jsonString = JSON.toJSONString(obj, filter, SerializerFeature.PrettyFormat); return jsonString; } }
取得这些 MXBean 之后,就能采集到对应的 Java 运行时信息,定时上报给某个系统,那么一个简单的监控就创建了。
当然,这么简单的事情,肯定有现成的轮子啦。比如 Spring Boot Actuator,以及后面介绍的 Micrometer 等。各种监控服务提供的 Agent-lib 中也会通过类似的手段采集相应的数据。
使用 JMX 工具远程连接
最常见的 JMX 客户端是 JConsole 和 JVisualVM(可以安装各种插件,十分强大)。两个工具都是标准 JDK 的一部分,而且很容易使用. 如果使用的是 JDK 7u40 及更高版本,还可以使用另一个工具:Java Mission Control(JMC,大致翻译为 Java 控制中心)。
监控本地 JVM 并不需要额外配置,如果是远程监控,还可以在服务端部署 Jstatd 服务暴露部分信息给 JMX 客户端。
所有 JMX 客户端都是独立的程序,可以连接到目标 JVM 上。目标 JVM 可以在本机,也可以是远端 JVM。
想要支持 JMX 客户端连接服务端 JVM 实例,则 Java 启动脚本中需要加上相关的配置参数,示例如下:
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false
如果服务器具有多张网卡(多个 IP),由于安全限制,必须明确指定 hostname, 一般是 IP。
-Djava.rmi.server.hostname=192.168.211.132
我使用的例子是tomcat,打开 catalina.sh 文件,在文件开头加上以下配置:
JAVA_OPTS="-server -Xms1024m -Xmx8192m -XX:PermSize=256M -XX:MaxPermSize=1024m -Dfile.encoding=utf-8 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1099 -Djava.rmi.server.hostname=192.168.211.132 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
然后执行以下命令 查看hostname
hostname -i
若显示以下
::1 127.0.0.1
则需要修改/etc/hosts,添加IP地址,增加 192.168.211.132 localhost。
配置好结果如下:
[root@localhost bin]# hostname -i ::1 127.0.0.1 192.168.211.132
这样启动之后,JMX 客户端(如 JConsole、JVisualVM、JMC)就可以通过 <IP:端口>
连接。(参考 JVisualVM 的示例)。
如这里对应的就类似于:192.168.211.132:1099。
如果想要远程查看 VisualGC,则服务端需要开启 Jstatd 来支持,JVisualVM 先连 Jstatd 远程主机,接着在远程主机上点右键添加 JMX 连接。关于 JVisualVM 的使用,请参考前面的文章JDK 内置图形界面工具。
以 JConsole 为例,我们看一下,连接到了远程 JVM 以后,在最后一个面板即可看到 MBean 信息。
例如,我们可以查看 JVM 的一些信息:
也可以直接调用方法,例如查看 VM 参数:
因为我们启动的进程是 Tomcat ,所以我们还可以看到很多 Tomcat 的信息:
JMX 的 MBean 创建和远程访问
前面讲了在同一个 JVM 里获取 MBean,现在我们再来写一个更完整的例子:创建一个 MBean,然后远程访问它。
先定义一个 UserMBean 接口(必须以 MBean 作为后缀):
public interface UserMBean { Long getUserId(); String getUserName(); void setUserId(Long userId); void setUserName(String userName); }
然后实现它:
public class User implements UserMBean { Long userId = 12345678L; String userName = "jvm-user"; @Override public Long getUserId() { return userId; } @Override public String getUserName() { return userName; } @Override public void setUserId(Long userId) { this.userId = userId; } @Override public void setUserName(String userName) { this.userName = userName; } }
最后实现一个类来启动 MBeanServer:
public class UserJmxServer { public static void main(String[] args){ MBeanServer server; User bean=new User(); try { int rmiPort = 1099; String jmxServerName = "TestJMXServer"; Registry registry = LocateRegistry.createRegistry(rmiPort); server = MBeanServerFactory.createMBeanServer("user"); ObjectName objectName = new ObjectName("user:name=User"); server.registerMBean(bean, objectName); JMXServiceURL url = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:1099/user"); System.out.println("JMXServiceURL: " + url.toString()); JMXConnectorServer jmxConnServer = JMXConnectorServerFactory.newJMXConnectorServer(url, null, server); jmxConnServer.start(); }catch (Exception e){ e.printStackTrace(); } } }
通过这几个代码我们可以看到,使用 MBean 机制,需要:
- 先定义 MBean 接口;
- 实现这个接口;
- 然后把接口和类,注册到 MBeanServer,这里可以用 JVM 里的默认 MBeanServer,也可以自己创建一个新的 Server,这里为了简单,就使用了默认的。
然后我们就可以使用客户端工具或者代码来访问 MBeanServer,查看和操作 MBean,由于 MBean 类似反射的机制,客户端不需要知道具体的 MBean 接口或者实现类,也能请求服务器端。
如果学习过 Apache Dubbo,就知道在 Dubbo 里消费端必须拿到服务提供者的服务接口,才能配置和调用,这里不同的地方就是客户端是不需要 MBean 接口的。
JConsole 里查看自定义 MBean
首先我们启动这个应用 UserJmxServer,接下来我们使用工具来查看和操作它。
打开 JConsole,在远程输入:
service:jmx:rmi:///jndi/rmi://localhost:1099/user
查看User的属性:
直接修改 UserName 的值:
使用 JMX 远程访问 MBean
我们先使用 JMXUrl 来创建一个 MBeanServerConnection,连接到 MBeanServer,然后就可以通过 ObjectName,也可以看做是 MBean 的地址,像反射一样去拿服务器端 MBean 里的属性,或者调用 MBean 的方法。示例如下:
public class UserJmxClient { public static void main(String[] args){ try { String surl = "service:jmx:rmi:///jndi/rmi://localhost:1099/user"; JMXServiceURL url = new JMXServiceURL(surl); JMXConnector jmxc = JMXConnectorFactory.connect(url, null); MBeanServerConnection mbsc = jmxc.getMBeanServerConnection(); System.out.println("Domains:---------------"); String domains[] = mbsc.getDomains(); for (int i = 0; i < domains.length; i++) { System.out.println("\tDomain[" + i + "] = " + domains[i]); } System.out.println("all ObjectName:---------------"); Set<ObjectInstance> set = mbsc.queryMBeans(null, null); for (Iterator<ObjectInstance> it = set.iterator(); it.hasNext();) { ObjectInstance objectInstance = (ObjectInstance) it.next(); System.out.println("\t" + objectInstance.getObjectName() + " => " + objectInstance.getClassName()); } System.out.println("user:name=User:---------------"); ObjectName mbeanName = new ObjectName("user:name=User"); MBeanInfo info = mbsc.getMBeanInfo(mbeanName); System.out.println("Class: " + info.getClassName()); if (info.getAttributes().length > 0){ for(MBeanAttributeInfo m : info.getAttributes()) System.out.println("\t ==> Attriber:" + m.getName()); } if (info.getOperations().length > 0){ for(MBeanOperationInfo m : info.getOperations()) System.out.println("\t ==> Operation:" + m.getName()); } System.out.println("Testing userName and userId ......."); Object userNameObj = mbsc.getAttribute(mbeanName,"UserName"); System.out.println("\t ==> userName:" + userNameObj); Object userIdObj = mbsc.getAttribute(mbeanName,"UserId"); System.out.println("\t ==> userId:" + userIdObj); Attribute userNameAttr = new Attribute("UserName","kimmking"); mbsc.setAttribute(mbeanName,userNameAttr); System.out.println("Modify UserName ......."); userNameObj = mbsc.getAttribute(mbeanName,"UserName"); System.out.println("\t ==> userName:" + userNameObj); jmxc.close(); }catch (Exception e){ e.printStackTrace(); } } }
直接运行,输出如下:
Domains:--------------- Domain[0] = JMImplementation Domain[1] = user all ObjectName:--------------- JMImplementation:type=MBeanServerDelegate => javax.management.MBeanServerDelegate user:name=User => com.fhj.jvm.User user:name=User:--------------- Class: com.fhj.jvm.User ==> Attriber:UserId ==> Attriber:UserName Testing userName and userId ....... ==> userName:test-user ==> userId:12345678 Modify UserName ....... ==> userName:kimmking
在前面的 JConsole 示例中,我们可以看到 JMX 的 MBeanServer 里的所有 MBean 就是一个树结构,那么怎么定位一个 MBean 对象,就是靠它的地址,ObjectName 属性,例如例子里的 user:name=User
。ObjectName 跟 LDAP 里定位的 DN 非常像,可以直接在客户端拿到一个服务端实际对象的代理对象。然后进行操作:
- queryMBeans:查询当前 Server 的所有 MBean 对象,进而可以拿到每个 MBean 内的 MBeanInfo 信息,有什么属性和方法。
- getAttribute:从 Server 上拿到某个 MBean 对象的某个属性值。
- setAttribute:设置 Server 上的某个 MBean 的某个属性值。
- invoke:调用 Server 上某个 MBean 的某个方法。
从上面的分析,我们可以看到,JMX 其实是基于 MBean 和 MBeanServer 模型、RMI 协议,在设计上非常精巧的远程调用技术。通过学习这个技术的细节,我们可以了解一般的 RPC 等技术。学会了这种 JVM 默认的管理 API 技术,我们也可以更方便的了解和分析 JVM 情况。
更多用法
JMX 是基于 RMI 的 JVM 管理技术,底层是 Java 平台专有的 RMI 远程方法调用,很难做到跨语言调用。怎么才能做到跨平台呢?现在最火的远程调用方式非 REST 莫属。能否让 JMX 使用 REST API 直接调用呢?答案是肯定的。
另外,想要进行性能分析,只有 JVM 的信息还是不够的,我们还需要跟其他的各类监控集成,比如 Datadog 或是其他 APM。
JMX 与 REST API
先说一下 JMX 的 REST API,有一个框架 Jolokia,它可以自动把 JMX 的结构转换成 REST API 和 JSON 数据。在开源软件 ActiveMQ 的控制台里就默认使用了这个框架,这样可以直接达到如下效果。
我们使用 curl 手工执行一次 REST 调用,会直接返回给我们 API 的 JSON 结果。
$ curl http://localhost:8161/hawtio/jolokia/read/org.apache.activemq:brokerName=localhost,type=Broker/Queues {"timestamp":1392110578,"status":200,"request":{"mbean":"org.apache.activemq:brokerName=localhost,type=Broker","attribute":"Queues","type":"read"},"value":[{"objectName":"org.apache.activemq:brokerName=localhost,destinationName=a,destinationType=Queue,type=Broker"}]}
JMX 与其他软件
JConsole 及 JVisualVM 等工具提供了实时查看的能力,但如果我们想监控大量 JVM 实例的历史数据,应该怎么办呢?
既然 JMX 提供了这些数据,只要我们有一个工具来定时采集,并上报给对应的 APM 收集系统,那么我们就保存了长期的历史数据,作为进一步分析和性能诊断的依据。例如 DataDog,听云等服务提供商都集成了对 JMX 的支持。
Jolokia
JMX 的客户端 API 使用起来非常的不方便,Jolokia 就是一个将 JMX 转换成 HTTP 的适配器,方便了 JMX 的使用。
Jokokia 可以通过 jar 包和 agent 的方式启动,在一些框架中,比如 Spring Boot 中,很容易进行集成。访问 http://start.spring.io,生成一个普通的 Spring Boot 项目。
直接在 pom 文件里加入 jolokia 的依赖。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.jolokia</groupId> <artifactId>jolokia-core</artifactId> </dependency>
在 application.properties 中简单地加入一点配置,就可以通过 HTTP 接口访问 JMX 的内容了。
management.endpoints.web.exposure.include=jolokia
server.port=8084
spring.profiles.active=dev
logging.level.root=info
接下来,我们将收集这个项目的 JMX 数据。
仓库地址:https://github.com/xiaojiesir/Jolokia
JVM 监控搭建
我们先简单看一下 JVM 监控的整体架构图:
JVM 的各种内存信息,会通过 JMX 接口进行暴露;Jolokia 组件负责把 JMX 信息翻译成容易读取的 HTTP 请求。
telegraf 组件作为一个通用的监控 agent,和 JVM 进程部署在同一台机器上,通过访问转化后的 HTTP 接口,以固定的频率拉取监控信息;然后把这些信息存放到 influxdb 时序数据库中;最后,通过高颜值的 Grafana 展示组件,设计 JVM 监控图表。
整个监控组件是可以热拔插的,并不会影响原有服务。监控部分也是可以复用的,比如 telegraf 就可以很容易的进行操作系统监控。
influxdb
influxdb 是一个性能和压缩比非常高的时序数据库,在中小型公司非常流行,点击这里可获取 influxdb。
在 CentOS 环境中,可以使用下面的命令下载。
wget -c https://dl.influxdata.com/influxdb/releases/influxdb-1.7.9_linux_amd64.tar.gz tar xvfz influxdb-1.7.9_linux_amd64.tar.gz
解压后,进入bin目录,然后启动influxdb服务器
cd influxdb-1.7.9-1/usr/bin/
./influxd -config ../../etc/influxdb/influxdb.conf
启动influxdb客户端
./influx
InfluxDB 将在 8086 端口进行监听。
Telegraf
Telegraf 是一个监控数据收集工具,支持非常丰富的监控类型,其中就包含内置的 Jolokia 收集器。
接下来,下载并安装 Telegraf:
wget -c https://dl.influxdata.com/telegraf/releases/telegraf-1.13.1-1.x86_64.rpm sudo yum localinstall telegraf-1.13.1-1.x86_64.rpm
Telegraf 通过 jolokia 配置收集数据相对简单,比如下面就是收集堆内存使用状况的一段配置。
[[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "Memory_" mbean = "java.lang:type=Memory" paths = ["HeapMemoryUsage", "NonHeapMemoryUsage", "ObjectPendingFinalizationCount"]
设计这个配置文件的主要难点在于对 JVM 各个内存分区的理解。将以下两个文件,复制到 /etc/telegraf/telegraf.d/ 目录下面,然后执行 systemctl restart telegraf 重启 telegraf。
jvm.conf
[[inputs.jolokia2_agent]] urls = ["http://127.0.0.1:8080/actuator/jolokia/list"] [inputs.jolokia2_agent.tags] rpc="monitor-demo" [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "ClassLoading_" mbean = "java.lang:type=ClassLoading" paths = ["LoadedClassCount", "TotalLoadedClassCount", "UnloadedClassCount"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "Compilation_" mbean = "java.lang:type=Compilation" paths = ["TotalCompilationTime"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "GarbageCollector_ParNew_" mbean = "java.lang:name=ParNew,type=GarbageCollector" paths = ["CollectionCount", "CollectionTime"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "GarbageCollector_ConcurrentMarkSweep_" mbean = "java.lang:name=ConcurrentMarkSweep,type=GarbageCollector" paths = ["CollectionCount", "CollectionTime"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "GarbageCollector_PS_Scavenge_" mbean = "java.lang:type=GarbageCollector,name=PS Scavenge" paths = ["CollectionCount", "CollectionTime"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "GarbageCollector_PS_MarkSweep_" mbean = "java.lang:type=GarbageCollector,name=PS MarkSweep" paths = ["CollectionCount", "CollectionTime"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "Memory_" mbean = "java.lang:type=Memory" paths = ["HeapMemoryUsage", "NonHeapMemoryUsage", "ObjectPendingFinalizationCount"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_CMS_Old_Gen_" mbean = "java.lang:type=MemoryPool,name=CMS Old Gen" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_Code_Cache_" mbean = "java.lang:type=MemoryPool,name=Code Cache" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_Compressed_Class_Space_" mbean = "java.lang:type=MemoryPool,name=Compressed Class Space" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_Metaspace_" mbean = "java.lang:type=MemoryPool,name=Metaspace" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_Par_Eden_Space_" mbean = "java.lang:type=MemoryPool,name=Par Eden Space" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_Par_Survivor_Space_" mbean = "java.lang:type=MemoryPool,name=Par Survivor Space" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_PS_Eden_Space_" mbean = "java.lang:type=MemoryPool,name=PS Eden Space" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_PS_Old_Gen_" mbean = "java.lang:type=MemoryPool,name=PS Old Gen" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "MemoryPool_PS_Survivor_Space_" mbean = "java.lang:type=MemoryPool,name=PS Survivor Space" paths = ["CollectionUsage", "PeakUsage", "Usage"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "OperatingSystem_" mbean = "java.lang:type=OperatingSystem" paths = ["AvailableProcessors", "CommittedVirtualMemorySize", "FreePhysicalMemorySize", "FreeSwapSpaceSize", "MaxFileDescriptorCount", "OpenFileDescriptorCount", "ProcessCpuLoad", "ProcessCpuTime", "SystemCpuLoad", "SystemLoadAverage", "TotalPhysicalMemorySize", "TotalSwapSpaceSize"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "Runtime_" mbean = "java.lang:type=Runtime" paths = ["StartTime", "Uptime"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "Threading_" mbean = "java.lang:type=Threading" paths = ["CurrentThreadCpuTime", "CurrentThreadUserTime", "DaemonThreadCount", "PeakThreadCount", "ThreadCount", "TotalStartedThreadCount"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "BufferPool_direct_" mbean = "java.nio:type=BufferPool,name=direct" paths = ["Count", "MemoryUsed", "TotalCapacity"] [[inputs.jolokia2_agent.metric]] name = "jvm" field_prefix = "BufferPool_mapped_" mbean = "java.nio:type=BufferPool,name=mapped" paths = ["Count", "MemoryUsed", "TotalCapacity"]
sys.conf
[global_tags] [agent] interval = "10s" round_interval = true metric_batch_size = 1000 metric_buffer_limit = 10000 collection_jitter = "0s" flush_interval = "10s" flush_jitter = "0s" precision = "" debug = false quiet = false logfile = "" hostname = "" omit_hostname = false [[outputs.influxdb]] urls = ["http://127.0.0.1:8086"] database = "telegraf" #[[outputs.file]] # files = ["stdout", "/tmp/metrics.out"] [[inputs.cpu]] percpu = true totalcpu = true collect_cpu_time = false report_active = false [[inputs.diskio]] [[inputs.mem]] [[inputs.processes]] [[inputs.swap]] [[inputs.system]] [[inputs.net]] [[inputs.netstat]] [[inputs.disk]] ignore_fs = ["tmpfs", "devtmpfs"]
grafana
grafana 是一个颜值非常高的监控展示组件,支持非常多的数据源类型,对 influxdb 的集成度也比较高,可通过以下地址进行下载:https://grafana.com/grafana/download
wget -c https://dl.grafana.com/oss/release/grafana-6.5.3.linux-amd64.tar.gz tar -zxvf grafana-6.5.3.linux-amd64.tar.gz
cd grafana-6.5.3/bin
./grafana-server &
开放端口
firewall-cmd --add-port=3000/tcp --permanent firewall-cmd --reload
访问Grafana
地址:http://ip:port,其中默认端口3000。默认用户名密码:admin/admin。首次登录成功后,需修改密码。
配置数据源
Configuration->Data sources->Add data source -> influxdb,输入influxdb数据源的信息,主要是输入name和url
配置数据库名
集成
把我们的 Spring Boot 项目打包(见仓库),然后上传到服务器上去执行。
打包方式:
mvn package -Dmaven.tesk.skip=true
执行方式(自行替换日志方面配置):
java -XX:+UseConcMarkSweepGC -Xmx512M -Xms512M -Djava.rmi.server.hostname=192.168.132.100 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=14000 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintTenuringDistribution -Xloggc:/tmp/logs/gc_%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/logs -XX:ErrorFile=/tmp/logs/hs_error_pid%p.log -XX:-OmitStackTraceInFastThrow -jar demo-0.0.1-SNAPSHOT.jar 2>&1 &
请将 IP 地址改成自己服务器的实际 IP 地址,这样就可以使用 jmc 或者 VisualVM 等工具进行连接了。
确保 Telegraf、InfluxDB、Grafana 已经启动,这样,Java 进程的 JVM 相关数据,将会以 10 秒一次的频率进行收集,我们可以选择 Grafana 的时间轴,来查看实时的或者历史的监控曲线。
这类监控信息,可以保存长达 1 ~ 2 年,也就是说非常久远的问题,也依然能够被追溯到。如果你想要对 JVM 尽可能地进行调优,就要时刻关注这些监控图。
举一个例子:我们发现有一个线上服务,运行一段时间以后,CPU 升高、程序执行变慢,登录相应的服务器进行分析,发现 C2 编译线程一直处在高耗 CPU 的情况。
但是我们无法解决这个问题,一度以为是 JVM 的 Bug。
通过分析 CPU 的监控图和 JVM 每个内存分区的曲线,发现 CodeCache 相应的曲线,在增加到 32MB 之后,就变成了一条直线,同时 CPU 的使用也开始增加。
通过检查启动参数和其他配置,最终发现一个开发环境的 JVM 参数被一位想要练手的同学给修改了,他本意是想要通过参数 “-XX:ReservedCodeCacheSize” 来限制 CodeCache 的大小,这个参数被误推送到了线上环境。
JVM 通过 JIT 编译器来增加程序的执行效率,JIT 编译后的代码,都会放在 CodeCache 里。如果这个空间不足,JIT 则无法继续编译,编译执行会变成解释执行,性能会降低一个数量级。同时,JIT 编译器会一直尝试去优化代码,造成了 CPU 的占用上升。
由于我们收集了这些分区的监控信息,所以很容易就发现了问题的相关性,这些判断也会反向支持我们的分析,而不仅仅是靠猜测。