Dart负载均衡,调度多个子isolate

265 阅读9分钟

负载均衡(Load Balance),意思是将负载(工作任务,访问请求)进行平衡、分摊到多个操作单元(服务器,组件)上进行执行。是解决高性能,单点故障(高可用),扩展性(水平伸缩)的终极解决方案。

Dart中的负载均衡, 是对多个子isolate的调度。

负载较低的条目(较小的 load 值)具有更高的优先级。

_LoadBalancerEntry

class _LoadBalancerEntry implements Comparable<_LoadBalancerEntry> {
  /// The position in the heap queue.
  ///
  /// Maintained when entries move around the queue.
  /// Only needed for [LoadBalancer._decreaseLoad].
  /*
  堆队列中的位置。
  当条目在队列中移动时维护。
  只需要[loadbalancer._decreaseLoad]。
   */
  int queueIndex;

  // The current load on the isolate.
  // 当前的isolate
  int load = 0;

  // The service used to execute commands.
  // 用于执行命令的服务。
  Runner runner;

  _LoadBalancerEntry(this.runner, this.queueIndex);

  Future<R> run<R, P>(LoadBalancer balancer, int load, FutureOr<R> Function(P argument) function, P argument, Duration? timeout,
      FutureOr<R> Function()? onTimeout) {
    // print('_LoadBalancerEntry 异步执行, load:$load, argument0: $argument');
    return runner.run<R, P>(function, argument, timeout: timeout, onTimeout: onTimeout).whenComplete(() {
      // print('_LoadBalancerEntry 同步执行, 先执行完成load:$load,  argument1: $argument');
      balancer._decreaseLoad(this, load);
    });
  }

  Future close() => runner.close();

  @override
  int compareTo(_LoadBalancerEntry other) => load - other.load;
}

实现 Comparable 接口,这样类的实例可以相互比较。每个条目代表一个隔离区(isolate)或工作线程,它们可以执行任务并具有负载(load)。

以下是这个类的关键点解释:

  1. 类成员变量:

    • queueIndex: 表示该条目在堆队列中的位置。当条目在队列中移动时,这个值会被更新。
    • load: 表示当前隔离区的负载量。
    • runner: 一个 Runner 类型的对象,用于执行命令。
  2. 构造函数:

    • _LoadBalancerEntry(this.runner, this.queueIndex): 构造函数接收一个 Runner 实例和一个队列索引,并将它们初始化。
  3. 方法:

    • run: 一个泛型方法,接收一个 LoadBalancer 实例、负载量、一个函数、函数参数、超时时间以及超时回调函数。这个方法使用 runner 的 run 方法异步执行传入的函数,并在执行完成后调用 balancer._decreaseLoad 方法来减少负载。
    • close: 返回 runner.close() 的结果,用于关闭 Runner 实例。
  4. compareTo 方法:

    • @override int compareTo(_LoadBalancerEntry other) => load - other.load;: 这个方法用于比较两个 _LoadBalancerEntry 实例的负载量。它返回当前实例的负载量与另一个实例负载量的差值。这是实现 Comparable 接口所必需的方法。

这个类在 LoadBalancer 中可能用于维护一个基于负载的优先队列,其中负载较低的条目(较小的 load 值)具有更高的优先级。这是为了实现工作负载的均匀分配,确保负载不会集中在单个隔离区或工作线程上。

LoadBalancer

/*
一个运行池,按负载排序。保持一个运行池,并允许运行功能,通过运行池与最低的isolate负载。
运行池条目的数量在创建池时是固定的。
当运行池关闭时,所有的运行者也都关闭了。负载均衡器不可重入。执行[run]函数不应该同步调用负载均衡器上的方法。
 */
class LoadBalancer implements Runner {
  /// A stand-in future which can be used as a default value.
  ///
  /// The future never completes, so it should not be exposed to users.
  /*
  可作为默认值使用的替代future。
  future永远不会完成,所以它不应该暴露给用户。
   */
  static final _defaultFuture = Completer<Never>().future;

  /// Reusable empty fixed-length list.
  /// 可重用的空定长列表。
  static final _emptyQueue = List<_LoadBalancerEntry>.empty(growable: false);

  /// A heap-based priority queue of entries, prioritized by `load`.
  ///
  /// The entries of the list never change, only their positions.
  /// Those with positions below `_length`
  /// are considered to currently be in the queue.
  /// All operations except [close] should end up with all entries
  /// still in the pool. Some entries may be removed temporarily in order
  /// to change their load and then add them back.
  ///
  /// Each [_LoadBalancerEntry] has its current position in the queue
  /// as [_LoadBalancerEntry.queueIndex].
  ///
  /// Is set to an empty list on clear.
  /*
  基于堆的条目优先级队列,按' load '排序。列表的条目永远不会改变,只是它们的位置。
  那些位置低于' _length '的被认为是当前在队列中。除了[close]之外的所有操作都应该在池中保留所有条目。
  有些条目可能会被暂时删除,以便改变它们的负载,然后再将它们添加回来。
  每个[_LoadBalancerEntry]在队列中的当前位置为[_LoadBalancerEntry.queueIndex]。
  被设置为空列表。
  小根堆-load值最小的在最上面。
   */
  List<_LoadBalancerEntry> _queue;

