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:
parent
ce35911f6f
commit
7696ae5f24
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue