当前位置:首页 > 数码 > 技巧和技术以优化您的运行程序-高性能网络编程-Linux (技巧和技术以及区别)

技巧和技术以优化您的运行程序-高性能网络编程-Linux (技巧和技术以及区别)

admin3个月前 (04-29)数码17

上一篇文章讲了高性能编程的工具,这一篇咱们基于前面的一些常识点和工具来聊一下下的性能优化(本常识点分为两篇,以后关键引见CPU和内存性能优化)。

第一部分:CPU和内存性能度量

系统调用

这张图论述一个运行程序须要经过这些模块调用,关于性能每一部分都或许会有影响,那么咱们先须要了解每个模块须要怎样度量?

1、CPU度量

(1)CPU经常使用率

CPU经常使用率是最直观形容以后服务形态的状况,假设CPU经常使用率过高,则示意以后遇到了性能瓶颈,其中过高的这个详细值在线上普通是70%-90%之间,要么扩容服务,要么就排查性能疑问。

(2)用户进程消耗CPU

用户进程消耗CPU是经常出现的状况,往往和业务代码或许经常使用的库关系,比如少量的循环,JSON解析大包等,在用户代码层有很多耗CPU的操作,都会体现CPU经常使用率意外,定位其疑问可以经过以下方式:

(3)内核消耗CPU

消耗CPU不止用户进程,还包括内核进程,系统调用等外核消耗CPU,或许的要素有少量的内存拷贝,锁,少量的高低文切换等等,详细剖析和上方相似:

(4)CPU期待

CPU破费在期待上的时期,关键是看能否少量的IO造成,也可以经过top定位详细进程,而后跟踪和剖析该进程或许线程的网络调用状况。

(5)Nice消耗CPU

形容的是破费的re-nicing进程上时期占比,关键是更改了进程的口头顺序或许优先级。

(6)平均负载

平均负载是一个判别系统快慢的关键要素,或许往往不是某个进程惹起的,关键有两个目的:

假设被阻塞,平均负载就会参与,可以经过uptime检查,往往负载参与这个时刻须要优化代码或许参与机器资源。

(7)运转进程

以后运转和曾经在队列中的进程数,往往进程过多会造成CPU调度忙碌,比如之前多进程的Server,所以可以依据以后CPU的核数选择进程个数,普通忙碌状况下的进程不倡导超越2倍CPU(以后闲暇的进程也不宜过大,倡导不超越10倍)。

(8)阻塞进程

阻塞进程是以后未到达口头条件的进程,和上方的CPU期待事情对应,普通是IO疑问造成,比如写文件数据过慢,或许socket读写数据未抵达等等状况,如何剖析呢?可以经过strace跟踪系统调用剖析。

(9)高低文切换

在系统上出现高低文切换的状况,也是判别CPU负载的关键要素,少量的高低文切换或许和少量终止或许锁关系,高低文切换会造成CPU的缓存被刷新,数据须要从内存换入换出等。

排查打算是经过perf或许vmstat工具查问,比如vmstat输入(也可以经过vmstat-s检查):

[root@VM-16-16-~]#vmstat22procs-----------memory-------------swap-------io-----system--------cpu-----rbswpdfreebuffcachesisobiboincsussyidwast00029840496824118973200134100099000002982849682411897360002147601315109910

其中system包括:CPU在内核态运转消息,包括in终止次数,cs高低文切换次数。

(10)终止

终止蕴含硬终止和软终止,硬终止是外设处置环节中发生的,经过配件控制器通知cpu的形态变动,而软终止是经过模拟硬终止的一种信号处置方式,终止过多会造成CPU破费一些时期相应终止,这里也会影响性能,如何排查?经过命令行mpstat-PALL52可以检查:

[root@VM-16-16-centos~]#mpstat-PALL52Linux4.18.0-348.7.1.el8_5.x86_64(VM-16-16-centos)2023年08月19日_x86_64_(2CPU)10时02分15秒CPU%usr%nice%sys%iowt%irq%soft%steal%guest%gnice%idle10时02分20秒all0.700.000.800.500.000.000.000.000.0098.0010时02分20秒00.600.000.800.200.000.000.000.000.0098.4010时02分20秒10.800.000.800.800.000.000.000.000.0097.60

其中输入中蕴含的:

2、内存度量

(1)闲暇内存

经过free咱们能看到以后内存状况:

[root@VM-0-11-centos~]#freetotalusedfreesharedbuff/cacheavailableMem:388019240722871302487227599403182872Swap:000

从上方可以看出,free的内存越大越好,这样有残余足够多的物理内存可以经常使用。

Swap如上方说的是替换空间的内存数据,是linux为了监禁一部分物理内存将数据暂时保留在Swap空间中,经过vmstat-s检查详细消息如下:

[root@VM-16-16-centos~]#vmstat-s1860492Ktotalmemory274936Kusedmemory701576Kactivememory707432Kinactivememory299040Kfreememory96824Kbuffermemory1189692Kswapcache0Ktotalswap0Kusedswap0Kfreeswap12318019non-niceusercputicks124590niceusercputicks11848347systemcputicks2844992141idlecputicks4677889IO-waitcputicks0IRQcputicks208152softirqcputicks0stolencputicks15879112pagespagedin985253486pagespagedout0pagesswedin0pagesswappedout1330511648interrupts260667271CPUcontextswitches1678004734boottime58996940forks

其中假设pagesswappedin和pagesswappedout每秒增长很多大,示意内存上遇到了瓶颈,须要更新机器的内存或许优化代码。

在Linux中,同伴系统是以页为单位治理和调配内存,但是事实的需求却以字节为单位,假设咱们须要放开20Bytes,总不能调配一页吧?那岂不是重大糜费内存。那么该如何调配呢?Slab调配器就应运而生了,专为小内存调配而生,Slab调配器调配内存以Byte为单位,但是Slab调配器并没有脱离同伴系统,而是基于同伴系统调配的大内存进一步细分红小内存调配,其作用如下:

假设要排查Slab的详细消息,可以经过slabtop或许cat/proc/slabinfo,输入如下(口头slabtop):

Active/TotalObjects(%used):1074142/1101790(97.5%)Active/TotalSlabs(%used):39843/39843(100.0%)Active/TotalCaches(%used):100/130(76.9%)Active/TotalSize(%used):250498.05K/253182.16K(98.9%)Minimum/Average/MaximumObject:0.01K/0.23K/8.00KOBJSACTIVEUSEOBJSIZESLABSOBJ/SLABCACHESIZENAME445302445302100%0.10K114183945672Kbuffer_head24910224907199%0.19K118622147448Kdentry836168355799%1.00K52261683616Kext4_inode_cache632404075464%0.04K6201022480Kext4_extent_status543765429799%0.57K38841431072Kradix_tree_node295472948799%0.19K1407215628Kkmalloc-192285442848899%0.06K446641784Kkmalloc-642162421624100%0.12K636342544Kkernfs_node_cache2040020400100%0.05K24085960Kshared_policy_node162761598998%0.58K12521310016Kinode_cache1091410914100%0.04K107102428Kselinux_inode_security77767776100%0.21K432181728Kvm_area_struct7232392154%0.12K22632904Kkmalloc-12853765376100%0.02K2125684Kkmalloc-1653765376100%0.03K42128168Kkmalloc-3251205120100%0.01K1051240Kkmalloc-84344430699%0.66K362122896Kproc_inode_cache40964096100%0.03K32128128Kjbd2_revoke_record_s38223822100%0.09K9142364Kkmalloc-963417321794%0.08K6751268Kanon_vma33443344100%0.25K20916836Kkmalloc-25631363136100%0.06K4964196Kext4_free_data21902190100%0.05K3073120Kavc_xperms_node21122112100%1.00K132162112Kkmalloc-1024

咱们可以从以上的消息中判别那些内核模块内存调配较多(比如OBJSIZE过大),进而剖析模块的性能瓶颈。

3、方法论

