风平浪静如码

风平浪静如码 查看完整档案

填写现居城市  |  填写毕业院校上海微创软件股份有限公司  |  Java后端研发 编辑填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

风平浪静如码 发布了文章 · 2020-12-18

应该没人比我更细了:17张图带你秒杀synchronized关键字!

synchronized关键字引入

我们知道,在多线程程序中往往会出现这么一个情况:多个线程同时访问某个线程间的共享变量。来举个例子吧:

假设银行存款业务写了两个方法,一个是存钱 store() 方法 ,一个是查询余额 get() 方法。假设初始客户小明的账户余额为 0 元。(PS:这个例子只是个 toy demo,为了方便大家理解写的,真实的业务场景不会这样。)

    // account 客户在银行的存款 
    public void store(int money){
        int newAccount=account+money;
        account=newAccount;
    }
    public void get(){
        System.out.print("小明的银行账户余额:");
        System.out.print(account);
    }

如果小明为自己存款 1 元,我们期望的线程调用情况如下:

  1. 首先会启动一个线程调用 store() 方法,为客户账户余额增加 1;
  2. 再启动一个线程调用 get() 方法,输出客户的新余额为 1。

但实际情况可能由于线程执行的先后顺序,出现如图所示的错误:

小明会惊奇的以为自己的钱没存上。这就是一个典型的由共享数据引发的并发数据冲突问题

解决方式也很简单,让并发执行会产生问题的代码段不并发行了。

如果 store() 方法 执行完,才能执行 get() 方法,而不是像上图一样并发执行,自然不会出现这个问题。那如何才能做到呢?

答案就是使用 synchronized 关键字。

我们先从直觉上思考一下,如果要实现先执行 store() 方法,再执行 get() 方法的话该怎么设计。

我们可以设置某个锁,锁会有两种状态,分别是上锁解锁。在 store() 方法执行之前,先观察这个锁的状态,如果是上锁状态,就进入阻塞,代码不运行;

如果这把锁是解锁状态,那就先将这把锁状态变为上锁,之后接着运行自己的代码。运行完成之后再将锁状态设置为解锁。

对于 get() 方法也是如此。

Java 中的 synchronized 关键字就是基于这种思想设计的。在 synchronized 关键字中,锁就是一个对象。

synchronized 一共有三种使用方法:

  • 直接修饰某个实例方法。像上文代码一样,在这种情况下多线程并发访问实例方法时,如果其他线程调用同一个对象的被 synchronized 修饰的方法,就会被阻塞。相当于把锁记录在这个方法对应的对象上。
    // account 客户在银行的存款 
    public synchronized void store(int money){
        int newAccount=account+money;
        account=newAccount;
    }
    public synchronized void get(){
        System.out.print("小明的银行账户余额:");
        System.out.print(account);
    }
  • 直接修饰某个静态方法。在这种情况下进行多线程并发访问时,如果其他线程也是调用属于同一类的被 synchronized 修饰的静态方法,就会被阻塞。相当于把锁信息记录在这个方法对应的类上。
    public synchronized static void get(){
        ···
    }
  • 修饰代码块。如果此时有别的线程也想访问某个被synchronized(对象0)修饰的同步代码块时,也会被阻塞。
    public static void get(){
        synchronized(对象0){
            ···
        }
    }

A问:我看了不少参考书还有网上资料,都说 synchronized 的锁是锁在对象上的。关于这句话,你能深入讲讲吗?

B回答道:别急,我先讲讲 Java 对象在内存中的表示。

Java 对象在内存中的表示

讲清 synchronized 关键字的原理前需要理清 Java 对象在内存中的表示方法。

上图就是一个 Java 对象在内存中的表示。我们可以看到,内存中的对象一般由三部分组成,分别是对象头、对象实际数据和对齐填充。

对象头包含 Mark Word、Class Pointer和 Length 三部分。

  • Mark Word 记录了对象关于锁的信息,垃圾回收信息等。
  • Class Pointer 用于指向对象对应的 Class 对象(其对应的元数据对象)的内存地址。
  • Length只适用于对象是数组时,它保存了该数组的长度信息。

对象实际数据包括了对象的所有成员变量,其大小由各个成员变量的大小决定。

对齐填充表示最后一部分的填充字节位,这部分不包含有用信息。

我们刚才讲的锁 synchronized 锁使用的就是对象头的 Mark Word 字段中的一部分。

Mark Word 中的某些字段发生变化,就可以代表锁不同的状态。

由于锁的信息是记录在对象里的,有的开发者也往往会说锁住对象这种表述。

无锁状态的 Mark Word

这里我们以无锁状态的 Mark Word 字段举例:

如果当前对象是无锁状态,对象的 Mark Word 如图所示。

我们可以看到,该对象头的 Mark Word 字段分为四个部分:

  1. 对象的 hashCode ;
  2. 对象的分代年龄,这部分用于对对象的垃圾回收;
  3. 是否为偏向锁位,1代表是,0代表不是;
  4. 锁标志位,这里是 01。

synchronized关键字的实现原理

讲完了 Java 对象在内存中的表示,我们下一步来讲讲 synchronized 关键字的实现原理。

从前文中我们可以看到, synchronized 关键字有两种修饰方法

  1. 直接作为关键字修饰在方法上,将整个方法作为同步代码块:
    public synchronized static void `get()`{
        ···
    }
  1. 修饰在同步代码块上
    public static void `get()`{
        synchronized(对象0){
            ···
        }
    }

针对这两种情况,Java 编译时的处理方法并不相同。

对于第一种情况,编译器会为其自动生成了一个 ACC_SYNCHRONIZED 关键字用来标识。

在 JVM 进行方法调用时,当发现调用的方法被 ACC_SYNCHRONIZED 修饰,则会先尝试获得锁。

对于第二种情况,编译时在代码块开始前生成对应的1个 monitorenter 指令,代表同步块进入。2个 monitorexit 指令,代表同步块退出。

这两种方法底层都需要一个 reference 类型的参数,指明要锁定和解锁的对象。

如果 synchronized 明确指定了对象参数,那就是该对象。

如果没有明确指定,那就根据修饰的方法是实例方法还是类方法,取对应的对象实例或类对象(Java 中类也是一种特殊的对象)作为锁对象。

每个对象维护着一个记录着被锁次数的计数器。当一个线程执行 monitorenter,该计数器自增从 0 变为 1;

当一个线程执行 monitorexit,计数器再自减。当计数器为 0 的时候,说明对象的锁已经释放。

A问:为什么会有两个 monitorexit 指令呢?

B答:正常退出,得用一个 monitorexit 吧,如果中间出现异常,锁会一直无法释放。所以编译器会为同步代码块添加了一个隐式的 try-finally 异常处理,在 finally 中会调用 monitorexit 命令最终释放锁。

重量级锁

A问:那么问题来了,之前你说锁的信息是记录在对象的 Mark Word 中的,那现在冒出来的 monitor 又是什么呢?

B答:我们先来看一下重量级锁对应对象的 Mark Word。

在 Java 的早期版本中,synchronized 锁属于重量级锁,此时对象的 Mark Word 如图所示。

我们可以看到,该对象头的 Mark Word 分为两个部分。第一部分是指向重量级锁的指针,第二部分是锁标记位。

而这里所说的指向重量级锁的指针就是 monitor

英文词典翻译 monitor 是监视器。Java 中每个对象会对应一个监视器。

这个监视器其实也就是监控锁有没有释放,释放的话会通知下一个等待锁的线程去获取。

monitor 的成员变量比较多,我们可以这样理解:

我们可以将 monitor 简单理解成两部分,第一部分表示当前占用锁的线程,第二部分是等待这把锁的线程队列

如果当前占用锁的线程把锁释放了,那就需要在线程队列中唤醒下一个等待锁的线程。

但是阻塞或唤醒一个线程需要依赖底层的操作系统来实现,Java 的线程是映射到操作系统的原生线程之上的。

而操作系统实现线程之间的切换需要从用户态转换到核心态,这个状态转换需要花费很多的处理器时间,甚至可能比用户代码执行的时间还要长。

由于这种效率太低,Java 后期做了改进,我再来详细讲一讲。

CAS算法

在讲其他改进之前,我们先来聊聊 CAS 算法。CAS 算法全称为 Compare And Swap。

顾名思义,该算法涉及到了两个操作,比较(Compare)和交换(Swap)。

怎么理解这个操作呢?我们来看下图:

我们知道,在对共享变量进行多线程操作的时候,难免会出现线程安全问题。

对该问题的一种解决策略就是对该变量加锁,保证该变量在某个时间段只能被一个线程操作。

但是这种方式的系统开销比较大。因此开发人员提出了一种新的算法,就是大名鼎鼎的 CAS 算法。

CAS 算法的思路如下:

  1. 该算法认为线程之间对变量的操作进行竞争的情况比较少。
  2. 算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。
  3. 如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N
  4. 如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。

当线程运行 CAS 算法时,该运行过程是原子操作,原子操作的含义就是线程开始跑这个函数后,运行过程中不会被别的程序打断。

我们来看看实际上 Java 语言中如何使用这个 CAS 算法,这里我们以 AtomicInteger 类中的 compareAndSwapInt() 方法举例:

public final native boolean compareAndSwapInt
(Object var1, long var2, int var3, int var4)

可以看到,该函数原型接受四个参数:

  1. 第一个参数是一个 AtomicInteger 对象。
  2. 第二个参数是该 AtomicInteger 对象对应的成员变量在内存中的地址。
  3. 第三个参数是上图中说的线程之前读取的值 P
  4. 第四个参数是上图中说的线程计算的新值 V

偏向锁

JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的。

如果是这种情况,不断的加锁解锁是没有必要的。

那么能不能让 JVM 直接负责在这种情况下加解锁的事情,不让操作系统插手呢?

因此开发者设计了偏向锁。偏向锁在获取资源的时候,会在资源对象上记录该对象是否偏向该线程。

偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。

下图表示偏向锁的 Mark Word结构:

可以看到,偏向锁对应的 Mark Word 包含该偏向锁对应的线程 ID、偏向锁的时间戳和对象分代年龄。

偏向锁的申请流程

我们再来看一下偏向锁的申请流程:

  1. 首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断;
  2. 判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4;
  3. 判断是否需要重偏向,重偏向逻辑在后面一节批量重偏向和批量撤销会说明。如果不用的话,直接获得偏向锁;
  4. 利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。

值得注意的是,在执行完同步代码后,线程不会主动去修改对象的 Mark Word,让它重回无锁状态。

所以一般执行完 synchronized 语句后,如果是偏向锁的状态的话,线程对锁的释放操作可能是什么都不做。

匿名偏向锁

在 JVM 开启偏向锁模式下,如果一个对象被新建,在四秒后,该对象的对象头就会被置为偏向锁。

一般来说,当一个线程获取了一把偏向锁时,会在对象头和栈帧中的锁记录里不仅说明目前是偏向锁状态,也会存储锁偏向的线程 ID。

在 JVM 四秒自动创建偏向锁的情况下,线程 ID 为0。

由于这种情况下的偏向锁不是由某个线程求得生成的,这种情况下的偏向锁也称为匿名偏向锁。

批量重偏向和批量撤销

生产者消费者模式下,生产者线程负责对象的创建,消费者线程负责对生产出来的对象进行使用。

当生产者线程创建了大量对象并执行加偏向锁的同步操作,消费者对对象使用之后,会产生大量偏向锁执行和偏向锁撤销的问题。

Russell K和 Detlefs D在他们的文章提出了批量重偏向和批量撤销的过程。

