Golangでコネクションプーリングを実現するためにsync.Poolを使ってみたら意図しない挙動があったのでメモしておきます。Goのバージョンは1.12.5です。
※database/sqlだと内部的にプーリングされているためこういう実装は必要ないのですが、今回は別のもの、具体的にはstreadway/amqpだったため、実装が必要になりました。ただし、普通のオブジェクトでも再現したため、AMQPかどうかは関係ないと思います。
経緯
HTTPでアクセスを受けてAMQPにメッセージをパブリッシュするというAPIを作ろうとしていました。その際のAMQPのチャンネルのプーリングとして、標準ライブラリーにあるsync.Poolを使ってみました。
sync.Pool自体は、単純にプールから取り出す、プールに戻す、プールにない場合は作って返すという機能しかありません。さらに、GCによってプールに入っているオブジェクトが勝手に捨てられてしまうため、コネクションプーリングには厳しそうな感じがしました。
しかし調べたところ、この辺を見るとruntime.SetFinalizerを使っても良さそうなので、GCに回収された時にCloseする挙動をSetFinalizerで実装し、コネクションプールとして使用してみました。(実際にはチャンネルのプールですが。)
何が起こったか?
性能の測定のために負荷をかけて動作したので満足していたのですが、ふとRabbitMQのWeb UIを確認すると、数百個のチャンネルが開きっぱなしに。その後もアクセスするたびにチャンネルは増え、明らかにチャンネルが足りるはずの状況でもなぜかチャンネルは増えていきます。(1000以上チャンネルを持っている状態で数百回のアクセスを発生させるなど。)
最初はSetFinalizerを疑ったものの、runtime.GCでGCを発生させるとチャンネルは綺麗に無くなりました。また、Finalizerの中でメッセージを表示させるようにしたら表示されたので、SetFinalizerは正常に動作しているようでした。
さらに、AMQPのチャンネルではなく、適当なオブジェクトをsync.Poolに入れるようにして試験してみたところ、問題が再現。どうやらAMQPとは関係なさそうでした。
原因は不明…
IntelliJのデバッガーで追ってみましたが、結局分からず…並列処理にまつわる挙動なのかなとは思います。ただ、その割に手でポチポチ連打する程度のリクエストでも再現してしまうのが不思議なところです。
とりあえずsync.Poolはバージョン1.3からある機能のようなので今更バグもないでしょうし、GetとPutしかないインターフェイスからしてみても、コネクションプーリングに使うものではなかったのだと解釈しました。現状の実装だとGCが走ると全部捨てられるらしいですしね。
余談
AMQPのChannelは論理的なものなので、毎回開いて閉じたらどうかと思いましたが、負荷をかけて大量のアクセスをしたら反応しなくなりました(笑)
なお、for文の中でGoルーチンを生成して、その中からsync.Poolにアクセスしても特に再現はしませんでした。