摘要:
综合分析之后,我们确定了仍使用MongoDB的最终的方案:“使用独立集合存储每个版本的人群包数据,集合名称中包含数据版本+无效的数据直接使用drop集合的方式进行删除”。
这种方案相较于之前的方案,在读取方面,每次只读取/索引一天的数据,数据量相对较少,效率会更高;
在写入数据方案,每次数据都是写入新集合,只需构建一个用于分页查询的sid索引,少构建了一个date索引,效率会更高;
在数据删除方面,无效的历史数据可以直接使用drop集合的方式进行删除,而不是用之前的delete/remove方式,效率简直有天壤之别,而且drop集合后磁盘空间也得到了释放。
正文:
在画像平台人群圈选模块中,人群包的计算和存储是在CilckHouse引擎中完成的。但是基于CilckHouse在并发查询、集群压力方面的考虑,在数据输出模块中,并不是直接从CilckHouse中查询获取数据,而是在人群包数据计算完成后,通过数据同步模块将数据从CilckHouse同步到MongoDB中。MongoDB作为数据输出模块的底层数据存储和查询引擎,需要满足高效的数据写入和读取性能。同时人群包并不必保存全部的历史数据,mongoDB中只需保留最近N个版本的人群包数据即可,所以为了删除无效数据,还需要有高效的数据删除性能。
自画像平台建设以来,人群包数量和数据量迅速增长,人群包数量从19年的XXX+到20年的XXX+再到21年现在的XXXX+。日均同步的数据量目前在XX亿左右(主要是用户的各类ID数据,不同人群包数据之前存在重复)。
在21年之前,我们主要是针对人群包的计算过程进行了若干优化,在数据同步CilckHouse2MongoDB这个过程中则并未进行过多的优化(也是一直满足业务性能要求),反而因为业务需求增加,在这个过程中还增加了一些数据处理的逻辑用于人群命中服务、人群包数据增量/减量变动通知等等。在今年XXX业务接入平台后,人群包数量迅速增长,该业务在平台上日常运行的人群包超过XXXX个,且业务方在拉取数据过程中对接口的性能要求较高。
人群包数量翻番,数据总量飞涨,业务方对接口的性能要求提高,平台之前架构已经不能满足目前的需求,促使我们对这些频繁暴露的性能瓶颈的进行探索和优化。
1、现象是某业务方设置了拉取数据接口超时2s,仍出现了大量的超时和报警。反映出目前在我们的使用中MongoDB读取性能较差;
2、现象是单版本的人群包数据完成CilckHouse2MongoDB的同步需要花费8个小时,耗时太久。反映出目前在我们的使用中MongoDB写入性能较差;
3、现象是MongDB服务器磁盘空间占用量大,一度超过85%。大量的历史无效数据没有得到及时的删除。而在代码实现中是有在凌晨进行历史数据删除的逻辑的(自动删除不了偶尔还得进行手动删除)。这反映出目前在我们的使用中MongoDB数据删除性能较差。
4、其他一些因为设置不合理导致的数据重复等问题;
在mongo读写性能问题出现前及解决的过程中,我们进行了一些探索,对之前的一些不合适的使用姿势进行了纠正修改。
修正1、在副本集Replica Set中才涉及到ReadPreference的设置,默认情况下,读写都是分发都Primary节点执行[【MongoDB】ReadPreference读偏好_lizhuquan0769_51CTO博客]。MongoDB有5种ReadPreference模式(Read Preference — MongoDB Manual)。我们修改了这个读取策略,从primary修改为secondary_preferrd,通过这种方式来让从节点处理读请求,让主节点处理写请求。预期通过这种读写分离的方式来提升读写性能。但是这种方式的效果也不算理想吧。在后期接口读取数据时还是经常出现超时。
修正2、在读取数据方面,因为数据量很大,所以接口是通过分页拉取数据的,每页的大小在100/500/1000不等。在使用过程中,有业务方反馈不同页之间拉取到的数据存在重复,因此我们也对这个读取的具体逻辑进行了复盘。发现代码中判断分页的逻辑是X >= $lt + limit page_size 的方式。而因为底层存储的原因,这种方式确实会导致数据重复的问题,我们对这个逻辑进行了修改,采用$lt <= X < $gt (加不加limit都行)的方式通过这种方式严格保证了每页数据的准确性。
纠正了这些使用合理性方面的问题后,当数据量增长到一定程度后,集群的读写性能依然出现了问题。
针对写入性能较差的问题,我们最先想到的是增加线程,加大写入并发的方式。同时因部分人群包数据是同步到S3中(这部分人群包数量较少,但单人群数量都比较大)供给业务方使用的,这就不涉及mongo。所以还将这一部分人群包的数据同步过程转移到一个单独的队列去运行。但是这种方式可以加快写入但是效果并不明显。在这个过程中倒是也发现主节点在挂掉重启后写入性能会有一些提升,所以在一段时间内,当报警比较频繁时偶尔采用了比较Low的办法,那就是依次重启集群的各个节点。一段时间内也没有更好的解决思路。
针对读取性能较差的问题,我们想到了索引优化的方式。在创建一个集合的时候,我们是创建了2个索引,一个是用于分页查询的sid索引,从1开始;另一个是用于数据的版本date索引。这两个索引是独立的。而在读取数据的时候一般是选取某个特定的数据版本,然后进行分页拉取。所以考虑到是否能将这两个索引做成一个/或是增一个复合索引[Compound Indexes — MongoDB Manual]。在mongo shell实践这个想法,通过explain分析命令的执行情况,发现并不是每次查询都能使用这个复合索引,这个和集合中的数据量也有关系。(当时也没有具体研究这个背后的具体逻辑,有点遗憾)。这也预示着我们的这个思路也行不通。
此时我们已经在思考mongoDB是否真的适合我们这个场景了,但是通过mongo服务器的监控发现出现问题的时候服务器的CPU/磁盘IO等负载并不高,而且mongo在业内应该也算是高性能的数据库了,也很有可能是我们的使用方式仍存在问题。
短时间没有头绪,但这段时间调研mongo也使我们无论是从业务逻辑还是对DB本身,都对它有了一定的了解。我们进行了以下两种方案的调研,
一种就是放弃使用mongo,转而使用像HBase等其他存储或者直接使用ClickHouse用于查询;这种方案比较大的问题就是改动比较大,涉及写入/读取/删除等多个逻辑,且短时间内也不能完全替换mongo。
另一种就是继续使用mongo,但是确定了一种全新的存储方案:集合名称增加时间版本,独立集合存储每个版本的数据。
这两种方案的实现细节对比和基本结论如下:
具体实现 | 优 | 劣 | ||
---|---|---|---|---|
仍使用Mongo | 表名上增加时间版本,每次写入数据前如果存在该表名,drop掉,然后重新建该表;读取数据的时候先确定版本,拼出表名,然后读取数据。 | 1、删除数据直接drop表,效率较高 2、每张表中不用存日期了,空间占用会减少。 3、拉取数据的API改动相对小。修改表名的同时需要修改查询mongo的条件 |
1、还是mongo,运维成本 2、同步至mongo要花几个小时。 |
|
索引优化,增加联合索引 | 1、之前的表应该只能手动增加索引了。2、写索引/存索引文件都存在成本 (意义不大,具体的执行计划跟数据量也有关系,难以确定是不是用了这个索引) |
|||
不使用mongo,如果是新起一个接口的话需要推动业务方更改接口,暂时也不能完全下掉mongo。 如果更改老接口的话,则需要做好回归测试,业务方无感知可以完全下掉mongo。 |
使用HBase | 通过设置过期时间删除无效数据; 可以放SSD;写入性能高;成熟的运维和技术支持 |
1、会不会存在读写热点问题? 2、查询的分页比较麻烦 |
|
使用CH | 不用推送了,人群包可用数据即可用; | 1、CH集群不可用,数据输出服务即不可用,风险较大; 2、其他的一些数据变动服务等需要重新设计 |
||
CH+HBase | CH集群不可用,查HBase,保证API可用 | 1、还是得推送到HBase,推送成本问题 2、人群包的表增加ID字段作为索引,改动成本较大 |
||
使用TiDB等其他存储 | 全新的存储,还不太熟悉; |
综合分析之后,我们确定了仍使用MongoDB的最终的方案:“使用独立集合存储每个版本的人群包数据,集合名称中包含数据版本+无效的数据直接使用drop集合的方式进行删除”。
这种方案相较于之前的方案,在读取方面,每次只读取/索引一天的数据,数据量相对较少,效率会更高;
在写入数据方案,每次数据都是写入新集合,只需构建一个用于分页查询的sid索引,少构建了一个date索引,效率会更高;
在数据删除方面,无效的历史数据可以直接使用drop集合的方式进行删除,而不是用之前的delete/remove方式,效率简直有天壤之别,而且drop集合后磁盘空间也得到了释放。
就这样,在一个月黑风高,几乎没有请求的夜晚,我们上线了这一优化方案,重跑了新的数据,并完成了验证。
当晚,原本需要8个小时才能同步完成的数据,在3个小时就完成了同步,写入性能明显提高,保守估计从60-70kops提高到了200kops且写入速度更加稳定。
接口性能也达到了毫秒级(我理解拉取数据的接口不比其他一些业务接口,耗时确实会高一些)。
满心欢喜,仿佛问题已经解决啦。也就没有着急删除之前的旧集合/旧数据,这也导致mongo中集合数量猛涨。
但是在第二天上午写入达到150K -200K 时直接mongo节点挂掉,DBA查看报错信息是Too many open files。最开始以为这个问题是并发太大导致的,于是将同步任务运行并发量减半又减半,紧急上线后勉强完成了当天的同步任务。
到了第三天,又出现了同样的问题,mongo节点连续挂掉,导致了mongo集群的短暂不可用。
搜索发现https://segmentfault.com/q/1010000019093663可能是这个问题,是因为集合的数量太多了,超出了限制[https://docs.mongodb.com/manual/reference/ulimit/]。
本着先解决问题的目的,决定先减少集合创建数量,删除无用集合。于是导出可以被删除的无效集合的名称,分批对这些数据进行了drop删除。在业务层面也删减了一些无用数据的同步,进一步减少了集合的创建数量。对于历史集合进行了更及时的drop。
这一通优化搞下来,截止到目前,mongo集群已经稳定3个多月的时间了,基本无报警。日常写入维持在120kops,接口读取性能维持在毫秒级别,服务器磁盘占用维持在30%左右。终于解决了业务问题,也对彻底消除了一直存在mongo报警问题。还是有一点成就感的。
当然,对于mongo这款数据库,还是有很多不太了解的地方,比如为啥集合数量要有限制呢?看其他一些资料还是推荐使用分区分片的存储方式,那数据delete/remove的低效问题如何解决呢?delete/remove数据为啥不能释放磁盘空间呢,底层是怎么设计的呢?这些问题还有待进一步的探索。
登陆/切换数据库/
集合数据查看find、count等/
集合数据删除delete、remove,删除集合等(效率比较)
集合索引的种类(单一索引、复合索引、TTL索引等等)
运行分析,用到的索引/扫描的数据量等等(explain)
正在运行的进程分析/kill等
导出全部的集合
mongo shell 与Linux shell 的交互使用等等
之前采用的是 X >= $lt + limit 的方式,发现不同页之间的数据存在重复
优化后采用 $lt <= X < $gt (加不加limit都行)的方式,保证了分页数据的准确。
还有一个读取数据时 主从节点选择的设置,
ReadPreference.secondaryPreferred()//在复制集中优先读secondary,如果secondary访问不了的时候就从master中读
ReadPreference.secondary();//只从secondary中读,如果secondary访问不了的时候就不能进行查询
delete 和 remove 可以通过设置条件删除集合中的某些数据,目前来看单集合删除上百万千万的数据,且同时删除多个(6个左右)集合,同时还有写入的情况下,删除的速度是相当慢的,人群包数据较多的时候根本就删不过来。
drop集合的方式还是很快的,没有发现啥性能问题。
此外,delete 和 remove数据后,磁盘空间是无法释放的,基本只能通过重启机器来释放。(另外有一种repair的方法可以在不重启机器的情况下释放磁盘空间,但是这过程耗时比较久,且这期间mongo不能对外提供服务,是个非常危险的命令,慎用)
在上下文中,相同的数据只获取一次。对于一些必要的数据,在上文中获取了,可以传递到下文,避免重复获取,减少接口耗时。