以下是我参照USE方法论整顿排查性能度量目的流程,其中最大应战点在于如何发现子模块中的疑问并且剖析疑问?后续可以独自写一篇剖析。

方法论

第二部分:系统层优化

(1)缓存

#defineN2048longtimecost(clock_tt1,clock_tt2){longelapsed=((double)t2-t1)/CLOCKS_PER_SEC*1000;returnelapsed;}intmain(intargc,char**argv){chararr[N][N];{clock_tstart,end;start=clock();for(inti=0;i<N;i++){for(intj=0;j<N;j++){arr[i][j]=0;}}end=clock();cout<<"timecost:"<<timecost(start,end)<<endl;}{clock_tstart,end;start=clock();for(inti=0;i<N;i++){for(intj=0;j<N;j++){arr[j][i]=0;}}end=clock();cout<<"timecost:"<<timecost(start,end)<<endl;}}

先来看一下上方一段代码,有两个timecost输入,大家感觉哪共性能更高呢?运转输入:

timecost:11timecost:67

可见第一段代码性能比第二段代码性能高6倍,之前了解过CPU缓存的应该都知道其中的原理!先看看这张图:

性能

CPU分位多级缓存,每一级比上一级耗时都差几倍,所以假设写的代码读取数据能命令更初级缓存,那么性能人造就会提高,咱们再看代码访问array[i][j]和array[j][i]的差异,array[i][j]是顺序访问,CPU读取数据时,前面的元素曾经载入缓存中了,而array[j][i]是距离访问,或许每次都不能命中缓存,既然明白了缓存的作用,那如何判别咱们代码能否由于缓存未命中而损失性能呢?经常使用工具perf,口头perfstat-ecache-references-ecache-misses./a.out,输入如下:

[root@VM-0-11-centos~]#perfstat-ecache-references-ecache-misses./a.out//第一段代码Performancecounterstatsfor'./a.out':6,115,254cache-references13,450cache-misses//第二段代码Performancecounterstatsfor'./a.out':913,732cache-references17,954cache-misses

因此,遇到这种遍历访问数组的状况时,依照内存规划顺序访问将会带来很大的性能优化。

(2)分支预测

#defineN128*1024*10intmain(intargc,char**argv){ofstreamofs;unsignedchararr[N];for(longi=0;i<N;i++)arr[i]=rand()%256;ofs.open("rand",::out|ios::binary);ofs.write((constchar*)arr,N);ofs.close();sort(arr,arr+N);ofs.open("sort",ios::out|ios::binary);ofs.write((constchar*)arr,N);ofs.close();{unsignedchararr[N];ifstreamifs;ifs.open("rand");ifs.read((char*)arr,N);clock_tstart,end;start=clock();for(longi=0;i<N;i++){if(arr[i]<128)arr[i]=0;}end=clock();cout<<"timecost:"<<timecost(start,end)<<endl;}{unsignedchararr[N];ifstreamifs;ifs.open("sort");ifs.read((char*)arr,N);clock_tstart,end;start=clock();for(longi=0;i<N;i++){if(arr[i]<128)arr[i]=0;}end=clock();cout<<"timecost:"<<timecost(start,end)<<endl;}}

以上代码做了两个操作,:一是循环遍历数组,判别每个数字能否小于128,假设小于则把元素的值置为0;二是将数组排序。那么,先排序再遍历速度快,还是先遍历再排序速度快呢?其输入结果:

timecost:11timecost:3

从耗时可以看出排序后的数据性能要比未排序的性能高3倍,为什么?咱们可以经过perfstat-ebranch-loads,branch-load-misses./a.out取得输入():

//第一段代码Performancecounterstatsfor'./a.out':263,372,189branch-loads89,137,210branch-load-misses//第二段代码Performancecounterstatsfor'./a.out':261,134,898branch-loads137,210branch-load-misses

可见分支预测关于性能优化有很大的影响,假设咱们遇到相似的疑问,可以经过优化代码优化指令缓存的命中率。

(3)多核

