数据脏了怎么办

作者: | 更新日期:

这周遇到了天灾,谁都没错,但是就是遇到了。

零、背景

曾经埋下的坑如果没有即使解决,时间越久,这个坑爆发时导致的问题越大。

2016年的两个坑竟然在最近连续爆发了。

这篇文章主要介绍第二次爆发的问题引发的简单思考,当然也会简单介绍下这两次坑的大概情况。

之前交接到我手上的一个服务有一个异常逻辑没有正确处理,这个月依赖的储存异常时,触发了这个BUG,从而给缓存引入脏数据。
由于之前从来没遇到脏数据的情况,服务自身也没有清理脏数据的能力,这里只能采用切储存的方法。
面对每秒千万级别的请求量,面对复杂的架构设计,切储存的成本也是很大的。

于是这里简单思考下,各种储存遇到脏数据时改怎么办。
第一小节和第二小节分享了两个坑,如果想看与储存脏数据相关的话题,可以直接从第三小节看起。
之后介绍了MYSQL脏数据、NOSQL脏数据、缓存脏数据三种情况。

PS:这里的脏数据并不是并发等引入的脏数据,而是因为BUG或意外情况导致储存中的大部分数据发生错误。

一、一个无关的坑

大概2016年12月,我的批量缓存服务实现了一致性hash功能,当时有个代码片段没用上,里面有一个BUG,会导致coredump。

PS:发现很多人对批量服务的理解偏差很大,认为一次请求一个数据和一次请求一万个数据处理性能、处理延时、消耗资源是一样的,面对这种认知偏差,后面有必要写篇文章啰嗦啰嗦。

说起一致性hash,其实可以牵扯到两层服务:路由层,处理层。
路由层对批量请求数据进行拆包一致性hash分别路由请求到处理层,处理层处理拆分的每一个数据,处理层全部回包后路由层再聚合所有请求的数据。 而我的这个BUG就在路由层,由于相关逻辑一直没有用上,服务跑了两年半了都没问题。

这个月新增了功能,用上了那个代码片段,当然开发时也随手修复了代码片段的BUG。
不过由于灰度的一些特点,导致灰度上线的时候,问题无法暴露出来。
后来本来准备全量上线,我突然一想,为了保险起见,还是留一部分机器第二天再全量。
结果那部分有BUG的未升级的路由层服务在线上裸跑了。

这个BUG只有异常的时候才会触发。
白天偶尔异常遇到了coredump,但是由于下面的原因没暴露出来。

新机器的coredump监控告警不知什么时候下线了,新的监控系统还没有开发出来。
针对服务coredump,我当然也留了一手,有自己的监控,结果在一周前调整500个监控属性时这个关键的属性也恰恰被莫名其妙的调整没了(怎么没了我也不知道)。
于是到了晚高峰期,底层很多服务有大量失败异常,从而频繁触发这里路由层的coredump。

我一看路由层服务异常,第一步当然是回滚路由层服务了。
结果回滚后,业务反馈失败率更高了。
于是马上把处理层的服务也回滚了,服务才回复正常。

这个问题后来想了下,问题的关键在于多层服务是一起发布的,而不是逐层发布。
对于那些监控,比如基本系统监控、业务监控肯定要保障的,但是这些并不是必然触发的。
逐层发布的话,遇到问题,至少可以马上对发布的那一层进行回滚,从而做到快速恢复。

二、一个天灾故障

大概是2016年1月份,一个同事重构了服务,API对于异常情况处理有问题。
当然他也没有做错什么,因为我们所有人一般对于API的使用都是参考旧项目的。
而这个API的使用,其实对于异常处理是有问题的:远程服务返回错误时,会当做空数据处理。
是的,现在2018年,故障后我马上去看了很多人最近一年的新项目代码,虽然API被重构过,但是大家依旧把错误包当空数据处理。

回到最近,这个服务早就交到我的手上了,由于服务运行稳定,也一直没有大的变更。
一天远程服务的同事告诉我底层进行调整,会有超时抖动。
由于上层有缓存,其实偶尔超时抖动几下大家都可以接受的。
之前晚高峰远程服务就会偶尔超时,对于超时的情况,这个服务都可以正确处理这种情况的。

这次调整可能超时比较严重吧,触发了远程服务另一个逻辑,快速返回了错误包。
对于远程服务来说,其实触发这个逻辑其实也算正常,因为有这样一个逻辑,满足条件了便触发了。
但是对于这个服务,一直把错误包当做空数据包,从而把空数据返回给缓存服务,从而把空数据长时间的缓存起来了。

由于上层缓存的复杂性,无法快速切换储存,也没有自动快速清理脏数据的机制,最后只好慢慢切换储存,花费了好长时间才切换完成。

这个问题后来我也想了下,发现问题的关键其实是信息不对称。
面对一个API,如果第一次使用,那肯定是边看文档边实现了。
由于对这个API不熟悉,实现方式难免会不完善了。

如果周围有人使用过,面对急迫的需求,自然是参考使用过人的代码了。
这样这个API不完善的使用样例就被广泛传开了。

当然,期间可能会有部分人去看文档,重新实现。
但是这对于其他人来说只是创造了一个轮子。
由于其他人的代码运行了那么久了,新的使用者肯定优先参考跑的更久的样例。
所以即使后来有人修复了,但是他没看过旧的样例,不知道旧的样例有问题,也很难说服其他人参考他的样例了。

而对于API的提供者,他们会认为API文档上写的很清楚,会返回什么数据以及是什么含义。
由于API的提供者与API的使用者的信息不对称,一旦在某个时间点发生没对齐的事情,自然会造成对应的影响了。

所以这个问题,我感觉算是天灾了,迟早会遇到的,不可避免的,就看遇到时影响的大小了。

三、MYSQL脏数据

MYSQL脏数据指的是由于误操作,把MYSQL的数据写错了。
比如把所有数据的标题写成固定值了。

这个问题在2014年还是2015年的时候,我遇到过一次。
大概是由于没有测试环境的缘故,大家都是测试数据和正式数据储存在一起。
一个同事想修改下测试数据的标题,结果忘记加where条件了,就把所有的标题修改了。

PS:我认为除了统计计算,其他的不加where和limit的SQL操作就应该禁止,太不安全了。

当然,互联网业务一般操作DB比较简单的,都是简单的INSERT和UPDATE,没有DELETE操作,而且对于UPDATE全是基于主键操作的。
所以面对DB的脏数据,修复方法比较简单,停止一切写,回滚数据到上次备份时间点,然后使用binlog对之后的数据重做即可。

回滚的时候有个注意点就是注意编码格式,一定要保持一致,不然就乱码了。

四、NOSQL脏数据

这里的NOSQL一般是REDIS,储存全量数据,对外网提供高性能服务,而数据一般是从MYSQL同步进来的。

原因往往是同步脚本的BUG或者MYSQL数据错误,导致NOSQL数据错误。
这里恢复的前提是数据源MYSQL数据是正确的,所以如果数据源MYSQL不正确,这里就谈不上恢复一说了。

如果数据量比较小,往往直接重新同步MYSQL数据到NOSQL就行了。
而数据量比较大,且在NOSQL请求量高峰期,这时修复数据就需要注意了。
需要先评估在大量更新时,是否会影响NOSQL正常对外服务。

如果高并发的对NOSQL写不影响对外服务,那自然是最好的,快速写进去就好了。
而如果有较大影响,就需要考虑放慢节奏慢慢更新了。

有时候时间紧迫,需要快速修复问题,怎么办呢?
重新搭建一个REDIS,把全量数据快速写入新的REDIS。
由于不需要对外服务,所以可以以最高速率把数据写进去,然后上层切换到新REDIS即可。

五、缓存脏数据

这里的缓存和NOSQL的区别在于,NOSQL是储存全量数据的,而缓存只储存热点数据的,数据主要是通过缓存没命中向下回源写进去的。

有些缓存也支持底层更新时把数据写进缓存来。
这里就不能简单的把全量数据写进缓存里面了,因为缓存主要储存热点数据的,这样把全量数据写进来,必然造成缓存的命中率极低,从而影响正常对外服务了。
如果更新时删除缓存的数据,由于只删一次,倒是没啥问题。
当然,我们这里不讨论缓存是应该更新还是删除,那不是这篇文章的重点。

对于架构简单的缓存,也可以像NOSQL那样,切换缓存储存,从而修复脏数据问题。
对于架构复杂的缓存,有时候切换缓存的成本很高很高,比如必须操作五六个步骤,再加上请求量比较大切换时需要预热,使得切换储存几乎成为不现实的操作。

面对储存切不动的情况,我们就需要想办法自动快速刷新缓存了。
说起缓存,一般都会有一个seq来标识数据是什么时候写进来的,比如常见的seq就是时间戳。
当前时间大于seq加上缓存时间时,一般就认为缓存的数据过期了,需要回源更新。

我们可以有一个全局seq的概念,当数据的seq小于全局seq时,就认为这个数据需要刷新。
此时,我们就可以依靠这个全局seq来刷新缓存的所有数据了。
考虑的全局seq可能导致一下数据都过期,可以加一下保护措施来防止发生这样的情况。

比如全局seq原先配置成缓存里面的最小seq,这样就都不满足过期。
然后按照步长逐渐增大,就可以逐渐的刷新缓存里面的数据了。

当然,另一个保护措施是增加一个每秒最大过期个数。
即每秒只能使用全局seq若干次,超过次数就不能使用了。
这样我们就可以控制每秒主动过期多少数据了。

当然,在全局seq的基础上,我们可以做很多细微的优化,目的都是控制向下的透传量,防止突然命中率降低,从而影响服务质量。
你有什么好的想法也可以留言告诉我。

六、最后

修复脏数据问题其实都比较消耗时间的。
比如MYSQL的备份回滚binlig重做,redis的同步全量数据,缓存的刷新数据。
这个期间都会对外服务的数据都是有问题,我们还是尽量避免发生这样的事情吧。
不过,有时候发生这些没办法避免的事情,那就欣然接受吧。
你有什么故事分享给我也可以留言。

点击查看评论

关注公众号,接收最新消息

关注小密圈,学习各种算法

tiankonguse +
穿越