Use sendfile() for zero-copy of uploaded images.

Implements bp support-sendfile

Avoid needless client-side copying through userspace of uploaded
image file content, using pysendfile to wrap the sendfile() system
call instead of reading the file one chunk at a time.

The existing iterator pattern is maintained for consistency and
to allow send progress to be followed by wrapping the iteration.

The performance gain only applies to the client-->glance API service
leg, so whether the overall speed-up is noticeable depends on the
image store in use. For example, it would be imperceptible with a
storage backend showing relatively high PUT latency, such as S3,
as the blocking Store.add() call would dominate.

At the other extreme, uploading large images via the loopback to
file-based store is about 60% faster. Detailed performance figures
for more realistic scenarios to follow when hardware is available
for benchmarking.

Change-Id: Ia8c74e76d3d6c63e9a9b38ab455a4e6edb47fba9
This commit is contained in:
Eoghan Glynn 2012-02-07 08:56:50 +00:00
parent ce35911f6f
commit 7696ae5f24
3 changed files with 114 additions and 10 deletions

View File

@ -20,6 +20,7 @@
# 577548-https-httplib-client-connection-with-certificate-v/
import collections
import errno
import functools
import httplib
import logging
@ -33,6 +34,12 @@ except ImportError:
import socket
import ssl
try:
import sendfile
SENDFILE_SUPPORTED = True
except ImportError:
SENDFILE_SUPPORTED = False
from glance.common import auth
from glance.common import exception
@ -102,6 +109,35 @@ class ImageBodyIterator(object):
break
class SendFileIterator:
"""
Emulate iterator pattern over sendfile, in order to allow
send progress be followed by wrapping the iteration.
"""
def __init__(self, connection, body):
self.connection = connection
self.body = body
self.offset = 0
self.sending = True
def __iter__(self):
class OfLength:
def __init__(self, len):
self.len = len
def __len__():
return self.len
while self.sending:
sent = sendfile.sendfile(self.connection.sock.fileno(),
self.body.fileno(),
self.offset,
CHUNKSIZE)
self.sending = (sent != 0)
self.offset += sent
yield OfLength(sent)
class HTTPSClientAuthConnection(httplib.HTTPSConnection):
"""
Class to make a HTTPS connection, with support for
@ -401,8 +437,18 @@ class BaseClient(object):
def _filelike(body):
return hasattr(body, 'read')
def _iterable(body):
return isinstance(body, collections.Iterable)
def _sendbody(connection, iter):
connection.endheaders()
for sent in iter:
# iterator has done the heavy lifting
pass
def _chunkbody(connection, iter):
connection.putheader('Transfer-Encoding', 'chunked')
connection.endheaders()
for chunk in iter:
connection.send('%x\r\n%s\r\n' % (len(chunk), chunk))
connection.send('0\r\n\r\n')
# Do a simple request or a chunked request, depending
# on whether the body param is file-like or iterable and
@ -411,20 +457,20 @@ class BaseClient(object):
if not _pushing(method) or _simple(body):
# Simple request...
c.request(method, path, body, headers)
elif _filelike(body) or _iterable(body):
# Chunk it, baby...
elif _filelike(body) or self._iterable(body):
c.putrequest(method, path)
for header, value in headers.items():
c.putheader(header, value)
c.putheader('Transfer-Encoding', 'chunked')
c.endheaders()
iter = body if _iterable(body) else ImageBodyIterator(body)
iter = self.image_iterator(c, headers, body)
for chunk in iter:
c.send('%x\r\n%s\r\n' % (len(chunk), chunk))
c.send('0\r\n\r\n')
if self._sendable(body):
# send actual file without copying into userspace
_sendbody(c, iter)
else:
# otherwise iterate and chunk
_chunkbody(c, iter)
else:
raise TypeError('Unsupported image type: %s' % body.__class__)
@ -454,6 +500,33 @@ class BaseClient(object):
except (socket.error, IOError), e:
raise exception.ClientConnectionError(e)
def _seekable(self, body):
# pipes are not seekable, avoids sendfile() failure on e.g.
# cat /path/to/image | glance add ...
# or where add command is launched via popen
try:
os.lseek(body.fileno(), 0, os.SEEK_SET)
return True
except OSError as e:
return (e.errno != errno.ESPIPE)
def _sendable(self, body):
return (SENDFILE_SUPPORTED and
hasattr(body, 'fileno') and
self._seekable(body) and
not self.use_ssl)
def _iterable(self, body):
return isinstance(body, collections.Iterable)
def image_iterator(self, connection, headers, body):
if self._sendable(body):
return SendFileIterator(connection, body)
elif self._iterable(body):
return body
else:
return ImageBodyIterator(body)
def get_status_code(self, response):
"""
Returns the integer status code from the response, which

View File

@ -20,6 +20,12 @@
import os
import shutil
try:
import sendfile
SENDFILE_SUPPORTED = True
except ImportError:
SENDFILE_SUPPORTED = False
import webob
from glance.api.middleware import version_negotiation
@ -81,9 +87,29 @@ def stub_out_registry_and_store_server(stubs, base_dir):
setattr(res, 'read', fake_reader)
return res
class FakeSocket(object):
def __init__(self, *args, **kwargs):
pass
def fileno(self):
return 42
class FakeSendFile(object):
def __init__(self, req):
self.req = req
def sendfile(self, o, i, offset, nbytes):
os.lseek(i, offset, os.SEEK_SET)
prev_len = len(self.req.body)
self.req.body += os.read(i, nbytes)
return len(self.req.body) - prev_len
class FakeGlanceConnection(object):
def __init__(self, *args, **kwargs):
self.sock = FakeSocket()
pass
def connect(self):
@ -94,6 +120,9 @@ def stub_out_registry_and_store_server(stubs, base_dir):
def putrequest(self, method, url):
self.req = webob.Request.blank("/" + url.lstrip("/"))
if SENDFILE_SUPPORTED:
fake_sendfile = FakeSendFile(self.req)
stubs.Set(sendfile, 'sendfile', fake_sendfile.sendfile)
self.req.method = method
def putheader(self, key, value):

View File

@ -20,6 +20,8 @@ httplib2
xattr>=0.6.0
kombu
pycrypto>=2.1.0alpha1
pysendfile==2.0.0
# The following allow Keystone to be installed in the venv
# along with all of Keystone's dependencies. We target a specific