2016-06-12

说明

本文是一篇 翻译文章,来自于前同事,红帽中间件架构师 Christian Posta @christianposta 。

当我第一次读到时,就知道是一篇好文,详尽的解释了Spring Cloud 中 Netflix OSS组件在Kubernetes容器管理场景下,最优实践是怎样的。 之前的 微服务知识体系,有一些相似的想法和技术方案,也借鉴一些观点和技术方案,如采用Turbine来统一输出断路器信息。

经过Christian的授权,我翻译了这篇文章为中文供国内Java微服务开发者作为参考,有不通顺的地方请见谅。

以下译文(采用第一人称)

本文以下的内容,在我的新书"面向Java开发者的微服务“中也涉及到一些,该书由OReilly出版社在2016年6月即将出版发行。我想在这里给出一些更明确的说明,因为很多人问到了关于Netflix OSS和如何在Kubernetes上运行的的问题(这些项目都是非常棒的!),以及这些组件完成的功能是重叠的,我会试着解释一些原因。

Netflix OSS是一组开源的框架和组件库,Netflix公司开发出来解决分布式系统的一些有趣的可扩展问题。如今对于Java开发者来说,它们是在云端环境中开发微服务的非常棒的工具代名词。在服务发现,负载均衡,容错等模式方面,都给出了非常重要的概念,并带来了漂亮的解决方案。

Netflix决定把这些项目贡献给开源社区,促进了其他互联网公司也这样做,我们要说声“谢谢”。而有些大型的互联网公司专利化它们的技术,保持闭源,比起来实在是太糟糕了。

Netflix OSS

总的来说,在多数Netflix的开源项目开发的时期,只有AWS公有云可以选择而没有其他的替代。这个因素导致这些库并不是直接为今天采用的运行环境(如Linux上的容器)开发的。采用了Linux容器,Docker,容器管理系统等基础环境后,我们看到了大量的运行在Linux容器上的微服务,它们可能运行在公有云,私有云或者都有。另外,因为容器是服务不透明的包装,我们往往不关心容器里究竟运行什么技术方案(Java/Node.js/Go)。Netflix OSS基本上是Java开发,它们是一组组件库/框架/配置项,可以包含在你的Java应用/服务器代码中运行。

所以我们有了观点#1:

微服务可以由多种框架/语言来实现,但服务发现,负载均衡,容错等这类服务是非常重要的。

如果我们运行在容器中这些服务,我们可以利用强大的与语言无关基础设施的优势,做这些事情:构建,打包,部署,健康检查,滚动升级,蓝绿部署,安全性,和其他。例如,OpenShift(基于Kubernetes构建的企业开发部署方案),可以做所有这些事情:而不需要开发者必须知道或者关心基础设施的这些事情。而集中精力来保持你的应用程序和服务简单。

为什么基础设施可以帮助做服务发现,负载平衡和容错这些服务,难道不应该是应用层的事情么? 如果使用Kubernetes或者其衍生项目,那么答案是可以在基础设施实现这些服务。

Kubernetes中服务发现的方式

使用Netflix OSS,通常需要设置一个服务发现服务器,作为客户端可以发现的服务端点注册表。比如,你可能使用Netflix Ribbon来与其他服务通信,并需要发现服务在哪里运行。服务可以自行停止,也可以在集群中加入更多的服务来实现扩展。这个中心服务发现注册表跟踪什么服务在集群中是可用的。

一个问题是:你作为一个开发者需要做这些事情:

  • 决定是使用一个AP系统(consul, eureka等)还是CP系统(zookeeper, etcd等)

  • 弄清楚如何运行,管理和监视这些大规模系统(而不是小型的练手项目)

此外,你需要了解客户端使用什么编程语言和服务发现通信。前面提到过,微服务可以由许多不同类型的语言实现,Java客户端是没有问题的,但如果没有Go或者NodeJS客户端,就需要自己开发了。每种语言和开发者都有可能用自己的想法来实现客户端,你会面临维护多个客户端,它们试图做相同的事情,却在语义上有不同的方式。或者每种语言都有自己的服务发现服务器,以及自己的客户端程序呢?你要管理和维护这么多的服务发现实现么,想想就头疼。

