Hi all,
I thought I'd write here about this for reference. I have an app that does
client/server with very long lived tcp sockets - connections intend to last
forever and in practice can last many months only needing to be re-established
if a process on one side is restarted.
Originally, I had a single gevent server process with many clients (dozens) -
and there are certain types of i/o operations or sending large files over the
socket that can be cpu-bound -- and the server process became the bottleneck.
So, I tried several ways to spawn a process when accepting a connection, but,
discarding the event-loop state and other state in the sub-process was always a
problem, but I recently discovered subprocess.Popen has this pass_fds=()
argument...
Under the hood on Linux what this does is a fork, then it immediately closes
all file descriptors not in this list, followed by an exec() call which
replaces the current process - so you get a clean process without the existing
event loop and files you don't care about. So this gave me the idea, I could
simply subprocess.Popen sys.argv[0] with some alternate arguments while
maintaining a couple sockets between the server process and now a sub-process
handling this new client connection. This is of course pretty heavy if the
new-connection rate is high, but for long-lived connections like this, it's
acceptable.
To communicate between the server proc and the proc handling the client, I
create a socket pair using socket.socketpair() keeping one end in the server
process, and the other in the client-handler process. This client-handler
process handles the heavy-weight messages and proxies other messages to the
server process [1]. This is fairly elegant I think, it starts here:
https://github.com/mattbillenstein/salty/blob/master/server.py#L90
Where we accept new connections - and the Popen()'d process runs here:
https://github.com/mattbillenstein/salty/blob/master/client_proc.py
I pass the fds to the new process via the cli (sys.argv) - thought being
explicit here was easiest.
So the picture before was just:
+--------+ +--------+
| server | <---> | client |
+--------+ +--------+
It became:
+--------+ +-------------+ +--------+
| server | <---> | client-proc | <---> | client |
+--------+ +-------------+ +--------+
Where server/client are typically on different machines, but server/client-proc
are always on the same machine.
This entire project implements a config management and deployment system in the
style of saltstack and ansible - stealing ideas from both. The client machines
run a long-lived agent, wait for commands from the server, and can make async
requests to the server for various file resources using a custom async msgpack
rpc protocol. I put this behind a chatops style deployment frontend and for
small changes can deploy to dozens or more systems in <10s.
thx
m
1.
https://github.com/mattbillenstein/salty/blob/master/client_proc.py#L76
--
Matt Billenstein
ma...@vazor.com
https://vazor.com