优化无极限:盘古Master优化实践

简介: 当集群规模快速扩张到5000节点,规模引发的相关问题纷至沓来。首当其冲的就是盘古Master IOPS的问题,对其性能的优化势在必行。对于盘古这样复杂的大型系统,需在不同的阶段解决不同的性能瓶颈。本文将分别从工具篇、架构篇、细节篇等多个角度,阐述性能优化过程中的实践,以期对后续的工作有所指导。

优化无极限:盘古Master优化实践

盘古是一个分布式文件系统,在整个阿里巴巴云计算平台----“飞天”中,它是最早被开发出的服务,因此用中国古代神话中开天辟地的盘古为其命名,希冀能创建出一个全新的“云世界”。在“飞天”平台中,它是负责数据存储的基石性系统,其上承载了一系列的云服务(如图1所示)。盘古的设计目标是将大量通用机器的存储资源聚合在一起,为用户提供大规模、高可用、高吞吐量和良好扩展性的存储服务。盘古的上层服务中,既有要求高吞吐量,期待I / O能力随集群规模线性增长的“开放存储”;又有要求低时延的“弹性计算”,而作为底层平台核心模块的盘古必须二者兼顾,同时具备高吞吐量和低时延。

f7d7947ed051d4c0e6b0ddb6e943f1bc8cf287b9

在内部架构上盘古采用Master / ChunkSer ver结构(如图2所示),Master管理元数据,多Master之间采用Primar y - Secondaries模式,基于PAXOS协议来保障服务高可用;ChunkServer负责实际数据读写,通过冗余副本提供数据安全;Client对外提供类POSIX的专有API,系统地提供丰富的文件形式,满足离线场景对高吞吐量的要求,在线场景下对低延迟的要求,以及虚拟机等特殊场景下随机访问的要求。

e79f1f45572391fcef9c0c9de895d69948b861b7

自5K项目以来,集群规模快速扩张到5000个节点,规模引发的相关问题纷至沓来。首当其冲的就是盘古Master IOPS问题,因为更大的集群意味着更多文件和更多访问,显然上层应用对存储亿级文件和十亿级文件集群的IOPS是有显著区别的。同时更大规模的集群让快速发展的上层应用看到了更多可能性,导致更多业务上云,存储更多数据,也间接导致了对IOPS的更高需求。此前盘古Master较低的IOPS已经限制了上层业务的快速扩张,业务高峰期时有告警,亟待提升。另外一个规模相关问题就是盘古Master冷启动速度,更多的文件和Chunk数导致更长的冷启动时间,影响集群可用性。

要解决上述问题,性能优化势在必行。但对于盘古这样复杂的大型系统,不可能毕其功于一役,需要在不同的阶段解决不同的性能瓶颈。优化通常会伴随整个生命周期,因此优化工作本身也需要进行经验、工具等方面的积累,为后续持续优化提供方便。因此在这次规模问题优化中,我们积极建设了自己的锁Profile工具,并依此解决了多个锁导致的性能问题;在解决主要的锁瓶颈后,我们进行了架构上的优化,包括Pipeline优化和Group Commit优化,取得了良好效果;最后我们通过对细节的不断深入,反复尝试,较好地解决了由规模导致冷启动时间过长的问题。

工具篇

在盘古这样复杂的大型系统上,锁是优化过程经常遇到的问题。优化的首要任务是找出具体瓶颈,切忌猜测和盲目修改代码。先用压测工具对盘古Master加压,通过Top、Vmstat等系统工具发现CPU负载不到物理核的一半,Context Switch高达几十万,初步怀疑是锁竞争导致。同事也觉得某些路径下的锁还有优化空间,但到底是哪些锁竞争厉害?哪些锁持有的时间长?哪些锁等待时间长?具体又是哪些操作需要这些锁?由于缺少切实可靠的数据支撑,不能盲目下手,而坚持数据说话是一个工程师应有的品质。

盘古Master提供了众多的读写接口,内部的不同模块使用了大量的锁,我们需要准确得知是哪类操作导致哪个锁竞争严重,此前常用的一些Profile工具难以满足需求,因此我们有必要打造自己的“手术刀”----锁分析工具,方便后续工作。