从CPU的缓存架构图可以看出,多核的CPU的L1,L2缓存是每颗外围独享的,假设启动某个线程,依据调度时期片,或许线程在某个时辰运转的外围1上,下一个调度时期片或许就在外围2上,这样L1,L2缓存存在不命中的疑问,但是假设咱们能让线程或许进程独立的跑在一个外围上,这样就不须要将缓存换入缓出,实践上就可以优化性能,在Linux系统中确实提供了这种才干,经过sched_setaffinity可以绑定CPU外围,而后perf检查cpu-migrations的CPU迁徙次数发现会缩小,这里就不开展代码了,有兴味的可以钻研一下的worker_cpu_affinity性能,设置Nginx进程与CPU启动绑定的。

(4)向量化优化(SIMD)

SIMD全称single-instructionmultiple-data(单指令少数据),在传统的计算机架构中,CPU一次性只能处置一个数据元素,但是,许多义务触及对少量数据口头相反的操作,例如对数组中的一切元素启动加法、乘法或逻辑操作等,SIMD编程经过向CPU提供专门的指令集,使得CPU能够同时对多个数据元素口头相反的操作,这种处置方式特意适宜触及向量、矩阵、图像、音频和视频等数据的计算,经常使用样例如下:

#include<stdio.h>#include<stdlib.h>#include<time.h>#include<emmintrin.h>#defineMAX200000#defineCOUNT100voidmul_test1(float*buf){for(inti=0;i<MAX;++i){buf[i]=buf[i]*buf[i];}}voidmul_test2(float*buf){for(inti=0;i<MAX;i+=4){_mm_storeu_ps(buf+i,_mm_mul_ps(_mm_loadu_ps(buf+i),_mm_loadu_ps(buf+i)));}}intmain(){floatbuf[MAX];for(inti=0;i<MAX;++i){buf[i]=(float)(rand()%1000);}{clock_tstart,end;floatduration;for(inti=0;i<COUNT;++i){start=clock();mul_test1(buf);end=clock();duration+=((double)(end-start))/CLOCKS_PER_SEC;}printf("costtime=%.3fn",duration*1000/COUNT);}{clock_tstart,end;floatduration;for(inti=0;i<COUNT;++i){start=clock();mul_test2(buf);end=clock();duration+=((double)(end-start))/CLOCKS_PER_SEC;}printf("costtime=%.3fn",duration*1000/COUNT);}return0;}

从输入来看,SIMD在性能上比通用写法要快很多,如下(这里编译时封锁优化选项g++O1/O2/O3等,防止编译器优化可以对比出性能):

costtime=0.513costtime=0.274

(5)PGO和LTO等编译器优化

通常在代码编译时期,编译器会做优化有很多,除了gcc经过-O1-O2-O3,内联,尾递归等优化外,如今了解比拟多的是PGO和LTO:

PGO优化样例:

#include<time.h>#include<iostream>#include<unistd.h>#include<stdlib.h>usingnamespacestd;longm=502000000;chararr[4]={'1','2','3',0};longtimecost(clock_tt1,clock_tt2){longelapsed=((double)t2-t1)/CLOCKS_PER_SEC*1000;returnelapsed;}longtest(){longsum=0;inta=0;for(a=0;a<m;++a){sum+=atoi(arr+(a%2));}returnsum;}intmain(intargc,constchar*argv[]){clock_tstart,end;start=clock();longsum=test();end=clock();cout<<"sum:"<<sum<<",timecost:"<<timecost(start,end)<<endl;return0;}//口头如下命令:g++test5.cc-O2-ooriging++test5.cc-O2-fprofile-generate-otrace./traceg++test5.cc-O2-fprofile-use-ooptimized./origin./optimized//输入结果:[root@VM-0-11-centos~]#./tracesum:36646000000,timecost:4710[root@VM-0-11-centos~]#g++test5.cc-O2-fprofile-use-ooptimized[root@VM-0-11-centos~]#./optimizedsum:36646000000,timecost:4670[root@VM-0-11-centos~]#./originsum:36646000000,timecost:4710

