阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

利用MySQL系统数据库做性能负载诊断

207次阅读
没有评论

共计 9177 个字符,预计需要花费 23 分钟才能阅读完成。

一样了解自己管理的数据库,个人认为包含了两个方面的了解:
1,在稳定性层面来说,更多的是关注高可用、读写分离、负载均衡,灾备管理等等 high level 层面的措施(就好比要保证生活的稳定性)
2,在实例级别的来说,需要关注内存、IO、网络,热点表,热点索引,top sql,死锁,阻塞,历史上执行异常的 SQL(好比生活品质细节)
MySQL 的 performance_data 库和 sys 库提供了非常丰富的系统日志数据,可以帮助我们更好地了解非常细节的,这里简单地列举出来了一些常用的数据。
sys 库是以较为可读化的方式封装了 performance_data 中的某些表,因此这些个数据来源还是 performance_data 库中的数据。
这里粗略列举出个人常用的一些系统数据,可以在实例级别更加清楚地了解 MySQL 的运行过程中资源分配情况。
 
Status 中的信息
MySQL 的 status 变量只是给出了一个总的信息,从 status 变量上无法得知详细资源的消耗,比如 IO 或者内存的热点在哪里,库、表的热点在哪里,如果想要知道具体的明细信息就需要系统库中的数据。
前提要开启 performance_schema,因为 sys 库的视图是基于 performance_schema 的库的。
利用 MySQL 系统数据库做性能负载诊断
 
 
内存使用:
内存 /innodb_buffer_pool 使用概要
innodb_buffer_pool 的使用情况 summary,已知当前实例 262144*16/1024 = 4096MB buffer pool, 已使用 23260*16/1024 363MB
利用 MySQL 系统数据库做性能负载诊断

innodb_buffer_pool 已占用内存的明细信息,可以按照库 \ 表的维度来统计

利用 MySQL 系统数据库做性能负载诊断
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;

SELECT 
     database_name,
     SUM(compressed_size)/1024/1024  AS allocated_memory,
     SUM(data_size)/1024/1024  AS data_memory,
     SUM(is_hashed)*16/1024 AS is_hashed_memory,
     SUM(is_old)*16/1024 AS is_old_memory
FROM 
(SELECT 
        case when INSTR(TABLE_NAME,'.')>0 then replace(SUBSTRING(TABLE_NAME,1,INSTR(TABLE_NAME,'.')-1),'`','')
        else     'system_database' end as database_name,
        case when INSTR(TABLE_NAME,'.')>0 then replace(SUBSTRING(TABLE_NAME,INSTR(TABLE_NAME,'.')+1),'`','')
        ELSE 'system_obj' END AS table_name,
        if(compressed_size = 0, 16384, compressed_size) AS compressed_size,
        data_size,
        if(is_hashed = 'YES',1,0) is_hashed,
        if(is_old = 'YES',1,0)  is_old
    FROM information_schema.innodb_buffer_page
    WHERE TABLE_NAME IS NOT NULL
) t
GROUP BY database_name
ORDER BY allocated_memory DESC
LIMIT 10;

缓存命中率统计及冷热数据变化

查询缓存命中率相关:
information_schema.innodb_buffer_pool_stats 中的数据行数是跟 buffer_pool_instance 一致的
也就是每个一行数据来描述一个 buffer_pool_instance,这里简单取和,缓存命中率取平局值的方式来统计
需要注意的是
1,modified_database_pages 是实时的,就是内存中的脏页的数量,经 checkpoint 之后被刷新到磁盘,因此会时大时小。
2,pages_made_young 和 pages_not_made_young 是累积的增加的,不会减少,就是 MySQL 实例截止到目前位置,做了多少 pages_not_made_young 和 pages_not_made_young。
3,hit_rate 在负载较低的情况下,没有参考意义,这一点很奇怪,低负载情况下,会发现很多 buffer_pool 的 hit_rate 是 0。
突然意识到,hit_rate 的计算,是不是以某个时间间隔为基准,统计这个时间段内请求的命中率,如果这一小段时间内没有请求,统计出来的 hit_rate 就是 0。
4,与其他视图不通,information_schema.innodb_buffer_pool_stats 中的数据会在服务重启后清零。

SELECT 
    SUM(modified_database_pages) AS total_modified_database_pages,
    SUM(pages_made_young) AS total_pages_made_young,
    SUM(pages_not_made_young) AS total_pages_not_made_young,
    SUM(hit_rate)/COUNT(hit_rate)*1000 AS hit_rate
FROM

(SELECT 
        pool_id,
        pool_size,
        database_pages,
        old_database_pages,
        modified_database_pages,
        pages_made_young,
        pages_not_made_young,
        hit_rate
    FROM information_schema.innodb_buffer_pool_stats
)t;

参考 https://www.cnblogs.com/geaozhang/p/7276802.html 这里对这 pages_made_young 和 page_not_made_young,个人觉得解释的非常好。

利用 MySQL 系统数据库做性能负载诊断

这里低负载下的 information_schema.innodb_buffer_pool_stats 中的信息,hit_rate 的值简直不可思议。
这个���例是 4GB 的内存,基本上没有访问量,hit_rate 竟然出来好多值为 0 的情况。

利用 MySQL 系统数据库做性能负载诊断

相反在对当前实例做压力测试的时候,这个数据看起来才是正常的,包括 modified_database_pages,pages_made_young,pages_not_made_young,hit_rate

这里用 mysqlslap 做混合读写的压力测试

./mysqlslap -uroot -proot -h127.0.0.1 -P8000 --concurrency=100 --iterations=10000 --auto-generate-sql --auto-generate-sql-add-autoincrement --auto-generate-sql-load-type=mixed --engine=innodb --number-of-queries=10000

用 Python 定时打印 innodb_buffer_pool_stats

import pymysql
import logging
import time
import decimal


def execute_query(conn_dict,sql):
    conn = pymysql.connect(host=conn_dict['host'],
                           port=conn_dict['port'],
                           user=conn_dict['user'],
                           passwd=conn_dict['password'],
                           db=conn_dict['db'])
    cursor = conn.cursor(pymysql.cursors.DictCursor)
    cursor.execute(sql)
    list = cursor.fetchall()
    cursor.close()
    conn.close()
    return list

def check_innodb_buffer_pool_stats(flag,conn_dict):
    result = execute_query(conn_dict, '''SELECT 
                                            modified_database_pages,
                                            pages_made_young,
                                            pages_not_made_young,
                                            hit_rate
                                        FROM information_schema.innodb_buffer_pool_stats;''')
    if result:
        column = result[0].keys()
        current_row = ''
        if(flag<=0):
            for key in column:
                current_row += str(key) + "    "
            print(current_row)

        for row in result:
            current_row = ''
            for key in row.values():
                current_row += str(key) + "    "
            print(current_row)

if __name__ == '__main__':
    conn  = {'host': '127.0.0.1', 'port': my_port, 'user': 'root', 'password': '***', 'db': 'mysql', 'charset': 'utf8mb4'}
    flag = 0
    while 1>0:
        check_innodb_buffer_pool_stats(flag,conn)
        time.sleep(3)
        flag = 1

这样子看下来,这个统计还是比较正常的。

利用 MySQL 系统数据库做性能负载诊断

突然意识到,hit_rate 的计算,是不是以某个时间间隔为基准,统计这个时间段内请求的命中率,如果这一小段时间内没有请求,统计出来的 hit_rate 就是 0。

库 \ 表的读写统计,逻辑层面的热点数据统计
目标表是 performance_schema.table_io_waits_summary_by_table,某些文章上说是逻辑 IO,其实这里跟逻辑 IO 并无关系,这个表中的字段含义是基于表,读写的到的行数的统计。
至于真正的逻辑 IO 层面的统计,笔者目前还有不知道有哪个可用的系统表来查询。
这个库可以很清楚地看到这个表中的统计结果是怎么计算出来的。
利用 MySQL 系统数据库做性能负载诊断

基于表的读写的行的次数统计,这是一个累计值,单纯的看这个值本身,个人觉得意义不大,需要定时收集计算差值,才具备参考意义。
以下按照库级别统计表的读写情况。

利用 MySQL 系统数据库做性能负载诊断
库 \ 表的读写统计,物理 IO 层面的热点数据统计
按照物理 IO 的维度统计热点数据,哪些库 \ 表消耗了多少物理 IO。
这里原始系统表中的数据是一个累计统计的值,最极端的情况就是一个表为 0 行,却存在大量的物理读写 IO。
利用 MySQL 系统数据库做性能负载诊断

 
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;


