忍不住想吐槽下 grpc 的 C++ async API

2020-03-11 09:35:01 +08:00
 DinoStray

grpc 当前版本的 C++ async API, 是基于 completion queue 实现的.

  1. 首先在 api 设计上, 对于单次 rpc 调用, 这个 completion queue 只能有两个, 分别是 call 和 notification(grpc 起的名字), 读写事件只能在 call 这一个 completion queue 回调
  2. completion queue 事件返回时会带有一个标识. 在官方的 demo 中, 这个标识用来标记是哪个对象产生了事件. 你想通过这个标识同时区分到底是哪种事件产生, 是不可能了.
  3. 简单来说, completion queue 返回的值, 只有一个, 这个值在有些 demo(官方) 里被用来标记对象, 有些 demo(网上的一些例子) 里被用来标记事件, 可是我写基于 stream(也就是 N 个请求 N 个返回) 的长连接, 需要同时获得这两个信息啊
  4. 如果参照 libevent 之类的办法, 最起码应该分别有"读"/"写"以及"连接事件"的 completion queue
  5. 可能官方也不知道怎么处理最好, 唯一的 async api demo 是不包含 stream 的, 简单的单次 rpc 调用

为了解决这个问题, 我只好用一个"全局唯一的 uint64 id"标识对象, 并在分配 id 的时候做了区间划分, 比如 * id + 0 用来标识 "连接创建事件"

然后下一个新的对象 id , 在上一个 id 的基础上 + 4
这是我的 demo:
https://github.com/DinoStray/grpc_example_async_cpp_api

关于这个问题, 几年前就有人开始吐槽(描述的问题和我一模一样)
https://groups.google.com/forum/#!topic/grpc-io/7P8QvBBNq_E
grpc 官方也给了回应, 并表示将优化 api
https://github.com/grpc/grpc/issues/7352
可是 2016 年的回应, 都过去 4 年了, 还没完成....吐血

5632 次点击
所在节点    程序员
28 条回复
xkeyideal
2020-03-11 09:42:11 +08:00
用亲儿子啊
codehz
2020-03-11 09:50:42 +08:00
(这设计确实挺恶心的)
不过作为 workaround,我觉得你可以用位运算来简化问题(
其中 id 就表示对象 id,按 1 递增
然后弄个事件类型的 enum (type)
(id << 2) | type 来合成最终的 id(设为 x)
提取就可以通过 id = x >> 2 和 type = x & 0b11 来解决了
类似的想法在 windows 的 handle 上也有应用( 你仔细看 win 的进程 id,会发现它永远是 4 的倍数,同样也是尾部的 2 个 bit 用于区分类型)
DinoStray
2020-03-11 09:56:08 +08:00
@codehz good idea.
可我刚刚发现自己转牛角尖了. 也许作者在设计这套 api 的时候, 就想让使用者每个 rpc 请求都有单独的 completion queue, 也就是最早的 "每个 tcp 连接 一个线程" 模型. 我 epoll 用惯了, 思维方式固化了, 老是想着限制线程数量,
DinoStray
2020-03-11 10:05:06 +08:00
@xkeyideal 我看了 java 和 golang 的 api, 实现都很简单易用, 唉
icylogic
2020-03-11 10:33:16 +08:00
这个 tag 是个 void*,不一定非得是 integer 啊?你放个 CallData* ,pair<id, event>或者 callback 指针不可以吗,CallData 里边啥都可以放啊。
icylogic
2020-03-11 10:38:01 +08:00
jonah
2020-03-11 10:38:43 +08:00
我一般是这么做的:传进去一个类或者结构体,里面保存请求时的上下文,比如你这里面的事件类型。
目前这个 grpc c++封装是基于 c 的,搞成 Async 异步接口意味着库里面要有线程读 cq,不一定灵活。
DinoStray
2020-03-11 11:10:57 +08:00
@icylogic 官方 demo 就是个指针, 我的问题是, 如果用返回值标识指针, 就没办法区分事件类型了, 比如异步读和异步写
DinoStray
2020-03-11 11:12:18 +08:00
@icylogic 这个 demo 我研究过了, 问题在于他没用 stream, 我的核心需求是实现 pubsub 模型, 所以必须用 stream, 基于一些设计, 还得是 bi-di stream
janxin
2020-03-11 11:19:56 +08:00
C++的 gRPC 确实很值得吐槽,要么考虑一下 brpc ?
tyrantZhao
2020-03-11 11:23:17 +08:00
cpp 的 grpc 槽点太多了
DinoStray
2020-03-11 11:25:31 +08:00
@icylogic 这个官方 demo , 如果同时做异步读写, 那这个 demo 的模型就跑不通了, 因为无法区分读完成和写完成
jonah
2020-03-11 11:30:47 +08:00
@DinoStray 你还是没有理解,指针可以指向一个对象,对象里的成员可以放东西的,放请求时的事件类型。
DinoStray
2020-03-11 11:35:36 +08:00
@jonah 读和写都是同步的, 不存在先后, 如果读写同步, 只能通过一个 completion queue 返回, 那你在对象成员里也没办法做区分了
DinoStray
2020-03-11 11:38:25 +08:00
@jonah 如果先读再写, 或者先写再读, 所有的事件基于线性产生, 那这个 demo 的模型的确可以跑通, 虽然实现有点繁琐. 可我的目标是 bi-di stream, 这时候就不适用了
DinoStray
2020-03-11 11:51:56 +08:00
@jonah 再详细点说, 异步读和异步写都开启了, 这时候 completion queue 返回, 你可以根据返回的 tag 知道是哪个对象产生了事件, 可没有办法区分是读完成了, 还是写完成了
jonah
2020-03-11 11:57:40 +08:00
@DinoStray 可以区分,completion queue Next()返回的指针就是你请求时传递的那个对象指针,请求时你是知道是读还是写的。 demo: https://gist.github.com/byteink/2b998cf1e641ce8ef8965d081f0814e1
DinoStray
2020-03-11 12:00:27 +08:00
@jonah bi-di stream, 是指同时开启异步的读写, 你这个 demo 依然还是把读写线性化了
DinoStray
2020-03-11 12:01:59 +08:00
@jonah 这个 demo 在同一时间, 只能读, 或者只能写, 要么写完了再读, 要么读完了再写. 在 bi-di stream 模型中, 我的需求是随机性有 N 个 request, 随机性有 M 个 reply, request 和 reply 没有任何关联
jonah
2020-03-11 12:05:31 +08:00
@DinoStray 没理解为啥线性化,你试试吧,completion queue Next 返回的顺序本来就是任意的,哪个先完成就先返回,不然也不需要有个指针来对应了。这个跟你现在的原理是等价的,只是传的结构优雅些。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/651747

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX