みずりゅの自由帳

主に参加したイベントやソフトウェア技術/開発について記録しています

gRPCのクライアントを動かそうとしてno_proxyに惑わされる。

前回に続き、gRPCとProxy配下の環境ネタ。

ちなみに、前回ハマってた内容はこちら。
mzryuka.hatenablog.jp

前提として、Proxy配下の環境。
localhost内でGo言語以外の言語で動かしたgRPCのクライアントが、gRPCのサーバ側にアクセスできなかった。
ホストOS上で直接実行した時と、Docker環境上で実行した時とでそれぞれ発生。

結論から言うとタイトル通りに「no_proxy」が原因。対応方法には差異があったので、それぞれについて説明する。

なお、解決については、以下のページの情報を参考にした。
https://stackoverflow.com/questions/45183076/grpc-c-client-14-connect-failed
https://github.com/grpc/grpc/issues/9989
https://github.com/grpc/grpc/issues/12815

ホストOS上で実行した時:

例えば、Rubyで書かれたgRPCのクライアントを実行。その際に出たエラーは以下の通り。

$ ruby greeter_client.rb 
bundler: failed to load command: greeter_client.rb (greeter_client.rb)
GRPC::Unavailable: 14:Connect Failed
  /home/username/.rvm/gems/ruby-2.4.0/gems/grpc-1.18.0-x86_64-linux/src/ruby/lib/grpc/generic/active_call.rb:31:in `check_status'
〜(中略)〜
  greeter_client.rb:31:in `main'
  greeter_client.rb:35:in `<top (required)>'

ここで出ている「14:Connect Failed」は、サーバ側にアクセスできないために発生したエラー。
他の言語(PythonPHP)でも試してみたが、同じエラーコード(14)が発生していた。

この問題の原因は、「no_proxy」の値を設定していなかったと言うもの。
今回の場合、localhost内で通信するはずがProxy側に通信が流れてしまい、Proxy側からlocalhostに通信が戻ってこなかったために発生した。
そのため、localhost同士の通信にはProxyを経由しないようにno_proxyの値を設定してやれば良い。
コンソール上で、以下のようにlocalhost127.0.0.1はproxyを利用しないように設定。

$ export  no_proxy=localhost,127.0.0.1

ちなみに、自分の環境だとno_proxyに空の値を設定しても動作した。どう言う理屈なのかいまいちよくわからない。

$ export  no_proxy=""

もしかして、環境変数にno_proxyがあるかだけを見ているのだろうか?

Docker環境で実行した時:

Docker環境上で、Go言語以外のクライアントを実行しようとした場合の話。
ホストOSの場合とは異なり、localhostに対してのno_proxy設定は、Docker実行時に設定されていた。にも関わらず、サーバに繋がらないエラーが発生。

以下、gRPCのクライアントにPHPを利用している場合の例。わかりやすく、クライアント側のコンテナに入って確認してみる。

まず、no_proxyの設定があるか。

root@a05b93c7cb28:/grpc-training/example/php/client# printenv | grep localhost
no_proxy=127.0.0.1,localhost
NO_PROXY=127.0.0.1,localhost

しかし、実行してみると、以下のようにエラーとなる。

root@a05b93c7cb28:/grpc-training/example/php/client# php client.php
object(stdClass)#12 (3) {
  ["metadata"]=>
  array(0) {
  }
  ["code"]=>
  int(14)
  ["details"]=>
  string(14) "Connect Failed"
}

Fatal error: Uncaught Error: Call to a member function getMessage() on null in /grpc-training/example/php/client/client.php:21
Stack trace:
#0 /grpc-training/example/php/client/client.php(24): request('World.')
#1 {main}
  thrown in /grpc-training/example/php/client/client.php on line 21
root@a05b93c7cb28:/grpc-training/example/php/client#

出力内容の「metadata」云々の箇所は、わかりやすくエラーメッセージの詳細を出力してみたもの。ここでもエラーコード14の「Connect Failed」が出ているので、サーバに通信が届いていない状況になっている。しかし、最初にも書いたがDocker内では環境変数「no_proxy」は設定されていた。

調べてみた結果、gRPCはDocker内のno_proxyは正しく判断できていないらしい。
このため、ホストOSのProxy設定を引き継いで動作していると、通信がProxy側に向かってしまいlocalhostに戻って来ない。
ではどうするかというと、Docker内で設定しているProxyの設定である環境変数「http_proxy」「https_proxy」を除去する。環境変数の削除には「unset」を利用する。(※空文字の設定では環境変数は残るので効果がない)

なお、これは実行の都度必要になってくるので、gRPCのクライアントをラップしたスクリプトを用意し、ラップしたスクリプト経由でクライアントを実行してやれば良い。

以下、ラップするスクリプトのイメージ。
なお、自分の環境では大文字の環境変数「HTTP_PROXY」「HTTPS_PROXY」は残っていても影響がなかった。そのためスクリプト内では削除せず残したままになっている。

root@a05b93c7cb28:/grpc-training/example/php/client# cat wrap_client.sh
#!/bin/bash

echo "-------------------------------------------------"
printenv | grep proxy
echo "-------------------------------------------------"
unset http_proxy
unset https_proxy
echo "-------------------------------------------------"
printenv |grep  proxy
echo "-------------------------------------------------"
php client.php

ラップしたスクリプトを実行して、gRPCのクライアントを動かす。

root@a05b93c7cb28:/grpc-training/example/php/client# bash wrap_client.sh
-------------------------------------------------
HTTP_PROXY=http://proxy.example.jp:8080
https_proxy=http://proxy.example.jp:8080
http_proxy=http://proxy.example.jp:8080
no_proxy=127.0.0.1,localhost
HTTPS_PROXY=http://proxy.example.jp:8080
-------------------------------------------------
-------------------------------------------------
HTTP_PROXY=http://proxy.example.jp:8080
no_proxy=127.0.0.1,localhost
HTTPS_PROXY=http://proxy.example.jp:8080
-------------------------------------------------
object(stdClass)#12 (3) {
  ["metadata"]=>
  array(0) {
  }
  ["code"]=>
  int(0)
  ["details"]=>
  string(0) ""
}
Hello World.

環境変数のhttp_proxyとhttps_proxyが無くなったことにより、gRPCのサーバに通信が届いて処理結果が戻ってきたのが確認できた。


まとめ:

gRPCを触るときは、できるだけ「Proxy環境配下」では実施しないこと。
やらざるを得ないときでも、どこの問題かを切り分けられるようにするためにも、一旦は「Proxy環境配下」ではないところであらかじめ動作確認をしておくことをお勧めする。

ちなみに、Proxyの問題だと気づくのに遅れた要因としては、golangで書かれたクライアントを実行しているとProxyに関する問題が発生しなかったこと。golang用のgRPCライブラリでは、no_proxyやhttp_proxyをロジック中で良さげに判断してくれているのかな。
うーん。。。