SELECT 
    database_name,
    IFNULL(cast(sum(total_read) as signed),0) AS total_read,
    IFNULL(cast(sum(total_written) as signed),0) AS total_written,
    IFNULL(cast(sum(total) AS SIGNED),0) AS total_read_written
FROM
(SELECT 
        substring(REPLACE(file, '@@datadir/', ''),1,instr(REPLACE(file, '@@datadir/', ''),'/')-1) AS database_name,
        count_read,
        case 
            when instr(total_read,'KiB')>0 then  replace(total_read,'KiB','')/1024
            when instr(total_read,'MiB')>0 then  replace(total_read,'MiB','')/1024
            when instr(total_read,'GiB')>0 then replace(total_read,'GiB','')*1024
        END AS total_read,
        case 
            when instr(total_written,'KiB')>0 then replace(total_written,'KiB','')/1024
            when instr(total_written,'MiB')>0 then replace(total_written,'MiB','')
            when instr(total_written,'GiB')>0 then replace(total_written,'GiB','')*1024
        END AS total_written,
        case 
            when instr(total,'KiB')>0 then replace(total,'KiB','')/1024
            when instr(total,'MiB')>0 then replace(total,'MiB','')
            when instr(total,'GiB')>0 then replace(total,'GiB','')*1024
        END AS total
    from sys.io_global_by_file_by_bytes 
    WHERE FILE LIKE '%@@datadir%' AND instr(REPLACE(file, '@@datadir/', ''),'/')>0 
)t
GROUP BY database_name
ORDER BY total_read_written DESC;
ps:个人不太喜欢 MySQL 自定义的 format_*** 函数,这个函数的初衷是好的,把一些数据(时间,存储空间)等格式化成更加可读的模式。
但是却不支持单位的参数,更多的时候想以某个固定的单位来显示,比如格式化一个的时间,格式化后根据单位大小可能会显示微妙,或者是毫秒,或者是秒,或者分钟,或者天。
比如想把时间统一格式化成秒,对不起,不支持,某些个数据不仅仅是看一眼那么简单,甚至是要读出来存档分析的,因此这里不建议也不会使用那些个 format 函数
 

TOP SQL 统计

可以按照执行时间,阻塞时间,返回行数等等维度统计 top sql。
另外可以按照时间筛选 last_seen,可以统计最近某一段时间出现过的 top sql

利用 MySQL 系统数据库做性能负载诊断

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;

SELECT 
    schema_name,
    digest_text,
    count_star,
    avg_timer_wait/1000000000000 AS avg_timer_wait,
    max_timer_wait/1000000000000 AS max_timer_wait,
    sum_lock_time/count_star/1000000000000 AS avg_lock_time ,
    sum_rows_affected/count_star AS avg_rows_affected,
    sum_rows_sent/count_star AS avg_rows_sent ,
    sum_rows_examined/count_star AS avg_rows_examined,
    sum_created_tmp_disk_tables/count_star AS avg_create_tmp_disk_tables,
    sum_created_tmp_tables/count_star AS avg_create_tmp_tables,
    sum_select_full_join/count_star AS avg_select_full_join,
    sum_select_full_range_join/count_star AS avg_select_full_range_join,
    sum_select_range/count_star AS avg_select_range,
    sum_select_range_check/count_star AS avg_select_range,
    first_seen,
    last_seen
FROM performance_schema.events_statements_summary_by_digest
WHERE last_seen>date_add(NOW(), interval -1 HOUR)
ORDER BY 
max_timer_wait
-- avg_timer_wait
-- sum_rows_affected/count_star 
-- sum_lock_time/count_star
-- avg_lock_time
-- avg_rows_sent
DESC
limit 10;

需要注意的是,这个统计是按照 MySQL 执行一个事务消耗的资源做统计的,而不是一个语句,笔者一开始懵逼了一阵子,举个简单的例子。
参考如下,这里是循环写个数据的一个存储过程,调用方式就是 call create_test_data(N),写入 N 条测试数据。
比如 call create_test_data(1000000)就是写入 100W 的测试数据,这个执行过程耗费了几分钟的时间,按照笔者的测试实例情况,avg_timer_wait 的维度,绝对是一个 TOP SQL。
但是在查询的时候,始终没有发现这个存储过程的调用被列为 TOP SQL,后面尝试在存储过程内部加了一个事物,然后就顺利地收集到了整个 TOP SQL.
因此说 performance_schema.events_statements_summary_by_digest 里面的统计,是基于事务的,而不是某一个批处理的执行时间的。

CREATE DEFINER=`root`@`%` PROCEDURE `create_test_data`(IN `loopcnt` INT
)
LANGUAGE SQL
NOT DETERMINISTIC
CONTAINS SQL
SQL SECURITY DEFINER
COMMENT ''
BEGIN
    -- START TRANSACTION; 
        while loopcnt>0 do
            insert into test_mrr(rand_id,create_date) values (RAND()*100000000,now(6));
            set loopcnt=loopcnt-1;
        end while;
    -- commit;
END

另外一点比较有意思的是,这个系统表是为数不多的支持 truncate 的,当然它在内部,也是在不断收集的一个过程。

利用 MySQL 系统数据库做性能负载诊断

执行失败的 SQL 统计

一直以为系统不会记录执行失败的 \ 解析错误的 SQL,比如想统计因为超时而执行失败的语句,后面才发现,这些信息,MySQL 会完整地记录下来

利用 MySQL 系统数据库做性能负载诊断

这里会详细记录执行错误的语句,包括最终执行失败(超时之类的),语法错误,执行过程中产生了警告之类的语句。用 sum_errors>0 or sum_warnings>0 去 performance_schema.events_statements_summary_by_digest 筛选一下即可。

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;

select 
    schema_name,
    digest_text,
    count_star,
    first_seen,
    last_seen
from performance_schema.events_statements_summary_by_digest
where sum_errors>0 or sum_warnings>0 
order by last_seen desc;

Index 使用情况统计

基于 performance_schema.table_io_waits_summary_by_index_usage 这个系统表,其统计的维度同样是“按照某个索引查询返回的行数的统计”。

可以按照哪些索引使用最多 \ 最少等情况进行统计。

利用 MySQL 系统数据库做性能负载诊断

不过这个统计有一个给人潜在一个误区:
count_read,count_write,count_fetch,count_insert,count_update,count_delete 统计了某个索引上使用到索引的情况下,受影响的行数,sum_timer_wait 是累计在该索引上等待的时间。
如果使用到了该索引,但是没有数据受影响(就是没有 DML 语句的条件没有命中数据),将 count_*** 不会统计进来,但是 sum_timer_wait 会统计进来
这就存在一个容易受到误导的地方,这个索引明明没有命中过很多次,但是却产生了大量的 timer_wait,索引看到类似的信息,也不能贸然删除索引。

等待事件统计

MySQL 数据库中的任何一个动作,都需要等待(一定的时间来完成),一共有超过 1000 个等待事件,分属不懂的类别,每个版本都不一样,且默认不是所有的等待事件都启用。

利用 MySQL 系统数据库做性能负载诊断

个人认为等待事件这个东西,仅做参考,不具备问题的诊断性,即便是再优化或者低负载的数据库,累计一段时间,某些事件仍旧会积累大量的等待事件。
这些事件的等待事件,不一定都是负面性的,比如事物的锁等待,是在并发执行过程中必然会生成的,这个等待事件的统计结果,也是累计的,单纯的看一个直接的值,不具备任何参考意义。
除非定期收集,做差值计算,根据实际情况,才具备参考意义。

利用 MySQL 系统数据库做性能负载诊断

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;

SELECT SUBSTRING_INDEX(NAME, '/', 1) as wait_type,COUNT(1)  
FROM performance_schema.setup_instruments
GROUP BY 1  
ORDER BY 2 DESC;


SELECT
event_name,
count_star,
sum_timer_wait
FROM performance_schema.events_waits_summary_global_by_event_name
WHERE event_name != 'idle'
order by sum_timer_wait desc
limit 100;

最后,需要注意的是,
1,MySQL 提供的诸多的系统表(视图)中的数据,单纯的看这个值本身,因为它是一个累计值,个人觉得意义不大,尤其是 avg_***,需要结合多方面的综合因素,做参考使用。
2,任何系统表的查询,都可能对系统性能的本身造成一定的影响,不要再对系统可能产生较大负面影响的情况下做数据的统计收集。

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-22发表,共计9177字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中