docker_helper.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. import os
  2. import socket
  3. import fnmatch
  4. import json
  5. import docker
  6. import time
  7. import re
  8. import traceback
  9. from threading import Thread
  10. from colorama import Fore, Style
  11. from toolset.utils.output_helper import log
  12. from toolset.utils.metadata_helper import gather_tests
  13. from toolset.utils.ordered_set import OrderedSet
  14. from toolset.utils.database_helper import test_database
  15. def clean(benchmarker_config):
  16. '''
  17. Cleans all the docker images from the system
  18. '''
  19. # Clean the app server images
  20. client = docker.DockerClient(
  21. base_url=benchmarker_config.server_docker_host)
  22. client.images.prune()
  23. for image in client.images.list():
  24. if len(image.tags) > 0:
  25. # 'techempower/tfb.test.gemini:0.1' -> 'techempower/tfb.test.gemini'
  26. image_tag = image.tags[0].split(':')[0]
  27. if image_tag != 'techempower/tfb' and 'techempower' in image_tag:
  28. client.images.remove(image.id, force=True)
  29. client.images.prune()
  30. # Clean the database server images
  31. client = docker.DockerClient(
  32. base_url=benchmarker_config.database_docker_host)
  33. client.images.prune()
  34. for image in client.images.list():
  35. if len(image.tags) > 0:
  36. # 'techempower/tfb.test.gemini:0.1' -> 'techempower/tfb.test.gemini'
  37. image_tag = image.tags[0].split(':')[0]
  38. if image_tag != 'techempower/tfb':
  39. client.images.remove(image.id, force=True)
  40. client.images.prune()
  41. def build(benchmarker_config, test_names, build_log_dir=os.devnull):
  42. '''
  43. Builds the dependency chain as well as the test implementation docker images
  44. for the given tests.
  45. '''
  46. docker_buildargs = {
  47. 'MAX_CONCURRENCY': str(max(benchmarker_config.concurrency_levels)),
  48. 'TFB_DATABASE': str(benchmarker_config.database_host)
  49. }
  50. tests = gather_tests(
  51. include=test_names, benchmarker_config=benchmarker_config)
  52. for test in tests:
  53. log_prefix = "%s: " % test.name
  54. if __build_dependencies(benchmarker_config, test, docker_buildargs,
  55. build_log_dir) > 0:
  56. return 1
  57. # Build the test image
  58. test_docker_file = "%s.dockerfile" % test.name
  59. build_log_file = build_log_dir
  60. if build_log_dir is not os.devnull:
  61. build_log_file = os.path.join(
  62. build_log_dir,
  63. "%s.log" % test_docker_file.replace(".dockerfile", "").lower())
  64. with open(build_log_file, 'w') as build_log:
  65. try:
  66. for line in docker.APIClient(
  67. base_url=benchmarker_config.server_docker_host).build(
  68. path=test.directory,
  69. dockerfile=test_docker_file,
  70. tag="techempower/tfb.test.%s" %
  71. test_docker_file.replace(".dockerfile", ""),
  72. buildargs=docker_buildargs,
  73. forcerm=True):
  74. if line.startswith('{"stream":'):
  75. line = json.loads(line)
  76. line = line[line.keys()[0]].encode('utf-8')
  77. log(line,
  78. prefix=log_prefix,
  79. file=build_log,
  80. color=Fore.WHITE + Style.BRIGHT \
  81. if re.match(r'^Step \d+\/\d+', line) else '')
  82. except Exception:
  83. tb = traceback.format_exc()
  84. log("Docker build failed; terminating",
  85. prefix=log_prefix,
  86. file=build_log,
  87. color=Fore.RED)
  88. log(tb, prefix=log_prefix, file=build_log)
  89. return 1
  90. return 0
  91. def run(benchmarker_config, test, run_log_dir):
  92. '''
  93. Run the given Docker container(s)
  94. '''
  95. client = docker.DockerClient(
  96. base_url=benchmarker_config.server_docker_host)
  97. containers = []
  98. log_prefix = "%s: " % test.name
  99. try:
  100. def watch_container(container, docker_file):
  101. with open(
  102. os.path.join(run_log_dir, "%s.log" % docker_file.replace(
  103. ".dockerfile", "").lower()), 'w') as run_log:
  104. for line in container.logs(stream=True):
  105. log(line, prefix=log_prefix, file=run_log)
  106. extra_hosts = None
  107. name = "tfb-server"
  108. if benchmarker_config.network is None:
  109. extra_hosts = {
  110. socket.gethostname(): str(benchmarker_config.server_host),
  111. 'tfb-server': str(benchmarker_config.server_host),
  112. 'tfb-database': str(benchmarker_config.database_host)
  113. }
  114. name = None
  115. sysctl = {'net.core.somaxconn': 65535}
  116. ulimit = [{
  117. 'name': 'nofile',
  118. 'hard': 200000,
  119. 'soft': 200000
  120. }, {
  121. 'name': 'rtprio',
  122. 'hard': 99,
  123. 'soft': 99
  124. }]
  125. container = client.containers.run(
  126. "techempower/tfb.test.%s" % test.name,
  127. name=name,
  128. network=benchmarker_config.network,
  129. network_mode=benchmarker_config.network_mode,
  130. stderr=True,
  131. detach=True,
  132. init=True,
  133. extra_hosts=extra_hosts,
  134. privileged=True,
  135. ulimits=ulimit,
  136. sysctls=sysctl)
  137. containers.append(container)
  138. watch_thread = Thread(
  139. target=watch_container,
  140. args=(
  141. container,
  142. "%s.dockerfile" % test.name,
  143. ))
  144. watch_thread.daemon = True
  145. watch_thread.start()
  146. except Exception:
  147. with open(
  148. os.path.join(run_log_dir, "%s.log" % test.name.lower()),
  149. 'w') as run_log:
  150. tb = traceback.format_exc()
  151. log("Running docker cointainer: %s.dockerfile failed" % test.name,
  152. prefix=log_prefix,
  153. file=run_log)
  154. log(tb, prefix=log_prefix, file=run_log)
  155. return containers
  156. def successfully_running_containers(benchmarker_config, test, out):
  157. '''
  158. Returns whether all the expected containers for the given docker_files are
  159. running.
  160. '''
  161. client = docker.DockerClient(
  162. base_url=benchmarker_config.server_docker_host)
  163. running_container_images = []
  164. for container in client.containers.list():
  165. # 'techempower/tfb.test.gemini:0.1' -> 'gemini'
  166. image_tag = container.image.tags[0].split(':')[0][21:]
  167. running_container_images.append(image_tag)
  168. if test.name not in running_container_images:
  169. log_prefix = "%s: " % test.name
  170. log("ERROR: Expected techempower/tfb.test.%s to be running container" %
  171. test.name,
  172. prefix=log_prefix,
  173. file=out)
  174. return False
  175. return True
  176. def stop(benchmarker_config=None,
  177. containers=None,
  178. database_container=None,
  179. test=None):
  180. '''
  181. Attempts to stop the running test container.
  182. '''
  183. client = docker.DockerClient(
  184. base_url=benchmarker_config.server_docker_host)
  185. if containers is None:
  186. for container in client.containers.list():
  187. if len(
  188. container.image.tags
  189. ) > 0 and 'techempower' in container.image.tags[0] and 'tfb:latest' not in container.image.tags[0]:
  190. container.stop()
  191. else:
  192. # Stop all our running containers
  193. for container in containers:
  194. container.stop()
  195. database_client = docker.DockerClient(
  196. base_url=benchmarker_config.database_docker_host)
  197. # Stop the database container
  198. if database_container is None:
  199. for container in database_client.containers.list():
  200. if len(
  201. container.image.tags
  202. ) > 0 and 'techempower' in container.image.tags[0] and 'tfb:latest' not in container.image.tags[0]:
  203. container.stop()
  204. else:
  205. database_container.stop()
  206. client.containers.prune()
  207. if benchmarker_config.server_docker_host != benchmarker_config.database_docker_host:
  208. database_client.containers.prune()
  209. def find(path, pattern):
  210. '''
  211. Finds and returns all the the files matching the given pattern recursively in
  212. the given path.
  213. '''
  214. for root, dirs, files in os.walk(path):
  215. for name in files:
  216. if fnmatch.fnmatch(name, pattern):
  217. return os.path.join(root, name)
  218. def start_database(benchmarker_config, test, database):
  219. '''
  220. Sets up a container for the given database and port, and starts said docker
  221. container.
  222. '''
  223. image_name = "techempower/%s:latest" % database
  224. log_prefix = image_name + ": "
  225. database_dir = os.path.join(benchmarker_config.fwroot, "toolset", "setup",
  226. "docker", "databases", database)
  227. docker_file = "%s.dockerfile" % database
  228. pulled = False
  229. client = docker.DockerClient(
  230. base_url=benchmarker_config.database_docker_host)
  231. try:
  232. # Don't pull if we have it
  233. client.images.get(image_name)
  234. pulled = True
  235. log("Found published image; skipping build", prefix=log_prefix)
  236. except:
  237. # Pull the dependency image
  238. try:
  239. log("Attempting docker pull for image (this can take some time)",
  240. prefix=log_prefix)
  241. client.images.pull(image_name)
  242. pulled = True
  243. log("Found published image; skipping build", prefix=log_prefix)
  244. except:
  245. pass
  246. if not pulled:
  247. for line in docker.APIClient(
  248. base_url=benchmarker_config.database_docker_host).build(
  249. path=database_dir,
  250. dockerfile=docker_file,
  251. tag="techempower/%s" % database):
  252. if line.startswith('{"stream":'):
  253. line = json.loads(line)
  254. line = line[line.keys()[0]].encode('utf-8')
  255. log(line,
  256. prefix=log_prefix,
  257. color=Fore.WHITE + Style.BRIGHT \
  258. if re.match(r'^Step \d+\/\d+', line) else '')
  259. client = docker.DockerClient(
  260. base_url=benchmarker_config.database_docker_host)
  261. sysctl = {'net.core.somaxconn': 65535, 'kernel.sem': "250 32000 256 512"}
  262. ulimit = [{'name': 'nofile', 'hard': 65535, 'soft': 65535}]
  263. container = client.containers.run(
  264. "techempower/%s" % database,
  265. name="tfb-database",
  266. network=benchmarker_config.network,
  267. network_mode=benchmarker_config.network_mode,
  268. detach=True,
  269. ulimits=ulimit,
  270. sysctls=sysctl)
  271. # Sleep until the database accepts connections
  272. slept = 0
  273. max_sleep = 60
  274. database_ready = False
  275. while not database_ready and slept < max_sleep:
  276. time.sleep(1)
  277. slept += 1
  278. database_ready = test_database(benchmarker_config, database)
  279. if not database_ready:
  280. log("Database was not ready after startup", prefix=log_prefix)
  281. return container
  282. def test_client_connection(benchmarker_config, url):
  283. '''
  284. Tests that the app server at the given url responds successfully to a
  285. request.
  286. '''
  287. client = docker.DockerClient(
  288. base_url=benchmarker_config.client_docker_host)
  289. try:
  290. client.images.get('techempower/tfb.wrk:latest')
  291. except:
  292. log("Attempting docker pull for image (this can take some time)",
  293. prefix="techempower/tfb.wrk:latest: ")
  294. client.images.pull('techempower/tfb.wrk:latest')
  295. try:
  296. client.containers.run(
  297. 'techempower/tfb.wrk',
  298. 'curl %s' % url,
  299. network=benchmarker_config.network,
  300. network_mode=benchmarker_config.network_mode)
  301. except:
  302. return False
  303. return True
  304. def benchmark(benchmarker_config, script, variables, raw_file):
  305. '''
  306. Runs the given remote_script on the wrk container on the client machine.
  307. '''
  308. def watch_container(container, raw_file):
  309. with open(raw_file, 'w') as benchmark_file:
  310. for line in container.logs(stream=True):
  311. log(line, file=benchmark_file)
  312. client = docker.DockerClient(
  313. base_url=benchmarker_config.client_docker_host)
  314. sysctl = {'net.core.somaxconn': 65535}
  315. ulimit = [{'name': 'nofile', 'hard': 65535, 'soft': 65535}]
  316. watch_container(
  317. client.containers.run(
  318. "techempower/tfb.wrk",
  319. "/bin/bash /%s" % script,
  320. environment=variables,
  321. network=benchmarker_config.network,
  322. network_mode=benchmarker_config.network_mode,
  323. detach=True,
  324. stderr=True,
  325. ulimits=ulimit,
  326. sysctls=sysctl), raw_file)
  327. def __gather_dependencies(benchmarker_config, docker_file):
  328. '''
  329. Gathers all the known docker dependencies for the given docker image.
  330. '''
  331. deps = []
  332. docker_dir = os.path.join(benchmarker_config.fwroot, "toolset", "setup",
  333. "docker")
  334. if os.path.exists(docker_file):
  335. with open(docker_file) as fp:
  336. for line in fp.readlines():
  337. tokens = line.strip().split(' ')
  338. if tokens[0] == "FROM":
  339. # This is magic that our base image points to
  340. if tokens[1].startswith('techempower/'):
  341. dep_ref = tokens[1].strip().split(':')[0].strip()
  342. if '/' not in dep_ref:
  343. raise AttributeError(
  344. "Could not find docker FROM dependency: %s" %
  345. dep_ref)
  346. depToken = dep_ref.split('/')[1]
  347. deps.append(tokens[1])
  348. dep_docker_file = os.path.join(
  349. os.path.dirname(docker_file),
  350. depToken + ".dockerfile")
  351. if not os.path.exists(dep_docker_file):
  352. dep_docker_file = find(docker_dir,
  353. depToken + ".dockerfile")
  354. deps.extend(
  355. __gather_dependencies(benchmarker_config,
  356. dep_docker_file))
  357. return deps
  358. def __build_dependencies(benchmarker_config,
  359. test,
  360. docker_buildargs,
  361. build_log_dir=os.devnull):
  362. '''
  363. Builds all the dependency docker images for the given test.
  364. Does not build the test docker image.
  365. '''
  366. dependencies = OrderedSet(
  367. list(
  368. reversed(
  369. __gather_dependencies(
  370. benchmarker_config,
  371. os.path.join(test.directory,
  372. "%s.dockerfile" % test.name)))))
  373. docker_dir = os.path.join(benchmarker_config.fwroot, "toolset", "setup",
  374. "docker")
  375. for dep in dependencies:
  376. log_prefix = dep + ": "
  377. pulled = False
  378. # Do not pull techempower/ images if we are building specifically
  379. if not benchmarker_config.build and 'techempower/' not in dep:
  380. client = docker.DockerClient(
  381. base_url=benchmarker_config.server_docker_host)
  382. try:
  383. # If we have it, use it
  384. client.images.get(dep)
  385. pulled = True
  386. log("Found published image; skipping build", prefix=log_prefix)
  387. except:
  388. # Pull the dependency image
  389. try:
  390. log("Attempting docker pull for image (this can take some time)",
  391. prefix=log_prefix)
  392. client.images.pull(dep)
  393. pulled = True
  394. log("Found published image; skipping build",
  395. prefix=log_prefix)
  396. except:
  397. log("Docker pull failed; %s could not be found; terminating"
  398. % dep,
  399. prefix=log_prefix,
  400. color=Fore.RED)
  401. return 1
  402. if not pulled:
  403. dep_ref = dep.strip().split(':')[0].strip()
  404. dependency = dep_ref.split('/')[1]
  405. build_log_file = build_log_dir
  406. if build_log_dir is not os.devnull:
  407. build_log_file = os.path.join(build_log_dir,
  408. "%s.log" % dependency.lower())
  409. with open(build_log_file, 'w') as build_log:
  410. docker_file = os.path.join(test.directory,
  411. dependency + ".dockerfile")
  412. if not docker_file or not os.path.exists(docker_file):
  413. docker_file = find(docker_dir, dependency + ".dockerfile")
  414. if not docker_file:
  415. log("Docker build failed; %s could not be found; terminating"
  416. % (dependency + ".dockerfile"),
  417. prefix=log_prefix,
  418. file=build_log,
  419. color=Fore.RED)
  420. return 1
  421. # Build the dependency image
  422. try:
  423. for line in docker.APIClient(
  424. base_url=benchmarker_config.server_docker_host
  425. ).build(
  426. path=os.path.dirname(docker_file),
  427. dockerfile="%s.dockerfile" % dependency,
  428. tag=dep,
  429. buildargs=docker_buildargs,
  430. forcerm=True):
  431. if line.startswith('{"stream":'):
  432. line = json.loads(line)
  433. line = line[line.keys()[0]].encode('utf-8')
  434. log(line,
  435. prefix=log_prefix,
  436. file=build_log,
  437. color=Fore.WHITE + Style.BRIGHT \
  438. if re.match(r'^Step \d+\/\d+', line) else '')
  439. except Exception:
  440. tb = traceback.format_exc()
  441. log("Docker dependency build failed; terminating",
  442. prefix=log_prefix,
  443. file=build_log,
  444. color=Fore.RED)
  445. log(tb, prefix=log_prefix, file=build_log)
  446. return 1