Archive for the ‘MySQL’ Category

Monitoring MySQL replication

Friday, June 12th, 2009

I found this pretty cool script the other day for monitoring MySQL replication, called replication_monitor.sh. The original one from patchlog.com has a few syntax errors and I wanted to include full paths so I have my version: replication_monitor.sh.

What I like about it is that it is very simple to use and implement. You just put it in cron and it checks to see if both IO threads are saying YES, and if they don’t it will e-mail you. Nothing fancy but it gets the job done. Be sure to edit the DB_USER, DB_PASS and alert_to variables before using it.

DTrace MySQL patch

Tuesday, December 2nd, 2008

I know it’s been a while. I took Sun’s older MySQL DTrace patch and updated it to mysql 5.0.67:

mysql-5.0.67-dtrace.patch

Apply with gpatch -p2 -i mysql-5.0.67-dtrace.patch

Using DTrace on MySQL

Wednesday, October 3rd, 2007

Even though there aren’t DTrace probes for MySQL released yet, we can still get useful information from MySQL. DTrace has a pid provider which allows us to get into any function the program is executing and see it’s arguments. The only drawback is you have to go digging around in the source code to find out what you want to see. But thanks to guys like Brendan Gregg, they have already done some of the digging for us. Even if we want to go digging around ourselves, it’s really not that hard; you just have to get your feet wet. I will show some examples of this and how easy it is to hunt down your own functions.

First let’s start with functions that have already been dug up for us:

mysql_parse(thd, thd->query, length, & found_semicolon);

This is the function MySQL uses to parse a query. So all we have to do is trace this function through the pid provider and we get to see all the queries coming through. This shows arg1 as being the query, and we must copy it in to kernel land where DTrace works for it to see the string:

root@ferrari:~# dtrace -qn ‘pid$target:mysqld:*mysql_parse*:entry { printf(”%Y %s\n”, walltimestamp, copyinstr(arg1)) }’ -p `pgrep -x mysqld`
2007 Sep 27 10:04:35 select * from blah
2007 Sep 27 10:04:58 select * from tablenothere

Notice that this will show all queries, even if they aren’t successful. Now that we can trace queries, this can give us good information. For example we can see what queries are executed the most:

root@ferrari:~# dtrace -qn ‘pid$target:mysqld:*mysql_parse*:entry { @queries[copyinstr(arg1)] = count() }’ -p `pgrep -x mysqld`
^C

select * from blah 5
select * from tablenothere 10

You can’t get this kind of information from MySQL unless you write some kind of script to parse through the query log. If we know that there is a query being executed 1000 more times than the others, we could always try to get this one to cache. Now lets say we want to find out how long a query took to execute. The function mysql_execute_command does the actual execution of the queries so all we do here is subtract the entry and return timestamps of this function. The script shown below uses this:

root@ferrari:~# ./exactquerytimes.d -p `pgrep -x mysqld`
Tracing… Hit Ctrl-C to end.

Query: SELECT COUNT(*) FROM joe_visitors where upper(vs_browser) not like ‘%GOOGLE%’ and upper(vs_browser) not like ‘%GOOGLE BOT%’ and upper(vs_browser) not like ‘%BOT%’ and upper(vs_browser) not like ‘%MSN%’ and upper(vs_browser) not like ‘%MSNBOT%’ and upper(,
Time: 2.32

On the MySQL side, it showed this query being executed at 2.32 seconds as well:
1 row in set (2.32 sec).

This is awesome information because as of now MySQL doesn’t allow you to see a slow query that is less than 1 second (I believe this is a fix in MySQL 5.1). So with this, we can see not just slow queries, but all queries and how long they take to execute with their times.

Now let’s try this same query but I bumped my query_cache_size up to 50M:

The first try (won’t hit the cache):

root@ferrari:~# ./exactquerytimes.d -p `pgrep -x mysqld`
Tracing… Hit Ctrl-C to end.

Query: SELECT COUNT(*) FROM joe_visitors where upper(vs_browser) not like ‘%GOOGLE%’ and upper(vs_browser) not like ‘%GOOGLE BOT%’ and upper(vs_browser) not like ‘%BOT%’ and upper(vs_browser) not like ‘%MSN%’ and upper(vs_browser) not like ‘%MSNBOT%’ and upper(,
Time: 2.28

And the second try hits the cache but doesn’t show anything through DTrace. So this means a query that is served from the cache won’t show up in mysql_parse. Right now this probably doesn’t mean much, but as you learn more about how the internals of MySQL work, then troubleshooting becomes much easier down the road.

So far this has all been information that was provided. Now I will show how simple it is search through MySQL’s source and look at functions.

First we need to decide what to look for. Let’s say we want to find out every time that slow query is written to the slow query log. First we download the MySQL source code from http://www.mysql.com. Now we can search through the source code for ’slow query’:

root@ferrari:/export/home/derek/mysql-5.0.45# ggrep -ri ’slow query’ *

This turns up only a few source code files, one of them looking most obvious called log.cc with the expression “Write to the query log”. The MySQL code is very well commented so it makes searching really easy. Looking in this file, that comment is right above the function:

/*
Write to the slow query log.
*/
bool MYSQL_LOG::write(THD *thd,const char *query, uint query_length,
time_t query_start_arg)

It’s obvious that this function is the function that writes to the slow query log. Running this script below, looking for function LOG while a slow query is being inserted shows this function being executed with some weird characters around it:

root@ferrari:~# dtrace -F -n ‘pid$1:mysqld:*LOG*:entry {} pid$1:mysqld:*LOG*:return {}’ `pgrep -x mysqld`
dtrace: description ‘pid$1:mysqld:*LOG*:entry ‘ matched 126 probes
CPU FUNCTION
0 -> _ZN9MYSQL_LOG5writeEP3THD19enum_server_commandPKcz
0 -> _ZN9MYSQL_LOG5writeEP3THDPKcjl
0 < - _ZN9MYSQL_LOG5writeEP3THDPKcjl

The only thing bad about tracing MySQL through the pid provider is that these weird characters change between MySQL versions, so we can’t always trace for ‘_ZN9MYSQL_LOG5writeEP3THDPKcjl’ if we want it to work on other machines. We have to trace for *MYSQ*LOG*write* which slowquerycounts.d uses:

root@ferrari:~# ./slowquerycounts.d -p `pgrep -x mysqld`
Tracing… Hit Ctrl-C to end.

SELECT COUNT(*) FROM joe_visitors where upper(vs_browser) not like ‘%GOOGLE%’ and upper(vs_browser) not like ‘%GOOGLE BOT%’ and upper(vs_browser) not like ‘%BOT%’ and upper(vs_browser) not like ‘%MSN%’ and upper(vs_browser) not like ‘%MSNBOT%’ and upper(
3

As you can see DTrace can be very powerful even if we don’t have probes released yet, we just have to do a little extra work. Some of the information shown can be obtained from MySQL but using DTrace still provides a benefit because we don’t have to enable anything in the MySQL configuration, possibly making us restart the server. I’m providing these scripts in the MySQLDTraceKit.tar.gz. Hopefully in the near future we will have real MySQL probes.

Tuning my wordpress plugin

Thursday, September 20th, 2007

I run this site on a smaller sized Accelerator (ah the beauty of root access). Because I have limited resources and memory, it’s essential to tune anything I can to use less. So last night I was taking a look at my MySQL slow query log to see what queries were taking the longest (most likely not using indexes properly). The only query that shows up really often is for my jVisitors plugin that tracks who visits the site:

select id, vs_ip, vs_session, vs_user_agent,
vs_country, vs_os, vs_os_ver, vs_browser, vs_browser_ver,
date_format(DATE_SUB(vs_date, INTERVAL 6 HOUR),’%a, %e/%m/%Y’) as vs_dte,
date_format(DATE_SUB(vs_date, INTERVAL 6 HOUR),’%h:%i:%s%p’) as vs_time
from joe_visitors
where upper(vs_browser) not like ‘%GOOGLE%’ and upper(vs_browser) not like ‘%GOOGLE BOT%’ and upper(vs_browser) not like ‘%BOT%’ and upper(vs_browser) not like ‘%MSN%’ and upper(vs_browser) not like ‘%MSNBOT%’ and upper(vs_browser) not like ‘%YAHOO%’ and upper(vs_browser) not like ‘%YAHOO! SLURP%’ and upper(vs_browser) not like ‘%ZYBORG%’ and upper(vs_browser) not like ‘%W3C%’ and upper(vs_browser) not like ‘%SURVEY BOT%’ and upper(vs_browser) not like ‘%ALEXA%’ and upper(vs_browser) not like ‘%BECOMEBOT%’ and upper(vs_browser) not like ‘%CRAWLER%’ and upper(vs_ip) not like ‘%127.0.0.1%’ and upper(vs_ip) not like ‘%82.69.92.174%’ and upper(vs_ip) not like ‘%24.83.205.219%’ and upper(vs_ip) not like ‘%60.34.15.16%’ and upper(vs_ip) not like ‘%64.158.138.48%’ and upper(vs_ip) not like ‘%64.210.196.198%’ and upper(vs_ip) not like ‘%64.246.161.30%’ and upper(vs_ip) not like ‘%65.214.36.60%’ and upper(vs_ip) not like ‘%65.88.178.10%’ and upper(vs_ip) not like ‘%66.147.154.3%’ and upper(vs_ip) not like ‘%66.151.189.7%’ and upper(vs_ip) not like ‘%66.180.233.4%’ and upper(vs_ip) not like ‘%66.196.90.%%’ and upper(vs_ip) not like ‘%66.196.91.%%’ and upper(vs_ip) not like ‘%66.249.64.79%’ and upper(vs_ip) not like ‘%66.250.128.131%’ and upper(vs_ip) not like ‘%66.98.170.93%’ and upper(vs_ip) not like ‘%68.142.249.160%’ and upper(vs_ip) not like ‘%68.142.249.201%’ and upper(vs_ip) not like ‘%68.142.250%’ and vs_os!=” and vs_browser!=” and 1 order by vs_date desc LIMIT 70110, 10;

That is one hell of a query. And I quickly found out that MySQL won’t use indexes that use database functions like upper(), or use indexes with ‘not like’ or anything with %’s around it. So that really throws this query out of the question, unless it can be re-written.

So the first thing I did was found out where in the PHP code this query was being executed, which turned out to be in the plugins/wp_jvisitors.php file. I searched for upper and found right where it was. What I noticed was it shouldn’t have to translate the vs_ip into an upper because vs_ip stores an IP address, it should be the same all the time, so I took that out. Then I started checking vs_browser for GOOGLE BOT, MSNBOT and all the matches in the query and realized it wasn’t matching anything. This query didn’t even need all the ‘not like’ matching and removing it would most likely speed the query up. Then a further look and the top of the file showed some configuration settings:

// Enter the IP you want to prevent from being listed
// Separate values with comma ‘,’. Set FALSE to disable blocking
$jBlockIP = “127.0.0.1,82.69.92.174,24.83.205.219,60.34.15.16,64.158.138.48,64.210.196.198,64.
246.161.30,65.214.36.60,65.88.178.10,66.147.154.3,66.151.189.7,66.180.233.4,66.196.90.%,66.196.91.%,66.249.64.79,66.
250.128.131,66.98.170.93,68.142.249.160,68.142.249.201,68.142.250″;

// Enter the search engine bot names (identified as Browser in jVisitors) you want to prevent from being listed
// Separate values with comma ‘,’. Set FALSE to disable blocking
$jBlockBrowser = “Google,Google Bot,Bot,MSN,MSNBot,Yahoo,Yahoo! Slurp,ZyBorg,W3C,Survey Bot,Alexa,B
ecomeBot,Crawler”;

All I had to do was set these to false to not use those options and now I see no more slow queries. So if you ever click on the “Total Visitors” link at the bottom of my blog, you may notice it’s a bit snappier now :)

Quick wins with MySQL

Wednesday, September 19th, 2007

Working at Joyent I get to see many different types of MySQL setups whether it’s replicated, clustered, load balanced or just a single instance. Each one runs a lot differently than the other so they all require different settings. Even though each one needs custom tuning, there are a few performance wins you can get with almost any MySQL database:

- If you are using MyISAM tables, make sure you have a good key_buffer size. If you are using InnoDB tables, make sure you are using a good innodb_buffer_pool_size. If you don’t have either of these set they default at only 8MB. Bumping these values up can decrease disk IO and make your database run a lot smoother. So many times I’ve seen these set to such low or default values with huge DB’s.

- Use indexes. Enable the slow query log and look for queries that show up most often, then learn the explain command on them.

- Check Created_tmp_disk_tables in show status; This value usually goes in hand with huge queries that aren’t using indexes. If it keeps increasing it means you are having to create on disk files to execute the query. Consider increasing tmp_table_size or putting your disk files on a TMPFS by setting tmpdir = /tmp.

- Use the query cache if it can help you. Check the query_cache_size. Check how well it is performing. You can do this by doing a ’show status’ and then using this formula:

qcache_hit_ratio = qcache_hits / (qcache_hits + qcache_inserts + qcache_not_cached)

If there are a lot of Qcache_lowmem_prunes, that means your query cache size isn’t big enough.

- If Opened_tables is increasing you probably have table_cache too small.

- If you have a lot of connections, check Threads_created in show status; If it keeps increasing, you need to up the thread_cache_size in my.cnf. The max connections in MySQL are default at 200. You can easily check how many connections you have by doing a prstat in Solaris and looking at the number of LWP (MySQL starts 10 LWP when it starts and then 1 LWP per connection). If you have a lot of connections it can eat up CPU having to create the threads over and over.

T2000 Apache/PHP/MySQL

Friday, March 2nd, 2007

An update to my previous post, here are some results for Apache/PHP/MySQL. I used Apache 2.2.3 (prefork), PHP 5.2.1, MySQL 5.0.27, and APC 3.0.13. These were custom compiled pretty much the same options the CoolStack uses, with Sun Studio 11.

APC is the PHP caching extension for PHP, and the apc.stat setting in the php.ini tells it to check if the php file has changed and if it has, recompile it and cache it. Using apc.stat on or off didn’t seem to make much of a difference. These tests were done with apc.stat on. For Apache I have mem_cache turned on as well.

5 rows and 1 column of MySQL returning:

# ./ab -n 50000 -c 500 http://x.x.x.x:30000/test.php
This is ApacheBench, Version 2.0.41-dev < $Revision: 1.121.2.12 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 2006 The Apache Software Foundation, http://www.apache.org/

Server Software: Apache/2.2.3
Server Hostname: x.x.x.x
Server Port: 30000

Document Path: /test.php
Document Length: 67 bytes

Concurrency Level: 500
Time taken for tests: 4.793971 seconds
Complete requests: 50000
Failed requests: 48583
(Connect: 0, Length: 48583, Exceptions: 0)
Write errors: 0
Total transferred: 11475208 bytes
HTML transferred: 823930 bytes
Requests per second: 10429.77 [#/sec] (mean)
Time per request: 47.940 [ms] (mean)
Time per request: 0.096 [ms] (mean, across all concurrent requests)
Transfer rate: 2337.52 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 4 3.8 4 30
Processing: 15 41 59.6 29 1036
Waiting: 14 40 59.1 28 1032
Total: 23 46 59.7 33 1042

36 rows and 6 columns of MySQL returning:

Requests per second: 10448.16 [#/sec] (mean)

134 rows and 7 columns of MySQL returning:

Requests per second: 801.05 [#/sec] (mean)

Performance really degrades on the 134 row test and I think MySQL is the bottleneck here. More to come on this when I get MySQL recompiled.

UPDATE: This is the results without APC cache turned on at all, with 36 rows and 6 columns:

Requests per second: 2242.18 [#/sec] (mean)

As you can see APC cache helps a lot, it went from 10,000 req/sec to 2,000 req/sec.

Â