etcd 在超大规模数据场景下的性能优化

简介: etcd 是一个开源的分布式的 kv 存储系统, 最近刚被 CNCF 列为沙箱孵化项目。etcd 的应用场景很广,很多地方都用到了它,例如 Kubernetes 就用它作为集群内部存储元信息的账本。本篇文章首先介绍我们优化的背景,为什么我们要进行优化, 之后介绍 etcd 内部存储系统的工作方式,之后介绍本次具体的实现方式及最后的优化效果。


1概述


etcd  是一个开源的分布式的 kv 存储系统, 最近刚被 CNCF 列为沙箱孵化项目。etcd 的应用场景很广,很多地方都用到了它,例如  Kubernetes 就用它作为集群内部存储元信息的账本。本篇文章首先介绍我们优化的背景,为什么我们要进行优化, 之后介绍 etcd  内部存储系统的工作方式,之后介绍本次具体的实现方式及最后的优化效果。



2优化背景


由于阿里巴巴内部集群规模大,所以对  etcd 的数据存储容量有特殊需求,之前的 etcd 支持的存储大小无法满足要求, 因此我们开发了基于 etcd proxy  的解决方案,将数据转储到了 tair 中(可类比 redis))。这种方案虽然解决了数据存储容量的问题,但是弊端也是比较明显的,由于 proxy  需要将数据进行搬移,因此操作的延时比原生存储大了很多。除此之外,由于多了 tair  这个组件,运维和管理成本较高。因此我们就想到底是什么原因限制了 etcd 的存储容量,我们是否可以通过技术手段优化解决呢?


提出了如上问题后我们首先进行了压力测试不停地像  etcd 中注入数据,当 etcd 存储数据量超过 40GB 后,经过一次 compact(compact 是 etcd  将不需要的历史版本数据删除的操作)后发现 put 操作的延时激增,很多操作还出现了超时。监控发现 boltdb 内部 spill  操作(具体定义见下文)耗时显著增加(从一般的 1ms 左右激增到了 8s)。之后经过反复多次压测都是如此,每次发生 compact  后,就像世界发生了停止,所有 etcd 读写操作延时比正常值高了几百倍,根本无法使用。


image.gif

3etcd 内部存储工作原理

etcd 存储层可以看成由两部分组成,一层在内存中的基于 btree 的索引层,一层基于 boltdb 的磁盘存储层。这里我们重点介绍底层 boltdb 层,因为和本次优化相关,其他可参考上文。


etcd 中使用 boltdb 作为最底层持久化 kv 数据库,boltdb 的介绍如下:


Bolt was originally a port of LMDB so it is architecturally similar.
Both use a B+tree, have ACID semantics with fully serializable transactions, and support lock-free MVCC using a single writer and multiple readers.
Bolt is a relatively small code base (<3KLOC) for an embedded, serializable, transactional key/value database so it can be a good starting point for people interested in how databases work。


如上介绍,它短小精悍,可以内嵌到其他软件内部,作为数据库使用,例如 etcd 就内嵌了 boltdb 作为内部存储 k/v 数据的引擎。

boltdb  的内部使用 B+ tree 作为存储数据的数据结构,叶子节点存放具体的真实存储键值。它将所有数据存放在单个文件中,使用 mmap  将其映射到内存,进行读取,对数据的修改利用 write 写入文件。数据存放的基本单位是一个 page, 大小默认为 4K.  当发生数据删除时,boltdb 不直接将删掉的磁盘空间还给系统,而是内部将他先暂时保存,构成一个已经释放的 page  池,供后续使用,这个所谓的池在 boltdb 内叫 freelist。例子如下:

image.gifimage.png

红色的 page 43, 45, 46, 50 页面正在被使用,而 page 42, 44, 47, 48, 49, 51 是空闲的,可供后续使用。


如下 etcd 监控图当 etcd 数据量在 50GB 左右时,spill 操作延时激增到了 8s。

image.png

image.gif

image.gif

4问题分析


由于发生了用户数据的写入,  因此内部 B+ tree 结构会频繁发生调整(如再平衡,分裂合并树的节点)。spill 操作是 boltdb 内部将用户写入数据   commit 到磁盘的关键一步, 它发生在树结构调整后。它释放不用的 page 到 freelist, 从 freelist 索取空闲 page  存储数据。


通过对 spill 操作进行更深入细致的调查,我们发现了性能瓶颈所在, spill 操作中如下代码耗时最多:


