mirror of
				https://git.proxmox.com/git/mirror_zfs.git
				synced 2025-10-26 18:05:04 +03:00 
			
		
		
		
	 2ce90dca91
			
		
	
	
		2ce90dca91
		
			
		
	
	
	
	
		
			
			Linux uses modinfo to get tunables descriptions, FreeBSD has to use sysctl. Move the existing function definition so it is defined that way on Linux, and add a definition in terms of sysctl for FreeBSD. Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov> Signed-off-by: Ryan Moeller <ryan@iXsystems.com> Closes #10062
		
			
				
	
	
		
			944 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			944 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/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
 | |
| 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()
 | |
| 
 | |
| 
 | |
| if sys.platform.startswith('freebsd'):
 | |
|     # Requires py36-sysctl on FreeBSD
 | |
|     import sysctl
 | |
| 
 | |
|     VDEV_CACHE_SIZE = 'vdev.cache_size'
 | |
| 
 | |
|     def load_kstats(section):
 | |
|         base = 'kstat.zfs.misc.{section}.'.format(section=section)
 | |
|         # base is removed from the name
 | |
|         fmt = lambda kstat: '{name} : {value}'.format(name=kstat.name[len(base):],
 | |
|                                                       value=kstat.value)
 | |
|         return [fmt(kstat) for kstat in sysctl.filter(base)]
 | |
| 
 | |
|     def get_params(base):
 | |
|         cut = 8 # = len('vfs.zfs.')
 | |
|         return {ctl.name[cut:]: str(ctl.value) for ctl in sysctl.filter(base)}
 | |
| 
 | |
|     def get_tunable_params():
 | |
|         return get_params('vfs.zfs')
 | |
| 
 | |
|     def get_vdev_params():
 | |
|         return get_params('vfs.zfs.vdev')
 | |
| 
 | |
|     def get_version_impl(request):
 | |
|         # FreeBSD reports versions for zpl and spa instead of zfs and spl.
 | |
|         name = {'zfs': 'zpl',
 | |
|                 'spl': 'spa'}[request]
 | |
|         mib = 'vfs.zfs.version.{}'.format(name)
 | |
|         version = sysctl.filter(mib)[0].value
 | |
|         return '{} version {}'.format(name, version)
 | |
| 
 | |
|     def get_descriptions(_request):
 | |
|         # py-sysctl doesn't give descriptions, so we have to shell out.
 | |
|         command = ['sysctl', '-d', 'vfs.zfs']
 | |
| 
 | |
|         # 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)
 | |
|         if 'run' in dir(subprocess):
 | |
|             info = subprocess.run(command, stdout=subprocess.PIPE,
 | |
|                                   universal_newlines=True)
 | |
|             lines = info.stdout.split('\n')
 | |
|         else:
 | |
|             info = subprocess.check_output(command, universal_newlines=True)
 | |
|             lines = info.split('\n')
 | |
| 
 | |
|         def fmt(line):
 | |
|             name, desc = line.split(':', 1)
 | |
|             return (name.strip(), desc.strip())
 | |
| 
 | |
|         return dict([fmt(line) for line in lines if len(line) > 0])
 | |
| 
 | |
| 
 | |
| elif sys.platform.startswith('linux'):
 | |
|     KSTAT_PATH = '/proc/spl/kstat/zfs'
 | |
|     SPL_PATH = '/sys/module/spl/parameters'
 | |
|     TUNABLES_PATH = '/sys/module/zfs/parameters'
 | |
| 
 | |
|     VDEV_CACHE_SIZE = 'zfs_vdev_cache_size'
 | |
| 
 | |
|     def load_kstats(section):
 | |
|         path = os.path.join(KSTAT_PATH, section)
 | |
|         with open(path) as f:
 | |
|             return list(f)[2:] # Get rid of header
 | |
| 
 | |
|     def get_params(basepath):
 | |
|         """Collect information on the Solaris Porting Layer (SPL) or the
 | |
|         tunables, depending on the PATH given. Does not check if PATH is
 | |
|         legal.
 | |
|         """
 | |
|         result = {}
 | |
|         for name in os.listdir(basepath):
 | |
|             path = os.path.join(basepath, name)
 | |
|             with open(path) as f:
 | |
|                 value = f.read()
 | |
|                 result[name] = value.strip()
 | |
|         return result
 | |
| 
 | |
|     def get_spl_params():
 | |
|         return get_params(SPL_PATH)
 | |
| 
 | |
|     def get_tunable_params():
 | |
|         return get_params(TUNABLES_PATH)
 | |
| 
 | |
|     def get_vdev_params():
 | |
|         return get_params(TUNABLES_PATH)
 | |
| 
 | |
|     def get_version_impl(request):
 | |
|         # 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()
 | |
| 
 | |
|         # 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)
 | |
|         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 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 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. 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 = {}
 | |
| 
 | |
|     for section in SECTION_PATHS.values():
 | |
|         if section not in result:
 | |
|             result[section] = load_kstats(section)
 | |
| 
 | |
|     return result
 | |
| 
 | |
| 
 | |
| 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.
 | |
|     """
 | |
| 
 | |
|     if request not in ('spl', 'zfs'):
 | |
|         error_msg = '(ERROR: "{0}" requested)'.format(request)
 | |
|         return error_msg
 | |
| 
 | |
|     return get_version_impl(request)
 | |
| 
 | |
| 
 | |
| def print_header():
 | |
|     """Print the initial heading with date and time as well as info on the
 | |
|     kernel 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.
 | |
|     """
 | |
| 
 | |
|     if sys.platform.startswith('freebsd'):
 | |
|         # No SPL support in FreeBSD
 | |
|         return
 | |
| 
 | |
|     spls = get_spl_params()
 | |
|     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_tunable_params()
 | |
|     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_vdev_params()
 | |
| 
 | |
|     if tunables[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()
 |