写点什么

肝了 15000 字性能调优系列专题(JVM、MySQL、Nginx and Tomcat),看不完先收藏

用户头像
北游学Java
关注
发布于: 2021 年 04 月 22 日

前言

性能调优,无疑是个庞大的话题,也是很多项目中非常重要的一环,性能调优难做是众所周知的,毕竟性能调优涵盖的面实在是太多了,在这里我就大概的讲一下企业中最常用的四种调优——JVM 调优、MySQL 调优、Nginx 调优以及 Tomcat 调优,一家之言,有什么说的不对的还请多包涵补充。


篇幅所限,有些东西是肯定写不到的,所以本文只是挑了一些重要部分来剖析,如果需要完整详细的掌握性能调优,可以来领取系统整理的性能调优笔记和相关学习资料


话不多说,坐稳扶好,发车喽!

一、Jvm 性能调优

1、JVM 类加载机制详解

如下图所示,JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。



1.1 加载

在加载阶段,虚拟机需要完成以下三件事情:


1)通过一个类的全限定名来获取定义此类的二进制字节流。注意这里的二进制字节流不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以从网络中获取,也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。


2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。


3)在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口。


相对于类加载过程的其他阶段,加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员们可以通过定义自己的类加载器去控制字节流的获取方式。

1.2 验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

1.3 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:


public static int v = 8080;
复制代码


实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080,将 v 赋值为 8080 的putstatic指令是程序被编译后,存放于类构造器<client>方法之中,这里我们后面会解释。


但是注意如果声明为:


public static final int v = 8080;
复制代码


在编译阶段会为 v 生成ConstantValue属性,在准备阶段虚拟机会根据 ConstantValue 属性将 v 赋值为 8080。

1.4 解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件中的:


  • CONSTANT_Class_info

  • CONSTANT_Field_info

  • CONSTANT_Method_info


等类型的常量。


下面我们解释一下符号引用和直接引用的概念:


  • 符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。

  • 直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

1.5 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。


初始化阶段是执行类构造器<clint>方法的过程。<clint>方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证<clint>方法执行之前,父类的<clint>方法已经执行完毕。p.s: 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<clint>()方法。


注意以下几种情况不会执行类初始化:


  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。

  • 定义对象数组,不会触发该类的初始化。

  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。

  • 通过类名获取 Class 对象,不会触发类的初始化。

  • 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。

  • 通过 ClassLoader 默认的 loadClass 方法,也不会触发初始化动作。

1.6 类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块被称为“类加载器”。


对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类是来源于同一个 Class 文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里所指的“相等”,包括代表类的 Class 对象的 equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括了使用 instanceof 关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生具有迷惑性的结果。


JVM 提供了 3 种类加载器:


  • 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如 rt.jar)的类。

  • 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过 java.ext.dirs 系统变量指定路径中的类库。

  • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。


JVM 通过双亲委派模型进行类的加载,当然我们也可以通过继承 java.lang.ClassLoader 实现自定义的类加载器。



双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。


双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。


采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。


在有些情境中可能会出现要我们自己来实现一个类加载器的需求,由于这里涉及的内容比较广泛,我想以后单独写一篇文章来讲述,不过这里我们还是稍微来看一下。我们直接看一下 jdk 中的ClassLoader的源码实现:


protected synchronized Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException {    // First, check if the class has already been loaded    Class c = findLoadedClass(name);    if (c == null) {        try {            if (parent != null) {                c = parent.loadClass(name, false);            } else {                c = findBootstrapClass0(name);            }        } catch (ClassNotFoundException e) {            // If still not found, then invoke findClass in order            // to find the class.            c = findClass(name);        }    }    if (resolve) {        resolveClass(c);    }    return c;}
复制代码


  • 首先通过Class c = findLoadedClass(name);判断一个类是否已经被加载过。

  • 如果没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型,首先会通过递归从父加载器开始找,直到父类加载器是Bootstrap ClassLoader为止。

  • 最后根据resolve的值,判断这个 class 是否需要解析。


而上面的findClass()的实现如下,直接抛出一个异常,并且方法是protected,很明显这是留给我们开发者自己去实现的,这里我们以后我们单独写一篇文章来讲一下如何重写findClass方法来实现我们自己的类加载器。


protected Class<?> findClass(String name) throws ClassNotFoundException {    throw new ClassNotFoundException(name);}
复制代码

2、JVM 内存模型


2.1 各部分的功能

这几个存储区最主要的就是栈区和堆区,那么什么是栈什么是堆呢?说的简单点,栈里面存放的是基本的数据类型和引用,而堆里面则是存放各种对象实例的。



堆与栈分开设计是为什么呢?


  • 栈存储了处理逻辑、堆存储了具体的数据,这样隔离设计更为清晰

  • 堆与栈分离,使得堆可以被多个栈共享。

  • 栈保存了上下文的信息,因此只能向上增长;而堆是动态分配


栈的大小可以通过-XSs 设置,如果不足的话,会引起 java.lang.StackOverflowError 的异常

栈区

线程私有,生命周期与线程相同。每个方法执行的时候都会创建一个栈帧(stack frame)用于存放 局部变量表、操作栈、动态链接、方法出口。

存放对象实例,所有的对象的内存都在这里分配。垃圾回收主要就是作用于这里的。


  • 堆得内存由-Xms 指定,默认是物理内存的 1/64;最大的内存由-Xmx 指定,默认是物理内存的 1/4。

  • 默认空余的堆内存小于 40%时,就会增大,直到-Xmx 设置的内存。具体的比例可以由-XX:MinHeapFreeRatio 指定

  • 空余的内存大于 70%时,就会减少内存,直到-Xms 设置的大小。具体由-XX:MaxHeapFreeRatio 指定。


因此一般都建议把这两个参数设置成一样大,可以避免 JVM 在不断调整大小。

2.2 程序计数器

这里记录了线程执行的字节码的行号,在分支、循环、跳转、异常、线程恢复等都依赖这个计数器。

2.3 方法区

类型信息、字段信息、方法信息、其他信息

2.4 总结


3、垃圾收集机制详解

3.1 如何定义垃圾

有两种方式,一种是引用计数(但是无法解决循环引用的问题);另一种就是可达性分析。


判断对象可以回收的情况:


  • 显示的把某个引用置位 NULL 或者指向别的对象

  • 局部引用指向的对象

  • 弱引用关联的对象

3.2 垃圾回收的方法

3.2.1Mark-Sweep 标记-清除算法


这种方法优点就是减少停顿时间,但是缺点是会造成内存碎片。

3.2.2 Copying 复制算法


这种方法不涉及到对象的删除,只是把可用的对象从一个地方拷贝到另一个地方,因此适合大量对象回收的场景,比如新生代的回收。

3.2.3 Mark-Compact 标记-整理算法


这种方法可以解决内存碎片问题,但是会增加停顿时间。

3.2.4 Generational Collection 分代收集

最后的这种方法是前面几种的合体,即目前 JVM 主要采取的一种方法,思想就是把 JVM 分成不同的区域。每种区域使用不同的垃圾回收方法。



上面可以看到堆分成两个个区域:


  • 新生代(Young Generation):用于存放新创建的对象,采用复制回收方法,如果在 s0 和 s1 之间复制一定次数后,转移到年老代中。这里的垃圾回收叫做 minor GC;

  • 年老代(Old Generation):这些对象垃圾回收的频率较低,采用的标记整理方法,这里的垃圾回收叫做 major GC。


这里可以详细的说一下新生代复制回收的算法流程:


在新生代中,分为三个区:Eden, from survivor, to survior。


  • 当触发 minor GC 时,会先把 Eden 中存活的对象复制到 to Survivor 中;

  • 然后再看 from survivor,如果次数达到年老代的标准,就复制到年老代中;如果没有达到则复制到 to survivor 中,如果 to survivor 满了,则复制到年老代中。

  • 然后调换 from survivor 和 to survivor 的名字,保证每次 to survivor 都是空的等待对象复制到那里的。

3.3 垃圾回收器


3.3.1 串行收集器 Serial

这种收集器就是以单线程的方式收集,垃圾回收的时候其他线程也不能工作。



3.3.2 并行收集器 Parallel

以多线程的方式进行收集



3.3.3 并发标记清除收集器 Concurrent Mark Sweep Collector, CMS

大致的流程为:初始标记--并发标记--重新标记--并发清除



3.3.4 G1 收集器 Garbage First Collector

大致的流程为:初始标记--并发标记--最终标记--筛选回收



篇幅所限,关于类字节码文件、调优工具以及 GC 日志分析这里就不写了,如果有感兴趣的朋友可以点击领取我整理的完整JVM性能调优笔记,里面会有详细叙述。

二、Mysql 性能调优

1、SQL 执行原理详解


1.1 SQL Server 组成部分


1.1.1 关系引擎:主要作用是优化和执行查询。

包含三大组件:


(1)命令解析器:检查语法和转换查询树。


(2)查询执行器:优化查询。


(3)查询优化器:负责执行查询。

1.1.2 存储引擎:管理所有数据及涉及的 IO

包含三大组件:


(1)事务管理器:通过锁来管理数据及维持事务的 ACID 属性。


(2)数据访问方法:处理对行、索引、页、行版本、空间分配等的 I/O 请求。


(3)缓冲区管理器:管理 SQL Server 的主要内存消耗组件 Buffer Pool。

1.1.3Buffer Pool

包含 SQL Server 的所有缓存。如计划缓存和数据缓存。

1.1.4 事务日志

记录事务的所有更改。保证事务 ACID 属性的重要组件。

1.1.5 数据文件

数据库的物理存储文件。


6.SQL Server 网络接口建立在客户端和服务器之间的网络连接的协议层

1.2 查询的底层原理


1.2.1 当客户端执行一条 T-SQL 语句给 SQL Server 服务器时,会首先到达服务器的网络接口,网络接口和客户端之间有协议层。


1.2.2 客户端和网络接口之间建立连接。使用称为“表格格式数据流”(TDS) 数据包的 Microsoft 通信格式来格式化通信数据。


1.2.3 客户端发送 TDS 包给协议层。协议层接收到 TDS 包后,解压并分析包里面包含了什么请求。


1.2.4 命令解析器解析 T-SQL 语句。命令解析器会做下面几件事情:


(1)检查语法。发现有语法错误就返回给客户端。下面的步骤不执行。


(2)检查缓冲池(Buffer Pool)中是否存在一个对应该 T-SQL 语句的执行计划缓存。


(3)如果找到已缓存的执行计划,就从执行计划缓存中直接读取,并传输给查询执行器执行。


(4)如果未找到执行计划缓存,则在查询执行器中进行优化并产生执行计划,存放到 Buffer Pool 中。


1.2.5 查询优化器优化 SQL 语句


当 Buffer Pool 中没有该 SQL 语句的执行计划时,就需要将 SQL 传到查询优化器,通过一定的算法,分析 SQL 语句,产生一个或多个候选执行计划。选出开销最小的计划作为最终执行计划。然后将执行计划传给查询执行器。


1.2.6 查询执行器执行查询


查询执行器把执行计划通过 OLE DB 接口传给存储引擎的数据访问方法。


1.2.7 数据访问方法生成执行代码


数据访问方法将执行计划生成 SQL Server 可操作数据的代码,不会实际执行这些代码,传送给缓冲区管理器来执行。


1.2.8 缓冲区管理器读取数据。


先在缓冲池的数据缓存中检查是否存在这些数据,如果存在,就把结果返回给存储引擎的数据访问方法;如果不存在,则从磁盘(数据文件)中读出数据并放入数据缓存中,然后将读出的数据返回给存储引擎的数据访问方法。


1.2.9 对于读取数据,将会申请共享锁,事务管理器分配共享锁给读操作。


1.2.10 存储引擎的数据访问方法将查询到的结果返回关系引擎的查询执行器。


1.2.11 查询执行器将结果返回给协议层。


1.2.12 协议层将数据封装成 TDS 包,然后协议层将 TDS 包传给客户端。

2、索引底层剖析

2.1 为何要有索引?

一般的应用系统,读写比例在 10:1 左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到最多的,也是最容易出问题的,还是一些复杂的查询操作,因此对查询语句的优化显然是重中之重。说起加速查询,就不得不提到索引了。

2.2 什么是索引?

索引在 MySQL 中也叫做“键”或者"key"(primary key,unique key,还有一个 index key),是存储引擎用于快速找到记录的一种数据结构。索引对于良好的性能非常关键,尤其是当表中的数据量越来越大时,索引对于性能的影响愈发重要,减少 io 次数,加速查询。(其中 primary key 和 unique key,除了有加速查询的效果之外,还有约束的效果,primary key 不为空且唯一,unique key 唯一,而 index key 只有加速查询的效果,没有约束效果)


索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高好几个数量级。索引相当于字典的音序表,如果要查某个字,如果不使用音序表,则需要从几百页中逐页去查。


强调:一旦为表创建了索引,以后的查询最好先查索引,再根据索引定位的结果去找数据

2.3 索引原理

索引的目的在于提高查询效率,与我们查阅图书所用的目录是一个道理:先定位到章,然后定位到该章下的一个小节,然后找到页数。相似的例子还有:查字典,查火车车次,飞机航班等,下面内容看不懂的同学也没关系,能明白这个目录的道理就行了。 那么你想,书的目录占不占页数,这个页是不是也要存到硬盘里面,也占用硬盘空间。


你再想,你在没有数据的情况下先建索引或者说目录快,还是已经存在好多的数据了,然后再去建索引,哪个快,肯定是没有数据的时候快,因为如果已经有了很多数据了,你再去根据这些数据建索引,是不是要将数据全部遍历一遍,然后根据数据建立索引。你再想,索引建立好之后再添加数据快,还是没有索引的时候添加数据快,索引是用来干什么的,是用来加速查询的,那对你写入数据会有什么影响,肯定是慢一些了,因为你但凡加入一些新的数据,都需要把索引或者说书的目录重新做一个,所以索引虽然会加快查询,但是会降低写入的效率。

2.4 索引的数据结构

前面讲了索引的基本原理,数据库的复杂性,又讲了操作系统的相关知识,目的就是让大家了解,现在我们来看看索引怎么做到减少 IO,加速查询的。任何一种数据结构都不是凭空产生的,一定会有它的背景和使用场景,我们现在总结一下,我们需要这种数据结构能够做些什么,其实很简单,那就是:每次查找数据时把磁盘 IO 次数控制在一个很小的数量级,最好是常数数量级。那么我们就想到如果一个高度可控的多路搜索树是否能满足需求呢?就这样,b+树应运而生。



3、Mysql 锁机制与事务隔离级别详解


3.1 为什么需要学习数据库锁知识

即使我们不会这些锁知识,我们的程序在一般情况下还是可以跑得好好的。因为这些锁数据库隐式帮我们加了


  • 对于UPDATE、DELETE、INSERT语句,InnoDB 自动给涉及数据集加排他锁(X)

  • MyISAM 在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁


只会在某些特定的场景下才需要手动加锁,学习数据库锁知识就是为了:


  • 能让我们在特定的场景下派得上用场

  • 更好把控自己写的程序

  • 在跟别人聊数据库技术的时候可以搭上几句话

  • 构建自己的知识库体系!在面试的时候不虚

3.2 表锁简单介绍

首先,从锁的粒度,我们可以分成两大类:


  • 表锁

  • 开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低

  • 行锁

  • 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高


不同的存储引擎支持的锁粒度是不一样的:


  • InnoDB 行锁和表锁都支持

  • MyISAM 只支持表锁


InnoDB 只有通过索引条件检索数据才使用行级锁,否则,InnoDB 将使用表锁


  • 也就是说,InnoDB 的行锁是基于索引的


表锁下又分为两种模式


  • 表读锁(Table Read Lock)

  • 表写锁(Table Write Lock)

  • 从下图可以清晰看到,在表读锁和表写锁的环境下:读读不阻塞,读写阻塞,写写阻塞

  • 读读不阻塞:当前用户在读数据,其他的用户也在读数据,不会加锁

  • 读写阻塞:当前用户在读数据,其他的用户不能修改当前用户读的数据,会加锁!

  • 写写阻塞:当前用户在修改数据,其他的用户不能修改当前用户正在修改的数据,会加锁!

  • 写锁和其他锁均布兼容,只有读和读之间兼容



从上面已经看到了:读锁和写锁是互斥的,读写操作是串行


  • 如果某个进程想要获取读锁,同时另外一个进程想要获取写锁。在 mysql 里边,写锁是优先于读锁的

  • 写锁和读锁优先级的问题是可以通过参数调节的:max_write_lock_countlow-priority-updates


值得注意的是:


  • MyISAM 可以支持查询和插入操作的并发进行。可以通过系统变量concurrent_insert来指定哪种模式,在 MyISAM 中它默认是:如果 MyISAM 表中没有空洞(即表的中间没有被删除的行),MyISAM 允许在一个进程读表的同时,另一个进程从表尾插入记录。

  • 但是 InnoDB 存储引擎是不支持的

3.3 MVCC 和事务的隔离级别

数据库事务有不同的隔离级别,不同的隔离级别对锁的使用是不同的,锁的应用最终导致不同事务的隔离级别


MVCC(Multi-Version Concurrency Control)多版本并发控制,可以简单地认为:MVCC 就是行级锁的一个变种(升级版)。


  • 事务的隔离级别就是通过锁的机制来实现,只不过隐藏了加锁细节在表锁中我们读写是阻塞的,基于提升并发性能的考虑,MVCC 一般读写是不阻塞的(所以说 MVCC 很多情况下避免了加锁的操作)

  • MVCC 实现的读写不阻塞正如其名:多版本并发控制--->通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本。快照有两个级别:

  • 语句级针对于 Read committed 隔离级别

  • 事务级别针对于 Repeatable read 隔离级别我们在初学的时候已经知道,事务的隔离级别有 4 种:

  • Read uncommitted 会出现脏读,不可重复读,幻读

  • Read committed 会出现不可重复读,幻读

  • Repeatable read 会出现幻读(但在 Mysql 实现的 Repeatable read 配合 gap 锁不会出现幻读!)

  • Serializable 串行,避免以上的情况!




Read uncommitted 会出现的现象--->脏读:一个事务读取到另外一个事务未提交的数据


  • 例子:A 向 B 转账,A 执行了转账语句,但 A 还没有提交事务,B 读取数据,发现自己账户钱变多了!B 跟 A 说,我已经收到钱了。A 回滚事务【rollback】,等 B 再查看账户的钱时,发现钱并没有多。

  • 出现脏读的本质就是因为操作(修改)完该数据就立马释放掉锁,导致读的数据就变成了无用的或者是错误的数据。Read committed 避免脏读的做法其实很简单:

  • 就是把释放锁的位置调整到事务提交之后,此时在事务提交前,其他进程是无法对该行数据进行读取的,包括任何操作但 Read committed 出现的现象--->不可重复读:一个事务读取到另外一个事务已经提交的数据,也就是说一个事务可以看到其他事务所做的修改

  • 注:A 查询数据库得到数据,B 去修改数据库的数据,导致 A 多次查询数据库的结果都不一样【危害:A 每次查询的结果都是受 B 的影响的,那么 A 查询出来的信息就没有意思了】上面也说了,Read committed 是语句级别的快照!每次读取的都是当前最新的版本!


Repeatable read 避免不可重复读是事务级别的快照!每次读取的都是当前事务的版本,即使被修改了,也只会读取当前事务版本的数据。


呃...如果还是不太清楚,我们来看看 InnoDB 的 MVCC 是怎么样的吧(摘抄《高性能 MySQL》)


InnoDB 的 MVCC,是通过在每行记录后面保存两个隐藏的列来实现的,这两个列一个保存了行的创建时间,一个保存了行的过期(删除)时间。当然存储的并不是真正的时间值,而是系统版本号。每开始一个新的事务,系统版本号会自动递增,事务开始的时候的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。select  InnoDB 会根据以下两个条件检查每行记录:    a. InnoDB 只查找版本早于当前事务版本的数据行,这样可以确保事务读取到的数据,要么是在事务开始前就存在的,要么是事务自身插入或更新的    b. 行的删除版本要么未定义要么大于当前事务版本号,确保了事务读取到的行,在事务开始前未被删除


至于虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。


  • 注:和不可重复读类似,但虚读(幻读)会读到其他事务的插入的数据,导致前后读取不一致

  • MySQL 的 Repeatable read 隔离级别加上 GAP 间隙锁已经处理了幻读了。

三、Nginx 调优

1、Nginx 定义

nginx 常用做静态内容服务和反向代理服务器,以及页面前端高并发服务器。适合做负载均衡,直面外来请求转发给后面的应用服务(tomcat 什么的)

2、熟练掌握 Nginx 核心配置

2.1 全局配置块

user  root;  #运行worker进程的账户,user   用户   [组],默认以nobody账户运行worker_processes  7;  #要使用的worker进程数,可设置为数值、auto(根据机器性能自动设置),默认值1
error_log logs/error.log; #nginx进程(master+worker)的日志设置,保存位置、输出级别,此即为默认保存位置#error_log logs/error.log notice; #输出级别可选,由低到高依次为:debug(输出信息最多),info,notice,warn,error,erit(输出信息最少)
pid logs/nginx.pid; #nginx主进程的pid的保存位置,此即为默认值
worker_rlimit_nofile 65535; #单个worker进程可打开的最大文件描述符数
复制代码


**worker_processes: **


实际运营时一般设置为很接近 CPU 的线程数,比如说 CPU 是 8 线程,一般设置为 6、7。


我们自己开发、用时一般设置为 1、2 即可,不然太吃资源。


worker_rlimit_nofile:


r 是 read,limit 是限制,单个 worker 进程最多只能打开指定个数的文件,超过便不能再读取文件。打开一次文件便会产生一个文件描述符。


此设置是为了防止单个 worker 进程消耗大量的系统资源。


ps  -ef | grep nginx 查询下 nginx 的进程:



不管设置多少个 worker 进程,主进程只有一个(即运行 sbin/nginx)。


主进程由 Linux 当前登录的账户运行,工作进程由 user 指令指定的账户运行。第一列数字是进程的 PID。


nginx 工作进程和 nginx 主进程都是 Linux 中的进程,但主进程(父进程)可以控制 worker 进程(子进程)的开启、结束。


master 进程可以看做老板,worker 进程可以看做打工仔。

2.2 events 块

events {   accept_mutex on;  #防止惊群   multi_accept on;  #允许单个worker进程可同时接收多个网络连接的请求,默认为off  use epoll;  #设置worker进程使用高效模式   worker_connections 1024;  #指定单个worker进程最多可建立的网络连接数,默认值1024。}
复制代码


accept_mutex:


惊群现象:一个网络连接到来,所有沉睡的 worker 进程都会被唤醒,但只用一个 worker 处理连接,其余被唤醒的 worker 又开始沉睡。


设置为 on:要使用几个 worker 就唤醒几个,不全部唤醒,默认值就是 on。


设置为 off:一律全部唤醒。一片 worker 醒来是要占用资源的,会影响性能。


use:


指定 nginx 的工作模式,可选的值:select、poll、kqueue、epoll、rtsig、/dev/poll。


其中 select、poll 都是标准模式,kqueue、epoll 都是高效模式,


kqueue 是在 BSD 系统中用的,epoll 是在 Linux 系统中用的。(BSD 是 Unix 的一个分支,Linux 是一种类 Unix 系统)。


全局块中的 worker_processes、events 块中的 worker_connections 是 nginx 支持高并发的关键,这 2 个数值相乘即 nginx 可建立的最大连接数。


一个连接要用一个文件来保存,


worker_connections 设置的单个 worker 进程的最大连接数,受全局块中 worker_rlimit_nofile 设置的单个 worker 进程可打开的最大文件数限制。


而 worker_rlimit_nofile 只是 nginx 对单个 worker 进程的限制,要受 Linux 系统对单个进程可打开的最大文件描述符数限制。


Linux 默认单个进程最多只能打开 1024 个文件描述符,需要我们修改下 Linux 的资源限制,设置单个进程可打开的最大文件描述符数:


ulimit -n 65536
复制代码


ulimit 命令可以限制单个进程使用的系统资源的尺寸、数量,包括内存、缓冲区、套接字、栈、队列、CPU 占用时间等。


可用 ulimit --help 查看参数。

2.3 http 块

http{    #http全局块    #server块}
复制代码


可以有多个 server 块。

(1)http 全局块

http 全局块的配置作用于整个 http 块(http 块内的所有 server 块)。


include       mime.types;  #将conf/mime.types包含进来    default_type  application/octet-stream;  #设置默认的MIME类型,二进制流。如果使用的MIME类型在mime.types中没有,就当作默认类型处理。    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '      #                '$status $body_bytes_sent "$http_referer" '    #                '"$http_user_agent" "$http_x_forwarded_for"';   access_log logs/access.log; #设置日志,这个日志保存的是客户端请求的信息,包括客户端地址、使用的浏览器、浏览器内核版本、请求的url、请求时间、请求方式、响应状态等。   #access_log  logs/access.log  main;  #可指定日志格式,上面定义的main格式即默认格式。保存位置默认是logs/access.log
sendfile on; #开启文件高效传输模式,默认为off,不开启。 #tcp_nopush on; #如果响应体积过大,默认会分多个批次传输给客户端,设置为on会一次性传给客户端,可防止网络阻塞  #tcp_nodelay on; #如果响应体积过小,默认会放在缓冲区,缓冲区满了才刷给客户端,设置为on直接刷给客户端,可防止网络阻塞 keepalive_timeout 65; #与客户端保持连接的超时时间,在指定时间内,如果客户端没有向Nginx发送任何数据(无活动),Nginx会关闭该连接。 gzip on; #使用gzip模块压缩响应数据。启用后响应体积变小,传输到客户端所需时间更少,节省带宽,但nginx压缩、客户端解压都有额外的时间、资源开销,nginx的负担也会加大。 upstream servers{ #设置负载均衡器,可同时设置多个负载均衡器。负载均衡器的名称中不能含有_,此处指定名称为servers server 192.168.1.7:8080; #tomcat服务器节点 server 192.168.1.8:8081; server 192.168.1.7:8080 down; #down表示该节点下线,暂不使用     server 192.168.1.8:8081 backup; #backup表示该节点是备胎,只有在其他节点忙不过来时才会启用(比如一些节点出故障了、其他节点负载变大)。     server 192.168.1.8:8081 max_fails=3 fail_timeout=60s; #如果对该节点的请求失败3次,就60s内暂时不使用该节点,60s后恢复使用 }
复制代码


日志格式常用的值:


  • $remote_addr  客户端的 ip 地址

  • $time_local : 访问时间与时区

  • $request : 请求的 url 与 http 协议

  • $status : 请求状态,成功是 200

  • $http_referer :从那个页面链接访问过来的

  • $http_user_agent :客户端浏览器的信息

(2)server 模块

server{     #server全局块     listen       80;  #要监听的端口        server_name  localhost;  #虚拟主机(即域名),要在dns上注册过才有效,没有注册的话只能用localhost。可指定多个虚拟主机,空格分开即可        charset utf-8;  #使用的字符集。        #access_log  logs/host.access.log  main;  #在http全局块、server全局块中任意一处设置日志即可。http全局块已经设置了日志,此处可不用设置。     #错误页设置     error_page  404  /404.html;  #html目录下默认只有index.html(nginx首页)、50x.html,需要自己写404.html        location = /404.html {            root   html;  #指定404.html所在目录,此处使用相对路径,nginx主目录下的html目录,也可以使用绝对路径        }        error_page   500 502 503 504  /50x.html;        location = /50x.html {            root   html;        }     #处理静态资源     location ~* \.(html|css|js|gif|jpg|png|mp4)$ {  #使用正则表达式匹配url,如果请求的是这些文件,就使用下面的处理方式            root static;    #如果使用nginx处理静态资源,需使用root指定静态资源所在目录。在nginx主目录下新建目录static,把静态资源放进去即可。       expires 30d;  #设置缓存过期时间             #proxy_pass http://192.168.1.10:80;  #如果使用apache等其他机器处理静态资源,使用proxy_pass转发过去即可,多台机器集群时使用负载均衡器即可。        }         #设置默认处理方式     location / {  #如果url没有指定匹配,就使用默认的处理方式来处理            root   html;  #指定处理请求的根目录。nginx本身作为web服务器直接处理客户端请求时,比如请求login.jsp,会调用root指定目录下的login来处理请求。            index  index.html index.htm;  #指定nginx服务器的首页地址。root、index2项配置都是必需的。            proxy_pass http://servers; #指定要使用的负载均衡器,转发给其中某个节点处理。如不设置此项(代理),则默认nginx本身作为web服务器,直接处理请求,会到root指定目录下找请求的文件        }    }
复制代码


设置的错误页面是 nginx 作为 web 服务器(处理静态资源)出现问题时,比如 nginx 上的静态资源找不到,返回给客户端的。


如果是 tomcat 出现的问题,比如 tomcat 上的 xxx.jsp 找不到,返回的是 tomcat 的错误页面,不是 nginx 的。


如果使用 nginx 本身要作为 web 服务器,直接处理客户端请求,比如处理静态资源,要将全局块中 user 设置为运行 nginx 的账户(即当前登陆 Linux 的账户),


否则 worker 进程(默认 nobody 账户)无权限读取当前账户(即运行 nginx 主进程的账户)的静态资源,客户端会显示 403 禁止访问。


可以使用正则表达式来过滤客户端 ip,也可以把客户端的 ip 过滤规则写在文件中,然后包含进来。

3、掌握 Nginx 负载算法配置

(1)轮询


将列表中的服务器排成一圈,从前往后,找空闲的服务器来处理请求。


轮询适合服务器性能差不多的情况。默认使用的就是轮询,不需要设置什么。


(2)加权轮询


upstream  servers{    server  192.168.1.7:8080 weight=1;    server  192.168.1.8:8081 weight=2;}
复制代码


设置权重,权重大的轮到的机会更大,适合服务器性能有明显差别的情况。


(3)ip_hash


upstream  servers{    ip_hash;    server  192.168.1.7:8080;    server  192.168.1.8:8081;}
复制代码


根据客户端 ip 的 hash 值来转发请求,同一客户端(ip)的请求都会被转发给同一个服务器处理,可解决 session 问题。


(4)url_hash(第三方)


upstream  servers{    hash $request_uri;    server  192.168.1.7:8080;    server  192.168.1.8:8081;}
复制代码


根据请求的 url 来转发,会将 url 相同的请求转发给同一服务器处理。


一直处理某个 url,服务器上一般都有该 url 的缓存,可直接从缓存中获取数据作为响应返回,减少时间开销。


(5)fair(第三方)


upstream  servers{    fair;    server  192.168.1.7:8080;    server  192.168.1.8:8081;}
复制代码


根据服务器响应时间来分发请求,响应时间短的分发的请求多。


fair 公平,nginx 先计算每个节点的平均响应时间,响应时间短说明该节点负载小(闲),要多转发给它;响应时间长说明该节点负载大,要少转发给它。


ip_hash、url_hash 都是使用特定节点来处理特定请求,如果特定节点故障,nginx 会剔除不可用的节点,将特定请求转发给其它节点处理,url_hash 影响不大,但 ip_hash 会丢失之前的 session 数据。

四、Tomcat 调优

1、基础参数设置

在 server.xml 中配置:


  • **maxThreads:**Tomcat 使用线程来处理接收的每个请求。这个值表示 Tomcat 可创建的最大的线程数。

  • **acceptCount:**指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。

  • **connnectionTimeout:**网络连接超时,单位:毫秒。设置为 0 表示永不超时,这样设置有隐患的。通常可设置为 30000 毫秒。

  • **minSpareThreads:**Tomcat 初始化时创建的线程数。

  • **maxSpareThreads:**一旦创建的线程超过这个值,Tomcat 就会关闭不再需要的 socket 线程

2、Tomat 的 4 种连接方式对比

tomcat 默认的 http 请求处理模式是 bio(即阻塞型,下面第二种),每次请求都新开一个线程处理。下面做一个介绍


<Connector port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol"   connectionTimeout="20000" redirectPort="8443"/>
复制代码


<Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000"  redirectPort="8443"/>
复制代码


<Connector executor="tomcatThreadPool"  port="8081" protocol="HTTP/1.1"  connectionTimeout="20000"  redirectPort="8443" />
复制代码


<Connector executor="tomcatThreadPool"  port="8081" protocol="org.apache.coyote.http11.Http11NioProtocol"  connectionTimeout="20000"  redirectPort="8443" />
复制代码


我们姑且把上面四种 Connector 按照顺序命名为 NIO, HTTP, POOL, NIOP。测试性能对比,数值为每秒处理的请求数,越大效率越高


NIO   HTTP   POOL  NIOP281   65     208    365666   66     110    398692   65     66     263256   63     94     459440   67     145    363
复制代码


得出结论:NIOP > NIO > POOL > HTTP 虽然 Tomcat 默认的 HTTP 效率最低,但是根据测试次数可以看出是最稳定的。且这只是一个简单页面测试,具体会根据复杂度有所波动。


配置参考:Linux 系统每个进程支持的最大线程数是 1000,windos 是 2000。具体跟服务器的内存,Tomcat 配置的数量有关联。


<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"              maxThreads="500" minSpareThreads="25" maxSpareThreads="250"              enableLookups="false" redirectPort="8443" acceptCount="300" connectionTimeout="20000" disableUploadTimeout="true"/>  
复制代码

3、Tomcat 的集群

Tomcat 的部署,是一台服务器部署一个 Tomcat(上线多个项目),还是一台服务器部署多个 tomact(每个 tomcat 部署 1~n 个项目)。多核必选配置多个 Tomcat,微服务多线程的思想模式。

4、Tomcat 内存设置

修改/bin/catalina.sh,增加如下设置:


JAVA_OPTS='-Xms【初始化内存大小】 -Xmx【可以使用的最大内存】'
复制代码


需要把这个两个参数值调大,大小的可以根据服务器内存的大小进行调整。例如:


JAVA_OPTS='-Xms1024m –Xmx2048m'
复制代码


服务器是 8G 内存,跑了 3 个 tomcat 服务,给分配了 2G 的内存,因为还有其他进程。




本篇文章写到这里差不多就结束了,当然也有很多东西还没有写到,不过限于篇幅也是没辙,我整理了很详细的JVM、MySQL、NGINX和Tomcat的学习笔记以及资料,需要的朋友直接点击领取就可以了。


最后,码字不易,所以,可以点个赞和收藏吗兄弟们!




end

发布于: 2021 年 04 月 22 日阅读数: 46
用户头像

北游学Java

关注

进群1044279583分享学习经验和分享面试心得 2020.11.16 加入

我秃了,也变强了

评论

发布
暂无评论
肝了15000字性能调优系列专题(JVM、MySQL、Nginx and Tomcat),看不完先收藏