🔗 Feature: SMP Scalability
- 目标:随着处理器或核心数量的增加,实现非磁盘吞吐量的线性扩展。
- 状态:进行中;已准备好在某些环境中部署
- 版本:Squid 3.2
- 开发者:AlexRousskov
🔗 术语
本节介绍 SMP 相关术语。术语及其定义仍在不断发展中,并非所有 Squid 文档和开发人员都使用相同的术语。
- Squid 实例:由单个“squid”命令运行的所有进程。这包括但不限于下面定义的 kid 进程。
- Kid:由 Master 进程创建的 Squid 进程(即运行 Squid 可执行代码的进程)。下面定义的 Coordinator、worker 和 disker 通常是 Squid kid。
- Worker:一个接受 HTTP 或 HTTPS 请求的 Squid 进程。Worker 通常由 Master 进程创建。总的来说,Worker 负责大部分事务处理,但可能会将部分工作外包给辅助进程(直接)、其他 Worker(通过 Coordinator)甚至独立服务器(通过 ICAP、DNS 等)。
- Disker:一个专门用于 cache_dir I/O 的 Squid 进程。Disker 由 Master 进程创建。如今,只有 Rock cache_dir 可能使用 disker。
- Coordinator:一个专门用于同步其他 kid 的 Squid 进程。
- Master:运行“squid”命令时创建的第一个 Squid 进程。Master 进程负责启动和重启所有 kid。此定义并非 100% 准确,因为操作系统创建第一个进程,然后第一个 Squid 进程会 fork 实际的 Master 进程以成为守护进程(“squid -N”除外)。由于第一个操作系统创建的进程在 fork 后立即退出,因此这个不准确的定义对于大多数情况来说都可以正常工作。欢迎提供更好的措辞!
- SMP 模式:当 worker 进程和 disker 进程的总数超过一个时,Squid 被称为工作在 SMP 模式。以下是 Squid 实例在 SMP 模式下工作的三个随机示例:2 个 worker 和 0 个 disker;1 个 worker 和 1 个 disker;2 个 worker 和 3 个 disker。有时,相同的“SMP 模式”术语用于表示“多个 worker”;这种用法排除了只有一个 worker 和多个 disker 的配置;应避免这种用法。
请注意,同一个进程可能扮演多个角色。例如,当您使用 -N 命令行选项启动 Squid 时,将只有一个 Squid 进程在运行,并且该单个进程扮演 Master 和 Worker 的角色。
🔗 当前状态和架构
Squid-3.2 使用 workers 支持基本的 SMP 扩展。管理员可以配置和运行一个 Squid 实例,该实例可以生成多个 worker 进程来利用所有可用的 CPU 核心。
Worker 接受新的 HTTP 请求,并处理每个接受的请求直到完成。Worker 可以共享 http_ports,但它们之间不传递事务。Worker 具有单个非 SMP Squid 的所有功能,但 Worker 可以配置不同(例如,服务于不同的 http_ports)。cpu_affinity_map 选项允许为每个 Worker 分配一个 CPU 核心。
🔗 Worker 如何协调?
当需要时,一个特殊的 Coordinator 进程会启动 Worker 并协调它们的活动。以下是 Coordinator 的一些职责:
- 重启失败的 worker 进程;
- 允许 Worker 共享监听套接字;
- 将重新配置和关机命令广播给 Worker;
- 将 Worker 统计信息合并和/或聚合以供 Cache Manager 响应。
Coordinator 不参与常规事务处理,也不决定哪个 Worker 处理传入的连接或请求。Coordinator 进程通常是空闲的。
🔗 Worker 可以共享什么?
通过使用 Coordinator 和通用的配置文件,Squid Worker 可以接收相同的配置信息并同步它们的一些活动。默认情况下,Squid Worker 共享以下内容:
- Squid 可执行文件;
- 通用配置;
- 监听端口(但共享的 ICP 和 HTCP 端口工作不佳;见下文);
- 日志;
- 内存对象缓存(在大多数环境中);
- 磁盘对象缓存(仅限 Rock Store);
- 不安全的缓存管理器统计信息( elsewhere 详细介绍);
- SNMP 统计信息。
缓存索引是共享的,无需复制。其他共享信息通常在 RAM 使用量方面很小,并且本质上是复制的,以避免锁定和相关的性能开销。
条件配置和与 Worker 相关的宏可用于限制共享。例如,每个 Worker 可以获得一个专用的 http_port 来监听。
目前,Squid Worker 不共享也不同步其他资源和服务,包括(但不限于):
- 内存对象缓存(在某些环境中);
- 磁盘对象缓存(Rock Store 除外);
- DNS 缓存(ipcache 和 fqdncache);
- 辅助进程和守护进程;
- 有状态的 HTTP 身份验证(例如,摘要身份验证;请参阅 bug 3517 );
- 延迟池;
- SSL 会话缓存(目前有一个活跃的项目允许 Worker 之间共享会话);
- 安全的缓存管理器统计信息( elsewhere 详细介绍);
- ICP/HTCP(存在一个警告:如果多个 Worker 共享同一个 ICP/HTCP 端口,ICP/HTCP 响应可能不会发送到发送请求的 Worker,导致请求的 Worker 超时;请使用专用的 ICP/HTCP 端口作为 变通方法 )。
一些不熟悉 SMP 的功能在 SMP 模式下仍然可以工作(例如,DNS 响应将被单个 Worker 缓存),但它们的性能会因缺乏同步而受到影响,并且由于信息重复而需要更多资源(例如,每个 Worker 可能独立解析并缓存同一个域名)。一些不熟悉 SMP 的功能会严重损坏(例如,基于 ufs 的 cache_dir 会损坏),除非使用 squid.conf 条件来防止此类损坏。一些不熟悉 SMP 的功能似乎可以工作,但会不正确地工作(例如,延迟池将在每个 Worker 的基础上限制带宽,而不会在 Worker 之间共享流量信息,也不会在 Worker 之间分配带宽限制)。
🔗 为什么是进程?线程不是更好吗?
有几个原因决定了为 Worker 选择进程而不是线程:
- 以其当前状态对 Squid 代码进行多线程处理将花费太长时间,因为大多数代码都是线程不安全的,包括几乎所有的基类。用户现在需要 SMP 扩展,无法等待 Squid 进行从头重写。
- 线程提供更快的上下文切换,但在典型的 SMP Squid 部署中,每个 Worker 都绑定到一个专用的核心,上下文切换开销并不那么重要。
- 进程和线程都有足够的同步和共享机制来实现 SMP 可扩展的实现。
总而言之,我们使用进程而不是线程,因为它们使我们能够在合理的时间范围内实现相似的 SMP 性能。使用线程被认为是不切实际的。
🔗 谁决定哪个 Worker 获得请求?
所有共享 http_port 的 Worker 都监听相同的 IP 地址和 TCP 端口。操作系统使用锁保护共享的监听套接字,并决定哪个 Worker 获得等待接受的新 HTTP 连接。一旦 Worker 接受了传入连接,该连接就一直保留在该 Worker 上。
🔗 相似的 Worker 会收到相似的工作量吗?
我们期望操作系统通过适当地将下一个传入连接请求分配给负载最轻的 Worker/核心来在多个 Worker 之间平衡负载。实验室测试和初始部署的 Worker 统计数据证明我们错了。以下是几个相同的 Worker 在处理中等负载一段时间后的累计 CPU 时间示例:
| 累计 CPU | Worker |
| 利用率(分钟) | |
| 20 | (squid-3) |
| 16 | (squid-4) |
| 13 | (squid-2) |
| 8 | (squid-1) |
该表显示了 Worker 负载的显着倾斜:squid-3 Worker 花费了 20 分钟处理流量,而 squid-1 只工作了 8 分钟。随着运行时间的推移,这种不平衡不会改善,因为最忙的 Worker 仍然是最忙的。
虽然我们不知道这种不平衡是否会导致忙碌 Worker 的响应时间变差,但从系统整体平衡的角度来看,这是相当不受欢迎的。
经过数天的 Squid 实验、研究 Linux TCP 堆栈源代码以及与其他开发人员讨论问题,我们最终找到了一种相对紧凑且开销低的变通方法。事实证明,最先被唤醒接受新客户端连接的 Worker 通常是最后将其监听描述符注册到 epoll(2) 的 Worker。这种依赖关系相当奇怪,因为 epoll 集合之间没有在 Squid Worker 之间共享;它必须在监听套接字级别工作(这些套接字是共享的)。特别感谢 HenrikNordström 进行的一次发人深省的讨论,它提供了解决问题的最后一块拼图。
负责此调度行为的确切内核 TCP 堆栈代码目前未知,并且即使对专家 Linux 内核开发人员来说也很难找到。最终,我们可能会找到它并提出一个内核模块或补丁,以更好地平衡 Squid 环境中的监听器选择。
但是,我们已经有足够的信息来在 Squid 层面解决这个问题。我们开发了一个补丁,该补丁每隔几秒钟指示一个 Squid Worker 从 epoll(2) 集合中删除并立即重新插入其监听描述符。这种 epoll 操作相对便宜。该补丁确保 Worker 通过这种方式轮流“轻触”其监听描述符,因此在一个给定的“轻触”间隔内只有一个 Worker 最活跃。
此更改导致 Worker 之间的负载分布合理。这是每台 Worker 的当前 CPU 核心利用率以及该 Worker 累积的总 CPU 时间的即时快照。
| CPU 利用率 | Worker | |
| 现在 | 累积 | |
| (%) | (分钟) | |
| 41 | 2826 | (squid-3) |
| 23 | 2589 | (squid-2) |
| 9 | 2345 | (squid-4) |
| 7 | 2303 | (squid-5) |
| 9 | 2221 | (squid-6) |
| 11 | 2107 | (squid-1) |
即时 CPU 利用率(最左边的列)如预期般不平衡。管理员监控该列会看到最活跃的 Worker 组每隔几秒钟就会变化,新的最忙碌的 Worker 离开,而该组中最老的 Worker 变得大部分空闲。
累积 CPU 时间分布(中间的列)现在更加均匀。尽管有七个 Worker,Squid 显示历史最活跃和最不活跃 Worker 之间的差异仅为 25%,而没有补丁的四个 Worker 之间的差异为 60%。通过调整补丁参数(如“轻触”间隔)或增加 Worker 负载,可以实现更好的分布,使得每个 Worker 不太可能错过“轻触”其 epoll(2) 集合的机会。
我们正在努力使该补丁与非 epoll 环境兼容,并将提议将其集成到 Squid v3.2 中。
🔗 为什么没有专用的进程来接受请求?
当前设计的一个常见替代方案是使用一个专用的进程来接受传入连接并将其交给一个 Worker 线程。我们已考虑并拒绝了这种方法作为初始实现,原因如下:
- 用户级别的调度和连接传递会带来性能开销。我们希望在初始实现中避免这些开销,以便 Squid v3.2 不会比早期版本慢。
- 由于大多数用户不期望区别对待 Worker,操作系统内核已经拥有平衡负载所需的所有信息,包括 Squid 无法获取的低级硬件信息。在一般情况下,重复和/或与内核 CPU 调度算法和调优参数竞争似乎不明智。
- 接受进程本身可能会成为瓶颈。我们可以支持多个接受进程,但然后用户将面临一个复杂的任务,即在 CPU 核心数量有限的情况下优化接受进程的数量和 Worker 的数量。即使现在,在没有接受进程的情况下,这种优化也很复杂。由于每个 Worker 本身都能够接受连接,因此在一般情况下,添加的复杂性似乎是不必要的。
- 如果 Worker 的配置不同,它们将需要不同的接受进程或某种类型的路由映射,从而使配置和性能优化复杂化。
如果需要,可以稍后添加一个专用的接受进程(带有一些额外的 HTTP 感知调度逻辑)。
🔗 一个 Squid 实例有多少个进程?
在未运行 -N 命令行选项的单个 Squid 实例中,Squid 进程的数量通常是:
+ 1 master process (counted only in SMP mode) plus
+ W workers (workers in squid.conf; defaults to 1) plus
+ D diskers (rock cache_dirs in squid.conf; defaults to 0) plus
+ 1 Coordinator process (exists only in SMP mode).
例如,如果您没有显式配置 Squid workers 和 rock cache_dirs,那么 Squid 将在非 SMP 模式下运行,总共将获得 0+1+0+0=1 个 Squid 进程。另一方面,如果您使用 3 个 worker 和 1 个 rock cache_dir 显式配置 Squid,那么 Squid 将在 SMP 模式下运行,总共将获得 1+3+1+1=6 个 Squid 进程。
上述公式不包括辅助进程和其他未运行 Squid 可执行代码的进程。
🔗 如何配置 SMP Squid 以获得最佳性能?
如果您拥有强大的硬件,希望优化性能,并愿意花费大量时间/精力/金钱来实现这一目标,那么请考虑以下 SMP 经验法则:
- 如果您想缓存,请使用系统能够安全处理的最大 cache_mem。请注意,Squid 不会告诉您何时分配过多,但可能会崩溃。如果您不想缓存,请将 cache_mem 设置为零,使用 cache 指令禁止缓存,并忽略以下规则 #3。
- 一个或两个 CPU 核心保留给操作系统,具体取决于网络使用级别。使用操作系统的 CPU 亲和性配置网络中断,将 NIC 中断限制在这些“仅限 OS”的核心上。
- 每个物理磁盘主轴一个 Rock cache_dir,没有其他 cache_dir。不使用 RAID。Disker 可能能够使用虚拟 CPU 核心。Rock 的调优很棘手。请参阅 Rock Store 功能页面 上的性能调优建议。请注意,与其他 cache_dir 类型相比,Rock cache_dir 在 Squid 启动期间加载速度较慢,并且可能存在其他与您的部署需求不兼容的问题。当然,您可以使用其他 cache_dir 类型而不是 Rock。这些SMP规则使用 Rock,因为其他 cache_dir 不是 SMP 感知的。
- 每个剩余的非虚拟 CPU 核心一个 SMP worker。请注意,不要过度加载共享同一物理 CPU 核心的多个虚拟 CPU 核心——这些虚拟核心通常能完成的有用工作比一个负载过重的物理 CPU 核心少,因为多个虚拟核心会浪费资源来争夺对它们唯一的物理核心的访问权,而这个物理核心是唯一能完成有用工作的地方。虚拟核心通常最适合半空闲的后台任务,而不是具有近乎实时约束的繁忙 Worker。
- 为每个 Squid kid 进程(disker 和 worker)使用 CPU 亲和性。禁止内核将 kid 从一个 CPU 核心移动到另一个 CPU 核心。没有您的帮助,通用的操作系统内核很可能无法很好地平衡您的 Squid 服务器负载。
- 监视单个 CPU 核心的利用率(而不仅仅是所有核心的平均值或总和!)。调整 worker 的数量、disker 的数量以及 CPU 亲和性映射,以在留有健康的过载安全裕度的同时实现平衡。
免责声明:结果可能因人而异。上述一般规则可能不适用于您的环境。遵循它们不太可能足以实现顶级的性能,尤其是在不理解根本问题的情况下。在给定硬件上实现顶级的 Squid 性能需要大量工作。如果您的 Squid 已经运行正常,请不要为此烦恼。
🔗 SMP 架构层
经过长时间的讨论,项目开发人员一致认为,在 Squid 架构的不同层面上可能存在不同的设计选择。本节试图记录这些与 SMP 相关的层。然而,对于基本层(顶层和底层)之外的许多细节,尚不清楚是否存在共识。
🔗 1. 顶层:Workers
多个 Squid worker 进程和/或线程。每个 worker 负责一部分事务,Worker 之间除了缓存外,交互很少。有一个主进程或线程用于 worker 协调。
🔗 2. 中间层:多线程进程
最终 SMP 架构的中间层将是一个处理多个线程中混合操作的进程。现有的二进制文件需要大量工作来识别哪些部分适合成为单独的线程,并清理代码以实现这一点。
某些组件可以被隔离成单独的应用程序进程。
此开发部分已知工作量巨大,预计将在多个发行版中逐步完成。这项工作始于早期 Squid-2 发行版,目前仍在进行中。
处理磁盘删除、ICMP、身份验证和 URL 重写的辅助进程的接口属于此层。
🔗 3. 最底层:多个事件和信号驱动的线程
在每个操作线程的最底层,将保留当前非阻塞事件的设计。这已被证明在扩展方面非常有效。
这项工作与中间层的工作是联合的。识别可以作为线程运行且与其他线程交互最少的事件组。每个线程拉取数据并在其上运行许多事件段的方法比频繁启动和停止线程更受青睐。
目前现有的处理路径需要进行审计,有些可能需要进行修改以减少资源交互。
🔗 进展和依赖关系
这构成了 Squid-3 维护者如何看待当前朝着 SMP 支持的工作流程。在每一个转折点都可能出现问题和意想不到的事情,首先是对此视图本身的分歧。
https://squid.org.cn/Devel/papers/threading-notes.txt 虽然已过时,但仍然包含对 Squid 内部 SMP 问题的一个良好且有效的分析,这些问题必须克服。
我们将 Squid 的 SMP 要求分解成一系列较小的工件,并尝试在有空余时间时完成以下工作:
- 将 Squid 代码模块化为适合 SMP 考虑的紧凑逻辑工作单元。跟踪为 Features/SourceLayout
- 然后,生成的模块库需要成为完全的异步 Features/NativeAsyncCalls 作业。
- 最后,将生成的作业制成 SMP 线程,以利用多个 CPU 之一。代码已检查线程安全性和高效资源处理。回想一下,一个作业需要它的调用按顺序发生。可能错了,但似乎异步调用可以在 CPU 之间浮动,只要它们保持顺序性。异步作业可以完全并行交错运行,可能在某个 Job 依赖于另一个 Job 的地方进行一些锁定。
一些其他功能旨在减少 SMP 的阻塞问题。并非完全沿着 SMP 功能路径前进,但为了使这些步骤成为可能而必需。
- Features/NoCentralStoreIndex
- Features/CommCleanup
- Features/ClientSideCleanup
- 转发 API 也需要工作,但还没有跟踪器功能。
🔗 资源和服务的共享
要安全地在 Worker 之间共享资源或服务,该资源或服务必须以安全共享的方式重写,或者其所有用法都必须被全局锁定。前者通常很困难,而后者通常效率低下。此外,由于众多相互依赖关系,通常无法重写单个资源/服务算法(因为所有底层代码都必须是安全共享的)或全局锁定它(因为这会导致死锁)。
目前已隔离但可能受益于共享的资源和服务包括:
- ipcache 和 fqdncache(可能受益于合并,以便只有一个 DNS 缓存需要共享)
- 基于 ufs 的缓存存储(见上文)
- 统计信息(从管理员的角度来看,当前的缓存管理器实现共享 worker 统计信息)
- 内存管理器
- 配置对象
可能需要以安全共享的方式重写或替换的低级组件包括:
- hash_link
- dlink_list
- FD / fde 处理
- 内存缓冲区
- String
- 任何带有静态变量的函数、方法或类。
经常被提及的一种可能性是使用具有更好线程安全实现的公共实现替换一个或多个低级组件(通常指链表和哈希算法)。但是,需要进行深入测试以检查是否适合快速高效的版本。
🔗 故障排除
🔗 Ipc::Mem::Segment::create 失败,无法 shm_open(…): (13) Permission denied
在 Linux 上,页面池应该“直接工作”。然而,它仍然依赖于 SHM 设备映射的初始化。
将以下行添加到您的 /etc/fstab 文件
shm /dev/shm tmpfs nodev,nosuid,noexec 0 0
之后,使用(作为 root)
mount shm
🔗 Ipc::Mem::Segment::create 失败,无法 shm_open(…): (2) No such file or directory
参见上文。
🔗 无法将套接字 FD NN 绑定到 [::]: (13) Permission denied
如果您禁用了 IPv6,也可能显示为 Cannot bind socket FD NN to 0.0.0.0: (13) Permission denied。
Squid 在 localstatedir 中注册其 IPC 通道套接字(Unix Domain Sockets 或 UDS)。对于标准安装,这通常是 /var/run/squid。如果您将 Squid 安装到其他目录,请参阅 –localstatedir ./configure 选项。
检查 /var/run/squid(或您的任何 localstatedir)的权限。localstatedir 目录需要由 Squid 用户拥有,SMP Squid 才能工作。
🔗 写入失败 (40) 消息过长
Squid Worker 交换 Unix Domain Sockets (UDS) 消息(请勿与 UDP 消息或 System V IPC 消息混淆)。这些消息的大小应该小于 16KB,但即使如此,在某些环境中也会因为非常低的默认 UDS 消息大小和缓冲区大小限制而产生问题。
通常,限制可以使用 sysctl 进行调整,但确切的控制名称未记录,并且因操作系统而异。以下是一个已知的变体,其中包含建议的最小设置(如果您知道,请添加更多!):
net.local.dgram.recvspace: 262144
net.local.dgram.maxdgram: 16384
类别:功能
导航:站点搜索,站点页面,类别,🔼 向上