把单体换到微服务时踩过的坑
如果只能用一句话说把单体换到微服务时踩过的坑:先把失败复现出来。
最初的单体架构
简单的单体
// 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/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。