Frances Hu's Blog

Born to be wild!


  • Startseite

  • Archiv

  • Tags

《深入理解计算机系统》第5周

Veröffentlicht am 2020-10-25

作为开发者需要理解存储器层次结构,以及了解系统是如何将数据在存储器层次结构中移动,这样在编写程序时可以利用这些特性来提升性能。其中最主要的就是局部性原理,具有良好局部性的程序会更倾向于从较高层次访问数据,减少CPU访问时间。

  • 将注意力集中在内循环上,大部分计算和内存访问都发生在这里;
  • 通过按照数据对象存储在内存中的顺序,以步长为1的来读取数据,使得程序空间局部性最大;
  • 一旦从存储器中读入了一个数据对象,就尽可能多地使用它,从而使得程序中的时间局部性最大;

这一章我是结合和CMU的课程视频来学习的,相比前几章来说,在看书之前已经了解了哪些重点概念需要关注,个人感觉效果还不错。

首先从基本的存储技术学习:

RAM 随机访问存储器:

  • SRAM,静态随机访问存储器,常用来做高速缓存,可以在CPU芯片上也可以在片下。将每个位都存储在一个双稳态存储器单元里,每个单元由一个六晶体管电路实现。只要有电就会永远保持它的值,即使有干扰来扰乱电压,消除后也会立即恢复到稳定值。
  • DRAM,动态随机访问存储器,常用来作为主存以及图形系统的帧缓冲。每个位存储为对一个电容的充电,对干扰非常敏感,当电容的电压被扰乱后就永远不会恢复了。
    • 两者相比,只要供电SRAM就会保持不变,与DRAM不同,不需要刷新。SRAM的存取也比DRAM快得多,但是SRAM比DRAM单元使用更多的晶体管,因此密集度低,更贵且功耗更大。
      传统DRAM,二维阵列,读取时先RAS(Row Access Strobe)请求再CAS(Column Access Strobe)请求。
  • 内存模块,DRAM芯片封装在内存模块中,插入主板的扩展槽,以64位为块传送数据到呢欧村控制器和从内存控制器传出数据。
  • 增强的DRAM:
    • 快页模式,允许对同一行连续地访问可以直接从行缓冲区得到服务。
    • 扩展数据输出DRAM,允许各个CAS信号在时间上靠得更紧密一些。
    • 同步DRAM,能够比一部的存储器更快输出单元内容。
    • 双倍数据速率同步DRAM,常见的DDR2 DDR4 DDR8等
    • 视频RAM,用在图形系统的帧缓冲区中。
      • 1)VRAM输出是通过依次对内部缓冲区的整个内容移位得到;
      • 2)VRAM允许对内存并行读写。
  • 非易失性存储器, 断电后依然能保存数据。 ROM, PROM, EPROM,flash memory等
    访问主存,通过总线的共享电子电路在处理器和DRAM主存之间传递数据。

磁盘存储器:从磁盘上读取数据时间为ms级,比DRAM慢10万倍,比SRAM慢100万倍。
磁盘的构造相关内容CMU的课程视频上讲解的比较清晰,所以看书时主要关注在于磁盘的访问。

CPU通过内存映射I/O技术来发送命令,磁盘接受到命令后,将逻辑块号翻译成扇区地址,读取该扇区内容。然后将内容直接传到主存,不需要CPU的干涉,即DMA传送。当传送完成后,磁盘控制器通过给CPU发送一个中断信号来通知。

SSD固态硬盘,由半导体存储器构成,没有移动的部件,因而随意访问时间比旋转磁盘快,能耗更低且结实。不过缺点是反复写后,闪存块会磨损,通过平均磨损逻辑来将擦除平均到所有块上来最大化每块的寿命。

然后是局部性原理:空间局部性和时间局部性。现代计算机系统各个层次都有利用到局部性原理,如高速缓存等(CPU芯片、操作系统、分布式文件系统和万维网中都使用了缓存)。

最后是高速缓存相关,其结构可以用四元组(S,E,B,m)来描述,容量大小C=SEB。其结构使得它能够通过简单地检查地址位,找到请求字(类似哈希表)。1)组选择;2)行匹配;3)字抽取。

按照类型可以分为直接映射高速缓存、组相联高速缓存、全相联高速缓存。

需要注意的是,即使程序具备良好的空间局部性,高速缓存中也有足够的空间存放块,但是还可能因为映射到相同组产生抖动现象,即高速缓存反复地加载和驱逐相同的高速缓存块的组。 此时可以通过数据填充将其映射到不同的组来消除抖动冲突不命中。

高速缓存写操作:

  • 命中时,向更低一层写时分为两种:直写和写回。直写简单但是每次写都会引起总线流量;写回尽可能推迟更新,只有当替换算法驱逐该块时才写,由于局部性显著减少总线流量,但增加了复杂性。
  • 不命中时,有写分配和非写分配两种。写分配需要加载相应第一层的块到高速缓存中,然后更新,试图利用局部性原理。非写分配避开高速缓存,直接把这个字写入到下一层。直写高速缓存通常是非写分配的,写回高速缓存是写分配的。

对开发者来说,在设计程序时可以心里默认采用使用写回合写分配的高速缓存模型,在高层次开发程序展示良好的空间和时间局部性。原因:

  • 通常较长的传送时间,存储器层次结构中较低的缓存更可能使用写回;
  • 逻辑电路密度提高,写回的高复杂性也越来越不成阻碍;
  • 写回写分配试图利用局部性,和处理读的方式对称;

《深入理解计算机系统》第4周

Veröffentlicht am 2020-10-18

上周第三章已经看完,因此这周选择了加看第五章优化程序性能,通常编译器可以代替我们进行程序优化,例如循环展开、代码移动等。但是由于函数存在编译器无法感知的副作用,往往还需要程序员在编写代码时注重效率。

一些看上去无足轻重的代码段可能隐藏着渐进低效率(asymptotic inefficiency),在数据量小时测试可能无法发现问题,但部署环境后代码会突然成为瓶颈。有经验的程序员工作的一部分就是避免引入这样的渐进低效率。

比如在循环中止条件我们经常会用到 strlen(s)来作为循环中止条件,而strlen所需时间和字符串长度N成正比,如果每次循环迭代前都判断一次,那么循环的整体运行时间会是NN。此时我们如果将字符串长度提前进行计算,再进行循环判断就会显著提升性能。书中实现对1048576字符进行测试,循环运行时间从17分钟降低到2毫秒, *50W倍左右的提升。

当前GCC版本会对整数运算执行重新结合,但不是总有好的效果,通常循环展开和并行地累计在多个值中是提高程序性能的更可靠的方法。

CPE Cycles Per Element,作为一种表示程序性能并指导我们改进代码的度量标准。可以帮助我们在更细节的级别上理解迭代程序的循环性能。

未经优化的代码通常效率比较低,简单地使用命令行选项就能显著地提高程序性能(超过两个数量级)。

代码移动,识别要执行多次(如循环内)但是计算结果不会改变的计算,将其移动到代码前不会多次求值的部分来提高性能。优化编译器会尝试进行代码移动,但是不能可靠地发现一个函数是否会有副作用。因此为了改进代码,程序员必须经常帮助编译器显示地完成代码移动。

过程调用会带来开销,而且妨碍大多数形式的程序优化。

尽量消除代码中不必要的内存读写,用寄存器来保存中间值来提升性能

要进一步提高性能,必须考虑利用处理器微体系结构的优化,也就是处理器用来执行指令的底层系统设计。要想充分提高性能,需要仔细分析程序,同时代码的生成也要针对目标处理器进行调整。

延迟界限(latency bound),在下一条指令开始之前,这条指令必须结束。当代码中的数据相关限制了处理器利用指令级并行的能力时,延迟界限能够限制程序性能。

吞吐量界限(throughput bound),刻画了处理器功能单元的计算能力,这个界限是程序性能的终极限制。

整体操作:

  • ICU从高速缓存中读取指令,通常会在当前正在执行的指令很早之前取指,才有足够的时间对指令进行译码并把操作发送给EU。
  • 当程序遇到分支时,通过分支预测猜测是否会执行分支,并且同时预测分支的目标地址。
  • 使用投机执行的技术,取出位于它预测的分支会调到的地方的指令,对指令进行译码,甚至在确定分支预测正确之前开始执行。
  • 如果分支预测错误的话,会将状态重新置于分支点的状态,接着往下执行。

功能单元的性能:由延迟、时间、容量来刻画。

  • 延迟latency,表示完成运算所需要的总时间;
  • 发射时间issue time表示两个连续的同类型运算之间需要的最小时钟周期数;
  • capacity表示能够执行该运算功能单元的个数。 其中除法不是完全流水线化的,发射时间=延迟 即在开始一条新运算之前,除法器必须完成整个除法,使得其成为一个相对开销很大的运算。

循环展开通过增加每次迭代计算的元素数量,减少循环的迭代次数。编译器可以很容易地执行循环展开,只要优化级别设置得足够高。GCC -O3就会执行。

提高并行性:

对于可结合和可交换的合并运算来说,通过将一组合并运算分割成两路或多路,最后再合并来提高性能。

重新结合变换打破顺序相关从而使性能提高到延迟界限之外的方法,减少计算中关键路径上操作的数量,更好地利用功能单元的流水线能力。 (大多数编译器不会对浮点运算重新结合,因为这些运算是不保证结合的)

其他一些制约程序在实际机器上性能的因素:

  • 寄存器溢出,并行度p超过了可用的寄存器数量,编译器会溢出,将某些临时值存放在内存中。此时再进行循环展开就没有效果,甚至会导致性能变差。
  • 分支预测错误处罚, 大约19个时钟周期,赌注很高。因此注意以下通用原则:
    • 不要过分关心可预测的分支;
    • 书写适合条件传送实现的代码

