Blame scripts/memcached-automove-extstore

Packit Service 584ef9
#!/usr/bin/python3
Packit Service 584ef9
# Copyright 2017 Facebook.
Packit Service 584ef9
# Licensed under the same terms as memcached itself.
Packit Service 584ef9
Packit Service 584ef9
import argparse
Packit Service 584ef9
import socket
Packit Service 584ef9
import sys
Packit Service 584ef9
import re
Packit Service 584ef9
import traceback
Packit Service 584ef9
from time import sleep, time
Packit Service 584ef9
Packit Service 584ef9
parser = argparse.ArgumentParser(description="daemon for rebalancing slabs")
Packit Service 584ef9
parser.add_argument("--host", help="host to connect to",
Packit Service 584ef9
        default="localhost:11211", metavar="HOST:PORT")
Packit Service 584ef9
parser.add_argument("-s", "--sleep", help="seconds between runs",
Packit Service 584ef9
                    type=int, default="1")
Packit Service 584ef9
parser.add_argument("-v", "--verbose", action="store_true")
Packit Service 584ef9
parser.add_argument("-a", "--automove", action="store_true", default=False,
Packit Service 584ef9
                    help="enable automatic page rebalancing")
Packit Service 584ef9
parser.add_argument("-w", "--window", type=int, default="30",
Packit Service 584ef9
                    help="rolling window size for decision history")
Packit Service 584ef9
parser.add_argument("-r", "--ratio", type=float, default=0.8,
Packit Service 584ef9
                    help="ratio limiting distance between low/high class ages")
Packit Service 584ef9
parser.add_argument("-f", "--free", type=float, default=0.005,
Packit Service 584ef9
                    help="free chunks/pages buffer ratio")
Packit Service 584ef9
parser.add_argument("-z", "--size", type=int, default=512,
Packit Service 584ef9
                    help="item size cutoff for storage")
Packit Service 584ef9
Packit Service 584ef9
args = parser.parse_args()
Packit Service 584ef9
Packit Service 584ef9
host, port = args.host.split(':')
Packit Service 584ef9
Packit Service 584ef9
MIN_PAGES_FOR_SOURCE = 2
Packit Service 584ef9
MIN_PAGES_FOR_RECLAIM = 2.5
Packit Service 584ef9
MIN_PAGES_FREE = 1.5
Packit Service 584ef9
MEMCHECK_PERIOD = 60
Packit Service 584ef9
Packit Service 584ef9
def window_check(history, sid, key):
Packit Service 584ef9
    total = 0
Packit Service 584ef9
    for window in history['w']:
Packit Service 584ef9
        s = window.get(sid)
Packit Service 584ef9
        if s and s.get(key):
Packit Service 584ef9
            total += s.get(key)
Packit Service 584ef9
    return total
Packit Service 584ef9
Packit Service 584ef9
def window_key_check(history, key):
Packit Service 584ef9
    total = 0
Packit Service 584ef9
    for window in history['w']:
Packit Service 584ef9
        v = window.get(key)
Packit Service 584ef9
        if v:
Packit Service 584ef9
            total += v
Packit Service 584ef9
    return total
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
def determine_move(history, stats, diffs, memfree):
Packit Service 584ef9
    """ Figure out of a page move is in order.
Packit Service 584ef9
Packit Service 584ef9
    - Use as much memory as possible to hold items, reducing the load on
Packit Service 584ef9
      flash.
Packit Service 584ef9
    - tries to keep the global free page pool inbetween poolmin/poolmax.
Packit Service 584ef9
    - avoids flapping as much as possible:
Packit Service 584ef9
      - only pull pages off of a class if it hasn't recently evicted or allocated pages.
Packit Service 584ef9
      - only pull pages off if a sufficient number of free chunks are available.
Packit Service 584ef9
      - if global pool is below minimum remove pages from oldest large class.
Packit Service 584ef9
      - if global pool is above maximum, move pages to youngest large class.
Packit Service 584ef9
    - extstore manages a desired number of free chunks in each slab class.
Packit Service 584ef9
    - automover adjusts above limits once per minute based on current sizes.
Packit Service 584ef9
    - if youngest is below the age ratio limit of oldest, move a page to it.
Packit Service 584ef9
    """
Packit Service 584ef9
    # rotate windows
Packit Service 584ef9
    history['w'].append({})
Packit Service 584ef9
    if (len(history['w']) > args.window):
Packit Service 584ef9
        history['w'].pop(0)
Packit Service 584ef9
    w = history['w'][-1]
Packit Service 584ef9
    oldest = (-1, 0)
Packit Service 584ef9
    youngest = (-1, sys.maxsize)
Packit Service 584ef9
    too_free = False
Packit Service 584ef9
    # Most bytes free
Packit Service 584ef9
    decision = (-1, -1)
Packit Service 584ef9
    if int(stats['slab_global_page_pool']) < memfree[0] / 2:
Packit Service 584ef9
        w['slab_pool_low'] = 1
Packit Service 584ef9
    if int(stats['slab_global_page_pool']) > memfree[0]:
Packit Service 584ef9
        w['slab_pool_high'] = 1
Packit Service 584ef9
    if args.verbose:
Packit Service 584ef9
        print("global pool: [{}]".format(stats['slab_global_page_pool']))
Packit Service 584ef9
Packit Service 584ef9
    pool_low = window_key_check(history, 'slab_pool_low')
Packit Service 584ef9
    for sid, slab in diffs.items():
Packit Service 584ef9
        small_slab = False
Packit Service 584ef9
        free_enough = False
Packit Service 584ef9
        # Only balance larger slab classes
Packit Service 584ef9
        if slab['chunk_size'] < args.size:
Packit Service 584ef9
            small_slab = True
Packit Service 584ef9
Packit Service 584ef9
        w[sid] = {}
Packit Service 584ef9
        if 'evicted_d' not in slab or 'total_pages_d' not in slab:
Packit Service 584ef9
            continue
Packit Service 584ef9
        # mark this window as dirty if total pages increases or evictions
Packit Service 584ef9
        # happened
Packit Service 584ef9
        if slab['total_pages_d'] > 0:
Packit Service 584ef9
            w[sid]['dirty'] = 1
Packit Service 584ef9
        if slab['evicted_d'] > 0:
Packit Service 584ef9
            w[sid]['dirty'] = 1
Packit Service 584ef9
            w[sid]['ev'] = 1
Packit Service 584ef9
        if slab['free_chunks'] > memfree[sid]:
Packit Service 584ef9
            free_enough = True
Packit Service 584ef9
        if memfree[sid] > 0 and slab['free_chunks'] > (memfree[sid] * 2):
Packit Service 584ef9
            w[sid]['excess_free'] = 1
Packit Service 584ef9
        w[sid]['age'] = slab['age']
Packit Service 584ef9
        age = window_check(history, sid, 'age') / len(history['w'])
Packit Service 584ef9
Packit Service 584ef9
        # if > 2.5 pages free, and not dirty, reassign to global page pool
Packit Service 584ef9
        if slab['free_chunks'] > slab['chunks_per_page'] * MIN_PAGES_FOR_RECLAIM and too_free == False:
Packit Service 584ef9
            dirt = window_check(history, sid, 'dirty')
Packit Service 584ef9
            excess = window_check(history, sid, 'excess_free')
Packit Service 584ef9
            if small_slab == True and dirt == 0:
Packit Service 584ef9
                # If we're a small slab, don't hang on to free memory forever.
Packit Service 584ef9
                decision = (sid, 0)
Packit Service 584ef9
                too_free = True
Packit Service 584ef9
            elif small_slab == False and dirt == 0 \
Packit Service 584ef9
                    and excess >= len(history['w']):
Packit Service 584ef9
                decision = (sid, 0)
Packit Service 584ef9
                too_free = True
Packit Service 584ef9
Packit Service 584ef9
        # are we the oldest slab class? (and a valid target)
Packit Service 584ef9
        # don't consider for young if we've recently given it unused memory
