把单体换到微服务时踩过的坑

如果只能用一句话说把单体换到微服务时踩过的坑:先把失败复现出来。

最初的单体架构

简单的单体

// app.js
const express = require('express');
const app = express();

// 用户相关
app.get('/api/users/:id', async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
  res.json(user);
});

// 订单相关
app.post('/api/orders', async (req, res) => {
  const order = await db.query('INSERT INTO orders SET ?', req.body);
  res.json(order);
});

// 支付相关
app.post('/api/payments', async (req, res) => {
  const payment = await db.query('INSERT INTO payments SET ?', req.body);
  res.json(payment);
});

app.listen(3000);

优势

  • 简单直接
  • 开发效率高
  • 部署容易

劣势

  • 难以扩展
  • 代码耦合严重
  • 技术栈受限

什么时候需要拆分

判断标准

团队规模

  • 小团队(<5 人):单体就好
  • 中等团队(5-20 人):考虑拆分
  • 大团队(>20 人):必须拆分

业务复杂度

  • 简单业务:单体就好
  • 复杂业务:考虑拆分

流量规模

  • 小流量:单体就好
  • 大流量:考虑拆分

技术需求

  • 技术栈统一:单体就好
  • 技术栈多样:考虑拆分

拆分策略

按业务域拆分

// 用户服务
// user-service/app.js
const express = require('express');
const app = express();

app.get('/api/users/:id', async (req, res) => {
  const user = await db.query('SELECT * FROM users WHERE id = ?', [req.params.id]);
  res.json(user);
});

app.listen(3001);

// 订单服务
// order-service/app.js
const express = require('express');
const app = express();

app.post('/api/orders', async (req, res) => {
  const order = await db.query('INSERT INTO orders SET ?', req.body);
  res.json(order);
});

app.listen(3002);

按功能拆分

用户域:
- 用户服务
- 认证服务
- 权限服务

订单域:
- 订单服务
- 支付服务
- 物流服务

数据拆分

数据库拆分

-- 拆分前:单库
CREATE DATABASE myapp;
USE myapp;
CREATE TABLE users (...);
CREATE TABLE orders (...);
CREATE TABLE payments (...);

-- 拆分后:分库
CREATE DATABASE user_db;
CREATE DATABASE order_db;

-- user_db
CREATE TABLE users (...);

-- order_db
CREATE TABLE orders (...);
CREATE TABLE payments (...);

数据同步

// 使用消息队列同步数据
const orderCreated = async (order) => {
  // 更新订单服务数据库
  await orderDb.query('INSERT INTO orders SET ?', order);
  
  // 发送消息到消息队列
  await mq.publish('order.created', order);
};

// 用户服务消费消息
mq.subscribe('order.created', async (order) => {
  // 更新用户服务的订单统计
  await userDb.query(
    'UPDATE user_stats SET order_count = order_count + 1 WHERE user_id = ?',
    [order.user_id]
  );
});

服务通信

HTTP 通信

// 使用 axios
const axios = require('axios');

class UserService {
  async getUser(userId) {
    const response = await axios.get(`http://user-service:3001/api/users/${userId}`);
    return response.data;
  }
}

// 使用 fetch
class OrderService {
  async createOrder(order) {
    const user = await fetch(`http://user-service:3001/api/users/${order.user_id}`)
      .then(res => res.json());
    
    const response = await fetch(`http://order-service:3002/api/orders`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(order)
    });
    
    return response.json();
  }
}

gRPC 通信

// user.proto
syntax = "proto3";

package user;

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  int32 id = 1;
}

message GetUserResponse {
  User user = 1;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
}
// 使用 grpc
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDefinition = protoLoader.loadSync('user.proto', {});
const userProto = grpc.loadPackageDefinition(packageDefinition).user;

const client = new userProto.UserService('user-service:50051', grpc.credentials.createInsecure());

