免费下载案例集|20+数字化领先企业人才培养实践经验 了解详情
写点什么

Cassandra 扩容超级慢?

  • 2019-11-25
  • 本文字数:4508 字

    阅读完需:约 15 分钟

Cassandra扩容超级慢?

缘起

TaurusDB 是华为云自主研发的最新一代云原生分布式数据库,完全兼容 MySQL8.0,采用计算与存储分离、日志即数据的架构设计,支持 1 写 15 读,性能达到原生 MySQL 的 7 倍。Taurus 构建在共享分布式存储上,存储空间最高达 128T,能跨 AZ 部署,具有可靠、高性能、易伸缩等特性。


华为云 TaurusDB 性能挑战赛是由华为云主办的数据库领域顶级赛事,大赛将综合科研教学成果及商业领域需求,探索数据库领域的技术问题的可行性,为需求方和开发者提供联接的桥梁;并联合合作伙伴,搭建一个技术交流、人才培养、机遇共创数据库开发者平台和生态。


听起来很激动人心吧,关键是奖金也很多, 同时还能刷脸! 最早是听一个同事说起的,恰好本人的工作也是“新一代数据库”开发,看着这么多人工智能的比赛不能参加,只能后悔自己选错了专业,但是机会来了,终于有自己擅长的领域了,心想不能错过这次机会,一边默默加了收藏。


比赛内容一句话概括,就是实现一个 kvstore,初赛是单机引擎,复赛要求存储计算分离,并且 kv 都是定长的,如果通用性做得好,可以直接用作块存储。

题目

初赛和复赛稍有区别,这里只说复赛的题目。存储引擎 K,V 都是定长的,key 8bytes, value 4K bytes。测试分为三轮:第一轮,16 个线程写,每个线程写 400 万次;第二轮,16 个线程读,每个线程按顺序读取 400 万次;第三轮,16 个线程读,每个线程逆序读取 400 万次。第三轮的逆序是局部随机,总体逆序,也即在 10M 的数据范围内随机读,然后以 10M 为单位逆序推进。成绩就是看总时间,时间越短越好。


前面已经说过,初赛要实现的是单机引擎,复赛要实现的是存储计算分离的引擎,最大的限制是计算节点不能持久化任何数据。

预选赛

不得不说,华为可真会玩,这次预选赛竟然还要做题,并且是必须要学习他们的资料才能通过的题目,题目包括数据库的基本常识,但是也有产品介绍,经过深夜两个小时的学习,我算是第一次知道了这么多种不同的产品究竟是干嘛的。第一次答题还错了一道,以防万一,又做了一遍,这次错了两道,算了不玩了,95 分也够了,睡觉去。但是心里总是不舒服,究竟是哪错了,又看了一遍,原来是幻读的理解这道题错了。

初赛

不出意外,预选赛顺利通过,初赛就开始写代码了,初赛只要写的差不多就可以晋级复赛。当然任何一个程序员都不会满足刚好够用的状态,因此正常能想到的优化都加上了:比如 Direct IO, 多文件做数据分区,引入写 buffer,比较频繁修改元数据,读取使用自己实现的 page cache,以免 4K 读不能打满磁盘带宽。

复赛

复赛要求存储计算分离, 我认真做了一些分析,同时也对他们的硬件做了测试。分析下来,这个比赛比拼的点和我平时在工作中要追求的还是很不一样的。平时无论做什么系统,都是在延迟差不多的情况下,把吞吐量做高,这次比赛最关键的一个点是:延迟第一,延迟是最重要的。特别是写的阶段,因为并发只有 16,想通过聚合换吞吐量都不好使了。


考虑持久化方法:


  • 4K 恰好能对齐 IO,所以 key 和 value 要分开存储, 想使用 rocksdb 之类的存储引擎即使不被禁止也是没有竞争力的。

  • value 非常随机,基本不用考虑压缩,就算有一点点好处,实现起来也太复杂了。


考虑如何优化 IO:


  • SSD 的 IO 吞吐量高于 4K * iops, 不管是读还是写,IO 聚合是必须的。

  • 单文件会遇到文件系统瓶颈,需要多文件, 也即要对数据做 partition。

  • 关于同步 IO 还是异步 IO 的选择, 因为延迟优先,所以应该选同步 IO。