在上图情景下,他们探讨了能不能直接将偏向的线程换成消费者的线程。

替换不是一件容易事,需要在 JVM 的众多线程中找到类似上文情景的线程。

他们最后提出的解决方法是:

以类为单位,为每个类维护一个偏向锁撤销计数器,每一次该类的对象发生偏向撤销操作时,该计数器计数 +1,当这个计数值达到重偏向阈值时,JVM 就认为该类可能不适合正常逻辑,适合批量重偏向逻辑。这就是对应上图流程图里的是否需要重偏向过程。

以生产者消费者为例,生产者生产同一类型的对象给消费者,然后消费者对这些对象都需要执行偏向锁撤销,当撤销过程过多时就会触发上文规则,JVM 就注意到这个类了。

具体规则是:

  1. 每个类对象会有一个对应的 epoch 字段,每个处于偏向锁状态对象的 Mark Word 中也有该字段,其初始值为创建该对象时,类对象中的 epoch 的值。
  2. 每次发生批量重偏向时,就将类对象的 epoch 字段 +1,得到新的值 epoch_new
  3. 遍历 JVM 中所有线程的栈,找到该类对象,将其 epoch 字段改为新值。根据线程栈的信息判断出该线程是否锁定了该对象,将现在偏向锁还在被使用的对象赋新值 epoch_new
  4. 下次有线程想获得锁时,如果发现当前对象的 epoch 值和类的 epoch 不相等,不会执行撤销操作,而是直接通过 CAS 操作将其 Mark Word 的 Thread ID 改成当前线程 ID。

批量撤销相对于批量重偏向好理解得多,JVM 也会统计重偏向的次数。

假设该类计数器计数继续增加,当其达到批量撤销的阈值后(默认40),JVM 就认为该类的使用场景存在多线程竞争,会标记该类为不可偏向,之后对于该类的锁升级为轻量级锁。

轻量级锁

轻量级锁的设计初衷在于并发程序开发者的经验“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”。

所以它的设计出发点也在线程竞争情况较少的情况下。我们先来看一下轻量级锁的 Mark Word 布局。

如果当前对象是轻量级锁状态,对象的 Mark Word 如下图所示。

我们可以看到,该对象头Mark Word分为两个部分。第一部分是指向栈中的锁记录的指针,第二部分是锁标记位,针对轻量级锁该标记位为 00。

A问:那这指向栈中的锁记录的指针是什么意思呢?

B答:这得结合轻量级锁的上锁步骤来慢慢讲。

如果当前这个对象的锁标志位为 01(即无锁状态或者轻量级锁状态),线程在执行同步块之前,JVM 会先在当前的线程的栈帧中创建一个 Lock Record,包括一个用于存储对象头中的 Mark Word 以及一个指向对象的指针。

然后 JVM 会利用 CAS 算法对这个对象的 Mark Word 进行修改。如果修改成功,那该线程就拥有了这个对象的锁。我们来看一下如果上图的线程执行 CAS 算法成功的结果。

当然 CAS 也会有失败的情况。如果 CAS 失败,那就说明同时执行 CAS 操作的线程可不止一个了, Mark Word 也做了更改。

首先虚拟机会检查对象的 Mark Word 字段指向栈中的锁记录的指针是否指向当前线程的栈帧。如果是,那就说明可能出现了类似 synchronized 中套 synchronized 情况:

synchronized (对象0) {
    synchronized (对象0) {
        ···
    }
}

当然这种情况下当前线程已经拥有这个对象的锁,可以直接进入同步代码块执行。

否则说明锁被其他线程抢占了,该锁还需要升级为重量级锁。

和偏向锁不同的是,执行完同步代码块后,需要执行轻量级锁的解锁过程。解锁过程如下:

  1. 通过 CAS 操作尝试把线程栈帧中复制的 Mark Word 对象替换当前对象的 Mark Word。
  2. 如果 CAS 算法成功,整个同步过程就完成了。
  3. 如果 CAS 算法失败,则说明存在竞争,锁升级为重量级锁。

我们来总结一下轻量级锁升级过程吧:

总结

这次我们了解了 synchronized 底层实现原理和对应的锁升级过程。最后我们再通过这张流程图来回顾一下 synchronized 锁升级过程吧。

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-16

为什么要学习数据结构和算法?你了解数据结构和算法吗?

前言

为什么要学习数据结构和算法,这里我举个简单的例子。

编程好比是一辆汽车,而数据结构和算法是汽车内部的变速箱。一个开车的人不懂变速箱的原理也是能开车的,同理一个不懂数据结构和算法的人也能编程。但是如果一个开车的人懂变速箱的原理,比如降低速度来获得更大的牵引力,或者通过降低牵引力来获得更快的行驶速度。那么爬坡时使用1档,便可以获得更大的牵引力;下坡时便使用低档限制车的行驶速度。

回到编程而言,比如将一个班级的学生名字要临时存储在内存中,你会选择什么数据结构来存储,数组还是ArrayList,或者HashSet,或者别的数据结构。如果不懂数据结构的,可能随便选择一个容器来存储,也能完成所有的功能,但是后期如果随着学生数据量的增多,随便选择的数据结构肯定会存在性能问题,而一个懂数据结构和算法的人,在实际编程中会选择适当的数据结构来解决相应的问题,会极大的提高程序的性能。

数据结构

数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。

通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关。

1、数据结构的基本功能

  • 如何插入一条新的数据项
  • 如何寻找某一特定的数据项
  • 如何删除某一特定的数据项
  • 如何迭代的访问各个数据项,以便进行显示或其他操作

2、常用的数据结构

这几种结构优缺点如下:先有个大概印象,后面会详细讲解!!!

对于数组,你们所说的查找快,我想只是随机查找快,因为知道数组下标,可以按索引获取任意值。但是你要查找某个特定值,对于无序数组,还是需要遍历整个数组,那么查找效率是O(n),效率是很低的(有序数组按照二分查找算法还是很快的)。

插入快,是在数组尾部进行插入,获取到数组的最后一个索引下标,加1进行赋值就可以了。

删除慢,除开尾部删除,在任意中间或者前面删除,后面的元素都要整体进行平移的,所以也是比较慢的。

综上所述:对于数组,随机查找快,数组尾部增删快,其余的操作效率都是很低的。

算法

算法简单来说就是解决问题的步骤。

在Java中,算法通常都是由类的方法来实现的。前面的数据结构,比如链表为啥插入、删除快,而查找慢,平衡的二叉树插入、删除、查找都快,这都是实现这些数据结构的算法所造成的。后面我们讲的各种排序实现也是算法范畴的重要领域。

1、算法的五个特征

  • 有穷性:对于任意一组合法输入值,在执行又穷步骤之后一定能结束,即:算法中的每个步骤都能在有限时间内完成。
  • 确定性:在每种情况下所应执行的操作,在算法中都有确切的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件下,算法都只有一条执行路径。
  • 可行性:算法中的所有操作都必须足够基本,都可以通过已经实现的基本操作运算有限次实现之。
  • 有输入:作为算法加工对象的量值,通常体现在算法当中的一组变量。有些输入量需要在算法执行的过程中输入,而有的算法表面上可以没有输入,实际上已被嵌入算法之中。
  • 有输出:它是一组与“输入”有确定关系的量值,是算法进行信息加工后得到的结果,这种确定关系即为算法功能。

2、算法的设计原则

  • 正确性:首先,算法应当满足以特定的“规则说明”方式给出的需求。其次,对算法是否“正确”的理解可以有以下四个层次:
  1. 程序语法错误。
  2. 程序对于几组输入数据能够得出满足需要的结果。
  3. 程序对于精心选择的、典型、苛刻切带有刁难性的几组输入数据能够得出满足要求的结果。
  4. 程序对于一切合法的输入数据都能得到满足要求的结果。
PS:通常以第 三 层意义的正确性作为衡量一个算法是否合格的标准。
  • 可读性:算法为了人的阅读与交流,其次才是计算机执行。因此算法应该易于人的理解;另一方面,晦涩难懂的程序易于隐藏较多的错误而难以调试。
  • 健壮性:当输入的数据非法时,算法应当恰当的做出反应或进行相应处理,而不是产生莫名其妙的输出结果。并且,处理出错的方法不应是中断程序执行,而是应当返回一个表示错误或错误性质的值,以便在更高的抽象层次上进行处理。
  • 高效率与低存储量需求:通常算法效率值得是算法执行时间;存储量是指算法执行过程中所需要的最大存储空间,两者都与问题的规模有关。

前面三点 正确性,可读性和健壮性相信都好理解。对于第四点算法的执行效率和存储量,我们知道比较算法的时候,可能会说“A算法比B算法快两倍”之类的话,但实际上这种说法没有任何意义。因为当数据项个数发生变化时,A算法和B算法的效率比例也会发生变化,比如数据项增加了50%,可能A算法比B算法快三倍,但是如果数据项减少了50%,可能A算法和B算法速度一样。

所以描述算法的速度必须要和数据项的个数联系起来。也就是“大O”表示法,它是一种算法复杂度的相对表示方式,这里我简单介绍一下,后面会根据具体的算法来描述。

相对(relative):你只能比较相同的事物。你不能把一个做算数乘法的算法和排序整数列表的算法进行比较。但是,比较2个算法所做的算术操作(一个做乘法,一个做加法)将会告诉你一些有意义的东西;

表示(representation):大O(用它最简单的形式)把算法间的比较简化为了一个单一变量。这个变量的选择基于观察或假设。

例如,排序算法之间的对比通常是基于比较操作(比较2个结点来决定这2个结点的相对顺序)。这里面就假设了比较操作的计算开销很大。但是,如果比较操作的计算开销不大,而交换操作的计算开销很大,又会怎么样呢?这就改变了先前的比较方式;

复杂度(complexity):如果排序10,000个元素花费了我1秒,那么排序1百万个元素会花多少时间?在这个例子里,复杂度就是相对其他东西的度量结果。

然后我们在说说算法的存储量,包括:

程序本身所占空间

输入数据所占空间

辅助变量所占空间

一个算法的效率越高越好,而存储量是越低越好。

总结

本篇文章我们简单的介绍了数据结构和算法的概念,算法是解决问题的步骤,而数据结构的实现离不开算法,可能理解起来比较模糊,不用担心,后面我们会在具体的数据结构和算法实现过程中详细讲解。

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-14

年轻同事不讲武德,直接怼上MySQL常见SQL的七大错误用法

今天来分享几个MySQL常见的SQL错误(不当)用法。我们在作为一个初学者时,很有可能自己在写SQL时也没有注意到这些问题,导致写出来的SQL语句效率低下,所以我们也可以自省自检一下。

常见SQL错误用法

  • 1、LIMIT 语句

分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般DBA想到的办法是在type, name, create_time字段上加组合索引。这样条件排序都能有效的利用到索引,性能迅速提升。

SELECT * 
FROM   operation 
WHERE  type = 'SQLStats' 
       AND name = 'SlowLog' 
ORDER  BY create_time 
LIMIT  1000, 10; 

好吧,可能90%以上的DBA解决该问题就到此为止。但当 LIMIT 子句变成 “LIMIT 1000000,10” 时,程序员仍然会抱怨:我只取10条记录为什么还是慢?

要知道数据库也并不知道第1000000条记录从什么地方开始,即使有索引也需要从头计算一次。出现这种性能问题,多数情形下是程序员偷懒了。在前端数据浏览翻页,或者大数据分批导出等场景下,是可以将上一页的最大值当成参数作为查询条件的。SQL重新设计如下:

SELECT   * 
FROM     operation 
WHERE    type = 'SQLStats' 
AND      name = 'SlowLog' 
AND      create_time > '2017-03-16 14:00:00' 
ORDER BY create_time limit 10;

在新设计下查询时间基本固定,不会随着数据量的增长而发生变化。

  • 2、隐式转换

SQL语句中查询变量和字段定义类型不匹配是另一个常见的错误。比如下面的语句:

SELECT   * 
FROM     operation 
WHERE    type = 'SQLStats' 
AND      name = 'SlowLog' 
AND      create_time > '2017-03-16 14:00:00' 
ORDER BY create_time limit 10;

其中字段bpn的定义为varchar(20),MySQL的策略是将字符串转换为数字之后再比较。函数作用于表字段,索引失效。

上述情况可能是应用程序框架自动填入的参数,而不是程序员的原意。现在应用框架很多很繁杂,使用方便的同时也小心它可能给自己挖坑。

  • 3、关联更新、删除

虽然MySQL5.6引入了物化特性,但需要特别注意它目前仅仅针对查询语句的优化。对于更新或删除需要手工重写成JOIN。

比如下面UPDATE语句,MySQL实际执行的是循环/嵌套子查询(DEPENDENT SUBQUERY),其执行时间可想而知。

UPDATE operation o 
SET    status = 'applying' 
WHERE  o.id IN (SELECT id 
                FROM   (SELECT o.id, 
                               o.status 
                        FROM   operation o 
                        WHERE  o.group = 123 
                               AND o.status NOT IN ( 'done' ) 
                        ORDER  BY o.parent, 
                                  o.id 
                        LIMIT  1) t); 

执行计划:

+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| id | select_type        | table | type  | possible_keys | key     | key_len | ref   | rows | Extra                                               |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY            | o     | index |               | PRIMARY | 8       |       | 24   | Using where; Using temporary                        |
| 2  | DEPENDENT SUBQUERY |       |       |               |         |         |       |      | Impossible WHERE noticed after reading const tables |
| 3  | DERIVED            | o     | ref   | idx_2,idx_5   | idx_5   | 8       | const | 1    | Using where; Using filesort                         |
+----+--------------------+-------+-------+---------------+---------+---------+-------+------+-----------------------------------------------------+

重写为JOIN之后,子查询的选择模式从DEPENDENT SUBQUERY变成DERIVED,执行速度大大加快,从7秒降低到2毫秒。

UPDATE operation o 
       JOIN  (SELECT o.id, 
                            o.status 
                     FROM   operation o 
                     WHERE  o.group = 123 
                            AND o.status NOT IN ( 'done' ) 
                     ORDER  BY o.parent, 
                               o.id 
                     LIMIT  1) t
         ON o.id = t.id 
SET    status = 'applying' 

执行计划简化为:

+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| id | select_type | table | type | possible_keys | key   | key_len | ref   | rows | Extra                                               |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
| 1  | PRIMARY     |       |      |               |       |         |       |      | Impossible WHERE noticed after reading const tables |
| 2  | DERIVED     | o     | ref  | idx_2,idx_5   | idx_5 | 8       | const | 1    | Using where; Using filesort                         |
+----+-------------+-------+------+---------------+-------+---------+-------+------+-----------------------------------------------------+
  • 4、混合排序

MySQL不能利用索引进行混合排序。但在某些场景,还是有机会使用特殊方法提升性能的。

SELECT * 
FROM   my_order o 
       INNER JOIN my_appraise a ON a.orderid = o.id 
ORDER  BY a.is_reply ASC, 
          a.appraise_time DESC 
LIMIT  0, 20 

执行计划显示为全表扫描:

+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
| id | select_type | table | type   | possible_keys     | key     | key_len | ref      | rows    | Extra    
+----+-------------+-------+--------+-------------+---------+---------+---------------+---------+-+
|  1 | SIMPLE      | a     | ALL    | idx_orderid | NULL    | NULL    | NULL    | 1967647 | Using filesort |
|  1 | SIMPLE      | o     | eq_ref | PRIMARY     | PRIMARY | 122     | a.orderid |       1 | NULL           |
+----+-------------+-------+--------+---------+---------+---------+-----------------+---------+-+

由于is_reply只有0和1两种状态,我们按照下面的方法重写后,执行时间从1.58秒降低到2毫秒。

SELECT * 
FROM   ((SELECT *
         FROM   my_order o 
                INNER JOIN my_appraise a 
                        ON a.orderid = o.id 
                           AND is_reply = 0 
         ORDER  BY appraise_time DESC 
         LIMIT  0, 20) 
        UNION ALL 
        (SELECT *
         FROM   my_order o 
                INNER JOIN my_appraise a 
                        ON a.orderid = o.id 
                           AND is_reply = 1 
         ORDER  BY appraise_time DESC 
         LIMIT  0, 20)) t 
ORDER  BY  is_reply ASC, 
          appraisetime DESC 
LIMIT  20; 
  • 5、EXISTS语句

MySQL对待EXISTS子句时,仍然采用嵌套子查询的执行方式。如下面的SQL语句:

SELECT *
FROM   my_neighbor n 
       LEFT JOIN my_neighbor_apply sra 
              ON n.id = sra.neighbor_id 
                 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
       AND EXISTS(SELECT 1 
                  FROM   message_info m 
                  WHERE  n.id = m.neighbor_id 
                         AND m.inuser = 'xxx') 
       AND n.topic_type <> 5 

执行计划为:

+----+--------------------+-------+------+-----+------------------------------------------+---------+-------+---------+ -----+
| id | select_type        | table | type | possible_keys     | key   | key_len | ref   | rows    | Extra   |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+
|  1 | PRIMARY            | n     | ALL  |  | NULL     | NULL    | NULL  | 1086041 | Using where                   |
|  1 | PRIMARY            | sra   | ref  |  | idx_user_id | 123     | const |       1 | Using where          |
|  2 | DEPENDENT SUBQUERY | m     | ref  |  | idx_message_info   | 122     | const |       1 | Using index condition; Using where |
+----+--------------------+-------+------+ -----+------------------------------------------+---------+-------+---------+ -----+

去掉exists更改为join,能够避免嵌套子查询,将执行时间从1.93秒降低为1毫秒。

SELECT *
FROM   my_neighbor n 
       INNER JOIN message_info m 
               ON n.id = m.neighbor_id 
                  AND m.inuser = 'xxx' 
       LEFT JOIN my_neighbor_apply sra 
              ON n.id = sra.neighbor_id 
                 AND sra.user_id = 'xxx' 
WHERE  n.topic_status < 4 
       AND n.topic_type <> 5 

新的执行计划:

+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
| id | select_type | table | type   | possible_keys     | key       | key_len | ref   | rows | Extra                 |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
|  1 | SIMPLE      | m     | ref    | | idx_message_info   | 122     | const    |    1 | Using index condition |
|  1 | SIMPLE      | n     | eq_ref | | PRIMARY   | 122     | ighbor_id |    1 | Using where      |
|  1 | SIMPLE      | sra   | ref    | | idx_user_id | 123     | const     |    1 | Using where           |
+----+-------------+-------+--------+ -----+------------------------------------------+---------+ -----+------+ -----+
  • 6、条件下推

外部查询条件不能够下推到复杂的视图或子查询的情况有:

  1. 聚合子查询;
  2. 含有LIMIT的子查询;
  3. UNION 或UNION ALL子查询;
  4. 输出字段中的子查询;

如下面的语句,从执行计划可以看出其条件作用于聚合子查询之后:

SELECT * 
FROM   (SELECT target, 
               Count(*) 
        FROM   operation 
        GROUP  BY target) t 
WHERE  target = 'rm-xxxx' 
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
| id | select_type | table      | type  | possible_keys | key         | key_len | ref   | rows | Extra       |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+
|  1 | PRIMARY     | <derived2> | ref   | <auto_key0>   | <auto_key0> | 514     | const |    2 | Using where |
|  2 | DERIVED     | operation  | index | idx_4         | idx_4       | 519     | NULL  |   20 | Using index |
+----+-------------+------------+-------+---------------+-------------+---------+-------+------+-------------+

确定从语义上查询条件可以直接下推后,重写如下:

SELECT target, 
       Count(*) 
FROM   operation 
WHERE  target = 'rm-xxxx' 
GROUP  BY target

执行计划变为:

+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
| 1 | SIMPLE | operation | ref | idx_4 | idx_4 | 514 | const | 1 | Using where; Using index |
+----+-------------+-----------+------+---------------+-------+---------+-------+------+--------------------+
  • 7、提前缩小范围

先上初始SQL语句:

SELECT * 
FROM   my_order o 
       LEFT JOIN my_userinfo u 
              ON o.uid = u.uid
       LEFT JOIN my_productinfo p 
              ON o.pid = p.pid 
WHERE  ( o.display = 0 ) 
       AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15 

该SQL语句原意是:先做一系列的左连接,然后排序取前15条记录。从执行计划也可以看出,最后一步估算排序记录数为90万,时间消耗为12秒。

+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
| id | select_type | table | type   | possible_keys | key     | key_len | ref             | rows   | Extra                                              |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+
|  1 | SIMPLE      | o     | ALL    | NULL          | NULL    | NULL    | NULL            | 909119 | Using where; Using temporary; Using filesort       |
|  1 | SIMPLE      | u     | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | SIMPLE      | p     | ALL    | PRIMARY       | NULL    | NULL    | NULL            |      6 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+--------+---------------+---------+---------+-----------------+--------+----------------------------------------------------+

由于最后WHERE条件以及排序均针对最左主表,因此可以先对my_order排序提前缩小数据量再做左连接。SQL重写后如下,执行时间缩小为1毫秒左右。

SELECT * 
FROM (
SELECT * 
FROM   my_order o 
WHERE  ( o.display = 0 ) 
       AND ( o.ostaus = 1 ) 
ORDER  BY o.selltime DESC 
LIMIT  0, 15
) o 
     LEFT JOIN my_userinfo u 
              ON o.uid = u.uid 
     LEFT JOIN my_productinfo p 
              ON o.pid = p.pid 
ORDER BY  o.selltime DESC
limit 0, 15

再检查执行计划:子查询物化后(select_type=DERIVED)参与JOIN。虽然估算行扫描仍然为90万,但是利用了索引以及LIMIT 子句后,实际执行时间变得很小。

+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
| id | select_type | table      | type   | possible_keys | key     | key_len | ref   | rows   | Extra                                              |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL          | NULL    | NULL    | NULL  |     15 | Using temporary; Using filesort                    |
|  1 | PRIMARY     | u          | eq_ref | PRIMARY       | PRIMARY | 4       | o.uid |      1 | NULL                                               |
|  1 | PRIMARY     | p          | ALL    | PRIMARY       | NULL    | NULL    | NULL  |      6 | Using where; Using join buffer (Block Nested Loop) |
|  2 | DERIVED     | o          | index  | NULL          | idx_1   | 5       | NULL  | 909112 | Using where                                        |
+----+-------------+------------+--------+---------------+---------+---------+-------+--------+----------------------------------------------------+

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-11

用MySQL 执行计划分析 DATE_FORMAT 函数对索引的影响

前言

最近公司在代码评审时,在使用DATE_FORMAT函数的问题上有了点不同的观点。具体DATE_FORMAT对索引会不会产生影响?哪种情况下会产生影响呢?周末无事,通过mysql的执行计划测试一波。

使用 explain 分析

执行计划就是展示Mysql如何执行一条Sql语句,使用 EXPLAIN 。输出包括Sql查询的顺序、是否使用索引、以及使用的索引信息等内容, 展示如图

id : 表示查询中select操作表的顺序,按顺序从大到依次执行

select_type :该表示选择的类型,可选值有: SIMPLE(简单的)

type :该属性表示访问类型,有很多种访问类型。最常见的其中包括以下几种: ALL(全表扫描), index(索引扫描),range(范围扫描),ref (非唯一索引扫描),eq_ref(唯一索引扫描,),(const)常数引用, 访问速度依次由慢到快。其中 : range(范围)常见与 between and …, 大于 and 小于这种情况。提示 : 慢SQL是否走索引,走了什么索引,也就可以通过该属性查看了。

table : 表示该语句查询的表

possible_keys : 顾名思义,该属性给出了,该查询语句,可能走的索引,(如某些字段上索引的名字)这里提供的只是参考,而不是实际走的索引,也就导致会有possible_Keys不为null,key为空的现象。

key :显示MySQL实际使用的索引,其中就包括主键索引(PRIMARY),或者自建索引的名字。

key_len : 表示索引所使用的字节数,

ref : 连接匹配条件,如果走主键索引的话,该值为: const, 全表扫描的话,为null值

rows : 扫描行数,也就是说,需要扫描多少行,才能获取目标行数,一般情况下会大于返回行数。通常情况下,rows越小,效率越高, 也就有大部分SQL优化,都是在减少这个值的大小。注意: 理想情况下扫描的行数与实际返回行数理论上是一致的,但这种情况及其少,如关联查询,扫描的行数就会比返回行数大大增加)

Extra :这个属性非常重要,该属性中包括执行SQL时的真实情况信息,如上面所属,使用到的是”using where”,表示使用where筛选得到的值,常用的有: “Using temporary”: 使用临时表 “using filesort”: 使用文件排序

表语句及数据插入sql如图:

如果有伙伴需要验证,也附上SQL,和上面图片内容一致:

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  `birth_date` date NULL DEFAULT NULL,
  `is_delete` tinyint(1) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `name`(`name`) USING BTREE,
  INDEX `age`(`age`) USING BTREE,
  INDEX `birth_date`(`birth_date`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '小明1', 1, '2000-01-01', 0);
INSERT INTO `user` VALUES (2, '小明2', 2, '2001-01-02', 0);
INSERT INTO `user` VALUES (3, '小明3', 3, '2002-01-03', 0);
INSERT INTO `user` VALUES (4, '小明4', 4, '2003-01-04', 0);
INSERT INTO `user` VALUES (5, '小明5', 5, '2004-01-05', 0);
INSERT INTO `user` VALUES (6, '小明6', 6, '2005-01-06', 0);
INSERT INTO `user` VALUES (7, '小明7', 7, '2006-01-07', 0);
INSERT INTO `user` VALUES (8, '小明8', 8, '2007-01-08', 0);
INSERT INTO `user` VALUES (9, '小明9', 9, '2008-01-09', 0);
INSERT INTO `user` VALUES (10, '小明1', 10, '2009-01-10', 0);
INSERT INTO `user` VALUES (11, '小明11', 11, '2010-01-11', 0);
INSERT INTO `user` VALUES (12, '小明12', 12, '2011-01-12', 0);
INSERT INTO `user` VALUES (13, '小明13', 13, '2012-01-13', 0);
INSERT INTO `user` VALUES (14, '小明14', 14, '2013-01-14', 0);
INSERT INTO `user` VALUES (15, '小明15', 15, '2014-01-15', 0);
INSERT INTO `user` VALUES (16, '小明16', 16, '2015-01-16', 0);
INSERT INTO `user` VALUES (17, '小明17', 17, '2016-01-17', 0);
INSERT INTO `user` VALUES (18, '小明18', 18, '2017-01-18', 0);
INSERT INTO `user` VALUES (19, '小明19', 19, '2018-01-19', 0);
INSERT INTO `user` VALUES (20, '小明20', 20, '2019-01-20', 0);
INSERT INTO `user` VALUES (21, '小明21', 21, '2020-01-21', 0);

验证过程

注意:本文中采用的数据库为mysql:5.7.24。

1、不用DATE_FORMAT函数

第一种方式:

explain select * from user where birth_date <= '2009-10-10';

如上图所示可能用到 key: birth_date 。

第二种方式:

EXPLAIN SELECT * FROM USER WHERE birth_date <= '2009-10-10' and birth_date >= '2009-10-10';

如上图所示用到 key: birth_date 。

2、使用DATE_FORMAT函数

第一种方式:

EXPLAIN
SELECT
 *
FROM
 USER
WHERE
 birth_date >= DATE_FORMAT('2019-10-10', '%Y-%m-%d');
  
EXPLAIN
SELECT
 *
FROM
 USER
WHERE
 birth_date >= DATE_FORMAT('2019-10-10', '%Y-%m-%d')
AND birth_date <= DATE_FORMAT('2020-12-10', '%Y-%m-%d');

第二种方式:

EXPLAIN
SELECT
 *
FROM
 USER
WHERE
 DATE_FORMAT(birth_date, '%Y-%m-%d') >= '2019-10-10';

EXPLAIN
SELECT
 *
FROM
 USER
WHERE
 DATE_FORMAT(birth_date, '%Y-%m-%d') >= '2019-10-10'
AND DATE_FORMAT(birth_date, '%Y-%m-%d') <= '2020-12-10';

以上两组SQL分别在字段和参数上加了DATE_FORMAT进行测试,结果如图,birth_date的索引均未生效, 主要看 字段 key

所以,在开发过程中,应避免使用该类函数,否则可能会导致索引失效,全表扫描。

总结

根据实际情况进行选择

①使用 DATE_FORMAT 中的第一种方式,在查询参数使用DATE_FORMAT,而不是查询字段使用

EXPLAIN
SELECT
 *
FROM
 USER
WHERE
 birth_date >= DATE_FORMAT('2019-10-10', '%Y-%m-%d')
AND birth_date <= DATE_FORMAT('2020-12-10', '%Y-%m-%d');

②不用DATE_FORMAT中的第二种方式,按照范围进行查询,如查询一

-- 举例:
EXPLAIN SELECT
 *
FROM
 USER
WHERE
 create_time >= '2026-10-24 00:00:00'
AND create_time <= '2026-10-24 23:59:59';

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-10

快速入门:两分钟带你从 0 开始手写一个Tomcat

前言

Tomcat,这只3脚猫,大学的时候就认识了,直到现在工作中,也常会和它打交道。这是一只神奇的猫,今天让我来抽象你,实现你!

Tomcat是非常流行的Web Server,它还是一个满足Servlet规范的容器。那么想一想,Tomcat和我们的Web应用是什么关系?

从感性上来说,我们一般需要把Web应用打成WAR包部署到Tomcat中,在我们的Web应用中,我们要指明URL被哪个类的哪个方法所处理(不论是原始的Servlet开发,还是现在流行的Spring MVC都必须指明)。

由于我们的Web应用是运行在Tomcat中,那么显然,请求必定是先到达Tomcat的。Tomcat对于请求实际上会进行下面的处理:

第一:提供Socket服务

Tomcat的启动,必然是Socket服务,只不过它支持HTTP协议而已!

这里其实可以扩展思考下,Tomcat既然是基于Socket,那么是基于BIO or NIO or AIO呢?

第二:进行请求的分发

要知道一个Tomcat可以为多个Web应用提供服务,那么很显然,Tomcat可以把URL下发到不同的Web应用。

第三:需要把请求和响应封装成request/response

我们在Web应用这一层,可从来没有封装过request/response的,我们都是直接使用的,这就是因为Tomcat已经为你做好了!
话不多说,先来看一眼工程截图:

一、封装请求对象

这里,你可以清楚的看到,我们通过输入流,对HTTP协议进行解析,拿到了HTTP请求头的方法以及URL。

二、封装响应对象

基于HTTP协议的格式进行输出写入。

三、Servlet 请求处理基类

前文说Tomcat是满足Servlet规范的容器,那么自然Tomcat需要提供API。这里你看到了Servlet常见的doGet/doPost/service方法。

四、Servlet 实现类

提供这2个具体的Servlet实现,只是为了后续的测试!

五、Servlet 配置

我们在servlet开发中,会在web.xml中通过和来进行指定哪个URL交给哪个servlet进行处理。

六、启动类

这里,你能够看到Tomcat的处理流程:把URL对应处理的Servlet关系形成,解析HTTP协议,封装请求/响应对象,利用反射实例化具体的Servlet进行处理即可。

七、测试

实现一个简易版的 Tomcat 就这么 7 大步搞定,大家可以动手实现一下吧,代码最好不要复制,自己动手一个个敲,这样实现之后,对 Tomcat 就没那么陌生了。

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-09

干坏事前一定要检查自己的域名是不是 HTTPS 的,不然……

一、HTTP 协议

在谈论 HTTPS 协议之前,先来回顾一下 HTTP 协议的概念。

1.1 HTTP 协议介绍

HTTP 协议是一种基于文本的传输协议,它位于 OSI 网络模型中的应用层。

HTTP 协议是通过客户端和服务器的请求应答来进行通讯,目前协议由之前的 RFC 2616 拆分成立六个单独的协议说明(RFC 7230、RFC 7231、RFC 7232、RFC 7233、RFC 7234、RFC 7235),通讯报文如下:

请求

POST http://www.baidu.com HTTP/1.1
Host: www.baidu.com
Connection: keep-alive
Content-Length: 7
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36

wd=HTTP

响应

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Encoding: gzip
Content-Type: text/html;charset=utf-8
Date: Thu, 14 Feb 2019 07:23:49 GMT
Transfer-Encoding: chunked

<html>...</html>

1.2 HTTP 中间人攻击

HTTP 协议使用起来确实非常的方便,但是它存在一个致命的缺点:不安全。

我们知道 HTTP 协议中的报文都是以明文的方式进行传输,不做任何加密,这样会导致什么问题呢?下面来举个例子:

小明在 JAVA 贴吧发帖,内容为我爱JAVA:

被中间人进行攻击,内容修改为我爱PHP

小明被群嘲
可以看到在 HTTP 传输过程中,中间人能看到并且修改 HTTP 通讯中所有的请求和响应内容,所以使用 HTTP 是非常的不安全的。

1.3 防止中间人攻击

这个时候可能就有人想到了,既然内容是明文那我使用对称加密的方式将报文加密这样中间人不就看不到明文了吗,于是如下改造:

双方约定加密方式

使用 AES 加密报文

这样看似中间人获取不到明文信息了,但其实在通讯过程中还是会以明文的方式暴露加密方式和秘钥,如果第一次通信被拦截到了,那么秘钥就会泄露给中间人,中间人仍然可以解密后续的通信:

那么对于这种情况,我们肯定就会考虑能不能将秘钥进行加密不让中间人看到呢?答案是有的,采用非对称加密,我们可以通过 RSA 算法来实现。这个步骤实际操作也是比较简单的,在码匠笔记订阅号后台回复HTTPS就可以查看搭建HTTPS服务视频。

在约定加密方式的时候由服务器生成一对公私钥,服务器将公钥返回给客户端,客户端本地生成一串秘钥(AES_KEY)用于对称加密,并通过服务器发送的公钥进行加密得到(AES_KEY_SECRET),之后返回给服务端,服务端通过私钥将客户端发送的AES_KEY_SECRET进行解密得到AEK_KEY,最后客户端和服务器通过AEK_KEY进行报文的加密通讯,改造如下:

可以看到这种情况下中间人是窃取不到用于AES加密的秘钥,所以对于后续的通讯是肯定无法进行解密了,那么这样做就是绝对安全了吗?

所谓道高一尺魔高一丈,中间人为了对应这种加密方法又想出了一个新的破解方案,既然拿不到AES_KEY,那我就把自己模拟成一个客户端和服务器端的结合体,在用户->中间人的过程中中间人模拟服务器的行为,这样可以拿到用户请求的明文,在中间人->服务器的过程中中间人模拟客户端行为,这样可以拿到服务器响应的明文,以此来进行中间人攻击:

这一次通信再次被中间人截获,中间人自己也伪造了一对公私钥,并将公钥发送给用户以此来窃取客户端生成的AES_KEY,在拿到AES_KEY之后就能轻松的进行解密了。

中间人这样为所欲为,就没有办法制裁下吗,当然有啊,接下来我们看看 HTTPS 是怎么解决通讯安全问题的。

二、HTTPS 协议

2.1 HTTPS 简介

HTTPS 其实是SSL+HTTP的简称,当然现在SSL基本已经被TLS取代了,不过接下来我们还是统一以SSL作为简称,SSL协议其实不止是应用在HTTP协议上,还在应用在各种应用层协议上,例如:FTP、WebSocket。

其实SSL协议大致就和上一节非对称加密的性质一样,握手的过程中主要也是为了交换秘钥,然后再通讯过程中使用对称加密进行通讯,大概流程如下:

这里我只是画了个示意图,其实真正的 SSL 握手会比这个复杂的多,但是性质还是差不多,而且我们这里需要关注的重点在于 HTTPS 是如何防止中间人攻击的。

通过上图可以观察到,服务器是通过 SSL 证书来传递公钥,客户端会对 SSL 证书进行验证,其中证书认证体系就是确保SSL安全的关键,接下来我们就来讲解下CA 认证体系,看看它是如何防止中间人攻击的。

2.2 CA 认证体系

上一节我们看到客户端需要对服务器返回的 SSL 证书进行校验,那么客户端是如何校验服务器 SSL 证书的安全性呢。

权威认证机构

在 CA 认证体系中,所有的证书都是由权威机构来颁发,而权威机构的 CA 证书都是已经在操作系统中内置的,我们把这些证书称之为CA根证书:

签发证书

我们的应用服务器如果想要使用 SSL 的话,需要通过权威认证机构来签发CA证书,我们将服务器生成的公钥和站点相关信息发送给CA签发机构,再由CA签发机构通过服务器发送的相关信息用CA签发机构进行加签,由此得到我们应用服务器的证书,证书会对应的生成证书内容的签名,并将该签名使用CA签发机构的私钥进行加密得到证书指纹,并且与上级证书生成关系链。

这里我们把百度的证书下载下来看看:

可以看到百度是受信于GlobalSign G2,同样的GlobalSign G2是受信于GlobalSign R1,当客户端(浏览器)做证书校验时,会一级一级的向上做检查,直到最后的根证书,如果没有问题说明服务器证书是可以被信任的。

如何验证服务器证书

那么客户端(浏览器)又是如何对服务器证书做校验的呢,首先会通过层级关系找到上级证书,通过上级证书里的公钥来对服务器的证书指纹进行解密得到签名(sign1),再通过签名算法算出服务器证书的签名(sign2),通过对比sign1和sign2,如果相等就说明证书是没有被篡改也不是伪造的。

这里有趣的是,证书校验用的 RSA 是通过私钥加密证书签名,公钥解密来巧妙的验证证书有效性。

这样通过证书的认证体系,我们就可以避免了中间人窃取AES_KEY从而发起拦截和修改 HTTP 通讯的报文。

总结

首先先通过对 HTTP 中间人攻击的来了解到 HTTP 为什么是不安全的,然后再从安全攻防的技术演变一直到 HTTPS 的原理概括,希望能让大家对 HTTPS 有个更深刻的了解。

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-08

五分钟带你了解Java是如何从容而优雅地实现接口数据校验

本篇文章给大家分享平时开发中总结的一点小技巧!在工作中写过Java程序的朋友都知道,目前使用Java开发服务最主流的方式就是通过Spring MVC定义一个Controller层接口,并将接口请求或返回参数分别定义在一个Java实体类中,这样Spring MVC在接收到Http请求(POST/GET)后,就会自动将请求报文自动映射成一个Java对象。这样的代码通常是这样写的:

@RestController
public class OrderController {

    @Autowired
    private OrderService orderServiceImpl;

    @PostMapping("/createOrder")
    public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
        return orderServiceImpl.createOrder(createOrderDTO);
    }
}

这样的代码相信大家并不陌生,但在后续的逻辑实现过程中却会遇到这样的问题:“在接收请求参数后如何实现报文对象数据值的合法性校验?”。一些同学也可能认为这并不是什么问题,因为具体某个参数字段是否为空、值的取值是否在约定范围、格式是否合法等等,在业务代码中校验就好了。例如可以在Service实现类中对报文格式进行各种if-else的数据校验。

从功能上说冗余的if-else代码没啥毛病,但从代码的优雅性来说冗长的if-else代码会显得非常臃肿。接下来的内容将给大家介绍一种处理此类问题的实用方法。具体将从以下几个方面进行介绍:

  • 使用@Validated注解实现Controller接口层数据直接绑定校验;
  • 扩展约束性注解实现数据取值范围的校验;
  • 更加灵活的对象数据合法性校验工具类封装;
  • 数据合法性校验结果异常统一返回处理;

Controller接口层数据绑定校验

实际上在Java开发中目前普通使用的Bean数据校验工具是"hibernate-validator",它是一个hibernete独立的jar包,所以使用这个jar包并不需要一定要集成Hibernete框架。该jar包主要实现并扩展了javax.validation(是一个基于JSR-303标准开发出来的Bean校验规范)接口。

由于Spring Boot在内部默认集成了"hibernate-validator",所以使用Spring Boot构建的Java工程可以直接使用相关注解来实现Bean的数据校验。例如我们最常编写的Controller层接口参数对象,可以在定义Bean类时直接编写这样的代码:

@Data
public class CreateOrderDTO {

    @NotNull(message = "订单号不能为空")
    private String orderId;
    @NotNull(message = "订单金额不能为空")
    @Min(value = 1, message = "订单金额不能小于0")
    private Integer amount;
    @Pattern(regexp = "^1[3|4|5|7|8][0-9]{9}$", message = "用户手机号不合法")
    private String mobileNo;
    private String orderType;
    private String status;
}

如上所示代码,我们可以使用@NotNull注解来约束该字段必须不能为空,也可以使用@Min注解来约束字段的最小取值,或者还可以通过@Pattern注解来使用正则表达式来约束字段的格式(如手机号格式)等等。

以上这些注解都是“hibernate-validator”依赖包默认提供的,更多常用的注解还有很多,例如:

利用这些约束注解,我们就可以很轻松的搞定接口数据校验,而不需要在业务逻辑中编写大量的if-else来进行数据合法性校验。而定义好Bean参数对象并使用相关注解实现参数值约束后,在Controller层接口定义中只需要使用@Validated注解就可以实现在接收参数后自动进行数据绑定校验了,具体代码如下:

@PostMapping("/createOrder")
public CreateOrderBO validationTest(@Validated CreateOrderDTO createOrderDTO) {
    return orderServiceImpl.createOrder(createOrderDTO);
}

如上所示,在Controller层中通过Spring提供的@Validated注解可以自动实现数据Bean的绑定校验,如果数据异常则会统一抛出校验异常!

约束性注解扩展

在“hibernate-validator”依赖jar包中,虽然提供了很多很方便的约束注解,但是也有不满足某些实际需要的情况,例如我们想针对参数中的某个值约定其值的枚举范围,如orderType订单类型只允许传“pay”、“refund”两种值,那么现有的约束注解可能就没有特别适用的了。此外,如果对这样的枚举值,我们还想在约束定义中直接匹配代码中的枚举定义,以更好地统一接口参数与业务逻辑的枚举定义。那么这种情况下,我们还可以自己扩展定义相应地约束注解逻辑。

接下来我们定义新的约束注解@EnumValue,来实现上面我们所说的效果,具体代码如下:

