Netflix OSS、Spring Cloud还是Kubernetes? 都要吧!
Netflix OSS是由Netflix公司主持开发的一套代码框架和库,目的是解决上了规模之后的分布式系统可能出现的一些有趣问题。对于当今时代的Java开发者们来说,Netflix OSS简直就是在云端开发微服务的代名词。服务发现、负载均衡、容错等对于可扩展的分布式系统来说都是非常非常重要的概念,Netflix对这些问题都给出了很好的解决方案。在这里Netflix要对那些在广大的开源社区中为这些代码框架和库做出过贡献的人们简单地说声“谢谢”,还有许多互联网公司也做出了贡献,所以在这里一并谢过。可是有一些比较大的互联网公司却为自己做的一些东西申请了专利,还把代码都保留起来没有开源,这实在不太好。
不过,Netflix OSS的许多内容都是在一个已经过去的年代写出来的,那时所有东西都只能运行在AWS云上而没有其它选择。关于那个年代的许多宝贵遗产和前提假设都已经被封装到了Netflix的库里面,对于现在你运行的环境(比如Linux容器)已经不适用了。在Linux容器、Docker、容器管理系统等等出现之后,我们越来越看到把我们的微服务运行在Linux容器(公有云、私有云,或者都要,等等)里的巨大价值。另外,因为这些容器都是直接把这些服务打包起来、对外不透明的,所以我们倾向于不要过多关心在容器里面运行的到底是什么技术(是Java?还是Node.js?或者Go?)。Netflix OSS主要是为Java开发者服务的,它是许多的库、框架和配置的集合,你需要把它们包含在你的Java程序或服务代码里面。
这就带来了第一个问题。
种类众多的微服务是可以用各种不同的框架或语言实现的,但像服务发现、负载均衡、容错等等功能还是非常重要而且必不可少的。如果我们在容器里面运行这些服务,我们可以借助于强大的语言无关的架构来做各种事情,比如构建、打包、部署、健康检查、滚动升级、蓝绿发布、安全、还有其它等等各种事情。作为一种类型的Kubernetes,OpenShift专注于为企业用户服务,它把所有事情都帮你做完了:没有什么东西必须要你的应用层程序去知道或者处理的了。你只要简简单单的实现你的应用程序和服务就好,只要专注于它们应该做的功能就好。
这样是不是说这些架构可以帮忙把大家从服务发现、负载均衡、容错等功能中解放出来?那为什么这些还要是应用层的事?
如果你使用Kubernets(或者某些变种),那么答案就是:是的。
Kubernetes方式的服务发现
用Netflix OSS时通常你要建立一台服务发现服务器,让所有可以被各种客户端发现的服务注册在这里。比如你用Netflix Ribbon来与各种其它服务交互,就需要能找出来它们都运行在哪里。各种服务可能下线,可能按它们自己的运行需要退出,也有可能我们要向集群中加入更多服务来做横向扩展。这种集中式的服务发现注册机制通常可以帮助我们跟踪集群中有哪些服务是可用的。
问题之一是,做为一个开发人员你需要做这些事情:
而且,你要找到对应你使用的编程语言的客户端库,才能与服务发现机制通信。回到我们刚才讨论的问题,微服务可能是用许多不同种语言实现的,所以可能一个成熟的Java客户端库好找,但相应的Go或Node.js库就没有了,那你只好自己写一个。可对每一种语言和每一个程序员,他们都可能对怎么实现这样的客户端库有自己的想法,这样你就会忙于维护各种不同的客户端库,做的事情功能都是一样的可是实现逻辑却各自不同。也有可能每种语言都会使用它自己的服务发现服务器而且也有它自己现成的客户端库呢?所以你就要为每一种语言都管理和维护不同的服务发现服务器吗?不管哪种方式都是非常令人讨厌的。
如果我们使用DNS能解决问题吗?
这样就解决了客户端库的问题吗?DNS是所有使用TCP/UDP的系统自带的,不管你是部署在单机、云、容器、Windows或是Solaris等等哪种系统上。你的客户端程序只需要指向一个域名(比如http://awesomefooservice/),然后底层框架就知道该把服务路由到DNS指向的地方了(也可能是用VIP加负载均衡,或者轮询DNS等等)。好!现在我们不必再关心我需要使用什么样的客户端程序来发现一个服务了,我只需要使用任意一种TCP客户端就好。我也不必关心如何管理DNS集群,它是网络路由器自带的,大家都用得很熟。
但DNS对弹性发现的情况可能表现很糟糕
缺点:DNS对于弹性的、动态的服务集群就表现不好了。要向集群中加入新服务时该怎么办?要下线服务呢?服务的IP地址可能缓存在DNS服务器或路由器中(也许并不属于你的管辖范围),也可能在你自己的IP栈中。还有如果你的程序或者服务要侦听非标准的80端口呢?DNS是默认使用标准的80端口的。要让DNS使用非标准端口,你就要用到DNS SRV记录,这样就又回到了最初的问题,你需要在应用程序层有特殊的客户端库来发现这些记录。
Kubernetes服务
咱们就用Kubernetes吧。反正我们要在Docker或者Linux容器里面运行东西,那就最好是把Docker容器(或者Rocket容器,或者Hyper.sh容器等等)运行在Kubernetes里面了。
(看,我的技术其实很差,尤其是对于那些看起来好象很“简单”的技术——因为你不可能用很复杂的组件来搭建出一个很复杂的系统。你会想要简单的组件。但写出简单的组件这事本身其实很复杂。要我理解象Google或Red Hat做的和Kubernetes相关的、来简化分布式系统部署、管理等等的事对于我来说实在是象是天方夜谭。:) )
使用Kubernetes我们只要创建并使用Kubernetes服务就好了。我们不必浪费时间去搭建服务发现服务器、写定制化的客户端库、调校DNS等等。它直接就可用,我们只要直接进入我们微服务的下一个主题即可,即提供业务价值。
它是怎么工作的呢?
下面这些简单的抽象都是Kubernetes实现了来支持这些功能的:
Pod很简单,它们基本上就是你的Linux容器。标签也很简单,它们基本上就是一些用于标记你的Pod的键-值型字符串(比如Pod A有如下标签:app=cassandra、tier=backend、version=1.0、language=java)。这些标签可以是任何你想起的名字。
最后一个概念是服务。也很简单,一个服务就是一个固定集群IP地址。这个IP地址是虚拟的,可以用来指向真正提供服务的Pod或者容器。那这个IP地址怎么知道该找哪一个Pod或者容器呢?它是用“标签选择器”来找到所有符合你要找的标签的Pod的。比如,假如我们需要一个有“app=cassandra AND tier=backend”标签的Kubernetes服务,它就会帮我们找到一个VIP,指向任何一个同时有这两个标签的Pod。这个选择器是动态工作的,任何加入集群的Pods都会根据它所带有的标签立刻自动参与服务发现。
另一个用Kubernetes作为Pod选择服务的好处是Kubernetes非常智能,它知道哪个Pod是属于哪个服务的,还会考虑它的存活和健康情况。Kubernetes会使用内置的存活和健康检查机制来判断一个Pod是否应该被加入集群,依据是它是否存活和它是否在正常工作。对于那些不符合条件的它会直接剔除掉。
要注意的是,一个Kubernetes服务的实例不是一个什么“东西”,也不是应用、Docker容器等等任何东西,它是个虚拟的东西,所以自然也不会有单点故障。它就是一个由Kubernetes路由的IP地址。
这对于程序员来说实在是令人难以置信的强大和简单。现在如果一个应用想要使用一个Cassandra服务,它会直接使用固定IP地址来找到Cassandra数据库。但写死一个固定IP地址肯定不是什么好做法,因为当你想要把你的程序或者服务挪个地方时就会遇到麻烦。所以一般做法是改IP(或者加个配置项),可这样又加重了程序配置功能的负担,最终解决方案通常就是DNS。
使用Kubernetes内的DNS集群就可以解决上述问题。因为对于一个特定的运行环境(开发、QA等)IP地址是固定的,那我们就不介意直接使用固定IP,反正它也永远不会变。但如果我们是使用DNS的话,举个例子,就可以把程序配置成与http://awesomefooservice上的服务交互,这样不管我们的运行环境怎么变,从开发改到QA再改到生产,我们都会部署那些Kubernetes服务,业务程序就不需要改了。
我们不需要做额外的配置,也不用费心DNS缓存或SRV记录、定制客户端库或管理额外的服务发现框架等等。Pod可以自动加入集群或从集群中剔除掉,Kubernetes服务的标签选择器会动态的依据标签分组。业务程序只需要与http://awesomefooservice/交互即可,随便你是个Java应用,或者是Python、Node.js、Perl、Go、.NET、Ruby、C++、Scala、Groovy等什么语言写的都行。这种服务发现机制就不强求必须使用什么特定的客户端,你随便用就好了。
这样服务发现功能就大大简化了。
客户端侧的负载均衡怎么办?
这事很有趣。Netflix提供了Eureka和Ribbon两个用于客户端侧的负载均衡,你也可以组合使用。基本实现就是服务注册(Eureka/Consul/ZooKeeper等等)功能在跟踪集群中都有什么服务,并且会向关心这些信息的客户端更新这些信息。这样客户端就知道了集群中都有哪些节点,它只需要选一个(随机,或者固定,或者任何它自己定制的算法)然后调用就好。等下一次再调用时,它想的话它也可以换另一个来调用。这样的好处是我们不需要那些可能会迅速成为系统瓶颈的软/硬负载均衡器。另一个重要方面是,当客户端知道服务在哪里之后,它直接与服务交互即可,中间不需要再经过中转。
但依我拙见,客户端侧的负载均衡案例只能占实际情况的5%。我来解释一下。
我们想要的是一种理想的、可扩展的负载均衡机制,而且不要有额外的设备和客户端库等。大多数情况下我们并不介意处理过程中请求会多一跳到负载均衡器(想想看,可能你99%的应用都是这么做的)。我们可能会碰到这样的情况:服务A要调用B,B还要调用C,然后D、E,等等,想像一下那条调用链。这种情况下如果每次调用都要增加额外的一跳的话,我们的整体延迟就会变得很大。所以可能的解决方案就是要“减掉这多余的一跳”,但这一跳也可能不止是到负载均衡器的,更好的办法是要减少那条调用链的层级。请参考我博客上事件驱动系统专题中关于“自治与集中”的讨论,我已经考虑过这样的问题了。
根据上文将Kubernetes服务用作服务发现一节所描述的办法 ,我们可以有不错的负载均衡机制(也不会有各种服务注册、定制客户端、DNS缺点等额外开销)。当我们通过DNS或IP来与Kubernetes服务交互时,Kubernetes会默认地就在集群中的Pod之间做负载均衡(注意集群是由标签和标签选择器定义的)。如果你不想有负载均衡的额外一跳也不用担心,虚拟IP是直接指向Pod的,不会经过实际的物理网络中转。
那95%的案例就轻松搞定了!所以不必过度设计,简单就好。
那剩下的5%的情况怎么办?可能你会有这样的情况,你的程序要在运行时决定调用集群中的哪个具体终端节点。一般来说你可能会想用些复杂的定制算法,而不是常用的轮询、随机、固定某一个等等。这时就可以使用客户端侧的负载均衡机制。你仍然可以用Kubernetes的服务发现机制来找出集群中有哪些Pod是可用的,再根据标签来决定调用哪个。fabric8.io社区的Kubeflix项目为Ribbon提供了发现插件,比如通过Kubernetes的REST API获得一个服务的所有Pod,然后调用者可以用代码来根据业务选择具体调用哪个,不限编程语言。对于这些情况,花些精力来实现根据不同客户端情况定制的发现机制代码库是值得的,更好的做法是把这些定制的逻辑模块化,把依赖关系从业务程序中独立出去。这样使用Kubernetes时,就可以把这些独立的算法模块也随着你的程序和服务部署上去,就可以方便的使用定制的负载均衡算法了。
我还是那句话,这是5%的需要有额外复杂处理的情况。对于95%的情况,使用内置的机制就够了。
容错又怎么办?
在搭建有依赖关系的系统时要时刻记得每个模块要对别人提供什么服务,就是说即使调用方不存在或者崩溃了,它也要记得自己的义务。Kubernetes在容错方面又有什么功能呢?
Kubernetes的确是有自愈功能的。如果一个Pod或者Pod中的一个容器挂掉了,Kubernetes可以把它再拉起来以维持ReplicaSet的不变性。比如你配置想要有10个叫“foo”的Pod,那Kubernetes就会帮你一直维持这个数量。即使某个Pod挂了,它也会再拉起一个来保持住10这个总数。
自愈功能太强大了,而且是随着Kubernetes原生提供的,但我们讨论这个的原因是如果被依赖物(数据库或其他服务)挂掉了,依赖它的业务程序该怎么样?这完全要靠业务程序自己决定怎么处理了。举例来说,如果你想在Netflix上看个电影,就会有个请求发送到授权服务上,校验你是否有权限去看那个电影。可如果授权服务挂了该怎么办呢?就不给用户看那个电影吗?或者把出错日志打给用户看?Netflix的做法是允许用户看。当授权服务出错时,允许一个无权限的用户看某个电影,这种体验比直接拒绝要好得多,也许人家有这个权限呢?
比较好的做法是优雅的降级,或者找出替代方案来持续提供服务。Netflix Hystrix就是一个非常好的Java解决方案,它实现了机制来做隔离、熔断和回滚。每一种都是针对不同业务的具体实现,所以在这种情况下,针对不同的编程语言有定制的客户端库也是非常合理的。
Kubernetes也有类似功能吗?当然!
再看看强大的Kubeflix项目,你可以用Netflix Turbine项目来累积并且将你的集群中运行的所有断路器可视化。Hystrix可以把所有的服务器事件以数据流的形式发送出去,由Turbine消费掉。那Turbine又怎么知道哪些Pod里面有Hystrix呢?好问题,这个可以用Kubernetes的标签解决。如果给所有有Hystrix的Pod全都打上个“hystrix.enabled=true”的标签,Kubeflix Turbine引擎就可以自动发现每个Hystrix断路器的SSE流,并且把它们展现在Turbine的网页上。太感谢你了,Kubernetes!
上图由Joseph Wilk绘制,谢谢。
配置管理怎么办?
Netflix Archaius是用于处理云上系统的分布式配置管理的。用法与Eureka和Ribbon一样,搭起一个配置服务器,再用一个Java库去查出配置项的值就可以了,它也支持动态更改配置等。请记住Netflix是为了在AWS上构建系统实现的这个功能,但是属于Netflix的。作为CI/CD管道的一部分,他们要构建AMI并且部署,可构建AMI或任何VM镜像都是非常耗时的,并且大多数时候都要有很多前置工作。有了Docker或Linux容器,事情就容易多了,接下来我将从配置的角度解释一下。
还是先说95%的情况。我们希望把环境相关的配置信息保存在我们的业务程序之外,再在运行时从运行环境(开发、QA、生产等)中获得它们。这里有个非常重要的区别,不是每个配置都和环境相关,要随着运行环境来改的。而且我们也非常想要有与编程语言不相关的办法来查找配置,避免强迫大家用Java,然后又要配一堆的Java库和路径等。
我们可以用Kubernetes来提供基于环境的配置管理:
我们可以把配置信息通过环境变量提供给Linux容器,这样不管Java、Node.js、GO、Ruby还是Python等等,大多数编程语言都可以很容易获取。也可以把配置信息保存在Git上,再把Git Repo和Pod捆绑起来,映射成Pod本地文件系统的文件,这样任何编程框架都可以以获取本地文件的方式来获取配置信息了,这是个好方案。最后,也可以通过Kubernetes的ConfigMap来把版本化的配置信息保存在ConfigMap中,它也是做为文件系统加载到Pod上,这样就可以将Git Repo解耦出去了。获取配置信息的方法仍是从文件系统的配置文件中读数据,你自己喜欢用什么编程语言或者框架都好。
另外5%的情况呢?
在剩下的5%的情况下你可能想要在程序运行时动态更改配置信息。这一点Kubernetes可以帮忙。你只需要在ConfigMap中更改配置文件,然后把那些改变动态的推送到加载了它的Pod上就好了。在这个方案里,你要使用客户端的库来帮助你感知到这些配置的改动,并且把它们提交到业务程序中。Netflix Archais就提供了有这样功能的客户端。Java版的Spring Cloud Kubernetes在用了ConfigMap时处理这样的事情更容易。
Spring Cloud怎么样?
使用Java的程序员们在Spring下开发微服务时常常把Spring Cloud和Netflix OSS等同起来,因为它很大一部分就是基于Netflix OSS实现的。fabric8.io社区中也有很多用Kubernetes运行Spring Cloud的好东西,请查看https://github.com/fabric8io/spring-cloud-kubernetes。包括配置、日志等在内的很多模式都可以用Kubernetes运行得非常好,不用借助服务发现引擎、配置管理引擎等额外的、复杂的框架。
小结
如果你正在构建自己的微服务,而且你也对Netflix OSS/Java/Spring/Spring Cloud等方案很感兴趣,请一定提醒自己你们不是Netflix,所以不必直接调用AWS EC2的原语来把自己的程序搞得非常复杂。如果你在调查Docker的方案,那采用Kubernetes是个非常明智的选择,它本身就自带许多这样的分布式系统功能。请在合适的时候将业务级程序库分层,这样可以从一开始就避免把你的服务搞的太复杂,因为Netflix在5年前就非常明智的开始这样做了。事实上他们也是不得不这样的,但想想如果5年前他们有Kubernetes又会是怎样?他们的Netflix OSS栈会看起来完全不同的! :)
阅读英文原文:Netflix OSS, Spring Cloud, or Kubernetes? How About All of Them!