  /// Current number of elements in [_queue].
  ///
  /// Always a number between zero and [_queue.length].
  /// Elements with indices below this value are
  /// in the queue, and maintain the heap invariant.
  /// Elements with indices above this value are temporarily
  /// removed from the queue and are ordered by when they
  /// were removed.
  /*
  [_queue]中的当前元素数。
  总是一个介于0和[_queue.length]之间的数字。
  索引低于此值的元素在队列中,并保持堆不变性。
  索引高于此值的元素将暂时从队列中删除,并按删除时间排序。
   */
  int _length;

  /// The future returned by [stop].
  ///
  /// Is `null` until [stop] is first called.
  /*
  由[停止]返回的future。在[stop]第一次被调用之前,为' null '。
   */
  Future<void>? _stopFuture;

  /// Create a load balancer backed by the [Runner]s of [runners].
  /// 创建一个由[runners]的[Runner]s支持的负载均衡器。
  LoadBalancer(Iterable<Runner> runners) : this._(_createEntries(runners));

  LoadBalancer._(List<_LoadBalancerEntry> entries)
      : _queue = entries,
        _length = entries.length;

  /// The number of runners currently in the pool.
  /// 当前池中运行者的数量。
  int get length => _length;

  /// Asynchronously create [size] runners and create a `LoadBalancer` of those.
  ///
  /// This is a helper function that makes it easy to create a `LoadBalancer`
  /// with asynchronously created runners, for example:
  /// ```dart
  /// var isolatePool = LoadBalancer.create(10, IsolateRunner.spawn);
  /// ```
  /*
  异步创建[size]运行器并创建一个' LoadBalancer '。
  这是一个辅助函数,可以很容易地创建一个“LoadBalancer”。
  使用异步创建的运行程序,例如:
  /// var isolatePool = LoadBalancer.create(10, IsolateRunner.spawn);
   */
  static Future<LoadBalancer> create(int size, Future<Runner> Function() createRunner) {
    return Future.wait(Iterable.generate(size, (_) => createRunner()), cleanUp: (Runner runner) {
      runner.close();

      /// 传入runners, 创建并返回LoadBalancer实例
    }).then((runners) => LoadBalancer(runners));
  }

  /// 创建_LoadBalancerEntry实例, 每个_LoadBalancerEntry实例分配一个索引index
  static List<_LoadBalancerEntry> _createEntries(Iterable<Runner> runners) {
    var index = 0;
    return runners.map((runner) => _LoadBalancerEntry(runner, index++)).toList(growable: false);
  }

  /// Execute the command in the currently least loaded isolate.
  ///
  /// The optional [load] parameter represents the load that the command
  /// is causing on the isolate where it runs.
  /// The number has no fixed meaning, but should be seen as relative to
  /// other commands run in the same load balancer.
  /// The `load` must not be negative.
  ///
  /// If [timeout] and [onTimeout] are provided, they are forwarded to
  /// the runner running the function, which will handle a timeout
  /// as normal. If the runners are running in other isolates, then
  /// the [onTimeout] function must be a constant function.
  /*
  在当前加载最少的isolate中执行命令。可选的[load]参数表示命令在其运行的isolate上造成的负载。
  这个数字没有固定的含义,但应该被看作是相对于在同一负载平衡器中运行的其他命令。
  “负载”不能是负的。如果提供了[timeout]和[onTimeout],它们将被转发给运行该函数的运行器,运行器将正常处理超时。
  如果运行程序在其他isolate中运行,则[onTimeout]函数必须是一个常数函数。
   */
  @override
  Future<R> run<R, P>(FutureOr<R> Function(P argument) function, P argument,
      {Duration? timeout, FutureOr<R> Function()? onTimeout, int load = 100}) {
    RangeError.checkNotNegative(load, 'load');
    if (_length == 0) {
      // Can happen if created with zero runners,
      // or after being closed.
      if (_stopFuture != null) {
        throw StateError("Load balancer has been closed");
      }
      throw StateError("No runners in pool");
    }

    // for (var element in _queue) {
    //   print('---run, load:${element.load}, queueIndex: ${element.queueIndex}---');
    // }
    var entry = _queue.first;
    entry.load += load;
    _bubbleDown(entry, 0);
    return entry.run(this, load, function, argument, timeout, onTimeout);
  }

