65.9K
CodeProject 正在变化。 阅读更多。
Home

分布式编程框架 -第一部分(摘要)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (15投票s)

2009年9月9日

CPOL

8分钟阅读

viewsIcon

52139

构建分布式程序,无需担心分发实现

引言

软件的健壮性和可扩展性在很大程度上取决于过程式、逻辑式、功能式和物理式组件的分离,以及每个组件内层(tiers)的分离。

本文将介绍一种通过识别和管理功能分离来抽象顺序代码的方法,并构建一个编写分布式计算机程序的框架,使其无需担心其分布和执行的实现。

本文的第一部分将侧重于该框架背后的理论,而第二部分将侧重于该模式的实现。

背景

阅读以下内容可能有所帮助:

  • n层架构
  • 分布式计算
  • 事件驱动架构
  • 面向服务架构
  • 多线程
  • 过程式编程语言 (C#)
  • 分布式共享内存 (DSM)

为什么要分布代码序列?

任务的分布需要从微观和宏观层面都有清晰的理解。为了形成更高阶任务而进行的抽象过程就需要这种理解。通过抽象任务,不仅可以通过引入分工来有效管理性能,还可以通过识别其内部关联性和依赖性来引入并行性。

http://upload.wikimedia.org/wikipedia/commons/thumb/c/c6/Distributed-parallel.svg/260px-Distributed-parallel.svg.png

如何做到?

最简单的形式,我们可以使用一个简单的数据结构,如 LinkedList<Action<I>>,它存储了一系列代码块,唯一的联系是它们的执行顺序。然后我们可以通过迭代这个 LinkedList 从第一个 Action<I> 到最后一个来执行这个动作列表。假设没有依赖关系,这与 ThreadPool 相同。如果这是过程式动作,那么它就等同于过程式编程。这是琐碎且不有趣的。

一个稍微更有趣的场景是使用 LinkedList<Func<I,O>> 来管道化函数,其中一个函数的输出被管道化到下一个函数的输入。这种情况的先决条件是所有函数都有一个输入参数(类似于 **ML** 或 **F#**)。同样,这种情况无法比传统方法获得任何优势。

我们真正想要什么?

我们真正想要的是通过异步执行、多线程执行和在多个处理器上并行执行来利用分布式架构。正如我们所知,在这些领域进行编码在各个方面都可能极其困难。更重要的是,能够不必担心这些架构,而只是编写好的代码。

摘要

为了实现这个概念,我们的数据结构不能再是 LinkedList,它暗示了执行顺序,而是我们需要明确定义每个函数的依赖关系,以获得更大的灵活性和管理性。

函数依赖

函数之间的依赖关系可以理解为这个规则:**给定函数 A 和 B,A 与 B 相关当且仅当 A 依赖于 B,或者 B 依赖于 A。**

我们可以使用集合来表示这种关系,如 {A, B, C | dp(B,A) ^ dp(B,C)}。这意味着给定一组函数 {A, B},B 依赖于 A 和 C。

示例

  1. 函数序列 <A, B, C> 可以看作是一组函数 {A, B, C},其中 B 依赖于 A,C 依赖于 B。 在这种情况下,C 依赖于 A 是隐含的。另外,由于 A 不依赖于任何其他函数,它可以被执行。*
  2. 给定一组函数 {A, B, C},其中 B 依赖于 A,C 依赖于 A 或 B。 在这种情况下,一旦 A 或 B 执行完毕,C 就可以执行。
  3. 给定一组函数 {A, B}。
    没有依赖关系,这意味着 A 和 B 可以并行发生。先决条件是假设没有副作用。
  4. {A, B | dp(A,B) ^ dp(B,A)}。 这个程序的结果将永远不会被执行。
  5. {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ dp(Stop, Walk)}。
    程序将按如下方式运行:
    Stand -> Walk -> Stop
    -> Look
    (注意:Stop 将在 Walk 或 Look 完成后运行,具体取决于哪个先完成。)
  6. {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ (dp(Stop, Walk) ^ dp(Stop, Look))}.
    程序将按如下方式运行:
    Stand -> Walk
    -> Look
    -> Stop

不同函数之间依赖关系的概念尤其有用,因为它使我们能够将函数划分为独立的组。

(**注意**:请记住,我们只关心过程式分布,而不是功能式分布。在我们上面的示例 5 中,“Walk”可能是一个持续 10 分钟的递归函数例程。)

过程式组

这样的一个组的定义是:**过程式组 G1 是一个函数序列 <x0, x1,...xn>,其中 x0 依赖于 yi,而 yi 不存在于 G1 中,并且 xn 依赖于 xn-1。**

示例

  1. {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ dp(Stop, Walk)}。
    G0 = <Stand, Walk, Stop>
    G1 = <Look>
    Stand -> Walk -> Stop
    -> Look
  2. {Stand, Walk, Look, Stop | dp(Walk,Stand) ^ (dp(Look,Stand) v dp(Look, Walk)) ^ (dp(Stop, Walk) ^ dp(Stop, Look))}.

    G0 = <Stand, Walk>
    G1 = <Look>
    G2 = <Stop>

    Stand -> Walk
    -> Look
    -> Stop

根据定义,过程式组独立运行,从而允许我们为每个组创建单独的线程/处理器/计算机。这种分离转化为一个分布式的 n 层架构,可以同时执行以极大地提高整体性能。前提是我们利用了恰当的通信技术。

类型推断和参数

通信和数据传输是任何分布式系统的关键要素。为了确保信息流,我们需要建立严格的类型检查规则。类似于 ML,函数 A 的类型表示为 A:a'->b',其中 a' 是输入参数类型,b' 是函数 A 的输出类型。在这种情况下,由于我们不知道 A 的内部工作,仅从 A 来看,我们无法确定其输入和输出类型。因此 {A, B | dp(B,A)} => A:a'->b' 和 B:b'-c'。在这种情况下,A 的返回类型必须与 B 的输入类型相同。

示例

  1. {A, B, C | dp(C,A) ^ dp(C,B)} => A:a'->b' and B:c'->d' and C:(b',d')->e'
  2. {A, B, C | dp(C,A) v dp(C,B)} => A:a'->b' and B:c'->b' and C:b'->d'
    在这种情况下,A 和 B 的返回类型必须相同。
  3. {A, B, C | dp(A,B) ^ (dp(C, B) v dp(C,A)) } => B:a'->b' and A:b'->b' and C:b'->c'
    因为 C 依赖于 B 或 A,而 A 依赖于 B,所以 A 必须具有相同的输入和输出类型。

通过确保依赖函数的类型统一,我们现在可以将注意力转移到通信和信息传输上。

沟通

如今存在许多传输方法。COM+、MSQ、Remoting、Web Services 等都涉及面向服务的分布式架构和事件驱动架构。然而,这些都很好,但我们仍然需要配置和实现它们。BizTalk、SQL Server Enterprise 等产品能够在无需编写代码的情况下实现具有负载均衡、集群和并行处理的分布式计算。如果只有这种技术可用于普通编译器,它们可以生成可分发的代码集群,这些集群不仅可以执行函数代码,还可以根据服务器、处理器等的数量相互通信和同步。如果我们真的想这样做,我们可以利用 COM+ 和 MSQ 来处理传输层,但关键在于我们自己去研究它!

这是架构
(事件驱动的同步函数处理)

DistributedNetwork.png

  • 每个分布式单元(处理器、计算机)将充当一个代理。
  • 所有代理都连接到一个服务总线。
  • 有一个连接到服务总线的程序协调器。
  • 协调器和代理彼此不知道对方,唯一的接口是服务总线。

事件

由于我们使用的是事件驱动架构,我们将需要为协调器和代理定义一些事件。

协调器

  • Distribute_Group
    • **消息**:Group
    • **描述**:协调器发出堆栈中的下一个组。

代理

  • Request_Group
    • **消息**:null
    • **描述**:代理当前没有处理任何组,因此它发出组请求。
  • Processed_Function
    • **消息**:Function
    • **描述**:代理已完成一个组的处理。

执行准备

协调器

  1. 每个函数都被分配了一个唯一的 ID。
    IDictionary<int,Function> GenerateID(IEnumerable<Function>)
  2. 协调器确定程序的过程式组。
    IEnumerable<Group> DistributeFunctions(IDictionary<int,Functions>)
  3. 每个组被分配一个唯一的 ID。
    IDictionary<int,Group> GenerateID(IEnumerable<Group>)

代理

无需准备。

执行

协调器

CoordinatorStates.png
  1. 等待 Request_Group 事件。
  2. 从字典中选择一个组。
  3. 发出 Distribute_Group 事件,将组作为其消息。

代理

AgentStates.png
  1. 如果代理是*空闲*的
    1. 发出 Request_Group 事件
    2. 等待 Distribute_Group 事件
    3. *拦截*事件
    4. 代理现在正在*处理组函数*
  2. 如果代理正在*处理组函数*
    1. 对于组序列中的每个函数 f(x)
      1. 对于 f(x) 的每个依赖项 d(x)
        1. 等待 d(x) 的 Processed_Function 事件
      2. 使用事件消息处理 f(x)
      3. 发出 Processed_Function 事件,将 f(x) 的输出作为消息。
    2. 代理现在*空闲*

部署

由函数关系集合组成的 main assembly 被部署在协调器上。部署完成后,协调器将负责其余的事务。

随着越来越多的代理加入服务总线,整体性能将变得更快。

函数编译

为了让代理执行从协调器接收到的函数,每个 Group 都需要成为一个独立的程序集。这样,代理本身一开始就不需要包含任何代码,而是会在运行时接收不同的代码。我们将在本文的第二部分更深入地探讨这一点。

摘要

一旦我们清楚地分离了函数、它们的依赖关系、过程式组和通信,我们就可以构建一个真正能够分布式工作的程序了!

本文的第一部分涵盖了该框架的摘要。下一部分将介绍其实现。

© . All rights reserved.