Blame scripts/memcached-automove

Packit 4e8bc4
#!/usr/bin/python3
Packit 4e8bc4
# Copyright 2017 Facebook.
Packit 4e8bc4
# Licensed under the same terms as memcached itself.
Packit 4e8bc4
Packit 4e8bc4
import argparse
Packit 4e8bc4
import socket
Packit 4e8bc4
import sys
Packit 4e8bc4
import re
Packit 4e8bc4
import traceback
Packit 4e8bc4
from time import sleep
Packit 4e8bc4
Packit 4e8bc4
parser = argparse.ArgumentParser(description="daemon for rebalancing slabs")
Packit 4e8bc4
parser.add_argument("--host", help="host to connect to",
Packit 4e8bc4
        default="localhost:11211", metavar="HOST:PORT")
Packit 4e8bc4
parser.add_argument("-s", "--sleep", help="seconds between runs",
Packit 4e8bc4
                    type=int, default="1")
Packit 4e8bc4
parser.add_argument("-v", "--verbose", action="store_true")
Packit 4e8bc4
parser.add_argument("-a", "--automove", action="store_true", default=False,
Packit 4e8bc4
                    help="enable automatic page rebalancing")
Packit 4e8bc4
parser.add_argument("-w", "--window", type=int, default="30",
Packit 4e8bc4
                    help="rolling window size for decision history")
Packit 4e8bc4
parser.add_argument("-r", "--ratio", type=float, default=0.8,
Packit 4e8bc4
                    help="ratio limiting distance between low/high class ages")
Packit 4e8bc4
Packit 4e8bc4
# TODO:
Packit 4e8bc4
# - age adjuster function
Packit 4e8bc4
#   - by ratio of get_hits
Packit 4e8bc4
#   - by ratio of chunk size
Packit 4e8bc4
Packit 4e8bc4
args = parser.parse_args()
Packit 4e8bc4
Packit 4e8bc4
host, port = args.host.split(':')
Packit 4e8bc4
Packit 4e8bc4
def window_check(history, sid, key):
Packit 4e8bc4
    total = 0
Packit 4e8bc4
    for window in history['w']:
Packit 4e8bc4
        s = window.get(sid)
Packit 4e8bc4
        if s and s.get(key):
Packit 4e8bc4
            total += s.get(key)
Packit 4e8bc4
    return total
Packit 4e8bc4
Packit 4e8bc4
def determine_move(history, diffs, totals):
Packit 4e8bc4
    """ Figure out of a page move is in order.
Packit 4e8bc4
Packit 4e8bc4
    - if > 2.5 pages of free space without free chunks reducing for N trials,
Packit 4e8bc4
      and no evictions for N trials, free to global.
Packit 4e8bc4
    - use ratio of how far apart age can be between slab classes
Packit 4e8bc4
    - TODO: if get_hits exists, allowable distance in age from the *youngest* slab
Packit 4e8bc4
      class is based on the percentage of get_hits the class gets, against the
Packit 4e8bc4
      factored max distance, ie:
Packit 4e8bc4
      1% max delta. youngest is 900, oldest allowable is 900+90
Packit 4e8bc4
      if class has 30% of get_hits, it can be 930
Packit 4e8bc4
    - youngest evicting slab class gets page moved to it, if outside ratio max
Packit 4e8bc4
    - use age as average over window. smooths over items falling out of WARM.
Packit 4e8bc4
      also gives a little weight: if still evicting, a few more pages than
Packit 4e8bc4
      necessary may be moved, pulling the classes closer together. Hopefully
Packit 4e8bc4
      avoiding ping-ponging.
Packit 4e8bc4
    """
Packit 4e8bc4
    # rotate windows
Packit 4e8bc4
    history['w'].append({})
Packit 4e8bc4
    if (len(history['w']) > args.window):
Packit 4e8bc4
        history['w'].pop(0)
Packit 4e8bc4
    w = history['w'][-1]
Packit 4e8bc4
    oldest = (-1, 0)
Packit 4e8bc4
    youngest = (-1, sys.maxsize)
Packit 4e8bc4
    decision = (-1, -1)
Packit 4e8bc4
    for sid, slab in diffs.items():
Packit 4e8bc4
Packit 4e8bc4
        w[sid] = {}
Packit 4e8bc4
        if 'evicted_d' not in slab or 'total_pages_d' not in slab:
Packit 4e8bc4
            continue
Packit 4e8bc4
        # mark this window as dirty if total pages increases or evictions
Packit 4e8bc4
        # happened
Packit 4e8bc4
        if slab['total_pages_d'] > 0:
Packit 4e8bc4
            w[sid]['dirty'] = 1
Packit 4e8bc4
        if slab['evicted_d'] > 0:
