【问题标题】:Scale a PostgreSQL stored procedure缩放 PostgreSQL 存储过程
【发布时间】:2014-11-05 23:10:23
【问题描述】:

我当前的 PostgreSQL 存储过程实现无法扩展,但问题很容易拆分为并行进程/线程。


设置

一个行为与约会平台非常相似的应用程序,即用户注册,输入一些个人资料详细信息,然后根据这些详细信息与所有其他用户进行匹配。详细信息可以总结为 60-70 个属性,主要是布尔值,这些属性存储在 user_attributes 表中的用户记录中。所以有一个大的user_attributes 表,由用户ID 和属性组成(其他配置文件数据存储在单独的表中)。出于性能考虑,选择了逐列属性方案,即防止为获取一个用户的所有属性而进行额外查询。对于每个匹配,每个用户都有一个匹配表,因此每个用户都有自己的表,由 user_id、other_user_id、matching_score 组成。

我们希望每个数据库实例拥有多达 30 万用户,但看看它如何扩展十倍(即多达 300 万用户)会很有趣。除此之外,我们可以通过分发到其他数据库实例来进行扩展。然而,我们开始遇到大约 80k 用户的可扩展性问题。


问题

如前所述,出于性能考虑,所有属性都放在了user_attributes 表中,每个属性一列。我们创建了一个存储过程 (create_user),它将所有 60-70 个属性作为参数,在用户表中创建一条记录,然后开始从 user_attributes 表中选择所有其他用户,包括他们的属性并开始计算匹配分数,并将最终结果插入到新创建的UserXYZ_matches 表中。

我们现在运行测试以查看设置的执行情况(每次插入一个用户,直到达到 30 万用户),结果发现大约 8 万用户,我们的 CPU 成为瓶颈。虽然测试机配备了 4 核 / 8 线程,但实际上只使用了一个。问题是每个其他用户的匹配需要很长时间(PL/pgSQL 在这里的性能很差),但核心问题是所有这些匹配都发生在一个 CPU 上。例如,对所有其他用户的匹配可以分成8个不同的操作,每个操作取user_attributes表记录的1/8,执行匹配并插入到结果表中。我们可以优化性能不佳的 PL/pgSQL,但我不知道如何在其他 CPU 内核/线程之间分配工作。


其他信息

请作为 cmets 发布有关该方法的整体建议。我非常感谢有关如何总体上做得更好的建议,但不能作为对这个特定问题的答案。

所有用户匹配的表都存储在一个表空间中,该表空间由跨几个磁盘的 XFS 和 LVM 条带化支持。用户匹配表的数量(每个用户一个)似乎不是可伸缩性问题(正如我们最初认为的那样)。所以磁盘不是问题,而且大量的表似乎都被特定的设置所覆盖。

create_user 的调用/查询应该是原子的,即基于事务的。这是为了我们的测试运行,但不一定是最终产品的硬性要求。

create_user 过程基本上是这样的(太长了,不能作为一个整体发布):

CREATE OR REPLACE FUNCTION create_user(...)
    -- (1) input_user = INSERT INTO user_attributes VALUES (parameter0, parameter1, ...)
    -- (2) create userXYZ_matching_table
    -- (3) FOR row IN SELECT * FROM "user_attributes" WHERE "id" <> input_user."id" LOOP
    --        -- repeat for every attribute
    --        IF row.this_attribute = input_user.this_attribute THEN
    --           match := match + 1
    --        END IF;       
    --        -- finally
    --        INSERT INTO userXYZ_matching_table VALUES (input.user.id, row.id, match)
    --     END LOOP;
LANGUAGE PLPGSQL;

我知道高 CPU 使用率来自于 IF、ELSIF、END IF 块的数量(60-70)。同样,这可以优化,但如何扩展这种存储过程的问题仍然存在。

当前运行测试的服务器如下所示,很好地说明了问题:

