将大量数据传输到PostgreSQL

今天,我将分享一些有用的体系结构解决方案,这些解决方案是在开发用于大规模分析PostgeSQL服务器性能的工具的过程中产生的,这些解决方案现在帮助我们将超过一千台主机的完全监视和分析“适配”到同一硬件中,起初仅够一百个...





介绍



让我提醒您一些介绍性注意事项:



  • 我们正在构建一个从PostgreSQL服务器的日志中接收信息的服务
  • 收集日志,我们希望在线处理它们(解析,分析,请求其他信息)
  • 所有收集和“分析”的东西都必须保存在某个地方


让我们谈谈 最后一点-如何将所有这些交付到PostgreSQL存储在我们的案例中,此类数据是原始数据的倍数-特定应用程序和计划模板的上下文中的负载统计信息,资源消耗以及对单个计划节点的准确性计算出的派生问题计算,监视锁等等。

可以在视频报告中看到更多有关服务原理的信息,并在文章“ PostgreSQL查询的质量优化”中阅读


推与拉



有两种用于获取日志或其他不断到达的指标的主要模型:



  • 推送服务在受监视的服务器上有许多对等接收器-一些本地代理会定期将累积的信息转储到服务中
  • -对服务,每个进程/线程/协同程序/ ...处理信息,从只有一个“自己的”源,数据的接收从中自行启动


这些模型中的每一个都有正面和负面的一面。





交互由观察到的节点启动:



在以下情况下是有益的:



  • 您有很多资源(数十万)
  • 它们之间的负载差异不大,并且不超过〜1rps
  • 不需要一些复杂的处理




示例:OFD运营商的接收方从每个客户收银机接收支票。



...导致问题:



  • 尝试在来自不同流的监视对象的上下文中编写字典/分析/聚合时发生锁定/死锁
  • 每个BL进程/与数据库的连接的高速缓存的利用率最差-例如,与数据库的同一连接必须首先写入一个表或索引段,然后立即写入另一个表或索引段
  • 需要在每个源放置一个特殊的代理,这会增加其负担
  • 网络交互的开销-标头必须“绑定”每个数据包的发送,而不是整个与源的整个连接




启动器是收集器的特定主机/进程/线程,它将节点“绑定”到自身并独立地从“目标”中提取数据:



在以下情况下是有益的:



  • 来源很少(十万)
  • 几乎总会有负载,有时甚至达到1Krps
  • 需要复杂的处理,并按来源进行细分




示例:在每个交易平台的上下文中的交易的加载器/分析器



...导致问题:



  • 限制用于通过一个进程(CPU内核)处理一个源的资源,因为不能在两个接收者之间“涂抹”该资源
  • 需要一个协调器来动态地跨现有进程/线程/资源从源重新分配负载


由于在监视PostgreSQL时,我们的负载模型显然偏向于pull算法,并且一个进程的资源和现代CPU的内核对于我们来说对于一个源就足够了,因此我们就停止了。



拉式原木



我们与服务器的通信提供了许多网络操作,并且可以处理slaboformatirovannymi文本字符串,因此作为收集器核心,JavaScript在作为服务器Node.js的化身中非常完美



事实证明,最简单的从服务器日志中获取数据的解决方案是使用简单的linux命令将整个日志文件“镜像”到控制台tail -F <current.log>只有我们的控制台不是简单的,而是虚拟的-通过SSH协议扩展到服务器安全连接。



因此,收集器位于SSH连接的第二侧,接收所有日志流量的完整副本作为输入。并在必要时向服务器请求有关当前事务状态的扩展系统信息。



为什么不使用syslog



主要有两个原因:



  1. syslog push-, . - «» , .



    «» / , .
  2. PostgreSQL, , «» (relation/page/tuple/...).

    您可以在文章“ DBA:追求飞锁”中阅读有关解决此问题的更多信息


设置接收器底座



原则上,其他解决方案也可以用作DBMS来存储从日志中解析出的数据,但是150-200GB /天的传入信息量并没有太大的回旋余地。因此,我们还选择了PostgreSQL作为存储。



-PostgreSQL用于存储日志?认真吗

-首先,除了各种分析表示法外,日志还远不止于此其次,“你就是不知道怎么做!” :)






服务器设定



这是主观的,在很大程度上取决于您的硬件,但是我们为自己配置了用于主动记录的PostgreSQL主机时遵循了以下原则。



文件系统设置

影响写性能的最重要因素是数据分区的[未正确安装]。我们选择了以下规则:



  • 使用参数挂载PGD​​ATA目录(对于ext4)noatime,nodiratime,barrier=0,errors=remount-ro,data=writeback,nobh
  • 目录PGD​​ATA / pg_stat_tmp移动tmpfs
  • 如果合理,PGD​​ATA / pg_wal目录移动到另一个介质


请参见PostgreSQL文件系统调整



选择最佳的I / O调度程序

