Slices:微服务的合适规模
Source: Dev.to
请提供您希望翻译的正文内容,我将为您将其翻译成简体中文并保留原有的格式、Markdown 语法以及技术术语。谢谢!
粒度陷阱
每个采用微服务的团队最终都会碰到同一道墙:服务应该有多大?
-
如果划得太小,你会被网络调用、分布式事务和部署复杂性淹没。
- 你原本简单的“获取用户资料”操作现在涉及五个服务,其中三个只是数据库表的代理。
- 延迟会叠加。
- 调试变成考古。
-
如果划得太大,又回到了单体。
- 不同团队相互踩踏。
- 部署需要协调。
- “微”服务的“微”字变得讽刺。
标准建议——“每个有界上下文对应一个服务” 或 “服务应可独立部署”——听起来合理,却没有提供可操作的指引。
- 一个上下文在哪里结束,另一个在哪里开始?
- 什么才算“可独立部署”?
团队在两端摇摆,把*“太小”的服务重构为更大的服务,又把“太大”*的服务拆分。循环往复,因为根本问题仍未得到解答:决定正确边界的因素是什么?
问题不在于大小,而在于边界。
一个定义良好的边界具有以下特性:
- 清晰的契约——调用方确切知道可以请求什么以及会收到什么。
- 显式的依赖——组件声明它需要从外部获取的内容。
- 内部自由——实现细节可以改变而不影响调用方。
大小是由边界决定的,而不是相反。当一个组件完全拥有它的边界——即满足其契约所需的一切都在内部,而所有外部的访问都通过显式依赖进行时,它的规模就是恰当的。
大多数微服务设计失败的原因在于它们基于以下方式划定边界:
- 技术层次(API 网关、业务逻辑、数据库访问)
- 组织结构(团队所有权)
这两种方法都无法产生稳定的边界,因为它们都没有聚焦于组件之间的实际契约。
介绍 Slices
Slice 是由其契约定义的可部署单元。你只需编写一个带有单一注解的接口:
@Slice
public interface OrderService {
Promise createOrder(CreateOrderRequest request);
Promise getOrder(GetOrderRequest request);
Promise cancelOrder(CancelOrderRequest request);
}
就这么简单。注解处理器会生成其余所有内容——工厂方法、依赖注入、部署元数据。
接口即边界
- 方法定义契约 —— 每个方法接受一个请求并返回
Promise类型的响应。 - 请求/响应类型显式 —— 没有隐藏参数,也没有隐式上下文。
- 默认异步 ——
Promise同时处理成功和失败路径。
实现位于该接口之后。实现可以很简单,也可以很复杂,调用其他 slice,或完全自包含。边界本身并不关心实现细节。
Slices 运行在 Aether 上,这是一个围绕 slice 合约设计的分布式运行时。你无需配置服务发现、序列化或跨 slice 通信——Aether 会根据 slice 接口的声明自动处理。只要集群存活,所有跨 slice 调用最终都会成功;运行时会透明地管理重试、故障转移和恢复。
Forge 为在真实条件下测试 slice 提供了开发环境——包括负载生成、混沌注入、后端仿真。你不必部署到预发布环境来观察 slice 在高压下的表现,只需在本地运行 Forge 并观察即可。
开发体验保持简洁:
- 编写
@Slice接口。 - 实现接口。
- 使用 Forge 进行测试。
- 部署到 Aether。
注解处理器会生成所有样板代码——工厂、依赖注入、路由元数据。
显式依赖
传统服务架构把依赖埋在配置文件、环境变量或运行时发现机制中。你只能通过阅读代码、追踪网络调用,或等到生产环境出错后才发现服务需要什么。
Slices 在 接口中直接声明依赖:
@Slice
public interface OrderService {
Promise createOrder(CreateOrderRequest request);
// Other methods...
// 工厂方法显式声明依赖
static OrderService orderService(InventoryService inventory,
PaymentService payment) {
return OrderServiceFactory.orderService(Aspect.identity(),
inventory, payment);
}
}
注解处理器会生成负责注入的工厂:
public final class OrderServiceFactory {
private OrderServiceFactory() {}
public static OrderService orderService(
Aspect aspect,
InventoryService inventory,
PaymentService payment) {
return aspect.apply(new OrderServiceImpl(inventory, payment));
}
}
- 工厂方法的签名 声明了依赖。
- 没有服务定位器、没有运行时发现、也不需要可能与实际不符的配置文件。
- 依赖在编译时即可可见,并在部署前得到验证。
这种显式性很重要:
- 通过阅读代码即可追踪依赖图。
- 通过传入不同实现,可以使用替代品进行测试。
- Forge 在启动任何东西之前会验证完整的依赖图。
三种运行时模式(相同的 Slice 代码)
| 模式 | 描述 |
|---|---|
| Ember | 单进程运行时,内部模拟多个集群节点。启动快,调试简单。适合本地开发。 |
| Forge | Ember + 负载生成和混沌注入。无需部署即可测试 slice 在高压下的行为。 |
| Aether | 完整的分布式集群。生产部署,提供全部弹性保证。 |
你的代码并不知道自己运行在哪种模式下。Slice 接口、实现以及依赖保持不变。
保持一致。运行时处理差异——无论 slice 之间的调用是进程内还是跨网络,都是透明的。
对开发工作流的影响
- 在 Ember 中编写 & 调试 – 快速的进程内执行。
- 在 Forge 中进行压力测试 – 注入负载和故障。
- 部署到 Aether – 生产级弹性。
在任何阶段都不需要重写合同、修改配置或为环境调整代码。slice 模型让你专注于 边界,而不是服务的规模。
切片及其“合适大小”
你会修改切片代码以适应环境吗?
当边界明确且部署灵活时,“合适大小”的问题就不复存在了。
何时切片是合适的大小?
- 接口 – 捕获一组连贯的操作。
- 依赖 – 准确反映切片实际需要的内容。
- 实现 – 能够满足其契约。
没有最小或最大限制。
- 一个认证切片可能只暴露两个方法。
- 一个订单处理切片可能暴露二十个方法。
大小取决于领域,而非关于代码行数或团队结构的任意规则。
可恢复性
切片出错是可恢复的:
| 情况 | 会发生什么 |
|---|---|
| 拆分 一个变得过于复杂的切片 | 边界会改变,但调用方只会看到新的接口。 |
| 合并 被人为分离的切片 | 调用方仍然只会看到单一接口;内部实现仅仅被合并。 |
重构切片仅仅是重构代码,而不是重写基础设施。
切片作为 JBCT 模式的归宿
每个切片方法都是一个 data‑transformation pipeline:
Parse input (validated request types)
↓
Gather data (dependencies, other slices)
↓
Process (business logic)
↓
Respond (typed response)
六大 JBCT 模式——Leaf、Sequencer、Fork‑Join、Condition、Iteration、Aspects——在 切片方法内部以及跨切片之间 进行组合。切片仅仅是围绕一组相关转换的 部署边界。
为什么这种组合有效
- JBCT 为切片内部提供 一致的结构。
- 切片 为切片之间提供 一致的边界。
二者共同消除了导致架构熵增的两个主要来源:
- 实现模式不一致
- 服务边界不清晰
从服务粒度到契约中心设计
微服务粒度问题仍然存在,因为它提出了错误的问题:
- 错误的问题: “服务应该有多大?” – 没有好的答案。
- 正确的问题: “组件之间的契约是什么?” – 可以给出精确的答案。
Slices 将关注点从规模转向边界:
- 定义接口。
- 显式声明依赖。
- 让部署拓扑适应 运营需求,而不是决定代码结构。
结果
- 边界 通过构造方式 清晰可见。
- 依赖 通过设计方式 显而易见。
- 部署灵活性 无需重写代码即可实现。
属于 Java Backend Coding Technology——一种编写可预测、可测试后端代码的方法论。