关于架构:软件教练说性能优化与性能设计相亲相爱的一对

46次阅读

共计 11061 个字符,预计需要花费 28 分钟才能阅读完成。

摘要:性能优化通常是在现有零碎和代码根底上做改良,考验的是开发者反向修复的能力,而性能设计考验的是设计者的正向设计能力,但性能优化的办法能够领导性能设计,两者互补。

性能优化是指在不影响正确性的前提下,使程序运行得更快,它是一个十分宽泛的话题。

优化有时候是为了降低成本,但有时候,性能能决定一个产品的成败,比方游戏服务器的团战玩法须要单服达到肯定的同时在线人数能力撑持起这类玩法,而电信软件的性能往往是竞标的外围竞争力,性能关乎商业成败。

软件产品多种多样,影响程序执行效率的因素很多,因而,性能优化,特地是对不相熟的我的项目做优化,不是一件容易的事。

性能优化可分为 宏观 宏观 两个层面。宏观层面包含架构重构,而宏观层面,则包含算法的优化,编译优化,工具剖析,高性能编码等,这些办法是有可能独立于具体业务逻辑,因此有更加宽泛的适应性,且更易于施行。

具体到性能优化的方法论,首先,应建设度量,你度量什么,你失去什么。所以,性能优化测试后行,须基于数据而不能凭空猜测,这是做优化的一个根本准则。搭建实在的压测环境,或者迫近实在环境,有时候是艰难的,也可能十分消耗工夫,但它仍然是值得的。

有许多工具能帮忙咱们定位程序瓶颈,有些工具能做很敌对的图形化展现,定位问题是解决问题的前置条件,但定位问题可能不是最难的,剖析和优化才是最耗时的关键环节,批改之后,要再回归测试,验证是否如预期般无效。

什么是高性能程序?架构致广远、实现尽精微。

架构优化的要害是辨认瓶颈,这类优化有很多套路:比方通过负载平衡做分布式革新,比方用多线程协程做并行化革新,比方用音讯队列做异步化和解耦,比方用事件告诉代替轮询,比方为数据拜访减少缓存,比方用批处理 + 预取晋升吞吐,比方 IO 与逻辑拆散、读写拆散等等。

架构调整和优化尽管收效很大,却因受限于各种事实因素,因此并不总是可行。

能不做的尽量不做、必须做的高效做 是性能优化的一个基本法令,晋升解决能力 升高计算量 可视为性能优化的两个方向。

有时候,咱们不得不从细节的维度去改良程序。通常,咱们应该应用简略的数据结构和算法,但如有必要,就应踊跃应用更高效的构造和算法,不止逻辑构造,实现构造(存储)同样影响执行效率;分支预测、反馈优化、启发性以及基于机器学习编译优化的成果日益凸显;熟练掌握编程语言深刻理解规范库实现能帮忙咱们躲避低性能陷阱;深刻细节做代码微调甚至指令级优化有时候也能获得意想不到的成果。

有时候,咱们须要做一些替换,比方用空间置换工夫,比方就义一些通用性可读性换取高性能,咱们只该当在十分必要的状况下才这么做,它是 衡量的艺术

1、架构优化

通常零碎的 throughput 越大,latency 就越高,但过高的 latency 不可承受,所以架构优化不是一味谋求 throughput,也须要关注 latency,谋求可承受 latency 下的高 throughput。

负载平衡

负载平衡其实就是解决一个分活的问题,对应到分布式系统,个别在逻辑服的后面都会安放一个负载均衡器,比方 NGINX 就是经典的解决方案。负载平衡不限于分布式系统,对于多线程架构的服务器外部,也须要解决负载平衡的问题,让各个 worker 线程的负载平衡。

多线程、协程并行化

尽管硬件架构的复杂化对程序开发提出了更高的要求,但编写充分利用多 CPU 多核个性的程序能取得令人惊叹的收益,所以,在同样硬件规格下,基于多线程 / 协程的并行化革新仍然值得尝试。

多线程不可避免要面临资源竞争的问题,咱们的设计指标应该是充分利用硬件多执行外围的劣势,缩小期待,让多个执行晦涩快的奔跑起来。

