程序员经常面临一个问题:如何提高程序性能?在本文中,我们循序渐进,从内存、磁盘I/O、网络I/O、CPU、缓存、架构、算法等,串联起高性能开发必须掌握的十大核心技术。-I/O优化:零拷贝技术-I/O优化:多路复用技术-线程池技术-无锁编程技术-进程间通信技术-RPC&&序列化技术-数据库索引技术-缓存技术&&BloomFilter-全文搜索技术-负载均衡技术你准备好了吗?坐好,我们走吧!首先,我们从最简单的模型开始。老大告诉你如何开发静态web服务器,通过网络发送磁盘文件(网页,图片)。你花了两天时间,搞了一个1.0版本:主线程进入循环,等待连接过来就启动一个worker线程处理worker线程,等待对方请求,然后从disk,向socket发送数据,上线一天。老大发现太慢了,加载大图感觉卡住了。让你优化。这时候就需要:I/O优化:零拷贝技术上的工作线程从磁盘读取文件,然后通过网络发送数据。数据需要从磁盘复制四次到网络。其中,CPUHandling需要两趟。零拷贝技术解放了CPU,文件数据直接从内核发送,无需拷贝到应用程序缓冲区,白白浪费资源。LinuxAPI:ssize_tsendfile(intout_fd,intin_fd,off_t*offset,size_tcount);函数名已经清楚地说明了函数的作用:发送文件。指定要发送的文件描述符和网络套接字描述符,一个函数搞定!使用零拷贝技术后,开发了2.0版本,图片加载速度有了明显提升。但是老板发现同时访问的人多了,又慢了下来,让你继续优化。这时候就需要:I/O优化:在之前版本的多路复用技术中,每个线程都必须阻塞在recv中等待对方的请求。线程阻塞,系统速度也会下降。这时候就需要多路复用技术,使用select模型,把所有的等待(accept,recv)都放在主线程中,工作线程就不用再等待了。过了一段时间,访问网站的人越来越多,连select都有些力不从心,老板不断找你优化性能。这时候就需要将多路复用模型升级为epoll。select有3个缺点,epoll有3个优点。select底层使用数组来管理socket描述符,同时管理的socket数量是有上限的,一般不超过几千个。epoll使用树和链表来管理,同时管理的socket数量可以很大。select不会告诉你哪个socket有消息,你需要一一询问。epoll不用轮询直接告诉你谁收到了消息。select在进行系统调用时,还需要在用户空间和内核空间之间来回拷贝socket列表,在循环调用select时比较浪费。epoll在内核中统一管理套接字描述符,不需要来回复制。使用epoll多路复用技术开发3.0版本,您的网站可以同时处理很多用户请求。但是贪心的老板还是不满足,他不愿意升级硬件服务器,而是让你进一步提高服务器的吞吐量。研究之后发现,在之前的方案中,工作线程总是用完再创建,用完再关闭。当大量的请求到来时,线程不断地创建、关闭、创建、关闭,开销相当大。这时候就需要:线程池技术我们可以在程序启动后批量开启一波工作线程,而不是等有请求的时候再创建,使用一个公共的任务队列,当请求来的时候,发送给queue在任务的传递中,各个工作线程统一从队列中取出任务进行处理。这就是线程池技术。多线程技术的使用在一定程度上提高了服务器的并发能力,但同时,对于多线程之间的数据同步,往往需要使用互斥锁、信号、条件变量等手段来同步多个线程。这些重量级的同步方式往往会导致线程多次在用户态和内核态之间切换,系统调用,线程切换都是不小的开销。在线程池技术中,提到了一个通用的任务队列,每个工作线程都需要从中提取任务进行处理。这涉及在这个公共队列上同步多个工作线程。有没有什么轻量级的方案可以实现多线程安全访问数据?这时候就需要:无锁编程技术在多线程并发编程中,遇到公共数据时需要线程同步。这里的同步可以分为阻塞同步和非阻塞同步。阻塞同步很容易理解。我们操作系统提供的互斥锁、信号、条件变量等机制都是阻塞同步,其实质就是加“锁”。对应的非阻塞同步就是实现无锁同步。目前有三种技术方案:Wait-freeLock-freeObstruction-free这三种技术方案都是通过一定的算法和技术手段实现无阻塞等待实现同步,其中Lock-free应用最为广泛.Lock-free之所以能够得到广泛应用,是因为目前主流的CPU都提供了原子级的read-modify-write原语,也就是著名的CAS(Compare-And-Swap)操作。在Intelx86系列处理器上,是cmpxchg系列指令。//Lock-freedo{...}while(!CAS(ptr,old_data,new_data))我们经常看到的无锁队列、无锁链表、无锁HashMap等数据结构通过CAS操作,它的lock-free核心大部分来自于此。在日常开发中,适当使用无锁编程技术,可以有效减少多线程阻塞和切换带来的开销,提高性能。服务器上线一段时间,发现服务经常异常崩溃。经过排查,发现是工作线程代码的bug。一旦崩溃,整个服务不可用。于是你决定将工作线程和主线程分离到不同的进程中,工作线程的崩溃不能影响整体的服务。这时候有多个进程,就需要:进程间通信技术说到进程间通信,你能想到什么?Pipes命名管道推荐一篇掌握进程间通信的文章,这里就不赘述了。对于需要高频的本地进程之间大量的数据交互,最推荐共享内存的方案。现代操作系统普遍采用基于虚拟内存的管理方案。在这种内存管理方式下,每个进程都被强制隔离。程序代码中使用的内存地址是一个虚拟地址,由操作系统的内存管理算法预先分配并映射到对应的物理内存页。CPU在执行代码指令时,对访问的内存地址进行实时转换和翻译。从上图可以看出,虽然不同的进程使用的是同一个内存地址,但是在操作系统和CPU的配合下,实际存放数据的内存页是不同的。共享内存的进程间通信方案的核心是:如果将同一个物理内存页映射到两个进程地址空间,双方是否可以不进行拷贝直接读写?当然,共享内存只是最终的数据传输载体,双方需要借助信号、信号量等其他通知机制来实现通信。使用高性能的共享内存通信机制,多个服务进程可以愉快地工作。即使某个工作进程挂掉了,整个服务也不会瘫痪。很快,老板增加了需求,不再满足于只提供静态网页浏览,而是需要能够实现动态交互。这次老板还挺良心的,给你加了个硬件服务器。于是你用Java/PHP/Python等语言创建了一套网页开发框架,并设置了一个单独的服务来提供动态网页支持,并与原来的静态内容服务器协同工作。这时候你发现静态服务和动态服务之间经常需要通信。一开始,您使用基于HTTP的RESTful接口在服务器之间进行通信,但后来您发现以JSON格式传输数据效率低下,您需要一种更高效的通信方案。这时候你需要:RPC&&序列化技术什么是RPC技术?RPC的全称是RemoteProcedureCall,远程过程调用。在我们平时的编程中,函数是随时调用的。这些函数基本都位于本地,也就是当前进程中某个位置的一个代码块。但是如果要调用的函数不在本地,而是在网络上的某个服务器上怎么办?这就是远程过程调用的用武之地。从图中可以看出,通过网络进行的函数调用涉及参数的打包和解包、网络传输、结果的打包和解包等,其中数据的打包和解包需要依靠序列化技术来完成。什么是序列化技术?简单来说,序列化就是将内存中的对象转化为可以传输和存储的数据,这个过程的逆向操作就是反序列化。序列化&&反序列化技术可以实现本地和远程计算机内存对象的传递。就像把大象关进冰箱门一样,分三步:将本地内存对象编码成数据流,并通过网络传输上述数据流从内存中接收到的数据流构造对象序列化技术序列化有几个指标framework:是否支持跨语言使用,可以支持哪些语言,是否只是简单的序列化功能,包中不包含RPC框架序列化传输性能扩展支持能力(data字段增删改后)objects,前后兼容是否支持动态解析(动态解析是指不需要提前编译,可以根据得到的数据格式定义文件立即解析)下面是三种流行序列化的对比protobuf、thrift和avro框架:ProtoBuf:供应商:Google支持的语言ges:C++、Java、Python等动态支持:差,一般需要提前编译是否包含RPC:否简介:ProtoBuf是Google出品的序列化框架。成熟、稳定、功能强大,被各大厂商采用。它只是一个序列化框架,不包含RPC功能,但是可以和Google出品的GPRC框架一起使用,是后端RPC服务开发的黄金搭档。缺点是动态支持较弱,不过这个现象有待更新版本改善。总的来说,ProtoBuf是一个非常值得推荐的序列化框架。Thrift厂商:Facebook支持语言:C++,Java,Python,PHP,C#,Go,JavaScript等动态支持:差是否包含RPC:是简介:这是Facebook出品的RPC框架,包含二进制序列化方案,但Thrift自身的RPC和数据序列化是解耦的,你甚至可以选择XML和JSON等自定义数据格式。国内也有多家大型厂商在使用,其性能可与ProtoBuf相媲美。缺点和ProtoBuf一样,对动态解析的支持不是很友好。Avro支持语言:C、C++、Java、Python、C#等动态支持:良好是否包含RPC:是简介:这是一个源自Hadoop生态的序列化框架。自带RPC框架,也可以独立使用。与前两者相比,最大的优势在于支持动态数据分析。为什么我一直在说这个动态分析功能呢?在之前的一个项目经历中,轩辕遇到了三种技术选型,而这三种解决方案就摆在了我们的面前。需要一个C++开发的服务和一个Java开发的服务才能执行RPC。Protobuf和Thrift都需要通过“编译”将相应的数据协议定义文件编译成相应的C++/Java源码,然后合并到项目中一起编译分析。当时Java项目组的同学强烈反对这种做法。原因是这样编译的强大的业务代码被集成到他们业务独立的框架服务中,业务不断变化,不够优雅。最后,经过测试,我们最终选择了AVRO作为我们的解决方案。Java端只需要动态加载相应的数据格式文件就可以对获取到的数据进行分析,性能还不错。(当然,对于C++端,我还是选择提前编译。)由于你的网站支持动态能力,所以不可避免地要和数据库打交道,但是随着用户量的增长,你发现数据库越来越慢。这时候你需要:数据库索引技术想象一下,你手里拿着一本数学课本,但是目录已经被人撕了,现在你需要翻到三角函数的那一页,你该怎么办?没有目录,你只有两种方法,要么一页一页地翻,要么随便翻到三角函数那一页。数据库也是如此。如果我们的数据表没有“目录”,那我们就得扫描全表,查询符合条件的记录行,很烦人。因此,为了加快查询速度,需要为数据表设置一个目录。在数据库领域,这是索引。一般情况下,一个数据表都会有多个字段,所以可以根据不同的字段设置不同的索引。索引分类主键索引聚簇索引非聚簇索引主键,众所周知,就是唯一标识一条数据记录的字段(也有多个字段共同唯一标识一条数据记录的联合主键),而对应的是主键索引。聚集索引是指逻辑顺序与表记录的物理存储顺序一致的索引。一般来说,主键索引是符合这个定义的,所以一般来说,主键索引也是一种聚簇索引。但是,这也不是绝对的,不同的数据库,或者同一个数据库下不同的存储引擎,还是有区别的。聚集索引的叶子节点直接存储数据,也是数据节点,而非聚集索引的叶子节点不存储实际数据,需要二次查询。索引实现主要有三种:B+树哈希表位图其中,B+树用的最多,其特点是树的结点很多。与二叉树相比,这是一种多叉树,是一种扁平的胖树,降低树的深度有利于减少磁盘I/O的次数,适合数据库的存储特性.哈希表实现的索引也称为哈希索引,数据的位置是通过哈希函数实现的。哈希算法的特点是速度快,时间复杂度常阶,但其缺点是只适用于精确匹配,不适用于模糊匹配和范围搜索。位图索引比较少见。想象这样一个场景,如果某个字段只有几个可能的取值,比如性别、省份、血型等,如果用B+树作为这样一个字段的索引会怎样?会出现大量索引值相同的叶子节点,其实是一种存储的浪费。位图索引就是基于这一点优化的。字段值只有少量的限制项。当数据表中的列字段出现大量重复时,就是位图索引大显身手的机会了。所谓位图就是Bitmap,它的基本思想是为字段的每一个值创建一个二进制位图来标记数据表中每条记录的列字段是否是对应的值。指数虽好,但不可滥用。一方面,索引最终会存储在磁盘上,这无疑会增加存储开销。另外,更重要的是,数据表的增删改查一般都伴随着索引的更新,因此也会对数据库的写入速度产生一定的影响。您的网站现在访问量越来越大,同时在线人数也大大增加。但是大量用户的请求带来了后台程序对数据库的大量访问。渐渐地,数据库的瓶颈开始出现,已经无法支撑越来越多的用户。老板又一次把提升业绩的任务交给了你。缓存技术&&BloomFilter从内存数据的物理CPU缓存到网页内容的浏览器缓存,缓存技术在计算机世界中无处不在。面对目前的数据库瓶颈,缓存技术也可以用来解决。每次访问数据库都需要数据库进行查表(当然数据库本身也有优化措施),体现在底层进行一个或多个磁盘I/O,但是凡是涉及到I/O的都会减速。如果是一些经常使用但不经常变化的数据,为什么不把它缓存在内存中,这样就不用每次都找数据库,从而减轻数据库的压力呢?有需求就有市场,有市场就会有产品,以memcached、Redis为代表的内存对象缓存系统应运而生。缓存系统存在三个众所周知的问题:缓存穿透:缓存的目的是在某一层拦截对数据库存储层的请求。穿透就是拦截不成功,请求最终去了数据库,缓存没有生成它应该有的值。缓存击穿:如果把缓存理解为挡在数据库前面的一堵墙,用来“抵挡”对数据库的查询请求,所谓击穿就是在墙上打个洞。通常发生在一个热点数据缓存过期的时候,这个时候大量查询这个数据的请求就来了,大家纷纷涌向数据库。CacheAvalanche:了解了breakdown之后,雪崩就比较好理解了。俗话说,一次崩溃是一个人的崩溃,一次崩溃是一群人的崩溃。如果缓存的墙壁千疮百孔,那墙壁怎么能站得住呢?服枣丸。关于这三个问题更详细的阐述,推荐一篇文章缓存系统的三座大山是什么。有了缓存系统,我们可以在向数据库请求之前询问缓存系统是否有我们需要的数据。如果有并且满足需要,我们可以保存一个数据库查询。如果没有,我们将再次请求数据库。注意这里有个关键问题,如何判断我们要的数据是否在缓存系统中?更进一步,我们将这个问题抽象出来:如何快速判断一个数据量很大的集合中是否包含我们指定的数据?这时候布隆过滤器就该大显身手了,它就是为了解决这个问题而诞生的。布隆过滤器是如何解决这个问题的?首先,让我们回到上面的问题。这实际上是一个搜索问题。对于搜索问题,最常用的解决方案是搜索树和哈希表。因为这个问题有两个关键点:速度快和数据量大。首先要排除树状结构。哈希表可以实现常序性能,但是当数据量很大时,一方面要求哈希表的容量很大。另一方面,如何设计好的哈希算法能够实现如此大数据量的哈希映射也是一个难题。对于容量的问题,考虑到我们只需要判断对象是否存在,而不是获取对象,我们可以将哈希表的表项大小设置为1位,1表示存在,0表示不存在,这大大减少了哈希表的大小。表的容量。至于hash算法问题,如果我们对hash算法的要求较低,hash碰撞的概率就会增加。哪个hash算法容易冲突,那么获取的多,多个hash函数同时发生冲突的概率就小很多。Bloomfilter基于这样的设计思想:当设置对应的key-value时,根据一组hash算法的计算,对应的bit位置为1。但是当对应的key-value被删除时,对应的bit位置为1position不能设置为0,因为不能保证另一个key的某个hash算法也映射到同一个position。也正是因为如此,引入了布隆过滤器的另一个重要特性:布隆过滤器的存在不一定存在,但布隆过滤器的不存在不一定存在。贵公司网站内容越来越多,用户对快速全站搜索的需求越来越强烈。这时候就需要:全文检索技术对于一些简单的查询需求,传统的关系型数据库还是可以应付的。但是,一旦搜索需求变得复杂,比如基于文章内容关键词、多个搜索条件但逻辑组合等,数据库就会捉襟见肘。这时候就需要单独的索引系统来支持了。ElasticSearch(简称ES)是当今业界广泛使用的一个强大的搜索引擎。集全文检索、数据分析、分布式部署等优势于一身,成为企业级搜索技术的首选。ES采用RESTful接口,使用JSON作为数据传输格式,支持多种查询匹配,提供所有主流语言的SDK,简单易用。另外,ES往往会与另外两个开源软件Logstash和Kibana一起组成完整的日志采集、分析、展示的解决方案:ELK架构。其中,Logstash负责数据采集和分析,ElasticSearch负责搜索,Kibana负责可视化交互,成为很多企业级日志分析和管理的铁三角。无论我们如何优化,服务器的能力毕竟是有限的。公司业务发展迅速,原有服务器已经不堪重负,因此公司购买了多台服务器,并部署了原有服务的多个副本,以满足不断增长的业务需求。现在,有多个服务器为同一个服务提供服务,需要将用户的请求均衡地分配给各个服务器。上业务节点。与缓存技术一样,负载均衡技术也存在于计算机世界的各个角落。按照均衡的实现实体,可以分为软件负载均衡(如LVS、Nginx、HAProxy)和硬件负载均衡(如A10、F5)。根据网络层次,可分为四层负载均衡(基于网络连接)和七层负载均衡(基于应用内容)。根据平衡策略算法,可以分为循环平衡、哈希平衡、权重平衡、随机平衡或结合这些算法的平衡。对于现在遇到的问题,可以使用nginx来实现负载均衡。nginx支持roundrobin、weight、IPhash、最小连接数、最短响应时间等多种负载均衡配置。轮询upstreamweb-server{server192.168.1.100;server192.168.1.101;}权重upstreamweb-server{server192.168.1.100weight=1;server192.168.1.101weight=2;}IP哈希值upstreamweb-server{ip_hash;server192.168.1.100weight=1;server192.168.1.101weight=2;}最小连接数upstreamweb-server{ip_hash;server192.168.1.100weight=1;server192.168.1.101weight=2;}最小响应时间upstreamweb-server{server192.168.1.100weight=1;server192.168.1.101weight=2;fair;}总结高性能是一个永恒的话题,涉及的技术和知识远不止上面列举的这些。从物理硬件CPU、内存、硬盘、网卡,到软件层面的通信、缓存、算法、架构等各个环节的优化,才是高性能之路。路漫漫其修远兮,上下寻觅。本文转载自微信公众号“编程技术宇宙”,可通过以下二维码关注。转载本文请联系编程技术宇宙公众号。
