Adventures in debugging: etcd, HTTP pipelining, and file descriptor leaks
projectclearwater.org
projectclearwater.org
Eventually, Python exits, and closes TCP connection A,
by sending a FIN packet – but etcd doesn’t send a FIN
of its own, or send a zero-byte chunk to finish the 200
OK, as it does in the success case. This causes the
socket to leak on etcd’s side – we think this is a bug
in Go’s HTTP stack.
This really should work so we need to investigate that one more deeply. I am not opposed to adding the timeout option as the author would like to do and we would happily consider a patch for this.Update: here is a proof-of-concept patch to address this feature request: https://github.com/coreos/etcd/pull/3816
Longer term etcd will use gRPC/http 2.0 in the new etcd v3 API[1] to address use cases like this. In particular gRPC will allow us to have multiple streams while keeping open a single TLS connection. A major expense of the current watch API is having to tear down connections as explained in this post. We will still support http/rest likely through the use of grpc-gateway[2].
The file descriptor rlimit issue that was the side-effect of this issues is why databases like etcd have to protect themselves. For example a few months ago etcd learned to reserve file descriptors for itself in case we get clients hogging up resources[2]. Longer term we would like to be able to let proxies handle watches directly to further protect the core quorum of etcd.
[1] https://github.com/coreos/etcd/blob/master/Documentation/rfc... [2] https://github.com/gengo/grpc-gateway [3] https://github.com/coreos/etcd/pull/3219
Kubernetes 1.1 will have an iptables based proxy too: https://github.com/kubernetes/kubernetes/issues/3760#issueco...
i am a bit puzzled why other people are not constantly bitten by this, though.
The server can't know[1] that client has fully closed the connection until it tries to write something to it - I'm guessing there is some sort of long polling going on here, so the server has parked the http request somewhere and isn't going to write anything to the connection until something happens (if ever). After all, at this point the server doesn't know the TCP connection is fully closed, it thinks it's half closed.
[1] Unless you also enable tcp keepalive, the keepalive would discover that the socket is closed on the other end (after a couple of hours by default).
In the past we (urllib3) have been pretty optimistic about assuming connections are still useful when we hit exceptions, but increasingly that optimism seems to be hurting us.
For that reason, I've taken your fix, added tests for it, then proposed it to the main urllib3 repository[0]. Right now the commits are in my name, but I'd be totally happy for you to make them yourself and open a pull request containing my commits. Either way, I've also added you to our contributors list.
Thanks for the write up!
Curious about what on go side doesn't actually close the tcp connection, I'd assume channel (assumming it's using channels per watcher) would catch some exception when socket is closed on python side, maybe just socket is never closed after exception? Time to try learn some go to see in etcd code base...
Either end of a connection doesn't know if the other end closed it until they try to do something with the socket, per the BSD socket API. The etcd watch API, from reading the article, doesn't reply until an event occurs, so if an event never occurs in time (before a whole bunch more requests come in), it won't do anything with the socket, and thus won't find out the other end closed it.