对于多线程模型,如果把每一个要干的活形象为一个 task,把干活的线程形象为 worker,那么,有两种典型的设计思路,一种是对 task 类型做出划分,让一类或者一个 worker 去干特定的 task,另一种是让所有 worker 去干所有 task。

第一种划分,能缩小数据争用,编码实现也更简略,只须要辨认无限的竞争,就能让零碎工作的很好,毛病是工作的工作量很可能不同,有可能导致有些 worker 繁忙而另一些闲暇。

第二种划分,长处是能平衡,毛病是编码复杂性高,数据竞争多。

有时候,咱们会综合上述两种模式,比方让独自的线程去做 IO(收发包)+ 反序列化(产生 protocol task),而后启动一批 worker 线程去解决包,两头通过一个 task queue 去连贯,这即是经典的生产者消费者模型。

协程是一种用户态的多执行流,它基于一个假如,即用户态的工作切换老本低于零碎的线程切换。

告诉代替轮询

轮询即不停询问,就像你每隔几分钟去一趟宿管那里查看是否有函件,而告诉是你通知宿管阿姨,你有信的时候,她打电话告诉你,显然轮询消耗 CPU,而告诉机制效率更高。

增加缓存

缓存的理论依据是 局部性原理

个别零碎的写入申请远少于读申请,针对写少读多的场景,很适宜引入缓存集群。

在写数据库的时候同时写一份数据到缓存集群里,而后用缓存集群来承载大部分的读申请,因为缓存集群很容易做到高性能,所以,这样的话,通过缓存集群,就能够用更少的机器资源承载更高的并发。

缓存的命中率个别能做到很高,而且速度很快,解决能力也强(单机很容易做到几万并发),是现实的解决方案。

CDN 实质上就是缓存,被用户大量拜访的动态资源缓存在 CDN 中是目前的通用做法。

音讯队列

音讯队列、消息中间件是用来做写申请异步化,咱们把数据写入 MessageQueue 就认为写入实现,由 MQ 去迟缓的写入 DB,它能起到削峰填谷的成果。

音讯队列也是解耦的伎俩,它次要用来解决写的压力。

IO 与逻辑拆散、读写拆散

IO 与逻辑拆散,这个后面曾经讲了。读写拆散是一种数据库应答压力的习用措施,当然,它也不仅限于 DB。

批处理与数据预取

批处理 是一种思维,分很多种利用,比方多网络包的批处理,是指把收到的包攒到一起,而后一起过一遍流程,这样,一个函数被屡次调用,或者一段代码反复执行多遍,这样 i -cache 的局部性就很好,另外,如果这个函数或者一段里要拜访的数据被屡次拜访,d-cache 的局部性也能改善,天然能晋升性能,批处理能减少吞吐,但通常会增大提早。

另一个批处理思维的利用是日志落盘,比方一条日志大略写几十个字节,咱们能够把它缓存起来,攒够了一次写到磁盘,这样性能会更好,但这也带来数据失落的危险,不过通常咱们能够通过 shm 的形式躲避这个危险。

指令预取是 CPU 主动实现的,数据预取是一个很有技巧性的工作,数据预取的根据是预取的数据将在接下来的操作中用到,它合乎空间局部性原理,数据预取能够填充流水线,升高访存期待,但数据预取会侵害代码,且并不总如预期般无效。

哪怕你不减少预取代码,硬件预取器也有可能帮你做预取,另外 gcc 也有编译选项,开启它会在编译阶段主动插入预取代码,手动减少预取代码须要小心解决,机会的抉择很重要,最初肯定要基于测试数据,另外,即便预取体现很好,但代码批改也有可能导致成果衰减,而且预取语句执行自身也有开销,只有预取的收益大于预取的开销,且 CACHE-MISS 很高才是值得的。

2、算法优化

数据量小的汇合上遍历查找即可,但如果循环的次数过百,便须要思考用更快的查找构造和算法替换蛮力遍历,哈希表,红黑树,二分查找很罕用。

哈希(HASH)

哈希也叫散列,是把任意长度的输出通过散列算法变换成固定长度的输入,该输入就是散列值,也叫摘要。比方把一篇文章的内容通过散列生成 64 位的摘要,该过程不可逆。

这种转换是一种压缩映射,也就是,散列值的空间通常远小于输出的空间,不同的输出可能会散列成雷同的输入,所以不可能从散列值来确定惟一的输出值,但如果输入的位数足够,散列成雷同输入的概率十分十分小。

字符串的比拟有时会成为耗费较大的操作,尽管 strcmp 或者 memcpy 的实现用到了很多减速和优化技巧,但实质上它还是一一比拟的形式。

字符串比拟的一个改良计划就是哈希,比拟哈希值(通常是一个 int64 的整数)而非比拟内容能快很多,但须要为字符串提前计算好哈希值,且须要额定的空间保留哈希值,另外,在哈希值相等的时候,还须要比拟字符串,但因为抵触的概率极低,所以后续的字符串比拟不会很屡次。

这样不肯定总是更高效,但它提供了另一个思路,你须要测试你的程序,再决定要不要这样做。

另一个哈希的用法是哈希表,哈希表的经典实现是提前开拓一些桶,通过哈希找到元素所在的桶(编号),如果抵触,再拉链解决抵触。

为了缩小抵触常常须要开拓更多的桶,但更多的桶须要更大的存储空间,特地是元素数量不确定的时候,桶的数量抉择变得两难,随着装载的元素变多,抵触加剧,在扩容的时候,将须要对已存在的元素从新哈希,这是很费的点。

哈希表的抵触极其状况下会进化成链表,当初构想的疾速查找变得不再可行,HashMap 是一般哈希表的改进版,联合哈希和二叉均衡搜寻树。

另一个罕用来做查找的构造是红黑树,它能确保最坏状况下,有 logN 的工夫复杂度,但红黑树的查找过程须要沿着链走,不同结点内存通常不间断,CACHE 命中性常常很差,红黑树的中序遍历后果是有序的,这是哈希表不具备的,另外,红黑树不存在哈希表那般预估容量难的问题。

基于有序数组的二分查找

二分查找的工夫复杂度也是 logN,跟红黑树统一,但二分查找的空间局部性更好,不过二分查找有束缚,它只能在有序数组上进行,所以,如果你须要在固定的数据汇合(比方配置数据)做查找,二分查找是个不错的抉择。

跳表(Skip List)

跳表减少了向前指针,是一种多层构造的有序链表,插入一个值时有肯定概率降职到下层造成间接的索引。

跳表是一个随机化的数据结构,本质就是一种能够进行二分查找的有序链表。跳表在原有的有序链表下面减少了多级索引,通过索引来实现疾速查找。跳表不仅能进步搜寻性能,同时也能够进步插入和删除操作的性能。

跳表适宜大量并发写的场景,能够认为是随机均衡的二叉搜寻树,不存在红黑树的再均衡问题。Redis 弱小的 ZSet 底层数据结构就是哈希加跳表。

相比哈希表和红黑树,跳表用的不那么多。

数据结构的实现优化

咱们通常只会讲数据的逻辑构造,但数据的实现(存储)构造也会影响性能。

数组在存储上肯定是逻辑地址间断的,但链表不具备这样的特点,链表通过链域寻找邻近节点,如果相邻节点在地址上发散,则沿着链域拜访效率不高,所以实现上能够通过从独自的内存配置器调配结点(尽量内存收敛)来优化拜访效率,同样的办法也适应红黑树、哈希表等其余构造。

排序

尽量对指针、索引、ID 排序,而不要对对象自身排序,因为替换对象比替换地址 / 索引慢;求 topN 不要做全排序;非稳固排序能满足要求不要搞稳固排序。

提早计算 & 写时拷贝

提早计算和写时拷贝(COW)思维上是一样的,即能够通过把计算尽量推延来缩小计算开销。

我拿游戏服务器开发来举例,假如玩家的战斗力(fight)是通过等级,血量,名称等其余属性计算出来的,咱们能够在等级、血量、名称变动的时候立刻重算 fight,但血量可能变动比拟频繁,所以就会须要频繁重算战力。通过提早计算,咱们能够为战力增加一个 dirtyFlag,在等级、血量、名称变动的时候设置 dirtyFlag,等到真正须要用到战力的时候(GetFight 函数)里判断 dirtyFlag,如果 dirtyFlag 为 true 则重算战力并清 dirtyFlag,如果 dirtyFlag 为 false 则间接返回 fight 值。

写时拷贝(COW)跟这个差不多,linux kernel 在 fork 过程的时候,子过程会共享父过程的地址空间,只有在子过程对本身地址空间写的时候,才会 clone 一份进去,同样,string 的设计也用到了相似的思维。

预计算

有些值能够提前计算出后果并保存起来,不必反复计算的尽量不反复计算,特地是循环内的计算,要防止反复的常量计算,C++ 甚至减少了一个 constexpr 的关键词。

增量更新

增量更新的原理不简单,只做增量,只做 DIFF,不做全量,这个思维有很多利用场景。

举个例子,游戏服务器每隔一段时间须要把玩家的属性(比方血量、魔法值等)同步到客户端,简略的做法是把所有属性打包一次性全发送过来,这样比拟消耗带宽,能够思考为每个属性编号,在发送的时候,只发送变动的属性。

在发送端,编码一个变动的属性的时候,须要发送一个属性编号 + 属性值的对子,接收端相似,先解出属性编号,再解出属性值,这种形式可能须要就义一点 CPU 换带宽。

3、代码优化

内存优化

(a)小对象分配器

C 的动态内存调配是介于零碎和应用程序的中间层,malloc/free 自身体现的就是一种按需分配 + 复用的思维。

当你调用 malloc 向 glibc 的动态内存分配器 ptmalloc 申请 6 字节的内存,理论消耗的会大于 6 字节,6 是动态分配块的有效载荷,动态内存分配器会为 chunk 增加首部和尾部,有时候还会加一下填充,所以,真正消耗的存储空间会远大于 6 字节,在我的机器上,通过 malloc_usable_size 发现申请 6 字节,返回的 chunk,理论可用的 size 为 24,加上首尾部就更多了。

但你真正申请(可用)的大小是 6 字节,可见,动态内存调配的 chunk 内有大量的碎片,这就是内碎片,而外碎片是存在 chunk 之间的,是另一个问题。

当你申请的 size 较大,有效载荷 / 消耗空间的比例会比拟高,内碎片占比不高,但但 size 较小,这个占比就高,如果这种小 size 的 chunk 十分多,就会造成内存的极大节约。

《C++ 设计新思维》一书中的 loki 库实现了一个小对象分配器,通过隐式链表的形式解决了这个问题,有趣味的能够去看看。

(b)cached obj

《C++ Primer》实现了一个 CachedObj 类模板,任何须要领有这种 cached 能力的类型都能够通过从 CachedObj<T> 派生而取得。

它的次要思维是为该种类型保护一个 FreeList,每个节点就是一个 Object,对象申请的时候,查看 FreeList,如果 FreeList 不为空,则摘除头结点返回,如果 FreeList 为空,则 new 一批 Object 并串到 FreeList,而后走 FreeList 不为空的调配流程,通过重载类的 operator new 和 operator delete,达到对类的使用者通明的目标。

(c)内存调配和对象构建拆散

c 的 malloc 用来动静分配内存,free 用来偿还内存;C++ 的 new 做了 3 件事,通过 operator new(实质上等同 malloc)分配内存,在调配的内存上构建对象,返回对象指针;而 delete 干了两件事,调用析构函数,偿还内存。

C++ 通过 placement new 能够拆散内存调配和对象构建,联合显示的析构函数调用,达到自控的目标。

我优化过一个游戏我的项目,启动工夫过长,记忆中须要几十秒(至多十几秒),剖析后发现次要是因为游戏执行预调配策略(对象池),在启动的时候按最大容量创立怪和玩家,对象构建很重,大量对象构建耗时过长,通过拆散内存调配和对象构建,把对象构建推延到真正须要的时候,实现了服务的重启秒起。

(d)内存复用

编解码、加解密、序列化反序列化(marshal/unmarshal)的时候个别都须要动静申请内存,这种调用频次很高,能够思考用动态内存,为了防止多线程竞争,能够用 thread local。

当然你也能够改良动态内存策略,比方封装一个 GetEncodeMemeory(size_t)函数,保护一个 void + size_t 构造体对象(初始化为 NULL+0),比照参数 size 跟对象的 size 成员,如果参数 size<= 对象 size,间接返回对象大的 void指针,否则 free 掉 void指针,再按参数 size 调配一个更大的 void,并用参数 size 更新对象 size。

cache 优化

i-cache 优化:i-cache 的优化能够通过精简 code path,简化调用关系,缩小代码量,缩小复杂度来实现。

具体措施包含,缩小函数调用(就地开展、inline),利用分支预测,缩小函数指针,能够思考把 code path 上相干的函数定义在一起,把相干的函数定义到一个源文件,并让它们在源文件上邻近,这样生成的 object 文件,在运行时加载后相干函数大概率也内存邻近,当然编译器也始终在做这方面的致力,但咱们写代码不应该依赖编译器优化,尽量去帮忙编译器生成更高效的代码。

d-cache 优化:d-cache 优化包含改良数据结构和算法获取更好的数据拜访时空局部性,比方二分查找就是 d -cache 敌对算法。一个 cache line 个别是 64B,如果数据逾越两个 cache-line,则会导致 load & store2 次,所以,须要联合 cache 对齐,尽量让相干拜访的数据在一个 cache-line。

如果构造体过大,则各成员不仅可能在不同 cache-line,甚至可能在不同 page,所以应该防止构造体过大。

如果构造体的成员变量过多,一般而言对各成员的拜访频次也会满足 2 - 8 定律,能够思考把 hot 和 cold 的成员离开,重排构造体成员变量程序,但这些骚操作我不倡议在开始的时候用,因为说不定哪天又要增删成员,从而毁坏苦心孤诣搭建的积木。

判断前置

判断前置指在函数中讲判断返回的语句前置,这样不至于忙活半天,你跟我说对不起不适合,玩儿呢?

在写多个判断的时候,把不满足可能性高的放在后面。

在写条件或的时候把为 true 的放在后面,在写条件与的时候把为 false 的放在后面。

另外,如果在循环里调用一个函数,而这个函数里查看某条件,不合乎就返回,这种状况,能够思考把查看放到调用函数的里面,这样不满足的话就不必陷入函数,当然,你也能够说,这样的操作违反软件工程,但看你想要什么,你不总是可能两败俱伤,对吧?

凑零为整与化整为零

凑零为整其实的思维在日志批处理里提了,不再开展。

化整为零体现了分而治之的思维,能够把一个大的操作,摊派开来,防止在做大操作的时候导致卡顿,从而让 CPU 占比更加安稳。

分频

之前我优化过一个游戏服务器,游戏服务器的逻辑线程是一个大循环,外面调用 tick 函数,tick 函数里调用了所有须要 check timer & do 的事件,而后所有须要 check timer & do 的事件都塞进 tick 里。

改良:tick 里调用了 tick50ms、tick100ms、tick500ms,tick1000ms,tick5000ms,而后把须要 check timer & do 的逻辑依据精度要求塞到不同的 tickXXms 里去。

减法

  • 缩小冗余
  • 缩小拷贝、零拷贝
  • 缩小参数个数(寄存器参数、取决于 ABI 约定)
  • 缩小函数调用次数 / 档次
  • 缩小存储援用次数
  • 缩小有效初始化和反复赋值

循环优化

这方面的常识很多,感觉一下子讲不完,提几点,循环套循环要内大外小,尽量把逻辑提取到循环外。

  • 提取与循环无关的表达式,尽量减少循环内不必要计算。
  • 循环内尽量应用局部变量。
  • 循环展开是一种程序变换,通过减少每次迭代计算的元素的数量,缩小循环的迭代次数。
  • 还有循环分块的骚操作。

防御性编程适可而止

有两个流派,一个是齐全的不信赖,即所有函数调用里都对参数判断,包含判空,有效性查看等,但这样做有几点不好:

  • 第一,它只是貌似更平安,并不是真的更平安。
  • 第二,它浓缩代码浓度,吞没要害语句。
  • 第三,如果通过返回值报告谬误,则减轻了调用者累赘,调用者须要增加额定代码查看,不然更奇怪。
  • 第四,反复判断空耗 CPU。
  • 第五,埋雷,把本该 crash 或者裸露的问题埋得更深。

但这种做法大行其道,它有肯定的市场和情理。

另一个是界定边界,辨别公开接口和外部实现,查看只在模块之间进行,就相当于进园区的时候,门卫会查看你证件,但之后,则不再查看。因为外部实现是受控的平安上下文,开发者应该齐全 cover 住。

