MySQLのハマりどころ

前回に引き続き、テーマはMySQLです。

パラメータのチューニングは、
MySQL チューニング」で検索すれば、山ほど出てくるので、
そちらに譲るとして、もっと初歩的なところに触れたいと思います。
(特に、http://dsas.blog.klab.org/archives/50860867.html には私も大変お世話になりました)

skip-resolve-nameは無効にしよう

まずは、my.cnfのパラメータ「skip-name-resolve」についてです。
以下は、Donutsで使っている標準的な設定で、一番下の行にあります。

[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
user=mysql

log-slow-queries
long_query_time=1
#log-queries-not-using-indexes
#log=/var/lib/mysql/query.log

# server-id=40
# log-bin=/var/lib/mysql_log/binlog
# log-slave-updates
# expire_logs_days = 1

max_connections=100
thread_cache_size=100

key_buffer_size=64M
myisam_sort_buffer_size=1M
sort_buffer_size=1M
read_buffer_size=1M

innodb_file_per_table
innodb_log_file_size=2000M
innodb_log_buffer_size=32M


innodb_buffer_pool_size=4000M
innodb_additional_mem_pool_size=20M
innodb_flush_log_at_trx_commit=0
innodb_support_xa=0
#innodb_flush_method=O_DIRECT
skip-innodb_doublewrite
skip-innodb_checksums 

skip-name-resolve 

以前から、MySQLへの新規接続が時々、遅いWebサーバーがあり、
疑問に思っていたところ、状況がわかってきました。

DBサーバーのhostsファイルに、
IPアドレスが書かれているWebサーバーは接続が早く、
IPアドレスが書かれていないWebサーバーは接続が遅かったのです。

そして、根本的な原因は、skip-resolve-nameを設定していたことでした。


skip-resolve-nameは、接続を受けた際に、
認証の目的でクライアントのIPアドレスを逆引きする機能です。

接続が遅かった原因は、hostsファイルにIPアドレスが書かれていなかったため、
DNSサーバーで名前解決をしようとしていたことによるものでした。
詳細はこちら: http://dev.mysql.com/doc/refman/5.5/en/dns.html


あまりに基本的すぎるためか、このことについて書いてある情報源が少ないです。
しかし、大量の接続を行う場合は、これを無効にしないと話になりません。
自前でDNSサーバーを運用していれば、まだ良いのですが、
プロバイダのDNSサーバーを使用している場合は、大迷惑。

最終的にはアクセスを禁止されて、「なぜか、DBサーバーに接続できない」事件に発展します。
なぜ、このような機能がデフォルトで有効なのか理解に苦しみます。


MySQL5.0で、無意味な文言が記録される

クエリのチューニングには欠かせない slowqueryのファイルに時々、SQL文が記録されず、

# administrator command: Prepare Execute;

という文言だけが記録されることがあるようです。
MySQL 5.0 だけで 5.5 では起きていないので、早くバージョンアップする必要があるということかもしれませんが。

また、他にもlong_query_time に1秒以下を指定できなかったりもするので、やはり、バージョンアップしたいところです。
(Zend_Db_Profilerを使用したプロファイリングについては次回紹介します)。


ちなみに、Donutsでは、最初に使用し始めたMySQLのバージョンが、CentOS5のデフォルトの5.0.77 であったため、
いまだに5.0.77を使用しているDBサーバーがあります。


MySQL5.5で、InnoDBがtmpfs上で動作しない

ところが、MySQL5.5に移行することに踏み切れない理由がありました。
tmpfs上でInnoDBを動かそうとすると、MySQLが落ちてしまうのです。
http://bugs.mysql.com/bug.php?id=58421

tmpfs にはasynch I/Oが無いのに、asynch I/Oでアクセスしようとして、
Kernelに怒られ、落ちてしまうようです。
1年近く前に、BugDBに挙がっているので、バグフィックスが待ち遠しいところです。


MySQL5.5では、unsingedカラムの更新に注意

MySQL 5.5では、unsingedを指定してあるカラムを更新するupdate 文で、
式の評価途中、負になる場合、エラーとなってしまうようです。
(以下の例では、「col1 - 2」の部分が負になります。)

mysql> create table testtable2 (col1 int unsigned);
mysql> insert into testtable2 (col1) values (1);
mysql> update testtable2 set col1=col1-2+10;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(`t`.`testtable2`.`col1` - 2)'

5.0 では何事もなく更新されるのですが…
式の評価途中、負になる場合は、明示的にキャストしてやる必要があるようです。

mysql> update testtable2 set col1=cast(col1 as signed)-2+10;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from testtable2;
+------+
| col1 |
+------+
|    9 |
+------+
1 row in set (0.00 sec)

UPDATE文ではWHERE句の書き方で速度が違う?

SQLのWHERE句で、複数のキーを指定して、データを取得する際、
1) SELECT * FROM testtable WHERE col1=1 and col2=1;
2) SELECT * FROM testtable WHERE (col1, col2) = (1, 1);
という書き方があり、どちらも全く同じ意味を指すものと思っていました。

実際、SELECT 文では、実行結果も、

mysql> CREATE TABLE testtable(col1 INT not null, col2 INT not null, col3 INT, KEY(col1, col2));
mysql> INSERT INT testtable (col1, col2) VALUES (1,1), (2,2), (3,3), (4,4), (5,5);

mysql> SELECT * FROM testtable WHERE (col1, col2) = (1, 1);
+------+------+------+
| col1 | col2 | col3 |
+------+------+------+
|    1 |    1 | NULL | 
+------+------+------+

EXPLAIN の結果も、

mysql> DESC SELECT * FROM testtable WHERE (col1, col2) = (1, 1)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: testtable
         type: ref
possible_keys: col1
          key: col1
      key_len: 8
          ref: const,const
         rows: 1
        Extra: 

期待通り、インデックスを使用していて問題なさそうです。

UPDATE でも一見同じように動作しているように見えます。

mysql> UPDATE testtable SET col3=1 WHERE (col1, col2) = (1, 1);
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

しかし、slow-queries (log-queries-not-using-indexes も有効にしてあります) を見ると

# Query_time: 0  Lock_time: 0  Rows_sent: 0  Rows_examined: 0
use wheretest;
UPDATE testtable SET col3=1 WHERE (col1, col2) = (1, 1);

インデックスを使ってくれていないようです。(環境によってはRows_examined にも全行の行数が出たのですが、今回記事にするに当たっての追試では0 になっていました)

上の例では、全行検索しても5行程度なので実質的に速度差はありませんが、データを655360件に増やして実行してみると、結果は0秒、0.16秒となり、明らかな差が出てしまいました。

mysql> UPDATE testtable SET col3=1 WHERE col1 = 1 and  col2 = 1;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> UPDATE testtable SET col3=1 WHERE (col1, col2) = (1, 1);
Query OK, 0 rows affected (0.16 sec)
Rows matched: 1  Changed: 0  Warnings: 0

ストレージエンジンをInnoDB/MyISAMMySQLのバージョンを 5.0.77/5.5.13 と変えてみたものの結果は変わりませんでした。

まだソースまでは確認していませんが、何か釈然としない結果になってしまいました。とりあえず、原因が分かるまでは (col1, col2) = (1, 1) の書き方は封印しています。

急激に落ちたネットワークのパフォーマンス

初回ということで、サービスイン直後に最初に陥った問題について紹介します。 結論としては、「ネットワーク周りの設定を見直そう」に尽きます。


低負荷のときには正常に動いているかのように見えていた環境が、ネットワーク負荷の高まりにより、急に動作しなくなることがよくあります。
Donutsの場合、まさにこの状況が発生しました。

このようなときは、まずNICの状況を把握し、NICの設定内容が使用している回線に合った設定になっているか、auto-neg、全二重/半二重、通信速度がプロバイダが指定している値と合っているか確認します。

NICの状態を確認する

最初に、ifconfigで現状を確認。

# ifconfig
eth0      Link encap:Ethernet  HWaddr xx:xx:xx:xx:xx:xx  
          inet addr:192.168.xx.xx  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: xxxx/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1236623 errors:7434 dropped:7434 overruns:0 frame:0
          TX packets:1145319 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:463793883 (442.3 MiB)  TX bytes:469182630 (447.4 MiB)

ifconfig で errors, dropped, overruns, frame に0以外の数値が出ていれば、まず間違いなく、何かパラメータがおかしいです。(各項目の詳しい意味については他に任せますが、ルータのマニュアル等が詳しいです)

上記の場合、「packets:1236623 errors:7434 dropped:7434」となっており、何か起こっています。そこで、NICの設定状況を確認します。

NICの設定確認と変更を行う

現在の設定の確認と変更は ethtool で行います。

# ethtool eth0
Settings for eth0:
        Supported ports: [ TP ]
        Supported link modes:   10baseT/Half 10baseT/Full 
                                100baseT/Half 100baseT/Full 
                                1000baseT/Full 
        Supports auto-negotiation: Yes
        Advertised link modes:  Not reported
        Advertised auto-negotiation: No
        Speed: 100Mb/s
        Duplex: Full
        Port: Twisted Pair
        PHYAD: 1
        Transceiver: internal
        Auto-negotiation: yes
        Supports Wake-on: g
        Wake-on: d
        Link detected: yes

プロバイダからは Autoneg は無効と指定されていたのに、yes になっていました。そのせいでDuplex が Full と Half でブレ、結果としてパケットロスが発生していたようです。

# ethtool -s eth0 speed 100 autoneg off duplex full

として、Autonegをoffに変更しましたた。しばらくしてから、もう一度 ifconfig でerrors がそれ以上増えていないことを確認し、一安心。

1000BASE-T の場合は仕様上 Auto-negotiation は常に on なので、

# ethtool -s eth0 speed 1000 autoneg off

などの設定はできません。

Offload処理をチェックする

ついでにOffload処理についても見ておきましょう。Offload処理は、CPUに代わってNICが処理を行うことでCPU負荷を下げる機能なのですが、環境によっては高負荷の時のパケットロスにつながることがあるようで、Donutsのサーバでは全てoff にしてあります。

# ethtool -k eth0
Offload parameters for eth0:
rx-checksumming: off
tx-checksumming: off
scatter-gather: off
tcp segmentation offload: off
udp fragmentation offload: off
generic segmentation offload: off
generic-receive-offload: off

全てoffにする設定はこちら↓

# ethtool -K eth0 rx off tx off tso off

余談ですが、ethtool のオプションは、

  小文字⇒内容確認、大文字⇒設定

である一方で、基本設定(generic options)だけは、

  オプションなし⇒内容確認、「 -s 」⇒設定

であるのは私にはどうも覚えにくくて…

ネットワーク周りの設定を見直そう

このブログでは、株式会社Donutsのサーバ運用でハマった点を中心に紹介していきます。

Donutsではモバゲーで「暴走列伝 単車の虎」というソーシャルゲームを提供していて、現在月間11億PVがあります。

2011年1月末にリリースした時には月間300万PVであったアクセス数が、飛躍的に増える過程で発生した様々な問題を取り上げていくので、分野があちこち飛んで読みにくいかもしれませんが、その点はご容赦願います。