DevOps实战:Graphite监控上手指南

在本文中,我将提供一个帮助读者了解用Graphite套件创建监控系统所涉及的全部工作的指南。

\u0026#xD;\u0026#xD;

主要讨论内容

\u0026#xD;\u0026#xD;

在本文中我们将会谈及如下用于创建Graphite监控系统的主题:

\u0026#xD;\u0026#xD;
  1. Carbon和Whisper简介\u0026#xD;\t
  2. Whisper存储模式和聚合\u0026#xD;\t
  3. Graphite Web应用\u0026#xD;

前提条件

\u0026#xD;\u0026#xD;

首先,我们需要能够运行Graphite套件的硬件资源。为了简单起见,我将使用Amazon Web Services EC2主机。不过,你也可以使用办公室或家中已有的任何型号的计算机。

\u0026#xD;\u0026#xD;

技术规格:

\u0026#xD;\u0026#xD;
  • 操作系统:Red Hat Enterprise Linux (RHEL) 6.5\u0026#xD;\t
  • 实例类型:m3.xlarge\u0026#xD;\t
  • 弹性存储区块(EBS)容量:250 GB\u0026#xD;\t
  • Python版本:2.6.6\u0026#xD;

Carbon和Whisper简介

\u0026#xD;\u0026#xD;

Graphite由多个后端和前端组件组成。后端组件用于存储数值型的时间序列数据。前端组件则用于获取指标项数据并根据情况渲染图表。在本文中,我首先会介绍后端组件:Carbon和Whisper

\u0026#xD;\u0026#xD;

9be32ed0e3d59b89e1b580f3f25976b4.png\"

\u0026#xD;\u0026#xD;

指标项可以被发布到一个负载均衡器或直接发布到一个Carbon线程中。Carbon线程与Whisper数据库交互,将时间序列数据存储到文件系统中。

\u0026#xD;\u0026#xD;

安装Carbon

\u0026#xD;\u0026#xD;

Carbon实际上是一系列守护进程,组成一个Graphite安装的存储后端。这些守护进程用一个名为Twisted的事件驱动网络引擎监听时间序列数据。Twisted框架让Carbon守护进程能够以很低的开销处理大量的客户端和流量。

\u0026#xD;\u0026#xD;

要安装Carbon,运行如下命令即可(假设目标系统是RHEL操作系统):

\u0026#xD;\u0026#xD;
\u0026#xD;# sudo yum groupinstall \"Development Tools\"\u0026#xD;# sudo yum install python-devel\u0026#xD;# sudo yum install git\u0026#xD;# sudo easy_install pip\u0026#xD;# sudo pip install twisted\u0026#xD;# cd /tmp\u0026#xD;# git clone https://github.com/graphite-project/carbon.git\u0026#xD;# cd /tmp/carbon\u0026#xD;# sudo python setup.py install
\u0026#xD;\u0026#xD;

/opt/graphite文件夹下将包含如下Carbon库和配置文件:

\u0026#xD;\u0026#xD;
\u0026#xD;# ls -l /opt/graphite\u0026#xD;drwxr-xr-x. 2 root root 4096 May 18 23:56 bin\u0026#xD;drwxr-xr-x. 2 root root 4096 May 18 23:56 conf\u0026#xD;drwxr-xr-x. 4 root root 4096 May 18 23:56 lib\u0026#xD;drwxr-xr-x. 6 root root 4096 May 18 23:56 storage
\u0026#xD;\u0026#xD;

bin文件夹下,能够找到如下三种不同类型的Carbon守护进程:

\u0026#xD;\u0026#xD;
  • Cache:接受通过各种协议传输来的指标项数据并以尽可能高的效率将它们写入磁盘;在接收到指标项时,将指标项值缓存在RAM中,并用底层的Whisper库按照指定的时间间隔将这些值写入磁盘。\u0026#xD;\t
  • Relay:有两个不同的用途:将输入的指标项复制并分区。\u0026#xD;\t
  • Aggregator运行于cache前方,在Whisper中记录指标项之前,缓存这些指标项一段时间。\u0026#xD;

安装Whisper

\u0026#xD;\u0026#xD;

Whisper是一个用于存储时间序列数据的数据库,之后应用程序可以用create,update和fetch操作获取并操作这些数据。

\u0026#xD;\u0026#xD;

安装Whisper,需要运行如下命令:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /tmp\u0026#xD;# git clone https://github.com/graphite-project/whisper.git\u0026#xD;# cd /tmp/whisper\u0026#xD;# sudo python setup.py install
\u0026#xD;\u0026#xD;

Whisper脚本现在应该已经相应的位置:

\u0026#xD;\u0026#xD;
\u0026#xD;# ls -l /usr/bin/whisper*\u0026#xD;-rwxr-xr-x. 1 root root 1711 May 19 00:00 /usr/bin/whisper-create.py\u0026#xD;-rwxr-xr-x. 1 root root 2902 May 19 00:00 /usr/bin/whisper-dump.py\u0026#xD;-rwxr-xr-x. 1 root root 1779 May 19 00:00 /usr/bin/whisper-fetch.py\u0026#xD;-rwxr-xr-x. 1 root root 1121 May 19 00:00 /usr/bin/whisper-info.py\u0026#xD;-rwxr-xr-x. 1 root root  674 May 19 00:00 /usr/bin/whisper-merge.py\u0026#xD;-rwxr-xr-x. 1 root root 5982 May 19 00:00 /usr/bin/whisper-resize.py\u0026#xD;-rwxr-xr-x. 1 root root 1060 May 19 00:00 /usr/bin/whisper-set-aggregation-method.py\u0026#xD;-rwxr-xr-x. 1 root root  969 May 19 00:00 /usr/bin/whisper-update.py
\u0026#xD;\u0026#xD;

启动Carbon缓存进程

\u0026#xD;\u0026#xD;

Carbon安装包中包含了关于端口号和其他多个配置参数的明确缺省值。拷贝这些已有的示例配置文件:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /opt/graphite/conf\u0026#xD;# cp aggregation-rules.conf.example aggregation-rules.conf\u0026#xD;# cp blacklist.conf.example blacklist.conf\u0026#xD;# cp carbon.conf.example carbon.conf\u0026#xD;# cp carbon.amqp.conf.example carbon.amqp.conf\u0026#xD;# cp relay-rules.conf.example relay-rules.conf\u0026#xD;# cp rewrite-rules.conf.example rewrite-rules.conf\u0026#xD;# cp storage-schemas.conf.example storage-schemas.conf\u0026#xD;# cp storage-aggregation.conf.example storage-aggregation.conf\u0026#xD;# cp whitelist.conf.example whitelist.conf\u0026#xD;# vi carbon.conf
\u0026#xD;\u0026#xD;