总结来说,优化程序性能的基本策略如下:

  • 高级设计: 选取合适的算法和数据结构,避免使用那些会产生渐进低效率的算法OR编码技术;
  • 基本编码原则:避免限制优化的因素,使编译器产生高效地代码。
  • 消除连续的函数调用:代码移动;
  • 消除不必要的内存引用: 引入临时变量;
  • 低级优化: 结构化代码以利用硬件功能;
    • 循环展开
    • 累积变量、重新结合等技术,提高指令级并行
    • 功能性的风格重写条件操作,使得编译器采用条件数据传送。

《深入理解计算机系统》第3周

Veröffentlicht am 2020-10-11

第三章主要是从汇编代码级来了解程序是如何运行,通过阅读汇编代码一方面可以理解编译器的优化能力帮助分析优化源码,另一方面可以了解程序存储运行控制信息的细节可以用来防御代码漏洞等。

有一定的汇编基础,所以这章比上章读起来稍微轻松一些,长假期间闲时顺手翻完。

主要记录了以下几个知识点:

首先是定义

  • PC,存储将要执行的下一条指令在内存中的地址
  • 整数寄存器,存储整数数据和指针,有的jicunq被用来记录某些重要的程序状态
  • 条件码,保存最近执行的算术或逻辑指令的状态信息,用来实现控制或数据流中的条件变化
  • 向量寄存器用来存放一个或多个整数或浮点数值。
  • 一个X86-64的CPU中包含一组16个64位通用目的寄存器,用来存储整数数据和指针。其中%rsp是栈指针用来指明运行时栈的结束位置。

数据传输指令、入栈出栈数据指令、算术逻辑操作以及跳转指令之前都有一定的了解,这次补充了下乘法和除法的运算逻辑。

2个64位有符号/无符号整数相乘需要128位来表示,X86-64对128位数的操作提供有限的支持。

mulq(无符号数乘法)和imulq(补码乘法)都要求一个参数必须在寄存器%rax中,另一个作为指令的源操作数给出,乘积存放在寄存器%rdx(高64位)和%rax(低64位)中。

除法或取模运算,由单操作数除法指令来提供,idivl将寄存器%rdx(高64位)和%rax(低64位)中的128位数作为被除数,除数作为指令操作数给出,将商存储在%rax中,余数存储在%rdx中。

条件码通常不会直接读取,常用的三种方法:

  • 根据条件码的某种组合将一个字节设置为0/1;
  • 条件跳转到程序的其他部分;
  • 有条件地传送数据。 常用的条件码寄存器:
    • CF:进位标志,可用来检查无符号操作的溢出
    • ZF:零标志位
    • SF:符号标志
    • OF:溢出标志,最近的操作导致一个补码溢出

基于条件数据传送的代码会比基于条件控制转移的代码性能要好是因为:

首先,现代处理器通过pipelining重叠连续指令的步骤来获取高性能。

当机器遇到条件跳转指令时只有当分支条件求值完成后才能决定分支往哪走,分支预测逻辑可靠的情况下流水线中依旧会充满指令并行,但是当跳转错误预测时需要丢掉它为跳转指令后续所有指令已做的工作,浪费大约15-30个时钟周期,导致程序性能严重下降。

同条件跳转不同,处理器不需要预测结果就可以执行条件传送,处理器只是读源值,检查条件码,然后要么更新目的寄存器要么保持不变。

不过条件传送也不总是会提高代码效率,需要权衡复杂计算开销和分支预测错误开销来判断具体使用哪种。

过程是软件中的一种重要抽象,要提供对过程的机器级支持需要处理传递控制、传递数据、分配和释放内存等机制。

C语言过程调用机制的一个关键特性在于使用了栈数据结构的后进先出的内存管理原则,每个过程在栈中都有自己的私有空间,多个未完成调用的局部变量不会相互影响。

C语言对数据引用不进行任何边界检查,局部变量和状态信息都存放在栈中。这两种情况结合在一起,对越界的数组元素写操作会破坏存储在栈中的状态信息,即常见的缓冲区溢出问题。

这种问题可以被用来攻击系统安全,将攻击代码包含在输入字符串中或者将一个指向攻击代码地址的指针覆盖返回地址等。

现代的编译器和操作系统实现了很多机制限制入侵者通过缓冲区溢出攻击获得系统控制。常见的机制:

  • 栈随机化:为了插入攻击代码,攻击者既要插入代码又要插入指向该代码的指针。这个指针也是攻击字符串的一部分,要产生这个指针需要知道字符串存放的栈地址。栈的位置在程序每次运行时都有变化使得改地址难以预测。在Linux中栈随机化已经成为标准行为,ASLR地址空间布局随机化。
  • 栈破坏检测:第二道防线是能够检测到何时栈被破坏,栈保护者机制通过canary金丝雀值是否被改变来判断,该值是程序每次运行随机产生的,如果被某个函数或操作改变那么异常中止该程序。
  • 限制可执行代码区域:消除攻击者向系统中插入可执行代码的能力。

《深入理解计算机系统》第2周

Veröffentlicht am 2020-09-27

本周主要学习第二章信息的表示和处理,数值处理部分相对比较枯燥.第一遍看到后面定理晕头转向的,后面看第二遍的时候将所有练习题都跟着动手完成,效果明显好了很多.

对于整数运算,表示数字的有限字长限制了可能值的取值范围,结果存在溢出可能.C语言中强制类型转换等一些规定可能会产生非直观的结果,需要注意.

浮点数计算只有有限的范围和精度,并且不遵守普遍的算数属性.

重新温顾的知识点:

大端/小端法

最低有效字节在前面的是小端法,最高有效字节在最前面的是大端法. (大多数Intel兼容机都只用小端法)

假设变量x=0x01234567在地址0x100处

 |0x100 0x101 0x102 0x103|
大端 |01 | 23 | 45 | 67 |
小端 |67 | 45 | 23 | 01 |
  • 网络传输二进制数据时通过网络标准转换来避免不同机器字节顺序的影响.
  • 反汇编代码阅读时需要考虑字节顺序.
  • C语言中强制类型转换/Union允许一种数据类型引用一个对象时需要考虑字节顺序.

有符号数和无符号数之间的转换

补码转换为无符号数时

T2U(x)=x (x>=0); T2U(x)=x+2^w(x<0,w位)

无符号数转换为补码时
U2T(u)=u (u<=TMax); U2T(u)=u-2^w(u>=TMax)

C语言中对同时包含有符号和无符号数的表达式处理时,会隐式的将有符号数强制转换为无符号数,对于标准运算符并无差异,但是遇到关系运算符时会导致问题.

如 -1 < 0U, 会先将-1转换为4294967295U 导致非直观结果出现.

扩充整数字长时, 补码数符号扩展,无符号数零位扩展.

无符号整数加法

x+y=x+y(正常) x+y=x+y-2^w (溢出时)

补码加法

x+y=x+y-2^w(正溢出), x+y=x+y(正常), x+y=x+y+2^w(负溢出)

无符号数乘法

xy=(xy) mod 2^w

补码乘法

xy=U2T((xy) mod 2^w)

浮点数

通过将数字编码为x*2^y的形式来近似地表示实数,最常见的浮点精度都float和double.

IEEE浮点通过符号s/尾数M/阶码E来表示,32位1+8+23,64位1+11+52,被编码的值有三种不同的情况:

  • 规格化的值, 阶码不全为0也不全为1
  • 非规格化的值,阶码域全为0
  • 特殊值, 阶码域全为1,小数域全为0时(正无穷,负无穷); 阶码域全为1,小数域非零(NaN)

四种舍入方式中默认的是Round-to-even,将数字向上或向下舍入使得结果的最低有效数字是偶数.(可以避免统计偏差)

《深入理解计算机系统》第1周

Veröffentlicht am 2020-09-20

第一章主要是以helloworld程序为切入点对计算机系统整体进行阐述,硬件和软件如何通过协同操作来运行应用程序。对于编译链接的流程和基本概念比较熟悉所以本章主要记录以下两点笔记:

Amdahl定律:

当对系统的某个部分进行加速时,整体性能的影响取决于该部分的重要性和加速程序。该定律主要用于并行计算领域预测多个处理器时的理论最大加速比,实际场景中程序往往并不能有效的利用多核,因为系统中不可避免的会存在一些需要串行访问的资源。因此在多核处理器中还要考虑如何降低串行计算部分的比例以及降低交互开销。

Concurrency和parallelism:

线程级并发:
单处理器系统中并发是通过进程执行切换来模拟出并发的效果,允许多用户同时与系统交互。随着多核处理器的出现减少了执行多个任务时模拟并发的需要,但是要求程序以多线程方式进行编写来高效地并行执行。

另外,超线程允许一个CPU执行多个控制流的技术,常规的处理器大约需要2w个时钟周期做线程切换,超线程处理器可以在单个周期的基础上决定执行哪个线程(FMT,通过拉长每个线程的平均执行时间来实现随时切换,英伟达和AMD的GPU中用的比较多)。现实中绝大多数程序并不会占用CPU的所有资源,超线程的引入主要就是为了更好得利用空闲资源。

指令级并行:

每条指令从开始到结束大约需要20+个时钟周期,但是处理器通过流水线技术,将指令划分为不同的阶段通过并行操作能够达到每个时钟周期2-4个指令的执行速率。

单指令多标量并行SIMD:

通过特殊的硬件来支持一条指令产生多个可以并行的操作。

这些应该都是本科学习过的知识点,重新温习了一遍。

设计模式之美第二周

Veröffentlicht am 2020-08-15

理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?

本文主要针对四大特性,结合实际代码,帮助我们了解每个特性存在的意义和目的,以及它们能解决哪些编程问题。

封装 Encapsulation

封装也叫作信息藏匿或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息或数据。

对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制。

如果对类中属性访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更灵活,但是从另一方面来说过度灵活意味着不可控。除此之外,类仅仅通过有限的方法暴露必要的操作,也提高类的易用性。调用者不需要了解太多背后的业务细节,用错的概率也会减少。

抽象 Abstraction

封装主要讲的是如何隐藏信息、保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者之需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

在面向对象编程中,我们常借助编程语言提供的接口类(Interface)或者抽象类(Abstract)这两种语法机制,来实现抽象。

抽象的意义,首先作为一种只关注功能不关注实现的设计思路,正好帮我们的大脑过滤掉许多非必要的信息。其次,抽象在代码设计中起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则、代码解耦等。

我们在定义类的方法时,也要有抽象思维,不要在方法定义中暴露太多的实现细节,以保证在某个时间点需要修改方法的实现逻辑时不用去修改其定义。

继承 Inheritance

为了实现继承特性,编程语言需要提供特殊的语法机制来支持,如Java中使用extends来实现继承,C++使用冒号,Python使用paraentheses()等。不过有些语言只支持单继承,如Java、PHP、C#、Ruby等,有些支持多重继承,如C++、python、Perl等。

继承存在的最大好处就是代码复用。不过过度使用继承层次过深过复杂,会导致代码可读性、可维护性变差。所以继承应该尽量少用,甚至不用。(多用组合少用继承)

多态 Polymorphism

多态能提高代码的可扩展性和复用性。除此之外多态也是很多设计模式、设计原则、编程技巧的代码实现基础。比如策略模式、基于接口而非实现编程、依赖倒置原则、里氏替换原则、利用多态去掉冗长的if-else语句等。

思考题

Java不支持继承的原因是多重继承存在副作用:钻石问题(菱形继承)

假设B和C都继承A,且都重写了A中同一方法,类D继承类B和类C,对于B、C重写的A中的方法,类D会继承哪一个会产生歧义。但是Java支持多接口实现,因为接口中的方法是抽象的,在实现接口时需要实现类自己实现,所以不会出现二义性问题。

理论三:面向对象比面向过程有哪些优势?面向过程过时了吗?

什么是面向过程编程与面向过程编程语言?

面向过程编程也是一种编程范式/风格,它以过程作为组织代码的基本单元。以数据与方法相分离为最主要的特点。面向过程风格是一种流程化的编程风格,通过拼接一组顺序执行的方法来操作数据完成一项功能。
面向过程编程语言首先是一种编程语言。它最大的特点是不支持类和对象两个语法概念,不支持丰富的面向对象编程特性。

面向对象编程相比面向过程编程有哪些优势?

OOP更加能够应对大规模复杂程序的开发

对于大规模复杂程序开发来说,整个程序的处理流程错综复杂,并非只有一条主线。如果用面向过程编程这种流程化、线性的思维方式,去翻译这个网状结构,去思考如何把程序拆解为一组顺序执行的方法会比较吃力。

面向对象编程以类为思考对象。在进行面向对象编程的时候,我们并不是一上来就去思考如何讲复杂的流程拆解为一个一个方法,而是采用曲线救国的策略。先去思考如何给业务建模,如何将需求翻译为类,如何给类之间建立交互关系,完成这些工作完全不需要考虑错综复杂的处理流程。

除此之外,面向对象编程还提供了一种更加清晰、更加模块化的代码组织方式。

实际上利用面向过程的编程语言那样,也可以写出面向对象风格的代码。只不过可能会比用面向对象编程语言付出的代价更高一些。两种编程风格并不是完全对立的。

OOP代码更易复用、易扩展、易维护

封装特性是两种编程风格最基本的区别,面向对象将数据和方法绑定在一起,通过访问权限控制,只允许外部调用者通过类暴露的有限方法访问数据,而不会像面向过程那样,数据可以被任意方法修改。因此面向对象提供的封装特性更有利于提高代码的易维护性。

函数本身就是一种抽象,它隐藏了具体实现。我们在使用函数时之需要了解函数具有什么功能,而不需要了解它是怎么实现的。从这一点上两种编程风格都支持抽象特性。只是面向对象还提供了其他抽象特性的实现方式,如基于接口实现的抽象。基于接口的抽象可以让我们在不改变原有实现的情况下,轻松替换新的实现逻辑,提高了代码的可扩展性。

继承特性是面向对象特有的,能避免代码重复写很多遍,提高了代码的复用性。

多态特性也是面向对象特有,在需要修改一个功能实现的时候,可以通过实现一个新的子类的方式,在子类重写原来的逻辑功能。用子类替换父类遵从了“对修改关闭,对扩展开放”的原则,提高代码的扩展性。除此之外,多态特性使得不同类对象可以传递相同的方法,执行不同的逻辑,提高代码复用性。

OOP语言更加人性化、更高级智能

面向对象时,开发者是在思考如何给业务建模、如何将真实世界映射为类或者对象,能更聚焦到业务本身,而不是思考如何和机器打交道。

思考

  • Unix/Linux这些复杂系统,也是基于C语言这种面向过程的编程语言开发的,怎么看待这种现象?
    • 操作系统是业务无关的,它更接近于底层计算机,因此更适合用面向过程的语言编写。并且和硬件打交道需要考虑到语言本身翻译成机器语言的成本和执行效率。
    • 不过操作系统虽然是面向过程的C语言实现,但是其设计逻辑是面向对象的。它用结构体同样实现了信息的封装,内核源码中也不乏继承和多态思想的体现。面向对象思想并不局限于具体语言。

理论四: 哪些代码设计看似面向对象,实际是面向过程的?

滥用getter、setter方法

在项目开发中,有时定义完类的属性之后,就顺手将属性的getter、setter方法都定义上。IDE或者Lombok插件会自动生成所有属性的getter、setter方法。

这种方法是不推荐的,因为其违反了面向对象的封装特性,相当于将面向对象编程风格退化成了面向过程编程风格。 例如下面这段代码

public class ShoppingCart {
    private int itemsCount;
    private double totalPrice;
    private List<ShoppingCartItem> items = new ArrayList<>();

    public int getItemsCount() {
        return this.itemsCount;
    }

    public void setItemCount(int itemsCount) {
        this.itemsCount = itemsCount;
    }

    public double getTotalPrice() {
        return this.totalPrice;
    }

    public void setTotalPrice(double totalPrice) {
        this.totalPrice = totalPrice;
    }

    public List<ShoppingCartItem> getItems() {
        return this.items;
    }

    public void addItems(ShoppingCartItem item) {
        items.add(item);
        itemCount++;
        totalPrice += item.getPrice();
    }
    ...
}

在这个代码中虽然我们将itemsCount和totalPrice定义为private,但是外部可以通过setter方法随意修改这两个属性的值。可能会导致和items属性的值不一致。暴露不该暴露的setter方法明显违反了面向对象的封装特性。数据没有任何访问权限,任何代码都可以随意修改,代码就退化成了面向过程编程风格了。

对于items我们没有设置setter方法,这样的设计看起来没有任何问题,而实际上并不是。items属性的getter方法返回的是一个List容器。外部调用者在拿到这个容器后,是可以操作容器内部数据的。比如obj.getItems().clear()会清空购物车,这样也会导致类属性中三个数据不一致。

正确的方法是应该专门在类中提供clear方法,并且修改getItems返回类型为Collections.undermidifiableList()。此时外部调用要修改就会抛出UnsupportedOperationException异常,避免容器中的数据被修改。(这里还存在一个问题,虽然items容器中数据不会被修改,但是容器中每个对象ShoppingCartItem的数据仍然可以修改)

滥用全局变量和全局方法

面向对象编程中,常见的全局变量有单例类对象、静态成员变量、常量等,常见的全局方法有静态方法。

  • 单例类对象在全局代码中只有一份,相当于全局变量
  • 静态成员变量归属类上的数据,被所有实例化对象共享,也相当于一定程度上的全局变量
  • 常量是非常常见的全局变量,放到一个Constant类中
  • 静态方法一般用来操作静态变量或者外部数据。如各种Utils类,里面的方法一般都会定义成静态方法。静态方法将方法和数据分离,破坏了封装特性,是典型的面向过程风格。

如:

public class Constants {
    public static final String MYSQL_ADDR_KEY = "mysql_addr";
    ...
}

我们会把程序中所有用到的常量都集中放到这个Constants类中,这并不是一个很好的设计思路。

  • 首先会影响代码的可维护性。开发同一项目的工程师很多,在开发过程中可能都要涉及修改这个类,查找修改可能比较费时,并且会增加提交代码冲突的概率。
  • 其次,这样的设计会增加代码的编译时间。 依赖这个类的代码很多,每次修改Constants类都会导致依赖它的类重新编译。
  • 最后,这样设计会影响代码的复用性。 如果我们在另一个项目中复用本项目的一个类,该类又依赖Constants类,即使只依赖其中的一部分我们仍然需要将整个Constants类也一起并入。引入许多无关的常量到新项目中。

如何改进呢?

  • 将Constants类拆解为功能更加单一的多个类
  • 另一种更好的思路是,并不单独地设计Constants常量类,而是哪个类用到了某个常量,就把这个常量定义到这个类中,提高了类设计的内聚性和代码的复用性

对于Utils类,它的出现主要是解决了多个类需要用到一块相同的功能逻辑,为了避免代码重复。通常为了复用会通过继承特性,将相同的属性和方法提取出来,定义到父类中,子类复用父类中的属性和方法。但是有时候从业务含义上,这些类并不一定具有继承关系,仅仅为了代码复用生硬地抽象一个父类,会影响代码可读性。所以只包含静态方法Utils类就出现了,它实现了公用的方法但是不需要共享任何数据,因此不需要定义任何属性。同时也要注意不要实现大而全的Utils类,最好细化一下。

定义数据和方法分离的类

传统的MVC分为Model层、Controller层、View层,在做前后端分离之后,三层结构在后端开发时会稍微有些调整,被分为Controller层、Service层、Repository层。Controller层负责暴露接口给前端调用,Service层负责核心业务逻辑,Repository层负责数据读写。在每一层中我们又会定义相应的VO(ViewObject)、BO(BusinessObject)、Entity。一般情况下VO、BO、Entity只会定义数据,不会定义方法,所有操作这些数据的业务逻辑都定义在对应的Controller类、Service类、Repository类中。这就是典型的面向过程的编程风格。

实际上这种开发模式叫做基于贫血模型的开发模式,也就是我们现在非常常用的一种Web项目开发模式。

面向对象编程中,为什么容易写出面向过程风格的代码?

主要是面向过程符合人的流程化思维方式。面向对象则是一种自底向上的思考方式,不是先去思考执行流程来分解任务,而是将任务翻译成一个一个的小模块,设计类之间的交互,最后按照流程将类组装起来完成整个任务。这种思考路径比较适合复杂程序开发,不是特别符合人类的思考习惯。

除此之外,面向对象中类的设计挺需要技巧,需要一定设计经验,要去思考如何封装合适的数据和方法到一个类里,如何设计类之间的关系,类之间的交互等诸多问题。

不管使用面向过程还是面向对象,最终目的都是写出易维护、易读、易复用、易扩展的高质量代码。只要我们能避免面向过程编程风格的一些弊端,控制好它的副作用,在掌控范围内为我们所用,就大可不用避讳在面向对象编程中写面向过程风格的代码。

理论五: 接口VS抽象类的区别?如何用普通的类模拟抽象类和接口?

什么是抽象类和接口?区别在哪里?

抽象类

  • 不允许被实例化,只能被继承。
  • 抽象类可以包含属性和方法。方法既可以包含代码实现,也可以不包含代码实现。不包含代码实现的方法叫做抽象方法。
  • 子类继承抽象类,必须实现抽象类中的所有抽象方法。

接口

  • 接口不能包含属性(即成员变量)
  • 接口只能声明方法,方法不能包含代码实现
  • 类实现接口的时候,必须实现接口中声明的所有方法

抽象类实际上就是类,只不过是一种不能被实例化的特殊类,只能被子类继承,is-a的关系。接口表示has-a的关系,表示具有某些功能。对于接口,有一个更加形象的叫法,就是协议。

抽象类和接口能解决什么编程问题?

抽象类是为代码复用而生的,多个子类可以继承抽象类中定义的属性和方法,避免在子类中重复编写相同的代码。普通的类继承虽然也可以解决代码复用问题,但是无法使用多态特性,会增加类被无用的风险。虽然也可以通过设置私有的构造函数的方式来解决,不过显然没有抽象类优雅。

接口更侧重于解耦,是对行为的一种抽象,相当于一组协议或者契约,可以类比API接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。

如何模拟抽象类和接口两个语法概念?

我们可以通过抽象类来模拟接口。首先接口的定义:接口中没有成员变量,只有方法声明,没有方法实现,实现接口的类必须实现接口中的所有方法。

class Strategy {
    public:
        ~Strategy();
        virtual void algorithm() = 0;
    protected:
        Strategy();
}

上述C++代码中用抽象类模拟了一个接口,类中没有定义任何属性,并且所有方法都是virtual类型。

除了用抽象类来模拟接口,我们还可以用普通类来模拟接口。类中虽然包含方法不符合接口定义,但是我们可以让类中的方法抛出异常来模拟不包含实现的接口,并且能强迫子类在继承这个父类的时候都去主动实现父类的方法,否则就会在运行时抛出异常。为了避免该类被实例化,我们将类的构造函数声明为protected方法就可以了。

public class MockInterface {
    protected MockInterface() {}
    public void funcA() {
        throw new MethodUnSupportedException();
    }
} 

如何决定该用抽象类还是接口?

要表示一种is-a的关系,并且是为了解决代码复用的问题,就用抽象类。

要表示一种has-a的关系,并且是为了解决抽象而非代码复用的问题,就可以使用接口。

抽象类是一种自下而上的设计思路,现有子类的代码重复,然后再抽象成上层的父类。而接口正好相反,它是一种自上而下的设计思路,在编程的时候一般是先设计接口再去考虑具体的实现。

理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?

Program to an interface, not an implematation。这句话最早出自1994年GoF的设计模式这本书,是一种比较抽象泛化的思想。此处的interface不要局限于编程语言中的接口。

如果落实到具体代码,这条原则中的接口可以理解为编程语言中的接口或者抽象类。

应用这条原则可以有效地提高代码质量,实现接口和实现相分离,封装不稳定的实现,暴露稳定的接口。当实现发生变化的时候,上游系统的代码基本上不需要做改动,以此来降低耦合性,提高扩展性。

  • 函数的命名不能暴露任何实现细节
  • 封装具体的实现细节
  • 为实现类定义抽象的接口,具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。

总之,我们在做软件开发的时候,一定要有抽象意识、封装意识、接口意识。在定义接口的时候,不要暴露任何实现细节,接口的定义只表明做什么,而不是怎么做。而且在设计接口的时候,我们要多思考一下,这样的接口设计是否足够通用,能否做到在替换具体的接口实现的时候,不需要任何接口定义的改动。

是否要为每个类定义接口?

如果业务场景中某个功能只有一种实现方式,未来也不可能被其他实现方式替换,那么我们就没有必要为其设计接口,也没有必要基于接口编程,直接使用实现类就可以。

理论七:为什么要多用组合少用继承?如何决定该用组合还是继承?

为什么不推荐用继承?

比如,我们设计一个关于鸟的类AbstractBird,所有细分的鸟都继承这个抽象类。 大部分鸟都可以飞,我们可不可以在抽象类中定义fly()方法呢?答案是不行,因为还有特例,如果我们对所有不会飞的鸟都重写fly方法并且抛出异常也可行,但是不够优雅。一方面增加了代码量另一方面也违背的Least Knowledge Principle(最小知识原则/迪米特法则),暴露了不该暴露的接口给外部,增加了类使用过程中被误用的概率。

此时再通过抽象类派生出两个细分的类,AbstractFlyableBird/AbstractUnFlyableBird(),这样继承关系就变成3层。如果此时再关注鸟会不会叫等等特点,继承关系就会越来越复杂,导致代码可读性变差。也破坏了类的封装特性,将父类的实现细节暴露给子类,子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改就会影响所有子类的逻辑。

组合相比继承有哪些优势?

我们可以利用组合Composition、接口、委托delegation三个技术手段,一起解决刚刚继承存在的问题。

接口表示某种行为特性,针对会飞特性我们可以定义Flyable接口,只让会飞的鸟去实现这个接口,对于会叫、会下蛋这些行为特性,类似定义Tweetable接口、EggLayable接口。不过接口只声明方法,不定义实现,也就是所有会下蛋的类都要实现一遍LayEgg()方法,会导致代码重复的问题。

我们可以针对三个接口再定义三个实现类,通过组合和委托技术来消除代码重复。

public interface Flyable() {
    void fly();
}
public class FlyAbility implements Flyable {
    @Override
    public void fly() {...}
}
...

public class Ostrich implements Tweetable, Egglayable {
    private TweetAbility tweetAbility = new TweetAbility();
    private EggLayAbility eggLayAbility = new EggLayAbility();

    @Overide 
    public void tweet() {
        tweetAbility.tweet();
    }

    @Overide
    public void layEgg() {
        eggLayAbility.layEgg();
    }
}

如何判断该用组合还是继承?

如果类之间的继承结构稳定,继承层次比较浅(最多两层),继承关系不复杂我们就可以大胆使用继承。反之则尽量使用组合。

有一些设计模式会固定使用继承或组合:

  • 装饰者模式decorator pattern、策略模式Strategy pattern、组合模式Composite pattern都使用了组合关系
  • 模版模式template pattern则使用了继承关系

还有一些特殊场景必须使用继承。如果不能改变函数的入参类型,而入参又非接口,为了支持多态只能采用继承来实现。

ES相关知识

Veröffentlicht am 2020-08-10

ES常见知识点

ES集群,一个node一般会分配几个分片?

  • ES中的数据组织成索引,每一个索引由一个或多个分片组成.每个分片是Luncene索引中的一个实例,可以把实例理解成自管理的搜索引擎,用于在ES集群中对一部分数据进行索引和处理查询. 分片是ES在集群周围分发数据的单位,ES在重新平衡数据时移动分片的速度取决于分片的大小,数量以及网络和磁盘性能.
  • 避免非常大的分片,因为大的分片可能会对集群从故障中恢复的能力产生负面影响.分片大小为50GB通常被界定为适用于各种用例的限制.
  • 在集群节点上保存的分片数量与可用的堆内存大小成正比,经验来说,每个节点的分片数量保持在低于每1GB堆内存对应集群的分片在20-25之间. 如30GB内存的堆内存节点最多可以有600-750个分片.
  • 参考文章:
    • Elasticsearch究竟要设置多少分片数
    • How many shard should I have in my Elasticsearch cluster