如果我们仅用DNS呢?

好吧,这个算是解决了客户端库的问题。DNS是每个操作系统都有的基础服务,利用TCP/UDP协议,在私有云,公有云,容器,Windows,Solaris等都有。客户端只需要指向域名就可以了,基础服务可以路由到服务上,可以采用多个轮转DNS配置来实现均衡负载。好处是客户端都不需要知道服务发现服务器,而使用TCP客户端就可以了。而且也不用管理DNS集群,网络路由器支持负载均衡特性,而且这些都很简单容易被理解。

但对于弹性发现,DNS方案就做的很差了。DNS不适合做弹性的,动态的服务集群。当服务加入到集群或者移除时,系统做了什么?服务的IP地址可能还在DNS服务器或者路由器(甚至有些不是你能掌控的),或者你自己的IP堆栈上进行了缓存。另外,如果你的应用侦听的是非80端口,而要DNS保存非标准的端口信息,需要使用DNS SRV记录,而这样你又需要使用特定的应用层客户端来发现这些记录了。

Kubernetes服务

让我们只使用Kubernetes,在docker/linux容器上运行程序,而kubernetes是最合适的运行docker容器的场所,或者Rocket容器,Hyper.sh容器。

(我偏爱简单的技术,或者看起来是简单的,因为你不可能用复杂的零件构建出复杂的系统,人人都渴望简单,最好的是内在复杂而外在简单,google和红帽都对于kubernetes做了大量工作,使得它对于分布式系统的部署和管理部分看起来都很简单。)

使用kubernetes,我们建立一个kubernetes的服务,大功告成了!我们不用浪费时间建立一个发现服务器,编写定制的客户端,使用DNS等,已经可以工作的很好了。我们转而看下一部分,微服务提供的商业价值。

是如何工作的?

以下是kubernetes的一些抽象概念:

  • Pods

  • Labels / Label Selectors

  • Services

Pod很简单,就是Linux容器。Label也很简单,它们是键-值字符串,用于标记 Pod。比如 Pod A可以标记为app=cassandra, tier-backend, version=1.0, language=java,这些标记可以表示任何你的意图。

最后一个概念是服务,也很简单。服务是一个固定的群集IP地址。该IP地址是一个虚拟IP地址,可用于发现/调用在Pod/容器中的实际端点地址。实际的IP地址是如何被发现的?服务使用了label selector来选取你定义过的标签Pod。举例,使用选择器“app=cassandra AND tier=backend”,就得到一个虚拟的IP地址,访问所有具备上述标记的Pod,这个选择器是即时生效的,所以任何离开集群的pod或者加入到集群中的都可以自动被启动并参与到服务发现中。

Kubernetes Simple Services

另一个使用kubernetes服务的好处是,智能的选取Pod来加入到服务中,根据它们的存活和健康信息。Kubernetes使用内建的存活和健康检查方法,来确定一个Pod是否包含在一个特定服务之中,如果不满足条件,Pod会被驱逐出去。

注意Kubernetes服务不是一个“东西”,一个设施或者docker容器等,它就是一个虚拟表示,所以没有单独故障点,是一个IP地址,由kubernetes来路由消息。

这个概念难以置信的强大,对于开发者来说很简单,现在一个应用想用cassadra作为后端数据库,只需要用一个固定IP地址对应的一组cassadra数据库。然而硬编码固定IP地址不是好的主意,因为可以迁移应用到不同的环境下(QA/PROD),需要更改IP(或者注入一些配置信息),这时我们使用DNS。

使用Kubernetes的DNS集群方案是正确答案。因为对于给定的环境,IP是固定的,我们不用考虑其缓存,它们不会变化。我们使用DNS服务。比如应用配置使用http://awesomefooservice,当我们从Dev换到QA以及Prod环境时,配置相应的kubernetes服务,我们的应用不需要改变。

