第四届全球数据库大赛赛道2:分布式NewSQL性能挑战--初赛方案分享


前言

一个偶然的机会,在阿里云天池大赛上看到“第四届全球数据库大赛赛道2:分布式NewSQL性能挑战”,抱着玩一玩的心态就报名了。
初赛目标50秒以内,由于之前并未接触过持久化内存,初赛主要是通过《持久内存快速编程手册》,边学习边实践,不断优化。不曾想初赛成绩居然10.45秒,位列第10名,超出预期。


一、初赛赛题

提供1台126G的高性能BPS机型,实现单机版的高效的KV存储引擎,支持write、read、init接口,如何充分利用BPS特性将成为获胜关键
对初赛题目进行分析,可以分为两部分来分别考虑:
1)50个线程,各100万条记录数据,如何持久化写入效率最高?
从所周知,写内存效率高,写硬盘效率很低。但是要实现数据持久化,又不能写内存。
如何充分利用BPS特性将成为获胜关键,这句话很重要,所以我们使用AEP内存来存储数据,比SSD硬盘效率高很多,同时又能实现持久化。
由于是50个线程同时写数据,分块存储可以很好的提高写入的效率。
2)如何建立索引与数据存放位置之间的映射关系
我们使用哈希表,读写的效率都很高。但是要考虑哈希冲突的处理。
解题关键:
1)正解性评测时如何保证程序意外退出时数据一致性问题
2)性能评测时如何提高读写的效率
3)另外还需要注意:字段name不是索引、SK是多值索引、初始化不计入性能测试时间。

二、解题思路

在这里插入图片描述

1)engine_write函数负责把User记录写入持久化内存,并分别建立PK、UK、SK三种索引与数据存储位置之间的映射关系,由于是50个线程同时写,所以要考虑多线程之间资源互斥。
写持久化内存时可以考虑分块存储,提高写内存效率。
2)engine_read函数负责根据三种索引与数据存储位置之间的映射关系,找到数据在持久化内存中位置,取出数据,由于初赛读写分离,读数据时不需要考虑多线程之间资源互斥。
3)engine_init函数负责初始化AEP内存、提前进行哈希数组内存分配,以及程序退出或者崩溃后恢复的处理:AEP内存中有数据时需要提前读取数据建立三种索引与数据存储位置之间的映射关系。
4)engine_deinit函数负责释放资源、解除AEP内存映射。

三、持久化模型

在这里插入图片描述

按照我们的设计,总数据量为320*50000000=15G。
为了避免写入时的资源冲突导致锁效率低下,我们把15G分成13块,按user.id%13取余决定写入哪一块。
每一块内部顺序写入,这样可以充分提高持久化内存的写性能。那么每一块内部写入的位置需要记录下来并加锁。
这时可以使用原子操作atomic来定义这个位置,彻底去掉持久化中的锁,实现持久化无锁。
为User增加两个字段:
int64_t complete; //complete表示是否写入持久化完成,这是为了解决崩溃后数据一致性问题
int64_t padding[5]; //padding用来补全320字节,使其满足64字节对齐,提高写效率

使用libpmem库来实现持久化,在/mnt/aep目录创建init.db和pmem.db两个文件。init.db文件大小为1个字节,表示是否第一次写。
为0表示第一次写,此时需要预写pmem.db全部数据为0(预写是为了提高后续写AEP的速度),同时更新init.db首字节为1。
为1表示pmem.db已经写过数据,此时engine_init函数从pmem.db文件中恢复数据,当complete字段为1时表示该条数据是写入成功的,按13块分别读取数据建立索引。
这就是对进程退出和异常崩溃后的恢复处理。

四、索引KV模型

在这里插入图片描述

索引KV模型使用链式哈希表(哈希数组+单向链表),记录索引与数据存放位置之间的映射关系。
哈希函数使用除留余数法 H(key)= key % p,我们定义p为50000000以内最大的质数49999991
每种索引分配一个p大小的数组,通过索引mod p来分配到不同的数组位置上。
user_id是字符串类型,它先用std::hash对前8个字节来做一次哈希,再使用哈希函数。
如果索引mod p后结果相同,则使用单向链表来解决冲突。
name不是索引,不需要建立KV,而sk是多值索引,所以它的冲突会比较严重。
typedef struct kv1
{
char key[128];
int64_t pos;
struct kv1* next;
} Kv1;

typedef struct kv2
{
	int64_t key;
	int64_t pos;
	struct kv2* next;
} Kv2;

pos记录该索引对应的数据存储在AEP中的绝对位置,计算如下:
blacknum = user.id % BLACKNUM
pos = blacknum * blacksize + posN * 320
建立索引时需要使用互斥锁,查询索引时不需要使用互斥锁

五、细节优化

1)由于User结构体长度为272字节,而libpmem是按照64字节对齐,所以改为按照64字节对齐。在User结构体中加入一个参数,补齐到320字节。测试后性能有提升。
2)分块存储时每块顺序存储位置使用原子操作atomic,实现持久化无锁。
3)由于engine_init不计入性能测试的时间,可以在此进行AEP内存预写(提高写速度)。
4)由于初赛总数据量只有15G,小于内存容量32G,初始化时把持久化中的数据拷贝一份到内存,然后读数据时候直接从内存取数据。
5)user_id做哈希时只用取前8个字节就能保证哈希的均匀分布。
6)CMAKE编译选项改为Release,由于初赛是读写分离,读数据时不用加锁。


总结

初赛主要考察持久化和哈希表,取得成绩的关键是细节处理,特别是AEP内存预写。

下面是我的微信公众号,欢迎大家关注
在这里插入图片描述

  • 30
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值