4.1 制约程序性能的根源

常用的性能评估指标

  • 并发:同一时间多少请求访问
  • TPS: transaction per second(写操作)
  • QPS: query per second(读请求)
  • 耗时:端到端耗时,服务端耗时,应用程序耗时
  • 95线:95%的请求落在什么范围内。(多少毫秒,屏蔽掉网络抖动等原因)
  • 99线: 99%的请求落在什么范围内

产生瓶颈的原因

  • 网络
  • 应用本身
  • 数据库
  • 缓存
  • 消息
  • 操作系统
  • 内存
  • IO
  • CPU

    4.2 JVM内存模型

    jdk8之前
    image.png

  • 程序计数器(线程私有)

计数器记录的是虚拟机字节码指令的地址(当前指令的地址)

  • java虚拟机栈

线程私有的。每个方法在执行的时候也会创建 一个栈帧,存储了局部变量,操作数,动态链接,方法返回地址。

  • 本地方法栈

和虚拟机栈类似,主要为虚拟机使用到的Native方法服务。

被所有线程共享的一块内存区域,在虚拟机启动的时候创建,用于存放对象实

  • 方法区

用于存储已经被虚拟机加载的类信息,常量,静态变量等。

JDK8之后,字符串会从方法区放到元数据区。
image.png

4.3 GC算法

标记清除算法
image.png
优点:简单
缺点:碎片化

复制回收算法
image.png

优点:吞吐量高,无碎片化
缺点:空间利用率低

标记整理
image.png
优点:兼顾了空间和时间问题
缺点:效率非最优

4.4 分代GC回收算法

分代回收

  • 年轻代: 1 Eden 2 Survivor复制算法
  • 老年代:标记整理

年龄超过15岁会之间进入老年代
image.png

回收策略总览
image.png

串行与并行

image.png
CMS
减少停顿,提高吞吐量
image.png

