资讯动态

从零搭建分布式微服务架构:Spring Cloud Alibaba实战与踩坑记录

发布时间:2026/10/21 19:51:06 来源:尧图企业网站定制
1. 为什么我要从零搭一套分布式微服务架构去年年底我接手了一个内部工具平台的重构任务。原来的系统是一个典型的单体应用所有功能模块——用户管理、订单处理、库存计算、消息通知——全部塞在一个代码仓库里部署时打成一个包扔到一台机器上跑。刚开始用户量小的时候没什么问题但后来日活慢慢涨到几千问题就集中爆发了改一个通知模板的文案要重新部署整个应用库存模块的一个内存泄漏把订单服务也拖垮了每次发版都像拆炸弹谁也不敢保证不出事。于是我决定动手把它拆成分布式微服务架构。这篇文章就是记录整个搭建过程——从服务拆分、通信选型、服务注册发现、配置中心、网关路由到链路追踪和容器化部署我会把每一步的决策逻辑、踩过的坑、以及最终跑通的方案完整写出来。如果你也正在面对单体应用的维护困境或者想了解微服务架构到底怎么落地这篇内容应该能帮你少走不少弯路。先说一下最终的技术栈选型方便你有个全局印象层次选型核心理由服务框架Spring Boot Spring Cloud Alibaba生态成熟中文文档丰富社区活跃服务注册与发现Nacos同时支持注册中心和配置中心运维成本低配置中心Nacos Config与注册中心复用同一套基础设施API网关Spring Cloud Gateway响应式非阻塞性能优于Zuul服务调用OpenFeign LoadBalancer声明式调用代码侵入性小熔断限流Sentinel控制台可视化规则动态推送链路追踪SkyWalking无侵入式探针对业务代码零改造消息队列RocketMQ事务消息支持好适合订单场景容器化Docker Docker Compose初期规模不大暂不上K8s这套组合不是拍脑袋选的每一个都有具体的考量后面我会逐个拆解。2. 服务拆分怎么拆才不至于把自己拆死2.1 拆分粒度的判断标准服务拆分是微服务架构里最容易翻车的环节。我见过不少团队一上来就拆成十几个服务结果光是服务间的调用链路就复杂到没人能理清楚。我的经验是初期宁可拆粗一点也不要拆太细。具体怎么判断我用了三个标准业务边界是否清晰用户管理和订单处理之间的边界很明确但订单和库存之间就有大量交互初期可以考虑放在一起。数据是否强关联如果两个模块共享同一张核心表强行拆开会导致分布式事务问题不如先合着。团队规模是否匹配一个人维护三四个服务已经是极限了如果团队只有三五个人拆成十几个服务就是自找麻烦。最终我把原来的单体应用拆成了五个服务用户服务负责注册、登录、权限校验、用户信息管理订单服务负责订单创建、状态流转、订单查询库存服务负责库存扣减、回滚、库存查询通知服务负责短信、邮件、站内信的发送网关服务负责路由转发、鉴权、限流2.2 数据库拆分的实操细节服务拆了数据库也得跟着拆。原来一个库里有二十多张表现在要按照服务边界分到不同的库里。这一步比拆代码麻烦得多因为涉及到数据迁移和一致性问题。我的做法是每个服务独享一个数据库实例初期可以用同一个MySQL实例的不同schema但逻辑上必须隔离跨服务的关联查询一律通过API调用解决不允许跨库JOIN对于必须保证一致性的操作比如订单创建时要扣库存采用最终一致性方案先本地事务写入订单再通过RocketMQ发送扣减库存的消息库存服务消费消息后执行扣减如果扣减失败则发送回滚消息注意跨库JOIN是微服务架构里的大忌。一旦你允许跨库查询服务边界就形同虚设后面想独立部署和扩容都做不到。2.3 共享代码的处理方式拆分过程中有一个很现实的问题原来很多工具类、常量定义、DTO对象是共享的拆成独立服务后怎么办我的方案是抽出一个公共依赖包包含通用的工具类日期处理、加密解密、JSON序列化统一的返回体定义Result、PageResult服务间调用的DTO对象公共异常定义这个包通过内部Maven仓库管理各服务按需引入。但要注意控制这个包的膨胀速度——如果什么代码都往里塞它很快就会变成一个“分布式单体”的粘合剂。3. 服务注册发现与配置中心Nacos的落地实践3.1 为什么选Nacos而不是Eureka或Consul选Nacos主要看中三点第一一个组件解决两个问题。服务注册发现和配置管理它都包了不用再单独部署一套Config Server。对于我这种人手有限的团队来说少维护一个组件就少一份运维负担。第二支持AP和CP两种模式切换。默认是AP模式可用性优先适合服务发现场景配置管理可以切到CP模式保证配置一致性。Eureka只支持APConsul用Raft协议是CPNacos给了灵活选择的空间。第三控制台好用。服务列表、配置管理、健康状态都在一个界面里看得清清楚楚排查问题很方便。3.2 Nacos集群部署的关键配置单机模式的Nacos只能用于开发环境生产环境必须上集群。我用了三台机器做集群部署每台配置2核4G。核心配置项application.properties# 数据库配置Nacos集群必须用外部MySQL spring.datasource.platformmysql db.num1 db.url.0jdbc:mysql://127.0.0.1:3306/nacos_config?characterEncodingutf8connectTimeout1000socketTimeout3000autoReconnecttrue db.usernacos db.password你的密码 # 集群节点配置 # cluster.conf文件中列出所有节点 # 192.168.1.101:8848 # 192.168.1.102:8848 # 192.168.1.103:8848启动时有个坑要注意三台机器的系统时间必须同步否则Raft协议选举会出问题。我一开始没注意导致集群一直选不出Leader排查了半天才发现是时间偏差超过了阈值。3.3 服务注册的代码配置在Spring Boot项目中集成Nacos注册发现依赖如下dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency配置文件里加上spring: application: name: order-service cloud: nacos: discovery: server-addr: 192.168.1.101:8848,192.168.1.102:8848,192.168.1.103:8848 namespace: prod group: DEFAULT_GROUP ephemeral: true这里有几个参数值得说明namespace用来隔离不同环境开发、测试、生产我建议每个环境一个namespace避免服务互相串门。ephemeral临时实例还是持久化实例。临时实例通过心跳保活适合无状态服务持久化实例需要主动注销适合有状态服务。大部分业务服务用临时实例就行。group可以用来做灰度发布比如把新版本服务注册到GRAY_GROUP网关按比例转发流量。3.4 配置中心的动态刷新机制Nacos配置中心最实用的功能是动态刷新——改了配置不用重启服务。实现方式有两种方式一RefreshScope注解RestController RefreshScope public class ConfigController { Value(${custom.config.value:default}) private String configValue; GetMapping(/config) public String getConfig() { return configValue; } }方式二ConfigurationProperties绑定Component ConfigurationProperties(prefix custom.config) RefreshScope public class CustomConfig { private String value; // getter/setter }实操心得RefreshScope的原理是销毁Bean并重新创建所以如果Bean里有状态比如缓存刷新后会丢失。对于有状态的Bean建议用ConfigurationProperties配合监听器手动处理刷新逻辑。4. 网关路由与鉴权Spring Cloud Gateway实战4.1 网关的核心职责划分网关不是简单的反向代理它承担了五个核心职责路由转发根据请求路径把流量分发到对应服务统一鉴权在网关层校验Token避免每个服务都写一遍鉴权逻辑限流熔断保护后端服务不被突发流量打垮日志记录统一记录请求日志方便排查问题跨域处理统一配置CORS不用每个服务单独处理4.2 路由配置的两种方式方式一配置文件方式spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix2 - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix2方式二代码配置方式Configuration public class GatewayConfig { Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(user-service, r - r .path(/api/user/**) .filters(f - f.stripPrefix(2)) .uri(lb://user-service)) .route(order-service, r - r .path(/api/order/**) .filters(f - f.stripPrefix(2)) .uri(lb://order-service)) .build(); } }我最终选了配置文件方式因为改路由规则不用重新编译代码配合Nacos配置中心还能动态刷新。4.3 全局鉴权过滤器的实现鉴权的核心逻辑是从请求头里取Token校验有效性把用户信息塞到请求头里传给下游服务。Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); // 白名单直接放行 if (isWhiteList(path)) { return chain.filter(exchange); } String token exchange.getRequest().getHeaders().getFirst(Authorization); if (token null || token.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 校验Token并解析用户信息 try { UserInfo userInfo jwtUtil.parseToken(token); ServerHttpRequest request exchange.getRequest().mutate() .header(X-User-Id, userInfo.getUserId()) .header(X-User-Name, userInfo.getUserName()) .build(); return chain.filter(exchange.mutate().request(request).build()); } catch (Exception e) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } Override public int getOrder() { return -100; // 优先级最高 } }注意网关层解析完用户信息后通过请求头传给下游服务。下游服务不要再解析Token直接信任网关注入的请求头即可。但前提是下游服务不能直接暴露给外部必须只能通过网关访问。4.4 限流策略的配置Spring Cloud Gateway内置了RequestRateLimiter过滤器基于Redis实现令牌桶限流spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 100 redis-rate-limiter.burstCapacity: 200 key-resolver: #{ipKeyResolver}参数说明replenishRate令牌桶每秒填充速率即平均QPS上限burstCapacity令牌桶最大容量即允许的突发流量key-resolver限流维度可以按IP、按用户、按接口路径我配置的是按IP限流单IP平均100 QPS突发不超过200 QPS。这个值是根据压测结果定的——后端服务在4核8G的机器上能扛住大约300 QPS网关层留出余量。5. 服务间调用与容错OpenFeign Sentinel组合拳5.1 OpenFeign的声明式调用服务间调用我用的是OpenFeign代码写起来很简洁FeignClient(name inventory-service, fallbackFactory InventoryFallbackFactory.class) public interface InventoryClient { PostMapping(/inventory/deduct) ResultBoolean deduct(RequestBody DeductRequest request); PostMapping(/inventory/rollback) ResultBoolean rollback(RequestBody RollbackRequest request); }调用方只需要注入这个接口像调本地方法一样调远程服务。但有几个坑要注意坑一超时时间默认太短。OpenFeign默认连接超时10秒读取超时60秒但实际业务中可能需要调整feign: client: config: default: connectTimeout: 5000 readTimeout: 10000坑二GET请求传对象要加SpringQueryMap。如果GET请求的参数是一个对象必须加这个注解否则Feign会把它当成POST处理。坑三文件上传要引入feign-form依赖。否则文件类型参数无法正确编码。5.2 Sentinel熔断降级的规则配置Sentinel的核心概念是资源和规则。资源就是被保护的代码块或接口规则定义了什么时候触发保护。我配置了三类规则流控规则限制QPSFlowRule rule new FlowRule(); rule.setResource(createOrder); rule.setGrade(RuleConstant.FLOW_GRADE_QPS); rule.setCount(50); // 每秒最多50次请求 rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); rule.setWarmUpPeriodSec(10); // 预热10秒 FlowRuleManager.loadRules(Collections.singletonList(rule));降级规则当异常比例超过阈值时熔断DegradeRule rule new DegradeRule(); rule.setResource(inventoryDeduct); rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO); rule.setCount(0.5); // 异常比例超过50%触发熔断 rule.setTimeWindow(10); // 熔断持续10秒 rule.setMinRequestAmount(10); // 至少10个请求才统计 DegradeRuleManager.loadRules(Collections.singletonList(rule));热点规则对特定参数限流ParamFlowRule rule new ParamFlowRule(getUserInfo) .setParamIdx(0) // 第一个参数 .setCount(100); // 该参数值每秒最多100次 ParamFlowRuleManager.loadRules(Collections.singletonList(rule));实操心得Sentinel的规则默认存在内存里重启就丢了。生产环境必须配置持久化我用的方案是Nacos配置中心 Sentinel Dashboard双向同步。Dashboard里改规则会推送到Nacos服务启动时从Nacos拉取规则。5.3 降级兜底逻辑的设计降级不是简单返回一个错误而是要给用户一个可接受的替代结果。比如库存扣减失败时不是直接报错而是Component public class InventoryFallbackFactory implements FallbackFactoryInventoryClient { Override public InventoryClient create(Throwable cause) { return new InventoryClient() { Override public ResultBoolean deduct(DeductRequest request) { // 记录日志发送告警 log.error(库存扣减失败进入降级逻辑, cause); // 返回一个处理中的状态后续通过定时任务补偿 return Result.success(true); } Override public ResultBoolean rollback(RollbackRequest request) { log.error(库存回滚失败进入降级逻辑, cause); return Result.success(true); } }; } }这种设计的前提是业务上允许最终一致性。如果业务要求强一致性那就不能用降级必须让请求失败并让用户重试。6. 链路追踪与日志聚合SkyWalking落地记录6.1 SkyWalking的部署架构SkyWalking的架构分三部分Agent挂在每个服务上负责采集数据OAP Server负责接收、分析、存储数据UI负责展示我用的存储是Elasticsearch因为数据量比较大用H2内存存储扛不住。部署命令Docker方式# 启动Elasticsearch docker run -d --name elasticsearch \ -p 9200:9200 -p 9300:9300 \ -e discovery.typesingle-node \ -e ES_JAVA_OPTS-Xms2g -Xmx2g \ elasticsearch:7.17.0 # 启动OAP Server docker run -d --name oap \ -p 11800:11800 -p 12800:12800 \ -e SW_STORAGEelasticsearch \ -e SW_STORAGE_ES_CLUSTER_NODESelasticsearch:9200 \ apache/skywalking-oap-server:9.0.0 # 启动UI docker run -d --name skywalking-ui \ -p 8080:8080 \ -e SW_OAP_ADDRESShttp://oap:12800 \ apache/skywalking-ui:9.0.06.2 服务接入Agent的配置每个服务启动时加上JVM参数java -javaagent:/path/to/skywalking-agent/skywalking-agent.jar \ -Dskywalking.agent.service_nameorder-service \ -Dskywalking.collector.backend_service192.168.1.200:11800 \ -jar order-service.jarAgent的配置文件agent.config里还可以细调# 采样率生产环境建议调低 agent.sample_n_per_3_secs100 # 忽略特定路径 agent.ignore_suffix.jpg,.jpeg,.png,.css,.js # 跨线程传递上下文 agent.trace_context_ignore_suffix.jpg,.jpeg注意采样率不要设太高否则OAP和ES的压力会很大。我一开始设了全量采集结果ES的磁盘三天就满了。后来调到每3秒100条既能看清链路又不会压垮存储。6.3 日志聚合方案链路追踪解决了“请求经过了哪些服务”的问题但“每个服务打印了什么日志”还需要日志聚合来解决。我的方案是每个服务的日志输出到标准输出Docker环境下通过Filebeat采集日志发送到Elasticsearch在Kibana里按TraceId查询完整链路日志关键是要在日志里打上TraceId。SkyWalking的Agent会自动把TraceId注入到MDC里日志配置里加上%X{tid}即可pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{tid}] %logger{36} - %msg%n/pattern这样在Kibana里搜一个TraceId就能看到这个请求在所有服务里的完整日志。7. 容器化部署与编排Docker Compose方案7.1 为什么初期不用K8sK8s确实是容器编排的终极方案但对于我这种只有五个服务、三台机器的场景来说K8s的复杂度太高了。光是搭建一个高可用集群就要折腾好几天后续的运维成本也不低。Docker Compose足够用了——一个YAML文件定义所有服务一条命令启动全部。等以后服务数量超过二十个或者需要自动扩缩容的时候再迁移到K8s也不迟。7.2 Dockerfile的编写要点每个服务的Dockerfile我用了多阶段构建减小镜像体积# 构建阶段 FROM maven:3.8-openjdk-11 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 运行阶段 FROM openjdk:11-jre-slim WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar]这样构建出来的镜像只有JRE和JAR包体积从原来的800MB降到了200MB左右。7.3 Docker Compose编排文件version: 3.8 services: nacos: image: nacos/nacos-server:v2.2.0 environment: - MODEstandalone - SPRING_DATASOURCE_PLATFORMmysql ports: - 8848:8848 networks: - micro-net user-service: build: ./user-service environment: - SPRING_PROFILES_ACTIVEprod - NACOS_ADDRnacos:8848 depends_on: - nacos networks: - micro-net order-service: build: ./order-service environment: - SPRING_PROFILES_ACTIVEprod - NACOS_ADDRnacos:8848 depends_on: - nacos networks: - micro-net gateway: build: ./gateway ports: - 8080:8080 depends_on: - user-service - order-service networks: - micro-net networks: micro-net: driver: bridge实操心得depends_on只保证启动顺序不保证服务就绪。如果服务启动慢网关可能先起来但后端还没注册到Nacos。解决方案是在网关里配置重试机制或者用healthcheckcondition: service_healthy。8. 踩坑记录与排查技巧实录8.1 服务注册失败排查现象服务启动日志显示注册成功但Nacos控制台看不到实例。排查思路检查网络连通性telnet nacos-host 8848检查namespace和group是否匹配检查服务名是否包含非法字符下划线在某些版本会有问题查看Nacos服务端日志logs/nacos.log我的实际原因namespace配置了一个不存在的值Nacos不会报错但实例注册到了错误的namespace里。8.2 Feign调用超时问题现象偶发性的Feign调用超时但后端服务日志显示处理很快。排查思路检查Feign的超时配置是否合理检查是否有大对象序列化耗时检查网络是否有抖动检查服务端线程池是否满了我的实际原因服务端Tomcat线程池默认200但有个接口处理慢把线程占满了导致后续请求排队。解决方案是给慢接口单独配置线程池隔离。8.3 分布式事务一致性现象订单创建成功但库存扣减失败数据不一致。解决方案我用了RocketMQ的事务消息 定时补偿订单服务先发半消息到MQ执行本地事务写入订单本地事务成功则提交消息失败则回滚消息库存服务消费消息执行扣减如果扣减失败发送回滚消息给订单服务定时任务扫描异常订单进行补偿这个方案不是强一致性但保证了最终一致性对于大部分业务场景够用了。8.4 常见问题速查表问题现象可能原因排查方向解决方案服务注册不上namespace/group不匹配检查配置统一namespaceFeign调用404路径不匹配检查Controller路径核对Path和StripPrefix配置刷新不生效缺少RefreshScope检查注解加上注解或手动监听网关限流不生效Redis未连接检查Redis配置修复Redis连接链路追踪断链异步线程未传递上下文检查线程池使用TransmittableThreadLocal熔断规则丢失未持久化检查规则存储配置Nacos持久化9. 这套架构跑了一年后的真实体会这套架构上线到现在差不多一年了中间经历过几次流量高峰整体表现还算稳定。最大的感受是微服务不是银弹它解决了一些问题但也引入了一些新问题。好处很明显服务可以独立部署了改通知模板不用重新部署订单服务故障隔离了库存服务挂了不会拖垮用户服务扩容灵活了订单高峰期单独给订单服务加机器就行。但代价也不小运维复杂度上来了以前盯一个应用就行现在要盯五个服务加一堆中间件排查问题变难了一个请求跨了三个服务没有链路追踪根本查不下去分布式事务让人头疼虽然最终一致性方案能解决大部分场景但代码复杂度确实增加了。如果让我重新选一次我可能还是会走微服务这条路但会更谨慎地控制拆分粒度。能拆三个服务解决的问题绝不拆五个。架构是演进来的不是设计出来的一开始就追求完美架构往往适得其反。最后分享一个我在实际运维中总结的小技巧给每个服务都加上一个/health接口返回当前服务的健康状态和依赖组件的连通性。然后在网关层配置定时探测发现某个服务不健康就自动从路由列表里摘除。这个简单的机制在几次中间件故障时帮了大忙避免了大量请求打到已经不可用的服务上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