docker_helper.py 21 KB

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