Photo @  Akshar Dave

文  |  故知


背景——讲个真实的“鬼”故事


菜鸟 CTO 线研发效能团队开发了一个大促协同平台,来提高大家在处理大促相关工作时的协同效率,内部应用名称为 iwork 。某一天 iwork 应用突然发生了一个线上问题:

线上的一张核心数据表,所有的数据突然全都不见了。

删库跑路,是程序员的终极技能,新闻上也数次见过报道。小伙伴们虽然第一次在现实中遇到这种紧急情况,但是大家镇定地处理了这个问题。

首先在 DBA 的帮助下,导出 binlog 日志, 一起临时恢复了数据,对问题进行了止血。紧接着是要定位造成问题的原因。

第一反应是生产环境的代码执行了数据删除逻辑,但是近一段时间小伙伴们都在埋头开发新功能,生产环境有一段时间没有发布过了,如果生产环境代码有问题的话,早该暴露出来了。查看了一下 master 分支上面的代码,只有一个 delete 方法会删除对应表的数据,但是这个方法并没有在任何地方被调用。

如果不是生产环境代码的问题,还有什么可能性会导致数据删除呢?我们想到预发环境也有可能,因为预发和正式共用同一个数据库。这个时候一位小伙伴提供了一条关键信息,在预发环境的数据库操作日志中,找到了 delete 操作的执行记录,看来是预发环境的代码删除了数据。

本以为事情到这里已经快水落石出了,但是当我们去查看预发环境代码的时候,发现代码中也没有任何地方会对数据表进行删除,更加诡异的是,甚至 master 分支上的那个  delete 方法,在预发代码的分支上,已经被注释掉了。为什么要注释掉这段代码呢?小伙伴在日常开发的时候,遇到了数据偶有丢失的问题,但是由于日常环境经常不稳定,并且问题也没有复现,代码中也找不到调用该 delete 方法的地方,为了保险起见,就把整个 delete 方法注释掉了。

问题排查到这里陷入了困境,所有的线索都指向这个推论:

被注释掉的delete方法,不甘心自己的生命被终结,它决定向程序员复仇。在某个时间,它复活并运行了自己,把对应的数据表中的所有数据,所有数据都给删掉,然后跑路。

时值盛夏,突然觉得公司的空调开得有点凉。

破案——幽灵是如何产生的


没有办法根据代码顺藤摸瓜,我们只能转头去寻找更多的线索作为输入。内部鹰眼( eagleeye )系统提供了强大的链路查询功能,让我们可以查出数据删除操作相关的整个链路调用情况。

基于预发环境的数据库操作日志,我们查询了这段时间附近的调用链路,最终找到了问题的根本原因:最近新开发的一个数据同步功能,其代码引入了一个 Bug ,代码如下:

代码没太多可说的, query 对象没有处理查询字段为空的情况,导致 list 方法返回了全表对象,在后续的循环中调用了 delete 方法,删除了整张表的数据。

至此根本原因已经找到了,但是案子只破了一半,我们还没有回答幽灵代码是如何产生的。以上这段代码,是在一个分支 B 当中,但是当前预发环境中运行的是分支 A ,而分支 A ,如前文所述,是不包含这段问题代码的,甚至 delete 方法都是注释掉的。

其实产生幽灵的元凶,是当前 iwork 应用采用的分支模式:

由于 iwork 日常环境不稳定/不可用,大家常常用预发来做测试。预发上面部署的分支,其功能通常还没有开发完成,不能直接集成,于是为了测试自己的分支,常常需要把其它人的分支踢掉(利用 Aone 提供的退出预发功能),部署上自己的分支。越是临近项目发布阶段紧张关头,这种踢来踢去的情况就越普遍。

幽灵正是这样产生的,问题代码所在的分支 B ,之前在预发环境部署运行过,现在被分支 A 踢掉了。分支 A 上运行着健康的代码,让我们误以为幽灵的存在。

需要指出的是,如果分支 A 和分支 B 能够尽早集成的话,是可以发现合并冲突的。并且,虽然当前菜鸟已经强制对所有应用进行代码审核卡点,分支 B 的代码并没有经过代码审核,就部署上了预发,因为大家都通常会等到功能完全开发验证完成,发布正式的时候才提交代码审核。

在团队内部事后的复盘中,提出了一系列方案,来防止删库跑路的问题:

  • 采用更加严格的 CodeReview 。

  • 代码中注意对边界值和异常值的处理,通过测试用例覆盖这些情况。

  • 代码中使用逻辑删除而非物理删除,这样至少数据恢复的时候可以更快。

删库跑路是客观确定的问题,通过一系列手段总是可以预防/恢复的。本文想讨论的,是更加棘手的幽灵代码的问题。

反思——如何避免幽灵代码


如果不从根本上解决问题,幽灵代码会一次次来敲你的门,可能是白天,也可能是午夜。

既然元凶是分支模式,那就让我们来聊聊各种分支模式。

AoneFlow

AoneFlow 就是日常工作中大家所说的分支模式,是Aone应用开发的默认模式,菜鸟大部分应用都是采用的这种模式,上文中也贴出了页面截图,大家应该都比较熟悉。

关于 AoneFlow 的详细描述,可以参考:

https://help.aliyun.com/document_detail/59315.html

用一张图来描述:

简单来说,AoneFlow分别给日常、预发和生产环境,建立了独立的发布分支(release branch)。实际开发时,自由地组合特性分支(feature branch),与各自环境的发布分支做集成。

核心特性

AoneFlow的核心特性在于灵活的特性分支:

  • 分支之间可以灵活组合。可以选择当前环境中的任意一个或几个分支进行组合,以便灵活地选择所需的功能集合进行集成和验证。

  • 分支可以灵活地进入和退出。可以选择所需的分支集成到相应环境,也可以将某个不需要的分支踢出当前环境,这个功能可以很方便的排除问题分支带来的干扰。

  • 分支可以灵活地选择阶段。可以自由地选择日常、预发或者生产环境来集成自己的分支,理论上可以跳过日常和预发直接部署生产,这在紧急发布代码修复问题时十分高效。

好处

灵活性的好处:开发者可以自由地选择所需的功能集合,并且可以对紧急问题修复作出迅速响应。

特性分支的好处:这里的好处是特性分支自带的,它可以让开发者专注于自己的功能不受干扰,不受干扰地工作对程序员的诱惑是巨大的。

坏处

灵活性的坏处:
  • 分支之间可以灵活组合。任选一个或多个分支进行组合,对于N个分支来说,最坏情况下会有2^N规模的复杂度,沟通成本非常高,导致大家在实际开发的过程中,只愿意关注自己的分支,而不愿意去考虑别人的分支。

  • 分支可以灵活地进入和退出。本意是可以灵活地退出问题分支,很容易演变成退出所有别人的分支,只保留自己的分支以便做测试,于是出现了争抢预发环境的问题,影响团队效率和氛围。

  • 分支可以灵活地选择阶段。本意是可以高效地处理紧急情况,很容易演变成把常规情况当做紧急情况来处理。开发一期项目,业务方很急、产品经理很急、项目经理很急,所以程序员也很急,常常跳过日常直接部署预发,导致日常环境质量很差,基本处于不可用状态。一旦一条链路上,某个应用的日常环境不可用,其它应用都会受到影响,于是其它应用也没有动力维护自己日常环境的质量,于是破窗效应开始发生,最后演变成大家都拿预发环境作为测试环境使用。

特性分支的坏处,这里的坏处也是特性分支与生俱来的,参考:

https://fire.ci/blog/why-you-should-not-use-feature-branches/

  • 推迟了合并的发生。虽然不受干扰地在特性分支上开发很开心,但是合并终归是要做的。延迟满足可以放大满足,延迟痛苦也会放大痛苦。合并发生地越晚,做起来就越痛,痛到无法承受,深陷merge hell而无法自拔。合并之后的代码,如果在运行时遇到了问题,都不知道是谁造成的,想骂人都不知道骂谁,如同往天空中丢斧头一样无力。于是大家都不愿意做合并,导致恶性循环,应用质量越来越差。

  • 推迟了CodeReview发生。CodeReview越早越好,最好代码push之前就触发强制的CodeReview,如果等到部署了预发环境还没有做CodeReview,就很容易造成日常和预发环境质量变差,甚至遇到上文提到的线上问题。但是在特性分支模式下,大家倾向于等到功能全部完成再提交CodeReview。除了Review时间晚之外,Review的工作量也是个问题,按每个特性分支开发两周估算,两周之内菜鸟人均提交约3000行代码,一次性review这么多代码,谁也不能保证看得足够仔细。

  • 让代码重构变得困难。别人在一个特性分支上开发功能,你在另一个特性分支上做重构,就像别人在上面盖屋顶,你在下面砸墙一样,到最后合并的时候两个人同时傻眼。

总体评价

灵活的特性分支,让犯错变得容易,发布变得困难,质量也难以保障。

Git/Github Flow

Git/Github Flow 是开源世界普遍使用的分支模式, Aone 也提供了很好的支持。在 Aone ,开启持续交付功能之后,默认使用的就是 git-flow 模式。

Git Flow 模式常用在企业级项目和大型的开源项目管理,分支模型包含了 Master Branch、Develop Branch、Feature Branch、Release Branch、HotFix Branch ,比较复杂但很好地满足了大型项目的管理要求。

GitHub Flow 对 Git Flow 做了精简,只保留了 Master Branch 和 Feature Branch ,大部分开源项目都使用这种分支模式。

核心特性

Git/GitHub Flow 避免了 Aone Flow 的灵活分支组合,但本质上仍是基于特性分支的模式,继承了特性分支的好处和坏处。

总体评价

采用Git/GitHub Flow 的团队,虽然特性分支的坏处无法完全避免,但避免了AoneFlow灵活分支组合带来的问题,通过一些规范约束,结合 Aone 的持续交付功能,可以得到较好的应用质量。

主干模式(Trunk-based development, TBD)

主干模式是所有开发者在同一主干上进行协作的分支模式,禁止其它长生命周期的开发分支存在,以避免集成地狱( merge hell ),其形式如下如所示:

主干模式:

https://trunkbaseddevelopment.com/

核心特性

主干分支的核心特性在于清晰简单,并且主干永远保持可发布状态。为了达到这样的特性,需要相应的持续交付实践配合。

在上古时期, git 还未诞生的时代,人们使用的是诸如 SVN、Perforce、Clearcase 这样的中央式版本管理系统( Centralized Version Control System ),和 git 不同,它们不具备灵活的分支管理和强大的分支合并功能,所以那时除了主干模式别无其他选择,但是由于当时并没有 Jenkins、Docker 之类的 DevOps 基础设施,持续交付实践也不成熟,所以主干长期处于不可发布的状态,主干模式在大家的灵魂深处留下了痛苦的记忆。

没有配合持续交付实践的主干模式,很容易阻塞整体的开发流程,带来的破坏远远大于收益。配合持续交付之后,主干模式会进入另一重天地。

持续交付

原子提交:保证提交的原子性和细粒度,只使用 git rebase 来合并代码。这样不仅可以产出清晰的提交记录,并且遇到问题的时候也能迅速定位,迅速撤销提交( git revert )。
特性开关( Feature Flag ):开发过程中常常遇到这样的情况,一期项目中的一个功能点,已经开发完成满足了发布要求,但是还不可以直接交付给用户,此时为了使主干始终保持可发布状态,我们需要使用特性开关把功能点对用户"隐藏"。

特性开关:

https://martinfowler.com/bliki/FeatureToggle.html

特性开关实现起来比较简单。如果是 web 应用,可以在前端通过 URL 控制,不在页面上暴露链接入口。也可以使用动态配置,通过配置管理中间件来启用/禁用功能。麻烦一点的做法,可以在代码入口处增加 if/else 逻辑,等功能正式上线后再删掉。

快速编译:持续交付提倡迅速反馈,为了在代码提交之后获得高效的反馈,我们需要让应用能够快速完成编译。

从架构上来说,可以把大应用拆分成微服务,让每个服务独立快速编译。对于单个应用,可以通过减少未使用 jar 包的方式提高编译速度。同时, Aone 也提供了热部署功能,通过热部署而非重新编译的方式,大大降低打包和启动的时间。

代码门禁:代码门禁是应用质量的第一道关卡,只有通过了 CodeReview 、规约检查、冒烟测试/单元测试的代码,才允许集成到主干。

对于 CodeReview 来说,要求团队成员完成一个小功能的开发验证之后,立刻 push 代码并创建 CodeReview 请求。这样做不仅可以避免自己的代码和别人的代码混合在一起 review ,也可以有效控制每次需要 review 的代码量,保证 review 质量。

持续测试:基于测试金字塔,通过单元测试、集成测试、链路测试来保证应用质量。一开始可以把测试作为一个异步检查的过程,测试出错发送相应修复通知,但不阻塞整体流程,后续待测试足够可靠之后,可以把测试检查加入流程卡点。

团队需要树立良好的测试理念:测试用例不是用来发现问题的,而是用来证明软件确实如预期那般可靠工作。

紧急发布代码

Aone Flow 对紧急代码发布做了非常灵活的支持,可以跳过日常和预发直接部署正式,那么主干模式如何实现同样的功能呢?回答是做不到也不允许。

事实上做好了主干模式,通过不断提高应用质量防患于未然,紧急的线上问题是可以被扼杀在摇篮之中的。退一步说,真的遇到了紧急线上问题需要恢复,我们也该优先采用代码回滚、修改配置项(比如特性开关)这样的非代码方式。再退一步说,如果一定要通过提交代码的方式修复紧急问题,需要考虑的不是在 CodeReview 、日常及预发的部署验证环节节省时间,而是应该思考如何更加迅速地定位问题,因为大部分时间往往是花在问题定位上。

团队案例

除了严格的单主干,一种常见的变种策略是多主干模式,我们团队的 iwatch 应用采用的是双主干模式:

其中:

1、 shadow 分支。可以看作 develop 分支的一个镜像,我们引入 shadow 分支,是为了适配 Aone 的代码审核功能。每次 push 代码到 shadow 分支之后,提交代码审核请求,系统基于 shadow 和 develop 分支之间的 diff 创建代码审核页面。通过代码审核及其他代码门禁检查的代码,将自动合并到 develop 分支。

2、 develop 分支。Aone 持续交付功能之后,默认侦听的是 develop 分支,一旦 develop 分支上有了新的提交,就会触发持续交付流水线,依次在流水线的各个环境中进行打包编译、启动应用以及测试验证。从预发环境到正式环境,依然需要人工验证之后再手动进行发布,所以我们是做到了持续交付而没有做到持续部署。实践中我们在 git 源码库将 develop 分支设置为 protected ,禁止开发人员直接 push 代码到 develop 。

3、master 分支。代码发布到正式环境之后,会自动向 master 分支写入基线,开发人员对此无需感知。

日常的开发流程简单明了,我们约法三章:

1、所有的代码提交都在 shadow 分支,所有的集成和发布都在 develop 分支, master 分支作为基线。

2、原子性提交,降低粒度。每次提交之后,立即创建合并请求,触发 code review 。

3、提交的代码,遇到了任何 code review、merge、应用启动、单元测试、集成测试方面的问题请立即修复( Aone 会发工作通知),保证应用永远处于可发布状态。

为了打造平等开放的 CodeReview 文化, code reviewer 需要添加所有相关的开发同学,而不只是指定团队内更资深更高层级的同学作为 reviewer 。鼓励团队内所有同学主动担当 code review 的责任,对代码提出自己的建议。代码面前无层级, JVM 不会因为是高P的代码就不抛空指针,当代码穿越磁盘站在程序计数器面前的时候,它们是平等的。

iwatch 是一个普通的 java web 应用,3~4 人在同时开发,已经持续运行了四年多的时间。两年多已前刚加入到这个应用的时候,当时使用的也是 Aone Flow ,后来升级到 git-flow ,并且在持续交付方面不断进行积累,在一年多之前升级到了现在的主干模式,此后至今应用未发生过大的线上问题,并且在两个月之前刚刚完成了一次大重构(修改超过 90% 文件)。统计了一下部署数据,近两周平均每天集成 9 次,发布 0.7 次。

总体评价

流畅的开发体验和高质量的应用,会在团队内部沉淀出良好的技术氛围与技术文化,氛围文化又会反过来进一步提促进开发体验和应用质量的提升,形成正向循环。

一个常见的说法,主干模式只适合小规模团队,但是谷歌和Facebook实践证明,大规模开发团队一样可以把主干模式做得很好。

谷歌:

http://www.ruanyifeng.com/blog/2016/07/google-monolithic-source-repository.html?20160703175643#comment-last

Facebook:

https://paulhammant.com/2013/03/04/facebook-tbd/

如果 iwork 应用也采用了主干分支模式,那么开头的幽灵代码问题,至少有三次挽救的机会:通过代码合并之前的 CodeReview 发现代码中的问题;通过自动化测试发现边界值异常;在主干代码上快速定位到问题代码。

结语——遇见更好的分支模式


什么是好的分支模式

好的分支模式/开发流程,要让做正确的事情很容易,做错误的很难,而不是在功能灵活的旗帜下,让做正确的事情很容易,错误的事情更容易。

好的分支模式/开发流程,需要约束人性的灰暗面,放大人性的光明面。人们喜欢沉浸于当下的舒适,倾向于延迟未来的痛苦,所以需要把大家的工作约束在主干,迫使集成更早更频繁地发生。人们希望自己的工作在团队内部创造更好的价值,而非给别人带来麻烦,所以需要鼓励大家频繁细粒度地提交代码,避免给别人带来的不必要的干扰。

逃出分支模式的肖申克

人生而自由,却永陷枷锁,每个人心中都有一座肖申克,详情参考:

http://mindhacks.cn/2009/01/18/escape-from-your-shawshank-part1/

常常听说“没有最好的分支模式,只有最合适团队的分支模式”。很多时候,当前的分支模式只是最适合团队现状的模式;不是分支模式最适合团队,只是团队适应了这种分支模式而不愿逃离。

希望每个团队的开发活动,都可以成为童话故事,而非鬼故事~

本文作者:

杨越,花名故知,菜鸟研发中台技术部-研发支撑部技术专家,目前主要关注大数据处理、自动化测试平台、工程效能平台等相关技术方向。

Hot

近期热文阅读推荐

1、《我在阿里做中后台开发》 -- 牧瞳

2、《那些年,我们见过的 Java 服务端乱象》-- 常意

3、《阿里巴巴在开源压测工具 JMeter 上的实践和优化》--  韩勇

4、《研发运维效率提升100%,机器成本下降50%,阿里巴巴在 Serverless 计算领域的探索》-- 誓嘉

Tips:

# 点下“在看”❤️

# 然后,公众号对话框内发送“浴巾”,试试手气?????

# 本期奖品是来自淘宝心选的精梳棉柔软素色浴巾 。

