2026年农历新年前后技术部立项评审会上出现了一个被疯狂吐槽的名字马年万能架构底座。名字是我起的一半是自嘲一半是想用这个“大词”逼着自己把这件事想清楚。这套底座不是什么业务系统更不是新造一个中间件而是我们团队沉淀了近三年的基础架构层统一解决配置管理、日志规范、数据访问、缓存接入、消息收发、异常处理、链路追踪这些横切关注点。这篇文章就是它的架构说明文档我会把立项背后的真实原因、模块设计、关键取舍、接入过程、治理方式以及那些被我们亲手删掉的功能一起讲清楚。如果你正带着一个十来人的后端团队或者负责公司内部的基础平台又或者被“每个项目都在重复踩同一批坑”折磨得够呛这篇应该对你有用。1. 从三个失败项目到底座立项问题到底出在哪1.1 我见过的最常见死法每个项目都在重新发明日志先说个印象很深的例子。有一年我们同时有三个业务项目在推进上线时发现告警群里日志根本对不上。项目A用的日志库能自动输出请求ID项目B的日志只有时间戳和消息项目C更夸张把敏感字段直接打到了生产日志里。三个项目的负责人各有一套说法但本质上都是同一件事日志格式、日志级别、脱敏规则、异步策略这些本应该在基础设施层统一解决的东西被每个项目重复实现了一遍。这不是个别现象。配置管理也一样有人把配置写在代码里有人用环境变量有人自己搭了一个简陋的配置读取工具。数据访问层更是五花八门有的团队直接写JDBC有的用ORM事务边界怎么控制全靠个人经验。缓存接入是最混乱的同一个Redis集群有人用默认序列化有人自定义了序列化器结果两边互相读不了对方的数据。这些问题的共同点是它们跟业务没有任何关系。支付项目的日志和内容项目的日志在格式和链路追踪的需求上几乎一模一样。但因为没有底座每个项目组都要从零开始研究日志切割、异步输出、脱敏策略、链路串联研究完了还要互相踩一遍已经踩过的坑。这种重复不是“多写几行代码”那么简单它会持续消耗团队的注意力和心智负担让人没有精力去关心真正的业务问题。1.2 真正的账单基础设施重复建设的隐形开销我们曾经做过一个粗略估算假设一个项目在配置管理、日志规范、数据访问、缓存接入、消息收发这五件事上每件事要花掉两到五个人天去实现和联调三个项目加起来就是三十到七十五个人天。而这还只是首次接入的成本后续维护才是大头。每个项目一套日志方案人一流动接手的人又要重新理解一遍每个项目一套配置读取方式出问题时排查路径完全不同运维和开发的协作成本直线上升。更深层的问题在于没有底座的时候团队对“什么叫做好”没有统一标准。A项目觉得日志能打出来就行B项目觉得必须有 traceId 串联C项目觉得还要加脱敏。谁也说服不了谁最后形成一堆局部最优解。立项做马年万能架构底座不是因为我们想搞一个大而全的平台而是想把这些横切关注点收拢成一个统一的模型让业务团队只需要关注业务逻辑本身。这个定位很重要底座不做业务不碰领域模型不定义业务表结构。它只解决“业务之外所有项目都需要面对的技术问题”。想清楚这一点后面的所有设计决策都有了判断依据。2. 骨架与边界底座的模块划分和取舍原则2.1 三横三纵底座的分层视图底座的整体结构我习惯用“三横三纵”来描述。三横是三个水平层次接入层、业务支撑层、基础设施层。接入层负责统一协议入口、身份认证、参数校验业务支撑层是底座的核心包含配置模块、日志模块、数据访问模块、缓存模块、消息模块、异常模块基础设施层提供连接池管理、序列化策略、定时调度、运行时环境适配。三纵是三个垂直切面可观测性、安全性、扩展性。可观测性穿透所有层次指标、日志、链路追踪三件事必须同时存在安全性处理脱敏、鉴权、审计扩展性则通过SPI机制让不同业务团队在底座默认行为之外有自定义空间。这个划分不是拍脑袋定的而是踩过坑后的结论。最早的版本我们只有“横”没有“纵”结果可观测性散落在各个模块里日志里缺 traceId指标没有统一采集排障时非常痛苦。后来把三纵独立出来每个纵切面都有自己的核心抽象和默认实现反而让横层变得更干净。纵切面最容易犯的错误是“附带实现”比如日志模块顺手做了脱敏配置模块又做了一次脱敏两边规则还不一样。所以底座里任何横切能力都只能归属一个主模块其他模块通过依赖引用来使用它。依赖方向必须严格自上而下接入层依赖业务支撑层业务支撑层依赖基础设施层。任何一层都不能反向依赖更不允许业务代码直接绕过支撑层去操作基础设施。这个约束在底座里是靠架构测试强制执行的我们专门写了一个校验规则在CI阶段扫描代码依赖关系出现环形依赖或反向依赖直接构建失败。没有这种硬约束架构文档写得再漂亮三周之后就会被各种“临时方案”撕碎。2.2 能砍就砍底座不包含什么比包含什么更重要确定底座的边界最难的不是决定“做什么”而是决定“不做什么”。我们明确排除了四类内容业务流程、领域模型、具体业务表结构、权限模型。业务流程和领域模型是业务团队自己的事底座如果越界去抽象就会演变成一个业务框架最后谁也改不动。权限模型更典型我们只提供权限判断的抽象入口和一个默认实现但不会定义角色、菜单、用户组这些业务概念。第三方平台适配也被挡在底座外面。很多团队喜欢把各种外部服务的封装沉淀到底座里比如某个支付渠道的SDK、某个地图服务的客户端。这些封装当然有价值但它足够场景化一旦放进底座就会让底座变得臃肿而且外部服务变更时底座的发版节奏会受制于这些适配层。我们的做法是单独建一个“集成组件库”来放这些内容组件库可以引用底座底座绝不反向引用组件库。还有一个容易被忽视的边界底座不做配置中心本身只是配置中心的客户端封装和规范约定。消息队列、缓存中间件、注册中心这些基础设施都是外部组件底座提供的是对这些组件的标准化访问方式而不是替代品。这就像插座只提供标准插孔不发电也不造电器。边界划清楚之后底座的核心包体积一直控制得很好业务项目引入底座带来的启动时间增加不到两百毫秒。3. 关键设计细节配置、契约、扩展点为何这样定3.1 配置中心的映射规则环境隔离与默认值配置模块是底座里最基础也最容易引发争议的部分。我们的设计围绕三件事命名规范、环境隔离、默认值兜底。命名规范是硬性的每个应用接入底座时必须按“应用名-环境-分组”的规则注册自己的配置空间。应用名不能乱取必须和注册中心、监控系统、日志索引里的名字保持一致我们为此专门维护了一个应用名词表新应用接入前先过词表评审。环境隔离方面底座只认 local、dev、test、prod 四种环境环境标识统一在构建时注入不允许通过启动参数临时覆盖。曾经有团队为了省事把测试环境的配置写到了生产分组里结果压测时把生产的一个通知服务给打爆了。后来底座在启动时会自动校验配置来源如果检测到运行环境与配置分组的环境标识不一致直接启动失败并给出明确提示。这个“宁可启动失败也不带着错误配置运行”的决策后来被证明是值得的。默认值兜底是让新项目快速跑起来的关键。配置模块对每一项配置都维护了一份默认值业务团队接入底座时不是所有配置都强制填写。例如日志滚动策略、脱敏开关、连接池初始大小这些配置缺省时底座使用默认值但会打出一条结构化告警提示“某项配置未显式指定当前使用默认值”。这样新项目不会因为少配一个东西就启动不起来同时生产项目如果忘配了也觉得不踏实自然会回头补齐。这套机制推广开后新服务从拉代码到启动成功平均耗时从一天半降到了四十分钟以内。3.2 统一返回体与错误码一页纸定死然后严格执行我们做的第一版统一返回体规范写了七八页文档结果没人看。后来把它压缩成一张纸只保留四个核心约定返回体必须包含 code、message、data 三个字段code 为 0 表示成功非 0 表示失败失败时必须附带可读的 message 且不能包含堆栈信息data 可以是 null 但不允许缺失。这种压缩是有代价的很多人一开始觉得太粗糙但运行一段时间后大家发现一页纸的规范才能被真正记住和执行更细的语义都沉淀在错误码表里。错误码表是底座里维护得最认真的单一事实来源。我们把错误码分成五段系统级错误码包括参数错误、鉴权失败、服务不可用等基础设施级错误码覆盖数据库连接失败、缓存超时、消息发送失败等业务级错误码由各业务团队在自己范围内定义但必须遵守统一样式第三方对接级错误码用于外部服务返回异常时的映射第五段是保留段不允许使用。这样规定后前端和网关拿到一个错误码能立刻看出它属于哪一层排查范围瞬间缩小了一大半。统一返回体和错误码看起来是小事但它直接决定了对外的契约稳定。我们规定所有对外的接口描述文件必须由代码自动生成文档生成过程中会做契约校验发现返回结构或错误码不合法就阻断发布。这只是底座里一个很小的模块但它带来的收益非常直接前后端联调时因为字段名对不上扯皮的事几乎消失了。3.3 扩展点与SPI底座怎么做到“万能”又不失控“万能”是个危险词我们常被挑战底座是不是什么都管为了对得起这个名字又不失控核心手段是SPI扩展点机制。底座里每个关键行为都定义了接口并附带一个默认实现业务团队如果需要定制通过SPI声明自己的实现底座会自动发现并加载。听起来很简单真正难的是让“扩展”这件事本身有约束。约束有三条。第一扩展点必须由底座定义业务方不能新增不受限的注入点防止有人把整个业务逻辑挂到启动流程里。第二任何扩展点都必须有默认实现不实现也能跑这是底线。第三扩展实现要显式声明生效范围一个服务里同一个扩展点原则上只允许一个激活实现多个实现必须指定优先级。举个实际例子数据访问模块的加密器接口默认实现是AES金融业务的团队可以扩展成国密的实现但必须在配置里声明“当前服务启用了国密扩展”底座启动时会在日志里打印激活的SPI清单避免“我以为用了AES其实别人改成了国密”的乌龙。用生活化的类比底座是墙上的插座SPI是不同类型的插头。插座只提供一个标准化接口但插什么电器、怎么用电器完全由业务侧决定。如果插座本身还内置了一堆电器功能那它很快就会变成一堆没人能维护的废铁。这是我反复强调的一点万能指的是“接入方式的通用”而不是“内置功能的堆积”。4. 接入实录某业务服务如何在一周内跑起来4.1 接入前的准备依赖清单和环境约定纸上谈兵没有说服力这里完整走一遍接入流程。我们内部有一个“一周接入挑战”目标是从空项目开始一周内跑通一个带缓存、消息、定时任务和对外接口的完整服务。接入前要准备三样东西一个符合应用名规范的空项目、一个环境标识、一份部署运维信息登记表。登记表包括服务所属组、负责人、重点接口的监控需求、日志保留时长等这些信息会同步到基座的可观测系统里。依赖引入方式用的是统一的BOM管理。业务项目里不需要逐个声明底座模块的版本号只需引用一个BOM文件底座所有模块的版本都由它统一锁定。这一步非常重要它解决的是“依赖版本的一致性问题”。我们曾经有三个服务引用了同一组件的不同版本线上表现诡异排了一天一夜才定位到是版本不一致引发的序列化兼容问题。统一BOM之后这种问题的发生概率基本降为零。接入前还要检查环境是否达标必须能连通配置中心、基础设施的探活地址、监控上报端点。我们提供了一个叫“环境自检”的命令它会一次性检查配置中心连通性、DNS解析、端口号是否冲突、依赖的中间件版本是否在支持列表里。这个命令建议在任何接入开始前先跑一遍等第二周再跑就没有意义了因为环境问题会淹没代码问题让排障变得很痛苦。4.2 逐步接入从启动到完成一个业务接口接入过程我拆成了七个步骤每一步都有明确的完成标志和检查点。第一步引入BOM依赖创建启动类并加上底座标准注解。这个注解会自动装配配置模块、日志模块、数据访问模块、监控上报模块。完成标志是应用能正常启动并在日志里看到“底座初始化完成”的横幅横幅上会打印应用名、环境标识、底座版本号、注册中心地址。环境标识打印在横幅上是防止连错环境的第一道防线。第二步按照模板填写最小配置集。模板只需要四项应用名、环境标识、数据库连接信息也可以先不配、日志级别。其余全部用默认值。完成标志是配置自检通过日志里没有强制级的告警项。第三步定义第一个对外接口。用底座的统一返回体封装结果不需要手动搭拦截器、不需要自己写参数校验模板这些已经包含在接入层的默认逻辑里了。完成标志是启动后访问接口返回体结构符合规范且日志里能看到自动生成的请求ID和耗时。第四步验证链路追踪是否自动串联。我们接入的链路追踪方案是基础组件级别的业务代码零侵入。操作方式是依次调用两个接口其中一个会异步调用数据访问模块然后去追踪控制台看这次调用的完整链路树。如果能在一开始就验证这一步后续所有依赖的排障都有了一把金钥匙。第五步接入缓存。代码里只需要一个注解标注缓存名和缓存键策略底座的缓存模块会负责序列化、过期策略、失效重试。这里特别提醒接入缓存时必须先确认要缓存的数据有没有正确的进程内一致性需求否则宁可先不接。第六步接入消息队列。底座提供一套声明式消息监听器和通用消息主题定义方式业务侧只写消费逻辑和消息处理幂等策略连接管理和序列化都在底座内部完成。完成标志是能发送一条测试消息并成功消费消费次数统计在监控面板可见。第七步加上定时任务并确认调度轨迹可查。底座的定时任务模块统一管理调度时钟和生命周期重复执行保护是默认开启的。完成标志是在任务控制台上能看到最近一次执行结果和执行耗时。这一整套流程走完之后业务团队再根据需求补齐自己的业务代码但“所有非业务能力”都已经就绪了。我们把这种状态叫做“地基已浇筑”。有之前没有接触过底座的同事评价前期有点懵但每一步的完成标志都很清晰照着做就行。4.3 接入过程中真正会踩的坑版本一致性与配置缺失从几次接入的真实反馈来看最容易踩的坑有三个。第一个是依赖版本漂移。某个业务服务之前自己引过一套开源工具包版本号很老和底座要求的基础库版本冲突启动时报了一堆莫名其妙的错误。最后排查发现问题的根源就是版本冲突而不是底座代码有bug。解法就是前面说的统一BOM并且在接入时执行一次“依赖冲突体检”把有冲突的依赖直接拦截在构建阶段。体检结果里会明确指出哪个依赖、哪个版本、被哪个模块传递引入方便直接排除或升级。第二个是配置分组写错导致的串环境。曾经有个同事在启动时把test标识填成了dev底座启动横幅上明明打印了环境标识但他当时没注意结果压测脚本把测试数据写进了开发库。后来我们在配置自检里增加了“环境冒烟写读”环节启动后自动写入一条带环境标识的标记数据并读回如果读写不一致立即报警。代价极低但非常有效。第三个是日志脱敏误伤。底座的日志脱敏默认开启手机号、身份证号、银行卡号的正则识别但有一次某个业务团队的订单号字段被误判为身份证号导致日志里的订单号全是星号排查线上问题时根本对不上。后来我们把脱敏规则改成了“白名单字段定义正则黑名单”双轨制需要保留的字段必须显式加入白名单其余字段按脱敏规则处理。为此还加了一条审计日志记录每次脱敏命中情况方便出问题时回溯。这些坑都不是底座接口设计的大问题更多是使用习惯和环境约定。所以我们在接入文档里花了大量篇幅讲“怎么自检”而不是“怎么调用”因为调用接口是给机器看的自检是给人看的。5. 底座自身的治理版本管理、兼容性、可观测性5.1 版本号背后的兼容性承诺底座自身的版本管理采用语义化版本但比标准语义化版本多了一条规定大版本号每年最多递增一次。这意味着底座的破坏性变更必须攒着一起发不能今天改一个接口明天删一个配置让业务团队天天跟着升级。小版本每个月可以发一到两次只做加法新增能力但不能破坏已有行为。补丁版本随时发只修bug和安全隐患。兼容性承诺写得很清楚小版本升级时业务代码零改动直接替换依赖即可补丁版本同样零改动大版本升级时允许移除已经废弃超过半年的API且必须提供平滑迁移工具。比如马年版删掉了早期的几个过时注解但提供了自动转换工具在构建阶段就能识别并提示替代写法。有了这套承诺业务团队对升级底座的预期是稳定的不会因为一个新的小版本发布就惶惶不安。版本号的背后其实是“升级成本归谁承担”的问题。底座团队必须主动承担升级带来的迁移成本而不是把成本甩给业务团队否则版本越多业务团队的怨气就越重。我们的做法是每次新版本发布底座团队自己在三个模拟业务项目上完整跑一遍升级流程验证零改动升上去再允许对外发布。这个内部验证步骤被写进了发布清单卡得很死。5.2 底座自身的监控先把自己管好一个专门给业务做规范的基础设施自身的运行情况如果不可观测是非常讽刺的。所以底座在启动时会把自身的关键指标接入统一的监控系统包括启动初始化耗时、SPI激活数量、配置命中率、默认值兜底触发次数、缓存序列化耗时、消息消费失败重试次数等。默认值兜底触发次数一开始超出了我们的预期因为很多业务团队确实没有填配置但指标上线后我们发现某几个服务的触发次数特别高反推回去发现是配置模板里的名称写错了这让我们意识到配置的默认值容易掩盖真实问题所以默认值触发时要持续告警不能安静地兜底。底座还强制开启自身的链路追踪。业务服务调用任何底层能力时这个调用的耗时、状态、是否走了降级路径全部作为独立节点出现在链路树里。这样如果出现性能问题可以精确看到是“业务代码慢”还是“底座的某个默认实现慢”避免互相甩锅。我们内部有个规矩底座的任何一次压测报告中都必须附上自身组件的性能报告否则不允许发版。让人意外的是底座自身最容易出现的性能瓶颈不在核心模块而在日志脱敏和序列化这两块。日志量大的时候脱敏正则的CPU开销会被放大序列化框架的选择会影响缓存的读写耗时。所以我们对默认实现做了几轮性能优化压测时也单独把这两个模块拎出来测毕竟它们是全公司每个请求都会经过的公共路径。5.3 演进节奏每年一个大版本日常只做加法底座团队的精力有限所以演进节奏必须刻意克制。我们现在执行的规则是日常只做加法年度做一次减法。日常周期里新功能以扩展点、新默认实现、观测面板等“非破坏性”形态加入不加新概念不重新定义已有语义。年度版本可以移除过时内容、调整模块边界、收敛异常分支但要提前一个季度在内部技术社区发出公告列出“即将废弃什么、替代方案是什么、迁移工具是什么”。马年版具体做完了三件事第一把日志、链路、指标三件套在底座内部完成对齐三个模块用同一个环境标识和应用名做关联避免各报各的第二收敛了SPI接口定义把历史遗留的十几个低质量扩展点合并成了几个核心扩展点同时保留兼容层让还在用旧扩展点的团队平滑迁移第三交付了环境自检和依赖体检两套命令行工具把接入门槛进一步降低。下一个年度的版本规划里重点也不是加新功能而是继续做减法把已经废弃超过半年的旧注解全部移除把消息模块的重试策略统一把配置默认值清单再精简一轮。有人问我为什么不趁机加一个规则引擎或者加一个低代码能力我说底座的价值不在于功能多而在于让人可以忘掉它。6. 反模式警示哪些“底座功能”我后来亲手删掉了6.1 过度抽象的领域模型底座差点变成一个业务框架早期版本里我们曾想做一个“通用实体基类”让所有业务表的实体都继承它这样统一提供创建人、创建时间、更新人、更新时间、逻辑删除标记这些公共字段。听起来很合理但落地时发现每个业务团队对公共字段的诉求完全不同有人要保留修改历史有人要记录来源渠道有人要支持多租户隔离。为了兼顾这些需求实体基类里的字段越来越多继承层级越来越深有些业务宁可自己另起炉灶也不想用这个“万能基类”。真正让人下定决心删掉它的是一次排查。有个服务出现了数据错乱查了两天最终发现是实体基类里的逻辑删除标记被某个子类复写成了业务字段读数据时框架层的逻辑和业务逻辑冲突了。那一刻我才意识到底座把爪子伸到了领域模型里就会破坏业务团队对自己数据模型的掌控力。后来我们把实体基类拆掉只保留一个无侵入的审计字段填充接口谁需要谁自己实现。这个删除没有引起任何不满反而有几个团队特意来道谢。这个教训让我明白底座的抽象只能停留在“技术横切面”绝对不能碰“业务建模”。技术横切面是每个项目长得一样的地方比如日志、配置、链路业务建模是每个项目都不一样的地方哪怕表面相似深层语义也完全不同。试图用底座去统一业务建模最终只会得到一个四不像。6.2 通用权限系统越通用越没人用我们犯过的另一个大错是花了一个季度做了一套“通用权限系统”。这个系统的想法是通过配置化的方式把功能权限、数据权限、操作审计全部统一管理起来任何业务接入后都能获得“开箱即用”的权限能力。结果对接了三个业务团队之后三个团队都开始写自己的私有适配层因为权限模型必须和业务模型深度绑定有的团队要按组织架构分权有的团队要按数据范围分权还有的团队要根据用户行为动态计算权限一个静态化的通用模型根本接不住这些需求。后来我们做了个决定从底座里删除权限系统的具体实现只保留权限抽象SPI和核心的鉴权扩展点提供默认实现只做基本的“登录后可访问”更复杂的权限模型由业务团队自行实现。改动之后业务团队反而更愿意提需求了他们可以在自己的服务里维护一套真正符合业务语义的权限体系只在入口处接入底座的鉴权接口。这个经验反复出现越是接近业务语义的能力越不能放在底座里底座只能留出接口让真正懂业务的人去填空。6.3 给后来者的建议清单最后整理几条对做类似事情的人可能有用的建议。第一先跑通三个以上真实项目的接入再谈抽象。没有足够的样本硬做架构做出来的往往是发明者的自嗨不是团队的共识。我们最早的两版底座基本就是给自己用的第三版才开始被别的团队接受因为那时候的设计是从三个项目的真实需求里提炼出来的而不是从理想模板里推出来的。第二硬约束比文档可靠。依赖方向、配置规范、SPI注册方式这些规则全部用代码在构建阶段强制执行。架构文档写得再清楚也没有“构建失败”四个字来得有力。第三每一次功能新增或删除都必须同步维护一份架构说明文档。底座代码本身更新很快但架构文档如果跟不上半年后连底座维护者自己都说不清边界在哪里。我甚至建议把架构文档当成代码的一部分评审它和单元测试一样重要。第四底座里没有“顺手做”的功能。任何被顺手加进来的能力最后都会变成负债。新增功能必须经过同样的评审、测试、监控、文档流程如果不是真有必要宁可不进底座。第五底座负责人要轮岗。连续由同一个人维护底座两年以上很容易形成路径依赖看什么需求都觉得应该放进底座里。我们规定底座项目组成员每半年至少轮换一次这样业务团队的诉求才能真正被听见。最后再分享一点个人的观察。写这份架构说明文档的过程比底座本身的代码压力更大因为要把边界、取舍、反例一条条写下来逼着我们把每一个“当时觉得合理”的决定重新审视一遍。这份文档我们当源代码维护每次大改都会更新它甚至先改文档再改代码。如果你也正在做类似的底层沉淀我建议先写文档再写代码。文档落地的那一刻架构的边界才开始变得清晰。
阅读完成 · 觉得有帮助?