当前位置:首页 > 企业资讯 > 米乐m6官网在线登录
联系姓名:
联系电话:
经营模式:
消费品 家居用品
米乐m6官网在线登录

软件架构一致性 —— 被忽视的研发成本

更新时间: 2024-10-02

  本文主要介绍了一些解决架构一致性问题的方法,以及我们该如何去理解和应对部分不得不付出的成本。

  广义的软件研发活动涉及到需求分析、源码阅读和理解、代码编写、测试编写、配置环境、发布运维、安全漏洞修复,各种基础软件升级等等,这些方方面面的工作,大致可以分为两类,第一类是价值创造活动,第二类是为了价值创造不得不付出的成本。

  新产品特性的研发,属于价值创造的部分。例如一个编辑器的软件,新增特性可显示用户当前编写文章的字数,这个特性可以激励用户更积极地创作,潜在的用户会更喜欢这个编辑器软件。新产品特性的研发,对于开发者来说,是一个学习和创造的过程,他可能需要和用户沟通,和产品经理沟通,需要理解现有系统的概念和运行逻辑,以及在必要的时候需要通过搜索学习新的技术以实现特性,有了这些上下文基础,才能进行编码和测试等工作。可以把编码理解成翻译工作,在我看来,把英文翻译成中文,和把领域知识翻译成编程语言,有着非常高的相似度。这类研发活动,通常是产品导向的,其关键目标是给用户创造增量的价值。

  软件研发活动中的很大一部分,是不得不付出的纯成本,并不创造用户价值。尽管很多人不承认这一点,并固执地在任何场合要求开发者解释自己工作的业务价值,但这实际是错误的。这类成本性质的工作,会包括各类基础软件的升级,包括 web 框架、java 语言的版本,操作系统的版本;也包括各类安全漏洞的修复;也包含一些依赖服务的升级治理,等等。在组织中,这些工作通常有一个实体或者虚拟的架构组去推行。

  今天随着大模型的发展和应用,在价值创造部分的工具智能化得到长足的进展,如 Github Copilot 可以帮助用户生成代码,通过对话的方式帮助用户快速学习各类知识。不过本文的重点在第二部分研发活动,即我们该怎么样去理解和应对这部分不得不付出的成本。

  实体企业家都会非常清晰地了解他自己生意所涉及的供应链。在用来和软件工程做类比之前,我们大家可以先简单分析下牛奶这一商品的供应链体系,消费者购买牛奶,为此付费、满足自己强身健体的欲望。

  为了生产超市货架上我们正真看到的牛奶,背后需要生产资料或者服务非常之多,例如需要冷链物流、需要巴氏杀菌的设备、并还有奶牛。再分析奶牛的背后,至少能够理解背后需要干草(有些高品质的牛奶需要特定的上等苜蓿干草),而大规模生产干草需要割草机、捆扎机、卡车等等。除了干草外,生产奶牛还需要牧场,而牧场又随时而来需要灌溉系统的支撑等等。

  类似的,有很多朋友都有开咖啡店的梦想,他们往往被咖啡店的氛围所吸引,但这其实只是消费者的视角,而从服务提供视角,他们至少需要思考供应链视角,包括门店、咖啡豆、牛奶、糖浆、咖啡师、甚至是宠物猫。仔细思考这样一些问题,他们的梦想可能就不会那么美好了。

  作为类比,软件研发也涉及供应链管理的问题。虽然开发者在大多数时间的工作都集中在增量价值(如 Maven 项目中 src/main/java 的部分)的开发,但是软件要运行起来,还依赖大量的下层软件模块,包括操作系统,JVM,基本的框架、中间件,以及大量的内部二方库。以一个 Java 应用 deploy-api 为例,它的镜像大小接近 3 GB,但是这其中真正属于增量价值部分的大小仅仅只有几十M,从文件大小来看,占比不到 1%。

  前文例子中的图片事实上只包含了 deploy-api 所依赖的运行时部分内容,更完整的供应链还会包含它依赖的数据库、云服务、网络配置等内容。以典型 Java 应用为例,完整的软件供应链会包含如下的部分:

  5、调度系统及服务:现代的技术架构通常运行在如 K8s 这样的调度服务上;

  7、云服务,业务服务:OSS,Redis,数据库,依赖的各类 HSF,HTTP 服务;

  新的业务代码无法运行在真空中,代码背后依赖了复杂的供应链,为了确认和保证供应链在可靠性、安全性等方面满足预期,衍生出了相关的管理工作,开发者在日常工作中遇到的大量的例子,包括:

  安全漏洞修复:下层软件组件被普遍的使用,因此如果出现安全漏洞就需要在所有被使用的地方得到修复。例如 2021 年爆发的 log4j2 漏洞,公司会希望所有应用在当天完成漏洞的修复。

  硬件的适配:硬件的升级换代有时候需要基础软件的配套升级。例如 JDK 对于国产化的 ARM 机型做了大量优化适配,公司会希望所有应用运行在新版本 JDK 上以更充分使用国产化 ARM 硬件。

  提升研发体验:无论是 Java 21,还是 Spring Boot 3,都在各种细节上优化框架和各类 API 的使用体验,降低业务代码的编写成本。

  降低维护成本:很多内部框架,以及内部二方库的维护者,由于其旧版本还是被大量使用,因此不得不同时维护众多的版本,有些 client 类型的二方库长期存在还导致了服务端的代码无法下线。

  去除脆弱依赖:下层软件组件中存在很多无人维护的依赖,有些是版本太旧如 Spring 2.x,有些则是根本社区已经消失了如 WebX,这些依赖的存在是长期存在的风险。

  依赖服务管理:随着自身业务的变化,可能依赖服务的能力,可靠性,性能不再满足需求了。例如把自己研发的文件存储服务迁移到云产品 OSS,或者发现某一个云产品的稳定性 SLA 不足以满足需求,决定换一个云服务。

  问题诊断:当依赖的所有软件、服务,其中任何一部分出现一些明显的异常问题的时候,就应该要依据各类日志和监控诊断问题,这通常是非常费时费力的。典型的有 Java 包的依赖冲突,应用启动依赖的配置出现问题等等。

  除了这些上述日常的管理维护工作之外,还有一种并不经常发生的场景,会清清楚楚地考验软件供应链的管理能力,这就是建站场景。建站场景的需求来源有很多种,包括新业务的开展(如海外新国家站点),容灾(跨区域建站),以及比较罕见的解决容量问题。在一个新的区域把整个软件系统部署一遍是极具技术挑战的工作,虽然我们很早就听到“一键建站”这个宣传语了,但真实的情况是我们离实现这一步还有较长的距离,我经常开玩笑说这个“一键”可能目前仅仅是 CTO 发邮件点击的那个回车键。

  既然软件供应链管理是我们在研发软件系统的时候不得不付出的成本,那么下一步应该思考的是,如何把这个成本降低。

  除了建站这种是未解决新的业务问题之外,几乎所有的软件供应链管理问题都能够理解成为:把一个或者多个目标软件/服务/配置升级到期望的版本。以 JDK 升级为例,为完成升级目标,需要修改的软件和配置非常多,包括基础镜像,环境变量,JVM 参数,Maven 配置,数十个 Maven 依赖,以及代码改造(如 Mockito,Velocity,Spring 等),降低这类工作的成本,可以从以下几个角度来分析。

  显式 or 隐式:软件/配置/服务的声明是显式(explicit)的还是隐式(implicit)的,显式的声明管理成本更低,反之则更高。例如我们可否在一个地方清晰地看到所负责应用依赖的所有云资源(explicit),还是需要去分析代码看应用使用的众多 Diamond 动态配置,逐个分析后才能知道其依赖的资源(implicit)。

  结构化的 or 无结构的。软件/配置/服务的声明是否有清晰一致的结构,结构化的内容更容易理解,管理成本低,反之则高。例如 Maven 的 POM 清晰定义了 Java 依赖的声明结构,相比之下,各应用的启动脚本是无结构的,可以没有约束地定义,理解成本很高。清晰的结构通常是一种是合理的的抽象。

  一处修改 or 处处修改。众多系统/应用的相关软件/配置变更可否在一处完成。例如当我们升级 JDK 的时候,会需要为几百个应用做几百几千次的代码修改才能完成,这样的做法显然成本是很高的。如果这几百个应用都在同一个代码库中,即用 mono repo 的形式管理,且这个代码库的结构得到很好的维护,那么升级 JDK 需要的代码修改量就会大大降低。

  自动验证 or 手工验证。软件/配置的修改,都需要在生产环境变更后才能生效,这一点和自动化单元测试和集成测试的逻辑是一致的,是不是真的存在自动验证也会极大影响修改的成本。若需要手工验证,再放大到数百应用,这个成本就非常高了。

  很多团队都存在虚拟或者实体的架构组,这个架构组的工作职责之一是推动架构一致性,具体的工作往往是识别到各种软件配置的问题,带领并推动相关团队去做相应的升级。我认为在做这个工作的过程中一定要关注上述的四点,需要把系统的供应链做到显式、结构化,需要想办法降低修改的次数,并建立完善自动化验证的手段。

  一名学生写一个用完即抛的,几千行代码量的课程作业,是不需要仔细考虑架构一致性问题的。一个只有几名研发的创业团队,也不会去关心架构一致性问题,遇到要升级的软件和服务,直接修改就完事了。只有当研发团队规模慢慢的变大,代码的规模随之增长到数千万数亿行的时候,架构一致性问题才会凸显,因为这样一个时间段所付出的成本增长得太快了。

  说起 Scalability,大家通常想到都是软件架构的横向伸缩能力,即基于负载均衡,横向增加计算资源以应对用户请求量的增长。这里暗含了几个要点,首先用户的请求量的增长通常是线性或者指数性增长,其次是系统应对用户请求量增长的时候不会有服务的品质(如响应时间)的下降,第三是应对用户请求量的增长涉及的研发人员投入是亚线性(sublinear)的。

  除了计算能力的 Scalability,另一个软件架构中常见的 Scalability 问题是数据库的 Scalability,在这篇简单的介绍文章中我们大家可以了解到,数据库基于数据复制和分片能力,可以支撑数据读写的增长。在这个例子中上述的三点也是成立的,包括访问量线性/指数性的增长,服务的品质的保持,以及过程中研发投入增长的亚线性,或者说架构能力具备后这个投入就是常量。

  架构一致性问题是一个典型的 Scalability 问题,我们期望的是随着代码量的增长,使用服务的增长,应用数的增长,需要为止投入的维护成本不要线性增长。我先尝试总结下 Scalable 方案的模式:首先它的诱因必然是一种业务的增长(growth),例如用户访问量的上升;其次为了应对这种增长需要有技术的引入(Technology),例如负载均衡和计算资源横向扩展技术;最后同时实现两个目标,其一是服务水平的保持(Keep Service Level),例如服务响应时间不变,其二是人员投入的亚线性(Sublinear Human Interaction),例如水平扩展的计算架构下研发的投入不会随着业务量上升而同步上升。

  基于前文总结的 Scalable 方案的模式,我现在分析下对于一个企业来说,代码修改这个场景是否是 Scalable 的。我们直接把几个关键的分析因子填入分析:

  Growth(业务增长):一个企业代码量的积累,从开始的几千几万,慢慢增长到千万行,数亿行的规模。必须要格外注意的是,应用数量的拆分并不会导致代码量的下降,相反有几率会使代码量上升。

  Technology(技术):一个企业是否有相关技术支撑 Scalable 目标的达成。

  Keep Service Level(服务水平保持):在全公司范围修改一部分代码(如修复 log4j 的漏洞)可否在可以可接受的时间范围内完成(如1天)。

  Sublinear Human Interaction (人员投入亚线性):当公司的代码量从数万,增长百倍千倍万倍的时候,修改代码的人员投入是否可以控制到极小的增长。(如1人日增长到5人日,而不是500人日)。

  结合到我们当下的现状分析,虽然我们我们存在数以亿行级别的代码量,但是很多代码的修改不需要考虑 Scalability 问题,这些代码主要集中在贴近上层业务的部分,例如淘宝的营销会场等,它们几乎不会被大规模复用。而一旦涉及到复用的代码变更,要在全公司层面完成统一的修改,就非常的困难且成本非常高(同样的升级,同样的修复,类似的验证,需要被重复成千上万次),各种基础软件的升级都是这样的例子,几乎需要每个研发去修改代码并发布,而且很难做到 100%。因此,代码修改的 Scalability 问题应该进一步明确为:被广泛复用代码(配置、服务),其被修改的 Scalability 问题。

  对软件供应链定义和架构一致性问题做了充分的分析后,下面我们讨论几种处理架构一致性问题,降低研发投入成本的几种方法。

  对于一个有着成百上千研发人员的技术团队来说,让每个研发去处理类似 JDK 升级这样的工作,是非常低效的。处理这样的问题需要非常丰富的知识,而且这些知识大家平时几乎都是用不到的,因此学习成本很高,而且学了一次之后,后续几乎都用不到了。因此,在一个团队中让少数几个专家去处理这类问题,效率会高很多。同样的问题,例如一个罕见的类冲突,专家几分钟就解决了,而普通的研发往往需要消耗数小时。进一步的,专家会把这些脑中的知识积累成高质量的文档,基于这些知识和大模型技术,这样的专家服务就可以 AI 服务的形式提供,如 Amazon 披露的:

  Aone Copilot 团队也投入在做类似的工作,相信不久的将来大家也能用到类似的产品能力。

  IaC(Infrastructure As Code)即基础设施代码化,前文提到我们期望软件供应链能够得到显式和结构一致的描述,这正是代码的优势。实际工作中的场景是,这些基础设施的数据分散在各类系统中,有些系统的数据质量高,有些系统的数据质量较差。在建站这类的场景中,架构师无法从单一的系统中获取系统的全貌,而需要组织一个临时的团队从四处搜集数据,然后通过一次次的尝试去验证。IaC 就是要把基础设施的数据交还给用户,各系统负责处理基础设施的变更。

  有了显式和一致结构的描述后,DRY(Dont Repeat Yourself)才有可能。例如,当数千 Java 应用的启动脚本都是各自维护和定制的时候,就无法从中去提取类似服务优雅上下线、服务预热、Spring Boot application profile 注入等通用的功能函数。这类编码抽象的思想在 Java / Go 这样的程序中大家都会自然而然的想到,但是在基础设施描述这类大量的配置类数据中,应用得就少很多。

  Serverless 这个词被赋予了非常的涵义,这里指的是通过把原来的应用分为 App 和 Runtime 两层,并实现这两层的单独维护演进。这种方案的核心思路是,通过让大量的 App 在运形态复用相同的 Runtime(这里包含了基本的 OS, JDK,Pandora),实现基础设施的收敛(一致);同时,通过相关的调度技术实现 Runtime 可独立升级,实现了原本需要大量重复的工作在一处修改就能完成。这个思想在云的 FaaS 产品上得到了广泛的应用,同时在内部业务中 Aone Serverless 也持续做了很多工作。

  相比于 Serverless 技术实在运形态通过调度技术把上层的代码和下层的代码组合起来,Mono Repo(大库)是在编译期间就可以确保 DRY。没有实践过 Mono Repo 的同学,可以想象一下把几十个应用的代码合并在一起,那大量的 infra 相关的代码,如 spring,http,jdk 依赖就都可以在唯一的地方处理和解决,那么版本升级就变得非常简单。当然,简单的把代码放在一起不能解决问题,还得做大量代码的重构才能实现我们的目标。这方面集团内有不少的先行者在尝试,例如在卓越工程的实践中就有相关介绍。

  前面介绍了很多解决架构一致性问题的方法,那为何这个问题一直没有得到很好的解决呢?而且我们看到的最常见的去尝试解这问题的方法,竟然是一个个的专项推动。

  短期调用团队是相对简单的,长期做扎实技术显得困难许多。而前面提到的有关技术,没有一个可以通过半年一年就能做到很高的水平的。以 IaC 为例,光把基础设施的数据做准确就要花非常长的时间,例如诺曼底云管系统负责管理集团云资源的管控,团队花了一年多的时间才把应用和云资源归属关系覆盖率从较低的水平提升到接近 90% 以上。

  但是仅仅是这个水平就可以为成本治理和技术风险场景贡献非常巨大的价值。Serverless 也是,在标准化 Runtime 的过程中,必须去理解和处理历史上各种自由的脚本定制,应用程序穿越边界和基础设施耦合的事情。Mono Repo 则更是颠覆式的,如果几百人每天在同时同一个代码仓库,我们具备完善的自动化测试覆盖吗?我们的构建和 CI 系统能给到快速的反馈吗?这就又回到卓越工程提倡的基本工程素养上去了。

  我们正真看到大模型在代码理解和编写上已经发挥很大的价值,而且这个价值会持续增大。但是从解决架构一致性问题的角度来看,还是没办法发挥银弹的作用,我认为我们第一步先得用代码把架构描述出来,大模型的能力才能够发挥出来。深入认识到软件供应链的复杂性,认识到架构一致性的问题对于研发成本的重要性,是架构师和关键技术决策者的责任。同时,从基础设施提供者的角度,也应该在产品设计上提供相关的配套能力,如让用户用代码描述资源,提供构建能力支持 Mono Repo 的实践,建设好 Serverless 调度和运行时能力。

  只有思考清晰,坚持长期投入,技术才会有进展,Scalable Solution 中关键的 Technology 一环才能被补上。