Elasticsearch如何实现master选举

  • Elasticsearch的任意一个节点都可以设置node.master和node.data属性
    • master=true,data=true: 即是Master Eligible又是data节点
    • master=true,data=false: 单纯的Master Eligible节点
    • master=false,data=true: 单纯的data节点
    • master=false,data=false: 纯粹的Coordinating Node, 协调节点负责查询时的数据收集,合并,以及聚合操作. ES中所有节点都是协调节点
  • ES针对当前集群的所有Master Eligible节点进行选举,为了避免split-brain现象,ES选取QUORUM思想,只有超过半数选票的节点才能成为master.(eligibleNodesNum/2 + 1)
  • 当满足下列条件时就会触发一次master选举
    • 当前master eligible节点不是master
    • 当前master eligible节点与其他节点通信无法发现master
    • 集群中无法连接到master的master eligible节点达到minimum_master_nodes的值
  • 某个节点决定要选举时,会实现如下操作:
    • 寻找ClusterStateVersion比自己高的master eligible节点,向其发送选举投票
    • 如果CLusterStateVersion相同,则计算自己能找到的master eligible节点(包含自己)中节点id最小的节点,向其发送选举投票
    • 如果一个节点收到足够多的选票,并且向自己也投票了,则该节点成为master开始发布集群信息
  • 与其他选举方法对比
    • Zookeeper: ES可以使用Zookeeper来进行选举,方法如下:
      • 所有master eligible尝试在ZK上创建指定路径
      • 只有第一个节点创建成功,该节点成为master,其余节点watch此路径
      • 一旦ZK失去master链接,该路径被删除,其他master eligible继续尝试创建路径, 重复上述操作
    • Raft: 相比ES自身的选举算法,Raft是经过严格论证的一致性算法,ES早期版本时Raft还未提出,可能后续会参考改进.

如何做到ES写入调优?

  • 客户端: 多线程批量写
    • 通过性能测试,确定最佳文档数量
    • 多线程,观察HTTP429返回,实现retry以及线程数量的自动调节
  • 服务器端:
    • 降低IO操作
      • 使用ES自动生成的文档ID(避免Get操作)
      • 调整Refresh interval等配置(降低搜索实时性)
      • 调整translog选项,降低写磁盘频率(牺牲容灾能力)
    • 降低CPU和存储开销
      • 减少不必要的分词
      • 避免不需要的doc_values
      • 文档字段尽量保持相同顺序,提高文档的压缩率
    • 尽量做到写入和分片的负载均衡,实现水平扩展
      • Shard Filtering
      • Write load balance
    • 调整Bulk线程池和队列
      • 客户端:
        • 单个bulk请求体的数据量不要太大,官方建议5-15mb
        • 写入bulk请求超时时间需要足够长,建议60s以上
        • 写入端尽量将数据轮询到不同节点
      • 客户端:
        • 索引创建属于计算密集型,应该使用固定大小的线程池,来不及处理的放入队列.线程数配置为CPU核数+1,避免过多上下文切换
        • 队列大小可以适当增加,不要太大否则占用内存导致GC
  • Index建模实践
    • 如果只需要聚合不需要搜索,index设置为false
    • 如果不需要算分,Norms设为false
    • 不对字符串使用默认的dynamic mapping,字段数量过多会对性能产生较大影响
    • Index_options控制在创建倒排索引时哪些内容会被添加到倒排索引中,节约CPU
    • 关闭_source减少IO操作,适合指标数据

如何避免split-brain?

当节点崩溃或者节点通讯故障时,如果一个子节点无法连接到主节点,那么它会发起选举从与之连接的master eligible节点中选举一个新的主节点.新主节点将接管开始工作,如果旧的主节点重新加入集群或恢复通信,那么新主节点将降级到子节点.这过程在大多数情况下是不存在冲突,高效无缝衔接的.

但是考虑集群中只有两个节点的极端情况,如果因为网络原因导致节点无响应,节点都相信对方已经挂掉.重新选举后会存在两个主节点,集群处于不一致状态,分片的两份数据分开了,如果不做全量的重索引很难进行重排序.

极端情况下,客户端无法感知到这种不一致状态,因为index请求无论发往哪个节点都会成功,只有在查询时才可能发现问题.

要避免脑裂,就是要关注discovery.zen.minimum_master_nodes的值,要设置为n/2+1,还有建议配置3节点以上的集群. 对于已经存在的两节点集群,可以添加一个新节点并将node.data设为false,放在一台便宜的服务器上.

ES对于大数据量(上亿量级)的聚合如何实现?

有些算法可以分布式执行,类似一些单次请求获得精确结果的聚合.它们无需额外代价,就能在多台机器上运行,如max度量:

  • 请求广播到所有分片
  • 查看每个文档的price字段,如果price>current_max就替换
  • 返回所有分片的最大值给协调节点
  • 找出所有分片返回的最大值即可
    这种算法会随着机器数的线性增长而横向扩展,无需任何协调操作且内存消耗小. 但是不是所有算法都可以如此,一些复杂的算法需要在算法性能和内存使用上做出权衡.

三角因子模型: 大数据,精确性,实时性

  • 精确+实时: 数据可以存入单台机器的内存中,结果会100%精确,响应会相对快速.
  • 大数据+精确: 传统的hadoop,可以处理PB级数据并且未我们提供精确的答案,但它可能需要几周的时间才能为我们提供答案.
  • 大数据+实时: 近似算法为我们提供准确但不精确的结果.

ES目前支持两种近似算法(cardinality和percentiles),以牺牲一点小小的估算错误为代价换回高速的执行效率和极小的内存消耗.

Cardinality

Elasticsearch提供的首个近似聚合是Cardinality度量.它提供一个字段的基数,即该字段的distinct或者unique值的数目.基于HyperLogLog++(HLL)算法,HLL会先对用户输入做哈希运算,然后根据哈希运算结果中的bits做概率估算从而得到基数. 算法特性如下:

  • 可配置的精度用来控制内存的使用(更精确=更多内存)
  • 小的数据集精度非常高
  • 可以通过配置参数,来设置去重需要的固定内存使用量.无论数千还是数十亿的唯一值,内存使用量只与配置的精度相关.

要配置精度,必须指定percision_threshold参数值,这个阈值定义了在何种基数水平下我们希望得到一个近乎精确的结果. 接受0-40,000之间的数字,更大的值还是会被当做40,000来处理.

对于指定阈值,HLL的数据结构大概使用percision_threshold*8字节的内存,所以必须在牺牲内存和获得额外的准确度之间做平衡. 实际使用中,阈值=100时可以在唯一值为百万的情况下仍将误差维持在5%以内.

如果想要获得唯一值的数目,通常需要查询整个数据集合,所有基于所有数据的操作都必须迅速.HyperLogLog已经很快了,它只是简单的对数据做哈希以及一些位操作,但是仍然可以进一步优化.

HLL只需要字段内容的哈希值,因此我们可以在索引时就预先计算好,在查询时跳过哈希计算将哈希值直接从fielddata中加载出来. 在执行聚合时使用X.hash字段而非X字段,cardinality会读取预先计算的哈希值取代动态计算原始值的哈希. 单个文档节省时间非常少,但是如果聚合一亿数据,每个字段会多花费10ns时间,这样每次查询时都会额外增加1s. 如果我们在非常大量的数据里面使用cardinality需要权衡使用预计算的意义,是否需要提前计算hash,从而在查询中获得更好的性能.

Percentiles

百分位数通常用来找出异常,比如监控网站的延时来判断响应是否能保证良好的用户体验. 和基数一样,计算百分位需要一个近似算法,percentiles中使用一个TDigest算法

  • 百分位的准确度和百分位的极端程度相关. 1和99的百分位要比50百分位要准确
  • 数据集合小的情况,百分位非常准确
  • 随着桶里数值的增长,算法会开始对百分位进行估算.有效在准确度和内存节省之间做出权衡.

通过修改compression参数来控制内存和准确度之间的比值.TDigest算法用节点近似计算百分比,节点越多准确度越高.compression参数限制节点的最大数目为20*compression.

通过增加compression可以以消耗更多内存未代价提高百分位数准确性,更大的压缩比值会使得算法运行更慢,因为底层的树形结构的存储会增长,导致操作的代价更高.默认compression=100

一个节点大约使用32字节的内存,所以在最坏情况下,默认设置会生成一个64KB的TDigest.

ES主分片数量可以在后期更改吗?为什么?

ES 2.X版本时不可以,因为ES对document的处理是通过路由算法来进行处理,更改主分片数量会导致路由被破坏,间接导致数据丢失.所以主分片数量不可以修改.

如果修改分片数量后重新分配数据,分片的切分成本和reindex成本差不多,所以官方直接使用reindex. 如果数据不重复,其实新的业务数据可以切换到新的索引上继续写,查询时查询新旧两个索引.

从ES6.1以后支持在线扩大shard的数量,但是操作期间需要对index锁写:

  • 创建一个新的目标索引,定义与源索引相同,但具有更多的主分片
  • 将段从源索引硬链接到目标索引,如果文件系统不支持hard link,则将所有段都复制到新索引中(非常耗时)
  • 创建低级文件后,再次对所有文档进行hash处理,删除属于不同分片的文档
  • 恢复目标索引

ES更新文档/删除文档的执行流程

