Netflix OSS、Spring Cloud 还是 Kubernetes? 都要吧!

阅读数:10691 2016 年 8 月 7 日

话题:语言 & 开发架构Kubernetes

在我的新书《Java 开发者的微服务》(O'Reilly 出版社,2016 年 6 月出版,就快发布了!)中提到了本文的部分内容,但我还是想在这里特别讲一下。有些朋友向我提了些 Netflix OSS 的问题,以及怎样用 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 程序或服务代码里面。

这就带来了第一个问题。

种类众多的微服务是可以用各种不同的框架或语言实现的,但像服务发现、负载均衡、容错等等功能还是非常重要而且必不可少的。如果我们在容器里面运行这些服务,我们可以借助于强大的语言无关的架构来做各种事情,比如构建打包部署健康检查滚动升级蓝绿发布安全、还有其它等等各种事情。作为一种类型的KubernetesOpenShift专注于为企业用户服务,它把所有事情都帮你做完了:没有什么东西必须要你的应用层程序去知道或者处理的了。你只要简简单单的实现你的应用程序和服务就好,只要专注于它们应该做的功能就好。

这样是不是说这些架构可以帮忙把大家从服务发现、负载均衡、容错等功能中解放出来?那为什么这些还要是应用层的事?

如果你使用 Kubernets(或者某些变种),那么答案就是:是的。

Kubernetes 方式的服务发现

用 Netflix OSS 时通常你要建立一台服务发现服务器,让所有可以被各种客户端发现的服务注册在这里。比如你用Netflix Ribbon来与各种其它服务交互,就需要能找出来它们都运行在哪里。各种服务可能下线,可能按它们自己的运行需要退出,也有可能我们要向集群中加入更多服务来做横向扩展。这种集中式的服务发现注册机制通常可以帮助我们跟踪集群中有哪些服务是可用的。

问题之一是,做为一个开发人员你需要做这些事情:

  • 决定我到底是想要一个AP系统(ConsulEureka等)还是CP系统(ZooKeeperetcd等等)。
  • 想明白在上了规模时如何运行、管理和监控这些系统(这可不是一个小小的演示系统)。

而且,你要找到对应你使用的编程语言的客户端库,才能与服务发现机制通信。回到我们刚才讨论的问题,微服务可能是用许多不同种语言实现的,所以可能一个成熟的 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!


感谢魏星对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ@丁晓昀),微信(微信号:InfoQChina)关注我们。