浅入浅出监控系统

如何搭建一个监控系统

生产环境必须是可监控的,一个对开发者黑盒的线上应用无异于灾难。

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

m0

从一个熟悉的画面开始:

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

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

m3

/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 服务中。用到了一个委托模式。

public interface SpringApplicationAdminMXBean {

    boolean isReady();
    boolean isEmbeddedWebApplication();
    String getProperty(String key);
    void shutdown();
}
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 语法 & 无结构

    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 语法
利用逗号和空格,简化插入语句, 如果插入数据时没有明确指定时间戳,则默认存储在数据库中的时间戳则为该条记录的入库时间。(纳秒)

weather,location=us-midwest temperature=82 1465839830100400200
  |    -------------------- --------------  |
  |             |             |             |
  |             |             |             |
+-----------+--------+-+---------+-+---------+
|measurement|,tag_set| |field_set| |timestamp|
+-----------+--------+-+---------+-+---------+
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

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=anything,e.g. 企业微信

m10

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

m11

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 配置读取地址、查询线程数、采集指标以及持久化方式。

{
    "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 程序引入依赖包,增加扩展。

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

总结

m13