性能优化总结(三)
数据库基本原理
数据库架构
SQL –> 连接器 –> 语法分析器 –> 语义分析与优化器 -> 执行引擎
连接器
数据库连接器为每个连接请求分配一块专用的内存空间用于会话的上下文管理。建立连接对于数据库而言相对比较重,需要花费一定时间,因此程序启动时会初始化建立一些连接放在连接池。
语法分析器
解析SQL,构建抽象语法树,验证语法。
语义分析与优化器
将各种复杂嵌套的SQL进行语义等价转换,得到有限几种关系代数计算结构,并利用索引等信息进一步优化。
执行计划
Explain select * from user where id=1;
Key:索引类型,Null无索引。
Rows:需要处理的行数。
Possible_keys:潜在可利用索引。
采用PrepareStatement优点
l PrepareStatement会预提交带占位符SQL到数据库进行预处理,提前生成执行计划,当给定占位符参数,真正执行SQL时,执行引擎可以直接执行,效率高。
l PrepareStatement可有效防止SQL注入攻击。
B+树
聚簇索引
聚簇索引的数据库记录和索引存储在一起。
MySQL数据库的主键就是聚簇索引,主键ID和所在的记录行存储在一个B+树中。
非聚簇索引
非聚簇索引的叶子节点记录的不是数据行记录,而是聚簇索引(主键)。
通过非聚簇索引找到聚簇索引,在通过主键索引找到行记录的过程叫做回表。
合理使用索引优化SQL性能
l 不要盲目添加索引,尤其生产环境。
添加索引的alert的操作耗时较长(分钟级)
Alert操作期间,所有数据库的增删改操作全部阻塞,对应用而言,因为连接不能被释放,事实上查询也被阻塞。
l 删除不用索引,避免不必要开销。
l 使用更小数据类型创建索引。
int占4字节,bigint占8字节,timestamp占4字节,Datetime占8字节。
数据库事务
l 原子性
l 隔离性
l 持久性
l 一致性
JVM虚拟机架构原理
JVM组成架构
JVM屏蔽底层系统的不同,为java字节码文件构成了统一的运行环境。
Java字节码文件
Java是如何实现在不同操作系统、不同硬件平台,不需修改代码就能顺畅执行?
计算机领域任何问题都可以通过增加中间层(虚拟层)来解决。
Java所有指令有200个左右,一个字节(8位)可以存储256种不同的指令信息,这样的字节称为字节码。在代码执行过程中,JVM将字节码解释执行,屏蔽对底层操作系统的依赖,JVM也可以将字节码编译执行,如果是热点代码,会通过JIT动态地编译为机器码,提高执行效率。
字节码执行流程
Java字节码文件编译过程
类加载器双亲委托模型
低层次的类加载器不能覆盖更高层次类加载器已经加载的类,如果低层次类加载器想要加载一个未知类,需上级类加载器确认,只有当上级类加载没有加载过这个类,并且允许加载时,才让当前类加载器加载该类。
自定义类加载器
l 隔离加载类:同一个JVM中不同组件加载同一个类的不同版本。
l 扩展加载源:从网络、数据库加载字节码。
l 字节码加密:加载自定义的加密字节码,在classLoader中解密。
堆&栈
堆:一个JVM实例对应唯一一个堆,应用程序在运行过程中所创建的所有类的实例或数组都放在这个堆中,并由应用所有线程共享。
堆栈:JVM为每个新创建的线程都分配一个堆栈。也就是说,对于java程序来说,它的运行是通过堆栈操作来完成的。
Java中所有对象的存储空间都是在堆中分配的,但是这个对象的引用都是在堆栈分配的,也就是在建立一个对象时从两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的引用而已。
方法区&程序计数器
方法区存放从磁盘加载进来的类字节码,而程序运行过程中创建的类实例存放在堆里。程序运行时,是以线程为单位运行的,当JVM进去启动类的main方法时,就会为应用创建一个主线程,main方法里代码就会被这主线程执行,每个线程都有自己的java栈,栈里存放方法运行期的局部变量。而当前线程执行到哪一行字节码指令,这个信息则被存放在程序技术寄存器。
Java(线程)栈
所有方法内定义的基本类型变量,都会被每个运行这个方法的线程放入自己的栈中,线程的栈彼此隔离,所以这些变量一定是线程安全的。
线程工作内存 & volatile
Java内存模型规定在多线程情况下,线程操作主内存变量,需要通过线程独有的工作内存拷贝主内存变量副本来进行。
一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰后,保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。
Java运行环境
JVM垃圾回收性能分析
Jvm垃圾回收
Jvm通过可达性分析算法进行垃圾对象的识别,具体过程是:从线程栈帧中的局部变量或者方法区的静态变量出发,将这些变量引用的对象进行标记,然后看这些被标记的对象是否引用了其他对象,继续进行标记,所有被标记的对象都是被使用中的对象,而没有被标记的对象就是可以回收的垃圾对象。
JVM对垃圾对象占用的空间进行回收,回收的三种方法:
l 清理:将垃圾对象占用内存清理掉,其实JVM不会真的对这些垃圾内存进行清理,而是将这些垃圾对象占用的内存空间标记为空闲,记录在一个空闲列表里,当应用程序需要创建新对象的时候,就从空闲列表中找一段空闲内存分配个新对象。
l 压缩:从堆对象头部开始,将存活对象拷贝放在一段连续的内存空间,那么其余空间就是连续的空闲空间。
l 复制:将堆空间分成两部分,只在其中一部分创建对象,当这部分空间用完的时候,将标记过的可用对象复制到另一个空间中。
JVM分代垃圾回收
JVM垃圾回收器算法
l 串行回收器
l 并行回收器
l 并发回收器CMS
1. 初始化标记(stop the world)
2. 并发标记(标记与处理任务并行)
3. 重标记(stop the world)
4. 并发清理
l G1回收器
Jave启动参数
JVM性能诊断工具
JPS:查看host上运行的所有java进程pid(jvmid),一般情况下使用这个工具目的是为找出运行的JVM进程ID,然后进一步使用其他工具来监控和分析JVM。
常用几个参数:
l -l:输出java应用程序的main class完整包。
l -q:显示pid,不显示其他任何相关信息。
l -m:输出传递给main方法的参数。
l -v:输出传递给JVM的参数。在诊断JVM相关问题时,这个参数可以查看JVM相关参数设置。
JSTAT:JDK自带工具,主要对java应用程序的资源和性能进行实时的命令行监控,包括了对Heap size和垃圾回收状况的监控。
语法:jstat [options] vmid [interval] [count]
l Options – 选项,我们一般使用 –gcutil 查看gc情况。
l Vmid – Vm进程号,即当前运行的java进程号。
l Interval – 间隔时间,单位:毫秒
l Count – 打印次数,缺省:无限次
JMAP:一个可以输出内存中对象的工具,甚至可以将VM中的heap,以二进制输出成文本。
使用方法:
l Jmap –histo pid>a.log 可以保存到文本,一段时间后,可以使用文本对比工具,可以对比出GC回收了哪些对象。
l Jmap –dump:format=b,file=f1 PID可以将该PID进程的内存heap输出到f1文件里。
Jstack:查看线程的堆栈信息。语法示例:jstack –l 92688
Java代码优化技巧和原理
合理并谨慎使用多线程
启动线程数=【任务执行时间/(任务执行时间-IO等待时间)】*CPU内核数
CPU密集型:线程数量 = CPU 核数(逻辑)+ 1 ,+1是因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
IO密集型:最佳线程数 = (1/CPU利用率) = 1 + (I/O耗时/CPU耗时)
竞态条件与临界区
同一个程序中启动多个线程本身不会导致问题,问题在于多线程访问相同资源。
当两个线程竞争同一资源时,如果对资源访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称为临界区。
在临界区使用适当的同步就可以避免竞态条件。
Java线程安全
允许被多个线程安全执行的代码称为线程安全代码。
方法局部变量
l 局部变量存储在线程自己的栈中。也就是说,局部变量永远不会被多个线程共享。所以基础类型的局部变量是线程安全的。
方法局部的对象引用
l 某个方法中创建的对象不会逃逸出该方法,它是线程安全的。
对象成员变量
l 对象成员存储在堆上,如果2个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的。
ThreadLocal
Java内存泄露
Java内存泄露是由开发人员的错误引起的。
如果程序保留对永远不再使用对象的引用,这些对象将会占用并耗尽内存。
l 长生命周期对象。
l 静态容器。
l 缓存。
合理使用线程池和对象池
使用合适JDK容器类
使用I/O Buffer 及 NIO
l 延迟写或提前读策略
l 异步无阻塞IO通信
使用组合代替继承
l 减少对象耦合。
l 避免太深的继承层次带来的对象创建性能损失。
合理使用单例模式
l 无状态对象。
l 线程安全。
系统性能优化案例:秒杀系统
XXX性能现状
XXX网站正常情况流量:
l 并发(单台),高峰期<10;
l 吞吐量(TPS,单台)高峰期<60;
l CPU负载Load高峰期<2,大部分高峰期<1;
l CPU使用率,一般只占1核,平均60%左右;
l 服务器平均响应时间,高峰期<150ms;
l 图片总流量带宽1.8G(各网站总和)。
高并发下风险:
l 网络带宽耗尽。
l 服务器Load飙高,停止响应。
l 数据库瘫痪。
高并发下的事故:
l 事故:网站运营推广页面弹出1M大图片导致带宽占尽。
n 解决方案:增加审核机制:运营推广增加的图片流量不能超过现有流量的30%。
l 合作媒体推广:迅雷、暴风影音弹出广告,导致ZZ集群crash。
秒杀:
l XXX开业24小时不间断秒杀。
技术挑战:
l 瞬间高并发
1. 8000高并发:预估秒杀在线人数8000人。
2. 风险:带宽耗尽。
3. 服务器:崩溃,可以理解自己给自己DDOS攻击。
l 秒杀器
1. 第一种,秒杀前不断刷新秒杀页面,直到最后一秒,抢着下单。
2. 第二种,跳过秒杀页,直接进入下单页,下单。
秒杀系统:服务器和网络准备
l 服务器准备
1. Style服务器
2. 图片服务器
3. 静态服务器
4. 交易服务器
l 带宽准备
1. 图片出口带宽上限
2. CDN准备
秒杀系统:设计原则
静态化
l 采用JS自动更新技术将动态页面转化为静态页面。
并发控制,防秒杀器
l 设置阈值,只放前面一小部分人进入秒杀系统。
简化流程
l 砍掉不重要流程分支,如下单页面所有数据库查询。
l 以下单成功作为秒杀成功标志。支付流程只在1天内完成即可。
前端优化
l 采用YSLOW原则提升页面响应速度。
秒杀系统:静态化
l 秒杀列表页和详情页是静态页。
l 秒杀页如何判断秒杀活动开始。
三道阀门的设计
阀门:基于TT的设计
l 限制进入秒杀页面:1000
l 限制进入下单页面:100
l 限制进入支付系统:56
秒杀预防
秒杀详情页:
l URL随机。
l 秒杀前2秒放出,脚本生成。
l 1000次访问上限。
下单页:
l 订单ID,随机。
l 不能直接跳过秒杀详情页进入。
l 每个秒杀商品,带预先生成的随机Token作为URL参数。
l 如果秒杀过,直接跳过秒杀结束页。
l 100次秒杀上限控制。
服务器调优
秒杀静态页优化
下单页优化
l 无数据库操作
l 秒杀流程精简
l 采用内存缓存
应急预案
壁虎断尾策略
在流量耗尽情况下,关闭非核心应用,保住核心系统。
万能出错页:秒杀活动已结束
版权声明: 本文为 InfoQ 作者【Mars】的原创文章。
原文链接:【http://xie.infoq.cn/article/337ae48b0b61b957add98ed29】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论