【发布时间】:2018-09-06 00:10:57
【问题描述】:
设置
我有一个最终将包含 130 亿行的数据库。这些行以 4 个值为键:(asn, cty (country), src (source), time)。
asn 大约有 60,000 个不同的值,country 大约有 200 个不同的值,source 大约有 55 个不同的值——尽管并非所有三元组都有效。大约有 500,000 个有效的三元组。
对于每个有效的三元组,我每 5 分钟将数据记录到数据库中一次,time 是记录数据的时间。 90 天后,我们会彻底删除数据。这个产量12 (iterations per hour) * 24 (hours) * 90 (days) = 25920 rows per (asn, country, source) tuple
我的指标表目前如下所示:
create table `metrics` (
`time` int(10) unsigned NOT NULL,
`asn` int(10) unsigned NOT NULL,
`cty` char(2) NOT NULL,
`src` char(3) NOT NULL,
`reqs` int(10) unsigned DEFAULT NULL,
`rtt` float unsigned DEFAULT NULL,
`rexb` float unsigned DEFAULT NULL,
`nae` float unsigned DEFAULT NULL,
`util` float unsigned DEFAULT NULL,
PRIMARY KEY (`time`, `asn`, `cty`, `src`),
KEY (`asn`, `cty`, `src`)
) ENGINE=InnoDB DEFAULT CHARACTER SET ascii
partition by range(time) (
PARTITION start VALUES LESS THAN (0),
PARTITION from20171224 VALUES LESS THAN (UNIX_TIMESTAMP('2017-12-31')),
PARTITION from20171231 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-07')),
PARTITION from20180107 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-14')),
PARTITION from20180114 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-21')),
PARTITION from20180121 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-28')),
PARTITION from20180128 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-04')),
PARTITION from20180204 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-11')),
PARTITION from20180211 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-18')),
PARTITION from20180218 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-25')),
PARTITION from20180225 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-04')),
PARTITION from20180304 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-11')),
PARTITION from20180311 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-18')),
PARTITION from20180318 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-25')),
PARTITION from20180325 VALUES LESS THAN (UNIX_TIMESTAMP('2018-04-01')),
PARTITION future VALUES LESS THAN MAXVALUE
);
我还有一个“阈值”表,它记录了在任何给定时间间隔内“好的 RTT”的样子和“坏 RTT”的样子:
create table `thresholds` (
`time` int(10) unsigned NOT NULL,
`rtt_good` float NOT NULL DEFAULT 0,
`rtt_bad` float NOT NULL DEFAULT 100,
`rexb_good` float NOT NULL DEFAULT 0,
`rexb_bad` float NOT NULL DEFAULT 100,
`nae_good` float NOT NULL DEFAULT 0,
`nae_bad` float NOT NULL DEFAULT 100,
`util_good` float NOT NULL DEFAULT 0,
`util_bad` float NOT NULL DEFAULT 100,
PRIMARY KEY (`time`)
) ENGINE=InnoDB
partition by range(time) (
PARTITION start VALUES LESS THAN (0),
PARTITION from20171224 VALUES LESS THAN (UNIX_TIMESTAMP('2017-12-31')),
PARTITION from20171231 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-07')),
PARTITION from20180107 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-14')),
PARTITION from20180114 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-21')),
PARTITION from20180121 VALUES LESS THAN (UNIX_TIMESTAMP('2018-01-28')),
PARTITION from20180128 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-04')),
PARTITION from20180204 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-11')),
PARTITION from20180211 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-18')),
PARTITION from20180218 VALUES LESS THAN (UNIX_TIMESTAMP('2018-02-25')),
PARTITION from20180225 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-04')),
PARTITION from20180304 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-11')),
PARTITION from20180311 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-18')),
PARTITION from20180318 VALUES LESS THAN (UNIX_TIMESTAMP('2018-03-25')),
PARTITION from20180325 VALUES LESS THAN (UNIX_TIMESTAMP('2018-04-01')),
PARTITION future VALUES LESS THAN MAXVALUE
);
查询
现在,我对此数据执行的最常见查询之一是返回给定 asn、国家或 asn+国家对的每次加权平均值。它看起来像这样:
SELECT
t.time * 1000 as time,
@rtt := coalesce(m_sum.weighted_rtt, @rtt) as rtt,
floor(least(100, greatest(0,
100 * (coalesce(m_sum.weighted_rtt, @rtt) - t.rtt_bad) / (t.rtt_good - t.rtt_bad)
))) as rtt_quality,
@util := coalesce(m_sum.weighted_util, @util) as util,
floor(least(100, greatest(0,
100 * (coalesce(m_sum.weighted_util, @util) - t.util_bad) / (t.util_good - t.util_bad)
))) as util_quality
FROM
thresholds as t
LEFT JOIN
(
SELECT
m.time,
sum(m.rtt*m.reqs)/sum(m.reqs) AS weighted_rtt,
sum(m.util*m.reqs)/sum(m.reqs) AS weighted_util
FROM metrics AS m
WHERE m.asn = '7018' and m.cty = 'us'
GROUP BY m.time
) AS m_sum ON t.time = m_sum.time
ORDER BY t.time asc;
它返回如下内容:
+---------------+---------+-------------+----------+--------------+
| time | rtt | rtt_quality | util | util_quality |
+---------------+---------+-------------+----------+--------------+
| 1521234900000 | NULL | NULL | NULL | NULL |
| 1521235200000 | 45 | 80 | 3000 | 40 |
| 1521235500000 | 45 | 80 | 3000 | 40 |
| 1521235800000 | 65 | 70 | 2000 | 60 |
| 1521236100000 | 65 | 70 | 2000 | 60 |
| 1521236400000 | 65 | 70 | 2000 | 60 |
| 1521236700000 | 65 | 70 | 2000 | 60 |
| 1521237000000 | 120 | 50 | 4500 | 10 |
... ... ... ... ...
分解这个查询,我们:
- 仅过滤我们关心的行(在本例中基于
asn和cty) - 为每个
time聚合这些值 - 计算加权指标 - 将这些汇总结果与包含每个给定时间的“阈值”值的表格相结合(也许在凌晨 5 点,我们认为 100 毫秒的 RTT 非常差,但在下午 5 点,当每个人都在观看 Netflix 时,我们认为 100 毫秒非常好)
- 按
time排序 - 如果我们没有记录给定时间的指标(也许我们没有在该 5 分钟间隔内将流量传送到该
asn+cty对)然后使用前 5 分钟间隔的值(使用用户定义的变量) - 计算每个指标的“相对优度”值 (
*_quality)
变量
我的目标是尽快得到这个SELECT 查询。我可以改变:
- 我的
SELECT查询 - 表架构
- 表引擎(我可以访问 MyISAM、InnoDB 和 MariaDB 的列存储)
- 表索引
- 分区
我无法改变:
- 数据库服务器
- 数据库配置
以前的测试
我之前只使用大约 1.5 亿行(最终数据集的 1% - 包括 300 个不同的 time 值而不是完整的 25920)进行了一些测试,看起来 InnoDB 是最快的 - 比列存储高 3 -4x(InnoDB 大约 0.7 秒返回数据,Columnstore 大约需要 2.5 秒)。
我相信这是真的,因为我们所做的第一件事是在完成任何聚合或其他工作之前过滤掉这 1.5 亿行中的大部分。 InnoDB 支持索引,这让我可以快速找到我想要过滤的行并且只使用这些行——从不从磁盘读取其他数据。
不过,这里有一个问题:我现在有 50 亿行(大约是最终数据集的 40%),并且我进行了相同的性能比较。这一次,Columnstore 似乎比 InnoDB 快 2 倍! (InnoDB 30 秒 vs 60 秒)
至少,当我第一次运行特定 asn+country 的查询时,它更快。 InnoDB 似乎有中间缓存,因为然后我可以使用相同的 asn+country 运行其他查询,它们在 1 秒内完成,但即使在 Columnstore 中运行 exact same 查询也需要另外 30秒
问题
- 为什么 Columnstore 比 InnoDB 更快,即使 Columnstore 不支持索引(因此我们必须扫描整个表)?
- 我是否在表定义或查询中做错了可能会减慢速度的问题?感谢 InnoDB 的索引,我应该只需要从磁盘读取少数行。我不知道为什么这样做需要一整分钟
- 是否有人可以提供与前两个问题无关的其他性能提示?
在理想情况下,我希望这个查询在 10 秒内返回完整的 130 亿行数据集 - 尽管如果这不可能,那么在 60 秒内返回是可以接受的。
补充说明
我有能力计算预聚合值并将它们存储在单独的表中。我已经在很小的程度上做到了这一点。我有三个表:metrics_by_asn、metrics_by_cty 和 metrics_by_time。前两个存储指标的加权平均值,并且仅在 (asn, time) 或 (cty, time) 上键入。这有效地减少了这个查询:
SELECT
m.time,
sum(m.rtt*m.reqs)/sum(m.reqs) AS weighted_rtt,
sum(m.util*m.reqs)/sum(m.reqs) AS weighted_util
FROM metrics AS m
WHERE m.asn = '7018'
GROUP BY m.time
到这个:
SELECT
m.time,
weighted_rtt,
weighted_util
FROM metrics_by_asn AS m
WHERE m.asn = '7018'
第三张表metrics_by_time 返回汇总统计信息,如最大 RTT、平均 RTT、行数等。
我没有创建metrics_by_asn_and_cty 表有两个原因。首先,我没想到会看到令人难以置信的性能提升。平均而言,特定的 asn+cty 对仅从 1.3 个不同的来源提供服务。因此,大多数情况下,预先聚合它不会减少我们需要选择的行数。其次,我们已经达到了一些主要的磁盘使用限制。仅查看我们的指标表,我们就有 130 亿行乘以大约每行 35 个字节。这个数据库有 455 GB。添加预聚合表和其他表,我们在其中转储用于计算这些指标的原始数据,我们在磁盘上大约有 850 GB。我没有被告知我可以存储多少数据的硬性限制,但为了安全起见,我试图保持在 TB 以下。
【问题讨论】:
-
错误:
1521234900000不适合INT?? -
time总是正好在 5 分钟标记上吗? -
@RickJames
1521234900000是存储值乘以1000的结果(如 select 语句所示)。除以 1000 得到实际存储在 int 中的内容:1521234900 - 一个有效的 unix 时间戳。 -
@RickJames
time总是精确到 5 分钟。将此数据放入数据库的代码在保存之前四舍五入到最接近的 300 倍数 -
只要你在玩 1000 的因子,你还不如再扔 300。这会让你把列缩小到 3 个字节 --
MEDIUMINT UNSIGNED。