首页 / 资讯中心 / 文章详情

OpenFalcon监控数据多重备份:transfer多活、Kafka持久化与双graph双写实践

OpenFalcon监控数据多重备份:transfer多活、Kafka持久化与双graph双写实践 ★ FEATURED ARTICLE
监控数据丢了比没监控更糟——它让人在出故障时对着一条断掉的曲线干瞪眼。我接手的那套OpenFalcon就是这种状态某个版本跑了大半年第一次遇到graph磁盘损坏时整整两天的重要生产指标全部消失复盘时才发现transfer到graph这条链路从头到尾只有一份数据、一条路没有任何冗余可言。后来我们花了几周时间把transfer链路完整做了一套多重备份改造。这篇内容不聊架构概念只讲实际落地transfer实例怎么从单点变成多活Kafka怎么在数据和后端存储之间当“保险箱”同一份监控数据怎么做到双graph写入以及rrd文件、配置文件的最后一层恢复手段。全程基于一套已经稳定运行的改造方案参数、代码片段、踩坑记录都摆出来给正在做同类工作的读者作个参考。1. 先想明白transfer在监控链路里到底卡在哪个位置1.1 一条监控数据的完整旅程在动手之前我们得先把OpenFalcon的数据流讲透。以一台机器为例agent每分钟或每step周期采集一次CPU、内存、磁盘等指标然后通过RPC协议上报给transfer。transfer不是终点它是一个中转站把数据去重、批量打包之后一边推给judge做实时告警判断一边推给graph做历史数据存储。也就是说从数据产生到真正落盘中间隔了一层transfer。而transfer本身是无状态的它只负责转发。无状态意味着它好扩容、好做多活但也意味着它自己不保存任何数据一旦下游处理慢了、挂了、或者网络出问题数据就在它手里丢掉没有任何追溯机会。这个拓扑关系想明白之后你会发现一个残酷的事实原生部署下每个agent上报的数据只对应一台transfer节点而每个metric的一致性哈希结果也只对应一台graph节点。任何一个节点出现问题监控数据就断一条。1.2 为什么“多重备份”要从transfer下手有人可能会问agent那边也能加备份啊graph那边也能做副本啊为什么偏偏围绕transfer做文章我的看法是transfer是整个链路里最适合做“备份锚点”的位置。往下看agent数量动辄几千台要在每台agent上加本地缓存、加重传机制等于同时维护几千个分布式节点成本极高。往上看graph是状态节点直接做双活需要考虑rrd文件的并发写、索引一致性这些问题原生版本根本不支持。而transfer天生无状态改造它不会引入数据一致性问题在它这一层加Kafka缓冲、加多实例负载均衡都是顺理成章的事。实际改造中也验证了这一点我们改transfer代码、加Kafka消费者全程没有动agent和graph的核心逻辑风险面控制得很小。这也符合生产系统改造的基本原则——能少动的地方就不动只在最关键的一层加保险。1.3 备份目标拆解要防住哪些“丢数据”场景“多重备份”这句话听起来很笼统但落到工程上必须拆成明确的目标。我列了一张表把可能丢数据的场景和对应的防线对应起来风险场景影响范围需要的备份手段transfer进程异常退出、机器宕机一个上报周期的数据全部丢失transfer多实例 负载均衡transfer到graph链路网络抖动、超时部分数据点写入失败引入Kafka持久化缓冲graph磁盘损坏、文件系统异常历史存储文件丢失多消费组双写 rrd周期快照Kafka自身节点故障缓冲层数据丢失Kafka多副本(3副本)配置修改错误、版本回退困难服务起不来、恢复了也不能用配置和二进制版本管理把这几个目标想清楚之后我们才敢动手。不然今天加个备份明天加个快照都是零散的永远不知道自己到底在防什么。2. 四层备份架构从实例高可用到数据可回溯2.1 agent侧本地缓冲先保住源头可选的前置保险严格来说完整的备份链条可以从agent端就开始。OpenFalcon原生的agent是不带spool机制的采集完直接推给transfer如果transfer不可达这个周期的数据就直接丢弃。但真正生产环境里我在agent侧只做了一个轻量方案在agent机器上跑一个很小的心跳守护进程定期检查agent进程状态挂了就自动拉起。至于agent本地数据缓冲我们评估后放弃了。原因是agent数量太多维护一个磁盘缓冲队列会让每个采集点的实现复杂几十倍而且agent本身通常不丢数据真正的风险集中在transfer之后。如果你们有极强的数据完整性要求可以考虑给agent加一个本地文件缓存把每次上报的数据同时追加到本地日志文件transfer恢复后补传。但这种方案会大幅增加代码复杂度和磁盘消耗我建议多数场景直接跳过从transfer这层开始做多重保障。2.2 第一层transfer多实例与负载均衡消除单点transfer单点的解法很直接跑两台transfer前面用域名加VIP做负载均衡。agent配置里指向这个域名keepalived检测transfer的健康状态一台down了流量自动切到另一台。这里有个关键点transfer是无状态的两台实例可以同时收数据不需要做主备切换的复杂逻辑。agent每60秒一个上报周期域名解析本身有缓存所以切换的窗口最多丢一个上报周期这个窗口内的数据丢失量是可控的。不过只做这一层远远不够。多活解决的是“transfer节点本身坏了怎么办”但解决不了“transfer把数据发出去了但graph没收到”的问题。所以这只是我们的第一层接下来要上真正的保险箱。2.3 第二层Kafka持久化给数据上保险箱第二层是我们整套方案的核心transfer不再是直连graph而是先把数据写入Kafka由Kafka做持久化缓冲然后再由消费服务把Kafka里的数据写入graph。为什么选Kafka而不是直接给graph做RAID或者定期快照因为我们要防的不是磁盘损坏那一种情况而是“数据在传输途中丢失”这个更普遍的问题。Kafka天然提供持久化、顺序写、消息位移管理graph挂了、网络断了、下游处理慢了数据都安全躺在Kafka里等下游恢复后继续消费一个点都不会丢。Kafka这层自身也要做备份。我们建的topic使用3副本acks设为all配合min.insync.replicas2保证任何一个broker宕机数据仍然有完整副本。这一步很容易被忽略——很多人把Kafka当透明管道结果Kafka自己单点挂了整个保险箱都丢了等于白做。2.4 第三层多消费组双写存储层也做冗余Kafka把数据保住了但graph作为最终存储节点本身还是单点。如果graph的磁盘坏掉Kafka里的数据虽然有——可要重新灌入一份亏损的rrd文件恢复时间很长。更理想的做法是同一份数据同时落到两套graph集群上。利用Kafka的消费组机制这是现成的多副本方案。同一个topic可以被多个消费组同时消费A消费组把数据写入graph集群AB消费组把同样的数据写入graph集群B两个消费组之间互不干扰。这个方案的妙处在于它完全绕开了“给graph做原生双活”的复杂问题。graph不需要感知自己是被双写的它只负责老老实实落盘。我们得到的效果是一台graph坏了另一套graph数据完整可用查询直接切过去RTO从原来的几个小时甚至一天压缩到分钟级。2.5 为什么选这套方案以及放弃的备选路线设计过程中也考虑过其他几条路说下我的取舍。一条路是agent双上报也就是每台agent同时把数据发给两套独立的OpenFalcon环境。这个方案数据隔离性最好但代价是agent配置、采集资源、存储资源全部翻倍而且两套环境独立维护成本很高我们最终放弃。另一条路是改graph源码让它支持原生的多副本一致性写入。这条路看起来最“正统”但graph内部的rrd写入涉及文件锁、内存索引、rra归档改动风险极大一不留神会把稳定性搞坏。而且原生社区基本没有现成实现全靠自己造轮子投入产出比太低。相比之下基于Kafka的消费组双写方案既不碰graph源码又能利用成熟的消息队列能力是我们在综合了稳定性、工作量、可维护性之后选出的最优解。一句话总结用消息队列的成熟能力换存储层的稳定扩展。3. 落地实操transfer改造与Kafka接入全流程3.1 环境前提与组件版本先交代一下我们这套改造基于的版本环境仅供参考OpenFalconfalcon-plus分支v0.2.x系列Kafka2.x版本至少3个broker节点ZookeeperKafka自带的一致性协调依赖开发语言Go 1.15依赖IBM/sarama原Shopify/sarama的新维护仓库我们建议把Kafka部署在独立的机器上不要和transfer、graph混部。Kafka对磁盘顺序IO和内存要求高混部容易互相干扰尤其是graph跑着大量rrd读写时会拖慢Kafka的写入延迟。3.2 transfer增加Kafka发送能力transfer的改造点在sender模块。原生逻辑里transfer从队列取数据后根据后端类型分别分发给graph和judge。我们要做的是把发送给graph的那条路径改道到Kafka。首先在transfer的cfg.json中加入Kafka配置{ kafka: { brokers: [10.0.1.11:9092, 10.0.1.12:9092, 10.0.1.13:9092], topic: falcon-metric, maxRetry: 3, retryQueueSize: 100000 }, graph: { nodes: [graph-a-01:6078, graph-a-02:6078] } }注意这里graph节点配置仍然保留。原因后面会讲消费服务要用这套一致性哈希配置来决定数据写到哪个graph节点。然后在sender代码里增加Kafka生产者的初始化逻辑import ( github.com/IBM/sarama ) func newKafkaProducer(brokers []string) (sarama.SyncProducer, error) { config : sarama.NewConfig() config.Producer.RequiredAcks sarama.WaitForAll config.Producer.Return.Successes true config.Producer.Compression sarama.CompressionSnappy config.Producer.Flush.Frequency 200 * time.Millisecond return sarama.NewSyncProducer(brokers, config) }核心发送函数改造原来是直接rpc调用graph写入现在改为同步发送到Kafka。等待Kafka的ack返回确认消息真正落盘后才算发送成功func (w *GraphWorker) Send(data []byte) error { msg : sarama.ProducerMessage{ Topic: w.kafkaTopic, Key: sarama.StringEncoder(dataItem.Key()), Value: sarama.ByteEncoder(data), } _, _, err : w.kafkaProducer.SendMessage(msg) if err ! nil { // 本地失败队列入盘由重试协程扫描补发 w.retryQueue.Push(data) return err } return nil }这里有一个我们反复强调的经验一定要用同步发送即等待Kafka返回ack。如果图吞吐量用异步发送transfer进程一旦在这个间隙崩溃内存里还没发出去的数据就全丢了Kafka这个保险箱等于白加。3.3 从Kafka到graph的消费落盘服务transfer改造完成后数据会源源不断进入Kafka的falcon-metric这个topic。接下来需要一个消费者服务把它读出来写进graph。这个消费服务可以直接复用transfer的sender和一致性哈希逻辑。核心结构是消费组拉取消息解码成Falcon数据项按endpointmetrictags的一致性哈希找到对应的graph节点通过graph暴露的RPC方法写入。消费端的核心代码框架type kafkaHandler struct { graphRing *consistent.HashRing graphClients map[string]*GraphRpcClient } func (h *kafkaHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { for msg : range claim.Messages() { item, err : decodeFalconItem(msg.Value) if err ! nil { // 解析失败的消息记录后跳过不阻塞消费 continue } node : h.graphRing.GetNode(item.Key()) if err : h.graphClients[node].Insert(item); err ! nil { // 写入graph失败不确认offset让Kafka重新消费 return err } sess.MarkMessage(msg, ) } return nil }这个服务的两个细节值得注意。第一写入失败时不要确认offset而是返回错误触发rebalance让它重新消费。代价是可能重复写入但graph那边按时间点覆盖写入重复的影响可控这个我们第5节会细讲。第二消费服务的concurrency要跟Kafka分区数匹配。一个partition同时只能被一个consumer消费如果要提高消费并行度就多开几个goroutine加多分配几个分区。我们当时的topic开了12个分区消费端开12个并发去跑压测下来完全跟得上数据生产速率。3.4 双消费组配置同一份数据写两套graphKafka的消费组机制天然支持多副本消费。我们起两个消费服务实例配置不同的group.id指向同一个topic就可以让同一份数据同时写入两套graph。比如消费服务A的配置{ kafka: { brokers: [10.0.1.11:9092, 10.0.1.12:9092, 10.0.1.13:9092], topic: falcon-metric, group: graph-a-consumer }, graph: { nodes: [graph-a-01:6078, graph-a-02:6078] } }消费服务B的配置{ kafka: { brokers: [10.0.1.11:9092, 10.0.1.12:9092, 10.0.1.13:9092], topic: falcon-metric, group: graph-b-consumer }, graph: { nodes: [graph-b-01:6078, graph-b-02:6078] } }这两个消费服务互相独立互不感知。哪怕B机器整个宕机A还能正常写graph-a集群数据一条不少。后面我们实际演练时直接把graph-b整个目录删掉等它重新同步期间graph-a稳定扛住所有查询没人感知到异常。这里有一个容易踩的坑不要把两个消费组放进同一个进程里。因为同一个进程里多group消费时某个分区的rebalance会互相影响。我见过有人图省事在一个服务里同时起两个消费者结果一个group rebalance时把另一个也顶下线数据消费出现抖动。我们最终拆成了两个独立进程部署干净利落。3.5 联通性验证怎么证明数据真的不丢了改造完成后最关键的环节是验证。单纯看服务起来没有完全不够必须证明“graph A里的数据和graph B里的数据是一致的而且和源头对得上”。我们的验证分三步走。第一步用OpenFalcon的api接口分别查询graph A和graph B取同一批endpoint在同一个时间段的曲线数据对比时间点的数值是否一致。写个脚本批量跑比对结果全部通过才认为双写生效。第二步观察Kafka的消费组lag。Kafka自带的工具可以查看某个group在每个partition上的消费进度正常情况lag应该稳定在很低的水平。如果lag持续上涨说明消费速度跟不上生产速度需要扩分区或增加消费并发。第三步做一次真实的故障演练。把graph A上某个rrd文件手动删除模拟存储损坏然后看消费组如何重新写入再对比A和B的数据是否最终收敛。这一步不仅验证了恢复能力也在团队内部建立了信心。4. 存储与配置的最后一层rrd快照和一键回滚4.1 graph本地rrd文件的周期快照虽然我们做了双graph写入但双写防的是“单台graph宕机”防不了“两套graph同时出问题”这种极端情况。比如机房断电、存储阵列同时掉电。所以rrd文件的冷备仍然要做作为最后一层兜底。rrd文件是graph的核心资产每个指标一个文件。我们的快照策略很简单每天凌晨3点低峰期把graph机器上整个rrd数据目录用rsync增量同步到备份磁盘和另一台存储机器。0 3 * * * /usr/local/scripts/rrd_backup.sh脚本内容大致是这样#!/bin/bash # 快照前先短暂暂停graph的写入保证文件一致性 systemctl stop falcon-graph sleep 5 rsync -av --delete /data/falcon-graph/data/ /backup/falcon-graph-data/ systemctl start falcon-graph停止graph这几十秒会有数据断点但没关系断点期间的数据仍然完好地躺在Kafka里恢复时从Kafka回放即可。这条冷备链路只用来恢复老数据不追求实时性。4.2 配置与二进制的版本管理备份不只是数据配置和二进制同样需要可追溯。我们的做法是把所有模块的cfg.json、自研的消费服务代码、编译好的二进制统一放进git仓库管理每次改动都走版本记录。这一步看起来很基础但真出故障时价值极大。有一次我们为了排查问题手工修改了graph的配置顺手把原始配置覆盖了第二天服务起不来全靠git回滚才救回来。从那以后我们对配置的改动作了硬性要求先改git仓库再同步到服务器。4.3 故障恢复演练graph机房级宕机怎么办我们每周做一次恢复演练流程已经固化成了文档。这里直接给出最坏情况的处理流程故障点影响恢复手段预期RTO / RPO单台graph宕机部分指标无存储双graph切换查询宕机节点自动从Kafka续传RTO 1分钟 / RPO 0整套graph A故障一套存储不可用切graph BKafka中消息保留等A恢复后重放RTO 5分钟 / RPO 0graph AB同时故障所有历史存储缺失恢复rrd快照 Kafka补发断点差异数据RTO 30分钟 / RPO 取决于快照时间这里的RPO达到0靠的就是Kafka。因为所有数据在写入graph之前都完整保存在Kafka里且Kafka的3副本保证消息不丢。只要broker不全部挂掉数据就在随时能重放。每次演练我们都会记录Kafka消费lag归零所需的时间基本都在分钟级。5. 实战中的坑与排查记录5.1 Kafka积压了告警却没挂先分清实时链路和存储链路改造之后我们发现一个有意思的现象Kafka消费积压非常严重的时候告警依然正常触发很多同事一度以为“Kafka没生效”。后来才意识到transfer对judge的实时告警通道根本没走Kafka。OpenFalcon的transfer同时连接judge和graph。judge负责实时判断延迟必须低graph负责历史存储对延迟容忍度高。改造时我们只把graph链路改道到了Kafkajudge链路保持原样直连。这是刻意为之——告警链路宁可简单也不要加缓冲否则判断延迟会导致告警不实时。所以排查性能问题时一定要把链路分开看。Kafka积压只影响历史数据写入速度不影响告警判决。如果发现告警延迟先查transfer到judge的网络和judge自身性能而不是盯着Kafka的lag。5.2 重复消费会不会把监控数据写花Kafka消费端最常见的场景之一就是rebalance发生或写入失败重试时同一批消息被消费两次。我们上线初期也担心过这个问题后来通过实际数据验证确认graph的rrd写入天然是幂等的。rrd文件按endpointmetrictagstimestamp确定一个数据点同一个时间点重复写入graph会做覆盖或去重处理不会产生两条重复曲线。对counter类型的数据graph内部会维护计数关系对gauge类型的数据重复写入同一个时间点值以新写入为准。因为我们写入的数据本身就是同一份原始数据重复写入的值完全一样所以最终曲线不会出现毛刺或跳变。唯一要注意的是重复消费不要造成“老数据覆盖新数据”的情况。比如消息A被消费了两次第二次写入的timestamp比第一次晚因为中间隔了重试而graph的rrd写入按timestamp定位这个时间点的实际值由后写入的覆盖最终结果仍然是正确的。5.3 磁盘成本翻倍的取舍双graph全量冗余等价于存储成本直接翻倍。如果你们的监控指标量很大这个成本压力会很直接。开个5万台机器规模的集群每台机器300个指标一年下来graph的数据量就是几十TB级别翻倍后不是小数目。我们在实际部署中做了个折中graph A保留全量数据graph B只作为近期热数据的冗余副本配置成与graph A不同的保留策略。具体做法是消费服务B写graph B时我们会过滤掉已经超过保留期限的旧数据只保留最近一个月。监控体系里绝大部分查询都是看近期历史数据很少翻这个取舍在没有明显体验损耗的情况下省掉了一半的磁盘成本。如果你的团队暂时接受不了双graph的成本我建议至少保留Kafka缓冲层。因为Kafka本身除了防丢数据还帮你解决了graph重建、数据回放、高峰削峰的一系列问题这个投入性价比远高于单纯买磁盘做raid。5.4 rrd文件快照恢复的细节坑rrd冷备这块我们踩过两个坑。第一个坑是运行期直接拷贝rrd文件会得到损坏副本。rrd文件是mmap方式写入的运行中直接cp文件虽然拷贝出来了但内部的数据结构和rra归档可能不一致恢复后graph启动时会拒绝加载。后来调整为快照前先停止graph进程再拷贝问题才解决。第二个坑是graph启动时的索引重建。graph启动后会对data目录做一次全量扫描构建内存索引。如果rrd文件数量很多几万个文件的情况下扫描需要10到20分钟期间查询接口返回很慢。所以每次恢复演练我们都会提前把“graph启动慢”这个时间算进RTO里不要天真地以为文件恢复完就万事大吉。这套多重备份方案跑到现在我最大的感受是真正稳的监控系统不是靠某一个环节有多先进而是把每个环节的失败场景都想清楚了。从transfer多活到Kafka持久化再到双graph消费组、rrd冷备每一层覆盖一种风险层与层之间互不依赖哪一层出了问题都有下面兜底。最后再分享一个小建议如果目前你们的OpenFalcon还只有一个transfer、一份graph先不要急着一步到位做全套可以先把Kafka缓冲加上它是最能解决痛点、改造风险又最小的一步。等这层跑稳了再考虑双graph、双消费组。监控系统的改造永远是一步步来的步子迈太大反而容易在执行层面出问题。
阅读完成 · 觉得有帮助?
咨询建站