背景:最近实习导师给了一个小需求,编写一个将书籍状态加载到内存中(组里负责小说相关的业务)的服务,用于推荐系统推荐书籍使用。思想非常简单,去数据表里读出所有书籍的状态,然后将bookid通过哈希映射到一个数组里,别人来获取书籍状态时就可以直接访问这个数组,达到O(1)查询的效率。这听起来真是美妙极了。服务发布以后,晚上美滋滋的下班了,谁知当天晚上收到了几十个服务告警,“服务双高”:耗时高,cpu使用率高。告警半夜发到了leader,mentor和其他师兄手机里,这谁顶得住,,,写个破服务,一晚上几十个告警,真是多有打扰,第二天一早赶紧到公司把服务停掉了,终于不再收到告警了,谢天谢地…………
下面进入正题,我们先来了解几个概念:
QPS
Queries Per Second,每秒查询数。每秒能够响应的查询次数。
QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。每秒的响应请求数,也即是最大吞吐能力。
TPS(拿问题去问mentor怎么解,mentor第一句话是,接口tps是多少 ???脑子一片空白,啥是TPS,难怪mentor在我刚来的时候就说本科生就是一张白纸)
Transactions Per Second 的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数,最终利用这些信息作出的评估分。
TPS 的过程包括:客户端请求服务端、服务端内部处理、服务端返回客户端。
例如,访问一个 Index 页面会请求服务器 3 次,包括一次 html,一次 css,一次 js,那么访问这一个页面就会产生一个“T”,产生三个“Q”。
PV(page view)即页面浏览量,通常是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。
PV 即 page view,页面浏览量。用户每一次对网站中的每个页面访问均被记录 1 次。用户对同一页面的多次刷新,访问量累计。
根据这个特性,刷网站的 PV 就很好刷了。
与 PV 相关的还有 RV,即重复访问者数量(repeat visitors)。
UV 访问数(Unique Visitor)指独立访客访问数,统计1天内访问某站点的用户数(以 cookie 为依据),一台电脑终端为一个访客。
IP(Internet Protocol)独立 IP 数,是指 1 天内多少个独立的 IP 浏览了页面,即统计不同的 IP 浏览用户数量。同一 IP 不管访问了几个页面,独立 IP 数均为 1;不同的 IP 浏览页面,计数会加 1。IP 是基于用户广域网 IP 地址来区分不同的访问者的,所以,多个用户(多个局域网 IP)在同一个路由器(同一个广域网 IP)内上网,可能被记录为一个独立 IP 访问者。如果用户不断更换 IP,则有可能被多次统计。
GMV,是 Gross Merchandise Volume 的简称。只要是订单,不管消费者是否付款、卖家是否发货、是否退货,都可放进 GMV 。
RPS 代表吞吐率,即 Requests Per Second 的缩写。吞吐率是服务器并发处理能力的量化描述,单位是 reqs/s,指的是某个并发用户数下单位时间内处理的请求数。
某个并发用户数下单位时间内能处理的最大的请求数,称之为最大吞吐率。
有人把 RPS 说等效于 QPS。其实可以看作同一个统计方式,只是叫法不同而已。RPS/QPS,可以使用 apche ab 工具进行测量。
基本概念有了,我们再来看看cpu方面的基本知识:cpu知识总结
下面我们再来看看几个线上追查问题的常用命令:
如何看查占用cpu最多的进程?
方法一
核心指令:ps
实际命令:
ps H -eo pid,pcpu | sort -nk2 | tail
执行效果如下:
[work@test01 ~]$ ps H -eo pid,pcpu | sort -nk2 | tail
31396 0.6
31396 0.6
31396 0.6
31396 0.6
31396 0.6
31396 0.6
31396 0.6
31396 0.6
30904 1.0
30914 1.0
结果:
瞧见了吧,最耗cpu的pid=30914。
画外音:实际上是31396。
方法二
核心指令:top
实际命令:
top
Shift + t
找到了最耗CPU的进程ID,对应的服务名是什么呢?
方法一
核心指令:ps
实际命令:
ps aux | fgrep pid
执行效果如下:
[work@test01 ~]$ ps aux | fgrep 30914
work 30914 1.0 0.8 309568 71668 ? Sl Feb02 124:44 ./router2 –conf=rs.conf
结果:
瞧见了吧,进程是./router2
方法二
直接查proc即可。
实际命令:
ll /proc/pid
执行效果如下:
[work@test01 ~]$ ll /proc/30914
lrwxrwxrwx 1 work work 0 Feb 10 13:27 cwd -> /home/work/im-env/router2
lrwxrwxrwx 1 work work 0 Feb 10 13:27 exe -> /home/work/im-env/router2/router2
画外音:这个好,全路径都出来了。
如何查看某个端口的连接情况?
方法一
核心指令:netstat
实际命令:
netstat -lap | fgrep port
执行效果如下:
[work@test01 ~]$ netstat -lap | fgrep 22022
tcp 0 0 10.58.xxx.29:22022 *:* LISTEN 31396/imui
tcp 0 0 10.58.xxx.29:22022 10.58.xxx.29:46642 ESTABLISHED 31396/imui
tcp 0 0 10.58.xxx.29:22022 10.58.xxx.29:46640 ESTABLISHED 31396/imui
方法二
核心指令:lsof
实际命令:
lsof -i :port
执行效果如下:
[work@test01 ~]$ /usr/sbin/lsof -i :22022
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
router 30904 work 50u IPv4 69065770 TCP 10.58.xxx.29:46638->10.58.xxx.29:22022 (ESTABLISHED)
router 30904 work 51u IPv4 69065772 TCP 10.58.xxx.29:46639->10.58.xxx.29:22022 (ESTABLISHED)
router 30904 work 52u IPv4 69065774 TCP 10.58.xxx.29:46640->10.58.xxx.29:22022 (ESTABLISHED)
我们再来看一个性能分析的利器:perf
1、perf命令简要介绍
性能调优时,我们通常需要分析查找到程序百分比高的热点代码片段,这便需要使用 perf record 记录单个函数级别的统计信息,并使用 perf report 来显示统计结果;
举例:
sudo perf record -e cpu-clock -g -p 2548 -- sleep 30
-F 表示每秒多少次
-g 选项是告诉perf record额外记录函数的调用关系
-e cpu-clock 指perf record监控的指标为cpu周期
-p 指定需要record的进程pid
sleep 30则是持续30秒
程序运行完之后,perf record会生成一个名为perf.data的文件,如果之前已有,那么之前的perf.data文件会被覆盖
获得这个perf.data文件之后,就需要perf report工具进行查看
perf report -i perf.data
-i 指定要查看的文件
2、使用火焰图展示结果
1、Flame Graph项目位于GitHub上:https://github.com/brendangregg/FlameGraph
2、可以用git将其clone下来:git clone https://github.com/brendangregg/FlameGraph.git
我们以perf为例,看一下flamegraph的使用方法:
1、第一步
$sudo perf record -e cpu-clock -g -p 28591
Ctrl+c结束执行后,在当前目录下会生成采样数据perf.data.
2、第二步
用perf script工具对perf.data进行解析
perf script -i perf.data &> perf.unfold
3、第三步
将perf.unfold中的符号进行折叠:
#./stackcollapse-perf.pl perf.unfold &> perf.folded
4、最后生成svg图:
./flamegraph.pl perf.folded > perf.svg
3、如何看火焰图
火焰图就是看顶层的哪个函数占据的宽度最大。只要有"平顶"(plateaus),就表示该函数可能存在性能问题。
好了,要介绍的性能分析的知识就介绍到这里了,相信还有很多性能分析的方法,这里我也是初步接触了一些,以后再慢慢积累啦!
前面说了一大堆,那最后服务的耗时问题是怎么解决的呢?
就是用perf工具生成火焰图啦,火焰图可以直观地看出哪个函数耗cpu,然后对其进行优化。
网上找的一张火焰图,每一个小长条代表该函数对cpu的消耗情况,小长条上会有相应的函数名。
最后,发现一个接口主要的cpu消耗有三点:特性上报,打印日志,接口返回的map数据的构建。
先介绍一下接口的入参与出参,入参是一个vector,里面存的要查询的bookid,出参是一个map,index为bookid,key为书籍状态。
对于特征上报:原来的写法是每查出一本书的状态就立马上报,而上报的函数会存在锁,导致耗时问题,于是将其优化成:当一批书籍(一个vector请求)过来,把各种状态的书籍数量统计好以后再统一上报,这样就可以避免上报产生的并发问题了。
对于日志,日志之所以耗时也是因为多线程锁,我之前写的代码在for循环里把每一本书籍的状态都打印出来了,这样就会产生大量的并发操作(vector的容量可达到5000),对于这个的优化是没有将每本书的状态给打印出来了,而是再主调方打印出部分书籍状态。
对于返回map数据的优化是新增加了一个返回为vector的接口,这样就可以避免map底层红黑树构建带来的耗时问题,直接返回一个vector里面存着相对应的书籍状态,这就对主调方有要求了,你要想知道这本书的状态,必须知道这是第几本书,才可以去访问vector获取的到。
好了,经过以上几点的优化,cpu的消耗降下来了,接口的耗时也大大降低了,从当初的不稳定(对于同样的数据量有的需要几十毫秒,有的几毫秒就可以了)达到了一个稳定状态(1-2毫秒左右),终于没有收到服务的告警消息了,可以睡个安稳觉了~
通过这次自己开发一个独立的服务,虽然是一个功能非常简单且单一的服务,但却暴露出了许多问题,这个难得机会,特此总结记录一番,前路漫漫,吾将上下而求索!
说明:文章部分内容来源于公众号:架构师之路,程序员共成长以及简书:https://www.jianshu.com/p/655da1d89041?utm_campaign