微服务拆分,我差点把公司系统搞崩了
上周五晚上十点半,我瘫在工位上盯着屏幕上密密麻麻的链路追踪图,心里一万只草泥马奔腾而过。这已经是连续第三天加班到深夜了,原因很简单——我们那个运行了快五年的单体应用,终于要向微服务架构迈进了。
作为Claude Code的早期尝鲜用户,我平时就喜欢在命令行里折腾各种新工具,但这次的微服务迁移可不是简单的技术升级。我在上海租房住在公司附近,早上8点就能开工,本来以为能从容应对这次架构改造,结果现实狠狠给我上了一课。
事情还得从上个月说起。产品经理拿着一份"区块链+供应链金融"的需求文档找到我们,说是要打造一个去中心化的交易验证平台。我当场就懵了——我们的老系统是用PHP写的单体架构,连基本的模块化都没做好,现在突然要集成区块链,还要支持高并发的交易验证?这不是开玩笑嘛!
更离谱的是,CTO直接拍板:三个月内必须上线,而且要保证99.99%的可用性。我当时真的想砸电脑,但想到下个月的房租还没着落,只能硬着头皮接了下来。
为什么非得拆微服务?
其实我们团队内部对是否要拆微服务一直有争议。运维老王觉得单体应用挺好的,部署简单,排查问题也方便;测试小李担心接口多了测试工作量会爆炸;只有我和几个后端同事觉得,再不拆迟早要出大事。
去年双11期间,我们的订单服务因为一个小小的数据库死锁,导致整个系统瘫痪了40分钟。那时候我就意识到,单体架构已经成了我们的瓶颈。所有的业务逻辑都耦合在一起,改一个功能可能会影响到其他十几个模块,简直就是"牵一发而动全身"。
这次区块链需求成了压垮骆驼的最后一根稻草。区块链相关的交易验证、智能合约调用、数据上链等功能,和我们现有的电商逻辑完全不是一个画风。如果硬塞进单体应用里,代码质量肯定会进一步恶化。
所以,拆!必须拆!
技术选型:Go还是Java?
确定要拆微服务后,第一个大问题是用什么语言重写核心服务。我们现有的系统是PHP,但没人愿意继续用PHP写微服务(别打我,PHP是最好的语言,但在微服务场景下确实不太合适)。
团队里分成了两派:
- Java派:觉得Spring Cloud生态成熟,各种组件齐全
- Go派:觉得Go的性能好,部署简单,内存占用低
作为Go的忠实粉丝,我自然是Go派的坚定支持者。但光靠情怀说服不了CTO,得拿数据说话。于是我花了两天时间做了个简单的性能对比测试:
| 指标 | Spring Boot (Java 11) | Go (1.19) |
|---|---|---|
| 启动时间 | 3.2s | 0.15s |
| 内存占用 | 350MB | 15MB |
| QPS (简单API) | 2800 | 12000 |
| Docker镜像大小 | 280MB | 18MB |
看到这个数据,连最顽固的Java派都沉默了。特别是在容器化部署的场景下,Go的优势太明显了。我们的K8s集群资源本来就紧张,用Go能省下不少钱。
更重要的是,Go的goroutine模型特别适合处理区块链场景下的高并发请求。想象一下,每个交易验证都需要和区块链节点通信,这种I/O密集型的操作用Go来处理简直是天作之合。
最终CTO拍板:核心的交易验证服务、区块链交互服务用Go重写,其他边缘服务可以慢慢迁移。
拆分策略:别一口吃成胖子
有了技术栈,接下来就是怎么拆的问题了。我见过太多团队一上来就想把所有功能都拆成微服务,结果把自己玩死了。我们的策略很明确:先拆最痛的点,再逐步扩展。
第一步,我们把订单服务独立出来。为什么选订单?因为:
- 订单相关的数据库表已经相对独立
- 订单业务逻辑复杂,经常需要单独优化
- 订单是核心业务,出问题影响最大
第二步,把用户认证和权限管理拆出来。这部分和业务耦合度低,而且很多服务都需要调用。
第三步,也就是最重要的一步——实现区块链交互层。这个服务专门负责:
- 与区块链节点通信
- 执行智能合约
- 验证交易签名
- 数据上链和查询
// blockchain_service.go
package main
import (
"context"
"encoding/json"
"time"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/common"
)
type BlockchainService struct {
client *ethclient.Client
timeout time.Duration
}
func NewBlockchainService(rpcURL string) (*BlockchainService, error) {
client, err := ethclient.Dial(rpcURL)
if err != nil {
return nil, err
}
return &BlockchainService{
client: client,
timeout: 30 * time.Second,
}, nil
}
func (bs *BlockchainService) VerifyTransaction(ctx context.Context, txHash string) (bool, error) {
// 设置超时,避免阻塞
ctx, cancel := context.WithTimeout(ctx, bs.timeout)
defer cancel()
hash := common.HexToHash(txHash)
receipt, err := bs.client.TransactionReceipt(ctx, hash)
if err != nil {
return false, err
}
// 验证交易是否成功(状态码为1)
return receipt.Status == 1, nil
}
func (bs *BlockchainService) ExecuteSmartContract(ctx context.Context, contractAddr string, data []byte) ([]byte, error) {
// 这里简化了,实际还需要处理ABI编码、gas估算等
// 但核心思想是:所有区块链操作都封装在这个服务里
// 其他服务只需要调用HTTP API,不用关心底层细节
return nil, nil
}
这个设计的关键在于隔离复杂性。其他微服务完全不需要知道区块链是怎么工作的,它们只需要调用POST /verify-transaction这样的REST API就行了。这样即使以后我们要换区块链平台(比如从Ethereum换成Hyperledger),也只需要修改这一个服务。
服务间通信:别让网络成为瓶颈
微服务拆分后,最大的挑战就是服务间通信。以前在单体应用里,函数调用是纳秒级的,现在变成了网络调用,动辄几十毫秒。
我们考虑过几种方案:
- RESTful HTTP:简单直接,但性能一般
- gRPC:性能好,但调试复杂
- 消息队列:异步解耦,但增加了系统复杂度
最后我们采用了混合策略:
- 同步调用用gRPC(比如订单服务调用用户服务验证权限)
- 异步通知用Kafka(比如订单创建后通知风控服务)
- 对外暴露的API还是用RESTful(方便前端和其他系统调用)
// order_service.proto
syntax = "proto3";
package orders;
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}
message CreateOrderRequest {
string user_id = 1;
repeated OrderItem items = 2;
string blockchain_tx_hash = 3; // 区块链交易哈希
}
message CreateOrderResponse {
string order_id = 1;
bool success = 2;
string message = 3;
}
选择gRPC还有一个重要原因:强类型契约。在微服务架构中,服务间的接口定义特别重要。gRPC的protobuf文件就像一份合同,明确了每个字段的类型和含义,避免了JSON API那种"猜字段"的痛苦。
不过gRPC也有坑。记得有一次我们升级了protobuf定义,但忘记通知调用方,结果线上服务直接报错。从那以后,我们建立了严格的版本管理流程:任何接口变更都要走Code Review,而且要保证向后兼容。
数据库拆分:最难啃的骨头
如果说服务拆分是外科手术,那数据库拆分就是开颅手术。我们的单体应用用的是MySQL,所有业务数据都在一个数据库里,表之间关系错综复杂。
最开始我想简单粗暴地按服务拆分数据库,每个微服务有自己的数据库。但很快发现这行不通——订单表里有用户ID,商品表里有分类ID,这些关联关系怎么办?
经过几轮讨论,我们采用了领域驱动设计(DDD) 的思路:
- 识别聚合根:订单、用户、商品都是独立的聚合根
- 消除跨聚合引用:订单表里不再存用户详细信息,只存用户ID
- 通过API获取关联数据:需要用户信息时,调用用户服务的API
-- 旧的订单表(单体应用)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT,
user_name VARCHAR(100), -- ❌ 不应该存在的冗余字段
user_phone VARCHAR(20), -- ❌
product_id BIGINT,
product_name VARCHAR(200), -- ❌
product_price DECIMAL(10,2), -- ❌
quantity INT,
total_amount DECIMAL(10,2),
status VARCHAR(20),
created_at TIMESTAMP
);
-- 新的订单表(微服务)
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL, -- ✅ 只保留外键
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL,
blockchain_tx_hash VARCHAR(66), -- 区块链交易哈希
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 订单商品表
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL, -- ✅ 只保留外键
quantity INT NOT NULL,
unit_price DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
这种设计虽然增加了网络调用次数,但保证了数据的一致性和服务的独立性。当然,为了优化性能,我们在应用层做了缓存:
- 用户信息缓存到Redis,TTL 5分钟
- 商品信息缓存到本地内存,TTL 1分钟
- 热点数据预加载
监控和可观测性:别等到出事才后悔
微服务架构下,问题排查难度指数级上升。以前在单体应用里,一个错误日志就能定位问题,现在可能要查十几个服务的日志。
所以我们从一开始就重视监控体系的建设:
- 日志:统一收集到ELK,用trace_id串联所有服务
- 指标:Prometheus + Grafana监控QPS、延迟、错误率
- 链路追踪:Jaeger跟踪完整的请求链路
特别是区块链相关的服务,我们加了额外的监控:
- 区块链节点的连接状态
- 智能合约执行的gas消耗
- 交易确认的延迟时间
# prometheus.yml
scrape_configs:
- job_name: 'order-service'
static_configs:
- targets: ['order-service:8080']
- job_name: 'blockchain-service'
static_configs:
- targets: ['blockchain-service:8080']
metrics_path: '/metrics'
params:
module: [blockchain]
上周五晚上那个让我崩溃的链路追踪图,其实就是因为区块链服务的一个bug导致的。某个智能合约执行超时,但没有正确处理超时异常,导致订单服务一直等待,最终雪崩。有了完整的监控体系,我们才能在30分钟内定位并修复问题。
心得体会:微服务不是银弹
经过这几个月的折腾,我深刻体会到:微服务不是万能的,它解决了一些问题,但带来了更多新问题。
微服务适合的场景:
- 团队规模大,需要独立开发部署
- 业务复杂度高,需要清晰的边界划分
- 有特定的性能要求(比如我们的区块链场景)
微服务不适合的场景:
- 小团队小项目(增加的复杂度远大于收益)
- 业务逻辑简单,变化不频繁
- 团队缺乏DevOps能力
对于我们来说,微服务架构最大的价值不是技术上的,而是组织上的。每个小团队可以独立负责一个或几个服务,不用再协调十几个开发人员改同一个代码库。产品经理也能更清晰地理解系统边界,提需求时更有针对性。
至于区块链,说实话我觉得现在很多场景都是为了用而用。但在供应链金融这种需要多方信任的场景下,区块链确实能解决一些传统技术无法解决的问题。关键是要把区块链当作一种工具,而不是目标。
现在每天早上8点,我坐在工位上看着各个服务的监控面板,心里踏实多了。虽然偶尔还会遇到半夜报警,但至少不会再出现"改一个按钮样式导致支付失败"这种离谱的事情了。
微服务之路还很长,但至少我们迈出了正确的第一步。如果你也在考虑微服务拆分,记住我的教训:别贪多,别求快,先把最痛的点解决掉。
对了,Claude Code最近更新了个新功能,可以直接生成gRPC的客户端代码,省了不少手工编写的时间。作为早期用户,我觉得这波更新相当给力!

评论 0