从输入的结果看优化一小部分性能,假设程序愈加复杂,性能优化会更多,假设有兴味也可以了解关于微软的团队经常使用ProfileGuidedOptimization(PGO)和Link-timeOptimization(LTO)来优化Linux内核和优化性能。

2、内存

(1)内存池

内存池或许对象池是高性能编程一种关键的优化方式,假定在实践代码开发环节中,须要频繁放开和监禁内存4个字节的内存,与其把这4字节监禁给操作系统,不如先缓存着放进内存池里,依然当作用户态内存留上去,进程再次放开4字节内存时就可以间接复用,这样速度快了很多,其中ptmalloc,tcmalloc和jemalloc库都是经过相似方式成功,这里为了极速了解,咱们间接tcmalloc为例剖析。

tcmalloc

(2)一些场景下可以优先经常使用栈

从以下代码咱们验证一下堆上和栈上调配内存,看看性能对比(这里取出了编译器优化):

voidtest_on_stack(){inta=10;}voidtest_on_heap(){int*a=(int*)malloc(sizeof(int));*a=10;free(a);}//输入如下:timecost:258timecost:6664

可见栈上调配内存性能更高,为什么?这里关键是栈是编译期提早调配好了,而且栈是顺序访问,再者栈的数据可以间接到寄存器映射,还有一个最大的长处是线程在栈是独立的,访问的数据是无需加锁的,所以在实践写代码环节中,关于占用空间少且频繁访问的都可以经过栈上内存调配来操作。顺便说以下,golang为了更好的性能,底层代码中很多都是经过栈调配,当剖析非逃逸的变量,即使经常使用make调配内存也是在栈上(详细可以读读golang的源码)。

第三部分:锁

多线程状况下,为了保障临界区数据分歧性,往往经过加锁处置疑问,包括互斥锁,自旋锁,失望锁等等,当然不同场景的方式不一样,那上方咱们来引见几种高性能状况下锁的经常使用。

(1)互斥锁与自旋锁

互斥锁:当你无法判别锁住的代码会口头多久时,应该首选互斥锁,互斥锁是一种独占锁,但是互斥锁有对应的疑问是:内核会不时尝试失掉锁,假设失掉不到就会休眠,只要失掉到了才会口头逻辑,这里要留意的是在线程失掉锁失败时,会参与两次高低文切换的老本,从运转中切换为休眠,以及锁监禁时从休眠形态切换为运转中,这种频繁的高低文切换和休眠在高并发服务无法容忍的行为;

自旋锁:通常假设关于一些耗时很短的操作,可以尝试经常使用自旋锁,自旋锁比互斥锁快得多,由于它经过CPU提供的CAS函数(全称CompareAndSwap),在用户态代码中成功加锁与解锁操作,比如while(!(CAS(lock,0,args))){...},CAS是原子操作,有三个参数(内存位置V、预期原值A、新值B),其中这段代码假设lock==0则更新lock=args,否则继续循环。但是自旋锁会面临ABA的疑问(线程1读到A值,但是线程2抢占将A改为B,再修正回A,而后线程1抢占就会以为没有修正,而后继续口头),所以在为了谋求高性能,同时也要思考各个锁的缺陷,从而防止BUG;

读写锁:假设业务场景能明白读写,可以选用经常使用读写锁,当写锁未被锁住时,读锁可以成功多线程并发,当写锁锁住后,读锁阻塞,所以读写锁真正施展长处的场景,肯定是读多写少的场景,否则读锁将很难并发持有;

(2)失望锁

什么是失望锁?基于失望的状况,假定以为数据普通状况下不会形成抵触,所以在数据启动提交更新的时刻,才会正式对数据的抵触与否启动检测。

失望锁罕用成功方式经过版本号,每个数据记载都有一个对应的版本号,事务在更新数据时,先读取数据的以后版本号,并在提交时审核该版本号能否出现变动,假设没有变动,说明操作是安保的,可以提交,假设出现变动,就须要启动回滚或重试操作。

从失望锁的场景可以看出,关于读多写少的状况下,失望锁是能缩小抵触,优化性能。

高性能网络编程

(3)无锁编程

为了高性能,咱们前面提到缩小高低文切换,缩小临界区抵触,其中锁是最大的阻碍之一,假设能经过无锁编程,这样能优化性能。

失望锁是一种无锁编程,上方曾经引见了,经过版本号或许CAS缩小抵触,能成功不加锁;

线程部分变量,经过在GCC定义__thread变量,成功线程部分存储,存取效率可以和全局变量相比,__thread变量每一个线程有一份独立实体,各个线程的值互不搅扰,某些场景下可以经过操作线程内的部分变量后,一致同步到全局变量,成功不加锁或许缩小锁;

临界区Hash,之前在业务场景中遇到须要频繁操作指定全局数据,但是线程之前操作的数据却在某个时辰是独立,这种场景可以将临界区的数据Hash到各个槽中,当线程须要操作数据,可以先取槽的位置,而后到对应的槽位上操作数据即可,这样缩小锁锁住的数据区域或许间接不加锁可以优化性能;

将性能设计为复线程,假设是复线程程序人造就不须要加锁了,比如Redis6.x之前的版本都是复线程处置,这样数据结构便捷,防止高低文切换等。


如何学习linux平台上的网络编程

呵呵,你问对人啦,我就是学习了C语言的基础知识(谭浩强的那本书),然后学习了网络编程。 现在在做linux云计算你需要找到《UNIX网络编程第1卷:套接口API》看这个书的同时,你从网上找些最简单的网络通讯程序小例子看看,对比书的介绍,很快你就会做个简单的聊天工具。 然后:《UNIX网络编程第2卷:进程间通信》尝试做个具备一定并发量的Server端程序,使用多线程方式。 用这本书做你的学习的总线,网上搜索学习做验证,测试。 相信很快可以学会。 如果对你有帮助,请给分哦,谢谢!

性能优化基础:深入理解Linux网络

Linux网络:基石与深度洞察

Linux操作系统的核心优势之一在于其强大的网络功能,它凭借TCP/IP模型连接全球的计算设备,实现了跨系统的无缝通信。OSI七层模型虽然理论精巧,但在实践中Linux主要依赖于TCP/IP的四层架构:应用层承载HTTP、FTP等协议,传输层包括TCP和UDP,网络层关注IP和ICMP,而网络接口层则负责MAC寻址和物理传输,数据包在每一层按照协议栈进行精细处理。

MTU(最大传输单元)是关键参数,决定数据包的大小。Linux的ifconfig命令能帮助我们查看网卡的MTU值,过大过小都会影响网络性能。一个理想的MTU值可以减少分片,提高数据包吞吐量。内核网络栈以四层结构为基础,应用层通过系统调用和套接字机制与底层交互,底层则由网卡驱动和物理设备负责核心数据处理,协议栈的逻辑通过软中断进行管理。

提升网络效率的实战指南

推荐视频《Linux C/C++高性能网络编程》深入探讨TCP/IP等技术,为服务器架构师提供了丰富的学习资源,加入Q群获取更多实用资料。

接下来,让我们详细了解Linux网络数据包的收发过程:

在分析网络性能时,务必关注ifconfig和ip命令的输出。这些命令揭示了接口配置、状态、MTU值、IP/MAC地址以及关键的网络统计信息,如丢包、碰撞、接收和发送队列等,这些都是诊断网络问题的重要线索。

套接字接口作为内核与应用层的桥梁,netstat和ss(推荐)提供了详尽的网络连接和统计信息,如套接字状态、地址、队列长度等,帮助我们实时监控网络流量和状态。

最后,ping命令是网络测试的得力工具,它显示了连通性、往返延时(如平均RTT)和丢包率等关键指标,比如在3个网络包测试中,主机192.168.2.129与我们的主机之间平均RTT仅为0.026毫秒,显示出出色的连通性和低延时。

在追求网络性能的道路上,理解这些基础概念和工具至关重要。通过优化MTU、监控网络状态和调整套接字设置,我们能够提升Linux网络的效率,确保数据传输的流畅和稳定。

免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。

标签: 网络编程