| #!/usr/bin/env python3 |
| # |
| # Copyright (c) 2008 Ben Rockwood <benr@cuddletech.com>, |
| # Copyright (c) 2010 Martin Matuska <mm@FreeBSD.org>, |
| # Copyright (c) 2010-2011 Jason J. Hellenthal <jhell@DataIX.net>, |
| # Copyright (c) 2017 Scot W. Stevenson <scot.stevenson@gmail.com> |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND |
| # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
| # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE |
| # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS |
| # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
| # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT |
| # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY |
| # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF |
| # SUCH DAMAGE. |
| """Print statistics on the ZFS ARC Cache and other information |
| |
| Provides basic information on the ARC, its efficiency, the L2ARC (if present), |
| the Data Management Unit (DMU), Virtual Devices (VDEVs), and tunables. See |
| the in-source documentation and code at |
| https://github.com/zfsonlinux/zfs/blob/master/module/zfs/arc.c for details. |
| The original introduction to arc_summary can be found at |
| http://cuddletech.com/?p=454 |
| """ |
| |
| import argparse |
| import os |
| import subprocess |
| import sys |
| import time |
| |
| DESCRIPTION = 'Print ARC and other statistics for ZFS on Linux' |
| INDENT = ' '*8 |
| LINE_LENGTH = 72 |
| PROC_PATH = '/proc/spl/kstat/zfs/' |
| SPL_PATH = '/sys/module/spl/parameters/' |
| TUNABLES_PATH = '/sys/module/zfs/parameters/' |
| DATE_FORMAT = '%a %b %d %H:%M:%S %Y' |
| TITLE = 'ZFS Subsystem Report' |
| |
| SECTIONS = 'arc archits dmu l2arc spl tunables vdev zil'.split() |
| SECTION_HELP = 'print info from one section ('+' '.join(SECTIONS)+')' |
| |
| # Tunables and SPL are handled separately because they come from |
| # different sources |
| SECTION_PATHS = {'arc': 'arcstats', |
| 'dmu': 'dmu_tx', |
| 'l2arc': 'arcstats', # L2ARC stuff lives in arcstats |
| 'vdev': 'vdev_cache_stats', |
| 'xuio': 'xuio_stats', |
| 'zfetch': 'zfetchstats', |
| 'zil': 'zil'} |
| |
| parser = argparse.ArgumentParser(description=DESCRIPTION) |
| parser.add_argument('-a', '--alternate', action='store_true', default=False, |
| help='use alternate formatting for tunables and SPL', |
| dest='alt') |
| parser.add_argument('-d', '--description', action='store_true', default=False, |
| help='print descriptions with tunables and SPL', |
| dest='desc') |
| parser.add_argument('-g', '--graph', action='store_true', default=False, |
| help='print graph on ARC use and exit', dest='graph') |
| parser.add_argument('-p', '--page', type=int, dest='page', |
| help='print page by number (DEPRECATED, use "-s")') |
| parser.add_argument('-r', '--raw', action='store_true', default=False, |
| help='dump all available data with minimal formatting', |
| dest='raw') |
| parser.add_argument('-s', '--section', dest='section', help=SECTION_HELP) |
| ARGS = parser.parse_args() |
| |
| |
| def cleanup_line(single_line): |
| """Format a raw line of data from /proc and isolate the name value |
| part, returning a tuple with each. Currently, this gets rid of the |
| middle '4'. For example "arc_no_grow 4 0" returns the tuple |
| ("arc_no_grow", "0"). |
| """ |
| name, _, value = single_line.split() |
| |
| return name, value |
| |
| |
| def draw_graph(kstats_dict): |
| """Draw a primitive graph representing the basic information on the |
| ARC -- its size and the proportion used by MFU and MRU -- and quit. |
| We use max size of the ARC to calculate how full it is. This is a |
| very rough representation. |
| """ |
| |
| arc_stats = isolate_section('arcstats', kstats_dict) |
| |
| GRAPH_INDENT = ' '*4 |
| GRAPH_WIDTH = 60 |
| arc_size = f_bytes(arc_stats['size']) |
| arc_perc = f_perc(arc_stats['size'], arc_stats['c_max']) |
| mfu_size = f_bytes(arc_stats['mfu_size']) |
| mru_size = f_bytes(arc_stats['mru_size']) |
| meta_limit = f_bytes(arc_stats['arc_meta_limit']) |
| meta_size = f_bytes(arc_stats['arc_meta_used']) |
| dnode_limit = f_bytes(arc_stats['arc_dnode_limit']) |
| dnode_size = f_bytes(arc_stats['dnode_size']) |
| |
| info_form = ('ARC: {0} ({1}) MFU: {2} MRU: {3} META: {4} ({5}) ' |
| 'DNODE {6} ({7})') |
| info_line = info_form.format(arc_size, arc_perc, mfu_size, mru_size, |
| meta_size, meta_limit, dnode_size, |
| dnode_limit) |
| info_spc = ' '*int((GRAPH_WIDTH-len(info_line))/2) |
| info_line = GRAPH_INDENT+info_spc+info_line |
| |
| graph_line = GRAPH_INDENT+'+'+('-'*(GRAPH_WIDTH-2))+'+' |
| |
| mfu_perc = float(int(arc_stats['mfu_size'])/int(arc_stats['c_max'])) |
| mru_perc = float(int(arc_stats['mru_size'])/int(arc_stats['c_max'])) |
| arc_perc = float(int(arc_stats['size'])/int(arc_stats['c_max'])) |
| total_ticks = float(arc_perc)*GRAPH_WIDTH |
| mfu_ticks = mfu_perc*GRAPH_WIDTH |
| mru_ticks = mru_perc*GRAPH_WIDTH |
| other_ticks = total_ticks-(mfu_ticks+mru_ticks) |
| |
| core_form = 'F'*int(mfu_ticks)+'R'*int(mru_ticks)+'O'*int(other_ticks) |
| core_spc = ' '*(GRAPH_WIDTH-(2+len(core_form))) |
| core_line = GRAPH_INDENT+'|'+core_form+core_spc+'|' |
| |
| for line in ('', info_line, graph_line, core_line, graph_line, ''): |
| print(line) |
| |
| |
| def f_bytes(byte_string): |
| """Return human-readable representation of a byte value in |
| powers of 2 (eg "KiB" for "kibibytes", etc) to two decimal |
| points. Values smaller than one KiB are returned without |
| decimal points. Note "bytes" is a reserved keyword. |
| """ |
| |
| prefixes = ([2**80, "YiB"], # yobibytes (yotta) |
| [2**70, "ZiB"], # zebibytes (zetta) |
| [2**60, "EiB"], # exbibytes (exa) |
| [2**50, "PiB"], # pebibytes (peta) |
| [2**40, "TiB"], # tebibytes (tera) |
| [2**30, "GiB"], # gibibytes (giga) |
| [2**20, "MiB"], # mebibytes (mega) |
| [2**10, "KiB"]) # kibibytes (kilo) |
| |
| bites = int(byte_string) |
| |
| if bites >= 2**10: |
| for limit, unit in prefixes: |
| |
| if bites >= limit: |
| value = bites / limit |
| break |
| |
| result = '{0:.1f} {1}'.format(value, unit) |
| else: |
| result = '{0} Bytes'.format(bites) |
| |
| return result |
| |
| |
| def f_hits(hits_string): |
| """Create a human-readable representation of the number of hits. |
| The single-letter symbols used are SI to avoid the confusion caused |
| by the different "short scale" and "long scale" representations in |
| English, which use the same words for different values. See |
| https://en.wikipedia.org/wiki/Names_of_large_numbers and: |
| https://physics.nist.gov/cuu/Units/prefixes.html |
| """ |
| |
| numbers = ([10**24, 'Y'], # yotta (septillion) |
| [10**21, 'Z'], # zetta (sextillion) |
| [10**18, 'E'], # exa (quintrillion) |
| [10**15, 'P'], # peta (quadrillion) |
| [10**12, 'T'], # tera (trillion) |
| [10**9, 'G'], # giga (billion) |
| [10**6, 'M'], # mega (million) |
| [10**3, 'k']) # kilo (thousand) |
| |
| hits = int(hits_string) |
| |
| if hits >= 1000: |
| for limit, symbol in numbers: |
| |
| if hits >= limit: |
| value = hits/limit |
| break |
| |
| result = "%0.1f%s" % (value, symbol) |
| else: |
| result = "%d" % hits |
| |
| return result |
| |
| |
| def f_perc(value1, value2): |
| """Calculate percentage and return in human-readable form. If |
| rounding produces the result '0.0' though the first number is |
| not zero, include a 'less-than' symbol to avoid confusion. |
| Division by zero is handled by returning 'n/a'; no error |
| is called. |
| """ |
| |
| v1 = float(value1) |
| v2 = float(value2) |
| |
| try: |
| perc = 100 * v1/v2 |
| except ZeroDivisionError: |
| result = 'n/a' |
| else: |
| result = '{0:0.1f} %'.format(perc) |
| |
| if result == '0.0 %' and v1 > 0: |
| result = '< 0.1 %' |
| |
| return result |
| |
| |
| def format_raw_line(name, value): |
| """For the --raw option for the tunable and SPL outputs, decide on the |
| correct formatting based on the --alternate flag. |
| """ |
| |
| if ARGS.alt: |
| result = '{0}{1}={2}'.format(INDENT, name, value) |
| else: |
| spc = LINE_LENGTH-(len(INDENT)+len(value)) |
| result = '{0}{1:<{spc}}{2}'.format(INDENT, name, value, spc=spc) |
| |
| return result |
| |
| |
| def get_kstats(): |
| """Collect information on the ZFS subsystem from the /proc Linux virtual |
| file system. The step does not perform any further processing, giving us |
| the option to only work on what is actually needed. The name "kstat" is a |
| holdover from the Solaris utility of the same name. |
| """ |
| |
| result = {} |
| secs = SECTION_PATHS.values() |
| |
| for section in secs: |
| |
| with open(PROC_PATH+section, 'r') as proc_location: |
| lines = [line for line in proc_location] |
| |
| del lines[0:2] # Get rid of header |
| result[section] = lines |
| |
| return result |
| |
| |
| def get_spl_tunables(PATH): |
| """Collect information on the Solaris Porting Layer (SPL) or the |
| tunables, depending on the PATH given. Does not check if PATH is |
| legal. |
| """ |
| |
| result = {} |
| parameters = os.listdir(PATH) |
| |
| for name in parameters: |
| |
| with open(PATH+name, 'r') as para_file: |
| value = para_file.read() |
| result[name] = value.strip() |
| |
| return result |
| |
| |
| def get_descriptions(request): |
| """Get the descriptions of the Solaris Porting Layer (SPL) or the |
| tunables, return with minimal formatting. |
| """ |
| |
| if request not in ('spl', 'zfs'): |
| print('ERROR: description of "{0}" requested)'.format(request)) |
| sys.exit(1) |
| |
| descs = {} |
| target_prefix = 'parm:' |
| |
| # We would prefer to do this with /sys/modules -- see the discussion at |
| # get_version() -- but there isn't a way to get the descriptions from |
| # there, so we fall back on modinfo |
| command = ["/sbin/modinfo", request, "-0"] |
| |
| # The recommended way to do this is with subprocess.run(). However, |
| # some installed versions of Python are < 3.5, so we offer them |
| # the option of doing it the old way (for now) |
| info = '' |
| |
| try: |
| |
| if 'run' in dir(subprocess): |
| info = subprocess.run(command, stdout=subprocess.PIPE, |
| universal_newlines=True) |
| raw_output = info.stdout.split('\0') |
| else: |
| info = subprocess.check_output(command, universal_newlines=True) |
| raw_output = info.split('\0') |
| |
| except subprocess.CalledProcessError: |
| print("Error: Descriptions not available (can't access kernel module)") |
| sys.exit(1) |
| |
| for line in raw_output: |
| |
| if not line.startswith(target_prefix): |
| continue |
| |
| line = line[len(target_prefix):].strip() |
| name, raw_desc = line.split(':', 1) |
| desc = raw_desc.rsplit('(', 1)[0] |
| |
| if desc == '': |
| desc = '(No description found)' |
| |
| descs[name.strip()] = desc.strip() |
| |
| return descs |
| |
| |
| def get_version(request): |
| """Get the version number of ZFS or SPL on this machine for header. |
| Returns an error string, but does not raise an error, if we can't |
| get the ZFS/SPL version via modinfo. |
| """ |
| |
| if request not in ('spl', 'zfs'): |
| error_msg = '(ERROR: "{0}" requested)'.format(request) |
| return error_msg |
| |
| # The original arc_summary called /sbin/modinfo/{spl,zfs} to get |
| # the version information. We switch to /sys/module/{spl,zfs}/version |
| # to make sure we get what is really loaded in the kernel |
| command = ["cat", "/sys/module/{0}/version".format(request)] |
| req = request.upper() |
| version = "(Can't get {0} version)".format(req) |
| |
| # The recommended way to do this is with subprocess.run(). However, |
| # some installed versions of Python are < 3.5, so we offer them |
| # the option of doing it the old way (for now) |
| info = '' |
| if 'run' in dir(subprocess): |
| info = subprocess.run(command, stdout=subprocess.PIPE, |
| universal_newlines=True) |
| version = info.stdout.strip() |
| else: |
| info = subprocess.check_output(command, universal_newlines=True) |
| version = info.strip() |
| |
| return version |
| |
| |
| def print_header(): |
| """Print the initial heading with date and time as well as info on the |
| Linux and ZFS versions. This is not called for the graph. |
| """ |
| |
| # datetime is now recommended over time but we keep the exact formatting |
| # from the older version of arc_summary in case there are scripts |
| # that expect it in this way |
| daydate = time.strftime(DATE_FORMAT) |
| spc_date = LINE_LENGTH-len(daydate) |
| sys_version = os.uname() |
| |
| sys_msg = sys_version.sysname+' '+sys_version.release |
| zfs = get_version('zfs') |
| spc_zfs = LINE_LENGTH-len(zfs) |
| |
| machine_msg = 'Machine: '+sys_version.nodename+' ('+sys_version.machine+')' |
| spl = get_version('spl') |
| spc_spl = LINE_LENGTH-len(spl) |
| |
| print('\n'+('-'*LINE_LENGTH)) |
| print('{0:<{spc}}{1}'.format(TITLE, daydate, spc=spc_date)) |
| print('{0:<{spc}}{1}'.format(sys_msg, zfs, spc=spc_zfs)) |
| print('{0:<{spc}}{1}\n'.format(machine_msg, spl, spc=spc_spl)) |
| |
| |
| def print_raw(kstats_dict): |
| """Print all available data from the system in a minimally sorted format. |
| This can be used as a source to be piped through 'grep'. |
| """ |
| |
| sections = sorted(kstats_dict.keys()) |
| |
| for section in sections: |
| |
| print('\n{0}:'.format(section.upper())) |
| lines = sorted(kstats_dict[section]) |
| |
| for line in lines: |
| name, value = cleanup_line(line) |
| print(format_raw_line(name, value)) |
| |
| # Tunables and SPL must be handled separately because they come from a |
| # different source and have descriptions the user might request |
| print() |
| section_spl() |
| section_tunables() |
| |
| |
| def isolate_section(section_name, kstats_dict): |
| """From the complete information on all sections, retrieve only those |
| for one section. |
| """ |
| |
| try: |
| section_data = kstats_dict[section_name] |
| except KeyError: |
| print('ERROR: Data on {0} not available'.format(section_data)) |
| sys.exit(1) |
| |
| section_dict = dict(cleanup_line(l) for l in section_data) |
| |
| return section_dict |
| |
| |
| # Formatted output helper functions |
| |
| |
| def prt_1(text, value): |
| """Print text and one value, no indent""" |
| spc = ' '*(LINE_LENGTH-(len(text)+len(value))) |
| print('{0}{spc}{1}'.format(text, value, spc=spc)) |
| |
| |
| def prt_i1(text, value): |
| """Print text and one value, with indent""" |
| spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(value))) |
| print(INDENT+'{0}{spc}{1}'.format(text, value, spc=spc)) |
| |
| |
| def prt_2(text, value1, value2): |
| """Print text and two values, no indent""" |
| values = '{0:>9} {1:>9}'.format(value1, value2) |
| spc = ' '*(LINE_LENGTH-(len(text)+len(values)+2)) |
| print('{0}{spc} {1}'.format(text, values, spc=spc)) |
| |
| |
| def prt_i2(text, value1, value2): |
| """Print text and two values, with indent""" |
| values = '{0:>9} {1:>9}'.format(value1, value2) |
| spc = ' '*(LINE_LENGTH-(len(INDENT)+len(text)+len(values)+2)) |
| print(INDENT+'{0}{spc} {1}'.format(text, values, spc=spc)) |
| |
| |
| # The section output concentrates on important parameters instead of |
| # being exhaustive (that is what the --raw parameter is for) |
| |
| |
| def section_arc(kstats_dict): |
| """Give basic information on the ARC, MRU and MFU. This is the first |
| and most used section. |
| """ |
| |
| arc_stats = isolate_section('arcstats', kstats_dict) |
| |
| throttle = arc_stats['memory_throttle_count'] |
| |
| if throttle == '0': |
| health = 'HEALTHY' |
| else: |
| health = 'THROTTLED' |
| |
| prt_1('ARC status:', health) |
| prt_i1('Memory throttle count:', throttle) |
| print() |
| |
| arc_size = arc_stats['size'] |
| arc_target_size = arc_stats['c'] |
| arc_max = arc_stats['c_max'] |
| arc_min = arc_stats['c_min'] |
| mfu_size = arc_stats['mfu_size'] |
| mru_size = arc_stats['mru_size'] |
| meta_limit = arc_stats['arc_meta_limit'] |
| meta_size = arc_stats['arc_meta_used'] |
| dnode_limit = arc_stats['arc_dnode_limit'] |
| dnode_size = arc_stats['dnode_size'] |
| target_size_ratio = '{0}:1'.format(int(arc_max) // int(arc_min)) |
| |
| prt_2('ARC size (current):', |
| f_perc(arc_size, arc_max), f_bytes(arc_size)) |
| prt_i2('Target size (adaptive):', |
| f_perc(arc_target_size, arc_max), f_bytes(arc_target_size)) |
| prt_i2('Min size (hard limit):', |
| f_perc(arc_min, arc_max), f_bytes(arc_min)) |
| prt_i2('Max size (high water):', |
| target_size_ratio, f_bytes(arc_max)) |
| caches_size = int(mfu_size)+int(mru_size) |
| prt_i2('Most Frequently Used (MFU) cache size:', |
| f_perc(mfu_size, caches_size), f_bytes(mfu_size)) |
| prt_i2('Most Recently Used (MRU) cache size:', |
| f_perc(mru_size, caches_size), f_bytes(mru_size)) |
| prt_i2('Metadata cache size (hard limit):', |
| f_perc(meta_limit, arc_max), f_bytes(meta_limit)) |
| prt_i2('Metadata cache size (current):', |
| f_perc(meta_size, meta_limit), f_bytes(meta_size)) |
| prt_i2('Dnode cache size (hard limit):', |
| f_perc(dnode_limit, meta_limit), f_bytes(dnode_limit)) |
| prt_i2('Dnode cache size (current):', |
| f_perc(dnode_size, dnode_limit), f_bytes(dnode_size)) |
| print() |
| |
| print('ARC hash breakdown:') |
| prt_i1('Elements max:', f_hits(arc_stats['hash_elements_max'])) |
| prt_i2('Elements current:', |
| f_perc(arc_stats['hash_elements'], arc_stats['hash_elements_max']), |
| f_hits(arc_stats['hash_elements'])) |
| prt_i1('Collisions:', f_hits(arc_stats['hash_collisions'])) |
| |
| prt_i1('Chain max:', f_hits(arc_stats['hash_chain_max'])) |
| prt_i1('Chains:', f_hits(arc_stats['hash_chains'])) |
| print() |
| |
| print('ARC misc:') |
| prt_i1('Deleted:', f_hits(arc_stats['deleted'])) |
| prt_i1('Mutex misses:', f_hits(arc_stats['mutex_miss'])) |
| prt_i1('Eviction skips:', f_hits(arc_stats['evict_skip'])) |
| print() |
| |
| |
| def section_archits(kstats_dict): |
| """Print information on how the caches are accessed ("arc hits"). |
| """ |
| |
| arc_stats = isolate_section('arcstats', kstats_dict) |
| all_accesses = int(arc_stats['hits'])+int(arc_stats['misses']) |
| actual_hits = int(arc_stats['mfu_hits'])+int(arc_stats['mru_hits']) |
| |
| prt_1('ARC total accesses (hits + misses):', f_hits(all_accesses)) |
| ta_todo = (('Cache hit ratio:', arc_stats['hits']), |
| ('Cache miss ratio:', arc_stats['misses']), |
| ('Actual hit ratio (MFU + MRU hits):', actual_hits)) |
| |
| for title, value in ta_todo: |
| prt_i2(title, f_perc(value, all_accesses), f_hits(value)) |
| |
| dd_total = int(arc_stats['demand_data_hits']) +\ |
| int(arc_stats['demand_data_misses']) |
| prt_i2('Data demand efficiency:', |
| f_perc(arc_stats['demand_data_hits'], dd_total), |
| f_hits(dd_total)) |
| |
| dp_total = int(arc_stats['prefetch_data_hits']) +\ |
| int(arc_stats['prefetch_data_misses']) |
| prt_i2('Data prefetch efficiency:', |
| f_perc(arc_stats['prefetch_data_hits'], dp_total), |
| f_hits(dp_total)) |
| |
| known_hits = int(arc_stats['mfu_hits']) +\ |
| int(arc_stats['mru_hits']) +\ |
| int(arc_stats['mfu_ghost_hits']) +\ |
| int(arc_stats['mru_ghost_hits']) |
| |
| anon_hits = int(arc_stats['hits'])-known_hits |
| |
| print() |
| print('Cache hits by cache type:') |
| cl_todo = (('Most frequently used (MFU):', arc_stats['mfu_hits']), |
| ('Most recently used (MRU):', arc_stats['mru_hits']), |
| ('Most frequently used (MFU) ghost:', |
| arc_stats['mfu_ghost_hits']), |
| ('Most recently used (MRU) ghost:', |
| arc_stats['mru_ghost_hits'])) |
| |
| for title, value in cl_todo: |
| prt_i2(title, f_perc(value, arc_stats['hits']), f_hits(value)) |
| |
| # For some reason, anon_hits can turn negative, which is weird. Until we |
| # have figured out why this happens, we just hide the problem, following |
| # the behavior of the original arc_summary. |
| if anon_hits >= 0: |
| prt_i2('Anonymously used:', |
| f_perc(anon_hits, arc_stats['hits']), f_hits(anon_hits)) |
| |
| print() |
| print('Cache hits by data type:') |
| dt_todo = (('Demand data:', arc_stats['demand_data_hits']), |
| ('Demand prefetch data:', arc_stats['prefetch_data_hits']), |
| ('Demand metadata:', arc_stats['demand_metadata_hits']), |
| ('Demand prefetch metadata:', |
| arc_stats['prefetch_metadata_hits'])) |
| |
| for title, value in dt_todo: |
| prt_i2(title, f_perc(value, arc_stats['hits']), f_hits(value)) |
| |
| print() |
| print('Cache misses by data type:') |
| dm_todo = (('Demand data:', arc_stats['demand_data_misses']), |
| ('Demand prefetch data:', |
| arc_stats['prefetch_data_misses']), |
| ('Demand metadata:', arc_stats['demand_metadata_misses']), |
| ('Demand prefetch metadata:', |
| arc_stats['prefetch_metadata_misses'])) |
| |
| for title, value in dm_todo: |
| prt_i2(title, f_perc(value, arc_stats['misses']), f_hits(value)) |
| |
| print() |
| |
| |
| def section_dmu(kstats_dict): |
| """Collect information on the DMU""" |
| |
| zfetch_stats = isolate_section('zfetchstats', kstats_dict) |
| |
| zfetch_access_total = int(zfetch_stats['hits'])+int(zfetch_stats['misses']) |
| |
| prt_1('DMU prefetch efficiency:', f_hits(zfetch_access_total)) |
| prt_i2('Hit ratio:', f_perc(zfetch_stats['hits'], zfetch_access_total), |
| f_hits(zfetch_stats['hits'])) |
| prt_i2('Miss ratio:', f_perc(zfetch_stats['misses'], zfetch_access_total), |
| f_hits(zfetch_stats['misses'])) |
| print() |
| |
| |
| def section_l2arc(kstats_dict): |
| """Collect information on L2ARC device if present. If not, tell user |
| that we're skipping the section. |
| """ |
| |
| # The L2ARC statistics live in the same section as the normal ARC stuff |
| arc_stats = isolate_section('arcstats', kstats_dict) |
| |
| if arc_stats['l2_size'] == '0': |
| print('L2ARC not detected, skipping section\n') |
| return |
| |
| l2_errors = int(arc_stats['l2_writes_error']) +\ |
| int(arc_stats['l2_cksum_bad']) +\ |
| int(arc_stats['l2_io_error']) |
| |
| l2_access_total = int(arc_stats['l2_hits'])+int(arc_stats['l2_misses']) |
| health = 'HEALTHY' |
| |
| if l2_errors > 0: |
| health = 'DEGRADED' |
| |
| prt_1('L2ARC status:', health) |
| |
| l2_todo = (('Low memory aborts:', 'l2_abort_lowmem'), |
| ('Free on write:', 'l2_free_on_write'), |
| ('R/W clashes:', 'l2_rw_clash'), |
| ('Bad checksums:', 'l2_cksum_bad'), |
| ('I/O errors:', 'l2_io_error')) |
| |
| for title, value in l2_todo: |
| prt_i1(title, f_hits(arc_stats[value])) |
| |
| print() |
| prt_1('L2ARC size (adaptive):', f_bytes(arc_stats['l2_size'])) |
| prt_i2('Compressed:', f_perc(arc_stats['l2_asize'], arc_stats['l2_size']), |
| f_bytes(arc_stats['l2_asize'])) |
| prt_i2('Header size:', |
| f_perc(arc_stats['l2_hdr_size'], arc_stats['l2_size']), |
| f_bytes(arc_stats['l2_hdr_size'])) |
| |
| print() |
| prt_1('L2ARC breakdown:', f_hits(l2_access_total)) |
| prt_i2('Hit ratio:', |
| f_perc(arc_stats['l2_hits'], l2_access_total), |
| f_hits(arc_stats['l2_hits'])) |
| prt_i2('Miss ratio:', |
| f_perc(arc_stats['l2_misses'], l2_access_total), |
| f_hits(arc_stats['l2_misses'])) |
| prt_i1('Feeds:', f_hits(arc_stats['l2_feeds'])) |
| |
| print() |
| print('L2ARC writes:') |
| |
| if arc_stats['l2_writes_done'] != arc_stats['l2_writes_sent']: |
| prt_i2('Writes sent:', 'FAULTED', f_hits(arc_stats['l2_writes_sent'])) |
| prt_i2('Done ratio:', |
| f_perc(arc_stats['l2_writes_done'], |
| arc_stats['l2_writes_sent']), |
| f_bytes(arc_stats['l2_writes_done'])) |
| prt_i2('Error ratio:', |
| f_perc(arc_stats['l2_writes_error'], |
| arc_stats['l2_writes_sent']), |
| f_bytes(arc_stats['l2_writes_error'])) |
| else: |
| prt_i2('Writes sent:', '100 %', f_bytes(arc_stats['l2_writes_sent'])) |
| |
| print() |
| print('L2ARC evicts:') |
| prt_i1('Lock retries:', f_hits(arc_stats['l2_evict_lock_retry'])) |
| prt_i1('Upon reading:', f_hits(arc_stats['l2_evict_reading'])) |
| print() |
| |
| |
| def section_spl(*_): |
| """Print the SPL parameters, if requested with alternative format |
| and/or descriptions. This does not use kstats. |
| """ |
| |
| spls = get_spl_tunables(SPL_PATH) |
| keylist = sorted(spls.keys()) |
| print('Solaris Porting Layer (SPL):') |
| |
| if ARGS.desc: |
| descriptions = get_descriptions('spl') |
| |
| for key in keylist: |
| value = spls[key] |
| |
| if ARGS.desc: |
| try: |
| print(INDENT+'#', descriptions[key]) |
| except KeyError: |
| print(INDENT+'# (No description found)') # paranoid |
| |
| print(format_raw_line(key, value)) |
| |
| print() |
| |
| |
| def section_tunables(*_): |
| """Print the tunables, if requested with alternative format and/or |
| descriptions. This does not use kstasts. |
| """ |
| |
| tunables = get_spl_tunables(TUNABLES_PATH) |
| keylist = sorted(tunables.keys()) |
| print('Tunables:') |
| |
| if ARGS.desc: |
| descriptions = get_descriptions('zfs') |
| |
| for key in keylist: |
| value = tunables[key] |
| |
| if ARGS.desc: |
| try: |
| print(INDENT+'#', descriptions[key]) |
| except KeyError: |
| print(INDENT+'# (No description found)') # paranoid |
| |
| print(format_raw_line(key, value)) |
| |
| print() |
| |
| |
| def section_vdev(kstats_dict): |
| """Collect information on VDEV caches""" |
| |
| # Currently [Nov 2017] the VDEV cache is disabled, because it is actually |
| # harmful. When this is the case, we just skip the whole entry. See |
| # https://github.com/zfsonlinux/zfs/blob/master/module/zfs/vdev_cache.c |
| # for details |
| tunables = get_spl_tunables(TUNABLES_PATH) |
| |
| if tunables['zfs_vdev_cache_size'] == '0': |
| print('VDEV cache disabled, skipping section\n') |
| return |
| |
| vdev_stats = isolate_section('vdev_cache_stats', kstats_dict) |
| |
| vdev_cache_total = int(vdev_stats['hits']) +\ |
| int(vdev_stats['misses']) +\ |
| int(vdev_stats['delegations']) |
| |
| prt_1('VDEV cache summary:', f_hits(vdev_cache_total)) |
| prt_i2('Hit ratio:', f_perc(vdev_stats['hits'], vdev_cache_total), |
| f_hits(vdev_stats['hits'])) |
| prt_i2('Miss ratio:', f_perc(vdev_stats['misses'], vdev_cache_total), |
| f_hits(vdev_stats['misses'])) |
| prt_i2('Delegations:', f_perc(vdev_stats['delegations'], vdev_cache_total), |
| f_hits(vdev_stats['delegations'])) |
| print() |
| |
| |
| def section_zil(kstats_dict): |
| """Collect information on the ZFS Intent Log. Some of the information |
| taken from https://github.com/zfsonlinux/zfs/blob/master/include/sys/zil.h |
| """ |
| |
| zil_stats = isolate_section('zil', kstats_dict) |
| |
| prt_1('ZIL committed transactions:', |
| f_hits(zil_stats['zil_itx_count'])) |
| prt_i1('Commit requests:', f_hits(zil_stats['zil_commit_count'])) |
| prt_i1('Flushes to stable storage:', |
| f_hits(zil_stats['zil_commit_writer_count'])) |
| prt_i2('Transactions to SLOG storage pool:', |
| f_bytes(zil_stats['zil_itx_metaslab_slog_bytes']), |
| f_hits(zil_stats['zil_itx_metaslab_slog_count'])) |
| prt_i2('Transactions to non-SLOG storage pool:', |
| f_bytes(zil_stats['zil_itx_metaslab_normal_bytes']), |
| f_hits(zil_stats['zil_itx_metaslab_normal_count'])) |
| print() |
| |
| |
| section_calls = {'arc': section_arc, |
| 'archits': section_archits, |
| 'dmu': section_dmu, |
| 'l2arc': section_l2arc, |
| 'spl': section_spl, |
| 'tunables': section_tunables, |
| 'vdev': section_vdev, |
| 'zil': section_zil} |
| |
| |
| def main(): |
| """Run program. The options to draw a graph and to print all data raw are |
| treated separately because they come with their own call. |
| """ |
| |
| kstats = get_kstats() |
| |
| if ARGS.graph: |
| draw_graph(kstats) |
| sys.exit(0) |
| |
| print_header() |
| |
| if ARGS.raw: |
| print_raw(kstats) |
| |
| elif ARGS.section: |
| |
| try: |
| section_calls[ARGS.section](kstats) |
| except KeyError: |
| print('Error: Section "{0}" unknown'.format(ARGS.section)) |
| sys.exit(1) |
| |
| elif ARGS.page: |
| print('WARNING: Pages are deprecated, please use "--section"\n') |
| |
| pages_to_calls = {1: 'arc', |
| 2: 'archits', |
| 3: 'l2arc', |
| 4: 'dmu', |
| 5: 'vdev', |
| 6: 'tunables'} |
| |
| try: |
| call = pages_to_calls[ARGS.page] |
| except KeyError: |
| print('Error: Page "{0}" not supported'.format(ARGS.page)) |
| sys.exit(1) |
| else: |
| section_calls[call](kstats) |
| |
| else: |
| # If no parameters were given, we print all sections. We might want to |
| # change the sequence by hand |
| calls = sorted(section_calls.keys()) |
| |
| for section in calls: |
| section_calls[section](kstats) |
| |
| sys.exit(0) |
| |
| |
| if __name__ == '__main__': |
| main() |