cache区段下,接收端口这一行包含一个默认值,用于通过平文本协议(plaintext protocol 接受输入指标项(如下所示):

\u0026#xD;\u0026#xD;
\u0026#xD;[cache]\u0026#xD;LINE_RECEIVER_INTERFACE = 0.0.0.0\u0026#xD;LINE_RECEIVER_PORT = 2003
\u0026#xD;\u0026#xD;

执行如下命令,可以启动一个carbon-cache进程:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /opt/graphite/bin\u0026#xD;# ./carbon-cache.py start\u0026#xD;Starting carbon-cache (instance a)
\u0026#xD;\u0026#xD;

这个进程现在应该正在监听2003端口:

\u0026#xD;\u0026#xD;
\u0026#xD;# ps -efla | grep carbon-cache\u0026#xD;1 S root      2674     1  0  80   0 - 75916 ep_pol 00:18 ?\u0026#xD;        00:00:03 /usr/bin/python ./carbon-cache.py start\u0026#xD;\u0026#xD;# netstat -nap | grep 2003  tcp    0    0 0.0.0.0:2003                0.0.0.0:*     LISTEN     2674/python 
\u0026#xD;\u0026#xD;

发布指标项

\u0026#xD;\u0026#xD;

指标项(metric 是一种随着时间不断变化的可度量的数量,例如:

\u0026#xD;\u0026#xD;
  • 每秒请求数\u0026#xD;\t
  • 请求处理时间\u0026#xD;\t
  • CPU使用情况\u0026#xD;

A datapoint is a tuple containing:

\u0026#xD;\u0026#xD;

数据点(datapoint是包含如下信息的三元组:

\u0026#xD;\u0026#xD;
  • 指标项名称\u0026#xD;\t
  • 度量值\u0026#xD;\t
  • 时间序列上某个特定的点(通常是一个时间戳)\u0026#xD;

客户端应用程序通过将数据点发送至Carbon进程发布指标项。应用程序在Carbon进程所监听的端口上建立TCP连接,然后以简单平文本格式发送数据点信息。在本文的示例中,这个端口号是2003。这个TCP连接可能会一直处于打开状态并根据需要尽可能多次重复使用。Carbon进程监听输入的数据但是并不会给客户端返回任何响应。

\u0026#xD;\u0026#xD;

数据点的格式定义如下:

\u0026#xD;\u0026#xD;
  • 每个数据点是一行文本\u0026#xD;\t
  • 位置0是带点的指标项名称\u0026#xD;\t
  • 位置1是一个值\u0026#xD;\t
  • 位置2是一个Unix时间戳\u0026#xD;\t
  • 空格作为各个位置之间的分隔符\u0026#xD;

例如,下面是一些有效的数据点:

\u0026#xD;\u0026#xD;
  • carbon-cache进程每分钟接收到的指标项数量\u0026#xD;\t
    • carbon.agents.graphite-tutorial.metricsReceived 28198 1400509108\u0026#xD;\t
    \u0026#xD;\t
  • carbon-cache每分钟创建的指标项数量\u0026#xD;\t
    • carbon.agents.graphite-tutorial.creates 8 1400509110\u0026#xD;\t
    \u0026#xD;\t
  • 示例服务器端点每分钟的p95响应次数\u0026#xD;\t
    • PRODUCTION.host.graphite-tutorial.responseTime.p95 0.10 1400509112\u0026#xD;\t
    \u0026#xD;

客户端应用程序发布指标项的方式可以有多种:

\u0026#xD;\u0026#xD;

为了简单起见,在本文示例中我将通过netcat命令用平文本协议发布指标项。发布如上所列的示例数据点,运行如下命令即可:

\u0026#xD;\u0026#xD;
\u0026#xD;sudo yum install nc\u0026#xD;echo \"carbon.agents.graphite-tutorial.metricsReceived 28198 `date +%s`\" | nc localhost 2003\u0026#xD;echo \"carbon.agents.graphite-tutorial.creates 8 `date +%s`\" | nc localhost 2003\u0026#xD;echo \"PRODUCTION.host.graphite-tutorial.responseTime.p95 0.10 `date +%s`\" | nc localhost 2003
\u0026#xD;\u0026#xD;

carbon-cache的日志文件中将包含接收到的新指标项的有关信息以及存储这些信息的位置:

\u0026#xD;\u0026#xD;
\u0026#xD;# tail -f /opt/graphite/storage/log/carbon-cache/carbon-cache-a/creates.log\u0026#xD;19/05/2014 10:42:44 :: creating database file /opt/graphite/storage/whisper/carbon/agents/graphite-tutorial/metricsReceived.wsp (archive=[(60, 129600)] xff=0.5 agg=average)\u0026#xD;19/05/2014 10:42:53 :: creating database file /opt/graphite/storage/whisper/carbon/agents/graphite-tutorial/creates.wsp (archive=[(60, 129600)] xff=0.5 agg=average)\u0026#xD;19/05/2014 10:42:57 :: creating database file /opt/graphite/storage/whisper/PRODUCTION/host/graphite-tutorial/responseTime/p95.wsp (archive=[(60, 1440)] xff=0.5 agg=average)
\u0026#xD;\u0026#xD;

Carbon与Whisper交互,将这些时间序列数据存储到文件系统中。切换到文件系统相应的位置,确保数据文件已经创建成功:

\u0026#xD;\u0026#xD;
\u0026#xD;# ls -l /opt/graphite/storage/whisper/carbon/agents/graphite-tutorial/\u0026#xD;total 3040\u0026#xD;-rw-r--r--. 1 root root 1555228 May 19 10:42 creates.wsp\u0026#xD;-rw-r--r--. 1 root root 1555228 May 19 10:42 metricsReceived.wsp\u0026#xD;# ls -l /opt/graphite/storage/whisper/PRODUCTION/host/graphite-tutorial/responseTime/\u0026#xD;total 20\u0026#xD;-rw-r--r--. 1 root root 17308 May 19 10:42 p95.wsp
\u0026#xD;\u0026#xD;

最后,你可以用whisper-info脚本获取为这些指标项创建的Whisper文件的元数据信息。

\u0026#xD;\u0026#xD;
\u0026#xD;# whisper-info.py /opt/graphite/storage/whisper/PRODUCTION/host/graphite-tutorial/responseTime/p95.wsp \u0026#xD;maxRetention: 86400\u0026#xD;xFilesFactor: 0.5\u0026#xD;aggregationMethod: average\u0026#xD;fileSize: 17308\u0026#xD;\u0026#xD;Archive 0\u0026#xD;retention: 86400\u0026#xD;secondsPerPoint: 60\u0026#xD;points: 1440\u0026#xD;size: 17280\u0026#xD;offset: 28
\u0026#xD;\u0026#xD;

whisper-dump是一个更完整的脚本,可以输出所有存储保留周期内的原始数据以及Whisper文件的元数据信息。

\u0026#xD;\u0026#xD;
\u0026#xD;# whisper-dump.py /opt/graphite/storage/whisper/PRODUCTION/host/graphite-tutorial/responseTime/p95.wsp \u0026#xD;Meta data:\u0026#xD;  aggregation method: average\u0026#xD;  max retention: 86400\u0026#xD;  xFilesFactor: 0.5\u0026#xD;\u0026#xD;Archive 0 info:\u0026#xD;  offset: 28\u0026#xD;  seconds per point: 60\u0026#xD;  points: 1440\u0026#xD;  retention: 86400\u0026#xD;  size: 17280\u0026#xD;\u0026#xD;Archive 0 data:\u0026#xD;0: 1400609220, 0.1000000000000000055511151231257827\u0026#xD;1: 0,          0\u0026#xD;2: 0,          0\u0026#xD;3: 0,          0\u0026#xD;4: 0,          0\u0026#xD;5: 0,          0\u0026#xD;...\u0026#xD;1437: 0,          0\u0026#xD;1438: 0,          0\u0026#xD;1439: 0,          0
\u0026#xD;\u0026#xD;

理解聚合方法,最大保留期,xFilesFactor和Whisper文件中的其他属性是相当重要的。如果现在你有一点迷茫,也不要过于担心,我将在接下来的章节中详细讨论这些属性。

\u0026#xD;\u0026#xD;

Whisper存储模式和聚合

\u0026#xD;\u0026#xD;

当你或你的开发者同事和系统管理员们开始发布数据点却得到了一些意想不到的结果时,可能会有一些困惑:

\u0026#xD;\u0026#xD;
  • 为什么我的数据点被平均了?\u0026#xD;\t
  • 我已经在周期性地发布数据点了,为什么还是没有任何数据点?\u0026#xD;\t
  • 我已经发布很多天数据点了,为什么我只得到一天的数据?\u0026#xD;

Whisper是如何存储数据的?

\u0026#xD;\u0026#xD;

首先我们需要理解数据是如何在Whisper文件中存储的。当Whisper文件被创建时,将拥有一个固定的文件尺寸,这个尺寸永远不会再改变。在Whisper文件中可能会包含多个用于不同分辨率的数据点的“存储区(bucket”,这些存储区是在配置文件中定义的。

\u0026#xD;\u0026#xD;

例如:

\u0026#xD;\u0026#xD;
  • 存储器A:拥有10秒分辨率的数据点\u0026#xD;\t
  • 存储区B:拥有60秒分辨率的数据点\u0026#xD;\t
  • 存储区C:拥有10分钟分辨率的数据点\u0026#xD;

每个存储区还拥有一个保留期(retention)属性用于标识该存储区中的数据点应该保留的时间长度。例如:

\u0026#xD;\u0026#xD;
  • 存储区A:分辨率为10秒,保留时间6小时的数据点\u0026#xD;\t
  • 存储区B:分辨率为60秒,保留时间1天的数据点\u0026#xD;\t
  • 存储区C:分辨率为10分钟,保留时间7天的数据点\u0026#xD;

根据上述两种信息,Whisper可以进行一些简单的数学计算,计算出在每个存储区中实际需要保存多少数据点。

\u0026#xD;\u0026#xD;
  • 存储区A:6小时 x 60分钟/小时 x 6数据点/分钟 = 2160点\u0026#xD;\t
  • 存储区B:1天 x 24小时/天 x 60分钟/小时 x 1数据点/分钟 = 1440点\u0026#xD;\t
  • 存储区C:7天 x 24小时/天 x 6数据点/小时 = 1008点\u0026#xD;

如果根据这个存储模式配置创建Whisper文件,该文件大小是56KB。如果在这个文件上执行whisper-dump.py脚本,会有如下输出。需要注意的是,一个archive对应一个存储区每点秒数(seconds per point 点数(points 属性则与我们之前的计算相匹配。

\u0026#xD;\u0026#xD;

元数据:

\u0026#xD;\u0026#xD;
\u0026#xD;aggregation method: average\u0026#xD;  max retention: 604800\u0026#xD;  xFilesFactor: 0.5\u0026#xD;\u0026#xD;Archive 0 info:\u0026#xD;  offset: 52\u0026#xD;  seconds per point: 10\u0026#xD;  points: 2160\u0026#xD;  retention: 21600\u0026#xD;  size: 25920\u0026#xD;\u0026#xD;Archive 1 info:\u0026#xD;  offset: 25972\u0026#xD;  seconds per point: 60\u0026#xD;  points: 1440\u0026#xD;  retention: 86400\u0026#xD;  size: 17280\u0026#xD;\u0026#xD;Archive 2 info:\u0026#xD;  offset: 43252\u0026#xD;  seconds per point: 600\u0026#xD;  points: 1008\u0026#xD;  retention: 604800\u0026#xD;  size: 12096
\u0026#xD;\u0026#xD;

关于聚合

\u0026#xD;\u0026#xD;

当数据从一个较高精度的存储区移动到一个较低精度的存储区时,聚合开始发挥作用。让我们以前一个示例中的存储区A和存储区B为例:

\u0026#xD;\u0026#xD;
  • 存储区A:分辨率为10秒,保留时间6小时的数据点(较高精度)\u0026#xD;\t
  • 存储区B:分辨率为60秒,保留时间1天的数据点(较低精度)\u0026#xD;

我们可能有一个每10秒钟发布一个数据点的应用程序。在存储区A中可以找到6小时之内发布的任何数据点。不过,如果我开始查询6小时之前发布的数据点,就可以在存储区B中找到它们。

\u0026#xD;\u0026#xD;

数据点如何移动到存储区B?

\u0026#xD;\u0026#xD;

用高精度值除以低精度值,以确定需要聚合的数据点的数量。

\u0026#xD;\u0026#xD;

l 60秒(存储区B)/10秒(存储区A)= 6个数据点需要聚合

\u0026#xD;\u0026#xD;

注:Whisper需要较高精度的值能够整除较低精度的值(也就是说,相除的结果必须是整数)。否则聚合的结果可能会不准确。

\u0026#xD;\u0026#xD;

聚合数据时,Whisper从存储区A中读取6个10秒数据点,然后将函数应用于这些数据点上,得出一个将被存储在存储区B中的60秒数据点。有5个聚合函数选项:average,sum,max,min和last聚合函数的选择取决于需要处理的数据点。例如,第95百分位的值可能应该用max函数聚合。另一方面,对于计数器来说,sum函数可能更合适。

\u0026#xD;\u0026#xD;

在聚合数据点时,Whisper还处理了xFilesFactor的概念。xFilesFactor表示为了保证聚合准确,一个存储区必须包含的数据点比率。在我们之前的示例中,Whisper确定了它需要聚合6个10秒数据点。由于网络问题,应用重启等原因,可能只有4个数据点有数据而其他2个数据点是空值。

\u0026#xD;\u0026#xD;

如果我们的Whisper文件的xFilesFactor是0.5,这意味着只有存在至少50%的数据点时,Whisper才会聚合数据。如果超过50%的数据点为空时,Whisper会创建一个空值聚合。在我们的例子中,即6个数据点中的4个——也就是66%。聚合函数会被应用在非空数据点上,创建聚合值。

\u0026#xD;\u0026#xD;

你可以将xFilesFactor设置为0到1之间的任意值。值0表示即使只有一个有效数据点,就会执行聚合。值1则表示只有全部的数据点都有效,才会执行聚合。

\u0026#xD;\u0026#xD;

在前一章节中,我们将所有的样例配置文件拷贝到了/opt/graphite/conf 文件夹中。控制Whisper文件如何创建的配置文件如下:

\u0026#xD;\u0026#xD;
  • /opt/graphite/conf/storage-schemas.conf\u0026#xD;\t
  • /opt/graphite/conf/storage-aggregation.conf\u0026#xD;

默认存储模式

\u0026#xD;\u0026#xD;

存储模式(storage-schemas配置文件由多个条目组成,每个条目中包含一个模式,用于匹配指标项名称和保留期定义。默认情况下,包含两个条目:Carbon和全部其他

\u0026#xD;\u0026#xD;

carbon条目匹配以“carbon”字符串开头的指标项名称。默认情况下,Carbon守护进程每60秒发布一次它们自己内部的指标项(这一间隔是可以更改的)。例如,carbon-cache进程会发布指标项,用于标识该进程每分钟创建的指标项文件的数量。保留期的定义则表示数据点每60秒记录一次,并保存90天。

\u0026#xD;\u0026#xD;
\u0026#xD;[carbon]\u0026#xD;pattern = ^carbon.\u0026#xD;retentions = 60s:90d
\u0026#xD;\u0026#xD;

全部其他条目通过指定带星号的模式捕捉全部其他与Carbon无关的指标项。这个保留期的定义表示数据点每60秒记录一次,并保存1天。

\u0026#xD;\u0026#xD;
\u0026#xD;[default_1min_for_1day]\u0026#xD;pattern = .*\u0026#xD;retentions = 60s:1d
\u0026#xD;\u0026#xD;

默认存储聚合

\u0026#xD;\u0026#xD;

storage-aggregation配置文件也是由多个条目组成,其中包括:

\u0026#xD;\u0026#xD;
  • 匹配指标项名称的模式\u0026#xD;\t
  • 一个xFilesFactor值\u0026#xD;\t
  • 一个聚合函数\u0026#xD;

默认情况下,包含4个条目:

\u0026#xD;\u0026#xD;
  • 以.min结尾的指标项\u0026#xD;\t
    • 使用min聚合函数\u0026#xD;\t\t
    • 至少有10%数据点才可以聚合\u0026#xD;\t
    \u0026#xD;\t
  • 以.max结尾的指标项\u0026#xD;\t
    • 使用max聚合函数\u0026#xD;\t\t
    • 至少有10%数据点才可以聚合\u0026#xD;\t
    \u0026#xD;\t
  • 以.max结尾的指标项\u0026#xD;\t
    • 使用sum聚合函数\u0026#xD;\t\t
    • 聚合的前提是至少要有一个数据点\u0026#xD;\t
    \u0026#xD;\t
  • 其他指标项\u0026#xD;\t
    • 使用average聚合函数\u0026#xD;\t\t
    • 至少有10%数据点才可以聚合\u0026#xD;\t
    \u0026#xD;
\u0026#xD;[min]\u0026#xD;pattern = .min$\u0026#xD;xFilesFactor = 0.1\u0026#xD;aggregationMethod = min\u0026#xD;\u0026#xD;[max]\u0026#xD;pattern = .max$\u0026#xD;xFilesFactor = 0.1\u0026#xD;aggregationMethod = max\u0026#xD;\u0026#xD;[sum]\u0026#xD;pattern = .count$\u0026#xD;xFilesFactor = 0\u0026#xD;aggregationMethod = sum\u0026#xD;\u0026#xD;[default_average]\u0026#xD;pattern = .*\u0026#xD;xFilesFactor = 0.5\u0026#xD;aggregationMethod = average
\u0026#xD;\u0026#xD;

在测试环境下,默认的存储模式和存储聚合函数可以很好的完成任务,不过真正应用到生产指标项时,可能还要修改配置文件。

\u0026#xD;\u0026#xD;

存储模式修改

\u0026#xD;\u0026#xD;

首先,我会修改Carbon条目。我希望Carbon每60秒记录一次指标项,并将这些指标项保存180天(6个月)。180天之后,我希望能够以10分钟的精度将这些指标项归档,再保存180天。

\u0026#xD;\u0026#xD;
\u0026#xD;[carbon]\u0026#xD;pattern = ^carbon.\u0026#xD;retentions = 1min:180d,10min:180d
\u0026#xD;\u0026#xD;

在Squarespace,我们用Dropwizard框架构建RESTful的Web Service。在准生产环境和生产环境中,我们运行了许多这样的服务,所有这些服务都使用Dropwizard Metrics库以每10秒一次的速度发布应用和业务指标项。这种10秒一次的数据我会保存3天。3天后,这些数据将被聚合为1分钟数据并保存180天(6个月)。最后,6个月之后,这些数据将被聚合为10分钟数据并再保存180天。

\u0026#xD;\u0026#xD;

注:如果我的指标项库以不同的速度发布数据点,我就需要修改保留的定义以匹配新的速度。

\u0026#xD;\u0026#xD;
\u0026#xD;[production_staging]\u0026#xD;pattern = ^(PRODUCTION|STAGING).*\u0026#xD;retentions = 10s:3d,1min:180d,10min:180d
\u0026#xD;\u0026#xD;

而Carbon,生产环境或准生产环境之外的指标项,可能只是用于测试。我会将这些数据保存1天并且假设他们会每1分钟发布一次。

\u0026#xD;\u0026#xD;
\u0026#xD;[default_1min_for_1day]\u0026#xD;pattern = .*\u0026#xD;retentions = 60s:1d
\u0026#xD;\u0026#xD;

修改存储聚合

\u0026#xD;\u0026#xD;

我会保留默认的存储聚合条目,不过会增加几条新的条目用于以ratio,m1_rate和p95结尾的指标项。

\u0026#xD;\u0026#xD;

注:新增的条目需要添加到default条目之前。

\u0026#xD;\u0026#xD;
\u0026#xD;[ratio]\u0026#xD;pattern = .ratio$\u0026#xD;xFilesFactor = 0.1\u0026#xD;aggregationMethod = average\u0026#xD;\u0026#xD;[m1_rate]\u0026#xD;pattern = .m1_rate$\u0026#xD;xFilesFactor = 0.1\u0026#xD;aggregationMethod = sum\u0026#xD;\u0026#xD;[p95]\u0026#xD;pattern = .p95$\u0026#xD;xFilesFactor = 0.1\u0026#xD;aggregationMethod = max
\u0026#xD;\u0026#xD;

目前为止,你已经完成了Graphite后端的配置以匹配应用程序发布数据点的速率并且已经完全理解数据点是如何在文件系统中存储的。接下来的一章,我们将尝试用graphite-webapp将这些数据可视化。

\u0026#xD;\u0026#xD;

Graphite Web应用

\u0026#xD;\u0026#xD;

现在,后端组件已经成功启动并运行,并且能够用我们指定的格式存储数值型的时间序列数据,接下来我们将了解Graphite的前端组件。具体说来,我们需要查询并可视化已存储的信息的途径。

\u0026#xD;\u0026#xD;

据其Github描述文件所介绍,Graphite Web应用程序是一个运行在Apache/mod_wsgi下的Django应用程序。一般来说,能够提供如下功能:

\u0026#xD;\u0026#xD;
  • 一个用于获取原始数据并生成图表的基于URL的API端点\u0026#xD;\u0026#xD;\t\u0026#xD;\t\u0026#xD;\t
  • 一个用于导航指标项以及构建并保存仪表盘的用户界面。\u0026#xD;\t\u0026#xD;\t\u0026#xD;

安装迷阵

\u0026#xD;\u0026#xD;

graphite-web的安装真的可以称得上是一个迷阵。我已经多次尝试安装graphite-web——在RHEL,CentOS,Ubuntu和Mac OS X上——而每一次的安装步骤都是有不同的。你可以把它当成一场游戏,享受这个过程,当所有必需的依赖都成功安装完成后,你就知道你已经完成了这个迷阵。

\u0026#xD;\u0026#xD;

RHEL 6.5安装指南:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /tmp\u0026#xD;# git clone https://github.com/graphite-project/graphite-web.git\u0026#xD;# cd /tmp/graphite-web\u0026#xD;# python check-dependencies.py\u0026#xD;[REQUIRED] Unable to import the 'django' module, do you have Django installed for python 2.6.6?\u0026#xD;[REQUIRED] Unable to import the 'pyparsing' module, do you have pyparsing module installed for python 2.6.6?\u0026#xD;[REQUIRED] Unable to import the 'tagging' module, do you have django-tagging installed for python 2.6.6?\u0026#xD;[OPTIONAL] Unable to import the 'memcache' module, do you have python-memcached installed for python 2.6.6? This feature is not required but greatly improves performance.\u0026#xD;[OPTIONAL] Unable to import the 'txamqp' module, this is required if you want to use AMQP as an input to Carbon. Note that txamqp requires python 2.5 or greater.\u0026#xD;[OPTIONAL] Unable to import the 'python-rrdtool' module, this is required for reading RRD.\u0026#xD;3 optional dependencies not met. Please consider the optional items before proceeding.\u0026#xD;3 necessary dependencies not met. Graphite will not function until these dependencies are fulfilled.
\u0026#xD;\u0026#xD;

目标是保证至少所有必需的依赖都安装成功。如果计划使用AMQ功能或Memcache的缓存功能,就还需要安装可选依赖。

\u0026#xD;\u0026#xD;
\u0026#xD;# sudo yum install cairo-devel\u0026#xD;# sudo yum install pycairo-devel\u0026#xD;# sudo pip install django\u0026#xD;# sudo pip install pyparsing\u0026#xD;# sudo pip install django-tagging\u0026#xD;# sudo pip install python-memcached\u0026#xD;# sudo pip install txamqp\u0026#xD;# sudo pip install pytz\u0026#xD;# cd /tmp/graphite-web\u0026#xD;# python check-dependencies.py\u0026#xD;[OPTIONAL] Unable to import the 'python-rrdtool' module, this is required for reading RRD.\u0026#xD;1 optional dependencies not met. Please consider the optional items before proceeding.\u0026#xD;All necessary dependencies are met.
\u0026#xD;\u0026#xD;

我已经安装了很多包,满足必需依赖的要求后,就可以开始安装graphite-web:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /tmp/graphite-web\u0026#xD;# sudo python setup.py install\u0026#xD;# ls -l /opt/graphite/webapp/\u0026#xD;total 12\u0026#xD;drwxr-xr-x.  6 root root 4096 May 23 14:33 content\u0026#xD;drwxr-xr-x. 15 root root 4096 May 23 14:33 graphite\u0026#xD;-rw-r--r--.  1 root root  280 May 23 14:33 graphite_web-0.10.0_alpha-py2.6.egg-info
\u0026#xD;\u0026#xD;

安装脚本会将web应用文件移动到/opt/graphite/webapp文件夹下适当的位置。

\u0026#xD;\u0026#xD;

数据库初始化

\u0026#xD;\u0026#xD;

web应用程序维护了一个内部数据库用于保存用户信息和仪表盘。运行如下命令初始化该数据库:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /opt/graphite\u0026#xD;# export PYTHONPATH=$PYTHONPATH:`pwd`/webapp\u0026#xD;# django-admin.py syncdb --settings=graphite.settings\u0026#xD;You just installed Django's auth system, which means you don't have any superusers defined.\u0026#xD;Would you like to create one now? (yes/no): yes\u0026#xD;Username (leave blank to use 'root'): feangulo\u0026#xD;Email address: feangulo@yaipan.com\u0026#xD;Password: \u0026#xD;Password (again): \u0026#xD;Error: Blank passwords aren't allowed.\u0026#xD;Password: \u0026#xD;Password (again): \u0026#xD;Superuser created successfully.\u0026#xD;Installing custom SQL ...\u0026#xD;Installing indexes ...\u0026#xD;Installed 0 object(s) from 0 fixture(s)
\u0026#xD;\u0026#xD;

上述命令将创建一个新的数据库并将其保存在/opt/graphite/storage文件夹下:

\u0026#xD;\u0026#xD;
\u0026#xD;# ls -l /opt/graphite/storage/graphite.db \u0026#xD;-rw-r--r--. 1 root root 74752 May 23 14:46 /opt/graphite/storage/graphite.db
\u0026#xD;\u0026#xD;

Graphite Web应用设置

\u0026#xD;\u0026#xD;

包含graphite-webapp设置的配置文件位于/opt/graphite/webapp/graphit文件夹下。将样例配置文件拷贝到该文件夹下:

\u0026#xD;\u0026#xD;
\u0026#xD;# vi /opt/graphite/webapp/graphite/local_settings.py\u0026#xD;#########################\u0026#xD;# General Configuration #\u0026#xD;#########################\u0026#xD;TIME_ZONE = 'UTC'\u0026#xD;##########################\u0026#xD;# Database Configuration #\u0026#xD;##########################\u0026#xD;DATABASES = {\u0026#xD;    'default': {\u0026#xD;        'NAME': '/opt/graphite/storage/graphite.db',\u0026#xD;        'ENGINE': 'django.db.backends.sqlite3',\u0026#xD;        'USER': '',\u0026#xD;        'PASSWORD': '',\u0026#xD;        'HOST': '',\u0026#xD;        'PORT': ''\u0026#xD;    }\u0026#xD;}
\u0026#xD;\u0026#xD;

到现在为止,如果你遵循前述章节的指令,现在只会有一个运行在2003端口上的carbon-cache进程和一个7002查询端口。这些是默认情况下graphite-webapp所需的端口。因此,配置文件无需任何修改。

\u0026#xD;\u0026#xD;
\u0026#xD;# ps -efla | grep carbon-cache\u0026#xD;1 S root     14101     1  0  80   0 - 75955 ep_pol May20 ?        00:00:26 /usr/bin/python ./carbon-cache.py start\u0026#xD;# netstat -nap | grep 2003\u0026#xD;tcp        0      0 0.0.0.0:2003                0.0.0.0:*                   LISTEN      14101/python\u0026#xD;# netstat -nap | grep 7002\u0026#xD;tcp        0      0 0.0.0.0:7002                0.0.0.0:*                   LISTEN      14101/python
\u0026#xD;\u0026#xD;

不过,你也可以在设置文件中显式指定从哪个carbon-cache进程读取数据:

\u0026#xD;\u0026#xD;
\u0026#xD;# vi /opt/graphite/webapp/graphite/local_settings.py\u0026#xD;#########################\u0026#xD;# Cluster Configuration #\u0026#xD;#########################\u0026#xD;CARBONLINK_HOSTS = [\"127.0.0.1:7002:a\"]
\u0026#xD;\u0026#xD;

上述代码的含义是我有一个本地运行的名为‘a’的carbon-cache进程,其查询端口设置为7002。查看Carbon配置文件,将会看到如下配置:

\u0026#xD;\u0026#xD;
\u0026#xD;# vi /opt/graphite/conf/carbon.conf\u0026#xD;[cache]\u0026#xD;LINE_RECEIVER_INTERFACE = 0.0.0.0\u0026#xD;LINE_RECEIVER_PORT = 2003\u0026#xD;CACHE_QUERY_INTERFACE = 0.0.0.0\u0026#xD;CACHE_QUERY_PORT = 7002
\u0026#xD;\u0026#xD;

注:‘a’ 是从何而来的呢?是默认分配的名字。如果要定义更多缓存,需要在配置文件中创建新的命名区块。

\u0026#xD;\u0026#xD;
\u0026#xD;[cache:b]\u0026#xD;LINE_RECEIVER_INTERFACE = 0.0.0.0\u0026#xD;LINE_RECEIVER_PORT = 2004\u0026#xD;CACHE_QUERY_INTERFACE = 0.0.0.0\u0026#xD;CACHE_QUERY_PORT = 7003
\u0026#xD;\u0026#xD;

仪表盘和图表模版配置

\u0026#xD;\u0026#xD;

Graphite Web应用中包含默认的仪表盘和图表模版。拷贝样例配置文件:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /opt/graphite/conf\u0026#xD;# cp dashboard.conf.example dashboard.conf\u0026#xD;# cp graphTemplates.conf.example graphTemplates.conf
\u0026#xD;\u0026#xD;

我对仪表盘配置文件作了一些修改,让图表展示区块更大。

\u0026#xD;\u0026#xD;
\u0026#xD;# vi /opt/graphite/conf/dashboard.conf\u0026#xD;[ui]\u0026#xD;default_graph_width = 500\u0026#xD;default_graph_height = 400\u0026#xD;automatic_variants = true\u0026#xD;refresh_interval = 60\u0026#xD;autocomplete_delay = 375\u0026#xD;merge_hover_delay = 750
\u0026#xD;\u0026#xD;

我对默认的图表模版也做了一些修改,让它有一个黑色背景和白色前景。另外我还把字体调小了一些。

\u0026#xD;\u0026#xD;
\u0026#xD;# vi /opt/graphite/conf/graphTemplates.conf\u0026#xD;[default]\u0026#xD;background = black\u0026#xD;foreground = white\u0026#xD;minorLine = grey\u0026#xD;majorLine = rose\u0026#xD;lineColors = blue,green,red,purple,brown,yellow,aqua,grey,magenta,pink,gold,rose\u0026#xD;fontName = Sans\u0026#xD;fontSize = 9\u0026#xD;fontBold = False\u0026#xD;fontItalic = False
\u0026#xD;\u0026#xD;

运行Web应用程序

\u0026#xD;\u0026#xD;

终于,一切准备就绪,可以运行Web应用程序了。我会在8085端口运行这个Web应用,你可以随意设置这个端口号。运行如下命令:

\u0026#xD;\u0026#xD;
\u0026#xD;# cd /opt/graphite\u0026#xD;# PYTHONPATH=`pwd`/storage/whisper ./bin/run-graphite-devel-server.py --port=8085 --libs=`pwd`/webapp /opt/graphite 1\u0026gt;/opt/graphite/storage/log/webapp/process.log 2\u0026gt;\u0026amp;1 \u0026amp;\u0026#xD;# tail -f /opt/graphite/storage/log/webapp/process.log
\u0026#xD;\u0026#xD;

打开一个Web浏览器并输入如下地址http://your-ip:8085。确保Graphite Web应用能够正常加载。用tail命令查看process.log文件的输出,应该可以看到资源的加载和来自于web应用的查询。

\u0026#xD;\u0026#xD;

(点击图片查看大图)

\u0026#xD;\u0026#xD;

012869a3dda8a1b9cd00e1dee5801071.png\"

\u0026#xD;\u0026#xD;

指标项导航

\u0026#xD;\u0026#xD;

在上一章中,我们用netcat命令将几个指标项发布到carbon-cache进程中。具体如下:

\u0026#xD;\u0026#xD;
\u0026#xD;\u0026#xD;carbon.agents.graphite-tutorial.metricsReceived\u0026#xD;carbon.agents.graphite-tutorial.creates\u0026#xD;PRODUCTION.host.graphite-tutorial.responseTime.p95
\u0026#xD;\u0026#xD;

Web应用程序以树状结构展示这些指标项。如果在左侧面板中操作指标树,就可以看到全部的指标项。

\u0026#xD;\u0026#xD;

bfc60a28d13e625e0636887f5da257de.png\"

\u0026#xD;\u0026#xD;

点击任意一个指标项,右边的面板上将绘制出这个指标项的图表(默认情况下是过去24小时的数据)。可以通过面板中图表上方的按钮修改查询的日期范围。

\u0026#xD;\u0026#xD;

(点击图片查看大图)

\u0026#xD;\u0026#xD;

e4d0877e5f60ca4f22b4a6bf1b73bc0a.png\"

\u0026#xD;\u0026#xD;

创建仪表盘

\u0026#xD;\u0026#xD;

默认的视图适于快速浏览指标项并将其可视化。不过如果想构建一个仪表盘,可以将浏览器地址指向http://your-ip:8085/dashboard。该页面的顶栏是另外一种方式的指标项导航。可以用点击选项的方式导航,也可以直接输入文本获取建议。点击某个指标项在底部区域将出现相应的图表区块。继续点击新的指标项,下方的面板中将会显示新增的图块,从而创建一个仪表盘。有时你可能想要在一个图表中展示多个指标项。要实现这个功能,将一个图块拖拽到另一个图块上方,多个指标项就会绘制在同一张图表上。通过图块的拖拽,也可以改变其在布局中的位置。

\u0026#xD;\u0026#xD;

(点击图片查看大图)

\u0026#xD;\u0026#xD;

edb6a234b14302c52d851371f5341d21.png\"

\u0026#xD;\u0026#xD;

用户界面看起来十分简单,不过不要感到灰心。你仍然可以在指标项数据上完成强大的操作。点击某个图表区块,会弹出一个对话框。对话框中展示了正在绘制的指标项列表,你可以直接编辑这些指标项。对话框中还有多个菜单,用于在数据上应用函数,修改可视化界面的外观以及其他许多操作。

\u0026#xD;\u0026#xD;

a9c110e2990cd190d4e7515e2bf44c0b.png\"

\u0026#xD;\u0026#xD;

ac761328abcd24bf83ede43119bbfd4b.png\"

\u0026#xD;\u0026#xD;

1a1ad38add207a8b8ea21044a1a5e4d8.png\"

\u0026#xD;\u0026#xD;

此外,还可以用最顶端的菜单保存仪表盘,加载其他仪表盘,修改当前仪表盘的日期范围,分享仪表盘以及其他一些功能。目前为止我最喜欢的功能是Dashboard -\u0026gt; Edit Dashboard。当我需要创建或修改仪表盘时,这个功能节省了我许多时间。

\u0026#xD;\u0026#xD;

(点击图片查看大图)

\u0026#xD;\u0026#xD;

b3e7e631128af22b89d2da5fe6eb298e.png\"

\u0026#xD;\u0026#xD;

为了举例说明这一功能,我将构建一个用于监控carbon-cache进程的仪表盘。前一章中曾经提到过,Carbon进程会记录内部指标项。相比于手工构建仪表盘,我更喜欢用Edit Dashboard这一功能。

\u0026#xD;\u0026#xD;

28b70e209a3547feda97a15a883d2e33.png\"

\u0026#xD;\u0026#xD;

94156b2663d7166ab8b8166d4c425e04.png\"

\u0026#xD;\u0026#xD;

Edit Dashboard窗口中输入如下代码,即可构建用于监控carbon-cache进程的仪表盘。

\u0026#xD;\u0026#xD;

注:该仪表盘会监控所有正在运行的carbon-cache进程。注意在指标项名称中星号(*)的使用会匹配所有以carbon.agents为前缀的值。

\u0026#xD;\u0026#xD;
\u0026#xD;\u0026#xD;[\u0026#xD;  {\u0026#xD;    \"target\": [\u0026#xD;      \"aliasByNode(carbon.agents.*.metricsReceived,2)\"\u0026#xD;    ],\u0026#xD;    \"title\": \"Carbon Caches - Metrics Received\"\u0026#xD;  },\u0026#xD;  {\u0026#xD;    \"target\": [\u0026#xD;      \"aliasByNode(carbon.agents.*.creates,2)\"\u0026#xD;    ],\u0026#xD;    \"title\": \"Carbon Caches - Create Operations\"\u0026#xD;  },\u0026#xD;  {\u0026#xD;    \"target\": [\u0026#xD;      \"aliasByNode(carbon.agents.*.cpuUsage,2)\"\u0026#xD;    ],\u0026#xD;    \"title\": \"Carbon Caches - CPU Usage\"\u0026#xD;  },\u0026#xD;  {\u0026#xD;    \"target\": [\u0026#xD;      \"aliasByNode(carbon.agents.*.memUsage,2)\"\u0026#xD;    ],\u0026#xD;    \"title\": \"Carbon Caches - Memory Usage\"\u0026#xD;  }\u0026#xD;]
\u0026#xD;\u0026#xD;

更新仪表盘的定义会看到如下界面:

\u0026#xD;\u0026#xD;

(点击图片查看大图)

\u0026#xD;\u0026#xD;

65242c0d08d7eaa938e6b546fe8e2dcb.png\"

\u0026#xD;\u0026#xD;

修改Edit Dashboard对话框中的内容会更新浏览器中的仪表盘。不过这些修改并没有保存到Graphite的内部仪表盘数据库中。继续并保存该仪表盘,以便可以分享并再次打开它。

\u0026#xD;\u0026#xD;

85235c4c3890ad95c0203db9e6583cd1.png\"

\u0026#xD;\u0026#xD;

bab0c866a0f32927cc17f2a4fcd48185.png\"

\u0026#xD;\u0026#xD;

如果要查找仪表盘,打开发现器(Finder

\u0026#xD;\u0026#xD;

df28a778eac69571b26a164bffafb2d6.png\"

\u0026#xD;\u0026#xD;

7dd80df229d0fffe423b65101334c9fe.png\"

\u0026#xD;\u0026#xD;

在Graphite的生产安装环境中,Graphite Caches仪表盘的外观界面更类似下图:

\u0026#xD;\u0026#xD;

35e37f7e9a7a305e8ac6c4501c0b0f96.png\"

\u0026#xD;\u0026#xD;

API才是重点

\u0026#xD;\u0026#xD;

与其他工具一样,Graphite也有一些缺点:例如扩展性不是很好,存储机制并非最优——但是Graphite的API是相当漂亮的。有用户界面的感觉很不错,不过最重要的是通过UI可以完成的工作,完全都可以通过graphite-web API请求来完成。用户可以通过构造一个简单的URL来请求定制化图表。在HTTP GET请求的查询串中指定参数。默认情况下,请求响应会返回一个PNG图片,不过用户也可以指明所需返回的格式——如,JSON数据。

\u0026#xD;\u0026#xD;

样例请求#1:

\u0026#xD;\u0026#xD;

fa79ee388c7e0aa1c62acd0a4e88de5d.png\"

\u0026#xD;\u0026#xD;

样例请求#2:

\u0026#xD;\u0026#xD;

http://your-ip:8085/render?target=carbon.agents.*.cpuUsage\u0026amp;width=500\u0026amp;height=300\u0026amp;from=-12h\u0026amp;until=-5min\u0026amp;format=json

\u0026#xD;\u0026#xD;

(点击图片查看大图)

\u0026#xD;\u0026#xD;

ec8e252075feb046d9e72b87edfce933.png\"

\u0026#xD;\u0026#xD;

Graphite API支持多种显示选项以及遵循简单的函数式语法的数据操作函数。考虑到复杂的表达式和计算过程,函数可以嵌套。查看在线文档以详细了解全部可用的函数:

\u0026#xD;\u0026#xD;

假设我有一个运行在上百台服务器上的应用,每个服务器每10秒钟发布一次各自的p95响应时间。利用API中所提供的函数,我可以对指标项进行加工并构建一个信息化图表:

\u0026#xD;\u0026#xD;
  • averageSeries:计算集合内所有值的平均值\u0026#xD;

我们想要看一下全部p95时延的平均值

\u0026#xD;\u0026#xD;
  • scale: 将一个值乘以一个常量\u0026#xD;

时延是以毫秒为单位记录的,我们想要以秒为单位展示。

\u0026#xD;\u0026#xD;
  • alias: 更改指标项展示的名称\u0026#xD;

我们想要在图表的图例中只展示avg p95,而不是指标项全名

\u0026#xD;\u0026#xD;

作为指标项查询的一部分传递给API的函数参数如下:

\u0026#xD;\u0026#xD;
\u0026#xD;alias(scale(averageSeries(PRODUCTION.host.*.requests.p95),0.001),'avg p95')
\u0026#xD;\u0026#xD;

API将返回如下图表:

\u0026#xD;\u0026#xD;

6946ae7ad08126784c9eb041dbcef39d.png\"

\u0026#xD;\u0026#xD;

恭喜!我们已经完成了Carbon,Whisper和graphite-webapp的安装和配置,指标项的发布和导航以及仪表盘的构建。现在你可以为你的业务和应用指标项构建漂亮的仪表盘。

\u0026#xD;\u0026#xD;

本文是关于Graphite的一篇入门级文章。关于更高级的主题请参考:

\u0026#xD;\u0026#xD;

关于作者

\u0026#xD;\u0026#xD;

9280d3380964c133e5ebd5ed7661860e.png\"

\u0026#xD;\u0026#xD;

Franklin AnguloSquarespace的负责管理构建和维护大型核心后端引擎的团队,Squarespace是一家位于纽约市的网站构建平台。Franklin是一名在带领复杂的跨领域的大型工程项目方面有丰富经验的专业人士。在加入Squarespace之前他曾经是亚马逊的一名资深软件开发工程师,主要负责全球进货物流和亚马逊储物柜计划的路由规划调优,运率购买和容量规划算法。

\u0026#xD;\u0026#xD;\u0026#xD;\u0026#xD;

查看英文原文:Getting Started with Monitoring using Graphite

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

2名名名名名名名名名名名名名名名名名名名

谢谢啊011702

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值