【译文】网飞(Netflix)如何真正使用 Java 的

作者介绍
Paul Bakker 是 Netflix Java 平台团队的 Java 冠军和开发人员。在 Netflix,他致力于发展 Java 技术栈和开发人员工具。他还是 DGS 框架(GraphQL)的原作者之一,并与他人合著了两本由 O’Reilly 出版的 Java 模块化书籍。

我将谈谈 Netflix 如何真正使用 Java。你可能知道,Netflix 其实只是在使用 RxJava 微服务,以及 Hystrix 和 Spring Cloud。实际上,Chaos Monkeys(混沌猴子)只是在跑龙套。我只说了一半,因为在几年前,也许除了 “混沌猴 “之外,大部分情况都是这样。这个堆栈是我们在过去几年中建立起来的。现在情况变了。我经常在这样的会议上与人交谈,他们会说,是的,我们在使用 Netflix 协议栈。你说的到底是哪个堆栈?这几乎从来都不是我们真正在用的堆栈。这些只是人们对 Netflix 的联想,因为我们已经谈论我们的技术这么多年了,但事情可能已经发生了一些变化。我们将打破一些神话。我们来看看我们究竟在用 Java 做什么。事物是不断发展的。事情确实一直在变化。

背景介绍

我叫保罗。我在 Netflix 的 Java Platform 部门工作。Java Platform 负责我们围绕 Java 构建的库、框架和工具,以便我们所有的 Java 开发人员都能愉快地开发 Java 应用程序。我也是 Java 的拥护者。我在 Java 领域工作了很长时间。过去,我写过两本关于 Java 模块化的书。我还是 DGS 框架的首批作者之一,这是我们用于 Java 的 GraphQL 框架。我们将讨论很多关于 DGS 的内容,以及这些内容如何与架构相匹配。

不断发展的架构

在我们开始深入研究 JVM、我们如何使用 Java 以及我们正在使用的框架之前,我们必须更好地了解一下我们的架构是如何演变的。这就解释了为什么几年前我们使用 Java 时采用的是某种方式,而今天我们采用的方式却截然不同。关于 Netflix 的 Java,您应该了解的是,我们有很多 Java。我们基本上是一家 Java 商店,Netflix 的每个后端基本上都是 Java 应用程序。我们有很多应用程序。以 Netflix 的规模,我们有很多内部应用程序来记录各种事情。我们还是世界上最大的电影制片厂之一。为了制作电影,我们开发了很多软件,基本上都是 Java。当然,我们还有所谓的流媒体应用程序,基本上就是你们可能知道的 Netflix 应用程序。我们现在看到的就是这个。这个屏幕就是我们所说的 LOLOMO,即电影列表。这只是 Java 支持的应用程序的一个例子。你必须明白,我所说的几乎所有内容,基本上适用于所有 Java 后端。我们现在几乎所有不同的系统都使用相同的架构,无论是内部系统还是面向消费者的系统,我们在所有地方都使用相同的技术栈。虽然我举了这个例子,但这只是一个可供玩味的大例子,它的普遍性远不止于此。

Groovy 时代

将近七年前我加入 Netflix 时,我们正处于我所说的 Groovy 时代。你可能对 Netflix 有所了解,现在也是如此,那就是 Netflix 拥有一个微服务生态系统。基本上,每项功能和每条数据都归某个特定的微服务所有。它们数量众多,多达数千个。在这张幻灯片上,我只是编了个故事,因为我觉得很有道理。这只是我们实际生产中的一个简化版本。想想这个 LOLOMO 屏幕,这个我们刚刚看过的电影列表,在上一张幻灯片中,你可能对这个屏幕很熟悉,为了呈现这个屏幕,我们必须从许多不同的微服务中获取数据。也许我们需要一个排名前十的服务,因为我们需要一个排名前十的电影列表。这需要一个特定服务的支持。然后还有一个艺术品服务,为我们提供在 LOLOMO 中展示的图片,这些也都是个性化的。可能还有电影元数据服务,为我们提供电影名称、演员和电影描述。可能还有一个 LOLOMO 服务,它实际上为我们提供了需要实际呈现的列表,这同样也是个性化的。我说,我们可能有 10 项服务可以调用。如果你的设备,比方说,你的电视,或者你的 iOS 设备只是对这些不同的微服务进行 10 次网络调用,效率通常会很低。它根本无法扩展。你会有非常糟糕的客户体验。感觉就像在使用迪斯尼应用程序。这并不理想。取而代之的是,我们需要一个单一的应用程序编程接口(API)前门,让您的设备可以调用。从这里,我们向所有不同的微服务进行扇出,因为现在我们在我们的网络中,我们在一个非常快速的网络上。现在,我们可以在不影响性能的情况下进行扇出。我们还有另一个问题要解决,因为所有这些不同的设备在细微之处都有些不同。我们试图让用户界面在每种不同的设备上看起来和表现都相似。所有这些不同的设备,比如电视和 iOS 设备,在内存和网络带宽方面都有非常不同的限制。实际上,它们加载数据的方式也有细微差别。

想一想,如何创建一个适用于所有这些不同设备的 API?比方说,创建一个 REST API。我们可能会得到太少或太多的数据。如果我们创建一个 REST API 来统治所有设备,那么对于所有这些不同的设备来说,体验都会很糟糕,因为我们总是会浪费一些数据,或者我们不得不进行多次网络调用,这也是很糟糕的。为了解决这个问题,我们采用了所谓的 “后端换前端 “模式。基本上,每个前端、每个用户界面都有自己的迷你后台。迷你后端负责进行扇出,并获取用户界面在该特定点所需的数据。它们过去由一个 Groovy 脚本支持。迷你后端基本上是一个 Groovy 脚本,用于特定设备上的特定屏幕,或者实际上是特定设备的某个版本。这些脚本由用户界面开发人员编写,因为只有他们才真正知道渲染特定屏幕需要哪些数据。这个 Groovy 脚本将保存在 API 服务器中,基本上就是一个巨大的 Java 应用程序。它将通过调用 Java 客户端库,向所有这些不同的微服务进行扇出。这些客户端库基本上就是 gRPC 服务或 REST 客户端的包装器。

现在,我们开始看到一个有趣的问题,因为如何在 Java 中处理这种扇出?这其实并不简单。因为如果用传统的方法,即创建一堆线程,然后开始用最少的线程管理来管理这种扇出,很快就会变得非常棘手,因为这不仅仅是管理一堆线程,还要考虑容错问题。如果其中一个服务响应不够快怎么办?如果它失效了怎么办?现在,我们必须清理线程,确保一切恢复正常。这同样不是一件小事。这就是 RxJava 和反应式编程的真正用武之地。因为反应式编程提供了一种更好的方法来完成这种扇出。它会处理所有线程管理和类似的事情。正是因为这种扇出行为,我们才会如此深入地研究反应式编程领域,多年前 RxJava 的大热也有我们的部分功劳。在 RxJava 的基础上,我们创建了 Hystrix,这是一个容错库,可以处理故障转移和隔板等所有问题。七年前,当我加入公司时,这样做很有意义。这是为大部分流量提供服务的大型架构。实际上,它仍然是我们架构的重要组成部分,因为取决于你使用的是什么设备,如果是稍微老一点的设备,你可能仍然会使用这个 API,因为我们并不只有一个架构,我们有很多架构,因为这样更好。

局限性

虽然这种架构效果显著,但也存在一些局限性,因为我们主要是通过这种架构来扩大会员数量的。一个缺点是每个端点都有一个脚本。同样,我们需要为每个不同的用户界面提供 API。有很多脚本需要维护和管理。另一个问题是,用户界面开发人员必须创建所有的小型后端,因为他们知道自己需要什么数据,所以必须编写这些后端。现在,他们在 Groovy Java 领域使用 RxJava。虽然他们很有能力这样做,但这可能并不是他们日常使用的主要语言。其实,主要问题在于反应式真的很难。就我自己而言,我做反应式编程至少有 10 年了。我曾经为此兴奋不已,并告诉每个人这有多棒。实际上,这很难,因为即使有了这些经验,再去看一段非同小可的反应式代码,我也完全不知道发生了什么。我需要花很多时间才能真正理解,好吧,这就是实际发生的事情。这些是应该发生的操作。这就是后备行为。这很难。

GraphQL Federation

慢慢地,我们已经迁移到了一个全新的架构,那就是,我们正在从一个不同的角度来看待问题。这一切都基于 GraphQL Federation。比较 GraphQL 和 REST,GraphQL 的一个非常重要的方面是,使用 GraphQL,你总是有一个模式。在模式中,你可以放置所有操作,包括查询和突变,并对其进行定义,告诉它查询返回的类型中哪些字段是可用的。在这里,我们有一个节目查询,它返回一个节目类型,一个节目作为标题,它还有评论。评论也是我们定义的另一种类型。然后,我们可以向我们的 API 发送查询,API 位于幻灯片的右侧。我们要做的是,这一点同样非常重要,我们必须明确我们的字段选择。我们不能只询问节目,然后从节目中获取旧数据。现在,我们必须明确指出,你想要获取节目的标题和评论星级。如果我们不要求一个字段,我们就得不到一个字段。这一点非常重要,因为与 REST 相比,基本上 REST 服务决定发送给你什么,你就能得到什么。你获取的只是你明确要求的数据。如果指定查询,工作量会更大,但可以解决过度获取的问题,即获取的数据远远超出实际需要。这样一来,创建一个为所有不同用户界面提供服务的 API 就容易多了。通常情况下,当您发送 GraphQL 查询时,只会得到编码为 JSON 的结果。