1// arrayAllocate returns the starting page id of a contiguous list of pages of a given size.
 2// If a contiguous block cannot be found then 0 is returned.
 3func (f *freelist) arrayAllocate(txid txid, n int) pgid {
 4         ...
 5    var initial, previd pgid
 6    for i, id := range f.ids {
 7        if id <= 1 {
 8            panic(fmt.Sprintf("invalid page allocation: %d", id))
 9        }
10
11        // Reset initial page if this is not contiguous.
12        if previd == 0 || id-previd != 1 {
13            initial = id
14        }
15
16        // If we found a contiguous block then remove it and return it.
17        if (id-initial)+1 == pgid(n) {
18            if (i + 1) == n {
19                f.ids = f.ids[i+1:]
20            } else {
21                copy(f.ids[i-n+1:], f.ids[i+1:]) # 复制
22                f.ids = f.ids[:len(f.ids)-n]
23            }
24
25            ...
26            return initial
27        }
28
29        previd = id
30    }
31    return 0
32}

之前  etcd 内部内部工作原理讲到 boltdb 将之前释放空闲的页面存储为 freelist 供之后使用,如上代码就是 freelist 内部  page 再分配的函数,他尝试分配连续的  n个  page页面供使用,返回起始页 page id。 代码中 f.ids 是一个数组,他记录了内部空闲的 page 的 id。例如之前上图页面里 f.ids=[42,44,47,48,49,51]


当请求  n 个连续页面时,这种方法通过线性扫描的方式进行查找。当遇到内部存在大量碎片时,例如 freelist  内部存在的页面大多是小的页面,比如大小为 1 或者 2,但是当需要一个 size 为 4  的页面时候,这个算法会花很长时间去查找,另外查找后还需调用 copy 移动数组的元素,当数组元素很多,即内部存储了大量数据时,这个操作是非常慢的。


image.gif

5优化方案


由上面的分析, 我们知道线性扫描查找空页面的方法确实比较 naive, 在大数据量场景下很慢。前 yahoo 的 chief scientist Udi Manber 曾说过在 yahoo 内最重要的三大算法是 hashing, hashing and hashing!(From algorithm design manual)


因此我们的优化方案中将相同大小的连续页面用 set 组织起来,然后在用 hash 算法做不同页面大小的映射。如下面新版 freelist 结构体中的 freemaps 数据结构。


1type freelist struct {
2  ...
3    freemaps       map[uint64]pidSet           // key is the size of continuous pages(span), value is a set which contains the starting pgids of same size
4    forwardMap     map[pgid]uint64             // key is start pgid, value is its span size
5    backwardMap    map[pgid]uint64             // key is end pgid, value is its span size
6    ...
7}

image.gifimage.png


除此之外,当页面被释放,我们需要尽可能的去合并成一个大的连续页面,之前的算法这里也比较简单,是个是耗时的操作 O(nlgn).我们通过 hash 算法,新增了另外两个数据结构 forwardMap  backwardMap, 他们的具体含义如下面注释所说。


当一个页面被释放时,他通过查询 backwardMap 尝试与前面的页面合并,通过查询 forwardMap 尝试与后面的页面合并。具体算法见下面mergeWithExistingSpan 函数。


1// mergeWithExistingSpan merges pid to the existing free spans, try to merge it backward and forward
 2func (f *freelist) mergeWithExistingSpan(pid pgid) {
 3    prev := pid - 1
 4    next := pid + 1
 5
 6    preSize, mergeWithPrev := f.backwardMap[prev]
 7    nextSize, mergeWithNext := f.forwardMap[next]
 8    newStart := pid
 9    newSize := uint64(1)
10
11    if mergeWithPrev {
12        //merge with previous span
13        start := prev + 1 - pgid(preSize)
14        f.delSpan(start, preSize)
15
16        newStart -= pgid(preSize)
17        newSize += preSize
18    }
19
20    if mergeWithNext {
21        // merge with next span
22        f.delSpan(next, nextSize)
23        newSize += nextSize
24    }
25
26    f.addSpan(newStart, newSize)
27}


新的算法借鉴了内存管理中的 segregated freelist 的算法,它也使用在 tcmalloc 中。它将 page 分配时间复杂度由 O(n) 降为 O(1), 释放从 O(nlgn) 降为 O(1),优化效果非常明显。


image.gif

6实际优化效果


以下测试为了排除网络等其他原因,就测试一台  etcd 节点集群,唯一的不同就是新旧算法不同, 还对老的 tair 作为后端存储的方案进行了对比测试. 模拟测试为接近真实场景,模拟 100  个客户端同时向 etcd put 1 百万的 kv 对,kv 内容随机,控制最高 5000qps,总计大约 20~30GB  数据。测试工具是基于官方代码的 benchmark 工具,各种情况下客户端延时如下:

