Java 微服务:实用指南
您可以使用本指南了解什么是 Java 微服务,以及如何构建它们。另请参阅:Java 微服务库概览及常见问题。
[编者注:本文近7000字,您可能不想在移动设备上阅读。请收藏并稍后再回来阅读。]
Java 微服务:基础知识
为了真正理解 Java 微服务,有必要从最基础的知识开始:臭名昭著的 Java 整体,它是什么以及它的优点和缺点是什么。
什么是 Java 整体?
想象一下,你在一家银行或金融科技初创公司工作。你为用户提供一款移动应用程序,用户可以使用该应用程序开设新的银行账户。
在 Java 代码中,这将导致控制器类看起来简化了,如下所示。
@Controller
class BankController {
@PostMapping("/users/register")
public void register(RegistrationForm form) {
validate(form);
riskCheck(form);
openBankAccount(form);
// etc..
}
}
您将需要:
-
验证注册表。
-
对用户的地址进行风险检查,以决定是否要给他一个银行账户。
-
开设银行账户
您的 BankController 类将与所有其他源代码一起打包到 bank.jar 或 bank.war 文件中以供部署:这是一个完善的、老旧的单体应用,包含您银行运行所需的所有代码。(粗略估计,您的 .jar/.war 文件初始大小将在 1-100MB 之间)。
然后,在您的服务器上,您只需运行您的 .jar 文件 - 这就是部署 Java 应用程序所需要做的全部工作。
Java 整体式架构存在什么问题?
从本质上来说,Java 单体式应用本身并没有错。只是项目经验表明,如果你:
-
让许多不同的程序员/团队/咨询公司……
-
在高压和不明确的要求下处理同一个整体……
-
几年来……
然后,您的小型 bank.jar 文件就会变成一个千兆字节的大型代码怪物,每个人都害怕部署它。
如何让 Java 整体变得更小?
这自然会引出一个问题:如何让单体应用变得更小。目前,你的 bank.jar 运行在一个 JVM 中,在一台服务器上运行一个进程。不多不少。
现在您可能会想到这样说:好吧,风险检查服务正在被我公司的其他部门使用,它实际上与我的 Mono(lithic) Bank域没有任何关系,
所以我们可以尝试将它从整体中分离出来并将其部署为自己的产品,或者更技术性地说,将其作为自己的 Java 进程运行。
什么是 Java 微服务?
从实际意义上讲,这意味着您无需调用 BankController 内部的 RiskCheck() 方法,而是将该方法/bean 及其所有辅助类移动到其自己的 Maven/Gradle 项目中,将其置于源代码控制之下,并独立于银行整体进行部署。
整个提取过程不会使您的新 RiskCheck 模块本身成为微服务,这是因为微服务的定义是开放的(这会导致团队和公司中的大量讨论)。
-
如果里面只有 5-7 个类,那么它算是微型吗?
-
100 个或 1000 个类还算微型吗?
-
这和班级数量有关系吗?
我们不会对此进行理论化,而是采取务实的态度并做两件事:
-
将所有可单独部署的服务称为微服务——独立于大小或域边界。
-
关注服务间通信这一重要主题,因为您的微服务需要相互通信的方式。
总结一下:之前你只有一个 JVM 进程,一个银行单体应用。现在你有一个银行单体应用的 JVM 进程和一个 RiskCheck 微服务,后者在自己的 JVM 进程中运行。你的单体应用现在必须调用该微服务进行风险检查。
你是怎样做到的?
Java 微服务之间如何通信?
您基本上有两种选择:同步通信或异步通信。
(HTTP)/REST - 同步通信
同步微服务通信通常通过返回 XML 或 JSON 的 HTTP 和 REST 类服务完成 - 尽管这绝不是必需的(例如,看看Google 的协议缓冲区)。
当您需要立即响应时,请使用 REST 通信,在我们的案例中就是这样做的,因为在开设账户之前必须进行风险检查:没有风险检查,就没有账户。
从工具角度来看,请查看#synchronous-rest-tools。
消息传递 - 异步通信
异步微服务通信通常通过使用JMS 实现和/或AMQP之类的协议进行消息传递。通常,这是因为在实践中,例如电子邮件/SMTP 驱动的集成数量不容小觑。
当您不需要立即响应时使用它,比如用户按下“立即购买”按钮并且您想要生成发票,这当然不必作为用户购买请求-响应周期的一部分发生。
从工具角度来看,请查看#asynchronous-rest-tools。
示例:在 Java 中调用 REST API
假设我们选择使用同步微服务通信,那么上面的Java 代码在底层看起来应该像这样。之所以说是底层,是因为对于微服务通信,你通常会创建客户端库,将实际的 HTTP 调用抽象出来。
@Controller
class BankController {
@Autowired
private HttpClient httpClient;
@PostMapping("/users/register")
public void register(RegistrationForm form) {
validate(form);
httpClient.send(riskRequest, responseHandler());
setupAccount(form);
// etc..
}
}
查看代码后,很明显,现在必须部署两个 Java(微)服务:银行服务和 RiskCheck 服务。最终会得到两个 JVM,两个进程。之前的图如下所示:
这就是开发 Java 微服务项目所需的全部内容:构建和部署较小的部分,而不是一个大的部分。
但这留下了一个问题:究竟如何拆分或设置这些微服务?这些小部件是什么?合适的尺寸是多少?
让我们来检验一下现实。
Java 微服务架构
在实践中,公司会尝试各种方法来设计或构建微服务项目。这取决于您是想将现有的单体应用转变为 Java 微服务项目,还是从零开始一个全新的 Greenfield 项目。
从单体到微服务
一个相当有机的想法是将微服务从现有的单体架构中拆分出来。需要注意的是,这里的“微”并不意味着提取出来的服务本身就是微的——它们本身仍然可能相当庞大。
让我们看一些理论。
理念:将整体式架构分解为微服务
遗留项目适合采用微服务方法。主要有三个原因:
-
它们通常很难维护/更改/扩展。
-
从开发人员、操作员到管理人员,每个人都希望让事情变得更简单。
-
您有(某种程度上)清晰的领域界限,这意味着:您知道您的软件应该做什么。
这意味着您可以查看您的 Java 银行整体并尝试沿域边界对其进行拆分- 这是一种明智的方法。
-
您可以得出结论,应该有一个“帐户管理”微服务,用于处理姓名、地址、电话号码等用户数据。
-
或者前面提到的“风险模块”,它可以检查用户风险级别,并且可以被公司中的许多其他项目甚至部门使用。
-
或者发票模块,通过 PDF 或实际邮件发送发票。
现实:让别人去做
虽然这种方法在纸面上和类似 UML 的图表上看起来确实不错,但它也有缺点。主要是,你需要非常强大的技术技能才能实现它。为什么?
因为理解将高度耦合的帐户管理模块从整体中提取出来是一件好事和(正确地)执行这件事之间存在巨大差异。
大多数企业项目都达到了这样的阶段,开发人员不敢将 7 年前的 Hibernate 版本升级到较新的版本,这只是一个库更新,但需要做大量工作才能确保不破坏任何东西。
这些开发人员现在是否应该深入研究数据库事务边界不明确的旧代码,并提取定义明确的微服务?这是可能的,但通常是一个真正的挑战,而且无法在白板上或架构会议上解决。
这已经是本文第一次引用Twitter 上 @simonbrown的言论了:
我会一直这么说……如果人们不能正确地构建单体应用,微服务也帮不上忙。
西蒙·布朗
Greenfield 项目微服务架构
在开发全新的 Java 项目时,情况会有所不同。现在,上面提到的三点看起来有些不同:
-
您是从一张白纸开始的,因此没有什么旧包袱需要保留。
-
开发人员希望将来的事情能够保持简单。
-
问题:您对领域边界的了解非常模糊:您不知道您的软件实际上应该做什么(提示:敏捷;))
这导致公司尝试以各种方式解决绿地 Java 微服务项目。
技术微服务架构
第一种方法对开发人员来说最明显,尽管强烈建议不要采用。感谢Hadi Hariri在 IntelliJ 中提出的“提取微服务”重构。
尽管下面的例子过于简单,但不幸的是,实际项目中的实际实现并不太相差。
微服务之前
@Service
class UserService {
public void register(User user) {
String email = user.getEmail();
String username = email.substring(0, email.indexOf("@"));
// ...
}
}
使用子字符串 Java 微服务
@Service
class UserService {
@Autowired
private HttpClient client;
public void register(User user) {
String email = user.getEmail();
// now calling the substring microservice via http
String username = httpClient.send(substringRequest(email), responseHandler());
// ...
}
}
所以,你实际上是把 Java 方法调用包装成了 HTTP 调用,虽然这样做没有明显的理由。然而,原因之一是:缺乏经验,并且试图强行采用 Java 微服务方法。
建议:不要这么做。
面向工作流的微服务架构
下一个常见方法是,在工作流程之后对 Java 微服务进行模块化。
现实生活中的例子:在德国,当您去看(公立)医生时,他需要在他的健康软件 CRM 中记录您的预约。
为了从保险公司获得赔付,他会通过 XML 将您的治疗数据以及他治疗的所有其他患者的治疗数据发送给中介。
中介将查看该 XML 文件并(简化):
-
尝试验证文件是否为正确的 XML
-
尝试验证其合理性:一个一岁的孩子一天要到妇科医生那里洗三次牙,这合理吗?
-
使用其他一些官僚数据增强 XML
-
将 XML 转发给保险公司以触发付款
-
并将整个过程反馈给医生,包括“成功”消息或“请重新发送该数据输入 - 一旦有意义”
如果您现在尝试使用此工作流程进行建模,那么您最终将获得至少六个 Java 微服务。
注意:在此示例中,微服务之间的通信无关紧要,但可以使用 RabbitMQ 等消息代理异步完成,因为医生无论如何都不会得到即时反馈。
同样,这在纸面上看起来不错,但立即引发了几个问题:
-
您是否觉得需要部署六个应用程序来处理 1 个 xml 文件?
-
这些微服务真的彼此独立吗?它们可以独立部署吗?它们有不同的版本和 API 方案吗?
-
如果验证微服务宕机了,合理性微服务会做什么?系统还能运行吗?
-
这些微服务现在是否共享同一个数据库(它们肯定需要数据库表中的一些公共数据)或者您是否要采取更大的措施为它们提供各自的数据库?
-
还有大量其他基础设施/运营问题。
有趣的是,对于一些架构师来说,上面的图表读起来更简单,因为现在每个服务都有其明确的、定义清晰的用途。之前,它看起来像一个可怕的巨石:
虽然争论的焦点是这些图表的简单性,但现在你肯定需要解决这些额外的操作挑战:
-
不仅需要部署一个应用程序,至少需要部署六个。
-
甚至可能是数据库,这取决于你想把它带到多远。
-
必须确保每个系统都在线、健康且正常运行。
-
必须确保微服务之间的调用确实具有弹性(参见#resilience)
-
以及此设置所暗示的所有其他内容 - 从本地开发设置到集成测试
推荐:
除非:
-
你是 Netflix(你不是)……
-
你有超强的操作技能:你打开你的开发IDE,它会触发一个混乱的猴子,它会删除你的生产数据库,但它会在5秒内轻松自动恢复
-
或者你像@monzo一样尝试 1500 个微服务,仅仅因为你可以。
→ 不要这样做。
不过,不要太夸张。
尝试根据领域边界来建模微服务是一种非常明智的方法。但是,领域边界(例如用户管理与发票)并不意味着将单个工作流拆分成最小的独立部分(接收 XML、验证 XML、转发 XML)。
因此,无论何时启动一个新的 Java 微服务项目,如果领域界限仍然非常模糊,请尝试将微服务的规模保持在较低水平。您以后可以随时添加更多模块。
并确保您的团队/公司/部门拥有非常强大的 DevOps 技能,以支持您的新基础设施。
多语言或面向团队的微服务架构
还有第三种几乎是自由主义的开发微服务的方法:让您的团队甚至个人能够使用他们想要的任意数量的语言或微服务来实现用户故事(营销术语:多语言编程)。
因此,上面的 XML 验证服务可以用 Java 编写,而 Plausibility 微服务可以用 Haskell 编写(使其在数学上听起来合理),保险转发微服务应该用 Erlang 编写(因为它确实需要扩展;))。
从开发人员的角度来看可能很有趣的事情(在孤立的环境中使用完美的语言开发完美的系统)基本上不是组织想要的:同质化和标准化。
这意味着一套相对标准化的语言、库和工具,以便其他开发人员可以在您进入更广阔的领域后继续维护您的 Haskell 微服务。
有趣的是:历史上标准化走得太远了。财富500强企业的开发人员有时甚至不被允许使用Spring,因为它“不在公司的技术蓝图中”。但全面采用多语言几乎是一回事,只是同一枚硬币的两面。
建议:如果你打算使用多语言,请尝试在同一个编程语言生态系统中减少多样性。例如:Kotlin 和 Java(基于 JVM,彼此之间 100% 兼容),而不是 Haskell 和 Java。
部署和测试 Java 微服务
快速回顾一下本文开头提到的基础知识会有所帮助。任何服务器端 Java 程序,以及任何微服务,都只是一个 .jar/.war 文件。
Java 生态系统(或者更确切地说是 JVM)有一件很棒的事情:您只需编写一次 Java 代码,就可以在任何您想要的操作系统上运行它,前提是您没有使用比目标 JVM 版本更新的 Java 版本来编译您的代码。
理解这一点很重要,尤其是在涉及 Docker、Kubernetes 或(颤抖)云等主题时。为什么?让我们来看看不同的部署场景:
最小 Java 微服务部署示例
继续以银行为例,我们最终得到了 monobank.jar 文件(整体)和刚刚提取的 RiskEngine.jar(第一个微服务)。
我们还假设这两个应用程序就像世界上任何其他应用程序一样,需要一个 .properties 文件,它只是数据库 URL 和凭据。
因此,最低限度的部署可能只包含两个目录,大致如下所示:
-r-r------ 1 ubuntu ubuntu 2476 Nov 26 09:41 application.properties
-r-x------ 1 ubuntu ubuntu 94806861 Nov 26 09:45 monobank-384.jar
ubuntu@somemachine:/var/www/www.monobank.com/java$ java -jar monobank-384.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
...
-r-r------ 1 ubuntu ubuntu 2476 Nov 26 09:41 application.properties
-r-x------ 1 ubuntu ubuntu 94806861 Nov 26 09:45 risk-engine-1.jar
ubuntu@someothermachine:/var/www/risk.monobank.com/java$ java -jar risk-engine-1.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
...
这就留下了一个问题:如何将 .properties 和 .jar 文件上传到服务器?
不幸的是,这个问题有各种各样诱人的答案。
如何使用 Build Tools、SSH 和 Ansible 进行 Java 微服务部署
对于 Java 微服务部署,一个虽然枯燥但却完美的答案,就是过去 20 年里,管理员在公司里部署 Java 服务器端程序的方式。其中混合了以下几种方式:
-
您最喜欢的构建工具(Maven、Gradle)
-
使用 SSH/SCP 将 .jar 文件复制到服务器
-
用于管理部署脚本和服务器的 Bash 脚本
-
或者甚至更好:一些Ansible脚本。
如果您不执着于创建一个由自动负载平衡服务器组成的呼吸云、混乱的猴子轰炸您的机器,或者看到 ZooKeeper 的领导者选举发挥作用的温暖和模糊的感觉,那么这个设置将带您走得很远。
老派、无聊,但有效。
如何使用 Docker 进行 Java 微服务部署
回到那些诱人的选择。几年前,Docker或容器化话题开始流行。
如果您以前没有使用过它,那么它对于最终用户或开发人员来说就是:
-
容器(简化版)类似于传统的虚拟机,但更轻量级。请查看Stackoverflow上的这个答案,了解轻量级的含义。
-
容器保证了它的可移植性,可以在任何地方运行。这听起来是不是很熟悉?
有趣的是,考虑到JVM 的可移植性和向后兼容性,这听起来似乎没什么好处。你只需在任何服务器、Raspberry Pi(甚至手机)上下载一个 JVM.zip 文件,解压后运行任何你想要的 .jar 文件即可。
对于 PHP 或 Python 等语言来说,情况看起来有点不同,因为这些语言的版本不兼容或部署设置在历史上更为复杂。
或者,如果您的 Java 应用程序依赖于大量其他已安装的服务(具有正确的版本号):请考虑像 Postgres 这样的数据库或像 Redis 这样的键值存储。
因此,Docker 对于 Java 微服务(或者更确切地说 Java 应用程序)的主要好处在于:
-
使用Testcontainers等工具设置同质化的测试或集成环境。
-
让复杂的可部署组件安装起来“更简单”。以Discourse论坛软件为例,只需一个 Docker 镜像即可安装,其中包含您所需的一切:从用 Ruby 编写的 Discourse 软件,到 Postgres 数据库,再到 Redis 以及其他所有功能。
如果您的可部署文件看起来很相似,或者您想在开发机器上运行一个不错的小型 Oracle 数据库,请尝试一下 Docker。
因此,总而言之,您现在将不再只是简单地 scp 一个 .jar 文件,而是:
-
将你的 jar 文件打包成 Docker 镜像
-
将该 Docker 镜像传输到私有 Docker 注册表
-
在目标平台上拉取并运行该图像
-
或者直接将 Docker 镜像 scp 到你的生产系统并运行它
如何使用 Docker Swarm 或 Kubernetes 进行 Java 微服务部署
假设您正在尝试使用 Docker。每次部署 Java 微服务时,您都会创建一个 Docker 镜像来打包您的 .jar 文件。您有几个这样的 Java 微服务,并且希望将它们部署到几台机器上:一个集群。
现在出现了一个问题:如何管理该集群,这意味着运行 Docker 容器、进行健康检查、推出更新、扩展(brrrr)?
该问题的两个可能答案是Docker Swarm和Kubernetes。
本指南无法详细介绍这两个选项,但实际情况是:这两个选项最终都依赖于您编写YAML文件(参见#yaml-tales)来管理集群。如果您想了解实际使用中会引发什么样的感受,可以在 Twitter 上快速搜索一下。
因此,Java 微服务的部署过程现在看起来有点像这样:
-
设置和管理 Docker Swarm/Kubernetes
-
上述 Docker 步骤中的所有内容
-
编写并执行 YAML,直到眼睛流血,一切正常
如何测试 Java 微服务
假设你已经解决了在生产环境中部署微服务的问题,那么如何在开发过程中对多个微服务进行集成测试呢?如何检查整个工作流程是否正常运作,而不仅仅是单个部分?
在实践中,你会发现三种不同的方法:
-
通过一些额外的工作(如果您使用 Spring Boot 之类的框架),您可以将所有微服务包装到一个启动器类中,并使用一个 Wrapper.java 类启动所有微服务 - 取决于您的机器上是否有足够的内存来运行所有微服务。
-
您可以尝试在本地复制 Docker Swarm 或 Kubernetes 设置。
-
干脆别再在本地做集成测试了。直接用专用的开发/测试环境就行了。很多团队实际上就是这么做的,他们受不了本地微服务搭建的痛苦。
此外,除了 Java 微服务之外,您可能还需要一个正在运行的消息代理(例如:ActiveMQ或RabbitMQ)或者电子邮件服务器或 Java 微服务相互通信所需的任何其他消息传递组件。
这导致 DevOps 方面的复杂性被低估了相当一部分。不妨看看微服务测试库,它们可以缓解一些问题。
无论如何,这种复杂性会导致我们遇到常见的微服务问题:
常见的 Java 微服务问题
让我们看看 Java 特定的微服务问题,从更抽象的东西(如弹性)到特定的库。
如何使 Java 微服务具有弹性?
总而言之,在构建微服务时,您实际上是在用同步 HTTP 调用或异步消息传递来替换 JVM 方法调用。
虽然方法调用的执行基本上是有保证的(除非 JVM 突然退出),但网络调用默认是不可靠的。
它可能有效,也可能由于各种原因而无效:从网络故障或拥塞,到实施新的防火墙规则,再到消息代理爆炸。
为了了解其含义,让我们看一个示例性的BillingService示例。
HTTP/REST 弹性模式
假设客户可以在您公司的网站上购买电子书。为此,您刚刚实现了一个计费微服务,您的网店可以调用该微服务来生成实际的 PDF 发票。
目前,我们将通过 HTTP 同步调用该服务。(异步调用该服务更合理,因为从用户的角度来看,PDF 生成不必是即时的。但我们想在下一节中重复使用这个示例,看看两者的区别。)
@Service
class BillingService {
@Autowired
private HttpClient client;
public void bill(User user, Plan plan) {
Invoice invoice = createInvoice(user, plan);
httpClient.send(invoiceRequest(user.getEmail(), invoice), responseHandler());
// ...
}
}
思考一下这个 HTTP 调用可能产生什么样的结果。概括起来,你会得到三种可能的结果:
-
确定:呼叫已接通并且发票已成功创建。
-
延迟:呼叫已接通,但耗时异常长。
-
错误:呼叫未接通,可能是因为您发送了不兼容的请求,或者系统出现故障。
任何程序都需要处理错误,而不仅仅是处理正常情况。微服务也是如此,尽管您必须格外小心,
确保所有已部署的 API 版本在开始单个微服务的部署和发布时都兼容。
如果你想全面使用 Chaos-Monkey,你还必须忍受这样的可能性:你的服务器在处理请求时可能会被摧毁,你可能希望将请求重新路由到另一个正常工作的实例。
一个有趣的“警告”案例是延迟情况。也许响应的微服务硬盘已满,响应时间不是 50 毫秒,而是 10 秒。当你遇到
一定负载时,这种情况会变得更加有趣,以至于 BillingService 的无响应会开始在整个系统中蔓延。想象一下,一个缓慢的厨房开始慢慢地阻塞餐厅里的所有服务员。
本节显然无法深入介绍微服务弹性主题,但可以提醒开发人员,这是一个需要实际解决的问题,在第一次发布之前不要忽略它(根据经验,这种情况发生的频率比应该的要高)。
Netflix 的 Hystrix是一个流行的库,可以帮助你思考延迟和容错问题。你可以参考它的文档来深入了解这个主题。
消息传递弹性模式
让我们仔细看看异步通信。如果我们使用Spring和RabbitMQ进行消息传递,我们的 BillingService 代码现在可能看起来像这样。
要创建发票,我们现在需要向 RabbitMQ 消息代理发送一条消息,该代理中有一些工作进程正在等待新消息。这些工作进程会创建 PDF 格式的发票并将其发送给相应的用户。
@Service
class BillingService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void bill(User user, Plan plan) {
Invoice invoice = createInvoice(user, plan);
// converts the invoice to,for example, json and uses it as the message's body
rabbitTemplate.convertAndSend(exchange, routingkey, invoice);
// ...
}
}
现在,潜在的错误情况看起来有些不同,因为你不再像同步 HTTP 通信那样立即收到 OK 或 ERROR 响应。相反,你大致会遇到以下三种错误情况:
-
我的消息是否被工作人员送达并消费了?还是丢失了?(用户没有收到发票)。
-
我的消息只发送了一次吗?还是发送了多次,但只处理了一次?(用户会收到多张发票)。
-
配置:从“我是否使用了正确的路由键/交换名称”到“我的消息代理是否设置并正确维护,或者它的队列是否溢出?”(用户没有收到发票)。
再次强调,本指南不打算详细介绍每一个异步微服务弹性模式。更重要的是,它旨在为大家提供正确的方向,尤其考虑到它还取决于你所使用的实际消息传递技术。示例:
-
如果您正在使用 JMS 实现(例如ActiveMQ ),您可能希望用速度来换取两阶段(XA)提交的保证。
-
如果您正在使用 RabbitMQ,您至少要确保已经阅读并理解本指南,然后认真思考确认、确认和消息可靠性。
-
并且还找到具有设置 Active 或 RabbitMQ 服务器等并正确配置它们经验的人,特别是当与集群和 Docker 结合使用时(网络分割,有人知道吗?;))
哪个 Java 微服务框架最好?
一方面,您已经有了像Spring Boot这样成熟且非常流行的选择,它让您可以非常轻松地构建带有嵌入式 Web 服务器(如 Tomcat 或 Jetty)的 .jar 文件,并且您可以在任何地方立即运行这些文件。
不过最近,受到反应式编程、Kubernetes或GraalVM等并行开发的启发,出现了一些专用的微服务框架。
举几个例子:Quarkus、Micronaut、Vert.x、Helidon。
最后,你必须做出自己的选择,但本文可以提供一些可能非常规的指导:
除了 Spring Boot 之外,所有微服务框架通常都将自己标榜为速度极快、启动时间极短、内存占用低、能够无限扩展,并且与 Spring Boot 庞然大物或其他框架相比都有令人印象深刻的图表。
这显然触动了那些维护遗留项目的开发人员的神经,因为这些项目有时需要几分钟才能启动,而云原生开发人员则希望在 50 毫秒内启动或停止尽可能多的微容器。
然而,问题在于(人为的)裸机启动时间和重新部署时间几乎不会对项目的整体成功产生影响,远不如强大的框架生态系统、强大的文档、社区和强大的开发人员技能产生影响。
你必须这样看待它。
如果到现在为止:
-
您让 ORM 肆意运行并为简单的工作流生成数百个查询。
-
您需要无限的 GB 来运行中等复杂的整体系统。
-
您添加了如此多的代码和复杂性(忽略像 Hibernate 这样可能启动缓慢的程序)您的应用程序现在需要几分钟才能启动。
然后,在此基础上添加额外的微服务挑战(例如:弹性、网络、消息传递、DevOps、基础设施)将比启动一个空的 Hello World 对你的项目产生更大的影响。对于开发过程中的热部署,你最终可能需要研究JRebel或DCEVM之类的解决方案。
回到西蒙·布朗的名言:如果人们无法构建(快速高效的)整体,那么他们将很难构建(快速高效的)微服务——无论采用何种框架。
因此,请明智地选择您的框架。
哪些库最适合同步 Java REST 调用?
接下来介绍调用 HTTP REST API 的更实际方面。从底层技术角度来看,您最终可能会使用以下 HTTP 客户端库之一:
Java 自己的 HttpClient(自 Java 11 起)、Apache 的 HttpClient或OkHttp。
请注意,我在这里说的是“可能”,因为还有无数其他方法,从旧的JAX-RS 客户端到现代的WebSocket客户端。
无论如何,现在的趋势是生成 HTTP 客户端,而不是自己动手处理 HTTP 调用。为此,您可以先查看OpenFeign项目及其文档,作为进一步阅读的起点。
哪些代理最适合异步 Java 消息传递?
从异步消息传递开始,您最终可能会选择ActiveMQ (Classic 或 Artemis)、RabbitMQ或Kafka。同样,这只是一个流行的选择。
不过,这里有几点随机要点:
-
ActiveMQ 和 RabbitMQ 都是传统的、功能齐全的消息代理。这意味着代理相当智能,而消息接收者则比较笨拙。
-
ActiveMQ 历来具有易于嵌入(用于测试)的优势,但可以通过 RabbitMQ/Docker/TestContainer 设置来缓解这一问题
-
Kafka并非传统意义上的 Broker。它恰恰相反,本质上是一个相对“愚蠢”的消息存储(类似日志文件),需要更智能的消费者来处理。
为了更好地了解何时使用 RabbitMQ(或一般传统消息代理)或 Kafka,请以Pivotal 的匹配博客文章作为起点。
不过,一般来说,在选择代理时,尽量忽略任何人为的性能因素。曾经有一段时间,团队和在线社区就 RabbitMQ 有多快、ActiveMQ 有多慢争论不休。
现在,你又在争论 RabbitMQ 速度慢,每秒只稳定地发送 20-30K 条消息。而 Kafka 每秒发送 100K 条消息。首先,这种比较很容易让人忽略一个事实:你实际上是在比较苹果和橘子。
更重要的是:对于阿里巴巴集团来说,这两个吞吐量数字可能都偏低或中等,但作者您从未在现实世界中见过这种规模的项目(每分钟数百万条消息)。这种规模的项目确实存在,但对于其他 99% 的常规 Java 业务项目来说,这些数字根本不算什么。
因此,请忽略炒作并做出明智的选择。
我可以使用哪些库进行微服务测试?
根据您的堆栈,您最终可能会使用Spring 特定工具(Spring 生态系统),或者Arquillian之类的工具(JavaEE 生态系统)。
您将需要了解 Docker 和非常好的Testcontainers库,它可以帮助您轻松快速地为本地开发或集成测试设置 Oracle 数据库。
要模拟整个 HTTP 服务器,请查看Wiremock。要测试异步消息传递,请尝试嵌入(ActiveMQ)或 Dockering(RabbitMQ),然后使用Awaitility DSL编写测试。
除此之外,所有常见的适用对象,如Junit、TestNG 、 AssertJ和Mockito。
请注意,这绝不是一份完整的清单,如果您缺少自己喜欢的工具,请将其发布在评论部分,我将在本指南的下一次修订中选择它。
如何为所有 Java 微服务启用日志记录?
使用微服务进行日志记录是一个有趣且相当复杂的话题。现在,您不再需要使用 less 或 grep 命令来查看单个日志文件,而是需要查看多个日志文件,并且需要将它们合并起来查看。
这篇文章是了解整个日志生态系统的一个很好的起点。请务必阅读,尤其是微服务方面的集中式日志记录部分。
在实践中,你会发现各种方法:
-
系统管理员编写一些脚本,收集来自各个服务器的日志文件并将其合并为一个日志文件,然后将其放到 FTP 服务器上供您下载。
-
在并行 SSH 会话中运行 cat/grep/unig/sort 组合。你可以告诉你的经理:这就是 Amazon AWS 内部的做法。
我的微服务如何相互找到?
到目前为止,我们假设我们的微服务彼此了解,知道它们对应的 IPS。这更像是一种静态设置。因此,我们的银行单体应用 [ip=192.168.200.1] 知道它需要与风险服务器 [ip=192.168.200.2] 通信,而该服务器已硬编码在属性文件中。
但是,您可以选择让事情变得更加动态:
-
您无法再使用微服务部署 application.properties 文件,而是使用所有微服务从中提取其配置的云配置服务器。
-
因为您的服务实例可能会动态地改变它们的位置(想象一下 Amazon EC2 实例获取动态 IP 并且您在云中进行弹性自动扩展),您很快就会看到一个服务注册表,它知道您的服务位于何处以及 IP 是什么,并且可以相应地进行路由。
-
现在,由于一切都是动态的,你会面临新的问题,比如自动选举领导者:谁是负责某些任务的主人,例如,避免重复处理这些任务?领导者失败后,谁来接替他?由谁来接替?
总的来说,这就是所谓的微服务编排,它本身又是另一个大话题。
像Eureka或Zookeeper这样的库试图“解决”这些问题,比如让客户端或路由器知道哪些服务在哪些地方可用。另一方面,它们也带来了很多额外的复杂性。
只要询问曾经运行过 ZooKeeper 设置的任何人即可。
如何用Java微服务进行授权和认证?
又是一个宏大的话题,值得单独写一篇文章。同样,选项范围很广,从使用自编码安全框架的硬编码 HTTPS 基本身份验证,到使用自己的授权服务器运行 Oauth2 设置。
我如何确保我的所有环境看起来都一样?
非微服务部署适用的原则也适用于微服务部署。您将尝试 Docker/Testcontainers 以及脚本/Ansible 的组合。
尝试并保持简单。
不是一个问题:Yaml 缩进故事
抛开具体的库问题,我们来快速了解一下 Yaml。它是“将配置写成代码”的默认文件格式。从像 Ansible 这样简单的工具到强大的 Kubernetes,它都适用。
要亲自体验 YAML 缩进的痛苦,请尝试编写一个简单的 Ansible 文件,看看在各种 IDE 支持级别下,需要多久重新编辑一次文件才能使缩进正常工作。然后回来完成本指南。
Yaml:
- is:
- so
- great
那么分布式事务呢?性能测试呢?还有其他主题吗?
很遗憾,这些主题未包含在指南的本次修订中。敬请期待更多内容。
概念性微服务挑战
除了特定的 Java 微服务问题之外,任何微服务项目都会遇到一些问题。这些问题更多来自组织、团队或管理的角度。
前端/后端不匹配
很多微服务项目中都会出现这种情况,我称之为前后端微服务不匹配。这是什么意思呢?
在传统的单体应用中,前端开发人员只有一个特定的数据来源。而在微服务项目中,前端开发人员突然有了多个数据来源。
想象一下,你正在构建一个 Java-IoT 微服务项目。假设你正在监控一些机器,比如遍布欧洲的工业烤箱。这些烤箱会定期向你发送温度等状态更新。
现在,您迟早会希望能够在管理界面中搜索烤箱,或许可以借助“搜索烤箱”微服务。根据您的后端同事对领域驱动设计或微服务规则的严格程度,“搜索烤箱”微服务可能只会返回烤箱的 ID,而不会返回其他数据,例如烤箱的类型、型号或位置。
为此,前端开发人员可能必须使用从第一个微服务获得的 ID 对“获取烤箱详细信息”微服务进行一次或 n 次额外的调用(取决于您的分页实现)。
虽然这只是一个简单的例子(但取自一个真实的项目(!)),但它说明了以下问题:
现实生活中的超市之所以受到广泛欢迎是有原因的。因为你不用跑10个不同的地方去买蔬菜、柠檬水、冷冻披萨和卫生纸。你只需要去一个地方。
它更简单、更快捷。对于前端开发人员和微服务来说都是一样的。
管理层期望
这个问题是个人开发人员、编程杂志或云计算公司推广微服务的一个不幸的副作用:
管理层觉得现在可以把无限数量的开发人员投入到(总体)项目中,因为开发人员现在可以完全独立地工作,每个人都在自己的微服务上工作。只需要在最后阶段(即上线前不久)进行一些微小的集成工作。
在接下来的段落中,让我们看看为什么这种心态会成为一个问题。
更小的碎片并不意味着更好的碎片
一个相当明显的问题是,20 个较小的组件(例如微服务)并不意味着20 个更好的组件。纯粹从技术质量的角度来看,这可能意味着您的单个服务仍然需要执行 400 个 Hibernate 查询,才能跨越层层难以维护的代码,从数据库中选择用户。
回到西蒙·布朗的名言,如果人们不能正确地构建整体式架构,那么他们将很难构建适当的微服务。
尤其是在许多微服务项目中,弹性和上线后发生的一切都是事后才想到的,看到微服务实时运行会让人感到有些害怕。
但这有一个简单的原因:因为 Java 开发人员通常对弹性、网络和其他相关主题不感兴趣,也没有接受过适当的培训。
更小的部件带来更多技术性部件
此外,不幸的是,用户故事会变得越来越技术化(因此也越来越愚蠢),它们对用户来说越来越微观和抽象。
想象一下,你的微服务团队被要求编写一个技术性的、针对数据库的登录微服务,大致如下:
@Controller
class LoginController {
// ...
@PostMapping("/login")
public boolean login(String username, String password) {
User user = userDao.findByUserName(username);
if (user == null) {
// handle non existing user case
return false;
}
if (!user.getPassword().equals(hashed(password))) {
// handle wrong password case
return false;
}
// 'Yay, Logged in!';
// set some cookies, do whatever you want
return true;
}
}
现在您的团队可能会决定(甚至可能说服商人):这太简单和无聊了,我们不需要登录服务,而是编写一个真正强大的 UserStateChanged 微服务 - 而不需要任何实际的、有形的业务需求。
由于 Java 目前已经过时了,我们用 Erlang 来编写 UserStateChanged 微服务。我们尝试在某些地方使用红黑树,因为Steve Yegge写道,要想申请 Google,你需要彻底了解红黑树。
从集成、维护和整个项目的角度来看,这与在同一个整体中编写多层意大利面条式代码一样糟糕。
这是捏造的、夸张的例子吗?是的。
不幸的是,现实生活中这种情况也并不少见。
碎片越小,理解就越少
如果你作为开发人员只负责处理独立的微服务[95:login-101:updateUserProfile],那么你就需要了解完整的系统、它的流程和工作流程。
它与上一段相融合,但取决于您的组织、信任和沟通水平,如果整个微服务链的某个随机部分发生故障,这可能会导致很多耸肩和指责 - 没有人再承担全部责任。
这不仅暗示着恶意,而且问题在于,实际上很难理解大量孤立的部分及其在全局中的位置。
通信与维护
这与上一个问题:沟通与维护,交织在一起。这显然很大程度上取决于公司规模,一般规律是:规模越大,问题越多。
-
谁在负责 47 号微服务?
-
他们是不是刚刚部署了一个不兼容的新微服务版本?相关记录在哪里?
-
我需要与谁讨论新功能请求?
-
Max 离开公司后谁来维护该 Erlang 微服务?
-
我们所有的微服务团队不仅使用不同的编程语言,而且分布在不同的时区!我们该如何协调呢?
这里的首要主题是,与 DevOps 技能类似,在规模更大、甚至可能是国际化的公司中全面采用微服务方法,会带来大量额外的沟通挑战。作为一家公司,你需要为此做好准备。
鳍
读完这篇文章,你可能会觉得作者完全不推荐使用微服务。其实这并不完全正确——我主要想强调一下在微服务热潮中被人们忽视的一些要点。
微服务处于钟摆状态
全面采用 Java 微服务就像钟摆的一端,另一端则像是一个整体式架构,包含数百个经典的 Maven 模块。你必须找到合适的平衡点。
特别是在绿地项目中,没有什么可以阻止您采取更保守、更单一的方法并构建更少、定义更好的 Maven 模块,而不是立即从二十个云就绪的微服务开始。
微服务会产生大量额外的复杂性
请记住,您拥有的微服务越多,您拥有的真正强大的 DevOps 人才越少(不,执行一些 Ansible 脚本或在 Heroku 上部署不算),您在以后的生产中遇到的问题就越多。
读完本指南的#问题与疑问部分已经让人精疲力尽。然后想想如何为所有这些基础设施挑战实施解决方案。你会突然意识到,这一切都与业务编程(也就是你拿钱来做的事情)无关,而是与对更多技术的执着有关。
Siva 在他的博客上完美地总结了这一点:
我无法形容当团队70%的时间都花在应对这种现代化的基础设施设置,而30%的时间都花在实际的业务逻辑上时,那种感觉有多么糟糕。Siva
Prasad Reddy
您应该创建 Java 微服务吗?
为了回答这个问题,我想用一个非常俏皮、类似谷歌面试题的预告来结束这篇文章。如果你根据经验知道这个问题的答案,即使它看起来与微服务无关,那么你可能已经准备好采用微服务方法了。
设想
想象一下,你有一个 Java 单体应用在最小的Hetzner专用机器上单独运行。你的数据库服务器也一样,它也在一台类似的 Hetzner 机器上运行。
我们还假设您的 Java 整体可以处理用户注册等工作流,并且您不会在每个工作流中产生数百个数据库查询,而只会产生少量(<10)。
问题
您的 Java 整体(连接池)应该向数据库服务器开放多少个数据库连接?
为什么?你认为你的单体应用大概能扩展到多少个并发活跃用户?
回答
请在评论区发表你对这些问题的回答。我期待所有答案。
现在,自己做决定吧
如果您还在这里和我在一起:感谢您的阅读!
还有更多
本文最初发表于https://www.marcobehler.com/guides/java-microservices-a-practical-guide,是现代 Java 编程系列指南的一部分。如需查找更多指南,请访问网站或订阅新闻简报以获取最新发布指南的通知:https://bit.ly/2K0Ao4F。
文章来源:https://dev.to/marcobehler/java-microservices-a-practical-guide-29kn