译|High-Performance Server Architecture(下)

简介: 译|High-Performance Server Architecture(下)

内存分配

分配和释放内存是许多应用程序中最常见的操作之一。因此,人们已经开发出许多巧妙的技巧来使通用存储器分配器更有效。然而,再聪明也弥补不了这样一个事实:在许多情况下,这种分配器的通用性不可避免地使它们的效率远远低于其他分配器。因此,关于如何完全避免使用系统内存分配器,我有三点建议。

建议一:简单的预分配。我们都知道,静态分配器如果导致程序功能受限,是非常不好的,但是还有许多其他形式的预分配可能会非常有益。通常,原因归结为这样一个事实:即使在此过程中“浪费”了一些内存,通过系统内存分配器的一次访问也要好于几次。因此,如果可以断言同时使用不超过N项,则在程序启动时进行预分配可能是一个有效的选择。即使不是这种情况,也可以在一开始就预先分配请求处理程序可能需要的所有内容,而不是根据需要分配每个内容。除了通过系统分配器在一次行程中连续分配多项的可能性之外,也通常大大简化了错误恢复代码。如果内存非常紧张,那么预分配可能不是一种选择,但在除最极端的情况外的所有情况下,结果通常都是净收益。

建议二:对经常分配和释放的对象使用 lookaside 列表。基本思想是将最近释放的对象放到列表中,而不是真正释放,希望如果很快再次使用,则只需将其从列表中移除,而不用从系统内存中分配。另一个好处是, lookaside 列表的存取转换的实现通常可以跳过复杂的对象初始化/终结。

通常不希望 lookaside 列表无限制地增长,即使程序处于空闲状态也从不释放任何内容。因此,通常有必要执行某种定期的 “sweeper” 任务以释放不活跃的对象,但是如果清理程序引入了不适当的加锁复杂性或竞争,则也不可取。因此,一个好的折衷方案是,lookaside 列表实际上由单独加锁的 “old” 列表和 “new” 列表组成的系统。优先从新列表开始分配,然后从旧列表开始分配,并且仅在万不得已的情况下才从系统中分配;对象总是被释放到新列表中。清理线程的操作如下:

  1. 锁定两个列表。
  2. 保存旧列表的表头。
  3. 通过表头赋值,将(以前)新列表变为旧列表。
  4. 解锁。
  5. 在空闲时将保存的旧列表中的所有对象都释放掉。

此类系统中的对象只有在至少一个但不超过两个完整的清除程序间隔不需要时才真正释放。最重要的是,清除程序在执行大部分工作时没有持有任何与常规线程竞争的锁。理论上,相同的方法可以推广到两级以上,但我还没有发现如此做有用。

使用 lookaside 列表的一个担心是列表指针可能会增加对象的大小。根据我的经验,使用 lookaside 列表的大多数对象都已经包含了列表指针,所以考虑此点没有实际意义。但是,即使指针只用于 lookaside 列表,但避免使用系统内存分配器(和对象初始化)方面所节省的开销,将远远弥补额外增加的内存。

建议三:实际上与尚未讨论到的加锁有关,但我仍然要加进来。即使使用 lookaside 列表,锁竞争通常也是分配内存的最大成本。一种解决方案是维护多个私有的 lookaside 列表,这样就绝对不可能争用任何一个列表。例如,每个线程可以有一个单独的 lookaside 列表。出于高速缓存 cache-warmth 的考虑,每个处理器一个列表更好,但是仅在线程无法被抢占的情况下才有效。如有必要,私有 lookaside 列表甚至可以与共享列表相结合,以创建具有极低分配开销的系统。

锁竞争