幽灵代码删库跑路引发的分支模式思考相关推荐

  1. 公司拖欠工资,删代码删库跑路,违法么?

    (给程序员的那些事加星标) 综合整理:程序员的那些事(ID:iProgrammer) 最近有个程序员在网上发帖求助: 公司拖欠了两个月的工资,现在想把这两个月敲的代码都删掉,然后删库跑路,这么做违法吗 ...

  2. 代码内容变成图片_网站只剩25行代码,真有程序员“删库跑路”?

    "可能我删了数据才会轻松吧 可能我格了硬盘才会休息吧 可能逃出了公司我就跑到天边 可能我还没找到 那个BUG--" 曾经,有一位程序员,在某个加班的深夜,一边听着<可能否&g ...

  3. 程序员因企业拖欠两月工资,想把代码删掉走人,网友:删库跑路?

    现在互联网企业的寒冬来了,很多企业都出现了裁员的情况,但是公司拖欠工资又是什么鬼,就有一程序员在网上吐槽,公司拖欠了两个月的工资,然后该程序员表示想把这两个月的代码删掉,在上演真实版删库跑路,这么做违 ...

  4. 狠!删库跑路!一行代码蒸发10亿人民币!

    年后复工大戏,又增加一出:删库跑路! 此举直接给公司带来数10亿的市值蒸发损失! 这次不是别人,正是微信生态的第三方服务商微盟,在这个"远程办公"的节骨眼出事了. 2月25日,微盟 ...

  5. 删库跑路大神的一生:曾在家造炸弹被捕,现卖房押宝NFT,原是开源创业之星...

    梦晨 萧箫 发自 凹非寺 量子位 报道 | 公众号 QbitAI "著名开源项目被作者本人删了库"这个瓜,现在是越吃越大了. 「faker.js」和「color.js」的作者Mar ...

  6. 运维进化论:微盟“删库跑路”给我们的启示

    作者:茹炳晟,软件质量和研发工程效能专家 事件背景 微盟是国内移动互联网营销引领者,中国最大的微信公众智能服务平台,基于微信为企业提供开发.运营.培训.推广一体化解决方案,帮助企业实现线上线下互通,社 ...

  7. 真实版“删库跑路”?程序员蓄意破坏线上生产环境!

    作者 | 阿文 责编 | 伍杏玲 出品 | 程序人生(ID:coder_life) 作为程序员经常相互开玩笑说,公司要是把我逼急了,大不了我们"删库跑路",这是一句玩笑话,没想到会 ...

  8. 删库跑路、“投毒”、改协议,开源有哪几大红线千万不能踩?

    作者 | 彭慧中 责编 | 屠敏 出品 | CSDN(ID:CSDNnews) 开源协议是开源世界里的根基.根据CSDN<2021-2022 中国开发者调查报告>数据显示,尽管目前已有94 ...

  9. 真实版“删库跑路”?程序员蓄意破坏线上生产环境

    作者 | 阿文 责编 | 伍杏玲 出品 | 程序人生(ID:coder_life) 作为程序员经常相互开玩笑说,公司要是把我逼急了,大不了我们"删库跑路",这是一句玩笑话,没想到会 ...

最新文章

  1. Python+OpenCV 图像处理系列(1)— Ubuntu OpenCV安装、图片加载、显示和保存
  2. 解决multiple definition of的方法
  3. mysql去除select换行符_MySQL中去除字段中的回车符和换行符
  4. jquery 获取Input 值
  5. Lab_2 OSPF
  6. Python 面向对象与 C++、Java 的异同
  7. 解决:Unable to open debugger port (127.0.0.1:55017): java.net.SocketException “Socket closed“
  8. 看看80万程序员怎么评论:前端程序员会不会失业?
  9. 多址接入技术TDMA CDMA FDMA Aloha等
  10. python画相关性可视化图_Python可视化16matplotlibseborn-相关性热图(correlation heatmap)...
  11. 好的安排小明(南阳19)(DFS)
  12. 三天两夜肝完这篇万字长文,终于拿下了 TCP/IP
  13. selinium如何多线程_求教个selenium+grid+testng多线程运行的问题
  14. [网络安全自学篇] 八十四.《Windows黑客编程技术详解》之VS环境配置、基础知识及DLL延迟加载详解(1)
  15. Ubuntu Python3安装
  16. 计算机联锁系统硬件结构,计算机联锁系统各部硬件.ppt
  17. Android Media Framework(3): Stagefright框架流程解读
  18. jeecg-boot环境搭建
  19. 完完整整地看完这个故事,你敢说还不懂Docker?
  20. PCB常用端子/排线

热门文章

  1. 简单的前端网页分享功能怎么做?
  2. Android进程间通信的几种方式
  3. 请实现一个算法,确定一个字符串的所有字符是否全都不同。这里我们要求不允许使用额外的存储结构。 给定一个string iniString,请返回一个bool值,True代表所有字符全都不同,False代
  4. 线性判别分析(Linear Discriminant Analysis, LDA)(含类内散度矩阵 类间散度矩阵 全局散度矩阵推导
  5. ppt模板怎样用到html中,PPT如何使用在线模板制作精美页面
  6. Team CIMAR’s NaviGATOR: An Unmanned Ground Vehicle for the 2005 DARPA Grand Challenge
  7. VR这么火,你选UE4还是Unity 3D? 1
  8. iOS开发之SceneKit框架--SCNView.h
  9. java ssj_SSJ中相关配置文件的写法DEMO
  10. TDengine Taos数据库同步服务