首先为了区分不同的锁,我们需要对代码中所有的锁进行统一命名,并将命名记录到锁实现内,同时为了区分不同的操作,还需要对所有的操作进行一个唯一的类型编号。在某个Worker线程从RPC中读取到具体请求时,将类型编号写入到该Worker线程私有数据中,随后在该RPC请求的处理过程中,不同锁的拿锁操作,都归类于该操作类型。在每个锁内部,维护一个Vector < LockPerfRecord > 数据,LockPerfRecord记录锁的Profile信息,具体定义如下:

d0bc8a423308f53b0ef8e20ee58ffbba0819b7d7 

每个操作对应Vector中的一个元素,线程私有数据中记录的操作编号即Vector下标。整体来看形成了表1中的稀疏二维数组,空记录表示该操作未使用对应锁。

1831187399cab39185c836c45239737d08ba24aa 

具体实现上,Acquire Counts采用原子变量,时间测量上采用Rdtsc。谈到Rdtsc有不少人色变,认为有CPU变频、多CPU之间不一致等问题,但在长时间粒度(分钟级)和大量调用(亿次)的压测背景下,这些可能存在的影响不会干扰最终结果,多次实验结果也证实了这一点。其他实现细节,如定时定量(每n分钟或者每发起x次操作)发起不影响主流程的Perf Dump就不再赘述。整个代码在百行左右,对平台的侵入也很小,仅需要为每个锁添加一个初始化命名,在RPC处理函数的入口设定操作编号即可。

利器在手,按图索骥,对存在性能问题的锁进行各种优化。例如使用多个锁来替换原来全局唯一锁,减少冲突概率;减少加锁范围;使用无锁的数据结构,或者使用更轻量级的锁来优化。整个优化过程极富趣味性,经常是解决一个锁瓶颈后,发现瓶颈又转移到另外一个锁上,在工具的Profile结果中有非常直观的展示。先后优化了Client Session、Placement等模块中锁的使用,取得了显著的效果,CPU基本可以跑满,Context Switch大幅度下降,整个过程酣战淋漓。

架构篇

在锁竞争问题解决到一定程度后,继续进行锁的优化就很难在IOPS上取得较大收益。此时结合业务逻辑,我们发现可以从架构上做出部分调整,以提升整体的IOPS。

读写分离(快慢分离)

整个盘古Master对外接口众多,根据是否需要在Primary和Secondaries之间同步Operation Log来分成读和写两大类。所有读操作都不需要同步Operation Log,所有的写操作基于数据一致性必须同步。考虑到Primary和Secondaries随时都可能发生切换,要保证数据一致,Client端发送的每一个写请求,Primary必须在同步Operation Log完全成功后才能返回Client。显而易见,写比读要慢得多,如果让同一个线程池来同时服务读和写,将会导致怎样的结果呢?一个形象的隐喻是多车道的高速公路,想象一下如果一条高速公路上不按照速度来划分多车道,而是随心所欲地混跑,20码和120码的车跑在同一条车道上,整体的吞吐量无论如何也不会高。参照高速公路设计,进行快慢分离,耗时低的读操作占用一个线程池,耗时高的写操作使用另外一个线程池,双方互不干扰,进行这样一个简单的切分后,读IOPS得到显著提升,而写操作并未受影响。

Pipeline

读写分离后,读的IOPS性能得到极大提升,但写操作依然有待提升。写操作的基本流程如图3所示。

c97f49f8cc059f2e63173d8684f95e9d2ee15311 

在Master端,写操作最大的时间开销在同步Operation Log到Secondaries。这里的关键缺陷在于同步Operation Log期间,工作线程只能被动地等待一段相当长的时间,这个过程包括将数据同步到Secondaries上,并由Secondaries将这个数据同步写入到磁盘,由于涉及到同步写物理磁盘(而非写Page Cache),这个时间是毫秒量级。所以写操作的IOPS肯定不会高。定位问题后,在结构上稍做调整如图4所示。

61bf6432710316eeb421552be92d7653a7064ddb 

类似中断处理程序,整个RPC的处理流程分成了上半部和下半部,上半部由Worker线程处理Request,将Operation Log提交到Oplog Server,不再阻塞等待同步Oplog成功,而是接着处理下一个Request。下半部的工作包括填充Response,将Response写入到RPC Server,下半部由另外一个线程池承担,通过Oplog同步成功的消息触发。这样Worker线程源源不断地处理新的Request,写操作IOPS显著提升。由于只有同步Oplog完全成功才会返回Response,所以数据一致性与此前的实现相同。

Group Commit

