前面的读书笔记服务已经可以打包、发布、观察和恢复。此时打开一张云原生生态图,仍然会看到大量没用过的项目。
不必急着补齐它们。系统需要增加一个组件,应该是因为某个具体问题反复出现,而且已有方法处理它的成本开始过高。下面沿着摘要任务的变化,看看几种常见架构选择究竟改变了什么。
先判断是否真的需要拆服务
最初,生成摘要只是保存笔记后的一个函数调用。后来摘要需要等待外部服务,用户每次保存笔记都要多等几秒。一个直接的改进是把摘要生成变成异步任务:保存笔记后记录待处理任务,worker稍后生成结果,界面显示“处理中”。
这个决定首先改变了用户可见的业务流程。保存成功不再意味着摘要已经完成,所以界面、接口和错误处理都要表达中间状态。只有把这些说清楚,队列才真正解决了等待问题。
是否进一步把worker拆成独立服务,则取决于运行和维护需求。摘要任务需要不同依赖、独立扩容,或由另一团队负责时,拆分可能有价值。如果只是目录里多了几个类,先保持清晰的模块边界也很好。
服务边界增加了部署自由,同时增加网络调用、认证、超时、版本兼容和观测成本。原来一个进程内的异常,变成了需要在两个进程之间解释的部分失败。业务分工不清楚时,拆成更多服务通常只会让原来的耦合更难看见。
网络失败留下的不是简单真假
API把摘要任务交给worker,等待响应时连接断开。API知道请求是否被worker执行了吗?
不知道。连接断开可能发生在worker收到请求之前,也可能发生在它已经写入结果之后。超时描述的是调用方没有及时拿到结果,不直接证明被调用方没有做事。
如果立即重试,可能重复生成摘要;如果永不重试,又可能丢掉任务。正确做法需要业务标识。例如以“笔记ID+正文版本+摘要算法版本”作为任务的幂等键,让重复处理能识别同一件工作,而不是每收到一次请求就无条件新增一条结果。
还要明确写入顺序。worker可以先生成结果,再在数据库中以唯一约束或事务方式提交,最后确认任务完成。若提交成功、确认消息失败,任务再次投递时应能识别已有结果。仅在内存中记一个“处理过”的集合,进程重启后就会丢失,无法承担这种保证。
Kubernetes Job也需要这样的设计。它负责推动任务完成,但重试和故障条件下,程序可能不止执行一次。1 调度层的成功语义,不能替代业务副作用的幂等性。
重试需要总预算
假设浏览器最多尝试三次,API调用摘要服务也最多尝试三次,摘要服务访问外部接口再最多尝试三次,这里的三次都包含首次请求。一次用户操作在故障情况下可能放大成许多次下游调用,最差组合可达到二十七次尝试。
每一层单独看都像在提高成功率,叠加起来却可能加重下游过载。更合理的做法是定义端到端时间预算,明确哪一层负责重试,限制次数,并使用退避与抖动分散恢复时的请求。
重试还需要区分错误类型。网络瞬时故障可能值得稍后再试;请求参数永远无效,重复提交通常没有帮助;下游已经明确限流,应尊重它的反馈并控制并发。
超时、限流、熔断和降级都是为了给失败划边界。它们并不会凭空提高下游容量。举例来说,摘要服务不可用时允许先保存笔记,是业务降级;直接返回一段编造的摘要,就改变了业务正确性,不能只看接口是否返回200。
各层独立重试会放大下游压力,需要共同时间预算和业务幂等。
服务网格把哪部分能力移出应用
服务数量增加后,多个语言栈可能都要处理服务间认证、流量策略和通信指标。服务网格尝试在基础设施层统一部分通信能力。Istio等实现提供流量管理、安全和可观测性相关能力;具体数据面形态与部署模式应按实现选择。2
它的价值在于重复的网络治理可以有统一入口。代价是请求链路和控制面增加了组件,团队需要理解代理或相关数据面的配置、证书、升级、资源开销及故障。
网格也不知道“生成摘要”是否允许重复。它可以帮你配置重试,却无法仅凭HTTP状态推导业务副作用。若基础设施层和应用层同时重试,反而可能形成前面描述的放大链条。超时与重试策略必须跨层协调。3
因此,只有两三个简单服务的团队,不必因为“微服务就应该有网格”而安装一整套系统。可以先统一客户端库与可观测性约定;当跨语言、跨团队的网络策略确实难以一致维护时,再评估网格。
Operator把领域知识放进控制器
Deployment知道如何维护一组应用实例,却不了解数据库升级顺序、主从切换或恢复条件。Operator模式使用自定义资源和控制器,把某个领域的运维知识编码成持续协调逻辑。4
例如,某个数据库Operator可以根据一个声明创建所需组件,并按它实现的流程处理特定备份或升级动作。但这些能力来自该Operator的具体实现,不能从“用了Operator”自动推出。
可以把它看成长期运行的软件系统,而不是安装后不需要维护的脚本。它需要权限、版本兼容、测试、告警与故障恢复方案。控制器本身出现错误时,也可能持续重复错误动作,所以权限范围和可中止性都很重要。
评估一个Operator,至少读清支持的数据库版本、备份恢复范围、升级与降级路径、维护状态以及失败时如何接管。对关键数据系统,能成功创建一个集群远远不够,还必须演练恢复。
Serverless改变的是资源管理接口
摘要任务也可能运行在按请求或事件触发的平台上。以Knative Serving这类平台为例,它在Kubernetes之上提供服务部署、版本和流量等抽象,并有自动扩缩相关能力。5
使用者更少直接管理常驻实例,但服务器并没有消失。底层资源、运行时和调度仍然存在,只是更多工作由平台承担。
对低频、突发任务,缩到零或按需启动可能节省空闲资源。代价可能包含冷启动延迟、执行时间限制、并发控制、状态保存方式和平台耦合。实际行为取决于选定平台与配置,不能把某一产品的限制写成Serverless的统一定义。
若用户点击后必须立即得到摘要,而冷启动加外部调用超过可接受时延,就需要预热、最小实例数、异步交互或其他设计。若任务每小时才发生一次,长期保持大量空闲实例也许没有必要。选择来自负载形态和体验要求。
用同一套问题评估新组件
面对新技术,可以沿着五个具体问题做一次小型设计评审。
第一,当前瓶颈是什么,有什么证据?是发布互相阻塞、重复网络策略、资源空闲,还是根本没有明确问题?
第二,它通过哪条机制改善瓶颈?答案应该能画出请求或控制路径,而不只是列出“弹性、可扩展、高可用”。
第三,失败会发生在哪里?新增控制器不可用、证书过期、队列积压、配置冲突时,业务怎样退化?
第四,谁维护它?安装、升级、告警和恢复都需要明确责任人。
第五,退出成本是多少?能否迁回更简单的方案,数据与业务协议是否被不必要地绑进平台特有接口?
读书笔记服务可以从一个结构清晰的单体开始,也可以在需求成熟后拆出独立worker。只要运行边界、状态、变更和故障有清楚的设计,就已经在实践这个合集讨论的工程方法。技术数量并不能替代这些基础。
动手想一想
题目 1
worker已经写入摘要,但确认任务时网络断开。队列再次投递任务,怎样避免产生重复结果?
提示
任务身份应该跨越进程重启存在,结果提交与重复检查要有可靠的一致性约束。
答案
使用稳定幂等键,例如笔记ID、正文版本和算法版本的组合,并通过数据库唯一约束或合适事务保证重复提交可识别。确认失败后的再次执行先检查相同任务结果,不能只靠内存记录。
题目 2
服务网格配置了自动重试,应用还能不考虑幂等性吗?
提示
网络层知道请求是否产生了业务副作用吗?
答案
不能。超时可能发生在业务执行之后,网络层未必知道。应用需要设计幂等语义,并统一不同层的重试预算,防止重复副作用和请求放大。
题目 3
一个低频任务迁到按需启动平台后成本下降,但第一次请求变慢。该如何判断是否值得?
提示
比较节省的空闲资源与用户等待时间,并考虑可选的异步流程或最小实例数。
答案
先确认冷启动和依赖初始化是否是延迟来源,再对照业务时延要求。可以选择异步交互、预热或最小实例数,也可以保留常驻服务。成本与体验的权衡应来自实际负载,不能只以平台名称决定。
