浅入浅出监控系统

如何搭建一个监控系统

生产环境必须是可监控的,一个对开发者黑盒的线上应用无异于灾难。一个简单的监控系统大致包含以下几部分:

  1. 采集数据
  2. 保存数据
  3. 数据可视化
  4. 监控告警

m0

从一个熟悉的画面开始:

m1

这是javaer每天都会看到的一个画面,当然为了减少bug,有时候也需要借助一下来自东方的神秘力量

m2

仔细看console的第一行,灰色字体&被折叠,看起来很不起眼,就被忽略了。

m3

展开后内容如下

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

如何做到管理功能呢?

  1. 监控指标,包括业务监控&系统性能监控
  2. 执行方法

我们通过架构图来看一下,JMX如何实现这两个功能。

m4

  1. 接入层,提供远程访问接口
  2. 适配层,对资源的管理和注册
  3. MBean,提供变量or函数

还是不够直观,我们来具体看一下jmx能做什么。

在控制台中输入jconsole,你可以看到一个java GUI风格的工具窗口,这是jdk自带用于jmx连接&展示的工具。

m5

可以通过JDK提供的MBean查看线程、内存、CPU占用,检测死锁、执行GC。也可以通过三方按照JMX标准提供的MBean,查看or执行封装的函数方法。

m6

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();
}

}

}
  1. JConsole会根据方法及返回值,判断是指标还是可执行函数。
  2. 除了指标和函数,还有通知。但是JMX并不保证所有通知都会被监听器接收

influxdb

知道了数据如何产生,接下来需要考虑数据如何持久化。

InfluxDB是一个由InfluxData开发的开源时序型数据库。它由Go写成,着力于高性能地查询与存储时序型数据。InfluxDB被广泛应用于存储系统的监控数据,IoT行业的实时数据等场景。

选型原因是influxdb有以下特点

  1. 可度量性:你可以实时对大量数据进行计算
  2. 无结构(无模式):可以是任意数量的列
  3. 原生的HTTP支持,内置HTTP API
  4. 基于时间序列,支持与时间有关的相关函数,如min, max, sum, count, mean, median 等一系列函数
  5. 强大的类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存储的数据从逻辑上由Measurementtag组field组以及一个时间戳组成的。

  1. tag信息是默认被索引的。
  2. Field信息用于展示,是无法被索引的。
  3. 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 table1

show series from table1
SHOW FIELD KEYS FROM table1
SHOW TAG KEYS FROM table1
drop measurement table1

insert 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 fieldVal
select * from table2 group by tagVal

select * from table1 group by *
select count(*) from table2 group by time(2d)

以下有两个常见教程出现的错误

  1. field 不可以作为group by条件,但是可以作为where条件
  2. 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 "

基于时间序列,支持与时间有关的相关函数,如min, max, sum, count, mean, median 等一系列函数

每次insert记录,如果没有指定,默认会保存数据库当前时间(单位纳秒)。复杂的函数计算不符合浅入浅出的定位,我们换一种直观的角度。

Grafana

The open platform for beautiful
analytics and monitoring

很炫很好很强大,没什么好讲的。

配置数据源

m8

支持多种数据源。Access两种形式

  1. Server 服务器请求后渲染
  2. Browser 浏览器直接请求

时间函数

m9
where 不能选择field,只能选择tag

画图参考官方文档

配置告警

配置告警通道,原生支持email、钉钉,但是支持webhook也就可以随意扩展,如企业微信、SMS等内部通信软件。e.g. 企业微信

m10

设定告警规则,包括统计方法、安全阈值。

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.

m12

可以通过 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发送元数据、或者特殊日志格式进行采集。

m13

附录

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