Etsy社区的信仰
如果 Etsy 社区也有信仰,那一定是图表,Ian Malpass 在 Code as Craft 发表的文章中这么描述: 只要是变化的事件,我们就追踪它。有时候,为了记录事件的变化,我们从它不变时就用图表进行记录。通常,我们会从三个层面进行测量:网络、设备以及应用。应用指标往往是这三者中最难测量却又最重要的。应用指标与业务息息相关,随着应用的变化而变化。在此,我们不会早早地规划要测量的所有指标,将它们放在经典的测量管理系统中,我们只会将工程师可能测量或计时的指标以最简便的方式做成图表。(我们可以随时随地修改代码并部署它,因此,测量“ X 的发生频率”,“ X 在过去半小时内的发生情况”,只要有需求,就能很快实现。)
statsD是什么
应用程序的监控是微服务中很重要的一环,监控主要包括四个方面的内容:指标(metrics)的采集、存储、展示以及相应的报警机制。目前相关的解决方案以及工具非常多,今天就介绍一种业界使用比较广泛的方案StatsD.
StatsD最早是2008年 Flickr 公司用 Perl 写的针对 Graphite、datadog 等监控数据后端存储开发的前端网络应用,2011 年 Etsy 公司用 node.js 重构。statsD狭义来讲,包含一个监听UDP(默认)或者TCP的守护程序,以及一套简单的协议。任何udp/tcp客户端都可以根据其协议发送数据到守护进程,守护进程聚合之后定时推送给后端,如graphite和influxdb等。
StatsD by Example
开始探究statsD之前,我们先来看一个简单例子:一个网站的登录过程,鉴权成功则login,鉴权失败则提示错误信息
以下是其中的鉴权函数:
def login(username,password): if password_valid(username,password): render_welcome_page() else: render_error(403)
看到这个函数你第一反应可能就会想知道这个网站的登录频率,因为:1、鉴权对任何一个网站都很重要。2、登录次数的突然改变可能会是一个异常的早期预警(例如,不正确的DNS修改,失效的TLS证书等),让我们修改一下这个函数:
Import statsd
statsd_client = statsd.StatsClient('localhost',8125)
def login(username,password):
statsd_client.incr('login.invocations') if password_valid(username,password): render_welcome_page() else: render_error(403)
以上代码定义了一个stasd client,login函数每次执行的时候,都会将login.invocations这个counter的值加1,我们将程序发布,然后我们可以看到以下的图表:
另外,我们对自己的函数执行耗时会有一个预期,比如,20ms,借助stasd我们可以很方便的验证并监控自己的函数运行时长,我们修改一下代码:
Import statsd
statsd_client = statsd.StatsClient('localhost',8125)
statsd_client.timer('login.time')
def login(username,password):
statsd_client.incr('login.invocations') if password_valid(username,password): render_welcome_page() else: render_error(403)
以上代码会测量login函数的执行时长,然后发送到StatsD Server。我们将程序发布,然后我们可以看到以下的图表:
从以上走势图,我们可以很清楚的看到login函数执行时长在20ms上下波动
StatsD系统包括三部分:客户端(client)、服务器(server)和后端(backend)。客户端植入于应用代码中,将相应的metrics上报给StatsD server。statsd server聚合这些metrics之后,定时发送给backends。backends则负责存储这些时间序列数据,并通过适当的图表工具展示
statsd协议
statsd采用简单的行协议,如下:
bucket: 是一个metric的标识,可以看成一个metric的变量。
value: metrics值,通常是数字。
type: metric的类型,通常有timer、counter、gauge和set四种
sample_rate: 采样率,降低客户端到statsd服务器的带宽。客户端减少数据上报的频率,然后在发送的数据中加入采样频率,如0.1。statsd server收到上报的数据之后,如cnt=10,得知此数据是采样的数据,然后flush的时候,按采样频率恢复数据来发送给backend,即flush的时候,数据为cnt=10/0.1=100,而不是容易误解的10*0.1=1
例子:
statsd.login:10|c
statsd.memory:20|g
statsd.cost:30|ms
statsd.user:1|s
udp和tcp
statsd可配置相应的server为UDP和TCP。默认为UDP。UDP和TCP各有优劣。但 UDP确实是不错的方式。
UDP不需要建立连接,速度很快,不会影响应用程序的性能。
“fire-and-forget”机制,就算statsd server挂了,也不会造成应用程序crash。 当然,UDP更适合于上报频率比较高的场景,就算丢几个包也无所谓,对于一些一天已上报的场景,任何一个丢包都影响很大。另外,对于网络环境比较差的场景,也更适合用TCP,会有相应的重发,确保数据可靠。
Metric type
counter: 计数器
counter类型的指标,用来计数。在一个flush区间,把上报的值累加。值可以是正数或者负数
user.logins:10|c // user.logins + 10
user.logins:-1|c // user.logins - 1
user.logins:10|c|@0.1 // user.logins + 100
// users.logins = 10-1+100=109
gauge:标量(提到counter后面)
gauge是任意的一维标量值(如:内存、身高等值)。gague值不会像其它类型会在flush的时候清零,而是保持原有值。statsd只会将flush区间内最后一个值发到后端。另外,如果数值前加符号,会与前一个值累加。
age:10|g // age 为 10
age:+1|g // age 为 10 + 1 = 11
age:-1|g // age为 11 - 1 = 10
age:5|g // age为5,替代前一个值
注:gauge通常是在client进行统计好在发给StatsD的,如capacity:100|g 这样的gauge,即使我们发送多次,在StatsD里面,也只会保存100
Timer: 计时器
timers用来记录一个操作的耗时,单位ms。statsd会记录平均值(mean)、最大值(upper)、最小值(lower)、累加值(sum)、平方和(sum_squares)、个数(count)以及部分百分值
rpt:100|ms
如下是在一个flush期间,发送了一个rpt的timer值100。以下是记录的值
count_80: 1,
mean_80: 100,
upper_80: 100,
sum_80: 100,
sum_squares_80: 10000,
std: 0,
upper: 100,
lower: 100,
count: 1,
count_ps: 0.1,
sum: 100,
sum_squares: 10000,
mean: 100,
median: 100
percentThreshold
对于timer数据,除了正常的统计之外还会计算一个百分比的数据(过滤掉峰值数据),默认是90%。可以通过percentThreshold修改这个值或配置多个值。例如在config.js中配置: percentThreshold: [50, 80]
以90为例,statsd会把一个flush期间上报的数据,去掉10%的峰值,即按大小取cnt*90%(四舍五入)个值来计算百分值。假如10s内上报以下10个值:
1,3,5,7,13,9,11,2,4,8
则只取10*90%=9个值,则去掉13。百分值即按剩下的9个值来计算
$KEY.mean_90 // (1+3+5+7+9+2+11+4+8)/9
$KEY.upper_90 // 11
$KEY.lower_90 // 1
histogram
有时仅记录操作的耗时并不能让我们很好的知道当前系统的情况,通常,timing都是跟histogram一起来使用的。在config.js配置文件中设置:
histogram: [ { metric: '', bins: [10, 100, 1000, 'inf']} ]
这样就开启了histogram,这个histogram的bin的间隔是[0, 10ms),[10ms - 100ms), [100ms - 1000ms), 以及[1000ms, +inf),如果一个timing落在了某个bin里面,相应的bin的计数就加1,譬如:
foo:1|ms
foo:100|ms
foo:1|ms
foo:1000|ms
那么statsd最终flush输出时:
histogram: { bin_10: 2, bin_100: 0, bin_1000: 1, bin_inf: 1 } } }
sets
记录flush期间,不重复的值。可以用来计算某个metric unique事件的个数,譬如对于一个接口,可能我们想知道有多少个user访问了,我们可以这样
request:1|s // user 1
request:2|s // user1 user2
request:1|s // user1 user2
StatsD就会展示这个request metric只有1,2两个用户访问了。
Multi-Metric Packets
statsD支持在一个packet里存放多个metric,以换行符分隔
gorets:1|c\nglork:320|ms\ngaugor:333|g\nuniques:765|s
Note:Be careful to keep the total length of the payload within your network's MTU. There is no single good value to use, but here are some guidelines for common network scenarios:
Fast Ethernet (1432) - This is most likely for Intranets.
Gigabit Ethernet (8932) - Jumbo frames can make use of this feature much more efficient.
Commodity Internet (512) - If you are routing over the internet a value in this range will be reasonable. You might be able to go higher, but you are at the mercy of all the hops in your route.
(These payload numbers take into account the maximum IP + UDP header sizes)
flush interval
statsd 默认是10s执行一次flush。可通过flushInterval设置,单位ms
flushInterval: 20000. //设为20s
reload 配置
设置automaticConfigReload,watch配置文件,如果修改,即reload配置文件。默认为true
delete系列配置
metric上报时,每次flush之后,就会重置为0(gauge是保持原有值)。如果不上报这些空闲值,可以通过delete*来设置。
deleteGuages: true,
deleteTimers: true,
deleteSets: true,
deleteCounters: true
StatsD Cluster Proxy
StatsD Cluster Proxy是个基于udp的proxy,它放置于多个statsD实例之前,它通过一致性哈希算法将一个metric发送到同一个StatsD实例,以便正确的聚合。同时它也完成简单的健康检测,判断某个实例是否掉线。
cp exampleProxyConfig.js proxyConfig.js
node proxy.js proxyConfig.js
statsd的安装非常简单。可选择两种方式:克隆源码和docker。
安装
克隆源码
首先需要安装node环境,然后到github克隆代码,修改相关配置启动即可。
cd /usr/local
git clone https://github.com/statsd/statsd.git
cd statsd
cp exampleConfig.js config.js
node /usr/local/statsd/stats.js /usr/local/statsd/config.js
Docker
statsd支持以下两种方式
The official docker image on docker hub
Building the image from the bundled Dockerfile
配置
statsd提供默认的配置文件exampleConfig.js。可以参考相应的注释按需配置,接下来将简单介绍一些配置项。
{ port:8125,//statsd 服务端口
backends:["./backends/console"],
console:{ prettyprint:true },
flushInterval:30000,//statsd flush时间
percentThreshold: [80,90]
}
{ port: 8125,
graphitePort: 2003,
graphiteHost: "graphite.example.com",
backends: [ "./backends/graphite" ]
}
注:在statsd目录下,backends中包含了默认的后端:graphite和console。
使用
配置statsd的backends为console进行调试,启动statsd。然后使用netcat发送数据进行测试
echo "test:9|ms" | nc -w 1 -u 127.0.0.1 8125
注意:netcat -w参数表示超时时间,单位是秒
根据配置的flushinterval,statsd服务端会定时的将聚合好的数据发送到backends,如果配置的后端是console,则会输出:
Flushing stats at Fri Aug 26 2022 14:12:34 GMT+0800 (中国标准时间)
{
counters: {
'statsd.bad_lines_seen': 0,
'statsd.packets_received': 1,
'statsd.metrics_received': 1
},
timers: { test: [ 9 ] },
gauges: { 'statsd.timestamp_lag': 0 },
timer_data: {
test: {
count_80: 1,
mean_80: 9,
upper_80: 9,
sum_80: 9,
sum_squares_80: 81,
count_90: 1,
mean_90: 9,
upper_90: 9,
sum_90: 9,
sum_squares_90: 81,
std: 0,
upper: 9,
lower: 9,
count: 1,
count_ps: 0.1,
sum: 9,
sum_squares: 81,
mean: 9,
median: 9
}
},
counter_rates: {
'statsd.bad_lines_seen': 0,
'statsd.packets_received': 0.1,
'statsd.metrics_received': 0.1
},
sets: {},
pctThreshold: [ 80, 90 ]
}
Flushing stats at Fri Aug 26 2022 14:12:44 GMT+0800 (中国标准时间)
{
counters: {
'statsd.bad_lines_seen': 0,
'statsd.packets_received': 0,
'statsd.metrics_received': 0
},
timers: { test: [] },
gauges: { 'statsd.timestamp_lag': 0 },
timer_data: { test: { count_ps: 0, count: 0 } },
counter_rates: {
'statsd.bad_lines_seen': 0,
'statsd.packets_received': 0,
'statsd.metrics_received': 0
},
sets: {},
pctThreshold: [ 80, 90 ]
}
backends
StatsD支持可插拔backends,安装包中默认带有graphite后端。可以将其他后端作为简单的npm软件包进行分发和安装:
amqp-backend ,
ganglia-backend ,
librato-backend ,
socket.io-backend ,
statsd-backend ,
mongo-backend ,
mysql-backend ,
datadog-backend ,
monitis backend,
instrumental backend ,
hosted graphite backend ,
statsd aggregation backend ,
zabbix-backend ,
opentsdb backend ,
influxdb backend ,
stackdriver backend ,
couchdb-backend ,
elasticsearch backend ,
Google BigQuery backend,
服务端实现
StatsD最初由Etsy的Erik Kastner编写,它基于Flickr的想法以及Cal Henderson的这篇文章:Counting and Timing。2011年该服务器用Nodejs编写重写,但是从那时起已经有其他语言的实现:
brubeck - Server in C ,
clj-statsd-svr — Clojure server ,
gographite — Server in Go ,
gostatsd — Server in Go ,
netdata - Embedded statsd server in the netdata server, in C, with visualization, Net::Statsd::Server — Perl server, also available on CPAN ,
Py-Statsd — Server and Client ,
Ruby-Statsdserver — Ruby server ,
statsd-c — Server in C ,
statsdaemon (bitly) — Server in Go ,
statsdaemon (vimeo) — Server in Go,
statsdcc - Server in C++ ,
statsdpy — Python/eventlet ,
Server Statsify - Server in C# ,
statsite — Server in C ,
bioyino — High performance multithreaded server written in Rust
客户端实现
客户端主要是根据statsd协议,通过UDP/TCP向守护进程通信。常见的实现有:
Node
lynx — Node.js client used by Mozilla, Nodejitsu, etc.
Node-Statsd — Node.js client
node-statsd-client — Node.js client
node-statsd-instrument — Node.js client
statistik - Node.js client with timers & CLI
statsy - clean idiomatic statsd client
Java
java-statsd-client — Lightweight (zero deps) Java client
Statsd over SLF4J — Java client with SLF4J logging tie-in
play-statsd — Play Framework 2.0 client for Java and Scala
statsd-netty — Netty-based Java 8 client
Python
Py-Statsd — Server and Client
Python-Statsd — Python client
pystatsd — Python client
Django-Statsd — Django client
Ruby
statsd-instrument — Ruby client
statsd — Ruby client (needs new maintainer)
Statsd-Client — Ruby client (not maintained)
C
C client — A trivial C client
Cpp
statsd-client-cpp — StatsD Client in CPP
cpp-statsd-client — A header-only StatsD client implemented in C++
.net
NStatsD.Client — .NET 4.0 client
graphite-client — .NET client library for StatsD and Graphite
StatsC — An asynchronous client with built-in support for batching
JustEat.StatsD — A .NET library for publishing metrics to statsd. Targets both .NET full framework and .NET Standard 2.0.
Statsify - .NET client
statsD接收client发送过来的metrics,进行聚合之后,发送到后端
statsD可以进行程序性能指标和业务指标的收集,监控
statsD默认支持graphite作为backend,但同时支持很多第三方backends
基于statsD可以方便的做一些适合自己业务场景的定制化开发,正所谓源码在手,天下我
七、例子
开发一个sdk获取操作系统的一些性能指标,包括cpu,free memory,procs,uptime等参数,以下是一些采集代码:
app采集操作系统的一些参数指标,通过statsD client发送给statsD,statsD经过聚合之后,发送到console,打印出来: