第三章:服务器性能剖析

​ 本章将针对如下三个问题进行解答:

​ 如何确认服务器是否达到了性能最佳的状态

​ 找出某条语句为什么执行不够快

​ 诊断被用户描述成“停顿”,“堆积”,“卡死”的某些间歇性疑难故障

1.性能优化简介:

​ 针对性能问题,1000个DBA,有1000个回答。诸如,“QPS”,"CPU Load",“可扩展性”之类。

原则一:我们将性能定义为完成某件任务所需要的时间度量。即:“性能就是响应时间” 。

​ 通过任务和时间来度量性能而不是资源。数据库的目的是执行SQL语句,上句中的“任务”即是查询或者SQL语句(SELECT UPDATE DELETE等等),综上,数据库服务器的性能用查询的响应时间度量,单位则是每个查询所花费的时间。在这里我们先假设性能优化就是在一定工作负载的情况下尽可能的降低响应时间。

​ CPU使用率只是一种现象,而不是很好的可度量的目标。

​ 吞吐量的提升可以看作性能优化的副产品,对查询的优化可以让服务器每秒执行更多的查询。因为每秒查询执行的时间更短了。(吞吐量即是性能的倒数:单位时间内查询的数量,QPS,QPM等等)

原则二:无法测量就无法有效地优化。第一步应当测量时间花在什么地方。

​ 完成一项任务所需要的时间可以分成两部分:执行时间和等待时间。

​ 如果要优化任务的执行时间,最好的办法是通过测量定位不同的子任务花费的时间,然后优化去掉一些子任务,降低子任务的执行频率或者提升子任务的执行效率。

​ 如果要优化任务的等待时间,则相对复杂一些,等待可能是因为其他系统间接影响所致。

1.1.通过性能剖析进行优化。

​ 性能剖析(Profiling)分为两个步骤:先测量任务所花费的时间,然后对结果进行统计和排序,讲重要的任务排到前面。

​ 性能剖析报告会列出所有任务列表。每行记录一个任务,包括任务名,任务的执行时间,任务的消耗时间,任务的平均时间,以及该任务执行时间占全部时间的百分比。

​ 基于执行时间的分析研究的是什么任务的执行时间最长。

​ 基于等待的分析则是判断任务在什么阶段被阻塞的时间最长。

​ 事实上,当基于执行时间的分析发现一个任务需要花费时间花费太多时间的时候,应该深入去分析一下,可能会发现某些“执行时间”其实是在等待。

1.2.理解性能剖析

​ 尽管性能剖析输出了排名,总计和平均值,但还是有许多重要的信息是缺失的。

​ 值得优化的查询:一些只占总响应时间比重很小的查询是不值得优化的。如果花费了1000美元去优化一个任务,单业务的收入没有任何增加,那么可以说是打水漂了。如果优化的成本大于收益,就应当停止优化。

​ 异常情况:某些任务即使没有出现在性能剖析输出的前面也需要优化。比如,某些任务执行次数很少,单每次执行都非常慢,严重影响用户体验。

​ 被掩藏的细节:性能剖析无法显示所有响应时间的冯部,只相信平均值是非常危险的。正如医院内所有病人的平均体温一样毫无意义。

2.对应用系统进行性能剖析

​ 实际上,剖析应用程序一般比剖析数据库服务器更容易,且回报率更高。建议对系统进行自上而下的性能分析,这样可以追踪自用户发起到服务器响应的整个流程,虽然性能问题大多数情况下都和数据库有关,但是应用导致的问题也不少。

# 应该尽可能地测量一切可以测量的地方,并且接受这些测量带来的额外开销。
# Oracle 的性能优化大师Tom Kyte 曾被问到Oracle中的测量点开销,他的回答是,测量点至少为性能优化共享10%
# 大多数应用并不需要每天都运行详尽的性能测量,所以实际上贡献至少超过10%

3.剖析MySQL查询

3.1剖析服务器负载

​ MySQL的每一个新版本都增加了更多的可测量点。但是如果只是需要剖析并找出和代价高的查询,慢查询日志应该就能满足我们的需求。可以通过将"long_query_time"设为0来捕获所有的查询,并且查询的响应时间已经可以做到微秒级 。在当前的版本中,慢日志是开销最低,同时精度最高的测量查询时间的工具。如果长期开启慢查询日志,主要要配合logrotate工具一同使用(
使用logrotate工具切割MySQL日志与慢日志分析发送到邮箱
)。Percona分支的MySQL比起官方社区版本记录了更多更有价值的信息。如查询计划,锁,I/O活动等。总的来说,慢日志是一种轻量且全面的性能剖析工具。

​ 可以使用pt-query-digest分析慢查询日志,如下图:

pt-query-digest slow.log >slow_log_analyze.log
    /data/mysql/3109/slow.log:  53% 00:25 remain
    /data/mysql/3109/slow.log:  98% 00:00 remain

cat slow_log_analyze.log
# 75.3s user time, 2s system time, 41.28M rss, 235.76M vsz
# Current date: Sun Feb 25 15:43:11 2018
# Hostname: MySQL-Cent7-IP001109
# Files: /data/mysql/3109/slow.log
# Overall: 445.27k total, 59 unique, 0.03 QPS, 0.04x concurrency _________
# Time range: 2017-09-28T16:00:25 to 2018-02-25T07:27:18
# Attribute          total     min     max     avg     95%  stddev  median
# ============     ======= ======= ======= ======= ======= ======= =======
# Exec time        461284s   100ms    150s      1s      3s      1s   740ms
# Lock time          1154s       0     10s     3ms    57us    83ms    21us
# Rows sent        426.70M       0   9.54M 1004.84   97.36  76.26k    0.99
# Rows examine     465.04M       0   9.54M   1.07k  299.03  76.26k    0.99
# Query size         4.55G       6 1022.79k  10.71k   76.28  73.23k   36.69

# Profile
# Rank Query ID           Response time     Calls  R/Call  V/M   Item
# ==== ================== ================= ====== ======= ===== =========
#    1 0x558CAEF5F387E929 238431.3966 51.7% 294383  0.8099  0.62 SELECT sbtest?
#    2 0x84D1DEE77FA8D4C3  53638.8398 11.6%  33446  1.6037  1.14 SELECT sbtest?
#    3 0x3821AE1F716D5205  53362.1845 11.6%  33504  1.5927  1.11 SELECT sbtest?
#    4 0x737F39F04B198EF6  53244.4816 11.5%  33378  1.5952  1.14 SELECT sbtest?
#    5 0x6EEB1BFDCCF4EBCD  53036.2877 11.5%  33539  1.5813  1.10 SELECT sbtest?
#    6 0x67A347A2812914DF   2619.2344  0.6%    200 13.0962 67.98 SELECT tpcc?.order_line
#    7 0x28FC5B5D583E2DA6   2377.9580  0.5%    215 11.0603 11.53 SHOW GLOBAL STATUS
#   10 0xE730A9F41A4AB139    259.9002  0.1%    355  0.7321  0.42 SHOW INNODB STATUS
#   11 0x88901A51719CB50B    131.1035  0.0%     39  3.3616 21.74 SELECT information_schema.tables
#   12 0x16F46891A99F2C89    127.1865  0.0%     88  1.4453  1.15 SELECT performance_schema.events_statements_history
#   14 0x153F1CE7D660AE82     79.2867  0.0%     46  1.7236  1.47 SELECT information_schema.processlist
# MISC 0xMISC               3976.0946  0.9%  16077  0.2473   0.0 <47 ITEMS>

# Query 1: 0.17 QPS, 0.14x concurrency, ID 0x558CAEF5F387E929 at byte 4877477857
# This item is included in the report because it matches --limit.
# Scores: V/M = 0.62
# Time range: 2018-02-03T11:26:24 to 2018-02-23T13:03:23
# Attribute    pct   total     min     max     avg     95%  stddev  median
:

​ 除了慢日志之外Percona Toolkit工具包中的pt-query-digest工具也可以进行剖析,使用--processlist参数可以不断的分析"show processlist"的输出。但是“show processlist”的输出瞬息万变。即使每秒收集一次仍会遗漏很多有用的信息,因此并不十分推荐这种方式。另一种方式是使用--type=tcpdump选项对网络抓包数据进行分析。

3.2 剖析单条查询

使用SHOW PROFILE

​ 默认禁用,但它是会话级别的参数。set profiling=1 ,然后在服务器上直送所有的语句,都会测量其耗费的时间和其他一些查询执行状态变更相关的数据。

​ 当一条查询提交给服务器时,此工具会记录剖析信息到一张临时表,并给查询赋一个从1开始的整数标识符。

​ 如:

set profiling=1
select * from t_Order;
select * from t_Product
show profiles
+----------+------------+-------------------------+
| Query_ID | Duration   | Query                   |
+----------+------------+-------------------------+
| 1        | 9.75e-05   | SHOW WARNINGS           |
| 2        | 0.00052075 | select * from t_order   |
| 3        | 0.000511   | select * from t_product |
| 4        | 5.3e-05    | SHOW WARNINGS           |
+----------+------------+-------------------------+
show profile for query 3
+----------------------+----------+
| Status               | Duration |
+----------------------+----------+
| starting             | 0.000065 |
| checking permissions | 0.000009 |
| Opening tables       | 0.000142 |
| init                 | 0.000022 |
| System lock          | 0.000010 |
| optimizing           | 0.000008 |
| statistics           | 0.000013 |
| preparing            | 0.000012 |
| executing            | 0.000007 |
| Sending data         | 0.000154 |
| end                  | 0.000010 |
| query end            | 0.000011 |
| closing tables       | 0.000010 |
| freeing items        | 0.000016 |
| cleaning up          | 0.000012 |
+----------------------+----------+

​ 剖析报告给出了查询执行的每个步骤及其花费的时间,看结果很难快速地确定哪个步骤花费的时间最多。输出是按照执行顺序进行排序,而不是按照花费的时间排序的。下面给出使用INFORMATION_SHCEMA来查询剖析报告的办法:

set @query_id=1
SELECT STATE,SUM(DURATION) AS  Total_R,
    ROUND(
    100*SUM(DURATION)/(SELECT SUM(DURATION) FROM INFORMATION_SCHEMA.PROFILING WHERE QUERY_ID = @query_id),2     
    ) AS Pct_R,COUNT(*) AS Calls,SUM(DURATION)/COUNT(*) AS "R/Call"
FROM INFORMATION_SCHEMA.PROFILING
WHERE QUERY_ID=@query_id
GROUP BY STATE
ORDER BY Total_R DESC
# 输出如下:
+----------------------+----------+-------+-------+--------------+
| STATE                | Total_R  | Pct_R | Calls | R/Call       |
+----------------------+----------+-------+-------+--------------+
| starting             | 0.000072 | 20.45 | 1     | 0.0000720000 |
| Sending data         | 0.000047 | 13.35 | 1     | 0.0000470000 |
| init                 | 0.000030 | 8.52  | 1     | 0.0000300000 |
| Opening tables       | 0.000026 | 7.39  | 1     | 0.0000260000 |
| checking permissions | 0.000025 | 7.10  | 1     | 0.0000250000 |
| cleaning up          | 0.000023 | 6.53  | 1     | 0.0000230000 |
| System lock          | 0.000019 | 5.40  | 1     | 0.0000190000 |
| statistics           | 0.000018 | 5.11  | 1     | 0.0000180000 |
| preparing            | 0.000016 | 4.55  | 1     | 0.0000160000 |
| optimizing           | 0.000015 | 4.26  | 1     | 0.0000150000 |
| freeing items        | 0.000014 | 3.98  | 1     | 0.0000140000 |
| query end            | 0.000013 | 3.69  | 1     | 0.0000130000 |
| closing tables       | 0.000012 | 3.41  | 1     | 0.0000120000 |
| executing            | 0.000011 | 3.13  | 1     | 0.0000110000 |
| end                  | 0.000011 | 3.13  | 1     | 0.0000110000 |
+----------------------+----------+-------+-------+--------------+
#通过这个结果可以很容易看到查询时间长主要是因为花了很大时间在sending data上
#这个状态 代表的原因非常多,可能是各种不同的服务器活动,包括在关联时搜索匹配的行记录等,这部分很难说能优化节省多少消耗的时间。
#若Sorting result花费的时间比较多,则可以考虑增大sort buffer size

使用show status

​ MySQL的show status 命令返回了一些计数器,既有服务器级别的全局技术去,也有基于某个连接的会话级别的计数器。MySQL官方手册对与所有的变量是全局还是会话级别的做了详细的说明。

show status的大部分结果都是一个计数器,可以显示某些活动如读索引的频繁程度,但无法给出消耗了多少时间。show status的结果中只有一条Innodb_row_lock_time指的是操作时间,而且这个是全局性的,还是无法测量会话级别的工作。最有用的计数器包括句柄计数器,临时文件和表计数器等。将会话级别的计数器重置为0,然后查询前面提到的视图,再检查计数器的结果:

flush status;
select * from sakila.nicer_but_slower_film_list;
#...............
show status where variable_name like "Handler%" or Variable_name like "Created%";
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Created_tmp_disk_tables    | 2     |
| Created_tmp_files          | 2     |
| Created_tmp_tables         | 3     |
| Handler_commit             | 1     |
| Handler_delete             | 0     |
| Handler_discover           | 0     |
| Handler_external_lock      | 10    |
| Handler_mrr_init           | 0     |
| Handler_prepare            | 0     |
| Handler_read_first         | 3     |
| Handler_read_key           | 12942 |
| Handler_read_last          | 0     |
| Handler_read_next          | 6462  |
| Handler_read_prev          | 0     |
| Handler_read_rnd           | 5462  |
| Handler_read_rnd_next      | 6478  |
| Handler_rollback           | 0     |
| Handler_savepoint          | 0     |
| Handler_savepoint_rollback | 0     |
| Handler_update             | 0     |
| Handler_write              | 0     |
+----------------------------+-------+

从结果可以看到该查询使用了三个临时表,其中两个是磁盘临时表,并且有很多的没有用到索引的读操作(Handler_read_rnd_next)。假设我们不知道这个视图的具体定义,仅从结果来推测,这个查询可能做了多表关联查询,且没有合适的索引,可能是其中一个子查询创建了临时表,然后和其他表做联合查询,而用于保存子查询结果的临时表没有索引。

​ 但是,请注意,使用show status本身也会创建一个临时表,而且也会通过句柄操作访问此临时表,也会影响到show status结果中对应的数字,而且不同的版本可能行为也不尽相同,比较前面通过show profiles获得的查询的的执行计划结果来看,至少临时表的计数器多加了2。
​ 通过explain看到的查询执行计划也可以获得和show status大部分相同的信息,但是通过explain是估计得到的结果,而通过show status则是实际测量的结果。比如,explain无法告诉你临时表是否是磁盘表。

使用performance_schema和sys视图库

​ 在5.6中,引入了成熟的performance_schema视图库,在5.7时为了方便performance_schema的使用,引入了建立在performance_schema上面的sys库。通过sys库,我们可以方便的观测很多基础数据,同时,可以使用MySQL WorkBench来方便的查看。如图:【MySQL】《高性能MySQL》 学习笔记,第三章,服务器性能剖析_第1张图片

4.诊断间歇性问题

​ 如果服务器的整体运行没问题,只有某条查询偶尔变慢,就需要将注意力放到这条特定的查询上面。

​ 服务器的问题往往能通过升级硬件或者升级到MySQL新版本解决。

使用show global status

​ 以一定的频率,如一秒执行一次show global status命令捕获数据,问题出现时,则可以通过某些计数器(比如:Threads_running、Threads_connected、Questions:不包括存储过程中的查询数、和Queries:全部查询数)的”尖刺“或者”凹陷“来发现。