经过Pipeline优化后,写操作性能显著提升,但我们对结果依然不太满意,想进一步提升IOPS,为上层客户提供一个更好的结果。继续Profile,发现新实现下同步Oplog吞吐量较低,主要原因是每个写请求都导致一次主备间同步Oplog,而且这条Oplog在Primary和Secondaries上都需要同步写到磁盘。压力较高时,将有大量的同步RPC和小片数据同步写磁盘,导致吞吐量低。通常分布式系统会使用Group Commit技术来优化这个问题,将时间上接近的Oplog组成一个Group,整个Group一次提交,吞吐量可以明显提升。但传统的Group Commit会带来Latency的明显增加,需要在吞吐量和Latency之间做权衡。鱼与熊掌如何兼得?我们对Group Commit过程进行了适当优化,较好地解决了这个问题。

将组Group和同步Group分离,前者由一个Serialize线程承担,后者由Sync线程完成。Serialize线程作为生产者,Sync线程作为消费者,两者通过一个队列来共享数据,当Serialize线程发现队列中大于M个Group等待被同步的时候,将暂停组新的Group;而当Sync线程发现队列中等待的Group小于N个的时候,将唤醒Serialize线程组新的Group,由于Serialize线程经过了一段时间的等待,积累了一批数据,此时可以组成更大的Group,同时又不会造成Latency的提升。当整个系统负载低的时候,队列为空,Serialize无需等待,Latency很低;当系统负载较高的时候,队列中有堆积,此时暂停Serialize,也不会增加额外的Latency,而且随后可以组成较大的Group,获得高吞吐量收益。通过对Serialize和Sync操作的压力测试,可以确定最优化的M、N值。

细节篇

专注细节,做深做透对一个大型复杂系统而言尤为重要。在整个优化过程中,我们遇到很多富有趣味性的细节问题,经过深入的挖掘后,取得了良好的成果。其中具有代表性的一个例子就是Sniff的深入优化。

在5K项目前,盘古Master冷启动时间以小时计,5K后由于规模扩张,Chunk数剧增,冷启动时间会更长。冷启动时,需要做Sniff操作,获取所有ChunkServer上Chunk的信息(Chunk的数量在十亿数量级),将这些信息汇聚到几个Map结构中,在多线程环境下Map结构的插入和更新必须锁保护。通过锁工具Profile也证实瓶颈就在锁保护的Map上。所以缩短启动时间就转化为如何对锁保护的Map进行读写性能优化这样一个十分具体的细节问题。为了减少锁竞争,调整结构如图5所示。

54538ba1e6819c4527cb906658623df3e82ab5da

引入一个无锁队列,所有的工作线程将数据更新到无锁队列,随后由单一线程从无锁队列中批量更新到Map,调整后Sniff期间Context Switch从40万降低到4万左右,耗时缩减到半小时,效果显著。但我们依然觉得过长,继续深挖,发现此时CAS操作异常频繁,而且单一的更新线程已经占满一个核,继续优化感觉无从下手了,此时回过头来研究业务特点,看看能否根据业务特点来减少某些约束。功夫不负有心人,最终我们发现了一些非常有趣的细节,即在Sniff阶段,我们基本不读取这些Map,只在Sniff完成后,第一个写请求的处理过程中需要读取Map,这意味着只要Sniff完成后Map结果保证正确即可,而在Sniff过程中,Map更新不及时导致的不准确是可接受的。根据这个细节,我们让每个工作线程在TSD(线程私有数据)中缓存一个同类型的Map,形成图6中的结构。

ee7ed1551e21695aeb2582874fe384dad2447cbd

每个工作线程直接将Sniff数据更新到线程私有的Map中,这个过程不需要锁保护,当TSD中汇聚的数据超过一定规模,或者数据沉淀了一定时间后再提交到无锁队列中,这样此前每个数据进一次无锁队列变成了批量数据进一次队列,效率明显提升。当然这里也产生了一个新的问题,Map进入无锁队列是由前端Sniff数据驱动的,当Sniff完成时,前端再无数据驱动,TSD中可能滞留了一批“沉没”数据无法提交到队列中,影响最终结果的准确性,这里我们引入一个超时线程来解决这个问题。最终优化完成后,Sniff在数分钟内完成。

总结与展望