众所周知,高效的加锁方案很难设计,因此我称之为 “Scylla” 和 “Charybdis”,取自《奥德赛》中的怪物。Scylla 是过于简单和/或粗粒度的锁,是可以或应该并行的串行化的活动,这些活动可以或应该并行进行,从而牺牲了性能和可伸缩性。Charybdis 是过于复杂或细粒度的锁,加锁的空间和加锁的操作时间会再次降低性能。靠近 Scylla 的陷阱是代表死锁和活锁的状态。靠近 Charybdis 的陷阱是代表竞态条件。两者之间,有一个狭窄的渠道代表既高效又正确的加锁……或者在哪?由于锁定往往与程序逻辑紧密相关,因此,如果不从根本上改变程序的工作方式,通常就不可能设计出良好的锁定方案。这就是为什么人们讨厌锁,并试图将不可伸缩的单线程实现合理化的原因。

几乎每个加锁方案都是从“围绕所有事物的一个大锁”开始,并且茫然地希望性能不会太糟。当希望破灭时(几乎总是这样),大锁被分解成小锁,然后继续祈祷,然后重复整个过程,大概直到性能足够为止。但是,通常每次迭代都会增加 20-50% 的复杂性和锁开销,以减少 5-10% 的锁竞争。幸运的是,最终结果性能仍然会有些许提高,但实际下降的情况也并不少见。设计师只能挠头了,“我把锁粒度做得更细,就像教科书上说的那样”,他想,“那为什么性能会变得更差呢?”

我认为情况变得更糟,因为上述方法从根本上讲是错误的。把“解决方案空间”想象成一座山脉,高点代表好的解决方案,低点代表差的解决方案。问题是,“大锁”的起点几乎总是被各种山谷,马鞍山,小山峰、死胡同与高峰隔开。这是一个经典的爬山问题。想从一个起点爬到更高的山峰,只迈出一小步,从不走下坡路,几乎是行不通的。我们需要的是一种完全不同的接近顶峰的方式。

您要做的第一件事是形成程序加锁的脑中地图。该地图有两个轴:

  • 纵轴表示代码。如果您使用的是非分支阶段的阶段体系结构,则可能已经有了一个显示划分的图表,就像每个人都在使用的 OSI 模型网络协议栈那样。
  • 横轴表示数据。在每个阶段中,应将每个请求分配给一个数据集,该数据集使用的资源应该独立于其他任何资源。

现在有了一个网格,其中每个单元格表示特定处理阶段中的特定数据集。最重要的是以下规则:两个请求不应处于争用状态,除非它们位于相同的数据集和相同的处理阶段。如果你能做到这一点,你已经成功了一半。

一旦定义了网格,就可以绘制程序的每种加锁类型,下一个目标是确保所得的点尽可能沿两个轴均匀分布。不幸的是,这部分是非常特定于应用的。你必须像钻石切割师一样思考,利用你对程序执行的知识来寻找阶段和数据集之间的自然“解理纹”。它们有时从一开始就很明显,有时很难找到,但回想起来似乎更明显。将代码分为多个阶段是一个复杂的程序设计问题,因此我能提供的内容不多,但以下是一些关于如何定义数据集的建议:

  • 如果有某种与请求相关联的块号或哈希或事务ID,那么最好将该值除以数据集的数量。
  • 有时,最好动态地将请求分配给数据集,根据哪个数据集拥有最多的可用资源,而不是请求的某些内在属性。把它想象成现代CPU中的多个整数单元;它们对离散请求流经系统略知一二。
  • 确保每个阶段的数据集分配不同通常是有用的,这样可以保证在一个阶段竞争的请求在另一阶段不会再次竞争。

如果您已经将“加锁域”进行了垂直和水平划分,并确保加锁活动均匀地分布在生成的单元格中,则可以确定加锁状态良好。不过,还有一步。您还记得我几段内容之前嘲笑的“小步走”方法吗?它仍然有它的作用,因为现在你处于一个好的起点而不是一个糟糕的起点。用比喻的话来说,你可能已经爬上了这座山脉最高峰之一的斜坡,但你可能还没有到达山顶。现在是时候收集竞争的统计信息了,看看您需要做些什么来改进,以不同的方式拆分阶段和数据集,然后收集更多的统计信息,直到满意为止。如果你做了这些,你一定能从山顶看到美丽的景色。