mysqladmin ext -i1 |awk '
/Queries/{q=$4-qp;qp=$4}
/Threads_connected/{tc=$4}
/Threads_running/{printf "%5d %5d %5d\n",q, tc, $4}'
#输出如下:
  798     136   7
  767     134   9
  828     134   7
  683     134   7
  784     135   7
  614     134   7
  108     134   24
  187     134   31
  179     134   28
 1179     134   7
 1151     134   7
 1120     135   7
#这个命令每秒捕获一次show global status的数据,输出给awk计算并输出每秒的查询数,连接线程,活跃线程。
#这三个数据的趋势对于数据库级别偶尔停顿的敏感性很高。

​ 在这个例子中,应用使用了连接池,所以Threads_connected没有出现一个尖刺,但正在执行查询的线程数明显上升,同时正在执行查询的线程数明显上升,同时每秒的查询数相比正常的数据有严重的下跌。

​ 如何解析这个现象呢,仅凭猜测有一定风险,但在实践中有两个原因的可能性比较大。其中之一是服务器内部碰到了某种瓶颈,导致新查询在开始执行前因为需要获取老查询正在等待的锁而造成堆积。这一类的锁一般也会也会对应用服务器造成后端压力,使得应用服务器也出现排队问题,另外一个常见的原因是服务器突然遇到了大量查询请求的冲击,比如前端的redis或者redis被击穿导致的查询风暴。

使用show processlist

​ 不停的捕获show processlist的输出,观察是否大量线程处于不正常的状态或者有其他不正常的特征。

mysql - e 'show processlist\G' |grep State: | sort | uniq -c | sort -rn
    744   State: 
     67   State: Sending data
     35   State:freeing items
      8   State:NULL
      6   State:end
      4   State:Updating
      4   State:cleaning up
      2   State:update
      1   State:sorting result
      1   State:logging slow query

​ 大多数案例中,State列都非常有用。从这个例子的输出可以看到,有很多线程处于查询执行的结束部分的状态,包括:”freeing items“、"end"、”cleaning up“和”logging slow query“。

附叶金荣老师总结的常见需要注意的状态

​ copy to tmp table :建议把alter table 语句放到低峰期运行

​ Copying to tmp table: 拷贝到内存中的临时表,常见于group by 操作,建议加索引

​ Copying to tmp table disk: 临时结果集太大,内存中放不下,需要将内粗那种的临时表拷贝到磁盘上,建议增大sort_buffer_size和tmp_table_size

​ Creating sort index: 当前的SELECT需要用到临时表来进行Order by。建议:创建适当索引

​ Creating tmp table:建议创建适当的索引,少用UNION,VIEW,SUBQUERY

​ Reading from net:建议减小客户端发送数据包大小,提高网络带宽质量

​ Sending data:从硬盘读取数据发给客户端,建议通过索引或加上Limit,减少需要扫描并且发送刚给客户端的数据量。

​ Sorting Result:正在对扫描结果进行排序,类似Creating sort index,但是正常表,而不是在内存表中,建议增加索引。

​ statistics:进行数据统计,以便解析执行计划,经常出现,说明I/O有问题。

使用lsof

​ 通过lsof采样mysqld对tmp文件夹的使用,判断磁盘临时表的使用情况。

lsof > lsof.txt
awk '
/mysqld.*tmp/ {
    total += $7;
}
total {
    printf "%s %7.2f MB\n", $4, total/1024/1024;
    total=0
}' lsof.txt
11uW   12.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB
mysql    0.00 MB

5.总结

​ 对一个系统进行检查的时候,应该先问下自己,是否也碰到过现在这种不合理的问题,如果有,就需要深入调查,应该尽量跟进每一个可能的问题知道发现结果,而不是被离题太多的问题分散了注意力,以至于最后都忘记了最初要调查的问题。可以把所有问题写在纸上,检查一个划掉一个,最后再确认一遍所有的问题都已经完成了调查。

​ 不要万事上来就分析慢查询,这种操作的莽撞之处等同你老婆听到你和异性通话,就认为你有外遇。慢查询到底是原因还是结果,在深入调查之前是无法知晓的,一个查询需要执行filesort和创建临时表并不一定意味着有问题。