blob: d6f36cccb784a9b59c564ae12d8d9b3c5d12e995 [file] [log] [blame]
Sasha Goldshteindd37d632017-02-09 05:44:43 -05001#!/usr/bin/python
2#
3# dbslower Trace MySQL and PostgreSQL queries slower than a threshold.
4#
Paul Chaignon46017922017-10-07 11:06:28 +02005# USAGE: dbslower [-v] [-p PID [PID ...]] [-b PATH_TO_BINARY] [-m THRESHOLD]
6# {mysql,postgres}
Sasha Goldshteindd37d632017-02-09 05:44:43 -05007#
8# By default, a threshold of 1ms is used. Set the threshold to 0 to trace all
Paul Chaignon46017922017-10-07 11:06:28 +02009# queries (verbose).
10#
11# Script works in two different modes:
12# 1) USDT probes, which means it needs MySQL and PostgreSQL built with
Igor Mazur5f7035e2017-07-18 10:06:34 +030013# USDT (DTrace) support.
Paul Chaignon46017922017-10-07 11:06:28 +020014# 2) uprobe and uretprobe on exported function of binary specified by
Igor Mazur5f7035e2017-07-18 10:06:34 +030015# PATH_TO_BINARY parameter. (At the moment only MySQL support)
Paul Chaignon46017922017-10-07 11:06:28 +020016#
Igor Mazur5f7035e2017-07-18 10:06:34 +030017# 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.
Sasha Goldshteindd37d632017-02-09 05:44:43 -050019#
20# Strongly inspired by Brendan Gregg's work on the mysqld_qslower script.
21#
Paul Chaignon956ca1c2017-03-04 20:07:56 +010022# Copyright 2017, Sasha Goldshtein
Sasha Goldshteindd37d632017-02-09 05:44:43 -050023# 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
Igor Mazur5f7035e2017-07-18 10:06:34 +030029import re
Sasha Goldshteindd37d632017-02-09 05:44:43 -050030import ctypes as ct
31import subprocess
32
33examples = """examples:
34 dbslower postgres # trace PostgreSQL queries slower than 1ms
35 dbslower postgres -p 188 322 # trace specific PostgreSQL processes
36 dbslower mysql -p 480 -m 30 # trace MySQL queries slower than 30ms
Paul Chaignon956ca1c2017-03-04 20:07:56 +010037 dbslower mysql -p 480 -v # trace MySQL queries & print the BPF program
Igor Mazur5f7035e2017-07-18 10:06:34 +030038 dbslower mysql -x $(which mysqld) # trace MySQL queries with uprobes
Sasha Goldshteindd37d632017-02-09 05:44:43 -050039"""
40parser = argparse.ArgumentParser(
41 description="",
42 formatter_class=argparse.RawDescriptionHelpFormatter,
43 epilog=examples)
44parser.add_argument("-v", "--verbose", action="store_true",
45 help="print the BPF program")
46parser.add_argument("db", choices=["mysql", "postgres"],
47 help="the database engine to use")
48parser.add_argument("-p", "--pid", type=int, nargs='*',
49 dest="pids", metavar="PID", help="the pid(s) to trace")
Igor Mazur5f7035e2017-07-18 10:06:34 +030050parser.add_argument("-x", "--exe", type=str,
51 dest="path", metavar="PATH", help="path to binary")
Sasha Goldshteindd37d632017-02-09 05:44:43 -050052parser.add_argument("-m", "--threshold", type=int, default=1,
53 help="trace queries slower than this threshold (ms)")
54args = parser.parse_args()
55
Sasha Goldshteindd37d632017-02-09 05:44:43 -050056threshold_ns = args.threshold * 1000000
57
Igor Mazur5f7035e2017-07-18 10:06:34 +030058mode = "USDT"
59if args.path and not args.pids:
60 if args.db == "mysql":
Paul Chaignon46017922017-10-07 11:06:28 +020061 regex = "\\w+dispatch_command\\w+"
62 symbols = BPF.get_user_functions_and_addresses(args.path, regex)
Igor Mazur5f7035e2017-07-18 10:06:34 +030063
64 if len(symbols) == 0:
65 print("Can't find function 'dispatch_command' in %s" % (args.path))
66 exit(1)
Paul Chaignon46017922017-10-07 11:06:28 +020067
Igor Mazur5f7035e2017-07-18 10:06:34 +030068 (mysql_func_name, addr) = symbols[0]
69
70 if mysql_func_name.find("COM_DATA") >= 0:
71 mode = "MYSQL57"
72 else:
73 mode = "MYSQL56"
74 else:
75 # Placeholder for PostrgeSQL
Paul Chaignon46017922017-10-07 11:06:28 +020076 # Look on functions initStringInfo, pgstat_report_activity, EndCommand,
77 # NullCommand
Igor Mazur5f7035e2017-07-18 10:06:34 +030078 print("Sorry at the moment PostgreSQL supports only USDT")
79 exit(1)
80
Sasha Goldshteindd37d632017-02-09 05:44:43 -050081program = """
Sasha Goldshteinaa124dd2017-03-31 17:16:45 -040082#include <uapi/linux/ptrace.h>
Sasha Goldshteindd37d632017-02-09 05:44:43 -050083
Igor Mazur5f7035e2017-07-18 10:06:34 +030084DEFINE_THRESHOLD
85DEFINE_USDT
86DEFINE_MYSQL56
87DEFINE_MYSQL57
88
Sasha Goldshteindd37d632017-02-09 05:44:43 -050089struct temp_t {
90 u64 timestamp;
Igor Mazur5f7035e2017-07-18 10:06:34 +030091#ifdef USDT
Sasha Goldshteindd37d632017-02-09 05:44:43 -050092 char *query;
Igor Mazur5f7035e2017-07-18 10:06:34 +030093#else
94 /*
95 MySQL clears query packet before uretprobe call - so copy query in advance
96 */
97 char query[256];
98#endif //USDT
Sasha Goldshteindd37d632017-02-09 05:44:43 -050099};
100
101struct data_t {
102 u64 pid;
103 u64 timestamp;
104 u64 duration;
105 char query[256];
106};
107
108BPF_HASH(temp, u64, struct temp_t);
109BPF_PERF_OUTPUT(events);
110
Igor Mazur5f7035e2017-07-18 10:06:34 +0300111int query_start(struct pt_regs *ctx) {
112
113#if defined(MYSQL56) || defined(MYSQL57)
114/*
Paul Chaignon46017922017-10-07 11:06:28 +0200115Trace only packets with enum_server_command == COM_QUERY
Igor Mazur5f7035e2017-07-18 10:06:34 +0300116*/
117 #ifdef MYSQL56
118 u64 command = (u64) PT_REGS_PARM1(ctx);
119 #else //MYSQL57
120 u64 command = (u64) PT_REGS_PARM3(ctx);
121 #endif
122 if (command != 3) return 0;
123#endif
124
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500125 struct temp_t tmp = {};
126 tmp.timestamp = bpf_ktime_get_ns();
Igor Mazur5f7035e2017-07-18 10:06:34 +0300127
128#if defined(MYSQL56)
129 bpf_probe_read(&tmp.query, sizeof(tmp.query), (void*) PT_REGS_PARM3(ctx));
130#elif defined(MYSQL57)
131 void* st = (void*) PT_REGS_PARM2(ctx);
132 char* query;
133 bpf_probe_read(&query, sizeof(query), st);
134 bpf_probe_read(&tmp.query, sizeof(tmp.query), query);
135#else //USDT
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500136 bpf_usdt_readarg(1, ctx, &tmp.query);
Igor Mazur5f7035e2017-07-18 10:06:34 +0300137#endif
138
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500139 u64 pid = bpf_get_current_pid_tgid();
140 temp.update(&pid, &tmp);
141 return 0;
142}
143
Igor Mazur5f7035e2017-07-18 10:06:34 +0300144int query_end(struct pt_regs *ctx) {
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500145 struct temp_t *tempp;
146 u64 pid = bpf_get_current_pid_tgid();
147 tempp = temp.lookup(&pid);
148 if (!tempp)
149 return 0;
150
151 u64 delta = bpf_ktime_get_ns() - tempp->timestamp;
Igor Mazur5f7035e2017-07-18 10:06:34 +0300152#ifdef THRESHOLD
153 if (delta >= THRESHOLD) {
Paul Chaignon46017922017-10-07 11:06:28 +0200154#endif //THRESHOLD
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500155 struct data_t data = {};
156 data.pid = pid >> 32; // only process id
157 data.timestamp = tempp->timestamp;
158 data.duration = delta;
159 bpf_probe_read(&data.query, sizeof(data.query), tempp->query);
160 events.perf_submit(ctx, &data, sizeof(data));
Igor Mazur5f7035e2017-07-18 10:06:34 +0300161#ifdef THRESHOLD
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500162 }
Igor Mazur5f7035e2017-07-18 10:06:34 +0300163#endif //THRESHOLD
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500164 temp.delete(&pid);
165 return 0;
Igor Mazur5f7035e2017-07-18 10:06:34 +0300166};
167""".replace("DEFINE_USDT", "#define USDT" if mode == "USDT" else "") \
168 .replace("DEFINE_MYSQL56", "#define MYSQL56" if mode == "MYSQL56" else "") \
169 .replace("DEFINE_MYSQL57", "#define MYSQL57" if mode == "MYSQL57" else "") \
Paul Chaignon46017922017-10-07 11:06:28 +0200170 .replace("DEFINE_THRESHOLD",
171 "#define THRESHOLD %d" % threshold_ns if threshold_ns > 0 else "")
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500172
Igor Mazur5f7035e2017-07-18 10:06:34 +0300173if mode.startswith("MYSQL"):
174 # Uprobes mode
175 bpf = BPF(text=program)
Paul Chaignon46017922017-10-07 11:06:28 +0200176 bpf.attach_uprobe(name=args.path, sym=mysql_func_name,
177 fn_name="query_start")
178 bpf.attach_uretprobe(name=args.path, sym=mysql_func_name,
179 fn_name="query_end")
Igor Mazur5f7035e2017-07-18 10:06:34 +0300180else:
181 # USDT mode
182 if not args.pids or len(args.pids) == 0:
183 if args.db == "mysql":
184 args.pids = map(int, subprocess.check_output(
185 "pidof mysqld".split()).split())
186 elif args.db == "postgres":
187 args.pids = map(int, subprocess.check_output(
188 "pidof postgres".split()).split())
189
190 usdts = map(lambda pid: USDT(pid=pid), args.pids)
191 for usdt in usdts:
192 usdt.enable_probe("query__start", "query_start")
193 usdt.enable_probe("query__done", "query_end")
194 if args.verbose:
195 print('\n'.join(map(lambda u: u.get_text(), usdts)))
196
197 bpf = BPF(text=program, usdt_contexts=usdts)
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500198
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500199if args.verbose:
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500200 print(program)
201
202class Data(ct.Structure):
203 _fields_ = [
204 ("pid", ct.c_ulonglong),
205 ("timestamp", ct.c_ulonglong),
206 ("delta", ct.c_ulonglong),
207 ("query", ct.c_char * 256)
208 ]
209
210start = BPF.monotonic_time()
211
212def print_event(cpu, data, size):
213 event = ct.cast(data, ct.POINTER(Data)).contents
214 print("%-14.6f %-6d %8.3f %s" % (
215 float(event.timestamp - start) / 1000000000,
216 event.pid, float(event.delta) / 1000000, event.query))
217
Igor Mazur5f7035e2017-07-18 10:06:34 +0300218if mode.startswith("MYSQL"):
219 print("Tracing database queries for application %s slower than %d ms..." %
220 (args.path, args.threshold))
221else:
222 print("Tracing database queries for pids %s slower than %d ms..." %
223 (', '.join(map(str, args.pids)), args.threshold))
224
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500225print("%-14s %-6s %8s %s" % ("TIME(s)", "PID", "MS", "QUERY"))
226
Mark Drayton5f5687e2017-02-20 18:13:03 +0000227bpf["events"].open_perf_buffer(print_event, page_cnt=64)
Sasha Goldshteindd37d632017-02-09 05:44:43 -0500228while True:
229 bpf.kprobe_poll()