旧的算法时间


有一些超时没有完成测试。


image.gifimage.png


新的 segregated hashmap


image.gifimage.png


etcd over tail 时间

image.png

image.gif


在数据量更大的场景下,并发度更高的情况下新算法提升倍数会更多。


image.gif

7总结


这次优化将   boltdb中 freelist 分配的内部算法由 O(n) 降为 O(1), 释放部分从 O(nlgn) 降为 O(1),  解决了在超大数据规模下 etcd 内部存储的性能问题,使 etcd 存储 100GB 数据时的读写操作也像存储 2GB  一样流畅。并且这次的新算法完全向后兼容,无需做数据迁移或是数据格式变化即可使用新技术带来的福利!

目前该优化经过 2 个多月的反复测试, 上线使用效果稳定,并且已经贡献到了开源社区(https://github.com/etcd-io/bbolt/pull/141),在新版本的 boltdb 和 etcd 中,供更多人使用。




相关文章
|
1月前
|
存储 缓存 NoSQL
Redis高效缓存:加速应用性能的利器
Redis高效缓存:加速应用性能的利器
|
存储 Kubernetes 监控
云原生必备知识: etcd性能
决定etcd性能的关键因素,包括:  延迟( agency):延迟是完成操作的时间。  吞吐量 (throughput):吞吐量是在某个时间期间之内完成操作的总数量。当etcd接收并发客户端请求时,通常平均延迟随着总体吞吐量增加而增加。
1431 0
云原生必备知识: etcd性能
|
2天前
|
存储 运维 Kubernetes
Kubernetes 集群的持续性能优化实践
【4月更文挑战第22天】在动态且复杂的微服务架构中,确保 Kubernetes 集群的高性能运行是至关重要的。本文将深入探讨针对 Kubernetes 集群性能优化的策略与实践,从节点资源配置、网络优化到应用部署模式等多个维度展开,旨在为运维工程师提供一套系统的性能调优方法论。通过实际案例分析与经验总结,读者可以掌握持续优化 Kubernetes 集群性能的有效手段,以适应不断变化的业务需求和技术挑战。
14 4
|
14天前
|
机器学习/深度学习 运维 Kubernetes
Kubernetes 集群的持续性能优化策略
【4月更文挑战第10天】 在容器编排领域,Kubernetes 因其强大的功能和灵活性而广受欢迎。然而,随着集群规模的扩大和应用复杂度的提升,性能优化成为了维护高效运行环境的关键挑战。本文将深入探讨针对 Kubernetes 集群的持续性能优化策略,涵盖监控、资源管理、网络优化及自动化工具的应用,旨在为运维工程师提供一套实用的调优框架,以实现更高效的服务响应和资源利用率。
|
3月前
|
SQL 架构师 大数据
提升企业级数据处理效率!3.0 系列版本的四个集群优化点详解
为了帮助企业更好地进行大数据处理,我们在此前 TDengine 3.x 系列版本中进行了几项与集群相关的优化和新功能开发,本文将对这几项重要优化进行详细阐述。
45 0
|
9月前
|
存储 Kubernetes 固态存储
etcd 实现与选型分析(二)
etcd 实现与选型分析(二)
130 0
|
9月前
|
存储 Kubernetes 算法
etcd 实现与选型分析(一)
etcd 实现与选型分析
66 0
|
10月前
|
存储 运维 Kubernetes
Kubernetes 本地持久化存储方案 OpenEBS LocalPV 落地实践下——原理篇
Kubernetes 本地持久化存储方案 OpenEBS LocalPV 落地实践下——原理篇
428 0
|
10月前
|
存储 Kubernetes Cloud Native
Kubernetes 本地持久化存储方案 OpenEBS LocalPV 落地实践上——使用篇
Kubernetes 本地持久化存储方案 OpenEBS LocalPV 落地实践上——使用篇
422 0
|
存储 缓存 运维
SOFARegistry | 大规模集群优化实践
SOFARegistry 在蚂蚁内部迭代升级过程中,每年大促都会引来一些新的挑战,通过不断的优化这些在大规模集群遇到的性能瓶颈,我们总结出一些优化方案,来解决大规模集群遇到的性能问题。 通过阅读这篇文章,读者可以学习到一些 Java 和 Go 语言系统的优化技巧,在系统遇到瓶颈的时候,能够知道有哪些优化手段针对性的进行优化。
SOFARegistry | 大规模集群优化实践

热门文章

最新文章