-
Notifications
You must be signed in to change notification settings - Fork 683
/
Copy pathMySQL · 引擎特性 · InnoDB Buffer Pool.md.html
1070 lines (544 loc) · 68.3 KB
/
MySQL · 引擎特性 · InnoDB Buffer Pool.md.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/文章/AQS 万字图文全面解析.md.html">AQS 万字图文全面解析.md.html</a>
</li>
<li>
<a href="/文章/Docker 镜像构建原理及源码分析.md.html">Docker 镜像构建原理及源码分析.md.html</a>
</li>
<li>
<a href="/文章/ElasticSearch 小白从入门到精通.md.html">ElasticSearch 小白从入门到精通.md.html</a>
</li>
<li>
<a href="/文章/JVM CPU Profiler技术原理及源码深度解析.md.html">JVM CPU Profiler技术原理及源码深度解析.md.html</a>
</li>
<li>
<a href="/文章/JVM 垃圾收集器.md.html">JVM 垃圾收集器.md.html</a>
</li>
<li>
<a href="/文章/JVM 面试的 30 个知识点.md.html">JVM 面试的 30 个知识点.md.html</a>
</li>
<li>
<a href="/文章/Java IO 体系、线程模型大总结.md.html">Java IO 体系、线程模型大总结.md.html</a>
</li>
<li>
<a href="/文章/Java NIO浅析.md.html">Java NIO浅析.md.html</a>
</li>
<li>
<a href="/文章/Java 面试题集锦(网络篇).md.html">Java 面试题集锦(网络篇).md.html</a>
</li>
<li>
<a href="/文章/Java-直接内存 DirectMemory 详解.md.html">Java-直接内存 DirectMemory 详解.md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决(上).md.html">Java中9种常见的CMS GC问题分析与解决(上).md.html</a>
</li>
<li>
<a href="/文章/Java中9种常见的CMS GC问题分析与解决(下).md.html">Java中9种常见的CMS GC问题分析与解决(下).md.html</a>
</li>
<li>
<a href="/文章/Java中的SPI.md.html">Java中的SPI.md.html</a>
</li>
<li>
<a href="/文章/Java中的ThreadLocal.md.html">Java中的ThreadLocal.md.html</a>
</li>
<li>
<a href="/文章/Java线程池实现原理及其在美团业务中的实践.md.html">Java线程池实现原理及其在美团业务中的实践.md.html</a>
</li>
<li>
<a href="/文章/Java魔法类:Unsafe应用解析.md.html">Java魔法类:Unsafe应用解析.md.html</a>
</li>
<li>
<a href="/文章/Kafka 源码阅读笔记.md.html">Kafka 源码阅读笔记.md.html</a>
</li>
<li>
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html</a>
</li>
<li>
<a class="current-tab" href="/文章/MySQL · 引擎特性 · InnoDB Buffer Pool.md.html">MySQL · 引擎特性 · InnoDB Buffer Pool.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">MySQL · 引擎特性 · InnoDB IO子系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 事务系统.md.html">MySQL · 引擎特性 · InnoDB 事务系统.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 同步机制.md.html">MySQL · 引擎特性 · InnoDB 同步机制.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB 数据页解析.md.html">MySQL · 引擎特性 · InnoDB 数据页解析.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · InnoDB崩溃恢复.md.html">MySQL · 引擎特性 · InnoDB崩溃恢复.md.html</a>
</li>
<li>
<a href="/文章/MySQL · 引擎特性 · 临时表那些事儿.md.html">MySQL · 引擎特性 · 临时表那些事儿.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 半同步复制.md.html">MySQL 主从复制 半同步复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制 基于GTID复制.md.html">MySQL 主从复制 基于GTID复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 主从复制.md.html">MySQL 主从复制.md.html</a>
</li>
<li>
<a href="/文章/MySQL 事务日志(redo log和undo log).md.html">MySQL 事务日志(redo log和undo log).md.html</a>
</li>
<li>
<a href="/文章/MySQL 亿级别数据迁移实战代码分享.md.html">MySQL 亿级别数据迁移实战代码分享.md.html</a>
</li>
<li>
<a href="/文章/MySQL 从一条数据说起-InnoDB行存储数据结构.md.html">MySQL 从一条数据说起-InnoDB行存储数据结构.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:事务和锁的面纱.md.html">MySQL 地基基础:事务和锁的面纱.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据字典.md.html">MySQL 地基基础:数据字典.md.html</a>
</li>
<li>
<a href="/文章/MySQL 地基基础:数据库字符集.md.html">MySQL 地基基础:数据库字符集.md.html</a>
</li>
<li>
<a href="/文章/MySQL 性能优化:碎片整理.md.html">MySQL 性能优化:碎片整理.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html">MySQL 故障诊断:一个 ALTER TALBE 执行了很久,你慌不慌?.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:如何在日志中轻松定位大事务.md.html">MySQL 故障诊断:如何在日志中轻松定位大事务.md.html</a>
</li>
<li>
<a href="/文章/MySQL 故障诊断:教你快速定位加锁的 SQL.md.html">MySQL 故障诊断:教你快速定位加锁的 SQL.md.html</a>
</li>
<li>
<a href="/文章/MySQL 日志详解.md.html">MySQL 日志详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL 的半同步是什么?.md.html">MySQL 的半同步是什么?.md.html</a>
</li>
<li>
<a href="/文章/MySQL中的事务和MVCC.md.html">MySQL中的事务和MVCC.md.html</a>
</li>
<li>
<a href="/文章/MySQL事务_事务隔离级别详解.md.html">MySQL事务_事务隔离级别详解.md.html</a>
</li>
<li>
<a href="/文章/MySQL优化:优化 select count().md.html">MySQL优化:优化 select count().md.html</a>
</li>
<li>
<a href="/文章/MySQL共享锁、排他锁、悲观锁、乐观锁.md.html">MySQL共享锁、排他锁、悲观锁、乐观锁.md.html</a>
</li>
<li>
<a href="/文章/MySQL的MVCC(多版本并发控制).md.html">MySQL的MVCC(多版本并发控制).md.html</a>
</li>
<li>
<a href="/文章/QingStor 对象存储架构设计及最佳实践.md.html">QingStor 对象存储架构设计及最佳实践.md.html</a>
</li>
<li>
<a href="/文章/RocketMQ 面试题集锦.md.html">RocketMQ 面试题集锦.md.html</a>
</li>
<li>
<a href="/文章/SnowFlake 雪花算法生成分布式 ID.md.html">SnowFlake 雪花算法生成分布式 ID.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html">Spring Boot 2.x 结合 k8s 实现分布式微服务架构.md.html</a>
</li>
<li>
<a href="/文章/Spring Boot 教程:如何开发一个 starter.md.html">Spring Boot 教程:如何开发一个 starter.md.html</a>
</li>
<li>
<a href="/文章/Spring MVC 原理.md.html">Spring MVC 原理.md.html</a>
</li>
<li>
<a href="/文章/Spring MyBatis和Spring整合的奥秘.md.html">Spring MyBatis和Spring整合的奥秘.md.html</a>
</li>
<li>
<a href="/文章/Spring 帮助你更好的理解Spring循环依赖.md.html">Spring 帮助你更好的理解Spring循环依赖.md.html</a>
</li>
<li>
<a href="/文章/Spring 循环依赖及解决方式.md.html">Spring 循环依赖及解决方式.md.html</a>
</li>
<li>
<a href="/文章/Spring中眼花缭乱的BeanDefinition.md.html">Spring中眼花缭乱的BeanDefinition.md.html</a>
</li>
<li>
<a href="/文章/Vert.x 基础入门.md.html">Vert.x 基础入门.md.html</a>
</li>
<li>
<a href="/文章/eBay 的 Elasticsearch 性能调优实践.md.html">eBay 的 Elasticsearch 性能调优实践.md.html</a>
</li>
<li>
<a href="/文章/不可不说的Java“锁”事.md.html">不可不说的Java“锁”事.md.html</a>
</li>
<li>
<a href="/文章/互联网并发限流实战.md.html">互联网并发限流实战.md.html</a>
</li>
<li>
<a href="/文章/从ReentrantLock的实现看AQS的原理及应用.md.html">从ReentrantLock的实现看AQS的原理及应用.md.html</a>
</li>
<li>
<a href="/文章/从SpringCloud开始,聊微服务架构.md.html">从SpringCloud开始,聊微服务架构.md.html</a>
</li>
<li>
<a href="/文章/全面了解 JDK 线程池实现原理.md.html">全面了解 JDK 线程池实现原理.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性理论与算法.md.html">分布式一致性理论与算法.md.html</a>
</li>
<li>
<a href="/文章/分布式一致性算法 Raft.md.html">分布式一致性算法 Raft.md.html</a>
</li>
<li>
<a href="/文章/分布式唯一 ID 解析.md.html">分布式唯一 ID 解析.md.html</a>
</li>
<li>
<a href="/文章/分布式链路追踪:集群管理设计.md.html">分布式链路追踪:集群管理设计.md.html</a>
</li>
<li>
<a href="/文章/动态代理种类及原理,你知道多少?.md.html">动态代理种类及原理,你知道多少?.md.html</a>
</li>
<li>
<a href="/文章/响应式架构与 RxJava 在有赞零售的实践.md.html">响应式架构与 RxJava 在有赞零售的实践.md.html</a>
</li>
<li>
<a href="/文章/大数据算法——布隆过滤器.md.html">大数据算法——布隆过滤器.md.html</a>
</li>
<li>
<a href="/文章/如何优雅地记录操作日志?.md.html">如何优雅地记录操作日志?.md.html</a>
</li>
<li>
<a href="/文章/如何设计一个亿级消息量的 IM 系统.md.html">如何设计一个亿级消息量的 IM 系统.md.html</a>
</li>
<li>
<a href="/文章/异步网络模型.md.html">异步网络模型.md.html</a>
</li>
<li>
<a href="/文章/当我们在讨论CQRS时,我们在讨论些神马?.md.html">当我们在讨论CQRS时,我们在讨论些神马?.md.html</a>
</li>
<li>
<a href="/文章/彻底理解 MySQL 的索引机制.md.html">彻底理解 MySQL 的索引机制.md.html</a>
</li>
<li>
<a href="/文章/最全的 116 道 Redis 面试题解答.md.html">最全的 116 道 Redis 面试题解答.md.html</a>
</li>
<li>
<a href="/文章/有赞权限系统(SAM).md.html">有赞权限系统(SAM).md.html</a>
</li>
<li>
<a href="/文章/有赞零售中台建设方法的探索与实践.md.html">有赞零售中台建设方法的探索与实践.md.html</a>
</li>
<li>
<a href="/文章/服务注册与发现原理剖析(Eureka、Zookeeper、Nacos).md.html">服务注册与发现原理剖析(Eureka、Zookeeper、Nacos).md.html</a>
</li>
<li>
<a href="/文章/深入浅出Cache.md.html">深入浅出Cache.md.html</a>
</li>
<li>
<a href="/文章/深入理解 MySQL 底层实现.md.html">深入理解 MySQL 底层实现.md.html</a>
</li>
<li>
<a href="/文章/漫画讲解 git rebase VS git merge.md.html">漫画讲解 git rebase VS git merge.md.html</a>
</li>
<li>
<a href="/文章/生成浏览器唯一稳定 ID 的探索.md.html">生成浏览器唯一稳定 ID 的探索.md.html</a>
</li>
<li>
<a href="/文章/缓存 如何保证缓存与数据库的双写一致性?.md.html">缓存 如何保证缓存与数据库的双写一致性?.md.html</a>
</li>
<li>
<a href="/文章/网易严选怎么做全链路监控的?.md.html">网易严选怎么做全链路监控的?.md.html</a>
</li>
<li>
<a href="/文章/美团万亿级 KV 存储架构与实践.md.html">美团万亿级 KV 存储架构与实践.md.html</a>
</li>
<li>
<a href="/文章/美团点评Kubernetes集群管理实践.md.html">美团点评Kubernetes集群管理实践.md.html</a>
</li>
<li>
<a href="/文章/美团百亿规模API网关服务Shepherd的设计与实现.md.html">美团百亿规模API网关服务Shepherd的设计与实现.md.html</a>
</li>
<li>
<a href="/文章/解读《阿里巴巴 Java 开发手册》背后的思考.md.html">解读《阿里巴巴 Java 开发手册》背后的思考.md.html</a>
</li>
<li>
<a href="/文章/认识 MySQL 和 Redis 的数据一致性问题.md.html">认识 MySQL 和 Redis 的数据一致性问题.md.html</a>
</li>
<li>
<a href="/文章/进阶:Dockerfile 高阶使用指南及镜像优化.md.html">进阶:Dockerfile 高阶使用指南及镜像优化.md.html</a>
</li>
<li>
<a href="/文章/铁总在用的高性能分布式缓存计算框架 Geode.md.html">铁总在用的高性能分布式缓存计算框架 Geode.md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析(上).md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析(上).md.html</a>
</li>
<li>
<a href="/文章/阿里云PolarDB及其共享存储PolarFS技术实现分析(下).md.html">阿里云PolarDB及其共享存储PolarFS技术实现分析(下).md.html</a>
</li>
<li>
<a href="/文章/面试最常被问的 Java 后端题.md.html">面试最常被问的 Java 后端题.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计在互联网业务开发中的实践.md.html">领域驱动设计在互联网业务开发中的实践.md.html</a>
</li>
<li>
<a href="/文章/领域驱动设计的菱形对称架构.md.html">领域驱动设计的菱形对称架构.md.html</a>
</li>
<li>
<a href="/文章/高效构建 Docker 镜像的最佳实践.md.html">高效构建 Docker 镜像的最佳实践.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>MySQL · 引擎特性 · InnoDB Buffer Pool</h1>
<h2>前言</h2>
<p>用户对数据库的最基本要求就是能高效的读取和存储数据,但是读写数据都涉及到与低速的设备交互,为了弥补两者之间的速度差异,所有数据库都有缓存池,用来管理相应的数据页,提高数据库的效率,当然也因为引入了这一中间层,数据库对内存的管理变得相对比较复杂。本文主要分析MySQL Buffer Pool的相关技术以及实现原理,源码基于阿里云RDS MySQL 5.6分支,其中部分特性已经开源到AliSQL。Buffer Pool相关的源代码在buf目录下,主要包括LRU List,Flu List,Double write buffer, 预读预写,Buffer Pool预热,压缩页内存管理等模块,包括头文件和IC文件,一共两万行代码。</p>
<h2>基础知识</h2>
<p>**Buffer Pool Instance: ** 大小等于innodb_buffer_pool_size/innodb_buffer_pool_instances,每个instance都有自己的锁,信号量,物理块(Buffer chunks)以及逻辑链表(下面的各种List),即各个instance之间没有竞争关系,可以并发读取与写入。所有instance的物理块(Buffer chunks)在数据库启动的时候被分配,直到数据库关闭内存才予以释放。当innodb_buffer_pool_size小于1GB时候,innodb_buffer_pool_instances被重置为1,主要是防止有太多小的instance从而导致性能问题。每个Buffer Pool Instance有一个page hash链表,通过它,使用space_id和page_no就能快速找到已经被读入内存的数据页,而不用线性遍历LRU List去查找。注意这个hash表不是InnoDB的自适应哈希,自适应哈希是为了减少Btree的扫描,而page hash是为了避免扫描LRU List。
<strong>数据页:</strong> InnoDB中,数据管理的最小单位为页,默认是16KB,页中除了存储用户数据,还可以存储控制信息的数据。InnoDB IO子系统的读写最小单位也是页。如果对表进行了压缩,则对应的数据页称为压缩页,如果需要从压缩页中读取数据,则压缩页需要先解压,形成解压页,解压页为16KB。压缩页的大小是在建表的时候指定,目前支持16K,8K,4K,2K,1K。即使压缩页大小设为16K,在blob/varchar/text的类型中也有一定好处。假设指定的压缩页大小为4K,如果有个数据页无法被压缩到4K以下,则需要做B-tree分裂操作,这是一个比较耗时的操作。正常情况下,Buffer Pool中会把压缩和解压页都缓存起来,当Free List不够时,按照系统当前的实际负载来决定淘汰策略。如果系统瓶颈在IO上,则只驱逐解压页,压缩页依然在Buffer Pool中,否则解压页和压缩页都被驱逐。
*Buffer Chunks: ** 包括两部分:数据页和数据页对应的控制体,控制体中有指针指向数据页。Buffer Chunks是最低层的物理块,在启动阶段从操作系统申请,直到数据库关闭才释放。通过遍历chunks可以访问几乎所有的数据页,有两种状态的数据页除外:没有被解压的压缩页(BUF_BLOCK_ZIP_PAGE)以及被修改过且解压页已经被驱逐的压缩页(BUF_BLOCK_ZIP_DIRTY)。此外数据页里面不一定都存的是用户数据,开始是控制信息,比如行锁,自适应哈希等。
**逻辑链表: ** 链表节点是数据页的控制体(控制体中有指针指向真正的数据页),链表中的所有节点都有同一的属性,引入其的目的是方便管理。下面其中链表都是逻辑链表。
**Free List: ** 其上的节点都是未被使用的节点,如果需要从数据库中分配新的数据页,直接从上获取即可。InnoDB需要保证Free List有足够的节点,提供给用户线程用,否则需要从FLU List或者LRU List淘汰一定的节点。InnoDB初始化后,Buffer Chunks中的所有数据页都被加入到Free List,表示所有节点都可用。
**LRU List: ** 这个是InnoDB中最重要的链表。所有新读取进来的数据页都被放在上面。链表按照最近最少使用算法排序,最近最少使用的节点被放在链表末尾,如果Free List里面没有节点了,就会从中淘汰末尾的节点。LRU List还包含没有被解压的压缩页,这些压缩页刚从磁盘读取出来,还没来的及被解压。LRU List被分为两部分,默认前5/8为young list,存储经常被使用的热点page,后3/8为old list。新读入的page默认被加在old list头,只有满足一定条件后,才被移到young list上,主要是为了预读的数据页和全表扫描污染buffer pool。
**FLU List: ** 这个链表中的所有节点都是脏页,也就是说这些数据页都被修改过,但是还没来得及被刷新到磁盘上。在FLU List上的页面一定在LRU List上,但是反之则不成立。一个数据页可能会在不同的时刻被修改多次,在数据页上记录了最老(也就是第一次)的一次修改的lsn,即oldest_modification。不同数据页有不同的oldest_modification,FLU List中的节点按照oldest_modification排序,链表尾是最小的,也就是最早被修改的数据页,当需要从FLU List中淘汰页面时候,从链表尾部开始淘汰。加入FLU List,需要使用flush_list_mutex保护,所以能保证FLU List中节点的顺序。
**Quick List: ** 这个链表是阿里云RDS MySQL 5.6加入的,使用带Hint的SQL查询语句,可以把所有这个查询的用到的数据页加入到Quick List中,一旦这个语句结束,就把这个数据页淘汰,主要作用是避免LRU List被全表扫描污染。
**Unzip LRU List: ** 这个链表中存储的数据页都是解压页,也就是说,这个数据页是从一个压缩页通过解压而来的。
**Zip Clean List: ** 这个链表只在Debug模式下有,主要是存储没有被解压的压缩页。这些压缩页刚刚从磁盘读取出来,还没来的及被解压,一旦被解压后,就从此链表中删除,然后加入到Unzip LRU List中。
**Zip Free: ** 压缩页有不同的大小,比如8K,4K,InnoDB使用了类似内存管理的伙伴系统来管理压缩页。Zip Free可以理解为由5个链表构成的一个二维数组,每个链表分别存储了对应大小的内存碎片,例如8K的链表里存储的都是8K的碎片,如果新读入一个8K的页面,首先从这个链表中查找,如果有则直接返回,如果没有则从16K的链表中分裂出两个8K的块,一个被使用,另外一个放入8K链表中。</p>
<h2>核心数据结构</h2>
<p>InnoDB Buffer Pool有三种核心的数据结构:buf_pool_t,buf_block_t,buf_page_t。
**but_pool_t: ** 存储Buffer Pool Instance级别的控制信息,例如整个Buffer Pool Instance的mutex,instance_no, page_hash,old_list_pointer等。还存储了各种逻辑链表的链表根节点。Zip Free这个二维数组也在其中。
**buf_block_t: ** 这个就是数据页的控制体,用来描述数据页部分的信息(大部分信息在buf_page_t中)。buf_block_t中第一字段就是buf_page_t,这个不是随意放的,是必须放在第一字段,因为只有这样buf_block_t和buf_page_t两种类型的指针可以相互转换。第二个字段是frame字段,指向真正存数据的数据页。buf_block_t还存储了Unzip LRU List链表的根节点。另外一个比较重要的字段就是block级别的mutex。
**buf_page_t: ** 这个可以理解为另外一个数据页的控制体,大部分的数据页信息存在其中,例如space_id, page_no, page state, newest_modification,oldest_modification,access_time以及压缩页的所有信息等。压缩页的信息包括压缩页的大小,压缩页的数据指针(真正的压缩页数据是存储在由伙伴系统分配的数据页上)。这里需要注意一点,如果某个压缩页被解压了,解压页的数据指针是存储在buf_block_t的frame字段里。</p>
<p>这里介绍一下buf_page_t中的state字段,这个字段主要用来表示当前页的状态。一共有八种状态。这八种状态对初学者可能比较难理解,尤其是前三种,如果看不懂可以先跳过。
**BUF_BLOCK_POOL_WATCH: ** 这种类型的page是提供给purge线程用的。InnoDB为了实现多版本,需要把之前的数据记录在undo log中,如果没有读请求再需要它,就可以通过purge线程删除。换句话说,purge线程需要知道某些数据页是否被读取,现在解法就是首先查看page hash,看看这个数据页是否已经被读入,如果没有读入,则获取(启动时候通过malloc分配,不在Buffer Chunks中)一个BUF_BLOCK_POOL_WATCH类型的哨兵数据页控制体,同时加入page_hash但是没有真正的数据(buf_blokc_t::frame为空)并把其类型置为BUF_BLOCK_ZIP_PAGE(表示已经被使用了,其他purge线程就不会用到这个控制体了),相关函数<code>buf_pool_watch_set</code>,如果查看page hash后发现有这个数据页,只需要判断控制体在内存中的地址是否属于Buffer Chunks即可,如果是表示对应数据页已经被其他线程读入了,相关函数<code>buf_pool_watch_occurred</code>。另一方面,如果用户线程需要这个数据页,先查看page hash看看是否是BUF_BLOCK_POOL_WATCH类型的数据页,如果是则回收这个BUF_BLOCK_POOL_WATCH类型的数据页,从Free List中(即在Buffer Chunks中)分配一个空闲的控制体,填入数据。这里的核心思想就是通过控制体在内存中的地址来确定数据页是否还在被使用。
**BUF_BLOCK_ZIP_PAGE: **当压缩页从磁盘读取出来的时候,先通过malloc分配一个临时的buf_page_t,然后从伙伴系统中分配出压缩页存储的空间,把磁盘中读取的压缩数据存入,然后把这个临时的buf_page_t标记为BUF_BLOCK_ZIP_PAGE状态(<code>buf_page_init_for_read</code>),只有当这个压缩页被解压了,state字段才会被修改为BUF_BLOCK_FILE_PAGE,并加入LRU List和Unzip LRU List(<code>buf_page_get_gen</code>)。如果一个压缩页对应的解压页被驱逐了,但是需要保留这个压缩页且压缩页不是脏页,则这个压缩页被标记为BUF_BLOCK_ZIP_PAGE(<code>buf_LRU_free_page</code>)。所以正常情况下,处于BUF_BLOCK_ZIP_PAGE状态的不会很多。前述两种被标记为BUF_BLOCK_ZIP_PAGE的压缩页都在LRU List中。另外一个用法是,从BUF_BLOCK_POOL_WATCH类型节点中,如果被某个purge线程使用了,也会被标记为BUF_BLOCK_ZIP_PAGE。
**BUF_BLOCK_ZIP_DIRTY: ** 如果一个压缩页对应的解压页被驱逐了,但是需要保留这个压缩页且压缩页是脏页,则被标记为BUF_BLOCK_ZIP_DIRTY(<code>buf_LRU_free_page</code>),如果该压缩页又被解压了,则状态会变为BUF_BLOCK_FILE_PAGE。因此BUF_BLOCK_ZIP_DIRTY也是一个比较短暂的状态。这种类型的数据页都在Flush List中。
**BUF_BLOCK_NOT_USED: ** 当链表处于Free List中,状态就为此状态。是一个能长期存在的状态。
**BUF_BLOCK_READY_FOR_USE: ** 当从Free List中,获取一个空闲的数据页时,状态会从BUF_BLOCK_NOT_USED变为BUF_BLOCK_READY_FOR_USE(<code>buf_LRU_get_free_block</code>),也是一个比较短暂的状态。处于这个状态的数据页不处于任何逻辑链表中。
**BUF_BLOCK_FILE_PAGE: ** 正常被使用的数据页都是这种状态。LRU List中,大部分数据页都是这种状态。压缩页被解压后,状态也会变成BUF_BLOCK_FILE_PAGE。
**BUF_BLOCK_MEMORY: ** Buffer Pool中的数据页不仅可以存储用户数据,也可以存储一些系统信息,例如InnoDB行锁,自适应哈希索引以及压缩页的数据等,这些数据页被标记为BUF_BLOCK_MEMORY。处于这个状态的数据页不处于任何逻辑链表中
**BUF_BLOCK_REMOVE_HASH: ** 当加入Free List之前,需要先把page hash移除。因此这种状态就表示此页面page hash已经被移除,但是还没被加入到Free List中,是一个比较短暂的状态。
总体来说,大部分数据页都处于BUF_BLOCK_NOT_USED(全部在Free List中)和BUF_BLOCK_FILE_PAGE(大部分处于LRU List中,LRU List中还包含除被purge线程标记的BUF_BLOCK_ZIP_PAGE状态的数据页)状态,少部分处于BUF_BLOCK_MEMORY状态,极少处于其他状态。前三种状态的数据页都不在Buffer Chunks上,对应的控制体都是临时分配的,InnoDB把他们列为invalid state(<code>buf_block_state_valid</code>)。
如果理解了这八种状态以及其之间的转换关系,那么阅读Buffer pool的代码细节就会更加游刃有余。</p>
<p>接下来,简单介绍一下buf_page_t中buf_fix_count和io_fix两个变量,这两个变量主要用来做并发控制,减少mutex加锁的范围。当从buffer pool读取一个数据页时候,会其加读锁,然后递增buf_page_t::buf_fix_count,同时设置buf_page_t::io_fix为BUF_IO_READ,然后即可以释放读锁。后续如果其他线程在驱逐数据页(或者刷脏)的时候,需要先检查一下这两个变量,如果buf_page_t::buf_fix_count不为零且buf_page_t::io_fix不为BUF_IO_NONE,则不允许驱逐(<code>buf_page_can_relocate</code>)。这里的技巧主要是为了减少数据页控制体上mutex的争抢,而对数据页的内容,读取的时候依然要加读锁,修改时加写锁。</p>
<h2>Buffer Pool内存初始化</h2>
<p>Buffer Pool的内存初始化,主要是Buffer Chunks的内存初始化,buffer pool instance一个一个轮流初始化。核心函数为<code>buf_chunk_init</code>和<code>os_mem_alloc_large</code>
。阅读代码可以发现,目前从操作系统分配内存有两种方式,一种是通过HugeTLB的方式来分配,另外一种使用传统的mmap来分配。
**HugeTLB: ** 这是一种大内存块的分配管理技术。类似数据库对数据的管理,内存也按照页来管理,默认的页大小为4KB,HugeTLB就是把页大小提高到2M或者更加多。程序传送给cpu都是虚拟内存地址,cpu必须通过快表来映射到真正的物理内存地址。快表的全集放在内存中,部分热点内存页可以放在cpu cache中,从而提高内存访问效率。假设cpu cache为100KB,每条快表占用1KB,页大小为4KB,则热点内存页为100KB/1KB=100条,覆盖1004KB=400KB的内存数据,但是如果也默认页大小为2M,则同样大小的cpu cache,可以覆盖100*2M=200MB的内存数据,也就是说,访问200MB的数据只需要一次读取内存即可(如果映射关系没有在cache中找到,则需要先把映射关系从内存中读到cache,然后查找,最后再去读内存中需要的数据,会造成两次访问物理内存)。也就是说,使用HugeTLB这种大内存技术,可以提高快表的命中率,从而提高访问内存的性能。当然这个技术也不是银弹,内存页变大了也必定会导致更多的页内的碎片。如果需要从swap分区中加载虚拟内存,也会变慢。当然最终要的理由是,4KB大小的内存页已经被业界稳定使用很多年了,如果没有特殊的需求不需要冒这个风险。在InnoDB中,如果需要用到这项技术可以使用super-large-pages参数启动MySQL。
<strong>mmap分配:</strong> 在Linux下,多个进程需要共享一片内存,可以使用mmap来分配和绑定,所以只提供给一个MySQL进程使用也是可以的。用mmap分配的内存都是虚存,在top命令中占用VIRT这一列,而不是RES这一列,只有相应的内存被真正使用到了,才会被统计到RES中,提高内存使用率。这样是为什么常常看到MySQL一启动就被分配了很多的VIRT,而RES却是慢慢涨上来的原因。这里大家可能有个疑问,为啥不用malloc。其实查阅malloc文档,可以发现,当请求的内存数量大于MMAP_THRESHOLD(默认为128KB)时候,malloc底层就是调用了mmap。在InnoDB中,默认使用mmap来分配。
分配完了内存,<code>buf_chunk_init</code>函数中,把这片内存划分为两个部分,前一部分是数据页控制体(buf_block_t),在阿里云RDS MySQL 5.6 release版本中,每个buf_block_t是424字节,一共有innodb_buffer_pool_size/UNIV_PAGE_SIZE个。后一部分是真正的数据页,按照UNIV_PAGE_SIZE分隔。假设page大小为16KB,则数据页控制体占的内存:数据页约等于1:38.6,也就是说如果innodb_buffer_pool_size被配置为40G,则需要额外的1G多空间来存数据页的控制体。
划分完空间后,遍历数据页控制体,设置buf_block_t::frame指针,指向真正的数据页,然后把这些数据页加入到Free List中即可。初始化完Buffer Chunks的内存,还需要初始化BUF_BLOCK_POOL_WATCH类型的数据页控制块,page hash的结构体,zip hash的结构体(所有被压缩页的伙伴系统分配走的数据页面会加入到这个哈希表中)。注意这些内存是额外分配的,不包含在Buffer Chunks中。
除了<code>buf_pool_init</code>外,建议读者参考一下<code>but_pool_free</code>这个内存释放函数,加深对Buffer Pool相关内存的理解。</p>
<h2>Buf_page_get函数解析</h2>
<p>这个函数极其重要,是其他模块获取数据页的外部接口函数。如果请求的数据页已经在Buffer Pool中了,修改相应信息后,就直接返回对应数据页指针,如果Buffer Pool中没有相关数据页,则从磁盘中读取。<code>Buf_page_get</code>是一个宏定义,真正的函数为<code>buf_page_get_gen</code>,参数主要为space_id, page_no, lock_type, mode以及mtr。这里主要介绍一个mode这个参数,其表示读取的方式,目前支持六种,前三种用的比较多。
**BUF_GET: ** 默认获取数据页的方式,如果数据页不在Buffer Pool中,则从磁盘读取,如果已经在Buffer Pool中,需要判断是否要把他加入到young list中以及判断是否需要进行线性预读。如果是读取则加读锁,修改则加写锁。
**BUF_GET_IF_IN_POOL: ** 只在Buffer Pool中查找这个数据页,如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则直接返回空。加锁方式与BUF_GET类似。
**BUF_PEEK_IF_IN_POOL: ** 与BUF_GET_IF_IN_POOL类似,只是即使条件满足也不把它加入到young list中也不进行线性预读。加锁方式与BUF_GET类似。
**BUF_GET_NO_LATCH: ** 不管对数据页是读取还是修改,都不加锁。其他方面与BUF_GET类似。
**BUF_GET_IF_IN_POOL_OR_WATCH: ** 只在Buffer Pool中查找这个数据页,如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则设置watch。加锁方式与BUF_GET类似。这个是要是给purge线程用。
**BUF_GET_POSSIBLY_FREED: ** 这个mode与BUF_GET类似,只是允许相应的数据页在函数执行过程中被释放,主要用在估算Btree两个slot之前的数据行数。
接下来,我们简要分析一下这个函数的主要逻辑。</p>
<ul>
<li>首先通过<code>buf_pool_get</code>函数依据space_id和page_no查找指定的数据页在那个Buffer Pool Instance里面。算法很简单<code>instance_no = (space_id << 20 + space_id + page_no >> 6) % instance_num</code>,也就是说先通过space_id和page_no算出一个fold value然后按照instance的个数取余数即可。这里有个小细节,page_no的第六位被砍掉,这是为了保证一个extent的数据能被缓存到同一个Buffer Pool Instance中,便于后面的预读操作。</li>
<li>接着,调用<code>buf_page_hash_get_low</code>函数在page hash中查找这个数据页是否已经被加载到对应的Buffer Pool Instance中,如果没有找到这个数据页且mode为BUF_GET_IF_IN_POOL_OR_WATCH则设置watch数据页(<code>buf_pool_watch_set</code>),接下来,如果没有找到数据页且mode为BUF_GET_IF_IN_POOL、BUF_PEEK_IF_IN_POOL或者BUF_GET_IF_IN_POOL_OR_WATCH函数直接返回空,表示没有找到数据页。如果没有找到数据但是mode为其他,就从磁盘中同步读取(<code>buf_read_page</code>)。在读取磁盘数据之前,我们如果发现需要读取的是非压缩页,则先从Free List中获取空闲的数据页,如果Free List中已经没有了,则需要通过刷脏来释放数据页,这里的一些细节我们后续在LRU模块再分析,获取到空闲的数据页后,加入到LRU List中(<code>buf_page_init_for_read</code>)。在读取磁盘数据之前,我们如果发现需要读取的是压缩页,则临时分配一个buf_page_t用来做控制体,通过伙伴系统分配到压缩页存数据的空间,最后同样加入到LRU List中(<code>buf_page_init_for_read</code>)。做完这些后,我们就调用IO子系统的接口同步读取页面数据,如果读取数据失败,我们重试100次(<code>BUF_PAGE_READ_MAX_RETRIES</code>)然后触发断言,如果成功则判断是否要进行随机预读(随机预读相关的细节我们也在预读预写模块分析)。</li>
<li>接着,读取数据成功后,我们需要判断读取的数据页是不是压缩页,如果是的话,因为从磁盘中读取的压缩页的控制体是临时分配的,所以需要重新分配block(<code>buf_LRU_get_free_block</code>),把临时分配的buf_page_t给释放掉,用<code>buf_relocate</code>函数替换掉,接着进行解压,解压成功后,设置state为BUF_BLOCK_FILE_PAGE,最后加入Unzip LRU List中。</li>
<li>接着,我们判断这个页是否是第一次访问,如果是则设置buf_page_t::access_time,如果不是,我们则判断其是不是在Quick List中,如果在Quick List中且当前事务不是加过Hint语句的事务,则需要把这个数据页从Quick List删除,因为这个页面被其他的语句访问到了,不应该在Quick List中了。</li>
<li>接着,如果mode不为BUF_PEEK_IF_IN_POOL,我们需要判断是否把这个数据页移到young list中,具体细节在后面LRU模块中分析。</li>
<li>接着,如果mode不为BUF_GET_NO_LATCH,我们给数据页加上读写锁。</li>
<li>最后,如果mode不为BUF_PEEK_IF_IN_POOL且这个数据页是第一次访问,则判断是否需要进行线性预读(线性预读相关的细节我们也在预读预写模块分析)。</li>
</ul>
<h2>LRU List中young list和old list的维护</h2>
<p>当LRU List链表大于512(<code>BUF_LRU_OLD_MIN_LEN</code>)时,在逻辑上被分为两部分,前面部分存储最热的数据页,这部分链表称作young list,后面部分则存储冷数据页,这部分称作old list,一旦Free List中没有页面了,就会从冷页面中驱逐。两部分的长度由参数innodb_old_blocks_pct控制。每次加入或者驱逐一个数据页后,都要调整young list和old list的长度(<code>buf_LRU_old_adjust_len</code>),同时引入<code>BUF_LRU_OLD_TOLERANCE</code>来防止链表调整过频繁。当LRU List链表小于512,则只有old list。
新读取进来的页面默认被放在old list头,在经过innodb_old_blocks_time后,如果再次被访问了,就挪到young list头上。一个数据页被读入Buffer Pool后,在小于innodb_old_blocks_time的时间内被访问了很多次,之后就不再被访问了,这样的数据页也很快被驱逐。这个设计认为这种数据页是不健康的,应该被驱逐。
此外,如果一个数据页已经处于young list,当它再次被访问的时候,不会无条件的移动到young list头上,只有当其处于young list长度的1/4(大约值)之后,才会被移动到young list头部,这样做的目的是减少对LRU List的修改,否则每访问一个数据页就要修改链表一次,效率会很低,因为LRU List的根本目的是保证经常被访问的数据页不会被驱逐出去,因此只需要保证这些热点数据页在头部一个可控的范围内即可。相关逻辑可以参考函数<code>buf_page_peek_if_too_old</code>。</p>
<h2>buf_LRU_get_free_block函数解析</h2>
<p>这个函数以及其调用的函数可以说是整个LRU模块最重要的函数,在整个Buffer Pool模块中也有举足轻重的作用。如果能把这几个函数吃透,相信其他函数很容易就能读懂。</p>
<ul>
<li>首先,如果是使用ENGINE_NO_CACHE发送过来的SQL需要读取数据,则优先从Quick List中获取(<code>buf_quick_lru_get_free</code>)。</li>
<li>接着,统计Free List和LRU List的长度,如果发现他们再Buffer Chunks占用太少的空间,则表示太多的空间被行锁,自使用哈希等内部结构给占用了,一般这些都是大事务导致的。这时候会给出报警。</li>
<li>接着,查看Free List中是否还有空闲的数据页(<code>buf_LRU_get_free_only</code>),如果有则直接返回,否则进入下一步。大多数情况下,这一步都能找到空闲的数据页。</li>
<li>如果Free List中已经没有空闲的数据页了,则会尝试驱逐LRU List末尾的数据页。如果系统有压缩页,情况就有点复杂,InnoDB会调用<code>buf_LRU_evict_from_unzip_LRU</code>来决定是否驱逐压缩页,如果Unzip LRU List大于LRU List的十分之一或者当前InnoDB IO压力比较大,则会优先从Unzip LRU List中把解压页给驱逐,否则会从LRU List中把解压页和压缩页同时驱逐。不管走哪条路径,最后都调用了函数<code>buf_LRU_free_page</code>来执行驱逐操作,这个函数由于要处理压缩页解压页各种情况,极其复杂。大致的流程:首先判断是否是脏页,如果是则不驱逐,否则从LRU List中把链表删除,必要的话还从Unzip LRU List移走这个数据页(<code>buf_LRU_block_remove_hashed</code>),接着如果我们选择保留压缩页,则需要重新创建一个压缩页控制体,插入LRU List中,如果是脏的压缩页还要插入到Flush List中,最后才把删除的数据页插入到Free List中(<code>buf_LRU_block_free_hashed_page</code>)。</li>
<li>如果在上一步中没有找到空闲的数据页,则需要刷脏了(<code>buf_flush_single_page_from_LRU</code>),由于buf_LRU_get_free_block这个函数是在用户线程中调用的,所以即使要刷脏,这里也是刷一个脏页,防止刷过多的脏页阻塞用户线程。</li>
<li>如果上一步的刷脏因为数据页被其他线程读取而不能刷脏,则重新跳转到上述第二步。进行第二轮迭代,与第一轮迭代的区别是,第一轮迭代在扫描LRU List时,最多只扫描innodb_lru_scan_depth个,而在第二轮迭代开始,扫描整个LRU List。如果很不幸,这一轮还是没有找到空闲的数据页,从三轮迭代开始,在刷脏前等待10ms。</li>
<li>最终找到一个空闲页后,page的state为BUF_BLOCK_READY_FOR_USE。</li>
</ul>
<h2>控制全表扫描不增加cache数据到Buffer Pool</h2>
<p>全表扫描对Buffer Pool的影响比较大,即使有old list作用,但是old list默认也占Buffer Pool的3/8。因此,阿里云RDS引入新的语法ENGINE_NO_CACHE(例如:SELECT ENGINE_NO_CACHE count(*) FROM t1)。如果一个SQL语句中带了ENGINE_NO_CACHE这个关键字,则由它读入内存的数取据页都放入Quick List中,当这个语句结束时,会删除它独占的数据页。同时引入两个参数。innodb_rds_trx_own_block_max这个参数控制使用Hint的每个事物最多能拥有多少个数据页,如果超过这个数据就开始驱逐自己已有的数据页,防止大事务占用过多的数据页。innodb_rds_quick_lru_limit_per_instance这个参数控制每个Buffer Pool Instance中Quick List的长度,如果超过这个长度,后续的请求都从Quick List中驱逐数据页,进而获取空闲数据页。</p>
<h2>删除指定表空间所有的数据页</h2>
<p>函数(<code>buf_LRU_remove_pages</code>)提供了三种模式,第一种(<code>BUF_REMOVE_ALL_NO_WRITE</code>),删除Buffer Pool中所有这个类型的数据页(LRU List和Flush List)同时Flush List中的数据页也不写回数据文件,这种适合rename table和5.6表空间传输新特性,因为space_id可能会被复用,所以需要清除内存中的一切,防止后续读取到错误的数据。第二种(<code>BUF_REMOVE_FLUSH_NO_WRITE</code>),仅仅删除Flush List中的数据页同时Flush List中的数据页也不写回数据文件,这种适合drop table,即使LRU List中还有数据页,但由于不会被访问到,所以会随着时间的推移而被驱逐出去。第三种(<code>BUF_REMOVE_FLUSH_WRITE</code>),不删除任何链表中的数据仅仅把Flush List中的脏页都刷回磁盘,这种适合表空间关闭,例如数据库正常关闭的时候调用。这里还有一点值得一提的是,由于对逻辑链表的变动需要加锁且删除指定表空间数据页这个操作是一个大操作,容易造成其他请求被饿死,所以InnoDB做了一个小小的优化,每删除BUF_LRU_DROP_SEARCH_SIZE个数据页(默认为1024)就会释放一下Buffer Pool Instance的mutex,便于其他线程执行。</p>
<h2>LRU_Manager_Thread</h2>
<p>这是一个系统线程,随着InnoDB启动而启动,作用是定期清理出空闲的数据页(数量为innodb_LRU_scan_depth)并加入到Free List中,防止用户线程去做同步刷脏影响效率。线程每隔一定时间去做BUF_FLUSH_LRU,即首先尝试从LRU中驱逐部分数据页,如果不够则进行刷脏,从Flush List中驱逐(<code>buf_flush_LRU_tail</code>)。线程执行的频率通过以下策略计算:我们设定<code>max_free_len = innodb_LRU_scan_depth * innodb_buf_pool_instances</code>,如果Free List中的数量小于max_free_len的1%,则sleep time为零,表示这个时候空闲页太少了,需要一直执行buf_flush_LRU_tail从而腾出空闲的数据页。如果Free List中的数量介于max_free_len的1%-5%,则sleep time减少50ms(默认为1000ms),如果Free List中的数量介于max_free_len的5%-20%,则sleep time不变,如果Free List中的数量大于max_free_len的20%,则sleep time增加50ms,但是最大值不超过<code>rds_cleaner_max_lru_time</code>。这是一个自适应的算法,保证在大压力下有足够用的空闲数据页(<code>lru_manager_adapt_sleep_time</code>)。</p>
<h2>Hazard Pointer</h2>
<p>在学术上,Hazard Pointer是一个指针,如果这个指针被一个线程所占有,在它释放之前,其他线程不能对他进行修改,但是在InnoDB里面,概念刚好相反,一个线程可以随时访问Hazard Pointer,但是在访问后,他需要调整指针到一个有效的值,便于其他线程使用。我们用Hazard Pointer来加速逆向的逻辑链表遍历。
先来说一下这个问题的背景,我们知道InnoDB中可能有多个线程同时作用在Flush List上进行刷脏,例如LRU_Manager_Thread和Page_Cleaner_Thread。同时,为了减少锁占用的时间,InnoDB在进行写盘的时候都会把之前占用的锁给释放掉。这两个因素叠加在一起导致同一个刷脏线程刷完一个数据页A,就需要回到Flush List末尾(因为A之前的脏页可能被其他线程给刷走了,之前的脏页可能已经不在Flush list中了),重新扫描新的可刷盘的脏页。另一方面,数据页刷盘是异步操作,在刷盘的过程中,我们会把对应的数据页IO_FIX住,防止其他线程对这个数据页进行操作。我们假设某台机器使用了非常缓慢的机械硬盘,当前Flush List中所有页面都可以被刷盘(<code>buf_flush_ready_for_replace</code>返回true)。我们的某一个刷脏线程拿到队尾最后一个数据页,IO fixed,发送给IO线程,最后再从队尾扫描寻找可刷盘的脏页。在这次扫描中,它发现最后一个数据页(也就是刚刚发送到IO线程中的数据页)状态为IO fixed(磁盘很慢,还没处理完)所以不能刷,跳过,开始刷倒数第二个数据页,同样IO fixed,发送给IO线程,然后再次重新扫描Flush List。它又发现尾部的两个数据页都不能刷新(因为磁盘很慢,可能还没刷完),直到扫描到倒数第三个数据页。所以,存在一种极端的情况,如果磁盘比较缓慢,刷脏算法性能会从O(N)退化成O(N*N)。
要解决这个问题,最本质的方法就是当刷完一个脏页的时候不要每次都从队尾重新扫描。我们可以使用Hazard Pointer来解决,方法如下:遍历找到一个可刷盘的数据页,在锁释放之前,调整Hazard Pointer使之指向Flush List中下一个节点,注意一定要在持有锁的情况下修改。然后释放锁,进行刷盘,刷完盘后,重新获取锁,读取Hazard Pointer并设置下一个节点,然后释放锁,进行刷盘,如此重复。当这个线程在刷盘的时候,另外一个线程需要刷盘,也是通过Hazard Pointer来获取可靠的节点,并重置下一个有效的节点。通过这种机制,保证每次读到的Hazard Pointer是一个有效的Flush List节点,即使磁盘再慢,刷脏算法效率依然是O(N)。
这个解法同样可以用到LRU List驱逐算法上,提高驱逐的效率。相应的Patch是在MySQL 5.7上首次提出的,阿里云RDS把其Port到了我们5.6的版本上,保证在大并发情况下刷脏算法的效率。</p>
<h2>Page_Cleaner_Thread</h2>
<p>这也是一个InnoDB的后台线程,主要负责Flush List的刷脏,避免用户线程同步刷脏页。与LRU_Manager_Thread线程相似,其也是每隔一定时间去刷一次脏页。其sleep time也是自适应的(<code>page_cleaner_adapt_sleep_time</code>),主要由三个因素影响:当前的lsn,Flush list中的oldest_modification以及当前的同步刷脏点(<code>log_sys->max_modified_age_sync</code>,有redo log的大小和数量决定)。简单的来说,lsn - oldest_modification的差值与同步刷脏点差距越大,sleep time就越长,反之sleep time越短。此外,可以通过<code>rds_page_cleaner_adaptive_sleep</code>变量关闭自适应sleep time,这是sleep time固定为1秒。
与LRU_Manager_Thread每次固定执行清理innodb_LRU_scan_depth个数据页不同,Page_Cleaner_Thread每次执行刷的脏页数量也是自适应的,计算过程有点复杂(<code>page_cleaner_flush_pages_if_needed</code>)。其依赖当前系统中脏页的比率,日志产生的速度以及几个参数。innodb_io_capacity和innodb_max_io_capacity控制每秒刷脏页的数量,前者可以理解为一个soft limit,后者则为hard limit。innodb_max_dirty_pages_pct_lwm和innodb_max_dirty_pages_pct_lwm控制脏页比率,即InnoDB什么脏页到达多少才算多了,需要加快刷脏频率了。innodb_adaptive_flushing_lwm控制需要刷新到哪个lsn。innodb_flushing_avg_loops控制系统的反应效率,如果这个变量配置的比较大,则系统刷脏速度反应比较迟钝,表现为系统中来了很多脏页,但是刷脏依然很慢,如果这个变量配置很小,当系统中来了很多脏页后,刷脏速度在很短的时间内就可以提升上去。这个变量是为了让系统运行更加平稳,起到削峰填谷的作用。相关函数,<code>af_get_pct_for_dirty</code>和<code>af_get_pct_for_lsn</code>。</p>
<h2>预读和预写</h2>
<p>如果一个数据页被读入Buffer Pool,其周围的数据页也有很大的概率被读入内存,与其分开多次读取,还不如一次都读入内存,从而减少磁盘寻道时间。在官方的InnoDB中,预读分两种,随机预读和线性预读。
**随机预读: ** 这种预读发生在一个数据页成功读入Buffer Pool的时候(<code>buf_read_ahead_random</code>)。在一个Extent范围(1M,如果数据页大小为16KB,则为连续的64个数据页)内,如果热点数据页大于一定数量,就把整个Extend的其他所有数据页(依据page_no从低到高遍历读入)读入Buffer Pool。这里有两个问题,首先数量是多少,默认情况下,是13个数据页。接着,怎么样的页面算是热点数据页,阅读代码发现,只有在young list前1/4的数据页才算是热点数据页。读取数据时候,使用了异步IO,结合使用<code>OS_AIO_SIMULATED_WAKE_LATER</code>和<code>os_aio_simulated_wake_handler_threads</code>便于IO合并。随机预读可以通过参数innodb_random_read_ahead来控制开关。此外,<code>buf_page_get_gen</code>函数的mode参数不影响随机预读。
**线性预读: ** 这中预读只发生在一个边界的数据页(Extend中第一个数据页或者最后一个数据页)上(<code>buf_read_ahead_linear</code>)。在一个Extend范围内,如果大于一定数量(通过参数innodb_read_ahead_threshold控制,默认为56)的数据页是被顺序访问(通过判断数据页access time是否为升序或者逆序来确定)的,则把下一个Extend的所有数据页都读入Buffer Pool。读取的时候依然采用异步IO和IO合并策略。线性预读触发的条件比较苛刻,触发操作的是边界数据页同时要求其他数据页严格按照顺序访问,主要是为了解决全表扫描时的性能问题。线性预读可以通过参数<code>innodb_read_ahead_threshold</code>来控制开关。此外,当<code>buf_page_get_gen</code>函数的mode为BUF_PEEK_IF_IN_POOL时,不触发线性预读。
InnoDB中除了有预读功能,在刷脏页的时候,也能进行预写(<code>buf_flush_try_neighbors</code>)。当一个数据页需要被写入磁盘的时候,查找其前面或者后面邻居数据页是否也是脏页且可以被刷盘(没有被IOFix且在old list中),如果可以的话,一起刷入磁盘,减少磁盘寻道时间。预写功能可以通过<code>innodb_flush_neighbors</code>参数来控制。不过在现在的SSD磁盘下,这个功能可以关闭。</p>
<h2>Double Write Buffer(dblwr)</h2>
<p>服务器突然断电,这个时候如果数据页被写坏了(例如数据页中的目录信息被损坏),由于InnoDB的redolog日志不是完全的物理日志,有部分是逻辑日志,因此即使奔溃恢复也无法恢复到一致的状态,只能依靠Double Write Buffer先恢复完整的数据页。Double Write Buffer主要是解决数据页半写的问题,如果文件系统能保证写数据页是一个原子操作,那么可以把这个功能关闭,这个时候每个写请求直接写到对应的表空间中。
Double Write Buffer大小默认为2M,即128个数据页。其中分为两部分,一部分留给batch write,另一部分是single page write。前者主要提供给批量刷脏的操作,后者留给用户线程发起的单页刷脏操作。batch write的大小可以由参数<code>innodb_doublewrite_batch_size</code>控制,例如假设innodb_doublewrite_batch_size配置为120,则剩下8个数据页留给single page write。
假设我们要进行批量刷脏操作,我们会首先写到内存中的Double Write Buffer(也是2M,在系统初始化中分配,不使用Buffer Chunks空间),如果dblwr写满了,一次将其中的数据刷盘到系统表空间指定位置,注意这里是同步IO操作,在确保写入成功后,然后使用异步IO把各个数据页写回自己的表空间,由于是异步操作,所有请求下发后,函数就返回,表示写成功了(<code>buf_dblwr_add_to_batch</code>)。不过这个时候后续的写请求依然会阻塞,知道这些异步操作都成功,才清空系统表空间上的内容,后续请求才能被继续执行。这样做的目的就是,如果在异步写回数据页的时候,系统断电,发生了数据页半写,这个时候由于系统表空间中的数据页是完整的,只要从中拷贝过来就行(<code>buf_dblwr_init_or_load_pages</code>)。
异步IO请求完成后,会检查数据页的完整性以及完成change buffer相关操作,接着IO helper线程会调用<code>buf_flush_write_complete</code>函数,把数据页从Flush List删除,如果发现batch write中所有的数据页都写成了,则释放dblwr的空间。</p>
<h2>Buddy伙伴系统</h2>
<p>与内存分配管理算法类似,InnoDB中的伙伴系统也是用来管理不规则大小内存分配的,主要用在压缩页的数据上。前文提到过,InnoDB中的压缩页可以有16K,8K,4K,2K,1K这五种大小,压缩页大小的单位是表,也就是说系统中可能存在很多压缩页大小不同的表。使用伙伴体统来分配和回收,能提高系统的效率。
申请空间的函数是<code>buf_buddy_alloc</code>,其首先在zip free链表中查看指定大小的块是否还存在,如果不存在则从更大的链表中分配,这回导致一些列的分裂操作。例如需要一块4K大小的内存,则先从4K链表中查找,如果有则直接返回,没有则从8K链表中查找,如果8K中还有空闲的,则把8K分成两部分,低地址的4K提供给用户,高地址的4K插入到4K的链表中,便与后续使用。如果8K中也没有空闲的了,就从16K中分配,16K首先分裂成2个8K,高地址的插入到8K链表中,低地址的8K继续分裂成2个4K,低地址的4K返回给用户,高地址的4K插入到4K的链表中。假设16K的链表中也没有空闲的了,则调用<code>buf_LRU_get_free_block</code>获取新的数据页,然后把这个数据页加入到zip hash中,同时设置state状态为BUF_BLOCK_MEMORY,表示这个数据页存储了压缩页的数据。
释放空间的函数是<code>buf_buddy_free</code>,相比于分配空间的函数,有点复杂。假设释放一个4K大小的数据块,其先把4K放回4K对应的链表,接着会查看其伙伴(释放块是低地址,则伙伴是高地址,释放块是高地址,则伙伴是低地址)是否也被释放了,如果也被释放了则合并成8K的数据块,然后继续寻找这个8K数据块的伙伴,试图合并成16K的数据块。如果发现伙伴没有被释放,函数并不会直接退出而是把这个伙伴给挪走(<code>buf_buddy_relocate</code>),例如8K数据块的伙伴没有被释放,系统会查看8K的链表,如果有空闲的8K块,则把这个伙伴挪到这个空闲的8K上,这样就能合并成16K的数据块了,如果没有,函数才放弃合并并返回。通过这种relocate操作,内存碎片会比较少,但是涉及到内存拷贝,效率会比较低。</p>
<h2>Buffer Pool预热</h2>
<p>这个也是官方5.6提供的新功能,可以把当前Buffer Pool中的数据页按照space_id和page_no dump到外部文件,当数据库重启的时候,Buffer Pool就可以直接恢复到关闭前的状态。
**Buffer Pool Dump: ** 遍历所有Buffer Pool Instance的LRU List,对于其中的每个数据页,按照space_id和page_no组成一个64位的数字,写到外部文件中即可(<code>buf_dump</code>)。
**Buffer Pool Load: ** 读取指定的外部文件,把所有的数据读入内存后,使用归并排序对数据排序,以64个数据页为单位进行IO合并,然后发起一次真正的读取操作。排序的作用就是便于IO合并(<code>buf_load</code>)。</p>
<h1>总结</h1>
<p>InnoDB的Buffer Pool可以认为很简单,就是LRU List和Flush List,但是InnoDB对其做了很多性能上的优化,例如减少加锁范围,page hash加速查找等,导致具体的实现细节相对比较复杂,尤其是引入压缩页这个特性后,有些核心代码变得晦涩难懂,需要读者细细琢磨。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/文章/Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/文章/MySQL · 引擎特性 · InnoDB IO子系统.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997fd12b458b66","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>