复杂度从来不是免费的:一个独立开发者的 SaaS 架构取舍

在 PigeonPod Cloud 的架构图上,每增加一个方框,我就多了一项需要长期承担的责任:部署、监控、升级、排错,以及未来修改它。因为整个系统由我独立开发和维护,这些责任最终都会回到我身上。
所以,我做架构决策时,会直接问一个问题:这项复杂度带来的收益,是否值得我长期支付它的全部成本?
这是一个听起来很普通的问题。但真正困难的地方,是把“成本”和“收益”说清楚。
PigeonPod Cloud 承载的实际工作负载
PigeonPod Cloud 可以把 YouTube 频道和播放列表转换成私人播客。用户订阅一个内容源后,系统会持续同步新内容,按照用户的设置自动下载单集、保存媒体文件,再通过私人 RSS 提供给常见的播客客户端。
这条流程涉及外部平台、订阅源同步、媒体下载、对象存储、用户访问状态、配额、计费和后台任务。这些实际问题共同决定了系统需要什么。
截至 2026 年 7 月
- PigeonPod Cloud 上线约 120 天,积累了约 1,300 名注册用户。
- 系统累计完成约 61,000 个下载任务,平均每天超过 500 个。
- 系统每天成功完成超过 16,000 次订阅源同步。
- 最近 7 天,新订阅触发的首个单集自动下载,从进入系统处理流程到下载完成,平均约 30 秒。
这些数字给后面的讨论划定了现实边界:PigeonPod Cloud 仍处于产品早期,同时已经在持续处理真实用户和生产工作负载。
复杂度从来不是免费的
架构成本既有一次性的,也有持续发生的。
实现一个方案需要开发时间;理解它需要占用注意力;让它稳定运行需要基础设施、监控和运维;出现问题以后,需要能从用户看到的现象一路追到具体状态和代码;业务变化时,还要修改、迁移,有时甚至删除它。
收益必须对应具体问题,例如维持面向用户的服务稳定运行,隔离不同的故障和负载,获得实际需要的扩展能力,以及缩短排错路径。“架构更清晰”本身还不足以说明收益。
这意味着,我会根据具体收益决定一项复杂度的去留。有些复杂度应该删除;另一些复杂度能够持续消除更大的成本,值得从一开始主动承担。
这类成本收益判断贯穿 PigeonPod Cloud 的日常开发。下面选择几个发生在不同层面的例子,说明它如何影响系统边界、产品范围和基础设施的部署位置。
架构图
图中只保留本文涉及的用户访问路径、工作节点扩展单元和家庭服务器辅助系统。它是一张用于说明核心取舍的叙事图,并未列出完整的生产环境拓扑。
运行边界应该回应真实的工作负载差异
PigeonPod Cloud 的后端从一开始就把接口服务和下载工作节点设计成独立的运行单元。
接口服务负责接收网络请求、认证、直接响应用户操作的业务编排,以及提交后台任务。工作节点负责订阅源同步、媒体下载、上传对象存储和其他后台工作。两者使用独立的 Docker 镜像和部署配置,但共享领域逻辑、持久化层、MySQL、Redis 和对象存储。
这条边界首先服务于面向用户的系统稳定性。
下载工作负载会持续消耗网络、处理器、内存和本地存储,也会受到外部平台限流、内容状态变化和下载工具异常的影响。它的负载具有波动性,失败方式也比普通接口请求更复杂。如果这些工作和接口服务运行在同一个进程里,后台负载很容易影响直接响应用户的服务。
把下载任务的执行过程剥离出去,再让 Redis 和少量进程内缓存承担会话、RSS、媒体元数据、配置和外部查询结果等高价值缓存后,接口服务可以在相对低而稳定的资源负载下运行。对 PigeonPod Cloud 来说,接口服务和工作节点都很重要,但直接响应用户操作的接口服务需要更高的稳定性优先级。
这条边界的另一个收益是,工作节点成为独立的扩展单元。
当下载负载突然增加,或者现有节点遇到短期内难以解决的外部平台风险时,我只需要扩展工作节点。找到一台合适的机器,无论是云服务器、家庭服务器,甚至是一台普通电脑,在完成必要配置后运行同一个 Docker 镜像,就能增加一个下载节点。节点上线后会参与心跳检测和任务路由,自动分担符合条件的下载任务。
这让我可以按照下载能力增加工作节点,同时保持接口服务的资源稳定。单个节点出现问题时,影响也会限制在后台任务范围内。水平扩展和故障隔离都是这条业务链路实际需要的能力。
这些收益足以支持独立运行,却不足以支持把后端进一步拆成微服务。
api 模块直接依赖 core 模块,两者共享领域模型、数据映射器、业务服务和数据库。一次请求继续使用进程内调用和共享数据库,省去了额外的内部接口和通用消息队列跳转。排查问题时,我仍然可以沿着控制器、业务服务、数据映射器和数据库查询走完一条相对短的路径。
这种设计存在共享数据库耦合,core 也同时承担共享模块和工作节点运行单元两种职责。但对当前阶段的 PigeonPod Cloud 来说,这正是我想要的边界:把需要独立保障和水平扩展的运行单元分开,同时保留较低的领域通信和排错成本。
删掉一个已经做完的产品模块
PigeonPod Cloud 最初的产品设计包含两个大模块:内容转换和内容消费。
内容转换负责把 YouTube 等外部内容转换成可以在播客客户端中订阅的私人 RSS。内容消费则试图让用户直接在 PigeonPod Cloud 内发现、组织和持续收听内容。这些能力全部进入了生产环境,我也花了不少时间把整个模块做完整。
这个模块包括内容浏览、推荐与预览、播放队列、收藏,以及云端播放进度等能力。为了支持它们,后端增加了控制器、业务服务、数据映射、领域模型和数据库表;前端增加了路由、页面、全局状态、复用逻辑和跨端交互。它已经成为一块完整的产品功能面。
上线后的两个月里,我通过埋点和使用统计逐渐确认,这些功能的实际使用率非常低。它们的直接基础设施成本接近零,这也使“反正已经做完了,就先留着”看起来很合理。
但代码只要继续属于产品,就一定会参与未来的变化。每次调整单集模型、修改播放器行为、处理用户数据、升级前端架构或排查相关错误时,我都要继续考虑这套消费状态。真正持续增长的是开发、排错、数据维护和迁移的范围。
已经投入的开发时间属于沉没成本,未来收益才应该决定我是否继续支付维护成本。在正式下线以前,我又检查了更详细的埋点和统计,确认这些功能消失对绝大多数用户的影响很小,然后删掉了整个站内内容消费模块。
这次删除远远超过关闭几个入口。对应的代码清理修改了 94 个文件,删除约 5,700 行代码和相关配置,并通过数据库迁移删除了 6 张相关数据表。数字本身只能描述变更规模,但它也说明了一个使用率很低的模块,实际占据了多大的维护范围。
PigeonPod Cloud 仍然保留直接播放、下载、复制链接和分享等基础动作,但不再维护播放队列、收藏、云端播放进度和内容浏览等站内消费系统。下线以后,我没有收到任何用户投诉,这与之前的使用数据一致。产品重新聚焦于内容转换、自动更新、媒体分发和私人 RSS,后续功能修改与架构演进也少了一整组需要兼容的状态。
这次删除给我的教训很直接:一项功能只要仍然属于产品,就会持续产生维护责任和成本,即使它的直接基础设施成本接近零。
把云服务账单当作早期架构约束
对于一个收入仍处于早期、同时缺少外部资金支持的 SaaS,固定云服务账单从第一天起就是架构约束。它持续消耗产品能够继续尝试的时间。
早期产品往往需要经历多轮验证、调整和积累。固定支出越高,留给产品找到稳定用户和收入的时间就越短。因此,我认为架构从一开始就应该计算云服务账单。依赖未来增长支付当前账单,会直接缩短产品可以继续探索的时间。
控制成本需要有选择地使用家庭服务器。我的划分标准是:一项工作负载是否位于同步用户流程上,以及它短暂不可用时会造成什么后果。
PigeonPod Cloud 直接响应用户的核心服务运行在云基础设施上。Loki、Grafana、Plausible、JobRunr 仪表盘,以及用于内部分析的生产数据镜像库,则运行在我自己的小型家庭服务器环境(其实就是一个好几年前买的低功耗 mini 主机)中。后面这些系统位于同步用户流程之外,负责可观测性、数据分析和内部运维;它们短暂不可用会降低我的观察和分析能力,核心用户流程仍可继续运行。
这种混合部署策略把云资源留给核心用户流程,同时用自托管基础设施承载辅助系统。降低固定成本以后,我为产品换来了更多成长、验证和迭代的时间。
自托管基础设施仍然需要维护,也必须处理安全、备份和故障恢复。但对于位于核心请求路径之外、允许暂时降级的工作负载,我认为这是早期 SaaS 值得认真考虑的策略。它把有限资金留给真正需要云服务可靠性的部分。
PigeonPod Cloud 如何把每月固定基础设施成本控制在 20 新西兰元以下,涉及具体的服务部署位置、成本口径和家庭服务器设计,适合在另一篇文章里单独展开。本节要强调的是:云服务账单本身就是架构约束。
每个决定都有有效期
当前架构正在承载 PigeonPod Cloud 的真实工作负载,它的有效期也取决于工作负载和约束如何变化。
当前架构同时包含从一开始就确定的设计,以及由生产运行问题推动的演进。接口服务和工作节点的边界属于前者。下载任务最开始使用 JobRunr,后来在生产运行中遇到具体问题,才迁移到 db-scheduler;其他后台任务则继续留在 JobRunr。这次迁移的原因、过程和边界,我会放到另一篇文章里讨论。
这两种情况遵循同一套判断。有些约束在产品开始时已经足够明确,可以提前设计;另一些问题只有经过真实工作负载才会暴露。第一天的设计负责解决已知约束,后续架构则需要在证据变化以后重新计算成本和收益。
如果未来出现独立团队负责不同系统、共享数据库争用,或者现有基础设施已经无法满足新的可靠性与扩展需求,同一笔成本收益账会得到不同答案。现在不值得引入的边界和抽象,到那时可能变成合理投资。
反过来,一个组件已经存在,只能说明它过去合理。它仍然需要持续证明自己的价值。
所以,我理解的架构成熟度,是一种持续判断的能力:知道自己为什么支付一项成本,它换来了什么,以及什么时候应该停止支付。
对一个人维护的生产级 SaaS 来说,每一层复杂度最终都会回到同一个人身上。值得承担的复杂度解决真实问题;其余的,只是在透支产品和自己的未来时间。