我主张防御性编程适可而止,一些驰名的开源我的项目也不会做过多进攻,比方 linux kernel、NGINX、skynet 等,但事实中,软件开发通常多人单干,每个开发者素质不一样,这就是客观现实,所以我也了解前一种做法。

release 洁净

开发过程中,咱们会加很多诊断信息,比方咱们可能接管内存调配,从而附加额定的首尾部,通过填写 magic Num 捕捉异样或者内存越界,但这些信息应该只用于开发阶段的 DEBUG 须要,在 release 阶段应该通过预处理的形式删除掉。

日志分级其实也体现了这种思维,通常有两种做法,一个是定义级别变量,另一个是预处理,预处理洁净,但须要从新编译生成 image,而变量更灵便,但变量的比拟还是有开销的。

不要漠视这些诊断调试信息的开销,牢记不用做的事件绝不做的准则。

慎用递归

递归的写法简略,了解起来也容易,但递归是函数调用,有栈帧建设撤销管制跳转的开销,另外也有爆栈的危险,在性能敏感要害门路,优先思考用非递归版本。

4、编译优化与优化选项

  • inline
  • restrict
  • LTO
  • PGO
  • 优化选项

5、其余优化

* 绑核

* SIMD

  1. 锁与并发
  2. 锁的粒度
  3. 无锁编程
  4. Per-cpu data structure & thread local
  5. 内存屏障
  6. 异构优化 /TCO 优化

比方用 GPGPU、FPGA、SmartNIC 来 offload 原来 cpu 的工作,TCO 优化指的是不以性能优化为繁多指标,而是在满足性能条件下以综合老本为优化直播,当然异构也包含被动利用 CPU 的 avx 或者其余逻辑单元,这类优化往往编译器不能主动开展(@zrg)

常识和数据

CPU 拷贝数据个别一秒钟能做到几百兆,当然每次拷贝的数据长度不同,吞吐不同。

一次函数执行如果消耗超过 1000 cycles 就比拟大了(刨除调用子函数的开销)。

pthread_mutex_t 是 futex 实现,不必每次都进入内核,首次加解锁大略耗时 4000-5000 cycles 左右,之后,每次加解锁大略 120 cycles,O2 优化的时候 100 cycles,spinlock 耗时略少。

lock 内存总线 +xchg 须要 50 cycles,一次内存屏障要 50 cycles。

有一些无锁的技术,比方 CAS,比方 linux kernel 里的 kfifo,次要利用了整型回绕 + 内存屏障。

几个如何?

1. 如何定位 CPU 瓶颈?

CPU 是通常大家最先关注的性能指标,宏观维度有核的 CPU 使用率,宏观有函数的 CPU cycle 数,依据性能的模型,性能规格与 CPU 使用率是相互关联的,规格越高,CPU 使用率越高,然而处理器的性能往往又受到内存带宽、Cache、发热等因素的影响,所以 CPU 使用率和规格参数之间并不是简略的线性关系,所以性能规格翻倍并不能简略地翻译成咱们的 CPU 使用率要优化一倍。

至于 CPU 瓶颈的定位工具,最有名也是最有用的工具就是 perf,它是性能剖析的第一步,能够帮咱们找到零碎的热点函数。就像人看病一样,只晓得症状是不够的,须要通过医疗机器进一步剖析病因,能力隔靴搔痒。

所以咱们通过性能剖析工具 PMU 或者其余工具去进一步剖析 CPU 热点的起因比方是指令数自身就比拟多,还是 Cache miss 导致的等,这样在做性能优化的时候不会走偏。

优化 CPU 的指标就是让 CPU 运行不受妨碍。

2. 如何定位 IO 瓶颈?

零碎 IO 的瓶颈能够通过 CPU 和负载的非线性关系体现进去。当负载增大时,零碎吞吐量不能无效增大,CPU 不能线性增长,其中一种可能是 IO 呈现阻塞。

零碎的队列长度特地是发送、写磁盘线程的队列长度也是 IO 瓶颈的一个间接指标。

对于网络系统来讲,我倡议先从内部察看零碎。所谓内部察看是指通过观察内部的网络报文交换,能够用 tcpdump, wireshark 等工具,抓包看一下。