@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {

    //默认错误消息
    String message() default "必须为指定值";

    //支持string数组验证
    String[] strValues() default {};

    //支持int数组验证
    int[] intValues() default {};

    //支持枚举列表验证
    Class<?>[] enumValues() default {};

    //分组
    Class<?>[] groups() default {};

    //负载
    Class<? extends Payload>[] payload() default {};

    //指定多个时使用
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        EnumValue[] value();
    }

    /**
     * 校验类逻辑定义
     */
    class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {

        //字符串类型数组
        private String[] strValues;
        //int类型数组
        private int[] intValues;
        //枚举类
        private Class<?>[] enumValues;

        /**
         * 初始化方法
         *
         * @param constraintAnnotation
         */
        @Override
        public void initialize(EnumValue constraintAnnotation) {
            strValues = constraintAnnotation.strValues();
            intValues = constraintAnnotation.intValues();
            enumValues = constraintAnnotation.enumValues();
        }

        /**
         * 校验方法
         *
         * @param value
         * @param context
         * @return
         */
        @SneakyThrows
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
            //针对字符串数组的校验匹配
            if (strValues != null && strValues.length > 0) {
                if (value instanceof String) {
                    for (String s : strValues) {//判断值类型是否为Integer类型
                        if (s.equals(value)) {
                            return true;
                        }
                    }
                }
            }
            //针对整型数组的校验匹配
            if (intValues != null && intValues.length > 0) {
                if (value instanceof Integer) {//判断值类型是否为Integer类型
                    for (Integer s : intValues) {
                        if (s == value) {
                            return true;
                        }
                    }
                }
            }
            //针对枚举类型的校验匹配
            if (enumValues != null && enumValues.length > 0) {
                for (Class<?> cl : enumValues) {
                    if (cl.isEnum()) {
                        //枚举类验证
                        Object[] objs = cl.getEnumConstants();
                        //这里需要注意,定义枚举时,枚举值名称统一用value表示
                        Method method = cl.getMethod("getValue");
                        for (Object obj : objs) {
                            Object code = method.invoke(obj, null);
                            if (value.equals(code.toString())) {
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }
    }
}

如上所示的@EnumValue约束注解,是一个非常实用的扩展,通过该注解我们可以实现对参数取值范围(不是大小范围)的约束,它支持对int、string以及enum三种数据类型的约束,具体使用方式如下:

/**
 * 定制化注解,支持参数值与指定类型数组列表值进行匹配(缺点是需要将枚举值写死在字段定义的注解中)
 */
@EnumValue(strValues = {"pay", "refund"}, message = "订单类型错误")
private String orderType;
/**
 * 定制化注解,实现参数值与枚举列表的自动匹配校验(能更好地与实际业务开发匹配)
 */
@EnumValue(enumValues = Status.class, message = "状态值不在指定范围")
private String status;

如上所示代码,该扩展注解既可以使用strValues或intValues属性来编程列举取值范围,也可以直接通过enumValues来绑定枚举定义。但是需要注意,处于通用考虑,具体枚举定义的属性的名称要统一匹配为value、desc,例如Status枚举定义如下:

public enum Status {
    PROCESSING(1, "处理中"),
    SUCCESS(2, "订单已完成");
    Integer value;
    String desc;

    Status(Integer value, String desc) {
        this.value = value;
        this.desc = desc;
    }

    public Integer getValue() {
        return value;
    }

    public String getDesc() {
        return desc;
    }
}

通过注解扩展,就能实现更多方便的约束性注解!

更加灵活的数据校验工具类封装

除了上面直接在Controller层使用@Validated进行绑定数据校验外,在有些情况,例如你的参数对象中的某个字段是一个复合对象,或者业务层的某个方法所定义的入参对象也需要进行数据合法性校验,那么这种情况下如何实现像Controller层一样的校验效果呢?

需要说明在这种情况下@Validated已经无法直接使用了,因为@Validated注解发挥作用主要是Spring MVC在接收参数的过程中实现了自动数据绑定校验,而在普通的业务方法或者复合参数对象中是没有办法直接绑定校验的。这种情况下,我们可以通过定义ValidateUtils工具类来实现一样的校验效果,具体代码如下:

public class ValidatorUtils {

    private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();

    /**
     * bean整体校验,有不合规范,抛出第1个违规异常
     */
    public static void validate(Object obj, Class<?>... groups) {
        Set<ConstraintViolation<Object>> resultSet = validator.validate(obj, groups);
        if (resultSet.size() > 0) {
            //如果存在错误结果,则将其解析并进行拼凑后异常抛出
            List<String> errorMessageList = resultSet.stream().map(o -> o.getMessage()).collect(Collectors.toList());
            StringBuilder errorMessage = new StringBuilder();
            errorMessageList.stream().forEach(o -> errorMessage.append(o + ";"));
            throw new IllegalArgumentException(errorMessage.toString());
        }
    }
}

如上所示,我们定义了一个基于"javax.validation"接口的工具类实现,这样就可以在非@Validated直接绑定校验的场景中通过校验工具类来实现对Bean对象约束注解的校验处理,具体使用代码如下:

public boolean orderCheck(OrderCheckBO orderCheckBO) {
    //对参数对象进行数据校验
    ValidatorUtils.validate(orderCheckBO);
    return true;
}

而方法入参对象则还是可以继续使用前面我们介绍的约束性注解进行约定,例如上述方法的入参对象定义如下:

@Data
@Builder
public class OrderCheckBO {

    @NotNull(message = "订单号不能为空")
    private String orderId;
    @Min(value = 1, message = "订单金额不能小于0")
    private Integer orderAmount;
    @NotNull(message = "创建人不能为空")
    private String operator;
    @NotNull(message = "操作时间不能为空")
    private String operatorTime;
}

这样在编程体验上就可以整体上保持一致!

数据合法性校验结果异常统一处理

通过前面我们所讲的各种约束注解,我们实现了对Controller层接口以及业务方法参数对象的统一数据校验。而为了保持校验异常处理的统一处理和错误报文统一输出,我们还可以定义通用的异常处理机制,来保证各类数据校验错误都能以统一错误格式反馈给调用方。具体代码如下:

@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
    /**
     * 统一处理参数校验错误异常(非Spring接口数据绑定验证)
     *
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(BindException.class)
    @ResponseBody
    public ResponseResult<?> processValidException(HttpServletResponse response, BindException e) {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        //获取校验错误结果信息,并将信息组装
        List<String> errorStringList = e.getBindingResult().getAllErrors()
                .stream().map(ObjectError::getDefaultMessage).collect(Collectors.toList());
        String errorMessage = String.join("; ", errorStringList);
        response.setContentType("application/json;charset=UTF-8");
        log.error(e.toString() + "_" + e.getMessage(), e);
        return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(),
                errorMessage);
    }

    /**
     * 统一处理参数校验错误异常
     *
     * @param response
     * @param e
     * @return
     */
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseBody
    public ResponseResult<?> processValidException(HttpServletResponse response, IllegalArgumentException e) {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        String errorMessage = String.join("; ", e.getMessage());
        response.setContentType("application/json;charset=UTF-8");
        log.error(e.toString() + "_" + e.getMessage(), e);
        return ResponseResult.systemException(GlobalCodeEnum.GL_FAIL_9998.getCode(),
                errorMessage);
    }

    ...
}

如上所示,我们定义了针对前面两种数据校验方式的统一异常处理机制,这样数据校验的错误信息就能通过统一的报文格式反馈给调用端,从而实现接口数据报文的统一返回!

其中通用的接口参数对象ResponseResult的代码定义如下:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonPropertyOrder({"code", "message", "data"})
public class ResponseResult<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 返回的对象
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;
    /**
     * 返回的编码
     */
    private Integer code;
    /**
     * 返回的信息
     */
    private String message;

    /**
     * @param data 返回的数据
     * @param <T>  返回的数据类型
     * @return 响应结果
     */
    public static <T> ResponseResult<T> OK(T data) {
        return packageObject(data, GlobalCodeEnum.GL_SUCC_0);
    }

    /**
     * 自定义系统异常信息
     *
     * @param code
     * @param message 自定义消息
     * @param <T>
     * @return
     */
    public static <T> ResponseResult<T> systemException(Integer code, String message) {
        return packageObject(null, code, message);
    }
}

当然,这样的统一报文格式也不仅仅只处理异常返回,正常的数据报文格式也可以通过该对象来进行统一封装!

本文内容从实用的角度给大家演示了,如何在日常工作中编写通用的数据校验逻辑,希望能对大家有所帮助!

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-06

复杂系统如何在不停机升级同时保持稳定?你必须考虑以下几个点...

背景

在互联网行业,线上服务的升级更新可谓家常便饭。据统计,在过去的一个季度中闲鱼工程师们执行了千余次发布,总计更新的代码数量超过百万行。

这些发布中,有一些可能只更新了几行代码,而有一些可能执行了整个集群的迁移升级。而无论这些变更的影响面有多大,我们都必须保证线上服务的可用性,用户无感知。本文将以闲鱼搜索服务的迁移升级为例,向大家介绍其背后的技术方案。

闲鱼搜索服务基本架构

闲鱼的底层搜索服务由查询规划服务 Search Planner、查询理解服务 Query Planner、打分排序服务 Rank Service 以及搜索引擎 Heaven Ask 3 所组成。它们之间的相互调用关系如下图所示:

可以看到,整个搜索服务是由多个相互独立的微服务所构成的。不同的微服务之间相互隔离,通过预先向外暴露的接口提供服务。所有的微服务最终通过 Search Planner 收口,对外提供统一、完整的搜索能力。

在底层搜索服务之上,还有业务逻辑层和接入网关层,具体架构在此不再赘述。用户的搜索请求先通过网关层转发给逻辑层处理,再向底层搜索服务发起搜索请求。这条请求链上包含数十个集群,调用深度达到两位数,整个过程中提供服务的服务器数量可能有成百上千。

对于这样一个复杂的系统,升级过程显然无法一蹴而就。好消息是各个微服务之间合理的解耦合给升级工作带来了很大的便利,有效避免牵一发动全身而导致无从下手,使我们可以分门别类地处理升级问题。

  • 注1:Search Planner 是一个基于函数式、服务化、可视化、并行化开发框架所构建的搜索服务网关层。
  • 注2:Query Planner 的主要作用是理解用户输入,然后对搜索词进行算法优化。最终获得更好的搜索召回结果。
  • 注3:Rank Service 是实时打分排序服务,它的作用是根据多维度的特征对搜素引擎召回的海选结果进行算法打分。分数越高的商品就越有机会出现在搜索结果的前列。
  • 注4:Heaven Ask 3 (问天3)是阿里巴巴研发的一款稳定高效、功能强大的搜索引擎。为阿里集团包括淘宝、天猫在内的核心业务提供搜索服务支持。

保持兼容

开始升级之前,我们首先需要确认被升级的服务是否保持了向前与向后兼容性。保持兼容不仅减少了工作量,也减少了升级所导致的故障风险。

为了尽量避免升级导致的不兼容,我们可以总结一些开发原则:

  • 远程过程调用(RPC)需要能够忽略未知参数,并且允许缺失参数。
  • 如果需要删除已有参数,需要与所有依赖方确认。可以先将参数标记为 Deprecated 而不是直接移除。
  • 使用参数时,区分缺省值和缺失值。
  • 如果接口无法保持兼容,则创建新接口代替旧接口。不要破坏旧接口的兼容性。

在升级时,先升级那些没有外部依赖的服务。等到被依赖方升级完毕之后,再去升级依赖方。确定了每一个服务的升级顺序之后,我们再根据服务的实际情况确定升级方案。

无状态服务升级

正式进入升级流程,我们首先关注搜索链路中的被设计成无状态服务的部分,例如用于处理业务逻辑的 Java 微服务、用于处理查询逻辑的 Search Planner 等。它们的共同特点是,每个请求处理完毕之后,关于该次请求的资源即被释放。不同的请求之间没有相互依赖和时序要求。同一个无状态服务内不同的机器节点是完全等价的。

无状态服务的特点使得它们很容易通过水平扩展来动态扩缩容。因此在保证兼容的前提下,它们的升级流程相对通用并且简单:

  1. 根据服务最小可用度决定分批数。
  2. 选取一批待更新的容器,停止服务。
  3. 批量升级容器、更新镜像。
  4. 等待这一批容器全部恢复服务后,继续更新下一批容器。

一般来说我们可以通过把状态存储在消息队列、缓存、数据库或者其它外部中间件中来达成服务的无状态。把服务设计成无状态的好处显而易见:升级时不需要分配额外的机器资源,升级速度快,变更代价小,因而可以支持频繁的迭代更新。但是,这种设计也给状态访问和更新带来了额外的开销,在某些性能敏感的场合可能是不适用的。

有状态服务升级

我们继续关注有状态的部分。有状态服务升级的麻烦之处在于,状态的存储、恢复、转移往往由服务根据实际情况单独设计(或者根本没有设计),因而升级较为困难。我们可以简单列举一些相对通用的有状态服务升级可选方案。

  • 接入层网关提供热更新的能力(例如 Nginx),把状态的保持隔离在接入层内部。适合需要长时间保持状态的场景。
  • 渐进更新,新请求逐步切换到新服务上处理,旧服务处理完存量请求后销毁。适合短时间保持状态的场景(例如游戏服务、实时音视频通讯服务)。
  • 创建全新的服务副本,通过数据双写保持新旧服务状态一致,逐步用新服务取代旧服务。

在闲鱼搜索的架构中,搜索引擎本身提供的虽然是无状态服务,但是引擎内部保存了用于处理索引分区,增量进度的各种状态。最终使用的升级方案如下:

  1. 使用新版本镜像创建一个完全独立的新引擎。
  2. 新旧引擎全量数据同步。
  3. 增量数据同时向新旧引擎发送。
  4. 新引擎上线,逐步扩大承接流量的比例。
  5. 旧引擎不再承接流量后下线。

和无状态服务的升级相比,这种方式不仅额外使用了一倍的机器资源,而且每次升级都需要做一次复杂而繁琐的服务配置。如果服务本身不是无状态的,还需要自行编码实现切流逻辑,保证同一个用户的请求能够落到同一个集群上。整体升级成本较为昂贵,只适合更新频率非常低的服务。如果服务的更新频率较高,则应该根据服务的实际情况设计实现升级成本更低的方案。

服务发现

在升级过程中,服务发现机制承担着重要作用。它为我们提供了以下功能:

  • 保证分布式一致性
  • 服务优雅上下线
  • 负载均衡
  • 流量调控与请求降级
  • 同机房优先调度
  • 跨机房容灾调度

服务发现是流量调控的总阀门。一个成熟稳定的服务发现机制不仅可以有效避免发布导致的请求成功率抖动,也为发生异常时快速回滚止血提供了保证。

风险防控

对搜索链路的每一个集群按照依赖顺序进行服务升级、挂载、切流无疑是高危操作,稍有不慎就可能引起线上故障。因此,我们按照阿里巴巴安全生产三板斧原则对升级流程进行了梳理:

  • 可监控:重要链路的重要指标均提前保证监控覆盖。例如请求总量,请求成功率,请求响应时长等等。确保重大问题可以通过监控指标及时发现。
  • 可灰度:任何变更都不允许未经灰度直接全量发布到线上。对于无状态服务,我们一般通过调整服务发现中的权重或者调整机器比例来完成灰度放量。对于部分不能随机灰度的情形,我们设计了按用户分批放量的机制。
  • 可回滚:变更系统提供了通用的一键回滚能力,但并非是最快的方式。在很多情况下,我们在执行变更前就做好了把待更新的机器或集群在服务发现上重新挂载或移除的准备,从问题发现到恢复的时间基本是秒级的。

总结

综上所述,复杂系统不停机升级的原则和流程可以概括如下:

  1. 服务间解耦与隔离,确保单次升级的范围和影响可控。
  2. 根据兼容性和依赖关系决定服务的升级顺序。
  3. 根据服务是否无状态决定升级方式。
  4. 提前准备好监控和回滚方案,灰度升级。

闲鱼搜索服务升级的整个执行过程经历了两个月的时间。这其中我们既保证了用户无感知,线上服务稳定运行,也保证了与我们合作开发的算法团队以及其他工程团队的正常开发不受影响。

在实际执行的过程中,我们还遇到了很多细节上的问题。例如创建新服务时未能提前合理预估预算需求,导致升级过程中不断挪借预算,拆东墙补西墙。又比如异地多活部署带来的延迟问题迫使服务保持单元化,给升级过程中的流量调控工作带来了很多挑战。这些暴露的问题也为我们继续完善架构和方案提供了指引。

作者:闲鱼技术
转载自公众号
链接:https://mp.weixin.qq.com/s/Vc...
查看原文

赞 0 收藏 0 评论 0

风平浪静如码 发布了文章 · 2020-12-04

Java经典笔试题:可以手写一个ArrayList的简单实现吗?

面试官Q1:可以手写一个ArrayList的简单实现吗?

我们都知道ArrayList是基于数组实现,如果让你实现JDK源码ArrayList中add()、remove()、get()方法,你知道如何实现吗?这一节,我们不看源码,我们想想如何简单的实现ArrayList几个基本方法?

确定数据结构

我们知道,ArrayList中无论什么数据都能放,是不是意味着它是一个Object类型,既然是数组,那么是不是Object[]数组类型的?所以我们定义的数据结构如下:

private Object[] elementData;  
private int size;

设置自定义的MyArrayList的长度为10

public MyArrayList(){
         this(10);
     }
 
     public MyArrayList(int initialCapacity){
         if(initialCapacity<0){
            try {
                 throw new Exception();
             } catch (Exception e) {
                e.printStackTrace();
            }
        }
        elementData = new Object[initialCapacity];
    }

有了存放数据的位置,接下来,我们想想如何将数据放入数组?

添加数据

public void add(Object obj){
        elementData[size++]=obj;
}

每添加一个元素,size就会自增1,我们定义的数组长度为10,当我们添加到11个元素的时候,显然没有地方存放新添加的数据,这个时候我们需要对数组进行扩容处理

对上面代码做如下修改:

 public void add(Object obj){
         if(size==elementData.length){
            //创建一个新的数组,并且这个数组的长度是原数组长度的2倍
             Object[] newArray = new Object[size*2];
            //使用底层拷贝,将原数组的内容拷贝到新数组
             System.arraycopy(elementData, 0, newArray, 0, elementData.length);
            //并将新数组赋值给原数组的引用
             elementData = newArray;
         }
        //新来的元素,直接赋值
        elementData[size++]=obj;
}

用一张图来表示就是这样的:

查询数据

接着我们看一下删除的操作。ArrayList支持两种删除方式:

  • 按照下标删除
  • 按照元素删除,这会删除ArrayList中与指定要删除的元素匹配的第一个元素

对于ArrayList来说,这两种删除的方法差不多,都是调用的下面一段代码:

 public void remove(int index){
         //删除指定位置的对象
         //a b d e
         int numMoved = size - index - 1;
         if (numMoved > 0){
             System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
         }
         elementData[--size] = null; // Let gc do its work
}

其实做的事情就是两件:

  • 把指定元素后面位置的所有元素,利用System.arraycopy方法整体向前移动一个位置
  • 最后一个位置的元素指定为null,这样让gc可以去回收它

用图表示是这样的:

指定位置添加数据

把从指定位置开始的所有元素利用System,arraycopy方法做一个整体的复制,向后移动一个位置(当然先要用ensureCapacity方法进行判断,加了一个元素之后数组会不会不够大),然后指定位置的元素设置为需要插入的元素,完成了一次插入的操作。

public void add(int index,Object obj){
        ensureCapacity();  //数组扩容
        System.arraycopy(elementData, index, elementData, index + 1,
                 size - index);
        elementData[index] = obj;
        size++;
}

用图表示这个过程是这样的:

总结一下

从上面的几个过程总结一下ArrayList的特点:

  • ArrayList底层以数组实现,是一种随机访问模式,通过下标索引定位数据,所以查找非常快
  • ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已(这里指的末尾添加数据)
  • 当删除元素的时候,涉及到一次元素复制移位,如果要复制的元素很多,那么就会比较耗费性能
  • 当插入元素的时候,涉及到一次元素复制移位,如果要复制的元素很多,那么就会比较耗费性能

因此,ArrayList比较适合顺序添加、随机访问的场景。

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 1 收藏 1 评论 0

风平浪静如码 发布了文章 · 2020-12-03

三分钟快速解析Kubernetes微服务监控体系

监控系统是运维体系乃至整个软件产品生命周期中最重要的一环,完善的监控可以帮助我们事前及时发现故障,事后快速追查定位问题。而在以微服务为代表的云原生架构体系中,系统分为多个层次,服务之间调用链路复杂,系统中需要监控的目标非常多,如果没有一个完善的监控系统就难以保证整体服务的持续稳定。

监控对象及分层

在实际场景中监控系统按照监控的对象及系统层次结构,从底向上可以依次划分为基础层、中间层、应用层、业务层等多个层面的监控。具体可如图所示:

基础层监控就是对主机服务器(包括宿主机、容器)及其底层资源进行监控,以保证应用程序运行所依赖的基础环境的稳定运行。基础层监控主要有两个方向:

  • 资源利用:是对像I/O利用率、CPU利用率、内存使用率、磁盘使用率、网络负载等这样的硬件资源进行监控。避免因应用程序本身或其它特殊情况引起的硬件资源耗尽而出现的服务故障。
  • 网络通信:是对服务器之间的网络状态进行监控。网络通信是互联网的重要基石,如果主机之间的网络出现如延迟过大、丢包率高这样的网络问题,将会严重影响业务。

需要说明的是,在基于Kubernetes容器化技术的新型云原生基础设施中,基础层的监控不仅要对宿主机本身进行监控,也要对Kubernetes集群状态及其容器资源使用情况进行监控。这在后面我们构建基于Kubernetes的基础层监控体系时将会具体介绍。

中间层监控主要是指对诸如Nginx、Redis、MySQL、RocketMQ、Kafka等应用服务所依赖的中间件软件的监控,它们的稳定也是保证应用程序持续可用的关键。一般来说特定的中间件软件都会根据自身特点构建针对性的监控体系。

应用层监控这里就是指对业务性服务程序的监控,一般来说我们对应用程序监控的关注点主要体现在以下几个方面:

  • HTTP接口请求访问。包括接口响应时间、吞吐量等;
  • JVM监控指标。对于Java服务,还会重点关注GC时间、线程数、FGC/YGC耗时等JVM性能相关的指标;
  • 资源消耗。应用程序部署后会消耗一定的资源,例如应用程序对内存、CPU的消耗情况;
  • 服务的健康状态。例如当前服务是否存活,运行是否稳定等;
  • 调用链路。在微服务架构中,由于调用链路变长,还需要重点监控服务之间的调用关系和调用情况,避免局部上下游服务之间的链路故障引发系统全局性雪崩;

业务层监控也是监控系统所关注的一个重要内容,在实际场景中如果你只是让应用程序稳定运行那肯定是远远不够的。因此,我们常常会对具体业务产生的数据进行监控,例如网站系统所关注的PV、UV等参数;后端如交易之类的系统我们则会关注订单量、成功率等。

业务指标也是体现系统稳定性的核心要素。任何系统,如果出现了问题,最先受到影响的肯定是业务指标。对于核心业务指标的设定因具体的业务和场景而异,所以对于业务层的监控需要构建具备业务特点的业务监控系统。

常见的监控指标类型

在指标类监控系统中,通过统计指标可以感性地认识到整个系统的运行情况。出现问题后,各个指标会首先出现波动,这些波动会反映出系统是那些方面出了问题,从而可以据此排查出现问题的原因。下面我们分别来看下统计指标到底有哪些类型,以及常见的统计指标都有哪些,它是我们进一步理解指标类监控系统的基础。

从整体上看,常见的Metrics指标类型主要有四类:

  • 计数器(Counter)
  • 测量仪(Gauge)
  • 直方图(Histogram)
  • 摘要(Summary)

1、计数器(Counter)

计数器是一种具有累加特性的指标类型,一般这个值为Double或者Long类型。例如常见的统计指标QPS、TPS等的值就是通过计数器的形式,然后配合一些统计函数计算得出的。

2、测量仪(Gauge)

表示某个时间点,对某个数值的测量。测量仪和计数器都可以用来查询某个时间点的固定内容的数值,但和计数器不同,测量仪的值可以随意变化,可以增加也可以减少。比如获取Java线程池中活跃的线程数,使用的是ThreadPoolExecutor中的getActiveCount方法;此外,还有比较常见的CPU使用率、内存占用量等具体指标都是通过测量仪获取的。

3、直方图(Histogram)

直方图是一种将多个数值聚合在一起的数据结构,可以表示数据的分布情况。比如以常见的响应耗时举例,可以把响应耗时数据分为多个桶(Bucket),每个桶代表一个耗时区间,例如0~100毫秒、100~500毫秒,以此类推。通过这样的形式,可以直观地看到一个时间段内的请求耗时分布,这将有助于我们理解耗时情况分布。

4、摘要(Summary)

摘要与直方图类似,表示的也是一段时间内的数据结果,但是摘要反应的数据内容不太一样。摘要一般用于标识分位值,分位值其实就是我们常说的TP90、TP99等。例如有100个耗时数值,将所有的数值从低到高排列,取第90%的位置,这个位置的值就是TP90的值,如果这个桶对应的值假设是80ms,那么就代表小于等于90%位置的请求都≤80ms。

Kubernetes微服务监控体系

前面我们从整体上描述了监控系统分层以及理解指标类监控系统所需要掌握的几类常见的指标类型。接下来我们重点探讨基于Kubernetes的微服务监控体系。

从监控对象及系统分层的角度看,监控系统需要监控的范围是非常广泛的,但从微服务监控的角度来说,如果你的微服务部署完全是基于Kubernetes云原生环境的,那么我们需要关注的监控对象主要就是Kubernetes集群本身以及运行其中的微服务应用容器。例如对容器资源使用情况,如CPU使用率、内存使用率、网络、I/O等指标的监控。

当然,这并不是说像基础层的物理机、虚拟机设备或者中间层软件的监控我们不需要关注,只是这部分工作一般会有专门的人员去维护。而如果使用的是云服务,那么云服务厂商大都已经为我们提供了监控支持。此外,对于基础物理层及大部分中间软件的监控并不是本文所要表达的重点,所以也就不再做过多的实践,大家对此有个全局的认识即可。

而回到以Kubernetes为载体的微服务监控体系,虽然曾经Kubernetes项目的监控体系非常复杂,社区中也有很多方案。但是这套体系发展到今天,已经完全演变成了以Prometheus项目为核心的一套统一方案。在本节的内容中我们就将演示如何围绕Prometheus来构建针对Kubernetes的微服务监控系统。

1、Prometheus简介

经过行业多年的实践和沉淀,目前监控系统按实现方式主要可以分为四类:

1)、基于时间序列的Metrics(度量指标)监控;

