Java 微服务实用指南(一)

Java 微服务:基础

要真正理解 Java 微服务,就必须从最基本的东西开始:为人诟病的 Java 大型单体应用是什么,它的优点和缺点是什么。


什么是 Java 大型单体应用?

假设你正在为一家银行或一家金融科技初创公司工作。你为用户提供一款可以用它来开设新的银行账户的移动应用程序。

如果用 Java 代码来写,实现一个简化版的控制器类如下所示:

@Controller
class BankController {
    @PostMapping("/users/register")
    public void register(RegistrationForm form) {
        validate(form);
        riskCheck(form);
        openBankAccount(form);
        // 略……
    }
}

这段代码要:

  1. 验证注册表单。

  2. 对用户的地址进行风险检查,以决定是否可以给他一个银行帐户。

  3. 打开这个银行账户

部署的时候,你会将 BankController 类与所有其他源代码一起打包到 bank.jar 或 bank.war 中:在远古时期,这个庞然大物还是不错的,它包含你的银行系统运行所需的所有代码。(粗略估算,一开始你的 jar 或 war 文件的大小会在 1-100MB 范围之内。)

然后在服务器上运行.jar 文件——这就是部署 Java 应用程序所需要做的全部工作。

image

Java 大型单体应用存在什么问题?

本质上,Java 大型单体应用没有什么问题。但通过以往的项目经验,我们可以明显发现,如果你让许多不同的程序员 / 团队 / 顾问面临着高压和不明确需求,围绕同一款大型单体应用工作好几年,那么你那个小小的 bank.jar 文件就会变成一只巨大的、有千兆字节的代码怪物,每个人都不敢部署它。


如何让 Java 大型单体应用变得更小?

这自然就引出了如何缩小大型单体应用的问题。现在,你的 bank.jar 是在一个 JVM 中运行的,一台服务器上运行一个进程。

这时,你可能会产生一个想法:风险检查服务是公司其他部门使用的,我的这款银行应用与它没什么关系,不妨把它分离出去,将它作为自己的产品去部署,作为单独的进程来运行。


什么是 Java 微服务?

实际上,这意味着你不需要在你的 BankController 中调用 riskCheck() 方法,而是将该方法或 bean 及其所有辅助类移动到它自己的 Maven/Gradle 项目中,对其进行源代码配置管理,并将其独立部署,不依赖于你的银行系统。

整个提取过程本身会不会使你新的 RiskCheck 模块成为微服务呢?大家对微服务定义有着不同的解释:

  1. 如果它里面只有 5-7 个类,那么算不算微?

  2. 100 或者 1000 个类仍然算是微吗?

  3. 微服务和类的数量有什么关系吗?

我们不去钻理论上的牛角尖,而是关注其实用性,做以下两件事:

  • 调用所有可单独部署的服务(即与大小或领域边界无关的微服务)。

  • 重点关注服务间的通信这一重要主题,因为你的微服务需要相互通信的方式。

所以总结一下:你之前拥有一个 JVM 进程,即一个银行大型单体应用。现在,除了这个银行 JVM 进程,还有一个在自己 JVM 进程中运行的 RiskCheck 微服务,大型单体应用现在必须调用这个微服务进行风险检查。


如何在 Java 微服务之间进行通信?

基本上,有两种选择:同步通信或异步通信。

(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,两个进程。之前的关系图看起来将是这样的:

image

这就是开发一个 Java 微服务项目所需的全部内容:构建和部署更小的部件 (.jar 或.war 文件)。

但这就留下了一个问题:你究竟如何切分或配置这些微服务?这些小部件是什么?多大合适?


2

Java 微服务体系结构

实际上,公司可以通过各种方式来设计或架构微服务项目。具体情况取决于你是试图将一个现有的大型单体应用变成一个微服务项目,还是从一个全新的项目开始。

从大型单体应用到微服务

一个更有机的想法是将微服务从现有的整体中分离出来。请注意,这里的“微”实际上并不意味着提取出来的服务本身很小,它们本身可能仍然相当大。

我们来看一些理论。

想法:将一个大型单体应用拆分成微服务

将遗留项目转换为微服务,主要是出于以下三个原因:

  1. 它们通常难以维护 / 变更 / 扩展。

  2. 每个人,都想让事情变得更简单,从开发人员、运维人员到管理人员。

  3. 在某种程度上,你对你的领域有着明确的边界界定,也就是说:你知道你的软件应该做什么。

这意味着你可以好好看看你的 Java 银行应用这个庞然大物,并尝试沿着领域边界拆分它。

  • 你可以得出这样的结论:应该有一个“账户管理”微服务,它可以处理用户的姓名、地址、电话号码等数据。

  • 还有前面提到过的“风险模块”,用来检查用户的风险级别,可以供公司的许多其他项目甚至部门使用。

  • 还有一个发票模块,它通过 PDF 或实际的邮件发送发票。

现实:让别人来做

虽然这种方法在在纸上和 uml 类图上呈现出来很美,但是它也有缺点,例如最重要的一点使用这种方法需要很强的技术能力。为什么呢?

因为理解将高度耦合的帐户管理模块从大型单体应用中提取出来是个好主意是一回事,正确地去执行它是另一回事,两者之间存在着巨大的差异。

大多数企业项目都到了这样一个阶段,即开发人员不敢将已经用了 7 年的 Hibernate 版本升级到新的版本,这只是更新一个类库而已,但也需要做大量工作以确保不会破坏任何东西。

这些开发人员现在要深入挖掘旧的遗留代码(它们没有清晰的数据库事务边界),并提取定义良好的微服务?通常真正的挑战是无法在白板或架构会议上解决的。

这里引用推特上 @simonbrown 的话:

我一直都说……如果你不能正确地构建大型单体应用,那么微服务也帮不了你。Simon Brown


全新项目的微服务架构

开发全新的 Java 项目时,情况看起来有点不同。现在,这三点与之前那三点略有不同:

  1. 你要从头开始,所以没有要保留的旧包袱。

  2. 开发人员希望事情在未来保持简单。

  3. 问题:你对领域边界的认识还非常模糊:你不知道你的软件实际上想要做什么(提示:敏捷)。

于是就产生了各种不同的方法,公司可以使用它们尝试处理全新的 Java 微服务项目。

技术型微服务架构

对于开发人员来说,马上就会想到这样一种方法,尽管我们强烈建议不要使用它。Hadi Hariri 在 IntelliJ 中提出了“提取微服务(Extract Microservice)”重构功能,这一点很是值得称道。

image

下面的例子做了极度的简化,但实际项目中的情况却与之非常接近。

微服务之前

@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 文件并做出处理(已做简化):

  1. 验证该文件是否是正确的 XML

  2. 验证它的合理性:例如一个 1 岁大的孩子每天从妇科医生做三次牙齿清洁,这合理吗?

  3. 使用其他一些形式数据对 XML 加以补充

  4. 将 XML 转发给保险以触发报销

  5. 然后向医生反馈结果,例如“成功”或“数据有异常,请重新核实修改后再次发送”

现在,如果你尝试使用微服务对这个工作流进行建模,至少会包括以上内容。

注意: 在本例中,微服务之间的通信与主题无关,但如果真要提一下的话,可以通过 RabbitMQ 之类的消息代理异步完成,因为医生不需立即得到反馈。

同样的,从纸面上看,这似乎挺不错的,但我们马上会发现以下几个问题:

  • 你觉得需要部署六个应用程序来处理一个 xml 文件吗?

  • 这些微服务真的相互独立吗?它们每一个可以独立部署吗?每个都具有不同的版本和 API 模式?

  • 如果验证微服务停了,那么合理性微服务会做什么?系统还会保持运行吗?

  • 这些微服务现在是否共享同一个的数据库(它们确实需要数据库表中的一些公共数据),或者你是否会采取更大的动作来为它们提供属于自己的数据库?

  • 以及基础设施或运维方面存在的其他问题。

有趣的是,对于一些架构师来说,上面的图理解起来更简单,因为现在每个服务都有它确切的、定义良好的用途。以前,它看起来像这个可怕的大型单体应用:

虽然这些图画起来简单,但是你肯定需要解决些额外的运维挑战。

  • 不仅需要部署一个应用程序,而是需要至少部署六个。

  • 甚至可能需要部署多个数据库,这取决于你希望微服务体系架构走多远。

  • 必须确保每个系统都保持在线、健康和工作。

  • 必须确保微服务之间的调用是有弹性的

  • 这么部署带来的一切差异——从本地开发配置到集成测试。

建议:除非你是 Netflix(但很显然你不是),你拥有超强的运维技能包:打开开发 IDE,出现故障,生产数据库被删掉,但你能轻易在 5 秒内自动恢复。或者你觉得自己像 @monzo,仅仅因为认为自己能行,就尝试了 1500 个微服务。否则,就不要这么做。

尝试根据领域边界对微服务建模是一种非常明智的方法。但是,领域边界(比如用户管理和发票)并不意味着拿来一条工作流将其分解为几个最小的部分(接收 XML、验证 XML、转发 XML)。

因此,每当你开始一个新的、领域边界还非常模糊的 Java 微服务项目时,且领域边界仍然非常模糊,请尽量保持微服务的规模,并保证当前系统在之后能够添加更多模块,确保整个团队 / 公司 / 部门都拥有非常强大的 DevOps 技能,以支持你新的基础架构。


多语言或面向团队的微服务架构

还有第三种,几乎是以自由意志主义的方法来开发微服务:让你的团队甚至个人有可能使用他们想用的任何语言或微服务来实现用(行业术语:多语言编程)。

例如,上面的合理性微服务是用 Haskell 编写的,保险的转发微服务应该用 Erlang 编写,而 XML 验证服务可以用 Java 编写。

从开发人员的角度来看很有趣的东西(即在一套隔离的环境中使用你的完美语言开发一个完美的系统),基本上不是组织想要的同质化和标准化。

这意味着,应该有一套相对标准化的语言、库和工具,这样即便你不在了,其他开发人员将来也可以继续维护。

有趣的是,回溯历史可以发现,标准化走得太远了。某些财富 500 强公司甚至不允许他们的开发人员使用 Spring,因为它“不在公司的技术蓝图中”。

建议:如果你打算使用多语言,请尝试减少同一编程语言生态系统中的多样性。例如选择Kotlin 和 Java(它们都基于 JVM,彼此之间 100% 兼容),而不是 Haskell 和 Java。


3

Java 微服务的部署和测试

请快速回顾一下本文开头提到的基础知识,任何服务器端的 Java 程序,都是.jar 或.war 文件,因此也包括微服务。

在 Java 生态系统(更确切地说是 JVM)中,有一件事情很棒:只写一次 Java 代码,基本上就可以在任何你想要的操作系统上运行,只要你用来编译代码的 JVM 版本不高于运行代码的 JVM 版本即可。

理解这一点很重要,尤其是涉及到 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 服务器端程序的方式。它包括:

  • 你最喜欢的构建工具 (Maven、Gradle)

  • 古老但好用的 SSH/SCP,用于将你的 jar 包复制到服务器

  • 用于管理部署脚本和服务器的 Bash 脚本

  • 或者一些更好的:Ansible 脚本。

如果你并不想自动处理所有的负载均衡,随时防备着攻击,时时关注着 ZooKeeper 的 leader 选举,那么这种配置就足以应付很长时间了。


如何使用 Docker 进行 Java 微服务部署

几年前,出现了 Docker 和容器化的主题。如果你以前没有使用过它,那么可以先了解一下它对最终用户或开发人员的意义所在:

  • 容器(简化过的)就像一个古老的虚拟机,但更轻量级。

  • 容器是可移植的,它可以在任何地方运行。

image

有趣的是,由于 JVM 的可移植性和向后兼容性,这个好处听起来好像没那么了不起。你可以在任何服务器、树莓派(甚至是移动电话)上下载一个 JVM.zip 文件,解压缩后运行任何你想要运行的.jar 文件。

但是,对于 PHP 或 Python 之类的语言来说,情况就有点不同了,因为这些语言的版本不互相兼容或其部署配置历来都比较复杂。

或者,如果你的 Java 应用程序依赖于大量其它要安装好的服务(使用正确的版本号):比如像 PostgreSQL 之类的数据库或者像 Redis 之类的键值存储。

所以,Docker 对于 Java 微服务,或者说 Java 应用程序的主要好处在于:

  • 使用像 Testcontainers 这样的工具来搭建同质化的测试或集成环境。

  • 使复杂的部署“更容易”。以 Discourse 论坛软件为例。你可以用一个 Docker 镜像部署,它包含了你需要的所有东西:从用 Ruby 编写的 Discourse 软件,到 PostgreSQL 数据库,再到 Redis 和几乎所有的一切。

如果你想在开发机上运行一个小巧的 Oracle 数据库,那么试试 Docker 吧。

所以总结一下,现在不再是简单地 scp 一个.jar 文件,而是将 jar 文件打包成 Docker 镜像,将该 docker 镜像传输到一个私有的 docker 注册表,在目标平台上拉取该镜像,然后运行它,或者将 Docker 镜像直接 scp 到你的生产系统,然后运行它。


使用 Docker Swarm 或 Kubernetes 部署

假设你正在尝试 Docker。现在每次部署 Java 微服务时,你都要创建一个 Docker 镜像,它绑定了你的.jar 文件。你有若干这样的 Java 微服务,你希望将这些服务部署到若干机器上:即集群。

那么问题来了:如何管理集群,也就是运行 Docker 容器、执行健康检查、发布更新、扩展等等?

答案可能有两个:Docker Swarm 和 Kubernetes。

由于篇幅所限,本文就不详细介绍它们了,本质上它们最终都是基于你编写 YAML 文件来管理你的集群。

那么,Java 微服务的部署过程现在看起来可能是这样的:安装和管理 Docker Swarm/Kubernetes,执行上面所述的 Docker 步骤,编写和执行 YAML,直到所有的东西都正常工作。


如何测试 Java 微服务

假设你解决了在生产环境中部署微服务的问题,但是在开发过程中如何集成测试微服务呢?如何查看完整的工作流是否正常工作?

在实践中,你会找到三种不同的方法:

  1. 做一点点额外的工作(如果你正在使用 Spring 之类的框架), 你可以把你所有的微服务包装成一个运行器类,使用一个 Wrapper.java 类来启动所有的微服务,当然,前提是你的机器上有足够的内存来运行所有的微服务。

  2. 你可以尝试在本地也安装一套 Docker Swarm 或 Kubernetes。

  3. 不再在本地进行集成测试,而是搭建一套专用的开发 / 测试环境。现实中,很多团队都是这么做的,以避免承受在本地部署微服务之痛。

此外,除了 Java 微服务之外,你可能还需要一个运行一个消息代理(比如:ActiveMQ 或 RabbitMQ),或者一个电子邮件服务器或任何其他消息传递组件,你的 Java 微服务需要通过这些组件来彼此通信。

DevOps 方面的复杂度被大大低估了,建议了解一下微服务测试类库。

你可能感兴趣的:(Java 微服务实用指南(一))