IstioCon2021:SpringClod到Istio最佳实践


记录在北京时间2月23日,在全球首届社区峰会IstioCon 2021中,发表的《Best practice:from Spring Cloud to Istio》技术演讲。回答经常被客户和同事们问到的一个问题,SpringCloud和Istio的关系,如何演进。

议题:

正文:

大家好,我是来自华为云的工程师。很荣幸有机会和大家分享Istio在生产中使用的实际案例。

华为云应用服务网格从2018年在公有云上线, 作为全球最早的几个网格服务之一,经历和见证了从早期对网格的了解、尝试到当前大规模使用的过程。服务的客户越来越多,场景也越来越复杂。这其中的通用功能作为feature大都贡献到Istio社区,解决方案层面的实践也希望通过这样的机会和大家交流。

本次我选取的主题是Spring Cloud to Istio。来自我们客户的Spring cloud的项目和Istio的结合与迁移案例。

演讲主要包含四部分的内容:

1)背景介绍

2)使用Spring cloud微服务框架遇到的问题

3)解决方案

4)通过示例来描述方案的实践细节

背景介绍

还是以微服务为切入点,微服务的诸多优势非常明显,但相应给整个系统带来的复杂度也非常显著。单体的系统变成了分布式后,网络问题,服务如何找到并访问到对端的服务发现问题,网络访问的容错保护问题等。连当年最简单的通过日志中的调用栈就能实现的问题定位,微服务化后必须要通过分布式调用链才能支持。怎样解决微服务带来的这些挑战?

微服务SDK曾经是一个常用的解决方案。将微服务化后通用的能力封装在一个开发框架中,开发者使用这个框架开发写自己的业务代码,生成的微服务自然就内置了这些能力。在很长的一段时间内,这种形态是微服务治理的标配,以至于初学者以为只有这些SDK才是微服务。

服务网格则通过另一种形态提供治理能力。不同于SDK方式,服务治理的能力在一个独立的代理进程中提供,完全和开发解耦。虽然从图上看两者差异非常小,后面我们将会从架构和实际案例来分析两者在设计理念上的差异,来体会前者是一个开发框架,而后者是一个基础设施。

SDK形态中Spring cloud是最有影响力的代表项目。Spring cloud提供了构建分布式应用的开发工具集,如列表所示。其中被大部分开发者熟知的是微服务相关项目,如:服务注册发现eureka、配置管理 config、负载均衡ribbon、熔断容错Hystrix、调用链埋点sleuth、网关zuul或Spring cloud gateway等项目。在本次分享中提到的Spring cloud也特指Spring cloud的微服务开发套件。

而网格形态中,最有影响力的项目当属Istio。Istio的这张架构图在这次演讲中会高频出现。作为本次分享的背景,我们只要知道架构上由控制面和数据面组成,控制面管理网格里面的服务和对服务配置的各种规则。数据面上每个服务间的出流量和入流量都会被和服务同POD的数据面代理拦截和执行流量管理的动作。

除了架构外,作为背景的另外一个部分,我们挑两个基础功能稍微打开看下两者的设计和实现上的相同和不同。首先是服务发现和负载均衡。

左边是Spring cloud,所有的微服务都会先注册中心,一般是Eureka进行服务注册,然后在服务访问时,consumer去注册中心进行服务发现得到待访问的目标服务的实例列表,使用客户端负载均衡ribbon选择一个服务实例发起访问。

右边Istio不需要服务注册的过程,只需要从运行平台k8s中获取服务和实例的关系,在服务访问时,数据面代理Envoy拦截到流量,选择一个目标实例发送请求。可以看到都是基于服务发现数据进行客户端负载均衡,差别是服务发现数据来源不同,负载均衡的执行体不同。

下面比较下熔断:

左边为经典的Hystrix的状态迁移图。一段时间内实例连续的错误次数超过阈值则进入熔断开启状态,不接受请求;隔离一段时间后,会从熔断状态迁移到半熔断状态,如果正常则进入熔断关闭状态,可以接收请求;如果不正常则还是进入熔断开启状态。

Istio中虽然没有显示的提供这样一个状态图,但是大家熟悉Istio规则和行为应该会发现,Istio中OutlierDection的阈值规则也都是这样设计的。两者的不同是Spring cloud的熔断是在SDK中Hystrix执行,Istio中是数据面proxy执行。Hystrix因为在业务代码中,允许用户通过编程做一些控制。

以上分析可以看到服务发现、负载均衡和熔断,能力和机制都是类似的。如果忽略图上的某些细节,粗的看框图模型都是完全一样的,对比表格中也一般只有一项就是执行位置不同,这一点不同在实际应用中带来非常大的差异。

使用Spring cloud微服务框架遇到的问题

本次演讲的重点是实践。以下是我们客户找到我们TOP的几个的问题,剖析下用户使用传统微服务框架碰到了哪些问题,这些大部分也是他们选择网格的最大动力。

1)多语言问题

在企业应用开发下,一个业务使用统一的开发框架是非常合理常见的,很多开发团队为了提升效率,经常还会维护有自己公司或者团队的通用开发框架。当然因为大部分业务系统都是基于Java开发,所以Spring cloud开发框架,或者衍生于Spring cloud的各种开发框架使用的尤其广泛。

但是在云原生场景下,业务一般更加复杂多样,特别是涉及到很多即存的老系统。我们不能要求为了微服务化将在用的一组成熟服务用Spring cloud重写下。用户非常希望有一种方式不重写原来的系统也能对其进行应用层服务访问管理。

2)将Spring cloud的微服务运行在K8s上会有很大的概率出现服务发现不及时

前面介绍过Spring cloud服务发现是基于各个微服务先向注册中心进行服务注册的数据来实现的,在传统Spring cloud场景下,当微服务部署在VM上,服务动态变化要求没有那么高,顶多个别实例运行不正常,通过服务发现的健康检查就足够了。但是在k8s场景下,服务实例动态迁移是非常正常场景。如图示,producer的某个Pod已经从一个节点迁移到另外一个节点了,这时需要新的pod2的producer实例向eureka注册,老实例Pod1要去注册。

如果该情况频繁发生,会出现注册中心数据维护不及时,导致服务发现和负载均衡到旧的实例pod1上,从而引起访问失败的情况。

3)升级所有应用以应对服务管理需求变化

第三个问题是一个比较典型的问题。客户有一个公共团队专门维护了一套基于Spring cloud的自有开发框架,在每次升级开发框架时,不得不求着业务团队来升级自己的服务。经常会SDK自身修改测试工作量不大,但却要制定很长周期的升级计划,来对上千个基于这个SDK开发的服务分组重新编译,打包,升级,而且经常要陪着业务团队在夜间变更。业务团队因为自身没有什么改动,考虑到这个升级带来的工作量和线上风险,一般也没有什么动力。

4)从单体式架构向微服务架构迁移