更新

  • 在进行写操作时,ES根据传入的_routing参数按照公式计算出文档要分配的分片,从集群元数据中找出对应主分片的位置,将请求路由到该分片.
  • 文档写入Lucene后不能被立即查询,ES提供refresh操作,定时的调用Lucene的reopen(OpenIfChanged)为内存中新写入的数据生成一个新的segment.此时文档可以被检索.
  • 即使refresh后文档仍然在文件系统缓存中,如果服务器宕机这部分数据依旧会丢失. ES为此增加了translog,文档写入时会先将文档写入Lucene,然后写入一份到translog落盘.(如果可靠性要求不高可以设置异步落盘)translog是追加写入,性能比随机写入要好.先写Lucene后写translog是因为写入Lucene可能会失败,减少写入失败回滚的复杂度.
  • 间隔30分钟或者translog大小到达阈值时触发flush操作,ES会先执行refresh操作将buffer生成segment,然后调用Lucene的commit方法将所有内存中的segment fsync到磁盘中.此时Lucene中数据完成持久化,清空translog中数据(6.X版本为了实现sequenceIDs,不删除translog)
  • 由于refresh默认间隔1s,会产生大量的小segment,ES会运行一个任务检测当前磁盘中的segment,对符合条件的进行合并,减少Lucene中的segment个数,提高查询速度减少负载.

Lucene仅支持文档整体更新,ES为了支持局部更新,在Lucene的Store索引中存储一个_source字段,key时文档ID内容是文档原文.更新时先从_source中获取原文,与更新部分合并,再调用Lucene API进行全量更新. 增加版本机制防止其他线程并发写.

删除

  • 提交删除操作,先查询要删除文档所属的segment
  • commit中包含一个.del文件,记录哪些segment中的哪些文档被标记为deleted.
  • 当.del文件中存储的文档足够多时,ES执行物理删除操作,清楚文档
    • 在删除中进行搜索操作: 依次查询所有segment,根据.del文件过滤掉标记为deleted的文档,然后返回搜索结果
    • 在删除过程中更新: 将旧文档标记为deleted,将新文档写入新的segment中.执行查询时通过.del过滤掉旧版本文档

ES shard内部是由什么组成的?

Shard 实际上就是一个Lucene的实例(Lucene Index),单个Lucene实例中最多包含Integer.MAX_VALUE-128个documents

一个LuceneIndex在文件系统表现上来看就是存储了一系列文件的目录,由许多个独立的segments组成. segments包含了文档中的词汇字典,词汇字典的倒排索引,以及document的字段数据. 所有segments数据存储于_.cfs文件中

Segments

segments直接提供了搜索功能,ES中的一个shard由大量的segments文件组成,且每一次fresh都会产生一个新的segment文件,segment文件有大有小,相当碎片化. ES内部则会开启一个线程将小的segment合并减少碎片化,降低文件打开数提升IO性能.

segment文件是不可变更的,当一个document更新时,实际上是将旧的文档标记删除,索引一个新文档(在_.del标记某个文档删除,查询时会跳过).在Merge时会将旧文档删除掉(物理删除).

ES中分析器是什么?

分析 包含以下过程:

  • 将文本分成适合于倒排索引的独立词条
  • 将词条统一化为标准格式以提高可搜索性
    分析器执行上述工作,实际上将三个功能封装到一个包中:
  • 字符过滤器: 分词前整理字符串
  • 分词器: 拆分字符串到单个词条
  • Token过滤器: 词条按顺序通过每个token过滤器,该过程可能会改变词条(大小写),删除此条(无用词删除),或者增加词条(同义词).

ES附带了可以直接使用的预包装的分析器:

  • 标准分析器: ES默认使用的分析器, 分析各语言文本最常用的选择,根据Unicode定义的单词边界划分文本. 删除绝大部分标点并将词条小写.
  • 简单分析器: 在任何不是字母的地方分割文本,将词条小写
  • 空格分析器: 在空格的地方划文本
  • 语言分析器: 考虑指定语言的特点,如英语分析器删除无用的单词(the and…),并且提取英语单词的词干

当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引.当全文域检索的时候,需要将查询字符串通过相同的分析过程,以保证搜索的词条格式和索引中词条格式一致.

客户端和集群连接时,如果选择特定的节点执行请求?

TransportClient利用transport模块远程连接一个elasticsearch集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的transport地址,并以 轮询 的方式与这些地址进行通信。

ES中倒排索引是什么?

Inverted Index也叫反向索引,通过value找key. 对比词典的话,Term就相当于词语,Term Dictionary相当于词典,Term Index相当于词典的目录索引, Posting List相当于词语在字典的页数集合.

  • Term: 一段文本经过分析器分析后会输出一串单词,单词即是Term
  • Term Dictionary: 里面维护的Term,可以理解为Term集合.
  • Term Index: 为了更快的找到某个单词,我们为单词建立索引.B-Tree通过减少磁盘寻道次数来提高查询性能. ES也是采用同样思路,直接通过内存查找Term,不读磁盘. 如果term过多,Term dictionary会很大无法都放入内存,因此通过TermIndex(字典树). 这棵树不会包含所有的Term,包含的是一些Term的前缀,通过term index快速定位到Term dictionary的offset,然后从这个位置向后顺序查找. 再加上一些压缩基数,term index的尺寸可以只有所有Term尺寸的几十分之一,使得内存可以缓存整个term index.
  • PostingList(倒排列表):记录了出现过某个单词的所有文档的文档列表,以及该单词出现的位置信息,每条记录成为一个倒排项Posting.

为什么Elasticsearch/Lucene检索可以比mysql快了? Mysql只有term dictionary这一层,是以b-tree排序的方式存储在磁盘上的。检索一个term需要若干次的random access的磁盘操作。而Lucene在term dictionary的基础上添加了term index来加速检索,term index以树的形式缓存在内存中。从term index查到对应的term dictionary的block位置之后,再去磁盘上找term,大大减少了磁盘的random access次数。

ES在建立倒排索引时,会对拆分的各个单词进行相应处理,以提升后面搜索的时候能够搜索到相关联的文档的概率,这就是标准化规则转换,主要包括:时态的转换(例如liked转换为like)、单复数的转换(hospitals转换为hospitals)、同义词的转换(small转换为little)、大小写的转换(默认转换为小写)

当利用ES进行查询时,查询结果都会返回一个对应词条的相关度分数(score)。相关度分数的计算基于TF/IDF算法(Term Frequence&Inverse Doucument Frequency)

  • Term Frequence ,TF(t in f):我们查询的词条在文本中出现多少次,出现次数越多,相关度越高.
  • Inverse Doucument Frequency,IDF(t in all-f):查询词条在所有文本中出现的次数,出现次数越高,相关度越低。
  • Field-length(字段长度规约):字段的长度越长,相关度越低。

设计模式之美第一周

Veröffentlicht am 2020-08-02

之前也下决心看过《HeadFirst设计模式》和《HeadFirst面向对象分析与设计》,但是都没有坚持读完。看了下极客上的这个课程比较适合自己,希望能坚持下来✊

码农要尽早学习并掌握设计模式相关知识

设计模式能更直接地提高我们的开发能力,如同数据结构和算法教人如何写出高效代码,设计模式教我们如何写出可扩展、可读、可维护的高质量代码。

为什么学习设计模式?

  • 应对面试中的设计模式相关问题
    • 不管是前端、后端还是全栈工程师,在面试中设计模式问题总是被问得频率比较高。因此平时要多注意积累。
  • 被人吐槽代码烂
    • Talk is cheap。 代码能力是码农最基础的能力,是展示程序员基础素养的最直接的衡量标准。这个专栏不仅讲解设计模式,还会通过实战教我们避免类似命名不规范、类设计不合理、分层不清晰、没有模块化概念、代码结构混乱、高度耦合等代码问题
  • 提高复杂代码的设计和开发能力
  • 让读源码、学框架事半功倍
    • 优秀的开源项目、框架、中间件、代码量、类个数都会比较多,为了保证代码的扩展性、灵活性、可维护性,代码中会使用到很多设计模式或者设计思想。学习设计模式相关知识,可以让我们更轻松地读开源项目。
  • 为职场发展做铺垫

投资要趁早,这样才能尽早享受复利。设计模式作为一门与编码、开发有着直接关系的基础知识,早点学习就可以在项目中早点实践锻炼。

从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?

如何评价代码质量的高低?

  • 可维护性 Maintainability,对于一个项目来说,维护代码的时间远远大于编写代码的时间。工程师大部分的时间可能都是花在了修bug,改老功能逻辑,添加新功能之类的工作上。如果代码分层清晰、模块化好、高内聚低耦合、遵从基于接口而非事先编程的设计原则,那么就可能意味着代码易维护。除此之外,还跟项目代码量的多少、业务复杂程度、利用到的技术复杂程序、文档是否全面、团队成员的开发水平等诸多因素有关。
  • 可读性 Readability,Any fool can write code that a computer can understand, Good programmer write code that humans can understand. 看代码是否符合编码规范、命名是否达意、注释是否详尽、函数是否长短合适、模块划分是否清晰、是否符合高内聚低耦合等。 Code revie是很好的检测代码可读性的手段。
  • 可扩展性 Extensibility,对修改关闭,对扩展开放设计准则
  • 灵活性 Flexibility, 代码易扩展、易复用、或者易用。
  • 简洁性 Simplicity, KISS原则, ”Keep It Simple, Stupid“,思从深而行从简,真正的高手能云淡风轻地用最简单的方法解决最复杂的问题。
  • 复用性 Reusability, DRY设计原则,Don’t Repeat Yourself
  • 可测试性 Testability, 易写单元测试。

如何写出高质量代码?

需要掌握一些更加细化、更加能落地的编程方法论,包括面向对象设计思想、设计原则、设计模式、编码规范、重构技巧等。

面向对象、设计原则、设计模式、编码规范重构,五者有何关系?

面向对象

  • 四大特性: 封装、抽象、继承、多态
  • 面向对象编程和面向过程编程的区别和联系
  • 面向对象分析、面向对象设计、面向对象编程
  • 接口和抽象类的区别以及各自的应用场景
  • 基于接口而非实现编程的设计思想
  • 多用组合少用继承的设计思想
  • 面向过程的贫血模型和面向对象的充血模型

