open-falcon 监控上报

监控平台应用背景

监控平台被形象地称为运维之眼,没有它软件产品的上线及后期维护将像摸虾一样。

从监控的对象来划分,监控又可分为基础监控和业务监控。

基础监控偏重基础组件和基础资源的监控,一般指标比较固定,很大程度上可默认采集。

业务监控偏重具体服务的指标监控,较为灵活,一般为具体业务自己定义。

监控平台在整个上线的各个过程中都是有帮助的,它使用的场合有以下几项:

  1. 提供服务上线的参考,如可观察新特性上线后的各项指标是否正常,作为稳定运行的依据
  2. 出现事故后,可作为定位问题的参考,让排查问题更方便
  3. 监控服务及资源的使用情况,为优化或扩容做准备

其中,12基础监控和业务监控兼有,一般方式为基础监控缩小范围,业务监控更准确地查找。业务监控一般会配合日志平台一起提供帮助。

3涉及到具体IaaS层资源,基础监控可以提供更多帮助。

常见架构设计

市面监控平台多以agent上报的方式采集数据,由于监控平台写多读少的缘故,上报链路中间多可插入一个消息队列或代理。

由于采集端已有监控数据,告警平台多与监控平台整合,这样一个采集,一个消费,就会方便很多。

open-falcon

open-falcon是小米的监控平台,open-falcon之前,小米使用的是zabbix。

open-falcon的架构图如下所示:

open-falcon架构图

乍一看挺复杂,捊一捊就比较清晰了。

数据上报链路:绿色粗线即为数据上报链路,数据从App产生或由agent采集,然后发送到transfer,transfer再将数据分发给judge和graph。judge即为告警判定模块,graph存储监控数据。

配置下发链路:橙色虚线为配置下发链接,portal为配置存储中心。为了加速配置的下发,同时减小portal的压力,hbs会将portal缓存一份到自己的内存,在需要的时候会下发给agent(监控配置)和judge(告警配置)。

监控查看链路:蓝色虚线部分为监控查看链路,graph是监控数据的存储组件,出于负载均衡的考虑,graph可能有多个实例,query组件负责整合多个实例的结果,供dashboard读取。dashboard为通往用户的web界面。

open-falcon中的大部分组件都是可以水平扩展的,transfer、judge和graph都是可能会出现瓶颈的组件。官网有一个机器量与瓶颈及应对的对应关系,很有参考价值。

agent 采集上报

agent 是数据采集和上报端,更接近数据,比较重要。

agent 默认已提供机器的大部分基础监控,modules/funcs/ 下可以看到监控的列表,cpu、内存、磁盘、负载、网络io等,涵盖了绝大部分机器相关监控。

agent 每隔一段会将 metrics 上报到 transfer,metrics 是一个列表,下面是一个比较典型的 metric 结构:

1
2
3
4
5
6
7
8
9
type MetricValue struct {
Endpoint string `json:"endpoint"`
Metric string `json:"metric"`
Value interface{} `json:"value"`
Step int64 `json:"step"`
Type string `json:"counterType"`
Tags string `json:"tags"`
Timestamp int64 `json:"timestamp"`
}

这里说一下 Type,主要有 GAUGE 和 COUNTER 两种。GAUGE 为实际的测量值,如内存使用率;COUNTER 为计数值,上报时会计算差值,比较典型的如磁盘读请求次数(linux内核提供的值为总计,我们希望了解的是上一分钟及这一分钟的读请求次数)。

在扩展性方面,agent 还提供了另外两种采集方式,http push 上报的方式和 plugin 采集。这两种方式都是以 MetricValue 为上报的基础数据结构。

push 方式,agent 会在本机提供一个 http 服务,应用可将数据以 http 请求的方式上报给 agent。http 提供了很大的灵活性,应用可整合上报功能来集成业务监控。

plugin 方式,需要提供一个包含采集脚本的仓库。所谓采集脚本,即是指本地可运行的脚本,该脚本会输出 MetricValue 结构的 json 数据,agent 将定期执行并将输出上报给 transfer。

下面是官方提供的一个 python 采集脚本:

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
# https://github.com/open-falcon/plugin/blob/master/demo/60_plugin.py
#! /usr/bin/env python
#-*- coding:utf-8 -*-

import json
import time

data = [
{
'metric': 'plugins.xx',
'endpoint': 'host-001.bj',
'timestamp': int(time.time()),
'step': 60,
'value': 1,
'counterType': 'GAUGE',
'tags': 'idc=aa'
},
{
'metric': 'plugins.yy',
'endpoint': 'host-002.bj',
'timestamp': int(time.time()),
'step': 60,
'value': 0,
'counterType': 'GAUGE',
'tags': 'idc=bb'
}
]

print(json.dumps(data))

NOTE: 需要注意的是,为了避免 plugin 全量上报造成干扰,plugin 上报需要在 dashboard/portal 中指定启用哪些上报脚本。

第三方监控多以 push 方式上报,有了上面两种扩展的上报方式,即可以实现其它基础监控或者业务监控。

总结

open-falcon 算是国内比较成熟的监控产品,虽说多组件的方式部署可能较为庞杂,但 falcon-plus 对 open-falcon 的组件做了整合,部署难度已经下降了好几个数量级,安装已经不再是大问题。

open-falcon 以 golang 和 python 为主要开发语言。提供水平扩展的组件多为 golang 开发,可单独打包,降低了部署的难度;web 端以 python 为主,开发效率较高。各取所长。组件粒度较细,解耦也较彻底,二次开发难度较小。

push 上报方式是许多监控产品采用的方式,较为灵活通用,可满足扩展的需要。其它监控如 mysql 或者具体业务的监控等也可基于此方式实现。

另外,open-falcon 组件内部多使用 rpc 协议沟通,效率也是比较高的。有时间再展开写一写。

除了监控数据,另一块是告警判定逻辑。后面来补充。

macosx上的平铺式窗口管理器

背景

在提供图形界面的操作系统里,都会存在一个问题,就是当我们需要查找大量资源时,窗口会变得较多不方便管理。

比如我现在打开的窗口,就包含了编辑博客的编辑器,一个命令终端,一个查找资料的浏览器,一个临时记录信息的备忘录,还有一些其它的软件。

这时会存在两个问题:

  1. 应用过多,切换应用时会有很多精力浪费在找目标应用的过程中
  2. 窗口布局过乱,重新规划放多个桌面又耗费太多精力,得不尝失

对于窗口的管理,现在已经存在一些方案,主要有以下几大类:

  1. 提供窗口管理工具,这类工具一般以快捷键为入口,方便对目标窗口调整大小,设置位置
  2. 加速操作,这类工具一般是牺牲精确度,将移动和修改窗口大小合并为一个操作,如将窗口拉到屏幕边缘自动贴合并调整位置和大小,或直接选择提前准备好的位置,magnet和divvy就属于此类
  3. 平铺式窗口设计,将所有窗口平铺到屏幕

其中,1和2虽能解决布局问题,但是耗费精力也不小,而且当应用过多时,仍然会有很大一部分精力耗费在切换应用上。目前来看,3似乎是一种比较理想的方式。

平铺式窗口

平铺式窗口管理器的设计存在很长一段时间了,最早应该是linux平台先行做的实践。

原因猜测应该是linux分层外包的协作方式,内核、核心工具、窗口系统、UI系统等都是由不同的开源团队分别完成,这样大家也有更多的精力和空间去做尝试。

我最早使用的linux上的平铺式窗口管理器是awesome,它将所有的窗口都平铺在电脑的屏幕上,不需要自己去调整窗口的位置和大小或窗口间做切换时的视觉适应,而且可以一眼看到其它应用,可以节省很多的精力。

不过,平铺式窗口也有一些问题,比如窗口过多而电脑屏幕过小时就会很尴尬,再比如使用一些gui软件如chrome时也会很别扭,所以它较适合用于多个终端的情况。一些软件如tmux、vim split window的设计也有异曲同工之妙。

mac os x 的情况稍有不同,它的开发模型是大一统,整个系统从内核到上层用户软件的设计都是由苹果公司包揽,所以做窗口管理只能提供有限的能力。但这是足够的,我们极少需要完全从底层自定义整个设计,这是一种偏执。