【问题讨论】:

    标签: postgresql stored-procedures plpgsql


    【解决方案1】:

    据我所知和阅读文档的能力,PL/pgSQL 不支持并行性,服务器也不并行处理单个查询。因此,我倾向于说,进一步扩展将需要 客户端 上的并行化(新用户通过具有单独连接的多个并发线程/进程插入)。

    不过,总体而言,您有一个固有的缩放问题,即要添加一条新记录,您需要将其与所有其他记录进行比较。为 N 条记录执行此操作的成本为 N^2,并且您已经将 CPU 固定在 25% 的路上。添加第 320,000 条记录的成本将是添加第 80,000 条记录的四倍,而添加总共 320,000 条记录的成本将至少是添加 80,000 条记录的 16 倍

    可以想象,使用SELECT INTO 查询而不是存储过程可以在一定程度上提高性能,但这不会提高渐近复杂度。您还可以考虑异步创建匹配表,以改善初始响应。

    【讨论】:

    • 感谢您的回答!我知道这不会永远扩展,并且在客户端级别进行并行化是我想到的一件事。只是希望也许有一个(postgres)技巧,我可以在存储过程中并行化 FOR...
    • @grasbueschel 它不会永远扩展,它会从相当少的用户数非常糟糕扩展。您需要完全重新考虑这一点,异步进行匹配,有效利用索引等。
    • create_user() 函数在服务器上运行。事实上,它在服务器上运行了一个大循环。我认为客户端上的并行化不会像 OP 需要的那样有帮助。我很确定解决方案将需要不同的算法。
    【解决方案2】:

    (详细说明约翰的回答;请接受他不是我的):

    PL/PgSQL 可能是这个任务的糟糕选择。

    数学和逻辑运算很慢。非常慢。 PL/PgSQL 非常适合将几个 SQL 语句粘合在一起——其中大部分工作由 SQL 语句完成。对于大量的数学工作和逻辑来说,这很糟糕。

    它也不能利用任何 CPU 并行性。

    此外,像您一样运行许多单独的小插入会非常缓慢。不要那样做。相反,如果您必须在 PL/PgSQL 中执行此操作,请让您的函数返回一组要插入的结果的元组,并将其称为 INSERT INTO target_table SELECT * FROM my_procedure(...) 或类似名称。会快很多。

    作为用户插入的一部分同步执行工作会使整个事情变得更糟,因为问题对用户来说更明显。特别是因为,正如 John 所指出的,这在 O(N2) 处缩放,即二次方。

    您的设计完全不可行,需要从头开始重新考虑。

    我建议一种依赖于事实表的方法,例如星型模式。每个属性都是关于用户的“事实”。每个事实表都是一个(user_id, fact_value) 元组。 (user_id, fact_value) 上存在复合索引。

    当插入用户时,将他们的用户记录标记为待匹配,并在用户记录自身中使用一个标志,并在插入用户的同一事务中将条目插入到事实表中。

    然后让您的应用程序以后台任务的形式异步处理一个等待匹配的用户队列。加入事实表以查找最相似的用户,即具有最相似值的最多事实。您的应用程序可以利用多个 PostgreSQL 连接来实现并行性,一次处理多个用户或执行部分连接以在临时表中生成中间结果,然后您连接以查找最终结果。

    【讨论】:

    • 感谢您的详细解释和提示!我错过了测试即将达到同步插入速度太慢的限制,并且没有办法异步执行它。 PL/PgSQL 不是必需的,但在数据库级别使用它似乎是有道理的(即我们可以切换到另一种语言)。此外,问题将始终保持 O(N²),因为我必须将每个用户相互匹配......
    • @grasbueschel 您应该能够通过适当的数据结构、算法和索引使用来实现更接近 O(n log n) 的结果。诀窍是确保每个新用户不必为每个以前的用户重复所有工作,只需通过适当的 b-tree 索引扫描使用已经标准化的属性数据。
    【解决方案3】:

    这是一个古老的步骤和一个有趣的阅读。当我试图找出我们是否可以在 SP 中使用线程类型的机制时,我遇到了这个问题。

    这个讨论线程中解释的设计(不确定这是否仍然相关),我们可以做的是将用户表中的插入保留在一个存储过程中,以及创建 user_attribute 表和填充它的所有逻辑可以移动到一个触发器。 不确定您是否可以根据您的设计为多级触发器设计流程。 不利的一面是它可能会增加失败点的数量..但是一旦测试良好就可以解决问题..

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-12-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-05
      • 2015-03-13
      相关资源
      最近更新 更多