掘金 后端 ( ) • 2024-04-25 14:19

背景

当前【业务检测】-【PFC/ECN状态】界面刷新较慢,默认9个交换机30分钟的查询范围,要耗时12秒多,15分钟也要3秒,严重影响用户体验。

通过代码分析,耗时主要是influxdb查询耗时。

问题分析

分析过程

这是从代码debug日志中看到的查询语句:

SELECT pfc_send_delta_3 FROM DcbPfcPauseStat WHERE time > 1712899812165000000 AND (device_name = 'Pod1-Leaf01-1' OR device_name = 'Pod1-Leaf01-2' OR device_name = 'Pod1-Leaf01-3' OR device_name = 'Pod1-Leaf01-4' OR device_name = 'Pod1-Leaf01-5' OR device_name = 'Pod1-Leaf01-6' OR device_name = 'Pod1-Leaf01-7' OR device_name = 'Pod1-Leaf01-8' OR device_name = 'Pod1-Leaf02-1') GROUP BY device_name, port_name LIMIT 1000;

先分析sql语句执行的几种情况:

简单查询:

SELECT pfc_send_delta_3 FROM DcbPfcPauseStat

增加group by,查询性能相差不大

在本地使用go程序执行一下sql(先忽略where语句):

SELECT pfc_send_delta_3 FROM DcbPfcPauseStat GROUP BY device_name, port_name

大约是11.8秒

从以上输出可以看到这20分钟320万数据有23392个series,每个seri有137个数据点,每一个数据点至少包含一个time时间戳(int64, 8字节)和pfc_send_delta_3 (float32 4字节),23392 * 137 * (4 + 8)byte = 38,456,448 byte = 38MB,用时11.8秒,这是我本地,如果走网络受带宽影响,会更慢。

增加limit,查询性能未见明显变化

增加时间戳(查询的数据量没有变少,因为这个时间戳是数据最早的时间)

进一步缩小时间范围,时间变快

增加1个device,作为过滤条件,耗时57毫秒

增加到9个device过滤,进行查询331毫秒

使用循环查询9个device,查询时间20分钟跟一次性查询基本一致,为318毫秒

为排除设备数量太少的原因,增加到122个设备直接查,3211毫秒

循环查,3546毫秒:

在本地运行程序后,发请求查询平湖上近15分钟的:

看后台日志打印,跟上面同样的设备,同样的查询语句,结果为:

本地查去掉group by,去掉limit

带group by, 去掉limit

比去掉group by要快,应该是influxdb针对时序数据做了特殊处理。

分析结论:

有无过滤条件:通过以上代码多次测试,普通查询性能最差,增加where过滤条件,包括time和device_name后查询速率可以提升。

limit: influxdb的limit是针对每一个series的限制,而不是跟传统的数据库一样对整个结果集的限制,而我们已经加了time来限制,要查询的每个series的条目是固定的,所以之前加不加limit也不会有影响,之前limit 1000就没起作用,

有无Group by: 去掉limit之后,有group by的查询更快,有group by再测试的查询要慢一些。

批量查VS循环查:当device数量较多时,循环查询较一次性查询稍差(循环查仅为满足ld的无理要求。)。所以,一般应该避开对数据库进行循环查询,避免造成查询无端放大,因为频繁的数据库查询一是会增加系统相应时间,二是会降低吞吐量。

数据量: ,同样查询15分钟122个设备,在平湖上能到30秒,我在本地测试即使在一个表中查,最多也是3秒多。

综合分析,因为influxdb自己对tag和time已经做了索引处理,在当前v1.8版本针对查询语句基本上没有可优化的空间,v2.0以上使用flux脚本优化了查询,支持索引下推等功能,可能在查询效率上有一定优化,暂未测试v2版本。

解决方法

在sql语句没有优化空间的情况下,可以考虑工程角度进行优化。工程角度我测试了以下几种方案:

数据准备

下面是平湖上的数据保存策略,按照1个交换机10个端口每10秒产生一个数据点来计算,如果有300台交换机一天的数据量估算为3001066024=25,920,000,数据量很大。

SHOW RETENTION POLICIES ON venus_master; name            duration shardGroupDuration replicaN default
rp_venus_master 48h0m0s  24h0m0s            1        true

为了避免对生产环境造成影响,我自己导出了20分钟大约320万的数据在本地复现问题。

一 重采样方案

最开始想的是除了优化查询语句、表结构外,还可以优化influxdb的配置,但是这些操作需要重启服务,我没有在现网尝试。

在查询官网文档时发现可以采用数据聚合的方式来降低采样率,因为现在慢主要就是因为数据量太大,根据汝宁所述,10秒一条数据,其实没有必要,所以可以通过聚合数据的方式来重采样,写入一个新的measurement中,来大大降低数据密度。

数据保留策略

重采样的时序数据一般只做临时展示用,可以不用一直保留,所以先对db设置一个保留策略,为跟平湖一致,我这里也是设置了一天。

CREATE RETENTION POLICY "1_day" ON venus_master DURATION 1d REPLICATION 1

全量重采样

现在针对320万的数据进行一次全量重采样

SELECT MEAN(pfc_send_delta_3)
INTO "1_day"."DcbPfcPauseStat_downsample"
FROM "DcbPfcPauseStat"
WHERE time > now() - 1h
GROUP BY time(1m),device_name,port_name

意思就是对DcbPfcPauseStat中的pfc_send_delta_3数据进行平均计算,按照每分钟一个数据点的频率和范围,写入到保留策略"1_day"的"DcbPfcPauseStat_downsample"中,数据保留一天。

现在再来运行一个查询语句,看看重采样后的查询效果:

SELECT  *  FROM "1_day"."DcbPfcPauseStat_downsample" group  by device_name, port_name

时间从11839毫秒降到了1520毫秒,快了整整10倍!

此时再看需要传输的数据大小为:23392 * 10 * 12 = 2,807,040byte = 2.8MB,比之前的38M小了13.5倍。

这还是全查询的情况,如果只匹配9台设备的话:

SELECT * FROM "1_day"."DcbPfcPauseStat_downsample" WHERE (device_name = 'Pod1-Leaf01-1' OR device_name = 'Pod1-Leaf01-2' OR device_name = 'Pod1-Leaf01-3' OR device_name = 'Pod1-Leaf01-4' OR device_name = 'Pod1-Leaf01-5' OR device_name = 'Pod1-Leaf01-6' OR device_name = 'Pod1-Leaf01-7' OR device_name = 'Pod1-Leaf01-8' OR device_name = 'Pod1-Leaf02-1') GROUP BY device_name, port_name;

只需要55毫秒。因为设备数从329掉到了9,正交数series大大降低,整体数据量就小了。

对比下重采样前后的三者的查询计划:

增量定时重采样

刚才手动执行了一次全量重采样,influxdb还提供了连续查询(CONTINUOUS QUERIES)的功能,其实就是influxdb自带的定时任务,可以设定采样周期和采样时间范围。

针对venus_master的DcbPfcPauseStat表创建如下CQ:

CREATE CONTINUOUS QUERY "DcbPfcPauseStat_downsample_1d" ON "venus_master"
BEGIN 
    SELECT MEAN(pfc_send_delta_3) 
    INTO "1_day"."DcbPfcPauseStat_downsample" 
    FROM "DcbPfcPauseStat"
    GROUP BY time(1m),device_name,port_name
END

Todo

1.针对业务,比如除了上面的PFC还有ECN、DCQCN等,都拆分出多个downsample的measurement,降低数据密度;

2.对每个重采样的measurement设置合理的保留策略,降低数据存储压力;

3.现执行全量重采样,再执行连续查询保证增量重采样;

4.现有代码中的查询语句,指向重采样后的measurement。

参考资料

https://influxdb-v1-docs-cn.cnosdb.com/influxdb/v1.8/query_language/continuous_queries/

https://www.cnblogs.com/vinsent/p/15814220.html

https://blog.csdn.net/qq_44766883/article/details/131586277

https://www.cnblogs.com/quchunhui/p/13402808.html

二 缓存方案

缓存一般是针对热点数据,这种时序数据其实不太适合这种场景。但是在有限条件下,既要保留数据精度,又想减少查询时间。其实可以尝试。

缓存设计

因为界面上最多只展示1小时的数据,所以可以缓存也是保留1小时的内容。

我在本地wsl虚拟机中针对DcbPfcPauseStat所有device所有port的pfc_send_delta_3做了1小时的redis缓存。

key就是device_name,value使用redis的zset类型:

127.0.0.1:6379> ZRANGE Pod1-Leaf01-5 0 -1
3593) "{"port_name":"FHGigabitEthernet 0/6:1","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
3594) "{"port_name":"FHGigabitEthernet 0/6:2","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
3595) "{"port_name":"FHGigabitEthernet 0/7:1","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
3596) "{"port_name":"FHGigabitEthernet 0/7:2","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
3597) "{"port_name":"FHGigabitEthernet 0/8:1","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
3598) "{"port_name":"FHGigabitEthernet 0/8:2","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
3599) "{"port_name":"FHGigabitEthernet 0/9:1","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
3600) "{"port_name":"FHGigabitEthernet 0/9:2","timestamp":"2024-04-12T10:03:00.439Z","pfc_send_delta_3":0}"
127.0.0.1:6379> keys *
320) "Core01-10"
321) "Pod1-Leaf04-2"
322) "Spine03-2-Pod1"
323) "Core01-14"
324) "Spine01-4-Pod2"
325) "Spine01-5-Pod1"
326) "Pod1-Leaf04-4"
327) "Pod1-Leaf08-7"
328) "Pod1-Leaf08-2"
329) "Spine02-8-Pod2"

一共有329个设备,329个key。

每次写缓存时,会判断长度是否超过1小时的数据量(1小时的数据量按照单台交换机80个端口10秒一个数据点来估算,是28800个数据点),若是,则删除最旧的数据。

资源占用

我在本地测试用的20分钟的数据(start: 2024-04-12T09:40:16.736Z, end: 2024-04-12T10:02:56.736Z),缓存占用200M,所以1小时占用大约600M。

127.0.0.1:6379> INFO MEMORY
# Memory
used_memory:227818392
used_memory_human:217.26M
used_memory_rss:250036224
used_memory_rss_human:238.45M
used_memory_peak:428207032
used_memory_peak_human:408.37M

测试结果

实际测试,从redis查询和从influxdb中查询耗时对比,提升了7倍。

Todo

1.针对业务,比如除了上面的PFC还有ECN、DCQCN等,在写influxdb后都写入缓存;

2.现有代码中的查询语句,指向缓存查询。

三 分库分表

当前所有的device数据都在一张表里面数据量且保持了一天的数据,量非常大,会有性能问题。

考虑进行分片,思路就是几个device存一张表,写库的时候,对device_name做个hash,再对分表数取个模;读库的时候,先根据用户输入查出目标表,然后并发查这几个目标表,结果进行汇总。

分表设计

具体几个device存一张表,需要进行测试。

有一种极端情况,前端传过来的多个device_name正好都在一个分片内,那么查询效率会变慢,但比查一个大表快。

我这里进行了10个分表,每个分表的表结构和保留策略要跟原表一样。

> show measurements;
name: measurements
name
----
DcbPfcPauseStat
dcb_pfc_pause_stat_sharding_0
dcb_pfc_pause_stat_sharding_1
dcb_pfc_pause_stat_sharding_2
dcb_pfc_pause_stat_sharding_3
dcb_pfc_pause_stat_sharding_4
dcb_pfc_pause_stat_sharding_5
dcb_pfc_pause_stat_sharding_6
dcb_pfc_pause_stat_sharding_7
dcb_pfc_pause_stat_sharding_8
dcb_pfc_pause_stat_sharding_9

测试结果

本地进行10个分片后,针对122个设备的查询结果:

比之前直接查的3211毫秒,快了近5倍。

Todo

1.针对业务,测试如何分片收益最佳

2.写库逻辑修改,对device_name做hash,再对分表数取个模,写入对应的分库

3.读库逻辑修改,先根据用户输入查出目标表,然后并发查这几个目标表,结果进行汇总后统一返回。