0Pricing
PHP Academy · 课时

从单体架构到微服务

决定拆分内容以及如何划分服务边界

从单体架构到微服务 是 CoddyKit 上的免费 PHP Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 PHP Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 PHP Academy 课程共包含 4 节课。

从单体到微服务

微服务是一项组织和运维层面的选择,而不是默认方案。将 PHP 单体拆分开来,意味着用网络调用、分布式数据和部署复杂性,换取进程内的简单性。如果理由不当,这样做只会让一切变慢。

本课讨论最困难的部分:划分服务边界。切分点找对了,剩下的只是基础设施工作;找错了,您就会构建出一个分布式单体——承担了全部成本,却没有任何收益。

为什么拆分(以及为什么不拆分)

适合拆分的理由:

  • 能够独立部署,并由团队独立负责。
  • 能够对热点子系统进行独立扩展。
  • 实现技术栈、运行时隔离以及故障隔离。

不好的理由包括:“代码很凌乱”(应先重构单体),或者只是追逐潮流。如果两个服务始终必须一起部署,或共享同一张数据库表,那么它们其实是一个服务,只是扮演了两个角色。

有界上下文

领域驱动设计为边界划分提供了最有力的工具:有界上下文。在同一个上下文中,术语具有唯一且明确的含义。在计费领域中,“客户”是发票对象;而在支持领域中,“客户”则是工单作者,两者并不相同。

每个有界上下文都是服务的有力候选。边界应遵循业务语言以及组织实际沟通的方式——这正是康威定律的体现。

高内聚、低耦合

良好的服务边界会将一起变化的内容保留在内部,将独立变化的内容排除在外。您可以通过以下方面评估一次拆分:

  • 有多少功能需要同时修改两个服务?(应该很少。)
  • 两个服务之间的调用模式有多频繁?(应该采用粗粒度调用。)

如果实现一个功能时总是要跨越某个边界,那么这个边界就划分错了位置。

<?php
// Chatty boundary smell: N network calls to render one view
foreach ($order->lineItems as $item) {
    $product = $catalogApi->get($item->productId); // one call PER item!
    $names[] = $product['name'];
}

// Coarse-grained: one batch call across the boundary
$ids   = array_map(fn($i) => $i->productId, $order->lineItems);
$names = $catalogApi->getMany($ids); // single round-trip
echo count($names) . " products in one call\n";

每个服务独立数据库

不可妥协的规则是:每个服务拥有自己的数据,其他服务不得访问它的表。共享数据库会重新造成您拆分服务时想要摆脱的耦合——一次模式变更就可能破坏无关的服务。

这意味着,单体中的 SQL JOIN 跨服务查询,会变成接口调用或复制的读取模型。这就是代价,也是拆分的意义所在。

<?php
// In the monolith: one JOIN across domains
$sql = 'SELECT o.id, c.email
        FROM orders o JOIN customers c ON c.id = o.customer_id';

// After split: Orders service holds only the foreign id;
// it asks the Customers service for the rest (or keeps a local read model).
$order = $orderRepo->find($id);          // local
$customer = $customersApi->get($order->customerId); // network call
echo $order->id . ' / ' . $customer->email . "\n";

绞杀藤模式

绝不要一次性重写整个单体。绞杀藤模式会逐步提取功能:通过外观或代理将一部分流量路由到新服务,逐步扩大范围,只有当新服务完全覆盖旧代码路径后,才停用旧路径。

反向代理(或 API 网关)位于前端,根据路由决定访问旧单体还是新服务。迁移一次只处理一项能力,并且每一步都可以交付。

<?php
// Facade routing: peel off one capability at a time
function route(string $path): string {
    $migrated = ['/invoices', '/invoices/pdf']; // moved to billing-svc
    foreach ($migrated as $prefix) {
        if (str_starts_with($path, $prefix)) {
            return 'http://billing-svc' . $path;
        }
    }
    return 'http://legacy-monolith' . $path; // everything else, for now
}
echo route('/invoices/pdf'), "\n";
echo route('/users/42'), "\n";

提取模块

一种务实的提取顺序是:

  1. 找到一个入站依赖较少且数据归属清晰的模块。
  2. 先在单体中通过接口封装该模块的进程内调用。
  3. 将它拥有的数据迁移到自己的模式或数据库中。
  4. 用网络客户端替换接口实现。
  5. 通过外观切换流量,然后删除旧代码。

先在单体内部完成接口封装,可以降低网络步骤的风险。

<?php
// Step 2: hide the implementation behind a port the monolith calls
interface InvoiceService {
    public function generate(string $orderId): string; // returns invoice id
}

// Today: local class. Tomorrow: HTTP client to billing-svc.
// The monolith's calling code never changes.
final class LocalInvoiceService implements InvoiceService {
    public function generate(string $orderId): string { return 'inv-1'; }
}

分布式数据一致性

数据拆分后,您将失去跨服务的 ACID 事务。请接受最终一致性:服务发布关于自身数据的事件,其他服务则根据这些事件构建本地读取模型。

订单服务不会在每次请求时都查询客户服务,而是保留一个小型投影(只包含所需字段),由 CustomerUpdated 事件更新。这样可以消除运行时依赖和一次网络延迟。

<?php
// Orders service keeps a tiny local projection of customer data
function onCustomerUpdated(PDO $db, array $evt): void {
    $db->prepare(
        'INSERT INTO customer_read_model (id, email)
         VALUES (:id, :email)
         ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email'
    )->execute(['id' => $evt['id'], 'email' => $evt['email']]);
}

运维现实

微服务会将复杂性从代码转移到运维。在拆分之前,您需要具备:

  • 集中式日志记录和分布式追踪(在各次调用之间传递关联 ID)。
  • 每个服务独立的 CI/CD 和带版本的独立部署。
  • 在每次调用上配置健康检查、超时、重试和断路器。
  • 契约测试,以防提供方悄悄破坏消费者。

如果您的团队无法把一个服务运行好,那么运行十个服务只会糟糕十倍。

合理确定服务规模

“微”这个词容易误导——应根据业务能力和团队负责范围确定服务规模,而不是根据代码行数。粒度过细(纳米服务)时,一个功能就会引发大量网络调用;粒度过粗时,您又回到了单体。

一个健康的判断标准是:一个服务应由一个团队负责,能够独立部署,并且无需通过许多同级服务组成的同步调用链,就能完成自己的核心用例。

模块化单体:另一种选择

在采用分布式架构之前,可以考虑模块化单体:只有一个可部署单元,但内部划分为边界明确的模块,每个模块拥有自己的模式,并且只能通过已发布的接口通信——不得跨模块访问数据表。

您可以在没有分布式系统成本的情况下获得清晰的边界和便捷的重构能力。当某个模块确实需要独立扩展或独立负责时,它已经具备了通过绞杀藤模式拆出的形态。对大多数团队来说,这是最合适的第一步。

<?php
// Modules talk only through interfaces, never each other's tables.
namespace App\Billing;          // owns billing_* tables
interface BillingFacade {
    public function invoiceForOrder(string $orderId): string;
}

namespace App\Sales;            // owns sales_* tables
final class Checkout {
    public function __construct(private \App\Billing\BillingFacade $billing) {}
    // Sales never SELECTs from billing_* directly - only via the facade.
}

快速检查

识别错误的拆分方式。

回顾

合理划分服务边界:

  • 为了可部署性、扩展能力和负责范围而拆分,而不是因为代码很凌乱。
  • 让边界与有界上下文保持一致;追求高内聚、低耦合。
  • 每个服务独立数据库——不共享表;用接口或读取模型替代 JOIN。
  • 使用绞杀藤模式逐步迁移。
  • 接受最终一致性,并在横向扩展之前投入运维能力建设。

下一步:了解这些服务实际如何通信——REST 和 gRPC。

常见问题解答

「从单体架构到微服务」课时是免费的吗?

是的 — 「从单体架构到微服务」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 PHP Academy 课程的其余内容,请升级到 CoddyKit PRO。 PHP Academy 课程共包含 4 节课。

「从单体架构到微服务」这节课中我会学到什么?

决定拆分内容以及如何划分服务边界 你通过在浏览器中直接运行的动手代码来练习 PHP Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 PHP Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 PHP Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「从单体架构到微服务」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 PHP Academy 课中编写并运行代码吗?

能。每节 PHP Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 从单体架构到微服务
  2. 服务通信:REST 和 gRPC
  3. API 网关与服务发现
  4. 韧性:熔断器与重试
← 返回 PHP Academy