src.http_server package

Summary

This simulation investigates how a SoyutNet based load distribution performs compared to a plain asyncio socket server implemented by Uvicorn for different number of concurrent requesters.

The auto-generated PT net diagram is given below for 4 concurrent requesters. Let us denote the number of concurrent requesters by \(N\).

_images/graph1.png

In the diagram,

  • pro0 to pro3 are producers and requests are distributed to these producers.

  • con0_0 to con3_0 are consumers and reads requests data and writes response to the socket.

  • It is assumed that, the processing time of each requests takes an amount of time distributed according to the normal distribution with mean, \(\mu\) and standard deviation \(\sigma\).

  • All arc labels are different. (\(\left\{1\right\}\) to \(\left\{N\right\}\))

In this simulation

  • \(\mu = 0.02 sec\)

  • \(\sigma = 0.001 sec\)

Each simulation starts an ab (server benchmarking tool) process which sends 4096 POST requests with 1024 byte request body size and varying number of concurrent requests.

System description

The only implementational difference between Uvicorn and this simulation is:

  • The asyncio task loop \(3N\) additional tasks added by SoyutNet.

  • Requests are handled after passing through 2 asyncio.Queues.

The whole implementation can be found at https://github.com/dmrokan/soyutnet-simulations/blob/main/src/http_server/main.py

Producer

In this case, the main asyncio loop starts a Uvicorn HTTP server.

208
209    uvicorn_server = [None]
210
211    async def canceller():
212        try:
213            ab_proc = psutil.Process(AB_PID)
214            while ab_proc.is_running() and ab_proc.status() != psutil.STATUS_ZOMBIE:
215                await asyncio.sleep(1)
216        except psutil.NoSuchProcess:
217            pass
218        await asyncio.sleep(1)
219        if uvicorn_server[0] is not None:
220            await uvicorn_server[0].shutdown()
221        soyutnet.terminate()
222
223    """Automatically terminate after ab ends"""
224
225    soyutnet.run(
226        reg,
227        extra_routines=[
228            uvicorn_main.main(
229                uvicorn_app, HOST, PORT, canceller, uvicorn_server, CONCURRENT_REQUESTS
230            )
231        ],
232    )
233    """Start simulation"""
234

The canceller task waits for benchmarking tool to be completed by checking its process status using psutil library. Then, it ends the simulation.

A new token is generated when the HTTP server receives a request. The request data is binded to the token.

105
106    LABEL_MAX = CONCURRENT_REQUESTS
107
108    label_counter = 0
109
110    def new_label():
111        nonlocal label_counter
112        label_counter %= LABEL_MAX
113        label_counter += 1
114        """Assign a label from 1 to LABEL_MAX to determine the path it will follow in the net."""
115        return label_counter
116
117    def new_http_request_token(scope, receive, send, cond):
118        label = new_label()
119        token = net.Token(label=label, binding=(scope, receive, send, cond))
120        treg.register(token)
121
122        return (token._label, token._id)
123
124    async def uvicorn_app(scope, receive, send):
125        if scope["type"] != "http":
126            return
127        cond = asyncio.Semaphore(value=0)
128        token = new_http_request_token(scope, receive, send, cond)
129        label = token[0]
130        req_queues[(label - 1) // BRANCH_COUNT].put_nowait(token)
131        await cond.acquire()
132        """Wait until endpoint fullfills HTTP request"""
133
134    async def producer(place):
135        index = int(place._name[3:])
136        token = await req_queues[index].get()
137        return [token]
138
139    """Inject token"""
140

Then, the token is injected to the PT net. However, only the label and ID of token travels through the net. The binded object is registered in the soyutnet.SoyutNet.TokenRegistry.

Consumers

Similar to the PI Controller simulation, consumers \(e_1\) and \(e_2\) receive tokens as a label and ID. Then they convert it to the actual token as given below.

146
147    async def consumer(place):
148        async def http_server(uvicorn_scope, uvicorn_receive, uvicorn_send):
149            await uvicorn_main.app(uvicorn_scope, uvicorn_receive, uvicorn_send)
150
151        nonlocal consumer_stats
152        t0 = time.time()
153        ident = place.ident()
154        if ident not in consumer_stats:
155            """Initialize stats at first call of the producer."""
156            consumer_stats[ident] = {"started_at": time.time(), "count": 0}
157            """Store initial time and number of requests processed to calculate requests per second."""
158
159        label = place._input_arcs[0]._labels[0]
160        token = place.get_token(label)
161        T = time.time()
162        if not token:
163            consumer_stats[ident]["last_at"] = time.time()
164            return
165
166        actual_token = treg.pop_entry(*token)
167        """Get actual SoyutNet.Token object from SoyutNet.TokenRegistry"""
168        if actual_token is None:
169            consumer_stats[ident]["last_at"] = time.time()
170            return
171
172        uvicorn_scope, uvicorn_receive, uvicorn_send, cond = actual_token.get_binding()
173        """Get object binded to the actual token"""
174        await http_server(uvicorn_scope, uvicorn_receive, uvicorn_send)
175        """Fulfill the request."""
176        cond.release()
177        """Inform uvicorn_app that request is replied"""
178
179        consumer_stats[ident]["count"] += 1
180        consumer_stats[ident]["last_at"] = time.time()
181

The Uvicorn application’s implementation is in https://github.com/dmrokan/soyutnet-simulations/blob/main/src/http_server/uvicorn_main.py

Controllers

SN

The only implementational difference between Uvicorn and this simulation is:

  • The asyncio task loop \(3N\) additional tasks added by SoyutNet.

  • Requests are handled after passing through 2 asyncio.Queues.

UV

This is a plain Uvicorn HTTP echo server which is implemented to compare the results to SN.

Results

It is assumed that, the processing time of servers are modeled by a normal random variable with an average processing delay of 0.02 seconds (50Hz) and standard deviation if 0.001 seconds.

Each simulation starts an ab (server benchmarking tool) process which sends 4096 POST requests with 1024 byte request body size and varying number of concurrent requests.

ab tool can save the results in CSV file with the structure below.

Percentage served,Time in ms
0,5.614
1,6.456
2,6.487
3,6.529
4,6.612
5,6.646
6,6.664

For example 5th line show that, 4% of requests replied in less than 6.65 milliseconds.

In summary, the same simulation run for three different controllers and several different number of concurrent requests and CSV files are obtained.

The figure below plots the columns of CSV files. The x axis shows the data in the second column of the CSV format given above. The y axis shows the first column divided by 100.

_images/result_11.png

The plot resembles a cumulative normal distrbution. When the numerical derivate of y axis data is taken with respect to x axis data, the plots below is obtained for different number of concurrent requests.

_images/result_21.png

The x axis is time and the y axis is time distrbution for different number of concurrent requests. The integer values on the left of plots show the number of concurrent requests. As the number of concurrent requests increases, the average serving time increases.

Also, a gaussian is fit on the data by using the gradient decent solution to the nonlinear least squares problem. The plot labels shows the mean (\(\mu\)) and standard deviation (\(\sigma\)) of the gaussion function which fits the data.

Comments

  • SN has a distribution which very closely matches the artifical delay in the HTTP request/response loop. But, it gets slower as the number of concurrent requesters goes beyond 8.

  • [Work in progress]

Reproduce

sudo apt install python3-venv apache2-utils
python3 -m venv venv
source venv/bin/activate

make build
make build=http_server
make clean=http_server
make run=http_server
make results=http_server
make graph=http_server
make docs

Usage

Submodules

src.http_server.main module

src.http_server.main.USAGE()

Arguments:

-T <time (sec)>

total simulation time in seconds (\(T\))

Default: 10

-o <filename>

output file name to write results. If empty, prints to stdout.

-p <rate (Hz)>

new token output rate of the producer at each second

Default: 10

-H hostname

Default: 127.0.0.1

-P port

Default: 5000

-G

if provided, the script generates PT net graph and exits

-A ab command’s PID

-C number of concurrent requests expected

Example

python src/http_balancer/main.py -p 100

src.http_server.results module

src.http_server.results.fit_gaussian(x, pdf)
src.http_server.results.load_results()
src.http_server.results.main(argv)
src.http_server.results.plot_results(results)

src.http_server.uvicorn_main module

async src.http_server.uvicorn_main.app(scope, receive, send, mean=0.02, std=0.001)

Echo the request body back in an HTTP response.

async src.http_server.uvicorn_main.main(app_, host, port, canceller, server_ref, concurrent_requesters)
src.http_server.uvicorn_main.server_main(args)

Module contents