朱晔的互联网架构实践心得S2E4:小议微服务的各种玩法(古典、SOA、传统、K8S、ServiceMesh)

3,982 阅读20分钟

十几年前就有一些公司开始践行服务拆分以及SOA,六年前有了微服务的概念,于是大家开始思考SOA和微服务的关系和区别。最近三年Spring Cloud的大火把微服务的实践推到了高潮,而近两年K8S在容器编排的地位确定之后大家又开始实践起以K8S为核心的云原生思想和微服务的结合如何去落地,2018年又多出一个ServiceMesh服务网格的概念,大家又在思考如何引入落地ServiceMesh,ServiceMesh和K8S以及Spring Cloud的关系如何等等。

确实有点乱了,这一波又一波的热潮,几乎每两年都会来一波有关微服务架构理念和平台,许多公司还没完成微服务的改造就又提出了服务+容器道路,又有一些公司打算从微服务直接升级成ServiceMesh。本文尝试总结一下我见过的或实践过的一些微服务落地方式,并且提出一些自己的观点,希望抛砖引玉,大家可以畅谈一下自己公司的微服务落地方式。

1、微服务v0.1——古典玩法

(图中灰色部分代表元数据存储区域,也就是Service和Endpoint关系所保存的地方,之后所有的图都是这样)

其实在2006年在使用.NET Remoting做服务拆分的时候(其实当时我们没有意识到这叫服务拆分,这是打算把一些逻辑使用独立的进程来承载,以Windows服务形式安装在不同服务器上分散压力),我们使用了F5来做服务的负载均衡。没有所谓的服务发现,针对每一个服务,我们直接在程序配置文件中写死F5的IP地址和端口,使用Excel来记录所有服务在F5的端口地址以及服务实际部署的IP:端口,然后在F5进行配置。F5在这里做了负载均衡、简单的路由策略(相同的客户端总是优先路由到相同的后端)以及简单的白名单策略等等。

2、微服务v0.2——改进版古典玩法

之后尝试过这种改进版的古典玩法。相比v0.1的区别是,不再使用硬件F5了,而是使用几组软件反向代理服务器,比如Nginx来做服务负载均衡(如果是TCP的负载均衡的话可以选择HaProxy),Nginx的配置会比F5更方便而且还不花钱。由于生产环境Nginx可能是多组,客户端不在配置文件中写死Nginx地址而是把地址放到了配置中心去,而Nginx的配置由源码仓库统一管理,运维通过文件同步方式或其它方式从源码仓库拉取配置文件下发到不同的Nginx集群做后端服务的配置(Nginx的配置也不一定需要是一个大文件放所有的配置,可以每一组服务做一个配置文件更清晰)。

虽然我的标题说这是古典玩法,但是可以说很多公司如果没有上RPC,没有上Spring Cloud,也没有上K8S的话很可能就是这样的玩法。无论是v0.2还是v0.1,本质上服务是固定在虚拟机或实体机部署的,如果需要扩容,需要迁移,那么肯定需要修改反向代理或负载均衡器的配置。少数情况下,如果调整了反向代理或负载均衡器的IP地址,那么还可能会需要修改客户端的配置。

3、微服务v0.5——SOA ESB玩法

SOA的一个特点是使用了服务总线,服务总线承担了服务的发现、路由、协议转换、安全控制、限流等等。2012年我参与了一个大型MMO游戏《激战2》项目的技术整合工作,这个游戏整个服务端就是这种架构。它有一个叫做Portal的服务总线,所有游戏的十几个子服务都会把自己注册到服务总线,不管是什么服务需要调用什么接口,都是在调用服务总线,由服务总线来进行服务的寻址路由和协议转换,服务总线也会做服务的精细化限流,每一个用户都有自己的服务请求队列。这种架构的好处是简单,服务总线承担了所有工作,但是服务总线的压力很大,承担了所有的服务转发工作。同时需要考虑服务总线本身如何进行扩容,如果服务总线是有状态的,显然要进行扩容不是这么简单。对于游戏服务器来说,扩容可能不是一个强需求,因为游戏服务天然会按照大区进行分流,一个大区的最大人数上限是固定的。

貌似互联网公司这样玩的不多,传统企业或是游戏服务端是比较适合服务总线这种架构的,如果服务和服务之间的协议不统一的话,要在客户端做协议转换的工作比较痛苦,如果可以由统一的中间层接入所有协议统一进行转换的话,客户端会比较轻量,但是这种架构的很大问题在于服务总线的扩容和可靠性。

4、微服务v1.0——传统服务框架玩法

上图是大多数RPC框架的架构图。大多数早期的微服务实践都是RPC的方式,最近几年Spring Cloud盛行后其实Spring Cloud的玩法也差不多,只是Spring Cloud推崇的是JSON over HTTP的RESTful接口,而大多数RPC框架是二进制序列化over TCP的玩法(也有JSON over HTTP的RPC)。

其实RPC框架我个人喜欢JSON over HTTP,虽然我们知道HTTP和JSON序列化性能肯定不如一些精简的二进制序列化+TCP,但是优点是良好的可读性、测试方便、客户端开发方便,而且我不认为15000的QPS和20000的QPS对于一般应用有什么区别。

总的来说,我们会有一个集群化的分布式配置中心来充当服务注册的存储,比如ZK、Consul、Eureka或etcd。我们的服务框架会有客户端和服务端部分,客户端部分会提供服务的发现、软负载、路由、安全、策略控制等功能(可能也会通过插件形式包含Metrics、Logging、Tracing、Resilience等功能),服务端部分对于RPC框架会做服务的调用也会辅助做一些安全、策略控制,对于RESTful的话就服务端一般除了监控没有额外的功能。

比如使用Spring Cloud来玩,那么:

  • Service Discovery:Eureka、Open Feign
  • Load Balancing:Ribbon、Spring Cloud LoadBalancer
  • Metrics:Micrometer、Spring Boot Actuator
  • Resilience:Hystrix、Resilience4j
  • Tracing:Sleuth、Zipkin

在之前《朱晔和你聊Spring系列S1E8:凑活着用的Spring Cloud(含一个实际业务贯穿所有组件的完整例子)》一文中,我有一个完整的例子介绍过Spring Cloud的这套玩法,可以说的确Spring Cloud给了我们构建一套微服务体系最基本的东西,我们只需要进行一些简单的扩展和补充,比如灰度功能,比如更好的配置服务,就完全可以用于生产。 这种模式和之前0.x的很大区别是,服务的注册有一个独立的组件,注册中心完成,通过配合客户端类库的服务发现,至少服务的扩容很轻松,扩容后也不需要手动维护负载均衡器的配置,相当于服务端从死到活的一个重大转变。而且在1.0的时代,我们更多看到了服务治理的部分,开始意识到成百上千的服务,如果没有Metrics、Logging、Tracing、Resilience等功能来辅助的话,微服务就是一个灾难。

Spring Cloud已经出了G版了,表示Netflix那套已经进入了维护模式,许多程序员表示表示扶我起来还能学。我认为Spring Cloud这个方向其实是挺对的,先有开源的东西来填补空白,慢慢再用自己的东西来替换,但是开发比较苦,特别是一些公司基于Spring Cloud辛苦二次开发的框架围绕了Netflix那套东西来做的会比较痛苦。总的来说,虽然Spring Cloud给人的感觉很乱,变化很大,大到E到G版的升级不亚于在换框架,而且组件质量层次不齐,但是它确实是一无所有的创业公司能够起步微服务的不多的选择之一。如果没有现成的框架(不是说RPC框架,RPC框架虽是微服务功能的80%重点,但却是代码量20%的部分,工作量最大的是治理和整合那套),基于Spring Cloud起步微服务,至少你可以当天起步,1个月完成适合自己公司的二次开发改造。

5、微服务v2.0——容器+K8S容器调度玩法

K8S或者说容器调度平台的引入是比较革命性的,容器使得我们的微服务对环境的依赖可以打包整合进行随意分发,这是微服务节点可以任意调度的基础,调度平台通过服务的分类和抽象,使得微服务本身的部署和维护实现自动化,以及实现更上一层楼的自动伸缩。在1.x时代,服务可以进行扩缩容,但是一切都需要人工介入,在2.x时代,服务本身在哪里存在甚至有多少实例存在并不重要,重要的只是我们有多少资源,希望服务的SLA是怎么样的,其余留给调度平台来调度。

如果说1.0时代大家纠结过Dubbo还是Spring Cloud,2.0时代我相信也有一些公司上过Mesos的“贼船”,我们不是先知很难预测什么框架什么技术会在最后存活下来,但是这却是也给技术带来了不少痛苦,相信还是有不少公司在干Mesos转K8S的事情。

如果引入了K8S,那么服务发现可以由K8S来做,不一定需要Eureka。我们可以为Pod创建Service,通过Cluster虚拟IP的方式(如上图所示,通过IP tables)路由到Pod IP来做服务的路由(除了Cluster IP方式也有的人对于内部连接会采用Ingress方式去做,路由方面会更强大,不过这是不是又类似v0.2了呢?)。当然,我们还可以更进一步引入内部DNS,使用内部域名解析成Cluster IP,客户端在调用服务的时候直接使用域名(域名可以通过配置服务来配置,也可以直接读取环境变量)即可。如果这么干的话其实就没有Eureka啥事了,有的公司没有选择这种纯K8S服务路由的方式还是使用了注册中心,如果这样的话其实服务注册到注册中心的就是Pod IP,还是由微服务客户端做服务发现的工作。我更喜欢这种方式,我觉得K8S的服务发现还是弱了一点,而且IP tables的方式让人没有安全感(IPVS应该是更好的选择),与其说是服务发现,我更愿意让K8S只做容器调度的工作以及Pod发现的工作。

虽然K8S可以做一部分服务发现的工作,我们还是需要在客户端中去实现更多的一些弹力方面的功能,因此我认为2.0时代只是说是微服务框架结合容器、容器调度,而不能是脱离微服务框架本身完全依靠K8S实现微服务。2.0和1.0的本质区别或者说增强还是很明显,那就是我们可以全局来统筹解决我们的微服务部署和可靠性问题,在没有容器和容器调度这层抽象之前,有的公司通过实现自动化虚拟机分配拉起,加上自动化初始脚本来实现自动的微服务调度扩容,有类似的意思,但是非常花时间而且速度慢。K8S真正让OPS成为了DEV而不是执行者,让OPS站在总体架构的层面通过DEV(咱不能说开发DSL文件不算开发吧)资源和资源之间的关系来统筹整个集群。在只有十几个微服务若干台服务器的小公司可能无法发挥2.0容器云的威力,但是服务器和服务一多,纯手工的命令式配置容易出错且难以管理,K8S真的释放了几十个运维人力。

6、微服务v3.0——ServiceMesh服务网格玩法

在之前提到过几个问题:

  • SOA的模式虽然简单,但是集中的Proxy在高并发下性能和扩容会是问题
  • 传统的RPC方式,客户端很重,做了很多工作,甚至协议转换都在客户端做,而且如果涉及到跨语言,那么RPC框架需要好几套客户端和服务端
  • K8S虽然是一个重要的变革,但是在服务调度方面还是太弱了,它的专项在于资源调度

于是ServiceMesh服务网格的概念腾空而出,巧妙解决了这几个问题:

  • 采用边车模式的Proxy随服务本身部署,一服务一边车与服务共生死(当然,有的公司会使用类似ServiceBus的Global Proxy作为Sidecar Proxy的后备,防止服务活着Sidecar死了的情况)可以解决性能问题
  • Sidecar里面做了路由、弹力等工作,客户端里可以啥都不干,如上图所示,上图是Istio的架构,Istio的理念是把ServiceMesh分成了数据面和控制面,数据面主要是负责数据传输,由智能代理负责(典型的组件是Envoy),控制面由三大组件构成,Pilot负责流量管理和配置(路由策略、授权策略)下发,Mixer负责策略和数据上报(遥测),Citadel用于密钥和证书管理
  • 由于我们双边都走Sidecar Proxy,我们对于流量的进出都可以做很细粒度的控制,这个控制力度是之前任何一种模式都无法比拟的,这种架构的方式就像把服务放到了网格之中,服务连接外部的通讯都由网格进行,服务本身轻量且只需要关注业务逻辑,网格功能强大而灵活
  • 对于Proxy的流量劫持可以使用IP table进行拦截,对于服务本身无感知,而且Sidecar可以自动注入Pod,和K8S进行自动整合,无需特殊配置,做到透明部署透明使用
  • Pilot是平台无关的,采用适配器形式可以和多个平台做整合,如果和K8S整合的话,它会和API Server进行通讯,订阅服务、端点的信息,然后把信息转变成Istio自己的格式作为路由的元数据
  • Mixer期望的是抽象底层的基础设施,不管是Logging还是Metrics、Tracing,在之前RPC时代的做法是客户端和服务端都会直接上报信息到InfluxDb、Tracing Server等,这让客户端变得很臃肿,Istio的理念是这部分对接后端的工作应该由统一的组件进行,不但使得Proxy可以更轻而且可以通过Plugin机制对接各种后端基础设施

说了这么多ServiceMesh的优势,我们来看一下这种模式的性能问题。想一下各种模式下客户端要请求服务端整个HTTP请求(跳)次数:

  • 古典模式:2跳,代理转发一次
  • SOA模式:2跳,总线转发一次
  • 传统模式:1跳,客户端直连服务端
  • K8S Service模式:1跳(路由表会有一定损耗)
  • ServiceMesh模式:3跳(其中2跳是localhost回环)

总的来说,3跳并不是ServiceMesh的瓶颈所在,而更多的可能性是Istio的倔强的架构理念。Istio认为策略和遥测不应该耦合在Sidecar Proxy应该放到Mixer,那么相当于在调用服务的时候还需要额外增加Mixer的同步请求(来获得策略方面的放行)。Istio也在一直优化这方面,比如为Mixer的策略在Proxy做本地缓存,为遥测数据做批量上报等等。虽然经过层层优化,但是Istio目前的TPS不足2000,还是和一般的RPC能达到的20000+有着十倍的差距,说不定将来Istio会有架构上的妥协,把Mixer变为非直接依赖,策略方面还是采用类似Pilot统一管理配置下发的方式,遥测方面还是由Sidecar直接上报数据到Mixer。

我个人认为,ServiceMesh是一个非常正确的道路,而且ServiceMesh和K8S结合会更好,理由在于:

  • K8S让资源调度变得自由,但微服务调度不是其所长也不应该由它深入实现
  • 以Istio为代表的ServiceMesh做了K8S少的,但是微服务又必须的那块工作
  • Istio的设计方面和K8S极其相似,低耦合,抽象的很好,两者结合的也很好,我非常喜欢和赞同Agent+统一的资源管理配置下发的方式(K8S的Agent就是KubeProxy和Kubelet,Istio的Agent就是Sidecar Proxy),这是松耦合和高性能的平衡
  • 在复杂的异构环境下,多协议的内部通讯,跨平台跨语言的内部通讯很常见,如果采用传统方式,框架太胖太重,把这部分工作从内部剥离出来好处多多

但是,可以看到目前ServiceMesh还不算非常成熟,Istio在不断优化中,Linkerd 2.x也想再和Istio拼一下,到底谁会胜出还难以知晓,经过之前Dubbo vs Spring Cloud的折腾,Mesos vs K8S的折腾,VM vs Docker的折腾,是否还能经得起折腾Istio vs Linkerd 2呢?我建议还是再看一看,再等一等。

7、畅想Everything Mesh模式?

