HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

30 万行代码的平台升级:给跑着的汽车换轮胎

  • 2021-03-22
  • 本文字数:5091 字

    阅读完需:约 17 分钟

30万行代码的平台升级:给跑着的汽车换轮胎

本文最初发布于 Mahmoud Hashemi 的个人博客,经原作者授权由 InfoQ 中文站翻译并分享。


2020 年可谓反复无常。尽管一切都超出了人们的控制,但随着时间的推移,我发现自己把越来越多的时间地投入到一件感觉唾手可及的事情中:为我帮助构建的大型企业级 Web 应用程序SimpleLegal设计一个面向未来的解决方案。


现在已经完成了,这次平台升级很容易就可以在我最复杂的项目中名列前茅,此时此刻,最幸福的结局。幸福是要付出代价的,但是借助一些恰当的方法,代价可能不会像你想的那么高。

概述

我们将SimpleLegal的主要产品,一个 30 万行的 Django-1.11-Python 2.7-Redis-Postgres-10 代码库,移植到 Django 2.2-Python 3.8-Postgres-12 技术栈,如期完成,而且没有发生重大站点事件。这感觉很棒。


作为这个项目的技术主管,它看起来是什么样子?对我来说,是这样的:



但作为工程总监,它的成本是多少?3.5 年的开发时间,每行代码只需要 2 美元。


我对这个结果感到特别自豪,因为在这个过程中,我们也大大提高了网站和开发过程本身的速度和可靠性。现在,该产品有了一个光明的未来,已经准备好在销售征求建议书和合规调查问卷上大放异彩了。最重要的是,你不必担心怎样委婉地告诉潜在客户,他们将使用的是不受支持的技术。


简而言之,这是一笔巨大的、稳健的投资,而且已经取得了回报。如果你来这里只是为了看看我们自己对这项工作的估计,那就是上面这些了。这篇文章是介绍如何让你的团队达到同样的结果。


背景

故事开始于 2013 年,刚刚从 YC 孵化出来的 SimpleLegal 为一家新成立的 SaaS 法律技术公司做了所有正确的决定:Python、Django、Postgres 和 Redis。在典型的初创公司模式中,在技术不成障碍的情况下,功能是第一位的。软件包只是顺带升级。


到 2019 年,这条技术跑道的终点已经临近。虽然 Python 2 可能得到了来自不同供应商的扩展支持,但在 2021 年,Django 1 CVE 补丁的志愿者已经非常少了。Web 框架成了风险较大的攻击面,所以是时候偿还我们的技术债务了。


开端

因此,我们在 2019 年第 4 季度开始了 Tech Refresh 平台升级计划。其目标是:升级技术栈,同时仍然提供新特性,就像给跑着的汽车换轮胎。我们要小心谨慎,而那需要时间。以下是一些长期项目的基本原则:

  1. 任何每周工作 10 小时以上的项目都应该每周花 30 分钟进行同步。

  2. 每次定期会议都应该有记录。把它放在邀请函里。使用项目日志记录进度、阻碍因素和决策。

  3. 这是一场马拉松,不是短跑。要避免在晚上、周末和假期工作。


我们从一个计划草图开始,经过开放地讨论,最终只有一半正确。有一些早期的猜测成功实现:

  1. 转到pip-tools,并根据广泛的变更日志分析解除依赖关系。识别不兼容 py23 版本的包。(尽管我们已经转向poetry。)

  2. 在 CI 中加入行覆盖率报告。

  3. 改进内部测试框架,让开发者可以快速编写测试。


下面有更多相关内容。其他的计划就不那么现实了:

  1. 在 6 个月内将 CI 行覆盖率从大约 60%提升到 95%。

  2. 在三个月内并行转换 app 程序包。

  3. 利用美国节假日(感恩节、圣诞节、新年)期间的低流量时间,在 2021 年之前逐步切换到新应用。


我们年轻!虽然我们天真,但至少我们知道有很多工作要做。为了分担这项工作,我们寻找、雇佣并培训了三名敬业的海外开发人员。


导向问题

即使新增了开发人员,到 2020 年中期,我们越来越认识到,95%的覆盖率就是在做梦,更不用说 100%了。全部覆盖可能是最佳实践,但 3 个半开发人员没法做到这样的覆盖范围。我们做了有价值的测试,甚至发现了以前的 Bug,但如果我们坚持这个计划,Django 2 最终将成为一个 2022 年的项目。70%,我们决定修改目标。


