本文将为大家介绍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调用可能会得到什么结果。概括地说,你可能会得到三个结果:
任何程序都需要做错误处理,而不仅仅是处理最顺利的情况。没错,微服务也是如此,即使你一开始进行单个微服务部署和发布时,已经格外注意保持了所有已部署API的版本兼容性。
如果你想杜绝所有的情况,你还必须考虑服务器在处理请求的过程中被核武器攻击的可能性,可能此时你希望该请求被重新路由到另一个工作实例。
有一种“警告”应该引起注意,那就是延迟。也许正在响应的微服务硬盘已经满了,响应时间不是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 响应。你大概会收到以下三种错误情况:
同样,详细介绍每个异步微服务弹性模式已经超出了本指南的范畴。本文的侧重点是指出一个正确的方向,具体思路还要取决于你正在实际使用的消息传递技术。例如:
一方面,你已经有了广受认可、非常流行的选择,比如Spring Boot,这使构建.jar文件变得非常容易,将这些文件与Tomcat或Jetty之类的嵌入式web服务器一起提供,可以立即在任何地方运行。这非常适合构建微服务应用程序。
然而,最近出现了一些专用的微服务框架,它们在一定程度上受到了诸如响应式编程、Kubernetes或GraalVM等并行开发的启发。
举几个例子:Quarkus、 Micronaut、Vert.x、Helidon。
最终,你将必须做出自己的选择,但这篇文章可以提供一些也许不太常规的建议:
除了Spring Boot之外,所有的微服务框架通常都标榜自己运行速度极快,启动速度极快,内存占用率极低,可以无限地扩展,并使用很具视觉冲击力的图表来与Spring Boot这个庞然大物进行比较。
这消除了那些维护遗留项目(这些遗留项目有时需要几分钟的时间来启动)的开发人员的顾虑,以及云原生开发人员(他们希望在50毫秒内启动或停止尽可能多的微型容器)的顾虑。
然而,问题是(人为的)裸金属启动时间和重新部署时间对项目的整体成功几乎没有什么影响,远远比不上强大的框架生态系统、强大的文档、社区和强大的开发人员技能。
你必须要认识到这一点。
如果截止到现在:
而且,在上面添加额外的微服务挑战可不仅仅是启动一个空的hello world,弹性、网络、消息传递、DevOps和基础设施将对你的项目产生更大的影响。对于开发期的热部署,你最终可能需要看看JRebel或DCEVM之类的解决方案。
回头看一下Simon Brown的那句名言:如果你不能构建(快速且高效)的大型独体应用,那么也很难构建(快速且高效)的微服务。
所以,明智地选择你的框架吧。
接下来将站在实用的角度介绍HTTP REST API的调用。在底层技术方面,你可能会用到以下其中一个HTTP客户端类库:
Java自己的HttpClient(自Java 11开始提供)、Apache的HttpClient或OkHttp。
注意,我在这里说“可能”,是因为从古老且仍然好用的JAX-RS客户端到现代的WebSocket客户端,还有无数种其他方式。
在任何情况下,都应选用合适的HTTP客户端,而不是自己在那里摆弄HTTP调用。为此,你需要从一开始起步时先了解一下OpenFeign项目及其文档。
开始做异步消息传递时,你可能会想到ActiveMQ (Classic或Artemis)、RabbitMQ或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。
特别说明:这绝不是一份大而全的列表,如果里面遗漏了你最喜欢的工具,欢迎留言指出,我们将在下一版指南中予以介绍。
使用微服务进行日志记录是一个有趣且相当复杂的主题。现在,你会有n个日志文件,而不仅仅是一个可以less或grep的日志文件,或许,你希望看到的是合并起来的日志文件。
这篇文章很不错,在开启日志生态系统之旅前,推荐先阅读一下,特别是关于微服务的集中式日志部分。
在实际工作中,你可以找到各种方法:
到目前为止,我们一直假设我们的微服务都互相认识,知道它们对应的IP。目前,更多的是静态设置。因此,我们的银行大型独体应用(其ip为192.168.2001)知道它必须与风险服务器(其ip为192.168.2002)进行通信,这些都硬编码在一个属性文件中。
然而,你可以让它们更加灵活一些:
概括来说,这就是所谓的微服务编排,它本身就是另一个很大的主题。
像Eureka或Zookeeper这样的类库试图“解决”这些问题,比如客户端或路由器知道哪些服务在哪里是可用的。而另一方面,它们也带来了大量额外的复杂度。
只要问问那些做过ZooKeeper 配置的人就知道了。
这是另一个值得探讨的主题。你可以选择硬编码HTTPS基本认证和自编码安全框架,以及在自己的授权服务器上运行Oauth2。
适用于非微服务部署的情况,也适用于微服务部署。你可以尝试Docker/Testcontainers和脚本/Ansible。
尽量保持简单。
告别特定的类库问题,让我们来快速了解一下Yaml。它是“配置即代码”的事实上的标准文件格式。从简单的Ansible到强大的Kubernetes,这些工具都支持这种格式。
要亲身体验YAML缩进之痛,你可以先自己尝试编写一个简单的Ansible文件,尽管不同的IDE有着各种级别的支持,看看你需要反复修改多久,才能使缩进正常无误。然后,再回过头来把这份指南看完。
Yaml:
- is:
- so
- great
很遗憾,本指标当前版本暂未涉及这些主题,敬请继续关注。
除了特定的Java微服务的问题之外,任何微服务项目都会带来一些问题。这些问题更多地出自于组织、团队或管理的视角。
在许多微服务项目中都会出现一种我称之为前后端微服务不匹配的情况。这是指什么呢?
在传统老式的大体独体应用中,前端开发人员只有一个获取数据的特定来源。在微服务项目中,前端开发人员突然有了n个获取数据的数据源。
假设你正在构建某个Java-IoT微服务项目。你正在监控一些机器,比如欧洲各地的工业烤箱。这些烤箱会定期向你发送温度等状态更新。
现在,你可能希望能够在管理界面中搜索烤箱,可能需要用到“搜索烤箱”微服务。由于后台同事对领域驱动设计或微服务条款的解读,可能“搜索烤箱”微服务只返回烤箱的id,而不返回其他数据,如类型、模型或位置。
为此,前端开发人员可能需要执行一次或多次额外的调用(取决于你的分页实现),使用从第一个微服务获得的id来调用“获取烤箱细节”微服务。
虽然这只是一个简单的(但它确实源自于真实的项目)示例,但它说明了以下问题:
在现实生活中,超市被广泛接受是有原因的。因为你不必去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个孤立的部分是很难理解的,很难弄清楚它们在全局中的位置。
下面来聊聊最后一个问题:沟通和维护。显然,这个问题在很大程度上取决于公司规模,一般来说:规模越大,问题就越大。
总体上,本节所述问题与应用DevOps遇到的问题类似,在更大的、甚至可能是国际化的公司中,全面推广微服务在沟通方面也会带来大量额外的挑战。作为一个公司,你需要为此做好准备。
读完这篇文章后,你可能会得出这样的结论:笔者强烈建议不要使用微服务。这并不完全正确——笔者主要是想强调那些在微服务热潮中被遗忘的要点。
全面使用Java微服务是钟摆的一端。另一端可能是一个有着数百个还不错的老式Maven模块的大型独体应用。你必须找到正确的平衡点。
特别是在全新的项目中,没有什么可以阻止你采用更保守的、大型独体应用式的方法,构建更少的、定义更好的Maven模块,而不是立即开始使用20个云就绪的微服务。
请记住,你拥有的微服务越多,而同时拥有的真正强力的DevOps越少(注意,只是执行一些Ansible脚本或在Heroku上部署都不算),以后在生产环境中遇到的问题就越多。
阅读本指南中常见的Java微服务问题部分就已经很令人疲惫了。接下来,还得考虑为所有这些基础设施挑战实现解决方案。你会突然意识到,这些都与业务编程(你能得到回报的东西)无关,只是将更多的技术应用于更多的技术。
Siva在他的博客上对此做了完美总结:
如果团队花了70%的时间在搭建、配置现代基础设施,而花在实际业务逻辑上的时间却只有30%,这种糟糕的感觉简直难以言表。 ——Siva Prasad Reddy
为了回答这个问题,我想厚着脸皮以一个谷歌式的面试题来结束这篇文章。如果你基于经验知道这个问题的答案,那么你可能已经做好了使用微服务的准备,即使这个问题看起来似乎与微服务无关。
场景
假设你有一个单独运行在最小的Hetzner专用机上的Java大型独体应用。同样,数据库服务器也运行在类似的一台Hetzner机器上。
再假设,你的Java 大型独体应用能够处理诸如用户注册之类的工作流,并且每个工作流只会产生几次(小于10)数据库查询,而不是数百次。
问题
你的Java 大型独体应用(连接池)应该打开多少个连向数据库服务器的数据库连接?
你认为你的大型独体应用大致上可以扩展到多少活跃的并发用户?为什么?
如果您已经有答案了,请在下方留言!
相关链接:
Java Microservices: A Practical Guide
译者简介:
冬雨,小小技术宅一枚,从事研发过程改进及质量改进方面的工作,关注编程、软件工程、敏捷、DevOps、云计算等领域,非常乐意将国外新鲜的IT资讯和深度技术文章翻译分享给大家。
领取专属 10元无门槛券
私享最新 技术干货