按住您的马匹 - 如何调试高CPU使用量
#sql #database #性能

我们数据库中的高CPU使用情况可能表明多个问题。背景中可能会执行很多计算。我们的数据库可能缺乏可用的内存,并且进行了很多分页。数据库不妨花费大量时间在忙碌等待上。在这篇文章中,我们将看到如何处理此类情况。

可能是什么原因

高CPU使用可能是由非常昂贵的计算引起的。这是我们想到的第一件事。但是,这也是关于内存管理,锁定争论或SQL引擎做出的错误假设的其他问题的体现。在本节中,我们将看到一些通用的计算机科学原因,用于使用高CPU。

昂贵的计算

某些操作很难根据CPU功率来计算。虽然加法和减法非常便宜,但分裂和模量可能会更难,但是真正的杀手是三角操作和总体上。它们的速度可能比纯添加操作(基线)慢。

另一组昂贵的操作是加密功能。它们的设计缓慢,以保护应用程序免受计时攻击。例如,良好的哈希功能将进行更多的重复,以花费更多的计算时间。

根据您的情况,您可能会以多种方式获得一些改进。某些操作提供更快的版本,例如,可以使用AVX和SSE实现快速的Sinus功能。尽管这些功能提供了较低的精度,但在CPU周期数方面它们的速度要快得多。

另一种方法是用更简单的当量替换功能。这可能是一种更好的算法(例如,用矩阵指数而不是循环计算斐波那契数,也可以使用快速的指数算法),但这也可以是三角标识或其他等效的公式,可以给出相同的结果。有时,如果结果不取决于实际操作,则不进行计算是有益的。例如,如果您想找到最接近的点,那么您就不需要掌握权力之和的平方根。您可以直接比较幂的总和,因为平方根操作不会改变结果。

另一种方法是计算一次。临时操作结果的缓存,桌子查找和其他坚持的方式总是有益的。

但是,您始终需要衡量自己的改进。例如,缓存结果听起来可能是一个不错的解决方案,但是让我们举以下示例:

WITH cte_performance AS (
    SELECT *, MD5(MD5(ticket_no)) AS double_hash
    FROM boarding_passes
)
SELECT COUNT(*)
FROM cte_performance AS C1
JOIN cte_performance AS C2 ON C2.ticket_no = C1.ticket_no
JOIN cte_performance AS C# ON C3.ticket_no = C1.ticket_no
WHERE
    C1.double_hash = 'HASH'
    AND C2.double_hash = 'HASH'
    AND C3.double_hash = 'HASH'

在这里,我们计算票证号的哈希,然后将临时结果存储在CTE中。我们可能认为上面的查询等同于此:

SELECT COUNT(*)
FROM boarding_passes AS C1
JOIN boarding_passes AS C2 ON C2.ticket_no = C1.ticket_no
JOIN boarding_passes AS C# ON C3.ticket_no = C1.ticket_no
WHERE
    MD5(MD5(C1.ticket_no)) = 'HASH'
    AND MD5(MD5(C2.ticket_no)) = 'HASH'
    AND MD5(MD5(C3.ticket_no)) = 'HASH'

虽然它们在结果方面相当,但在绩效方面却大不相同。前者在13秒内运行,而后者则在8中完成。该课程是:始终衡量您的改进。

内存分页

几乎总是通过页面访问内存。内存页面是一个字节的块,通常为4KB长(取决于CPU架构)。该页面由虚拟地址解决,因此我们几乎永远不会直接触摸物理内存。 CPU需要将此虚拟地址映射到指向帧的物理地址中。帧再次是存储在物理内存中的字节块。

由于可以通过多个应用程序使用一个帧(考虑多个过程执行的相同二进制文件),因此可以将一个帧映射到多个虚拟地址中。这样可以保存内存(因为每个应用程序都使用相同的内存框架),但是由于寻址时的间接级别,内存管理较慢。

CPU和操作系统需要照顾加载内存框架并将其冲入驱动器。这个过程通常非常快,但是它不能忽略。有时,加载和卸载内存需要大量时间,尤其是当我们缺乏物理内存(因此系统重载)时。

另一个有趣的现象是bã©lâdy的异常,如果我们增加帧数(物理内存量),那么我们会增加页面故障的数量(在需要的情况下,在物理内存中不可用页面)这会导致更多页面加载。这可以降低性能。

由于分页对我们的应用程序是透明的,因此我们在发生时没有任何例外或错误。我们唯一可以观察到的是性能下降,因为内存访问只是较慢。当我们需要分类实体或加入表时,分页可能会特别痛苦。

内存分页也与我们访问的页面顺序强烈结合。例如,常见的优化现象之一是我们以编程语言遍历数组的方式。基于c的语言使用行序列订单,而基于fortran的语言则使用列 - 马约尔。行专业意味着当我们具有二维数组时,这些元素在行又一行存储,所以这样:


+------+------+-----+
| a11  | a12  | a13 |
+------+------+-----+
| a21  | a22  | a23 |
+------+------+-----+
| a31  | a32  | a33 |
+------+------+-----+

使用Row-Major订单,我们应该在IJ订单中穿越数组,因此使用以下代码:

For i from 1 to m
    For j from 1 to n
        A[i,j]