该设计的主要目标还是解放人的精力,让我们能够投入到更有价值的事情中。所以下面我以此为基础,探索一下mac os x上的相关实现。

Amethyst

这是我在寻找平铺式窗口管理器时找到的一个开源方案,仓库的地址在 https://github.com/ianyh/Amethyst

Amethyst是一个基于xmonad的软件,xmonad运行在linux平台上,类似awesome。

Amethyst默认提供了十多个布局供用户使用,用户可以使用快捷键来管理窗口布局或窗口大小。

缺点是,Amethyst提供的用户界面只有快捷键,所以上手难度较大,用户需要自己去寻找符合自己的操作方式。

比如,我现在的方式是,使用 Widescreen tall 布局,左半屏即是一个主要的工作窗口,其余窗口都竖排在右侧窗口。可以鼠标点击需要切换的窗口,然后快捷键切换到左侧使用。

默认布局和默认快捷键在官方文档都有,这里就不赘述了。

总结

这篇文章对操作讲解地较少,对于高度可配的软件来说,具体的使用是见仁见智的事情。

第一种方案,大多工具只是提供了一个快捷键操作的方式而已,对我的问题没有太大帮助。

我之前也在第二种方案上试用过很长一段时间,总体来说如果打开的应用较为稳定且数量较少,也是一种不错的方案。使用的工具类似magnet,是一个开源方案,具体工具名称倒记不得了。

当然工具是一方面,良好的使用习惯也是很重要的,甚至它完全可以取代大部分效率提升工具。真正的效率提升还是在人,工具只是在为人打补丁而已。

golang并行编程

关于本文

本文主要阐述作者对golang并行编程设计的理解,重在讲解思路而非实际使用。读者可以通过阅读本文,来了解golang中并行设计的关键技术及其作用,知其所以然。

本文并不追求完整和严谨,重点是提供一个学习思路,理清基本概念。使用的细节仍需要参考官方文档。

由于作者没有专门研究过golang的核心实现,所以文中可能存在理解或表述不当之处,欢迎批评指正。

并行需求的背景

在计算机诞生之初,系统还是批处理的方式。彼时,碍于性能和基础工具的不完善,一台计算机同时只能跑一个任务。

后来出现了能在一台计算机上同时跑多个任务的多任务操作系统,*nix系统是典型代表。

但是多任务同时也只能有一个任务在运行,只是硬件性能足够高时,多任务切换快速,让人类看上去像是多个任务在并行处理。

再后来,硬件进一步升级,出现了多核CPU的技术,这时计算机真正具有了并行处理的能力。

伴随多核技术的出现的,还有相关的计算机编程语言。如何能便利地开发利用多核技术的程序,是这类编程语言要解决的核心问题之一。

golang标榜的即是该优势,它在语言层面提供了并行机制,并提供了相关语言工具,来简化多核心编程的过程。

golang中的并行实现:goroutine

golang实现了一种轻量级的线程,即goroutine,来为用户提供并行编程的支持。

goroutine由golang的运行时管理,它使用go关键字创建,使用较为简单。

下面是一个例子:

1
2
3
4
5
6
go func() {
h1 := hash("hello")
fmt.Printf("hello => %s\n", h1)
}()

doSomeThing()

如果CPU有多个核心,golang会将耗时的hash计算自动分配到单独的核心,以充分利用多核心的优势。

但是这里有一个很明显的问题,上例中我们只是把计算结果打印出来,大多时候这不是我们想要的。我们想把计算的结果返回到主进程,供后面使用。

这就涉及到多个逻辑单元间的通信问题,有这么几个类似的总是,函数间参数的传递,goroutine间的数据通信,及进程间的数据通信,其实说的是同一个问题。

当然实现手段是多样的,golang提供了一种channel技术来解决goroutine间通信的问题(关键字CSP)。

goroutine间的通信:channel

我们先来考查一下编程语言函数中的数据通信,来更好的理解数据通信的方式。

  1. 一个比较简单易得的方式是使用全局变量,在语言层面来说,这是一个可行的设计。但是当把它放到并行的场景下时,它的缺点就非常明显了,为了保证数据的一致性,需要引入锁机制或者一些其它的同步机制,这就使问题变得更为复杂。
  2. 另一个可行的方式是把数据作为参数传递给具体的变量,类比于逻辑单元间的消息传递(实际上在object-c中即是把普遍意义上的函数调用定义为信号传递,本质类似,只是不同的看待方式)。这种方式的问题是,需要底层来提供支持,比如linux的管道。

