summer 2020 version of server_bench, take 1
This commit is contained in:
parent
3baf28ccae
commit
fac1440934
465
tests/server_bench.py
Executable file
465
tests/server_bench.py
Executable file
@ -0,0 +1,465 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
#
|
||||||
|
# Benchmark an HTTP server with wrk
|
||||||
|
#
|
||||||
|
# @author gback; Spring 2016, Spring 2018, Summer 2002
|
||||||
|
#
|
||||||
|
|
||||||
|
import getopt, sys, os, subprocess, signal, re, json, resource, time, socket, atexit
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from http.client import HTTPConnection, OK
|
||||||
|
|
||||||
|
runconfig = namedtuple(
|
||||||
|
"runconfig",
|
||||||
|
[
|
||||||
|
"name", # name of configuration
|
||||||
|
"nthreads", # number of threads
|
||||||
|
"nconnections", # number of conn per thread
|
||||||
|
"timeout", # number of conn per thread
|
||||||
|
"window", # number of conn per thread
|
||||||
|
"overlap", # number of conn per thread
|
||||||
|
"description", # description
|
||||||
|
"path", # target path to be retrieved
|
||||||
|
"duration",
|
||||||
|
],
|
||||||
|
) # duration (with unit as string, i.e. "1s")
|
||||||
|
|
||||||
|
VERSION = "1.1"
|
||||||
|
server_exe = "./server"
|
||||||
|
server_root = "_serverroot_"
|
||||||
|
wrk_exe = "/home/courses/cs3214/bin/wrk"
|
||||||
|
nthreads = 64
|
||||||
|
|
||||||
|
# tests will be run in this order
|
||||||
|
tests = [
|
||||||
|
runconfig(
|
||||||
|
name="login40",
|
||||||
|
timeout="1s",
|
||||||
|
nthreads=40,
|
||||||
|
nconnections=40,
|
||||||
|
duration="10s",
|
||||||
|
path="/api/login",
|
||||||
|
description="""
|
||||||
|
Can your server handle 40 parallel connections request /api/login?
|
||||||
|
""",
|
||||||
|
window=500,
|
||||||
|
overlap=250,
|
||||||
|
),
|
||||||
|
runconfig(
|
||||||
|
name="login500",
|
||||||
|
timeout="5s",
|
||||||
|
nthreads=nthreads,
|
||||||
|
nconnections=500,
|
||||||
|
duration="10s",
|
||||||
|
path="/api/login",
|
||||||
|
description="""
|
||||||
|
Using 500 connections, each of which is repeatedly requesting /api/login (~2bytes in
|
||||||
|
HTTP body). We believe this should be enough to make the server CPU bound.
|
||||||
|
""",
|
||||||
|
window=1000,
|
||||||
|
overlap=500,
|
||||||
|
),
|
||||||
|
runconfig(
|
||||||
|
name="login10k",
|
||||||
|
timeout="5s",
|
||||||
|
nthreads=nthreads,
|
||||||
|
nconnections=10000,
|
||||||
|
duration="30s",
|
||||||
|
path="/api/login",
|
||||||
|
description="""
|
||||||
|
Handling 10k simultaneous connections has been a target of scalability since 1999:
|
||||||
|
http://www.kegel.com/c10k.html
|
||||||
|
Can your server handle it?
|
||||||
|
""",
|
||||||
|
window=1000,
|
||||||
|
overlap=500,
|
||||||
|
),
|
||||||
|
runconfig(
|
||||||
|
name="wwwcsvt100",
|
||||||
|
timeout="5s",
|
||||||
|
nthreads=nthreads,
|
||||||
|
nconnections=100,
|
||||||
|
duration="20s",
|
||||||
|
path="/www.cs.vt.edu-20200417.html",
|
||||||
|
description="""
|
||||||
|
The home page of the CS Department, as of 4/17/2020, is about 66KB large (not counting embedded objects).
|
||||||
|
If 100 clients accessed it simultaneously, how much throughput could they expect?
|
||||||
|
""",
|
||||||
|
window=1000,
|
||||||
|
overlap=500,
|
||||||
|
),
|
||||||
|
runconfig(
|
||||||
|
name="doom100",
|
||||||
|
timeout="10s",
|
||||||
|
nthreads=40,
|
||||||
|
nconnections=40,
|
||||||
|
duration="20s",
|
||||||
|
path="/large",
|
||||||
|
description="""
|
||||||
|
According to https://mobiforge.com/research-analysis/the-web-is-doom the combined size of all
|
||||||
|
objects that make an average web page was 2,250kBytes as of April 2016. If these were transferred
|
||||||
|
all in a single objects, how much throughput would you get?
|
||||||
|
This should max out the 10Gbps Ethernet links, even with only 40 connections.
|
||||||
|
""",
|
||||||
|
window=500,
|
||||||
|
overlap=250,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
testsbyname = dict((c.name, c) for c in tests)
|
||||||
|
teststorun = map(lambda t: t.name, tests)
|
||||||
|
|
||||||
|
|
||||||
|
def listtests():
|
||||||
|
for test in tests:
|
||||||
|
print(
|
||||||
|
"""
|
||||||
|
Test: %s
|
||||||
|
Connections: %d
|
||||||
|
Duration: %s
|
||||||
|
Path: %s
|
||||||
|
Description: %s
|
||||||
|
"""
|
||||||
|
% (test.name, test.nconnections, test.duration, test.path, test.description)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
script_dir = "/".join(os.path.realpath(__file__).split("/")[:-1])
|
||||||
|
if script_dir == "":
|
||||||
|
script_dir = "."
|
||||||
|
|
||||||
|
script_dir = os.path.realpath(script_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def usage():
|
||||||
|
print(
|
||||||
|
"""
|
||||||
|
Usage: %s [-hv] [-l] [-s server] [-R serverroot] [-t test1,test2,...] [url]
|
||||||
|
|
||||||
|
-h display this help
|
||||||
|
-v run verbose
|
||||||
|
-s path to server executable, default %s
|
||||||
|
-R server_root path to server root, default %s
|
||||||
|
-t test run just the tests specified
|
||||||
|
-l list available tests with their descriptions
|
||||||
|
-i activate ink tracing tool
|
||||||
|
url URL where your server can be reached, i.e.
|
||||||
|
http://hickory.rlogin:12306/
|
||||||
|
|
||||||
|
This script must be started on two different rlogin nodes.
|
||||||
|
On the first node, run it without a URL to start the server.
|
||||||
|
|
||||||
|
Then run it on a second node with the URL printed out by the
|
||||||
|
first run.
|
||||||
|
|
||||||
|
"""
|
||||||
|
% (sys.argv[0], server_exe, server_root)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(sys.argv[1:], "ihvs:R:t:l", ["help", "verbose"])
|
||||||
|
except getopt.GetoptError as err:
|
||||||
|
print(str(err))
|
||||||
|
usage()
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
verbose = False
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
useInk = False
|
||||||
|
|
||||||
|
for opt, arg in opts:
|
||||||
|
if opt == "-h":
|
||||||
|
usage()
|
||||||
|
sys.exit(0)
|
||||||
|
if opt == "-v":
|
||||||
|
verbose = True
|
||||||
|
elif opt == "-i":
|
||||||
|
useInk = True
|
||||||
|
elif opt == "-s":
|
||||||
|
server_exe = arg
|
||||||
|
elif opt == "-R":
|
||||||
|
server_root = arg
|
||||||
|
elif opt == "-l":
|
||||||
|
listtests()
|
||||||
|
sys.exit(0)
|
||||||
|
elif opt == "-t":
|
||||||
|
teststorun = arg.split(",")
|
||||||
|
else:
|
||||||
|
assert False, "unhandled option"
|
||||||
|
|
||||||
|
|
||||||
|
def raise_fd_limit():
|
||||||
|
print("I will now try to raise the file descriptor limit")
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||||
|
print("Your server process can open %d file descriptors simultaneously." % soft)
|
||||||
|
|
||||||
|
|
||||||
|
def raise_thread_limit():
|
||||||
|
print("I will now try to raise the max number of threads you can spawn")
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
|
||||||
|
resource.setrlimit(resource.RLIMIT_NPROC, (hard, hard))
|
||||||
|
soft, hard = resource.getrlimit(resource.RLIMIT_NPROC)
|
||||||
|
print("Your server process can spawn %d threads simultaneously." % soft)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Start the server.
|
||||||
|
#
|
||||||
|
def start_server(root_dir):
|
||||||
|
print("I will now prepare your server for benchmarking.")
|
||||||
|
if not os.access(server_exe, os.X_OK):
|
||||||
|
print("Did not find server executable: %s" % (server_exe))
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# prepare files to be served
|
||||||
|
print("I will use the directory %s to store 2 files" % (root_dir))
|
||||||
|
if not os.access(root_dir, os.W_OK):
|
||||||
|
os.mkdir(root_dir)
|
||||||
|
|
||||||
|
def make_synthetic_content(sz):
|
||||||
|
return "0123456789ABCDEF" * int(sz / 16)
|
||||||
|
|
||||||
|
def write_file(name, content):
|
||||||
|
with open("%s/%s" % (root_dir, name), "wb") as sfile:
|
||||||
|
sfile.write(content.encode("utf-8"))
|
||||||
|
sfile.close()
|
||||||
|
|
||||||
|
sfilecontent = make_synthetic_content(1024)
|
||||||
|
write_file("small", sfilecontent)
|
||||||
|
lfilecontent = make_synthetic_content(2250 * 1024)
|
||||||
|
write_file("large", lfilecontent)
|
||||||
|
wwwcscont = open("%s/res/www.cs.vt.edu-20200417.html" % script_dir).read()
|
||||||
|
write_file("www.cs.vt.edu-20200417.html", wwwcscont)
|
||||||
|
|
||||||
|
port = (os.getpid() % 10000) + 20000
|
||||||
|
|
||||||
|
cmd = [server_exe, "-p", str(port), "-R", root_dir, "-s"]
|
||||||
|
|
||||||
|
raise_fd_limit()
|
||||||
|
raise_thread_limit()
|
||||||
|
|
||||||
|
server = subprocess.Popen(cmd, stdout=open(os.devnull, "w"), stderr=sys.stderr)
|
||||||
|
|
||||||
|
def clean_up_testing():
|
||||||
|
try:
|
||||||
|
os.kill(server.pid, signal.SIGKILL)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
atexit.register(clean_up_testing)
|
||||||
|
|
||||||
|
print("I will now test that your server works.")
|
||||||
|
|
||||||
|
def test_server():
|
||||||
|
http_conn = HTTPConnection(hostname, port)
|
||||||
|
http_conn.connect()
|
||||||
|
for url, expected in zip(
|
||||||
|
["/small", "/large", "/www.cs.vt.edu-20200417.html", "/api/login"],
|
||||||
|
[sfilecontent, lfilecontent, wwwcscont, "{}"],
|
||||||
|
):
|
||||||
|
http_conn.request("GET", url)
|
||||||
|
server_response = http_conn.getresponse()
|
||||||
|
sfile = server_response.read().decode("utf-8")
|
||||||
|
if server_response.status != OK:
|
||||||
|
print(
|
||||||
|
"Server returned %s for %s, expected %d."
|
||||||
|
% (server_response.status, url, OK)
|
||||||
|
)
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(expected, int)
|
||||||
|
and len(sfile) >= expected
|
||||||
|
or isinstance(expected, (str, bytes))
|
||||||
|
and sfile == expected
|
||||||
|
):
|
||||||
|
print("Retrieved %s ok." % (url))
|
||||||
|
else:
|
||||||
|
print("Did not find expected content at %s." % (url))
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
http_conn.close()
|
||||||
|
|
||||||
|
for tries in range(10):
|
||||||
|
try:
|
||||||
|
time.sleep(1)
|
||||||
|
test_server()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f'starting server failed: {e}')
|
||||||
|
pass
|
||||||
|
|
||||||
|
if tries == 9:
|
||||||
|
print("Your server did not start, giving up after 10 tries")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
this_script = os.path.realpath(sys.argv[0])
|
||||||
|
print(
|
||||||
|
f"""
|
||||||
|
Congratulations, you are now ready to run the benchmark!
|
||||||
|
Now, find another unloaded rlogin machine and run:
|
||||||
|
|
||||||
|
{this_script} http://{hostname}:{port}/
|
||||||
|
|
||||||
|
To use the ink tool, instead run
|
||||||
|
|
||||||
|
{this_script} -i http://{hostname}:{port}/
|
||||||
|
|
||||||
|
When you are done, don't forget to hit ^C here.
|
||||||
|
|
||||||
|
Your server's stdout is going to /dev/null.
|
||||||
|
Your server's stderr is going to the driver's stderr.
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
sys.stdout.flush()
|
||||||
|
server.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def start_wrk(url, test):
|
||||||
|
|
||||||
|
cmd = [ wrk_exe ]
|
||||||
|
cmd += [] if useInk else ['--no-trace']
|
||||||
|
cmd += [
|
||||||
|
"-c", str(test.nconnections),
|
||||||
|
"-t", str(test.nthreads),
|
||||||
|
"-d", test.duration,
|
||||||
|
"-r", test.name + ".tar",
|
||||||
|
"-x", test.timeout,
|
||||||
|
"-w", str(test.window),
|
||||||
|
"-o", str(test.overlap),
|
||||||
|
"-s", script_dir + "/cs3214bench.lua",
|
||||||
|
url + test.path,
|
||||||
|
]
|
||||||
|
if verbose:
|
||||||
|
print("I will now run", " ".join(cmd))
|
||||||
|
|
||||||
|
resfile = "ssresults.json"
|
||||||
|
luajson = "%s/JSON.lua" % script_dir
|
||||||
|
assert os.access(luajson, os.R_OK)
|
||||||
|
server = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=sys.stdout,
|
||||||
|
stderr=sys.stderr,
|
||||||
|
env=dict(os.environ, JSON_OUTPUT_FILE=resfile, JSON_LUA=luajson),
|
||||||
|
)
|
||||||
|
server.wait()
|
||||||
|
with open(resfile) as jfile:
|
||||||
|
r = json.load(jfile)
|
||||||
|
os.unlink(resfile)
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
|
if len(args) == 0:
|
||||||
|
start_server(server_root)
|
||||||
|
else:
|
||||||
|
url = args[0]
|
||||||
|
# strip ending / since the path args contain them
|
||||||
|
while url.endswith("/"):
|
||||||
|
url = url[:-1]
|
||||||
|
|
||||||
|
if hostname in url:
|
||||||
|
print("Please do not start the client on the same machine as the server.")
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
raise_fd_limit()
|
||||||
|
raise_thread_limit()
|
||||||
|
results = dict(version=VERSION)
|
||||||
|
ran = []
|
||||||
|
for testname in teststorun:
|
||||||
|
ran.append(testname)
|
||||||
|
if testname not in testsbyname:
|
||||||
|
print("Test: %s not found, skipping" % testname)
|
||||||
|
continue
|
||||||
|
|
||||||
|
test = testsbyname[testname]
|
||||||
|
print("Now running test: %s\n" % (testname))
|
||||||
|
try:
|
||||||
|
results[testname] = start_wrk(url, test)
|
||||||
|
except Exception as e:
|
||||||
|
print("An exception occurred %s, skipping this test" % (str(e)))
|
||||||
|
|
||||||
|
ofilename = "pserv.results.%d.json" % (os.getpid())
|
||||||
|
print("Writing results to %s" % ofilename)
|
||||||
|
with open(ofilename, "w") as ofile:
|
||||||
|
json.dump(results, ofile)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"""
|
||||||
|
Submit your results to the scoreboard with ~cs3214/bin/sspostresults.py %s
|
||||||
|
"""
|
||||||
|
% ofilename
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(ofilename, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
# the following rubric encode the performance expectations for this semester
|
||||||
|
# there are 20 pts currently, 4 pts per benchmark.
|
||||||
|
#
|
||||||
|
# If errors (p) is set, deduct p pts if there are one or more errors
|
||||||
|
#
|
||||||
|
# If served (a, b) is set, deduct b pts unless a fraction of a clients was served
|
||||||
|
#
|
||||||
|
rubric = {
|
||||||
|
# 200 or more yields 4 pts, 100 or more yields 2 points
|
||||||
|
"login40": {"rps": [(320, 4), (160, 2)], "errors": -1},
|
||||||
|
"login500": {"rps": [(800, 4), (500, 2)], "errors": -1},
|
||||||
|
"login10k": {"rps": [(650, 4), (450, 2)], "served": (0.80, 2)},
|
||||||
|
# these max out the 10GBps link, so these are MByte/s
|
||||||
|
"wwwcsvt100": {"mbps": [(900, 4), (800, 2)]},
|
||||||
|
"doom100": {"mbps": [(900, 4), (800, 2)]},
|
||||||
|
}
|
||||||
|
extra = ""
|
||||||
|
for test in ran:
|
||||||
|
points = rubric[test]
|
||||||
|
category = 0
|
||||||
|
if "rps" in points:
|
||||||
|
rps = 1e3 * (
|
||||||
|
data[test]["summary"]["requests"] / data[test]["summary"]["duration"]
|
||||||
|
)
|
||||||
|
for requiredmin, value in points["rps"]:
|
||||||
|
if rps > requiredmin:
|
||||||
|
category += value
|
||||||
|
break
|
||||||
|
if test == "login10k" and rps > 900:
|
||||||
|
extra = "+10 points extra credit for login10k! If your error count isn't > 5,000..."
|
||||||
|
if "mbps" in points:
|
||||||
|
mbps = (
|
||||||
|
1e6
|
||||||
|
* data[test]["summary"]["bytes"]
|
||||||
|
/ data[test]["summary"]["duration"]
|
||||||
|
/ 1024
|
||||||
|
/ 1024
|
||||||
|
)
|
||||||
|
for requiredmin, value in points["mbps"]:
|
||||||
|
if mbps > requiredmin:
|
||||||
|
category += value
|
||||||
|
break
|
||||||
|
if "served" in points:
|
||||||
|
(threshold, deduction) = points["served"]
|
||||||
|
percent = (
|
||||||
|
data[test]["summary"]["served"] / data[test]["summary"]["connections"]
|
||||||
|
)
|
||||||
|
if percent < threshold:
|
||||||
|
category = max(category - deduction, 0)
|
||||||
|
if "errors" in rubric[test]:
|
||||||
|
errors = sum(data[test]["summary"]["errors"].values())
|
||||||
|
if errors > 0:
|
||||||
|
category = max(category - rubric[test], 0)
|
||||||
|
print("%s: %d/4" % (test, category))
|
||||||
|
score += category
|
||||||
|
|
||||||
|
print("Your server got a performance score of %d/20" % score)
|
||||||
|
if extra != "":
|
||||||
|
print(extra)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"""
|
||||||
|
Submit your individual <test_name>.tar reports with ~cs3214/bin/p4api.sh\nThis will return a link to visualize your server's performance.
|
||||||
|
"""
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user