经过这一轮优化,Master IOPS有数倍提升,冷启动时间大幅缩减,取得了良好效果。优化过程中我们形成了如下的一些共识,希望能对后续工作有所指导。

  • 坚持用数据来确认瓶颈。一个点是不是瓶颈,是否需要优化,这是一个根本的方向性问题。可以大胆猜想,但一定要用数据来确认具体的瓶颈,否则方向性问题搞错了,会导致后续在非关键路径上浪费大量资源。
  • 密切结合业务逻辑。很多时候系统的优化不是一个简单的计算机科学领域问题,而是与业务逻辑高度相关,结合业务逻辑特点进行优化,往往会事半功倍。
  • 追求极致。在达到预期目标后,再问问自己是否达到了理论极限?是否还有潜力可挖?有时候多走一步,性能可能会有质的飞跃。

性能优化不仅仅是一项工作,更是一种锲而不舍、精益求精的态度,优化没有终点,我们一直在路上!


原文链接
目录
相关文章
|
存储 运维 Kubernetes
【深度】阿里巴巴万级规模 K8s 集群全局高可用体系之美
台湾作家林清玄在接受记者采访的时候,如此评价自己 30 多年写作生涯:“第一个十年我才华横溢,‘贼光闪现’,令周边黯然失色;第二个十年,我终于‘宝光现形’,不再去抢风头,反而与身边的美丽相得益彰;进入第三个十年,繁华落尽见真醇,我进入了‘醇光初现’的阶段,真正体味到了境界之美”。
【深度】阿里巴巴万级规模 K8s 集群全局高可用体系之美
|
3月前
|
人工智能 Java 测试技术
就AI 基础设施的演进与挑战问题之ZooKeeper的稳定性提升配置优化的问题如何解决
就AI 基础设施的演进与挑战问题之ZooKeeper的稳定性提升配置优化的问题如何解决
|
3月前
|
存储 监控 固态存储
【性能突破】揭秘!如何让您的数据库在高并发风暴中稳如磐石——一场关于WAL写入性能优化的实战之旅,不容错过的技术盛宴!
【8月更文挑战第21天】在高并发环境下,数据库面临极大挑战,特别是采用Write-Ahead Logging (WAL)的日志机制。本文通过一个在线交易系统的案例,分析了WAL写入性能瓶颈,并提出优化方案:理解WAL流程;分析磁盘I/O瓶颈、缓冲区设置与同步策略;通过增大WAL缓冲区、使用SSD及调整同步策略来优化;最后通过测试验证改进效果,总结出一套综合优化方法。
60 0
|
缓存 算法 大数据
倚天710规模化应用 - 性能优化 - 软件预取分析与优化实践
软件预取技术是编程者结合数据结构和算法知识,将访问内存的指令提前插入到程序,以此获得内存访取的最佳性能。然而,为了获取性能收益,预取数据与load加载数据,比依据指令时延调用减小cachemiss的收益更大。
|
供应链 安全 新能源
基于主从博弈的社区综合能源系统分布式协同优化运行策略(Matlab代码实现)
基于主从博弈的社区综合能源系统分布式协同优化运行策略(Matlab代码实现)
189 0
|
6月前
|
人工智能 弹性计算 缓存
带你读《弹性计算技术指导及场景应用》——2. 技术改变AI发展:RDMA能优化吗?GDR性能提升方案
带你读《弹性计算技术指导及场景应用》——2. 技术改变AI发展:RDMA能优化吗?GDR性能提升方案
217 1
|
存储 消息中间件 数据库
Milvus性能优化提速之道:揭秘优化技巧,避开十大误区,确保数据一致性无忧,轻松实现高性能
Milvus性能优化提速之道:揭秘优化技巧,避开十大误区,确保数据一致性无忧,轻松实现高性能
Milvus性能优化提速之道:揭秘优化技巧,避开十大误区,确保数据一致性无忧,轻松实现高性能
|
JSON 运维 Kubernetes
一文详解如何在 ChengYing 中通过产品线部署一键提升效率
本文介绍如何通过对ChengYing产品线部署的技术设计,一键完成繁琐工作,极大提升部署效率,希望帮助对「一站式全自动化全生命周期大数据平台运维管家 ChengYing」感兴趣的开发者更好地了解和使用 ChengYing。
142 0
|
存储 缓存 NoSQL
【分布式技术专题】「架构实践于案例分析」盘点高并发场景的技术设计方案和规划
【分布式技术专题】「架构实践于案例分析」盘点高并发场景的技术设计方案和规划
344 0
【分布式技术专题】「架构实践于案例分析」盘点高并发场景的技术设计方案和规划