
1. 为什么我要从零搭一套分布式微服务1.1 单体架构撑不住之后我踩过的那些坑去年下半年我负责维护的一个内部业务系统开始频繁出问题。这套系统最早是一个典型的单体应用所有模块——用户管理、订单处理、库存扣减、消息通知——全部打包在一个进程里部署在一台配置还算不错的机器上。刚开始日活几百的时候跑得挺稳改个功能重新部署也就几分钟的事。但业务量上来之后问题就藏不住了。最直接的表现是每次发版都像在拆炸弹。一个很小的改动比如调整一下通知模板的文案也得把整个应用重新构建、重新部署。构建过程要跑十几分钟部署期间服务不可用用户那边直接报错。更麻烦的是有一次库存模块的一个内存泄漏问题把整个进程拖垮了连带用户登录都进不去。这种“一处出问题全盘皆输”的局面让我下定决心要做服务拆分。另一个绕不开的问题是扩容。单体应用里订单模块压力大我想单独给它加机器做不到——只能把整个应用复制一份部署到新机器上结果就是用户模块、通知模块也跟着被复制了资源浪费严重。这就好比一家餐厅炒菜的师傅忙不过来你不能只加一个炒菜师傅而是要把整个餐厅从厨师到服务员到收银员全部复制一套成本高得离谱。所以分布式微服务架构对我来说不是一个“赶时髦”的选择而是业务发展到一定阶段后被现实逼出来的必然路径。这篇文章记录的就是我从零开始搭建一套分布式微服务架构的完整过程包括技术选型、服务拆分、通信机制、服务治理、部署运维等环节。如果你也正面临单体应用的瓶颈或者想了解微服务落地到底要经历哪些步骤这篇内容应该能给你一些参考。1.2 这套架构最终长成了什么样先给你看一下最终的整体结构心里有个底。我搭建的这套系统包含以下几个核心部分服务注册与配置中心负责所有微服务的注册发现和统一配置管理。网关层所有外部请求的统一入口做路由转发、鉴权、限流。业务服务层拆分为用户服务、订单服务、库存服务、通知服务四个独立服务。服务间通信同步调用走轻量级远程调用协议异步消息走消息队列。可观测性体系链路追踪、日志聚合、指标监控三件套。部署与编排容器化部署用编排工具管理服务生命周期。这套架构不是一步到位的中间经历了好几次调整。下面我会按照实际搭建的顺序把每个环节的思路、操作和踩过的坑都讲清楚。2. 技术选型为什么是这些组件而不是别的2.1 注册中心选型从“能用”到“好用”的权衡服务注册与发现是微服务的地基。没有它服务之间就不知道对方在哪里扩容、缩容、故障转移都无从谈起。市面上常见的注册中心有好几种我当初主要对比了三种方案。第一种是基于键值存储的注册中心它的优点是简单、一致性强很多团队已经在用类似的存储组件运维成本低。但缺点也很明显它本身不是为服务发现设计的健康检查机制需要自己实现服务变更通知有延迟不适合对实时性要求高的场景。第二种是专门为服务发现设计的组件支持多数据中心健康检查机制完善社区活跃。但它的部署和运维相对复杂需要额外维护一套集群对于我这种中小规模的系统来说有点“杀鸡用牛刀”。第三种是轻量级的注册中心部署简单和容器化环境配合得很好健康检查机制也够用。最终我选的是这一类。原因很直接我的服务数量不多初期就四个不需要跨数据中心也不需要极致的性能我要的是部署简单、运维省心、和容器环境无缝集成。选型心得不要一上来就选最“强大”的组件而是选最匹配当前规模和团队能力的。注册中心这种东西一旦上线就很难换所以选型时要考虑未来一到两年的业务增长但也不要过度设计。2.2 远程调用框架性能与开发效率的平衡服务拆开之后服务之间的调用方式就成了核心问题。我对比了两种主流方案一种是基于HTTP的RESTful调用另一种是基于二进制协议的远程过程调用框架。RESTful的好处是通用、直观、调试方便用浏览器或者命令行工具就能直接测试接口。但它的性能开销相对较大每次调用都要建立HTTP连接即使有连接池序列化效率也不如二进制协议。而且RESTful的接口定义比较松散服务之间的契约不够强容易出现“接口改了但调用方不知道”的情况。远程过程调用框架的好处是性能高、接口契约强、支持多种序列化方式。但它的调试相对麻烦需要专门的工具而且和某些语言或框架的集成可能不够顺畅。最终我选择了远程过程调用框架主要基于两点考虑第一我的服务之间调用频率较高尤其是订单服务和库存服务之间每次下单都要扣减库存性能不能成为瓶颈第二我希望服务之间的接口有强契约避免“改了一个服务另一个服务挂了”的情况。具体来说我选了一个支持多种语言的框架接口定义用中间语言描述然后自动生成各语言的代码。这样服务提供方和调用方都基于同一份接口定义契约清晰不容易出错。2.3 消息队列异步解耦的关键一环有些操作不需要同步等待结果比如用户下单成功后发送通知、记录操作日志、更新统计数据。这些操作如果放在主流程里同步执行会拖慢响应时间而且一旦通知服务挂了整个下单流程都会失败。消息队列就是解决这个问题的。我把这些“可以晚一点做”的操作全部异步化订单服务在下单成功后往消息队列里发一条消息通知服务从队列里消费消息然后发送通知。这样即使通知服务暂时不可用消息也会在队列里等着不会丢失也不会影响下单主流程。消息队列的选型我主要考虑了吞吐量、可靠性、运维复杂度三个维度。最终选了一个社区成熟、文档丰富、支持持久化和消息确认机制的方案。这里不展开具体品牌核心是对于中小规模系统选一个你团队最熟悉的、社区最活跃的就行不要为了“先进”而选一个没人会运维的。2.4 容器化与编排让部署不再是一场噩梦微服务拆开之后服务数量从1个变成了4个甚至更多。如果还像以前那样手动部署每次发版都要登录好几台机器改配置、重启服务不仅效率低而且容易出错。容器化解决的是“环境一致性”问题把每个服务打包成一个容器镜像里面包含了运行所需的所有依赖开发环境、测试环境、生产环境跑的是同一个镜像不会出现“在我机器上能跑”的情况。编排工具解决的是“管理”问题它负责容器的调度、扩缩容、健康检查、滚动更新。我选了一个主流的编排平台它的声明式配置方式很符合我的习惯——我只需要描述“我想要三个订单服务的实例在运行”编排工具会自动帮我实现这个状态某个实例挂了它会自动拉起新的。3. 服务拆分的具体操作与核心细节3.1 怎么拆按业务能力而不是按技术分层服务拆分是微服务架构里最容易做错的一步。我见过不少团队按照技术分层来拆一个服务负责数据库操作一个服务负责业务逻辑一个服务负责接口暴露。这种拆法看起来“层次清晰”实际上是灾难——因为每次业务变更都要同时改三个服务部署也要协调三个服务比单体还麻烦。正确的拆法应该是按业务能力来拆。以我的系统为例用户服务负责用户的注册、登录、信息管理、权限校验。订单服务负责订单的创建、查询、状态流转、取消。库存服务负责库存的查询、扣减、回滚、预警。通知服务负责短信、邮件、站内信的发送。每个服务独立拥有自己的数据库表服务之间通过接口或消息通信不直接访问对方的数据库。这一点非常关键如果订单服务直接去查库存服务的数据库表那本质上还是单体只是把代码拆开了而已。拆分原则一个服务应该是一个“业务能力的完整闭环”。判断标准很简单——如果你要改一个业务规则只需要改一个服务那拆得就对了如果要同时改好几个服务那说明拆错了。3.2 数据库拆分最痛但也最不能跳过的一步服务拆分了数据库也必须跟着拆。我最初想偷懒让所有服务共用一个数据库只是表分开。但很快发现这样不行订单服务的事务和库存服务的事务在同一个数据库里虽然可以用本地事务保证一致性但这就意味着两个服务必须部署在一起失去了独立部署的意义。所以我把数据库也拆开了每个服务一个独立的数据库实例。这样做的好处是每个服务可以独立选择最适合的数据库类型比如用户服务用关系型日志服务用文档型。一个服务的数据库出问题不会影响其他服务。每个服务可以独立扩容数据库。但代价也很明显跨服务的事务一致性成了大问题。比如下单要同时创建订单和扣减库存这两个操作在不同的数据库里不能用本地事务保证。我的解决方案是引入“最终一致性”模式订单服务先创建订单状态为“待确认”然后通过消息队列通知库存服务扣减库存库存服务扣减成功后回一条消息订单服务收到后把订单状态改为“已确认”。如果库存扣减失败订单服务收到失败消息后把订单取消。这个过程中消息队列的可靠性至关重要。我做了几件事来保证消息不丢消息持久化、生产者确认机制、消费者手动确认、失败重试加死信队列。3.3 接口设计契约先行代码后写服务之间的接口设计我采用的是“契约先行”的方式。先用中间语言把接口定义写好包括方法名、参数、返回值、异常。然后根据这份定义自动生成服务端和客户端的代码骨架。这样做的好处有三个第一接口定义是唯一的真相来源。服务提供方和调用方都基于同一份定义不会出现“我以为你返回的是这个字段结果你返回的是另一个”的情况。第二接口变更可控。如果我要改接口必须先改定义文件然后重新生成代码编译时会自动检查出所有受影响的地方。这比“改完代码直接上线结果调用方挂了”要安全得多。第三跨语言支持。虽然我目前只用了一种语言但万一以后某个服务需要用另一种语言实现接口定义不用改直接生成对应语言的代码就行。接口定义我遵循了几个原则方法名用动词加名词参数尽量用对象而不是多个基本类型返回值统一包装包含状态码、消息、数据异常定义清晰。4. 服务治理让微服务“可控”而不是“失控”4.1 服务注册与发现的实际运作服务注册与发现的流程是这样的每个服务启动时会把自己的地址IP和端口注册到注册中心并定期发送心跳。注册中心维护一份服务列表调用方从注册中心获取可用服务列表然后根据负载均衡策略选择一个实例发起调用。这里有几个细节值得注意心跳间隔和超时时间心跳间隔太短注册中心压力大太长服务挂了之后调用方不能及时发现。我设置的是心跳间隔10秒超时时间30秒。也就是说一个服务如果30秒没有发送心跳注册中心会把它从列表里摘掉。服务下线服务正常关闭时会主动从注册中心注销自己。但如果是异常崩溃比如进程被强制杀掉就只能靠心跳超时来摘除了。所以调用方需要处理“调用到一个已经挂掉的实例”的情况通常的做法是重试加故障转移。负载均衡我用的策略是轮询加权重。默认每个实例权重相同轮询调用。但如果某台机器的配置更好可以调高权重让它承担更多流量。4.2 熔断、限流、降级三道防线微服务架构里一个服务出问题可能会引发连锁反应。比如库存服务响应变慢订单服务调用库存服务的线程池被占满订单服务也跟着变慢然后网关的线程池被占满整个系统雪崩。为了防止这种情况我加了三道防线。第一道限流。在网关层对每个接口设置QPS上限超过的请求直接拒绝返回“请求过于频繁”。这样即使有突发流量也不会把后端服务打垮。限流算法我选的是令牌桶因为它允许一定的突发流量比漏桶更灵活。第二道熔断。在服务调用方比如订单服务调用库存服务设置熔断器。当调用失败率超过阈值比如50%时熔断器打开后续调用直接返回降级结果不再真正发起调用。等过一段时间比如30秒后熔断器进入半开状态允许少量请求通过如果成功则关闭熔断器如果失败则继续保持打开。第三道降级。当熔断器打开或者服务不可用时返回一个预设的降级结果。比如库存服务不可用时订单服务可以返回“库存查询暂时不可用请稍后重试”而不是让整个下单流程失败。实操心得熔断器的阈值设置很关键。设得太高起不到保护作用设得太低正常波动就会触发熔断。我的经验是先观察一周的正常运行数据取失败率的99分位值作为阈值然后再根据实际情况微调。4.3 链路追踪出问题时能快速定位微服务架构下一个请求可能经过网关、订单服务、库存服务、通知服务等多个环节。如果某个请求变慢了或者报错了怎么快速定位是哪个环节的问题靠日志一行行看是不现实的必须要有链路追踪。链路追踪的原理是每个请求在进入系统时生成一个全局唯一的追踪ID这个ID会随着调用链一路传递下去。每个服务在处理请求时记录自己的处理时间、调用的下游服务、返回结果等信息并关联到同一个追踪ID。最后把这些信息汇总起来就能看到一个完整的调用链路。我用的方案是在网关层生成追踪ID通过请求头传递给下游服务每个服务用拦截器自动记录追踪信息最后上报到一个集中的追踪存储和分析平台。这样出问题时我只需要根据追踪ID搜索就能看到整个调用链的耗时分布快速定位瓶颈。5. 部署与运维让服务跑起来只是开始5.1 容器化打包一次构建到处运行每个服务我都写了一个容器构建文件内容大致包括基础镜像选择、依赖安装、应用打包、启动命令。基础镜像我选的是轻量级的版本因为微服务本身不重不需要一个完整的操作系统镜像。构建过程我做了几件事来优化分层构建把不常变的部分比如依赖安装放在前面的层常变的部分比如应用代码放在后面的层。这样每次构建时前面的层可以复用缓存构建速度大幅提升。多阶段构建编译阶段用一个包含完整构建工具的镜像运行阶段只用一个包含运行时环境的精简镜像。这样最终镜像体积小启动快攻击面也小。镜像标签管理每次构建都打上版本号标签不用“latest”标签部署生产环境避免版本混乱。5.2 编排配置声明式管理服务编排配置我用的是声明式的方式写一个配置文件描述我想要的状态需要几个实例、每个实例的资源限制、环境变量、健康检查方式、滚动更新策略等。编排工具会自动实现这个状态。几个关键配置副本数初期每个服务设2个副本保证一个挂了另一个还能顶上。后续根据负载情况调整。资源限制给每个容器设置CPU和内存的请求值和上限。请求值是调度依据上限值是防止某个容器占用过多资源影响其他容器。健康检查分为存活检查和就绪检查。存活检查判断容器是否还在运行失败则重启容器就绪检查判断容器是否准备好接收流量失败则从负载均衡里摘除。滚动更新更新时先启动新版本的实例等新实例就绪后再停掉旧实例保证更新过程中服务不中断。5.3 日志与监控看不见的才是危险的微服务架构下日志分散在各个容器里如果还像以前那样登录机器看日志效率极低。我的做法是日志聚合每个服务的日志输出到标准输出由容器运行时收集然后统一发送到日志聚合平台。在聚合平台上可以按服务名、追踪ID、时间范围等条件搜索日志。指标监控每个服务暴露一个指标接口包含请求量、响应时间、错误率、线程池状态、数据库连接池状态等。监控平台定期抓取这些指标并设置告警规则。告警通知当错误率超过阈值、响应时间变长、服务实例数不足时触发告警通过邮件或即时消息通知我。避坑经验监控指标不要贪多先监控最核心的几个请求量、错误率、响应时间的P99值、服务实例数。这几个指标能覆盖80%的问题场景。等系统稳定了再逐步增加更细粒度的指标。6. 常见问题与排查技巧实录6.1 服务调用超时原因可能不在被调用方服务调用超时是最常见的问题之一。我遇到过一次订单服务调用库存服务频繁超时但库存服务的监控指标看起来很正常响应时间并不高。排查过程是这样的先看订单服务侧的日志发现超时集中在某些特定实例上。然后去看库存服务的实例列表发现有一个实例的IP地址和实际容器IP不一致——原来是注册中心里残留了一个已经下线的旧实例调用方偶尔会路由到这个不存在的地址导致超时。解决办法是在注册中心配置里缩短心跳超时时间同时调用方增加重试机制遇到连接超时自动重试另一个实例。这个问题的教训是服务调用超时不一定是被调用方处理慢也可能是网络问题、注册中心数据不一致、负载均衡策略问题。排查时要沿着整个调用链路逐段检查。6.2 数据库连接池耗尽一个容易被忽视的瓶颈有一次大促期间订单服务突然大量报错错误信息是“无法获取数据库连接”。检查数据库本身连接数并没有达到上限。问题出在服务侧的数据库连接池配置上。连接池的最大连接数设得太小只有10个而大促期间并发请求量是平时的好几倍大量请求在等待数据库连接等待超时后就报错了。调整连接池参数时要注意最大连接数不是越大越好。数据库能承受的连接数是有限的如果每个服务实例都设很大的连接池总连接数可能超过数据库上限。我的做法是先算出数据库的最大连接数然后除以服务实例数再留一些余量得到每个实例的连接池大小。6.3 消息重复消费幂等性设计不能省消息队列的“至少一次投递”语义意味着消息可能会重复投递。如果消费者没有做幂等处理重复消费就会导致数据错误。比如库存扣减消息被消费两次库存就被多扣了一次。我的解决方案是给每条消息生成一个唯一ID消费者在处理消息前先检查这个ID是否已经处理过。如果处理过直接跳过如果没有处理并记录ID。这个“检查加记录”的操作需要和业务操作在同一个事务里保证原子性。6.4 常见问题速查表问题现象可能原因排查方向解决方案服务调用超时网络问题、注册中心数据不一致、被调用方处理慢检查调用方日志、注册中心实例列表、被调用方指标重试机制、缩短心跳超时、优化被调用方性能数据库连接获取失败连接池太小、连接泄漏、数据库连接数上限检查连接池配置、数据库连接数、慢查询调整连接池大小、修复连接泄漏、优化慢查询消息重复消费消息队列重复投递、消费者未做幂等检查消息ID、消费者处理逻辑幂等设计、唯一ID去重服务实例被误摘除健康检查配置不当、网络抖动检查健康检查日志、网络状况调整健康检查参数、增加重试配置更新不生效配置中心推送延迟、服务未监听检查配置中心推送日志、服务配置监听逻辑增加配置刷新机制、手动触发刷新7. 我在这套架构上踩过的坑和总结的经验7.1 不要为了微服务而微服务这是我最大的体会。微服务不是银弹它解决了一些问题但也引入了新的问题分布式事务、服务治理、运维复杂度、调试难度。如果你的系统规模不大团队人数不多单体应用反而是更合适的选择。我之所以做微服务拆分是因为单体应用确实遇到了无法解决的瓶颈。如果你现在还在单体阶段建议先做好模块化把业务边界划分清楚等真正需要的时候再拆。模块化做得好将来拆分成微服务会容易很多。7.2 自动化是一切的基础微服务架构下手动操作是不可持续的。服务数量多了之后部署、扩容、故障恢复都必须自动化。我的经验是先把持续集成和持续部署流水线搭好再开始拆服务。否则拆到一半你会发现每次发版要手动操作好几个服务比单体还累。7.3 可观测性要提前建设链路追踪、日志聚合、指标监控这三样东西不要等到出问题了才想起来建。我最初只做了日志聚合结果第一次遇到跨服务问题时花了几个小时才定位到根因。后来补上了链路追踪和指标监控排查效率提升了不止一个档次。7.4 从简单开始逐步演进这套架构不是一天建成的。我最初只拆了两个服务注册中心用的是最简单的方案消息队列也是后来才加的。随着业务发展和问题暴露才逐步引入了熔断、限流、链路追踪等组件。我的建议是先让服务跑起来再考虑治理。不要一开始就追求“完美架构”那样你可能永远上不了线。先解决最痛的问题然后根据实际需要逐步完善。最后分享一个我在配置管理上的小技巧所有服务的配置都放在配置中心本地只保留一个指向配置中心的地址。这样改配置不需要重新构建镜像只需要在配置中心修改并推送服务会自动刷新。但要注意不是所有配置都支持热更新比如数据库连接池大小、线程池大小这类配置修改后需要重启服务才能生效。所以我在配置中心里把配置分为“热更新”和“需重启”两类避免误操作。