質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

Q&A

解決済

1回答

1565閲覧

ローカルホストが突然全く反応しなくなります

ryoyai

総合スコア5

Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

0グッド

1クリップ

投稿2020/01/29 13:55

編集2020/01/31 05:41

現象

GoでLocalhostを立て、そこにブラウザでアクセスする形式(同一マシン内)のアプリケーションを作成しているのですが、
ときどきローカルホストから全くレスポンスが返ってこなくなるという事象が生じており、非常に困っています。

たとえば以下のようになります。

  1. しばらく正常に動作している
  2. リクエスト1をおこなう →すると突然レスポンスが返ってこない
  3. このあと、リクエスト2、リクエスト3と続けてリクエストを出しても、全て無反応となる。

上記のように、ときどきローカルホストが全く無反応の状態になります。

ローカルホストが動作しているWindowsのdos窓を見ると、リクエスト1の時点でログの流れがストップしています。
しかしここでdos窓をクリックしてf5キーを押すと、突然ログが流れ始めます。そして、リクエスト1,2,3に対するレスポンスが一度に返ってきます。

環境などは以下です。

Win10 64bit
Go1.13
github.com/gorilla/mux を利用

go

1r := mux.NewRouter() 2r.HandleFunc("/path", Func).Methods("GET", "POST", "OPTIONS") ... 3 4srv := http.Server{ 5 Addr: ":48000", 6 Handler: r, 7 TLSConfig: nil, 8 ReadTimeout: 0, 9 ReadHeaderTimeout: 0, 10 WriteTimeout: 0, 11 IdleTimeout: 0, 12 MaxHeaderBytes: 0, 13 TLSNextProto: nil, 14 ConnState: nil, 15 ErrorLog: nil, 16 BaseContext: nil, 17 ConnContext: nil, 18} 19srv.SetKeepAlivesEnabled(false) //keepaliveが悪影響を及ぼす場合があると聞いてオフにしてみた 20err := srv.ListenAndServe() 21if err != nil { 22 log.Fatalf(err.Error()) 23} 24

条件、再現性など

上記停止現象の発生条件は分かりません。

メソッドはget のときも postのときもあります。
またlocalhost内でエラー(ロジックに由来するエラー)が起きているときもありますが、全く問題が無い(リクエストは所定の形式でなされており、ローカルホストでの処理中のエラーも起きていない)場合もあります。

リクエストはブラウザ(fetchを使用)から送る場合でも、ツール(postmanを使用)で送る場合でも発生します。
発生頻度はそれなりに高く、リクエスト数十回に1回くらい見かける印象です(プロダクトでこの頻度だと使い物にならないと思います)。

以上のような状況であるため、この場合にこうなるというのは見つかられていません。あるとき突然なるという認識です。
ただしその状態が生じた場合、「dos窓をクリックしてf5キー」によって確実に回復します。これは今のところ例外無しです。

試したこと

正直なにが起きているのかよく理解できていません。
F5キーで回復するというところに何か現象を理解するヒントがあるようには思えるのですが。
keepaliveが悪影響を及ぼす場合があるという情報を見かけたので、srv.SetKeepAlivesEnabled(false)としてみましたが改善しませんでした。

質問

以上のような状況ですが、どのように対処したらよいでしょうか。
アプリケーションの動作の根幹にかかわる部分で、かなり深刻なのですが、解決方法が分からず非常に困っています。
アドバイスいただければ幸いです。
よろしくお願いいたします。

追記:サンプルコード

ご指摘を受け、実際のプログラムの挙動に似せたものをサンプルコードとして作成してみました。

これを作っている過程で気づいたのですが、このプログラムはグローバル変数としてキャッシュを持っています。
そのキャッシュデータに対する同時アクセス制御(mutex)に問題があるのではないかと思い、mutexまわりをいくつか修正してみたところ、停止現象がかなり減った感じがしています。
そのため現時点ではグローバル変数への同時アクセスが影響しているのではないかと疑っています。
(ただし、それがF5で解消するというのもよく分からない話のように思え、この点の疑問は残っています)

実際のプログラムに似せるため、プログラム内にファイルシステムへのアクセスが存在します。ご注意お願いします。

go