设计原则

  • SOLID原则-SRP 单一职责原则
  • SOLID原则-OCP 开闭原则
  • SOLID原则-LSP 里式替换原则
  • SOLID原则-ISP 接口隔离原则
  • SOLID原则-DIP 依赖倒置原则
  • DRY原则、KISS原则、YAGNI原则、LOD法则

设计模式

经典的设计模式有23种,随着编程语言的演进,一些设计模式Singleton随时过时,甚至成了反模式,一些则被内置在编程语言中(如Iterator),另外还有一些新模式诞生(如Monostate)。

23中经典的设计模式,可以分为三大类:创建型、结构型、行为型。

  • 创建型
    • 常用的: 单例模式、工厂模式、建造者模式
    • 不常用: 原型模式
  • 结构型
    • 常用的: 代理模式、桥接模式、装饰者模式、适配器模式
    • 不常用: 门面模式、组合模式、享元模式
  • 行为型
    • 常用的: 观察者模式、模版模式、策略模式、指责链模式、迭代器模式、状态模式
    • 不常用: 访问者模式、备忘录模式、命令模式、解释器模式、中介模式

编程规范

编程规范主要解决的是代码的可读性问题。编程规范相对于设计原则、设计模式,更加具体、更加偏重代码细节。 《重构》《代码大全》《代码整洁之道》书籍推荐

代码重构

重构是软件开发中非常重要的一个环节。持续重构是保持代码质量不下降的有效手段,能有效避免代码腐化到无可救药的地步。

重构的工具就是前面罗列的那些面向对象设计思想、设计原则、设计模式、编码规范。实际上,设计思想、设计原则、设计模式一个最重要的应用场景就是在重构的时候。虽然设计模式可以提高代码的可扩展性,但是过度不恰当地使用,会增加代码的复杂度,影响代码的可读性。在开发初期,除非特别必须,一定不要过度设计,应用复杂的设计模式。而是当代码出现问题的时候,再针对问题,应用原则和模式进行重构。这样就能有效避免前期的过度设计。

  • 重构的目的Why、对象What、时机When、方法How
  • 保证重构不出错的技术手段:单元测试和代码的可测试性
  • 两种不同规模的重构:大重构(大规模高层次)和小重构(小规模低层次)

五者之间的联系

  • 面向对象编程因其具有丰富的特性,可以实现很多复杂的设计思路,是很多设计原则设计模式等编码实现的基础。
  • 设计原则指导我们代码设计的一些经验总结。
  • 设计模式是针对软件开发中经常遇到的一些设计问题,总结出来的一套解决方案或者设计思路。相比设计原则更具体更加可执行。
  • 编程规范主要解决代码的可读性问题,相比设计原则、设计模式更具体,更偏重代码细节、更加能落地。持续小重构依赖的理论基础就是编程规范。
  • 重构作为保持代码质量不下降的有效手段,利用的就是面向对象、设计原则、设计模式、编程规范这些理论。

本周回顾

专栏中所涉及的知识点都在下图中。

TalkGo读书会第一期总结

Veröffentlicht am 2020-07-25

个人总结

选择加入读书会首先是本身对性能优化这个Topic比较感兴趣,另外公司底层架构升级维护也需要关注性能问题。课程计划表是两个月,一天一篇的话也不会特别占时间,但是真正执行下来才深刻体会到惰性的强大(如果不是读书会的话应该很容易就弃掉)。平时工作的话回家看书的时间也不会太多,有两三次都是周末抓紧时间搞定,尤其是后面网络那部分平时不太会去关注的指标,可能以前遇到这种章节都会直接略过。

这个课程主要还是偏向原理,基础的性能指标,主要提供了一些遇到性能问题如何下手分析解决的思路。真正在环境中遇到问题分析出原因定位代码还是需要一些经验。特别是一些开源软件遇到性能问题时定位具体代码还是需要耗费一些精力。我们组这个月正好遇到了ES小版本升级后,xpack monitor导致的内存使用问题,定位代码借助了jstack和阿里的Arthas才最终找到原因。

总之,这个课程还是需要经常拿来回顾下,遇到性能问题多积攒经验才是王道。

接下来的看书计划的话,正好前两天收到了一本《架构修炼之道》,准备先翻看下。

《Linux 性能优化实战》第八周--套路篇

Veröffentlicht am 2020-07-19

个人感悟

在实际的性能分析中,发生性能瓶颈后登陆服务器想要排查的时候发现瓶颈已经消失且很难复现。因此要事先搭建监控系统,把系统和应用程序的运行状况监控下来,并定义一系列的策略,在发生问题第一时间告警处理。

系统监控通过USE法则利用prometheus+grafana来监控系统资源。应用程序监控分为指标监控和日志监控两大块,在复杂业务场景中通常搭建全链路跟踪系统来定位应用瓶颈。

性能问题可以从系统资源瓶颈和应用程序瓶颈两个角度来梳理分析。系统时应用的运行环境,系统的瓶颈会导致应用的性能下降,而应用的不合理设计也会引发系统资源的瓶颈。做性能分析要结合应用程序和操作系统的原理,就出引发问题的真凶。

本周回顾了前几周学习到的常见性能优化方法。值得注意的是一定切记要避免过早优化,性能优化往往会提高复杂度,一方面降低了可维护性,另一方面也为适应复杂多变的新需求带来障碍。所以要逐步完善,首先保证能满足当前的性能要求。发现性能不满足要求或者出现性能瓶颈后,再根据性能分析的结果选择最重要的性能问题进行优化。

不能把性能工具当成性能分析和优化的全部,一方面性能分析和优化的核心是对系统和应用程序运行原理的掌握,性能工具只是辅助我们更快完成此过程的帮手;另一方面完善监控系统可以提供绝大部分性能分析所需的基准数据,从这些数据中很可能大致定位出性能瓶颈,也就不用再去手动执行各类工具了。

捞评论

  1. 除了USE原则,还有RED原则。更偏重于应用,在很多微服务中会用到。
    • Rate: 每秒请求数量
    • Errors: 失败的请求次数
    • Duration: 处理一条请求所需的时间
  2. 想要学习eBPF来说,可以从BPF Compiler Collection(BCC)这个项目开始。BCC提供了很多短小的示例,可以快速了解eBPF的工作原理并熟悉eBPF程序的开发思路。了解这些基本的用法后,再去深入了解eBPF。

Lesson 53 套路篇:系统监控的综合思路

一个好的监控系统,不仅可以实时暴露系统的各种问题,更可以根据这些监控到的状态,自动分析和定位大致的瓶颈来源,从而更精确地把问题汇报给相关团队。

  • 从系统来说,监控系统要涵盖系统的整体资源使用情况。如CPU、内存、磁盘、文件系统和网络等
  • 从应用程序来说,监控系统要涵盖应用程序内部的运行状态。即包括进程的CPU、磁盘I/O等整体运行状况,也包括接口调用耗时、执行中的错误、内部对象的内存使用等程序内部的运行状况

USE法

USE, Utilization Saturation and Errors

  • 使用率,资源用于服务的时间或容量百分比
  • 饱和度,资源的繁忙程度,通常和队列长度相关
  • 错误数,表示发生错误的事件个数

常见指标分类如下图所示:

监控系统

一个完整的监控系统通常由数据采集、数据存储、数据查询和处理、告警以及可视化展示等多个模块组成。

常见的开源监控工具有Zabbix、Nagios、Prometheus等,下面介绍Prometheus的基本架构:

数据采集

Prometheus targets就是数据采集的对象,Retrieval负责采集这些数据。Prometheus支持两种采集模式:

  • Pull模式,服务端的采集模块触发采集。只要采集目标提供HTTP接口即可(常用的采集模式)
  • Push模式,各个采集目标主动向Push Gateway推送目标,再由服务器端从Gateway中拉取(移动应用中常用)

Prometheus提供服务发现机制,自动根据配置的规则动态发现需要监控的对象。在K8S容器平台中非常有效。

数据存储

TSDB(Time Series Database),负责将采集到的数据持久化到SSD等磁盘设备中。 TSDB是专门为时序数据设计的数据库,以时间为索引、数据量大且以追加的方式写入。

数据查询和处理

TSDB在存储数据的同时,还提供了数据查询和基本的数据处理功能。即PromQL语言,提供了简介的查询、过滤功能。

告警模块

AlertManager提供了告警功能,基于PromQL语言的触发条件、告警规则的配置管理以及告警的发送。还支持通过分组、抑制或者静默等多种方式来聚合同类告警。

可视化

Prometheus WebUI提供了简单的可视化界面。通常配合Grafana来构建强大的图形界面。

Lesson 54 套路篇:应用监控的一般思路

指标监控

应用程序的核心指标,不再是资源的使用情况,而是请求数、错误率和响应时间。
除了上述三个指标外,下面几种指标也是应用程序监控必不可少的,可以帮助我们快速定位性能瓶颈。

  • 应用进程的资源使用情况,比如进程占用的CPU、内存、磁盘I/O、网络等。
  • 应用程序之间的调用情况,如调用频率、错误数、延时等
    • 可以迅速分析出一个请求处理的调用链中哪个组件导致性能问题
  • 应用程序内部核心逻辑的运行情况,比如关键环节的耗时以及执行过程中的错误等
    • 直接进入应用程序内部,定位到底是哪个处理环节的函数导致性能问题

由于业务系统通常会涉及到一连串的多个服务,形成一个复杂的分布式调用链。为了迅速定位这类跨应用的性能瓶颈,可以使用Zipkin、Jaeger、Pinpoint等各类开源工具来构建全链路跟踪系统。