Kubernetes Services

我们不需要额外的配置,我们并不需要担心的DNS缓存/SRV记录,自定义库的客户端和管理额外的服务发现的基础设施。Pod可以被加入到集群中,标签选择器积极的选取符合标记的Pod,应用可以是Java, Python, Node.js, Perl, Go, .NET, Ruby, C++, Scala, Groovy等任何语言开发的。服务发现机制不关心特定的客户端而只是使用它。

那么客户端均衡负载的情况呢?

很有趣的是,Netflix编写了Eureka和Ribbon,组合使用它们,可以用来客户端的负载均衡。一般来说,服务注册器管理和跟踪集群中存在哪些服务,并且把这些数据发送给感兴趣的客户端。这样,客户端知道了集群中节点的信息,它可以选择一个(随机,粘滞或者自定义的算法),然后调用它。下一次调用时,又可以选择集群中另外的一个服务。

另一个重要的方面是:由于客户端知道服务在哪里,客户端可以直接联系服务端,而不用经过中途的跳转。

在我看来,客户端的负载均衡大概占据5%的用例。原因解释一下: 我们要的是一种方法,可以理想的做可扩展的负载均衡,而不需要任何额外的设施和客户端库。在大多数情况下,我们并不关心是否有中间的负载均衡器额外的跳转(大概你99%的应用情景是这样的)。我们遇到的情景是,服务A调用服务B,需要调用服务C,D,E,这样才能获得图片信息。这种情况下,很多的跳转带来更多的延时。所以可能的方案是“移除多余的跳转”,而不是均衡负载的跳转,调用下游服务的次数是必须的。我的博客中有关于事件驱动系统的文章,以及自治管理相关的讨论,可以关注。

使用Kubernetes的服务,我们完成了适度的负载均衡(没有服务注册,定制的客户端,DNS缺陷等的开销)。当我们通过DNS和一个Kubernetes服务进行交互时,将使用集群中的Pod进行负载均衡操作(使用标签选择器)。如果你不希望在负载均衡处有额外的跳转,请不要担心,虚拟IP直接路由到Pod,并不涉及到物理的网络。

好极了,95%的用例都变得很简单!因为应用情景总是在95%的场景中,所以不用过度设计,让事情越简单越好。

那么对于剩下的5%的用例呢?有时你会遇到这样的情况,需要根据业务决策来决定使用具体集群中哪一个后端服务。一般情况下,使用一些特定的算法,而比像“轮询”,“随机”,“会话粘滞”要复杂的自定义算法,根据具体的应用程序。这时使用客户端的均衡负载。在这种模式下,依然可以使用kubernetes的服务发现来找到是在哪个Pod集群,然后用客户端代码直接调用其中的Pod。fabric8.io社区的kubeflix项目,就使用Robbon作为发现插件,来使用REST API获得服务对应的所有Pod列表,然后客户端的代码决定调用哪一个pod。其他语言使用kubernetes的REST API也可以做类似的工作。可以投入做一些客户端发现库来简化这个操作。更确切的说,是把这样的业务逻辑,从应用程序中分离出来作为独立的中间件。通过使用kubernetes,你可以部署这样的模块,作为应用的独立部分,并把自定义的均衡负载逻辑定义在那儿。

Client load balance

重申一次,以上只是用在5%的用例中,有更多的复杂性。对于95%用例,就用内建的和特定语言的客户端无关的方式就好。

关于容错

依赖相关的系统,构建时应当总是要记住承诺性。这意味着,应用总是要时刻注意它的依赖系统是否处在不可用或者崩溃的状态下。问题是kubernetes是否有容错方面的考虑?

kubernetes的确有自愈的能力,如果一个pod或者pod中的一个容器停止了,可以将它重新启动,并保持ReplicaSet保持不变(比如,如果需要有10个“foo” pods,那么kubernetes总是保持有10个,如果有停掉的,会再次启动pod)

自愈设施是非常棒,而且是自带的服务。但我们需要讨论的是,当应用对应的依赖(如数据库或者其他服务)停止时,会发生什么?这取决于应用程序如何与之交互。例如,在Netflix,如果你尝试观看特定电影时,要调用“授权”服务,来知道你什么特权,是否可以看电影。如果该服务停止,我们应该阻止观看该影片的用户?还是显示异常堆栈信息? Netflix的做法是让用户观看电影。这是更好的方案,订阅者应当观看,一个服务依存关系在一段时间出现错误时,不应当影响到用户享受一次电影。

我们需要的是一种正常的降级方案,或者寻找一种替换的方法来保持服务承诺。Netflix的Hystrix项目就是给Java开发者一个非常棒的方案。它实现了方法来做到"bulkheading","circuit breaking", "fallbacks"。每种对应一个应用相关的实现,也有不同语言的客户端库。

Hystrix

这里kubernetes也能帮上忙么?是的!

我们再看看很棒的kubeflix项目,你可以使用Netflix Turbine项目来聚集和可视化集群中所有的运行中的断路器。Hystrix可以用SSE暴露信息送到Turbine中。然而Turbine如何发现哪些Pod中包含了Hystrix?可以使用kubernetes的标签。如果我们标记所有使用hystrix的pod,"hystrix.enable=true",那么Kubeflix Turbine引擎自动发现每个断路器,获得SSE流并且显示到Turbine页面上。这点感谢Kubernetes。

Turbine Hystrix

关于配置

Netflix Archaius用来处理云端的分布式配置管理服务。可以设置一个配置服务器,使用Java库来查找配置项。还支持动态配置改变。

这里又有95%用例情景。我们希望让环境特定的配置(这是一个重要的区别,不是每一个环境特定的配置需要根据运行环境而改变)存储在应用程序之外,并且基于运行的环境(DEV,QA,PROD)来注入这些配置项。但我们真的想要一个与语言无关的方式来查找配置,而不是使用Java库,或者使用classpath等使得配置复杂化。

在Kubernetes我们使用三个结构来注入基于环境的配置:

  1. Environment Variables 环境变量

  2. GitRepo Volume git仓库作为文件卷

  3. ConfigMap

通常我们可以设置环境变量,注入配置数据到Linux容器中,大多数语言都可以轻松读取到这些信息。 我们可以存储这些配置到git中,然后绑定配置仓库到我们的pod上(作为文件系统的文件),这样可以用任何框架来获取配置文件信息了。 最后,我们可以使用Kubernetes的ConfigMap来存储配置版本信息,作为文件系统mount到Pod上,同样的,可以用任何语言和框架来处理配置文件。

5%的用例情况呢?

在5%的使用情况,你可能希望在运行时动态更改配置。Kubernetes帮助做到了这一点。您可以更改配置文件,通过ConfigMap这些变化动态地传播到了mount的pod。在这种情况下,你需要有一个客户端库,能够检测这些配置的变化并通知你的应用程序。Netflix的Archais有一个客户端可以做到这一点。Spring Cloud Kubernetes的Java项目使这个更容易(使用ConfigMaps)。

关于Spring Cloud

使用Spring开发Java微服务,往往使用Spring Cloud中的Netflix项目,基本就是Netflix OSS项目。fabric8.io社区也有很多好用的项目,比如spring-cloud-kubernetes,大多数模式(包括配置,跟踪等)可以无须额外的,复杂的基础设施(如服务发现引擎,配置引擎等)而直接运行在kubernetes之上。

总结

如果你开始构建微服务的方法,你肯定已经被Netflix OSS/Java/Spring/SpringCloud所吸引。但是要知道你不是Netflix,也不需要直接使用AWS EC2,使得应用程序变得很复杂。如今使用docker和采用kubernetes是明智之举,它们已经具备大量的分布式系统特性。在应用层进行分层,是因为netflix5年前面临的问题,而不得不这样做(打赌说如果那时有了kubernetes,netflix OSS栈会大不相同)。避免应用程序复杂是一个明智的选择。