最佳实践

最佳实践

通用

  • 请**使用回调 API**。
  • **查找带有注释的头文件**,位置在 third_party/grpc/include/grpcpp
  • **始终为 RPC 设置截止时间 (deadline)。** 这里有一篇博客文章提供了一些解释。对于长时间存在的流式 RPC 来说,设置截止时间比较困难,但应用程序可以实现自定义逻辑来为消息添加截止时间。

流式 RPC

  • 如果您需要所有发送的消息,请**读取所有消息直到失败**。对于回调 API,读取直到回调函数被调用且 bool ok=false;对于异步 API,读取直到 tag 为 ok=false;对于同步 API,读取直到 Read 失败。这比计算消息数量更可靠。
  • **在同一时间只能有一个正在进行的读取操作和一个正在进行的写入操作**。这是一个 API 要求而不是最佳实践,但值得再次提及。
  • 如果您的应用程序涉及双向数据流,请**使用双向流式处理 (bi-directional streaming),而不是客户端-服务器和服务器-客户端模型**。这将有助于实现一致的负载均衡,并且在 gRPC 中得到更好的支持。

回调 API 特定

在本节中,“操作”定义为 StartReadStartWrite(及其变体)和 SendInitialMetadataFinish 也是一个操作,但通常与此处讨论无关。

“回调函数 (Reactions)”定义为 reactor 中可覆盖的回调函数,例如 OnReadDoneOnWriteDoneOnCancelOnInitialMetadataDoneOnDone 也是一个回调函数,但作为最终的回调函数,此处的一些指导可能不适用于它。

最佳实践

  • **回调函数 (Reactions) 应该快速执行。**不要执行阻塞或长时间运行/重量级任务,也不要休眠。这可能会影响进程内的其他 RPC。

流式处理

  • **在回调函数 (Reactions) 之外启动操作时使用 Holds。** 如果您在回调函数之外的客户端上启动操作,可能需要使用 holds。这会阻止 OnDone() 运行,直到移除 holds,从而防止在流出错时,最终清理与未完成操作之间出现竞争条件。回调函数中的 bool ok 值将反映流是否已结束,在此之后启动的操作将全部 ok=false

  • **同步回调函数。** 回调函数可以并行运行。例如,OnReadDone 可能与 OnWriteDone 同时运行。请相应地进行同步。

  • **读取直到 ok=false**。与其计数消息数量等,不如读取直到 OnReadDone(ok=false)

    在服务器端,请注意这要求客户端调用 writes done,这是推荐的做法。服务器端无需执行任何特殊操作 - Finish 表示流的结束。

    通过 Finish 调用由应用程序发送的状态,可能直到所有接收到的消息都被消费后才对客户端可见。

常见问题

通用

  1. 如何调试 gRPC 问题?

    请参阅故障排除