这是一个比较普遍的问题,就是渐进的微服务化。马丁福勒在著名的文章单体到微服务的拆分中(https://martinfowler.com/articles/break-monolith-into-microservices.html )也提到了对渐进微服务化的倡议,如何能从业务上将一个大的业务分割,解耦,然后逐步微服务化。马丁福勒强调 “解耦的是业务能力不是代码” ,大神将代码的解耦留给了开发者。

但是站在开发者的角度讲渐进的微服务不是一个容易的事情。以基于Spring cloud框架进行微服务开发为例,为了所有的微服务间进行统一的服务发现、负载均衡,消费和执行同样的治理策略,必须要求所有的微服务基于同样的,甚至是统一版本的SDK来开发。

当然我们客户在这种情况下也有基于API层面做适配,将原有的未微服务化的和已微服务化的并存,使用这种类似于灰度方式,实际操作非常麻烦。

曾经有客户问过有没有不用费劲搞两套,是否可以直接有些大的单体微服务化,另外一些单体很长时间内完全不动,直到有时间或者认为安全想动它的时候去动。

解决方案

对于客户实际碰到的4个典型的微服务框架的问题,我们推荐的解决方案都是服务网格。下面我们分别看下Istio如何解决上面的几个问题。

首先,多语言问题。基于服务网格,业务和治理的数据面无需运行在同一个进程里,也无需一起编译,因此也没有语言和框架上的绑定。无论什么语言开发的服务,只要有一个对外可以被访问和管理的一定应用协议上的端口,都可以被网格进行管理。通过统一的网格控制面,下发统一的治理规则给统一的网格数据面执行,进行统一的治理动作,包括前面介绍到的灰度、流量、安全、可观察性等等。

关于Spring cloud服务在Kubernetes运行时,关于原有的服务注册和发现不及时的问题。根本原因是两套服务发现导致的不一致问题,那么解决办法也比较简单,统一服务发现即可。既然K8s已经在Pod调度的同时维护有服务和实例间的数据,那么就没有必要再单独搞一套名字服务的机制,还要费劲的进行服务注册,然后再发现。

比较之前Spring cloud注册发现那张图,注册中心没了,服务基于注册中心的服务注册和服务发现的动作也不需要了,Istio直接使用k8s的服务发现数据,但从架构上看也简洁很多。

我们也总结过,大部分碰到这个问题的场景,都是将微服务框架从VM迁移到k8s时候碰到的,有点把容器当作之前的VM使用,只使用了k8s作为容器部署运行的平台,并没有用到k8s的service。

对于SDK自身升级导致业务全部重新升级的问题,解决办法就是把服务治理的公共能力和业务解耦。在网格中,将治理能力下沉到基础设施后,业务的开发、部署、升级都和服务治理的基础设施解耦了。业务开发者专注自己的业务部分。只要没有修改业务代码,就无需重新编译和上线变更。

当治理能力升级只需基础设施升级即可,基础设施的升级对用户业务完全没有影响。像华为云ASM这样大部分网格服务的服务提供商都能做到一键升级,用户完全感知不到这个过程。

关于渐进微服务化的问题,使用Isito服务网格可以非常完美的解决。Istio治理的是服务间的访问,只要一个服务被其他服务访问,就可以通过Istio来进行管理,不管是微服务还是单体。Istio接管了服务的流量后,单体和微服务都可以接收统一的规则进行统一的管理。

如图中,在微服务化的过程中,可以对某个单体应用svc1根据业务拆分优先进行微服务化,拆分成三个微服务svc11、svc12和svc13,svc1服务依赖的另外一个单体应用svc2不用做任何变更,在网格中运行起来就可以和另外三个微服务一样的被管理。同样在运行一段时间后,svc2服务可以根据自身的业务需要再进行微服务化。从而尽量避免一次大的重构带来的工作量和业务迁移的风险,真正做到马丁富勒倡导的渐进微服务化的实践。

实践

以上是对实际工作中客户的几个典型问题的解决方案。在实践中,怎么把这些解决方案落地呢?下面基于实际客户案例总结,分享具体的实践。

我们的主要是思路是解耦和卸载。卸载原有SDK中非开发的功能,SDK只提供代码框架、应用协议等开发功能。涉及到微服务治理的内容都卸载到基础设施去做。

从图上可以看到开发人员接触到开发框架变薄了,开发人员的学习、使用和维护成本也相应的降低了。而基础设施变得厚重了,除了完成之前需要做的服务运行的基础能力外,还包括非侵入的服务治理能力。即将越来越多的之前认为是业务的能力提炼成通用能力,交给基础设施去做,交给云厂商去做,客户摆脱这些繁琐的非业务的事务,更多的时间和精力投入到业务的创新和开发上。在这种分工下,SDK才真的回归到开发框架的根本职能。

要使用网格的能力,前提是微服务出来的流量能走到网格的数据面来。主要的迁移工作在微服务的服务调用方。我们推荐3个步骤:

第一步:废弃原有的微服务注册中心,使用K8S的Service。

第二步:短路SDK中服务发现和负载均衡等逻辑,直接使用k8s的服务名和端口访问目标服务;

第三步:结合自身项目需要,逐步使用网格中的治理能力替换原有SDK中提供的对应功能,当然这步是可选的,如调用链埋点等原有功能使用满足要求,也可以作为应用自身功能保留。

为了达成以上迁移,我们有两种方式,供不同的用户场景采用。

一种是只修改配置的方式:Spring cloud本身除了支持基于Eureka的服务端的服务发现外,还可以给Ribbon配置静态服务实例地址。利用这种机制给对应微服务的后端实例地址中配置服务的Kubernetes服务名和端口。

当Spring cloud框架中还是访问原有的服务端微服务名时,会将请求转发到k8s的服务和端口上。这样访问会被网格数据面拦截,从而走到网格数据面上来。服务发现负载均衡和各种流量规则都可以应用网格的能力。

这种方式其实是用最小的修改将SDK的访问链路和网格数据面的访问链路串接起来。在平台中使用时,可以借助流水线工具辅助,减少直接修改配置文件的工作量和操作错误。可以看到我这个实际项目中,只是修改了项目的application.yaml配置文件,其他代码都是0修改。当然对于基于annotation的方式的配置也是同样的语义修改即可。

前面一种方式对原有项目的修改比较少,但是Spring cloud的项目依赖都还在。

我们有些客户选择了另外一种更简单直接的方式,既然原有SDK中服务发现负载均衡包括各种服务治理能力都不需要了,干脆这些依赖也全部干掉。从最终的镜像大小看,整个项目的体量也得到了极大的瘦身。这种方式客户根据自己的实际使用方式,进行各种裁剪,最终大部分是把Spring cloud退化成Spring boot。

迁移中还有另外一部分比较特殊,就是微服务外部访问的Gateway。

Spring cloud 有两种功能类似的Gateway,Zuul和Spring cloud Gateway。基于Eureka的服务发现,将内部微服务映射成外部服务,并且在入口处提供安全、分流等能力。在切换到k8s和Istio上来时,和内部服务一样,将入口各个服务的服务发现迁移到k8s上来。

差别在于对于用户如果在Gateway上开发了很多私有的业务强相关的filter时,这时候Gateway其实是微服务的门面服务,为了业务延续性,方案上可以直接将其当成普通的微服务部署在网格中进行管理。

但是大多数情况下我们推荐使用Istio的Ingress Gateway直接替换这个微服务网关,以非侵入的方式提供外部TLS终止、限流、流量切分等能力。

经过以上的简单改造,各种不同语言、各种不同开发框架开发的服务,只要业务协议相通,彼此可以互相访问,访问协议可以被网格管理,就都可以通过Istio进行统一的管理。

控制面上可以配置统一的服务管理规则。数据面上,统一使用Envoy进行服务发现、负载均衡和其他流量、安全、可观察性等相关能力。数据面上的服务即可以运行在容器里,也可以运行在虚机上。并且可以运行在多个k8s集群中。

当然在迁移过程中间,我们也支持阶段性的保留原有微服务框架的注册中心,使Istio和其他的服务发现结合使用的中间状态,让网格中的服务可以访问到微服务注册中心的服务。

这里是一个Spring cloud开发的服务运行在Istio服务网格上进行灰度发布的示例。上面的日志是服务调用方Sidecar的日志,可以看到网格将流量分发到不同的服务后端上。下面的截图是使用了华为云ASM服务的灰度功能,可以看到这个Spring cloud服务通过网格配置的分流策略,将30%的流量分发到灰度版本上。

下面这个示例是Spring cloud开发的服务使用Istio的熔断功能。这个过程就是就是前面原理一节Hystrix的状态迁移图的实践,不同在于这个实现是基于Istio来实现的。基于服务网格不管这里的服务是什么语言或者框架开发的,都可以对访问进行熔断保护。

这里的效果截图是来自华为云应用服务网格ASM的应用拓扑,可以非常清新的看到服务级别、服务实例级别流量变化情况,服务和服务实例的健康状态,从而展示故障的Spring cloud实例被隔离的全过程。从拓扑图上可以看到有个实例异常满足熔断阈值,触发了熔断,网格数据面向这个故障实例上分发的流量逐渐减少,直到完全没有流量,即故障实例被隔离。通过这种熔断保护保障服务整体访问的成功率。

下面三个流量拓扑演示了故障恢复的过程。

可以看到:

  • 初始状态这个故障实例被隔离中,没有流量;

  • 当实例自身正常后,网格数据面在将其隔离配置的间隔后,重新尝试给其分配流量,当满足阈值要求则该实例会被认为是正常实例,可以和其他两个实例一样接收请求。

  • 最终可以看到三个实例上均衡的处理请求。

    即实现了故障恢复。

结语

最后,通过微服务、容器、k8s和Istio的关系图来总结今天的内容:

1)微服务和容器都有轻量和敏捷的共同特点,容器是微服务非常适合的一个运行环境;

2)在云原生场景下,在微服务场景下,容器从来都不是独立存在的,使用k8s来编排容器已经是一个事实标准;

3)Istio和k8s在架构和应用场景上的紧密结合,一起提供了一个端到端的微服务运行和治理的平台。

4)也是我们推荐的方案,使用Istio进行微服务治理正在成为越来越多用户的技术选择。

以上四个关系顺时针结合在一起为我们的解决方案构造一个完整的闭环。

完。

附: