gRPC 跨进程使用引发的问题

问题描述 在 Python 项目中使用 gRPC 进行通信,跨进程使用时,会出现阻塞或报错的情况(根据 gRPC.io 的版本不同,现象不同)。下面代码展示了一个跨进程使用的 DEMO,主进程向 30001 端口上的 gRPC 服务器发送请求,子进程也向相同的服务器发送请求。 def send(): channel = grpc.insecure_channel('localhost:30001') stub = message_pb2_grpc.GreeterStub(channel) response = stub.SayHello(message_pb2.HelloRequest(name='you')) print(f"Greeter client received 1: " + response.message) def main(): channel = grpc.insecure_channel('localhost:30001') stub = message_pb2_grpc.GreeterStub(channel) response = stub.SayHello2(message_pb2.HelloRequest(name='you')) print("Greeter client received 2: " + response.message) p = multiprocessing.Process(target=send) p.start() p.join() if __name__ == '__main__': main() 使用 gRPC.io 1.28.1 的情况下,会发生报错,主进程可以正常收到服务器的返回,但是子进程报 Socket operation on non-socket。 raise _InactiveRpcError(state) grpc._channel._InactiveRpcError: <_InactiveRpcError of RPC that terminated with: status = StatusCode.UNAVAILABLE details = "Socket operation on non-socket" debug_error_string = "{"created":"@1587481625.192071231","description":"Error received from peer ipv6:[::1]:50051","file":"src/core/lib/surface/call.cc","file_line":1056,"grpc_message":"Socket operation on non-socket","grpc_status":14}" > 排查过程 根据代码,主进程和子进程分别创建了自己的 Channel,看上去逻辑没什么问题,没有什么思路,所以多尝试几种情况先测试一下吧。首先尝试了一下主进程和子进程请求不同的server,在 30001 和 30002 端口分别启动两个 gRPC Server,然后将客户端代码改为主进程请求 30001 端口,子进程请求 30002 端口,代码可以正常运行。测试到这里就更摸不着头脑了,代码明明写的是主进程子进程分别创建 Channel,现在的现象看上去像是在请求相同服务器的情况下,子进程复用了主进程的socket连接。gRPC 底层使用的是 HTTP2,而 HTTP2 使用了长连接,会不会是这个原因? ...

四月 23, 2020 · 3 分钟 · Zhiya

探究vscode debug流程,解决无法运行go程序的问题

问题描述 vscode 无法以 run 模式运行 go 项目(只能以 debug 模式调试),并且有如下报错。 图中被遮盖的部分是项目内的 package,并非第三方 package,也就是说在以 run 模式运行 go 项目时无法找到其他的 go 文件,只能找到入口文件。 初步排查 找不到其他文件,首先想到的是 GO_PATH 的问题,但是项目使用了 go mod,允许在 GO_PATH 之外的路径创建项目,所以这个怀疑点排除。接下来怀疑 vscode 的配置有问题,每个 vscode 项目中都有 .launch.json 文件,配置运行代码时的环境,下面是项目中的 .launch.json。 { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Launch", "type": "go", "request": "launch", "mode": "auto", "program": "${workspaceRoot}/src/main.go", "env": {}, "args": [] } ] } 可以看到 .launch.json 里没有指定程序的工作目录,debug 模式和 run 模式会不会默认的工作路径不同呢?于是在 main 函数里使用 os.Getwd() 打印一下当前的路径,结果如下: ...

四月 20, 2020 · 2 分钟 · Zhiya

viper从etcd读取配置失败的问题

问题描述 Viper (本文环境是 Viper 1.1.0)是 Go 应用程序的完整配置解决方案,在很多项目中都有应用。etcd是一个分布式 KV 存储,最直接的应用是配置中心。 Viper 除了支持从文件中读取配置,还支持从远程的配置中心读取配置,使用下面的代码进行配置。 viper.AddRemoteProvider("etcd", "http://127.0.0.1:2379", "conf.toml") viper.SetConfigType("toml") err := viper.ReadRemoteConfig() if err != nil { panic(err) } 运行后报错panic: Remote Configurations Error: No Files Found,检查后发现 etcd 开启了 tls,所以需要用 https 协议访问 etcd 的 API,更新代码如下。 viper.AddSecureRemoteProvider("etcd", "https://127.0.0.1:2379", "conf.toml", "key_path") viper.SetConfigType("toml") err := viper.ReadRemoteConfig() if err != nil { panic(err) } 使用AddSecureRemoteProvider方法替换AddRemoteProvider方法,问题依旧。 定位问题 跟踪源码发现,最终像 etcd 发送请求的是go-etcd包(目前 go-etcd 已经不维护),在 go-etcd 的 requests.go 文件中找到了相关的源码,go-etcd 调用了 net/http 包向 etcd 发送请求。 这个时候忽然想到 etcd 的证书是自签名的,访问自签名证书的 https 接口应该会报错啊,怎么会请求到内容呢?如下图,在 Chrome 中访问 etcd 的自签名 https 接口,会提示证书无效。 ...

四月 16, 2020 · 1 分钟 · Zhiya

使用Pipfile代替reqirements.txt

很多语言都提供了环境隔离的支持,例如nodejs的node_module,golang的go mod,python也有virtualenv和pyvenv等机制。为了建立依赖快照,通常会用pip freeze > requirements.txt 命令生成一个requirements.txt文件,在一些场景下这种方式就可以满足需求,但是在复杂场景下requirements.txt就力不从心了。 requirements.txt appdirs==1.4.3 astroid==2.3.3 attrs==19.3.0 black==19.3b0 certifi==2019.11.28 chardet==3.0.4 click==7.1.1 et-xmlfile==1.0.1 Flask==1.1.1 gevent==1.4.0 greenlet==0.4.15 idna==2.9 isort==4.3.21 itsdangerous==1.1.0 jdcal==1.4.1 Jinja2==2.11.1 lazy-object-proxy==1.4.3 MarkupSafe==1.1.1 mccabe==0.6.1 numpy==1.18.2 openpyxl==3.0.3 pandas==1.0.3 pylint==2.4.4 python-dateutil==2.8.1 pytz==2019.3 requests==2.23.0 six==1.14.0 tinydb==3.15.2 toml==0.10.0 typed-ast==1.4.1 urllib3==1.25.8 Werkzeug==1.0.0 wrapt==1.11.2 requirements.txt文件中只记录了依赖的版本,所以如果遇到官方的pypi源下载速度慢,需要使用更快的国内镜像下载,通常只能使用pip install -i安装或者修改全局的pip.conf文件。 当某个项目使用确定的python版本,这个版本也并不能在requirements.txt中体现,只能通过readme或者文档来记录,并且需要在创建虚拟环境时手动调用正确的python版本。 项目需要使用flake8、pylint、black等代码优化工具时,这些依赖也会被pip freeze命令写入requirements.txt中,然而这些依赖是不需要出现在生产环境的。 Pipfile Pipenv的出现,一举解决了上面的问题,Pipenv是Kenneth Reitz在2017年1月发布的Python依赖管理工具,他所基于的Pipfile则用来替代requirements.txt。 [[source]] name = "pypi" url = "https://pypi.doubanio.com/simple" verify_ssl = false [dev-packages] isort = "*" black = "==19.3b0" pylint = "*" [packages] flask = "*" tinydb = "*" pandas = "*" requests = "*" gevent = "*" openpyxl = "*" [requires] python_version = "3.6" 好处1:记录内容更详细 相比于requirements.txt,Pipfile多了pip源的设置,可以针对不同项目使用不同环境。并且将依赖分为dev和默认环境,例如pylint、flake8、black等依赖,可以将他们放入dev依赖中。 ...

三月 31, 2020 · 1 分钟 · Zhiya

Docker COPY 复制文件夹的诡异行为

问题现象 在制作 docker 镜像时,有复制某一个路径下所有文件和文件夹到镜像的需求,写下了如下 dockerfile: FROM alpine WORKDIR /root/test_docker_proj COPY * ./ 原始目录结构是这样的: /projects/test_docker_proj ├── Dockerfile ├── dir1 │ ├── dir11 │ │ └── file11 │ └── file1 └── file2 然而复制到 docker 镜像里的目录结构变成了这样: /root/test_docker_proj ├── Dockerfile ├── dir11 │ └── file11 ├── file1 └── file2 可以看到 dir1 这个文件夹并没有被复制到镜像里,但是 dir1 中的子文件夹和文件都被复制进来了,和 dir1 同级的文件也被复制了。也就是说,在 COPY 执行的过程中,第一层文件夹被「解包」了。 COPY/ADD 行为逻辑 为了确定 COPY 和相似的 ADD 命令的行为,做了以下测试: FROM alpine WORKDIR /root/test_docker_proj_1 COPY * ./ WORKDIR /root/test_docker_proj_2 ADD * ./ WORKDIR /root/test_docker_proj_3 COPY ./ ./ WORKDIR /root/test_docker_proj_4 ADD ./ ./ WORKDIR /root/test_docker_proj_5 COPY ./dir* ./ WORKDIR /root/test_docker_proj_6 ADD ./dir* ./ 通过测试可以发现 COPY/ADD 命令有这么几个规则: ...

十月 28, 2019 · 1 分钟 · Zhiya