我们不只是在使用 GraphQL,实际上我们还在使用 GraphQL Federation,以便将其重新融入我们的微服务架构中。在这张图片中,我们仍然使用微服务,但现在我们称之为 DGS。这只是我们 Netflix 想出的一个术语。它是一个域图服务。基本上,它只是一个 GraphQL 服务。其实没有什么特别之处,但我们称之为 DGS。DGS 只是一个 Java 微服务,但它有一个 GraphQL 端点。它有一个 GraphQL API。这也意味着它有一个模式,因为我们说过,对于 GraphQL 来说,你总是有一个模式。有趣的是,我们当然有许多不同的 DGS,许多不同的微服务。从设备的角度来看,例如从电视的角度来看,只有一个大的 GraphQL 模式。GraphQL 模式包含我们必须呈现的所有可能数据,比方说,一个 LOLOMO。你的设备并不关心后端可能有一大堆不同的微服务,而这些不同的微服务可能会提供该模式的一部分。在微服务方面的故事的另一面,在这个例子中,我们的 LOLOMO DGS 定义了一个类型 show,只有一个标题。图像 DGS 可以扩展该类型展示,并为其添加艺术品 URL。这两个不同的 DGS 对彼此一无所知,只知道有一个展示类型。它们都可以贡献该模式的部分内容,甚至是相同类型的内容。它们只需将自己的模式发布到联合网关。现在,联合网关知道如何与 DGS 对话,因为它们都有一个 /GraphQL 端点。就是这样。它知道模式的这些不同部分,因此,如果有一个查询进来,我们需要标题和艺术品 URL,它就知道必须呼叫这些不同的 DGS,然后获取所需的数据。从很高的层面上看,这与你以前使用的并无太大区别,但在细节上却有很多不同。

在这里,我也要改变一下我们的故事。首先,我们不再有任何重复的 API。我们不再需要为前端提供后端,因为 GraphQL 作为 API 已经足够灵活,而且由于字段选择的原因,我们实际上不再需要创建那些特定于设备的 API。这也意味着我们不再需要用户界面工程师进行服务器端开发。这很好。我们确实可以在模式上进行合作。这是件大事,因为现在我们缩小了用户界面开发人员和后端工程师之间的差距,因为现在他们可以在模式上进行协作,并找出:好的,我们需要什么格式的数据?最重要的是,我们不再需要任何 Java 客户端库,因为联合网关只知道如何与通用 GraphQL 服务对话。它不需要特定的代码来调用特定的 API。这一切都只是 GraphQL。它只需知道如何与 GraphQL 服务对话。仅此而已。这一切都基于 GraphQL 规范。我们不再需要特定的代码来调用特定的微服务。

这对我们的 Java 栈意味着什么?

现在我们来看看这对我们的 Java 栈有什么影响?我们真的不再需要 Rx 或 Hystrix 之类的东西了,因为以前我们需要这些东西,因为我们需要特定的代码来调用:ok,我想调用这个微服务,然后调用这个微服务,同时调用另一个微服务。为此,我们需要一个 API。现在不需要了,因为 GraphQL 联合规范已经解决了这个问题。这并不完全正确,因为联盟网关本身实际上仍在使用网络客户端来调用不同的 DGS,而且仍是反应式的。不过,它不再为这个微服务使用任何特定代码。它实际上是一段非常简单的网络客户端代码,它知道,好吧,我必须调用这三个服务,去做吧。这都是 GraphQL,所以非常简单。后端的所有 DGS 和其他微服务都是普通的 Java 应用程序。它们并没有什么特别之处。它们不需要在任何地方进行任何反应式编程。

微服务中的微

在深入探讨 Java 栈的其他部分之前,我想先谈谈微服务中的微,因为人们似乎对微服务在实践中的实际运作方式感到困惑。的确,微服务拥有特定的功能或数据集。更重要的是,这种微服务由一个团队拥有。这是关于微服务的一个非常重要的部分。有了 GraphQL 联合架构,这一切就更加真实了,因为现在更容易将事情分割到不同的微服务中,并让所有事情都能很好地运行。不过,不要被这些微服务的规模所迷惑,因为 Netflix 的很多所谓微服务,仅从代码库来看,就比我在其他很多公司工作过的大型单体服务要大得多。有些系统真的很大。那里有大量的代码。当然,当它们被部署时,可能会部署在由数千个 AWS 实例组成的集群上。它们真的一点都不小。这也回答了 “我是否应该做微服务 “这个问题。这取决于你的团队规模。你是否只有一个团队负责所有事务,而且只是一个小团队?如果你想在这里添加微服务,那你就会无端增加复杂性。如果你想把你的团队拆分成更小的团队,基本上,只是因为团队规模的原因,那么把你的大系统拆分成更小的部分也是有意义的,这样每个团队都可以拥有和操作其中的一个或多个服务。

