ARTICLE DETAIL

资讯详情

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

云原生架构演进:从微服务到Service Mesh的落地实践

云原生架构演进:从微服务到Service Mesh的落地实践 云原生时代的网络架构演进从微服务到Service Mesh最近几年微服务几乎成了中大型系统的标配但跑着跑着大家会发现一个尴尬的问题服务拆得越细服务之间的通信就越复杂。超时、重试、熔断、限流、灰度发布、链路追踪……这些治理能力原本在分布式框架里还能靠SDK解决可一旦上了Kubernetes、容器弹性伸缩成为常态SDK方案的短板就暴露得越来越明显。于是Service Mesh开始频繁出现在技术讨论里。这篇文章我想从一线实践的角度聊聊这波网络架构演进微服务化之后网络层面到底发生了什么变化为什么越来越多的团队开始转向Service Mesh以及如果你准备落地Service Mesh有哪些可以直接参考的思路和踩坑记录。全文不搞理论堆砌尽量说人话适合正在做微服务改造、想搞清楚服务网格到底能解决什么问题的同学阅读。1. 微服务化之后网络通信为什么会成为“隐形瓶颈”1.1 从单体到微服务网络从“内部调用”变成“第一公民”单体架构时代模块之间是进程内的方法调用效率高、路径短出了问题一个线程栈就能追完。但微服务架构把原本在一个进程里的逻辑拆成了多个独立部署的进程通信从函数调用变成了网络请求。这一步转变的代价很多人一开始是低估的。原来你在单体里调用一个支付模块就是payService.pay(order)编译器直接帮你完成类型检查异常处理也走的是语言层面的机制。拆成微服务之后支付变成了http://pay-service/order/pay这样的HTTP请求引入了网络延迟、连接管理、超时控制、序列化协议、负载均衡等问题。你不再只是写业务代码还得考虑服务怎么被发现、请求怎么路由、失败怎么兜底。更关键的是微服务的通信不是线性的它是网状扩散。假设你有20个服务理论上最多存在380条调用关系每一条都可能出问题。单体时代的“发现问题靠日志定位问题靠断点”在微服务里完全行不通。这也是为什么微服务落地过程中团队普遍会经历一个阵痛期服务变多了故障也变多了而且变得“查无可查”。网络通信在微服务架构里已经从“业务之外的技术细节”上升为“决定系统稳定性的第一公民”。如果你不把这一层的治理能力体系化地建设起来后续每加一个服务都是在往不稳定的天平上加砝码。1.2 微服务时代网络通信的四大痛点我在多个项目里做过微服务改造总结下来网络通信层面的痛点基本集中在四个方面服务发现与负载均衡。Java生态里Spring Cloud体系靠Nacos、Eureka这类注册中心解决“服务在哪”的问题客户端从注册中心拉取实例列表再通过RestTemplate或Feign做负载均衡。但问题在于这种发现机制和语言、框架深度绑定如果团队里有多个技术栈Java、Go、Python每个技术栈都要各自实现一套服务发现和负载均衡逻辑维护成本极高。超时、重试、熔断等容错策略。这部分传统上是Hystrix、Sentinel、Resilience4j这些库的活儿。库方案的问题在于配置分散在各个服务里每个团队自己写配置、自己调参数A服务的超时时间可能是1秒B服务是3秒上下游一联动超时叠加导致的级联故障很难排查。而且这些库一旦升级引入新特性所有依赖方都得跟着升级牵一发而动全身。流量治理与灰度发布。早期做灰度发布只能按服务维度整体发布做不到用户维度的精细灰度。比如你想让某个百分比的请求走新版本服务在SDK模式下需要自己实现流量特征的传递和路由规则改动业务代码是免不了的。更麻烦的是这些规则散落在不同代码库里没法集中管理。可观测性。微服务拆得越细一次用户请求背后可能跨五六七八个服务。如果没有一套统一的链路追踪只能靠各个服务自己打日志再手工串。出了问题排查一次线上故障的时间可能比修复的时间还长。这四个痛点归纳起来其实就一件事微服务拆完之后网络通信的治理能力没有跟上。你有一堆房子服务但连接房子之间的道路网络没有统一规划久而久之交通必然瘫痪。2. Service Mesh的架构设计与核心原理2.1 边车模式把治理能力从业务代码里剥离出来前面说的痛点本质上都源于一个结构性问题治理能力被嵌入到了业务进程里。无论是Hystrix、Sentinel还是各种服务发现客户端它们都以SDK的形式和业务代码运行在同一个进程里。SDK方式有一个天然的分裂问题一方面业务开发者被迫关注“非业务”的技术细节每个团队的代码库里都充满了熔断、限流、超时配置另一方面SDK的版本更新会导致全链路升级因为老版本修复的Bug或者新特性的引入都需要每一个服务方配合升级。大版本更迭的时候几十个服务轮番上线这种升级周期可以拖上数周既痛苦又危险。边车模式不是把所有治理能力塞进SDK而是把这些能力下沉到一个独立于业务进程之外的代理进程里。这个代理进程和业务容器一起部署在同一个Pod内局域网内共享网络所有进出Pod的流量都必须经过它。它拦截业务进程的入站Inbound和出站Outbound流量在流经的同时完成负载均衡、熔断、重试、限流、指标采集等治理动作。如果拿交通做类比sidecar就相当于你要从小区到城市各处不用自己改装车上的导航和定位系统而是在每个小区门口建一个智能收费站你只管开车经过行车路线、收费规则、拥堵预判都是收费站帮你搞定。服务与服务的通信不再关心“对方在哪”只需要把流量交给身边的收费站。这种模式的最大价值是解耦。业务代码只关心“我要调用哪个服务的什么接口”至于服务从哪里发现、超时怎么处理、失败要不要重试全部由sidecar代理完成。2.2 数据平面与控制平面网络治理的“手脚”和“大脑”真正深入Service Mesh之后你会频繁听到两个词数据平面Data Plane和控制平面Control Plane。理解这两个概念基本就理解了Service Mesh的工作方式。数据平面由Mesh里所有sidecar代理组成它负责处理每一个业务请求。一个请求从服务A发往服务B流程是A的业务进程把请求发出经过A身边的sidecarsidecar根据规则决定转发到哪个实例然后请求到达B身边的sidecar再进入B的业务进程。整个过程中sidecar是真正“干活”的节点它要完成负载均衡、连接池管理、TLS终止、超时和重试、指标采集等工作。控制平面是管理sidecar配置输出的中枢。它通过API Server和配置下发机制把统一的路由规则、熔断策略、证书信息下发到每个sidecar代理。举个例子你想把5%的流量切到金丝雀版本。控制平面就是你下发策略的“总控制台”数据平面才是真正执行这5%切分的“执行者”。实际落地的三个平台方案通常是Istio、Linkerd和Consul Connect。其中Istio是社区最活跃、生态最完整、生产案例最多的选择。它默认的数据平面是Envoy控制平面自Istio 1.5之后统一为istiod模块XDS协议负责配置下发。这个“手脚分离、由大脑统一指挥”的设计让网络治理能力首次具备了“集中定义、分散执行”的形态。你在控制平面定义一次规则全Mesh范围内的所有sidecar统一生效不用再一个个服务去改配置、发版本运维效率的提升是肉眼可见的。2.3 为什么选了Istio而不是自研或Linkerd选型这件事每个团队的情况不同但经过多个项目的比较我更倾向于Istio。理由有几方面首先是Envoy的成熟度。Envoy作为数据平面不是Istio团队从零造的它本身是Lyft开源的高性能代理在API Gateway、边缘代理、大流量入口都有大量生产验证。它提供的熔断、重试、批量访问日志、健康检查、Load Balancing策略等能力覆盖面之广在同类代理里很难找到对手。其次是生态和市场验证。Istio有Google和IBM背书CNCF治理下的项目围绕它的可观测性、安全性、多集群方案层出不穷。真到出了问题时能找到的参考案例和经验远比自研方案多得多。自研方案当然也有诱惑完全可控、没有外部依赖、可以贴合业务定制。但代价同样明显自研一套稳定的数据平面代理至少耗费两三个季度的人力和金钱而且同类问题别人已经踩过无数坑。对大多数团队来说自研Service Mesh的性价比很低。Linkerd的优势是轻量、简单但控制平面能力相对弱尤其在多集群、外部服务的集成上明显不如Istio丰富。3. 从微服务到Service Mesh的落地实操一次真实的改造记录3.1 落地前先想清楚你的系统适不适合引入Service Mesh如果你以为Service Mesh是安装好就能用的那就错了。我们第一次尝试的时候就是因为“准备工作不足”后来在灰度阶段被坑得很惨。先评估系统规模。如果只有五六个服务引入Service Mesh的运维成本可能比收益还大。一般来说服务数量达到二三十个以上或者团队存在多语言技术栈才值得考虑Service Mesh。服务数量少完全可以在服务框架层面解决通信治理问题不需要额外维护一堆sidecar。再评估团队运维能力。Service Mesh会引入新一套控制平面你得有人熟悉Istio或Linkerd的安装、配置、升级和故障排查什么时候Envoy配置下发失败、什么时候DNS解析异常都是老司机才知道的经验。如果团队缺乏这方面积累建议先小范围试点不要一下子全量上。最后是网络环境。Kubernetes是Service Mesh的最佳运行场所如果你目前还没上Kubernetes想在传统虚拟机环境里硬跑Service Mesh不是不行但会非常痛苦。服务发现、Pod生命周期调度、DNS解析都要你自己处理。以我们当时的情况为例整个系统有30多个微服务Java和Go混编Kubernetes集群已经跑了半年服务之间的调用依赖很复杂原有的Spring CloudNacos方案在跨语言场景下已经力不从心。我们是典型的“该上Service Mesh的时机”了。3.2 网格的部署从零搭建Istio完整流程如果确定要上部署Istio是第一步。这里给出一个安装和基础配置的参考流程细节比较多但每一步都有讲究。首先是安装Istio CLI工具。Istio官方维护了一个命令行工具可以直接管理istio的安装、升级和卸载# 下载指定版本的istio二进制 curl -L https://istio.io/downloadIstio | sh - cd istio-1.21.0 export PATH$PWD/bin:$PATH安装方式选择了“profiles”。Istio把一些常见场景的默认配置做成了profile测试环境用demo最方便生产环境建议使用default它默认关闭了多余组件只保留最核心的Pilot和Ingress Gateway。istioctl install --set profiledefault -y启用自动注入边车标记需要注入的命名空间。这个动作会让后续部署到该命名空间下所有带sidecar.istio.io/inject: true注解的Pod自动挂载sidecar容器kubectl label namespace default istio-injectionenabled这里特别提醒一个坑istio-injectionenabled这种标签注入是全局的如果命名空间里已经存在存量Pod需要滚动重启它们才会注入边车。更推荐的做法是使用注解的方式注入比如在Deployment模板中加注解metadata: annotations: sidecar.istio.io/inject: true这种方式更精确可以按需注入不会把无关的Pod也拉入网格。我们后来就是靠注解方式做渐进式改造的一次只注入一批服务避免全量注入导致学习成本和故障冲击过大。3.3 渐进式改造先让业务无感再逐步接流量部署完网格接下来是改造上线。我们的策略是“三次分批”不追求一步到位。第一批只做可观测性。把服务接入网格后先不开任何流量路由规则只是让sidecar自动收集mTLS、HTTP指标、调用链信息。这一批的目的非常明确先把服务之间的调用拓扑看清楚看看有无隐藏的依赖排查掉那些运行了好几年但还没人完全理清的调用链。建好Grafana大盘后业务的调用量、延迟、错误率一目了然。这一步对团队来说几乎是透明的因为流量路径没变只是绕行了一个sidecar。第二批启用流量管理能力。在已有的Nacos注册中心基础上叠加上Istio的VirtualService和DestinationRule。具体做法是不改业务代码通过标签策略把服务A的某个子集流量切到新版本服务A上。这期间需要保持Nacos和Istio两者的服务发现并存因为可能存在部分服务没接入网格仍然走老的服务发现方式。我们的经验是接入网格的服务之间的调用走Istio没接入网格的仍然走Nacos。这个混合状态会持续一段时间但不要焦虑这是正常的过渡阶段。第三批逐步关闭非必要的SDK能力。等网格内的服务稳定运行一段时间再移除代码里对Hystrix、Sentinel的依赖。注意移除的动作不要太激进。我们当时先从流量不那么重要的批量任务服务开始跑了一个月之后再把核心链路服务切换为纯Istio治理。这个渐进式改造的好处是任何一步出问题回滚范围都很小就算第一批出问题回滚也只是撤销标签注入几分钟就完成业务代码零改动。3.4 核心配置解析VirtualService和DestinationRule接入Istio后流量管理的核心都落到两个CRD资源上VirtualService和DestinationRule。理解这两个资源的关系是使用Service Mesh的门槛。VirtualService定义“怎么走”把哪个主机的流量、按什么条件、路由到哪个目标子集。它负责配置路由规则比如HTTP header里的X-version: v2走v2版本默认走v1版本。这个资源就是前面说的金丝雀发布入口配置所在地。DestinationRule定义“走到哪”某个服务下包含了哪些子集每个子集对应哪些Pod标签以及连接到这些子集时采取什么策略如连接池大小、熔断阈值、TLS设置。举个例子假设有一个订单服务order-service我们计划把10%的流量切到v2版本apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: order-service-vs namespace: order spec: hosts: - order-service http: - match: - headers: x-deploy-group: exact: v2 route: - destination: host: order-service subset: v2 - route: - destination: host: order-service subset: v1 weight: 90 - destination: host: order-service subset: v2 weight: 10 --- apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: order-service-dr namespace: order spec: host: order-service trafficPolicy: loadBalancer: simple: ROUND_ROBIN connectionPool: tcp: maxConnections: 100 subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2这段配置加入之后order-service域名下的流量的10%会自动路由到v2标签的Pod90%走v1。同时我们定义了一个基于请求头x-deploy-group: v2的强制路由只要上线测试时带了这个Header请求就一定走v2。这样不需要改动任何业务代码就可以完成灰度发布前的内部验证。这里有个容易被忽略的细节hosts字段如果你写的是order-service只用服务短名字那这个VirtualService只在本命名空间生效如果你要跨命名空间支持得写成order-service.order.svc.cluster.local。我们前期就是因为没注意这个导致跨命名空间调用时路由规则未生效排查了半天。3.5 与现有微服务注册中心的协同方案很多已有微服务架构的团队已经有了Nacos或Eureka。接入Service Mesh之后是否需要移除注册中心这是每支团队都会纠结的问题。我的观点是不要急着移除先并存再逐步收敛。注册中心和服务网格在服务发现层面存在一定功能重叠但它们在异常场景下的表现各有优势而业务代码里对注册中心的依赖往往藏在很多角落短时间完全断开并不现实。实际操作上我们保留了Nacos作为服务注册配置中心同时让Istio接管服务间调用的流量。Istio自身具备服务发现能力它可以从Kubernetes API Server同步到所有Service和Endpoint。对于已经接入网格的调用方来说目标服务会被Istio识别为ServiceEntry流量被sidecar接管而需要访问网格外部的服务时我们会显式地配置ServiceEntry将它们纳入网格管理。另外很多团队在Nacos上还存有配置文件这属于配置管理职责不是服务发现职责两者在功能边界上是清晰区分的。Service Mesh目前更擅长解决的是通信治理和流量管理配置中心依然建议保留不需要强行替换。4. 常见问题与排查技巧实录4.1 性能开销延迟增加的那几毫秒到底花在哪了Service Mesh最大的质疑声往往来自性能。我们内部实测下来HTTP协议下sidecar代理带来的延迟增加平均在2到5毫秒左右在请求动辄几十毫秒上百毫秒的微服务场景中这个开销通常可以接受。但这不代表你可以忽视它。延迟增加主要由两部分构成一是sidecar之间的连接建立和TLS握手尤其是启用mTLS之后每次新建连接都比原来多一跳二是Envoy的过滤链处理每个请求经过它要执行一堆Filter逻辑包括鉴权、指标、路由匹配等。我们优化性能花了不小的功夫最管用的三招开启HTTP/2连接复用。Envoy天生支持HTTP/2把服务间通信都走HTTP/2之后连接数大大减少握手开销明显降低。如果业务使用的是gRPC这一项基本是必选gRPCHTTP/2是标配。调整连接池和空闲超时。默认的Envoy连接池参数偏向保守我们后来调大了http1MaxPendingRequests和maxRequestsPerConnection连接复用率上去了但注意不要调太大不然会占用大量内存。关闭不必要的Filter。Istio默认开了不少过滤器比如Mixer老版本、Stats、RBAC等。如果某些指标你暂时用不上建议在MeshConfig里禁用掉能节省每请求的CPU消费。4.2 边车注入失败与Pod一直Pending这是我们上线初期遇到频率最高的一个问题。边车注入失败通常表现为容器始终处于Init:CrashLoopBackOff状态原因不外乎几个一是istio-proxy容器需要的镜像拉不下来内网环境没有配置对应的proxyv2镜像仓库地址。解决办法是提前把istio的Proxy镜像推送到内网仓库然后修改istiod的Deployment里sidecar注入模板的镜像地址。二是资源限制不合理。istio-proxy默认的资源请求是100mCPU/128Mi内存对于一些小的Pod如果资源余量不足是不会被调度成功的。我们用的时候把资源调成requests: cpu: 50m, memory: 96Mi配合Kubernetes的LimitRange避免每个Pod都为了sidecar分配超量资源。三是Pod里已有容器占用了Envoy需要的端口15001/15006/15090。如果你在业务镜像里自己暴露了这些端口就需要改istio-injection模板里的端口定义或者改业务服务的端口号。4.3 流量不按路由规则走这个问题在灰度发布时特别让人头疼明明VirtualService里配好了10%的流量切到v2但就是看不到v2有任何流量。排查后我们总结出三个高频原因Pod标签不匹配。DestinationRule里定义的subsets依赖Pod的标签分组。常常有人把标签写在Deployment的metadata中而不是spec.template里。前者是Deployment自身的标签后者才是Pod创建时的标签写错位置导致匹配不到v2子集。VirtualService的host名称与Kubernetes Service名称不一致。默认情况下VirtualService里只能路由到Kubernetes Service的DNS名称如果你写的是IP或者别名要么创建ServiceEntry要么修正host。未注入边车的服务绕过路由规则。前面说了如果调用方服务没有注入sidecar调用会绕过网格路由规则自然不生效。判断方法很简单kubectl get pod -n xxx -o yaml | grep -c istio-proxy如果为0那就是没注入。4.4 常见问题速查表现象可能原因排查思路Pod一直Init:CrashLoopBackOffistio-proxy镜像拉取失败或资源不足检查镜像仓库地址、Pod资源请求服务间调用超时服务网格启用了mTLS但证书过期或未安装检查istiod日志确认证书Secret是否存在VirtualService不生效调用方未注入sidecar或host名称不匹配检查Pod内是否有istio-proxy核对host字段流量全量到v1v2无请求Pod标签不匹配或权重错误kubectl get pods --show-labels检查标签sidecar内存使用过高连接池参数过大或Envoy版本过老调整连接池、升级istio版本服务发现仍走Nacos但控制平面无日志数据平面未正确同步查看istiod的xds日志检查ServiceEntry这张表我保存了很久每次团队有人问我就直接贴过去。排查Service Mesh类问题前路比较窄大部分都是配置和同步这两个大方向抓住这两个方向去深挖基本都能解决。5. 一点经验总结什么时候我建议你“别上Service Mesh”写了这么多还是想泼几盆冷水。Service Mesh不是银弹。如果你现在的服务数量不多、技术栈统一、团队规模小传统微服务框架完全够用强行引入Istio只会增加技术栈的复杂度、排障成本和维护负担。另外团队如果对Kubernetes本身还不熟悉也别急着上Service Mesh。Service Mesh是构建在Kubernetes之上的网络治理层底座的稳定性直接影响上层功能。我们当时是先花了一个季度把Kubernetes集群的稳定性、监控告警、容器镜像发布流程全部理顺才开始动Service Mesh的。从微服务到Service Mesh的演进本质上是把“网络通信的治理能力”从业务进程中剥离出来形成独立、集中管理的基础设施层。这条路线的方向和收益已经在越来越多的生产环境中得到验证。但落地的方法一定要务实渐进式改造、同时保留原有治理手段、以可观测性为切入点都是值得借鉴的思路。我个人在实际操作中体会最深的一点是Service Mesh相关的坑十有八九不是技术原理上的难而是分布式环境下各种“局部正确但全局不生效”的诡异组合。遇到问题别慌先确认边车有没有注入、配置有没有下发、标签有没有匹配这几板斧下去大多数问题都会现出原形。希望这篇分享能帮打算上Service Mesh的你少踩一些我们踩过的坑。
返回列表