閱讀763 返回首頁    go 阿裏雲 go 技術社區[雲棲]


pdo,mysql 中binlog日誌記錄的一個 bug

最近發現數據庫同步總是出問題,最詭異的時,主從數據庫寫入的數據不一樣,我勒個去。程浩同學看了半天終於找到原因,原來是PDO的一個大坑,加上binlog的一個大坑。


首先聲明,這篇文章有很強的攻擊性,如果你利用這裏麵寫的東西攻擊,所造成的一切後果,自負!


       起因:
       2010/12/15 我的領導,突然要求我們開始折騰一下機器。主要的目的是,沒做備份的,做一下備份,單個的數據庫做主從,線上的機器要做一個能快速恢複的熱備份。經過檢查發現機器若幹台需要整理,於是開始一一處理,其他的還算順利,但發現了一個 數據庫的同步經常有問題,主要問題表現在做好主從同步以後,經過一段時間就會發現重複的插入,引起同步失效。
 
       排查過程:
       1、首先考慮數據本身就不一致,造成的同步失效。
       處理方法: 
       在一個周五的晚上 12 點,夜深人靜的時候,趁大家都熟睡的時候,直接對那台線上的數據庫 shutdown ,在回寫硬盤緩存之後,打包數據,從新同步。當天夜裏、及次日均未發現數據不同步的情況。

       2、本以為問題解決,結果在周一,再次發現數據同步失敗。
       處理方法: 
       考慮主從數據庫係統及版本不一致的情況。從裝係統,及數據庫,係統(CentOS 5.5) 數據庫 (Mysql-5.8.87) ,之後重新做的數據同步。

       3、 次日,悲劇在次發生,同步又掉了 …… 
       處理方法:
       現在有點茫然了,主從兩台機器,從係統到數據庫完全一樣,數據也保證沒有任何問題。為什麼就同步不起來呢。現在隻能考慮其他因素了。係統我使用 kickstar 裝的,數據庫是我自己製作的 rpm , mysql 的數據更是從一個 tar 包裏解出來的。為了方便測試,我把主庫放到了一個卷上,對從數據庫做了隻讀。清除所有日誌,從新同步了數據庫。並且求助於我的同事,張文亭、包鵬、李鎖住,讓他們和我共同觀察這個問題,果不出所料,在之後的一段時間內,同步在一次的失敗了。 經過數次的測試(因為有卷了,可以做卷影複製),我們發現了一個問題,每次同步失敗的原因都是因為同一個錯誤,就是重複的鍵值,而且這些錯誤都是出現在同一個庫的幾張表內。我把這個庫的數據 dump 出來,然後把庫刪除了,手工重建 庫 和 表 ,然後把數據導入,重新同步。

        4、 結果大家應該猜得到,同步依舊失效 ……
        處理方法:
        實在沒辦法了,隻好吧這個庫挪到另外的單獨的一台機器上,單獨觀察,其他剩餘的庫做了同步。

        5、 在那個庫挪走以後同步竟然神奇的好了,觀察了一周。
        處理方法:
        看來這問題就出在哪個被挪走的庫上麵了。於是把那個庫的結構單獨拿出來,做了主從,手工插入若幹條,依舊沒有出現任何問題,在把那個庫的數據也導入了,手工插入更新若幹條,也沒有出現問題。觀察使用了一周,一切正常。換到線上的同步一測試,不出半天同步又失效了 !~~難道這就是傳說中的人品問題?鑒於我最近沒幹啥虧心事,決定對這個東西出大招,一探究竟, 無奈之下,在網上請教了一下mysql 的大牛人物(葉金融),他也認為這可能是一個 mysql 的 bug ,於是就和我的同事李鎖住開始折騰。

       依次查找用到的 apache ,php ,zend,pdo 等。這真是不看不知道,一看嚇一跳!我發現這做運維的網管和程序員那就是一對天敵。看這些東西那叫一個鬱悶。
       首先查看 apache ,發現是使用 apache 的 proxy 模塊代理到另一個機器。
       到了那個機器我怎麼也找不到訪問的那個路徑,我不懂 php 但配置 apache 不菜啊,咋就沒有呢,這一通折騰才發現人家在那個目錄下寫了一個 .htaccess 又重定向了 !~
       這次總算找到那個 php 了,一搜索光 Insert 函數就有 4、5 個。這玩意太多,還是找 mysql_query() 。找了半天沒有~~,仔細一看用的是 PDO
       你用 PDO 你就 $var = new PDO(‘mysql:host=xx;dbname=xx;charset=gbk’,'xx’,”); 用吧,一找 new PDO 還沒有。
       一點點的找下去發現 人家用的是 Zend_Db::factory(),還搞了一個超複雜的對象。 $config->db->adapter,$config->db->config->toArray() 這裏咬牙、跺腳若幹次。
       總算找到正主了,找這個可真不容易: 訪問不直接訪問,用 proxy 弄跑了,弄到另一個機器上也不老老實實的訪問,整一個 302 跳轉了,php 鏈接 mysql 有現成的 mysql_query() 不用,非要調用 PDO ,PDO還不直接調用,要用 Zend 的框架調用。
       接下來的事情就是跟蹤 PDO Zend 調用過程,抓包查看交互的數據,根據許許多多的調試信息來看,終於發現了,這個 PDO 處理數據的方式比較特別。
       訪問 mysql server 的方式有兩種。
       1、 直接訪問模式
       2、 預處理模式
       先說說這兩個結構的區別,直接訪問就像我們用客戶端連接進數據那樣,標準的 sql 語句插入、更新、刪除和查詢。這個要求就是每個命令裏麵都要指明 表、字段、等信息。
       預處理就是:先告訴 mysql 一個表的結構,然後,後麵的全都按照這個表結構來,這樣就不用每次都發送 表、字段等信息了。這樣的優勢是大量的插入會快一點。特點是隻在第一次發送表結構。而不是每次都發送一遍,問題是 mysql binlog 裏麵不支持這種格式。
       這兩種方式比較起來,第一種 安全,第二種 快速 。第二種因為沒有表結構,所以當任何一個字段出現問題,就會造成所有的數據問題,而不像第一種,隻影響那一句。那個 PDO 使用的就是第二種方式,而且他錯誤的認為一切都是字符串,把所有的數據都轉換成 16 進製了。
       在第一次插入的時候 mysql 使用第二種方式插入數據,但 binlog 裏麵因為沒有這種結構,所以他自己把語句轉換成了 第一種模式,加上了表、及字段信息,但 mysql 不會對 int 形做相應的轉換,(這個在字符串表示中是沒有錯誤的),造成了記錄的日誌是按照字符串的方式記錄的。這樣在吧一個字符串插入 int 形就出現了插入的數據和日誌不一致的情況。要解決這個問題隻有 1、給mysql 寫一個補丁,解決這個問題。(現在功力不夠還寫不出來) 2、在我們公司禁用 預處理結構體方式的數據寫入。看來目前我們隻能使用第二種方法了。

       結論:
       總的來說,pdo 寫的有問題,mysql 的 log 記錄轉換的方式也存在問題。下麵是我寫的一個能夠觸發這個 bug 的代碼。
 
 
 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mysql.h>

#define INSERT_QUERY "INSERT INTO a(a) VALUES(?)"
#if !defined(MC68000) && !defined(DS90)
char *strmov(register char *dst, register const char *src)
{
  while ((*dst++ = *src++)) ;
  return dst-1;
}
#else

char *strmov(dst, src), char *dst, *src;
{
  asm(" movl 4(a7),a1 ");
  asm(" movl 8(a7),a0 ");
  asm(".L4: movb (a0)+,(a1)+ ");
  asm(" jne .L4 ");
  asm(" movl a1,d0 ");
  asm(" subql #1,d0 ");
}

#endif


int main(int argc, char **argv)
{
    if (argc != 2)
    {
       printf("%s digit\n",argv[0]);
       return(1);
    }
    char *server="localhost",*user="root",*password="";
    MYSQL *conn;
    MYSQL_RES *res;
    MYSQL_ROW row;
    conn = mysql_init(NULL);
    if (!mysql_real_connect(conn, server, user, password, "test", 0, NULL, 0))
    {
        fprintf(stderr, "%s\n", mysql_error(conn));
        exit(EXIT_FAILURE);
    }
    MYSQL_STMT *stmt = mysql_stmt_init(conn);
    mysql_stmt_prepare(stmt, INSERT_QUERY, strlen(INSERT_QUERY));

    MYSQL_BIND bind[1];
    memset(bind, 0, sizeof(bind));
    unsigned long length;

    char query[100] = {0};

    char *pos = query;
    strcpy(query,argv[1]);

    bind[0].buffer_type= MYSQL_TYPE_BLOB;
    bind[0].buffer= query;
    bind[0].is_null= 0;
    bind[0].length= &length;
  
/* Bind the buffers */
    mysql_stmt_bind_param(stmt, bind);
  
/* Supply data in chunks to server */
    mysql_stmt_send_long_data(stmt,0, pos, strlen(query));
    mysql_stmt_execute(stmt);
    mysql_stmt_close(stmt);
}

 

 

測試過程如下:

 

mysql -uroot -p <<'EOF'
CREATE DATABASE test;
USE test;
DROP TABLE IF EXISTS a;
CREATE TABLE a (
  a int(11) NOT NULL COMMENT 'id',
  UNIQUE KEY a (a)
) ENGINE=MyISAM;
EOF

# 製作同步數據庫,省略代碼若幹條

gcc -g $(mysql_config --cflags --libs) -o mysql_test mysql_test.c
./mysql_test 12345
./mysql_test 23456

# 現在查看你的從數據庫已經不同步了。


以上轉自:https://blog.chinaunix.net/uid-8746761-id-2015321.html

下麵是我們的php測試代碼:

<?php
$dsn = "mysql:host=localhost;dbname=wanke";
$db = new PDO($dsn, 'wanke', 'wanke');
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES,false); //必須加
$db->exec('SET NAMES gbk'); //必須加
//$sth = $db->prepare("DELETE FROM `atest` WHERE t2=:t1");
//$sth = $db->prepare("INSERT INTO atest (`t1`,`t2`) VALUES(?,?)"); 
$sth = $db->prepare("INSERT INTO atest (`t1`,`t2`) VALUES(:a,:b)"); //必須是這種形式,不能是問號的
//$sth->bindValue(':t1','666777',PDO::PARAM_STR);
//$sth->bindValue(1,67890);
//$sth->bindValue(2,'ttttttasdfasttttt');
$sth->bindValue(':a','6784444');
$sth->bindValue(':b','ttttttasdfasttttt');
$count = $sth->execute();
echo $count;
$db = null;
?>

另外一種解決方案

主庫使用:binlog_format="ROW" 模式可以避免這種情況的發生,不用修改PDO屬性。mysql 默認的binlog 使用的是  Statement 模式



最後更新:2017-04-03 18:52:08

  上一篇:go ZOJ和PKU 題目分類
  下一篇:go HDU 4316 凸包+半麵相交