一、背景
一个典型的视频平台后端,原本采用 Spring Cloud Alibaba 微服务架构,包含 8 个 Java 服务、约 500 个 Java 文件。功能完整,但存在几个核心问题:
| 组件 | 当前实现 | 问题 |
|---|---|---|
| 消息队列 | RocketMQ | 换 MQ 必须改业务代码 |
| 服务调用 | OpenFeign | 只能 Java 调用 Java |
| 服务发现 | Nacos | 弱设备无法运行 |
| 网关 | Spring Cloud Gateway | JVM 占用大,功能耦合 |
业务代码和基础设施强耦合,导致无法在弱设备部署、无法跨语言、无法切换实现。
此外还有代码质量问题:部分 Service 类超过 800 行、DTO 与 Entity 混用、异常处理不统一、测试覆盖不足。
已有亮点: 存储层已做抽象(StorageService → MinIO/Local 实现),说明抽象层思路已被验证可行。
二、重构思路:可插拔基础设施
设计目标
-
业务代码只依赖接口:不依赖具体实现
-
配置驱动:改配置就能切换实现
-
多实现:每个接口有轻量/企业级/测试多种实现
-
渐进迁移:一个服务一个服务切,可回滚
核心接口定义
ServiceDiscovery(服务发现):
go
type ServiceDiscovery interface {
Register(ctx context.Context, instance ServiceInstance) error
Deregister(ctx context.Context) error
Discover(ctx context.Context, serviceName string) ([]ServiceInstance, error)
Watch(ctx context.Context, serviceName string) (<-chan []ServiceInstance, error)
}
实现:FileDiscovery(嵌入式单机)、EtcdDiscovery(K8s 标准)、NacosDiscovery(兼容现有)、MemoryDiscovery(单元测试)
MessageQueue(消息队列):
go
type MessageQueue interface {
Publish(ctx context.Context, topic string, msg Message) error
Subscribe(ctx context.Context, topic string, group string) (<-chan Message, error)
Ack(ctx context.Context, topic string, msg Message) error
}
实现:MemoryQueue(测试)、RedisStreamQueue(轻量)、NATSQueue(CNCF 标准)、RocketMQQueue(企业级兼容)
CacheStore(缓存):
go
type CacheStore interface {
Get(ctx context.Context, key string) ([]byte, error)
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
Lock(ctx context.Context, key string, ttl time.Duration) (bool, error)
Incr(ctx context.Context, key string, ttl time.Duration) (int64, error)
}
实现:MemoryCache(测试)、SQLiteCache(嵌入式持久化)、RedisCache(分布式)、BoltDBCache(嵌入式 KV)
ServiceCaller(服务间调用):
go
type ServiceCaller interface {
Call(ctx context.Context, target string, method string, req []byte) ([]byte, error)
CallStream(ctx context.Context, target string, method string, req []byte) (<-chan []byte, error)
}
实现:MemoryCaller(测试)、HTTPClient(简单服务)、GRPCClient(跨语言标准)
配置驱动
yaml
infrastructure:
servicediscovery:
type: etcd # etcd / nacos / file / memory
endpoints: ["http://localhost:2379"]
messagequeue:
type: redis-stream # redis-stream / rocketmq / nats / memory
cachestore:
type: redis # redis / sqlite / boltdb / memory
servicecaller:
type: grpc # grpc / http / memory
切换环境只需改配置:
| 场景 | 配置组合 |
|---|---|
| 嵌入式设备 | sqlite + memory + redis-stream |
| 老旧服务器 | sqlite + redis + file |
| 云上生产环境 | mysql + redis + etcd + rocketmq |
| 单元测试 | memory + memory + memory |
三、无状态与高并发
无状态原则
服务不保存任何与请求绑定的状态,全部外置:
| 状态类型 | 存储位置 |
|---|---|
| 用户会话 | Redis(JWT + 会话) |
| WebSocket 连接 | Redis Pub/Sub(跨实例广播) |
| 任务进度 | Redis Stream |
| 计数器 | Redis |
| 缓存 | Redis |
| 数据库 | MySQL |
WebSocket 跨实例广播
text
客户端A ──WS──▶ realtime-1 ──┐
├── Redis Pub/Sub ──▶ realtime-2 ──WS──▶ 客户端B
└──(弹幕/通知广播)
每个 realtime 实例订阅 Redis 主题,发消息只往 Redis Publish,所有实例收到后推给本地连接。实例增减不影响广播。
Redis Stream 作为消息队列
使用 Redis Stream 替代传统消息中间件,适合弱设备场景:
| 指标 | Redis Stream | RocketMQ |
|---|---|---|
| 内存 | 复用 Redis | 独立几百 MB |
| 部署 | 已在跑 Redis | 额外服务器 |
| 弱设备 | 可跑 | 不现实 |
核心用法:XADD(入队)、XREADGROUP(消费)、XACK(确认)、XCLAIM(认领超时任务)、XTRIM(清理已 ACK 消息)
四、技术选型
服务语言分配
| 服务 | 语言 | 原因 |
|---|---|---|
| gateway | Traefik(配置即用) | K8s Gateway API 实现,不写代码 |
| core | Go | 大量 CRUD + 缓存,Go 足够 |
| realtime | Go | WebSocket 高并发,goroutine 天然适配 |
| search | Go | Bleve 是 Go 库,天然集成 |
| ads | Go | 轻量业务,Go 足够 |
| media | Java (GraalVM) | 视频转码/AI 处理,Java 生态 |
| store | Java (GraalVM) | 支付事务,Spring 生态 |
为什么混合语言
| 语言 | 优势 | 劣势 |
|---|---|---|
| Go | 单二进制、内存小、并发强 | 生态不如 Java 成熟 |
| Java | 生态全(支付/安全)、成熟 | 启动慢、内存大(GraalVM 缓解) |
策略:在线轻量服务用 Go(跑盒子),复杂业务用 Java GraalVM(跑电脑)。
GraalVM 注意事项
-
Lombok 反射需要
reflect-config.json -
FFmpeg/Whisper 用子进程调用,避免 JNI 反射配置
五、安全架构:零信任与网关职责分离
问题:旧做法把网关当万能工具
原网关承担了过多职责:路由转发、JWT 认证、三层授权、用户身份透传、CORS、异常处理。授权逻辑耦合在网关里,每次改权限都要改网关代码。
原则:网关不做业务层授权
text
用户请求
↓
Traefik(网关)
├── JWT 验签(token 有效吗?)
├── 限流(请求太多吗?)
├── 路由(转发到哪个服务?)
└── 透传原始 JWT(不注入 X-User-Id)
↓
各服务自己从 JWT 解析身份,自行判断权限
网关职责边界
网关应该做的事: JWT 验证、路由转发、限流、自动证书、CORS
网关不该做的事: 细粒度授权("video:manage 权限能不能删这个视频"------这是业务规则)、用户身份透传(下游服务自己从 JWT 解析更安全)
各服务自己认证
网关验证 JWT 有效性后,将原始 JWT 透传给下游。每个服务自己从 JWT 中解析用户身份,自行判断权限。
六、组件选型
| 组件 | 决定 | 替代掉 | 原因 |
|---|---|---|---|
| 服务发现 | etcd | Nacos | K8s 底层就是 etcd,Raft 强一致 |
| 消息队列 | NATS | RocketMQ | CNCF 孵化,Go 单二进制 ~20MB |
| 缓存 | Redis | - | 保留,老电脑内存管够 |
| 主数据库 | MySQL | - | 保留,支付强一致 |
| 本地缓存 | SQLite | MongoDB | 零依赖,文件型 |
| 服务调用 | gRPC | Feign | CNCF 毕业,HTTP/2 + Protobuf,跨语言 |
| 网关 | Traefik | Spring Cloud Gateway | K8s Gateway API 官方实现,~30MB |
| 对象存储 | MinIO | 自建文件 | S3 兼容,可迁移云 OSS |
| 搜索 | Bleve | Elasticsearch | Go 嵌入式,内存从 500MB 降到 50MB |
| 任务调度 | NATS JetStream | XXL-Job | 复用 MQ 消费组 |
| 监控 | Prometheus + Grafana | - | CNCF 毕业 |
| 日志 | Loki | ELK | CNCF 毕业,轻量 |
| 链路追踪 | OpenTelemetry | - | CNCF 毕业 |
七、中间件汇总
| 组件 | 部署位置 | 内存 |
|---|---|---|
| etcd | 老电脑 | ~50MB |
| Redis | 老电脑 | ~100MB |
| MySQL | 老电脑 | ~200MB |
| NATS | 老电脑 | ~20MB |
| MinIO | 主力机 | ~100MB |
| SQLite | 每台设备 | 0(文件) |
| Bleve | search 服务内 | ~50MB |
| Traefik | 盒子入口 | ~30MB |
| Prometheus | 老电脑/主力机 | ~100MB |
| Loki | 老电脑/主力机 | ~50MB |
全部基础设施内存合计:老电脑 ~520MB,盒子 ~230MB(含业务),1GB 盒子无压力。
八、对比总结
| 维度 | 旧架构 | 新架构 |
|---|---|---|
| 服务发现 | Nacos(几百 MB) | etcd(几十 MB) |
| 消息队列 | RocketMQ(几百 MB) | NATS(几十 MB) |
| 搜索 | ES(500MB+) | Bleve(几十 MB) |
| 服务调用 | Feign(Java only) | gRPC(跨语言) |
| 网关 | Spring Cloud Gateway | Traefik(K8s Gateway API) |
| 语言 | Java 全栈 | Go + Java GraalVM |
| 内存合计 | 2GB+ | 老电脑 ~520MB,盒子 ~230MB |
| 弱设备 | 不可行 | 可行 |
| 云迁移 | 绑定阿里云 | 标准云原生,可上 K8s |
九、重构优先级
| 优先级 | 项 | 工作量 | 风险 |
|---|---|---|---|
| P0 | 抽象 MessageQueue | 中 | 高 |
| P0 | 抽象 ServiceCaller | 中 | 高 |
| P0 | 抽象 ServiceDiscovery | 低 | 中 |
| P1 | 统一错误码/异常处理 | 低 | 低 |
| P1 | DTO/Entity 分层 | 中 | 低 |
| P2 | 长类拆分 | 中 | 低 |
| P2 | 硬编码配置化 | 低 | 低 |
策略:先抽接口(改动集中在注入点),确认编译通过且功能不变后再逐个替换实现。每个服务独立迁移,先支持配置切换(保持原实现),再逐步替换。
十、核心原则
-
CNCF 标准优先:etcd、gRPC、NATS、Prometheus、Loki、OpenTelemetry
-
K8s 生态对齐:Traefik(Gateway API)
-
数据集中:MySQL + Redis + etcd 放老电脑,好管
-
业务无状态:盒子随便扩缩,挂了换一个
-
抽象接口:所有组件可切换,不锁死
-
二进制化:Go 原生 + Java GraalVM
-
双模式部署:嵌入式(盒子)和企业级(云)都能跑
-
可观测性标配:没有监控的分布式系统等于盲人摸象