本文将为大家介绍什么是 Java 微服务,了解 Java 微服务的体系架构,以及如何设计、开发、部署和测试 Java 微服务。
要真正理解 Java 微服务,就必须从最基本的东西开始:为人诟病的 Java 大型单体应用是什么,它的优点和缺点是什么。
假设你正在为一家银行或一家金融科技初创公司工作。你为用户提供一款可以用它来开设新的银行账户的移动应用程序。
如果用 Java 代码来写,实现一个简化版的控制器类如下所示:
@Controller
class BankController {
@PostMapping("/users/register")
public void register(RegistrationForm form) {
validate(form);
riskCheck(form);
openBankAccount(form);
// 略……
}
}
这段代码要:
部署的时候,你会将 BankController 类与所有其他源代码一起打包到 bank.jar 或 bank.war 中:在远古时期,这个庞然大物还是不错的,它包含你的银行系统运行所需的所有代码。(粗略估算,一开始你的 jar 或 war 文件的大小会在 1-100MB 范围之内。)
然后在服务器上运行.jar 文件——这就是部署 Java 应用程序所需要做的全部工作。
本质上,Java 大型单体应用没有什么问题。但通过以往的项目经验,我们可以明显发现,如果你让许多不同的程序员 / 团队 / 顾问面临着高压和不明确需求,围绕同一款大型单体应用工作好几年,那么你那个小小的 bank.jar 文件就会变成一只巨大的、有千兆字节的代码怪物,每个人都不敢部署它。
这自然就引出了如何缩小大型单体应用的问题。现在,你的 bank.jar 是在一个 JVM 中运行的,一台服务器上运行一个进程。
这时,你可能会产生一个想法:风险检查服务是公司其他部门使用的,我的这款银行应用与它没什么关系,不妨把它分离出去,将它作为自己的产品去部署,作为单独的进程来运行。
实际上,这意味着你不需要在你的 BankController 中调用 riskCheck() 方法,而是将该方法或 bean 及其所有辅助类移动到它自己的 Maven/Gradle 项目中,对其进行源代码配置管理,并将其独立部署,不依赖于你的银行系统。
整个提取过程本身会不会使你新的 RiskCheck 模块成为微服务呢?大家对微服务定义有着不同的解释:
我们不去钻理论上的牛角尖,而是关注其实用性,做以下两件事:
所以总结一下:你之前拥有一个 JVM 进程,即一个银行大型单体应用。现在,除了这个银行 JVM 进程,还有一个在自己 JVM 进程中运行的 RiskCheck 微服务,大型单体应用现在必须调用这个微服务进行风险检查。
基本上,有两种选择:同步通信或异步通信。
(http)/rest(同步通信)
同步微服务通信通常通过 HTTP 和返回 XML 或 JSON 的类似 rest 的服务来完成。
如果你需要立即响应,可以使用 REST 通信,具体到我们的案例就是这样做的,因为在开户之前必须进行风险检查:不做风险检查,就不给开户。在工具方面,可以看看哪些类库最适合同步 Java REST 调用。
消息传递(异步通信)
异步微服务通信通常通过JMS 实现或 AMQP 等协议的消息传递来完成。通常,是因为实际上如 email/smtp 驱动集成的数量是不可低估的。
有时,使用一个微服务并不需要得到立即响应,比如用户按下“立即购买”按钮并希望生成发票,不必在用户购买这一请求 / 响应周期内完成。
示例:在 Java 中调用 REST API
假设我们选择使用同步微服务通信,那么上面的Java 代码看起来就像是更底层的代码。因为对于微服务通信,通常会创建 client 类库,将实际的 HTTP 调用抽象出来。
@Controller
class BankController {
@Autowired
private HttpClient httpClient;
@PostMapping("/users/register")
public void register(RegistrationForm form) {
validate(form);
httpClient.send(riskRequest, responseHandler());
setupAccount(form);
// 略......
}
}
看到这段代码就会发现,现在必须部署两个 Java(微) 服务:Bank 和 RiskCheck 服务。最终会得到两个 jvm,两个进程。之前的关系图看起来将是这样的:
这就是开发一个 Java 微服务项目所需的全部内容:构建和部署更小的部件 (.jar 或.war 文件)。
但这就留下了一个问题:你究竟如何切分或配置这些微服务?这些小部件是什么?多大合适?
实际上,公司可以通过各种方式来设计或架构微服务项目。具体情况取决于你是试图将一个现有的大型单体应用变成一个微服务项目,还是从一个全新的项目开始。
从大型单体应用到微服务
一个更有机的想法是将微服务从现有的整体中分离出来。请注意,这里的“微”实际上并不意味着提取出来的服务本身很小,它们本身可能仍然相当大。
我们来看一些理论。
想法:将一个大型单体应用拆分成微服务
将遗留项目转换为微服务,主要是出于以下三个原因:
这意味着你可以好好看看你的 Java 银行应用这个庞然大物,并尝试沿着领域边界拆分它。
现实:让别人来做
虽然这种方法在在纸上和 uml 类图上呈现出来很美,但是它也有缺点,例如最重要的一点使用这种方法需要很强的技术能力。为什么呢?
因为理解将高度耦合的帐户管理模块从大型单体应用中提取出来是个好主意是一回事,正确地去执行它是另一回事,两者之间存在着巨大的差异。
大多数企业项目都到了这样一个阶段,即开发人员不敢将已经用了 7 年的 Hibernate 版本升级到新的版本,这只是更新一个类库而已,但也需要做大量工作以确保不会破坏任何东西。
这些开发人员现在要深入挖掘旧的遗留代码(它们没有清晰的数据库事务边界),并提取定义良好的微服务?通常真正的挑战是无法在白板或架构会议上解决的。
这里引用推特上 @simonbrown 的话:
我一直都说……如果你不能正确地构建大型单体应用,那么微服务也帮不了你。Simon Brown
开发全新的 Java 项目时,情况看起来有点不同。现在,这三点与之前那三点略有不同:
于是就产生了各种不同的方法,公司可以使用它们尝试处理全新的 Java 微服务项目。
技术型微服务架构
对于开发人员来说,马上就会想到这样一种方法,尽管我们强烈建议不要使用它。Hadi Hariri 在 IntelliJ 中提出了“提取微服务(Extract Microservice)”重构功能,这一点很是值得称道。
下面的例子做了极度的简化,但实际项目中的情况却与之非常接近。
微服务之前
@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 文件并做出处理(已做简化):
现在,如果你尝试使用微服务对这个工作流进行建模,至少会包括以上内容。
注意: 在本例中,微服务之间的通信与主题无关,但如果真要提一下的话,可以通过 RabbitMQ 之类的消息代理异步完成,因为医生不需立即得到反馈。
同样的,从纸面上看,这似乎挺不错的,但我们马上会发现以下几个问题:
有趣的是,对于一些架构师来说,上面的图理解起来更简单,因为现在每个服务都有它确切的、定义良好的用途。以前,它看起来像这个可怕的大型单体应用:
虽然这些图画起来简单,但是你肯定需要解决些额外的运维挑战。
建议:除非你是 Netflix(但很显然你不是),你拥有超强的运维技能包:打开开发 IDE,出现故障,生产数据库被删掉,但你能轻易在 5 秒内自动恢复。或者你觉得自己像 @monzo,仅仅因为认为自己能行,就尝试了 1500 个微服务。否则,就不要这么做。
尝试根据领域边界对微服务建模是一种非常明智的方法。但是,领域边界(比如用户管理和发票)并不意味着拿来一条工作流将其分解为几个最小的部分(接收 XML、验证 XML、转发 XML)。
因此,每当你开始一个新的、领域边界还非常模糊的 Java 微服务项目时,且领域边界仍然非常模糊,请尽量保持微服务的规模,并保证当前系统在之后能够添加更多模块,确保整个团队 / 公司 / 部门都拥有非常强大的 DevOps 技能,以支持你新的基础架构。
还有第三种,几乎是以自由意志主义的方法来开发微服务:让你的团队甚至个人有可能使用他们想用的任何语言或微服务来实现用(行业术语:多语言编程)。
例如,上面的合理性微服务是用 Haskell 编写的,保险的转发微服务应该用 Erlang 编写,而 XML 验证服务可以用 Java 编写。
从开发人员的角度来看很有趣的东西(即在一套隔离的环境中使用你的完美语言开发一个完美的系统),基本上不是组织想要的同质化和标准化。
这意味着,应该有一套相对标准化的语言、库和工具,这样即便你不在了,其他开发人员将来也可以继续维护。
有趣的是,回溯历史可以发现,标准化走得太远了。某些财富 500 强公司甚至不允许他们的开发人员使用 Spring,因为它“不在公司的技术蓝图中”。
建议:如果你打算使用多语言,请尝试减少同一编程语言生态系统中的多样性。例如选择Kotlin 和 Java(它们都基于 JVM,彼此之间 100% 兼容),而不是 Haskell 和 Java。
请快速回顾一下本文开头提到的基础知识,任何服务器端的 Java 程序,都是.jar 或.war 文件,因此也包括微服务。
在 Java 生态系统(更确切地说是 JVM)中,有一件事情很棒:只写一次 Java 代码,基本上就可以在任何你想要的操作系统上运行,只要你用来编译代码的 JVM 版本不高于运行代码的 JVM 版本即可。
理解这一点很重要,尤其是涉及到 Docker、Kubernetes 或云这样的主题时。为什么呢?让我们看看以下几个不同的部署场景:
我们继续以上文的银行系统为例,我们现在有一个 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 文件放到服务器上呢?这个问题的答案可就多喽。
对于 Java 微服务的部署,最无聊但又完美的答案是过去 20 年中管理员为公司部署 Java 服务器端程序的方式。它包括:
如果你并不想自动处理所有的负载均衡,随时防备着攻击,时时关注着 ZooKeeper 的 leader 选举,那么这种配置就足以应付很长时间了。
几年前,出现了 Docker 和容器化的主题。如果你以前没有使用过它,那么可以先了解一下它对最终用户或开发人员的意义所在:
有趣的是,由于 JVM 的可移植性和向后兼容性,这个好处听起来好像没那么了不起。你可以在任何服务器、树莓派(甚至是移动电话)上下载一个 JVM.zip 文件,解压缩后运行任何你想要运行的.jar 文件。
但是,对于 PHP 或 Python 之类的语言来说,情况就有点不同了,因为这些语言的版本不互相兼容或其部署配置历来都比较复杂。
或者,如果你的 Java 应用程序依赖于大量其它要安装好的服务(使用正确的版本号):比如像 PostgreSQL 之类的数据库或者像 Redis 之类的键值存储。
所以,Docker 对于 Java 微服务,或者说 Java 应用程序的主要好处在于:
如果你想在开发机上运行一个小巧的 Oracle 数据库,那么试试 Docker 吧。
所以总结一下,现在不再是简单地 scp 一个.jar 文件,而是将 jar 文件打包成 Docker 镜像,将该 docker 镜像传输到一个私有的 docker 注册表,在目标平台上拉取该镜像,然后运行它,或者将 Docker 镜像直接 scp 到你的生产系统,然后运行它。
假设你正在尝试 Docker。现在每次部署 Java 微服务时,你都要创建一个 Docker 镜像,它绑定了你的.jar 文件。你有若干这样的 Java 微服务,你希望将这些服务部署到若干机器上:即集群。
那么问题来了:如何管理集群,也就是运行 Docker 容器、执行健康检查、发布更新、扩展等等?
答案可能有两个:Docker Swarm 和 Kubernetes。
由于篇幅所限,本文就不详细介绍它们了,本质上它们最终都是基于你编写 YAML 文件来管理你的集群。
那么,Java 微服务的部署过程现在看起来可能是这样的:安装和管理 Docker Swarm/Kubernetes,执行上面所述的 Docker 步骤,编写和执行 YAML,直到所有的东西都正常工作。
假设你解决了在生产环境中部署微服务的问题,但是在开发过程中如何集成测试微服务呢?如何查看完整的工作流是否正常工作?
在实践中,你会找到三种不同的方法:
此外,除了 Java 微服务之外,你可能还需要一个运行一个消息代理(比如:ActiveMQ 或 RabbitMQ),或者一个电子邮件服务器或任何其他消息传递组件,你的 Java 微服务需要通过这些组件来彼此通信。
DevOps 方面的复杂度被大大低估了,建议了解一下微服务测试类库。
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8