那些年发版上线的神操作
翻了翻日志,发现居然有2年没更新了,不知不觉距离上次跳槽到某力已经2年,从一开始的各种加班到如今按部就班,从遇到线上问题抓耳挠腮到现在的宠辱不惊,从接手新需求的茫然到系统分析会上的挥斥方遒,其间也成长了一些, 包括mysql,redis,mongo,solr 等,公司采用App + 管理后台结合快速迭代的模式(java微服务接口+net后台/接口管理+APP/钉钉应用开发),所以一个月会有一个大版本更新,为了不影响用户体验,一般采用在线发版,发版在10点之后,发版如果过于“顺利”,能从晚上10点一直搞到第二天10点(本人就有幸经历过),如果是大版本更新,正常也要到夜里12点以后,我们的功能要经历 本地》 测试》预发布》线上 四个环境,所以原则上发版不会有太大的问题,原则之外还是会发生七七八八的事情,有些值得记录一二, 话不多说,今天先分享一些。
示例一: 旧数据升级,事发凌晨12点,脚本如下,升级后,测试人员在正式环境发现部分数据无效,查询线上数据库,发现只有部分字段更新(type\route_name),而route_id 等字段未更新, 当时交接的人员武断的以为脚本未执行,选择再次执行升级
结果可想而知,执行后线上异常数据不减反增,阿里云日志上能看到脚本的执行记录double,当时仍未排查出原因,针对这类问题,还是在脚本本身,仔细分析发现, 执行的是多笔更新脚本,而不同脚本之间又存在依赖关系,如第一条脚本的条件 作为后续脚本的更新字段,所以就很明显,脚本是一次性的,若重复执行,第二次只会执行第一笔脚本,后面的脚本都不会运行,而该脚本会覆盖掉第一次执行的2,3,4条脚本,造成的后果就是凡是2,3,4 脚本执行的数据全部都被覆盖掉,线上这么多租户,每个租户又有大量的数据,想到这些简直是脑壳疼,当务之急只能是先行修复线上数据,于是乎修复工具+变更记录 各种神操作手段一一上演。当然问题产生的原因以及处理方式也值得深究
1,脚本都是jenkins 批量升级,不可能单独漏掉
2,脚本的执行记录,阿里云上都可查,发现问题后为何没有优先去查服务器执行记录,以及去分析异常数据,而是武断认为未执行
3,手动执行脚本前没有审查脚本是否可重复执行,
其实如果认真分析,问题产生的原因不难找出,可能是身体在凌晨时早已身心俱疲,脑子不清晰,反应迟缓,在测试第一时间反馈异常数据时,研发可以自行校验,比较异常数据的产生时间和数据库升级的时间进行对比,不难得出,这些异常数据全部都是在数据库升级期间执行,所以升级数据并未执行到位导致, 而脚本升级很快,升级完后可以直接在对这些数据进行点对点处理,或者说这些数据是测试人员方便测试而在特定的时间段处理,记录极少,这是其一,同样在脚本升级时,还需要考虑是否支持重复执行,而不是武断的执行这是其二,临近发版,提交代码的风险是直线上升的,需要格外审查。
还有更离谱的
有个小可爱跳过预发布环境直接把脚本上到正式环境(审核机制漏洞),导致线上瞬间所有的人脸识别瘫痪,一群人大半夜的回滚数据,并且受到总监以下各级领导的“贴心交流”。
此外还有:
1. 菜单脚本漏提交,用户账号登录后显示菜单缺失
2. 权限缓存未执行,脚本升级上去,但是对应的缓存未及时刷新,导致上线后用户针对调整功能无权限
3. 小问题优化未考虑到影响范围,导致上线后各种连锁反应产生(拧水管效应)
4. 代码被其他小可爱覆盖,导致产生新问题/旧问题复现
这些都是相对而言血的教训,还有一些鸡毛蒜皮的问题更是数不胜数,什么多语言问题,UI不兼容,旧包不兼容,超时处理等等,都应该把这些统统列到新人指南上,避免踩坑,而不是靠老员工口耳相传,不过有些问题不是自己亲身体验就无法真正领略到其中的奥妙。
附录: 封装批量删缓存方法
/// <summary> /// 批量删除用户缓存key /// </summary> /// <param name="userIds">用户Id</param> /// <param name="bussessKey">业务名</param> ///<param name="delCount">批量移除数量</param> public void RemoveUserContentRedis(List<string> userIds, string tenantId, string bussessKey, int delCount = 200) { using (IRedisClient client = this.GetClient()) { var index = 0; //拼接租户:业务 key var bussKey = string.Format("{0}:{1}", tenantId, bussessKey); //200一组,获取组数 int aryLen = userIds.Count / delCount; for (int i = 0; i <= aryLen; i++) { //获取每200/组用户id 集合 var tempUserIdList = userIds.Skip(index).Take(delCount).ToList(); //拼接每组集合对应的业务key var listKey = new List<string>(); foreach (var item in tempUserIdList) { //拼接完整key 集合 eg: 租户:业务名: 用户Id listKey.Add(string.Format("{0}:{1}", bussKey, item)); } //批量删除每组key client.RemoveAll(listKey); index = (i + 1) * delCount; } } }