原文地址:https://www.marcobehler.com/guides/java-microservices-a-practical-guide
你可以用本指南来了解 Java 微服务是什么,如何架构以及创建它们。另外,看一下Java微服务库和常见问题。
一、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、一个进程中运行。仅此而已。
现在你可以提出这样的想法:嗯,风险检查服务正在被公司中的其他部门所使用,并且实际上与我的单体银行领域没有任何关系,所以我们可以尝试将其从单体应用中删除,并把它独立部署,或者更严格地说,让它在自己的 Java 进程中运行。
什么是Java微服务?
实际上,这意味着你不必在 BankController
内调用 riskCheck()
方法,而是将该方法/bean以及所有其辅助类都移到它自己的 Maven/Gradle 项目中,将其置于源代码控制下,并独立于你的银行单体应用部署。
整个提取过程不会让你的新 RiskCheck
模块本身成为一个微服务,这是因为微服务的定义是开放的(这会在团队和公司中引起大量的讨论)。
- 如果里面只有5-7个类,它是微的吗?
- 100或者1000个类还是微的吗?
- 这跟类的数目有关系吗?
我们与其将其理论化,还不如保持实用,做两件事:
- 称所有可单独部署的服务为微服务 — 与大小或者领域边界无关。
- 关注服务间通信的重要主题,因为你的微服务需要彼此通信的方法。
因此,总结一下:之前你有一个JVM进程,一个Banking单体应用。现在,你有一个 Banking 单体应用 JVM 进程,和一个运行在它自己的JVM进程中的 RiskCheck 微服务。并且,现在你的单体应用必须调用该微服务进行风险检查。
你该怎么做呢?
Java微服务之间如何通信?
你基本上有两种选择:同步通讯,或异步通讯。
(HTTP)/REST - 同步通讯
同步微服务通信通常通过返回XML或JSON的HTTP,已经类似REST的服务完成 — 尽管这并不是必需的(比如,请查看Google的Protocal Buffers)。
当需要立即响应时,请使用REST通讯。我们在我们的案例中会这样做,因为在开设帐户之前必须进行风险检查:无风险检查,无帐户。
至于工具,请看后面《哪个库最适合同步Java REST调用》一节。
消息传递 - 异步通信
异步微服务通信通常通过使用一种JMS实现或者一种AMQP这样的协议进行消息传递。通常,在实践中,不要低估例如电子邮件/SMTP驱动的集成的数量。
当不需要立即响应时(例如,用户按下“立即购买”按钮,以及想要生成发票)就可以使用它,这确实不必在用户的购买请求-响应周期中进行。
至于工具,请看看后面《哪些代理最适合异步Java消息传递》一节。
示例:在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(微)服务:Bank和RiskCheck服务。最终会有两个JVM,两个进程。之前的图形会看起来像这样的:
这就是开发Java微服务项目所需的全部:构建和部署较小的块(.jar或.war文件),而不是一个较大的块。
但这留下了一个问题:你如何精确削减或设置这些微服务?这些较小的块是什么?合适的大小是多少?
下面我们做一个现实的检验。
二、Java微服务架构
实际上,公司尝试设计或架构微服务项目的方式多种多样。这取决于你是想将现有的单体应用变成微服务项目,还是要开始一个全新的项目。
从单体到微服务
一个相当有说服力的想法是将微服务从现有的单体应用中分离出来。请注意,此处的“微”实际上并不意味着抽取的服务本身确实会是微的 — 它们本身可能仍然很大。
下面我们来看看一些理论。
想法:将单体分拆为微服务
遗留项目有助于采用微服务方法。主要有以下三个原因:
- 它们通常很难维护、更改、扩展。
- 从开发人员到管理人员,每个人都想把事情变得更简单。
- 你有(某种程度上)明确的领域边界,这意味着:你知道你的软件应该做什么。
这意味着你可以看看Java银行单体应用,并尝试沿着领域边界进行拆分 — 这是一种明智的方法。
- 你可以得出结论,应该有一个
帐户管理
微服务,该微服务处理诸如姓名、地址、电话号码之类的用户数据。 - 或前面提到的
风险
模块,它可以检查用户风险级别,并且可以被公司的很多其他项目甚至部门使用。 - 或
开发票
模块,可通过PDF或实际邮件发送发票。
现实:让别人去做
尽管这种方法在纸上和类似UML的图上看起来确实不错,但它有其缺点。主要是,你需要非常强大的技术技能才能实现。为什么呢?
因为理解之间的巨大差异是将高度耦合的帐户管理
模块从你的单体应用中提取出来,并正确地进行是一件好事。
大多数企业项目都达到了这样的阶段:开发人员不敢,比方说,将7年之久的Hibernate版本升级到较新的版本。这还只是个库的更新,却要做大量工作确保不破坏任何内容。
现在,那些同样的开发人员应该深入研究旧的遗留代码,在不清晰的数据库事务边界的情况下,就能抽取定义良好的微服务吗?可能,但通常是一个真正的挑战,在白板上或架构会议中是没法解决的。
这已经是在本文中,Simon Brown的语录第一次适合的地方:
我会一直这样说…如果人们不能正确构建单体应用,那么微服务也无济于事。 — Simon Brown
全新项目的微服务架构
在开发全新的Java项目时,情况看起来有些不同。现在,上面的三点看起来有点不同:
- 你是从一张白纸开始,所以没有旧包袱要维持。
- 开发人员希望事情在将来也保持简单。
- 问题是:你对领域边界的理解更模糊:你不知道你的软件实际上应该做什么(提示:敏捷 -;))。
这导致了公司尝试和处理全新 Java 微服务项目的各种方式。
技术微服务架构
第一种方式对于开发人员来说是最明显的,尽管强烈建议不要这样做。支持Hadi Hariri想出IntelliJ中的“抽取微服务”重构。
尽管下面的示例过于简化,但不幸的是,在实际项目中看到的实际实现距离也不远。
微服务之前
@Service
class UserService {
public void register(User user) {
String email = user.getEmail();
String username = email.substring(0, email.indexOf("@"));
// ...
}
}
使用了一个 substring Java 微服务
@Service
class UserService {
@Autowired
private HttpClient client;
public void register(User user) {
String email = user.getEmail();
// 现在通过 http 调用 substring 微服务
String username = httpClient.send(substringRequest(email), responseHandler());
// ...
}
}
因此,你实际上是将Java方法调用包装成HTTP调用,没有明显的理由这样做。不过,一个原因是:缺乏经验,并试图强制采用Java微服务方式。
建议:不要这样做。
面向工作流程的微服务架构
下一种常见的方式是,在工作流程之后对Java微服务进行模块化。
现实生活的例子:在德国,当你去看公立医生时,他需要在他的健康软件CRM中记录你的预约。
为了从保险中获得报酬,他将通过XML,将你的治疗数据和他治疗的所有其他患者的治疗数据发送给中介。
中介将查看该XML文件并(简化):
- 尝试验证文件是否是正确的XML。
- 试着验证它的合理性:一个1岁的孩子一天从妇科医生那里得到三次牙齿清洁有意义吗?
- 用其他一些官僚数据增强XML。
- 将XML转发到保险以触发付款。
- 并且将完整信息返回给医生,包括一条“成功”的消息或“搞清楚了的话,请重新发送该数据条目”。
如果你现在尝试用微服务来建模这个工作流,那么你至少会得到:
注意:在本例中,微服务之间的通信是不相关的,但是很可能使用像 RabbitMQ 这样的消息代理异步完成,因为医生无论如何不会得到立即的反馈。
同样,这在纸上看起来不错,但马上会引出几个问题:
- 你觉得需要部署6个应用程序来处理1个xml文件吗?
- 这些微服务真的相互独立吗?它们可以相互独立部署吗?有不同的版本和API方案吗?
- 如果
validation
微服务关闭,plausibility
微服务会怎么做?系统还在运行吗? - 这些微服务现在是否共享同一个数据库(它们确实需要在一个数据库表中的一些公共数据),或者你是否打算让所有微服务拥有自己的数据库?
- 还有很多其他的基础设施/运营问题。
有趣的是,对于一些架构师来说,上面的图读起来更简单,因为现在每个服务都有其准确、定义良好的用途。以前,它看起来像一个可怕的庞然大物:
虽然对这些图的简单性可以作出争论,但现在你肯定有这些额外操作难题需要解决。你:
- 不是仅需要部署一个应用程序,而是至少需要部署六个。
- 甚至可能需要部署多个数据库,这取决于你想在多大程度上采用微服务架构。
- 必须确保每一个系统都是在线的、健康的和工作的。
- 必须确保微服务之间的调用实际上是弹性的(请参阅后面《如何让Java微服务具有弹性》小节)。
- 以及这个设置所暗示的一切 — 从本地开发设置到集成测试。
推荐:
除非:
- 你是 Netflix(然而你不是)。。。
- 你有超强的运维技能:你打开你的开发IDE,触发一个 Chaos Monkey,Drop 掉你的生产数据库,你能在5秒内轻松自动恢复。
- 或者你喜欢像@monzo那样试试1500个微服务,仅仅因为你可以。
Chaos Monkey 是 Netflix 开发的一套用来故意把服务器搞下线的软件,可以测试云环境的恢复能力。
别这么做。
不过,不是那么夸张。
尝试在领域边界之后对微服务建模是一种非常明智的方法。但是领域边界(比如用户管理和开发票)并不意味着采用单个工作流程,并将其拆分为最小的单个部分(接收XML、验证XML、转发XML)。
因此,当你开始一个新的Java微服务项目,而领域边界仍然很模糊时,请试着将微服务的大小保持在较低端。以后总是可以添加更多模块。
并确保你的团队、公司、部门有非常强大的DevOps技能,以支持你的新基础架构。
多语种或者面向团队的微服务架构
开发微服务有第三种几乎是自由主义的方式:让你的团队甚至个人有可能用他们想要的语言或微服务实现用户故事(营销术语:多语言编程)。
因此,上面的 XML Validation
服务可以用 Java 编写,而 Plausibility
微服务则用 Haskell 编写(让其在数学上更合理),Insurance Forwarding
微服务应该用 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
文件放到服务器上?
不幸的是,这个问题有很多吸引人的答案。
如何使用构建工具、SSH和Ansible进行Java微服务部署
在过去的20年里,管理员是如何在公司中部署_任何_Java服务器端程序的,这是对Java微服务部署的枯燥而完美的回答。混合了:
- 你最喜欢的构建工具(Maven,Gradle)。
- 很好的旧 SSH/SCP,用于将
.jar
复制到服务器。 - Bash 脚本管理部署脚本和服务器。
- 或者更好的:一些Ansible脚本。
如果您不愿意为自动负载均衡服务器创建喘不过气来的云,或者混乱的猴子在打您的机器,或者看到ZooKeeper的领导者选举在起作用,那是一种温暖而模糊的感觉,那么此设置将带您走得很远。
If you are not fixated on creating a breathing cloud of ever auto-load-balancing servers, chaos monkeys nuking your machines, or the warm and fuzzy-feeling of seeing ZooKeeper’s leader election working, then this setup will take you very far.
老派,无聊,但能起作用。
如何使用 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 容器,进行健康检查,发布更新,缩放(瑟瑟发抖)?
这个问题的两个可能答案是 Docker Swarm 和 Kubernetes。
在本指南的范围内,详细介绍这两个选项是不可能的,但实际情况是这样的:这两个选项最终都依赖于你编写 YAML 文件来管理你的集群。如果你想知道在实践中会引起什么样的感觉,可以在Twitter上快速搜索一下。
因此,Java微服务的部署过程现在看起来有点像这样:
- 建立和管理Docker Swarm/Kubernetes
- 来自上面的Docker步骤的一切
- 写并执行YAML,直到你的眼睛流血
如何测试Java微服务
假设您已经解决了在生产环境中部署微服务的问题,那么在开发过程中该如何集成测试您的n个微服务呢?是要看一个完整的工作流程是否有效,还是仅看单个的工作流程?
在实践中,你会发现三种不同的方法:
- 通过一些额外的工作(如果您使用的是像SpringBoot这样的框架),您可以将所有的微服务打包到一个launcher类中,并用一个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 调用可能会产生什么样的结果。概括起来,你将得到三个可能的结果:
- OK:调用完成,发票创建成功。
- DELAYED:调用完成,但花了很长时间。
- ERROR:调用没有完成,可能是因为发送了不兼容的请求,或者系统坏了。
任何程序都有可能出错,而不仅仅是愉快的情况。对于微服务也是一样的,即使在开始进行单项的微服务部署和发布时,你也必须格外注意保持所有部署的 API 版本兼容。
并且如果你想充分利用 Chaos monkey,你还必须面对这样的可能性:你的服务器在请求处理过程中可能会受到攻击,你可能希望请求被重新路由到另一个工作实例。
一个有趣的“警告”案例是延迟的案例。可能响应的微服务硬盘已满,响应需要10秒,而不是50微秒。当你遇到某个负载时,这会变得更加有趣,这样您的BillingService的无响应性就会开始在您的系统中级联。想象一下一个缓慢的厨房,从一个餐厅的所有服务员开始,慢慢地开始。
An interesting ‘warning’ case is the delayed case. Maybe the responding’s microservice hard-disk is running full and instead of 50ms, it takes 10 seconds to respond. This can get even more interesting when you are experiencing a certain load, so that the unresponsiveness of your BillingService starts cascading through your system. Think of a slow kitchen slowly starting the block all the waiters of a restaurant.
本节显然无法深入介绍微服务弹性主题,但它提醒开发人员,在您的第一个版本发布之前,这是一个真正的解决和不忽略的问题(根据经验,这种情况比应该发生的次数更多)
This section obviously cannot give in-depth coverage on the microservice resilience topic, but serves as a reminder for developers that this is something to actually tackle and not ignore until your first release (which from experience, happens more often than it should)
帮助你考虑延迟和容错的一个流行库是 Netflix 的 Hystrix 。请用它的文档来深入研究这个主题。
消息传递弹性模式
下面我们更仔细地看一下异步通信。我们的 BillingService
代码现在可能看起来像这样,假如我们使用 Spring 和 RabbitMQ进行消息传递。
为创建发票,我们现在向 RabbitMQ 消息代理发送一条消息,它让一些 worker 等待新消息。这些 worker 创建 PDF 发票,并将其发送给相应的用户。
@Service
class BillingService {
@Autowired
private RabbitTemplate rabbitTemplate;
public void bill(User user, Plan plan) {
Invoice invoice = createInvoice(user, plan);
// 将 invoice 转换为 json,并用它作为消息体
rabbitTemplate.convertAndSend(exchange, routingkey, invoice);
// ...
}
}
现在,潜在的错误情况看起来有点不同,因为你不再像使用同步 HTTP 通信那样立即得到OK,或错误响应。相反,你将大致有以下三种错误情况:
- 我的信息是被一个 worker 传递和消费了,还是丢了?(用户得不到发票)。
- 我的信息是只传递过一次,还是多次传递并只处理一次?(用户会得到多个发票)。
- 配置:从“我是否使用了正确的路由密钥/交换名称”,到“我的消息代理设置和维护是否正确,或者它的队列是否溢出?”(用户得不到发票)。
同样,本指南不包括详细介绍每个异步微服务弹性模式。更确切地说,它是指指向正确方向的指针,特别是它还取决于您正在使用的实际消息传递技术。示例:
如果你是在用RabbitMQ,至少要确保已经阅读过并理解本指南,然后仔细考虑确认、确认和消息的可靠性。
同时,还得有人在设置 ActiveMQ 或 RabbitMQ 服务器和正确配置它们方面有经验,特别是与集群和Docker结合使用时。
哪个Java微服务框架是最好的?
一方面,您已经建立了非常流行的选项,比如Spring Boot Boot),这使得构建.jar文件变得非常容易,这些文件与Tomcat或Jetty这样的嵌入式web服务器一起提供,并且您可以立即在任何地方运行。非常适合构建微服务应用程序。
不过,最近出现了两个专用的微服务框架,其中部分是受响应式编程、Kubernetes或GraalVM等并行开发的启发。
举几个例子:Quarkus、Micronaut、Vert.x、Helidon。
最后,你必须做出自己的选择,但本文可以提供一些也许是非常规的指导:
除了Spring Boot之外,所有的微服务框架通常都将自己推销为“极快的速度”、“极快的启动时间”、“低内存占用率”,能够“无限扩展”,并用令人印象深刻的图表将自己与Spring Boot庞然大物或彼此进行比较。
这消除了开发人员的紧张情绪,他们正在维护那些有时需要几分钟才能启动的遗留项目,或者云本机开发人员希望在50毫秒内启动并停止尽可能多的微容器。
然而,问题是(人工的)裸机启动时间和重新部署时间对项目的总体成功几乎没有影响,远不及强大的框架生态系统、强大的文档、社区和强大的开发人员技能。
你得这样看。
如果到目前为止:
- 你让你的ORMs疯狂运行,并为简单的工作流生成数百个查询。
- 你需要无限的千兆字节才能让你的中等复杂的单体应用程序运行。
- 您添加了太多的代码和复杂性(忽略了像Hibernate这样的潜在慢速启动),您的应用程序现在需要启动几分钟。
再加上额外的微服务挑战(想想:弹性、网络、消息传递、DevOps、基础设施)对你的项目的影响会比启动一个空的hello world要重得多。对于开发过程中的热重新部署,您最终可能需要研究JRebel或DCEVM等解决方案。
回到Simon Brown’s Brown)引述:如果人们不能(快速高效的)创建单体应用程序,那么他们将很难构建(快速高效的)微服务——无论框架是什么。
所以,明智地选择你的框架。
哪些库最适合同步Java REST调用?
关于调用HTTP REST API的更实际的方面。在低级技术方面,您可能会得到以下HTTP客户端库之一:
Java自己的HttpClient(自Java 11以来)、Apache的HttpClient或OkHttp。
请注意,我在这里说“可能”是因为还有很多其他的方法,从不错的老JAX-RS客户端,到现代的WebSocket客户端。
无论如何,有一种趋势是HTTP客户端生成,而不是自己瞎搞HTTP调用。为此,您需要查看OpenFeign项目及其文档,作为进一步阅读的起点。
哪个代理最适合异步Java消息传递?
异步消息传递,您可能要么用ActiveMQ,要么用RabbitMQ,要么用Kafka。再说一次,这只是一个流行的选择。
不过,这里有几个随机点:
- ActiveMQ 和 RabbitMQ 都是传统的、成熟的消息代理。这意味着一个相当聪明的经纪人和愚蠢的消费者。
- ActiveMQ 历史上具有易于嵌入(用于测试)的优势,不过这个优势是可以通过 RabbitMQ/Docker/TestContainer 设置来削弱。
- Kafka不是传统的经纪人。这恰恰相反,本质上是一个相对“愚蠢”的消息存储(想想日志文件),需要更聪明的消费者来处理。
为了更好地了解何时使用 RabbitMQ(或一般的传统消息代理)或 Kafka,请查看Pivotal的匹配博客文章 作为起点。
不过,一般来说,在选择经纪人时,尽量不考虑任何人为的性能原因。曾几何时,团队和在线社区对RabbitMQ有多块和ActiveMQ有多慢争论不休。
现在,您在RabbitMQ上也遇到了同样的问题,速度很慢,每秒钟只有20-30K条消息。Kafka每秒被引用10万条消息。首先,这些比较很方便地忽略了你实际上是在比较苹果和橘子。
Now you are having the same arguments on RabbitMQ being slow with just a consistent 20-30K/messages every.single.second. Kafka is cited with 100K messages/a second. For one, these kinds of comparisons conveniently leave out that you are, in fact, comparing apples and oranges.
但更重要的是:对于阿里巴巴集团,二者的吞吐量数字可能都处于较低或中等的水平,但您的作者从未在现实世界中见过这种规模的项目(每分钟有数百万条消息)。它们确实存在,但对于其他99%个常规Java业务项目而言,这些数字并不值得担心。
所以,忽略炒作,明智选择。
哪些库可用于微服务测试?
根据技术栈的不同,你可能最终会用Spring特定工具(Spring生态系统)或类似Arquillian(JavaEE生态系统)的工具。
您需要查看Docker和真正优秀的Testcontainers库,它可以帮助您轻松快速地为本地开发或集成测试设置Oracle数据库。
要模拟整个HTTP服务器,请查看Wiremock。要测试异步消息传递,请尝试嵌入(ActiveMQ)或容器化(RabbitMQ),然后使用Awaitibility DSL编写测试。
除此之外,所有常见的猜想都适用于Junit、TestNG到AssertJ costigliola.github.io/AssertJ/)和Mockito。
请注意,这绝不是一个全面的列表,如果您缺少您最喜欢的工具,请将其发布在“评论”部分,我将在本指南的下一个修订版中进行选择。
如何为所有Java微服务启用日志记录?
使用微服务进行日志记录是一个有趣且相当复杂的主题。现在,您有了n个日志文件,而不是一个可以减少或grep的日志文件,您希望看到它们组合在一起。
整个日志生态系统的一个很好的起点是https://www.marcobehler.com/guides/A-guide-to-logging-in-java 这篇文章。请务必阅读它,特别是有关微服务的集中日志部分。
在实践中,您会发现各种方法:
- 一个系统管理员编写一些脚本,这些脚本收集并合并来自不同服务器的日志文件到一个日志文件中,并将它们放到FTP服务器上供您下载。
- 在并行SSH会话中运行cat/grep/unig/sort组合。你可以告诉你的经理:亚马逊AWS内部就是这么做的。
- 使用像Graylog)或ELK Stack (Elasticsearch, Logstash, Kibana)这样的工具。
我的微服务如何找到彼此?
到目前为止,我们假设我们的微服务都知道彼此,知道它们对应的IP。更多的是静态设置。因此,我们的银行单体应用[ip=192.168.200.1]知道,他必须与风险服务器[ip=192.168.200.2]通话,而后者是在属性文件中硬编码的。
不过,你可以选择让事情更灵活:
- 你不能再把
application.properties
文件和你的微服务一起部署,而是使用一个云配置服务器,所有微服务都从中提取配置。 - 因为你的服务实例可能会动态地改变它们的位置(想想Amazon EC2实例获得动态IP,你可以在云计算中灵活地自动伸缩),你很快就会看到一个服务注册中心,它知道你的服务的IP地址,并可以相应地路由。
- 现在既然一切都是动态的,你就有了新的问题,比如领导人自动选举:谁是负责某些任务的“主人”,比如不处理两次?当领导失败时,谁来接替他?和谁一起?
一般来说,这就是所谓的“微服务编排”,它本身又是另一个巨大的主题。
像 Eureka 或 Zookeeper 这样的库试图“解决”这些问题,比如客户机或路由器知道哪些服务在哪里可用。另一方面,它们引入了额外的复杂性。
你问问曾经运行过 ZooKeeper 设置的人就晓得了。
如何使用Java微服务进行授权和身份验证?
另一个巨大的话题,值得单独写一篇文章。同样,选项也从带有自编码安全框架的硬编码HTTPS basic auth,到使用您自己的授权服务器 social_login_auth Server)运行Oauth2安装程序。
如何确保我的所有环境看起来都一样?
对于非微服务部署,情况也是如此。您将尝试Docker/Testcontainers和scripting/Ansible的组合。
尽量保持简单。
不是问题:Yaml缩进的故事
从具体的库问题中插入一个问题,下面我们快速地看一下Yaml。它是作为事实上的文件格式使用的文件格式,用于“将配置编写为代码”。从简单的工具,如Ansible,到强大的Kubernetes。
要亲自体验YAML缩进的痛苦,请尝试编写一个简单的Ansible文件,并查看需要多久重新编辑一次该文件才能使缩进正常工作,尽管IDE支持的级别各不相同。然后回来完成这本指南。
Yaml:
- is:
- so
- great
分布式事务呢?性能测试?其他话题?
不幸的是,这些主题并没有出现在本指南的修订版中。请继续关注。
五、概念性微服务挑战
除了特定的Java微服务问题之外,任何微服务项目也有问题。更多的是从组织、团队或管理的角度。
前端/后端不匹配
在许多微服务项目中发生的事情,我称之为前端-后端微服务不匹配。那是什么意思?
在好的旧单体应用中,前端开发人员有一个特定的数据源来获取数据。在微服务项目中,前端开发人员突然有了n个源来获取数据。
假设您正在构建一些Java IoT微服务项目。比如说,你在监视机器,就像整个欧洲的工业烤箱。这些烤箱会定期向你发送最新的温度等信息。
现在,你迟早会希望能够在管理用户界面中搜索烤箱,也许可以借助于“搜索烤箱”的微服务。取决于后端同事对域驱动设计或微服务法则的解释有多严格,可能是“搜索烤箱”微服务只返回烤箱的ID,而不返回其他数据,如其类型、型号或位置。
为此,前端开发人员可能需要对“获取烤箱详细信息”微服务执行一个或n个额外的调用(取决于分页实现),使用他们从第一个微服务获得的ID。
虽然这只是一个简单的(但取自一个真实的项目(!)例如,它演示了以下问题:
现实生活中的超级市场获得了巨大的接受是有原因的。因为你不必去10个不同的地方买蔬菜,柠檬水,冷冻披萨和卫生纸。相反,你去一个地方。
更简单更快。对于前端开发人员和微服务也是如此。
管理层期望
这个问题是个别开发人员、编程杂志或云公司推动微服务的不幸副作用:
管理层的印象是,现在你可以将无限多的开发人员投入到(总体)项目中,因为开发人员现在可以完全独立地工作,每个人都可以使用自己的微服务。只需要一些“微小”的集成工作,在最后(即上线前不久)。
让我们在下一段中看看为什么这种心态是这样一个问题。
小件并不意味着好件
Smaller pieces do not mean better pieces
一个相当明显的问题是,20个较小的部分(在微服务中)实际上并不意味着20个更好的部分。纯粹从技术质量的角度来看,这可能意味着您的单个服务仍然执行400个Hibernate查询,以便跨无法维护的代码层从数据库中选择用户。
回到Simon Brown的引述:如果人们不能正确地构建单体应用,那么他们将很难构建正确的微服务。
尤其是弹性和所有在上线后发生的事情,在许多微服务项目中都是事后诸葛亮,以至于看到微服务在现场运行有点吓人。
不过,这有一个简单的原因:因为Java开发人员通常对弹性、网络和其他相关主题不感兴趣,没有经过适当的培训。
小件会带来更多的技术产品
Smaller pieces lead to more technical pieces
此外,用户故事越来越技术化(因此也越来越愚蠢),越来越微观,越来越抽象。
假设您的微服务团队被要求编写一个技术性的、针对数据库的登录微服务,大致如下:
@Controller
class LoginController {
// ...
@PostMapping("/login")
public boolean login(String username, String password) {
User user = userDao.findByUserName(username);
if (user == null) {
// 处理不存在的用户的情况
return false;
}
if (!user.getPassword().equals(hashed(password))) {
// 处理密码错误的情况
return false;
}
// 'Yay, Logged in!';
// set some cookies, do whatever you want
return true;
}
}
现在,您的团队可能会做出决定(甚至可能说服业务人员):这太简单和乏味了,让我们编写一个真正有能力的UserStateChanged微服务,而不是一个登录服务 — 没有任何真正的、切实的业务需求。
由于Java目前已经过时,让我们在Erlang中编写UserStateChanged微服务。让我们试着在某个地方使用红黑树,因为Steve Yegge )写了你需要了解他们才能去谷歌应聘。
从集成、维护和整个项目的角度来看,这和在同样的单体应用中编写多层意大利面条式代码一样糟糕。
捏造的和过激的例子?对。
不幸的是,在现实生活中也并不少见。
小片段导致小理解
Smaller pieces lead to smaller understanding
如果您作为一个开发人员只负责处理独立的微服务[95:login-101:updateUserProfile]。
它与前一段融合在一起,但是根据你的组织、信任和沟通水平,如果整个微服务链中的任意一部分出现故障,没有人再承担全部责任,这会导致很多人耸耸肩和责怪。
不仅仅是暗示不诚实,还有一个问题,那就是很难理解n个孤立的片段,以及它们在大局中的位置。
沟通与维护
这与最后一期的文章:沟通与维护融为一体。显然,这很大程度上取决于公司规模,一般规律是:规模越大,问题就越多。
- 谁在做47号微服务?
- 他们刚刚部署了一个新的,不兼容的微服务版本吗?这是哪里记录的?
- 我需要和谁谈一个新的功能要求?
- Max 离开公司后,谁来维护Erlang的微服务?
- 我们所有的微服务团队不仅使用不同的编程语言,而且在不同的时区工作!我们如何协调?
这里最重要的主题是,与DevOps 技能类似,在一家更大的、甚至是国际性的公司中,全面的微服务方法会带来大量额外的沟通挑战。作为一个公司,你需要做好准备。
六、结束
阅读本文后,您可能会得出结论,您的作者建议严格禁止微服务。这并不完全正确——我主要是想强调在微服务狂热中被遗忘的要点。
微服务处于一个钟摆上Microservices are on a pendulum
完全使用Java微服务是一个钟摆的结束。另一端则是一个整体中数百个很好的老Maven模块。你必须找到正确的平衡点。
尤其是在全新项目中,没有什么能阻止您采取更加保守、单一的方法,构建更少、定义更好的Maven模块,而不是立即开始使用20个云就绪的微服务。
微服务产生大量额外的复杂性
记住,你拥有的微服务越多,你拥有的真正强大的DevOps人才越少(不,执行一些Ansible脚本或在Heroku上部署不算),你在生产中遇到的问题就越多。
阅读本指南的《常见Java微服务问题》一节已经让人筋疲力尽。然后考虑为所有这些基础设施挑战实施解决方案。你会突然意识到,这些都不再与业务编程有关(你得到的报酬),而是更多的技术与更多的技术相结合。
Siva 在他的博客上完美地总结了这一点:
我无法解释当团队将70%的时间花在这个现代化的基础设施设置上,30%的时间花在实际的业务逻辑上时,会有多可怕。 — Siva Prasad Reddy
您应该创建Java微服务吗?
为了回答这个问题,我想用一个非常厚颜无耻的,类似谷歌的采访调侃结束这篇文章。如果您通过“体验”知道这个问题的答案,即使它看起来与微服务无关,那么您可能已经准备好使用微服务方法了。
场景
假设有一个Java 单体应用运行在最小的Hetzner 专用机器上。数据库服务器也是如此,它也运行在类似的Hetzner机器上。
我们还假设您的 Java 单体应用可以处理用户注册之类的工作流程,并且您不会为每个工作流程生成数百个数据库查询,但只有一小部分(
问题
Java 单体应用应该向数据库服务器打开多少个数据库连接(连接池)?
为什么?你认为你的单体应用可以(大致)扩展到多少同时活跃的用户?
感谢阅读!