默认情况下cfq,在RedHat和CentOS-中,许多发行版已被选作I / O调度程序并经过了优化,可用于“桌面” noop。但是事实证明对我们来说更有用deadline



参见PostgreSQL vs. I / O调度程序(cfq,noop和截止日期)



减小“脏”缓存的大小

此参数vm.dirty_background_bytes以字节为单位设置缓存的大小,达到该值时,系统将启动将其刷新到磁盘的后台进程。有一个相似但互斥的参数vm.dirty_background_ratio-它设置的值与总内存大小的百分比相同-默认情况下,它是设置的,而不是“ ... bytes”。



在大多数发行版中为10%,在CentOS上为5%。这意味着在服务器总内存为16GB的情况下,系统可能会尝试一次将850MB以上的内容写入磁盘,从而导致IOps峰值负载。



我们通过实验降低它,直到记录峰值开始平滑。从经验来看,为避免出现峰值,该大小应小于最大媒体吞吐量(以IOps为单位)乘以内存页面大小。也就是说,例如,对于7K IOps(〜7000 x 4096)-大约28MB。



请参见postgresql.conf中的为PostgreSQL优化设置配置Linux内核选项





应该看到什么参数,扭曲以加快录制速度。这里的一切纯粹是个人的,因此,我将只对该主题进行一些思考:



  • shared_buffers -应该减小它的大小,因为有针对性地记录特别重叠的“公共”数据,因此不会产生处理
  • synchronous_commit = off -如果您信任RAID控制器的电池,则可以始终禁用等待提交写入
  • fsync-如果数据不是很关键,则可以尝试将其关闭-“在极限”甚至可以获取内存数据库


数据库表结构



我已经发表了一些有关物理数据存储优化的文章:





但是关于数据中的不同键-还没有。我会告诉你他们的。对于写繁重的系统,



外键是有害的。实际上,这些是“拐杖”,它们不允许粗心的程序员将可能不应该存在的内容写入数据库。



许多开发人员习惯于这样的事实,即在描述数据库表的级别上与逻辑相关的业务实体必须通过FK链接。但这种情况并非如此!



当然,这一点很大程度上取决于您将数据写入数据库时​​设置的目标。如果您不是银行(如果您也是银行,那么就不进行处理!),那么在大量写入数据库中是否需要FK是一个大问题。插入记录时,



“技术上”每个FK都进行单独的SELECT从引用的表。现在看一下您正在积极编写的表,其中挂有2-3个FK,并评估对于特定任务是否值得,确保性能降低3-4倍的完整性……或者按值进行逻辑连接是否足够?在这里,我们删除了所有FK。



UUID键很好由于在不同的不相关点处生成的UUID发生冲突的可能性非常小,因此可以通过从数据库到“消费者”安全地删除此负载(通过生成一些代理ID)。在连接的非同步分布式系统中,UUID的使用是一种好习惯。

您可以在文章“ PostgreSQL反模式:唯一标识符中了解PostgreSQL中唯一标识符的其他变体


即使自然键包含多个字段,自然键也很好人们应该不怕复合键,而要担心一个额外的替代PK字段和它在已加载表中的索引,您可以很容易地做到这一点。



同时,没有人禁止组合方法。例如,我们有一个代理UUID分配给与一个原始事务相关的顺序日志记录“批” (因为根本没有自然键),但是将一对用作PK (pack::uuid, recno::int2),其中recno批中记录“自然”序号



“无尽”的COPY流



PostgreSQL与OC一样,当将数据以大批量(例如INSERT1000行写入其中时,“不喜欢”它但这COPY更能容忍平衡的写入流(通过)。但是他们必须能够非常小心地做饭。



  1. 由于在上一阶段我们删除了所有FK,因此现在我们可以以任意顺序异步地写入有关其自身pack和一组相关FK的信息在这种情况下,最有效的方法是为每个目标表保留一个持续活动的通道reordCOPY
  2. , , «», ( — COPY-) . , — 100, .
  3. , , . . .



    , , «» , . , .
  4. , node-pg, PostgreSQL Node.js, API — stream.write(data) COPY- true, , false, .





    , , « », COPY .
  5. COPY- LRU «». .




在这里应该注意的是,通过这种读取和写入日志的方案,我们获得了主要优势-在我们的数据库中,几秒钟后“事实”几乎可以在线分析了



用文件优化



一切似乎都很好。先前方案中的“耙”在哪里?让我们开始简单...



过度同步



加载系统的最大麻烦之一是某些不需要它的操作过度同步有时是“因为他们没有注意到”,有时是“这样比较容易”,但是迟早您必须摆脱它。



这很容易实现。我们已经设置了将近1000个服务器进行监视,每个服务器都由单独的逻辑线程处理,并且每个线程以一定的频率转储累积的信息以发送到数据库,如下所示:



setInterval(writeDB, interval)


这里的问题恰好在于所有流都大约在同一时间开始,因此发送它们的时刻几乎总是“精确到点”。





幸运的是,这很容易解决-通过为开始时刻和该间隔添加一个“随机”时间间隔:



setInterval(writeDB, interval * (1 + 0.1 * (Math.random() - 0.5)))






通过此方法,您可以统计地“分散”录音中的负载,将其变为几乎均匀。



通过CPU内核扩展



一个处理器内核显然不足以满足我们的整个负载,但是集群模块将在这里为我们提供帮助,这使我们能够轻松管理子进程的创建并通过IPC与它们进行通信。



现在,我们有16个子进程用于16个处理器内核-很好,我们可以使用整个CPU!但是在每个过程中,我们都写入16个目标板,并且在达到峰值负载时,我们还会打开其他COPY通道。也就是说,基于不断地256个以上的主动编写线程...哦!这样的混乱对磁盘性能没有良好的影响,并且开始燃烧基础。



尝试写下一些常用字典(例如,来自不同节点的相同请求文本)-不必要的锁,等待中时,这尤其令人难过。





让我们“逆转”这种情况-也就是说,让子进程仍从其源收集并处理信息,但不要写入数据库!相反,让他们通过IPC向主服务器发送一条消息,而他已经在需要的地方写了一些东西:





谁立即在上一段的计划中看到了问题,那就做得很好。正是在这一刻,掌握也是一个资源有限的过程。因此,在某个时候,我们发现他已经开始燃烧-它只是停止应对将所有线程转移到数据库的问题,因为它也受到一个CPU内核资源的限制结果,我们将大部分负载最小的“字典”流通过master写入,而负载最大但又不需要其他处理的流将返回给worker:





多收集器



但是,即使只有一个节点也不足以服务所有可用负载-现在该考虑线性缩放了。解决方案是一个收集器,根据负载自动平衡,并在头部设置协调器。





每个主节点将其所有工作人员的当前负载转储给他,并作为响应接收有关应该将节点监视转移给另一个工作人员甚至另一个收集器的建议。将有单独的文章介绍这种平衡算法。



池化和队列限制



下一个正确的问题是在突然出现峰值负载时如何处理写流



毕竟,我们不能无休止地与基地建立越来越多的新连接-这是无效的,也无济于事。一个简单的解决方案-让我们对其进行限制,以使每个目标表的同时活动线程不超过16个。但是如何处理我们仍然“没有时间”写入的数据呢?..



如果负载的这种“激增”正好是峰值,即是短期的,那么我们可以将数据暂时保存在收集器本身的内存中的队列中。一旦释放了一些到达基础的通道,我们就会从队列中检索记录并将其发送到流。



是的,这要求收集器具有一些用于存储队列的缓冲区,但是它很小并且可以快速释放:





队列优先级



细心的读者在看了前一张图片后再次感到困惑,“当记忆完全用尽时会发生什么?。”已经很少的选择-必须牺牲一些人。



但是,并非我们要传递到数据库的所有记录都是“同等有用的”。为了我们的利益,尽可能地将它们定量地记录下来。原始的“指数优先级”(按写入的字符串的大小)将帮助我们:



let priority = Math.trunc(Math.log2(line.length));
queue[priority].push(line);


因此,在写入通道时,我们总是从“较低的”队列中开始搜寻-只是每个单独的行在那里较短,我们可以定量地发送它们:



let qkeys = Object.keys(queue);
qkeys.sort((x, y) => x.valueOf() - y.valueOf()); // - - !


击败障碍



现在让我们返回两个步骤。到我们决定在一张表的地址上最多保留16个线程时。如果目标表是“流”,即记录彼此不相关,则一切正常。最大值-我们将在磁盘级别上具有“物理”锁。



但是,如果这是一个聚合表,甚至是一个“字典”,那么当我们尝试从不同的流中写入具有相同PK的行时,我们将收到等待锁,甚至死锁。真可悲……



但是,毕竟要写些什么-我们定义自己!关键不是要尝试从不同的地方写一个PK



也就是说,在传递队列时,我们立即查看是否有这样的PK在某个表中写入了某个线程(我们记得它们都在一个进程的公共地址空间中)。如果不是,我们将其视为自己,然后将其写到“为我们自己”的内存字典中;如果它已经是别人的字典,则将其放入队列中。



在交易结束时,我们只是简单地从字典中“清除”附件“自己”。



一点证明



首先,使用LRU,“第一个”连接和为其提供服务的PostgreSQL进程几乎始终在运行。这意味着OS很少在CPU内核之间切换它们,从而最大程度地减少了停机时间。





其次,如果您几乎始终在服务器端使用相同的进程,则两个进程同时处于活动状态的机会将大大降低-因此,整个CPU的峰值负载将减少(左图第二张图中的灰色区域) ),而LA之所以倒闭,是因为等待轮到的进程更少。





今天就这些。



并且提醒您,借助explain.tensor.ru,您可以看到各种用于可视化查询执行计划的选项,这将帮助您清楚地了解问题区域。



All Articles