日志监控

  • 指标是特定时间段的数值型测量数据,通常以时间序列的方式处理,适合于实时监控
  • 日志都是某个时间点的字符串消息,通常需要对搜索引擎进行索引后,才能进行查询和汇总分析

日志监控经典的方法是ELK技术栈,Elasticsearch、Logstash、Kibana三个组件的组合。

  • Logstash负责从各个日志源采集日志,进行预处理,最后再把初步处理过的日志发送给Elasticsearch进行索引
  • Elasticsearch负责对日志进行检索,并提供一个完整的全文搜索引擎
  • Kibana负责对日志进行可视化分析,包括日志搜索、处理以及绚丽的仪表板展示

ELK中logstash资源消耗较大,在资源紧张时往往使用Fluentd来替代,即EFK技术栈。采集端还可以使用filebeat,架构拓展为filebeat-kafka(zookeeper)-logstash或sparkstreaming-es,除了日志查询外可以做业务关联等。

Lesson 55 套路篇:分析性能问题的一般步骤

在收到监控系统的告警,发现系统戏院或者应用程序出现性能瓶颈,如何进一步分析根源?

系统资源瓶颈

系统资源的瓶颈通过USE法,即使用率、饱和度和错误数三类指标来衡量。系统的资源可以分为硬件资源和软件资源两大类:

  • CPU、内存、磁盘和文件系统以及网络等,都是常见的硬件资源
  • 文件描述符、连接跟踪数、套接字缓冲区大小等都是典型的软件资源。

在收到监控系统告警后就可以对照这些资源列表,再根据指标的不同来定位。

CPU性能分析

利用top、vmstat、pidstat、strace以及perf等几个常见的工具,获得CPU性能指标后,再结合进程与CPU的工作原理,就可以迅速定位CPU性能瓶颈的来源。

top、vmstat、pidstat等工具所汇报的CPU性能指标都来源于/proc文件系统,这些指标都应该通过监控系统监控起来。

比如当收到CPU使用率告警时,从监控系统中直接查询导致CPU使用率过高的进程,然后登陆到服务器分析该进程行为。可以使用strace查看进程的系统调用汇总,也可以使用perf找出热点函数,甚至可以使用动态追踪的方法来观察进程的当前执行过程,直到确定瓶颈个根源。

内存性能分析

通过free和vmstat输出的性能指标确认内存瓶颈,然后根据内存问题的类型,进一步分析内存的使用、分配、泄漏以及缓存等,最后找出问题的根源。

内存的性能指标也来源于/proc文件系统,它们也都应该通过监控系统监控起来。如当收到内存不足的告警时,可以从监控系统中找出占用内存最多的几个进程。然后根据这些进程的内存占用历史,观察是否存在内存泄漏问题。确定可疑进程后,再登陆服务器分析该进程的内存空间或内存分配,查明原因。

磁盘和文件系统I/O性能分析

当使用iostat发现磁盘I/O存在性能瓶颈后,可以再通过pidstat、vmstat等确认I/O的来源。再根据来源的不同进一步分析文件系统和磁盘的使用率、缓存以及进程的I/O等,从而找出I/O问题所在。

磁盘和文件系统的性能指标也来源于/proc和/sys文件系统,也应该通过监控系统监控起来。

如果发现某块磁盘的I/O使用率为100%时,首先可以从监控系统中,找出I/O最多的进程。然后登陆服务器借助strace、lsof、perf等工具,分析该进程的I/O行为。最后再结合应用程序的原理找出大量I/O的原因。

网络性能分析

网络性能其实包含两类资源,网络接口和内核资源。网络分析要从Linux网络协议栈的原理来切入。

  • 链路层,从网络接口的吞吐量、丢包、错误以及软中断和网络功能卸载等角度分析;
  • 网络层,从路由、分片、叠加网络等角度分析
  • 传输层,从TCP、UDP的协议原理出发,从连接数、吞吐量、延迟重传等角度分析
  • 应用层,从应用层协议、请求数、套接字缓存等角度进行分析

网络的性能指标也都来源于内核,包括/proc文件系统、网络接口以及conntrack等内核模块,这些指标同样需要被监控系统监控。

例如,当收到网络不同的告警时,就可以从监控系统中查找各个协议层的丢包指标,确认丢包所在的协议层。然后从监控系统的数据中,确认网络带宽、缓冲区、连接跟踪数等软硬件,是否存在性能瓶颈。最后再登录到服务器中,借助netstat、tcpdump、bcc等工具分析网络的收发数据,并且结合内核中的网络选项以及TCP等网络协议的原理找出问题所在。

应用程序瓶颈

应用程序瓶颈本质来源有三种:资源瓶颈、依赖服务瓶颈、应用自身瓶颈。

资源瓶颈可以用前面的方法来分析。

依赖服务的瓶颈,也就是诸如数据库、分布式缓存、中间件等应用程序,直接或间接调用的服务出现了性能问题从而导致应用程序的响应变慢或者错误率升高。使用全链路跟踪系统可以帮助快速定位这类问题的根源。

应用程序自身的性能问题,包括了多线程处理不当、死锁、业务的复杂度过高等,这类问题可以通过应用程序指标监控以及日志监控中,观察关键环节的耗时和内部执行过程中的错误,帮助缩小问题范围。

不过应用程序内部的状态,外部通常不能直接获取详细的性能数据,需要应用程序在设计和开发时提供这些指标。

如果上述手段还是无法找出瓶颈,可以通过系统资源模块提供的各类进程分析工具来定位分析。 比如:

  • strace观察系统调用
  • perf和火焰图分析热点函数
  • 动态追踪技术分析进程的执行状态

Lesson 56 套路篇:优化性能问题的一般方法

系统优化

CPU优化

CPU性能优化的核心,在于排除所有不必要的工作、充分利用CPU缓存并减少进程程度对性能的影响。

  • 把进程绑定到一个或者多个CPU上,充分利用CPU缓存的本地性,并减少进程间的相互影响。
  • 为中断处理程序开启多CPU负载均衡,以便在发生大量中断时可以充分利用多CPU的优势分摊负载
  • 使用cgroups等方法为进程设置资源限制,避免个别进程消耗过多的CPU。同时为核心应用程序设置更高的优先级,减少低优先级任务的影响

内存优化

  • 除非有必要,Swap应该禁止掉。避免Swap的额外I/O,带来内存访问变慢的问题
  • 使用Cgroups方法为进程设置内存限制。对于核心应用还应该降低oom_score,避免被OOM杀死
  • 使用大页、内存池等方法,减少内存的动态分配,从而减少缺页异常

磁盘和文件系统I/O优化

  • 通过SSD替代HDD、或者用RAID方法来提升I/O性能。
  • 针对磁盘和应用程序I/O模式的特征,选择最合适的I/O调度算法。比如,SSD和虚拟机中的磁盘,通常用的是noop调度算法;数据库应用更推荐使用deadline算法
  • 优化文件系统和磁盘的缓存、缓冲区,比如优化藏也的刷新频率、脏页限额,以及内核回收目录项缓存和索引节点缓存的倾向等

网络优化

从内核资源和网络协议的角度:

  • 增大套接字缓冲区、连接跟踪表、最大半连接数、最大文件描述符数、本地端口范围等内核资源配额
  • 减少TIMEOUT超时时间、SYN+ACK重传数、Keepalive探测时间等异常参数处理
  • 还可以开启端口复用、反向地址校验,并调整MTU大小等降低内核的负担

从网络接口的角度:

  • 将原来CPU上执行的工作,卸载到网卡中执行,即开启网卡的GRO、GSO、RSS、VXLAN等卸载功能;
  • 也可以开启网络接口的多队列功能,这样每个队列就可以用不用的中断号,调度到不同CPU上执行
  • 增大网络接口的缓冲区大小以及队列长度等,提升网络传输的吞吐量

在极限性能情况下,内核的网络协议栈可能是最主要的性能瓶颈,所以一般考虑绕过内核协议栈。

  • DPDK技术跳过内核协议栈,直接由用户态进程用轮询的方式,来处理网络请求。同时再结合大页、CPU绑定、内存对齐、流水线并发等多种机制,优化网络包的处理效率
  • 内核自带的XDP技术,在网络包进入内核协议栈前,就对其进行处理。

应用程序优化

性能优化的最佳位置,还是应用程序内部

  • 从CPU的角度来说,简化代码、优化算法、异步处理以及编译器优化等
  • 从数据访问的角度,使用缓存、写时复制、增加I/O尺寸等都是常用的减少磁盘I/O的方法
  • 从内存管理的角度,使用大页、内存池等方法,可以预先分配内存,减少内存的动态分配,从而更好地内存访问性能
  • 从网络的角度,使用I/O多路复用,长连接代替短连接、DNS缓存等,可以优化网络I/O并减少网络请求数,从而减少网络延时带来的性能问题
  • 从进程的工作模型来说,异步处理、多线程或多进程可以充分利用每一个CPU的处理能力,从而提高应用程序的吞吐能力

还可以使用消息队列,CDN、负载均衡等各种方法来优化应用程序的架构,将原来单机要承担的任务调度到多台服务器中并行处理。

Lesson 57 套路篇:Linux性能工具速查

CPU性能工具

内存性能工具

磁盘I/O性能工具

网络性能工具

基准测试工具

  • 在文件系统和磁盘I/O模块中,使用fio工具
  • 在网络模块,使用iperf、pktgen等
  • 在基于Nginx的案例中,使用ab、wrk等

现在重新回看Brendan Gregg的这张Linux基准测试工具图谱,收获良多。

123…5
Frances Hu

Frances Hu

48 Artikel
14 Tags
© 2021 Frances Hu
Erstellt mit Hexo
Theme - NexT.Muse