Java 在 Netflix

是时候真正了解 Java 的方方面面了。现在,我们从更高的层面了解了如何以及在哪里使用 Java。现在我们来谈谈它的实际样子。我们现在主要使用 Java 17。是时候了。我们也已经在积极测试和推出 Java 21。Java 21 刚刚正式发布。我们使用的是普通的 Azul Zulu JVM。这只是一个 OpenJDK 版本。我们没有构建自己的 JVM,也没有任何构建自己的 JVM 的计划。尽管 Reddit 上有一个非常有趣的线程声称我们会这样做。我们确实没有,也没有兴趣这么做。OpenJDK 真的很棒。我们有大约 2800 个 Java 应用程序。其中大部分都是各种规模的微服务。还有大约 1500 个内部库。其中有些是真正的库,很多只是客户端库,基本上就是坐在 gRPC 或 REST 服务前面。在构建系统方面,我们使用 Gradle,在 Gradle 的基础上还有 Nebula,这是一套开源的 Gradle 插件。Nebula最重要的方面,我强烈建议大家研究一下,首先是解决库的问题。众所周知,Java 的类路径是扁平的。在给定时间内,你只能拥有一个版本的库,如果你拥有多个版本,就会发生有趣的事情。为了防止这些有趣的事情发生,基本上你只想选择一个版本,Nebula就能解决这个问题。星云的下一个功能是版本锁定。基本上,你将获得可重现的构建,在你明确升级之前,你总是使用同一套版本的库进行构建。这使得一切都非常具有可重复性。我们几乎只使用 IntelliJ 作为集成开发环境。在过去几年中,我们还投入了大量精力实际开发 IntelliJ 插件,以帮助开发人员做正确的事情。

Java 17 升级

我们现在主要使用 Java 17。这其实是件大事,因为这很尴尬,但在今年年初,我们还主要使用 Java 8。Java 8 已经过时了。为什么我们还在用 Java 8 呢?因为我们已经有了 Java 11 和 Java 17 很长时间了。不知何故,我们就是不动。其中一个原因是,直到一年前,我们大约一半的微服务,尤其是大型微服务,还在使用旧的应用栈。那不是 Spring。它是基于 Guice 和大量旧 Java EE API、大量不再维护的旧库的自制产品。最开始,当我们开始升级到 Java 11 时,很多旧库都不兼容。于是,开发人员就有了这样的印象:升级很难,而且会破坏一些东西,我还是不要升级了。另一方面,对于开发人员来说,升级带来的好处也非常有限,因为如果将 Java 8 与 Java 17 相比,肯定会有一些不错的语言特性。光是文本块就足以让我升级,但这并不是什么大问题。8 和 17 之间的差异固然不错,但也不至于改变你的生活。升级到 Kotlin 比升级到 JDK 更让我们兴奋。

当我们终于开始推动更新到 Java 17 时,我们发现了一些非常有趣的事情。与 Java 8 相比,在不修改任何代码的情况下,我们发现 Java 17 的 CPU 使用率提高了约 20%。这完全是因为我们主要使用的垃圾回收器 G1 的改进。在我们运行的规模上,CPU 使用率提高 20% 是件大事。这可能是一大笔钱。说到 G1,G1 是我们目前用于大部分工作负载的垃圾回收器。我们测试过所有可用的垃圾回收器。一般来说,G1 是我们取得最佳平衡的垃圾回收器。但也有一些例外,比如我们的代理 Zuul。它在 Shenandoah 上运行,这是暂停时间较短的垃圾回收器。对于大多数工作负载而言,Shenandoah 的运行效果不如 G1。虽然 G1 不再那么令人激动,但它仍然非常出色。

Java 21+

现在,我们终于大力推进了 Java 17,而且大部分服务都已升级,我们也有了 Java 21。我们已经用它进行了几个月的测试。现在,事情真的变得令人兴奋了。第一件令人兴奋的事情是,如果您使用的是 Java 17,升级到 Java 21 几乎不费吹灰之力。这简直易如反掌。你不会再遇到从 Java 8 升级到新版本时遇到的问题。而且还有更多有趣的功能。第一个让我超级兴奋的是虚拟线程。这只是复制粘贴,它来自 Java 21 虚拟线程规范 JEP。它可以让以简单的线程-请求(thread-per-request)方式编写的服务器应用程序以接近最佳的硬件利用率进行扩展。听起来不错。如果你使用的是基于 servlets(如 Spring Web MVC 或其他任何基于 servlets 的框架)的应用程序,那么这种按请求线程的风格基本上就是你能得到的。请求进来后,Tomcat 或你使用的任何服务器会给它一个线程。该线程基本上就是所有工作的发生地,或者说是为特定请求开始工作的地方,并在该请求中一直工作到请求完成。这是一种非常简单易懂的编程风格,所有框架都是基于这种风格。它有一些可扩展性限制,因为一个系统中只能有效运行这么多线程。如果你有大量的请求进来,而我们显然就有大量的请求,那么线程的数量就是你如何扩展系统的一个限制因素。改变这种模式非常重要。当然,另一种方法是再次进行反应式编程,比如 WebFlux。这也会让你陷入反应式编程的泥潭,同样,也会带来我们已经讨论过的所有复杂性。

