Can run swift-bench across multiple cores/servers.
You run one or more swift-bench-client processes like this: $ swift-bench-client 127.0.0.1 20001 $ swift-bench-client 127.0.0.1 20002 Then you run swift-bench with a new option, --bench-clients (-b), which is specified once for each swift-bench-client: $ swift-bench -b 127.0.0.1:20001 -b 127.0.0.1:20002 You get log lines from each client (interleaved) along with a final report for all clients: 127.0.0.1:20002 swift-bench-server 2012-08-25 22:44:06,148 INFO Auth version: 1.0 127.0.0.1:20001 swift-bench-server 2012-08-25 22:44:06,148 INFO Auth version: 1.0 127.0.0.1:20001 swift-bench-server 2012-08-25 22:44:12,249 INFO 83 PUTS [0 failures], 41.5/s 127.0.0.1:20002 swift-bench-server 2012-08-25 22:44:14,430 INFO 74 PUTS [0 failures], 34.3/s ... 127.0.0.1:20002 swift-bench-server 2012-08-25 22:45:18,942 INFO Auth version: 1.0 127.0.0.1:20002 swift-bench-server 2012-08-25 22:45:20,946 INFO 238 DEL [2 failures], 118.9/s swift-bench 2012-08-25 22:45:27,549 INFO 2000 PUTS **FINAL** [0 failures], 56.8/s swift-bench 2012-08-25 22:45:27,550 INFO 30000 GETS **FINAL** [50 failures], 974.6/s swift-bench 2012-08-25 22:45:27,550 INFO 2000 DEL **FINAL** [20 failures], 237.1/s The concurrency, PUT count, and GET count config settings are divided by the number of bench_clients. In other words, the same volume of work is attempted (vs. not specifying --bench-clients), but it can now span servers and CPU cores. Benchmark containers are created (if use_proxy = yes) and deleted (if delete = yes), with appropriate concurrency, in the initiating swift-bench process, not any of the swift-bench-client processes. Change-Id: Idbf31a23093244ab357a9bf77e6031257774f24a
This commit is contained in:
parent
c509ac2371
commit
ed3b12d05c
@ -21,7 +21,8 @@ import signal
|
|||||||
import uuid
|
import uuid
|
||||||
from optparse import OptionParser
|
from optparse import OptionParser
|
||||||
|
|
||||||
from swift.common.bench import BenchController
|
from swift.common.bench import (BenchController, DistributedBenchController,
|
||||||
|
create_containers, delete_containers)
|
||||||
from swift.common.utils import readconf, LogAdapter
|
from swift.common.utils import readconf, LogAdapter
|
||||||
|
|
||||||
# The defaults should be sufficient to run swift-bench on a SAIO
|
# The defaults should be sufficient to run swift-bench on a SAIO
|
||||||
@ -49,6 +50,8 @@ CONF_DEFAULTS = {
|
|||||||
'devices': 'sdb1', # space-sep list
|
'devices': 'sdb1', # space-sep list
|
||||||
'log_level': 'INFO',
|
'log_level': 'INFO',
|
||||||
'timeout': '10',
|
'timeout': '10',
|
||||||
|
'auth_version': '1.0',
|
||||||
|
'bench_clients': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
SAIO_DEFAULTS = {
|
SAIO_DEFAULTS = {
|
||||||
@ -81,6 +84,13 @@ if __name__ == '__main__':
|
|||||||
help='User name for obtaining an auth token')
|
help='User name for obtaining an auth token')
|
||||||
parser.add_option('-K', '--key', dest='key',
|
parser.add_option('-K', '--key', dest='key',
|
||||||
help='Key for obtaining an auth token')
|
help='Key for obtaining an auth token')
|
||||||
|
parser.add_option('-b', '--bench-clients', action='append',
|
||||||
|
metavar='<ip>:<port>',
|
||||||
|
help=('A string of the form "<ip>:<port>" which matches '
|
||||||
|
'the arguments supplied to a swift-bench-client '
|
||||||
|
'process. This argument must be specified '
|
||||||
|
'once per swift-bench-client you want to '
|
||||||
|
'utilize.'))
|
||||||
parser.add_option('-u', '--url', dest='url',
|
parser.add_option('-u', '--url', dest='url',
|
||||||
help='Storage URL')
|
help='Storage URL')
|
||||||
parser.add_option('-c', '--concurrency', dest='concurrency',
|
parser.add_option('-c', '--concurrency', dest='concurrency',
|
||||||
@ -125,6 +135,8 @@ if __name__ == '__main__':
|
|||||||
options.put_concurrency = options.concurrency
|
options.put_concurrency = options.concurrency
|
||||||
options.get_concurrency = options.concurrency
|
options.get_concurrency = options.concurrency
|
||||||
options.del_concurrency = options.concurrency
|
options.del_concurrency = options.concurrency
|
||||||
|
options.containers = ['%s_%d' % (options.container_name, i)
|
||||||
|
for i in xrange(int(options.num_containers))]
|
||||||
|
|
||||||
def sigterm(signum, frame):
|
def sigterm(signum, frame):
|
||||||
sys.exit('Termination signal received.')
|
sys.exit('Termination signal received.')
|
||||||
@ -145,5 +157,13 @@ if __name__ == '__main__':
|
|||||||
'%(message)s')
|
'%(message)s')
|
||||||
loghandler.setFormatter(logformat)
|
loghandler.setFormatter(logformat)
|
||||||
|
|
||||||
controller = BenchController(logger, options)
|
if options.use_proxy:
|
||||||
|
create_containers(logger, options)
|
||||||
|
|
||||||
|
controller_class = DistributedBenchController if options.bench_clients \
|
||||||
|
else BenchController
|
||||||
|
controller = controller_class(logger, options)
|
||||||
controller.run()
|
controller.run()
|
||||||
|
|
||||||
|
if options.delete:
|
||||||
|
delete_containers(logger, options)
|
||||||
|
59
bin/swift-bench-client
Executable file
59
bin/swift-bench-client
Executable file
@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# Copyright (c) 2010-2012 OpenStack, LLC.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
# implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import signal
|
||||||
|
from optparse import OptionParser
|
||||||
|
|
||||||
|
from swift.common.bench import BenchServer
|
||||||
|
from swift.common.utils import LogAdapter
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
usage = "usage: %prog <ip> <port>"
|
||||||
|
usage += "\n\nRun a client for distributed swift-bench runs."
|
||||||
|
parser = OptionParser(usage=usage)
|
||||||
|
parser.add_option('-o', '--log-level', dest='log_level',
|
||||||
|
default='info',
|
||||||
|
help='Logging level (debug, info, etc)')
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
options, args = parser.parse_args()
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel({
|
||||||
|
'debug': logging.DEBUG,
|
||||||
|
'info': logging.INFO,
|
||||||
|
'warning': logging.WARNING,
|
||||||
|
'error': logging.ERROR,
|
||||||
|
'critical': logging.CRITICAL}.get(
|
||||||
|
options.log_level.lower(), logging.INFO))
|
||||||
|
loghandler = logging.StreamHandler()
|
||||||
|
logger.addHandler(loghandler)
|
||||||
|
logger = LogAdapter(logger, 'swift-bench-client')
|
||||||
|
logformat = logging.Formatter('%(server)s %(asctime)s %(levelname)s '
|
||||||
|
'%(message)s')
|
||||||
|
loghandler.setFormatter(logformat)
|
||||||
|
|
||||||
|
def sigterm(signum, frame):
|
||||||
|
sys.exit('Termination signal received.')
|
||||||
|
signal.signal(signal.SIGTERM, sigterm)
|
||||||
|
signal.signal(signal.SIGINT, sigterm)
|
||||||
|
|
||||||
|
server = BenchServer(logger, args[0], args[1])
|
||||||
|
server.run()
|
1
setup.py
1
setup.py
@ -47,6 +47,7 @@ setup(
|
|||||||
'bin/swift-account-replicator',
|
'bin/swift-account-replicator',
|
||||||
'bin/swift-account-server',
|
'bin/swift-account-server',
|
||||||
'bin/swift-bench',
|
'bin/swift-bench',
|
||||||
|
'bin/swift-bench-client',
|
||||||
'bin/swift-container-auditor',
|
'bin/swift-container-auditor',
|
||||||
'bin/swift-container-replicator',
|
'bin/swift-container-replicator',
|
||||||
'bin/swift-container-server',
|
'bin/swift-container-server',
|
||||||
|
@ -13,21 +13,61 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import signal
|
import signal
|
||||||
|
import socket
|
||||||
|
import logging
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
from optparse import Values
|
||||||
|
|
||||||
|
import eventlet
|
||||||
import eventlet.pools
|
import eventlet.pools
|
||||||
from eventlet.green.httplib import CannotSendRequest
|
from eventlet.green.httplib import CannotSendRequest
|
||||||
|
|
||||||
from swift.common.utils import TRUE_VALUES
|
from swift.common.utils import TRUE_VALUES, LogAdapter
|
||||||
import swiftclient as client
|
import swiftclient as client
|
||||||
from swift.common import direct_client
|
from swift.common import direct_client
|
||||||
from swift.common.http import HTTP_CONFLICT
|
from swift.common.http import HTTP_CONFLICT
|
||||||
|
|
||||||
|
try:
|
||||||
|
import simplejson as json
|
||||||
|
except ImportError:
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def _func_on_containers(logger, conf, concurrency_key, func):
|
||||||
|
"""Run a function on each container with concurrency."""
|
||||||
|
|
||||||
|
bench = Bench(logger, conf, [])
|
||||||
|
pool = eventlet.GreenPool(int(getattr(conf, concurrency_key)))
|
||||||
|
for container in conf.containers:
|
||||||
|
pool.spawn_n(func, bench.url, bench.token, container)
|
||||||
|
pool.waitall()
|
||||||
|
|
||||||
|
|
||||||
|
def delete_containers(logger, conf):
|
||||||
|
"""Utility function to delete benchmark containers."""
|
||||||
|
|
||||||
|
def _deleter(url, token, container):
|
||||||
|
try:
|
||||||
|
client.delete_container(url, token, container)
|
||||||
|
except client.ClientException, e:
|
||||||
|
if e.http_status != HTTP_CONFLICT:
|
||||||
|
logger.warn("Unable to delete container '%s'. "
|
||||||
|
"Got http status '%d'." % (container, e.http_status))
|
||||||
|
|
||||||
|
_func_on_containers(logger, conf, 'del_concurrency', _deleter)
|
||||||
|
|
||||||
|
|
||||||
|
def create_containers(logger, conf):
|
||||||
|
"""Utility function to create benchmark containers."""
|
||||||
|
|
||||||
|
_func_on_containers(logger, conf, 'put_concurrency', client.put_container)
|
||||||
|
|
||||||
|
|
||||||
class ConnectionPool(eventlet.pools.Pool):
|
class ConnectionPool(eventlet.pools.Pool):
|
||||||
|
|
||||||
@ -39,6 +79,62 @@ class ConnectionPool(eventlet.pools.Pool):
|
|||||||
return client.http_connection(self.url)
|
return client.http_connection(self.url)
|
||||||
|
|
||||||
|
|
||||||
|
class BenchServer(object):
|
||||||
|
"""
|
||||||
|
A BenchServer binds to an IP/port and listens for bench jobs. A bench
|
||||||
|
job consists of the normal conf "dict" encoded in JSON, terminated with an
|
||||||
|
EOF. The log level is at least INFO, but DEBUG may also be specified in
|
||||||
|
the conf dict.
|
||||||
|
|
||||||
|
The server will wait forever for jobs, running them one at a time.
|
||||||
|
"""
|
||||||
|
def __init__(self, logger, bind_ip, bind_port):
|
||||||
|
self.logger = logger
|
||||||
|
self.bind_ip = bind_ip
|
||||||
|
self.bind_port = int(bind_port)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
self.logger.info('Binding to %s:%s', self.bind_ip, self.bind_port)
|
||||||
|
s.bind((self.bind_ip, self.bind_port))
|
||||||
|
s.listen(20)
|
||||||
|
while True:
|
||||||
|
client, address = s.accept()
|
||||||
|
self.logger.debug('Accepting connection from %s:%s', *address)
|
||||||
|
client_file = client.makefile('rb+', 1)
|
||||||
|
json_data = client_file.read()
|
||||||
|
conf = Values(json.loads(json_data))
|
||||||
|
|
||||||
|
self.logger.info(
|
||||||
|
'Starting run for %s:%s [put/get/del_concurrency: %s/%s/%s, '
|
||||||
|
'num_objects: %s, num_gets: %s]', address[0], address[1],
|
||||||
|
conf.put_concurrency, conf.get_concurrency,
|
||||||
|
conf.del_concurrency, conf.num_objects, conf.num_gets)
|
||||||
|
|
||||||
|
logger = logging.getLogger('bench-server')
|
||||||
|
level = logging.DEBUG if conf.log_level.lower() == 'debug' \
|
||||||
|
else logging.INFO
|
||||||
|
logger.setLevel(level)
|
||||||
|
loghandler = logging.StreamHandler(stream=client_file)
|
||||||
|
logformat = logging.Formatter(
|
||||||
|
'%(server)s %(asctime)s %(levelname)s %(message)s')
|
||||||
|
loghandler.setFormatter(logformat)
|
||||||
|
logger.addHandler(loghandler)
|
||||||
|
logger = LogAdapter(logger, 'swift-bench-server')
|
||||||
|
|
||||||
|
controller = BenchController(logger, conf)
|
||||||
|
try:
|
||||||
|
controller.run()
|
||||||
|
except socket.error:
|
||||||
|
logger.warning('Socket error', exc_info=1)
|
||||||
|
|
||||||
|
logger.logger.removeHandler(loghandler)
|
||||||
|
client_file.close()
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
self.logger.info('...bench run completed; waiting for next run.')
|
||||||
|
|
||||||
|
|
||||||
class Bench(object):
|
class Bench(object):
|
||||||
|
|
||||||
def __init__(self, logger, conf, names):
|
def __init__(self, logger, conf, names):
|
||||||
@ -64,8 +160,6 @@ class Bench(object):
|
|||||||
self.account = conf.account
|
self.account = conf.account
|
||||||
self.url = conf.url
|
self.url = conf.url
|
||||||
self.ip, self.port = self.url.split('/')[2].split(':')
|
self.ip, self.port = self.url.split('/')[2].split(':')
|
||||||
self.containers = ['%s_%d' % (conf.container_name, i)
|
|
||||||
for i in xrange(int(conf.num_containers))]
|
|
||||||
|
|
||||||
self.object_size = int(conf.object_size)
|
self.object_size = int(conf.object_size)
|
||||||
self.object_sources = conf.object_sources
|
self.object_sources = conf.object_sources
|
||||||
@ -129,6 +223,88 @@ class Bench(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class DistributedBenchController(object):
|
||||||
|
"""
|
||||||
|
This class manages a distributed swift-bench run. For this Controller
|
||||||
|
class to make sense, the conf.bench_clients list must contain at least one
|
||||||
|
entry.
|
||||||
|
|
||||||
|
The idea is to split the configured load between one or more
|
||||||
|
swift-bench-client processes, each of which use eventlet for concurrency.
|
||||||
|
We deliberately take a simple, naive approach with these limitations:
|
||||||
|
1) Concurrency, num_objects, and num_gets are spread evenly between the
|
||||||
|
swift-bench-client processes. With a low concurrency to
|
||||||
|
swift-bench-client count ratio, rounding may result in a greater
|
||||||
|
than desired aggregate concurrency.
|
||||||
|
2) Each swift-bench-client process runs independently so some may
|
||||||
|
finish up before others, i.e. the target aggregate concurrency is
|
||||||
|
not necessarily present the whole time. This may bias aggregate
|
||||||
|
reported rates lower than a more efficient architecture.
|
||||||
|
3) Because of #2, some swift-bench-client processes may be running GETs
|
||||||
|
while others are still runinng their PUTs. Because of this
|
||||||
|
potential skew, distributed runs will not isolate one operation at a
|
||||||
|
time like a single swift-bench run will.
|
||||||
|
3) Reported aggregate rates are simply the sum of each
|
||||||
|
swift-bench-client process reported FINAL number. That's probably
|
||||||
|
inaccurate somehow.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, logger, conf):
|
||||||
|
self.logger = logger
|
||||||
|
# ... INFO 1000 PUTS **FINAL** [0 failures], 34.9/s
|
||||||
|
self.final_re = re.compile(
|
||||||
|
'INFO (\d+) (.*) \*\*FINAL\*\* \[(\d+) failures\], (\d+\.\d+)/s')
|
||||||
|
self.clients = conf.bench_clients
|
||||||
|
del conf.bench_clients
|
||||||
|
for k in ['put_concurrency', 'get_concurrency', 'del_concurrency',
|
||||||
|
'num_objects', 'num_gets']:
|
||||||
|
setattr(conf, k, max(1, int(getattr(conf, k)) / len(self.clients)))
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
eventlet.patcher.monkey_patch(socket=True)
|
||||||
|
pool = eventlet.GreenPool(size=len(self.clients))
|
||||||
|
pile = eventlet.GreenPile(pool)
|
||||||
|
for client in self.clients:
|
||||||
|
pile.spawn(self.do_run, client)
|
||||||
|
results = {
|
||||||
|
'PUTS': dict(count=0, failures=0, rate=0.0),
|
||||||
|
'GETS': dict(count=0, failures=0, rate=0.0),
|
||||||
|
'DEL': dict(count=0, failures=0, rate=0.0),
|
||||||
|
}
|
||||||
|
for result in pile:
|
||||||
|
for k, v in result.iteritems():
|
||||||
|
target = results[k]
|
||||||
|
target['count'] += int(v['count'])
|
||||||
|
target['failures'] += int(v['failures'])
|
||||||
|
target['rate'] += float(v['rate'])
|
||||||
|
for k in ['PUTS', 'GETS', 'DEL']:
|
||||||
|
v = results[k]
|
||||||
|
self.logger.info('%d %s **FINAL** [%d failures], %.1f/s' % (
|
||||||
|
v['count'], k, v['failures'], v['rate']))
|
||||||
|
|
||||||
|
def do_run(self, client):
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
ip, port = client.split(':')
|
||||||
|
s.connect((ip, int(port)))
|
||||||
|
s.sendall(json.dumps(self.conf.__dict__))
|
||||||
|
s.shutdown(socket.SHUT_WR)
|
||||||
|
s_file = s.makefile('rb', 1)
|
||||||
|
result = {}
|
||||||
|
for line in s_file:
|
||||||
|
match = self.final_re.search(line)
|
||||||
|
if match:
|
||||||
|
g = match.groups()
|
||||||
|
result[g[1]] = {
|
||||||
|
'count': g[0],
|
||||||
|
'failures': g[2],
|
||||||
|
'rate': g[3],
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
sys.stderr.write('%s %s' % (client, line))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class BenchController(object):
|
class BenchController(object):
|
||||||
|
|
||||||
def __init__(self, logger, conf):
|
def __init__(self, logger, conf):
|
||||||
@ -177,16 +353,6 @@ class BenchDELETE(Bench):
|
|||||||
self.total = len(names)
|
self.total = len(names)
|
||||||
self.msg = 'DEL'
|
self.msg = 'DEL'
|
||||||
|
|
||||||
def run(self):
|
|
||||||
Bench.run(self)
|
|
||||||
for container in self.containers:
|
|
||||||
try:
|
|
||||||
client.delete_container(self.url, self.token, container)
|
|
||||||
except client.ClientException, e:
|
|
||||||
if e.http_status != HTTP_CONFLICT:
|
|
||||||
self._log_status("Unable to delete container '%s'. " \
|
|
||||||
"Got http status '%d'." % (container, e.http_status))
|
|
||||||
|
|
||||||
def _run(self, thread):
|
def _run(self, thread):
|
||||||
if time.time() - self.heartbeat >= 15:
|
if time.time() - self.heartbeat >= 15:
|
||||||
self.heartbeat = time.time()
|
self.heartbeat = time.time()
|
||||||
@ -242,11 +408,7 @@ class BenchPUT(Bench):
|
|||||||
self.concurrency = self.put_concurrency
|
self.concurrency = self.put_concurrency
|
||||||
self.total = self.total_objects
|
self.total = self.total_objects
|
||||||
self.msg = 'PUTS'
|
self.msg = 'PUTS'
|
||||||
if self.use_proxy:
|
self.containers = conf.containers
|
||||||
with self.connection() as conn:
|
|
||||||
for container_name in self.containers:
|
|
||||||
client.put_container(self.url, self.token,
|
|
||||||
container_name, http_conn=conn)
|
|
||||||
|
|
||||||
def _run(self, thread):
|
def _run(self, thread):
|
||||||
if time.time() - self.heartbeat >= 15:
|
if time.time() - self.heartbeat >= 15:
|
||||||
|
Loading…
Reference in New Issue
Block a user