跳至正文

程序中的时间问题

leap second

最近Russ Cox提了一条proposal《Monotonic Elapsed Time Measurements in Go》

这个proposal主要解决了一个问题,计算时间差。
我一开始觉得,这不是很简单么?例如:

t1 := time.Now()# ... 10 ms of work 
t2 := time.Now() 
const f = "15:04:05.000" 
fmt.Println(t1.Format(f), t2.Sub(t1), t2.Format(f))

简单的相减,就可以得到时间差。但这里隐含了一个谬误,就是:

默认:程序后执行的Now,肯定比之前执行的Now晚。

时间,在人的认知里肯定是单向向前,但在计算机系统里,却不是。因为有正、负闰秒的存在,常常有bug出现,例如:Cloudflare RRDNS 故障

这起事故中,CF的RRDNS使用两个时间的差值进行权重计算,平时是没有问题,但当闰秒的出现时,程序差值就出现了负数,最终酿成了事故。所以在对时间差有严重依赖的程序(纳入计算权重等),需要使用的是monotonic time(单调递增时间),而不是wall time(系统时间),这也就促成了Russ Cox的proposal。

不但如此,程序员还有对于时区、时差、时间精度、起始的谬误。因此,就有人总结出了《程序员认知时间的谬误(Falsehoods programmers believe about time)》 ,我摘抄了部分国内程序员常犯的错误,括号中是我个人对于这些谬误的理解:

  1. 一天总有24小时
  2. 一个月有30或者31天(还有二月份)
  3. 一年有365天 (闰秒导致多一秒,1582年整整消失了10天
  4. 二月份总是28天(有29天)
  5. 24小时是一天、周、月的周期值(对时间取模算天数、我也犯过)
  6. 每年相同月份里,周的起始是相同的
  7. 机器的时区永远是GMT,不,开玩笑的,是机器的时区永远不变(时区的设置可能会被调整)
  8. 系统时钟永远设置的是本地对应的时区的时间
  9. 目标时区和GMT永远有相同的间隔时间(时区会变化,夏令时)
  10. 客户端的时间和服务器时间永远相同
  11. 客户端时间和服务器时间的差值没什么大不了的
  12. 就算CS有差值,总是相同的间隔(客户端可能会动态调整时间)
  13. 服务器和客户端的时间永远在同一个时区
  14. 系统时间不会在5000年前、或者5000年后
  15. 时间没有起点和尽头(千年虫、Unix epoch)
  16. 系统中的一分钟和其他机器上的一分钟应该是一样的(各个机器上CPU频率等问题导致时钟偏移,所以请使用ntp)
  17. 最小的时间单位是秒、呃、毫秒……(最小单位是CPU确定的)
  18. 大家都能明白时间戳格式(1339972628 或者是 133997262837)
  19. 时间戳格式永远是同一个格式(64位和32位就有区别)
  20. 时间戳精度永远相同(float精度问题)
  21. 时间戳的精度保证了可以做uid
  22. 全世界都能明白11/07/05是什么时间格式(年月日显示格式不明确)

我的经历中,见得比较多的是对于时区了解的匮乏,很多程序员并不知道夏令时,甚至有产品或者老板的需求就只是给国人服务,当要全球化时就抓瞎了。当然,最多的还是对闰秒的无视,或者有人根本不知道闰秒的存在。不过,这些bug都因为系统对时间依赖程度不高,或因为系统跑的时间不够长,(没长到碰到闰秒就下线了),所以并没有导致算错帐或者生产事故。不过,这些理由都不能为这种bug开脱,所以程序员要学习东西还是很多的。

p.s. 你在生产系统里碰到过什么关于时间的bug?欢迎讨论~

《思考的技术》读后感

最近读《思考的技术》,摘抄一些重点,梳理一下读后感。

总结:逻辑思考力可以解决问题,预见未来!

科学地思考

怎么做比做到了没有重要,而科学证明结论的顺序是:
收集数据 -> 假设 -> 结论

容易犯错的地方:

  1. 把假设视为结论(不证明)
  2. 仅仅把初步结论或现象视为根本原因或问题所在(缺乏进一步思考,问为什么?)
  3. 信息收集错误(一定要保证数据的正确性)
  4. 全方位的努力是错的。(自身时间、资源有限,要抓重点)
  5. 信息要全面的综合分析,不能仅仅分析数据,要注意其他影响因素。(MECE原则)

如何提建议

  1. 最好要判断题(一个建议就够了)
  2. 要有事实依据
  3. 说明时提出充分必要条件(必要=必须要这么做,充分=这么做会更好)
  4. 前期沟通(感性)、事后追踪效果(理性)。
  5. 从事前工作开始报告,然后简要地得出结论。
  6. 要按对方能理解的顺序讲

提案的构成

  1. 业界的动态 (大趋势)
  2. 其他竞争对手的动态 (对手趋势)
  3. 自身状态分析 (我方状态)
  4. 改善条件
  5. 解决方案
  6. 建议
  7. 计划步骤

思考问题的本质

  1. 5W1H (Who、When、Where、What、Why、How)
  2. 对事不对人
  3. 头脑风暴、否定自己
  4. 思考动机,加以验证

非线性思维

  • 要多问为什么
  • 不相信权威、不盲从
  • 储蓄智慧:与人接触获取知识(杂志、报纸、书、电视、广播、与人交流、互联网、自己思考)
  • 生活简单才能思考:应该思考、彻底思考;不该思考,别浪费精力。以能否规律化为依据。
  • 先分解功能再进行思考。(先将问题简单化条理化)

TCP BBR 测评与开启方法

速率提升490%!
这就是Google在Linux 4.9内核中提交的TCP BBR拥塞控制算法带来的提升.😲

源站点位于香港,我分别从东京, 美国 和深圳进行了下载测试.结果如下图
BBR vs Cubic benchmark

可见开启BBR后,东京的服务器下载速度达到了36MB/s,近乎内网的速度.
对于深圳的提速也十分明显,几乎是之前的4倍.

而BBR的方法也很简单.我以Debian8做例子
在/etc/apt-get/source.list中添加

deb http://http.hk.debian.org/debian sid main non-free contrib

升级和安装新内核

apt-get update && apt-get upgrade

下载新的内核后,在/etc/sysctl.d/local.conf中添加

net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr

根据文件进行开启

sysctl -p /etc/sysctl.d/local.conf

验证

sysctl net.ipv4.tcp_congestion_control

如果返回bbr,就说明成功了.

FPM 应用打包神器

pkg

背景

应用程序写好了以后,需要分发给其他人,这时就需要控制启动脚本啊、配置管理啊,这些事情其实还是需要操作系统提供的rpm、deb、pkg来完成,但是要写rpm-build, dpkg-config。之前写过rpm打包过程,要有自动构建等操作,还需要对文件attr进行控制,各种细节都要写。 累都累死了。
包对安装者很方便,对写包的人太蛋疼,而这就是我们今天的主角要解决的问题。

fpm @Github

真不是php-fpm模块

fpm官方号称支持以下类型打包

  • deb
  • rpm
  • solaris
  • freebsd
  • tar
  • directories
  • Mac OS X .pkg files (osxpkg)
  • pacman (ArchLinux) packages

太赞了!我的主机是Ubuntu(deb),那这次我们的目标就是Redhat的rpm好了!

安装fpm

首先安装fpm,这货依赖ruby的gem,所以

sudo apt-get install ruby ruby-dev rubygems gcc make
sudo gem install --no-ri --no-rdoc fpm

这些都安装好了以后,我们就可以开始打包了。

打包方式

推荐直接构建文件目录的方式,因为这样很简单。简单到你怎么安排文件目录结构,fpm就忠实地从根目录开始还原。例如下面这个结构

 $ mkdir test && cd test && mkdir -p usr/bin && mkdir -p var/log/dummy
$ cd .. && tree test
.
├── usr
│   └── bin
└── var
└── log
└── dummy

执行

fpm -s dir -C test

就是把test当成根目录/ , test/var 就会变成/var

简单吧!

目标包确定

我们这次选择的是rpm,所以-t (type)就指定rpm了。

fpm -s dir -C test -t rpm

接下来要确定包的名字,例如我们这次叫hi,所以-n (name) 就是 hi

$ fpm -s dir -C test -t rpm -n hi
Created package {:path=>"hi-1.0-1.x86_64.rpm"}

执行完了以后,就自动出了一个rpm包。
看看里面有什么

rpm -qlp hi-1.0-1.x86_64.rpm
/usr/bin
/var/log/dummy

Bingo!这样就做好rpm包了。

高级用法

脚本

某些包需要执行一些脚本, 可以通过--after-install --before-install等参数指定.需要注意的是, fpm会自动读取并放在包中.

配置文件

rpm和deb都有自动配置文件管理(自动diff等功能)所以最好一开始就加上所有的配置文件. --config--files 就可以指定了. 不过,deb会默认/etc下的所有文件都是配置.

一些小tips

  • git自动生成版本号 git describe --tags
  • rpm 查询包内容 rpm -qlp *.rpm
  • deb 查询包内容 dpkg -c *.deb
  • logrotate是个好功能 fpm -d logrotate会自动更新配置

2016年终总结

工作

😂这个表情真的代表了我今年的心情,本来以为可以在Glu再多干一年的,结果4月份公司就解散了。加上之前答应老婆去深圳,于是就搬来深圳了。
刚到的那几天就开始面试,结果各个公司都问,为啥不去鹅厂试试,没想到,一面试,就真的进了鹅厂清水衙门TEG😂

6月份入的职,当时就震惊啦,朗科这边不比腾大差啊,超级海景办公室,配合着六月的阳光,那是相当的漂亮。

鹅厂大牛是多,问题也多。不过得到了离职那天再来评论评论。

博客

这里也开始长草了,不过新版的bla已经日趋完善了,希望明年能和川普一样能猛刷博客。

读书

  • 《那些古怪又让人忧心的问题》
  • 《思考的技巧》
  • 《Wireshark网络分析》
  • 《数据科学入门》
  • 《机器学习 在线课程》

自己在AI领域没有优势,今年只是积累了些基础,明年一定要好好在实战中啃下来。
Go语言方面最好能学会用汇编编写,这也是提高性能,深入学习计算机的好办法。

给程序添加Prometheus监控

Demo

最近在编写Bla的时候,发现需要统计平均响应时间,CPU和内存之类的数据,在公司已经用过了metricbeat,感觉还好,但技术栈是Java的,而且内存要求太高了,所以只好换成Golang编写Prometheus做分析数据库。

Prometheus跟Golang官方标准库expvar是属于Pull的监控类型。需要服务器去拉取采集器上面的数据,因此,daemon类的程序(比如说server)最方便的地方就是直接bind一个接口暴露数据就可以了。

代码时间

go get github.com/prometheus/client_golang/prometheus

然后在自己的启动main.go里添加

func init() {
prometheus.MustRegister(httpRequestCount)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":8080", nil)
}
var (
httpRequestCount = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: "http",
Subsystem: "request",
Name: "requests_count",
Help: "The total number of http request",
})
)

代码部分就完成了。
当你需要变更数值时,直接Inc()那个httpRequestCount就好了。

测试一下

curl http://localhost:8080/metrics
# HELP http_request_requests_count The total number of http request
# TYPE http_request_requests_count counter
http_request_requests_count 73

官方对于这些Namespace、Subsystem如何命名有详细的规定,具体的大家自己看就好了。

配置Prometheus

下载安装好Prometheus之后,将刚才我们暴露的metric接口配置进去

 - job_name: "bla"
scrape_interval: "15s"
static_configs:
- targets: ['localhost:9200']

大功告成啦~~

3种优雅的Go channel用法

写Go的人应该都听过Rob Pike的这句话

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

相信很多朋友和我一样,在实际应用中总感觉不到好处,为了用channel而用。但以我的切身体会来说,这是写代码时碰到的场景不复杂、对channel不熟悉导致的,所以希望这篇文章能给大家带来点新思路,对Golang优雅的channel有更深的认识
:)

Fan In/Out

数据的输出有时候需要做扇出/入(Fan
In/Out),但是在函数中调用常常得修改接口,而且上下游对于数据的依赖程度非常高,所以一般使用通过channel进行Fan
In/Out,这样就可以轻易实现类似于shell里的管道。

func fanIn(input1, input2 <-chan string) <-chan string {
c := make(chan string)
go func() {
for {
select {
case s := <-input1: c <- s
case s := <-input2: c <- s
}
}
}()
return c
}

同步Goroutine

两个goroutine之间同步状态,例如A goroutine需要让B goroutine退出,一般做法如下:

func main() {
g = make(chan int)
quit = make(chan bool)
go B()
for i := 0; i < 3; i++ {
g <- i
}
quit <- true // 没办法等待B的退出只能Sleep
fmt.Println("Main quit")
}
func B() {
for {
select {
case i := <-g:
fmt.Println(i + 1)
case <-quit:
fmt.Println("B quit")
return
}
}
}
/*
Output:
1
2
3
Main quit
*/

可是了main函数没办法等待B合适地退出,所以B quit
没办法打印,程序直接退出了。然而,chan是Go里的第一对象,所以可以把chan传入chan中,所以上面的代码可以把quit 定义为chan chan
bool,以此控制两个goroutine的同步

func main() {
g = make(chan int)
quit = make(chan chan bool)
go B()
for i := 0; i < 5; i++ {
g <- i
}
wait := make(chan bool)
quit <- wait
<-wait //这样就可以等待B的退出了
fmt.Println("Main Quit")
}
func B() {
for {
select {
case i := <-g:
fmt.Println(i + 1)
case c := <-quit:
c <- true
fmt.Println("B Quit")
return
}
}
}
/* Output
1
2
3
B Quit
Main Quit
*/

分布式递归调用

在现实生活中,如果你要找美国总统聊天,你会怎么做?第一步打电话给在美国的朋友,然后他们也会发动自己的关系网,再找可能认识美国总统的人,以此类推,直到找到为止。这在Kadmelia分布式系统中也是一样的,如果需要获取目标ID信息,那么就不停地查询,被查询节点就算没有相关信息,也会返回它觉得最近节点,直到找到ID或者等待超时。
好了,这个要用Go来实现怎么做呢?

func recursiveCall(ctx context.Context, id []byte, initialNodes []*node){
seen := map[string]*node{} //已见过的节点记录
request := make(chan *node, 3) //设置请求节点channel
// 输入初始节点
go func() {
for _, n := range initialNodes {
request <- n
}
}()
OUT:
for {
//循环直到找到数据
if data != nil {
return
}
// 在新的请求,超时和上层取消请求中select
select {
case n := <-request:
go func() {
// 发送新的请求
response := s.sendQuery(ctx, n, MethodFindValue, id)
select {
case <-ctx.Done():
case msg :=<-response:
seen[responseToNode(response)] = n //更新已见过的节点信息
// 加载新的节点
for _, rn := range LoadNodeInfoFromByte(msg[PayLoadStart:]) {
mu.Lock()
_, ok := seen[rn.HexID()]
mu.Unlock()
// 见过了,跳过这个节点
if ok {
continue
}
AddNode(rn)
// 将新的节点送入channel
request <- rn
}
}
}
}()
case <-time.After(500 * time.Millisecond):
break OUT // break至外层,否则仅仅是跳至loop外
case <-ctx.Done():
break OUT
}
}
return
}

这时的buffered
channel类似于一个局部queue,对需要的节点进行处理,但这段代码的精妙之处在于,这里的block操作是select的,随时可以取消,而不是要等待或者对queue的长度有认识。

你对这三种channel的用法有什么疑问,欢迎讨论╮(╯▽╰)╭

Golang与树莓派

最近买了个树莓派3b,本来是做下载机用的,但是发现在上面写Go代码,编译,其实和在一般机器上的体验是一样的。

不过树莓派本身有其他电脑没有的玩法,那就是GPIO的支持,配合Go-gpio库,就可以控制这些接口

下面是一个简单的跑马灯+CPU温度探测程序

因为没加散热片……所以温度有点高┐( ̄ヮ ̄)┌

代码如下,根据/sys下的温度文件读数值,另一个是根据负载改变闪烁的频率。

很简单,所以我就不加注释了:)至于为啥叫jurassic,因为侏罗纪公园的电网就是蓝橙指示灯,然后写代码的胖子就被吃掉了

package main
import (
"fmt"
"io/ioutil"
"strconv"
"time"
"runtime"
"github.com/stianeikeland/go-rpio"
"github.com/shirou/gopsutil/load"
)
const (
BLUE = 20
ORANGE = 21
CORE_TEMP_PATH = "/sys/class/thermal/thermal_zone0/temp"
)
func init(){
runtime.GOMAXPROCS(1)
}
func main() {
fmt.Printf("System initial...")
if rpio.Open() == nil{
fmt.Println("[OK]")
} else {
fmt.Println("[ERROR]")
}
defer rpio.Close()
orange := rpio.Pin(ORANGE)
blue := rpio.Pin(BLUE)
orange.Output()
blue.Output()
orange.Low()
blue.High()
for {
stat, err := load.Avg()
if err != nil {
fmt.Println(err)
break
}
interval := int(stat.Load1)
if stat.Load1 < 1 {
interval = 1
}
fmt.Printf("Load1:%.2f Temp:%.2f'C", stat.Load1, loadTemp())
time.Sleep(time.Millisecond * time.Duration(interval * 900))
blue.Toggle()
orange.Toggle()
fmt.Printf("\r")
}
}
func loadTemp() float64 {
b, err := ioutil.ReadFile(CORE_TEMP_PATH)
if err != nil {
return -1000
}
raw, err := strconv.ParseFloat(string(b[:len(b)-2]), 64)
if err != nil {
fmt.Println(err)
return -1001
}
return raw/100
}

小窥Go ast库及初步使用

本文需要你有写Golang代码经验,阅读大概需要20分钟。

最近一直在研究Go的依赖注入(dependency
injection)
,方便日后写比较容易测试的代码(以便偷懒)。目前学到ast解析代码,现拿出来跟大家分享一下:)

Tokenizer 和 Lexical anaylizer

如果你知道tokenizer和lexical anaylizer是什么的话,请跳到下一章,不熟的话请看下面这个最简单的go代码

package main
func main() {
println("Hello, World!")
}

这段go代码做了什么?很简单吧,package是main,定义了个main函数,main函数里调用了println函数,参数是"Hello,
World!"。好,你是知道了,可当你运行go
run时,go怎么知道的?go先要把你的代码打散成自己可以理解的构成部分(token),这一过程就叫tokenize。例如,第一行就被拆成了package和main。
这个阶段,go就像小婴儿只会理解我、要、吃饭等词,但串不成合适句子。因为"吃饭我要"是讲不通的,所以把词按一定的语法串起来的过程就是lexical
anaylize或者parse,简单吧!和人脑不同的是,被程序理解的代码,通常会以abstract syntax
tree(AST)的形式存储起来,方便进行校验和查找。

Go的AST

那我们来看看go的ast库对代码的理解程度是不是小婴儿吧(可运行的源代码在此),其实就是token+parse刚才我们看到的上一章代码,并且按AST的方式打印出来,结果在这里

package main
import (
"go/ast"
"go/parser"
"go/token"
)
func main() {
// 这就是上一章的代码.
src := `
package main
func main() {
println("Hello, World!")
}
`
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
// Print the AST.
ast.Print(fset, f)
}

为了不吓到你,我先只打印前6行:

 0 *ast.File {
1 . Package: 2:1
2 . Name: *ast.Ident {
3 . . NamePos: 2:9
4 . . Name: "main"
5 . }
// 省略之后的50+行

可见,go
解析出了package这个关键词在文本的第二行的第一个(2:1)。“main"也解析出来了,在第二行的第9个字符,但是go的解析器还给它安了一个叫法:ast.Ident,
标示符 或者大家常说的ID,如下图所示:

Ident +------------+
|
Package +-----+ |
v v
package main

接下来我们看看那个main函数被整成了什么样。

 6 . Decls: []ast.Decl (len = 1) {
7 . . 0: *ast.FuncDecl {
8 . . . Name: *ast.Ident {
9 . . . . NamePos: 3:6
10 . . . . Name: "main"
11 . . . . Obj: *ast.Object {
12 . . . . . Kind: func
13 . . . . . Name: "main"
14 . . . . . Decl: *(obj @ 7)

此处func main被解析成ast.FuncDecl(function
declaration),而函数的参数(Params)和函数体(Body)自然也在这个FuncDecl中。Params对应的是*ast.FieldList,顾名思义就是项列表;而由大括号”{}“组成的函数体对应的是ast.BlockStmt(block
statement)。如果不清楚,可以参考下面的图:

 FuncDecl.Params +----------+
|
FuncDecl.Name +--------+ |
v v
+----------------------> func main() {
| +->
FuncDecl ++ FuncDecl.Body +-+ println("Hello, World!")
| +->
+----------------------> }

而对于main函数的函数体中,我们可以看到调用了println函数,在ast中对应的是ExprStmt(Express
Statement),调用函数的表达式对应的是CallExpr(Call
Expression),调用的参数自然不能错过,因为参数只有字符串,所以go把它归为ast.BasicLis (a literal of basic
type)。如下图所示:

+-----+ ExprStmt +---------------+
| |
| CallExpr BasicLit |
| + + |
| v v |
+---> println("Hello, World!")<--+

还有什么?

 50 . Scope: *ast.Scope {
51 . . Objects: map[string]*ast.Object (len = 1) {
52 . . . "main": *(obj @ 11)
53 . . }
54 . }
55 . Unresolved: []*ast.Ident (len = 1) {
56 . . 0: *(obj @ 29)
57 . }
58 }

我们可以看出ast还解析出了函数的作用域,以及作用域对应的对象。

小结

Go将所有可以识别的token抽象成Node,通过interface方式组织在一起,它们之间的关系如下图示意:

监控入门: 收集正确的数据

本文为Datadog《[monitoring 101:collecting the right
data](https://www.datadoghq.com/blog/monitoring-101-collecting-
data/)》的中文翻译,有部分删减。原作者:Alexis Le-Quoc

本文是关于有效地监控系统的系列文章之一。敬请关注本系列的其它文章《有关的,才告警》和《调查性能问题》(译注:后续会翻译)。

监控的数据有多种形式—-
有些系统持续地输出数据而另一些是在罕见事件发生时产出数据。有些数据对于定位问题十分有用;而另一些则在调查问题时起主导地位。(译注:分别对应错误日志,性能日志)。本文涵盖了什么数据需要收集,以及如何对数据进行分类,读完后你就能:

  1. 对于潜在的问题接受有意义的,自动化的警告
  2. 进行快速调查并追查到性能问题的根源

不管你监控的数据形式如何,不变的主题是

收集的监控数据平时是没啥用,但是如果在你需要的时候你却没收集到,那监控数据就是宝贵且昂贵的,因此你需要尽可能地插手一切系统,收集你能收集到的一切的合理的数据。

监控(Metrics)

监控捕获的是系统在特定时间点上的有关数值—-举个例子,当前登入你web应用的用户总数。因此,监控常常设定在每秒、每分或其它常规间隔时间收集一次数据。

在我们的监控框架中有两个重要的分类:工况监控(work metrics)和资源监控(resource
metrics)。每一个系统都是你软件的基础设施,对工况和资源监控都是合理的需求,请把他们都收集起来。

工况指标

工况通过有意义的输出数据,揭示了系统的顶层健康状况。当你给工况指标分类时,将它们分成4小类会相当有帮助:

  • 吞吐量(throughput) 是系统在单位时间内的工作量,常常用绝对值表示。
  • 成功(success) 显示所有工作中成功的百分比。
  • 失败(error) 捕获了失败的结果,常常是单位时间内的失败率或者是由吞吐量归一化(normalized)。失败的结果常常与成功的分别对待,因为它们是潜在的错误来源,可能是更严重的问题,而且处理起来也比成功的结果更加容易。
  • 性能(performance) 监控着组件的工作效率高不高。最常见的性能指标就是完成所有操作需要的时间—-延迟时间(latency)。延迟时间常常用平均数或百分点(percentile)表示,例如:“99%的请求在0.1秒内完成”。

下面是web服务器和数据存储应用中常见的工况指标例子。

Example work metrics: Web server (at time 2015-04-24 08:13:01 UTC)

Subtype Description Value
throughput requests per second (每秒请求数) 312
success percentage of responses that are 2xx since last measurement(距上次测量时2xx返回值的百分比) 99.1
error percentage of responses that are 5xx since last measurement 0.1
performance 90th percentile response time in seconds (90%的请求响应时间) 0.4

Example work metrics: Data store (at time 2015-04-24 08:13:01 UTC)

Subtype Description Value
throughput queries per second 949
success percentage of queries successfully executed since last measurement 100
error percentage of queries yielding exceptions since last measurement 0
error percentage of queries returning stale data since last measurement 4.2
performance 90th percentile query time in seconds 0.02

资源指标

对于其它系统来说,大部分的组件都是你软件基础设施都是它们的资源。有些资源是底层的—-
例如,一台服务器的资源包括的物理组件有CPU,内存,磁盘和网络接口。但是高层组件,如数据库或地理位置服务,也可以看成是其它系统正常工作所需的资源。

资源监控对于调查问题常常是非常有价值的。对于每个资源项,你应该尽力地收集4个关键领域:

  1. 使用率(utilization) 是资源忙/闲时的百分比或者是资源总量的使用率。
  2. 饱和量(saturation) 已经请求的、但是尚未处理的工作,通常是队列。
  3. 错误(errors) 表示的是系统运转过程中也许无法侦测到的内部错误。
  4. 可用率(availability) 表示请求返回总数中正常的百分比。这个指标仅仅在资源可被定期地检查才可以用。

下面是一个对于常见资源类型的资源监控:

Resource Utilization Saturation Errors Availability
Disk IO % time that device was busy wait queue length # device errors % time writable
Memory % of total memory capacity in use swap usage N/A (not usually observable) N/A
Microservice average % time each request-servicing thread was busy #enqueued requests # internal errors such as caught exceptions % time service is reachable
Database average % time each connection was busy # enqueued queries #internal errors, e.g. replication errors % time database is reachable

其它指标

还有一些指标既不是资源也不是工况,但是对于诊断问题根源很有用。常见的例子有缓存的命中率和数据库的锁数量。有疑问时,收集它们。

事件

对于指标的监控或多或少都是持续地进行的。有些监控系统还可以捕获事件:分离的,无规律的,对于了解什么改变了你系统的行为提供了关键的上下文。一些例子:

  • 变更:内部代码发布,构建和构建失败
  • 警告:应用内部产生的警告或者第三方通知
  • 缩扩容: 添加或者减少服务器

一个事件常常携带了足以能解释自己的信息,不像其它数据指标,只能提供一个大概的环境。合理的事件包含了,发生了什么,在什么时候,附带信息。这里有些例子:

What happened Time Additional information
Hotfix f464bfe released to production 2015-05-15 04:13:25 UTC Time elapsed: 1.2 seconds
Pull request 1630 merged 2015-05-19 14:22:20 UTC Commits: ea720d6
Nightly data rollup failed 2015-05-27 00:03:18 UTC Link to logs of failed job

事件有时用来生成警告—-像上表中第三行,有人应该收到通知,关键的任务失败了。但更通常的情况下,事件用来调查跨系统的问题。总之,把事件当指标—-
它们有价值,能收集的尽量收集。

好的监控数据长啥样

得有四个特性:

  • 易懂 你应当能快速地由数据像分析出指标或者捕获的事件。在系统不可用的时候,你估计已经不想猜测你的数据意味着什么了,所以尽量简单明了地保存这些数据,用刚才的概念来命名你的数据。
  • 粒度合适 (granular)如果你设置过长或者过短的收集窗口事件,你很有可能失去关于系统行为的重要数据。例如,平时使用量比较低的系统,在资源100%使用的时候会让你费解。以对系统没有巨大影响的情况下收集数据,否则监控系统就成了"税务"负担(详见observer effect)或者由于过短的收集间隔给数据带来噪音。
  • 按域打标签 (Tagged by scope)每一台你的服务器都是同时属于某个特定域的,你很有可能需要按域查检服务的健康情况,或者是综合起来。例如:生产环境现在的健康状况如何?北美的生产环境呢?特定的软硬件组合的情况下呢?保存数据所属的域是重要的,因为你可能会从任意域收到警告,并能按域快速展开服务不可用的调查,而不仅仅限制于特定机器。
  • 长时间保存。如果你过早地丢弃的,或者为了节省存储费用丢弃了数据,那么你就失去了关于过去的重要信息。保存一年以内的数据,你能知道系统正常情况下是什么样子的,特别是监控数据有每月、每季度、年度的波动时。

为了告警和诊断的数据

下面有张关于不同等级警告的描述和它们的紧急程度。简单来说,低等级的告警时不需要通知任何人的,只需要记录在案以便不时之需。一个通知(notification)等级的警告算是中等级的,可以通过不打断人工作的方式通知,例如邮件或者聊天室。一个Page(译注:寻呼机)等级的告警是十分紧急的,需要立即有人工参与,不管维护的人是在睡觉还是享受个人时光。

Data Alert Trigger
Work metric: Throughput Page value is much higher or lower than usual, or there is an anomalous rate of change
Work metric: Success Page the percentage of work that is successfully processed drops below a threshold
Work metric: Errors Page the error rate exceeds a threshold
Work metric: Performance Page work takes too long to complete (e.g.,performance violates internal SLA)
Resource metric: Utilization Notification approaching critical resource limit (e.g., free disk space drops below a threshold)
Resource metric: Saturation Record number of waiting processes exceeds a threshold
Resource metric: Errors Record number of errors during a fixed period exceeds a threshold
Resource metric: Availability Record the resource is unavailable for a percentage of time that exceeds a threshold
Event: Work-related Page critical work that should have been completed is reported as incomplete or failed

总结:全收了

  • 尽可能多地收集数据,包括工况、资源和各种事件。
  • 以合适的粒度收集数据,以便发现波峰或者波谷。这个粒度取决于你要监控的系统,时常变化的数据—-CPU,内存,能量消耗都是典型的要收集的。
  • 让你的数据发挥最大化的价值,包括标签,事件,完整地保留至少一年