下面我们再来看一下上面两种方式在golang多个goroutine间的实现:

全局变量必然是行得通的。我们使用锁机制来保证数据的一致性,下面是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"sync"
)

var mutex sync.RWMutex
var y int

func main() {
mutex.Lock()
go func() {
y = 4
mutex.Unlock()
}()

mutex.RLock()
fmt.Println(y)
mutex.RUnlock()
}

第二种方式即是golang中提供的channel工具,它的操作方式有点类似linux中的管道,数据从一端流入,另一端流出。它是一个内置的数据结构,基本使用如下:

1
2
3
ch := make(chan int)
ch <- 3
i := <-ch

上面定义了具有1个元素的channel,写入元素时会阻塞当前goroutine,并等待其它goroutine读取该元素(还可以指定channel元素个数来为其添加缓存,这里不展开)。

使用channel工具来重写上面的代码会更为简洁,可读性更好。下面是重构后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)

func main() {
y := make(chan int)
go func() {
y <- 4
}()

fmt.Println(<-y)
}

代码确实简洁了许多,需要注意的是,go func()中的 y <- 4 语句会阻塞该goroutine,直到最后 <-y 将数据读出后,go func()才会继续执行。

golang中有一个原则性的表述,可以看出其对使用channel来进行数据通信的倾向:

Do not communicate by sharing memory; instead, share memory by communicating.

goroutine协同:select

现在我们已经有了多个goroutine间通信的工具,但是这个工具是阻塞的,有时我们希望能更灵活地处理channel的状态,如指定3秒的超时时间。

goroutine提供了一个select工具来解决这类问题,这个select跟linux中的select有些类似,监控多个目标,如果监控到某个目标达到了我们期望的状态,就做某类操作。

golang中的select跟switch有着一样的结构,而且都使用case关键字来处理多分支,不同的是select处理的是channel的输入输出。

假如我们定义了两个channel,并由两个goroutine来分别写channel数据,select的处理结构类似下面这种:

1
2
3
4
5
6
7
8
select {
case <-ch1:
doSomeThing()
case <-ch2:
doOtherThing()
default:
allIsBlocking()
}

程序会检测ch1和ch2中是否有写入数据,并随机选择一个有写入数据的分支执行,注意这里是随机。如果没有,执行default分支。具体执行过程更复杂些,具体可点击文章底部的链接查看。

可以使用下面的例子来检测超时:

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
package main

import (
"fmt"
"time"
)

func main() {
blockCh := make(chan int)
timeoutCh := make(chan int)

go func() {
time.Sleep(time.Second*3) // 1
blockCh <- 1
}()

go func() {
time.Sleep(time.Second*1) // 2
timeoutCh <- 1
}()

select {
case <-blockCh:
fmt.Println("block routine is finished!")
case <-timeoutCh:
fmt.Println("block routine is timeout!")
}
}

由于1处的goroutine执行太慢(3秒),程序将在等待一秒后,打印”block routine is timeout!”并退出。

case语句同时支持写入和读出,具体可以参考官方文档。

辅助工具

golang还为并行编程提供了更为丰富的工具,使并行编程的过程更为便利,下面列举一些比较常用的工具。这里省去了示例代码。

goroutine会在主进程退出时自动退出,如果我们想让主进程等待goroutine的运行,可以使用sync.WaitGroup()工具。

超时检测的情况比较多,time包借助运行时,实现了一个更为高效的超时检测,time.After(),另外,time.Tick()可以定期写channel配置for实现周期性任务。

参考资料

https://golang.org/ref/spec
https://golang.org/doc/effective_go.html

布朗运动的计算机模拟

布朗运动是指悬浮在液体或气体中的微粒所做的永不停息的无规则运动。

薛定谔在《生命是什么》中描述高锰酸钾的扩散现象,他说虽然高锰酸钾看上去是从高浓度向低浓度扩散,但并非是分子的冲撞造成的,本质上是分子的无规则随机运动。

这里的无规则随机运动是指,分子在某一时刻的运动方向,是完全随机的,不受液体浓度的影响。

看上去似乎难以置信,但正是由于高浓度+随机运动,才产生了一种从高浓度流向低浓度的观察效果。

书中给出了一个解释,假设其中有一薄膜,由于分子运动随机,左右运动的概率是相等的,但假设左侧浓度更高,向右运动穿过薄膜的分子必然更多。

本文试图通过计算机模拟的方式,来呈现分子的运动。

在试验中,我们有一个正方体水箱,开始时,在水箱的左下方矩形区域放置高锰酸钾溶液,然后在每一次迭代中,让分子做布朗运动。

下图是在做了10000次迭代后的情形,从图片中的状态已经可以明显看出扩散的痕迹了:

布朗运动模拟器截图

可以点击下面的链接查看属于自己的随机过程:布朗运动模拟器

好雨知时节吗

下了一夜雨,下了一夜的往事。又是三月雨。

早起后,淅淅沥沥的雨还在恍恍惚惚的下。

阴沉潮湿的空气,昏暗的房间。

不想开灯,唯恐破坏了这种氛围,竟然对这种原始的气氛有些不舍,真是好玩。

听着几首老歌,低沉冗长的音色把时间都拉长了很多。

不知道自己能不能听出自己的声音,或者能不能想象出自己呐喊的神情。

python 函数着色器设想

背景

跟食品一样,代码也有过期时间。但糟糕的是,食品出产时就已标明了何时过期,但是一段代码是否过期却不好判定。

我们通常有这样的经验,当一个资源地址(url)要下线时,我们会从 nginx 的请求日志里查看该接口在最近一段时间内有没有被访问。

函数也可以做类似的处理,要解决的问题是,由谁来充当 nginx 记录请求的角色。

现状

当前网络上已经有比较好的支持,最典型的实现有以下几类:

  1. python 用来性能调优的库,如 profile、hotshot 等
  2. 使用装饰器实现,每个要统计的对象都需要加装饰器,操作不够优雅且略笨拙
  3. 自己维护变量统计,这种人工成本较大

方案 2、3 成本都比较高,且实现不够优雅,难以在高层次上复用。

方案 1 是比较好的一种方式,但也存在着它的问题。

对于脚本来说,这种方式很具优势。但是,对于大型系统来说,这类库的接入成本不低,可操作性较差。

其次它的抽象程度不够,需要自己再做额外分析处理,很难做到开箱即用。

设想

基于以上原因,我们可以设想一种更为友好的方式,以一种系统的抽象粒度给出。

这里有一个很重要的指标需要评估,就是对系统性能的影响有多大。当然,这是个技术问题,可以从技术角度解决。

我们这里使用着色来形象地表达记录访问状态的动作。不考虑性能影响,我们的目标是可以针对某命名空间下的对象及其方法进行着色,并获得着色后的可视化效果,以辅助对旧的代码进行重构。

我们希望有一个开关来控制着色,并可以热启动和热停止,且停止后尽可能减少或彻底取消对性能的影响。

着色器记录的信息,包括函数的调用次数及耗时。我们希望可以对各层级进行比较,可以很简单地看出对象下哪个函数在近一个月没有调用过,或者一个函数调用的平均耗时。

更上一层次,我们希望该功能以一种成熟的第三方模块给出,仅需配置极少内容即可使用。如作为 django 的第三方模块。

实现细节

我们将使用钩子或其它技术手段,接管函数的调用逻辑。我们将记录函数的如下信息:

梯度时间 函数或属性 调用次数 平均耗时
2020-03-28 20:01 views.WelcomeView.get() 20 0.2
2020-03-28 20:01 views.WelcomeView.post() 3 0.5
2020-03-28 20:01 views.WelcomeView.var 2 0.5

由于请求量较大,可对数据做缓存处理,并定期刷新到数据库;由于数据量较大,可对过旧的数据进行压缩处理。

系统将对该数据制作报表,报表主要有两种:

第一种,一段时间内函数的调用次数热力图,并对不同函数按调用次数作升序排列。

第二种,一段时间内函数的调用耗时热力图,并对不同函数按调用平均耗时作降序排列。