-
-
Save damoxc/6267899 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
# | |
# Dumb script to dump (some) of bcache status | |
# Copyright 2013 Darrick J. Wong. All rights reserved. | |
# | |
# This file is part of Bcache. Bcache is free software: you can | |
# redistribute it and/or modify it under the terms of the GNU General Public | |
# License as published by the Free Software Foundation, version 2. | |
# | |
# This program is distributed in the hope that it will be useful, but WITHOUT | |
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more | |
# details. | |
# | |
# You should have received a copy of the GNU General Public License along with | |
# this program; if not, write to the Free Software Foundation, Inc., 51 | |
# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
# | |
import os | |
import sys | |
import argparse | |
MAX_KEY_LENGTH = 28 | |
DEV_BLOCK_PATH = '/dev/block/' | |
SYSFS_BCACHE_PATH = '/sys/fs/bcache/' | |
SYSFS_BLOCK_PATH = '/sys/block/' | |
def file_to_lines(fname): | |
try: | |
with open(fname, "r") as fd: | |
return fd.readlines() | |
except: | |
return [] | |
def file_to_line(fname): | |
ret = file_to_lines(fname) | |
if ret: | |
return ret[0].strip() | |
return '' | |
def str_to_bool(x): | |
return x == '1' | |
def format_sectors(x): | |
'''Pretty print a sector count.''' | |
sectors = int(x) | |
asectors = abs(sectors) | |
if asectors == 0: | |
return '0B' | |
elif asectors < 2048: | |
return '%.2fKiB' % (sectors / 2) | |
elif asectors < 2097152: | |
return '%.2fMiB' % (sectors / 2048) | |
elif asectors < 2147483648: | |
return '%.2fGiB' % (sectors / 2097152) | |
else: | |
return '%.2fTiB' % (sectors / 2147483648) | |
def interpret_sectors(x): | |
'''Interpret a pretty-printed disk size.''' | |
factors = { | |
'k': 1 << 10, | |
'M': 1 << 20, | |
'G': 1 << 30, | |
'T': 1 << 40, | |
'P': 1 << 50, | |
'E': 1 << 60, | |
'Z': 1 << 70, | |
'Y': 1 << 80, | |
} | |
factor = 1 | |
if x[-1] in factors: | |
factor = factors[x[-1]] | |
x = x[:-1] | |
return int(float(x) * factor / 512) | |
def pretty_size(x): | |
return format_sectors(interpret_sectors(x)) | |
def dump_bdev(bdev_path): | |
'''Dump a backing device stats.''' | |
global MAX_KEY_LENGTH, devnum_map | |
attrs = [ | |
('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)), | |
('../size', 'Size', format_sectors), | |
('cache_mode', 'Cache Mode', None), | |
('readahead', 'Readahead', None), | |
('sequential_cutoff', 'Sequential Cutoff', pretty_size), | |
('sequential_merge', 'Merge sequential?', str_to_bool), | |
('state', 'State', None), | |
('writeback_running', 'Writeback?', str_to_bool), | |
('dirty_data', 'Dirty Data', pretty_size), | |
] | |
print('--- Backing Device ---') | |
for (sysfs_name, display_name, conversion_func) in attrs: | |
val = file_to_line('%s/%s' % (bdev_path, sysfs_name)) | |
if conversion_func is not None: | |
val = conversion_func(val) | |
if display_name is None: | |
display_name = sysfs_name | |
print(' %-*s%s' % (MAX_KEY_LENGTH - 2, display_name, val)) | |
def dump_cachedev(cachedev_path): | |
'''Dump a cachding device stats.''' | |
def fmt_cachesize(val): | |
return '%s\t(%d%%)' % (format_sectors(val), float(val) / cache_size * 100) | |
global MAX_KEY_LENGTH, devnum_map | |
attrs = [ | |
('../dev', 'Device', lambda x: '%s (%s)' % (devnum_map.get(x, '?'), x)), | |
('../size', 'Size', format_sectors), | |
('block_size', 'Block Size', pretty_size), | |
('bucket_size', 'Bucket Size', pretty_size), | |
('cache_replacement_policy', 'Replacement Policy', None), | |
('discard', 'Discard?', str_to_bool), | |
('io_errors', 'I/O Errors', None), | |
('metadata_written', 'Metadata Written', pretty_size), | |
('written', 'Data Written', pretty_size), | |
('nbuckets', 'Buckets', None), | |
(None, 'Cache Used', lambda x: fmt_cachesize(used_sectors)), | |
(None, 'Cache Unused', lambda x: fmt_cachesize(unused_sectors)), | |
] | |
stats = get_cache_priority_stats(cachedev_path) | |
cache_size = int(file_to_line('%s/../size' % cachedev_path)) | |
unused_sectors = float(stats['Unused'][:-1]) * cache_size / 100 | |
used_sectors = cache_size - unused_sectors | |
print('--- Cache Device ---') | |
for (sysfs_name, display_name, conversion_func) in attrs: | |
if sysfs_name is not None: | |
val = file_to_line('%s/%s' % (cachedev_path, sysfs_name)) | |
if conversion_func is not None: | |
val = conversion_func(val) | |
if display_name is None: | |
display_name = sysfs_name | |
print(' %-*s%s' % (MAX_KEY_LENGTH - 2, display_name, val)) | |
def hits_to_str(hits_str, misses_str): | |
'''Render a hits/misses ratio as a string.''' | |
hits = int(hits_str) | |
misses = int(misses_str) | |
ret = '%d' % hits | |
if hits + misses != 0: | |
ret = '%s\t(%.d%%)' % (ret, 100 * hits / (hits + misses)) | |
return ret | |
def dump_stats(sysfs_path, indent_str, stats): | |
'''Dump stats on a bcache device.''' | |
stat_types = [ | |
('five_minute', 'Last 5min'), | |
('hour', 'Last Hour'), | |
('day', 'Last Day'), | |
('total', 'Total'), | |
] | |
attrs = ['bypassed', 'cache_bypass_hits', 'cache_bypass_misses', 'cache_hits', 'cache_misses'] | |
display = [ | |
('Hits', lambda: hits_to_str(stat_data['cache_hits'], stat_data['cache_misses'])), | |
('Misses', lambda: stat_data['cache_misses']), | |
('Bypass Hits', lambda: hits_to_str(stat_data['cache_bypass_hits'], stat_data['cache_bypass_misses'])), | |
('Bypass Misses', lambda: stat_data['cache_bypass_misses']), | |
('Bypassed', lambda: pretty_size(stat_data['bypassed'])), | |
] | |
for (sysfs_name, stat_display_name) in stat_types: | |
if len(stats) > 0 and sysfs_name not in stats: | |
continue | |
stat_data = {} | |
for attr in attrs: | |
val = file_to_line('%s/stats_%s/%s' % (sysfs_path, sysfs_name, attr)) | |
stat_data[attr] = val | |
for (display_name, str_func) in display: | |
d = '%s%s %s' % (indent_str, stat_display_name, display_name) | |
print('%-*s%s' % (MAX_KEY_LENGTH, d, str_func())) | |
def get_cache_priority_stats(cache): | |
'''Retrieve priority stats from a cache.''' | |
attrs = {} | |
for line in file_to_lines('%s/priority_stats' % cache): | |
x = line.split() | |
key = x[0] | |
value = x[1] | |
attrs[key[:-1]] = value | |
return attrs | |
def dump_bcache(bcache_sysfs_path, stats, print_subdevices, device): | |
'''Dump bcache stats''' | |
global devnum_map | |
def fmt_cachesize(val): | |
return '%s\t(%d%%)' % (format_sectors(val), 100.0 * val / cache_sectors) | |
attrs = [ | |
(None, 'Device', lambda x: '%s (%s)' % (devnum_map.get(device, '?'), device)), | |
(None, 'UUID', lambda x: os.path.basename(bcache_sysfs_path)), | |
('block_size', 'Block Size', pretty_size), | |
('bucket_size', 'Bucket Size', pretty_size), | |
('congested', 'Congested?', str_to_bool), | |
('congested_read_threshold_us', 'Read Congestion', lambda x: '%.1fms' % (int(x) / 1000)), | |
('congested_write_threshold_us', 'Write Congestion', lambda x: '%.1fms' % (int(x) / 1000)), | |
(None, 'Total Cache Size', lambda x: format_sectors(cache_sectors)), | |
(None, 'Total Cache Used', lambda x: fmt_cachesize(cache_used_sectors)), | |
(None, 'Total Cache Unused', lambda x: fmt_cachesize(cache_unused_sectors)), | |
('dirty_data', 'Dirty Data', lambda x: fmt_cachesize(interpret_sectors(x))), | |
('cache_available_percent', 'Evictable Cache', lambda x: '%s\t(%s%%)' % (format_sectors(float(x) * cache_sectors / 100), x)), | |
(None, 'Replacement Policy', lambda x: replacement_policies.pop() if len(replacement_policies) == 1 else '(Unknown)'), | |
(None, 'Cache Mode', lambda x: cache_modes.pop() if len(cache_modes) == 1 else '(Unknown)'), | |
] | |
# Calculate aggregate data | |
cache_sectors = 0 | |
cache_unused_sectors = 0 | |
cache_modes = set() | |
replacement_policies = set() | |
for obj in os.listdir(bcache_sysfs_path): | |
if not os.path.isdir('%s/%s' % (bcache_sysfs_path, obj)): | |
continue | |
if obj.startswith('cache'): | |
cache_size = int(file_to_line('%s/%s/../size' % (bcache_sysfs_path, obj))) | |
cache_sectors += cache_size | |
cstats = get_cache_priority_stats('%s/%s' % (bcache_sysfs_path, obj)) | |
unused_size = float(cstats['Unused'][:-1]) * cache_size / 100 | |
cache_unused_sectors += unused_size | |
replacement_policies.add(file_to_line('%s/%s/cache_replacement_policy' % (bcache_sysfs_path, obj))) | |
elif obj.startswith('bdev'): | |
cache_modes.add(file_to_line('%s/%s/cache_mode' % (bcache_sysfs_path, obj))) | |
cache_used_sectors = cache_sectors - cache_unused_sectors | |
# Dump basic stats | |
print("--- bcache ---") | |
for (sysfs_name, display_name, conversion_func) in attrs: | |
if sysfs_name is not None: | |
val = file_to_line('%s/%s' % (bcache_sysfs_path, sysfs_name)) | |
else: | |
val = None | |
if conversion_func is not None: | |
val = conversion_func(val) | |
if display_name is None: | |
display_name = sysfs_name | |
print('%-*s%s' % (MAX_KEY_LENGTH, display_name, val)) | |
dump_stats(bcache_sysfs_path, '', stats) | |
# Dump sub-device stats | |
if not print_subdevices: | |
return | |
for obj in os.listdir(bcache_sysfs_path): | |
if not os.path.isdir('%s/%s' % (bcache_sysfs_path, obj)): | |
continue | |
if obj.startswith('bdev'): | |
dump_bdev('%s/%s' % (bcache_sysfs_path, obj)) | |
dump_stats('%s/%s' % (bcache_sysfs_path, obj), ' ', stats) | |
elif obj.startswith('cache'): | |
dump_cachedev('%s/%s' % (bcache_sysfs_path, obj)) | |
def map_uuid_to_device(): | |
'''Map bcache UUIDs to device files.''' | |
global SYSFS_BLOCK_PATH | |
ret = {} | |
for bdev in os.listdir(SYSFS_BLOCK_PATH): | |
link = '%s%s/bcache/cache' % (SYSFS_BLOCK_PATH, bdev) | |
if not os.path.islink(link): | |
continue | |
basename = os.path.basename(os.readlink(link)) | |
ret[basename] = file_to_line('%s%s/dev' % (SYSFS_BLOCK_PATH, bdev)) | |
return ret | |
def map_devnum_to_device(): | |
'''Map device numbers to device files.''' | |
global DEV_BLOCK_PATH | |
ret = {} | |
for bdev in os.listdir(DEV_BLOCK_PATH): | |
ret[bdev] = os.path.realpath('%s%s' % (DEV_BLOCK_PATH, bdev)) | |
return ret | |
def main(): | |
'''Main function''' | |
global SYSFS_BCACHE_PATH | |
global uuid_map, devnum_map | |
stats = set() | |
reset_stats = False | |
print_subdevices = False | |
run_gc = False | |
parser = argparse.ArgumentParser(add_help=False) | |
parser.add_argument('--help', help='show this help message and exit', action='store_true') | |
parser.add_argument('-f', '--five-minute', help='Print the last five minutes of stats.', action='store_true') | |
parser.add_argument('-h', '--hour', help='Print the last hour of stats.', action='store_true') | |
parser.add_argument('-d', '--day', help='Print the last day of stats.', action='store_true') | |
parser.add_argument('-t', '--total', help='Print total stats.', action='store_true') | |
parser.add_argument('-a', '--all', help='Print all stats.', action='store_true') | |
parser.add_argument('-r', '--reset-stats', help='Reset stats after printing them.', action='store_true') | |
parser.add_argument('-s', '--sub-status', help='Print subdevice status.', action='store_true') | |
parser.add_argument('-g', '--gc', help='Invoke GC before printing status.', action='store_true') | |
args = parser.parse_args() | |
if args.help: | |
parser.print_help() | |
return 0 | |
if args.five_minute: | |
stats.add('five_minute') | |
if args.hour: | |
stats.add('hour') | |
if args.day: | |
stats.add('day') | |
if args.total: | |
stats.add('total') | |
if args.all: | |
stats.add('five_minute') | |
stats.add('hour') | |
stats.add('day') | |
stats.add('total') | |
if args.reset_stats: | |
reset_stats = True | |
if args.sub_status: | |
print_subdevices = True | |
if args.gc: | |
run_gc = True | |
if not stats: | |
stats.add('total') | |
uuid_map = map_uuid_to_device() | |
devnum_map = map_devnum_to_device() | |
for cache in os.listdir(SYSFS_BCACHE_PATH): | |
if not os.path.isdir('%s%s' % (SYSFS_BCACHE_PATH, cache)): | |
continue | |
if run_gc: | |
with open('%s%s/internal/trigger_gc' % (SYSFS_BCACHE_PATH, cache), 'w') as fd: | |
fd.write('1\n') | |
dump_bcache('%s%s' % (SYSFS_BCACHE_PATH, cache), stats, print_subdevices, uuid_map.get(cache, '?')) | |
if reset_stats: | |
with open('%s%s/clear_stats' % (SYSFS_BCACHE_PATH, cache), 'w') as fd: | |
fd.write('1\n') | |
if __name__ == '__main__': | |
main() |
I fixed a problem when any of the sizes reported by bcache yields empty string. You can pull a patch from my fork. This should fix the vovan69's problem
i'm not good at Python, so i'll just leave this here ;-)
--- bcache ---
Device /dev/bcache0 (253:0)
UUID 40fdac55-1be6-4a46-802a-aad6a29f9164
Block Size 0.50KiB
Bucket Size 512.00KiB
Congested? False
Read Congestion 2.0ms
Write Congestion 20.0ms
Total Cache Size 93.17GiB
Total Cache Used 954.02MiB (1%)
Total Cache Unused 92.23GiB (99%)
Traceback (most recent call last):
File "./bcache-status", line 348, in <module>
main()
File "./bcache-status", line 341, in main
dump_bcache('%s%s' % (SYSFS_BCACHE_PATH, cache), stats, print_subdevices, uuid_map.get(cache, '?'))
File "./bcache-status", line 242, in dump_bcache
val = conversion_func(val)
File "./bcache-status", line 209, in <lambda>
('dirty_data', 'Dirty Data', lambda x: fmt_cachesize(interpret_sectors(x))),
File "./bcache-status", line 75, in interpret_sectors
if x[-1] in factors:
IndexError: string index out of range
This happened with values like 0
, 512k
or 3.4M
in /sys/block/bcache0/bcache/dirty_data
@jangrewe: I ran into the same problem.
To fix it I added a simple check to the interpret_sectors(x)
function:
if not x: return 0
Insert this (for example) at line 74 to make the script work.
This error occurs because dirty_data
file location has been changed.
Changing L209 to the following will fix the problem.
$ diff -u bcache-status.orig bcache-status
--- bcache-status.orig 2017-10-09 12:56:23.919354245 +0900
+++ bcache-status 2017-10-09 12:53:35.940732415 +0900
@@ -206,7 +206,7 @@
(None, 'Total Cache Size', lambda x: format_sectors(cache_sectors)),
(None, 'Total Cache Used', lambda x: fmt_cachesize(cache_used_sectors)),
(None, 'Total Cache Unused', lambda x: fmt_cachesize(cache_unused_sectors)),
- ('dirty_data', 'Dirty Data', lambda x: fmt_cachesize(interpret_sectors(x))),
+ (None, 'Dirty Data', lambda x: fmt_cachesize(interpret_sectors(file_to_line('%s%s/bcache/dirty_data' % (SYSFS_BLOCK_PATH, devnum_map.get(device, '?').split('/')[2]))))),
('cache_available_percent', 'Evictable Cache', lambda x: '%s\t(%s%%)' % (format_sectors(float(x) * cache_sectors / 100), x)),
(None, 'Replacement Policy', lambda x: replacement_policies.pop() if len(replacement_policies) == 1 else '(Unknown)'),
(None, 'Cache Mode', lambda x: cache_modes.pop() if len(cache_modes) == 1 else '(Unknown)'),
NOTE: The updated file is available at my gist.
You can download it by:
$ wget https://gist.githubusercontent.com/visvirial/22513f28671752962f71c75a758a00f6/raw/53244e149b78b5b8ecb12a8d588191208b9d9d0d/bcache-status
@visvirial Adam has made some changes related to sector? factors here:
https://gist.github.com/adamryczkowski/8b9a1e55ac85a2ee83e2/revisions
Would you consider updating yours to include this? I'm thinking of packaging this script, and if yours is accepting updates, I'll pick yours :)
This (last posted update) shows only one device even though I should have 5 backing devices and one cache. Is this normal?
Just want to mention that this script is slow (takes ~2s on my machine) only because of the read of priority_stats. Removing that (only used for unused cache size) it's almost instant.
This (last posted update) shows only one device even though I should have 5 backing devices and one cache. Is this normal?
Try this https://gist.github.com/froloffw7/8e1103f1d7dee6fd9a6cc80043fcbf66
I just checked, and @froloffw7's gist includes the changes that I mentioned on Dec 3, 2019
With kernel 3.13 I got an error (see below). If I comment out the 'dirty_data' string the rest looks fine, except the "Block Size" is "0.00Kib".
Traceback (most recent call last):
File "/usr/local/bin/bcache-status.py", line 349, in
main()
File "/usr/local/bin/bcache-status.py", line 342, in main
dump_bcache('%s%s' % (SYSFS_BCACHE_PATH, cache), stats, print_subdevices, uuid_map.get(cache, '?'))
File "/usr/local/bin/bcache-status.py", line 243, in dump_bcache
val = conversion_func(val)
File "/usr/local/bin/bcache-status.py", line 209, in
('dirty_data', 'Dirty Data', lambda x: fmt_cachesize(interpret_sectors(x))),
File "/usr/local/bin/bcache-status.py", line 75, in interpret_sectors
if x[-1] in factors:
IndexError: string index out of range