Understanding performance impact on a concurrent server because of GIL

Viewed 149

I am bit confused with what is happening with my code below. I have created a concurrent server to find nth number in fibonacci series. Server creates a thread for each client connection and thread delegates calculation of finding fibonacci number in fibonacci series to a separate process.

Server

# server.py
# Fibonacci Microservice

from socket import *
from fib import fib
from threading import Thread
from concurrent.futures import ProcessPoolExecutor as Pool


pool = Pool(4)


def fib_server(address):
    sock = socket(AF_INET, SOCK_STREAM)
    sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    sock.bind(address)
    sock.listen(5)

    while True:
        client, addr = sock.accept()
        print("Connection", addr)
        Thread(target=fib_handler, args=(client,), daemon=True).start()


def fib_handler(client):
    while True:
        req =  client.recv(100)
        if not req:
            break
        n = int(req)
        future = pool.submit(fib, n)
        result = future.result()
        resp = str(result).encode('ascii') + b'\n'
        client.send(resp)
    print('Closed')


fib_server(('', 25000))

I have also created a sample client that bombards the server for calculating 1st number in fibonacci series. I have also coded a sample monitoring thread around client to find number of requests client is able to send to a server in one second.

Client:

# perf.py
# requests/sec of fast requests

from socket import *
import time
from threading import Thread

sock = socket(AF_INET, SOCK_STREAM)
sock.connect(('localhost', 25000))

n = 0


def monitor():
    global n
    while True:
        time.sleep(1)
        print(n, 'reqs/sec')
        n = 0


Thread(target=monitor).start()

while True:
    sock.send(b'1')
    resp = sock.recv(100)
    n += 1

Now when I run client from one terminal I see that the request rate per second is pretty high (~1900 req/sec). But as soon as I run one more client I observe that the reqs/sec per client is reduced by 50%.

Running one client:

Terminal#1

$ python perf2.py 
1891 reqs/sec
1863 reqs/sec
1813 reqs/sec

Running one more client:

Terminal#2

$ python perf2.py 
1098 reqs/sec
1080 reqs/sec
1080 reqs/sec

Termina#1

$ python perf2.py 
1891 reqs/sec
1863 reqs/sec
1813 reqs/sec 
1089 reqs/sec
1091 reqs/sec
1034 reqs/sec

Running yet another client

However, when I open another terminal and submit a request to find very large number in the fibonacci sequence (CPU intensive job), I see that the reqs/sec of the existing two clients is not impacted.

Terminal#3

$ nc localhost 25000
50
_

Terminal#2

$ python perf2.py 
1098 reqs/sec
1080 reqs/sec
1080 reqs/sec
1078 reqs/sec

Termina#1

$ python perf2.py 
1891 reqs/sec
1863 reqs/sec
1813 reqs/sec 
1089 reqs/sec
1091 reqs/sec
1034 reqs/sec
1058 reqs/sec

Is this all happening because of GIL? Can someone please help me understand what am I missing here.

Python version 3.6.5

1 Answers

Your code suffers from 2 issues:

  1. The process pool doesn't actually do what you intend:

    future = pool.submit(fib, n)
    result = future.result()
    

    Means that you're submitting and immediately waiting / blocking for the other process to finish. De-facto, you have only 1 process.

  2. Your code is much more CPU intensive than you think. You're using a SOCK_STREAM and not a SOCK_DGRAM. A tcp message isn't divided, it's a stream of bytes. Since you recv 100 bytes, multiple sock.send(b'1') might combine together and cause you to search for the 111 fibonacci number.

Related