6
\$\begingroup\$

I put up a simple WSGI static file handler, but I'm skeptical about its efficiency, also because I got a handful of errors while benchmarking it with Apache Benchmark.

The errors are:

IOError: [Errno 24] Too many open files: 'test.png'
Client 101 hit errno 32
Client 601 hit errno 32
Client 408 hit errno 32
Client 225 hit errno 32
Client 668 hit errno 32
Client 415 hit errno 32
Client 237 hit errno 32
Client 316 hit errno 104
...

This is what brought me to think it's inefficient.

The following is the Wsgi static file handler:

import bjoern
import os

# Get file size
size = os.path.getsize("test.png")

def app(environ, start_response):
    status = "200 OK"
    # Open image file
    the_file = open("test.png", "rb") 
    response_headers = [ ('Content-Type', 'image/png'), ('Content-length', str(size)) ]

    start_response( status, response_headers ) 

    # return the entire file 
    if 'wsgi.file_wrapper' in environ:
        # Return env[wsgi.fw](file, block size)
        return environ['wsgi.file_wrapper'](the_file , 1024) 
    else: 
        return iter(lambda: the_file.read(1024), '')

bjoern.run(app, "localhost", 8000)

Any advice/tips/suggestions?

\$\endgroup\$

3 Answers 3

8
\$\begingroup\$

In total, a process in Unix systems can have a limited number of open filehandles. By default this is only say 1024 per process in Linux; all of the open sockets also consume file descriptors. Even if you optimize the system, then reading the file for each request will be costly.

In this case, if you are really concerned about the performance, read the image into memory (as bytes/str), and send it from there instead:

with open('test.png', 'rb') as f:
    image_data = f.read()

...

    return ( image_data, )

However if you have a veery big file, then you could mmap it into memory in the beginning of the process (this consumes just 1 file descriptor) globally and send it from there in each client:

im_file = open('verybigfile.iso', 'rb')
mm = mmap.mmap(im_file, 0)

    ...
    return ( mm, )

Both of these would save you 1 file descriptor per a client requesting the file.


However, if you need to serve lots of files like this, then you might want to increase the limit of open file descriptors per a process, use a front-side cache such as Varnish and/or scale up into multiple processes, or hosts, or even use a CDN to deliver your files.

\$\endgroup\$
1
  • \$\begingroup\$ Cool! Didn't know you could mmap straight from Python. Images medium size should be around 80kb, so no problem as of big files. With the memory solution the file_wrapper won't even be needed, thus removing a bit of overhead. Thank you :) \$\endgroup\$
    – user965091
    Aug 16, 2014 at 20:36
5
\$\begingroup\$

The code:

# return the entire file 
if 'wsgi.file_wrapper' in environ:
    # Return env[wsgi.fw](file, block size)
    return environ['wsgi.file_wrapper'](the_file , 1024) 
else: 
    return iter(lambda: the_file.read(1024), '')

can be a problem for a couple of reasons.

Now although you have simply copied this from the WSGI specification itself, the use of iter/lambda is arguably bad practice as it doesn't ensure that the file object is closed promptly when consumed. Instead when the file object is closed is dependent on the particular Python interpreter implementation and so may not happen immediately and may only happen when the garbage collector reclaims it.

The end result is that one possible reason for running out of file descriptors is that the file objects aren't being closed in time before you run of file descriptors.

The better way of writing the above code is actually:

# return the entire file

if 'wsgi.file_wrapper' in environ:
    return environ['wsgi.file_wrapper'](the_file , 1024) 

else:

    def file_wrapper(fileobj, block_size=1024):
        try:
            data = fileobj.read(block_size)
            while data:
                yield data
                data = fileobj.read(block_size)
        finally:
            fileobj.close()

    return file_wrapper(the_file, 1024)

What this is relying on for the prompt explicit closing of the file is that when the WSGI server consumes the generator, the finally block will be executed and so the file will be closed.

Even if the generator isn't completely consumed, then the WSGI server is required to call close() on the generator and in doing that, then the finally block will be executed and the file closed.

When you use:

iter(lambda: the_file.read(1024), '')

there is no explicit closing of the file and so you are at the mercy of the interpreter as to when it happens.

Now the second reason there could be a problem is that if the WSGI server does actually implement wsgi.file_wrapper, it doesn't mean that how it is implemented is correct, with it ensuring that the file is closed. It itself could have simply used the iter/lambda trick and be broken.

A further problem is that there have also been various WSGI servers that didn't always call close() properly on the iterable returned from the WSGI application when the request completed, especially in the case of an exception occurring part way through consuming the response. Examples of WSGI servers which have had this problem at certain times are the wsgiref WSGI server in the Python standard library, the Django development server and uWSGI.

I have never looked at bjoern to see whether it implements the WSGI specification correctly, but I am certainly not surprised these days when I find WSGI servers that are broken.

\$\endgroup\$
6
  • \$\begingroup\$ Could you check your code? I think it contains a few errors. \$\endgroup\$ Aug 17, 2014 at 5:32
  • \$\begingroup\$ github.com/jonashaag/bjoern/blob/master/bjoern/filewrapper.c \$\endgroup\$
    – user965091
    Aug 17, 2014 at 6:18
  • \$\begingroup\$ How about indicate what the errors are? I wrote that from memory and didn't actually test it. I could well have made a mistake beyond what I already fixed. \$\endgroup\$ Aug 18, 2014 at 8:06
  • \$\begingroup\$ As to the bjoern C code that was linked to, I see no evidence in that of close() being called, so it looks a bit like it isn't doing the right thing. \$\endgroup\$ Aug 18, 2014 at 8:07
  • 1
    \$\begingroup\$ That bjoern FileWrapper is also implemented poorly in other ways. It doesn't use the block size and instead defers to the file object iter() which will return one line at a time. That could result in memory exhaustion for large files with no new lines in it, and poor performance on files with lots of short lines. \$\endgroup\$ Aug 18, 2014 at 8:12
3
\$\begingroup\$

On systems that support sendfile(2), that should be the fastest way to transfer data from a static file to a socket. Python has had os.sendfile() since Python 3.3; third-party libraries are also available.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ On the other hand, sendfile will still require you to keep an open file descriptors, and on HTTP the network (of the client) is often the bottleneck so you need to adjust the open fd limits. \$\endgroup\$ Aug 17, 2014 at 5:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.