2)、基于调用链的Tracing(链路)监控;

3)、基于Logging(日志)的监控;

4)、健康性检查(Healthcheck)。而在上述几种监控方式中Metrics监控是其中最主要的一种监控方式。

简单理解Metrics的表现形式,就是在离散的时间点上产生的数值点[Time,Value],由某个指标组成的一组[Time,Value]数值点序列也被称为时间序列,所以Metrics监控也常常被称为时间序列监控。

如上所述,我们简单阐述了指标系统的基本特点,而接下来要介绍的Prometheus就是一款基于时间序列的开源Metrics类型的监控系统,它可以很方便地进行统计指标的存储、查询和告警。从整体上看Prometheus的系统结构,如下图所示:

从上图中可以看出,Prometheus工作的核心,主要是使用Pull(拉取)的模式去收集被监控对象的Metrics数据(监控指标数据),然后由Prometheus服务器将收到的指标数据进行聚合后存储到TSDB(时间序列数据库,例如OpenTSDB、InfluxDB)中,以便后续根据时间自由检索。

有了这套核心机制,Prometheus剩下的组件就主要是用来配合这套机制运行的了。比如PushGateway,它可以允许被监控对象以Push的方式向Prometheus推送Metrics数据。而Alertmanager,则可以根据Metrics信息灵活地设置报警。