Packit Service 584ef9
        if small_slab == False:
Packit Service 584ef9
            if age > oldest[1] and slab['total_pages'] > MIN_PAGES_FOR_SOURCE:
Packit Service 584ef9
                oldest = (sid, age)
Packit Service 584ef9
            if age < youngest[1] and slab['total_pages'] > 0 \
Packit Service 584ef9
                    and window_check(history, sid, 'excess_free') < len(history['w']) \
Packit Service 584ef9
                    and not (window_check(history, sid, 'relaxed') and free_enough):
Packit Service 584ef9
                youngest = (sid, age)
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
    if w.get('slab_pool_high') and youngest[0] != -1:
Packit Service 584ef9
        # if global pool is too high, feed youngest large class.
Packit Service 584ef9
        if slab['free_chunks'] <= memfree[youngest[0]]:
Packit Service 584ef9
            decision = (0, youngest[0])
Packit Service 584ef9
        w[youngest[0]]['relaxed'] = 1
Packit Service 584ef9
    elif too_free == False and pool_low and oldest[0] != -1:
Packit Service 584ef9
        # if pool is too low, take from oldest large class.
Packit Service 584ef9
        if args.verbose:
Packit Service 584ef9
            print("oldest:  [class: {}] [age: {:.2f}]".format(int(oldest[0]), oldest[1]))
Packit Service 584ef9
        decision = (oldest[0], 0)
Packit Service 584ef9
    elif too_free == False and youngest[0] != -1 and oldest[0] != -1 and youngest[0] != oldest[0]:
Packit Service 584ef9
        # youngest is outside of the tolerance ratio, move a page around.
Packit Service 584ef9
        if args.verbose:
Packit Service 584ef9
            print("old:   [class: {}] [age: {:.2f}]\nyoung: [class: {}] [age: {:.2f}]".format(
Packit Service 584ef9
                int(oldest[0]), oldest[1], int(youngest[0]), youngest[1]))
Packit Service 584ef9
Packit Service 584ef9
        slab = diffs[youngest[0]]
Packit Service 584ef9
        #print("F:{} L:{} Y:{} R:{}".format(slab['free_chunks'], memfree[youngest[0]], int(youngest[1]), int(oldest[1] * args.ratio)))
Packit Service 584ef9
        if youngest[1] < oldest[1] * args.ratio:
Packit Service 584ef9
            w[youngest[0]]['relaxed'] = 1
Packit Service 584ef9
            if slab['free_chunks'] <= memfree[youngest[0]]:
Packit Service 584ef9
                decision = (0, youngest[0])
Packit Service 584ef9
Packit Service 584ef9
    if (len(history['w']) >= args.window):
Packit Service 584ef9
        return decision
Packit Service 584ef9
    return (-1, -1)
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
def run_move(s, decision):
Packit Service 584ef9
    s.write("slabs reassign " + str(decision[0]) + " " + str(decision[1]) + "\r\n")
Packit Service 584ef9
    line = s.readline().rstrip()
Packit Service 584ef9
    if args.verbose:
Packit Service 584ef9
        print("move result:", line)
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
def diff_stats(before, after):
Packit Service 584ef9
    """ fills out "diffs" as deltas between before/after,
Packit Service 584ef9
    and "totals" as the sum of all slab classes.
Packit Service 584ef9
    "_d" postfix to keys means the delta between before/after.
Packit Service 584ef9
    non-postfix keys are total as of 'after's reading.
Packit Service 584ef9
    """
Packit Service 584ef9
    diffs = {}
Packit Service 584ef9
    totals = {}
Packit Service 584ef9
    for slabid in after.keys():
Packit Service 584ef9
        sb = before.get(slabid)
Packit Service 584ef9
        sa = after.get(slabid)
Packit Service 584ef9
        if not (sb and sa):
Packit Service 584ef9
            continue
Packit Service 584ef9
        slab = sa.copy()
Packit Service 584ef9
        for k in sa.keys():
Packit Service 584ef9
            if k not in sb:
Packit Service 584ef9
                continue
Packit Service 584ef9
            if k not in totals:
Packit Service 584ef9
                totals[k] = 0
Packit Service 584ef9
                totals[k + '_d'] = 0
Packit Service 584ef9
            if k + '_d' not in slab:
Packit Service 584ef9
                slab[k + '_d'] = 0
Packit Service 584ef9
            if re.search(r"^\d+$", sa[k]):
Packit Service 584ef9
                totals[k] += int(sa[k])
Packit Service 584ef9
                slab[k] = int(sa[k])
Packit Service 584ef9
                slab[k + '_d'] = int(sa[k]) - int(sb[k])
Packit Service 584ef9
                totals[k + '_d'] += int(sa[k]) - int(sb[k])
Packit Service 584ef9
        slab['slab'] = slabid
Packit Service 584ef9
        diffs[slabid] = slab
Packit Service 584ef9
    return (diffs, totals)
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
def read_slab_stats(s):
Packit Service 584ef9
    slabs = {}
Packit Service 584ef9
    for statcmd in ['items', 'slabs']:
Packit Service 584ef9
        #print("stat cmd: " + statcmd)
Packit Service 584ef9
        # FIXME: Formatting
Packit Service 584ef9
        s.write("stats " + statcmd + "\r\n")
Packit Service 584ef9
        while True:
Packit Service 584ef9
            line = s.readline().rstrip()
Packit Service 584ef9
            if line.startswith("END"):
Packit Service 584ef9
                break
Packit Service 584ef9
Packit Service 584ef9
            m = re.match(r"^STAT (?:items:)?(\d+):(\S+) (\S+)", line)
Packit Service 584ef9
            if m:
Packit Service 584ef9
                (slab, var, val) = m.groups()
Packit Service 584ef9
                if slab not in slabs:
Packit Service 584ef9
                    slabs[slab] = {}
Packit Service 584ef9
                slabs[slab][var] = val
Packit Service 584ef9
            #print("line: " + line)
Packit Service 584ef9
    return slabs
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
# HACK: lets look at 'evictions' being nonzero to indicate memory filled at some point.
Packit Service 584ef9
def read_stats(s):
Packit Service 584ef9
    stats = {}
Packit Service 584ef9
    s.write("stats\r\n")
Packit Service 584ef9
    while True:
Packit Service 584ef9
        line = s.readline().rstrip()
Packit Service 584ef9
        if line.startswith("END"):
Packit Service 584ef9
            break
Packit Service 584ef9
Packit Service 584ef9
        m = re.match(r"^STAT (\S+) (\S+)", line)
Packit Service 584ef9
        if m:
Packit Service 584ef9
            (key, val) = m.groups()
Packit Service 584ef9
            stats[key] = val
Packit Service 584ef9
    return stats
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
def pct(num, divisor):
Packit Service 584ef9
    if not divisor:
Packit Service 584ef9
        return 0
Packit Service 584ef9
    return (num / divisor)
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
def show_detail(diffs, totals):
Packit Service 584ef9
    """ just a pretty printer for some extra data """
Packit Service 584ef9
    print("\n  {:2s}: {:8s} (pct  ) {:10s} (pct    ) {:6s} (pct)   {:6s}".format('sb',
Packit Service 584ef9
                'evicted', 'items', 'pages', 'age'))
Packit Service 584ef9
Packit Service 584ef9
    for sid, slab in diffs.items():
Packit Service 584ef9
        if 'evicted_d' not in slab:
Packit Service 584ef9
            continue
Packit Service 584ef9
        print("  {:2d}: {:8d} ({:.2f}%) {:10d} ({:.4f}%) {:6d} ({:.2f}%) {:6d}".format(
Packit Service 584ef9
              int(sid), slab['evicted_d'], pct(slab['evicted_d'], totals['evicted_d']),
Packit Service 584ef9
              slab['number'], pct(slab['number'], totals['number']),
Packit Service 584ef9
              slab['total_pages'], pct(slab['total_pages'],
Packit Service 584ef9
              totals['total_pages']),
Packit Service 584ef9
              slab['age']))
Packit Service 584ef9
Packit Service 584ef9
def memfree_check(s, diffs, totals):
Packit Service 584ef9
    info = {}
Packit Service 584ef9
    # manage about this many free chunks in each slab class.
Packit Service 584ef9
    for sid, slab in diffs.items():
Packit Service 584ef9
        if sid == 0:
Packit Service 584ef9
            continue
Packit Service 584ef9
        hold_free = int((slab['used_chunks'] + slab['free_chunks']) * args.free)
Packit Service 584ef9
        # Hold a minimum of 1.5 pages so page moves are unlikely to lose items.
Packit Service 584ef9
        if slab['chunks_per_page'] * MIN_PAGES_FREE > hold_free:
Packit Service 584ef9
            hold_free = int(slab['chunks_per_page'] * MIN_PAGES_FREE)
Packit Service 584ef9
        info[sid] = hold_free
Packit Service 584ef9
        # TODO: only adjust if different?
Packit Service 584ef9
        s.write("extstore free_memchunks {} {}\r\n".format(sid, hold_free))
Packit Service 584ef9
        s.readline()
Packit Service 584ef9
Packit Service 584ef9
    # how many pages to leave in the global pool.
Packit Service 584ef9
    info[0] = int(totals['total_pages'] * args.free)
Packit Service 584ef9
    return info
Packit Service 584ef9
Packit Service 584ef9
Packit Service 584ef9
stats_pre = {}
Packit Service 584ef9
history = { 'w': [{}] }
Packit Service 584ef9
memfree = { 0: 2 }
Packit Service 584ef9
last_memfree_check = 0
Packit Service 584ef9
while True:
Packit Service 584ef9
    try:
Packit Service 584ef9
        with socket.create_connection((host, port), 5) as c:
Packit Service 584ef9
            s = c.makefile(mode="rw", buffering=1)
Packit Service 584ef9
            s.write("slabs automove 0\r\n")
Packit Service 584ef9
            print(s.readline().rstrip())
Packit Service 584ef9
            while True:
Packit Service 584ef9
                stats_post = read_slab_stats(s)
Packit Service 584ef9
                stats = read_stats(s)
Packit Service 584ef9
                (diffs, totals) = diff_stats(stats_pre, stats_post)
Packit Service 584ef9
                #if args.verbose:
Packit Service 584ef9
                #    show_detail(diffs, totals)
Packit Service 584ef9
                if int(stats['evictions']) > 0:
Packit Service 584ef9
                    if (last_memfree_check < time() - 60) and totals.get('total_pages'):
Packit Service 584ef9
                        memfree = memfree_check(s, diffs, totals)
Packit Service 584ef9
                        last_memfree_check = time()
Packit Service 584ef9
                    decision = (-1, -1)
Packit Service 584ef9
                    decision = determine_move(history, stats, diffs, memfree)
Packit Service 584ef9
                    if int(decision[0]) > 0 and int(decision[1]) >= 0:
Packit Service 584ef9
                        print("moving page from, to:", decision)
Packit Service 584ef9
                        if args.automove:
Packit Service 584ef9
                            run_move(s, decision)
Packit Service 584ef9
Packit Service 584ef9
                # Minimize sleeping if we just moved a page to global pool.
Packit Service 584ef9
                # Improves responsiveness during flushes/quick changes.
Packit Service 584ef9
                if decision[1] == 0:
Packit Service 584ef9
                    continue
Packit Service 584ef9
                else:
Packit Service 584ef9
                    sleep(args.sleep)
Packit Service 584ef9
                stats_pre = stats_post
Packit Service 584ef9
    except:
Packit Service 584ef9
        err = sys.exc_info()
Packit Service 584ef9
        print("disconnected:", err[0], err[1])
Packit Service 584ef9
        traceback.print_exc()
Packit Service 584ef9
        stats_pre = {}
Packit Service 584ef9
        history = { 'w': [{}] }
Packit Service 584ef9
        sleep(args.sleep)
Packit Service 584ef9