Packit 4e8bc4
            w[sid]['dirty'] = 1
Packit 4e8bc4
            w[sid]['ev'] = 1
Packit 4e8bc4
        w[sid]['age'] = slab['age']
Packit 4e8bc4
        age = window_check(history, sid, 'age') / len(history['w'])
Packit 4e8bc4
Packit 4e8bc4
        # if > 2.5 pages free, and not dirty, reassign to global page pool and
Packit 4e8bc4
        # break.
Packit 4e8bc4
        if slab['free_chunks'] > slab['chunks_per_page'] * 2.5:
Packit 4e8bc4
            if window_check(history, sid, 'dirty') == 0:
Packit 4e8bc4
                decision = (sid, 0)
Packit 4e8bc4
                break
Packit 4e8bc4
Packit 4e8bc4
        # are we the oldest slab class? (and a valid target)
Packit 4e8bc4
        if age > oldest[1] and slab['total_pages'] > 2:
Packit 4e8bc4
            oldest = (sid, age)
Packit 4e8bc4
Packit 4e8bc4
        # are we the youngest evicting slab class?
Packit 4e8bc4
        ev_total = window_check(history, sid, 'ev')
Packit 4e8bc4
        window_min = args.window / 2
Packit 4e8bc4
        if age < youngest[1] and ev_total > window_min:
Packit 4e8bc4
            youngest = (sid, age)
Packit 4e8bc4
            #if args.verbose:
Packit 4e8bc4
            #    print("window: {} range: {}".format(ev_total, window_min))
Packit 4e8bc4
Packit 4e8bc4
    # is the youngest slab class too young?
Packit 4e8bc4
    if youngest[0] != -1 and oldest[0] != -1:
Packit 4e8bc4
        if args.verbose:
Packit 4e8bc4
            print("old:   [class: {}] [age: {:.2f}]\nyoung: [class: {}] [age: {:.2f}]".format(
Packit 4e8bc4
                int(oldest[0]), oldest[1], int(youngest[0]), youngest[1]))
Packit 4e8bc4
        if youngest[1] < oldest[1] * args.ratio and w[youngest[0]].get('ev'):
Packit 4e8bc4
            decision = (oldest[0], youngest[0])
Packit 4e8bc4
Packit 4e8bc4
    if (len(history['w']) >= args.window):
Packit 4e8bc4
        return decision
Packit 4e8bc4
    return (-1, -1)
Packit 4e8bc4
Packit 4e8bc4
Packit 4e8bc4
def run_move(s, decision):
Packit 4e8bc4
    s.write("slabs reassign " + str(decision[0]) + " " + str(decision[1]) + "\r\n")
Packit 4e8bc4
    line = s.readline().rstrip()
Packit 4e8bc4
    if args.verbose:
Packit 4e8bc4
        print("move result:", line)
Packit 4e8bc4
Packit 4e8bc4
Packit 4e8bc4
def diff_stats(before, after):
Packit 4e8bc4
    """ fills out "diffs" as deltas between before/after,
Packit 4e8bc4
    and "totals" as the sum of all slab classes.
Packit 4e8bc4
    "_d" postfix to keys means the delta between before/after.
Packit 4e8bc4
    non-postfix keys are total as of 'after's reading.
Packit 4e8bc4
    """
Packit 4e8bc4
    diffs = {}
Packit 4e8bc4
    totals = {}
Packit 4e8bc4
    for slabid in after.keys():
Packit 4e8bc4
        sb = before.get(slabid)
Packit 4e8bc4
        sa = after.get(slabid)
Packit 4e8bc4
        if not (sb and sa):
Packit 4e8bc4
            continue
Packit 4e8bc4
        slab = sa.copy()
Packit 4e8bc4
        for k in sa.keys():
Packit 4e8bc4
            if k not in sb:
Packit 4e8bc4
                continue
Packit 4e8bc4
            if k not in totals:
Packit 4e8bc4
                totals[k] = 0
Packit 4e8bc4
                totals[k + '_d'] = 0
Packit 4e8bc4
            if k + '_d' not in slab:
Packit 4e8bc4
                slab[k + '_d'] = 0
Packit 4e8bc4
            if re.search(r"^\d+$", sa[k]):
Packit 4e8bc4
                totals[k] += int(sa[k])
Packit 4e8bc4
                slab[k] = int(sa[k])
Packit 4e8bc4
                slab[k + '_d'] = int(sa[k]) - int(sb[k])
Packit 4e8bc4
                totals[k + '_d'] += int(sa[k]) - int(sb[k])
Packit 4e8bc4
        slab['slab'] = slabid
Packit 4e8bc4
        diffs[slabid] = slab
Packit 4e8bc4
    return (diffs, totals)
Packit 4e8bc4
Packit 4e8bc4
Packit 4e8bc4
def read_stats(s):
Packit 4e8bc4
    slabs = {}
Packit 4e8bc4
    for statcmd in ['items', 'slabs']:
Packit 4e8bc4
        #print("stat cmd: " + statcmd)
Packit 4e8bc4
        # FIXME: Formatting
Packit 4e8bc4
        s.write("stats " + statcmd + "\r\n")
Packit 4e8bc4
        while True:
Packit 4e8bc4
            line = s.readline().rstrip()
Packit 4e8bc4
            if line.startswith("END"):
Packit 4e8bc4
                break
Packit 4e8bc4
Packit 4e8bc4
            m = re.match(r"^STAT (?:items:)?(\d+):(\S+) (\S+)", line)
Packit 4e8bc4
            if m:
Packit 4e8bc4
                (slab, var, val) = m.groups()
Packit 4e8bc4
                if slab not in slabs:
Packit 4e8bc4
                    slabs[slab] = {}
Packit 4e8bc4
                slabs[slab][var] = val
Packit 4e8bc4
            #print("line: " + line)
Packit 4e8bc4
    return slabs
Packit 4e8bc4
Packit 4e8bc4
Packit 4e8bc4
def pct(num, divisor):
Packit 4e8bc4
    if not divisor:
Packit 4e8bc4
        return 0
Packit 4e8bc4
    return (num / divisor)
Packit 4e8bc4
Packit 4e8bc4
Packit 4e8bc4
def show_detail(diffs, totals):
Packit 4e8bc4
    """ just a pretty printer for some extra data """
Packit 4e8bc4
    print("\n  {:2s}: {:8s} (pct  ) {:10s} (pct    ) {:6s} (pct)   {:6s}".format('sb',
Packit 4e8bc4
                'evicted', 'items', 'pages', 'age'))
Packit 4e8bc4
Packit 4e8bc4
    for sid, slab in diffs.items():
Packit 4e8bc4
        if 'evicted_d' not in slab:
Packit 4e8bc4
            continue
Packit 4e8bc4
        print("  {:2d}: {:8d} ({:.2f}%) {:10d} ({:.4f}%) {:6d} ({:.2f}%) {:6d}".format(
Packit 4e8bc4
              int(sid), slab['evicted_d'], pct(slab['evicted_d'], totals['evicted_d']),
Packit 4e8bc4
              slab['number'], pct(slab['number'], totals['number']),
Packit 4e8bc4
              slab['total_pages'], pct(slab['total_pages'],
Packit 4e8bc4
              totals['total_pages']),
Packit 4e8bc4
              slab['age']))
Packit 4e8bc4
Packit 4e8bc4
Packit 4e8bc4
stats_pre = {}
Packit 4e8bc4
history = { 'w': [{}] }
Packit 4e8bc4
while True:
Packit 4e8bc4
    try:
Packit 4e8bc4
        with socket.create_connection((host, port), 5) as c:
Packit 4e8bc4
            s = c.makefile(mode="rw", buffering=1)
Packit 4e8bc4
            s.write("slabs automove 0\r\n")
Packit 4e8bc4
            print(s.readline().rstrip())
Packit 4e8bc4
            while True:
Packit 4e8bc4
                stats_post = read_stats(s)
Packit 4e8bc4
                (diffs, totals) = diff_stats(stats_pre, stats_post)
Packit 4e8bc4
                if args.verbose:
Packit 4e8bc4
                    show_detail(diffs, totals)
Packit 4e8bc4
                decision = determine_move(history, diffs, totals)
Packit 4e8bc4
                if decision[0] != -1 and decision[1] != -1:
Packit 4e8bc4
                    print("moving page from, to:", decision)
Packit 4e8bc4
                    if args.automove:
Packit 4e8bc4
                        run_move(s, decision)
Packit 4e8bc4
Packit 4e8bc4
                # Minimize sleeping if we just moved a page to global pool.
Packit 4e8bc4
                # Improves responsiveness during flushes/quick changes.
Packit 4e8bc4
                if decision[1] == 0:
Packit 4e8bc4
                    sleep(0.05)
Packit 4e8bc4
                else:
Packit 4e8bc4
                    sleep(args.sleep)
Packit 4e8bc4
                stats_pre = stats_post
Packit 4e8bc4
    except:
Packit 4e8bc4
        err = sys.exc_info()
Packit 4e8bc4
        print("disconnected:", err[0], err[1])
Packit 4e8bc4
        traceback.print_exc()
Packit 4e8bc4
        stats_pre = {}
Packit 4e8bc4
        history = { 'w': [{}] }
Packit 4e8bc4
        sleep(args.sleep)
Packit 4e8bc4