其他内容

正如我所承诺的,我已经讨论了服务设计中四个最大的性能问题。不过,特定的服务仍然有其他重要的问题需要解决。主要是要了解平台/环境:

  • 存储子系统如何处理较大和较小的请求?顺序还是随机?read-ahead 和 write-behind 的能力如何?
  • 使用的网络协议的效率如何?是否可以设置参数或标志以获得更好的性能?是否有诸如TCP_CORK,MSG_PUSH 或 Nagle-toggling 技巧之类的工具可用于避免发送微小消息?
  • 系统是否支持分散/集中 I/O(例如readv / writev)?使用这些可以提高性能,也可以减轻使用缓冲链的痛苦。
  • 页大小是多少?缓存行大小是多少?在边界上内容对齐是否值得?相对于其他操作,系统调用或上下文切换的成本多高?
  • reader/writer 加锁原语是否处于饥饿?因何饥饿?事件有“惊群效应”的问题吗?睡眠/唤醒是否有一种恶劣的(但非常常见的)行为,即当 X 唤醒 Y 时,即使 X 还有事情要做,上下文也会立即切换到 Y?

我相信我能想出更多这样的问题。相信你也可以。在任何特定情况下,针对任何一个问题做点什么都不值得,但通常至少值得考虑一下。如果您不知道答案 — 其中许多答案在系统文档中找不到 — 请找出答案。编写一个测试程序或微观基准,从经验上寻找答案;无论如何,编写这样的代码本身就是一种有用的技能。如果您要编写在多个平台上运行的代码,那么其中许多问题都与您应该将功能抽象到每个平台库中的点相关,这样您就可以在支持特定功能的平台上实现性能提升。

“知道答案”理论也适用于你自己的代码。找出代码中重要的高级操作是什么,并在不同的条件下对它们进行计时。这与传统的概要性能剖析不太一样;这是衡量 设计 元素,而不是实际的实现。低级优化通常是搞砸设计的人最后的选择。

原文:High-Performance Server Architecture

本文作者 : cyningsun

本文地址https://www.cyningsun.com/06-02-2021/high-performance-server-architecture-cn.html

版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!

目录
相关文章
|
7月前
|
Oracle 关系型数据库 Linux
Disable NUMA on database servers to improve performance of Linux file system utilities
Disable NUMA on database servers to improve performance of Linux file system utilities
47 3
|
运维 监控 网络协议
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
译|llustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
143 0
|
缓存 前端开发 安全
译|High-Performance Server Architecture(上)
译|High-Performance Server Architecture
77 0
《Performance Characterization of In-Memory Data Analytics on a Scale-up Server》电子版地址
Performance Characterization of In-Memory Data Analytics on a Scale-up Server
79 0
《Performance Characterization of In-Memory Data Analytics on a Scale-up Server》电子版地址
|
SQL 关系型数据库 RDS
Troubleshooting High CPU Usage on Alibaba Cloud SQL Server
A primary issue with SQL Server is its sensitivity to latency, often resulting in performance issues.
1360 0
Troubleshooting High CPU Usage on Alibaba Cloud SQL Server
|
NoSQL
High-availability MongoDB Cluster Configuration Solutions
In this post, we will share an in-depth discussion about High-availability Cluster Solutions along with several MongoDB High-availability Cluster Configurations.
2944 0
High-availability MongoDB Cluster Configuration Solutions
|
网络协议 大数据 Go
高性能服务器架构 的几个注意点 (High-Performance Server Architecture)
High-Performance Server Architecture 高性能服务器架构 来源:http://pl.atyp.us/content/tech/servers.html译文来源:http://www.lupaworld.com/home/space-341888-do-blog-id-136718.html (map注:本人看了一遍,“于我心有戚戚焉”,翻译得也很好,于是整理了一下,重新发布,备忘) 引言本文将与你分享我多年来在服务器开发方面的一些经验。
1129 0