1.Initial Mark初始标记:标记可直达的存活对象,老年代才会被标记,只标记可达的第一个节点。
image.png
2. Concurrent Mark (并发标记) :通过遍历第一个阶段 (Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象
image.png
3. Concurrent Preclean (并发预清理) :将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引|用的对象,然后清除Card标识,因为数据发生了变化所以进行标记
image.png

  1. Concurrent Abortable Preclean (可中止的并发预清理) :尽可能承担更多的并发预处理工作,从而减轻在Final Remark阶段的stop-the-world,处理From和To区的对象,标记可达的老年代对象,并处理dirty对象
    5. Final Remark (重新标记) :重新扫描之前并发处理阶段的所有残留更新对象
    6. Concurrent Sweep (并发清理) : 清理所有未被标记的死亡对象,回收被占用的空间
    7. Concurrent Reset (并发重置) :清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构

G1
颠覆了分代收集策略。它本质上来说仍然会有eden、survivor区、老年代,但是这些内存区域并不一定是连续的内存区域,比如说他将一块内存划分成以最大32MB大小的内存空间,并且每个内存的region块都可以被定义为eden、survivor区、老年代,也可以做转化,然后当触发对应的young gc 或者old gc之后,g1会去评估每一个内存区域的垃圾收集的价值,也就是说假设,有四块eden区region ,回去扫描判断,这4块回收哪一块收益是最高的
为什么要做这样的收益的判断,主要是为了控制垃圾收集的时间,在一个大型互联网应用中,对垃圾收集时间都是有要求的,不能非常的长,假设要求垃圾收集时间最多不能超过50ms,g1会尽最大程度,完成50ms内的垃圾收集,假设我们要收集4块区域需要100ms的时间,g1就会判断对某一块的收集收益最高,垃圾是最多的,优先收集,一旦到达了时间,就不去收集退回到最终的状态,这是它的第一个优势,可以去控制垃圾收集器的时间。
第二个优势是使用了空间换时间的思想,其实CMS最大的一个劣势,还是remark过程的一个扫描问题,虽然经历了并发标记、并发预清理、并发预清理,使得重新标记的时间减少,但是依然没办法避免在做这几个阶段的时候,浪费CPU去做一些往根路径的递归寻址

g1本质上来说通过划分成不同的region区块,在每个区块中其实保存了remmberset,它是一个记忆集合,在记忆集合内本质上来说管理每个内存单元自己的所使用的GC ROOT的状态,也就是说,每个单元在做内存对象分配的时候就已经知道了我对应目前内存内的一个现状,每一个对象分别指向了什么对象

于是,在做真正意义的remark的时候,我们不需要做跨区的remark,而只需要通过这个区,如果和另一个eden区有关联对象,无需遍历所有的关联对象,而只需要查找remmberset中有多少个对象,要往里面关联,通过这种方式用空间换时间的算法来帮助减少路径扫描的耗时,同时也可以提供给我们G1收集器更快的决策,知道每一个对象的分布,这是g1最大的优点。
image.png

4.5 内存大小的取舍

1.扩大内存可以更少的触发gc .
2.内存太大触发gc时候的停顿时间会长

因此要根据你实际的业务场景设置成一个“合适”的值,并配合压测和线上环境的实际情况做不断的调优

吞吐量=花费在非GC停顿上的工作时间/总时间至少需要优化到95%
-Xms启动JVM时堆内存的大小
-Xmx堆内存最大限制
两者需要设置的一样防止扩缩容,不利于CPU计算,又要负责业务又要负责扩缩容。
-XX:NewSize年轻代大小
-XX:MaxNewSize最大年轻代大小
两者需要设置的一样防止扩缩容,一般比老年代大
-XX:SurvivorRatio Eden Survivor占比,默认为8,经过一次minor gc大概至少百分之90被清除。
Eden需要比Survivor尽可能的大,防止多次触发young gc导致年龄快速增长到可以进入老年代的case
-XX:MetaspaceSize元空间初始空间大小
-XX:MaxMetaspaceSize=512m元空间最大空间,默认是没有限制的,不建议设置,如果限制了内存空间,程序就会报错,如果不设置,就可以忽略参数由JVM决定。

4.6 GC优化

  • 将进入老年代的对象减少到最低个
  • young gc: 40ms内
  • major gc: stop the world时间总和100ms内
  • full gc: 尽可能少,且时间在1s内
  • 除了cms和g1外,其余的major gc = full gc

image.png
image.png

JDK 1.8 还是建议使用CMS
CMS Full GC条件

  • Promotion failure:由于内存碎片导致的晋升空间不足
  • Concurrent mode failed: 还未完成cms又触发了下一次major gc

CMS调优

-XX:ParallelGCThreads= N 设置年轻代的并行收集线程数,避免docker踩坑,会按照cpu核数而不是docker分配的核数
-XX:ParallelCMSThreads= N 设置cms的并行收集线程数,避免docker踩坑,这个参数会根据上一个参数做线性调整,一般上一个设置了,这个不需要设置
-XX:+UseCMSCompactAtFullCollection FullGC情况下的Initial remark or Final remark都整理内存碎片
-XX:+CMSFullGCsBeforeCompaction=4两次FullGC情况下的Initial remark or Final remark 4次后才整理内存碎片,否则每次触发FullGC才会去整理
-XX:+UseCMSInitiatingOccupancyOnly 让阈值驱动cms触发时机,设置了下面的就要设置这个否则只会一次按照70%
-XX:CMSInitiatingOccupancyFraction=70 老年代占满70%才触发cms
-XX:+CMSParallelRemarkEnabled,并行remark
-XX:+CMSScavengeBeforeRemark remark前先做一次minor gc,防止回溯到新生代,stop the world时间更长,为了在做final remark之前之前做一次,先尝试做一次minorgc,将young gc中的内容释放掉一部分,这个参数是否开启还要看具体引用场景,如果大部分是常驻内存,在老年代,开启后可能效果更差。

整个cms要根据情况去判断,去作取舍。

G1参数调优

-XX:+UseG1GC开启G1参数
-XX:MaxGCPauseMillis=n GC最大停顿时间,软性参数,JVM会尽可能满足
-XX:G1 HeapRegionSize=n每个region的大小,通过实践找到合适的值

调优Best practise

  • 多分析线上case,并设置不同的内存大小观察gc日志,寻找最佳策略
  • 通过改善参数避免common类型问题

4.7 日志优化

  • 同步日志/异步日志
  • 日志归档时间
  • 日志大小拆分

同步日志,要考虑是否有必要打出来,会导致日志刷新到硬盘的阻塞。
image.png

另一种策略异步刷盘,另一个线程从buffer中取出消息,像磁盘异步刷盘,可能会有秒级别的时间差,不一定立刻查到日志,或者宕机来不及记录日志。
image.png

log4j默认每天备份一个文件,内部日志切换的方式是:将真正读取或者写入的error.log文件给他加上一把锁,然后打一个zip包也就是日志文件其实是以zip包的方式去打在另一个log文件的zip包里,在全程打包的过程当中,这个error.log文件是被锁死的,如果当天日志量非常大,打zip包的操作就会占用很长一段时间,然后打成zip包之后才会将error.log去做清空或者去做删除,新建一个error.log。

在打zip包过程当中,所有的写操作都是无法进行的,即使是异步也会受影响。内存管道已满无法写入到内存管道,跨天日志归档,会对程序造成影响。

可以配置按天切分外,可以按大小切分,比如说按照200MB一切分。

分层归档怎么办?如controller、service、rpc,解决办法分日志归档,分时间方式,还有一种log4j提供的比较灵活的参数配置,随机偏移秒级别的日志归档,不会造成太大的影响和阻塞,对于日志优化,可能会对查找问题有影响,习惯在同一个时间查问题,如果同一个trace在不同文件存在,会在查找问题效率上有一定影响,一般来和结合ELK和日志服务器来解决。

4.8 池化策略

  • idle数量

比如线程池,coreSize设置成多少合适
4核CPU最多只能同时执行4个线程,设置太多就会发生浪费。

核心线程数=cpu核数 * 2(IO密集型的)
核心线程数=cpu核数 + 1(计算密集型的)

连接池
不宜开的过大,数据库压力会很大,如果承受10000,应用500个链接,如果两千个应用服务器,连接池要被打爆,要根据应用数量和数据库能承受数量来做取舍。

4.9 提高数据库的读写性能

单机数据库

  • 查询优化
  • 批量写
  • 索引优化
  • innodb相关优化

查询优化,读多写少进行优化。
批量写,批量写入
索引优化:之前说过
innodb:了解innodb特性

查询优化

  • 主键查询千万条记录1-10ms
  • 唯一索引 千万条记录10-100ms
  • 非唯一索引千万条记录100-1000ms
  • 无索引百万条记录1000ms+

批量写

  • for each {insert into table values(1) }
  • Execute once insert into table values (1)(2)(3).,(4)…
  • Sq|编译N次和1次的时间与空间复杂度
  • 网络消耗的时间复杂度
  • 磁盘寻址的复杂度

单机配置优化

  • max_ connection=1000增加最大链接数,默认为100
  • innodb file per_table=1 可以存储每个innodb表和他的索引在自己的文件中,对索引文件和数据文件进行拆分
  • innodb buffer_pool size=1G 缓存池大小,设置为当前数据库服务内存的60%-80%,决定了性能的好坏
  • innodblog file_ size= =256M 一般取256M可以兼顾性能和recovery的速度, 写满后只能切换日志靠buffer存储
  • innodb log _buffer size=16M该参数确保有足够大的日志缓冲区来保存脏数据在被写入到日志文件之前可以继续mysq|事务操作
  • innodb flush log _at_trx_commit = 2

1时,在每个事务提交时,日志缓冲被写到日志文件,对日志文件做到磁盘操作的刷新。Truly ACID。速度慢。
2时,在每个事务提交时,日志缓冲被写到系统缓冲(跟着操作系统走的),但不对日志文件做到磁盘操作的刷新。然后根据innodb flush log_ at_timeout (默认为1s) 时间flush disk只有操作系统崩溃或掉电才会删除最后一秒的事务,不然不会丢失事务。
0时,效率更高,但安全性差。每秒才write 日志任何mysqld进程的崩溃会删除崩溃前最后一秒的事务
使用备用电源和稳定的操作系统的方式尽可能规避最后一秒的问题。

  • innodb_ data_file_path=ibdata1:1G;ibdata2:1G;bdata3:1G:autoextend 本质上来做数据归档用的,切分归档的文件,写在指定表数据和索引存储的空间可以是一个或者多个文件,保证同一个文件不会太大,如果小于1G写ibdata1文件,大于1G小于2G写在ibdata2文件,大于2G小于3Gbdata3,超过3G的进行自动扩展,

    4.10 mysql 读写分离

    4.10.1 读写分离

  • 一主多从

  • 读库延迟问题处理
  • 主从切换处理

一主多从,通过binlog进行监听
image.png
读写分离,将写操作路由到master上,读操作路由到slave。对于读延迟不敏感的情况这种方案很不错,可以将压力分散到slave上每个能承担1000qps,三个就能承受3000qps。

客户端只要做了开启事务就路由到master,否则路由到slave,readOnly等于true,也会被select操作路由到从库上。

读库延迟问题处理如何处理?
binlog采用异步复制方案,master提交完就会发送给slave,再分发给其他slave,永远没办法获得最新数据,只能保证最终一致性。

第一种方案:业务层面做让步,比如说一个用户注册到master用户信息,但是用户想马上看到自己的头像,没办法很快展示变更,比如说转一个loading页面,等待五秒或十秒才能看到信息。

如果1ms没有完成同步可能是网络原因,或者是数据量和负载都很高。

如果毫秒级别接受不了,就强制去路由到master库上。

主从切换问题处理
当主库出问题,可以将slave切换为mater,但是会有一致性问题。
mysql提供了一个半同步的解决方案,master提交完本地事务不做内容返回,先同步给slave,直到至少一个slave收到刷新到slave消息返回之后,在执行提交并返回给client,这种就叫做半同步。

dba直接找到slave中binlog最靠前的一个,就可以之间生成master。
但是这种方式也不能完全保证强一致。
image.png
可能会出现slave比master多了一条数据。
要想保证可以使用二阶段提交。但是mysql没有采用二阶段提交,是为了提高性能。主从切换多多少少还是具有一定风险。

4.10.2 分库分表

  • 垂直拆分
  • 水平拆分
  • 多主多从

分库分表本质上来说就是多主多从,每个主库对应多个从库。
往往都会讨论水平拆分忽略垂直拆分,其实垂直拆分是非常重要的。

相同业务性质强放到一个数据库内,仍然可以使用join语句,按照小表驱动大表,join是合法的。
image.png

水平拆分有三种拆法,固定路由位的拆法
image.png
时间戳维度做分库分表
预估增量以一个月做分库分表的操作,往后面加库加标,对应查询只能按照日期进行拆分,如果用户id进行查询,要跨多个数据库

image.png
适用于增量和时间戳的方式,如果100哥数据库没办法满足要求,又扩了100个,先以createTime作区分,如果大于某个时间点,就路由到新的库,否则,就到老的库进行查询,而新老路由查询规则都是以用户id或者sellerid。

第三种:Item查询维度非常复杂,并且没有userId这种的分区键,可以忍不分库分表。
image.png

直接用id区分可以是自增id或者自定义id进行分库分表。

4.11应对缓存问题

穿透,击穿,雪崩

  • 穿透:数据不存在
  • 击穿:同一数据击穿到数据库
  • 雪崩:不同的数据击穿到数据库

4.11.1缓存穿透

查找不存在的数据,设置一个无效数据标志位,代表数据不存咋,如null,new Item(-1);
image.png
使用redis.keyExist(key),redis.get(key),因为不是原子性操作,可能会出现拿到的是null。

4.11.2 缓存击穿

如果缓存还没来得及预热,或者是热点key,并发去查数据库。
解决方案:使用消息队列,去排队,比较麻烦,要解决重试问题,如果被block了,系统其他请求就会被阻塞住。
image.png
有三个java应用,总接收10000qps,三个就是30000qps,java应用内部,上锁只能请求1qps,使用同步锁,ReentrantLock或者synchronized,可以做一个超时时间。

image.png

4.11.3 缓存雪崩

一瞬间大量key失效,所有请求都落到了数据库上。
解决方案就是:设置随机缓存时间,避免所有缓存同时失效。
第二个方案保证常驻缓存,不设置超时时间,但是不推荐。
image.png

4.12 缓存脏数据问题

4.12.1 脏读如何避免

  • 脏读产生的原因
  • 脏读如何避免

    缓存未过期,但是数据库修改了。
    image.png
    解决方案:
    后台应用发一个消息,会做redis缓存更新,第一种方式,是删除缓存key,第二种,主动加载缓存。

缓存的脏读是无法避免的,要保证强一致,只能用二阶段提交。
缓存必须要设置超时时间,可以重塑数据,避免一直是脏数据。

4.12.2多级缓存

  • 前台
  • 中台
  • 后台

静态页面缓存动静结合,h5通过cdn缓存,并且加上版本号,去发送一个check指令,在java内部进行check,当前对应文章的状态是否是可以被发布的,h5直接进行下架,解决了前端页面缓存。

中台缓存
nginx和java,nginx可以使用本地host cache,可以使用jvm可以存商品详情页数据,需要注意,更新是比较难的,需要建立通知机制,通知到所有实例。

后台缓存
redis最有可能和mysql保持一致的。

4.12 如何优化网络瓶颈

网络瓶颈的根源

  • 公网:带宽,出口调用量
  • 内网:带宽,出口调用量

量很大,内网和公网的带宽压力都会承受不住。
image.png

解决方式:
分散:部署多态

image.png
压缩:压缩请求流量,nginx也可以压缩请求,减少请求带宽,内网如redis可以做如压缩传输字符串等操作。