Sora公测之后,我们的视频生成系统流量暴涨,原来的代码彻底扛不住了。
说实话,原来的代码写得很烂。因为是早期快速迭代出来的,各种临时方案、硬编码、复制粘贴,能跑就行。Sora没公测的时候,用户少,问题还不明显。公测之后,用户量翻了几十倍,各种问题全暴露出来了:响应慢、经常崩溃、bug修不完、加个新功能要改好几天。
老板拍板:重构。给我们两周时间,把系统重写一遍。
这篇文章,我想分享一下这次代码重构的过程,聊聊我们是怎么把一堆烂代码,重构成优雅、可维护、高性能的系统的。
重构前的烂代码有多烂
先说说重构前的代码有多烂。
我们的系统是一个AI视频生成平台,用户输入文字描述,系统调用Sora API生成视频,然后处理、存储、返回给用户。
重构前的代码,主要有这么几个问题:
第一个问题是,所有逻辑都写在一个文件里。整个后端就是一个几千行的Python文件,API接口、业务逻辑、数据库操作、第三方API调用、视频处理,全混在一起。改一个地方,可能影响到其他地方,每次改代码都提心吊胆。
第二个问题是,硬编码满天飞。API Key、数据库地址、文件路径、超时时间、各种参数,全是硬编码在代码里的。换个环境就要改代码,而且经常忘了改某个地方,导致线上出问题。
第三个问题是,没有错误处理。调用Sora API失败了怎么办?视频处理出错了怎么办?数据库连接不上怎么办?这些都没有处理。出了问题,就是一个500错误,用户看到一片空白,我们也不知道哪里出了问题。
第四个问题是,复制粘贴严重。类似的逻辑,在不同的地方复制了好几份。比如,调用Sora API的代码,在三个地方都有,每份还不太一样。改一个bug,要改好几个地方,经常漏改。
第五个问题是,没有日志和监控。出了问题,只能靠猜。用户说生成失败了,我们不知道是API调用失败了,还是视频处理出错了,还是存储出问题了。排查问题全靠翻代码、加print、重新部署。
第六个问题是,性能差。视频生成是异步的,但我们的代码是同步等待的。一个请求要等几分钟,占用一个连接。并发一高,连接池就满了,新请求进不来。
第七个问题是,没有测试。整个项目一个测试都没有。改完代码,只能手动测几个场景,经常改出bug,上线之后才发现。
这样的代码,在用户量小的时候还能凑合,但Sora公测之后,用户量暴涨,这些问题全变成了灾难。
重构的目标和原则
开始重构之前,我们先明确了目标和原则。
重构的目标:
- 系统能支撑10倍于现在的并发量。
- 代码结构清晰,新人能快速上手。
- 有完善的错误处理和日志监控,出了问题能快速定位。
- 加新功能的效率提升50%以上。
- 有基本的测试覆盖,核心逻辑有单元测试。
重构的原则:
第一个原则是,不改变外部行为。重构是内部结构的优化,对外的API接口、功能、行为都不变。用户感知不到重构,只觉得系统变快了、变稳定了。
第二个原则是,小步快跑,逐步替换。不是一次性把所有代码都重写,而是分模块、分步骤地重构。每重构完一个模块,就部署上线,验证没问题了再继续下一个。这样风险小,出了问题也容易回滚。
第三个原则是,先搭骨架,再填肉。先把整体架构搭好,定义好模块之间的接口,然后逐个模块把旧代码迁移过来。这样,即使重构没做完,新架构也能跑起来。
第四个原则是,每一步都可验证。重构完一个模块,就跑测试、做对比验证,确保新代码和旧代码的行为一致。不能凭感觉说"应该没问题"。
第一步:分层架构设计
重构的第一步,是重新设计架构。
我们把原来的"大泥球"架构,改成了清晰的分层架构:
第一层是,接口层(API Layer)。负责接收HTTP请求,参数校验,返回响应。这一层很薄,不包含业务逻辑。
第二层是,业务层(Service Layer)。负责核心的业务逻辑,比如创建视频生成任务、查询任务状态、管理用户配额等。
第三层是,集成层(Integration Layer)。负责和外部系统的交互,比如调用Sora API、调用视频处理服务、操作对象存储。
第四层是,数据层(Data Layer)。负责数据库操作,封装所有的SQL和ORM调用。
第五层是,基础设施层(Infrastructure Layer)。负责配置管理、日志、监控、消息队列、缓存等通用设施。
每一层只能调用下一层,不能跨层调用,也不能反向调用。这样,各层之间的依赖关系清晰,改一层不会影响其他层。
除了分层,我们还按业务领域做了模块划分。比如,视频生成是一个模块,用户管理是一个模块,计费是一个模块。每个模块内部有自己的Service、Repository、Model,模块之间通过接口通信,不直接依赖内部实现。
这个架构设计,花了我们两天时间。但这两天花得值,因为后面的开发都是在这个架构上进行的,方向对了,效率就高。
第二步:配置管理和硬编码清理
架构搭好之后,我们做的第一件事,是把所有的硬编码清理掉。
我们用了一个配置管理库(pydantic-settings),把所有的配置都集中到一个地方:
- 数据库连接信息
- Sora API的Key和地址
- 对象存储的Access Key和Secret
- 各种超时时间和重试次数
- 功能开关
配置从环境变量读取,不同的环境(开发、测试、生产)用不同的环境变量。代码里不再有任何硬编码的配置。
这样做的好处是:
- 换环境不需要改代码,只需要改环境变量。
- 敏感信息(API Key、密码)不会出现在代码里,更安全。
- 配置集中管理,一目了然,不会散落在代码的各个角落。
我们还加了配置校验,启动的时候检查所有必要的配置是否都设置了,如果缺了,直接报错退出,而不是跑到一半才发现配置不对。
第三步:异步化和任务队列
原来的系统最大的性能问题,是同步等待。
视频生成是一个长时间的任务,调用Sora API生成一个视频,可能需要几分钟。原来的代码是同步等待的,一个请求占用一个连接几分钟,并发一高就扛不住。
重构之后,我们改成了异步架构:
- 用户提交生成请求,接口层立刻返回一个任务ID,不等待生成完成。
- 任务放到消息队列(我们用的是Redis Queue)里。
- 后台的Worker进程从队列里取任务,调用Sora API生成视频。
- 生成完成后,更新任务状态,把视频存到对象存储。
- 用户通过任务ID查询生成状态和结果。
这样,接口层的响应时间从几分钟降到了几十毫秒,能支撑的并发量提升了几十倍。
我们还做了任务队列的优先级和限流:
- 付费用户的任务优先级高,免费用户的任务优先级低。
- 每个用户有并发限制,防止一个用户占用所有资源。
- 队列有长度限制,满了之后拒绝新任务,返回"系统繁忙"的提示,而不是把系统拖垮。
Worker进程可以水平扩展,任务多的时候多加几个Worker,任务少的时候减少Worker。这样,系统的处理能力可以根据负载动态调整。
第四步:错误处理和重试
原来的代码几乎没有错误处理,重构之后,我们给每个可能出错的地方都加了错误处理。
具体来说:
第一,调用Sora API的时候,加了重试机制。网络超时、API限流、5xx错误,自动重试,最多重试3次,每次重试的间隔递增(指数退避)。
第二,区分可重试错误和不可重试错误。比如,网络超时是可重试的,参数错误是不可重试的。不可重试的错误,直接返回给用户,不浪费时间重试。
第三,每个任务有明确的状态流转:等待中、处理中、成功、失败。失败的任务,记录失败原因,用户可以看到具体的错误信息,也可以重试。
第四,所有的错误都有详细的日志。错误发生的时间、任务ID、用户ID、错误类型、错误信息、堆栈跟踪,都记录下来。出了问题,通过日志能快速定位。
第五,有降级策略。如果Sora API不可用,系统自动切换到备用的视频生成模型,或者提示用户"当前服务繁忙,请稍后再试",而不是直接崩溃。
第五步:日志和监控
原来的系统没有日志和监控,出了问题全靠猜。重构之后,我们加了完善的日志和监控。
日志方面:
- 每个请求有一个唯一的Request ID,贯穿整个处理流程,方便追踪。
- 关键节点都有日志:请求进来、任务创建、API调用、视频处理、任务完成、错误发生。
- 日志分级别:DEBUG、INFO、WARNING、ERROR。生产环境只记录INFO及以上,开发环境记录DEBUG。
- 日志集中收集到ELK,支持按Request ID、用户ID、任务ID搜索。
监控方面:
- 业务指标:任务总数、成功率、平均生成时间、队列长度、活跃用户数。
- 系统指标:CPU、内存、磁盘、网络、Worker进程数。
- 外部依赖指标:Sora API的调用次数、成功率、响应时间、错误率。
- 告警:关键指标超过阈值,自动发告警到钉钉和邮件。
有了日志和监控,出了问题不再是"用户说用不了,我们不知道为什么",而是"监控告警了,我们看一下日志,几分钟就定位到问题"。
第六步:代码质量和测试
重构的最后一步,是提升代码质量和加测试。
代码质量方面:
- 统一了代码风格,用Black做代码格式化,用Flake8做代码检查。
- 函数和类都有文档字符串,说明功能、参数、返回值。
- 复杂的逻辑有注释,说明为什么这么做。
- 代码审查(Code Review),每个PR至少有一个人审查之后才能合并。
- 消除了重复代码,把公共逻辑抽成工具函数和基类。
测试方面:
- 核心业务逻辑加了单元测试,覆盖率达到了70%以上。
- 加了集成测试,测试整个流程从创建任务到生成完成。
- CI/CD流水线,每次提交代码自动跑测试和代码检查,不通过不能合并。
- 加了契约测试,确保对外的API接口不发生破坏性变更。
有了测试之后,改代码就有信心了,不用担心改出bug。以前改代码要手动测半天,现在跑一遍测试,几分钟就知道有没有问题。
重构的效果
两周之后,重构完成,新系统上线。
效果非常明显:
第一个效果是,性能提升。接口响应时间从平均30秒(同步等待的时候)降到了50毫秒。系统能支撑的并发量,从原来的几十个并发,提升到了几千个并发。
第二个效果是,稳定性提升。上线一个月,没有出现过系统崩溃的情况。Sora API偶尔出问题,系统能自动重试和降级,用户几乎感知不到。
第三个效果是,开发效率提升。加一个新功能,以前要改好几天,现在因为结构清晰,一两天就能搞定。新人入职,看一遍架构文档和代码,一周就能上手开发。
第四个效果是,问题排查效率提升。以前出了问题,排查要几个小时,现在看监控和日志,几分钟就能定位。
第五个效果是,团队心情变好了。以前面对一堆烂代码,每天都很痛苦,改代码像拆炸弹。现在代码结构清晰,写代码变成了一种享受。
重构中的坑和经验
这次重构,我们也踩了不少坑,总结一些经验。
第一个坑是,不要试图一次性重写所有代码。我们一开始想两周内把所有代码都重写,后来发现不现实。改成了分模块逐步替换,先把核心的视频生成流程重写,其他模块后面慢慢迁移。这样风险小,也能早点看到效果。
第二个坑是,重构期间不要加新功能。重构期间,如果同时加新功能,会让重构变得复杂,容易出问题。我们的做法是,重构期间冻结新功能,只修bug。重构完成之后,再开始加新功能。
第三个坑是,一定要有回归测试。重构最担心的是,新代码和旧代码的行为不一致。我们在重构之前,先写了一批端到端的测试用例,重构之后跑同样的测试,确保行为一致。这一点非常重要。
第四个坑是,不要过度设计。重构的时候,容易想"这次一定要设计一个完美的架构,以后都不用改了"。但实际上,没有完美的架构,需求会变,技术会变。设计够用就好,不要为了不确定的未来,做过度的抽象和设计。
第五个坑是,和团队对齐。重构不是一个人的事,要整个团队都理解新的架构和规范,并且遵守。我们在重构之前,开了好几次会,讨论架构设计,达成共识之后才开始动手。
写在最后
这次Sora公测后的代码重构,是我职业生涯中最有成就感的项目之一。
看着一堆烂代码,在自己手里变成了结构清晰、性能优异、可维护的系统,那种成就感是无法形容的。
代码重构,不是为了重构而重构,而是为了让系统能支撑业务的发展,让团队能更高效地工作。当你的代码已经成为业务发展的瓶颈的时候,重构就是必须的。
如果你也在维护一堆烂代码,每天都很痛苦,我的建议是:不要抱怨,行动起来。先从最小的改进开始,比如清理硬编码、加日志、加测试,然后逐步重构架构。烂代码不是一天写成的,也不是一天能改完的,但只要开始,就会越来越好。
希望我们的重构经验,能给正在做类似事情的朋友一些参考。如果你也有代码重构的经验或者问题,欢迎在评论区交流。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录