当前位置: 首页 > 科技观察

携程分布式图数据库星云图运维治理实践

时间:2023-03-16 01:42:43 科技观察

作者简介PatrickYu,携程云原生研发专家,专注于非关系型分布式数据存储及相关技术。一、背景随着互联网世界中产生的数据越来越多,数据之间的联系也越来越复杂和深入。人们对从这些错综复杂的数据中探寻各种关联的需求也与日俱增。.为了更有效地应对此类场景,图技术得到了越来越多的关注和应用。DB-ENGINES趋势报告显示,图数据库增长趋势遥遥领先。在携程,一些业务已经尝试了很长时间的图技术并将其应用到生产中,主要是Neo4j和JanusGraph。从2021年开始,我们将对图数据库进行集中运维管理,期望规范业务的使用,适配携程现有的各种系统,更好地服务于业务方。经过研究,我们选择分布式图数据库NebulaGraph作为管理对象,主要基于以下考虑:1)NebulaGraph的开源版本具有水平扩展能力,为大规模部署提供了基础条件;2)采用自研的图数据库,相比于基于JanusGraph等第三方存储系统构建的图数据库,在性能和资源使用效率上具有优势;3)支持两种语言,特别兼容主流图技术语言Cypher,帮助用户从其他图数据库(如Neo4j)迁移使用Cypher语言;4)具有后发优势(2019年开始开源),社区活跃,主流互联网公司都有参与(腾讯、快手、美团、网易等);5)使用主流技术,代码清晰,技术债少,适合二次开发;2.NebulaGraph架构和集群部署NebulaGraph是一个分布式的计算存储分离架构,如下图:主要由Graphd、Metad和Storaged三部分组成,分别负责计算、元数据access,以及图形数据(点、边、标签等数据)的访问。在携程的网络环境中,我们提供了三种部署方式来支撑业务:2.1采用三个机房的部署来满足一致性和容灾的需求。好处是如果机房级别的任何一个机房出现故障,集群仍然可以使用。核心应用。但缺点也很明显。通过raft协议同步数据时,会出现跨机房的问题,影响性能。2.2单机房部署集群的所有节点都在同一个机房??,节点之间的通信可以避免跨机房问题(应用端和服务端还是会存在跨机房调用).无法使用,所以适合非核心应用访问。2.3蓝绿双活部署在实际使用中,上述两种常规部署方式无法满足部分业务方的需求,如对性能要求较高的核心应用,三机房部署带来的网络损耗可能超过期望。根据携程酒店某业务场景的真实测试数据,本地三机房的部署方式延迟比单机房高50%+,但是单机房的部署机房无法抵抗单个IDC故障。此外,一些用户希望有类似数据回滚的能力来处理应用发布和集群版本升级可能导致的错误。考虑到使用图数据库的业务数据大部分来自离线系统,通过离线操作将数据导入图数据库时,对数据一致性的要求不高。在这种情况下,使用蓝绿部署可以极大地提高容灾和性能。很高兴认识。同时我们还增加了一些配套的辅助功能,比如:分配:可以按比例分配机房的访问权限,也可以主动切断某个机房的流量访问。访问流量和写访问流量切换是手动操作的。蓝绿双活模式是性能、可用??性和一致性的折中选择。在使用该方案时,应用端的架构也需要做更多的调整。配合数据访问。生产中的一个例子:三机房蓝绿部署三.中间件和运维管理我们基于k8scrd和operator部署NebulaGraph,将服务集成到已有的部署配置页面和运维管理页面。获得对pod执行和迁移的控制。基于sidecar模式监控,采集NebulaGraph的核心指标,通过telegraf发送至携程自研的Hickwall进行集中展示,以及设置告警等一系列相关工作。另外我们集成了跨机房域名分配功能,自动为内部访问的节点分配域名(域名只在集群内部使用,集群外部通过ip直接连接),这是这样做是为了避免节点漂移导致的ip变化,影响集群的可用性。在客户端方面,与原生客户端相比,我们主要做了以下改进和优化:3.1会话管理功能原生客户端的会话管理比较薄弱,尤其是2.x早期版本,多线程访问Session不是Thread-safe,session过期或失效需要调用者处理,不适合大规模使用。同时,官方客户端创建的Session虽然可以复用,不需要释放,官方也鼓励用户复用,但是没有提供统一的Session管理功能来帮助用户复用,所以我们添加SessionPool的概念来实现复用。它本质上是管理一个或多个SessionObjectQueues,通过borrow-and-return(下图)保证一个Session同时只被一个executor使用,避免了共享Session带来的问题。同时,通过队列的管理,我们可以对会话的数量和版本进行管理,比如预先生成一定数量的会话,或者在管理中心发送消息后改变会话数量或访问路由。3.2蓝绿部署(含读写分离)上节介绍了蓝绿部署,需要修改相应的客户端,支持访问两个集群。在生产中,读写的逻辑往往是不同的。比如读操作希望两个集群一起提供数据,而写只希望影响一侧,所以我们在进行蓝绿处理的时候也增加了读写分离。(下图)。3.3分流如果要考虑读写的单边切换和不同的路由策略,需要增加分流功能。我们没有使用携程广泛使用的VirtualIP作为访问路由,希望有更强大的自定义管理能力和更好的性能。a)直连代替VirtualIPtransfer可以减少一次转发的损失b)在保持长连接的同时,也可以对每个请求使用不同的链接,分担graphd的访问压力c)完全自主控制路由,你可以实现更灵活的路由方案d)当有无法访问的节点时,客户端可以自动暂时排除有问题的IP,避免在短时间内再次使用。而如果使用VirtualIP的话,由于一个VirtualIP会对应多个物理IP,所以没办法直接这样。通过为不同的idc构建SessionPools,并根据配置进行权重轮询,可以达到按比例分配访问流量的目的(下图)。将流量分发融入蓝绿模式,基本实现了客户端的基本改造(下图)。3.4结构化语句查询图目前主流的DSL有Gremlin和Cypher两种,前者是过程式语言,后者是声明式语言。NebulaGraph支持openCypher(Cypher的开源项目)语法和自己设计的nGQL原生语法,两者都是声明式语言,在风格上类似于SQL。但是,对于一些相对简单的语句,Gremlin风格的过程语法更加人性化,可以用来监控埋点。为此,我们封装了一个过程语句生成器。例如:Cypher样式MATCH(v:user{name:"XXX"})-[e:follow|:serve]->(v2)RETURNv2ASFriends;新的程序样式Builder.match().vertex("v").hasTag("user").property("name","XXX",DataType.String()).edge("e",Direction.OUTGOING).type("follow").type("serve").vertex("v2").ret("v2","Friends")4.系统调优实践由于建模、使用场景、业务需求的差异,在使用NebulaGraph的过程中遇到的问题很可能完全不同。下面以携程酒店信息图线上的一个具体例子来说明我们在整个落地过程中遇到的问题以及处理过程(本文以下内容基于NebulaGraph2.6.1)。关于酒店业务的更多详情,可以阅读这篇文章《信息图谱在携程酒店的应用》。4.1酒店集群不稳定的原因是酒店应用上线后出现故障,大量访问超时,并伴有“Leaderhaschanged”等错误信息。经过一番调查,我们发现metad集群有问题。metad0的本机ip与metad_server_address的配置不一致,所以metad0实际上一直没有工作。但是这本身不会造成系统问题,因为3节点部署只需要2个节点就可以工作,然后不小心漂移了metad1容器,导致换了IP。这时候metad集群实际上已经无法工作了(下图),导致整个集群都受到影响。处理完以上故障重启后,整个系统还没有恢复正常,CPU使用率很高。此时外部应用访问不到流量,但是整个metad集群的内网流量非常大,如下图:监控显示metad的磁盘空间占用非常大,经检查WAL在不断增加,说明流量主要是数据写操作。我们打开一些WAL数据的文件,其中大部分是session元数据,因为session信息会持久化到Nebula集群中,所以问题可能出在这里。阅读源码发现graphd会从metad同步所有session信息,修改后将所有数据写回metad,所以如果流量全是session信息,那么问题可能是:a)session还没有过期b)createdtoomanySessionchecks,发现集群没有配置timeout,所以我们修改如下配置来处理这个问题:type配置项原值修改值说明graphdsession_idle_timeout_secsdefault(0)86400该配置控制session过期时间,由于最初我们没有设置这个参数,也就是说session永不过期,这会导致过去访问过graphd的session一直存在于metad存储层,导致session元数据堆积。session_reclaim_interval_secs(10)默认设置为30表示graphd每隔10s会发送一次session信息给metad进行持久化。这也可能导致写入过多的数据。考虑到即使机器宕机,也只是丢失了部分session元数据更新,而这些丢失造成的损害比较小,所以我们改为30s,以减少metad之间的元数据同步次数。metadwal_ttldefault(14400)3600wal用于记录修改操作,一般来说不需要保存太久,nebulagraph为了安全起码会为每个shard保留最后2个wal文件,所以减少ttl加速wal淘汰,节省空间修改,metad的磁盘空间占用减少,通讯流量和磁盘读写也明显减少(下):系统逐渐恢复正常,但仍然存在一个问题一直没有解决,所以session数据那么多?查看应用端的日志,发现session的创建次数异常,如下图所示:通过日志,我们发现是我们自己开发的客户端存在bug导致的。我们会让客户端在报错的时候释放对应的session并重新创建,但是由于系统抖动,这种行为会造成更多的超时,导致更多的session被释放重建,造成恶性循环。针对该问题,客户端做了如下优化:修改1将session的创建行为由并发改为串行,一次只允许一个线程创建工作,不参与创建的线程监听session池2进一步加强会话重用。当会话执行失败时,根据失败原因判断是否需要释放。本来的逻辑是一旦执行失败就释放当前session,但是有时候不是session本身的问题,比如超时时间太短,nGQL有错误,这些应用层情况也会导致执行失败。如果此时直接释放,会导致会话数明显下降,创建了大量会话。根据问题的合理划分来处理错误情况,可以最大程度的保持会话状态的稳定。3增加预热功能,根据配置提前创建指定数量的session,避免启动时集中创建session导致的超时。4.2酒店集群存储服务CPU使用率过高。当酒店业务端增加访问量时,每次达到80%时,集群中的少量存储就不稳定,cpu使用率突然暴涨,导致整个集群的响应变大,从而导致应用端大量超时错误。如下图:经与酒店方核对,初步怀疑是点密问题(图论中,密点是指一个点有很多相邻边,相邻边可以outgoingedgesorincomingedges),部分存储集中访问,导致系统不稳定。由于业务方强调在他们的业务场景中密集点是不可避免的,所以我们决定采取一些调优措施来缓解这个问题。1)尝试通过Balance来分担接入压力。回忆一下之前官方的架构图。数据在storaged中是分片的,raft协议中只有leader可以处理请求。因此,可以重新平衡数据,将多个densePoints分配给不同的服务,以减轻单个服务的压力。同时,我们对整个集群进行compaction操作(由于Storaged使用RocksDB作为存储引擎,数据是通过append修改的,compaction可以清除过期数据,提高访问效率)。运行后,集群整体cpu有一定程度的下降,服务的响应速度也略有提升,如下图所示。但是运行一段时间后,仍然出现了CPU突然升高,密集点明显不均衡的情况,这也说明密集点带来的访问压力并不能在分片层面得到缓解。2)尝试通过配置减轻锁竞争来进一步调查有问题的存储的CPU使用率。可以看到当流量增加时,内核占用的CPU非常高,如下图:抓取perf,看到锁竞争比较激烈,即使在“正常”的情况下,锁的占比也是非常大,竞争激烈的时候,存储服务有问题的比例超过50%。如下图所示:所以我们从减少冲突入手,主要对nebulagraphcluster做了如下改动:type配置项修改值原值描述Storagedrocksdb_block_cacheDefault(4)8192blockcache缓存解压后的数据,缓存越大,数据越多淘汰越低,越有可能更快的命中数据,减少从pagecache重复加载和压低操作通过前缀更快,减少不必要的数据查询,减少数据竞争RocksDBdisable_auto_compactions默认为false启用自动压缩,以缓解由于数据碎片导致的查询cpu增加write_buffer_size默认为134217728设置memtable为128MB,减少刷新次数max_background_compactions默认为4以控制后台compac线程数据恢复上线后,整个集群服务变得更加顺畅,CPU负载也比较低。一般情况下,锁竞争也减少很多(下图),酒店成功将客流量推到100%。但是运行了一段时间后,还是遇到了服务响应突然变慢的情况。热点接入带来的压力确实超过了优化带来的提升。3)尽量降低锁的粒度考虑到分片层面的平衡不起作用,CPU的增加主要是锁竞争导致的,那么我们认为如果降低锁的粒度,是不是可以可能的?减少竞争?RocksDB的LRUCache允许调整共享数量,我们修改了这个:LRUCache版本默认分片方式为2.5.028。修改代码,将分片改为2102.6.1及以上。28通过配置cache_bucket_exp=10,将分片数改为210观察到的效果不明显,无法解决热点竞争导致的雪崩问题。它的本质和平衡操作是一样的,只是粒度不同而已。在热点非常集中的情况下,数据层面的处理是行不通的。4)尝试使用ClockCache竞争的锁源是blockcache引起的。nebula存储使用rocksdb作为存储,它使用LRUCache作为blockcache等一系列缓存的存储模块。LRUCache需要为任何类型的访问加锁更新一些LRU信息,调整排序和数据吞吐量限制的存在。由于我们主要面临的是锁竞争,所以希望在业务数据无法更改的情况下,其他缓存模块能够提高访问吞吐量。据rocksdb官方介绍,它还支持一种缓存类型ClockCache,特点是查询时不需要加锁,插入时只需要加锁,访问吞吐量会更大。考虑到我们主要是读操作,看UpClockCache会比较合适。LRU缓存和Clock缓存的区别:https://rocksdb.org.cn/doc/Block-Cache.html修改源码重新编译后,我们将缓存模块改为ClockCache,如下图:但是使用集群时,需要几分钟才能拿到核心。查找资料后发现ClockCache支持(https://github.com/facebook/rocksdb/pull/8261)仍然存在问题,目前暂无此解决方案。5)限制线程的使用可以看到整个系统在当前配置下有很多线程,如下图所示。如果是单线程,肯定没有锁竞争。但是作为图服务,每次访问几乎都解析为多个executor进行并发访问。强行改成单线程,必然会造成访问堆积。因此,我们考虑减少原有线程池的进程数,避免过多线程执行同步等待导致线程切换,从而降低系统的CPU占用率。type配置项原值说明Storagednum_io_threadsdefault(16)4or8num_worker_threadsdefault(32)4or8reader_handlersdefault(32)8or12官方未公开的配置调整后整体系统cpu非常稳定,大部分物理机器cpu在20%,没有之前遇到的突然大的波动(瞬时激烈的锁竞争会大大增加CPU占用率),说明本次调整对当前业务有一定的影响。然后我遇到了以下问题。前端服务突然发现nebula的访问明显超时,但是从系统监控的角度看并没有波动(下图24,19:53系统其实已经响应了一个问题,但是cpu没有波动)).原因是限制线程确实有效果,减少竞争,但是随着压力的增加,线程吞吐量达到了极限,但是如果加入更多的线程,对资源的竞争就会加剧,找不到平衡点。6)关闭数据压缩,关闭块缓存。如果没有特别好的办法避免锁竞争,我们回顾一下锁竞争的整个过程。锁的产生本身是由缓存本身的结构带来的,尤其是读操作。有时,我们不想要任何锁定行为。使用blockcache就是在合理的缓存空间内,尽可能的提高缓存的命中率,以提高缓存的效率。但是如果缓存空间非常充足,并且长时间命中的数据在特定范围内,其实并没有观察到大量的缓存淘汰,当前服务的缓存其实并没有满了,所以我想,对吧?您可以通过关闭块缓存并直接访问页面缓存来避免在读取操作期间锁定。除了blockcache之外,存储端还有一大类内存使用就是Indexes和filterblocks,相关的设置是RocksDB中的cache_index_and_filter_blocks。当这个设置为true时,数据会缓存在blockcache中,所以如果关闭blockcache,我们还需要关闭cache_index_and_filter_blocks(在NebulaGraph中,使用配置项enable_partitioned_index_filter,而不是直接修改cache_index_and_filter_blocks的岩石数据库)。但是仅仅修改这些并不能解决问题。其实观察perf我们还是看到了锁竞争导致的阻塞(下图):这是因为当cache_index_and_filter_blocks为false时,并不意味着index和filter数据不会被加载到内存中,这些数据实际上会被放到内存中进入tablecache,需要淘汰哪些文件的信息还需要通过LRU维护,所以LRU带来的问题并没有彻底解决。处理方法是将max_open_files设置为-1,为系统提供无限制的表缓存使用??。在这种情况下,由于没有要替换的文件信息,所以算法逻辑被关闭。总结核心修改如下:type配置项原值修改值说明Storagedrocksdb_block_cache8192-1关闭blockcacherocksdb_compression_per_levelz4no:no:no:no:lz4:lz4:lz4关闭L0~L3级别压缩enable_partitioned_index_filtertruefalse避免缓存索引和filterintoblockcacheRocksDB09_open_files-1避免文件被表缓存淘汰,避免文件描述符被关闭,加快文件读取。关闭blockcache后,整个系统进入一个非常稳定的状态,在线集群访问量翻了一倍多,系统的peakcpu稳定在30%以下,大部分时间都在10%以内(以下)。需要注意的是,酒店场景禁用blockcache是??一个非常有效的手段,可以在特定情况下对热点访问起到更好的效果,但这不是常规的方法。我们其他业务方的星云图集群中没有关闭blockcache。4.3写入数据时服务宕机。原因是当酒店业务写满的时候,即使量不是很大(4~5w/s),整个Graphd集群也会在某个时间完全宕机。因为graphdcluster都是无状态的,彼此之间没有任何关系。如果这样一个统一的机器在某个时间点集体宕机,我们猜测是访问请求导致的。通过查看堆栈发现了一个明显的异常(下图):可以看到上图中的三行语句是重复执行的。很明显这里有递归调用,在合理范围内无法退出。估计是栈满了。增加堆栈大小后,整体执行力并没有提高,说明递归不仅深度大,而且可能呈指数级增长。同时观察宕机时的业务请求日志。大量执行在失败的瞬间失败,但是有部分执行失败显示为空引用错误,如下图:这是因为返回了错误,但是没有错误提示,导致出现空引用(空引用现象是客户端没有合理处理这种情况,也是我们客户端的一个bug),但是这种情况很奇怪,为什么没有报错信息,查看它的tracelog,发现这些requests执行nebula需要很长时间,并且有非常大的语句,如下图:预感是这些语句导致graphd宕机,客户端因为执行被中断产生了null值。重试这些语句以确保机器已关闭。查看这样一个请求,发现它由500条语句组成(业务端语句拼接上限为500条),没有超过配置设置的最大执行语句数(512条)。看来这是官方的星云BUG,我们已经把这个问题提交给官方了。同时将业务端的语句拼接限制从500条降低到200条,成功避免了该问题导致的宕机。5.NebulaGraph的二次开发目前我们对NebulaGraph的修改主要集中在运维相关的几个环节,比如增加指定迁移storaged分片的命令,将leader迁移到指定实例(下图)).6、未来规划与携程大数据平台集成,充分利用Spark或Flink实现数据传输和ETL,提高异构集群间的数据迁移能力。提供Slowlog检查功能,捕获导致slowlog的具体语句。参数化查询函数以避免依赖注入。增强可视化能力,增加定制功能。