回调流式 API

  1. 客户端半关闭是必需还是期望的?

    强烈建议客户端执行半关闭,以便服务器可以继续读取直到 OnReadDone(ok=false),这在服务器和客户端两侧都推荐。但是,这不是必需的——服务器也可以随时选择在消费完所有客户端数据之前调用 Finish()。

  2. 如何在客户端取消操作?

    ClientContext::TryCancel()

  3. 在调用 OnDone 之前,客户端是否需要读取线上的数据?

    最佳实践始终是在客户端读取直到 ok=false

    客户端必须在读取完所有传入数据后才能看到服务器 Finish 返回的 OK 状态。然而,来自取消、截止时间过期或流中止等错误状态可能随时到达。

    无法保证服务器显式调用带有错误状态的 Finish 是排在服务器写操作之后还是立即传递。因此,客户端应始终通过读取直到 ok=false 来消费所有传入消息,以确保状态的传递。

    由于如果服务器调用带有错误状态的 Finish,消息无法保证被传递,因此不应使用错误状态向客户端传达成功和进一步的指示;应改用尾部元数据 (trailing metadata) 来实现此目的。

  4. 客户端何时调用 OnDone

    当客户端有一个可用状态(所有传入数据已读取,服务器已调用 Finish,或者状态是会立即传递的错误)、所有用户回调函数 (reactions) 已完成运行,以及所有 holds 已移除时,就会调用它。

  5. 服务器何时调用 OnDone

    所有回调函数必须完成运行(包括相关的 OnCancel),并且服务器必须已调用 Finish

  6. 什么是“reactor 内流程”与“reactor 外流程”,以及它们为何重要?

    Reactor 内流程是指在回调函数(或 reactor 构造函数)内部启动操作,例如 OnWriteDone 启动另一个写入操作。这样做是有意义的,因为一次只能有一个读取和一个写入正在进行,从回调函数中启动它们有助于保持这一状态。

    Reactor 外流程是指从回调函数外部启动流上的操作。这样做也是有意义的,因为回调函数不应该阻塞,并且应用程序可能还没有准备好执行下一次读取或写入。请注意,reactor 外流程在客户端需要使用 holds 来同步。服务器端使用 Finish 来同步 reactor 外调用;应用程序在调用 Finish 后不应再启动更多操作。

  7. 什么是 holds,以及何时何地使用它们?

    它们用于同步 OnDone 何时被调用,并且仅在使用了 reactor 外流程时才需要。请注意,holds 仅存在于客户端。

  8. 如果服务器调用 Finish,但客户端继续启动新操作,例如使用 StartWrite,会发生什么?

    每次启动写入时都会调用 OnWriteDone(ok=false),直到调用 OnDone

  9. 我们如何知道何时可以删除一个 reactor?

    可以在 OnDone 中删除 reactor。在 OnDone 中不能调用 reactor 基类的任何方法,并且在调用 OnDone 后 gRPC 不会再访问该 reactor 对象。应用程序有责任确保在 OnDone 运行时没有启动任何操作。

  10. 回调函数可以同时运行吗?例如,OnReadInitialMetadataDone 可以与 OnReadDone 同时运行吗?

    是的,大多数回调函数可以并行运行。只有 OnDone 作为最终操作单独运行。

  11. OnReadInitialMetadataDone 每次都会被调用吗,即使元数据为空?

    是的,这用于通知客户端元数据是空的。与所有回调函数一样,用户应用程序无需覆盖此回调函数。

  12. 如果初始元数据随第一次写入发送,而不是由于显式调用 SendInitialMetadata,那么服务器端会调用 OnSendInitialMetadataDone 吗?

    不会,必须显式请求。隐式调用不会触发回调。

  13. 如果客户端调用 WriteLast,会同时触发 OnWriteDoneOnWritesDoneDone 回调函数吗?如果他们调用 Write(options.set_last_message = true) 会发生什么?

    如果存在有效载荷 (payload),只会调用 OnWriteDone()OnWritesDoneDone 只会在响应 WritesDone() 时被调用。

  14. 在有未完成的写入操作时(即尚未调用 OnWriteDone())可以调用 WritesDone() 吗?

    可以,传输层会对此进行排序。

  15. 客户端的 OnReadInitialMetadataDone 何时会被调用且 ok=false

    只有在发生错误时才会调用此函数。

  16. 用户可以在调用 SendInitialMetadataStartWrite 之间不等待 OnSendInitialMetadataDone 被调用吗?

    这类似于 StartWriteWritesDone。如果传输层进行排序,我们无需强制排序,但用户可能会以任意顺序收到回调函数调用。

  17. 服务器的 OnCancel 何时运行?

    请注意,这并非仅针对流式处理。它是在带外取消时被调用,即当流上发生错误(例如连接中止)、客户端或服务器端请求取消或截止时间过期时。它可以用作客户端不再处理任何数据的信号。

    它可能与其他回调函数并行运行。在 OnCancel 后调用或在 OnCancel 内部启动的操作,其对应的回调函数将以 ok=false 被调用。

    请注意,如果服务器调用 Finish 并指定错误状态,则 OnCancel 将不再运行,因为这不被视为带外取消。

    根据排序,如果调用 FinishOnCancel 可能运行也可能不运行。但是,OnDone 始终是最终的回调函数。

  18. 如果 OnCancel 运行,服务器是否仍然需要调用 Finish

    是的,尽管传递给 Finish 的状态会被忽略。

  19. 如果调用 OnCancelOnDone 还会被调用吗?

    是的,OnDone 是最终的回调函数,一旦服务器也调用了 Finish 并且所有其他回调函数都已完成运行,就会调用它。

  20. 用户可以在 OnCancel 之后调用额外的操作 (Start*) 吗?

    可以,但它们对应的回调函数都将以 ok=false 运行。在调用服务器 Finish 之后再调用它们是无效的。

  21. 何时 context 上的 IsCancelled() 返回 true?

    当流上发生错误、客户端或服务器端请求取消或截止时间过期时,就会设置此标志。一旦 b/138186533 得到解决,如果此类错误是导致回调函数以 ok=false 运行的原因,则此标志将在调用这些回调函数之前设置。

  22. 是否有每个操作(例如 StartRead)的截止时间,还是只有每个 RPC 的截止时间?

    只有每个 RPC 有截止时间。

  23. 如果用户执行 stub->MyBidiStreamRPC(); context->TryCancel(),是否仍需要调用 StartCall

    是的,一旦调用了 stub->MyBidiStreamRPC() 就必须调用 StartCall

  24. 在有未完成的读取或写入操作时,服务器调用 Finish 是否合法?

    对于读取操作来说这是可以的,并且会调用 OnReadDone(ok=false)。在有未完成的写入操作时调用 Finish 是无效的 API 用法,因为 Finish 可以被视为一个不带数据的最终写入操作,这会违反“一次只能有一个正在进行的写入操作”的规则。

  25. 当服务器在有未完成的读取操作时调用 Finish 会发生什么?

    会调用 OnReadDone(ok=false)

  26. 可以在客户端 reactor 的 OnDone 中启动另一个操作(例如读取或写入)吗?

    不行,这是无效的 API 用法。

  27. 在调用 Finish 后,可以在服务器端启动操作吗?

    这不是一个好的实践。但是,如果在回调函数内部启动新操作,则其对应的回调函数将以 ok=false 被调用。使用 reactor 外流程启动它们是非法的,并且可能产生问题,因为这些操作可能与 OnDone 发生竞争。

最后修改于 2024 年 6 月 25 日:C++ Best Practices (#1309) (4709643)