1#!/usr/bin/python3 2# Copyright 2017 Facebook. 3# Licensed under the same terms as memcached itself. 4 5import argparse 6import socket 7import sys 8import re 9import traceback 10from time import sleep, time 11 12parser = argparse.ArgumentParser(description="daemon for rebalancing slabs") 13parser.add_argument("--host", help="host to connect to", 14 default="localhost:11211", metavar="HOST:PORT") 15parser.add_argument("-s", "--sleep", help="seconds between runs", 16 type=int, default="1") 17parser.add_argument("-v", "--verbose", action="store_true") 18parser.add_argument("-a", "--automove", action="store_true", default=False, 19 help="enable automatic page rebalancing") 20parser.add_argument("-w", "--window", type=int, default="30", 21 help="rolling window size for decision history") 22parser.add_argument("-r", "--ratio", type=float, default=0.8, 23 help="ratio limiting distance between low/high class ages") 24parser.add_argument("-f", "--free", type=float, default=0.005, 25 help="free chunks/pages buffer ratio") 26parser.add_argument("-z", "--size", type=int, default=512, 27 help="item size cutoff for storage") 28 29args = parser.parse_args() 30 31host, port = args.host.split(':') 32 33MIN_PAGES_FOR_SOURCE = 2 34MIN_PAGES_FOR_RECLAIM = 2.5 35MIN_PAGES_FREE = 1.5 36MEMCHECK_PERIOD = 60 37 38def window_check(history, sid, key): 39 total = 0 40 for window in history['w']: 41 s = window.get(sid) 42 if s and s.get(key): 43 total += s.get(key) 44 return total 45 46def window_key_check(history, key): 47 total = 0 48 for window in history['w']: 49 v = window.get(key) 50 if v: 51 total += v 52 return total 53 54 55def determine_move(history, stats, diffs, memfree): 56 """ Figure out of a page move is in order. 57 58 - Use as much memory as possible to hold items, reducing the load on 59 flash. 60 - tries to keep the global free page pool inbetween poolmin/poolmax. 61 - avoids flapping as much as possible: 62 - only pull pages off of a class if it hasn't recently evicted or allocated pages. 63 - only pull pages off if a sufficient number of free chunks are available. 64 - if global pool is below minimum remove pages from oldest large class. 65 - if global pool is above maximum, move pages to youngest large class. 66 - extstore manages a desired number of free chunks in each slab class. 67 - automover adjusts above limits once per minute based on current sizes. 68 - if youngest is below the age ratio limit of oldest, move a page to it. 69 """ 70 # rotate windows 71 history['w'].append({}) 72 if (len(history['w']) > args.window): 73 history['w'].pop(0) 74 w = history['w'][-1] 75 oldest = (-1, 0) 76 youngest = (-1, sys.maxsize) 77 too_free = False 78 # Most bytes free 79 decision = (-1, -1) 80 if int(stats['slab_global_page_pool']) < memfree[0] / 2: 81 w['slab_pool_low'] = 1 82 if int(stats['slab_global_page_pool']) > memfree[0]: 83 w['slab_pool_high'] = 1 84 if args.verbose: 85 print("global pool: [{}]".format(stats['slab_global_page_pool'])) 86 87 pool_low = window_key_check(history, 'slab_pool_low') 88 for sid, slab in diffs.items(): 89 small_slab = False 90 free_enough = False 91 # Only balance larger slab classes 92 if slab['chunk_size'] < args.size: 93 small_slab = True 94 95 w[sid] = {} 96 if 'evicted_d' not in slab or 'total_pages_d' not in slab: 97 continue 98 # mark this window as dirty if total pages increases or evictions 99 # happened 100 if slab['total_pages_d'] > 0: 101 w[sid]['dirty'] = 1 102 if slab['evicted_d'] > 0: 103 w[sid]['dirty'] = 1 104 w[sid]['ev'] = 1 105 if slab['free_chunks'] > memfree[sid]: 106 free_enough = True 107 if memfree[sid] > 0 and slab['free_chunks'] > (memfree[sid] * 2): 108 w[sid]['excess_free'] = 1 109 w[sid]['age'] = slab['age'] 110 age = window_check(history, sid, 'age') / len(history['w']) 111 112 # if > 2.5 pages free, and not dirty, reassign to global page pool 113 if slab['free_chunks'] > slab['chunks_per_page'] * MIN_PAGES_FOR_RECLAIM and too_free == False: 114 dirt = window_check(history, sid, 'dirty') 115 excess = window_check(history, sid, 'excess_free') 116 if small_slab == True and dirt == 0: 117 # If we're a small slab, don't hang on to free memory forever. 118 decision = (sid, 0) 119 too_free = True 120 elif small_slab == False and dirt == 0 \ 121 and excess >= len(history['w']): 122 decision = (sid, 0) 123 too_free = True 124 125 # are we the oldest slab class? (and a valid target) 126 # don't consider for young if we've recently given it unused memory 127 if small_slab == False: 128 if age > oldest[1] and slab['total_pages'] > MIN_PAGES_FOR_SOURCE: 129 oldest = (sid, age) 130 if age < youngest[1] and slab['total_pages'] > 0 \ 131 and window_check(history, sid, 'excess_free') < len(history['w']) \ 132 and not (window_check(history, sid, 'relaxed') and free_enough): 133 youngest = (sid, age) 134 135 136 if w.get('slab_pool_high') and youngest[0] != -1: 137 # if global pool is too high, feed youngest large class. 138 if slab['free_chunks'] <= memfree[youngest[0]]: 139 decision = (0, youngest[0]) 140 w[youngest[0]]['relaxed'] = 1 141 elif too_free == False and pool_low and oldest[0] != -1: 142 # if pool is too low, take from oldest large class. 143 if args.verbose: 144 print("oldest: [class: {}] [age: {:.2f}]".format(int(oldest[0]), oldest[1])) 145 decision = (oldest[0], 0) 146 elif too_free == False and youngest[0] != -1 and oldest[0] != -1 and youngest[0] != oldest[0]: 147 # youngest is outside of the tolerance ratio, move a page around. 148 if args.verbose: 149 print("old: [class: {}] [age: {:.2f}]\nyoung: [class: {}] [age: {:.2f}]".format( 150 int(oldest[0]), oldest[1], int(youngest[0]), youngest[1])) 151 152 slab = diffs[youngest[0]] 153 #print("F:{} L:{} Y:{} R:{}".format(slab['free_chunks'], memfree[youngest[0]], int(youngest[1]), int(oldest[1] * args.ratio))) 154 if youngest[1] < oldest[1] * args.ratio: 155 w[youngest[0]]['relaxed'] = 1 156 if slab['free_chunks'] <= memfree[youngest[0]]: 157 decision = (0, youngest[0]) 158 159 if (len(history['w']) >= args.window): 160 return decision 161 return (-1, -1) 162 163 164def run_move(s, decision): 165 s.write("slabs reassign " + str(decision[0]) + " " + str(decision[1]) + "\r\n") 166 line = s.readline().rstrip() 167 if args.verbose: 168 print("move result:", line) 169 170 171def diff_stats(before, after): 172 """ fills out "diffs" as deltas between before/after, 173 and "totals" as the sum of all slab classes. 174 "_d" postfix to keys means the delta between before/after. 175 non-postfix keys are total as of 'after's reading. 176 """ 177 diffs = {} 178 totals = {} 179 for slabid in after.keys(): 180 sb = before.get(slabid) 181 sa = after.get(slabid) 182 if not (sb and sa): 183 continue 184 slab = sa.copy() 185 for k in sa.keys(): 186 if k not in sb: 187 continue 188 if k not in totals: 189 totals[k] = 0 190 totals[k + '_d'] = 0 191 if k + '_d' not in slab: 192 slab[k + '_d'] = 0 193 if re.search(r"^\d+$", sa[k]): 194 totals[k] += int(sa[k]) 195 slab[k] = int(sa[k]) 196 slab[k + '_d'] = int(sa[k]) - int(sb[k]) 197 totals[k + '_d'] += int(sa[k]) - int(sb[k]) 198 slab['slab'] = slabid 199 diffs[slabid] = slab 200 return (diffs, totals) 201 202 203def read_slab_stats(s): 204 slabs = {} 205 for statcmd in ['items', 'slabs']: 206 #print("stat cmd: " + statcmd) 207 # FIXME: Formatting 208 s.write("stats " + statcmd + "\r\n") 209 while True: 210 line = s.readline().rstrip() 211 if line.startswith("END"): 212 break 213 214 m = re.match(r"^STAT (?:items:)?(\d+):(\S+) (\S+)", line) 215 if m: 216 (slab, var, val) = m.groups() 217 if slab not in slabs: 218 slabs[slab] = {} 219 slabs[slab][var] = val 220 #print("line: " + line) 221 return slabs 222 223 224# HACK: lets look at 'evictions' being nonzero to indicate memory filled at some point. 225def read_stats(s): 226 stats = {} 227 s.write("stats\r\n") 228 while True: 229 line = s.readline().rstrip() 230 if line.startswith("END"): 231 break 232 233 m = re.match(r"^STAT (\S+) (\S+)", line) 234 if m: 235 (key, val) = m.groups() 236 stats[key] = val 237 return stats 238 239 240def pct(num, divisor): 241 if not divisor: 242 return 0 243 return (num / divisor) 244 245 246def show_detail(diffs, totals): 247 """ just a pretty printer for some extra data """ 248 print("\n {:2s}: {:8s} (pct ) {:10s} (pct ) {:6s} (pct) {:6s}".format('sb', 249 'evicted', 'items', 'pages', 'age')) 250 251 for sid, slab in diffs.items(): 252 if 'evicted_d' not in slab: 253 continue 254 print(" {:2d}: {:8d} ({:.2f}%) {:10d} ({:.4f}%) {:6d} ({:.2f}%) {:6d}".format( 255 int(sid), slab['evicted_d'], pct(slab['evicted_d'], totals['evicted_d']), 256 slab['number'], pct(slab['number'], totals['number']), 257 slab['total_pages'], pct(slab['total_pages'], 258 totals['total_pages']), 259 slab['age'])) 260 261def memfree_check(s, diffs, totals): 262 info = {} 263 # manage about this many free chunks in each slab class. 264 for sid, slab in diffs.items(): 265 if sid == 0: 266 continue 267 hold_free = int((slab['used_chunks'] + slab['free_chunks']) * args.free) 268 # Hold a minimum of 1.5 pages so page moves are unlikely to lose items. 269 if slab['chunks_per_page'] * MIN_PAGES_FREE > hold_free: 270 hold_free = int(slab['chunks_per_page'] * MIN_PAGES_FREE) 271 info[sid] = hold_free 272 # TODO: only adjust if different? 273 s.write("extstore free_memchunks {} {}\r\n".format(sid, hold_free)) 274 s.readline() 275 276 # how many pages to leave in the global pool. 277 info[0] = int(totals['total_pages'] * args.free) 278 return info 279 280 281stats_pre = {} 282history = { 'w': [{}] } 283memfree = { 0: 2 } 284last_memfree_check = 0 285while True: 286 try: 287 with socket.create_connection((host, port), 5) as c: 288 s = c.makefile(mode="rw", buffering=1) 289 s.write("slabs automove 0\r\n") 290 print(s.readline().rstrip()) 291 while True: 292 stats_post = read_slab_stats(s) 293 stats = read_stats(s) 294 (diffs, totals) = diff_stats(stats_pre, stats_post) 295 #if args.verbose: 296 # show_detail(diffs, totals) 297 if int(stats['evictions']) > 0: 298 if (last_memfree_check < time() - 60) and totals.get('total_pages'): 299 memfree = memfree_check(s, diffs, totals) 300 last_memfree_check = time() 301 decision = (-1, -1) 302 decision = determine_move(history, stats, diffs, memfree) 303 if int(decision[0]) > 0 and int(decision[1]) >= 0: 304 print("moving page from, to:", decision) 305 if args.automove: 306 run_move(s, decision) 307 308 # Minimize sleeping if we just moved a page to global pool. 309 # Improves responsiveness during flushes/quick changes. 310 if decision[1] == 0: 311 continue 312 else: 313 sleep(args.sleep) 314 stats_pre = stats_post 315 except: 316 err = sys.exc_info() 317 print("disconnected:", err[0], err[1]) 318 traceback.print_exc() 319 stats_pre = {} 320 history = { 'w': [{}] } 321 sleep(args.sleep) 322 323