考虑网络框架:


  • 首先,我直接放弃考虑任何已有的网络框架,因为这是 benchmark, 再小的开销也是开销,都会让程序变慢。

  • 其次,实际上任何 IO multiplexing 的框架都会造成额外的延迟,不仅复杂,也是得不偿失的。


综上,我决定就使用最简单的阻塞式 IO 来做 socket 通信。

总体设计

计算节点和存储节点分工, 关键就是索引维护在哪?经过思考, 决定索引维护在计算节点。索引的内容是:key -> <file_id, pos>,索引在计算节点的内存中维护为 hash 表。


因为 build 索引大概需要 400ms,写入再读取 index 比重新构建一遍 index 时间还长, 所以索引不需要持久化,这其实是一个反直觉的决定。计算节点在发送第一个读请求之前,会从存储节点把所有写入的 key 和 pos 发送过来, 然后由计算节点构造索引。


存储节点起 16 个线程,listen 在 16 个不同的端口。计算节点也会有 16 个线程,每个计算节点的线程只会连接一个存储节点的端口,从而和一个存储节点线程通信。


写入请求:


1 写入过程不同的线程完全独立,每个线程负责一部分数据。


2 请求发到存储节点后由接受请求的线程写入。


读取过程:


1 读取过程每个存储线程会读取任意一个线程写入的数据。


2 由计算节点指定要读取哪个分区的数据。


即写请求发到哪个存储线程,就由哪个存储线程写入,每个存储线程实际上就对应了一个数据分区。读请求发到一个存储线程,它可能要跨线程读取其它分区的数据。

存储文件

存储节点把数据分为 16 个 partition,每个 partition 由一个线程负责写入。每个 partition 共三个文件,之所以有三个文件是因为首先 key 和 value 要分开存储,这就要用两个文件。其次为了优化写,额外引入了文件做写入 buffer。


文件命名规则如下, 以第一个分区为例:


  • 00.k:保存写入的 key,fallocate 4M * 8B, mmap 到内存。

  • 00.v:保存写入的 value,fallocate 4M * 4K, DIO 读写。

  • 00.b:用作写入 buffer,fallocate 16K buffer + 4K 元数据,mmap 到内存。

写入原子性

先写 key 和 value,再更新 key count。key count 改成功,则写成功;否则写失败,下次重启进程当作这次写没发生过。换句话说,key count 改成功实际上表示这次写入 commit 成功。


key count 记录在 00.k 的第一个 8 字节, 如前所诉, 00.k 是 mmap 到内存的,所以更新 key count 是没有什么代价的。


value 先记录到写 buffer 里,然后批量刷到 00.v 文件。


key file 内容如下:


value file 内容如下:


buffer file 内容如下:


write buffer 也是 mmap 到内存的, 前 64K 记录数据。紧跟 64K 的 8 个字节记录 flushed pos。因为 mmap 必须以 page 为单位,所以实际内存占用 64K + 4K。它实际上是一个 mmap 持久化的 ring buffer。Ring Buffer 的元数据包括 filled pos 和 flushed pos。filled pos 由 key count 可以算出来, 所以不需要单独再记。

build index

build index 实际上是构造一个 key -> offset 的 hash 表。要在 400ms 内完成 build index,难点是如何并行:基本思路是把 key 做 partition,每个 partition 内独立构造 hash。这本质上是一个 MapReduce 的过程。


map 阶段:并行划分 range,16 个线程,每个线程负责处理一个 key 文件。


reduce 阶段:并行构造 hash,16 个线程,每个线程处理一个 range。


线程同步:第二阶段开始之前要等第一全部完成,也即两个 Stage 之间是个 Barrier,这个同步过程和 map reduce 的 shuffle 是类似的。

读取 cache 的实现

cache 是 value 的镜像,value 文件分成了 16 个,cache 也对应分成 16 组。因为顺序和热点访问模式对 cache 都很友好,cache 不需要特别大,每一组 cache 大小为 64M。


为了应对热点读,cache 的最小单元设为 16M,借用 CPU cache 的术语,我把它叫 cache line,这是一个很大的值。cache 的内存因为是定长的,所以通过 mmap 一次申请好,不需要动态分配。cache 的索引本质是一个 hash,但是不用解决冲突, 用 file offset 直接计算得到索引的下标。


对 16 组 cache 中的每一组来说,有 4 个 cache line, 每个 cache line 有三种状态,用一个 uint32_t 表示:


1 Invalid: UINT32_MAX


2 Locked: UINT32_MAX - 1


3 Valid: file_offset »12


cache line 的状态被叫做 indexStat,它被单独记在另外一个小数组里。


读取不用加锁,使用 double check 即可:


1 读取之前检查 IndexStat 有效,并且 offset 和我要读取的内容匹配。


2 拷贝 cache line 的内容到私有 buffer。


3 拷贝完 cache line 的内容后再检查 IndexStat 是否发生过变化, 如果 indexStat 没有变化过,则说明我们拷贝的 cache line 内容是对的。


为了让上述 double check 生效, 更新 cache 的线程需要在写入之前把 indexStat 改为 Locked,更新完成后再把 indexStat 改为有效值。

网络通信

关于网络框架还有一个问题,就是是否有必要用 UDP,犹豫之下,觉得 UDP 还是会更复杂,稳妥起见,选择了 TCP。后来得知在测试环境里 UDP 是不同的,也就释然了。


但是 TCP 看起来开箱即用,用起来却暗藏机关,至少要调整以下三个参数:


1 tcp_nodelay


2 tcp_quickack


3 send/recv buf


最后为了规避 TCP 的流控,我们要避免另外两个坑:


1 避免发送太快,超过交换机的队列长度,从而导致丢包,因为 TCP 的工作方式就是,只要你给它数据,它就会不停的加大发送窗口,直到发生丢包,然后触发限流。要避免这种情况,本质是要限制 TCP 的连接数, 因为只要连接数限制住了,发送窗口总长度也就限制住了。


2 避免 TCP“冷却”之后重新进入慢启动状态,如果有 root 权限,是可以通过 sysctl 关闭 slow start 的,但是我们没法控制系统参数,这就要求我们一个连接最好要不停的发包,不要停下来。


有人定义了 packet 格式,而我遵循一切从简的原则,没有实现通常 RPC 要有的功能:


1 没有定义 pcode,因为整个通信过程就只有读和写两种 request,每一个 socket 只用来发送一种类型的 request,如果要发送不同的 request,只需要切换 socket 即可。


2 没有实现序列化,因为 request 很简单,只需要把结构体直接发送到 socket 即可。


细节决定性能


细节也就是很多关于性能的小点:


1 首先关于文件 IO:同步 IO 比异步 IO 延迟要小; open 的时候加入 O_NOATIME 避免读取操作也修改元数据


2 其次关于内存分配:分配完内后提前触发 page fault 至少可以让性能更稳定,如果不能提高性能的话;分配内存可以尝试 hugepage, 失败之后再使用 4K page; mmap 的内存可以不用清零


3 最后关于 CPU, 绑核,通用可以让延迟更稳定, 理论上对性能是有提升的。


正确性测试


如前所属,我基本上是一切从简,但即使如此,提交了好多次,都通不过正确性测试。为了排查问题,专门写了一个随机的验证程序,确实发现了一个 readv 之后更新 iovec 的 bug,还有一处 cache 的 bug。 事后看来,在这个测试上花的时间非常值得,否则我可能到最后都没有成绩。


总结


这次比赛感觉有点像马拉松,听完大家的分享,感觉每个人的时间都比较有限,大家都是争分夺秒,每个人都有一些优化没来得及试。本人最大的一个遗憾是读预取没有实现,另外一个是写的过程中网络和 IO 并行化做的不好。


最后听了第一名的方案,深有体会,要想拿到最好的成绩,需要把写和读分开考虑,同时把网络传输和本地 IO 分开考虑。


单纯从工程上说,我的代码有一些特有的风格, 简单来说就是总爱重复造轮子,不愿意引入依赖:


1 没有用 pthread mutex/cond,全部用 atomic ops + futex


2 没有 spin,等待的地方都有睡眠和唤醒机制


另外,本人虽然工作中用 C++,但是我情愿用 plain old C,所以我很少用高级 C++特性。


如果未来还做这种系统实现的比赛,希望有可以利用 RDMA 的和 NVM 的比赛,毕竟,新硬件总是有更多的可能。当然比赛的设置要考虑引入更多自由度,这样会更加有趣。


本文转载自 HW 云数据库。


2019-11-25 08:00600

评论

发布
暂无评论
发现更多内容

质量基础设施一站式服务平台建设,检验检测系统平台开发搭建

电微13828808271

英特尔深耕元宇宙算力技术,剑指开“元”盛世

科技新消息

自身的强大,无惧任何威胁!

Tiger

28天写作

Linux一学就会之Linux计划任务与日志的管理

学神来啦

Linux centos 运维 SSH linux云计算

公安合成作战平台建设解决方案,情指勤一体化合成作战系统搭建

电微13828808271

哪些企业需要做等保测评?快速过等保测评用什么堡垒机好呢?

行云管家

网络安全 等保 等保测评 等保2.0

公安情报研判分析系统开发,情指勤一体化实战平台

电微13828808271

【量化】用数据验证巴菲特推荐的指数定投的收益真相

恒生LIGHT云社区

数据 金融科技 量化投资 量化

基于社交网络的客户智能 ( AI ) 推荐系统研究

索信达控股

推荐系统 推荐算法 个性化推荐

低代码如何让中小型企业蓬勃发展?

低代码小观

低代码 企业管理系统 低代码平台 中小企业

云原生监控高可用集群 Thanos 架构剖析 | 内容合集

耳东@Erdong

内容合集 签约计划第二季

14. 《重学JAVA》-- 内部类

杨鹏Geek

Java 25 周年 28天写作 12月日更

从0开始学VUE - 运行第一个VUE项目

恒生LIGHT云社区

JavaScript node.js Vue 前端

「Spark从精通到重新入门(二)」Spark中不可不知的动态资源分配

尔达Erda

大数据 spark 程序员 云原生 心得

TDengine典型用户案例|内容合集

TDengine

tdengine 时序数据库 技术专题合集

RocketMQ和Kafka的差异对比

编程江湖

大数据

利用Java反射处理private变量

FunTester

Java 工具 反射 Groovy FunTester

聚势聚能,共擎共飞 PKS安全先进计算2021生态大会将启

InfoQ 天津

DotNet项目利用拦截器记录访问日志

为自己带盐

日志 dotnet 28天写作 12月日更

开发者故事|朝九晚六大小周,我就是快乐的技术人

尔达Erda

程序员 云原生 经验分享 成长笔记 思考路径

【实战】网络安全学习:内网渗透实例

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 内网渗透

低代码助力直播带货走进制造业,冲击工厂传统供应链模式

优秀

低代码 直播带货

百分点大数据技术团队:Elasticsearch多数据中心大规模集群的实战经验

百分点科技技术团队

常见序列化算法学习笔记一

风翱

序列化 12月日更

企业数字化的4个痛点,低代码平台如何解决?

J2PaaS低代码平台

低代码 数字化 低代码平台

埃文科技荣获“郑州市企业技术中心”

郑州埃文科技

埃文科技 ip技术 企业技术中心

RESAR 性能工程:一个性能项目真正体现价值的方式

zuozewei

内容合集 签约计划第二季

西南林业大学:用宜搭打造智慧校园,节省百万费用

一只大光圈

钉钉 低代码 数字化 高校 钉钉宜搭

CSS之选择器(七):empty

Augus

CSS 12月日更

使用kubernetes,你真的降本了吗

Geek_cd6rkj

Docker Kubernetes 云原生 成本治理 提效降本

黑客进行网络欺骗攻击的手段有哪些?

喀拉峻

黑客 网络安全 安全

Cassandra扩容超级慢?_语言 & 开发_HW云数据库_InfoQ精选文章