最近在 Rails 项目中使用了 Etcd, 在 Rails initializers 中初始化 Etcd client, 在生产 / 测试环境均没有问题, 但是发现在开发环境的 Rails console 中, 后续使用其他 GRPC 请求, 在初始化 GRPC Stub 的时候, 会出现如下 Exception:
- RuntimeError: grpc cannot be used before and after forking
- from /Users/larry/.Gem/Ruby/2.4.2/gems/grpc-1.17.1-universal-darwin/src/Ruby/lib/grpc/generic/client_stub.rb:47:in `initialize'
按照错误找了一下, 发现这段检查是在 grpc 1.16.0 版本中加入的: .
再深入看一下源码, Ruby 的 GRPC lib 大部分实现是在 c 的 extension 里. 首先这个 Exception 是定义如下:
- void grpc_ruby_fork_guard() {
- if (grpc_ruby_forked_after_init()) {
- rb_raise(rb_eRuntimeError, "grpc cannot be used before and after forking");
- }
- }
再看一下 grpc_ruby_forked_after_init 的定义:
- #if GPR_WINDOWS
- static void grpc_ruby_set_init_pid(void) {}
- static bool grpc_ruby_forked_after_init(void) { return false; }
- #else
- static pid_t grpc_init_pid;
- static void grpc_ruby_set_init_pid(void) {
- GPR_ASSERT(grpc_init_pid == 0);
- grpc_init_pid = getpid();
- }
- static bool grpc_ruby_forked_after_init(void) {
- GPR_ASSERT(grpc_init_pid != 0);
- return grpc_init_pid != getpid();
- }
- #endif
在 Windows 上什么也不做; 在非 Windows 上, 定义 grpc_init_pid 来保存初始化的 pid, 并且定义 grpc_ruby_set_init_pid 方法来设置 grpc_init_pid, 和 grpc_ruby_forked_after_init 来检查当前 pid 是不是初始化时候的 pid.
再回来看报错 src/Ruby/lib/grpc/generic/client_stub.rb:47:in 'initialize', 找到对应的源码可以看到这是初始化 Stub 的时候创建 GRPC::Core::Channel 的地方, 那么找到对应的 c 语言源码, 是在 GitHub.com/grpc/grpc/src/Ruby/ext/grpc/rb_channel.c 中:
- static VALUE grpc_rb_channel_init(int argc, VALUE* argv, VALUE self) {
- /* 此处省略 800 字 */
- grpc_ruby_once_init();
- grpc_ruby_fork_guard();
- /* 此处省略 800 字 */
- }
我们可以看到在初始化 GRPC::Core::Channel 的时候, 前后调用了 grpc_ruby_once_init 和 grpc_ruby_fork_guard, 稍微再跟一下就可以发现 grpc_ruby_once_init 中调用了 grpc_ruby_set_init_pid.
到此其实就很清楚了, 理一下思路, 调用 GRPC::Core::Channel.new 会先调用 grpc_ruby_once_init, 如果是首次使用 GRPC, 那么会记录下当前进程的 pid; 下次再调用 GRPC::Core::Channel.new 的时候, grpc_ruby_fork_guard 会检查当前进程 pid 是否是第一次记录下来的 pid, 如果不是, 那么会返回开头的错误.
到这里, 熟悉 Rails 的同学应该已经明白为什么在 development 环境会有这个问题了吧. 由于 Rails 加入了 Spring https://github.com/rails/spring 的支持, 在开发环境启动 Rails 的时候, initializers 被调用 (也就是 Rails Application 被 initialize) 的进程, 并不是后续代码执行的进程.
那需要怎么解决呢? Spring 提供了一个 hook:
- Spring.after_fork do
- # run arbitrary code
- end
可以注册一个 block, 这个 block 中的代码将会在 Spring fork 出来之后的进程中执行, 所以我们只需要在这个 block 中初始化 Etcd 的 client, 就不会碰到再之后使用 GRPC 不在同一个进程的问题了.
◆ comments powered by Disqus https://disqus.com
来源: https://juejin.im/entry/5c481b286fb9a04a0c2ec958