Thursday, August 24, 2006

MySQL Benchmarking 2

In my last post MySQL Benchmarking 1, I was installing Sysbench on a Solaris 10 machine. However I stopped at
./configure
because I was getting compiler errors due to the SUNWsprot and SUNWsprot Sun OS packages not being installed.

After getting them installed on the server I tried to run the ./configure again and this time although the compile process went a little further, it stopped after throwing the following errors.
checking how to run the C preprocessor... /lib/cpp
configure: error: C preprocessor "/lib/cpp" fails sanity check
See `config.log' for more details.

Tuesday, August 22, 2006

MySQL Benchmarking 1

From time to time every MySQL DBA needs to benchmark queries. Fortunately there are many benchmarking tools available. In this and future posts I will walk you through using various MySQL benchmarking tools.



Sysbench
After downloading Sysbench, uncompress and extract files from the archive:
gunzip sysbench-0.4.7.tar.gz
tar -xvf sysbench-0.4.7.tar

Then navigate to the sysbench-0.4.7 directory and run
./configure
to configure the package.
[root@db32:/home/fmashraqi/install/bench/sysbench-0.4.7] ./configure
checking build system type... sparc-sun-solaris2.10
checking host system type... sparc-sun-solaris2.10
checking target system type... sparc-sun-solaris2.10
checking for a BSD-compatible install... config/install-sh -c
checking whether build environment is sane... yes
checking for gawk... no
checking for mawk... no
checking for nawk... nawk
checking whether make sets $(MAKE)... no
checking whether to compile with MySQL support... (cached) yes
checking whether to compile with Oracle support... (cached) no
checking whether to compile with PostgreSQL support... (cached) no
checking for style of include used by make... none
checking for gcc... no
checking for cc... no
checking for cc... no
checking for cl... no
configure: error: no acceptable C compiler found in $PATH
See `config.log' for more details.

Notice we got an error about no acceptable C compiler being found. To resolve this situation we need to add the path to gcc (or cc) to $PATH. So we open .bashrc file and export new value of $PATH
PATH=$PATH:/usr/local/bin:.
export PATH

Now to apply changes to our current shell, we "dot" the bashrc file.
 . /.bashrc

Running "gcc -v" now produces
 Reading specs from /usr/local/lib/gcc-lib/sparc-sun-solaris2.10/3.3.2/specs
Configured with: ../configure --with-as=/usr/ccs/bin/as --with-ld=/usr/ccs/bin/ld --disable-nls
Thread model: posix
gcc version 3.3.2


Yay, we can now proceed with running ./configure.

To be continued...

Monday, August 21, 2006

Storing Passwords in MySQL

Securing plain text passwords in MySQL is NEVER a good idea. As a DBA you should take great care in protecting the users' information. Fortunately MySQL provides you with several options to protect passwords.

After a quick scan of the manual, you may be tempted to store the password by applying the password function to it which is NOT a good idea. MySQL itself advises against using PASSWORD to manage application passwords.

Instead of using PASSWORD(), we can use SHA1 or MD5. Unfortunately exploits for both of these encryption functions have been quite common these days. Still, SHA1 or MD5 keep your password more protected than storing them as plain text.

You can apply SHA1 algorithm to a password string:

mysql>  SELECT SHA1('mysecretpassword');
+------------------------------------------+
| SHA1('mysecretpassword')                 |
+------------------------------------------+
| 08cd923367890009657eab812753379bdb321eeb |
+------------------------------------------+
1 row in set (0.00 sec)


Since SHA is an alias for SHA1, it produces the same result
mysql>  SELECT SHA('mysecretpassword');
+------------------------------------------+
| SHA('mysecretpassword')                  |
+------------------------------------------+
| 08cd923367890009657eab812753379bdb321eeb |
+------------------------------------------+
1 row in set (0.00 sec)


To store passwords encrypted with SHA1, we need to be able to store 40 characters.
mysql> SELECT CHARACTER_LENGTH(SHA1('mysecretpasswordsssssss'));
+---------------------------------------------------+
| CHARACTER_LENGTH(SHA1('mysecretpassword')) |
+---------------------------------------------------+
|                                                40 |
+---------------------------------------------------+
1 row in set (0.00 sec)


On the other hand, to store passwords encrypted with MD5, we need the column to be able to hold 32 characters.

mysql>  SELECT MD5('secretpassword');
+----------------------------------+
| MD5('secretpassword')            |
+----------------------------------+
| 2034f6e32958647fdff75d265b455ebf |
+----------------------------------+
1 row in set (0.00 sec)