1package main 2 3import ( 4 "bufio" 5 "encoding/json" 6 "io/ioutil" 7 "log" 8 "net/http" 9 "os" 10 "path" 11 "strconv" 12 "sync" 13 14 "github.com/gorilla/mux" 15) 16 17var ( 18 DataCache = map[string]string{} 19 Datapath string 20 mutex sync.RWMutex 21) 22 23type res_ping struct { 24 Msg string `json:"msg"` 25} 26 27func GetPing(w http.ResponseWriter, r *http.Request) { 28 log.Println("GetPingStart") 29 defer log.Println("GetPingEnd") 30 31 res := res_ping{Msg: "server_alive"} 32 b, _ := json.Marshal(res) 33 w.Write(b) 34} 35 36type res_get struct { 37 Msg string `json:"msg"` 38} 39 40func GetData(w http.ResponseWriter, r *http.Request) { 41 log.Println("GetDataStart") 42 defer log.Println("GetDataEnd") 43 44 line, _ := mux.Vars(r)["line"] 45 46 if line == "" { 47 line = "0" 48 } 49 mutex.RLock() 50 msg := DataCache[line] 51 mutex.RUnlock() 52 53 res := res_get{Msg: msg} 54 b, _ := json.Marshal(res) 55 w.Write(b) 56 57} 58 59type req_post struct { 60 Data string `json:"data"` 61} 62 63type res_post struct { 64 Msg string `json:"msg"` 65} 66 67func PostData(w http.ResponseWriter, r *http.Request) { 68 log.Println("PostDataStart") 69 defer log.Println("PostDataEnd") 70 71 body := r.Body 72 defer body.Close() 73 b, _ := ioutil.ReadAll(body) 74 req := req_post{} 75 _ = json.Unmarshal(b, &req) 76 data := req.Data 77 78 mutex.Lock() 79 defer mutex.Unlock() 80 file, _ := os.OpenFile(Datapath, os.O_WRONLY|os.O_APPEND, 0666) 81 defer file.Close() 82 file.WriteString(data + "\n") 83 file.Close() 84 readdata() 85 86 res := res_post{Msg: "OK"} 87 rb, _ := json.Marshal(res) 88 w.Write(rb) 89 90} 91 92func SetJsonheader(w http.ResponseWriter) { 93 w.Header().Set("Content-Type", "application/json") 94} 95 96func Wrap(f func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { 97 return func(w http.ResponseWriter, r *http.Request) { 98 EnableCors(&w) 99 SetJsonheader(w) 100 f(w, r) 101 } 102} 103 104func EnableCors(w *http.ResponseWriter) { 105 (*w).Header().Set("Access-Control-Allow-Origin", "http://localhost:8004") 106 (*w).Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") 107 (*w).Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") 108} 109 110func setup() { 111 log.Println(Datapath) 112 file, err := os.Create(Datapath) 113 if err != nil { 114 log.Fatal(err) 115 } 116 defer file.Close() 117 // 実際の挙動に近づけるためにファイル書き込み(データ多め)をおこなっています。問題があれば適宜修正してください。 118 for i := 0; i < 10000; i++ { 119 _, _ = file.WriteString("data898999898989898998989ddddd\n") 120 } 121 file.Close() 122 123 readdata() 124 //DataCache["0"] = "data0" 125 //DataCache["1"] = "data1" 126 //DataCache["2"] = "data2" 127} 128 129func readdata() { 130 file, _ := os.Open(Datapath) 131 defer file.Close() 132 sc := bufio.NewScanner(file) 133 count := 0 134 for sc.Scan() { 135 DataCache[strconv.Itoa(count)] = sc.Text() 136 count++ 137 } 138} 139 140func main() { 141 142 datadir := os.TempDir() 143 datapath := path.Join(datadir, "data_6f3eb709jlgq0w2h.txt") 144 Datapath = datapath 145 setup() 146 defer os.Remove(datapath) 147 148 r := mux.NewRouter() 149 r.HandleFunc("/ping", Wrap(GetPing)).Methods("GET", "POST", "OPTIONS") 150 r.HandleFunc("/data/{line}", Wrap(GetData)).Methods("GET", "OPTIONS") 151 r.HandleFunc("/data", Wrap(GetData)).Methods("GET", "OPTIONS") 152 r.HandleFunc("/data", Wrap(PostData)).Methods("POST", "OPTIONS") 153 154 srv := http.Server{ 155 Addr: ":48000", 156 Handler: r, 157 TLSConfig: nil, 158 ReadTimeout: 0, 159 ReadHeaderTimeout: 0, 160 WriteTimeout: 0, 161 IdleTimeout: 0, 162 MaxHeaderBytes: 0, 163 TLSNextProto: nil, 164 ConnState: nil, 165 ErrorLog: nil, 166 BaseContext: nil, 167 ConnContext: nil, 168 } 169 log.Println("start") 170 srv.SetKeepAlivesEnabled(false) 171 err := srv.ListenAndServe() 172 if err != nil { 173 log.Fatalf(err.Error()) 174 } 175} 176

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nobonobo

2020/01/30 00:41

セキュリティソフトにブロックされていないか確認してみてください。 また、パッと試せるよう動作可能なコードを貼ってもらえると回答しやすいです。 (問題が再現する最小のコードだとより助かります)
nobonobo

2020/01/31 01:06 編集

みれるコードをみてみたところおかしいところはなさそうです。(Windows10で問題なく動作することを確認しました)
ryoyai

2020/01/31 05:43

ご回答ありがとうございます。サンプルコードを追加してみました。 そこにも書きましたが、mutex関連の問題があるのではないか?と現在考え中です。
ryoyai

2020/01/31 05:50

なお、上記サンプルコードでは停止現象は再現できていません。(サンプルコードのmutex周りを悪いほうにいじって、現象を再現できたらおそらくヒットということになるかもしれません)
nobonobo

2020/01/31 23:02

あ、*http.RequestのBodyはCloseしてはいけません。それが原因かはまだわかりませんが。
nobonobo

2020/01/31 23:04

コネクション数の制御がおかしくなってる可能性は高いです。
ryoyai

2020/02/01 02:22

! Bodyの件、こちらですか。始めて知りました。ありがとうございます。 type Request // Body is the request's body. // // For client requests, a nil body means the request has no // body, such as a GET request. The HTTP Client's Transport // is responsible for calling the Close method. // // For server requests, the Request Body is always non-nil // but will return EOF immediately when no body is present. // The Server will close the request body. The ServeHTTP // Handler does not need to. Body io.ReadCloser https://golang.org/pkg/net/http/#Request
ryoyai

2020/02/02 04:06

数日間問題が起きていないので、一応解決したということにして、まとめを投稿しました。ご丁寧に対応いただきありがとうございました。
guest

回答1

0

自己解決

数日間、停止現象は出ていません。完璧とは言えませんが、一応解決としたいと思います。
以下、知見を記します。

変数への同時読み書きに注意

GolangのLocalhostが特段のエラー表示も無く突然動作を停止する場合、変数に対する同時読み書き(以下、同時RW)が問題となっている可能性があります。
グローバル変数にキャッシュを持たせているような構成の場合には特に注意してください。

Goのサーバーは標準で並行処理を行います(goroutine)。サーバー内でグローバル変数を考え無しに利用すると、常に変数への同時RWのリスクが生じます。
手元の検証結果によれば、Mapへの同時RWはすぐにPanicを起こしますが、stringや構造体への同時RWはPanicを起こすとは限らないようです(検証範囲ではPanicが起きなかった)。そのため問題の発見が難しくなるかもしれません。

変数の同時RWを理解する上で、下記の記事が役に立ちました。注意則を抽出するならば、**「複数のプロセスから、対策無しに同じ変数を扱うことは、常に危険である」**と言えると思います。

Goでスレッド(goroutine)セーフなプログラムを書くために必ず注意しなければいけない点

Mutexの利用

変数の同時アクセスの制御には、sync.Mutexを利用します。(サンプルコード参照)
sync.Mutexはインスタンスごとにロックアンロックのスコープ?を持ちますので、変数A,B,Cについてそれぞれ別の制御をおこなうならば、3つのMutexをインスタンス化し、それをなんらかの方法で共有します(引数での受け渡しのほか、Mutex自体をグローバル変数として共有しても問題無いと理解しています)。

完全ではないものの、以上で一応の解決としたいと思います。

補足:サンプルコードの重要性

なお、途中でサンプルコードを作るようにという助言をいただきました。
掲載したサンプルコードを作るのに90分を要しましたが、作っている過程で上記の問題にすぐに気づくことができました。
また、その後、本番プロダクトでは難しい「実験」に利用することもできるようになって、利便性も感じています。
本質部分で本番コードと似たサンプルコードを作る、というのは、もちろん基本と言えると思いますが、あらためて有用性を理解しました。

投稿2020/02/02 04:05

ryoyai

総合スコア5

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nobonobo

2020/02/02 22:53 編集

全ての型の値について同時読み書きが不定な挙動を招くことはありません。 起こるとすれば正常に読み書きか、panicかの二択です。スライスは意図しないでデータセットになる可能性はありますが、勝手に処理が中断するようなことはありません。
nobonobo

2020/02/03 01:40

停止したかのような挙動はデッドロックしたgoroutineが発生しているか、panicを捨てている処理があるはず(http.Serverはrecover処理で補足したpanicをErrorLogまたは標準ロガーで出力しようとします)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問