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