# 程序框架（Pv2）： [HBASE-12439](https://issues.apache.org/jira/browse/HBASE-12439)

_ 程序 v2 ...旨在提供一种统一的方法来构建具有回滚/前滚能力的多步骤程序（例如创建/删除表） - Pv2 的作者 Matteo Bertozzi。_

使用 Pv2，您可以构建和运行状态机。它是由 Matteo 构建的，用于在过程失败时使 HBase 中的分布式状态转换具有弹性。在 Pv2 之前，状态转换处理在代码库中传播，实现因转换类型和上下文而异。 Pv2 的灵感来自 Apache Accumulo 的 [FATE](https://accumulo.apache.org/1.8/accumulo_user_manual.html#_fault_tolerant_executor_fate) 。

早期的 Pv2 方面已经在 HBase 发布了很长一段时间，但随着它涉及更多涉及的场景，它继续发展。我们现在拥有的是强大但功能复杂，不完整，需要清理和加固。在本文档中，我们对系统进行了概述，以便您可以使用它（并帮助其进行抛光）。

这个系统有一个尴尬的名字 Pv2，因为 HBase 已经有了快照中使用的过程的概念（参见 hbase-server _org.apache.hadoop.hbase.procedure_ 而不是 hbase-procedure _org .apache.hadoop.hbase.procedure2_ ）。 Pv2 取代并取代程序。

## 180.程序

过程是对 HBase 实体进行的转换。 HBase 实体的示例是区域和表格。过程由 ProcedureExecutor 实例运行。过程当前状态保存在 ProcedureStore 中。 ProcedureExecutor 只有一个原始视图，可以查看过程中发生的事情。从其 PoV，提交过程，然后 ProcedureExecutor 继续调用 _#execute（Object）_，直到过程完成。在失败或重启的情况下，可以多次调用 Execute，因此每次运行时，过程代码必须是幂等的，产生相同的结果。程序代码也可以实现 _ 回滚 _，因此如果失败则可以撤消步骤。调用 _execute（）_ 可能会导致以下可能性之一：

*   _execute（）_ 返回

    *   _null_ ：表示我们已经完成了。

    *   ：表示还有更多这样做，坚持当前程序状态并重新 _ 执行（）_。

    *   _ 子程序的数组 _：表示在我们继续之前需要运行完成的一组程序（之后我们希望框架再次调用我们的执行）。

*   _execute（）_ 抛出异常

    *   _suspend_ ：表示程序的执行被暂停，并且由于某些外部事件可以恢复。过程状态是持久的。

    *   _yield_ ：将程序添加回调度程序。过程状态不会持久化。

    *   _ 中断 _：目前与 _ 产量 _ 相同。

    *   上面没有列出任何 _ 异常 _：程序 _ 状态 _ 变为 _FAILED_ （之后我们期望框架将尝试回滚）。

ProcedureExecutor 将过程状态的框架概念标记到过程本身;例如它将程序标记为提交时的初始化。它在执行时将状态移动到 RUNNABLE。完成后，程序将标记为 FAILED 或 SUCCESS。以下是撰写本文时所有州的清单：

*   **_INITIALIZING_ **施工程序，尚未加入执行人

*   **_RUNNABLE_ **程序添加到执行程序中，准备执行。

*   **_WAITING_ **该程序正在等待儿童（子程序）完成

*   **_WAITING _TIMEOUT_ **程序正在等待超时或外部事件

*   **_ROLLEDBACK_ **程序失败并被回滚。

*   **_SUCCESS_ **程序执行成功完成。

*   **_FAILED_ **程序执行失败，可能需要回滚。

每次执行后，Procedure 状态将持久保存到 ProcedureStore。在过程上调用挂钩，以便它们可以保留自定义状态。故障发生后，ProcedureExecutor 通过重放 ProcedureStore 的内容来重新补充其崩溃前状态。这使得过程框架可以抵御过程失败。

### 180.1。履行

在实施过程中，程序倾向于将变换划分为更细粒度的任务，并且当这些工作项中的一些被移交给子程序时，批量作为处理 _ 步骤 _ in-Procedure;每次执行 execute 都用于执行一个步骤，然后 Procedure 放弃返回到框架。程序会自行跟踪处理过程中的位置。

在执行过程中包含子任务或 _ 步骤 _ 的内容由程序作者决定，但通常它是一小部分工作，无法进一步分解并将处理向前移动到其最终状态。由许多小步骤而不是几个大步骤组成的程序允许程序框架提供关于我们在处理中的位置的洞察。它还允许框架在执行时更公平。如上所述，每个步骤可以被多次调用（失败/重启），因此步骤必须是幂等的。很容易混淆过程本身与框架本身保持一致的状态。尽量保持它们的区别。

### 180.2。回滚

当过程或其中一个子过程失败时，将调用回滚。回滚步骤应该清除 execute（）步骤中创建的资源。如果失败并重新启动，可能会多次调用 rollback（），因此代码必须是幂等的。

### 180.3。度量

有关于提交程序和完成时收集指标的钩子。

*   updateMetricsOnSubmit（）

*   updateMetricsOnFinish（）

各个过程可以覆盖这些方法以收集过程特定的度量标准这些方法的默认实现尝试获取一个实现接口 ProcedureMetrics 的对象，该接口封装了以下一组通用度量：

*   SubmittedCount（Counter）：提交类型的过程实例总数。

*   时间（直方图）：过程实例的运行时直方图。

*   FailedCount（Counter）：失败的过程实例总数。

各个过程可以实现此对象并定义这些通用的度量标准集。

### 180.4。行李

程序可以携带行李。一个例子是 _ 步骤 _ 最后获得的程序（参见上一节）;程序持续存在标记它们当前的位置。其他示例可能是 Procedure 当前正在运行的 Region 或 Server 名称。每次执行调用后，都会调用 Procedure＃serializeStateData。程序可以坚持下去。

### 180.5。结果/状态和查询

（来自 Matteo 的 [ProcedureV2 和 Notification Bus](https://issues.apache.org/jira/secure/attachment/12693273/Procedurev2Notification-Bus.pdf) doc）在异步操作的情况下，必须保持结果，直到客户端请求它为止。一旦我们收到结果的“获取”，我们就可以安排删除记录。对于某些操作，结果可能是“不必要的”，尤其是在失败的情况下（例如，如果创建表失败，我们可以查询操作结果，或者我们可以只执行列表来查看它是否已创建）所以在某些情况下我们可以超时后安排删除。在客户端，操作将返回“过程 ID”，此 ID 可用于等待过程完成并获得结果/异常。

```
Admin.doOperation() { longprocId=master.doOperation(); master.waitCompletion(procId); }  + 
```

如果主机在执行操作时发生故障，备用主机将拾取半进行操作并完成它。客户端不会注意到失败。

## 181.子程序

子过程是 _ 过程 _ 实例，由过程实例（父过程）的 _#execute（Object）_ 方法创建和返回。由于子程序属于 _ 程序 _，它们可以实例化自己的子程序。作为递归，过程堆栈由框架维护。该框架确保父过程不会继续，直到过程堆栈中的所有子过程及其子过程成功完成。

## 182.程序执行人

_ProcedureExecutor_ 使用 _ProcedureStore_ 和 _ProcedureScheduler_ 并执行提交给它的程序。支持的一些基本操作是：

*   _abort（procId）_：如果未完成，则中止指定的过程

*   _ 提交（程序）_：提交执行程序

*   _ 检索：_ get 方法列表获取 _ 程序 _ 实例和结果

*   _ 注册/取消注册 _ 监听器：用于监听与程序相关的通知

当 _ProcedureExecutor_ 启动时，它会在前一次运行的 _ProcedureStore_ 中加载过程实例。从最后存储的状态恢复所有未完成的过程。

## 183\. Nonces

您可以将 RPC 中的随机数传递给执行程序提交的过程。然后将这个随机数与 w 持续的过程序列化。如果崩溃，在重新加载时，如果客户端尝试第二次运行相同的程序（将被拒绝），则会将 nonce 放回到 pid 的 nonce 映射中。请参阅基本过程以及 nonce 是基本数据成员的方式。

## 184.等待/唤醒/暂停/收益

'suspend'意味着停止处理程序，因为在条件改变之前我们不能再进步;即我们发送 RPC 并需要等待响应。这种方法的工作方式是，一个过程在其内部引发一个暂停异常作为 GOTO 结束当前处理步骤。挂起还会将过程放回调度程序。有问题的是，即使在暂停时，我们也会对出路进行一些计算，因此可能需要时间退出（我们必须在 WAL 中更新状态）。

RegionTransitionProcedure＃reportTransition 在收到 RS 的报告时被调用。对于分配和取消分配，来自我们发送 RPC 的服务器的此事件响应唤醒暂停的分配/取消分配。

## 185.锁定

过程锁不是关于并发性的！它们是为了对 HBase 实体（例如表或区域）提供过程读/写访问权限，这样就可以阻止其他过程在当前过程运行时修改 HBase 实体状态。

锁定是可选的，直到过程实现者，但如果过程正在操作实体，则所有转换都需要通过使用相同锁定方案的过程完成，否则会造成严重破坏。

两个 ProcedureExecutor Worker 线程实际上最终都会处理同一个 Procedure 实例。如果它发生了，那么线程就意味着运行一个过程的不同部分 - 那些没有相互标记的变化（这在程序框架概念'暂停'方面变得很尴尬。更多内容见下文）。

可选地，可以在程序的有效期内保持锁。例如，如果移动 Region，您可能希望拥有对 HBase Region 的独占访问权，直到 Region 完成（或失败）。这与{@link #holdLock（Object）}一起使用。如果{@link #holdLock（Object）}返回 true，则过程执行程序将调用 acquireLock（）一次，之后不调用{@link #releaseLock（Object）}，直到过程完成（通常，它调用每个周围的释放/获取）调用{@link #execute（Object）}。

锁也可以过程的生命;即一旦一个分配程序开始，我们就不希望另一个程序干涉该分配的区域。持有程序生命周期锁定的程序过程#holdLock 为 true。 AssignProcedure 执行此操作与拆分和移动一样（如果在区域移动的中间，您不希望它拆分）。

锁定可以终止程序。

一些锁具有层次结构。例如，获取区域锁定还会对其包含的表和命名空间进行（读取）锁定，以防止另一个过程获得对宿主表（或命名空间）的独占锁定。

## 186.程序类型

### 186.1。 StateMachineProcedure

可以将对 _#execute（Object）_ 方法的每次调用视为在状态机中从一种状态转换到另一种状态。抽象类 _StateMachineProcedure_ 是基础 _ 过程 _ 类的包装器，它提供了用于实现状态机的构造作为 _ 过程 _。在每个状态转换之后，当前状态被持久化，使得在崩溃/重启的情况下，可以从崩溃/重启之前的过程的先前状态恢复状态转换。各个过程需要定义初始状态和终止状态和钩子 _executeFromState（）_ 和 _setNextState（）_ 是为状态转换提供的。

### 186.2。 RemoteProcedureDispatcher

新的 RemoteProcedureDispatcher（+子类 RSProcedureDispatcher）原语负责运行基于过程的 Assignments 的“远程”组件。该调度员了解“服务器”。它按时间/计数基于时间聚合分配，因此可以批量发送过程而不是每个 RPC 发送一个过程。过程状态返回到 RegionServer 心跳报告在线/离线区域的背面（不再通过 ZK 通知）。响应将传递给 AMv2 以进行“处理”。它将检查内存中的状态。如果存在不匹配，则假设 RS 端出现问题，它会禁用 RegionServer。超时会触发重试（尚未实施！）。过程机器使用实体 _ 锁定 _ 确保任何一个区域/表上一次只能执行一个操作，并且智能关于什么是串行以及可以同时运行什么（锁定是基于 zk 的 - 你放了一个 znode 在 zk 中表 - 但现在已经转换为基于程序的项目的一部分）。

## 187.参考文献

*   Matteo 有一个关于程序框架看起来像什么的幻灯片，以及它最初解决的问题[附加到 Pv2 问题上。](https://issues.apache.org/jira/secure/attachment/12845124/ProcedureV2b.pdf)

*   [Matteo](https://issues.apache.org/jira/secure/attachment/12693273/Procedurev2Notification-Bus.pdf) 关于问题以及 Pv2 如何使用路线图（来自 Pv2 JIRA）的优秀文档。我们应该回到路线图来做通知总线，将日志分割转换为 Pv2 等。