几个月前,我们开始了一个新项目。我们的目标是设计一个可以处理许多并发连接的微服务。我们预测该应用程序将花费大量时间等待多个并行I / O操作。理想的体系结构解决方案似乎使用了非阻塞方法。经过简短的调查,我们决定使用Spring WebFlux作为主要框架。这是因为它基于无阻塞堆栈,具有出色的可伸缩性和灵活性。流stream形式的出色抽象看起来真的很方便使用,远比基于 Java NIO 或 Netty的 传统非阻塞代码要好得多。这就是为什么我们尝试基于Spring WebFlux实施该服务的原因。但是,一旦我们的开发工作开始,麻烦便随之而来。
新思维
如果您习惯于以传统的顺序方式编写代码,则可能会感到有些迷茫。那是我们开始该项目时的印象。有时候,简单的事情似乎很困难。最令人惊讶的是,即使是经验丰富的Java开发人员也不得不重新发明轮子。对于他们来说,这是一个新的编程范例,经过多年的不同编程方式转换到它并不容易。事实是,反应流可以使某些任务大大简化,但其他任务似乎要复杂得多。此外,以操作符序列的形式编写的代码,例如map或flatMap,不像传统的逐行阻塞方法调用那样容易理解。您不能简单地将任何一段代码提取到私有方法中以使其更具可读性。您必须在流上使用运算符序列。
我的观察是,反应灵敏的流stream对于思想开阔的开发人员而言不那么痛苦。虽然学习曲线绝对陡峭,但我认为值得花时间。
记录上下文信息
我们的技术要求之一是可追溯服务之间的对话。 相关ID模式 正是我们所需要的东西。简而言之:每条传入的消息都附加了一个唯一的文本值,它是在我们服务之外的某个位置生成的对话标识符。在我们的情况下,我们有两种类型的传入消息:
为了提供可追溯性,我们必须在每条日志行的旁边包括相关性ID,该ID由在给定对话上下文中的代码执行打印出来。如果参与对话的每个服务都打印一个“相关性ID”,则很容易找到与其相关的所有日志。日志聚合器很有帮助,因为它将所有日志收集在一个地方,提供了便捷的搜索功能。
最常用的日志记录库实现了“ 映射诊断上下文” (MDC)功能,该功能完全符合我们的需求。
在Servlet的世界中,我们将编写一个过滤器来处理每个传入的HTTP请求。过滤器将在请求处理程序(即Spring控制器)启动之前将Correlation ID放入MDC,并在请求处理完成后清理映射。它将完美地工作,但请记住,MDC使用线程相似性模式(实现为 ThreadLocal Java类)中,保存与单个线程关联的上下文信息。这意味着只能在单个线程从开始接受请求和到处理请求结束时使用。
不幸的是,Spring WebFlux不能那样工作。一个线程很可能有多个线程参与单个请求处理。那我们该怎么办呢?
幸运的是,Reactor库的作者实现了另一种保存上下文信息的机制 -Context 。它可用于在流的运算符之间传输相关ID。要将其与知名的日志记录库MDC链接,我们使用此处描述的解决方案: https://www.novatec-gmbh.de/en/blog/how-can-the-mdc-context-be-used-in-the-reactive-spring-applications/. 如果您不喜欢Kotlin语言,请查看Java中类似的实现: https://github.com/archie-swif/webflux-mdc
我们实现了这个想法,并且效果很好!我们只需要创建一个自定义 WebFilter ,即可从HTTP请求中读取Correlation ID并将其设置在响应流的上下文中。我们发现了一个缺点-由于隐式调用MdcContextLifter,堆栈跟踪变得更长了。
很长的堆栈跟踪
反应流的优点在于它们形成声明链,告诉发布者发出的每个元素发生了什么。这种抽象允许清晰地实现复杂的逻辑。如果代码按预期工作,则一切看起来都很光彩。您仅用几行代码就解决了一个非同寻常的问题,您为此感到自豪。
这种快乐一直持续到有人从生产中向您发送堆栈跟踪为止。您看到的绝大多数行都以Reactor.core.publisher(以及包含MdcContextLifter class的软件包中的其他行)开头,如果您实现了上一节中提到的上下文信息记录的变通方法。它不会告诉您任何信息,因为它是与您的代码无关的程序包。我会说这没用。
此外,我还记得我们遇到的一个非常严重的问题。用户抱怨该应用程序偶尔会返回错误,但我们无法弄清楚原因。日志没有任何有趣的内容,尽管我们知道应用程序会捕获所有可能的错误并使用stacktrace记录错误级别。
那是什么问题呢?我们使用Papertrail作为日志聚合器。将应用程序日志推送到Papertrail的转发器将日志条目的最大大小限制为100,000字节。这听起来绰绰有余,但在我们看来并非如此。Stacktrace更大,并且充满了不相关的信息,例如包reacte.code.publisher中的上述方法调用。如果Papertrail转发器看到很长的消息,则直接跳过它。这就是最重要的日志条目被忽略的原因。那我们做了什么?我们仅实现了对Logback的扩展,该扩展删除了包含有包的程序行,这些程序包从react.core.publisher开始,并将stacktrace修剪为90,000个字符。
我的结论是,需要进一步的努力来实现类似的功能,尤其是如果您不想看到不相关的信息并且认真考虑降低问题的风险时。
Swagger
我们希望向我们的服务用户提供发现RESTful API的能力。Swagger是最著名且最成熟的工具。不幸的是,当我们看到Spring WebFlux没有集成库时,我们再次感到惊讶。经过一番调查后,它发现集成方面的开发仍在进行中,公开Swagger UI的唯一方法是使用io.springfox:springfox-spring-webflux的快照版本。在生产就绪型应用程序中使用不稳定的库听起来很可怕,但是,另一方面,Swagger不是我们服务的关键组件,只有在非生产部署中才启用。我们使用快照版本已有六个多月的时间了,目前还没有稳定的版本。真是可悲。
数据库
如果您很幸运,并且拥有MongoDB,那么您就不必担心。有一个官方的反应式驱动程序可用。由于JDBC的阻塞性质,因此没有对关系数据库的任何反应式支持。这正是我们面临的问题。客户告诉我们,我们必须在MySQL风味中使用Aurora DB。最糟糕的是,当我们解决了上述所有问题时,它处于项目的后期,因此现在回到非反应堆已为时已晚。我们决定要做的是使用官方的MySQL驱动程序,因为我们熟悉它,并且我们可以非常快地发布该服务。我们意识到了所有局限性,包括缺乏对事务性的现成支持Spring中的注解,用于返回Mono / Flux的方法。目前,我们的服务根本不使用事务机制。但是我们知道潜在的后果。
幸运的是, R2DBC 项目正在积极开发中。目标之一是使SQL数据库与反应式API配合使用。它不是官方标准,因此我们怀疑在与其他库集成时会出现问题,因为JDBC是很多年以来众所周知的API。最近发布的Spring 5.2支持在后台使用R2DBC在Reactive Streams Publishers上进行反应式事务管理。听起来确实很有前途,这绝对值得一试,但是我们还没有任何经验。
总结