1#!/usr/bin/env @PYTHON_SHEBANG@ 2# SPDX-License-Identifier: CDDL-1.0 3# 4# Print out statistics for all zil stats. This information is 5# available through the zil kstat. 6# 7# CDDL HEADER START 8# 9# The contents of this file are subject to the terms of the 10# Common Development and Distribution License, Version 1.0 only 11# (the "License"). You may not use this file except in compliance 12# with the License. 13# 14# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 15# or https://opensource.org/licenses/CDDL-1.0. 16# See the License for the specific language governing permissions 17# and limitations under the License. 18# 19# When distributing Covered Code, include this CDDL HEADER in each 20# file and include the License file at usr/src/OPENSOLARIS.LICENSE. 21# If applicable, add the following below this CDDL HEADER, with the 22# fields enclosed by brackets "[]" replaced with your own identifying 23# information: Portions Copyright [yyyy] [name of copyright owner] 24# 25# This script must remain compatible with Python 3.6+. 26# 27 28import sys 29import subprocess 30import time 31import copy 32import os 33import re 34import signal 35from collections import defaultdict 36import argparse 37from argparse import RawTextHelpFormatter 38 39cols = { 40 # hdr: [size, scale, kstat name] 41 "time": [8, -1, "time"], 42 "pool": [12, -1, "pool"], 43 "ds": [12, -1, "dataset_name"], 44 "obj": [12, -1, "objset"], 45 "cc": [5, 1000, "zil_commit_count"], 46 "cwc": [5, 1000, "zil_commit_writer_count"], 47 "cec": [5, 1000, "zil_commit_error_count"], 48 "csc": [5, 1000, "zil_commit_stall_count"], 49 "cSc": [5, 1000, "zil_commit_suspend_count"], 50 "cCc": [5, 1000, "zil_commit_crash_count"], 51 "ic": [5, 1000, "zil_itx_count"], 52 "iic": [5, 1000, "zil_itx_indirect_count"], 53 "iib": [5, 1024, "zil_itx_indirect_bytes"], 54 "icc": [5, 1000, "zil_itx_copied_count"], 55 "icb": [5, 1024, "zil_itx_copied_bytes"], 56 "inc": [5, 1000, "zil_itx_needcopy_count"], 57 "inb": [5, 1024, "zil_itx_needcopy_bytes"], 58 "idc": [5, 1000, "icc+inc"], 59 "idb": [5, 1024, "icb+inb"], 60 "iwc": [5, 1000, "iic+idc"], 61 "iwb": [5, 1024, "iib+idb"], 62 "imnc": [6, 1000, "zil_itx_metaslab_normal_count"], 63 "imnb": [6, 1024, "zil_itx_metaslab_normal_bytes"], 64 "imnw": [6, 1024, "zil_itx_metaslab_normal_write"], 65 "imna": [6, 1024, "zil_itx_metaslab_normal_alloc"], 66 "imsc": [6, 1000, "zil_itx_metaslab_slog_count"], 67 "imsb": [6, 1024, "zil_itx_metaslab_slog_bytes"], 68 "imsw": [6, 1024, "zil_itx_metaslab_slog_write"], 69 "imsa": [6, 1024, "zil_itx_metaslab_slog_alloc"], 70 "imc": [5, 1000, "imnc+imsc"], 71 "imb": [5, 1024, "imnb+imsb"], 72 "imw": [5, 1024, "imnw+imsw"], 73 "ima": [5, 1024, "imna+imsa"], 74 "se%": [3, 100, "imb/ima"], 75 "sen%": [4, 100, "imnb/imna"], 76 "ses%": [4, 100, "imsb/imsa"], 77 "te%": [3, 100, "imb/imw"], 78 "ten%": [4, 100, "imnb/imnw"], 79 "tes%": [4, 100, "imsb/imsw"], 80} 81 82hdr = ["time", "ds", "cc", "ic", "idc", "idb", "iic", "iib", 83 "imnc", "imnw", "imsc", "imsw"] 84 85ghdr = ["time", "cc", "ic", "idc", "idb", "iic", "iib", 86 "imnc", "imnw", "imsc", "imsw"] 87 88cmd = ("Usage: zilstat [-hgdv] [-i interval] [-p pool_name]") 89 90curr = {} 91diff = {} 92kstat = {} 93ds_pairs = {} 94pool_name = None 95dataset_name = None 96interval = 0 97sep = " " 98gFlag = True 99dsFlag = False 100 101def prettynum(sz, scale, num=0): 102 suffix = [' ', 'K', 'M', 'G', 'T', 'P', 'E', 'Z'] 103 index = 0 104 save = 0 105 106 if scale == -1: 107 return "%*s" % (sz, num) 108 109 # Rounding error, return 0 110 elif 0 < num < 1: 111 num = 0 112 113 while num > scale and index < 5: 114 save = num 115 num = num / scale 116 index += 1 117 118 if index == 0: 119 return "%*d" % (sz, num) 120 121 if (save / scale) < 10: 122 return "%*.1f%s" % (sz - 1, num, suffix[index]) 123 else: 124 return "%*d%s" % (sz - 1, num, suffix[index]) 125 126def print_header(): 127 global hdr 128 global sep 129 for col in hdr: 130 new_col = col 131 if interval > 0 and cols[col][1] > 100: 132 new_col += "/s" 133 sys.stdout.write("%*s%s" % (cols[col][0], new_col, sep)) 134 sys.stdout.write("\n") 135 136def print_values(v): 137 global hdr 138 global sep 139 for col in hdr: 140 val = v[cols[col][2]] 141 if interval > 0 and cols[col][1] > 100: 142 val = v[cols[col][2]] // interval 143 sys.stdout.write("%s%s" % ( 144 prettynum(cols[col][0], cols[col][1], val), sep)) 145 sys.stdout.write("\n") 146 147def print_dict(d): 148 for pool in d: 149 for objset in d[pool]: 150 print_values(d[pool][objset]) 151 152def detailed_usage(): 153 sys.stderr.write("%s\n" % cmd) 154 sys.stderr.write("Field definitions are as follows:\n") 155 for key in cols: 156 sys.stderr.write("%11s : %s\n" % (key, cols[key][2])) 157 sys.stderr.write("\n") 158 sys.exit(0) 159 160def init(): 161 global pool_name 162 global dataset_name 163 global interval 164 global hdr 165 global curr 166 global gFlag 167 global sep 168 169 curr = dict() 170 171 parser = argparse.ArgumentParser(description='Program to print zilstats', 172 add_help=True, 173 formatter_class=RawTextHelpFormatter, 174 epilog="\nUsage Examples\n"\ 175 "Note: Global zilstats is shown by default,"\ 176 " if none of a|p|d option is not provided\n"\ 177 "\tzilstat -a\n"\ 178 '\tzilstat -v\n'\ 179 '\tzilstat -p tank\n'\ 180 '\tzilstat -d tank/d1,tank/d2,tank/zv1\n'\ 181 '\tzilstat -i 1\n'\ 182 '\tzilstat -s \"***\"\n'\ 183 '\tzilstat -f zcwc,zimnb,zimsb\n') 184 185 parser.add_argument( 186 "-v", "--verbose", 187 action="store_true", 188 help="List field headers and definitions" 189 ) 190 191 pool_grp = parser.add_mutually_exclusive_group() 192 193 pool_grp.add_argument( 194 "-a", "--all", 195 action="store_true", 196 dest="all", 197 help="Print all dataset stats" 198 ) 199 200 pool_grp.add_argument( 201 "-p", "--pool", 202 type=str, 203 help="Print stats for all datasets of a speicfied pool" 204 ) 205 206 pool_grp.add_argument( 207 "-d", "--dataset", 208 type=str, 209 help="Print given dataset(s) (Comma separated)" 210 ) 211 212 parser.add_argument( 213 "-f", "--columns", 214 type=str, 215 help="Specify specific fields to print (see -v)" 216 ) 217 218 parser.add_argument( 219 "-s", "--separator", 220 type=str, 221 help="Override default field separator with custom " 222 "character or string" 223 ) 224 225 parser.add_argument( 226 "-i", "--interval", 227 type=int, 228 dest="interval", 229 help="Print stats between specified interval" 230 " (in seconds)" 231 ) 232 233 parsed_args = parser.parse_args() 234 235 if parsed_args.verbose: 236 detailed_usage() 237 238 if parsed_args.all: 239 gFlag = False 240 241 if parsed_args.interval: 242 interval = parsed_args.interval 243 244 if parsed_args.pool: 245 pool_name = parsed_args.pool 246 gFlag = False 247 248 if parsed_args.dataset: 249 dataset_name = parsed_args.dataset 250 gFlag = False 251 252 if parsed_args.separator: 253 sep = parsed_args.separator 254 255 if gFlag: 256 hdr = ghdr 257 258 if parsed_args.columns: 259 hdr = parsed_args.columns.split(",") 260 261 invalid = [] 262 for ele in hdr: 263 if ele not in cols: 264 invalid.append(ele) 265 266 if len(invalid) > 0: 267 sys.stderr.write("Invalid column definition! -- %s\n" % invalid) 268 sys.exit(1) 269 270 if pool_name and dataset_name: 271 print ("Error: Can not filter both dataset and pool") 272 sys.exit(1) 273 274def FileCheck(fname): 275 try: 276 return (open(fname)) 277 except IOError: 278 print ("Unable to open zilstat proc file: " + fname) 279 sys.exit(1) 280 281if sys.platform.startswith('freebsd'): 282 # Requires py-sysctl on FreeBSD 283 import sysctl 284 285 def kstat_update(pool = None, objid = None): 286 global kstat 287 kstat = {} 288 if not pool: 289 file = "kstat.zfs.misc.zil" 290 k = [ctl for ctl in sysctl.filter(file) \ 291 if ctl.type != sysctl.CTLTYPE_NODE] 292 kstat_process_str(k, file, "GLOBAL", len(file + ".")) 293 elif objid: 294 file = "kstat.zfs." + pool + ".dataset.objset-" + objid 295 k = [ctl for ctl in sysctl.filter(file) if ctl.type \ 296 != sysctl.CTLTYPE_NODE] 297 kstat_process_str(k, file, objid, len(file + ".")) 298 else: 299 file = "kstat.zfs." + pool + ".dataset" 300 zil_start = len(file + ".") 301 obj_start = len("kstat.zfs." + pool + ".") 302 k = [ctl for ctl in sysctl.filter(file) 303 if ctl.type != sysctl.CTLTYPE_NODE] 304 for s in k: 305 if not s or (s.name.find("zil") == -1 and \ 306 s.name.find("dataset_name") == -1): 307 continue 308 name, value = s.name, s.value 309 objid = re.findall(r'0x[0-9A-F]+', \ 310 name[obj_start:], re.I)[0] 311 if objid not in kstat: 312 kstat[objid] = dict() 313 zil_start = len(file + ".objset-" + \ 314 objid + ".") 315 kstat[objid][name[zil_start:]] = value \ 316 if (name.find("dataset_name")) \ 317 else int(value) 318 319 def kstat_process_str(k, file, objset = "GLOBAL", zil_start = 0): 320 global kstat 321 if not k: 322 print("Unable to process kstat for: " + file) 323 sys.exit(1) 324 kstat[objset] = dict() 325 for s in k: 326 if not s or (s.name.find("zil") == -1 and \ 327 s.name.find("dataset_name") == -1): 328 continue 329 name, value = s.name, s.value 330 kstat[objset][name[zil_start:]] = value \ 331 if (name.find("dataset_name")) else int(value) 332 333elif sys.platform.startswith('linux'): 334 def kstat_update(pool = None, objid = None): 335 global kstat 336 kstat = {} 337 if not pool: 338 k = [line.strip() for line in \ 339 FileCheck("/proc/spl/kstat/zfs/zil")] 340 kstat_process_str(k, "/proc/spl/kstat/zfs/zil") 341 elif objid: 342 file = "/proc/spl/kstat/zfs/" + pool + "/objset-" + objid 343 k = [line.strip() for line in FileCheck(file)] 344 kstat_process_str(k, file, objid) 345 else: 346 if not os.path.exists(f"/proc/spl/kstat/zfs/{pool}"): 347 print("Pool \"" + pool + "\" does not exist, Exitting") 348 sys.exit(1) 349 objsets = os.listdir(f'/proc/spl/kstat/zfs/{pool}') 350 for objid in objsets: 351 if objid.find("objset-") == -1: 352 continue 353 file = "/proc/spl/kstat/zfs/" + pool + "/" + objid 354 k = [line.strip() for line in FileCheck(file)] 355 kstat_process_str(k, file, objid.replace("objset-", "")) 356 357 def kstat_process_str(k, file, objset = "GLOBAL", zil_start = 0): 358 global kstat 359 if not k: 360 print("Unable to process kstat for: " + file) 361 sys.exit(1) 362 363 kstat[objset] = dict() 364 for s in k: 365 if not s or (s.find("zil") == -1 and \ 366 s.find("dataset_name") == -1): 367 continue 368 name, unused, value = s.split() 369 kstat[objset][name] = value \ 370 if (name == "dataset_name") else int(value) 371 372def zil_process_kstat(): 373 global curr, pool_name, dataset_name, dsFlag, ds_pairs 374 curr.clear() 375 if gFlag == True: 376 kstat_update() 377 zil_build_dict() 378 else: 379 if pool_name: 380 kstat_update(pool_name) 381 zil_build_dict(pool_name) 382 elif dataset_name: 383 if dsFlag == False: 384 dsFlag = True 385 datasets = dataset_name.split(',') 386 ds_pairs = defaultdict(list) 387 for ds in datasets: 388 try: 389 objid = subprocess.check_output(['zfs', 390 'list', '-Hpo', 'objsetid', ds], \ 391 stderr=subprocess.DEVNULL) \ 392 .decode('utf-8').strip() 393 except subprocess.CalledProcessError as e: 394 print("Command: \"zfs list -Hpo objset "\ 395 + str(ds) + "\" failed with error code:"\ 396 + str(e.returncode)) 397 print("Please make sure that dataset \""\ 398 + str(ds) + "\" exists") 399 sys.exit(1) 400 if not objid: 401 continue 402 ds_pairs[ds.split('/')[0]]. \ 403 append(hex(int(objid))) 404 for pool, objids in ds_pairs.items(): 405 for objid in objids: 406 kstat_update(pool, objid) 407 zil_build_dict(pool) 408 else: 409 try: 410 pools = subprocess.check_output(['zpool', 'list', '-Hpo',\ 411 'name']).decode('utf-8').split() 412 except subprocess.CalledProcessError as e: 413 print("Command: \"zpool list -Hpo name\" failed with error"\ 414 "code: " + str(e.returncode)) 415 sys.exit(1) 416 for pool in pools: 417 kstat_update(pool) 418 zil_build_dict(pool) 419 420def calculate_diff(): 421 global curr, diff 422 prev = copy.deepcopy(curr) 423 zil_process_kstat() 424 diff = copy.deepcopy(curr) 425 for pool in curr: 426 for objset in curr[pool]: 427 for key in curr[pool][objset]: 428 if not isinstance(diff[pool][objset][key], int): 429 continue 430 # If prev is NULL, this is the 431 # first time we are here 432 if not prev: 433 diff[pool][objset][key] = 0 434 else: 435 diff[pool][objset][key] \ 436 = curr[pool][objset][key] \ 437 - prev[pool][objset][key] 438 439def zil_build_dict(pool = "GLOBAL"): 440 global kstat 441 for objset in kstat: 442 for key in kstat[objset]: 443 val = kstat[objset][key] 444 if pool not in curr: 445 curr[pool] = dict() 446 if objset not in curr[pool]: 447 curr[pool][objset] = dict() 448 curr[pool][objset][key] = val 449 450def zil_extend_dict(): 451 global diff 452 for pool in diff: 453 for objset in diff[pool]: 454 diff[pool][objset]["pool"] = pool 455 diff[pool][objset]["objset"] = objset 456 diff[pool][objset]["time"] = time.strftime("%H:%M:%S", \ 457 time.localtime()) 458 diff[pool][objset]["icc+inc"] = \ 459 diff[pool][objset]["zil_itx_copied_count"] + \ 460 diff[pool][objset]["zil_itx_needcopy_count"] 461 diff[pool][objset]["icb+inb"] = \ 462 diff[pool][objset]["zil_itx_copied_bytes"] + \ 463 diff[pool][objset]["zil_itx_needcopy_bytes"] 464 diff[pool][objset]["iic+idc"] = \ 465 diff[pool][objset]["zil_itx_indirect_count"] + \ 466 diff[pool][objset]["zil_itx_copied_count"] + \ 467 diff[pool][objset]["zil_itx_needcopy_count"] 468 diff[pool][objset]["iib+idb"] = \ 469 diff[pool][objset]["zil_itx_indirect_bytes"] + \ 470 diff[pool][objset]["zil_itx_copied_bytes"] + \ 471 diff[pool][objset]["zil_itx_needcopy_bytes"] 472 diff[pool][objset]["imnc+imsc"] = \ 473 diff[pool][objset]["zil_itx_metaslab_normal_count"] + \ 474 diff[pool][objset]["zil_itx_metaslab_slog_count"] 475 diff[pool][objset]["imnb+imsb"] = \ 476 diff[pool][objset]["zil_itx_metaslab_normal_bytes"] + \ 477 diff[pool][objset]["zil_itx_metaslab_slog_bytes"] 478 diff[pool][objset]["imnw+imsw"] = \ 479 diff[pool][objset]["zil_itx_metaslab_normal_write"] + \ 480 diff[pool][objset]["zil_itx_metaslab_slog_write"] 481 diff[pool][objset]["imna+imsa"] = \ 482 diff[pool][objset]["zil_itx_metaslab_normal_alloc"] + \ 483 diff[pool][objset]["zil_itx_metaslab_slog_alloc"] 484 if diff[pool][objset]["imna+imsa"] > 0: 485 diff[pool][objset]["imb/ima"] = 100 * \ 486 diff[pool][objset]["imnb+imsb"] // \ 487 diff[pool][objset]["imna+imsa"] 488 else: 489 diff[pool][objset]["imb/ima"] = 100 490 if diff[pool][objset]["zil_itx_metaslab_normal_alloc"] > 0: 491 diff[pool][objset]["imnb/imna"] = 100 * \ 492 diff[pool][objset]["zil_itx_metaslab_normal_bytes"] // \ 493 diff[pool][objset]["zil_itx_metaslab_normal_alloc"] 494 else: 495 diff[pool][objset]["imnb/imna"] = 100 496 if diff[pool][objset]["zil_itx_metaslab_slog_alloc"] > 0: 497 diff[pool][objset]["imsb/imsa"] = 100 * \ 498 diff[pool][objset]["zil_itx_metaslab_slog_bytes"] // \ 499 diff[pool][objset]["zil_itx_metaslab_slog_alloc"] 500 else: 501 diff[pool][objset]["imsb/imsa"] = 100 502 if diff[pool][objset]["imnw+imsw"] > 0: 503 diff[pool][objset]["imb/imw"] = 100 * \ 504 diff[pool][objset]["imnb+imsb"] // \ 505 diff[pool][objset]["imnw+imsw"] 506 else: 507 diff[pool][objset]["imb/imw"] = 100 508 if diff[pool][objset]["zil_itx_metaslab_normal_alloc"] > 0: 509 diff[pool][objset]["imnb/imnw"] = 100 * \ 510 diff[pool][objset]["zil_itx_metaslab_normal_bytes"] // \ 511 diff[pool][objset]["zil_itx_metaslab_normal_write"] 512 else: 513 diff[pool][objset]["imnb/imnw"] = 100 514 if diff[pool][objset]["zil_itx_metaslab_slog_alloc"] > 0: 515 diff[pool][objset]["imsb/imsw"] = 100 * \ 516 diff[pool][objset]["zil_itx_metaslab_slog_bytes"] // \ 517 diff[pool][objset]["zil_itx_metaslab_slog_write"] 518 else: 519 diff[pool][objset]["imsb/imsw"] = 100 520 521def sign_handler_epipe(sig, frame): 522 print("Caught EPIPE signal: " + str(frame)) 523 print("Exitting...") 524 sys.exit(0) 525 526def main(): 527 global interval 528 global curr, diff 529 hprint = False 530 init() 531 signal.signal(signal.SIGINT, signal.SIG_DFL) 532 signal.signal(signal.SIGPIPE, sign_handler_epipe) 533 534 zil_process_kstat() 535 if not curr: 536 print ("Error: No stats to show") 537 sys.exit(0) 538 print_header() 539 if interval > 0: 540 time.sleep(interval) 541 while True: 542 calculate_diff() 543 if not diff: 544 print ("Error: No stats to show") 545 sys.exit(0) 546 zil_extend_dict() 547 print_dict(diff) 548 time.sleep(interval) 549 else: 550 diff = curr 551 zil_extend_dict() 552 print_dict(diff) 553 554if __name__ == '__main__': 555 main() 556 557