比方咱们优化一个 RPC 我的项目,它的吞吐量是 10TPS,客户心愿是 100TPS。咱们应用 wireshark 抓取 TCP 报文流,能够剖析报文之间的工夫戳,响应提早等指标来判断是否是由网络引起来的。

而后能够通过 netstat -i/- s 选项查看网络谬误、重传等统计信息。还能够通过 iostat 查看 cpu 期待 IO 的比例。IO 的概念也能够扩大到过程间通信。

对于磁盘类的应用程序,咱们最心愿看到写磁盘有没有时延、频率如何。其中一个办法就是通过内核 ftrace、perf-event 事件来动静观测零碎。比方记录写块设施的起始和返回工夫,这样咱们就能够晓得磁盘写是否有延时,也能够统计写磁盘工夫消耗散布。有一个开源的工具包 perf-tools 外面蕴含着 iolatency, iosnoop 等工具。

3. 如何定位 IO 瓶颈?

应用程序罕用的 IO 有两种:Disk IO 和网络 IO。判断零碎是否存在 IO 瓶颈能够通过观测零碎或过程的 CPU 的 IO 期待比例来进行,比方应用 mpstat、top 命令。

零碎的队列长度特地是发送、写磁盘线程的队列长度也是 IO 瓶颈的一个重要指标。

对于网络 IO 来讲,咱们能够先应用 netstat -i/- s 查看网络谬误、重传等统计信息,而后应用 sar -n DEV 1 和 sar -n TCP,ETCP 1 查看网路实时的统计信息。ss(Socket Statistics)工具能够提供每个 socket 相干的队列、缓存等详细信息。

更间接的办法能够用 tcpdump, wireshark 等工具,抓包看一下。

对于 Disk IO,咱们能够通过 iostat -x -p xxx 来查看具体设施使用率和读写均匀等待时间。如果使用率靠近 100%,或者等待时间过长,都阐明 Disk IO 呈现饱和。

一个更粗疏的察看办法就是通过内核 ftrace、perf-event 来动静观测 Linux 内核。比方记录写块设施的起始和返回工夫,这样咱们就能够晓得磁盘写是否有延时,也能够统计写磁盘工夫消耗散布。有一个开源的工具包 perf-tools 外面蕴含着 iolatency, iosnoop 等工具。

4. 如何定位锁的问题?

大家都晓得锁会引入额定开销,但锁的开销到底有多大,预计很多人没有实测过,我能够给一个数据,个别单次加解锁 100 cycles,spinlock 或者 cas 更快一点。

应用锁的时候,要留神锁的粒度,但锁的粒度也不是越小越好,太大会减少撞锁的概率,太小会导致代码更难写。

多线程场景下,如果 cpu 利用率上不去,而零碎吞吐也上不去,那就有可能是锁导致的性能降落,这个时候,能够察看程序的 sys cpu 和 usr cpu,这个时候通过 perf 如果发现 lock 的开销大,那就没错了。

如果程序卡住了,能够用 pstack 把堆栈打进去,定位死锁的问题。

5. 如何提⾼ Cache 利用率?

内存 /Cache 问题是咱们常见的负载瓶颈问题,通常可利用 perf 等一些通用工具来辅助剖析,优化 cache 的思维能够从两方面来着手,一个是减少部分数据 / 代码的连续性,晋升 cacheline 的利用率,缩小 cache miss,另一个是通过 prefetch,升高 miss 带来的开销。

通过对数据 / 代码依据冷热进行重排分区,可晋升 cacheline 的无效利用率,当然触发 false-sharing 另当别论,这个须要依据运行 trace 进行深刻调整了;说到 prefetch,用过的人往往都有一种领会,事实成果比预期差的比拟远,的确无论是数据 prefetch 还是代码 prefetch,不确定性太大,指望编译器更靠谱点。

小结

性能优化是一项粗疏的工作,性能优化也是一个系统性工程。性能优化通常是在现有零碎和代码根底上做改良,它并非推倒重来,考验的是开发者反向修复的能力,而性能设计考验的是设计者的正向设计能力,但性能优化的办法能够领导性能设计,两者互补。

点击关注,第一工夫理解华为云陈腐技术~

正文完
 0