我们意识到,对于大多数站点来说,CI 比大多数用户更敏感。所以我们专注于测试影响最大的代码。怎么才算影响大?1)失败了最易被察觉的代码;2)最难重试的代码。通过查看流量统计数据、批处理作业计划和询问支持人员,你可以在一周内构建出高影响代码清单。


大约 80%的代码库都不在这个高流量/高影响列表中。那 80%该怎么办呢?利用错误检测和快速修复。


转换 Sentry 的角色



创业生活的一个好处是,尝试新工具很容易。我们在 SimpleLegal 所采用的一种做法是,把每 5 个周的最后一周(即 20%的时间)留给开发人员,让他们专注于开发过程本身。即使是最好的厨师也不能在脏乱的厨房里做出五星级的食物。这是我们改进工作的方法,最终加快了交付速度。


在这样一个时期,有人想出了一个天才的主意,使用Sentry将专门的错误报告添加到系统中。在一两天内,我们就有了一个网站,你可以访问并获取堆栈跟踪。这非常神奇,但直到 Tech Refresh 计划开始我们才意识到,虽然集成只需要一天的开发时间,但完全采用却需要团队几个月的时间。


你看,在一个成熟但快速运转的系统上增加 Sentry 意味着一件事:噪音。我们的网站一直在出错。大多数错误是不可见的,也没有妨碍用户使用,有些用户已经悄悄学会了如何处理长期存在的网站怪癖。很快,我们的开发人员就学会了把 Sentry 当作调试信息的存储库。2019 年,Sentry 事件本身并不值得认真对待。2020 年,情况发生了变化,负责将平台无缝升级的团队需要把 Sentry 变成另一种东西:响应性网站质量工具。


我们是怎么做到的呢?第一步,通过以下最佳实践增强流入 Sentry 的数据:

  1. 将产品拆分成单独的Sentry项目。这包括前端和后端。

  2. 标记版本。不要用分支来标记开发环境部署,这会导致 Releases UI 混乱。添加一个单独的分支标签用于搜索。

  3. 把环境分开。这对于定向报警至关重要。Sentry 客户端环境是通过域约定和 Django 的sites框架来配置的。为了便于理解,这里有一个基线,我们使用这些环境:

  4. 生产环境:当前正式版本。DevOps 监控。

  5. 沙箱环境:当前正式版本(部分公司会做下一次发布)。供用户测试变更使用。DevOps 监控。

  6. 演示/销售环境:上一个正式版本。主要是内部流量,但在前景演示时外部也可见。DevOps 监控。

  7. 金丝雀环境:下一个正式版本。也称为过渡环境。内部流量。Dev 监控。

  8. ProdQA 环境:当前正式版本。内部用于重现技术支持问题。Dev 监控。

  9. QA 环境:Dev 分支、dev 发布、内部流量。未监控调试数据。

  10. 本地测试/CI 环境:默认不发布到 Sentry。


当问题最终被正确标记并且可以搜索之后,我们使用 Sentry 新增的Discover工具每周导出问题,并对遗留错误进行优先级排序。我们首先关注的是对于非内部人类用户高可见的生产错误。具体查询是:has:user !transaction:/api/* event.type:error !user.username:*@simplelegal.*


我们将其分为 4 类:快速修复(小漏洞)、快速错误(将一个含糊的 500 错误转变成某种形式的可操作的 400 错误)、Spike(比较大的漏洞,需要研究)和 Silence(使用 Sentry 的忽略功能)。在 6 周的时间里,每周事件量由每周超过 2500 次下降到了不到 500 次。


通过进一步的努力,每周的事件量已经少于 100 次,并且分散在几个问题上,对于一个精益团队来说,这非常容易管理。虽然“Sentry Zero”是最理想的,但我们实现并维持了响应流的真正目标,这在很大程度上要归功于Slack集成。我们的团队不再从支持团队那里获取服务器错误信息。事实上,现在,当客户遇到麻烦时,我们会告诉他们,而我们已经有了一个处理中的工单。


和支持团队建立紧密的联系非常重要。在上面的策略中,我们嵌入了比真实用户更敏感的 CI。虽然完美很诱人,但要求企业用户有一点耐心也是可以的,前提是支持团队已经做好了准备。每周都和他们同步,这样惊喜就少了。如果他们干劲十足,你也可以教他们一些 Sentry 基础知识。


新征程



随着噪音的消除,我们已准备好快速行动。以下是我们在做出这些改变时积累的一些经验。


诉诸事务

如果使用得当,回滚可以使错误看起来像从未发生过,这是快速修复策略的完美补充。


真正的原子请求

把操作尽可能地放入事务中。打开ATOMIC_REQUESTS(如果没打开的话)。但是,有些请求所做的不仅仅是更改数据库,比如它们会发送通知,将后台任务入队。


在 SimpleLegal,我们重新设计了架构,将所有副作用(除了日志记录)推迟到成功返回响应时。中间件可以提供帮助,但我们主要是通过将 Redis 队列切换到基于 PostgreSQL 的任务队列/代理来实现的。这种配置可以确保,如果发生错误,事务将被回滚,任务不会进入队列,用户将得到一个干净的失败。我们在 Sentry 中定位故障,切换到旧站点进行消除,他们下一次重试就会成功。


事务性测试设置

事实证明,事务性对我们的测试策略来说也很关键。SimpleLegal 早已超过了 Django 原始的 fixture 系统。大多数测试都需要复杂的 Python 设置,这使得编写测试和运行测试都很慢。为了加快编写和运行的速度,我们将整个测试会话封装到一个事务中,然后,在运行任何测试用例之前,我们设置了示例性的基本状态。测试用例使用这些基本状态作为fixture,并在每个测试用例之后回滚到基本状态。详情请参阅contest.py摘录


有些最佳实践并不适合你

软件场景的差别如此之大,知道哪些建议不适合你是一门艺术。以下是我们亲身了解到的各种死胡同。


命名空间的运用

考虑到代码被划分成模块、包、Django 应用等的方式,把它们作为工作单元可能很有诱惑力。开始时不要这样。代码划分可能非常随意,很难知道你何时就进入了一个有风险的思路。


假如有自动重构,就像在2to3转换中一样,首先要按转换类型进行移植。这样,你只需要查看一个命令和受影响的路径列表。另外,自动修复必须遵循一种模式,这意味着更多的人可以修复重构导致的错误。


覆盖率工具



覆盖率对我们来说是好坏参半。显然,覆盖率优先策略是站不住脚的,但对优先级划分和状态检查,它仍然有用。就单次变更来说,我们发现覆盖率工具有些不可靠。我们从来没有弄清楚为什么覆盖率的作用有不确定性,我们得出了这样的结论:“像 codecov 这样的现成工具可能并不是针对我们这种规模的 monorepos。”


在撞上覆盖率墙的过程中,我们研究了其他许多关于覆盖率的解释。对我们来说,“路由覆盖”(即每个 URL 至少有一个集成测试)和“模型表示覆盖”(即每个模型对象都有一个有用的文本表示,可以用于 Sentry 调试)比行覆盖优先级高得多。如果有更多的时间,我们会希望围绕这些构建工具,甚至是围绕基于在线分析的覆盖率统计,从而优先考虑流量最高的路由,而不仅仅是流量最高的代码行。如果你听说过这些方法,我们很想和你讨论一下。


扁平化数据库迁移

从表面上看,减少需要升级的文件数量似乎是合理的。事实证明,扁平化迁移是一种消除文件的低收益策略。更改历史迁移文件结构会使上线过程变得复杂,而升级没有扁平化的迁移文件则很简单。更不用说,如果只是想要加速 CI,你可以像我们在Open edX平台上所做的那样:建立一个基本的DB缓存,每隔几个月检查一次


事实证明,你可以从开源应用程序中学到很多东西


慢慢适应新技术栈

如果你有多个应用程序,请使用相对比较小也比较简单的应用程序来试验更改。幸运的是,我们有一个独立的应用,它的测试运行速度更快,这让我们能够更紧凑地了解开发循环。同样地,如果你有多个生产环境,则从影响最小的一个环境开始推出。


把 CI 作业复制到新的技术栈中,它们都会失败,但要克制住把它们标记为可选项的冲动。相反,构建一个包含所有测试及其当前测试状态的单文件清单。我们为测试运行程序pytest构建了一个小扩展,它基于状态清单文件批量跳过测试。然后,ratchet:取消并修复测试,更新文件,检查测试是否通过,然后重复。这比遍布代码库的pytest标记装饰器更方便和可扫描。详情请参阅contest .py摘录


上线试运行

在 2020 年第四季度,我们增加了基础设施,以在相同的数据库支持下并行运行新旧站点。我们进入了这样一个循环,使流量到达新技术栈,构建一个需要修复的 Sentry 问题队列,然后关闭它,并跟踪时间。使用新技术栈大约 120 个小时后,经过昼夜不停地策略性扩展,组织已经建立起足够的信心,我们可以在最关键的时间让站点继续运行:在月初的周一和周二。


唯一的问题是AWS在感恩节周的宕机。此时我们已经提前完成了计划,并且对快速修复工作流建立起了足够的信心,不再需要最初的假日测试窗口。为此,我们感谢了很多人。


我们一直用快速修复的方法,直到我们完成。“完成”不是指新系统没有错误,而是指流量在新系统上时事件比旧系统少。然后,继续修复,并开始安排时间删除脚手架。



后记

所以,一旦你使用了 Django、Python、Linux 和 Postgres 当前的 LTS 版本,任务就完成了,对吧?


谢天谢地,技术债务从不会到 0。虽然按期更新并更换核心技术不是一件小事,但用闪亮的部件替换生锈的部件并不会改变设计。架构技术债务——抽象中的错误,包括缺乏抽象——可能会带来更大的挑战。这些问题的解决方案并不能在项目之间完全推广,但它们确实会受益于这个最新的、无错误的基础。


对于所有希望更换轮胎的项目,我们希望这次回顾能够帮助你在未来几年充满信心地、务实地改进技术栈。


查看英文原文:

Changing the Tires on a Moving Codebase

2021-03-22 18:563113

评论

发布
暂无评论
发现更多内容

RocketMQ一行代码造成大量消息发送失败

Java 程序员 后端

Redis(十八):服务器

Java 程序员 后端

Redis持久化方式AOF技术原理?一文带你从底层彻底吃透

Java 程序员 后端

redis数据迁移之redis-shake

Java 程序员 后端

RocketMQ 主从同步读写分离机制

Java 程序员 后端

RocketMQ消息丢失场景及解决办法

Java 程序员 后端

macOS 环境安装Flutter

坚果

flutter 11月日更 安装部署

Redis安装与部署新手入门教程

Java 程序员 后端

Redis小白入门教程

Java 程序员 后端

Redis(十一):键的生存时间与过期时间

Java 程序员 后端

RocketMQ 千锤百炼--哈啰在分布式消息治理和微服务治理中的实践

Java 程序员 后端

RocketMQ消息轨迹-设计篇

Java 程序员 后端

RocketMQ源码分析之NameServer

Java 程序员 后端

redis之单机多节点集群

Java 程序员 后端

Redis哨兵原理,我忍你很久了!

Java 程序员 后端

Redis哨兵模式原理剖析,监控、选主、通知客户端你真的懂了吗?

Java 程序员 后端

【Flutter 专题】12 图解圆形与权重/比例小尝试

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 11月日更

Servlet 入门

Java 程序员 后端

Redis的各种用途以及使用场景(1)

Java 程序员 后端

Redis(二十六):Sentinel—

Java 程序员 后端

SAP为Java 16贡献JEP 387 “弹性元空间”

Java 程序员 后端

Redis常用命令总结

Java 程序员 后端

Redis的各种用途以及使用场景

Java 程序员 后端

Serverless 如何在阿里巴巴实现规模化落地?

Java 程序员 后端

Redis(十六):事件

Java 程序员 后端

RocketMQ msgId与offsetMsgId释疑(实战篇)

Java 程序员 后端

RocketMQ消息丢失场景及解决办法(1)

Java 程序员 后端

Spring @Lookup实现单例bean依赖注入原型bean

Java 程序员 后端

Redis(四):整数集合

Java 程序员 后端

Redis实现feed流(1)

Java 程序员 后端

Redis实现feed流

Java 程序员 后端

30万行代码的平台升级:给跑着的汽车换轮胎_架构_Mahmoud Hashemi_InfoQ精选文章