如何搭建一个监控系统 生产环境必须是可监控的,一个对开发者黑盒的线上应用无异于灾难。一个简单的监控系统大致包含以下几部分:
采集数据
保存数据
数据可视化
监控告警
从一个熟悉的画面开始:
这是javaer每天都会看到的一个画面,当然为了减少bug,有时候也需要借助一下来自东方的神秘力量
仔细看console的第一行,灰色字体&被折叠,看起来很不起眼,就被忽略了。
展开后内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/bin/java -XX:TieredStopAtLevel=1 -noverify -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=61764 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1 -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=61765:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath ...
出现频率最高的单词是jmxremote
,这是我们需要了解的第一个概念JMX
。
JMX
JMX(Java Management Extensions,即Java管理扩展)是Java平台上为应用程序、设备、系统等植入管理功能的框架。 –wikipedia
如何做到管理功能 呢?
监控指标,包括业务监控&系统性能监控
执行方法
我们通过架构图来看一下,JMX如何实现这两个功能。
接入层,提供远程访问接口
适配层,对资源的管理和注册
MBean,提供变量or函数
还是不够直观,我们来具体看一下jmx能做什么。
在控制台中输入jconsole
,你可以看到一个java GUI风格的工具窗口,这是jdk自带用于jmx连接&展示的工具。
可以通过JDK提供的MBean查看线程、内存、CPU占用,检测死锁、执行GC。也可以通过三方按照JMX标准提供的MBean,查看or执行封装的函数方法。
以SpringApplicationAdminMXBean
为例,声明了一个包含函数的interface作为MBean,并将实现类注册到MBeanServer服务中。用到了一个委托模式。
1 2 3 4 5 6 7 public interface SpringApplicationAdminMXBean { boolean isReady () ; boolean isEmbeddedWebApplication () ; String getProperty (String key) ; void shutdown () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 public class SpringApplicationAdminMXBeanRegistrar implements ApplicationContextAware , EnvironmentAware , InitializingBean , DisposableBean , ApplicationListener <ApplicationReadyEvent > { private static final Log logger = LogFactory.getLog(SpringApplicationAdmin.class); private ConfigurableApplicationContext applicationContext; private Environment environment = new StandardEnvironment(); private final ObjectName objectName; private boolean ready = false ; public SpringApplicationAdminMXBeanRegistrar (String name) throws MalformedObjectNameException { this .objectName = new ObjectName(name); } @Override public void setApplicationContext (ApplicationContext applicationContext) throws BeansException { Assert.state(applicationContext instanceof ConfigurableApplicationContext, "ApplicationContext does not implement ConfigurableApplicationContext" ); this .applicationContext = (ConfigurableApplicationContext) applicationContext; } @Override public void setEnvironment (Environment environment) { this .environment = environment; } @Override public void onApplicationEvent (ApplicationReadyEvent event) { if (this .applicationContext.equals(event.getApplicationContext())) { this .ready = true ; } } @Override public void afterPropertiesSet () throws Exception { MBeanServer server = ManagementFactory.getPlatformMBeanServer(); server.registerMBean(new SpringApplicationAdmin(), this .objectName); if (logger.isDebugEnabled()) { logger.debug("Application Admin MBean registered with name '" + this .objectName + "'" ); } } @Override public void destroy () throws Exception { ManagementFactory.getPlatformMBeanServer().unregisterMBean(this .objectName); } private class SpringApplicationAdmin implements SpringApplicationAdminMXBean { @Override public boolean isReady () { return SpringApplicationAdminMXBeanRegistrar.this .ready; } @Override public boolean isEmbeddedWebApplication () { return (SpringApplicationAdminMXBeanRegistrar.this .applicationContext != null && SpringApplicationAdminMXBeanRegistrar.this .applicationContext instanceof EmbeddedWebApplicationContext); } @Override public String getProperty (String key) { return SpringApplicationAdminMXBeanRegistrar.this .environment .getProperty(key); } @Override public void shutdown () { logger.info("Application shutdown requested." ); SpringApplicationAdminMXBeanRegistrar.this .applicationContext.close(); } } }
JConsole会根据方法及返回值,判断是指标还是可执行函数。
除了指标和函数,还有通知。但是JMX并不保证所有通知都会被监听器接收
influxdb 知道了数据如何产生,接下来需要考虑数据如何持久化。
InfluxDB是一个由InfluxData开发的开源时序型数据库。它由Go写成,着力于高性能地查询与存储时序型数据。InfluxDB被广泛应用于存储系统的监控数据,IoT行业的实时数据等场景。
选型原因是influxdb有以下特点
可度量性:你可以实时对大量数据进行计算
无结构(无模式):可以是任意数量的列
原生的HTTP支持,内置HTTP API
基于时间序列,支持与时间有关的相关函数,如min, max, sum, count, mean, median 等一系列函数
强大的类SQL语法
强大的类SQL语法 & 无结构 看一下influxdb的语法,似曾相识。
1 2 3 4 5 influx show databases; create database wyh_dev; use wyh_dev; show Measurements;
Measurement
等价于mysql中的table
,区别在于mysql表中存储字段,字段既可以作为展示也可以建立索引。但是influxdb存储的数据从逻辑上由Measurement
、tag组
、field组
以及一个时间戳组成的。
tag信息是默认被索引的。
Field信息用于展示,是无法被索引的。
time表示该条记录的时间属性。
Line Protocol 语法 利用逗号和空格,简化插入语句, 如果插入数据时没有明确指定时间戳,则默认存储在数据库中的时间戳则为该条记录的入库时间。(纳秒)
1 2 3 4 5 6 7 weather,location=us-midwest temperature=82 1465839830100400200 | -------------------- -------------- | | | | | | | | | +-----------+--------+-+---------+-+---------+ |measurement|,tag_set| |field_set| |timestamp| +-----------+--------+-+---------+-+---------+
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 insert table1,tag1= a field1= "fieldA" insert table1,tag1= tagB field1= "fieldA",field2= "fieldB" insert table1,tag1= a,tag2= b field1= "fieldA",field2= "fieldB",field3= "fieldC" select * from table1show series from table1SHOW FIELD KEYS FROM table1SHOW TAG KEYS FROM table1drop measurement table1insert table2,tagVal= tagA fieldVal= "fieldA" insert table2,tagVal= tagB fieldVal= "fieldA" insert table2,tagVal= tagA fieldVal= "fieldB" select * from table2 where fieldVal= 'fieldA' select * from table2 where tagVal= 'tagA' select * from table2 group by fieldValselect * from table2 group by tagValselect * from table1 group by * select count (* ) from table2 group by time (2 d)
以下有两个常见教程出现的错误
field 不可以作为group by条件,但是可以作为where条件
8083端口停用,web管理界面通过独立组件chronograf实现
原生的HTTP支持,内置HTTP API 1 2 3 curl -i -X POST 'http://localhost:8086/write?db=wyh_dev' --data-binary 'table2,tagVal=tagA fieldVal="http" ' curl -G 'http://localhost:8086/query?pretty=true' --data-urlencode "db=wyh_dev" --data-urlencode "q=SELECT * FROM table2 "
每次insert记录,如果没有指定,默认会保存数据库当前时间(单位纳秒)。复杂的函数计算不符合浅入浅出的定位,我们换一种直观的角度。
Grafana
The open platform for beautiful analytics and monitoring
很炫很好很强大,没什么好讲的。
配置数据源
支持多种数据源。Access两种形式
Server 服务器请求后渲染
Browser 浏览器直接请求
时间函数 where 不能选择field,只能选择tag
画图参考官方文档
配置告警 配置告警通道,原生支持email、钉钉,但是支持webhook也就可以随意扩展,如企业微信、SMS等内部通信软件。e.g. 企业微信
设定告警规则,包括统计方法、安全阈值。
jmxtrans 上面介绍完JMX之后,其实缺少了一个通道,将JMX指标输出给influxdb。放到后面介绍的原因是因为独立组件,不依赖JMX,数据来源可以是http、日志、kafka
This is effectively the missing connector between speaking to a JVM via JMX on one end and whatever logging / monitoring / graphing package that you can dream up on the other end.
可以通过 JSOM | YAML 配置读取地址、查询线程数、采集指标以及持久化方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 { "servers" : [ { "port" : "12000" , "host" : "users" , "numQueryThreads" : 2 , "queries" : [ { "obj" : "java.lang:type=Memory" , "attr" : [ "HeapMemoryUsage" , "NonHeapMemoryUsage" ], "resultAlias" : "MemoryUsage" , "outputWriters" : [ { "@class" : "com.googlecode.jmxtrans.model.output.InfluxDbWriterFactory" , "url" : "http://influxdb:8086/" , "username" : "wyh" , "password" : "wyh" , "database" : "wyh" , "tags" : { "application" : "MemoryUsage" } } ] }, { "obj" : "kafka.consumer:type=consumer-metrics,client-id=*" , "attr" : [ "connection-close-rate" , "connection-creation-rate" , "network-io-rate" , "outgoing-byte-rate" , "request-rate" , "request-size-avg" , "request-size-max" , "incoming-byte-rate" , "response-rate" , "select-rate" , "io-wait-time-ns-avg" , "io-wait-ratio" , "io-time-ns-avg" , "io-ratio" , "connection-count" , "successful-authentication-rate" , "failed-authentication-rate" ], "resultAlias" : "ConsumerMetrics" , "outputWriters" : [ { "@class" : "com.googlecode.jmxtrans.model.output.InfluxDbWriterFactory" , "url" : "http://influxdb:8086/" , "username" : "wyh" , "password" : "wyh" , "database" : "consumer" , "tags" : { "application" : "ConsumerMetrics" } } ] } ] } ] }
也可以通过java程序引入依赖包,增加扩展。
1 2 3 4 5 6 7 8 9 10 11 public class MemoryPool { public static void main (String[] args) throws Exception { Injector injector = JmxTransModule.createInjector(new JmxTransConfiguration()); ProcessConfigUtils processConfigUtils = injector.getInstance(ProcessConfigUtils.class); JmxProcess process = processConfigUtils.parseProcess(new File("memorypool.json" )); new JsonPrinter(System.out).print(process); JmxTransformer transformer = injector.getInstance(JmxTransformer.class); transformer.executeStandalone(process); } }
总结 开源项目对JMX支持较好,但是作为普通应用,通过JMX暴露指标,需要业务开发编写大量代码,不够友好。而且,RMI存在注入风险,不能暴露外网接口。本人介绍的更多为组件和大致思路,实际使用过程中可以考虑提供通过http发送元数据、或者特殊日志格式进行采集。
附录 docker-compose.yml文件,可以通过docker-compose up -d
执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 version: "3" services: influxdb: image: influxdb:latest container_name: influxdb ports: - 8086:8086 volumes: - ./influxdata:/var/lib/influxdb environment: INFLUXDB_DB: metrics restart: always grafana: image: grafana/grafana container_name: grafana ports: - 3000:3000 volumes: - ./grafana:/var/lib/grafana restart: always jmxtrans: image: jmxtrans/jmxtrans container_name: jmxtrans volumes: - ./jmxtrans:/var/lib/jmxtrans/ restart: always