ARTICLE DETAIL

资讯详情

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

从B/S到云原生:架构演进的核心逻辑与落地实践

从B/S到云原生:架构演进的核心逻辑与落地实践 做了十多年软件架构我最大的感受是每一次架构演进都不是技术人员拍脑袋想出来的而是业务体量和复杂度一步步逼出来的。从最早的单机程序到B/S结构再到分布式、微服务最后走到云原生这条演进路径背后藏着一条清晰的逻辑线——计算资源的组织方式在变系统拆分与协作的颗粒度在变而架构师要解决的核心矛盾始终是如何用更低的成本支撑业务的快速变化和规模化增长。今天这篇内容我打算把这十几年踩过的坑、总结的经验按照“从B/S到云原生”这条主线完整梳理一遍。适合刚入行的后端开发、正在做架构选型的技术负责人以及准备把老系统往云上迁移的团队。我会讲清楚每个阶段架构长什么样、为什么长这样、它解决了什么问题又留下了什么隐患以及落到实操层面你应该怎么做选型和迁移。不吹概念只说人话。1. 先说结论架构演进从来不是炫技而是被业务逼出来的很多人在学习软件架构时会陷入一个误区总觉得新的架构模式一定比旧的先进什么都想往微服务上靠什么都想容器化。实际上架构演进的历史反复告诉我们一件事没有最好的架构只有最匹配当前业务阶段的架构。你看B/S架构当年为什么能取代C/S架构不是因为浏览器技术有多惊艳而是因为企业软件有一个极其头疼的问题——客户端升级。C/S时代业务逻辑分散在每个用户的电脑上发一个新版本就要挨个装装错了还要派运维去现场处理。B/S把业务逻辑全部收敛到服务端客户端只需要一个浏览器升级一次服务端所有用户立刻就拿到了新功能。这个转变的本质不是技术胜利而是运维效率的胜利。到了云原生阶段也是一样。容器和Kubernetes之所以流行是因为当服务数量从几个变成几十个、上百个时手工部署、手工配置、手工扩容已经完全做不过来了。自动化成为唯一的出路。所以你看架构演进的每个节点都是在回答一个迫在眉睫的业务问题规模变大了、团队变多了、需求变快了现有架构撑不住了。理解了这个前提后面所有技术细节你都能找到归属感。你学B/S不是为了怀旧是为了理解“集中式管理”为什么有效你学微服务不是为了追新是为了理解“拆分的边界在哪里”你学云原生不是为了赶时髦是为了理解“基础设施自动化”能帮你省下多少人力。带着这条主线去阅读下面每个章节你会比绝大多数只会背概念的人看得通透得多。2. B/S架构的二十年把复杂度收敛到服务端2.1 B/S架构的本质浏览器只是壳核心在服务端B/S架构全称Browser/Server架构翻译成大白话就是业务逻辑和数据处理全部放在服务器上浏览器只负责展示和收集用户操作。这一句话听起来简单但它背后带来的连锁反应是革命性的。在C/S架构时代客户端承担了大量业务逻辑。比如一家银行的柜台系统网点电脑上要装专门的客户端软件版本混乱、环境依赖复杂、安全补丁滞后每次升级都是一场灾难。而B/S架构把这一切反了过来服务器端统一承载业务逻辑客户端浏览器通过HTTP协议与服务器交互页面用HTML/CSS/JavaScript渲染。你不需要在用户电脑上装任何东西部署和升级变成了一件“只动服务器”的事。从技术实现上看典型的B/S系统包括三个角色浏览器发起HTTP请求Web应用服务器执行业务逻辑Tomcat、Jetty、Nginx等数据库服务器存储数据MySQL、Oracle、PostgreSQL等。三者通过标准协议解耦每一层都可以独立升级和替换。这种解耦带来的维护性优势是B/S能在企业级应用里横跨二十多年而不衰的根本原因。我在实际项目中见过不少团队直到今天还在用“B/S的心智模型”做系统设计服务端渲染页面、会话保持、集中式权限控制。这套模型对内部管理系统、中小型电商后台、政务系统依然非常适用。不是新架构不好而是这个场景里集中式管理的优势就是最大的优势分布式反而徒增复杂度。2.2 经典分层与核心技术栈B/S架构落地时业内沉淀出了一套几乎标准的分层模式层级职责典型技术表现层页面渲染、用户交互、表单校验JSP、Thymeleaf、Vue/React前后端分离后业务逻辑层业务规则、流程编排、权限判断Spring Boot、Struts2、PHP Laravel数据访问层数据库读写、ORM映射MyBatis、Hibernate、Spring Data JPA基础设施层缓存、消息、文件存储Redis、RabbitMQ、NFS/OSS这套分层的关键点在于“单向依赖”表现层只调业务层业务层只调数据层禁止跨层调用。很多人写代码时图省事在Controller里直接写JDBC连数据库短期看很快长期看维护成本极高。一旦表结构变更SQL散落在所有接口里改一处漏十处。我见过太多系统最后变成“一座屎山”根源不是技术选型差而是分层纪律从一开始就没有执行下去。另一个核心概念是会话状态管理。HTTP协议本身是无状态的但业务系统天然需要“记住用户”。早期方案是Cookie SessionSession存在服务器内存里负载均衡做不了Session共享一扩机器用户就掉线。后来演进出Redis集中式Session现在更普遍的是JWT Token。这里我要特别提醒无状态化是B/S架构演进过程中最重要的转折点一旦你把用户状态从服务器内存挪到客户端Token横向扩容就畅通无阻了这套思路一直影响到了后面的微服务和云原生设计。2.3 B/S架构的瓶颈它解决了什么还留下什么B/S架构解决的是“客户端维护难”的问题但它自身也有三个绕不开的天花板。第一个是服务端压力集中。所有计算都堆在服务器业务量一大单机就扛不住。解决方案只能靠堆硬件和加集群而加集群又会引入Session同步、缓存一致性、数据库单点等连锁问题。第二个是网络延迟的放大效应。每一次页面跳转、每一次Ajax请求都要走“浏览器—服务器—数据库”的完整链路UI交互体验天然比本地应用差一截。第三个是数据库瓶颈难以突破。传统B/S系统最常见的架构是“一台应用服务器 一台数据库”数据库一旦成为瓶颈读写分离、分库分表这些手段就会接踵而至而这些手段已经属于分布式架构的范畴了。所以B/S架构并没有消失它只是把接力棒交给了下一棒当单个应用服务器的处理能力无法匹配业务增长时你需要把应用拆成多个可以独立部署的模块这就是后面分布式架构的来源。组织架构上也会出现一个有趣的现象——开发团队从十个人扩展到五十个人时大家在一个代码仓库里提交代码的冲突会越来越频繁单体应用的发布互相牵制谁都不敢随便上线。这个矛盾是逼着大家走向微服务的最大内部驱动力。3. 从单体到微服务拆分的逻辑与代价3.1 为什么要拆单体架构写不下去的五个信号是不是所有系统都要拆成微服务绝对不是。我在前面重复强调过很多次架构要匹配业务阶段。但如果你所在的团队出现了下面五个信号中的两到三个就得认真考虑拆分了代码仓库已经大到连IDE打开都要卡几秒编译一次要等三五分钟。不同模块的业务逻辑互相纠缠改一个订单状态结果支付模块也跟着返工。发布频率被严重拖累高价值功能要等低优先级功能一起联调上线窗口越排越长。团队之间互相踩脚多个小组在同一仓库里频繁产生冲突代码评审流于形式。扩展出现结构性失衡比如你只想扩容短信发送模块但单体应用只能整机复制连带把不需要扩容的模块也扩了一遍。这里我要说一句容易被骂但很实在的话如果你没有出现这些信号就老老实实待在单体里。微服务解决的是复杂度问题而不是帮你的简历镀金的问题。一个只有几十万日活的系统硬拆成二十个微服务结果是每个服务都闲得发慌但运维成本、机器成本、代码之间调用的心智成本翻了好几倍。我见过不止一个初创团队死磕微服务最后把人力和资金全耗在基础设施搭建上业务增长反而被拖死了。3.2 微服务的核心基础设施没有这些别动手当你决定拆分你要做好心理准备微服务不是一个技术方案而是一整套工程体系。你可以简单类比一下把一个单体公司拆成十几个独立小公司独立是自由了但谁来统一财税谁来协调跨公司沟通谁来处理纠纷这些“公共服务”都得重新建设。微服务的基础设施门神至少包括下面四件套服务注册与发现服务实例启停频繁IP地址动态变化需要一个注册中心来维护“谁在哪、还活着没”。主流选型有Nacos、Consul、Eureka。配置中心几十个服务的配置分散在各自的配置文件里改一个参数就要分别改动再重启太痛苦。用Apollo或Nacos配置中心集中管理支持动态刷新是微服务的硬需求。链路追踪一次用户请求会跨三四个服务任何一个环节慢你要能快速定位到底慢在哪。SkyWalking和Jaeger是两种主流方案建议在微服务改造的第一天就接上不要等出了故障再补。熔断限流与降级依赖的下游服务如果响应变慢或挂了你要有自己的兜底策略不能让故障像雪崩一样传递。Sentinel和Resilience4j都值得参考。另外微服务的拆分还伴随着一个最容易被低估的问题——分布式事务。单体的数据库事务跨了库之后不再有效。典型场景是“下单减库存”订单库和库存库在数据库层面分开了你怎么保证两个操作要么都成功要么都失败分布式事务的成熟方案包括基于消息的最终一致性、TCC补偿模式、Saga模式但我要诚实告诉你每一种方案都有复杂的实现代价没有银弹。所以很多团队采用的折中策略是能不拆库就不拆库服务拆了但数据仍然集中在同一个数据库里这虽然不“纯粹”但在一定规模内完全是好方案。3.3 服务网关与API治理微服务的门面微服务化之后客户端直接面对的是几十个不同的服务地址显然不现实。API网关的出现就是为了解决这个问题统一入口、协议转换、鉴权、限流、路由转发。实际项目中我推荐两个方向如果要深度定制而且团队有Java底子Spring Cloud Gateway是顺滑的选择如果希望性能更强、可编程性好APISIX或Kong这类基于Nginx/OpenResty的网关是更好的选择。选网关的时候要重点看三个能力插件扩展机制能不能方便地加自定义过滤器、控制面的管理友好度可视化界面、配置发布、性能开销网关会成为所有流量的必经之地延迟损耗必须低。API治理层面还要注意版本管理。移动端App更新是滞后的你今天把接口从v1改到v2不能要求用户立刻升级。所以网关层面要支持路由到不同版本的服务实例同时维护多版本并存。这是很多团队做微服务时容易忽视的细节等线上用户因为接口不兼容而大批量报错时你才会追悔莫及。4. 云原生容器、编排与平台工程4.1 云原生的本质让基础设施变成“API”云原生这个词这几年被炒得很热但很多人没抓住它的本质。我的理解是云原生不是一种具体技术而是一种思维方式——把基础设施当成可编程的、自服务的资源。传统方式是你向运维申请一台服务器、申请一个数据库、再申请一个负载均衡流程走完要几天云原生方式是你通过配置文件声明我要多少个实例、多少内存、需要什么存储平台自动帮你创建、调度、回收。CNCF云原生计算基金会给出的四大核心方向是容器化、微服务、DevOps、持续交付。听起来很多实际落地时你每天打交道的主要是这么几样用Docker或者containerd把你的应用打包成镜像用Kubernetes管理容器的运行和伸缩用CI/CD流水线实现代码提交后自动测试、自动构建、自动发布。这一套组合拳打下来你会发现一个最直观的变化部署从“手工操作工单”变成了“提交代码自动生效”。以前发版要熬夜要写一堆操作手册怕遗漏现在只需要一条流水线回滚也只是切换镜像版本这个转变对研发效率的提升是革命性的。还有一点我想特别强调Serverless无服务器计算也属于云原生的重要分支。它比容器更进一步你连容器节点都不用关心了直接写函数平台按调用次数计费。适合事件处理类场景比如图片压缩、消息推送、定时任务。但它不太适合有状态、长连接的核心业务系统。我的建议是Serverless可以先去边缘场景用起来但核心系统暂时不要碰。4.2 容器化实践从 Dockerfile 到镜像仓库容器化是云原生的第一步让我用一个具体例子说明完整的实操流程。第一步你要把项目做成镜像。假设我们有一个Spring Boot项目最简单的Dockerfile写法如下FROM openjdk:17-jre-slim WORKDIR /app COPY target/demo.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, app.jar, --spring.profiles.activeprod]这里有几个细节值得说。镜像基础层不要选“openjdk:latest”这种大而全的Tag尽量挑slim版镜像体积能小一半以上。构建时千万不要把整个项目目录COPY进去只COPY构建产物jar包层数尽量少这样后续发布时可以利用缓存构建速度会快很多。第二步把镜像推到镜像仓库。企业内部一般用Harbor或Nexus公共的可以用Docker Hub或者各云厂商的镜像服务。推之前一定要配置好安全扫描基础镜像有漏洞会连带你的应用一起暴露。第三步在Kubernetes里部署。最简单的Deployment声明如下apiVersion: apps/v1 kind: Deployment metadata: name: demo-app spec: replicas: 3 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: demo image: harbor.example.com/demo:1.0.0 ports: - containerPort: 8080 resources: requests: memory: 512Mi cpu: 250m limits: memory: 1Gi cpu: 500m配置resources我特别提醒一下。很多团队第一次上K8s都不写资源限制结果某个服务内存泄漏把整个节点的内存耗尽节点上的其他Pod跟着陪葬。limits不能省requests也要认真评估。刚上手时requests给10%-20%的冗余limits设成requests的1.5到2倍比较稳妥后面根据监控数据再慢慢收敛。4.3 Kubernetes选型与落地要不要上怎么上Kubernetes已经是容器编排的事实标准但“要不要上K8s”这个问题我的答案非常明确看你的系统规模和团队运维能力。如果你只有三五个服务用户量也不大直接用云平台的容器服务或者Docker Compose管理就够了。K8s的学习曲线和运维成本是客观存在的它解决的问题自动扩缩容、自愈、滚动发布在小规模场景里根本体现不出来。但如果你有十几个以上的服务或者有明确的弹性扩缩容需求那K8s几乎是绕不开的选项。落地方式上我建议按阶段走第一步业务代码容器化先把Docker镜像交付做标准化不碰K8s。第二步引入托管K8s优先使用云厂商托管版本如ACK、TKE、EKS不要自己用二进制搭建集群。自己搭集群意味着etcd备份、证书续期、控制平面高可用都要自己维护这些坑每一个都能让你痛苦很久。第三步逐步接入自动伸缩和滚动发布用HPAHorizontal Pod Autoscaler根据CPU或者自定义指标扩缩容再配上健康检查、滚动更新策略。第四步再考虑服务网格、多集群、跨可用区容灾等高级能力。不要一上来就全都要。这里我想重点讲一下Pod健康检查。很多人部署了Deployment但没做readinessProbe就绪检查结果服务刚启动还没初始化好流量就立刻打进来了导致前几分钟大量500报错。正确做法是配置好两个探针readinessProbe负责判断“能接流量了吗”livenessProbe负责判断“进程还活着吗”。探针路径用应用的健康检查接口不要把业务接口当探针用否则业务逻辑异常时K8s会一直重启Pod造成可用性反而下降。4.4 服务网格与可观测性云原生时代的运维底座服务网格Service Mesh是近几年微服务和云原生深化后绕不开的话题典型代表是Istio、Linkerd。它的核心思路是服务间通信的逻辑重试、超时、熔断、流量管理、灰度发布从业务代码里抽出来下沉到一个轻量级代理Sidecar里业务代码只关心业务。理念上很好但我要给一个诚实的警告不要轻易上Service Mesh。这个技术对团队基础设施能力的要求非常高Sidecar带来的额外延迟、资源开销以及排查分布式问题的难度远超一般团队的承受能力。我接触过的绝大多数团队在引入Istio后又放弃核心原因是“太复杂出了问题不知道怎么排查”。我的建议是如果当前没有明显的灰度发布治理、流量精细管理需求先用网关客户端负载均衡做起来服务网格这些高级能力等团队真正具备了平台工程能力再考虑。不管上不上Service Mesh可观测性建设都必须做扎实。云原生架构下服务数量多、实例动态变化靠“登录服务器看日志”已经是石器时代的方法了。三件套必须齐全观测维度解决什么问题常用技术指标监控系统整体健康状况、资源水位Prometheus Grafana日志收集错误排查、审计、行为分析ELK、Loki、ClickHouse链路追踪一次请求跨服务的耗时分析SkyWalking、Jaeger、Zipkin这三样东西要在微服务改造的第一天就埋进去越晚补越痛苦。我见过太多系统线上出故障后全公司的人都去捞日志翻了几百兆文件才定位到某一个服务超时效率极低。有了链路追踪后一次请求从头到尾的调用树都清楚了哪个环节慢一目了然。我再分享一个实操技巧链路追踪里有一个“慢调用采样”的做法全量采样很消耗性能生产环境一般用10%-20%的采样率但把大于某个阈值比如3秒的请求强制上报这样你既控制了成本又不会漏掉真正的性能问题。5. 架构选型实操从0到1设计一个系统的决策框架5.1 业务阶段与架构匹配别让架构跑在业务前面在我做架构咨询和评审时最常看到的问题就是“架构跑在业务前面”。产品还在验证阶段需求每天都在变团队却花三个月搭微服务基础设施。等基础设施搭好业务方向可能已经变了。所以我一直推荐一个非常务实的“分阶段选型”策略业务阶段推荐架构理由产品验证期MVP单体应用 单库部署方式最简单越好快速验证、快速推翻成本最低业务增长期单体分层 Redis缓存 数据库读写分离撑住流量增长架构不动方案加码规模化期按业务边界拆微服务 容器化 自动化发布团队扩张、独立伸缩、独立发布的需求显现平台化期全面云原生 平台工程 服务治理多产品线共享基础设施追求标准化这个表格不是我拍脑袋写的而是大量项目的真实轨迹。你会发现架构演进的节奏应该跟着业务痛点走而不是跟着技术热点走。当系统在单库单应用下运行时没有任何痛点就不需要引入分布式。这个“痛点驱动”原则是我这么多年做架构决策时最重要的衡量标准。5.2 技术选型的权衡清单做技术选型时我会把一个候选方案放在三个维度里打分团队熟悉度、社区活跃度、运维成本。技术再先进如果团队里没人真的用过学习成本和踩坑成本是隐性炸弹。我会看这门技术的社区活跃度一个周更issue都处理不过来的冷门框架再好的设计理念也抵不过出了bug没人答的风险。而运维成本是最容易被低估的——新增一个中间件意味着升级、监控、告警、备份、权限管理都要跟着建起来一个组件每月的隐性运维成本大约要算成0.2到0.5个人力。比如消息队列选型团队里有精通RabbitMQ的人就先用RabbitMQ别为了“性能更强”直接上Kafka。等消息量真正大到RabbitMQ撑不住时再引入Kafka也不迟因为到那个阶段你才真正知道需要Kafka的哪些能力。判断一个系统是否真的需要引入新组件我会问自己一句话不加它能扛到什么时候加了它能多扛多少如果答案不明确就不加。5.3 老系统演进路径绞杀者模式实操对于已经有老系统的团队我推荐“绞杀者模式”Strangler Pattern来做渐进式重构。这个模式的核心思路是不要一次性推翻重写而是在老系统外面慢慢包裹新架构把功能一个接一个迁移到新系统最后让老系统自然消亡。具体操作上我常用的路径是这样的在老系统入口处加一层网关从新老系统的接口路由开始做平滑切换。选择业务边界清晰、独立性强、变更频繁的模块作为第一批迁移对象比如“用户中心”“消息通知”把这类模块拆出来做成独立服务。新服务上线后用灰度流量切换验证先放5%的用户到新服务观察监控指标稳定后再逐步放量。老系统里迁空的模块确保没有新流量后再下线代码不要急着删数据库表保留只读权限观察几个大版本周期。这个模式最大的好处是风险可控。迁移过程中如果新系统出问题老系统还能随时顶上不会出现“断崖式重构失败”的情况。我见过太多项目因为“反正要重构干脆推倒重来”结果业务部门等不了三个月的重构周期团队在巨大压力下仓促上线bug遍地。实际踩坑中我提醒一句绞杀者模式里最难的其实是数据迁移。服务拆分后原来的用户表、订单表可能被多个服务共享拆分时必须明确数据的“属主”。共享数据不要直接拆先通过API访问等接口稳定后再考虑物理拆分这是最稳妥的策略。6. 我踩过的坑与排障实录6.1 分布式事务拆库之后最难啃的骨头去年我给一个电商客户做订单服务拆分拆完服务、联调通过、上线试运行前三天一切正常。第四天大促预热时出现了严重的对账不平订单库里订单状态已经是“已支付”支付流水库里找不到对应记录。问题就出在“下单减库存、扣款生成流水”这两个跨库操作上没有统一事务保证。后来我们改成了基于本地消息表消息队列的最终一致性方案核心流程先把业务操作和消息写入同库的本地消息表通过事务保证“业务操作成功消息一定落库”再由一个后台任务把消息表里的记录可靠地发到MQ下游消费者消费消息执行扣库存或送积分等操作。这套方案牺牲了强一致性换来的是极高的可用性和可排查性——每一条消息的状态都能在消息表里查到出问题重放即可。我要强调一点分布式事务的原则是能避则避。与其引入复杂的TCC、Saga框架不如从业务设计层面规避跨库事务。比如把“订单”和“支付流水”设计成同一个库里的不同表或者把用户发积分这类弱一致操作改成异步补偿。技术永远是为业务服务的能通过业务设计消解的技术复杂度就不要用更高级的技术去硬扛。6.2 性能排查一次典型的线上故障定位过程再分享一个性能排查的实例。有个老客户报障说“每周三上午十点系统必卡”单次请求耗时从正常50ms飙升到3秒。我们的排查路径是这样的第一步看监控大盘发现Web服务器的CPU没有异常数据库慢查询数量却暴涨。第二步拉慢查询日志发现一条SQL的执行时间从几十毫秒变成了两秒多但这个SQL本身没变过。第三步看执行计划发现表走了全表扫描——索引明明存在但优化器没走。最后定位的原因很反直觉有个定时任务每周三上午扫全表更新数据它开启的事务一直没提交导致另一个事务读数据时元数据版本信息冲突优化器干脆放弃了索引。问题不是SQL写错了而是长事务把整个库的查询计划搅乱了。解决方式也不难定时任务改成小批量提交加上合理的索引覆盖问题瞬间消失。这个案例送给大家一个经验排查性能问题别一头扎进代码里先看监控、再看慢查询、再看执行计划、再看是否有长事务从上到下扫一遍。90%的线上性能问题都是慢SQL、长事务、缺索引、缓存穿透这几个常见根因逻辑清晰后定位并不难。6.3 成本意识架构复杂度是隐形负债最后聊一个我特别想提醒的话题架构决策里的成本意识。很多人只关注架构“能带来什么”很少关注它“在消耗什么”。任何一个引入的中间件、一个拆出来的微服务、一套“高大上”的治理平台都会消耗团队的研发时间、运维精力和机器预算。我见过一个团队为了追求“最先进”线上同时维护了Spring Cloud、Service Mesh、Serverless三种调用形态每个服务团队各搞一套互相之间的规范完全不一致。新同事入职三个月都搞不清服务到底部署在哪里更别说排查问题。这种过度设计的架构实际上是在为企业制造巨大的隐形负债。我的体会非常直接架构选型有一个很俗但很准的判断标准——如果明天其中某两个核心组件挂了你的团队需要多久才能恢复如果答案超过一天说明你的架构复杂度已经超过了团队的可控范围。控制欲望、保持简单、把技术栈收敛到团队真正熟悉的范围内这本身就是一种能力。云原生、微服务都不是终点它们只是帮助你以更高效的方式去响应业务变化的手段。判断一个架构好坏不看你用了多少新潮技术只看它是否让团队在迭代新功能时感觉轻松让团队在系统出故障时能快速定位让公司不必为“技术先进性”付出高昂的沉默成本。回头来看从B/S到云原生这条路贯穿始终的关键词其实只有一个——控制复杂度。B/S把客户端复杂度往服务端收微服务把单体复杂度往外拆云原生把基础设施复杂度交给平台自动化每一步都是在跟复杂度做斗争。作为开发者或架构师你真正需要修炼的不是背多少概念而是准确判断“当前最大的复杂度在哪里用什么手段去控制它”。把这条主线想透了这套架构全景图就真正变成你自己的了。
返回列表