康心伴Logo
康心伴WellAlly
开发

健身应用后端:Node.js单体到微服务的5倍扩展 | WellAlly康心伴

5 分钟阅读

关键要点

  • 绞杀者无花果模式降低迁移风险:不是冒险的"大爆炸"重写,而是在现有单体周围逐步构建微服务并增量重定向流量——允许你独立测试和回滚每个部分。
  • 独立数据库确保松耦合:每个微服务拥有自己的数据库可防止共享模式锁定并允许独立扩展,尽管它引入了跨服务事务需要Saga等分布式模式的挑战。
  • Kubernetes自动化容器编排:Kubernetes处理容器化微服务的部署扩展、容错和服务发现,实现零停机部署和自动故障恢复。
  • API网关提供统一入口:API网关将请求路由到适当的服务,处理认证、速率限制和响应聚合——向客户端应用隐藏微服务的分布式本质。
  • 消息代理实现异步通信:对于不需要立即响应的事件,RabbitMQ等消息代理解耦服务并提高弹性,允许它们以自己的节奏处理事件。

在竞争激烈的健身科技领域,应用性能和可扩展性至关重要。一个迟缓的应用可能意味着忠实用户和流失用户之间的差距。本案例研究详细介绍了"FitTrack"(一个虚构但具有代表性的健身应用)将其后端从单体应用迁移到分布式微服务系统的历程。我们将探讨这一重大架构转变背后的动机、分解单体的过程,以及使这一切成为可能的技术——Node.js、Docker和Kubernetes。

本文面向面临快速增长的应用挑战的开发者和工程负责人。如果你的团队正在为缓慢的开发周期、部署瓶颈和难以扩展特定功能而苦恼,这个真实世界的叙述将为你提供成功迁移的实用路线图。

前提条件:

  • 对Node.js和RESTful API有扎实的理解。
  • 熟悉Docker和容器化的基本概念。
  • 对微服务架构有高层次的理解。

理解问题

FitTrack以经典的单体架构启动。一个Node.js应用处理所有事务:用户认证、训练记录、社交互动和数据报告。这种方法非常适合初始发布,允许快速开发和轻松部署。然而,随着用户群的指数级增长,单体开始显现裂痕。

增长中的单体之痛:

  • 扩展效率低下:用户创建社交帖子的激增意味着需要扩展整个应用,即使训练追踪和用户资料部分负载正常。这导致了不必要的基础设施成本。
  • 开发瓶颈:越来越多的开发者同时在同一个代码库上工作,导致了合并冲突、复杂的依赖关系,以及害怕做出可能破坏整个应用的更改。
  • 部署风险:社交信息流功能中的一个小bug可能导致整个应用瘫痪,阻止用户记录训练——一个关键功能。部署变成了高压的、孤注一掷的事件。
  • 技术栈僵化:单体架构使得难以采用可能对特定功能有益的新技术或语言。

FitTrack团队意识到,要支持增长并加快创新,他们需要一个更灵活、可扩展和弹性的架构。迁移到微服务不再是是否的问题,而是何时如何的问题。

前提条件

在开始迁移之前,团队建立了必要的工具和本地开发环境:

  • Node.js(v18或更高版本):微服务的核心运行时。
  • Docker Desktop:用于在本地构建和运行容器化服务。
  • Minikube:模拟本地Kubernetes集群用于开发和测试。
  • kubectl:用于与Kubernetes集群交互的命令行工具。
  • 每个新微服务一个Git仓库:独立管理代码。

第1步:分解单体——绞杀者无花果模式

团队选择了绞杀者无花果模式,而非冒险且耗时的"大爆炸"重写。这种方法涉及在现有单体周围逐步构建新的微服务,慢慢"绞杀"其功能,直到可以退役。

我们要做什么

第一步是识别单体内不同的业务能力并将它们分解为逻辑服务。在领域驱动设计(DDD)的指导下,这一过程产生了以下初始服务划分:

  • 用户服务:负责用户资料、认证和设置。
  • 训练服务:处理训练数据的创建、检索、更新和删除(CRUD)。
  • 社交服务:管理社交信息流,包括帖子、点赞和评论。

