🌟 JVM调优
🍊 目的和原则
JVM调优的主要目的是减少GC的频率和Full GC的次数,并降低STW的停顿时间和次数。
首先,尽可能让对象都在新生代里分配和回收。由于新生代的垃圾回收速度比老年代要快得多,因此将对象尽量分配到新生代中可以减少老年代的负担,降低GC的频率和Full GC的次数。为避免大量对象进入老年代,可以设置适当的新生代大小和比例,以确保不会频繁地进行老年代的垃圾回收。
其次,给系统充足的内存大小。为避免频繁的垃圾回收和Full GC,可以适当增加系统的内存大小。此外,还可以设置合理的堆空间大小,使得堆空间不会快速被占满。这样可以减少GC的频率和Full GC的次数,降低STW的停顿时间和次数,提高系统的稳定性和性能。
最后,避免频繁进行老年代的垃圾回收。老年代的垃圾回收通常是比较耗时的,因此应该尽量避免频繁进行老年代的垃圾回收。可以通过合理设置新生代大小、年龄等参数以及使用CMS等垃圾回收器来减少对老年代的垃圾回收,从而降低GC的频率和Full GC的次数,提高系统的性能和稳定性。
🍊 可能导致Full GC
以下是可能引起内存泄漏并导致Full GC的一些情况:
(1)对象的长期存活:如果某些对象在JVM中存活了很长时间,可能会导致内存泄漏,并在堆积积累的过程中触发Full GC。
(2)大对象:如果程序中创建了大的对象,但这些对象无法被回收,可能会导致内存泄漏,并触发Full GC。
(3)永久代内存溢出:当应用程序使用大量字符串或其他可序列化的类时,可能会导致永久代内存耗尽,并触发Full GC。
(4)字符串:如果在程序中频繁创建字符串,并且它们不被清除,可能会导致内存泄漏,并触发Full GC。
(5)无用的类和对象:如果程序中存在许多无用的类和对象,可能会导致内存泄漏,并触发Full GC。
(6)ThreadLocal:如果程序中使用了ThreadLocal,但没有正确地清除线程本地存储,可能会导致内存泄漏,并触发Full GC。
(7)频繁创建对象:如果程序中频繁地创建对象,但没有正确地清除这些对象,可能会导致内存泄漏,并触发Full GC。
首先,在使用System.gc()方法时,尽量避免使用该方法,因为调用该方法会建议JVM进行Full GC,而可能会增加Full GC的频率,从而增加间歇性停顿的次数。为了减少该方法的使用,可以禁止RMI调用System.gc(),通过-XX:+DisableExplicitGC参数来实现。
其次,如果Survivor区域的对象满足晋升到老年代的条件,但是晋升进入老年代的对象大小大于老年代的可用内存时,就会触发Full GC。为了避免这种情况,可以通过调整JVM参数或者设计应用程序的算法,来减少对象在老年代的数量。
JDK8开始,Metaspace区取代了永久代(PermGen),Metaspace使用的是本地内存。通过调整JVM参数限制Metaspace的大小,当Metaspace区内存达到阈值时,也会引发Full GC。可以通过调整JVM参数或者设计应用程序的算法,来减少Metaspace区内存的使用。
在Survivor区域的对象满足晋升到老年代的条件时,也可能会引起Full GC。可以通过调整JVM参数来控制对象的晋升行为,从而减少Full GC的发生。
此外,如果堆中产生的大对象超过阈值,也会引发Full GC。可以通过调整JVM参数或者优化应用程序算法,来减少大对象的产生。
最后,老年代连续空间不足或者CMS GC时出现promotion failed和concurrent mode failure所致的Full GC,也可以通过调整JVM参数或者设计应用程序算法来减少其发生。
🍊 场景调优
对于场景问题,可以从如下几个大方向进行设计:
- 大访问压力下,MGC 频繁一些是正常的,只要MGC 延迟不导致停顿时间过长或者引发FGC ,那可以适当的增大Eden 空间大小,降低频繁程度,同时要保证,空间增大对垃圾回收产生的停顿时间增长是可以接受的。
- 如果MinorGC 频繁,且容易引发 Full GC。需要从如下几个角度进行分析。
- 每次MGC存活的对象的大小,是否能够全部移动到 S1区,如果S1 区大小 < MGC 存活的对象大小,这批对象会直接进入老年代。注意 了,这批对象的年龄才1岁,很有可能再多等1次MGC 就能被回收了,可是却进入了老年代,只能等到Full GC 进行回收,很可怕。这种情况下,应该在系统压测的情况下,实时监控MGC存活的对象大小,并合理调整eden和s 区的大小以及比例。
- 还有一种情况会导致对象在未达到15岁之前,直接进入老年代,就是S1区的对象,相同年龄的对象所占总空间大小>s1区空间大小的一半,所以为了应对这种情况,对于S区的大小的调整就要考虑:尽量保证峰值状态下,S1区的对象所占空间能够在MGC的过程中,相同对象年龄所占空间不大于S1区空间的一半, 因此对于S1空间大小的调整,也是十分重要的。
- 由于大对象创建频繁,导致Full GC 频繁。对于大对象,JVM专门有参数进行控制,-XX: PretenureSizeThreshold。超过这个参数值的对象,会直接进入老年代,只能等到full GC 进行回收,所以在系统压测过程中,要重点监测大对象的产生。如果能够优化对象大小,则进行代码层面的优化,优化如:根据业务需求看是否可以将该大对象设置为单例模式下的对象,或者该大对象是否可以进行拆分使用,或者如果大对象确定使用完成后,将该对象赋值为null,方便垃圾回收。
如果代码层面无法优化,则需要考虑:
- 调高-XX: PretenureSizeThreshold参数的大小,使对象有机会在eden区创建,有机会经历MGC以被回收。但是这个参数的调整要结合MGC过程中Eden区的大小是否能够承载,包括S1区的大小承载问题。
- 这是最不希望发生的情况, 如果必须要进入老年代,也要尽量保证,该对象确实是长时间使用的对象,放入老年代的总对象创建量不会造成老年代的内存空间迅速长满发生Full GC,在这种情况下,可以通过定时脚本,在业务系统不繁忙情况下,主动触发full gc。
- MGC 与 FGC 停顿时间长导致影响用户体验。其实对于停顿时间长的问题无非就两种情况:
- gc 真实回收过程时间长,即real time时间长。这种时间长大部分是因为内存过大导致,从标记到清理的过程中需要对很大的空间进行操作,导致停顿时间长。这种情况,要考虑减少堆内存大 小,包括新生代和老年代,比如之前使用16G的堆内存,可以考虑将16G 内存拆分为4个4G的内存区域,可以单台机器部署JVM逻辑集群,也可以为了降低GC回收时间,进行4节点的分布式部署,这里的分布式部署是为了降低 GC垃圾回收时间。
- gc真实回收时间 real time 并不长,但是user time(用户态执行时间) 和 sys time(核心态执行时间)时间长,导致从客户角度来看,停顿时间过长。这种情况,要考虑线程是否及时达到了安全点,通过
-XX:+PrintSafepointStatistics
和-XX: PrintSafepointStatisticsCount=1
去查看安全点日志,如果有长时间未达到安全点的线程,再通过参数-XX: +SafepointTimeout
和-XX:SafepointTimeoutDelay=2000
两个参数来找到大于2000ms到达安全点的线程,这里 的2000ms可以根据情况自己设置,然后对代码进行针对的调整。除了安全点问题,也有可能是操作系统本身负载比较高,导致处理速度过慢,线程达到安全点时间长,因此需要同时检测操作系统自身的运行情况。
- 内存泄漏导致的MGC和FGC频繁,最终引发oom。纯代码级别导致的MGC和FGC频繁。如果是这种情况,那就只能对代码进行大范围的调整,这种情况就非常多了,而且会很糟糕。如大循环体中的new 对象,未使用合理容器进行对象托管导致对象创建频繁,不合理的数据结构使用等等。 总之,JVM的调优无非就一个目的,在系统可接受的情况下达到一个合理的MGC和FGC的频率以及可接受的回收时间。
🍊 JVM调优工具
Jstack是用于获取Java线程转储的工具,适用于在程序出现死锁、线程挂起等问题时,通过获取线程转储,更好地诊断和解决问题。在找出占用CPU最高的线程堆栈信息时,可以按以下步骤操作:
(1)打开命令行窗口,并进入Java应用程序所在的目录。
(2)在命令行中输入以下命令,查询Java应用程序的进程id(PID),代码如下:
ps -ef | grep java
该命令将返回所有正在运行的Java应用程序进程的详细信息,需要查找要分析的Java应用程序进程的PID。
(3)输入以下命令,使用Jstack导出Java应用程序的线程堆栈信息(将PID替换为前面查询到的Java应用程序进程的PID),代码如下:
jstack PID > thread_dump.txt
该命令将会导出当前时间点Java应用程序的线程堆栈信息到thread_dump.txt文件。
(4)打开thread_dump.txt文件,查找占用CPU最高的线程。在文件中,可以查找到每个线程的ID,状态和堆栈信息。找到CPU使用率最高的线程,查看其堆栈信息,尝试从中找到问题所在。可以使用线程ID在文件中搜索线程的堆栈信息,代码如下:
grep "nid=0x1234" thread_dump.txt
(5)根据线程堆栈信息,定位并解决问题。根据线程堆栈信息,可以判断线程是否处于阻塞状态,是否存在死锁等问题,从而定位并解决问题。
🌟 MySQL调优
🍊 表结构设计
在进行数据库设计时,开发者需要关注表的规划。
首先,开发者要了解MySQL数据库的页大小。当表中的单行数据达到16KB时,这意味着表中只能存储一条数据,这对于数据库来说是不合理的。MySQL数据库将数据从磁盘读取到内存,它使用磁盘块作为基本单位进行读取。如果一个数据块中的数据一次性被读取,那么查询效率将会提高。
以InnoDB存储引擎为例,它使用页作为数据读取单位。页是磁盘管理的最小单位,默认大小为16KB。由于系统的磁盘块存储空间通常没有这么大,InnoDB在申请磁盘空间时会使用多个地址连续的磁盘块来达到页的大小16KB。
查询数据时,一个页中的每条数据都能帮助定位到数据记录的位置,从而减少磁盘I/O操作,提高查询效率。InnoDB存储引擎在设计时会将根节点常驻内存,尽力使树的深度不超过3。这意味着在查询过程中,I/O操作不超过3次。树形结构的数据可以让系统高效地找到数据所在的磁盘块。
在这里讨论一下B树和B+树的区别。B树的结构是每个节点既包含key值也包含value值,而每个页的存储空间是16KB。如果数据较大,将会导致一个页能存储数据量的数量很小。相比之下,B+树的结构是将所有数据记录节点按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储key值信息。这样可以大大加大每个节点存储的key值数量,降低B+树的高度。
通过了解MySQL数据库底层存储的原理和数据结构,开发者在设计表时应该尽量减少单行数据的大小,将字段宽度设置得尽可能小。
在设计表时,开发者要注意以下几点以提高查询速度和存储空间利用率:
(1)避免使用text、Blob、Clob等大数据类型,它们占用的存储空间更大,读取速度较慢。
(2)尽量使用数字型字段,如性别字段用0/1的方式表示,而不是男女。这样可以控制数据量,增加同一高度下B+树容纳的数据量,提高检索速度。
(3)使用varchar/nvarchar代替char/nchar。变长字段存储空间较小,可以节省存储空间。
(4)不在数据库中存储图片、文件等大数据,可以通过第三方云存储服务存储,并提供图片或文件地址。
(5)金额字段使用decimal类型,注意长度和精度。如果存储的数据范围超过decimal的范围,建议将数据拆成整数和小数分开存储。
(6)避免给数据库留null值。尤其是时间、整数等类型,可以在建表时就设置非空约束。NULL列会使用更多的存储空间,在MySQL中处理NULL值也更复杂。为NULL的列可能导致固定大小的索引变成可变大小的索引,例如只有整数列的索引。
🍊 建索引
在建立索引时,需要权衡数据的维护速度和查询性能。以下是一些关于如何确定是否为表中字段建立索引的示例:
(1)对于经常修改的数据,建立索引会降低数据维护速度,因此不适合对这些字段建立索引,例如状态字段。
(2)对于性别字段,通常用0和1表示,但由于其区分度不高(100万用户中90万为男性,10万为女性),因此一般不需要建立索引。然而,如果性别字段的区分度非常高(例如90万男性和10万女性),而且该字段不经常更改,则可以考虑为该字段建立索引。
(3)可以在where及order by涉及的列上建立索引。
(4)对于需要查询排序、分组和联合操作的字段,适合建立索引,以提高查询性能。
(5)索引并非越多越好,一个表的索引数最好不要超过6个。当为多个字段创建索引时,表的更新速度会减慢,因此应选择具有较高区分度且不经常更改的字段创建索引。
(6)尽量让字段顺序与索引顺序一致,复合索引中的第一个字段作为条件时才会使用该索引。
(7)遵循最左前缀原则:尽量确保查询中的索引列按照最左侧的列进行匹配。
🍊 SQL优化
为了优化SQL语句,需要了解数据库的架构、索引、查询优化器以及各种SQL执行引擎的机制等技术知识。
🎉 SQL编写
在编写SQL语句时,开发者需要注意一些关键点以提高查询性能。以下是一些建议:
(1)避免在WHERE子句中对查询的列执行范围查询(如NULL值判断、!=、<>、or作为连接条件、IN、NOT IN、LIKE模糊查询、BETWEEN)和使用“=”操作符左侧进行函数操作、算术运算或表达式运算,因为这可能导致索引失效,从而导致全表扫描。
(2)对于JOIN操作,如果数据量较大,先分页再JOIN可以避免大量逻辑读,从而提高性能。
(3)使用COUNT()可能导致全表扫描,如有WHERE条件的SQL,WHERE条件字段未创建索引会进行全表扫描。COUNT()只统计总行数,聚簇索引的叶子节点存储整行记录,非聚簇索引的叶子节点存储行记录主键值。非聚簇索引比聚簇索引小,选择最小的非聚簇索引扫表更高效。
(4)当数据量较大时,查询只返回必要的列和行,LIMIT 分页限制返回的数据,减少请求的数据量,插入建议分批次批量插入,以提高性能。
(5)对于大连接的查询SQL,由于数据量较多、又是多表,容易出现整个事务日志较大,消耗大量资源,从而导致一些小查询阻塞,所以优化方向是将它拆分成单表查询,在应用程序中关联结果,这样更利于高性能可伸缩,同时由于是单表减少了锁竞争效率上也有一定提升。
(6)尽量明确只查询所需列,避免使用SELECT *。SELECT *会导致全表扫描,降低性能。若必须使用SELECT *,可以考虑使用MySQL 5.6及以上版本,因为这些版本提供了离散读优化(Discretized Read Optimization),将离散度高的列放在联合索引的前面,以提高性能。
索引下推(ICP,Index Condition Pushdown)优化:ICP优化将部分WHERE条件的过滤操作下推到存储引擎层,减少上层SQL层对记录的索取,从而提高性能。在某些查询场景下,ICP优化可以大大减少上层SQL层与存储引擎的交互,提高查询速度。
多范围读取(MRR,Multi-Range Read)优化:MRR优化将磁盘随机访问转化为顺序访问,提高查询性能。当查询辅助索引时,首先根据结果将查询得到的索引键值存放于缓存中。然后,根据主键对缓存中的数据进行排序,并按照排序顺序进行书签查找。
这种顺序查找减少了对缓冲池中页的离散加载次数,可以提高批量处理对键值查询操作的性能。
在编写SQL时,使用EXPLAIN语句观察索引是否失效是个好习惯。索引失效的原因有以下几点:
(1)如果查询条件中包含OR,即使其中部分条件带有索引,也无法使用。
(2)对于复合索引,如果不使用前列,后续列也无法使用。
(3)如果查询条件中的列类型是字符串,则在条件中将数据使用引号引用起来非常重要,否则索引可能失效。
(4)如果在查询条件中使用运算符(如+、-、*、/等)或函数(如substring、concat等),索引将无法使用。
(5)如果MySQL认为全表扫描比使用索引更快,则可能不使用索引。在数据较少的情况下尤其如此。
🎉 SQL优化工具
常用的SQL优化方法包括:业务层逻辑优化、SQL性能优化、索引优化。
业务层逻辑优化:开发者需要重新梳理业务逻辑,将大的业务逻辑拆分成小的逻辑块,并行处理。这样可以提高处理效率,降低数据库的访问压力。
SQL性能优化:除了编写优化的SQL语句、创建合适的索引之外,还可以使用缓存、批量操作减少数据库的访问次数,以提高查询效率。
索引优化:对于复杂的SQL语句,人工直接介入调节可能会增加工作量,且效果不一定好。开发者的索引优化经验参差不齐,因此需要使用索引优化工具,将优化过程工具化、标准化。最好是在提供SQL语句的同时,给出索引优化建议。
🎉 慢SQL优化
影响程度一般的慢查询通常在中小型企业因为项目赶进度等问题常被忽略,对于大厂基本由数据库管理员通过实时分析慢查询日志,对比历史慢查询,给出优化建议。
影响程度较大的慢查询通常会导致数据库负载过高,人工故障诊断,识别具体的慢查询SQL,及时调整,降低故障处理时长。
当前未被定义为慢查询的SQL可能随时间演化为慢查询,对于核心业务,可能引发故障,需分类接入:
(1)未上线准慢查询:需要通过发布前集成测试流水线,通常都是经验加上explain关键字识别慢查询,待解决缺陷后才能发布上线。
(2)已上线准慢查询:表数据量增加演变为慢查询,比较常见,通常会变成全表扫描,开发者可以增加慢查询配置参数log_queries_not_using_indexes记录至慢日志,实时跟进治理。
🍊 数据分区
在面对大量数据时,分区可以帮助提高查询性能。分区主要分为两类:表分区和分区表。
🎉 表分区
表分区是在创建表时定义的,需要在表建立的时候创建规则。如果要修改已有的有规则的表分区,只能新增,不能随意删除。表分区的局限性在于单个MySQL服务器支持1024个分区。
🎉 分区表
当表分区达到上限时,可以考虑垂直拆分和水平拆分。垂直拆分将单表变为多表,以增加每个分区承载的数据量。水平拆分则是将数据按照某种策略拆分为多个表。
垂直分区的优点是可以减少单个分区的数据量,从而提高查询性能。但缺点是需要考虑数据的关联性,并在SQL查询时进行反复测试以确保性能。
对于包含大文本和BLOB列的表,如果这些列不经常被访问,可以将它们划分到另一个分区,以保证数据相关性的同时提高查询速度。
🎉 水平分区
随着数据量的持续增长,需要考虑水平分区。水平分区有多种模式,例如:
(1)范围(Range)模式:允许DBA将数据划分为不同的范围。例如DBA可以将一个表按年份划分为三个分区,80年代的数据、90年代的数据以及2000年以后的数据。
(2)哈希(Hash)模式:允许DBA通过对表的一个或多个列的Hash Key进行计算,最后通过这个Hash码不同数值对应的数据区域进行分区。例如DBA可以建立一个根据主键进行分区的表。
(3)列表(List)模式:允许系统通过DBA定义列表的值所对应行数据进行分割。例如DBA建立了一个横跨三个分区的表,分别根据2021年、2022年和2023年的值对应数据。
(4)复合模式(Composite):允许将多个模式组合使用,如在初始化已经进行了Range范围分区的表上,可以对其中一个分区再进行Hash哈希分区。
🍊 灾备处理
在MySQL中,冷热备份可以帮助 开发者在不影响性能的情况下确保数据的安全性。
🎉 冷备份
当某些数据不再需要或不常访问时,可以考虑进行冷备份。冷备份是在数据库关闭时进行的数据备份,速度更快,安全性也相对更高。例如您可以将一个不再需要的月度报告数据备份到外部存储设备,以确保在需要时可以轻松访问这些数据。
🎉 热备份
对于需要实时更新的数据,可以考虑热备份。热备份是在应用程序运行时进行的数据备份,备份的是数据库中的SQL操作语句。例如您可以将用户的购物记录备份到一个在线存储服务中,以便在需要时可以查看这些数据。
🎉 冷备份与热备份的权衡
(1)冷备份速度更快,因为它不涉及应用程序的运行,但可能需要外部存储设备。
(2)热备份速度较慢,因为它涉及应用程序的运行和数据库操作的记录。
(3)冷备份更安全,因为它在数据库关闭时进行,不受应用程序影响。
(4)热备份安全性稍低,因为它在应用程序运行时进行,需要保持设备和网络环境的稳定性。
🎉 备份注意事项
(1)备份过程中要保持设备和网络环境稳定,避免因中断导致数据丢失。
(2)备份时需要仔细小心,确保备份数据的正确性,以防止恢复过程中出现问题。
(3)热备份操作要特别仔细,备份SQL操作语句时不能出错。
总之,通过对冷热数据进行备份,可以在不影响应用程序性能的情况下确保数据的安全性。在实际应用中,应根据数据的需求和业务场景选择合适的备份策略。
🍊 高可用
在生产环境中,MySQL的高可用性变得越来越重要,因为它是一个核心的数据存储和管理系统,任何错误或中断都可能导致严重的数据丢失和系统瘫痪。因此,建立高可用的MySQL环境是至关重要的。
🎉 MMM
用于监控和故障转移MySQL集群。它使用虚拟IP(VIP)机制实现集群的高可用。集群中,主节点通过一个虚拟IP地址提供数据读写服务,当出现故障时,VIP会从原主节点漂移到其他节点,由这些节点继续提供服务。双主故障切换(MMM)的主要缺点是故障转移过程过于简单粗暴,容易丢失事务,因此建议采用半同步复制以降低失败概率。
🎉 MHA
它是一种用于故障切换的工具,能在30秒内完成故障切换,并在切换过程中最大程度地保证数据一致性。高可用性与可伸缩性(MHA)主要监控主节点的状态,当检测到主节点故障时,它会提升具有最新数据的从节点成为新的主节点,并通过其他从节点获取额外信息来避免数据一致性方面的问题。MHA可以单独部署,分为Manager节点和Node节点,分别部署在单独的机器上和每台MySQL机器上。Node节点负责解析MySQL日志,而Manager节点负责探测Node节点并判断各节点的运行状况。当检测到主节点故障时,Manager节点会直接提升一个从节点为新主节点,并让其他从节点挂载到新主节点上,实现完全透明。为了降低数据丢失的风险,建议使用MHA架构。
🎉 MGR
它是MySQL官方在5.7.17版本中正式推出的一种组复制机制,主要用于解决异步复制和半同步复制中可能产生的数据不一致问题。组复制(MGR)由若干个节点组成一个复制组,事务提交后,必须经过超过半数节点的决议并通过后才能提交。引入组复制主要是为了解决传统异步复制和半同步复制可能出现的数据不一致问题。
组复制的主要优点是基本无延迟,延迟较异步复制小很多,且具有数据强一致性,可以保证事务不丢失。然而,它也存在一些局限性:
(1)仅支持InnoDB存储引擎。
(2)表必须具有主键。
(3)仅支持GTID模式,日志格式为row格式。