此外,Prometheus还提供了一套完整的PromQL查询语言,通过其提供的HTTP查询接口,使用者可以很方便地将指标数据与Grafana(可视化监控指标展示工具)结合起来,从而灵活地定制属于系统自身的关键指标监控Dashboard(看板)。

2、Prometheus Operator安装部署

前面我们简单介绍了Prometheus监控系统的基本原理,接下来的内容将以实操的方式演示如何使用Prometheus构建一套针对Kubernetes集群的微服务监控体系。

在实际的应用场景中,针对不同的监控对象Prometheus的部署方式也会有所不同。例如要监控的对象是底层的物理机,或者以物理机方式部署的数据库等中间件系统,那么这种情况下一般也会将Prometheus监控系统的部署环境放置在物理机下。

而如果针对的是Kubernetes集群的监控,那么现在主流的方式是采用Promethues-Operator将Promethues部署到Kubernetes集群之中,这样能以更原生的方式实施对Kubernetes集群及容器的监控。这里所说的Promethues-Operator 是指专门针对Kubernetes的Promethues封装包,它可以简化Promethues的部署和配置。

接下来我们具体演示如何通过Promethues-Operator在Kubernetes中快速安装部署Promethues,具体步骤如下:

1)、安装Helm

在本次安装过程中,将使用到Kubernetes的包管理工具Helm。Helm是Kubernetes的一种包管理工具,与Java中的Maven、NodeJs中的Npm以及Ubuntu的apt和CentOS的yum类似。主要用来简化Kubernetes对应用的部署和管理。

首先从Github下载相应的Helm安装包,具体命令参考如下:

#找到Github中Helm相关的发布包,参考链接如下
https://github.com/helm/helm/releases

#确定好相关版本后,将具体安装版本下载至某个安装了kubectl的节点 
wget https://get.helm.sh/helm-v3.4.0-rc.1-linux-amd64.tar.gz

解压,并将下载的可执行Helm文件拷贝到文件夹/usr/local/bin下,命令如下:

tar -zxvf helm-v3.4.0-rc.1-linux-amd64.tar.gz 
#将下载的可执行helm文件拷贝到文件夹/usr/local/bin下
mv linux-amd64/helm /usr/local/bin/

之后执行helm version,如果能看到Helm版本信息,就说明Helm客户端安装成功了,具体如下:

$helm version
version.BuildInfo{Version:"v3.4.0-rc.1", 
GitCommit:"7090a89efc8a18f3d8178bf47d2462450349a004", 
GitTreeState:"clean", GoVersion:"go1.14.10"}

安装完Helm客户端后,由于一些公共Kubernetes包是在远程仓库中管理的,所以还需要添加helm charts(Helm中的Kubernetes安装包又叫charts)官方仓库,命令如下:

$helm repo add stable https://charts.helm.sh/stable

查看本地helm仓库是否添加成功,命令如下:

$helm repo list
NAME      URL                          
stable    https://charts.helm.sh/stable

此时,查看Helm仓库就能看到各种组件的charts列表了,命令效果如下:

$helm search repo stable

NAME                              CHART VERSION   APP VERSION     DESCRIPTION                                       
stable/acs-engine-autoscaler      2.1.3           2.1.1           Scales worker nodes within agent pools            
stable/aerospike 
...

如上所示,此时通过“helm search”命令就可以查看到各种stable版本的Kubernetes安装包了!

2)、Helm搜索Prometheus-Operator安装包

在具体安装Prometheus-Operator之前,我们先用“helm”命令搜索Prometheus相关的charts包,命令如下:

$ helm search repo prometheus

具体搜索结果如下图所示:

如上图所示,我们可以看到Helm仓库中可以搜索到版本为0.38.1的“stable/prometheus-operator”的安装包。接下来就可以通过helm具体安装了!

3)、Helm安装Prometheus-Operator监控系统

接下来啊,通过Helm具体安装prometheus-operator监控系统,命令如下:

#创建k8s名称空间
kubectl create ns monitoring

#通过helm安装promethues-operator监控系统
helm install promethues-operator stable/prometheus-operator -n monitoring

执行安装命令后,输出结果如下:

WARNING: This chart is deprecated
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
manifest_sorter.go:192: info: skipping unknown hook: "crd-install"
NAME: promethues-operator
LAST DEPLOYED: Mon Oct 26 10:15:45 2020
NAMESPACE: monitoring
STATUS: deployed
REVISION: 1
NOTES:
*******************
*** DEPRECATED ****
*******************
* stable/prometheus-operator chart is deprecated.
* Further development has moved to https://github.com/prometheus-community/helm-charts
* The chart has been renamed kube-prometheus-stack to more clearly reflect
* that it installs the `kube-prometheus` project stack, within which Prometheus
* Operator is only one component.

The Prometheus Operator has been installed. Check its status by running:
  kubectl --namespace monitoring get pods -l "release=promethues-operator"

Visit https://github.com/coreos/prometheus-operator for instructions on how
to create & configure Alertmanager and Prometheus instances using the Operator.

执行完安装命令后,查看具体的Kubernetes Pods信息,命令如下:

$ kubectl get po -n monitoring

NAME                                                     READY   STATUS    RESTARTS   AGE
alertmanager-promethues-operator-promet-alertmanager-0   2/2     Running   0          5m42s
prometheus-promethues-operator-promet-prometheus-0       3/3     Running   1          5m31s
promethues-operator-grafana-5df74d9cb4-5d475             2/2     Running   0          6m53s
promethues-operator-kube-state-metrics-89d8c459f-449k4   1/1     Running   0          6m53s
promethues-operator-promet-operator-79f8b5f7ff-pfpbl     2/2     Running   0          6m53s
promethues-operator-prometheus-node-exporter-6ll4z       1/1     Running   0          6m53s
promethues-operator-prometheus-node-exporter-bvdb4       1/1     Running   0          6m53s

如上所示,可以看到Prometheus监控系统相关的组件都以Pod的方式运行在了Kubernetes集群中。

Prometheus监控效果演示

通过前面的实际操作,我们通过Helm的方式已经将Prometheus Operator安装包部署在了Kubernetes集群之中。而此时的Prometheus实际上就已经开始发挥作用,并采集了各类Kubernetes的运行指标信息。可以通过Promethues内置的监控界面对此进行查看,具体步骤如下:

查看Kubernetes中查看内置监控界面所在的Pod节点,命令如下:

kubectl -n monitoring get svc

使用nodeport方式将promethues-operator内置界面服务暴露在集群外,并指定使用30444端口,命令如下:

kubectl  patch svc promethues-operator-promet-prometheus -n monitoring -p '{"spec":{"type":"NodePort","ports":[{"port":9090,"targetPort":9090,"nodePort":30444}]}}'
service/promethues-operator-promet-prometheus patched

此时在浏览器中输入Pod节点所在的宿主机IP+端口地址,URL示例如下:

http://10.211.55.11:30444/graph

此时就可以看到Promethues内置的监控可视化界面了,效果如下图所示:

而如果此时以PromeQL的方式查看一个具体指标,以“http_requests_total”为例,展示效果如图所示:

由此说明,此时Promethues监控系统已经开始运行,并采集了相关Metrics指标数据!

Grafana可视化监控系统

Grafana是一个强大的跨平台的开源度量分析和可视化工具,可以将采集的指标数据进行定制化的图形界面展示,经常被用作为时间序列数据和应用程序分析的可视化。Grafana支持多种数据源,如InfluxDB、OpenTSDB、ElasticSearch以及Prometheus。

前面我们在Kubernetes中安装部署Prometheus-Operator时,实际上Grafana就已经被集成并运行了,可以通过Kubernetes的相关命令查询Grafana的实际运行Pod,并将其Web端口对外进行暴露,具体如下:

#查看服务节点信息
kubectl -n monitoring get svc

#使用nodeport方式将promethues-operator-grafana暴露在集群外,指定使用30441端口
kubectl  patch svc promethues-operator-grafana -n monitoring -p '{"spec":{"type":"NodePort","ports":[{"port":80,"targetPort":3000,"nodePort":30441}]}}'

需要注意由于Grafana的应用运行的默认端口为80,为避免实验环境冲突,这里映射时将目标容器端口指定为3000,并最终将节点端口映射为30441。完成后,浏览器输入URL:

#IP地址为映射命令执行时所在的节点
http://10.211.55.11:30441

如果映射正常,此时会返回Grafana可视化图形界面的登录界面,如图所示:

这里缺省登录账号密码为:admin/prom-operator。输入后可进入Grafana主界面如下图所示:

可以看到部署完成的Grafana已经默认内置了许多针对Kubernetes平台的企业级监控Dashboard,例如针对Kubernetes集群组件的“Kubernetes/API server”、“Kubernetes/Kubelet”,以及针对Kubernetees计算资源的“Kubernetes/Compute Resources/Pod”、“Kubernetes/Compute Resources/Workload”等等。

这里我们找一个针对Kubernetes物理节点的“Nodes”监控Dashboard,点击打开后看到的监控效果如下图所示:

上图所示的Dashboard中展示了Kubernetes集群所在的各物理节点CPU、负载、内存、磁盘I/O、磁盘空间、网络传输等硬件资源的使用情况。从这些丰富的视图可以看出Grafana强大的监控指标可视化能力!

后记

本文给大家从理论到实践简单介绍了Kubernetes微服务监控体系的构建步骤,希望能够对大家学习Kubernetes有所帮助。目前以Kubernetes为代表的容器化技术已经成为现代软件应用发布的标准方式,作为一名普通研发人员,对Kubernetes的学习将有助于我们更深入的理解整体软件系统的构建原理,也是我们进阶提升必不可少的知识储备!

写在最后

欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。

觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 11 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-10-14
个人主页被 1.2k 人浏览