docker_helper.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import os
  2. import socket
  3. import json
  4. import docker
  5. import time
  6. import re
  7. import traceback
  8. from threading import Thread
  9. from colorama import Fore, Style
  10. from toolset.utils.output_helper import log
  11. from toolset.utils.database_helper import test_database
  12. class DockerHelper:
  13. def __init__(self, benchmarker=None):
  14. self.benchmarker = benchmarker
  15. self.client = docker.DockerClient(
  16. base_url=self.benchmarker.config.client_docker_host)
  17. self.server = docker.DockerClient(
  18. base_url=self.benchmarker.config.server_docker_host)
  19. self.database = docker.DockerClient(
  20. base_url=self.benchmarker.config.database_docker_host)
  21. def __build(self, base_url, path, build_log_file, log_prefix, dockerfile,
  22. tag):
  23. '''
  24. Builds docker containers using docker-py low-level api
  25. '''
  26. self.benchmarker.time_logger.mark_build_start()
  27. with open(build_log_file, 'w') as build_log:
  28. try:
  29. client = docker.APIClient(base_url=base_url)
  30. output = client.build(
  31. path=path,
  32. dockerfile=dockerfile,
  33. tag=tag,
  34. forcerm=True,
  35. timeout=3600,
  36. pull=True)
  37. buffer = ""
  38. for token in output:
  39. if token.startswith('{"stream":'):
  40. token = json.loads(token)
  41. token = token[token.keys()[0]].encode('utf-8')
  42. buffer += token
  43. elif token.startswith('{"errorDetail":'):
  44. token = json.loads(token)
  45. raise Exception(token['errorDetail']['message'])
  46. while "\n" in buffer:
  47. index = buffer.index("\n")
  48. line = buffer[:index]
  49. buffer = buffer[index + 1:]
  50. log(line,
  51. prefix=log_prefix,
  52. file=build_log,
  53. color=Fore.WHITE + Style.BRIGHT \
  54. if re.match(r'^Step \d+\/\d+', line) else '')
  55. if buffer:
  56. log(buffer,
  57. prefix=log_prefix,
  58. file=build_log,
  59. color=Fore.WHITE + Style.BRIGHT \
  60. if re.match(r'^Step \d+\/\d+', buffer) else '')
  61. except Exception:
  62. tb = traceback.format_exc()
  63. log("Docker build failed; terminating",
  64. prefix=log_prefix,
  65. file=build_log,
  66. color=Fore.RED)
  67. log(tb, prefix=log_prefix, file=build_log)
  68. self.benchmarker.time_logger.log_build_end(
  69. log_prefix=log_prefix, file=build_log)
  70. raise
  71. self.benchmarker.time_logger.log_build_end(
  72. log_prefix=log_prefix, file=build_log)
  73. def clean(self):
  74. '''
  75. Cleans all the docker images from the system
  76. '''
  77. self.server.images.prune()
  78. for image in self.server.images.list():
  79. if len(image.tags) > 0:
  80. # 'techempower/tfb.test.gemini:0.1' -> 'techempower/tfb.test.gemini'
  81. image_tag = image.tags[0].split(':')[0]
  82. if image_tag != 'techempower/tfb' and 'techempower' in image_tag:
  83. self.server.images.remove(image.id, force=True)
  84. self.server.images.prune()
  85. self.database.images.prune()
  86. for image in self.database.images.list():
  87. if len(image.tags) > 0:
  88. # 'techempower/tfb.test.gemini:0.1' -> 'techempower/tfb.test.gemini'
  89. image_tag = image.tags[0].split(':')[0]
  90. if image_tag != 'techempower/tfb' and 'techempower' in image_tag:
  91. self.database.images.remove(image.id, force=True)
  92. self.database.images.prune()
  93. def build(self, test, build_log_dir=os.devnull):
  94. '''
  95. Builds the test docker containers
  96. '''
  97. log_prefix = "%s: " % test.name
  98. # Build the test image
  99. test_docker_file = "%s.dockerfile" % test.name
  100. build_log_file = build_log_dir
  101. if build_log_dir is not os.devnull:
  102. build_log_file = os.path.join(
  103. build_log_dir,
  104. "%s.log" % test_docker_file.replace(".dockerfile", "").lower())
  105. try:
  106. self.__build(
  107. base_url=self.benchmarker.config.server_docker_host,
  108. build_log_file=build_log_file,
  109. log_prefix=log_prefix,
  110. path=test.directory,
  111. dockerfile=test_docker_file,
  112. tag="techempower/tfb.test.%s" % test_docker_file.replace(
  113. ".dockerfile", ""))
  114. except Exception:
  115. return 1
  116. return 0
  117. def run(self, test, run_log_dir):
  118. '''
  119. Run the given Docker container(s)
  120. '''
  121. log_prefix = "%s: " % test.name
  122. container = None
  123. try:
  124. def watch_container(docker_container, docker_file):
  125. with open(
  126. os.path.join(
  127. run_log_dir, "%s.log" % docker_file.replace(
  128. ".dockerfile", "").lower()), 'w') as run_log:
  129. for line in docker_container.logs(stream=True):
  130. log(line, prefix=log_prefix, file=run_log)
  131. extra_hosts = None
  132. name = "tfb-server"
  133. if self.benchmarker.config.network is None:
  134. extra_hosts = {
  135. socket.gethostname():
  136. str(self.benchmarker.config.server_host),
  137. 'tfb-server':
  138. str(self.benchmarker.config.server_host),
  139. 'tfb-database':
  140. str(self.benchmarker.config.database_host)
  141. }
  142. name = None
  143. sysctl = {'net.core.somaxconn': 65535}
  144. ulimit = [{
  145. 'name': 'nofile',
  146. 'hard': 200000,
  147. 'soft': 200000
  148. }, {
  149. 'name': 'rtprio',
  150. 'hard': 99,
  151. 'soft': 99
  152. }]
  153. container = self.server.containers.run(
  154. "techempower/tfb.test.%s" % test.name,
  155. name=name,
  156. network=self.benchmarker.config.network,
  157. network_mode=self.benchmarker.config.network_mode,
  158. stderr=True,
  159. detach=True,
  160. init=True,
  161. extra_hosts=extra_hosts,
  162. privileged=True,
  163. ulimits=ulimit,
  164. sysctls=sysctl,
  165. remove=True,
  166. log_config={'type': None})
  167. watch_thread = Thread(
  168. target=watch_container,
  169. args=(
  170. container,
  171. "%s.dockerfile" % test.name,
  172. ))
  173. watch_thread.daemon = True
  174. watch_thread.start()
  175. except Exception:
  176. with open(
  177. os.path.join(run_log_dir, "%s.log" % test.name.lower()),
  178. 'w') as run_log:
  179. tb = traceback.format_exc()
  180. log("Running docker cointainer: %s.dockerfile failed" %
  181. test.name,
  182. prefix=log_prefix,
  183. file=run_log)
  184. log(tb, prefix=log_prefix, file=run_log)
  185. return container
  186. @staticmethod
  187. def __stop_container(container):
  188. try:
  189. client = container.client
  190. container.kill()
  191. while container.id in map(lambda x: x.id,
  192. client.containers.list()):
  193. pass
  194. except:
  195. # container has already been killed
  196. pass
  197. @staticmethod
  198. def __stop_all(docker_client):
  199. for container in docker_client.containers.list():
  200. if len(container.image.tags) > 0 \
  201. and 'techempower' in container.image.tags[0] \
  202. and 'tfb:latest' not in container.image.tags[0]:
  203. DockerHelper.__stop_container(container)
  204. def stop(self, containers=None):
  205. '''
  206. Attempts to stop a container or list of containers.
  207. If no containers are passed, stops all running containers.
  208. '''
  209. is_multi_setup = self.benchmarker.config.server_docker_host != \
  210. self.benchmarker.config.database_docker_host
  211. if containers:
  212. if not isinstance(containers, list):
  213. containers = [containers]
  214. for container in containers:
  215. DockerHelper.__stop_container(container)
  216. else:
  217. self.__stop_all(self.server)
  218. if is_multi_setup:
  219. self.__stop_all(self.database)
  220. self.database.containers.prune()
  221. if is_multi_setup:
  222. # Then we're on a 3 machine set up
  223. self.server.containers.prune()
  224. self.client.containers.prune()
  225. def build_databases(self):
  226. '''
  227. Builds all the databases necessary to run the list of benchmarker tests
  228. '''
  229. built = []
  230. for test in self.benchmarker.tests:
  231. db = test.database.lower()
  232. if db not in built and db != "none":
  233. image_name = "techempower/%s:latest" % db
  234. log_prefix = image_name + ": "
  235. database_dir = os.path.join(self.benchmarker.config.db_root,
  236. db)
  237. docker_file = "%s.dockerfile" % db
  238. self.__build(
  239. base_url=self.benchmarker.config.database_docker_host,
  240. path=database_dir,
  241. dockerfile=docker_file,
  242. log_prefix=log_prefix,
  243. build_log_file=os.devnull,
  244. tag="techempower/%s" % db)
  245. built.append(db)
  246. def start_database(self, database):
  247. '''
  248. Sets up a container for the given database and port, and starts said docker
  249. container.
  250. '''
  251. image_name = "techempower/%s:latest" % database
  252. log_prefix = image_name + ": "
  253. sysctl = {
  254. 'net.core.somaxconn': 65535,
  255. 'kernel.sem': "250 32000 256 512"
  256. }
  257. ulimit = [{'name': 'nofile', 'hard': 65535, 'soft': 65535}]
  258. container = self.database.containers.run(
  259. "techempower/%s" % database,
  260. name="tfb-database",
  261. network=self.benchmarker.config.network,
  262. network_mode=self.benchmarker.config.network_mode,
  263. detach=True,
  264. ulimits=ulimit,
  265. sysctls=sysctl,
  266. remove=True,
  267. log_config={'type': None})
  268. # Sleep until the database accepts connections
  269. slept = 0
  270. max_sleep = 60
  271. database_ready = False
  272. while not database_ready and slept < max_sleep:
  273. time.sleep(1)
  274. slept += 1
  275. database_ready = test_database(self.benchmarker.config, database)
  276. if not database_ready:
  277. log("Database was not ready after startup", prefix=log_prefix)
  278. return container
  279. def build_wrk(self):
  280. '''
  281. Builds the techempower/tfb.wrk container
  282. '''
  283. self.__build(
  284. base_url=self.benchmarker.config.client_docker_host,
  285. path=self.benchmarker.config.wrk_root,
  286. dockerfile="wrk.dockerfile",
  287. log_prefix="wrk: ",
  288. build_log_file=os.devnull,
  289. tag="techempower/tfb.wrk")
  290. def test_client_connection(self, url):
  291. '''
  292. Tests that the app server at the given url responds successfully to a
  293. request.
  294. '''
  295. try:
  296. self.client.containers.run(
  297. 'techempower/tfb.wrk',
  298. 'curl --max-time 5 %s' % url,
  299. remove=True,
  300. log_config={'type': None},
  301. network=self.benchmarker.config.network,
  302. network_mode=self.benchmarker.config.network_mode)
  303. except Exception:
  304. return False
  305. return True
  306. def server_container_exists(self, container_id_or_name):
  307. '''
  308. Returns True if the container still exists on the server.
  309. '''
  310. try:
  311. self.server.containers.get(container_id_or_name)
  312. return True
  313. except:
  314. return False
  315. def benchmark(self, script, variables, raw_file):
  316. '''
  317. Runs the given remote_script on the wrk container on the client machine.
  318. '''
  319. def watch_container(container):
  320. with open(raw_file, 'w') as benchmark_file:
  321. for line in container.logs(stream=True):
  322. log(line, file=benchmark_file)
  323. sysctl = {'net.core.somaxconn': 65535}
  324. ulimit = [{'name': 'nofile', 'hard': 65535, 'soft': 65535}]
  325. watch_container(
  326. self.client.containers.run(
  327. "techempower/tfb.wrk",
  328. "/bin/bash /%s" % script,
  329. environment=variables,
  330. network=self.benchmarker.config.network,
  331. network_mode=self.benchmarker.config.network_mode,
  332. detach=True,
  333. stderr=True,
  334. ulimits=ulimit,
  335. sysctls=sysctl,
  336. remove=True,
  337. log_config={'type': None}))