V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
felix021
V2EX  ›  程序员

聊聊监控

  •  
  •   felix021 ·
    felix021 · 2020-10-24 22:33:10 +08:00 · 2243 次点击
    这是一个创建于 1250 天前的主题,其中的信息可能已经有所发展或是发生改变。

    指标

    踩坑记:Goroutine 泄漏》开篇那张截图,展示了单个服务进程启动的 Goroutine 数量;除此之外,我们的服务进程在后台还采集了很多其他指标,例如:

    image

    (当前存活在堆上的对象所占空间)

    这些数据是哪儿来的呢? runtime 包给我们提供了一些 API,例如 runtime.NumGoroutine() 可以获得当前 Goroutine 数量,而 runtime.ReadMemStats() 则返回一个 MemStats 类型,给我们提供了内存相关的一系列监控指标。

    以下摘取 MemStats 中的一些成员,略作解释:

    • TotalAlloc
      • (累计)在堆上分配的对象所占内存;计入已回收对象。
    • HeapAlloc
      • 当前存活对象所占内存;不计入已回收对象。
    • StackInUse
      • 当前栈占用的内存(包括尚未分配的栈空间);更准确地说是目前被栈占用的 span ( go runtime 内存管理的一个结构)的内存合计(单位为字节)。
    • PauseTotalNs
      • 进程启动以来累计的 GC STW 时间(单位为纳秒)
    • NumGC
      • 进程启动以来累计的 GC cycle 数。

    还有很多指标没有在这里列出,感兴趣的同学可以查看参考资料 runtime.MemStats [1]。

    Go Runtime 的这些性能指标,反应了其运行状态,可以帮助我们排查性能问题:例如上篇《踩坑记:Goroutine 泄漏》我们是通过 Goroutine 的上涨发现有泄漏;而在《踩坑记:go 服务内存暴涨》,我们其实也可以借助 HeapAlloc 来实锤是否有内存泄漏(如果有内存泄漏的话,HeapAlloc 也应该是不断增长,与进程的 RSS 保持同步)。

    服务本身的性能指标也很重要,例如接口 QPS 、延迟、cache 命中率等也很重要。例如在我们的微服务框架中,就采集了每次请求的延迟、请求成功 /失败等信息,基于这些信息配置的报警可以帮助我们快速发现下游服务的异常。

    实际工作中,还需要关注业务指标 —— 例如点击率、转化率、交易量等等,需要结合自身业务的特定设计合理的指标体系。

    采集

    有指标还远远不够,还需要想办法采集下来,供后续查询和监控使用。

    对于一般的业务数据,我们可能会考虑使用 MySQL 等 RDBMS 来存储,但是对于这类指标往往数据量非常庞大,因而在采集、存储、查询上都需要特殊考量。

    例如一个占地 5 万平方米的数据中心,可能部署了 10 万台服务器。如果每秒采集一次 CPU 占用率,那就达到 10w QPS 了,更何况除了机器本身的指标,还有大量服务的性能指标、业务指标等。

    好在这些指标有一个很重要的共同点:它们都是定时采样的,因此也被称“时序数据”( time series,时间序列)或“度量”( metric )。

    以 CPU 占用率为例,我们可以取名为 "sys.cpu" ,它可能包含多个 tag,例如 ip 、datacenter,那么一次典型的采集如下所示:

    #   NAME    TIMESTAMP  VAL  TAG1        TAG2
    put sys.cpu 1356998400 35   ip=10.0.0.1 datacenter=sh
    

    在这里 sys.cpu {ip=10.0.0.1, datacenter=sh} 就是一个时间序列。

    针对其时序特点,我们可以为其设计专用数据结构,并且通过降低采样频率(例如 30s 一个采样点)来降低负载。很多开源项目就是这么做的,例如 OpenTSDB, Prometheus, influxdb, StatsD 等,都实现了一个时序数据库( Time Series DB,TSDB )。

    以 OpenTSDB 为例,它会将时序数据保存在 HBase 中,每一行保存某个时间序列一整个小时的数据,具体而言就是

    • ROW KEY = <名称><时间><tag k1><v1><k2><v2>...
      • 时间会对齐到小时开始
      • 名称、k 、v 会用另一个表映射到一个 6 字节整数,从而减少存储量、提高存储和查询效率
    • COLUMN FAMILY
      • t = 连续存储该 ROW KEY 下每一个采样点的数据(时间偏移量+数据格式+数据)

    从上述存储方式我们可以看到,相比于 RDBMS,TSDB 通过定制化的数据结构,能够大幅提高对时序数据的采集、存储和查询效率。

    在具体实现 /使用中还有一些点值得关注:

    1. 时序数据库是为了帮助我们发现问题,但不应因此影响线上业务,因此 client 的实现往往会采用 udp 或者 sidecar 的方式实现,从而达到 nonblocking 的效果(当然其代价是可能会丢失一些数据);

    2. OpenTSDB 底层只存储了数据点的采样值,这适合用来存储 cpu 使用率、goroutine 进程数等数据(当前值和历史值无关),对于更复杂的需求,例如计数器、延迟(需要计算 avg/p95/p99)等,需要在客户端或 sidecar 里实现一个累加器、计时器,并上报它们的采样值;

    3. 由于每一组 tag key/value 组合(例如前述 ip=10.0.0.1, datacenter=sh )都对应一个独立的 Time Series,因此需要控制这些 tag 取值组合的总数;一个典型的 badcase 是使用 uid 作为 tag,可能导致千万甚至更多的独立组合,从而对存储和查询造成过大的压力;

    4. 在性能要求特别苛刻的场景,例如超高并发、低延迟业务采集 QPS,可以考虑进一步采样,例如只随机抽取 1%的请求累加计数器,每个请求+100,从而降低采样对性能的影响。

    关于 OpenTSDB 的更多细节,感兴趣的同学可以参考其官网[2],这里不过多展开。

    监控

    基于 TSDB 提供的 API,我们就可以实现必要的监控和报警。

    一个常用的工具是 Grafana [3],支持各种 TSDB 作为数据源,并实现了一整套图表工具用于展示,方便创建各类看板,对于排查问题非常有帮助:

    image

    不仅如此,Grafana 从 4.0 版开始,还增加了一个 Alert 模块,可以很方便地配置报警规则,且支持邮件等常见报警方式(还可通过 API 扩展);不过其规则的灵活度不够,不能承载很复杂的报警需求。

    比如有这么一个 metric:svc.thoughput{success=1 或 0},用于记录累计请求数,并且加上了 tag "success" 用来区分请求成功 /失败。

    一个常见的监控需求是,针对 QPS 的异常波动进行报警,但由于晚高峰和凌晨的 QPS 差别很大,不能只是设置一个简单的阈值;又或者,我们希望基于错误率进行报警,这就需要计算:

    svc.thoughput{success=0} / svc.thoughput{}
    

    这些需求对于 Grafana 来说就超纲了。

    监控+

    因此我们基于开源项目 Bosun[4] 进行二次开发,以支持复杂的报警需求。它是 Stack Exchange 开发的一个监控报警系统,其特点是实现了一套基于对 metrics 进行计算的表达式。

    以前述 QPS 异常报警为例,虽然日内 QPS 会有显著的波动,但是通常日间的请求量却是相对稳定的:

    image

    如上图所示,凌晨、中午、晚上由于用户作息带来了明显的低谷和高峰,而代表 T 日和 T - 1 日数据的黄线和绿线则有相当程度的重合;因此我们可以设置这样的报警规则:如果日同比降幅超过 30% 则表示异常。

    使用 bosun 表达式,实现这样的规则就很简单了:

    # 当日过去 30 分钟 QPS
    $today = avg(q("sum:rate:svc.thoughput{}", "31m", "1m"))
    # 前日同一时间段 QPS
    $yesterday = avg(q("sum:rate:svc.thoughput{}", "1471m", "1441m"))
    warn = ($today / $yesterday) < 0.7
    

    注:

    • sum:rate:svc.thoughput{} 计算的是 svc.thoughput 的斜率,准确地说是对于两个相邻采样点,计算 (value2 - value1) / (ts2 - ts1) ,也就是 QPS ;
    • 使用过去 31m ~ 1m 的数据,是因为最近 1m 的数据还没有采集完。

    bosun 表达式还提供了很多更复杂的玩法。例如,采集时添加一个 tag "api",用于区分具体是哪个接口的请求,然后我们只要简单地将 svc.thoughput{} 改成 svc.thoughput{api=*} 就能同时监控所有接口的 QPS 了;又或者我们可以用 epoch() 获取当前时间戳,以针对夜间使用更宽松的阈值。

    对 bosun 感兴趣的同学,可以看一下它的官网[4]。这里顺便吐槽一下,它的文档实在写得不咋地,尤其是表达式的那部分,很多方法只提供了描述、没有样例。

    监控++

    虽然 bosun 已经很强大,但是仍然不能满足所有场景。其根本缺陷在于,规则仍然需要我们从过去的经验中总结 —— 有多少人工,才有多少智能。

    还是以 QPS 为例,虽然我们通过监控日同比变化率,绕过了日内的波动,但是却绕不过周内的波动 —— 周一早晨的请求量往往会低于周日同时间段。当然我们也可以在表达式里再加上相应的判断,但还有法定节假日的情况呢?表达式过于复杂,也会导致报警规则难以维护。

    如果我们能够基于过去的数据,学习到异常点(离群点)的特征,那就能较好地解决这一类问题。

    用于检测异常点的方法有很多,在具体实践中,我们采用了适用于孤立森林算法( Isolation Forest ),它通常更适用于连续型、结构化数据(如时序数据)。

    孤立森林算法有两个前提:1) 异常数据在总样本中的占比较小; 2) 异常点的特征与正常点差异很大。因而,如果在数据空间某个区域里点的分布很稀疏,我们就可以认为该区域中的点为异常点。

    基于这俩前提,算法提出了一个很有意思的训练思路。假设从数据点分布在一个二维平面上:

    1. 用一个随机直线将平面分为两部分
    2. 对每一部分统计点的数量
    3. 如果点的数量大于 1 、且切割次数小于阈值,则重复上述过程

    很直观地,数据点密集的区域,所需切割次数会显著高于稀疏区域;找到了稀疏区域,也就确定了离群点。

    具体实践中:

    • 数据点通常有多个特征(高维空间),因此需要用超平面来做划分;
    • 计算所有数据的代价过高,通常是从数据集中抽取一定数量的点作为样本,训练得到一棵决策树;
    • 为了降低单次采样 /训练误差的影响,我们还需要训练多棵树(森林),综合每棵树的结果得到异常得分;
    • 最后与人工设置的阈值对比,决定是否需要报警。

    这个算法我自己没有实现过,这一节只能先装到这里了。感兴趣的同学可以阅读参考材料[5],文中内容详实,还有一个对武林外传的人物性格进行训练、生成决策树的例子,很有意思。

    - 小结 -

    照例小结一下:

    • 通过采集和利用各种性能和业务指标,可以帮助我们快速发现和解决问题;
    • 时序数据库(如 OpenTSDB )通过定制化的架构,能够提供高性能的指标采集、存储、查询能力;
    • 通过 Grafana 和 Bosun 等开源项目,我们能够更直观地观察这些指标,以及进行针对性的监控和报警;
    • 基于孤立森林等异常点检测算法,可以更智能地发现问题。

    限于各种原因,有些细节未能在文中展开(比如我们基于 OpenTSDB 实现的时序数据服务在架构上做了很多改造,以及生产中的具体案例);而且除了时序数据之外,我们还有很多其他监控报警的方案,感兴趣的同学不如投个简历,到厂里来慢慢看:

    ↓↓↓ 长期招聘 ↓↓↓

    欢迎关注

    weixin2s.png

       ▄▄▄▄▄▄▄   ▄      ▄▄▄▄ ▄▄▄▄▄▄▄  
       █ ▄▄▄ █ ▄▀ ▄ ▀██▄ ▀█▄ █ ▄▄▄ █  
       █ ███ █  █  █  █▀▀▀█▀ █ ███ █  
       █▄▄▄▄▄█ ▄ █▀█ █▀█ ▄▀█ █▄▄▄▄▄█  
       ▄▄▄ ▄▄▄▄█  ▀▄█▀▀▀█ ▄█▄▄   ▄    
       ▄█▄▄▄▄▄▀▄▀▄██   ▀ ▄  █▀▄▄▀▄▄█  
       █ █▀▄▀▄▄▀▀█▄▀█▄▀█████▀█▀▀█ █▄  
        ▀▀  █▄██▄█▀  █ ▀█▀ ▀█▀ ▄▀▀▄█  
       █▀ ▀ ▄▄▄▄▄▄▀▄██  █ ▄████▀▀ █▄  
       ▄▀▄▄▄ ▄ ▀▀▄████▀█▀  ▀ █▄▄▄▀▄█  
       ▄▀▀██▄▄  █▀▄▀█▀▀ █▀ ▄▄▄██▀ ▀   
       ▄▄▄▄▄▄▄ █ █▀ ▀▀   ▄██ ▄ █▄▀██  
       █ ▄▄▄ █ █▄ ▀▄▀ ▀██  █▄▄▄█▄  ▀  
       █ ███ █ ▄ ███▀▀▀█▄ █▀▄ ██▄ ▀█  
       █▄▄▄▄▄█ ██ ▄█▀█  █ ▀██▄▄▄  █▄  
    

    参考资料:

    1. runtime.MemStats
    2. OpenTSDB
    3. Grafana
    4. Bosun
    5. 异常检测算法 -- 孤立森林( Isolation Forest )剖析
    3 条回复    2020-10-25 17:46:35 +08:00
    DoctorCat
        1
    DoctorCat  
       2020-10-25 00:07:52 +08:00
    感谢分享,有没有实践过 Amazon 的 Random Cut Forest (RCF)效果呢?
    felix021
        2
    felix021  
    OP
       2020-10-25 10:38:31 +08:00
    @DoctorCat 不太确定,基于机器学习的异常检测是另一个团队的工作,我了解不多
    wangyzj
        3
    wangyzj  
       2020-10-25 17:46:35 +08:00
    原来是字节的招聘广告
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3263 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 14:07 · PVG 22:07 · LAX 07:07 · JFK 10:07
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.