mysql>  SELECT CHARACTER_LENGTH(MD5('secretpassword'));
+-----------------------------------------+
| CHARACTER_LENGTH(MD5('secretpassword')) |
+-----------------------------------------+
|                                      32 |
+-----------------------------------------+
1 row in set (0.00 sec)


Using MD5 in your application is easy. Here's how your queries will need to be to take advantage of encryption offered by MD5.


First, let's create a table:
mysql> create table user_md5 (user_name VARCHAR(16), password VARCHAR(32));
Query OK, 0 rows affected (0.00 sec)


Now let's insert a record with MD5 applied to the password field.
mysql> INSERT INTO user_md5 VALUES ('member1',MD5('secretpassword') );
Query OK, 1 row affected (0.00 sec)


Finally, let's see if it all works when we try to authenticate a user.
mysql> SELECT * FROM user_md5 WHERE user_name='member1' AND password=MD5('secretpassword');
+-----------+----------------------------------+
| user_name | password                         |
+-----------+----------------------------------+
| member1   | 2034f6e32958647fdff75d265b455ebf |
+-----------+----------------------------------+
1 row in set (0.00 sec)


SHA1
Here's how we can store passwords encrypted with SHA1 algorithm, which is "cryptographically more secure" than MD5. Note that we are changing the definition of password field so it can store 40 characters.
mysql> create table user_sha1 (user_name VARCHAR(16), password VARCHAR(40));
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO user_sha1 VALUES ('member1',SHA1('secretpassword') );
Query OK, 1 row affected (0.00 sec)

mysql> SELECT * FROM user_sha1 WHERE user_name='member1' AND password=SHA1('secretpassword');
+-----------+------------------------------------------+
| user_name | password                                 |
+-----------+------------------------------------------+
| member1   | edbd1887e772e13c251f688a5f10c1ffbb67960d |
+-----------+------------------------------------------+
1 row in set (0.00 sec)


Since the exploits for MD5 and SHA1, especially in the form of dictionary attacks, are out there, using them may not provide us the level of encryption and protection we desire. Fortunately, MySQL offers


AES
If you would like to implement AES (Advanced Encryption Standard) encryption, you will need to specify the password field to be of type BLOB. AES functions encode with a 128-bit key length which can be changed by modifying source and then recompiling MySQL.

mysql> create table user_aes (user_name VARCHAR(16), password BLOB);
Query OK, 0 rows affected (0.00 sec)

Now to insert a record, we will call AES function and in addition to providing the secret password to be protected, we will also provide the key_str.
mysql> INSERT INTO user_aes VALUES ('member1',AES_ENCRYPT('secretpassword','my_secret_key_to_encrypt') );
Query OK, 1 row affected (0.02 sec)

