最初版本

MQ堵车了
2025-06-24 08:54
阅读 772

从单体到云原生:我的后端架构演进之路

引言:一次系统崩溃引发的思考

去年秋天,我们公司的一款在线教育产品上线不到三个月,用户数却像坐上了火箭般飞涨。起初我们只是觉得“这增长真快”,但真正的问题很快显现出来。

某天晚上十点左右,我们的单体架构后端服务突然出现大面积超时。日志显示数据库连接池爆了,Redis 缓存打满,消息队列堆积了上万条任务。更可怕的是,任何代码改动都得重新部署整个系统,而每次部署都会造成3~5分钟的不可用期。

那晚我通宵排查问题,一边改配置一边祈祷系统不要再崩溃。事后我意识到一个残酷的事实:我们引以为豪的“快速开发能力”,在快速增长的业务面前,反而成了拖累系统稳定性的最大隐患。

这次事故让我下定决心推动系统架构转型——从原来的单体应用,逐步演进到微服务、再到云原生架构。接下来我会结合实际项目经验,带你一起经历这一段真实的架构演进历程。


一、最初的起点:单体架构的甜蜜与阵痛

1.1 项目背景

我们最初搭建的是一款在线作业批改系统,支持学生上传作业、老师查看批改结果,以及一些简单的统计功能。为了快速验证市场,团队采用了典型的 LAMP 架构:PHP + MySQL + Redis + RabbitMQ,所有逻辑集中在一个 Git 仓库中。

这种架构有几个显著优势:

  • 开发效率高:本地起个 PHP 内置服务器就能跑起来
  • 调试方便:所有模块都在同一个进程中,调用链清晰
  • 部署简单:一行脚本搞定打包和上线

1.2 初现端倪的问题

随着功能越来越多,几个明显的痛点开始浮现:

  • 代码臃肿:核心 API 文件已经超过8000行,查找函数需要靠 Ctrl+F
  • 依赖混乱:订单模块和作业解析模块之间有复杂的交叉引用
  • 测试困难:修改一个功能可能影响其他不相关的模块
  • 部署风险大:每次上线都要担心“会不会把别处的东西带坏了”
  • 性能瓶颈:数据库成为最严重瓶颈,读写分离也没解决根本问题

印象最深的一次是某次上线导致整个支付流程失败。原因竟然是有人不小心删掉了某个全局变量的初始化语句,而这个变量被十几个类共享。

1.3 单体架构适合什么样的阶段?

总结一下,我认为单体架构非常适合以下场景:

  • 创业初期或MVP阶段
  • 团队规模小于5人
  • 功能模块间耦合度低
  • 对可用性要求不高(可以接受短暂停机)
  • 预算有限,没有专业运维支撑

一旦超出这个范围,就需要认真考虑架构升级了。


二、拆分第一步:走向微服务化

2.1 拆分原则

我们决定采用按业务领域拆分的方式。主要参考了如下三个维度:

维度 说明
业务边界 尽量让每个服务只负责一个明确的业务功能
数据独立性 不同服务尽可能使用独立的数据源
技术自治权 允许不同服务使用不同的技术栈

比如我们将原本的大系统拆分为:

api-gateway
user-service
homework-service
payment-service
notification-service

2.2 拆分过程中的关键决策

1. 接口规范先行

我们采用了 gRPC + Protobuf 的方案定义服务间通信协议。虽然一开始学成本略高,但从长远来看收益很大:

// user.proto
syntax = "proto3";

package user;

service UserService {
  rpc GetUserInfo (UserRequest) returns (UserResponse);
}

message UserRequest {
  string user_id = 1;
}

2. 数据库拆分策略

早期为了赶进度,多个服务共用一个数据库。后来我们按照以下步骤完成数据迁移:

graph TD
    A[原始单体数据库] --> B[创建影子表]
    B --> C[双写同步]
    C --> D[校验数据一致性]
    D --> E[切流量到新服务]
    E --> F[清理旧数据]

3. 服务发现机制

选用 Consul 作为服务注册中心。Go 服务启动时自动注册:

// register with consul
config := api.DefaultConfig()
client, _ := api.NewClient(config)

registration := new(api.AgentServiceRegistration)
registration.Name = "homework-service"
registration.Port = 50051
registration.Check = &api.AgentServiceCheck{
    CheckID: "health-check",
    Name: "Health Check",
    Notes: "Basic Health Check for homework service",
    Status: "passing",
}

client.Agent().ServiceRegister(registration)

4. 熔断降级策略

集成 Hystrix 实现服务熔断:

@hystrix.command(name="get_user_info", command_properties=[
    hystrix.CommandProperty(name="execution.isolation.thread.timeoutInMilliseconds", value="500")
])
def get_user_info(user_id):
    # 实际请求逻辑
    return real_api_call(user_id)

微服务架构示意图-2

三、拥抱云原生:容器化与平台化建设

3.1 容器化改造

当我们开始容器化改造时,遇到了不少坑。比如某个 Python 服务在宿主机运行正常,但在 Docker 中运行时频繁 OOM。最后才发现是因为我们没有限制虚拟内存大小。

这是我们的基础镜像优化路线图:

FROM python:3.9-slim
COPY . /app
RUN pip install -r requirements.txt
CMD ["gunicorn", "app:app"]

# 改进后的多阶段构建
# 构建阶段
FROM python:3.9-slim as builder
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir -r requirements.txt

# 发布阶段
FROM gcr.io/distroless/python-debian11
COPY --from=builder /root/.cache/pip/wheels/ /wheels/
COPY app.py .
RUN pip install --no-index --find-links=/wheels/ -r requirements.txt
CMD ["app.py"]

数据流转过程-1

3.2 Kubernetes 实践

在部署到 K8s 时,我们犯了一个经典错误——没设置就绪探针导致流量在Pod刚启动时就涌入。后来我们做了全面改进:

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

同时我们引入了 Horizontal Pod Autoscaler:

kubectl autoscale deployment hw-service \
  --cpu-percent=60 \
  --min=2 \
  --max=10

3.3 CI/CD 流水线建设

我们采用 GitLab CI + ArgoCD 实现真正的持续交付:

stages:
  - build
  - test
  - deploy

build-image:
  script:
    - docker build -t myregistry/hw-service:$CI_COMMIT_TAG .
    - docker push myregistry/hw-service:$CI_COMMIT_TAG

deploy-staging:
  environment: staging
  script:
    - argocd app set hw-service --path hw-service --repo https://gitlab.example.com/myproject.git --target-revision $CI_COMMIT_REF_NAME

四、那些年我们一起踩过的坑

4.1 分布式事务陷阱

曾经有一段时间,我们在两个服务间直接开启分布式事务,结果发现网络延迟成倍增加。后来改为事件驱动模式:

# 订单服务发布事件
event_bus.publish("order.created", order_data)

# 库存服务消费事件
@event_bus.on("order.created")
def handle_order_created(order):
    deduct_inventory(order.product_id, order.quantity)

4.2 日志黑洞问题

微服务环境下日志散落在各个节点,后来我们建立了 ELK+Fluent Bit 的日志采集体系:

ELK架构示意图

通过 Logstash 多级过滤:

filter {
  grok {
    match => { "message" => "%{COMBINEDAPACHELOG}" }
  }
}

并建立关键业务指标看板:

监控大盘截图

4.3 版本兼容噩梦

接口版本管理是个头疼问题。我们最终采用了一套组合拳:

  • 使用语义化版本号(如 v1.3.2)
  • 所有接口保留向后兼容至少6个月
  • 新增字段默认值保持兼容
  • 定期淘汰老旧版本

五、效果与反思

5.1 性能提升

指标 单体时代 微服务+云原生
平均响应时间 850ms 210ms
QPS 1200 7500
故障隔离率 <30% >85%
部署频率 每周1次 每天多次

5.2 开发效率变化

虽然初期投入较大,但从长期看确实提升了效率:

  • 功能迭代周期从2周缩短到3天
  • 新员工入职熟悉时间从1个月减到1周
  • 代码冲突率下降70%

5.3 值得反思的地方

有些事情当时觉得是对的,现在回过头来看还有改进空间:

  • 过早做服务拆分:应该先做代码模块化重构
  • 忽视可观测性:前期对监控重视不足
  • 跟风新技术:某些服务完全没必要用Go重构

六、给后辈工程师的几点建议

6.1 关于架构选择

  • 不要盲目追求所谓“最佳实践”,适合自己当前阶段的才是最好的
  • 架构演进要循序渐进,比如从模块化 → 插件化 → 微服务
  • 优先优化代码结构,再考虑服务拆分

6.2 数据库设计建议

  • 不同服务尽量使用独立数据库
  • 跨服务查询采用异步同步方式
  • 关键数据定期进行一致性校验

6.3 关于 DevOps

  • 从小处着手:从单个自动化脚本开始
  • 监控不是可选项:尽早搭建基础监控体系
  • 文档要及时更新:特别是接口变更部分

6.4 我的一些心得

有一次凌晨3点处理线上故障时,我发现最大的问题往往出现在最“理所当然”的地方——比如缓存没设置失效时间、或者索引字段写错了类型。这提醒我要永远保持敬畏之心:

“系统越是复杂,越要把简单的事情做到极致。”

另一个深刻体会是:“工具是死的,人是活的。” 当你面临选择微服务还是继续维护单体时,请记住一句话:

“没有银弹,只有最适合当下情况的选择。”


结语:架构演进是一场持久战

转眼距离那次宕机已经过去一年多了。现在的我们依然每天面对新的挑战:如何更好地实现灰度发布?如何进一步降低服务间的依赖?要不要尝试 Service Mesh?

这些都没有标准答案。但我始终相信一点:只要坚持“以业务价值为导向,以开发体验为核心”的原则,技术选型就不会走错方向。

希望我的这段经历能给你一些启发。记住,架构设计从来都不是非黑即白的技术判断题,而是一道需要结合业务特征、团队能力和发展阶段的综合应用题。

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