现在,我认为虚拟线程可能是自 lambdas 以来最令人兴奋的 Java 功能。我认为,它将真正改变我们编写和扩展 Java 代码的方式。我认为,到最后,它可能会进一步减少反应代码,因为已经不再需要它了。它只是去除了复杂性。在过去的一个多月里,我们已经在生产中运行虚拟线程,并对其进行了一些尝试。我会再详细介绍的。Java 21 中另一个有趣的功能是新的垃圾收集器或更新的垃圾收集器,因为 ZGZ 并不是新功能。在之前的版本中就已经有了。现在,他们将其改为世代垃圾收集器,这使得它比 G1 垃圾收集器具有更多优势。这将使 ZGC 更适合各种工作负载。它仍然专注于低暂停时间,但会适用于更广泛的用例。现在下结论还有点早,因为我们还没有对此进行足够的测试,但我们预计 ZGC 现在基本上将成为我们很多工作负载和服务的一个非常好的性能升级。同样,这些都是非常重要的事情,我们可以节省大量的资源。Shenandoah 现在也是一代产品,但仍处于预览阶段。同样,我们只需运行它,看看会发生什么。垃圾回收实在是一个太复杂的话题,我们不能只知道,把这个垃圾回收器和这个灵活的垃圾回收器放在一起,一切就会变得神奇和超快。事实并非如此。我们需要不断尝试,然后稍作调整,再试一次,最终找到最佳状态。我们还没有达到这个境界。我们期待看到一些非常有趣的东西。最后,在 Java 21 中,你还会发现很多不错的语言特性。我们在 Java 语言中获得了面向数据编程的概念。这真的很不错。它结合了记录和模式匹配等功能。Java 现在非常不错。

虚拟线程

回到虚拟线程。虽然我说过这是件大事,很可能会改变我们编写代码和扩展代码的方式,但它也不是免费的午餐。这不仅仅是在实例上启用 Java 21,然后通过虚拟线程的魔力让一切运行得更快。事情并非如此。首先,我们必须更改我们的框架库,并在一定程度上更改应用程序代码,以便真正开始利用虚拟线程,这就是第一步。有几个明显的地方我们可以做到这一点,并且已经开始尝试,比如 Tomcat 连接池。同样,这些都是线程池,它按请求提供线程。这似乎是一个相当明显的地方,我们可以直接使用虚拟线程来代替。不使用线程池,而是使用虚拟线程。在启用虚拟线程之前,你已经在生产中使用虚拟线程运行了一些大型服务。但这并不会自动加快运行速度,因为你还需要做其他事情才能真正利用它。它也不会让事情变得更糟。如果你能安全地启用这个基本功能,有时会从中获得一些好处,有时并不会真正改变它,因为它并不是一个限制因素。你也许应该从这一点入手。Spring 中的异步任务执行同样也只是一个线程池,而且很多时候你会因为其他网络调用而阻塞代码。它似乎是虚拟线程的理想候选者,所以我们在这里启用了它。还有一个我们还没有深入研究过的大问题,但我预计它将改变游戏规则,那就是我们如何执行 GraphQL 查询。使用 GraphQL,每个字段都可以并行获取。我们在虚拟线程上这样做是非常合理的,因为在代码中,我们经常会进行更多的网络调用和类似操作。虚拟线程在这里很有意义,但我们必须实现它并进行测试,在获得最佳模型之前可能还需要一点时间。

然后,我们还有其他一些看似显而易见的地方。例如,我们有一个用于 gRPC 客户端的线程工作池,gRPC 调用外发服务就发生在这里。这似乎是一个显而易见的地方,比如,让我们在这里添加虚拟线程。然后我们发现,我们实际上降低了百分之几的性能。原来,这些 gRPC 客户端工作池的 CPU 密度非常高。如果在其中加入虚拟线程,实际情况会更糟。这并不一定是坏事。这只是我们不得不学习的东西。这确实表明,这不是免费的午餐。实际上,我们必须弄清楚,在哪些地方使用虚拟线程是合理的,在哪些地方使用虚拟线程是不合理的。好消息是,目前这主要都是框架工作。我们可以作为一个平台团队来做,也可以在我们使用的开源库中做。基本上,我们的开发人员将获得更快的应用程序。这很好。在 Spring 6.1 或 Spring Boot 3.2 中,有很多工作正在进行,以充分利用虚拟线程,这将在下个月推出。我们可能会在明年年初的某个时候采用它。GitHub 上的 GraphQL Java 正在进行一场非常有趣的讨论,讨论的内容是改变 GraphQL 查询的执行方式,甚至有可能重写它以充分利用虚拟线程。这个问题还没有解决。这是一个正在进行的讨论。如果你在这个领域,我认为这绝对是一个值得贡献的领域。对于用户代码来说,因为所有其他的东西大多都是框架代码,所以对于用户代码来说,我认为结构化并发是我们将看到大量反应式代码被替代的另一个地方。因为结构化并发最终为我们提供了处理扇出(fanout)等问题的 API,然后将所有东西重新整合在一起。在 Java 21 中,结构化并发仍处于预览阶段。它似乎已经非常接近最终版本了,所以我认为至少可以安全地开始使用它并进行尝试。再往后一点,我们还将获得范围值,这是与虚拟线程相关的另一个新规范。这将为我们提供一种基本上摆脱 ThreadLocal 的方法。这也主要是与框架相关的工作。这只是一种类似于 ThreadLocal 的更好、更高效的方法。

Spring Boot Netflix

我已经提到过一点我们使用 Spring Boot 的情况。大约从一年前开始,我们完全使用 Spring Boot。直到一年前,我们大约 50% 的应用程序还在使用我们自己开发的基于 Guice 的 Java 栈和大量过时的 Java EE 库。我们并没有很好地推动所有应用程序都使用 Spring Boot。所有新应用程序都已经基于 Spring Boot。这变得非常混乱,尤其是因为旧的自制框架没有得到很好的维护。我们花了很大力气将所有服务迁移到 Spring Boot 上。这次迁移主要是很多团队的血汗和泪水。从一种编程模型迁移到另一种编程模型并非易事。作为平台团队,我们确实提供了很多工具,例如,IntelliJ 插件可以在可能的情况下处理代码迁移和配置迁移之类的事情。尽管如此,工作还是非常繁重。非常痛苦。不过,现在我们使用 Spring Boot 了,我们有了一个大家都在使用的框架,这对每个人来说都好很多。我们正在尝试使用开源软件 Spring Boot 的最新版本。我们将使用 3.1 版,并尽量与开源社区保持密切联系,因为这是我们获得最大收益的地方。除此之外,我们还需要与 Netflix 生态系统和基础设施进行大量整合。这就是我们所说的 Spring Boot Netflix,基本上就是我们在 Spring Boot 基础上构建的一整套模块。基本上,它的开发方式与 Spring Boot 本身的开发方式相同,因此有很多自动配置。这就是我们添加 gRPC 客户端和服务器支持的地方,它与我们的 SSO 堆栈(AuthZ 和 AuthN)非常集成。你可以获得可观察性,因此可以进行跟踪、度量和分布式日志记录。我们有一大堆 HTTP 客户端,可以处理 mTLS,并再次实现可观察性和与安全堆栈的集成。我们使用嵌入式 Tomcat 部署所有这些应用程序,这对于 Spring Boot 应用程序来说是非常标准的。

让我们来了解一下它的功能。例如,我们有一个 gRPC Spring 客户端。这看起来很像 Spring,但却是我们添加的。基本上,它引用了一个属性文件,该文件描述了 gRPC 服务,并说明了服务的位置。它配置了故障转移行为。这样,你就可以使用带有额外注释的 Java API 来调用另一个 gRPC 服务。这样,你还可以完全免费地获得可观察性等功能。对于任何请求,无论是 gRPC 还是 HTTP,你都可以免费获得可观察性,包括跟踪和度量,以及所有这些可用的东西。另一个例子是与 Spring 安全性集成,这样我们就能获得 SSO 颜色。即使在冷链中间有许多服务,你基本上也能获得用户,这就是所谓的服务。正如我所说,我们与 Spring Security 集成后,还可以基于我们自己的认证模型进行基于角色的认证。

为什么要使用 Spring Boot?

您可能会问,为什么我们要使用 Spring Boot,而不是其他更花哨的框架呢?当然,因为在过去几年中,Java 领域出现了很多创新的框架。Spring Boot 确实是最流行的 Java 框架,但这并不一定意味着它更好,但它确实在使用开源社区(当然,Spring Boot 的开源社区非常大)、获取文档、培训和所有这些方面提供了很多优势。我认为,更重要的是,Spring 框架多年来一直得到了很好的维护。我想我 15 年前就开始使用 Spring 框架了。实际上,随着时间的推移,这个框架一直如此稳定,而且发展得如此之好,真是令人惊叹,因为它已经不是 15 年前的那个框架了,但很多概念仍然存在。这让我们对 Spring 团队充满了信任,相信在未来,这里也会是一个非常好的地方。

