ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

智能CRM系统云原生演进:从单体到微服务的架构实践

智能CRM系统云原生演进:从单体到微服务的架构实践 1. 这个项目是什么以及为什么要折腾先说结论这三年我带着团队把一个单体架构的智能客户关系AI系统一步步重构成云原生架构。整个过程踩过的坑、推翻过重来过的方案、半夜爬起来看监控的夜晚都记在这篇里。如果你正好也在做AI类业务系统的架构演进或者正准备从单体切换到微服务和云原生这篇应该能帮你省掉不少弯路。我们这套系统的定位是“智能客户关系管理”核心能力不是传统CRM那套增删改查而是嵌入了大量AI能力销售线索的智能评分、客服场景的意图识别、话术推荐、客户流失预测、知识库问答机器人。最初部署形态很朴素——一个Spring Boot单体应用前端、规则引擎、AI推理服务全塞在一起MySQL存数据Redis做缓存部署在三台物理服务器上靠Nginx做简单的负载均衡。你们可能想问为什么不一开始就上微服务原因很现实当时团队只有六个人业务刚验证阶段老板只关心功能能不能跑起来、客户能不能用上。单体架构在这种情况下是最合理的——开发快速、部署简单、调试方便。但等到客户量涨上去了AI模型越加越多问题开始集中爆发。真正促使我们启动架构演进的有三件事。第一件是每次发布新功能整个系统都要停机因为AI模型文件直接打进JAR包里动一个模型就得重新打包发布第二件是模型推理时CPU和内存占用总是把业务线程拖垮一条高并发请求进来整台机器响应变慢第三件是销售团队要求把客户画像、沟通记录、情绪打分这些能力开放给第三方系统对接但单体应用根本没有稳定的开放接口和鉴权体系。总之一句话不是架构想变是业务逼着你变。这篇文章我按时间线拆成六个部分从单体阶段的问题剖析到服务拆分的决策过程再到云原生改造的具体操作和踩坑记录最后聊聊复盘心得。适合正在思考架构演进路径的开发者、架构师也适合想了解AI系统落地形态的技术管理者。2. 单体阶段的亲身感受崩溃不是一次而是常态2.1 当时的系统到底长什么样先还原一下我们系统在“事故爆发前”的真实形态。技术栈并不复杂Spring Boot 2.x作为主干MyBatis操作MySQLRedis管缓存和会话定时任务处理异步报表AI部分用的是Python训练的模型通过Java进程内嵌的PMML推理引擎来加载。数据库没有分库分表三台服务器部署同一个JAR包Nginx做轮询转发。这套结构跑了一年多前期非常舒坦。开发流程是改代码、Jenkins打包、ssh到服务器、停服务、覆盖JAR、启动。一天可以发布三四次出了问题回滚也快。但AI功能开始密集落地之后问题就一个个冒出来了。我记得很清楚的一个事故场景某次上线一个新的客户流失预警模型需要把模型文件放进classpath发布时正好赶上销售团队集中使用系统。重启过程中服务中断了八分钟客服主管直接打电话到老板那里抱怨。老板转头问我们能不能做到发版本不影响客户使用这个问题直接成了架构演进的导火索。另一个结构性隐患是资源抢占。Java的规则引擎和调度任务与Python模型推理共用同一个进程内存。推理任务来了就吃CPU跑矩阵计算业务线程就卡住。线上表现就是仪表盘加载慢、工单保存超时、对话机器人的响应从几百毫秒飙到十几秒。我们试过调线程池、限流、加服务器但都是治标不治本。2.2 单体架构的真实账单我把当时攒下来的问题做个归类只有把这些问题列清楚才能说服自己也说服团队“必须动刀”发布耦合任何一处改动哪怕只是前端模板调色也要全量重启AI模型和Web应用完全无法独立发布。资源竞争Java业务逻辑和Python模型推理混在一起谁都抢不过谁故障爆炸半径是整个系统。扩展性差哪个模块负载高了不能单独加机器只能整系统扩容成本高效果差。AI能力复用难销售预测、意图识别这些能力只能跟着主应用走第三方系统想调用只能爬接口既没有鉴权也没有限流。技术栈隔离失效为了让Java和Python共存我们用了进程内推理但模型一复杂内存就爆训练好的新模型迟迟不敢上线。如果你正在单体系统里熬着看到这几条多少会有点共鸣。其实我后来复盘单体本身没错错的是“所有东西都必须挂在主进程里”这个思路。AI系统尤其如此——模型推理的生命周期和业务接口的生命周期完全不同模型需要频繁灰度、回滚、多版本并存业务接口需要稳定地对外承诺SLA这两者放在一个进程里等于把两组不同节奏的需求绑死。3. 第一步不是写代码是重新划边界3.1 领域拆分为什么先拆“模型域”而不是“业务域”开始演进的时候团队里最兴奋的人提出一上来就拆十几个微服务什么用户服务、订单服务、客户服务、报表服务……结果被我叫停了。经验告诉我微服务拆分最忌讳步子迈太大。拆得太细基础设施跟不上分布式事务和链路追踪还没建起来团队会先被运维复杂度压垮。我们的策略是“先拆出最痛的部分”。当时最痛的点是AI模型推理和业务逻辑耦合所以第一个拆分目标是“模型推理服务”。我把整个系统分成两个大域业务域和模型域。业务域保留了原有的CRM主链路包括客户管理、销售流程、工单、权限继续用Spring Boot那一套。模型域则是一个独立服务负责所有AI能力会话意图识别、客户情绪分析、线索评分、流失预测。业务域需要AI能力时不能直接调用Python模型而是通过模型域提供的HTTP接口或异步消息来获取结果。这样拆的原因有三个。其一模型域天然适合独立部署——模型推理吃CPU/GPU资源波动大和业务流量要隔离开其二模型训练和上线节奏本身就快经常一周迭代一个版本独立部署后不会影响主业务其三AI能力是公司未来的输出面独立成服务后后续开放给第三方对接会容易得多。3.2 服务拆分时容易忽略的三个细节光把服务拆出来还远远不够联调阶段我们踩了几个看上去不大、但很折磨人的细节问题第一个是服务间通信协议的选择。一开始图省事用HTTP JSON结果模型推理频繁超时。原因是JSON序列化和网络开销在并发高时被放大。后来改成了gRPC推理交互的延迟从平均400毫秒降到了150毫秒。如果你也在拆AI服务服务间通信尽量用gRPC尤其当你有大量结构化数据要传的时候。第二个是模型文件的管理。老架构里模型直接打进JAR包拆出独立服务后还是有人把模型放Git仓库里体积飙到几百MB拉代码苦不堪言。后来我们专门搭了模型仓库服务用对象存储存模型文件发布时按版本号拉取启动时加载到内存。这一步直接解决了“AI模型和业务代码绑死”的根源问题。第三个是接口的兼容性管理。老系统的业务方有十几个内部系统在调用拆服务时必须保证老接口不能断。我们采用的办法是先做一层适配层老接口继续存在内部转发到新服务等所有调用方确认稳定后再逐步下线。拆分不是一刀切而是有过渡、有兼容、有回退路径的。拆完模型域之后大约半年时间里系统稳定性明显改善。模型更新不再影响主服务推理服务也可以独立扩容。但新的问题也随之出现本来只在单体内部调用的方法现在变成了网路调用网络抖动、超时、重试都成了新常态。这就是分布式系统的代价但方向是对的。4. 云原生改造的落地路径容器化、编排、弹性4.1 容器化的实际动作不只写个Dockerfile那么简单服务拆完之后部署还是传统的“服务器JAR包”模式运维压力开始显现。模型域需要GPU服务器业务域需要普通CPU服务器两套环境手动部署环境不一致经常导致“在我机器上是好的上服务器就挂”。这时候才真正觉得得走容器化这条路。容器化的过程说实话不难难的是把镜像做得干净、小、安全。拿我们的Java业务服务举例子基础镜像不能用带完整JDK的太大了得用JRE精简版。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/crm-service.jar . EXPOSE 8080 ENTRYPOINT [java,-Xms512m,-Xmx1024m,-jar,crm-service.jar]发现没有镜像第二阶段只有运行需要的JRE和产物JAR没有多余的东西。这样镜像大小能从800多MB降到200多MB。另外建议所有服务统一使用同一个基础镜像版本不要各有各的版本否则后面排查安全漏洞时你会被逼疯——每个镜像都要重新拉一遍基础层。还有一个实操细节启动参数要放到镜像外面不要写死在Dockerfile里。内存配置、日志级别、数据库连接串这些用环境变量注入。原因是同样的镜像要跑在不同规格的容器里写死了就失去弹性了。4.2 Kubernetes落地我们踩过的调度和资源坑容器化做好了只是第一步真正让架构变成“云原生”的是引入Kubernetes做编排。我们的集群用了托管版K8s三个节点起步后来扩到十几个。但K8s这东西上手简单用好却非常讲究。第一个坑就是资源请求和限制的设置。刚开始大家习惯只写requests不写limits或者干脆都不写结果某个服务的Pod把节点内存吃满导致同节点的其他Pod被驱逐。后来我们强制要求每个Deployment必须同时配置requests和limits并用垂直推荐工具定期调整。下面是一个我们生产环境里用在推理服务上的配置片段resources: requests: cpu: 4 memory: 8Gi limits: cpu: 8 memory: 16Gi为什么要留一倍余量因为模型推理的负载波动极大高峰期CPU需求可能是平时的几倍。limits设小了会直接OOM杀进程设大了资源又浪费。但注意limits不是越大越好如果limits超过节点总容量K8s可能会拒绝调度。这个balance需要结合实际压测数据反复调。第二个坑是HPA水平自动伸缩的配置。我们最早只按CPU使用率伸缩结果流量高峰来得快新Pod启动慢没等扩容完成用户已经堆了很多请求。后来叠加了基于QPS的伸缩策略每分钟请求数超过阈值就提前扩容。伸缩的最小实例数也要思考一下我们的AI服务最少要有两个实例不然一个挂了就完全没服务了。第三个坑是优雅下线。K8s滚动更新时如果服务处理一个请求需要几秒而Pod直接接受SIGTERM就退出正在处理的请求就会中断。解决办法是配置preStop钩子让Pod在真正退出前先等待几秒把存量请求处理完再配合readiness探针把流量摘掉。代码是这样的lifecycle: preStop: exec: command: [sh, -c, sleep 10]这些问题看起来琐碎但每一个都在生产环境里真实发生并导致过线上故障。云原生不是说用了K8s就完了K8s只是给你提供了一个基础设施骨架真正让系统稳定运行的是把这些细节一个个抠干净。5. 数据层的演进比服务拆分更烧脑的部分5.1 分布式事务承认无法强一致改用最终一致服务拆分后我们遇到的最头疼问题就是跨服务的数据一致性。举一个真实场景一个销售线索从线索池进到客户库里这个过程要更新业务域的客户表还要触发模型域生成一条线索评分记录给销售推荐话术。两个服务都写各自的数据库但业务要求二者不能出现“客户有了但推荐话术没有”的空档。如果继续用单体里的本地事务一句话就搞定。拆分后就复杂了我们不能用一个全局事务去锁两个库那样性能完全承受不住。我们的思路是“尽量不跨服务写数据”具体做法是把某些跨服务动作改成异步消息。线索生成后主服务先写自己的数据然后发一条MQ消息给模型域模型域消费消息写评分和话术如果失败就重试实在不行进入死信队列人工补单。一句话理解我们的取舍逻辑强一致保证不了就对等业务上允许秒级延迟。线索进来了即使话术推荐晚两秒出现销售人员的体验变化并不大。我们用本地消息表加MQ重试机制保证经过几轮重试后数据最终是一致的。这套方案的核心其实是事务消息的思路但实现简单得多适合中小团队。不使用强一致分布式事务如Seata的AT模式的原因还有一个——AI模型的输出本身不是完全确定性的参与分布式事务的写操作太不可控。模型返回的结果偶尔会有延迟或超时如果硬塞进XA事务里整个事务都会被拖死。面向AI场景的系统异步加补偿才是常态。5.2 缓存和数据库一致性一个让人睡不着的话题缓存和数据库的一致性在单体时相对好处理因为写操作经过同一个进程可以先写库再删缓存问题不大。拆分之后写操作被分到多个服务每个服务读缓存的方式还不一样这就乱了。我们的做法是先把读取路径收敛所有数据读取必须走数据访问层禁止各服务绕道直连数据库。然后对缓存更新策略做统一规定不是先更新缓存而是先更新数据库然后删除缓存等下次读取时再回填。这个策略看起来简单但有个坑删除缓存后如果有高并发读请求同时到达缓存回填的压力会很大。我们配合使用了本地缓存加分布式缓存的两级结构热点数据经常命中在一级缓存本地缓存没有就查RedisRedis缺失才回源数据库。可以理解为给查询路径做了一层减速带避免全部流量瞬间打到数据库上。还有一个细节是缓存的过期时间要加随机偏移。如果所有key的过期时间都一致某个业务高峰期就会出现“缓存雪崩”——大量key同时失效请求全部穿透到数据库数据库直接扛不住。我们给每个key的过期时间加一个随机雪崩因子比如基础时间3600秒偏移0到300秒随机数这样过期时间就不会集中在同一个时间点。这些经验今天看起来都是常识但当时都是被线上故障教育出来的。数据层的问题不像服务拆分那样立竿见影而是像慢性病一样慢慢消耗系统的稳定性和你的睡眠质量。6. 线上问题排查实录那些熬夜排查出的架构教训6.1 AI推理超时不是代码问题而是排队机制缺失有一次模型域升级后线上突然出现大面积推理超时。查服务监控发现CPU负载不高、内存正常但响应延迟居高不下。一开始以为是模型代码写得有问题几个人围着模型调参完全没效果。后来看了调用链才发现模型服务是CPU密集型的但请求是并发进来的而模型服务内部是单线程推理队列。其实就是排队机制缺失每个请求进来都直接排队等上一个推理算完一旦并发量上来后面的请求全部超时。这可能听起来很基础但单体阶段不会出现这种问题——因为业务线程池承担了排队功能模型请求被并发量冲淡了。解决方式是在模型服务前面加一层请求缓冲池控制最大并发推理数量超出部分直接返回“繁忙”状态让上游稍后重试。同时把模型实例从2个扩到5个。这样改完之后同样负载下超时率从30%降到了0。这个案例给我的教训是拆分服务后每个服务的流量形态和容错方式都要重新设计直接把老代码搬进新容器解决不了问题。6.2 链路追踪与“日志丢失”事件服务一多排查问题的时间成本暴涨。单体时想看日志直接tail一个文件就行拆成微服务后一次用户请求会经过好几个服务日志散落在不同Pod里。我们一开始用的是“上服务器grep日志”这种原始方式效率极低每次排查都要几个人同时操作不同的机器然后对着时间戳拼现场。后来接入了全链路追踪系统给每个请求分配一个traceId贯穿所有服务调用。Java端通过OpenTelemetry的Agent自动埋点Python推理服务通过手动封装拦截器上报Span。改造完后的感受是排查问题从“考古”变成了“看地图”哪里慢、哪里报错、哪两个服务之间网络耗时大一目了然。链路追踪上线中间出过一个插曲排查时发现日志记录里大量请求的traceId是空的链路断断续续。检查后发现是K8s滚动更新时老Pod还没完全退出新Pod已经开始接收流量两个Pod的日志收集器配置版本不一致导致部分日志丢失。这个问题非常隐蔽最终通过统一所有服务的基础镜像和sidecar日志采集配置才解决。6.3 告警轰炸和无效告警的反思云原生架构上线的头三个月我的手机几乎没有安静过。Prometheus配了一堆告警规则结果每一条都在响磁盘使用率超过80%报警、Pod重启报警、QPS突增报警……但大多数告警根本不需要人处理属于“狼来了”效应——看多了以后真正重要的告警反而容易错过。我后来花了一个周末专门清理告警规则。核心原则是告警必须可执行。每一条告警都要回答三个问题这个告警说明什么谁负责处理处理动作是什么满足不了这三个条件的告警直接删掉。最后留下的告警不到原来的三分之一但每一条响了都值得马上打开电脑看一眼。告警不是越多越好告警质量比告警数量重要得多。7. 回到起点复盘三年演进中我认为最值得说的事如果你问我这套智能客户关系AI系统从单体到云原生最重要的一步是什么。我的答案是想清楚“哪些东西可以拆哪些东西不能拆”。可拆的是计算和存储模型推理是独立计算可以拆出来做成单独服务客户数据、对话记录是业务数据可以拆出独立数据库和缓存层。不那么好拆的是状态和一致性用户会话状态、跨服务的业务事务、模型数据和业务数据之间的关联这些必须谨慎设计不能简单粗暴地“一拆了之”。我们的拆分顺序和控制节奏简单概括就是先拆AI模型域这种资源孤岛再拆业务域内的稳定模块最后才碰用户会话和交易链路这些核心敏感区。整个过程走完用了近两年中间经历了好几次反复。第二个经验是架构演进要跟着业务走不要为了技术先进而上云原生。团队的精力是有限的上半年我们拆了服务下半年就专心补监控和追踪再下一年才配合容器化和K8s做弹性伸缩。每一个阶段的改造都被真实的线上问题驱动改造完成后也确实解决了问题。如果一开始就“一步到位”搞微服务加K8s大概率会像很多团队那样把时间耗在基础设施折腾上实际业务没跑通。第三个经验是AI系统的架构演进不能完全照搬传统业务系统的模式。传统业务系统讲究的是事务一致性、强数据可靠性但AI系统是概率性的模型输出天然有不确定性推理延迟也受资源影响较大。所以AI服务的架构设计要更加宽容异步化、重试机制、缓存预测结果、降级到规则引擎这些都是AI系统特有的容错手段。如果你正在做一个AI相关的系统建议在架构设计之初就把这些因素考虑进去。最后说一个最实际的体会架构师这个角色最重要的产出不是架构图而是让团队每个人知道“为什么要这样设计”。如果没有共识再好的架构落地时也会变形。三年里我开了无数次技术分享会把每一次拆分的原因、每一个方案的取舍都讲给团队听。事实证明这些东西比任何架构评审都有用。转型成功不是靠某一个人的远见而是靠整个团队在理解架构意图之后把自己的部分做扎实。
返回列表