【工作总结】1、生产环境发布事故&对于数据库升级转换的总结

0、前言

犯错可以,但要尽量做到不贰过。

学习他人犯的错,也要尽量做到不贰过。

1、背景描述

一次正常的敏捷迭代完毕,从编码、自测、转测、稳定性全部流程走完后,但在迭代最末期 copy 生产环境的数据库信息准备实打实的做模拟升级时,突然发现迭代中某位同事所负责的数据库升级转换出现了问题,导致数据库直接挂掉,业务严重受到影响。且没有事先考虑升级失败的场景,也就是说我们只能通过回滚数据库的方式来恢复环境。并且另一个数据库升级转换问题没在测试阶段发现,遗留到生产环境后,导致生产环境部分数据升级成功,部分失败,属于严重业务问题。

简单描述下数据库升级转换的场景:

1、clickhouse 数据表新增3个字段,且其中一个需要随机赋值,该字段作为数据记录行的唯一标识。
2、mongodb 一个文档中存有相关配置,现在新增几个配置,在这个文档中插入几条数据。

这是前同事接到的需求中的一部分,预计一个数据库升级 1 天工作量,总计 2 天的工作量,他也很快写完了,并在研发环境自测升级通过,当时我也在使用这个环境,查看了升级效果,他也向我确认了此次升级效果(PS:我们是平级同事)。紧接着公司人力调整,团队大量裁员,他是其中之一,测试按照流程走到了拷贝数据模拟升级的流程中时,发现了上述背景描述 1 中的问题。团队只剩我一人,bug 就自然的分配到我这边。

2、问题解决

2.1、clickhouse 数据库升级转换问题

首先遇见的是 ck 这边升级转换的问题,因为它足够显眼,代码运行完后,要升级的数据表直接无法访问了,查看日志都是连接数据库超时。平台涉及到这个表单的业务也无一例外的超时报错。

2.1.1、原升级手段

可以先去看看背景描述中的场景 1:

1、原表基础上,新增 3 个字段。

采用 ALTER TABLE xxx.xxx ADD COLUMN IF NOT EXISTS id UInt64 comment 'xxx';
意思是:如果 xxx.xxx 表中不存在名为 id 的列,则新增,若存在则不处理。其余两个新增字段也是相同的处理方式。

2、给新增字段赋值

采用 ALTER TABLE xxx.xxx UPDATE is_read = 0;
ALTER TABLE xxx.xxx UPDATE id = abs(intDiv(rand64(), %d))
意思是:如果 xxx.xxx 表中的 is_read 字段的所有记录全部修改为 0,将 id 字段赋予随机值(细节的加了 abs 取绝对值,按照他的说法是会有溢出导致的负数)。

2.1.2、所存在的问题

乍一看没啥问题啊,ck 加字段肯定没有问题,但字段赋值这块吧,emmmm…不好说,估计有性能问题。

性能问题的缺陷预防也是 技术指导经理 在做 CR 的时候提出来的。但碍于时间关系就延后在做了,结果做着做着人做没了…

现在开上帝视角直接点明问题所在:

1、增加字段确实没有问题,ck 中加字段只是加一个 txt 文档,没有丝毫压力。
2、但这个 ALTER … UPDATE … 更新操作,在 ck 的 MergeTree 引擎下,和 redis、mysql 中都不一样,在 ck 中将其称为 “突变” 操作,即:mutation。具体可以取查看 ck 官网,或者这篇博文:ClickHouse系列–Mutations操作:数据的删除和修改

总结来看就是:不要在 ck 中做 ALTER … UPDATE … 更新操作,尤其是大数据量的批量更新,会导致数据表直接崩溃。Mutations 操作对于 ck 来讲是很慢的,破坏原有的存储结构。

2.1.3、解决方案

既然不能再原表上进行更新,那么也就只剩下最后一条路可以走:临时表

1、在原表 a 的基础上,采用相同建表语句并加上新增的三个字段,建立临时表 b。
2、分批将原表 a 的数据读取至内存,给共有字段赋值,给新增的 3 个字段赋值。
3、将这批数据 插入 进临时表 b。
4、将原表 a 更名为 c,将临时表 b 更名为 a,替代 a 的功能,完成数据库升级转换。

结合 ck 的数据库特性,在 查、插 的速度上很快。再结合批量操作,就完成了本次数据库升级转换。

2.1.4、解决过程中出现的问题

整体方案十分明确、简单。但过程中还是遇见了两个小问题,值得记录下,给自己提个醒。

1、之前用 gorm 处理 mysql 数据时,习惯使用 Limit 和 Offset 配合做分页。只需要传入 页数、一页大小 即可完成分页查询。在 ck 中采用的是 手搓 sql,在分页时只用了 Limit,写出了 Limit 1000, 2000 的代码。当初的想法是需要 [1000, 2000] 行的数据,但实际上给到我的是 [1000, 3000] 行的数据。就是将 Limit 的意义弄错了,导致 sql 错误,数据库升级后多出很多数据…


2、内存中处理好数据往临时表插入的时候,我们内部封装有异步批量写接口,里面会对接收到的数据再次进行分批插入,每批之前有一定的等待时间。问题出现在最后的一批数据调用异步批量写接口后,程序正常向下去更改原表、临时表名称,到真正去准备数据插入时,发现数据表找不见了,因为改名字了…导致部分数据就从数据库中丢失了。这个也是严重问题。所以,需要在更改名称之前,加等待时间,校验异步批量写队列是否被消费者消费完毕。

2.1.5、总结

问题十分明确,必现问题,数据库都有备份,有恢复机制,测试也比较方便。整体花费 1.5天 时间,从问题发现到最终解决。

实际上这很明显的暴露了一个新手程序员的代码质量意识及性能意识,只是拿到手直接干,功能实现能跑就行。结果到最后,挖的坑还是得来填。

当然,此类问题没有经历过的话,当然允许犯错,但最好只犯这一次。

2.2、mongodb 数据库升级转换问题

紧接着遇到的的是 mongodb 升级转换的问题,不够显眼,问题现象是部分数据用户升级成功,部分失败,且跟业务息息相关,最为严重的是因为不了解这块的业务,在测试环境中并没有发现这个问题,真正上到生产环境升级时,才发现有这个问题…但升级失败不影响主业务的运行,当时判断不进行失败回滚,后续采用补丁方式解决。

2.2.1、原升级手段

可以先去看看背景描述中的场景 2:

1、针对 mongodb 我们有一套分库分表机制,原逻辑将所有的库、表中的待升级数据全部取出来。
2、判断是否需要升级,第一次的话就是全部都需要。
3、升级后,将数据一个个重新写回数据库。

2.2.2、所存在的问题

这个问题十分明显,看一眼就能发现问题…

整个过程中没有任何批量操作,没有任何 sleep 等待操作,也不管服务端能不能处理的过来,反正就一股脑往服务端发送更新请求。

我们有单独操作 mongodb 的微服务,服务间使用 grpc 进行通信,然后因为预先处理好了待更新数据,判断操作是在内存中操作,整体过程很快,发送的速率、并发量都很高,直接导致 mongodb 的负载陡然变高,随即出现大量 slow-sql,数据库升级不到 1/10 就崩溃。

总结来看就是:无脑的操作,导致数据库崩溃。

2.2.3、解决方案

很明显的问题,方案如下:

1、服务本地建立 mongo 连接,直接操作 mongo 去做查询、更新。不通过 grpc 在服务间进行通信。
2、应用 mongo 的事务接口,保证能够失败回滚。
3、按照单个库下的所有数据表来进行更新,且更新表单时,需要分批、限速,添加一定的 sleep 等待时间,防止负载过高,影响其他业务。

2.2.4、解决过程中出现的问题

就遇到一个 bson 序列化操作导致配置多了几个 xxx_ 开头的无用字段,这个采用 json 序列化、反序列化干掉就行了。其他的都没有什么问题,常规代码操作。

2.2.5、总结

这个问题没啥好总结的,经验不足想当然的操作导致的。不贰过。

3、总结

突然遇见的问题,导致加班,公司变动导致团队中一起编码的同事走的只剩我一个,写这篇文章更多的是记录一下最后遇见的问题,并没有什么 diss 的含义,比起这些问题导致的加班,我还是更怀念一起发现问题、解决问题的时光。

简单总结一下:

1、数据库升级操作要慎重,一定需要考虑大数据量的情况。

新手尤其需要重视,做好数据备份,准备好升级失败的第二手方法。提前预知风险,做好测试准备,做好缺陷预防。

2、编码时,考虑下如果此次升级失败、升级中断等异常场景,能否有机制保证我重启服务后,再继续运行拉起升级程序,保证最终升级成功

实际上生产环境的 mongodb 问题,加班很久,就是为了梳理他的业务逻辑,看看能否有此机制,将未升级的数据最终升级成功。

3、将批量写、等待时间等参数最好作为配置。

因为环境的不同,网络通信的不同,如果写死在代码中,难以保证测试环境能扛得住这些批量数据。这个时候,就可以灵活配置这些参数,保证代码不用修改,因为代码修改还要做合并、合入、打包、推送镜像,很是不便。

4、升级过程中做好日志记录。

其实对于升级操作而言,一般都是在程序初始化阶段进行,可以打印 INFO 日志,记录一些关键信息,不建议选择 debug 日志等级,因为 debug 日志等级如果不开的话,就不打印在前台终端,而有时候崩溃就是一瞬间的负载过高,还没来得及开日志等级,服务就挂掉了,抓取不到错误情况,很是无语。
在这个升级过程中也可以打一点日志,比如:一开始打印时间,执行到某个阶段了,可以利用 INFO 日志打印一些关键信息,能看到确实正在升级中,执行完毕后,打印 INFO 日志,记录关键信息和花费时间。这点我觉得是此类需要长时间执行的必备习惯了,而在工作中经常遇见到的是:程序运行后啥也没打印,就看着黑框发呆干等着,根本掌握不到升级进度。或者打印的都是些废话,没有什么意义。

5、对于语言、数据库层面。

因为在短时间大量从数据库分批将数据读出到内存,需要考虑内存增长问题。golang 的 GC 好像是2.5m 才做一次,此时内存会激增。考虑使用 对象池,或者主动调用 runtime.GC() 主动进行垃圾回收释放内存。
sql 查询时,尽量采用主键、索引,提高查询速率。分页批量查询也是必备的技能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值