Now to authenticate a user, we encrypt the password which the user is using to login with the same key_str as we used before. Then we can match it against the records in the table.
mysql> SELECT * FROM user_aes WHERE user_name='member1' AND password=AES_ENCRYPT('secretpassword','my_secret_key_to_encrypt');
+-----------+------------------+
| user_name | password         |
+-----------+------------------+
| member1   | ├▒Ga·`·
                    +'U |
+-----------+------------------+
1 row in set (0.00 sec)


Since AES provides reversible encryption (provided you have the key_str), we can obtain the password in plain text format.
mysql> SELECT AES_DECRYPT(password, 'my_secret_key_to_encrypt') AS unencrypted FROM user_aes ;
+----------------+
| unencrypted    |
+----------------+
| secretpassword |
+----------------+
1 row in set (0.00 sec)


According to MySQL AES functions (AES_ENCRYPT() and AES_DECRYPT()) were added in MySQL 4.0.2 and are currently the most cryptographically secure encryption functions in MySQL.

Note: A common error is to use AES function to encrypt instead of AES_ENCRYPT. If you do so you'll get the following error:
ERROR 1305 (42000): FUNCTION db_name.AES does not exist


Now we can see the table status for each of the table to see the differences in Avg_row_length etc.

Using SHA1
mysql> show table status like 'user_sha1' \G
*************************** 1. row ***************************
           Name: user_sha1
         Engine: MyISAM
        Version: 10
     Row_format: Dynamic
           Rows: 1
 Avg_row_length: 56
    Data_length: 104
Max_data_length: 281474976710655
   Index_length: 1024
      Data_free: 48
 Auto_increment: NULL
    Create_time: 2006-08-21 13:38:10
    Update_time: 2006-08-21 14:06:43
     Check_time: NULL
      Collation: latin1_swedish_ci
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.00 sec)


Using MD5
mysql> show table status like 'user_md5' \G
*************************** 1. row ***************************
           Name: user_md5
         Engine: MyISAM
        Version: 10
     Row_format: Dynamic
           Rows: 1
 Avg_row_length: 48
    Data_length: 48
Max_data_length: 281474976710655
   Index_length: 1024
      Data_free: 0
 Auto_increment: NULL
    Create_time: 2006-08-21 13:58:59
    Update_time: 2006-08-21 14:01:55
     Check_time: NULL
      Collation: latin1_swedish_ci
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.00 sec)


For table with AES encryption
mysql> show table status like 'user_aes' \G
*************************** 1. row ***************************
           Name: user_aes
         Engine: MyISAM
        Version: 10
     Row_format: Dynamic
           Rows: 1
 Avg_row_length: 32
    Data_length: 32
Max_data_length: 281474976710655
   Index_length: 1024
      Data_free: 0
 Auto_increment: NULL
    Create_time: 2006-08-21 14:08:22
    Update_time: 2006-08-21 14:17:25
     Check_time: NULL
      Collation: latin1_swedish_ci
       Checksum: NULL
 Create_options:
        Comment:
1 row in set (0.00 sec)


If MySQL has been configured with SSL support, you can use the DES functions which use the Triple-DES algorithm to encrypt a string with the supplied key.

The syntax for these functions is
DES_DECRYPT(crypt_str[,key_str])
DES_ENCRYPT(str[,{key_num|key_str}])


I will discuss them in more detail in the near future.

So now that you have read this post do you mind sharing what algorithm do you use to store passwords in your application?

--Frank

References:
SHA1: Secure Hash Algorithm version 1.0
SHA functions
MD5: Message Digest Algorithm 5
RFC 1321: The Message Digest Algorithm
AES: Advaned Encryption Standard
Storing Passwords in MySQL
Storing passwords safely in mysql
Keeping passwords secure with MySQL

Friday, August 11, 2006

mysqld restarted: MyISAM tables crashing frequently

When MyISAM tables start crashing somewhat frequently, you can be sure that's not a good sign.

The first step in investigating the issue is to analyze the MySQL error log file (usually host-name.err). There you may find messages indicating that either the slave thread was killed, if the replication was enabled, or that mysqld was restarted just before the corruption happened.

If the reason for corruption was that the slave thread was killed, you can avoid it by ensuring that you issue "STOP SLAVE" first before shutting down MySQL.

On the other hand, if the reaason for MyISAM corruption was the "mysqld restarted" messages that you are seeing in the error log files, then you need to find out whether the current parent process of mysqld is the same as when MySQL was last restarted manually/properly.


If the parent process is new then you may need to investigate whether there is a cron job or a monitoring script that is automatically restarting MySQL leading to corruption. In the event that there is such a script, you should modify it so it stops the slave threads explicitly by issuing "STOP SLAVE".

If the parent process is the same as when MySQL was restarted manually then another explanation is that some query is causing mysqld to fail which is in turn causing "mysqld restarted" messages to appear in the log file.

Here's what you can do to troubleshoot and investigate instances like this:

1. Check all MyISAM tables not just the ones that have been crashing on db9. It may be the case that there is some table that is corrupted but not being reported by MySQL and queries on that table are triggering a crash.

2. Enable full query logging to investigate and find out which exact query is causing MySQL to fail and producing the "mysqld restarted" messages. The drawback is that you will need ample disk space for each and every query executed on the server to be logged.

3. Enable automatic recovery by using "myisam-recover" server startup option (this will not help us troubleshoot the crashes, however it will repair the tables automatically if they were marked as 'not closed properly' or 'crashed'). If we enable automatic recovery, we need to decide whether MySQL should keep a backup of the original corrupted data file (can use a lot of disk space).

The ideal configuration for automatic recovery is to use BACKUP with FORCE options which will cause MySQL to forcefully repair the corrupted table (even if the repair would cause some rows to be lost). MySQL will however keep a backup of the corrupted table if this option is used.

Anything else that I missed that can be done to investigate and troubleshoot frequent MyISAM corruptions?

Thursday, August 10, 2006

MySQL Error 1032: A somewhat ambiguous error?

Today I experienced error 1032 on one of my slave servers which has MyISAM tables. The full error message was:

060810 14:46:45 [ERROR] Slave: Error 'Can't find record in 'group_upload_counter'' on query. Default database: 'db1'. Query: 'insert into group_counter (...) values('username', now(), 1, now())', Error_code: 1032

I invoked "perror 1032" to see what the error was all about but got the following message:

-bash-3.00$ perror 1032
Illegal error code: 1032


I asked on the #mysql and Jay and Roland were quick to respond. We all researched for some time but the only thing we were leading to was something about running out of disk space which wasn't the issue in this case. So I decided to check the table for corruption and guess what, the table was indeed corrupt and was fixed with a simple REPAIR TABLE invocation. After that I started the slave and it caught up fully.

Also see: error codes

Monday, August 07, 2006

Renaming a stored procedure

Yesterday night I was working to create stored procedures to simplify my work to manage hierarchical data in MySQL. I had seen Mike Hillyer's excellent presentation at MySQL UC and truly enjoyed it. BTW, he has put up a nice guide on dev.mysql.com in case you are interested (links below).

While working with the stored procedures (will post them soon) I wanted to rename one of the stored procedure I created. So I searched for it and found that we should be able to rename it using the ALTER PROCEDURE command however I couldn't figure out the exact syntax to rename it. I did drop the SP and recreated it but wanted to find out how can I rename an existing SP. Any ideas, tips, hints are appreciated.

Selecting NULL

So I got a question from a developer today who was trying to SELECT NULL values from a table. As I have been asked about this many times in the past, I decided to write a little post about it.

mysql> SHOW CREATE TABLE testing_null \G
*************************** 1. row ***************************
Table: testing_null
Create Table: CREATE TABLE `testing_null` (
`id` int(11) unsigned NOT NULL auto_increment,
`mycolumn` varchar(12) default NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
1 row in set (0.00 sec)



mysql> INSERT INTO testing_null (mycolumn) VALUES('ho'), (NULL), (NULL);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0

mysql> SELECT * FROM testing_null;
+----+----------+
| id | mycolumn |
+----+----------+
| 1 | ho |
| 2 | NULL |
| 3 | NULL |
+----+----------+
3 rows in set (0.00 sec)


Now the wrong way to query the table to find the NULL values (Some examples are really ridiculous to test for NULL but presented here any way):


mysql> SELECT * FROM testing_null WHERE mycolumn='NULL';
Empty set (0.00 sec)

mysql> SELECT * FROM testing_null WHERE mycolumn=NULL;
Empty set (0.00 sec)

mysql> SELECT * FROM testing_null WHERE mycolumn=\N;
Empty set (0.00 sec)

mysql> SELECT * FROM testing_null WHERE !mycolumn;
+----+----------+
| id | mycolumn |
+----+----------+
| 1 | ho |
+----+----------+
1 row in set (0.00 sec)

mysql> SELECT * FROM testing_null WHERE !mycolumn != NULL;
Empty set (0.00 sec)

mysql> SELECT * FROM testing_null WHERE mycolumn != NULL;
Empty set (0.00 sec)

mysql> SELECT * FROM testing_null WHERE mycolumn != '' ;
+----+----------+
| id | mycolumn |
+----+----------+
| 1 | ho |
+----+----------+
1 row in set (0.00 sec)
mysql> SELECT * FROM testing_null WHERE mycolumn =0 ;
+----+----------+
| id | mycolumn |
+----+----------+
| 1 | ho |
+----+----------+
1 row in set (0.00 sec)


The problem is that while NULL represents a value that is unknown or missing, we cannot use the comparison operators to determine whether a value is NULL. The correct way to test for NULL or to query based on NULL is to use the "IS NULL" and "IS NOT NULL" operators:

mysql> SELECT * FROM testing_null WHERE mycolumn IS NULL ;
+----+----------+
| id | mycolumn |
+----+----------+
| 2 | NULL |
| 3 | NULL |
+----+----------+
2 rows in set (0.00 sec)
mysql> SELECT * FROM testing_null WHERE mycolumn IS NOT NULL ;
+----+----------+
| id | mycolumn |
+----+----------+
| 1 | ho |
+----+----------+
1 row in set (0.00 sec)


Also see:

Saturday, August 05, 2006

Take care of disk paritions filling up

One of our MySQL db servers was filling up the disk space pretty quickly due to normal reasons. Since we use 3Par for storage purposes, we created a larger partition and then linked the db directory to point to the new partition.

ln -s /path/to/new/mysql/data/partition/db_1 /path/to/mysql/data/db_1



The sequence of steps needed were:
  • create a new parition
  • initialize and clear filesystem with newfs before using for the first time.
  • Create appropriate entries in /etc/vfstab
  • Create the directory
  • mount (mount -a)
  • Copy data [tar cf - db_1 | ( cd /path/to/new/mysql/data/guestbook; tar xvf - )]
  • invoke "fuser -k /path/to/old/mysql/data" to view the processes using the filesystem you just unmounted
  • unmount the old partition
  • backup the data that was on old file system.
  • create a soft link that points to the new partition.
  • confirm
Some references
  • luxadm: Sun Enterprise Network Array (SENA) administration program
    • use it with the "-e" option for expert mode
    • get the "using luxadm" guide
    • example:
      • luxadm -e dump_map /devices/pci@1d,700000/SUNW,qlc@1/fp@0,0:fc


  • Adding filesystem under Solaris