1#!/usr/bin/env python 2# 3# dbslower Trace MySQL and PostgreSQL queries slower than a threshold. 4# 5# USAGE: dbslower [-v] [-p PID [PID ...]] [-b PATH_TO_BINARY] [-m THRESHOLD] 6# {mysql,postgres} 7# 8# By default, a threshold of 1ms is used. Set the threshold to 0 to trace all 9# queries (verbose). 10# 11# Script works in two different modes: 12# 1) USDT probes, which means it needs MySQL and PostgreSQL built with 13# USDT (DTrace) support. 14# 2) uprobe and uretprobe on exported function of binary specified by 15# PATH_TO_BINARY parameter. (At the moment only MySQL support) 16# 17# If no PID or PATH_TO_BINARY is provided, the script attempts to discover 18# all MySQL or PostgreSQL database processes and uses USDT probes. 19# 20# Strongly inspired by Brendan Gregg's work on the mysqld_qslower script. 21# 22# Copyright 2017, Sasha Goldshtein 23# Licensed under the Apache License, Version 2.0 24# 25# 15-Feb-2017 Sasha Goldshtein Created this. 26 27from bcc import BPF, USDT 28import argparse 29import re 30import subprocess 31 32examples = """examples: 33 dbslower postgres # trace PostgreSQL queries slower than 1ms 34 dbslower postgres -p 188 322 # trace specific PostgreSQL processes 35 dbslower mysql -p 480 -m 30 # trace MySQL queries slower than 30ms 36 dbslower mysql -p 480 -v # trace MySQL queries & print the BPF program 37 dbslower mysql -x $(which mysqld) # trace MySQL queries with uprobes 38""" 39parser = argparse.ArgumentParser( 40 description="", 41 formatter_class=argparse.RawDescriptionHelpFormatter, 42 epilog=examples) 43parser.add_argument("-v", "--verbose", action="store_true", 44 help="print the BPF program") 45parser.add_argument("db", choices=["mysql", "postgres"], 46 help="the database engine to use") 47parser.add_argument("-p", "--pid", type=int, nargs='*', 48 dest="pids", metavar="PID", help="the pid(s) to trace") 49parser.add_argument("-x", "--exe", type=str, 50 dest="path", metavar="PATH", help="path to binary") 51parser.add_argument("-m", "--threshold", type=int, default=1, 52 help="trace queries slower than this threshold (ms)") 53parser.add_argument("--ebpf", action="store_true", 54 help=argparse.SUPPRESS) 55args = parser.parse_args() 56 57threshold_ns = args.threshold * 1000000 58 59mode = "USDT" 60if args.path and not args.pids: 61 if args.db == "mysql": 62 regex = "\\w+dispatch_command\\w+" 63 symbols = BPF.get_user_functions_and_addresses(args.path, regex) 64 65 if len(symbols) == 0: 66 print("Can't find function 'dispatch_command' in %s" % (args.path)) 67 exit(1) 68 69 (mysql_func_name, addr) = symbols[0] 70 71 if mysql_func_name.find(b'COM_DATA') >= 0: 72 mode = "MYSQL57" 73 else: 74 mode = "MYSQL56" 75 else: 76 # Placeholder for PostrgeSQL 77 # Look on functions initStringInfo, pgstat_report_activity, EndCommand, 78 # NullCommand 79 print("Sorry at the moment PostgreSQL supports only USDT") 80 exit(1) 81 82program = """ 83#include <uapi/linux/ptrace.h> 84 85DEFINE_THRESHOLD 86DEFINE_USDT 87DEFINE_MYSQL56 88DEFINE_MYSQL57 89 90struct temp_t { 91 u64 timestamp; 92#ifdef USDT 93 char *query; 94#else 95 /* 96 MySQL clears query packet before uretprobe call - so copy query in advance 97 */ 98 char query[256]; 99#endif //USDT 100}; 101 102struct data_t { 103 u32 pid; 104 u64 timestamp; 105 u64 duration; 106 char query[256]; 107}; 108 109BPF_HASH(temp, u64, struct temp_t); 110BPF_PERF_OUTPUT(events); 111 112int query_start(struct pt_regs *ctx) { 113 114#if defined(MYSQL56) || defined(MYSQL57) 115/* 116Trace only packets with enum_server_command == COM_QUERY 117*/ 118 #ifdef MYSQL56 119 u64 command = (u64) PT_REGS_PARM1(ctx); 120 #else //MYSQL57 121 u64 command = (u64) PT_REGS_PARM3(ctx); 122 #endif 123 if (command != 3) return 0; 124#endif 125 126 struct temp_t tmp = {}; 127 tmp.timestamp = bpf_ktime_get_ns(); 128 129#if defined(MYSQL56) 130 bpf_probe_read_user(&tmp.query, sizeof(tmp.query), (void*) PT_REGS_PARM3(ctx)); 131#elif defined(MYSQL57) 132 void* st = (void*) PT_REGS_PARM2(ctx); 133 char* query; 134 bpf_probe_read_user(&query, sizeof(query), st); 135 bpf_probe_read_user(&tmp.query, sizeof(tmp.query), query); 136#else //USDT 137 bpf_usdt_readarg(1, ctx, &tmp.query); 138#endif 139 140 u64 pid = bpf_get_current_pid_tgid(); 141 temp.update(&pid, &tmp); 142 return 0; 143} 144 145int query_end(struct pt_regs *ctx) { 146 struct temp_t *tempp; 147 u64 pid = bpf_get_current_pid_tgid(); 148 tempp = temp.lookup(&pid); 149 if (!tempp) 150 return 0; 151 152 u64 delta = bpf_ktime_get_ns() - tempp->timestamp; 153#ifdef THRESHOLD 154 if (delta >= THRESHOLD) { 155#endif //THRESHOLD 156 struct data_t data = {}; 157 data.pid = pid >> 32; // only process id 158 data.timestamp = tempp->timestamp; 159 data.duration = delta; 160#if defined(MYSQL56) || defined(MYSQL57) 161 // We already copied string to the bpf stack. Hence use bpf_probe_read_kernel() 162 bpf_probe_read_kernel(&data.query, sizeof(data.query), tempp->query); 163#else 164 // USDT - we didnt copy string to the bpf stack before. 165 bpf_probe_read_user(&data.query, sizeof(data.query), tempp->query); 166#endif 167 events.perf_submit(ctx, &data, sizeof(data)); 168#ifdef THRESHOLD 169 } 170#endif //THRESHOLD 171 temp.delete(&pid); 172 return 0; 173}; 174""".replace("DEFINE_USDT", "#define USDT" if mode == "USDT" else "") \ 175 .replace("DEFINE_MYSQL56", "#define MYSQL56" if mode == "MYSQL56" else "") \ 176 .replace("DEFINE_MYSQL57", "#define MYSQL57" if mode == "MYSQL57" else "") \ 177 .replace("DEFINE_THRESHOLD", 178 "#define THRESHOLD %d" % threshold_ns if threshold_ns > 0 else "") 179 180if mode.startswith("MYSQL"): 181 # Uprobes mode 182 bpf = BPF(text=program) 183 bpf.attach_uprobe(name=args.path, sym=mysql_func_name, 184 fn_name="query_start") 185 bpf.attach_uretprobe(name=args.path, sym=mysql_func_name, 186 fn_name="query_end") 187else: 188 # USDT mode 189 if not args.pids or len(args.pids) == 0: 190 if args.db == "mysql": 191 args.pids = map(int, subprocess.check_output( 192 "pidof mysqld".split()).split()) 193 elif args.db == "postgres": 194 args.pids = map(int, subprocess.check_output( 195 "pidof postgres".split()).split()) 196 197 usdts = list(map(lambda pid: USDT(pid=pid), args.pids)) 198 for usdt in usdts: 199 usdt.enable_probe("query__start", "query_start") 200 usdt.enable_probe("query__done", "query_end") 201 if args.verbose: 202 print('\n'.join(map(lambda u: u.get_text(), usdts))) 203 204 bpf = BPF(text=program, usdt_contexts=usdts) 205 206if args.verbose or args.ebpf: 207 print(program) 208 if args.ebpf: 209 exit() 210 211start = BPF.monotonic_time() 212 213def print_event(cpu, data, size): 214 event = bpf["events"].event(data) 215 print("%-14.6f %-7d %8.3f %s" % ( 216 float(event.timestamp - start) / 1000000000, 217 event.pid, float(event.duration) / 1000000, event.query)) 218 219if mode.startswith("MYSQL"): 220 print("Tracing database queries for application %s slower than %d ms..." % 221 (args.path, args.threshold)) 222else: 223 print("Tracing database queries for pids %s slower than %d ms..." % 224 (', '.join(map(str, args.pids)), args.threshold)) 225 226print("%-14s %-7s %8s %s" % ("TIME(s)", "PID", "MS", "QUERY")) 227 228bpf["events"].open_perf_buffer(print_event, page_cnt=64) 229while True: 230 try: 231 bpf.perf_buffer_poll() 232 except KeyboardInterrupt: 233 exit() 234