  /// Execute the same function in the least loaded [count] isolates.
  ///
  /// This guarantees that the function isn't run twice in the same isolate,
  /// so `count` is not allowed to exceed [length].
  ///
  /// The optional [load] parameter represents the load that the command
  /// is causing on the isolate where it runs.
  /// The number has no fixed meaning, but should be seen as relative to
  /// other commands run in the same load balancer.
  /// The `load` must not be negative.
  ///
  /// If [timeout] and [onTimeout] are provided, they are forwarded to
  /// the runners running the function, which will handle any timeouts
  /// as normal.
  List<Future<R>> runMultiple<R, P>(int count, FutureOr<R> Function(P argument) function, P argument,
      {Duration? timeout, FutureOr<R> Function()? onTimeout, int load = 100}) {
    RangeError.checkValueInInterval(count, 1, _length, 'count');
    RangeError.checkNotNegative(load, 'load');
    if (count == 1) {
      return List<Future<R>>.filled(1, run(function, argument, load: load, timeout: timeout, onTimeout: onTimeout));
    }
    var result = List<Future<R>>.filled(count, _defaultFuture);
    if (count == _length) {
      // No need to change the order of entries in the queue.
      for (var i = 0; i < _length; i++) {
        var entry = _queue[i];
        entry.load += load;
        result[i] = entry.run(this, load, function, argument, timeout, onTimeout);
      }
    } else {
      // Remove the [count] least loaded services and run the
      // command on each, then add them back to the queue.
      for (var i = 0; i < count; i++) {
        _removeFirst();
      }
      // The removed entries are stored in `_queue` in positions from
      // `_length` to `_length + count - 1`.
      for (var i = 0; i < count; i++) {
        var entry = _queue[_length];
        entry.load += load;
        _addNext();
        result[i] = entry.run(this, load, function, argument, timeout, onTimeout);
      }
    }
    return result;
  }

  @override
  Future<void> close() {
    var stopFuture = _stopFuture;
    if (stopFuture != null) return stopFuture;
    var queue = _queue;
    var length = _length;
    _queue = _emptyQueue;
    _length = 0;
    return _stopFuture = MultiError.waitUnordered(
      [for (var i = 0; i < length; i++) queue[i].close()],
    ).then(ignore);
  }

  /// Place [element] in heap at [index] or above.
  ///
  /// Put element into the empty cell at `index`.
  /// While the `element` has higher priority than the
  /// parent, swap it with the parent.
  ///
  /// Ignores [element]'s initial [_LoadBalancerEntry.queueIndex],
  /// but sets it to the final position when the element has
  /// been placed.
  /*
  上浮算法
   */
  void _bubbleUp(_LoadBalancerEntry element, int index) {
    while (index > 0) {
      var parentIndex = (index - 1) ~/ 2;
      var parent = _queue[parentIndex];
      if (element.compareTo(parent) > 0) break;
      _queue[index] = parent..queueIndex = index;
      index = parentIndex;
    }
    _queue[index] = element..queueIndex = index;
  }

  /// Place [element] in heap at [index] or above.
  ///
  /// Put element into the empty cell at `index`.
  /// While the `element` has lower priority than either child,
  /// swap it with the highest priority child.
  ///
  /// Ignores [element]'s initial [_LoadBalancerEntry.queueIndex],
  /// but sets it to the final position when the element has
  /// been placed.
  /*
  下沉算法, 从上往下堆化
  将元素放入' index '处的空单元格中。当“element”的优先级低于任何一个子元素时,将其与优先级最高的子元素交换。
   */
  void _bubbleDown(_LoadBalancerEntry element, int index) {
    while (true) {
      var childIndex = index * 2 + 1; // Left child index.
      if (childIndex >= _length) break;
      var child = _queue[childIndex];
      var rightChildIndex = childIndex + 1;
      if (rightChildIndex < _length) {
        var rightChild = _queue[rightChildIndex];
        if (rightChild.compareTo(child) < 0) {
          childIndex = rightChildIndex;
          child = rightChild;
        }
      }
      if (element.compareTo(child) <= 0) break;
      _queue[index] = child..queueIndex = index;
      index = childIndex;
    }
    _queue[index] = element..queueIndex = index;
    // print('---😄😄下沉算法---');
    for (var element in _queue) {
      print('---_bubbleDown, load:${element.load}, queueIndex: ${element.queueIndex}---');
    }
    print('\n');
  }

  /// Removes the first entry from the queue, but doesn't stop its service.
  ///
  /// The entry is expected to be either added back to the queue
  /// immediately or have its stop method called.
  ///
  /// After the remove, the entry is stored as `_queue[_length]`.
  _LoadBalancerEntry _removeFirst() {
    assert(_length > 0);
    _LoadBalancerEntry entry = _queue.first;
    _length--;
    if (_length > 0) {
      var replacement = _queue[_length];
      _queue[_length] = entry..queueIndex = _length;
      _bubbleDown(replacement, 0);
    }
    return entry;
  }

  /// Adds next unused entry to the queue.
  ///
  /// Adds the entry at [_length] to the queue.
  /*
  将下一个未使用的条目添加到队列中。将[_length]条目添加到队列中。
   */
  void _addNext() {
    assert(_length < _queue.length);
    var index = _length;
    var entry = _queue[index];
    _length = index + 1;
    _bubbleUp(entry, index);
  }

  /// Decreases the load of an element which is currently in the queue.
  ///
  /// Elements outside the queue can just have their `load` modified directly.
  /*
  减少当前在队列中的元素的负载。队列外的元素可以直接修改它们的“load”。
   */
  void _decreaseLoad(_LoadBalancerEntry entry, int load) {
    assert(load >= 0);
    entry.load -= load;
    var index = entry.queueIndex;
    // Should always be the case unless the load balancer
    // has been closed, or events are happening out of their
    // proper order.
    if (index < _length) {
      _bubbleUp(entry, index);
    }
  }
}
  • 这个类使用一个基于堆的优先级队列来管理运行器,确保任务尽可能均匀地分配到负载最低的运行器上。
  • _load 值越小,表示运行器的负载越低,优先级越高。
  • run 和 runMultiple 方法用于调度任务,close 方法用于关闭所有运行器。
  • _loadBalancerEntry 类的实例在 _queue 中维护其在堆中的位置,并通过 compareTo 方法进行比较。

这个类的设计目的是实现一个高效的负载均衡器,能够根据运行器的负载动态调度任务。

使用

void main() {
  var N = 45;
  var sw = Stopwatch()..start();
  // Compute fib up to 42 with 6 isolates.
  parfib(N, 6).then((v1) {
    var t1 = sw.elapsedMilliseconds;
    sw.stop();
    sw.reset();
    print('fib#6($N) = ${v1[N]}, ms: $t1');
    sw.start();
    // Then compute fib up to 88 with 2 isolates.
    // parfib(N, 2).then((v2) {
    //   var t2 = sw.elapsedMilliseconds;
    //   sw.stop();
    //   print('fib#3($N) = ${v2[N]}, ms: $t2');
    // });
  });
}

// 处理单个任务的结果
void handleTaskResult(int result) {
  print('Task result: $result');
}

// Compute fibonacci 1..limit
Future<List<int>> parfib(int limit, int parallelity) async {
  /// 使用 runners 初始化 LoadBalancer
  var pool = await LoadBalancer.create(parallelity, IsolateRunner.spawn);

  // 创建一个任务列表,用于收集所有任务的 Future。
  var fibs = List<Future<int>?>.filled(limit + 1, null);
  // Schedule all calls with exact load value and the heaviest task
  // assigned first.
  /*
    安排所有具有准确负载值的调用,并首先分配最重的任务。
     */
  void schedule(int a, int b, int i) {
    // print('parfib 调度任务, schedule0, a: $a, i0: $i');
    if (i < limit) {
      schedule(a + b, a, i + 1);
    }

    // 处理单个任务
    var future = pool.run<int, int>(fib, i, load: a);
    // print('parfib 调度任务, schedule1, a: $a, i1: $i');
    // 单个任务结果-记录过程
    // 执行这个then后, 会打印每个任务的结果, 不用等到下面的Future.wait才打印结果
    future.then(handleTaskResult);

    // 将每个任务的执行包装在 Future 中,并添加到任务列表
    fibs[i] = future;
  }

  schedule(0, 1, 0);
  print('---开始Future.wait---');
  // 使用 Future.wait 并发执行所有任务,并等待它们全部完成
  final results = await Future.wait(fibs.cast<Future<int>>());

  // 打印所有任务的结果
  print('---results: $results---');

  /// 任务执行完毕后关闭 LoadBalancer
  await pool.close();

  return results;

  return LoadBalancer.create(parallelity, IsolateRunner.spawn).then((LoadBalancer pool) {
    var fibs = List<Future<int>?>.filled(limit + 1, null);
    // Schedule all calls with exact load value and the heaviest task
    // assigned first.
    /*
    安排所有具有准确负载值的调用,并首先分配最重的任务。
     */
    void schedule(int a, int b, int i) {
      if (i < limit) {
        schedule(a + b, a, i + 1);
      }
      fibs[i] = pool.run<int, int>(fib, i, load: a);
    }

    schedule(0, 1, 0);
    // And wait for them all to complete.
    return Future.wait(fibs.cast<Future<int>>()).whenComplete(pool.close);
  });
}

int fib(int n) {
  if (n < 2) return n;
  return fib(n - 1) + fib(n - 2);
}

参考资料

负载均衡

isolate