作者 | MarcoBehler 策划 | 田晓旭 本文将为大家介绍 Java 微服务的常见问题, Java 微服务框架的选型,以及微服务实践常遇到的挑战。 1 常见的 Java 微服务问题
让我们看看在 Java 方面的特定微服务问题,例如具体的类库以及一些更抽象的东西。
如何使 Java 微服务具有弹性?回顾一下,在构建微服务时,你实际上是用 同步 HTTP 调用 或 异步消息传递 来进行 JVM 方法调用的。这种方式虽然基本上可以保证方法调用的执行(JVM 突然关闭除外),但是一般网络调用并不可靠,有时它会因为各种原因突然中断工作,例如网络故障或拥塞、正在实施新的防火墙规则、消息代理崩溃了等等。
我们来看一个典型的 BillingService 示例,以做进一步的了解。
HTTP / REST 弹性模式
假设顾客可以在你公司的网站上购买电子书,这时你只需实现一个计费微服务,线上商店可以调用它来生成实际的 PDF 发票。
现在,我们将通过 HTTP 进行同步调用。(异步调用该服务更为合理,因为从用户的角度看,不必即时生成 PDF。但我们想在下一节中重用这个示例,看看它们之间的区别。)
@Serviceclass BillingService {@Autowiredprivate 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 的版本兼容性。如果夸张一点说,你甚至需要考虑服务器在处理请求过程中被核武器攻击的可能性。
另外,有一种“警告”应该引起注意,那就是延迟。也许正在响应的微服务硬盘已经满了,响应时间从之前的 50ms,变成了 10 秒。如果你正在承受一定的负载,更要引起注意,若 BillingService 不再响应,将在你的系统中开始产生级联反应。在处理延迟和容错方面,Netflix 的 Hystrix 是一个流行的类库,建议大家可以研究一下。
消息传递弹性模式
我们再来看看异步通信,如果使用 Spring 和 RabbitMQ 进行消息传递,那么 BillingService 代码就可能是下面这样的。为了创建发票,我们向 RabbitMQ 消息代理发送一条消息,该代理有一些 worker 在等待新消息。这些 worker 创建 PDF 发票并将它们发送给相应的用户。
@Serviceclass BillingService {@Autowiredprivate RabbitTemplate rabbitTemplate; public void bill(User user, Plan plan) {Invoice invoice = createInvoice(user, plan);// 将 invoice 转换为 json 串,并将其作为消息体rabbitTemplate.convertAndSend(exchange, routingkey, invoice);// ...}}这时,潜在的错误和上面可能有所不同了,不再是像同步 HTTP 通信那样立即获得 OK 或 ERROR 的响应。
我的消息由 worker 传递和消费了吗?还是丢失了?(用户没有得到发票)。
我的消息只传递了一次吗?还是发送了多次,只处理了一次?(用户将得到多张发票)。
配置:从“我是否使用了正确的路由键 / 交易名称”到“是否正确设置和维护了消息代理,或者它的队列是否已经满了?”(用户没有得到发票)。
每个异步微服务弹性模式都是不一样的,具体实现思路还要取决于目前正在实际使用的消息传递技术。如果你正在使用 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 毫秒内启动或停止尽可能多的微型容器)的顾虑。
然而,问题是(人为的)裸金属启动时间和重新部署时间对项目的整体成功几乎没有什么影响,远远比不上强大的框架生态系统、强大的文档、社区和强大的开发人员技能。
如果截止到现在:
你让 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 调用。
哪些代理最适合异步 Java 消息传递?做异步消息传递时,你可能首先会想到 ActiveMQ (Classic 或 Artemis)、RabbitMQ 或 Kafka。没错,这是很流行的选择。
但我也有一些自己的观点,仅供大家参考:
ActiveMQ 和 RabbitMQ 都是传统的、功能完备的消息代理。它们假设代理相当聪明,而消费者很愚蠢。
ActiveMQ 历来都有着易于嵌入 (用于测试) 的优势,可以使用 RabbitMQ/Docker/TestContainer 来迁移。
Kafka 不是一个传统的代理。相反,它本质上是一个相对“愚蠢”的消息存储(比如日志文件),需要更聪明的消费者来处理。
一般来说,在选择代理时要尽量排除任何人为的性能原因。曾经有一段时间,有些团队和在线社区对 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 的日志文件,或许,你希望看到的是合并起来的日志文件。
在实际工作中,你可以找到各种方法:
由一名系统管理员写一些脚本,从不同的服务器收集和合并日志文件到一个文件中,并把它们放在 FTP 服务器上以供下载。
在 SSH 会话中并行执行 cat/grep/unig/sort 一组指令。你可以告诉你的经理:亚马逊 AWS 内部就是这么做的。
使用像 Graylog 或 ELK Stack (Elasticsearch, Logstash, Kibana) 之类的工具。
微服务之间如何找到彼此?到目前为止,我们一直假设我们的微服务互相认识,知道它们对应的 IP。目前,更多的是静态设置。因此,我们的银行大型单体应用(其 ip 为 192.168.2001)知道它必须与风险服务器(其 ip 为 192.168.2002)进行通信,这些都硬编码在一个属性文件中。
然而,你可以让它们更加灵活一些:你不用再和微服务一起来部署 application.properties,而是使用一台 云配置服务器,所有的微服务都从那里获取配置。
因为你的服务实例 可能会动态更改它们的位置(设想一下,Amazon EC2 实例获得动态 IP,使你可以灵活地自动伸缩云计算),所以你可能很快就会想到要有一个服务注册中心,它知道你的服务位于哪个 IP 中,并且可以相应地进行路由。
既然一切都是动态的,那么新的问题就出现了,比如自动选举 leader :谁是处理某些任务的专家,如何保证不会重复处理?当 leader 出现问题时,谁来接替它?和谁一起?
概括来说,这就是所谓的微服务编排,它本身就是另一个很大的主题。
像 Eureka 或 Zookeeper 这样的类库试图“解决”这些问题,比如客户端或路由器知道哪些服务在哪里是可用的。而另一方面,它们也带来了大量额外的复杂度。
如何使用 Java 微服务进行授权和身份验证?这是另一个值得探讨的主题。你可以选择硬编码 HTTPS 基本认证和自编码安全框架,以及在自己的授权服务器上运行 Oauth2。
如何确保所有环境看起来都是一样的?适用于非微服务部署的情况,也适用于微服务部署。你可以尝试 Docker/Testcontainers 和脚本 /Ansible。
尽量保持简单。
不是问题:YAML 缩进的故事告别特定的类库问题,让我们来快速了解一下 Yaml。它是“配置即代码”的事实上的标准文件格式。从简单的 Ansible 到强大的 Kubernetes,这些工具都支持这种格式。
要亲身体验 YAML 缩进之痛,你可以先自己尝试编写一个简单的 Ansible 文件,尽管不同的 IDE 有着各种级别的支持,看看你需要反复修改多久,才能使缩进正常无误。然后,再回过头来把这份文章看完。
Yaml:- is:- so- great 2 微服务的概念性挑战除了特定的 Java 微服务的问题之外,任何微服务项目都会带来一些问题。这些问题更多地出自于组织、团队或管理的视角。
前后端不匹配在许多微服务项目中都会出现一种前后端微服务不匹配的情况。这是指什么呢?
在传统老式的大体单体应用中,前端开发人员只有一个获取数据的特定来源。在微服务项目中,前端开发人员突然有了 n 个获取数据的数据源。假设你正在构建某个 Java-IoT 微服务项目,正在监控一些机器,比如欧洲各地的工业烤箱。这些烤箱会定期向你发送温度等状态更新。
现在,你可能希望能够在管理界面中搜索烤箱,可能需要用到“搜索烤箱”微服务。由于后台同事对领域驱动设计或微服务条款的解读,可能“搜索烤箱”微服务只返回烤箱的 id,而不返回其他数据,如类型、模型或位置。
为此,前端开发人员可能需要执行一次或多次额外的调用(取决于你的分页实现),使用从第一个微服务获得的 id 来调用“获取烤箱细节”微服务。
虽然这只是一个简单的(但它确实源自于真实的项目)示例,但它说明了以下问题:
在现实生活中,超市被广泛接受是有原因的。因为你不必去 10 个不同的地方去买蔬菜、柠檬水、冷冻披萨和卫生纸,而是去一个地方就够了。它更简单、更快速。前端开发人员和微服务也是如此。
管理期望一些开发人员、编程杂志或云公司在大力推动微服务时,也带来了一个负作用:
管理层形成了这样一种印象:现在,你可以向项目中注入无限的开发人员了,因为开发人员现在可以完全独立地工作,每个人都可以在他们自己的微服务开展工作。只需要在最后(即将要上线的时候)进行一些微小的集成即可。
下面,我们来看看为什么这种心态会成为一个问题。
部件更小,未必更好把一个部件拆成 20 份,未必会得到 20 件更好的部件。纯粹从技术质量的角度来看,这可能意味着你的各个服务要执行 400 个 Hibernate 查询,从而跨过各层从数据库中查出一个用户,而且代码也更难维护了。
再来回顾一下 Simon Brown 的话,如果人们不能正确地构建大型单体应用,他们也很难构建正确的微服务。
特别是在许多微服务项目中,总是在事后才想起弹性,每件事情都是在上线后实际发生了才放马后炮,看看那些在现场运行的微服务,总让人觉得有点不大放心。
原因其实也很简单,就是因为 Java 开发人员通常对弹性、网络和其他相关主题不感兴趣,没有经过适当的培训。
部件更小,则更技术化此外,有一个很不好的趋势是,用户故事越来越技术化(因此也越来越愚蠢),于是其越来越微观、抽象。
想象一下,你的微服务团队被要求编写一个针对数据库的技术登录微服务,大致如下:
@Controllerclass 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 微服务吧。从集成、维护和整个项目的角度来看,这与在同一个大型单体应用中编写一堆意大利面式的代码一样糟糕。
这例子是虚构的吧?有些夸大其词吧?是的。但不幸的是,在现实生活中这也并不少见。
部件更小,则了解更片面作为一名开发人员,即使你只负责独立的微服务 [95:login-101:updateUserProfile],也需要理解整个系统及其流程和工作流。
当然,这取决于你们组织的信任和沟通水平,如果大家各自为战,如果整个微服务链的不确定哪个环节出现了故障,可能很多人只会耸耸肩说与我无关,甚至互相指责,没有人去承担整体责任。
这是一个实实在在的问题,实际上 n 个孤立的部分是很难理解的,很难弄清楚它们在全局中的位置。
沟通与维护下面来聊聊最后一个问题:沟通和维护。显然,这个问题在很大程度上取决于公司规模,一般来说:规模越大,问题就越大。
谁正在使用第 47 号微服务?
他们是不是刚刚部署了一个新的、不兼容的微服务版本?这些情况记在哪里了?
我需要向谁申请新功能?
在 Max 离开公司之后,谁来维护 Erlang 微服务呢?
我们所有的微服务团队不仅使用的编程语言不一样,而且上班的时间也不一样!我们如何恰当地协调?
总体上,本节所述问题与应用 DevOps 遇到的问题类似,在更大的、甚至可能是国际化的公司中,全面推广微服务在沟通方面也会带来大量额外的挑战。
3 结语读完这篇文章后,你可能会得出这样的结论:笔者强烈建议不要使用微服务。这并不完全正确——笔者主要是想强调那些在微服务热潮中被遗忘的要点。
微服务就像一个钟摆全面使用 Java 微服务是钟摆的一端。另一端可能是一个有着数百个还不错的老式 Maven 模块的大型单体应用。你必须找到正确的平衡点。
特别是在全新的项目中,没有什么可以阻止你采用更保守的、大型单体应用式的方法,构建更少的、定义更好的 Maven 模块,而不是立即开始使用 20 个左右的微服务。
微服务会产生大量额外的复杂度请记住,你拥有的微服务越多,同时拥有的真正强力的 DevOps 就越少(注意,只是执行一些 Ansible 脚本或在 Heroku 上部署都不算),以后在生产环境中遇到的问题就越多。
常见的 Java 微服务问题部分就已经很令人疲惫了。接下来,还得考虑为所有这些基础设施挑战实现解决方案。你会突然意识到,这些都与业务编程(你能得到回报的东西)无关,只是将更多的技术应用于更多的技术。
Siva 在他的博客上对此做了完美总结:
如果团队花了 70% 的时间在搭建、配置现代基础设施,而花在实际业务逻辑上的时间却只有 30%,这种糟糕的感觉简直难以言表。——Siva Prasad Reddy
那么应该使用 Java 微服务吗?为了回答这个问题,我想厚着脸皮以一个谷歌式的面试题来结束这篇文章。如果你基于经验知道这个问题的答案,那么你可能已经做好了使用微服务的准备。
场景
假设你有一个单独运行在最小的 Hetzner 专用机上的 Java 大型单体应用。同样,数据库服务器也运行在类似的一台 Hetzner 机器上。
再假设,你的 Java 大型单体应用能够处理诸如用户注册之类的工作流,并且每个工作流只会产生几次(小于 10)数据库查询,而不是数百次。
问题
你的 Java 大型单体应用(连接池)应该打开多少个连向数据库服务器的数据库连接?
你认为你的大型单体应用大致上可以扩展到多少活跃的并发用户?为什么?
如果您已经有答案了,请在下方留言!
拓展阅读
相关阅读Java 微服务实用指南(一)
点个在看少个 bug 👇