之前看到过ShardingSphere受到ServiceMesh的理念影响提出了DB Mesh的架构。其实DB Proxy的中间件已经存在很多年了(集中化的Proxy类似服务总线的方式),DB Mesh把Proxy也变为轻量的Sidecar方式,DB的访问也都走本地代理。那么这里我也在想,是不是有可能所有东西都有本地的代理呢?

作为应用服务本身而言,只需要和本地代理做通讯调用外部服务、缓存、数据库、消息队列,不需要关心服务和资源所在何地,以及背后的实际服务的组件形态。当然,这只是一个畅想了,对于有状态的资源,Mesh的难度很大,对于类似DB这样的资源因为调用层次并不复杂,也不太会存在异构场景,Mesh的意义不大,综合起来看Everything Mesh的投入产出比相比Service Mesh还是小很多。

8、Spring Cloud、K8S和ServiceMesh的关系

如果搞Java微服务的话,Spring Boot是离不开的,但是是否要用Spring Cloud呢?我的观点是,在目前阶段如果没有什么更好的选择,还是应该先用。Spring Cloud和K8S首先并不是矛盾的东西,K8S是偏运维的,主要做资源整合和管理,如果彻底没有服务治理框架纯靠K8S的话会很累,而且功能不完整。开发和架构可以在Spring Cloud方面深耕,运维可以在容器和K8S方面发力,两套体系可以协作形成目前来说比较好的微服务基石。至于K8S的推行,这一定是一个正确的方向,而且和软件架构方面的改进工作一点不矛盾,毕竟K8S是脱离于具体语言和平台的。

至于Service Mesh,它做的事情和Spring Cloud是有很多重复的,在将来Istio如果发展的更好的情况下,应该可以替代Spring Cloud,开发人员只需要用Spring Boot开发微服务即可,客户端方面也可以很瘦,不需要过多关心服务如何通讯和路由,服务的安全、通讯、治理、控制都由Service Mesh进行(但是,是否有了Sidecar,客户端真的完全不需要SDK了呢?我认为可能还是需要的,对于Tracing,如果没有客户端部分显然是不完整的,虽然Sidecar是localhost但是还是跨进程了)。

Spring Cloud目前虽然针对K8S和Istio做了一些整合,但是并没看到一套针对ServiceMesh的最佳实践出来,是否将来Spring Cloud会在微服务这方面做退化给ServiceMesh让步还不得而知。总的来说,长期我看好Spring Boot + K8S + Istio的组合,短期我认为还是Spring Boot + K8S + Spring Cloud这么用着。

9、总结

本文总结了各种微服务落地的形态,由于技术多样,各种理念层出不穷,造成了微服务的落地方式真的很难找到两家相同的公司,本文中我们介绍了:

  • 客户端写死地址+F5代理的方式
  • 客户端把地址配置在配置服务+Nginx代理的方式
  • SOA+集中式ESB的方式
  • 传统的具有注册中心的服务框架SDK形式
  • 服务框架+K8S方式
  • K8S Service Iptables路由方式
  • ServiceMesh代理3跳转发方式

当然,可能还会有更多的方式:

  • 内部DNS方式(直接DNS轮询)
  • K8S内部服务走Ingress方式(内部服务也走Ingress,类似所有服务Nginx代理的方式)
  • ServiceMesh代理2跳转发方式(可以根据需要跳过远端的Sidecar来提高性能等等)
  • 瘦服务框架SDK+ServiceMesh方式(也就是还是有一个小的SDK来对接ServiceMesh的Sidecar,而不是让应用程序自己发挥Http Client,这个方式的好处在于更灵活,这个SDK可以在这一层再做一次路由,甚至在Sidecar出问题的时候直接把流量切换出去,切换为直连远端或统一的Global Proxy)

也可能很多公司在混用各种方式,具有N套服务注册中心,正在做容器化迁移,想想就头痛,微服务的理念层出不穷伴随着巨头之间的技术战役,苦的还是架构和开发,当然,运维可能也苦,2019新年快乐,Enjoy微服务!