在当前的计算机领域中,CPU 单核性能的增长已逐渐停滞,而业务问题的复杂度却不断上升。为了更好地解决这一冲突,在 CPU 中增加核数成为了常见的应对方案。在这种情况下,并行设计的重要性日益凸显,它能够充分发挥硬件多核的运行性能。
然而,要通过并行设计将计算负载均衡到每个 CPU 核上,并将软件性能提升至最大化,面临着诸多挑战。例如,并行拆分不合理可能导致产品性能不可控甚至恶化,程序员的串行编程惯性思维使得并发同步互斥实现中的故障难以定位,进而导致产品在较长时间内处于不可用状态。
为了应对这些挑战,我们首先需要了解并行计算模型。在面对具体的业务问题时,我们需要将其拆分成可并行的逻辑单元,并实现同步交互。并行计算模型可以帮助我们建立对并发系统的抽象模型和基本概念的认识。
具体来说,并行计算模型可以抽象为两个层次:一是由结构数据和相应的计算逻辑组成并发执行单元,通过组合实现更复杂的业务;二是基于各种手段(如内存、互斥量、消息队列、数据库等)对并发执行单元计算的结果进行交互同步,保证业务计算结果的确定性。
需要注意的是,并行执行单元的粒度可大可小,不仅仅局限于线程。在设计并发架构时,我们应根据处理的特定领域问题,选择合适的并行执行单元粒度,并选择或定制实现相应的并发调度框架。
接下来,我们介绍六种针对不同业务问题的典型并行设计架构模式。
第一种是任务线性分解架构,它是按照计算逻辑维度进行确定性拆分的并行架构设计模式。当单核处理性能存在瓶颈时,通过依赖分析,发现计算逻辑相对独立,便可按照计算逻辑拆分成独立的并行执行单元,从而提升性能。这种架构模式适用于业务逻辑确定性的场景,但需要注意消除或隔离数据依赖、确定执行单元工作量以及任务线性拆分扩展性差等问题。
图片
当我们观察图中的左侧时,可以看到计算逻辑 A、B 和 C 在同一个数据块上进行操作。通过依赖性分析,我们可以发现 A、B 和 C 这三个计算逻辑是相对独立的。因此,当单核处理性能达到瓶颈时,可以通过计算逻辑维度进行并行拆分来提升性能。
图的右侧展示了通过这种拆分形成的三个独立并行执行单元。这种方式可以映射到两个硬件线程上,从而减少处理时延。
补充说明:对于 Java 工程师来说,需要显式地将任务映射到硬件线程的情况可能比较少;但对于嵌入式工程师来说,任务与硬件线程的映射绑定是并行设计中的一个关键环节。
实际上,在许多业务领域中,都存在需要根据同一个事件或数据并行触发多个任务的场景。例如,在电商购物场景下,一笔交易成功后,系统会同时触发多项任务,包括生成邮件通知责任人、进行多维度数据统计及更新等。
当这些触发的业务计算逻辑相互独立时,可以创建多个并行执行单元,分别处理拆分后的不同子任务,并根据各执行单元的工作量大小,将其与具体的硬件线程建立映射和绑定关系。
这种并行设计架构相对简单,适用的业务场景也比较多。例如,在观察者模式中处理类似问题、在消息队列中解决一对多通信的业务问题等,都隐含着任务线性并发的可能性。
总的来说,任务线性分解架构适用于业务逻辑确定性的场景。在实际应用中需要注意以下几点:
- 在并行执行单元间,通过一些手段消除或隔离数据依赖,例如使用 ThreadLocal 变量,通过数据冗余来消除依赖。
- 执行单元的工作量较为确定,便于与硬件线程建立绑定和映射关系。
- 进行并行拆分时,需要先了解全局的业务功能。任务线性拆分的扩展性相对较差。
通过这些方法,任务线性分解架构能够在特定场景下有效提升性能。
第二种是任务分治架构,它是按照计算逻辑进行动态拆分的并行架构设计模式。在许多业务场景中,计算逻辑并非全局确定,需要根据场景判断是否拆分成更小的子问题进行求解。在这种情况下,需要动态创建任务,并借助任务队列来管理执行任务。这种架构模式的使用场景相对较少,但在一些实时性要求高、性能要求苛刻的场景下,如股票交易等,任务队列以及硬件资源绑定关系通常需要单独设计实现。
图片
通过图中的左侧,我们可以发现,在许多业务场景中,计算逻辑并不是全局确定的。在计算过程中,有些业务需要根据具体场景来判断是否将其拆分成更小的子问题进行求解。例如,A 计算过程中会拆分出 2 个 B 子问题,而这 2 个子问题在计算过程中又需要进一步拆分为 3 个 C 子问题来求解。
针对这种动态变化的场景进行并行设计时,不能在系统运行前完成任务的拆分,而是需要在运行时动态创建任务,并借助任务队列来管理和执行这些任务。执行线程可以从队列中拉取任务。在并行执行单元间,数据依赖可以通过一些手段进行消除或隔离,比如利用 ThreadLocal 变量或通过数据冗余来处理。这样,执行单元的工作量相对确定,便于与硬件线程建立绑定和映射关系。
通常情况下,在进行并行拆分时,需要先了解全局的业务功能,尽管任务线性拆分的扩展性相对较差。这种并行设计架构模式的使用场景相对少一些。
例如,我之前基于 Akka 框架设计开发了一款智能对话引擎。在这个对话引擎系统中,用户对话的语义信息是有限的。当收到某个用户对话数据时,在特定上下文中,其语义可能只是全局语义中的一个较小子集。因此,我需要在这个子集中选择语义匹配率最高的一个进行回复。
每个语义匹配率的计算逻辑与对话数据是独立的。为了实现用户对话消息的快速回复,我需要在特定上下文下,动态创建多个并行执行单元,分别计算语义匹配度,然后汇总选择匹配率最高的一个。这种实现框架基于任务分治架构进行设计。
实际上,在 Java 的 java.util.concurrent.Executors 以及 Akka 等框架中,已经内置了并发任务队列,并支持与 CPU 等硬件线程的映射,从而满足大部分场景下的业务需求。然而,在一些实时性要求较高、性能要求非常苛刻的场景下,比如股票交易,任务队列以及硬件资源的绑定关系通常需要单独设计实现。
1.数据几何分解架构
数据几何分解和任务线性分解架构风格相似,但几何分解架构的主要特点是在不同的数据上执行相同的计算逻辑。正如图中右侧所示,拆分成不同的并行计算单元后,计算逻辑是相同的(同色表示),但数据是不同的(不同颜色表示)。
在互联网微服务场景中,业务关键数据通常记录在数据库表中。当数据规模较大时,需要对数据库表进行分表策略保存,这就是一种典型的数据几何分解方式。针对这种场景,当接收到业务数据库表查询分析请求时,需要基于相同的计算逻辑和不同的数据库分表组合,创建多个执行单元并行计算以提升性能。
在业务发展过程中,待处理数据规模增加是一个非常重要的变化方向,通过弹性计算资源提升业务处理能力是核心关注点之一。数据几何分解架构是一种解决此类问题的典型方法,具有很多优点,应用非常广泛。
最后,让我们看看数据几何分解架构的隐式约束条件:
- 扩展性强:采用数据几何分解架构,其可支持的扩展性会比较强。
- 适用于 SPMD 架构:这种性能架构模式比较适合于 SPMD(Single Program Multi Data)架构。SPMD 架构使用一套相同的代码实体并行运行在多个硬件线程上,这样用户只需要管理一套代码实体即可,成本比较低。
- 独立更新:在数据几何分解架构中,不同并行计算单元的更新数据是独立的。
2.递归数据结构
图片
从图中可以看到,业务处理的数据是树状或图状组织的,这表明线性几何拆分数据会比较困难。
因此,在实际应用中,需要在遍历过程中动态创建任务,然后逐步合并每个中间计算单元的运算结果,最终计算得到结果,如图中右侧所示。
MongoDB 是目前应用非常广泛的开源文档数据库,它支持将灵活的 JSON 格式业务数据保存到数据库中。在对业务记录 JSON 格式内的多个字段进行数据分析时,代码需要递归遍历 JSON 中所有嵌套字段并进行分析计算。为了最大化并发执行,减少处理时延,可以采用递归数据架构模式,在递归遍历字段过程中动态创建相应字段分析的并行执行单元。
这种架构的应用场景也相对较少,主要用于非规则结构数据的计算分析,比如树状结构、有向图等数据结构。
3.数据流交互架构
图片
从上图中我们可以发现,这种业务场景的典型特征是计算单元的确定性较强,可以静态规划与硬件线程的映射关系。设计的核心是如何高效实现并发计算单元间的信息交互。
具体如何实现呢?让我举个例子。
在大数据领域中,ETL(Extract-Transform-Load)是一个非常典型的场景,它描述了将数据从来源端经过抽取(extract)、转换(transform)和加载(load)至目的端的过程。在这种架构模式下,计算任务单元需要动态创建,且工作量不确定。
一般来说,递归数据架构对应的算法是递归算法。在这种架构中,一个计算单元的输出正好是另一个计算单元的输入,消息交互是单向确定性的。同时,业务场景中还会源源不断接收到新的输入,需要使用相似的计算策略进行处理。
业务数据处理需求通常由多个 ETL 阶段组合完成,因此在这种场景下,使用数据流交互架构会比较合适。此外,在嵌入式领域,网络协议栈的报文处理、不同协议栈解析特定头部字节、完成业务处理后透传给下一层,也是使用数据流交互架构的典型场景。
在数据流交互架构中,不同并行执行单元的处理消息速率通常不一致,因此需要借助消息队列缓存来协调。在 Java 中,各种并发的 BlockingQueue 就是这种消息队列的一种实现方式,即典型的生产者消费者模型的处理方法。
通过这些手段,可以高效地实现并发计算单元间的信息交互,满足动态创建任务和处理不确定工作量的需求。
4.异步交付架构
图片
从图上我们可以发现,该业务场景的典型特点如下:
这种系统的计算逻辑可能需要进行全局拆分,也可能无法拆分,需要根据实际情况进行处理。
让我给你举个例子。在微服务架构中,微服务在完成一个 REST 请求业务功能的过程中,可能需要进行多次数据库操作,还可能需要多次调用其他微服务提供的 REST 接口。为了充分发挥性能,当我们将业务逻辑拆分为多个并行执行单元后,并行执行单元间的运行开销差异较大时,可以使用异步交互来实现业务功能。
请注意,要想最大化地发挥这种架构的性能,还需要做到以下一点:并行执行单元能够动态灵活地映射到特定的硬件 CPU 核上。比如,Node.js 后端业务中的 async 机制和 Java 语言中的 Future 并发机制,都是支持异步交互架构的较好语言机制。
通过这些方法,可以在微服务架构中实现高效的异步交互,提高系统性能。