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

2,849 阅读34分钟

前言

性能调优,无疑是个庞大的话题,也是很多项目中非常重要的一环,性能调优难做是众所周知的,毕竟性能调优涵盖的面实在是太多了,在这里我就大概的讲一下企业中最常用的四种调优——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实现自定义的类加载器。

jvm 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服务器的首页地址。rootindex2项配置都是必需的。            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  NIOP
281   65     208    365
666   66     110    398
692   65     66     263
256   63     94     459
440   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