从单体到微服务,我只干对了两件事:拆分与治理
这篇文章是我今天初步学习微服务有感而发想谈谈我的理解有时候我们做项目时线上还动不动因为某个模块的锅整站瘫痪。这种场面你要是也经历过那这篇内容就是给你写的。文章比较长我先把路线图放这儿方便你跳着看单体架构到底坑在哪微服务怎么解决什么项目需要微服务别瞎拆从单体拆到两个服务拆完不会互相调用了怎么办Nacos 是怎么让几百个服务不乱套的拆完还有坑我自己收集了几道高频面试题一、你的项目有没有这些症状不用急着往下读先花十秒钟回答三个问题你们改一个功能是不是要整个系统重新打包、一起发布发布窗口还得专门排期团队是不是已经大到这行代码到底归谁管都说不清了是不是经常出现某个热点功能把服务器 CPU 吃满其它模块跟着一起卡死三条里你中了任意两条说明你的项目已经进入了单体的痛苦期。至于什么是单体咱们接着看。二、单体架构单体架构一句话概括所有功能模块塞进同一个工程部署的时候打包成一个包、跑在一个进程里底下还共用一套数据库。这么干的好处很实在——项目小的时候上手快、部署简单、运维省心一两个人就能搞定。早期的小项目几乎都是这么过来的。坏处嘛藏在后面。业务一旦长大团队一旦变多三个问题就藏不住了1.协作成本高几十个人改同一份代码模块之间物理边界越来越模糊改个公共类都可能引发战争。2.发布效率低任何模块变更都要全量发布一处出错整个发布失败大家干瞪眼。3.可用性差模块之间互相影响热点功能能把系统资源榨干其它服务跟着遭殃。说白了单体的痛不是不能用了而是人一多、量一大就喘不过气。三、把一个大家伙拆成一伙小团队微服务的思路很朴素既然一个工程扛不住那就把功能模块拆出来每个模块独立部署成一个服务。拆完之后的组织方式有三个关键词单一职责一个服务只负责一块业务核心数据不依赖别的模块。团队自治每个服务有自己的开发、测试、发布、运维人员队伍规模控制在 10 人以内。服务自治服务独立部署、独立数据库做好隔离不拖累邻居。拿一个常见的电商项目举例商品、用户、购物车、订单、支付这些模块完全可以拆成几个独立服务分给不同的小团队各自维护、各自上线。回到上一节那三个问题逐个对照单体的痛拆完微服务之后几十人改同一份代码每个服务代码量骤减后台就一两个人维护改一处要全量发布哪个服务变了单独打包部署那一个就行热点功能拖垮全系统服务独立部署、独立资源隔离到位互不干扰我自己画了一张图四、不是所有项目都该上微服务很多人一听到大厂都在用微服务就热血沸腾马上就好奇心使然把自己的项目拆成微服务。拆完发现服务是拆了运维成本翻了好几倍线上问题反而更多了。拆之前还得先判断你的项目值不值得拆而不是盲目跟风团队规模人就三五个单体完全够用别硬拆。业务复杂度模块之间天然独立比如商品和支付拆起来才划算高度耦合的业务硬拆只会增加沟通成本。发布频率业务迭代快、要频繁上线独立部署的价值才体现得出来。量级预期真有高并发、大流量的预期才需要独立的扩展能力。能拆得动的才叫微服务拆不动的那叫灾难。小项目老老实实用单体等团队和业务长到单体现在那个痛法再拆也不迟。五、我们先把单体项目跑起来道理讲完了来点真格的。为了把拆分和治理讲透咱们拿一个典型的电商单体项目练手——它有用户、商品、购物车、订单、支付五个模块一开始全在一个工程里我自己是把mysqlnacosmq等这些部署docker里面的。一般简单的电商单体项目都长这样模块干什么的用户登录、余额变更商品商品检索购物车增删改查购物车订单创建订单、查订单、改状态支付生成支付单、支付、查支付单六、服务怎么拆拆服务不是拍脑袋切一刀。目标层面就八个字高内聚、低耦合。高内聚一个服务里的业务要相互关联、能自圆其说职责尽量单一。低耦合服务之间依赖要少实在要依赖接口也要稳定。方式层面分两种拆法纵向拆分按功能模块拆。电商项目顺着业务线拆成、用户、商品、购物车、订单、支付天然清晰。横向拆分把模块之间共用的能力抽出来做成通用服务。比如用户、订单、支付都要发消息那就单独抽一个消息服务谁用谁调。工程组织上还有两种形态独立工程每个服务一个仓库。解耦最彻底但仓库多管理起来繁琐。聚合工程所有服务作为模块聚在一个大工程里。代码集中、管理方便代价是耦合度高、编译时间长。初学者建议从聚合工程入手先专注把拆分逻辑跑通别一上来就折腾多仓库。那到底哪些因素会影响拆分效果我画了张鱼骨图把六大类关键因素一次性摆出来建议收藏看图你会发现拆得好不好远不止切几刀那么简单——粒度、数据、调用、团队、运维、选型这些都需要我们关注。这也正是后面几节要逐个解决的。七、跨服务远程调用我今天在拆的时候就遇到第一个坑。我先把商品、购物车两个模块拆成独立服务。拆完一测购物车列表接口返回的商品信息全是空的——为什么因为查询购物车的时候代码里本来要查商品详情可商品相关的逻辑已经整个搬到商品服务那边去了本地根本查不到。这时候就得把本地方法调用改成跨服务远程调用。改造之前先认识两个家伙服务提供者被别的服务调用的那个比如商品服务。服务消费者发起调用的一方比如购物车服务。要注意这两个角色不是固定的是相对的——购物车调商品的时候它是消费者但它也可能被别的服务调用。最简单的远程调用方式是用 Spring 自带的 RestTemplate 发一个 HTTP 请求。做法三步第一步在购物车服务里注册一个 RestTemplate 的 BeanConfiguration public class RemoteCallConfig { Bean public RestTemplate restTemplate() { return new RestTemplate(); } }第二步在购物车服务的实现类里用 RestTemplate 把商品 ID 发给商品服务的接口拿回商品信息// 1. 取出购物车里所有商品id SetLong itemIds vos.stream() .map(CartVO::getItemId) .collect(Collectors.toSet()); // 2. 远程调用商品服务批量查询商品 ResponseEntityListItemDTO response restTemplate.exchange( http://localhost:8081/items?ids{ids}, // 商品服务的地址和接口 HttpMethod.GET, // GET 请求 null, // 无请求体 new ParameterizedTypeReferenceListItemDTO() {}, // 返回类型 Map.of(ids, String.join(,, itemIds)) // 请求参数 ); if (!response.getStatusCode().is2xxSuccessful()) { return; } ListItemDTO items response.getBody(); // 3. 把商品信息按id组装成map回填到购物车VO里 MapLong, ItemDTO itemMap items.stream() .collect(Collectors.toMap(ItemDTO::getId, Function.identity())); for (CartVO v : vos) { ItemDTO item itemMap.get(v.getItemId()); if (item null) { continue; } v.setNewPrice(item.getPrice()); v.setStatus(item.getStatus()); v.setStock(item.getStock()); }第三步重启购物车服务再测一次商品信息就能正常查到了。但是调通了是好事我当时细想一下这段代码里藏着大问题商品服务的 IP 和端口被硬编码在代码里了。这不就是写死吗地址一换就要改代码重新发布商品服务要是起了多个实例也没法做负载均衡。这个烂摊子下一小节我专门来收拾。八、服务治理上一节结尾的问题一句话概括就是服务多了调用关系靠人记是记不住的。这时候就需要一个中间人——注册中心。注册中心的工作模型有四个关键动作服务注册服务启动时主动把自己的 IP、端口上报给注册中心相当于入职报到。服务发现消费者启动时从注册中心拉取它要调用的服务的地址列表相当于查通讯录。服务续约服务定期给注册中心发心跳报告我还活着不然会被当失联。服务剔除注册中心发现某实例长时间没心跳就把它的地址从列表里清掉防止请求打到死机器上。目前国内用得最多的注册中心是阿里开源的Nacos文档全、功能强还能当配置中心用。Eureka 是 Netflix 家的老前辈现在新项目基本都不碰了。搭建 Nacos 也就三步建一个库用来存 Nacos 自己的元数据。改一下环境配置文件里的 MySQL 地址把整个目录拷到服务器上。用 Docker 起容器端口 8848/9848/9849加 --restartalways 保证意外重启能自己拉起来。这是我自己用的docker命令docker run -d \ --name nacos \ --env-file /root/nacos/custom.env \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --network app-net \ --restartalways \ nacos/nacos-server:v2.1.0-slim起来之后访问 http://你的服务器IP:8848/nacos/登录账号密码默认都是 nacos把服务接进来每个服务要做两件事先在 pom.xml 里加两个依赖一个管注册发现一个管负载均衡!-- nacos 服务注册发现 -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency !-- 负载均衡 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency再在 application.yml 里告诉它 Nacos 在哪spring: cloud: nacos: server-addr: 你的服务器IP:8848 # nacos 地址重启服务打开 Nacos 控制台看到两个服务都注册上来了这一步就齐活了。服务发现怎么落地之前购物车调商品用的是 http://localhost:8081/... 这种写死的地址。现在改成通过服务名调用做法很简单给 RestTemplate 的 Bean 加一个 LoadBalanced 注解然后把请求地址里的 IP 换成服务名Bean LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } // 服务名 item-service 会自动被解析成真实地址 restTemplate.exchange( http://item-service/items?ids{ids}, // 换成服务名不再写IP HttpMethod.GET, null, new ParameterizedTypeReferenceListItemDTO() {}, Map.of(ids, String.join(,, itemIds)) );负载均衡顺手就来了。给商品服务再起一个实例换个端口启动后去 Nacos 控制台能看到两个实例都在线。再访问购物车接口观察两个实例的访问日志——你会发现请求被轮询着分发这个实例一轮、那个实例一轮雨露均沾。这就是默认的轮询策略。到这里服务怎么找到服务这个核心问题就闭环了。下面这张思维导图把微服务技术栈的全貌画了出来如果我画的有什么缺失的话请各位大佬们及时提醒我一下顺便说一句注册中心只是治理的第一步。上面的思维导图里还有网关、熔断、配置中心、链路追踪——每一样都是专门解决一个痛点的后面可以沿着这张图逐个击破。九、拆完还有一些坑实战跑通了你以为就完事了天真。下面五个坑几乎每个拆微服务的团队都会至少踩一个坑一为拆而拆服务数量爆炸每个功能都拆一个服务听着很酷结果几十个服务上线运维累到吐血调用链绕成蜘蛛网。坑二数据库还在共享表面上是微服务底层大家还在连同一张表。服务 A 改了表结构服务 B 直接查崩。铁律只有一条每个服务拥有自己的数据库跨服务的数据只能通过接口或消息拿。坑三分布式事务没想清楚下单要同时扣库存、建订单、清购物车原来一个本地事务搞定拆完变成三个服务的操作本地事务管不了了。资金类的强一致场景拆之前必须想清楚方案Saga、本地消息表、消息队列重试坑四没有监控就敢上线服务一多一个请求要穿三四个服务出问题根本不知道卡在哪个环节。全链路追踪、日志采集、告警这三件套拆完第一天就该铺上别等线上事故来提醒你。坑五链路越长性能越差一次请求多跳一次网络调用延迟就多一分。拆得太碎光服务间通信就能把性能拖垮。这也是为什么说能拆得动的才叫微服务。十、我收集到几个和这个知识点相关的面试题大家可以看一下或者有别的更好的也欢迎交流单体架构和微服务架构的区别是什么微服务一定比单体好吗什么时候该选单体什么时候该拆注册中心的注册、发现、续约、剔除分别解决什么问题服务提供者和消费者是固定的角色吗为什么负载均衡有哪些常见策略LoadBalanced 注解到底做了什么拆完微服务跨服务的数据一致性怎么保证每道题都试着用先答概念、再说场景、最后说取舍的结构回答一遍面试基本就稳了。结尾聊聊你踩过的坑写这篇的初衷很简单网上讲微服务的文章很多但要么太抽象要么一上来就是大厂架构图新手根本接不住。所以我把整个拆分 治理的过程掰开揉碎用一套能跑通的例子串起来。结尾留个互动你所在的项目现在是什么架构拆微服务的过程中踩过最痛的坑是什么欢迎在评论区聊聊我每条都会看。如果这篇对你有帮助点个赞、收藏一下后面我会沿着思维导图继续写网关、熔断、配置中心、链路追踪这几个主题——关注不迷路咱们下篇见。文中涉及的 IP、端口、账号密码均为本地开发示例请按你自己的实际环境替换。实战类文章建议边读边敲命令和环境差异以你本机的报错为准。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →