最近我告诉你如何使用标准食谱
#1.切片
一篇关于如何以及为何值得组织的文章
“那些年的事情……”
最初,和任何 MVP 一样,我们的项目在相当轻的负载下开始 - 只对最关键的十台服务器进行监控,所有表都相对紧凑......但随着时间的推移,监控的主机数量越来越多,我们再次尝试用其中之一做某事 表大小为 1.5TB,我们意识到虽然可以继续这样生活,但是很不方便。
那个时代几乎就像史诗般的时代,不同版本的 PostgreSQL 9.x 是相关的,因此所有分区都必须“手动”完成 - 通过 表继承和触发器 动态路由 EXECUTE
.
事实证明,最终的解决方案足够通用,可以转换为所有表:
- 声明了一个空的“header”父表,它描述了所有 必要的索引和触发器.
- 从客户端的角度来看的记录是在“根”表中进行的,并且内部使用 路由触发
BEFORE INSERT
该记录被“物理”插入到所需的部分。如果还没有这样的事情,我们就会捕获一个异常并且...... - … 通过使用
根据父表的模板创建 对所需日期有限制的部分这样当检索数据时,仅在其中执行读取。CREATE TABLE ... (LIKE ... INCLUDING ...)
PG10:第一次尝试
但从历史上看,通过继承进行分区并不适合处理活动写入流或大量子分区。例如,您可以回忆一下用于选择所需部分的算法 二次复杂度,它适用于 100 多个部分,您自己了解如何...
在PG10中,通过实施支持大大优化了这种情况
仔细阅读手册后发现,该版本中的本机分区表是:
- 不支持索引描述
- 不支持触发器
- 不能成为任何人的“后代”
- 不支持
INSERT ... ON CONFLICT
- 无法自动生成节
额头被耙子狠狠地敲了一下,我们意识到如果不修改应用程序就不可能做到这一点,因此将进一步的研究推迟了六个月。
PG10:第二次机会
于是,我们开始一一解决出现的问题:
- 因为触发器和
ON CONFLICT
我们发现我们仍然到处需要它们,所以我们做了一个中间阶段来解决它们 代理表. - 摆脱“路由” 在触发器中 - 也就是说,从
EXECUTE
. - 他们分别拿出来 包含所有索引的模板表因此它们甚至不存在于代理表中。
最后,在这一切之后,我们对主表进行了原生分区。新部分的创建仍然取决于应用程序的良心。
“锯”字典
与任何分析系统一样,我们也有 “事实”与“删减” (词典)。在我们的案例中,他们以此身份行事,例如,
“事实”已经按天划分很长时间了,所以我们平静地删除了过时的部分,它们并没有打扰我们(日志!)。但是字典有问题...
并不是说有很多,但大约 100TB 的“事实”生成了 2.5TB 的字典。您无法方便地从这样的表中删除任何内容,也无法在足够的时间内压缩它,并且写入它的速度逐渐变慢。
就像一本字典......其中,每个条目应该只出现一次......这是正确的,但是!......没有人阻止我们拥有 每天都有一本单独的词典!是的,这带来了一定的冗余,但它允许:
- 写/读速度更快 由于截面尺寸较小
- 消耗更少的内存 通过使用更紧凑的索引
- 存储较少的数据 由于能够快速删除过时的
由于采取了一系列复杂的措施 CPU 负载降低约 30%,磁盘负载降低约 50%:
与此同时,我们继续将完全相同的内容写入数据库,只是负载较少。
#2.数据库演化与重构
所以我们决定了我们所拥有的 每天都有自己的部分 与数据。实际上, CHECK (dt = '2018-10-12'::date)
— 并且有一个分区键和一条记录落入特定部分的条件。
由于我们服务中的所有报告都是在特定日期的背景下构建的,因此自“非分区时间”以来它们的索引都是所有类型 (服务器, 日期,计划模板), (服务器, 日期,计划节点), (日期,错误类别,服务器),...
但现在他们住在每个区域 你的副本 每个这样的索引......以及在每个部分中 日期是一个常数...事实证明,现在我们在每个这样的索引中 只需输入一个常数 作为字段之一,这会增加其数量和搜索时间,但不会带来任何结果。他们把耙子留给了自己,哎呀......
优化的方向很明显——简单 从所有索引中删除日期字段 在分区表上。考虑到我们的数量,收益约为 1TB/周!
现在让我们注意,这个太字节仍然必须以某种方式记录。也就是说,我们也 磁盘现在应该加载更少!这张图清楚地显示了我们花了一周时间进行清洁所获得的效果:
#3。 “分散”高峰负荷
负载系统的一大麻烦是 冗余同步 一些不需要它的操作。有时“因为他们没有注意到”,有时“这样更容易”,但迟早你必须摆脱它。
让我们放大上一张图片,看看我们有一个磁盘 双振幅负载下的“泵” 相邻样本之间的差异,显然“统计上”不应发生如此数量的操作:
这是很容易实现的。我们已经开始监控 近1000台服务器,每个都由一个单独的逻辑线程处理,每个线程都会以一定的频率重置要发送到数据库的累积信息,如下所示:
setInterval(sendToDB, interval)
这里的问题恰恰在于: 所有线程几乎同时启动,因此他们的发送时间几乎总是“恰到好处”地一致。哎呀#2...
幸运的是,这很容易解决, 添加“随机”助跑 按时间:
setInterval(sendToDB, interval * (1 + 0.1 * (Math.random() - 0.5)))
#4。我们缓存我们需要的东西
第三个传统的高负载问题是 无缓存 他在哪 可能 是的。
例如,我们可以根据计划节点进行分析(所有这些 Seq Scan on users
),但立即认为它们在很大程度上是相同的 - 他们忘记了。
不,当然,没有任何内容再次写入数据库,这会切断触发器 INSERT ... ON CONFLICT DO NOTHING
。但这些数据仍然到达数据库,而且没有必要 阅读以检查冲突 得做。哎呀#3...
启用缓存之前/之后发送到数据库的记录数差异很明显:
这是随之而来的存储负载下降:
在总
“每天太字节”听起来很可怕。如果你做的一切都是正确的,那么这只是 2^40 字节/86400 秒 = ~12.5MB/s甚至桌面 IDE 螺丝也能固定。 🙂
但说实话,即使白天的负载“倾斜”十倍,您也可以轻松满足现代 SSD 的功能。
来源: habr.com