jasper的技术小窝

关注DevOps、运维监控、Python、Golang、开源、大数据、web开发、互联网

性能调优之应用篇

作者:jasper | 分类:Linux | 标签:   | 阅读 729 次 | 发布:2016-09-17 12:33 a.m.

性能的调整最好是从上层开始,也就是应用层。现在的应用程序越来越复杂,尤其是涉及到众多组件的分布式应用程序环境中。那么本篇文章就从应用程序性能的基础原理、编程语言和编译器,以及怎样做性能分析等方面来谈谈对应用层的优化。

1、应用程序的基础

在深入研究之前,需要了解该应用程序的作用、基本特征(比如是io密集型的还是CPU密集型的)以及在业界的生态系统。除此之外更为细节的,你需要知道该应用程序有哪些外部请求,如何配置(特别是和性能相关的一些参数,包括缓冲区大小、并发等),相关的metric(比如java应用通过jvm暴露出来的指标),应用程序的日志,版本是否最新,是否有已知的bug等等,这些都是你要去知道的。

2、应用程序性能技术

2.1、选择IO尺寸

执行IO的开销包括初始化缓冲区、系统调用、上下文切换、分配内核元数据、检查进程权限和限制、映射地址到设备、执行内核和驱动代码来执行IO,以及在最后释放元数据和缓冲区。从效率上看,每次IO传输的数据越多,则效率越高。
所以增加IO对于提高吞吐量很有帮助,比如一次256KB肯定比256次传输1KB的IO要高效得多,尤其是寻道时间,每次IO都会有很大的开销在这上面,但是也不是越大越好,这和应用程序本身是有关系的,如果应用程序每次IO不需要那么大,那么大的IO反而会会使IO延时,而且也会浪费缓存空间。

2.2、缓存

将经常执行的操作的结果保存在本地缓存中以备后来使用,而不是重复执行一些高开销的操作,这样能大大提高执行的效率,就像数据库缓冲区高速缓存一样。缓存一个重要的方面就是要保证数据的完整性,确保不会查询到过期的数据,也称之为缓存的一致性。

2.3、缓冲区

缓存提高了读操作的性能,而写的操作的提升则需要使用到缓冲区。数据在进入下一层级之前,先放在缓冲区中,等合并之后再写入磁盘。这样增加了IO的大小,提高了操作的效率。

2.4、轮询

轮询是在循环中检查事件状态,等待事件发生的技术。轮询的性能问题:

  • 重复检查CPU的开销很高;
  • 事件发生和下一次检查的延时较高。

其实应用程序可以自身监听事件的发生,当发生时立即通知应用程序执行相应的过程。在系统层面来看,通过poll来检查文件描述符的状态,这和轮询很相似,但是是基于事件的,性能开销低很多,复杂度是O(n),在Linux中提供了epoll,也是基于事件的,但是没有了扫描,复杂度为O(1)。

2.5、并发和并行

除了并发执行不同的应用程序,应用程序的内部函数也是可以并发的,可以用多进程或是多线程来实现,每个进程或是线程都执行自己的任务,还有一个方法是基于事件的并发,应用程序服务于不同的函数并在事件发生的时候在这些函数之间切换,比如说nodejs即是采用这样的方法,这种方法提供了并发性,但是可能只利用到了一个CPU。

为了利用多处理器的优势,应用程序需要同时运行在多个CPU上,即是并行。除了增加CPU工作的吞吐量,多进程或多线程也让IO可以并发执行,当一个线程IO阻塞的时候,其他线程还能执行。

多线程共享同一进程内的地址空间,线程可以直接对同一内存读取和写入,不需要代价更高的接口(比如多进程里的进程间通信IPC)。使用同步原语来保证完整性,这样数据不会因为多线程同时读写而损坏,并使用哈希表来提高效率。其中同步原语通过三把锁来实现:mutex锁、自旋锁、读写锁。哈希表用来对大量数据结构的锁做数目优化。

2.6、非阻塞IO

非阻塞IO模型是异步地发起IO,而不阻塞当前的线程,线程可以去执行其他的工作,这样能大大节省资源的消耗,这也是nodejs的一个关键的特性。

2.7、处理器绑定

NUMA对于进程或线程保持在一个CPU上面是有优势的,执行IO的前后运行在同一个CPU上,这样可以减少内存IO,提高应用程序的整体性能。设计的本意就是让应用程序依附在同一个CPU上,也就是CPU的亲缘性,这个在后面的文章中会细讲。

一些程序强制将自身与CPU绑定,这样能提高性能,但是如果绑定和其他CPU的绑定冲突,比如CPU上的设备中断映射,这样的绑定就会损坏性能。所以如果还有其他应用在一个系统上,就要小心CPU绑定带来的风险。

3、编程语言

3.1、编译语言

编译是在运行之前将程序生成机器指令,保存在二进制可执行文件里。这些文件可以在任何时间运行而不需再度编译。编译语言包括C、C++以及近来年很火的GO。编译过的代码总体是高效的,在被CPU执行之前不需要进一步的转换,操作系统的内核都是C写的,当然有一部分的关键路径使用汇编完成的。由于所执行的机器代码总是和原始映射很紧密,所以编译语言的性能分析通常是很直观的。

3.2、解释语言

解释语言的执行时将语言在运行时翻译成行为,这一过程会增加执行的开销,解释语言不期望能表现出很高的性能,比如shell脚本就是解释语言的一个例子。

3.3、虚拟机

虚拟机是模拟计算机的软件,比如java、erlang等都是在VM上面执行,应用程序先要编译成字节码,再由虚拟机来执行。这样的编译对象有了可移植性,只要有虚拟机上就能在目标平台上运行。虚拟机其实起到了解释的作用,将字节码解释为机器码。Java HotSpot虚拟机支持JIT编译,可以提前将字节码编译成机器码,这样就带来了编译后的代码在性能上的优势。

3.4、垃圾回收

一些语言自动内存管理,分配的内存不需要显示地释放,留给异步的垃圾回收来处理。这样让程序更容易编写,但是也带来了性能问题:

  • 内存增长:当没有能自动识别出对象适合被释放时,内存的使用会增加。
  • CPU成本:GC通常间歇地运行,会搜索和扫描内存中的对象,这很耗CPU资源。
  • 延时异常值:GC执行期间应用程序可能中止执行,偶尔出现高延时的响应,比如java中GC时的STW。

GC是常见的性能调整对象,比如java虚拟机就提供了许多可调的参数来设置GC的类型、GC的线程数、堆大小等等,如果调整没有作用,肯定是代码问题了。

4、方法和分析

这一趴会提供几个常见的分析方法,可以单独使用,也可以结合使用。

  • 线程状态分析:线程一般有六种状态:执行、可运行、匿名换页、睡眠、锁、空闲。内核的schedstat会跟踪可运行的线程,并将信息显示在/proc/*/schedstat中;
  • CPU剖析:判断应用程序是如何消耗掉CPU的,可以对CPU上的用户栈跟踪来剖析;
  • 系统调用分析:在Linux上可以使用strace来追踪syscall的消耗;
  • 工作负载特征归纳:其实就是压测,用压测来观察各个资源消耗的变化;
  • USE方法:检查使用率、饱和度以及所有硬件资源的错误;
  • 向下挖掘法:从应用程序的服务操作开始,向下至应用程序内部、再到系统库、系统调用甚至是内核;
  • 锁分析:对于多线程的应用,锁可能成为阻碍并行化和扩展的瓶颈,锁分析可以通过检查竞争以及检查过长的持锁时间来考量

5、静态性能调优

静态性能重点在于环境的配置问题,可以检查以下一些方面:

应用程序的版本?新版本有性能提升吗?应用程序如何配置?如果调整配置与默认值不同是出于什么原因?应用程序用到了对象缓存吗?是并发的话,并发的线程如何配置?应用程序用到了哪些系统库?使用怎样的内存分配器?编译语言的话,其编译器的版本、编译时的优化,以及OS的位数是啥?有没有系统的限制,比如CPU、mem、fs、disk、net使用资源的控制?


转载请注明出处:http://www.opscoder.info/application_performance.html

其他分类: