Java 微服务实用指南(二)

发布于:2020 年 6 月 2 日 08:26

Java微服务实用指南(二)

本文将为大家介绍 Java 微服务的常见问题, Java 微服务框架的选型,以及微服务实践常遇到的挑战。

常见的 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 调用可能会得到什么结果。概括地说,你可能会得到三个结果:

  1. OK:完成调用,成功创建了发票。
  2. DELAYED:完成调用,但是花了很长时间。
  3. ERROR:调用失败,可能是因为你发送了一个不兼容的请求,或者是因为系统故障。

任何程序都需要做错误处理,而不仅仅是处理最顺利的情况。没错,微服务也是如此,即使你一开始进行单个微服务部署和发布时,已经格外注意保持了所有已部署 API 的版本兼容性。

如果你想杜绝所有的情况,你还必须考虑服务器在处理请求的过程中被核武器攻击的可能性,可能此时你希望该请求被重新路由到另一个工作实例。

Java微服务实用指南(二)

有一种“警告”应该引起注意,那就是延迟。也许正在响应的微服务硬盘已经满了,响应时间不是 50ms,而是 10 秒。如果你正在承受一定的负载,更要引起注意,若 BillingService 不再响应,将在你的系统中开始产生级联反应。想象一下,如果厨房慢吞吞的,就会让餐厅里的所有服务员都在等它出菜。

本节无法对微服务弹性这个主题进行更深入探讨,在此仅提醒开发人员,在发布第一个版本之前,需要切实解决这一问题,不可疏忽(根据笔者经验,这种情况出现得要比你认为的更加频繁)。

在处理延迟和容错方面, Netflix 的 Hystrix 是一个流行的类库。阅读它的文档,可更深入地研究这个主题。

消息传递弹性模式

让我们再来好好看看异步通信。如果我们使用 Spring 和 RabbitMQ 进行消息传递,那么我们的 BillingService 代码现在可能类似于以下代码。

为了创建发票,我们现在向 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 或 ERROR 响应。你大概会收到以下三种错误情况:

  1. 我的消息由 worker 传递和消费了吗?还是丢失了?(用户没有得到发票)。
  2. 我的消息只传递了一次吗?还是发送了多次,只处理了一次?(用户将得到多张发票)。
  3. 配置:从“我是否使用了正确的路由键 / 交易名称”到“是否正确设置和维护了消息代理,或者它的队列是否已经满了?”(用户没有得到发票)。

同样,详细介绍每个异步微服务弹性模式已经超出了本指南的范畴。本文的侧重点是指出一个正确的方向,具体思路还要取决于你正在实际使用的消息传递技术。例如:

  • 如果你正在使用 JMS 实现,比如 ActiveMQ ,那么你可能希望用速度来换取两阶段 (XA) 提交的保证。
  • 如果你正在使用 RabbitMQ,你至少应该阅读并理解这份指南,然后认真思考发布者确认、消息确认和消息可靠性。
  • 还有一些人曾经搭建过 Active 或 RabbitMQ 服务器,具有正确配置它们的经验,特别是结合集群和 Docker、网络分割的使用经验。

哪个 Java 微服务框架是最好的?

一方面,你已经有了广受认可、非常流行的选择,比如 Spring Boot ,这使构建.jar 文件变得非常容易,将这些文件与 Tomcat 或 Jetty 之类的嵌入式 web 服务器一起提供,可以立即在任何地方运行。这非常适合构建微服务应用程序。

然而,最近出现了一些专用的微服务框架,它们在一定程度上受到了诸如响应式编程、 Kubernetes GraalVM 等并行开发的启发。

举几个例子: Quarkus 、  Micronaut Vert.x Helidon

最终,你将必须做出自己的选择,但这篇文章可以提供一些也许不太常规的建议:

除了 Spring Boot 之外,所有的微服务框架通常都标榜自己运行速度极快,启动速度极快,内存占用率极低,可以无限地扩展,并使用很具视觉冲击力的图表来与 Spring Boot 这个庞然大物进行比较。

这消除了那些维护遗留项目(这些遗留项目有时需要几分钟的时间来启动)的开发人员的顾虑,以及云原生开发人员(他们希望在 50 毫秒内启动或停止尽可能多的微型容器)的顾虑。

Java微服务实用指南(二)

然而,问题是(人为的)裸金属启动时间和重新部署时间对项目的整体成功几乎没有什么影响,远远比不上强大的框架生态系统、强大的文档、社区和强大的开发人员技能。

你必须要认识到这一点。

如果截止到现在:

  • 你让 ORM 在系统中四处横行,并为简单的工作流生成了数百个查询。
  • 你需要无数个 GB 来运行中等复杂度的单体应用。
  • 你添加了如此多的代码和复杂度,以至于(忽略像 Hibernate 这个缓慢的大明星)你的应用程序现在需要几分钟才能启动。

而且,在上面添加额外的微服务挑战可不仅仅是启动一个空的 hello world,弹性、网络、消息传递、DevOps 和基础设施将对你的项目产生更大的影响。对于开发期的热部署,你最终可能需要看看 JRebel DCEVM 之类的解决方案。

回头看一下 Simon Brown 的那句名言:如果你不能构建(快速且高效)的大型独体应用,那么也很难构建(快速且高效)的微服务。

所以,明智地选择你的框架吧。

哪些是最好的 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 不是一个传统的代理。相反,它本质上是一个相对“愚蠢”的消息存储(比如日志文件),需要更聪明的消费者来处理。

若要更好地理解什么时机适合使用 RabbitMQ(或传统的消息代理)或 Kafka,请先阅读一下 Pivotal 的相关博文

但是,一般来说,在选择代理时要尽量排除任何人为的性能原因。曾经有一段时间,有些团队和在线社区对 RabbitMQ 有多快和 ActiveMQ 有多慢争论不休。

现在,以相同的参数,在 RabbitMQ 上速度很慢,每一秒只有 20-30K 条消息,而 Kafka 则每秒 10 万条消息。首先要明确一点,做这种比较,可能很容易就会忽略掉你实际上是在拿苹果跟橘子比。

但更重要的是:对于阿里巴巴集团来说,这两个吞吐量,可能都处于较低或中等水平,但我们可能从未在现实世界中看到过如此规模的项目(每分钟数百万条消息)。它们肯定存在,但是对于其他 99% 的常规 Java 业务项目来说,实在没有必要去担心这些指标。

所以,不要理会那些天花乱坠的宣传,做出明智的选择吧。

我可以使用哪些类库进行微服务测试?

根据你的软件栈,你可能最终会使用 Spring 的特定工具(Spring 生态系统),或类似于 Arquillian (JavaEE 生态系统)的东西。

你需要了解 Docker 和真正优秀的 Testcontainers 类库,它们可以帮助你轻松、快速地为本地开发或集成测试配置 Oracle 数据库。

要模拟整个 HTTP 服务器,可以看一下 Wiremock 。要测试异步消息传递,请尝试嵌入 ActiveMQ 或部署 RabbitMQ,然后使用 Awaitility DSL 编写测试。

除此之外,只要你觉得能用的,就可以用,从 Junit TestNG AssertJ Mockito

特别说明:这绝不是一份大而全的列表,如果里面遗漏了你最喜欢的工具,欢迎留言指出,我们将在下一版指南中予以介绍。

如何为所有 Java 微服务器启用日志记录?

使用微服务进行日志记录是一个有趣且相当复杂的主题。现在,你会有 n 个日志文件,而不仅仅是一个可以 less 或 grep 的日志文件,或许,你希望看到的是合并起来的日志文件。

这篇文章很不错,在开启日志生态系统之旅前,推荐先阅读一下,特别是关于微服务的集中式日志部分。

在实际工作中,你可以找到各种方法:

微服务之间如何找到彼此?

到目前为止,我们一直假设我们的微服务都互相认识,知道它们对应的 IP。目前,更多的是静态设置。因此,我们的银行大型独体应用(其 ip 为 192.168.2001)知道它必须与风险服务器(其 ip 为 192.168.2002)进行通信,这些都硬编码在一个属性文件中。

然而,你可以让它们更加灵活一些:

  • 你不用再和微服务一起来部署 application.properties,而是使用一台云配置服务器,所有的微服务都从那里获取配置。
  • 因为你的服务实例可能会动态更改它们的位置(设想一下,Amazon EC2 实例获得动态 IP,使你可以灵活地自动伸缩云计算),所以你可能很快就会想到要有一个服务注册中心,它知道你的服务位于哪个 IP 中,并且可以相应地进行路由。
  • 现在,既然一切都是动态的,你就有了新的问题,比如自动选举 leader :谁是处理某些任务的专家,如何保证不会重复处理?当 leader 出现问题时,谁来接替它?和谁一起?

概括来说,这就是所谓的微服务编排,它本身就是另一个很大的主题。

Eureka Zookeeper 这样的类库试图“解决”这些问题,比如客户端或路由器知道哪些服务在哪里是可用的。而另一方面,它们也带来了大量额外的复杂度。

只要问问那些做过 ZooKeeper 配置的人就知道了。

如何使用 Java 微服务进行授权和身份验证?

这是另一个值得探讨的主题。你可以选择硬编码 HTTPS 基本认证和自编码安全框架,以及在自己的授权服务器上运行 Oauth2。

如何确保所有环境看起来都是一样的?

适用于非微服务部署的情况,也适用于微服务部署。你可以尝试 Docker/Testcontainers 和脚本 /Ansible。

尽量保持简单。

不是问题:YAML 缩进的故事

告别特定的类库问题,让我们来快速了解一下 Yaml。它是“配置即代码”的事实上的标准文件格式。从简单的 Ansible 到强大的 Kubernetes,这些工具都支持这种格式。

要亲身体验 YAML 缩进之痛,你可以先自己尝试编写一个简单的 Ansible 文件,尽管不同的 IDE 有着各种级别的支持,看看你需要反复修改多久,才能使缩进正常无误。然后,再回过头来把这份指南看完。

复制代码
Yaml:
- is:
- so
- great

那么分布式事务呢?性能测试呢?其他的主题呢?

很遗憾,本指标当前版本暂未涉及这些主题,敬请继续关注。

微服务的概念性挑战

除了特定的 Java 微服务的问题之外,任何微服务项目都会带来一些问题。这些问题更多地出自于组织、团队或管理的视角。

前后端不匹配

在许多微服务项目中都会出现一种我称之为前后端微服务不匹配的情况。这是指什么呢?

在传统老式的大体独体应用中,前端开发人员只有一个获取数据的特定来源。在微服务项目中,前端开发人员突然有了 n 个获取数据的数据源。

假设你正在构建某个 Java-IoT 微服务项目。你正在监控一些机器,比如欧洲各地的工业烤箱。这些烤箱会定期向你发送温度等状态更新。

现在,你可能希望能够在管理界面中搜索烤箱,可能需要用到“搜索烤箱”微服务。由于后台同事对领域驱动设计或微服务条款的解读,可能“搜索烤箱”微服务只返回烤箱的 id,而不返回其他数据,如类型、模型或位置。

为此,前端开发人员可能需要执行一次或多次额外的调用(取决于你的分页实现),使用从第一个微服务获得的 id 来调用“获取烤箱细节”微服务。

Java微服务实用指南(二)

虽然这只是一个简单的(但它确实源自于真实的项目)示例,但它说明了以下问题:

在现实生活中,超市被广泛接受是有原因的。因为你不必去 10 个不同的地方去买蔬菜、柠檬水、冷冻披萨和卫生纸,而是去一个地方就够了。

它更简单、更快速。前端开发人员和微服务也是如此。

管理期望

一些开发人员、编程杂志或云公司在大力推动微服务时,也带来了一个负作用:

管理层形成了这样一种印象:现在,你可以向项目中注入无限的开发人员了,因为开发人员现在可以完全独立地工作,每个人都可以在他们自己的微服务开展工作。只需要在最后(即将要上线的时候)进行一些微小的集成即可。

下面,我们来看看为什么这种心态会成为一个问题。

部件更小,未必更好

显然,把一个部件拆成 20 份,未必会得到 20 件更好的部件。纯粹从技术质量的角度来看,这可能意味着你的各个服务要执行 400 个 Hibernate 查询,从而跨过各层从数据库中查出一个用户,而且代码也更难维护了。

再来回顾一下 Simon Brown 的话,如果人们不能正确地构建大型独体应用,他们也很难构建正确的微服务。

特别是在许多微服务项目中,总是在事后才想起弹性,每件事情都是在上线后实际发生了才放马后炮,看看那些在现场运行的微服务,总让人觉得有点不大放心。

原因其实也很简单,就是因为 Java 开发人员通常对弹性、网络和其他相关主题不感兴趣,没有经过适当的培训。

部件更小,则更技术化

此外,有一个很不好的趋势是,用户故事越来越技术化(因此也越来越愚蠢),于是其越来越微观、抽象。

想象一下,你的微服务团队被要求编写一个针对数据库的技术登录微服务,大致如下:

复制代码
@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;
}
// 棒棒的,登录成功!
// 设置 cookies, 做些你想做的事
return true;
}
}

现在,你的团队可能觉得(甚至可能说服对方):这太简单、太无聊了,我们不写什么登录,而是要写真正酷炫的 UserStateChanged 微服务(没有任何实际、切实的业务需求)。

而且,Java 现在都已经过时了,让我们用 Erlang 编写 UserStateChanged 微服务吧。让我们试着用用红黑树,因为  Steve Yegge  这篇文章写过,想去谷歌工作就得对这些了如指掌。

从集成、维护和整个项目的角度来看,这与在同一个大型独体应用中编写一堆意大利面式的代码一样糟糕。

这例子是虚构的吧?有些夸大其词吧?是的。

但不幸的是,在现实生活中这也并不少见。

部件更小,则了解更片面

作为一名开发人员,即使你只负责独立的微服务 [95:login-101:updateUserProfile],也需要理解整个系统及其流程和工作流。

当然,这取决于你们组织的信任和沟通水平,如果大家各自为战,如果整个微服务链的不确定哪个环节出现了故障,可能很多人只会耸耸肩说与我无关,甚至互相指责,没有人去承担整体责任。

这是一个实实在在的问题,实际上 n 个孤立的部分是很难理解的,很难弄清楚它们在全局中的位置。

沟通与维护

下面来聊聊最后一个问题:沟通和维护。显然,这个问题在很大程度上取决于公司规模,一般来说:规模越大,问题就越大。

  • 谁正在使用第 47 号微服务?
  • 他们是不是刚刚部署了一个新的、不兼容的微服务版本?这些情况记在哪里了?
  • 我需要向谁申请新功能?
  • 在 Max 离开公司之后,谁来维护 Erlang 微服务呢?
  • 我们所有的微服务团队不仅使用的编程语言不一样,而且上班的时间也不一样!我们如何恰当地协调?

Java微服务实用指南(二)

总体上,本节所述问题与应用 DevOps 遇到的问题类似,在更大的、甚至可能是国际化的公司中,全面推广微服务在沟通方面也会带来大量额外的挑战。作为一个公司,你需要为此做好准备。

结语

读完这篇文章后,你可能会得出这样的结论:笔者强烈建议不要使用微服务。这并不完全正确——笔者主要是想强调那些在微服务热潮中被遗忘的要点。

微服务就像一个钟摆

全面使用 Java 微服务是钟摆的一端。另一端可能是一个有着数百个还不错的老式 Maven 模块的大型独体应用。你必须找到正确的平衡点。

特别是在全新的项目中,没有什么可以阻止你采用更保守的、大型独体应用式的方法,构建更少的、定义更好的 Maven 模块,而不是立即开始使用 20 个云就绪的微服务。

微服务会产生大量额外的复杂度

请记住,你拥有的微服务越多,而同时拥有的真正强力的 DevOps 越少(注意,只是执行一些 Ansible 脚本或在 Heroku 上部署都不算),以后在生产环境中遇到的问题就越多。

阅读本指南中常见的 Java 微服务问题部分就已经很令人疲惫了。接下来,还得考虑为所有这些基础设施挑战实现解决方案。你会突然意识到,这些都与业务编程(你能得到回报的东西)无关,只是将更多的技术应用于更多的技术。

Siva 在他的博客上对此做了完美总结:

如果团队花了 70% 的时间在搭建、配置现代基础设施,而花在实际业务逻辑上的时间却只有 30%,这种糟糕的感觉简直难以言表。
——Siva Prasad Reddy

那么应该使用 Java 微服务吗?

为了回答这个问题,我想厚着脸皮以一个谷歌式的面试题来结束这篇文章。如果你基于经验知道这个问题的答案,那么你可能已经做好了使用微服务的准备,即使这个问题看起来似乎与微服务无关。

场景

假设你有一个单独运行在最小的 Hetzner 专用机上的 Java 大型独体应用。同样,数据库服务器也运行在类似的一台 Hetzner 机器上。

再假设,你的 Java 大型独体应用能够处理诸如用户注册之类的工作流,并且每个工作流只会产生几次(小于 10)数据库查询,而不是数百次。

问题

你的 Java 大型独体应用(连接池)应该打开多少个连向数据库服务器的数据库连接?

你认为你的大型独体应用大致上可以扩展到多少活跃的并发用户?为什么?

如果您已经有答案了,请在下方留言!

相关链接:

Java Microservices: A Practical Guide

Java 微服务实用指南(一)

译者简介:

冬雨,小小技术宅一枚,从事研发过程改进及质量改进方面的工作,关注编程、软件工程、敏捷、DevOps、云计算等领域,非常乐意将国外新鲜的 IT 资讯和深度技术文章翻译分享给大家。

阅读数:71 发布于:2020 年 6 月 2 日 08:26

评论

发布
暂无评论