通往 Spring Boot 3 的道路

将近一年前,Spring Boot 3 横空出世,这是一件大事,因为 Spring Boot 3 确实涉及到了 Java 生态系统,我认为,这是因为 Java 生态系统在两个不同方面有点卡住了。第一个原因是,如果你看一下 Java 的开源生态系统,它还停留在 Java 8 上,因为很多公司都停留在 Java 8 上,没有人愿意成为第一个打破这一局面的人。公司没有升级,因为无论如何,一切都在 Java 8 上运行得很好。现在,Spring 团队终于说,我们不再使用 Java 8 了,Java 17 是你们的新基准。现在,我们基本上迫使整个社区说,好吧,好吧,我们就用 Java 17,一切都可以重新开始了。现在,我们可以开始利用这些新的语言特性了。虽然这只是 Java 17 的基线,但我们也可以开始使用带有虚拟线程的 Java 21。这正是他们正在做的。第二部分是围绕 Javax 到 Jakarta 的混乱局面,这要归功于 Oracle。这只是一个简单的命名空间变更,但对于库生态系统来说却极为复杂,因为一个库既可以使用 Javax,也可以使用 Jakarta,这使得它要么兼容其中一个,要么不兼容另一个。这让他们现在非常痛苦,因为 Spring 团队现在说:”好吧,如果你们只做雅加达,现在整个生态系统都可以开始行动了,因为雅加达产生了如此大的影响。我们终于摆脱了他们的困境。使用这些新东西仍然是一个很大的变化,因此迁移到 Spring Boot 3 并没有实现,我们已经做了很多工具工作来实现这一点。其中最有趣的可能是我们开源了一个 Gradle 插件,它可以在工件解析时进行字节码转换。当你下载一个工件(JAR 文件)时,如果你使用的是 Spring Boot 3,从 Javax 到 Jakarta,它就会进行字节码转换,所以它基本上只是在运行中解决了整个命名空间的问题,而你不必更改你的库。这就解决了我们的问题。

DGS 框架

然后我谈了很多关于 DGS 的内容。DGS 并不是什么概念,GraphQL Federation 才是概念。DGS 框架只是我们用来在 Java 中构建 GraphQL 服务的一个框架。大约三四年前,当我们开始 GraphQL 和 GraphQL 联合的旅程时,还真没有什么好的 Java 框架成熟到可以在我们的规模上使用。当时有 GraphQL Java,这是一个较低级的 GraphQL 库。该库非常棒,我们正在其基础上进行构建。这对我们来说至关重要,但至少在我看来,它的级别太低,无法直接用于应用程序。v1 是 Spring Boot 的 GraphQL 框架,基本上提供了一个基于注解的编程模型,就像您在 Spring Boot 中使用的那样。我们需要为模式类型生成代码、支持联盟等。这正是 DGS 框架所能提供的。差不多三年前,我们决定开源 DGS 框架。它在 GitHub 上。这是一个非常庞大的社区。现在有很多公司都在使用它。这也正是我们在 Netflix 使用的版本,所以我们没有使用分叉或类似的东西。在过去的几年里,它的发展真的很不错。

如果您是 GraphQL 和 Spring 领域的专家,您可能会想,在 Spring Boot 3 中,Spring 团队也添加了 GraphQL 支持,他们称之为 Spring GraphQL。这对更大的社区来说并不理想,因为现在社区必须做出选择:好吧,我是选择 DGS 框架,还是选择 Spring GraphQL?两者看起来都很有趣,都很棒。两者都有有趣的功能集,但功能集又各不相同。我该选哪个?我可以向你推销 DGS 框架,说它如何更好、发展得更好、更快,以及所有这些现在可能是真的东西,因为我们已经存在了更久的时间。这不是重点,重点是你不应该做出选择。在过去的几个月里,我们一直在与 Spring 团队合作,以实现这两个框架之间的完全集成。这样,您就可以在同一个应用程序中结合 DGS 和 Spring GraphQL 编程模型及其功能,并愉快地共存。之所以能做到这一点,是因为我们都使用 GraphQL Java 作为底层库。这就是它们的结合方式。我们刚刚对框架进行了深度整合。我们还在完成这项工作,可能会在 2024 年初发布。至少这给了你这样的想法。如果你今天选择 DGS 框架,这其实并不重要。因为很快你就能将两者很好地结合起来。

问题与解答

与会者 2:你们还在使用 Zuul 吗?

Bakker:是的,我们还在用。Zuul 可以处理所有请求。Zuul 只是一个代理。基本上,它做了很多流量控制。它不是我们之前提到的 API 服务器。Zuul 位于 DGS 联合架构或旧架构的前端。