function getUser(id, callback) {
  client.getUser({ id }, (error, response) => {
    if (error) {
      callback(error, null);
    } else {
      callback(null, response.user);
    }
  });
}

API 网关

Nginx 网关

upstream user_service {
    server user-service:3001;
}

upstream order_service {
    server order-service:3002;
}

server {
    listen 80;
    server_name api.example.com;

    location /api/users/ {
        proxy_pass http://user_service/;
    }

    location /api/orders/ {
        proxy_pass http://order_service/;
    }
}

Kong 网关

# 添加用户服务
curl -X POST http://localhost:8001/services \
  --data "name=user-service" \
  --data "url=http://user-service:3001"

# 添加路由
curl -X POST http://localhost:8001/services/user-service/routes \
  --data "paths[]=/api/users"

# 添加订单服务
curl -X POST http://localhost:8001/services \
  --data "name=order-service" \
  --data "url=http://order-service:3002"

# 添加路由
curl -X POST http://localhost:8001/services/order-service/routes \
  --data "paths[]=/api/orders"

服务发现

服务注册

// 使用 Consul
const Consul = require('consul');

const consul = new Consul();

// 注册服务
consul.agent.service.register({
  name: 'user-service',
  port: 3001,
  check: {
    http: 'http://localhost:3001/health',
    interval: '10s'
  }
}, (err) => {
  if (err) throw err;
});

// 注销服务
consul.agent.service.deregister('user-service', (err) => {
  if (err) throw err;
});

服务发现

// 查找服务
consul.agent.service.list((err, services) => {
  if (err) throw err;
  
  const userService = services['user-service'];
  console.log(userService);
});

// 健康检查
consul.health.service('user-service', (err, result) => {
  if (err) throw err;
  
  const healthyServices = result.filter(service => service.Checks[0].Status === 'passing');
  console.log(healthyServices);
});

踩过的坑

坑一:过度拆分

一开始把每个功能都拆成独立服务,结果服务太多,管理困难。

解决:合理规划服务边界,不要过度拆分。

坑二:分布式事务

微服务之间的事务一致性难以保证。

解决:使用最终一致性、Saga 模式。

// Saga 模式
class CreateOrderSaga {
  async execute(order) {
    try {
      // 步骤 1:创建订单
      const createdOrder = await orderService.create(order);
      
      // 步骤 2:扣减库存
      await inventoryService.deduct(order.items);
      
      // 步骤 3:创建支付
      const payment = await paymentService.create(order);
      
      return { order: createdOrder, payment };
    } catch (error) {
      // 补偿操作
      await this.compensate(order);
      throw error;
    }
  }
  
  async compensate(order) {
    try {
      await orderService.cancel(order.id);
      await inventoryService.restore(order.items);
    } catch (error) {
      // 记录错误,需要人工处理
      logger.error('Compensation failed:', error);
    }
  }
}

坑三:服务间调用复杂

服务间调用复杂,容易出现循环调用。

解决:理清服务依赖,避免循环调用。

graph TB A[网关] --> B[用户服务] A --> C[订单服务] A --> D[支付服务] B -.->|避免| C C --> D style A fill:#FFD700 style D fill:#FFB6C1

写在最后

架构演进这东西,不是技术问题,是业务问题。

解决了

  • 扩展性问题
  • 团队协作问题
  • 技术选型问题

带来了

  • 复杂度增加
  • 运维成本
  • 数据一致性挑战

拆分之前先评估:

  • 团队规模
  • 业务复杂度
  • 技术能力
  • 运维能力

不是所有系统都需要拆分,有时候单体架构更合适。


这次架构演进花了三个月,从单体到微服务。演进完成后,团队能并行开发,部署速度提升了 3 倍,但运维成本也增加了。

版权声明: 本文首发于 指尖魔法屋-把单体换到微服务时踩过的坑https://blog.thinkmoon.cn/post/73-architecture-evolution-monolith-microservices-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!