使用列订单,我们应该选择ji订单。这对于矩阵乘法尤其重要,在矩阵乘法中,ikj顺序比ijk One快得多。

磁盘播放

这与我们在上一节中讨论的分页非常相关。但是,这次我们完全专注于SQL引擎。

在这种情况下,SQL Server决定将临时结果溢出到驱动器。尽管SQL引擎有足够的内存(因此我们配置的数量可以通过引擎可用并可以访问),但SQL具有如此多的运行操作,因此需要在驱动器上存储临时结果。例如,引擎可能需要对行进行排序,计算瞬态内存中的哈希表,计算临时表等。引擎不使用更多内存(由于限制或由于交换而使用),因此需要将结果冲洗到磁盘。

我们可以通过减少SQL引擎上的负载,或重写查询以使用较少的数据,或者通过手动计算临时表而不是隐式使用它们(通过Subqueries或CTES)来帮助这种情况。同样,许多优化将适用于服务器的运行配置,因此,如果我们更改参数,则优化可能会停止工作(甚至降低性能)。

锁和争论

锁和闩锁用于每次交易。它们可以明确使用(取决于隔离级别),也可以隐式使用(例如,穿越结构)。如果多个交易访问相同的数据,那么我们可能会得到锁定的争论。

但是,锁的大问题之一是所谓的虚假共享。因为事物是被缓存或同时访问的,所以即使我们不使用它,我们也可能不小心锁定相同的内存地址。请参阅以下代码:

int[] array = new int[64];
Thread t1 = new Thread(() => {
    for(int i=0;i<iterations;++i){
        array[1]++;
    }
}).Run();
Thread t2 = new Thread(() => {
    for(int i=0;i<iterations;++i){
        array[2]++;
    }
}).Run();

我们有一个64个整数。我们创建两个线程,它们俩都需要循环并增加一个独立的整数。似乎这里没有锁定,没有什么可以减慢我们的速度。但是,由于CPU缓存数据(带有MESI协议),并且高速缓存线大于一个整数(通常是16个整数或更高),因此每次都需要将内存从L1 CPU缓存到RAM。任何线程都执行修改,因为两个线程都在同一缓存线上运行。

虚假共享可以在多种情况下弹出。例如,采用以下方案:

  • 第一次交易附加在群集索引末尾的实体
  • 第二笔交易从索引读取第一个实体

这两项交易是否竞争任何资源?好像他们没有。但是,当您在索引中添加新页面时,您可能需要更新root b-tree节点。要更新节点,您需要锁定它。因此,即使另一个事务不修改节点(因为它只是读取数据),它仍然需要等待锁定锁定。这可以大大降低性能。

当应用程序需要等待锁可用时,还有更多示例。例如,每个内存分配可能需要获取堆锁(操作系统提供)。每个动态类型的创建可能都需要锁定内部结构。错误的共享和内存争论经常是如此,以至于某些应用程序决定实施自己的同步原始功能以提高性能。一些SQL引擎甚至决定实施自己的操作系统(例如Microsoft SQLOS)。

错误的统计数据

SQL引擎中的统计信息用于计划查询执行。该引擎可能以多种方式执行操作,例如,加入两个表可以是具有多种策略之一。该引擎决定如何根据行,数据分布,索引,配置参数以及许多其他内容来执行操作。它们中的大多数被存储为有关表的统计数据。

统计信息将自动存储和更新。当大量实体被修改后,发动机会刷新它们。我们可以对统计数据进行重新计算,但我们通常不会自行解决。

统计信息包括行数,行尺寸,数据分布(零值等)以及其他有用的聚合物的详细信息,这些汇总有助于引擎决定该怎么做以及如何做。但是,统计数据可能会过时,尤其是当我们经常修改实体时。这可能会导致非最佳操作,例如以非最佳顺序访问表或使用效率较低的加入策略。

我们需要关注统计数据并在它们变成陈旧时刷新它们。

延迟更新和分裂

当我们修改表中的数据时,引擎可以自由标记为墓碑(或死行)。这意味着行仍存储在驱动器上,但被标记为删除,不再适用。这导致了碎片。

碎片可以是内部和外部的。内部碎片是我们可以使用的页面内有一些空白空间的情况。该页面是空的,有效地浪费了。但是,在扫描表时,我们需要加载此内存并跳过它。这需要时间。

当驱动器上的页面与实体的逻辑顺序不同时,

外部碎片就会发生。即使我们将表作为群集索引(执行行的逻辑顺序)存储时,数据仍然可以存储在顺序中。想象一下,您的实体具有从一到五十到八十到一百的标识符。如果您现在添加带有五十到八十之间的标识符的行,则需要将这些行存储在五十到八十的标识符之间。但是,索引中没有物理空间。在这种情况下,引擎可以决定在侧面创建一个其他存储页面。扫描索引时,引擎可能需要在页面之间跳跃以获取正确的订单数据。

我们可以通过重新组织或重建索引,删除死行或使用自动流程来消除碎片。数据库可以在幕后运行多个操作,以提高存储质量。但是,这些操作可能是CPU密集的,并且会导致CPU使用高。

概括

我们看到了高CPU使用量的多种原因。在下一部分中,我们将讨论解决问题的方法。我们将看到如何调查CPU使用的原因是什么以及如何修复它。