每个服务将拥有自己独立的数据库以确保松耦合。

实现代码

团队从用户服务开始。他们创建了一个新的Node.js项目,配备专用的PostgreSQL数据库。

code
// 用户服务的简化视图(使用Express.js)
// users-service/index.js
const express = require('express');
const app = express();
const port = 3001;

app.use(express.json());

// 虚拟用户数据
const users = [
  { id: 1, name: 'Alex', email: 'huifer97@163.com' },
  { id: 2, name: 'Maria', email: 'huifer97@163.com' },
];

app.get('/users/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) {
    return res.status(404).send('User not found');
  }
  res.json(user);
});

app.listen(port, () => {
  console.log(`Users service listening on port ${port}`);
});
Code collapsed

工作原理

引入了一个API网关作为所有客户端请求的统一入口。最初,网关将大部分流量路由到单体。然而,对于 /api/users/:id 的请求,它现在会将其导向新的用户服务。这种渐进式转移对终端用户是透明的。

第2步:使用Docker容器化服务

在第一个微服务定义好后,下一步是使用Docker对其进行容器化。这将确保每个服务从本地开发到生产环境的一致性和可重复性。

我们要做什么

为每个微服务创建了一个 Dockerfile。该文件包含构建Docker镜像的指令,包括基础Node.js镜像、安装依赖和指定运行应用的命令。

实现代码

code
# users-service/Dockerfile

# 使用官方轻量级Node.js镜像
FROM node:18-alpine

# 设置容器中的工作目录
WORKDIR /usr/src/app

# 复制package.json和package-lock.json
COPY package*.json ./

# 安装生产依赖
RUN npm install --only=production

# 复制其余应用代码
COPY . .

# 暴露应用运行的端口
EXPOSE 3001

# 运行应用的命令
CMD [ "node", "index.js" ]
Code collapsed

工作原理

使用此 Dockerfile,开发者可以用两个简单的命令在容器中构建和运行用户服务:

code
# 构建Docker镜像
docker build -t fittrack-users-service .

# 运行容器,将端口3001映射到主机
docker run -p 3001:3001 fittrack-users-service
Code collapsed

此过程对训练服务社交服务重复进行,每个都有自己的 Dockerfile 并在不同的端口上运行。

第3步:使用Kubernetes编排

在本地机器上运行多个Docker容器是一回事;在生产环境中管理它们并满足扩展、容错和零停机部署的要求则是另一回事。这就是Kubernetes发挥作用的地方。

我们要做什么

团队使用Kubernetes自动化容器化微服务的部署、扩展和管理。他们创建了Kubernetes清单文件(YAML格式)来定义每个服务的期望状态。

实现代码

以下是用户服务的简化 deployment.yaml

code
# users-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-service-deployment
spec:
  replicas: 2 # 从2个实例开始以实现高可用性
  selector:
    matchLabels:
      app: users-service
  template:
    metadata:
      labels:
        app: users-service
    spec:
      containers:
      - name: users-service
        image: fittrack-users-service:latest # 要使用的Docker镜像
        ports:
        - containerPort: 3001
Code collapsed

为了在集群内暴露部署,创建了 service.yaml

code
# users-service-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: users-service
spec:
  selector:
    app: users-service
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3001
Code collapsed

工作原理

这些YAML文件使用 kubectl apply -f <文件名> 应用到Kubernetes集群。Kubernetes然后确保始终运行两个 users-service 容器实例(副本)。Kubernetes的 Service 为部署提供稳定的IP地址和DNS名称,允许其他服务与之通信而无需知道各个容器的IP。

整合在一起:服务间通信

微服务架构中的一个主要挑战是管理服务间的通信。例如,社交服务如何获取发帖用户的姓名和头像?

团队决定了混合通信方式:

  1. 同步通信(REST API):对于直接的、实时的数据请求,服务通过RESTful HTTP调用通信。例如,社交服务会向用户服务(http://users-service/users/:id)发起 GET 请求以获取用户详情。Kubernetes服务发现机制使这变得无缝。

  2. 异步通信(消息代理):对于不需要立即响应的事件,团队实现了消息代理(RabbitMQ)。当用户创建新训练时,训练服务会发布 workout_created 事件。其他服务,如社交服务(生成"新训练"帖子)或未来的成就服务,可以订阅此事件并做出相应反应。这解耦了服务并提高了弹性。

克服挑战:数据、监控和部署

迁移并非没有困难:

  • 数据一致性:由于使用独立数据库,跨服务维护数据一致性成为挑战。团队使用Saga模式处理跨越多个服务的复杂事务。该模式将事务分解为一系列本地事务,如果某一步失败,则通过补偿事务回滚更改。

  • 监控和日志:由于请求在多个服务之间跳转,定位错误来源变得困难。实施了使用ELK Stack(Elasticsearch、Logstash、Kibana)Prometheus + Grafana的集中式日志和监控解决方案。这提供了整个系统健康和性能的统一视图。

  • CI/CD流水线:团队使用GitHub Actions为每个微服务设置了独立的CI/CD流水线。推送到服务仓库主分支会自动触发测试、构建新的Docker镜像、推送到容器注册表,并以零停机将更新部署到Kubernetes集群。

结论

FitTrack从单体到微服务架构的迁移是一项重大的工程,但带来的变革性收益是显著的:

  • 改善的可扩展性:团队现在可以根据需求单独扩展各个服务。
  • 提高的开发效率:更小的、专注的团队可以独立开发、测试和部署他们的服务。
  • 增强的弹性:一个服务的问题不再导致整个应用瘫痪。
  • 技术灵活性:团队现在可以为每个工作选择最佳的工具。

本案例研究表明,通过周密的规划、正确的工具和分阶段的方法,从单体到微服务的成功迁移是可以实现的,并可以为增长中的应用释放巨大潜力。

资源

常见问题解答

问:单体什么时候"太大"需要迁移到微服务?

答:关键指标包括:部署时间超过可接受范围、团队在代码中频繁冲突、无法独立扩展组件、不同功能需要不同的技术栈。如果你还没有从单体中感受到显著痛苦,微服务的复杂性可能还值得等待。

问:当每个服务有独立的数据库时,如何处理跨多个服务的事务?

答:你需要分布式事务模式。Saga模式将事务分解为一系列本地事务,每个都有用于回滚的补偿事务。或者,使用事件驱动架构,服务发出关于状态变化的事件,其他服务异步响应——以最终一致性换取可用性。

问:有效管理多个微服务的推荐团队结构是什么?

答:使用康威定律将团队与业务能力对齐——一个"两个披萨团队"(6-10人)端到端地拥有一个或几个相关服务。每个团队需要开发、测试和运维技能。避免让一个团队管理所有服务,那会重新创建单体的瓶颈;相反,让团队通过API契约和文档协作。

问:当请求流经多个微服务时如何调试问题?

答:使用Jaeger、Zipkin或AWS X-Ray等工具实施分布式追踪。为每个请求分配一个跨服务传播的追踪ID,在每个跳跃记录时间和元数据。结合集中式日志(ELK stack、CloudWatch)和适当的相关ID来追踪问题请求的完整旅程。

问:迁移到微服务的成本影响是什么——会增加基础设施费用吗?

答:微服务通常会增加基础设施成本,因为有开销(多个运行时、数据库、网络重复)。然而,这可以通过更高效的扩展来抵消——只扩展需要的部分。真正的成本在于运维复杂性、监控工具和开发者的认知负荷。确保业务收益在迁移前能证明这些成本的合理性。

相关文章

#

文章标签

Node.js
Docker
Kubernetes
架构
微服务
扩展

觉得这篇文章有帮助?

立即体验康心伴,开始您的健康管理之旅