写点什么

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:563141

评论

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

阿里面试,一面就倒在了Java内存模型上?赶紧来看看

Seven七哥

面试 Java并发 内存模型

极客父母送给孩子的 ABC Book 就是这么 GEEK

魏彬(rockybean)

GEEK BOOK

国内10大前端团队网站

bigezhang

技术 大前端

Fire Fast 再深一层的是什么?

树上

管理 考核 Fire Hire 用人

给业务线的总经理多交代了几句

霍太稳@极客邦科技

创业 效率 团队管理

媒体的经营 01 | 媒体/内容行业投资分析的维度

邓瑞恒Ryan

创业 内容 重新理解创业 媒体 投资

公司大了,人多事杂,如何落地项目制?

树上

项目制 落地 公司管理 业务线 考核

当我们在说5G网络安全的时候,究竟在说什么?

石君

5G 5G网络安全 5G安全 网络安全

一个值得推荐的人才测量标准

Selina

关于Iterator和Iterable

shengjk1

Java Iterator和Iterable

三点思考,判断一家公司是否值得加入

邓瑞恒Ryan

高效工作 个人成长 职业

像黑客一样思考

Fooying

黑客思维 黑客 安全攻防

回“疫”录:开篇

小天同学

疫情 回忆录 现实纪录 纪实

我从来不在朋友圈晒投资人合影,却融了很多钱

邓瑞恒Ryan

高效工作 人脉 职业规划

复用到何种程度

孙苏勇

Java 程序设计 复用 面向对象 抽象

如果明天没有恐惧——两小时看完余欢水后想到的……

伯薇

个人成长 心理学 小说 恐惧

一篇文章搞定 java 中的 path 和 classpath

shengjk1

Java classpath vs path classpath path

Arduino 蓝牙遥控+超声避障小车

黄耗子皮

树莓派 极客

你不必读完一本书

池建强

学习 读书

Java中的Stream用还是不用

孙苏勇

Java 流计算 程序设计 性能

机房运维需要了解东西

Spider man

Windows环境MySql8.0忘记root密码重置

玏佾

MySQL

Flink获取kafka中每条消息对应的topic

shengjk1

flink kafka flink 消费 kafka 获取 topic等信息

2020,这个世界会好吗?

IT民工大叔

读书笔记

破解 Java Agent 探针黑科技!

谭建

Java JVMTI APM Profile

Scrum vs Kanban,如何选择

TerryLee

Scrum Kanban 敏捷开发 Worktile 研发管理

一文搞定 equals 和 hashCode

shengjk1

Java equals vs hashcode

死磕Java并发编程(1):探究Java并发机制的底层原理

Seven七哥

Java Java并发 并发编程

聊聊:Java

谢烟客

Java 编程 开发者 随笔杂谈 「Java 25周年」

Idea工程启动时报错:Command line is too long

玏佾

intellij-idea

程序员陪娃看绘本之启示

孙苏勇

程序员 生活 读书 成长 陪伴

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