与会者 2:你谈到 Java 的升级价值有限。我认为这很有趣。我认为很多企业倾向于有这样一种心态:如果它没坏,就不要去修它,[听不清 00:44:02] 。你们是如何改变这种观念的,还是仅仅因为 Spring 升级,你们的员工就开始升级了?

巴克尔:不,实际上,主要是性能方面的优势。事实上,我们可以说,你的性能提高了 20%。这取决于服务的具体情况,以及这个数字的实际效果和实际意义。这个数字是真实的。事实上,你可以这么说,这让很多服务所有者对它更感兴趣,同时也让更高层的领导推动说,这会省钱,去做吧。这实际上是最有帮助的事情。Spring Boot 的升级是后来的事,也迫使我们去解决这个问题,但那是事后的事。

与会者 3:OpenJDK 有很多进步,那么从 8 到 17,是直接从 8 到 17 吗?

Bakker:我们的服务运行在 Java 11 上,因为计划是 8、11、17。我们在 Java 11 上运行了服务,但它从未真正起飞,因为没有足够的收益。我们主要是从 8 到 17。

与会者 3:正如他所说的那样,这也是取决于收集器的其中一件事,这对 “世界停顿 “和 “神户 “和 ZGC 的一些背景收集有一定的影响。这需要权衡利弊,但在减少内存集等方面做了很多改进。

与会者4:你提到 20% 是你所需要的,但你是如何确保有时间进行实际实验的?你是如何说服利益相关者说,我们要花一些时间对一些服务进行升级,然后我们再展示其价值的?

Bakker:我们拥有一个平台团队是有好处的。如果我看看自己的时间,我可以做任何我想做的事情。如果我认为在垃圾回收实验中会出现一些有趣的失败,实际上我主要做的并不是性能方面的工作,实际上还有其他人在这方面做得更好。这只是一个例子。如果有潜在的失败,如果你能抽出时间来试验一下,基本上就可以了,因为我们一两个人的时间就像水滴一样。

与会者 5:在相同数量的请求-响应中,你有没有发现虚拟线程与传统线程的内存占用有什么不同?第二个问题是关于 GraphQL 与传统 SOAP 的比较,因为 SOAP 早在我认为 REST 非常珍贵的年代就被 REST 取代了,如果你没有大量数据轻松通过,你的网络就非常重要。现在数据很便宜,所以它的缺点是模式在客户端和服务器之间传递。我看到 GraphQL 也有同样的问题,现在我们有了其他查询和模式,在客户端和服务器之间进行。您如何看待 REST、SOAP 和 GraphQL 的这一猜想?

Bakker:我认为 SOAP 在概念上有一些东西。例如,有一个模式,这是件好事。它非常难用,也非常复杂,做正确事情的开销实在太大了。然后,REST,至少大家使用 REST 的方式,走向了另一个极端,比如没有模式,什么都没有,什么都没有定义。你只要扔进一些数据,一切就都搞定了。我认为 GraphQL 处于中间位置。它不会给开发人员带来很多实现模式的开销。它非常简单。光是使用它,就比 SOAP 简单得多。你会得到一个模式,这就消除了在模式中使用 REST 的许多弊端。感觉它已经找到了应用程序接口的最佳位置。如果 10 年后我回到这里,我可能会说:”GraphQL,一个糟糕的想法。我们是怎么做到的?你知道这是怎么回事。现在,我感觉它就像一个甜蜜点。

这两者是有区别的,这就是为什么我们必须非常小心地结束虚拟线程,以取代传统的线程池。这取决于这些线程池是否非常耗费 CPU。内存占用似乎不是一个大因素。我们还没有看到内存占用有任何明显增加。还是那句话,一切都还很早,我们还在不断尝试。我们还没有完全弄明白。根据记忆,这似乎很简单明了。

与会者 6:那我想知道你们的 Kotlin 使用率是多少?

Bakker:相当低。有一段时间,我们有很多团队,包括我自己的团队,都对 Kotlin 感到非常兴奋。DGS 框架本身就是用 Kotlin 编写的,不过它主要面向 Java 应用程序。这是我的选择。我们的微服务也是用 Kotlin 编写的。使用 Kotlin 的唯一缺点是,我们在开发工具上投入了更多精力,因此基于 Gradle 的 IntelliJ 插件和自动化工具可以帮助实现 Spring 的版本升级,以及所有这些事情。如果你必须处理多种语言,这对平台团队来说就难上加难了。因为无论是 IntelliJ 插件还是 JetBrains 插件,如果你想同时使用 Java 和 Kotlin,就需要在 IntelliJ 中编写两次检查。这需要做更多的工作。如果每个人都能愉快地使用 Java,平台团队就会轻松很多。但这并不意味着 Kotlin 就不好。我们只看到了 Kotlin 的优点,而且它运行得相当不错。这是一门伟大的语言。

本文文字及图片出自 How Netflix Really Uses Java

余下全文(1/3)
分享这篇文章:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注