轉載請署名:印風
在之前我已經寫了一系列介紹audit plugin的博文,當時還很青澀,這裏把之前的內容整理出來,併成爲mysql plugin編寫手冊系列的一部分
-----------------------------------------------------------------------------------------------
1.什麼是Audit Plugin
在MySQL5.5之前,MySQL本身缺少一套的對服務器操作的審計機制,對於非法或者危險的操作、錯誤捕捉、登錄審計尚不能很好的支持。如果誰drop了一個表或者不慎不帶where的刪除表數據,這種危險的操作應該被明確的記錄下來。
在MySQL5.5裏,添加了額外的流程來對這些我們所關心的地方進行事件捕獲;然後將捕獲到的事件傳遞給Audit Plugin;而Audit Plugin所要做的,就是對這些事件進行判別,並做必要的反應,比如記錄到log或發送一個報警
2.Audit Plugin能做什麼
實際上,每個SQL都會觸發審計插件,並且,在審計插件中,我們可以獲得執行SQL的THD對象;而THD對象中包含了非常豐富的信息,包括用戶名、客戶端IP、當前操作的數據庫等信息;這爲我們進行審計提供了無限的可能;
但要注意的是,如果不是我們關心的審計類型,應儘量避免過多的操作,以免影響系統的性能。
3.如何編寫Audit Plugin
新增加的審計插件其實很簡單,其實現思想是在內核代碼的不同位置增加相應的接口函數,具體實現在sql_audit.cc和sql_audit.h文件,這裏我們只涉及到Plugin的編寫,有興趣的同學可以看看審計是如何實現的。
Audit Plugin能夠識別不同的實現,具體包括兩種類型,連接審計和SQL執行審計,以宏來定義類型。
1)第一種事件類型:General Class
宏定義 |
描述 |
MYSQL_AUDIT_GENERAL_CLASS 0 |
事件的類型爲general |
MYSQL_AUDIT_GENERAL_CLASSMASK (1 << MYSQL_AUDIT_GENERAL_CLASS) |
掩碼,用於聲明插件關注的審計類型 |
MYSQL_AUDIT_GENERAL_LOG 0 |
LOG EVENT,在提交到general query log前觸發 |
MYSQL_AUDIT_GENERAL_ERROR 1 |
ERROR EVENT,在將錯誤傳遞給用戶前觸發 |
MYSQL_AUDIT_GENERAL_RESULT 2 |
RESULT EVENT,在將結果集傳遞給用戶後觸發 |
MYSQL_AUDIT_GENERAL_STATUS 3 |
STATUS EVENT,在將結果集或錯誤傳遞給用戶後觸發 |
2)第二種事件類型:CONNECTIONCLASS
宏定義 |
描述 |
MYSQL_AUDIT_CONNECTION_CLASS 1 |
標記事件類型爲CONNECTION |
MYSQL_AUDIT_CONNECTION_CLASSMASK (1 << MYSQL_AUDIT_CONNECTION_CLASS) |
掩碼,用於聲明插件關注的審計類型 |
MYSQL_AUDIT_CONNECTION_CONNECT 0 |
CONNECT事件,客戶端連接完成認證階段 |
MYSQL_AUDIT_CONNECTION_DISCONNECT 1 |
DISCONNECT事件,連接被終止時 |
MYSQL_AUDIT_CONNECTION_CHANGE_USER 2 |
CHANGE_USER事件,當完成 |
3)聲明插件
Audit插件的定義結構體爲st_mysql_audit
字段 |
類型 |
描述 |
interface_version |
int |
插件版本: MYSQL_AUDIT_INTERFACE_VERSION |
release_thd |
void (*release_thd)(MYSQL_THD) |
當線程將被釋放時被調用 |
event_notify |
void (*event_notify)(MYSQL_THD, unsigned int, const void *); |
當事件發生時調用該函數 |
class_mask[MYSQL_AUDIT_CLASS_MASK_SIZE] |
unsigned long |
掩碼,用來標示關注的事件類型 |
在官方文檔裏這麼解釋release_thd和event_notify:
MYSQL服務器可以將這兩個函數聯合起來一起用,在每個特定線程內調用,當某個線程第一次調用event_notify時,創建了一個線程與插件的綁定,當這種綁定存在時,插件是不可以卸載的,當沒有事件發生在線程上時,Server通過調用release_thd來告訴插件,然後銷燬綁定;例如一個客戶端在執行一個SQL,完成一系列的消息通知後,在沒有任務sleep之前,會通知到插件。
那麼這種方式有什麼用呢?我們可以在event_notify函數中分配內存,並在release_thd中將內存釋放。這顯然比每次調用event_notify時分配內存要效率更高
4)event_notify函數
這是處理審計事件的主函數,函數原型爲:
void (*event_notify)(MYSQL_THD, unsigned int, const void*)
第1個參數表示當前的線程THD
第2個參數表示事件的類型:
MYSQL_AUDIT_CONNECTION_CLASS 或者MYSQL_AUDIT_GENERAL_CLASS
第三個參數表示當前事件的詳細信息,我們可以根據第二個參數轉換爲相應的結構體類型。
GENERAL類型對應的結構體爲mysql_event_general,如下所示:
字段 |
類型 |
描述 |
event_subclass |
unsigned int |
子事件類型 |
general_error_code |
int |
錯誤碼,可以調用mysql_errno()來獲得具體信息 |
general_user |
const char* |
當前事件的用戶名 |
general_user_length |
unsigned int |
用戶名長度 |
general_command |
const char* |
對於general quere log子事件,表示命令,例如Connect、Query、Shutdown等;對於ERROR類型存儲錯誤信息,其他子事件類型爲NULL |
general_query |
const char* |
當前線程的SQL語句 |
general_query_length |
unsigned int |
SQL長度 |
general_charset |
struct charset_info_st * |
字符集 |
general_time |
unsigned long long |
event_notify被調用時的時間戳 |
general_rows |
unsigned long |
對於general query log子事件類型爲0,對於ERROR子事件表示發生錯誤的行號,對於RESULT子事件,表示產生的行數,對於那些不會產生結果集的SQL,其值爲0,這區別與select,如果select的結果集爲空,該值爲1 |
CONNECTION類型對應的結構體爲mysql_event_connection,如下所示:
字段 |
類型 |
描述 |
event_subclass |
unsigned int |
子事件類型 |
thread_id |
unsigned long |
線程id |
user |
const char* |
用戶名 |
user_length |
unsigned int |
用戶名長度 |
const char* |
|
|
priv_user_length |
unsigned int |
|
external_user |
const char* |
|
external_user_length |
unsigned int |
|
proxy_user |
const char* |
代理用戶名(似乎在auth插件裏有提到,後面再議) |
proxy_user_length |
unsigned int |
長度 |
host |
const char* |
主機名 |
host_length |
unsigned int |
主機名長度 |
ip |
const char* |
客戶端IP |
ip_length |
unsigned int |
IP長度 |
database |
const char* |
客戶端連接的數據庫名 |
database_length |
unsigned int |
數據庫名長度 |
4.一個簡單的例子:audit_null(摘自MySQL源代碼),這個示例用於審計GENERAL類型的事件,並提供了三個狀態變量。雖然示例很簡單,但給出了最基本的流程。
#include <stdio.h>
#include <mysql/plugin.h>
#include <mysql/plugin_audit.h>
#if !defined(__attribute__) &&(defined(__cplusplus) || !defined(__GNUC__) || __GNUC__ == 2 && __GNUC_MINOR__ < 8)
#define __attribute__(A)
#endif
static volatile int number_of_calls; /* forSHOW STATUS, see below */
static volatile int number_of_calls_general_log;
static volatile intnumber_of_calls_general_error;
static volatile intnumber_of_calls_general_result;
/*
Initialize the plugin at server start or plugin installation.
SYNOPSIS
audit_null_plugin_init()
DESCRIPTION
Does nothing.
RETURN VALUE
0 success
1 failure(cannot happen)
*/
static int audit_null_plugin_init(void *arg__attribute__((unused)))
{
number_of_calls= 0;
number_of_calls_general_log= 0;
number_of_calls_general_error= 0;
number_of_calls_general_result= 0;
return(0);
}
/*
Terminate the plugin at server shutdown or plugin deinstallation.
SYNOPSIS
audit_null_plugin_deinit()
Does nothing.
RETURN VALUE
0 success
1 failure(cannot happen)
*/
static int audit_null_plugin_deinit(void*arg __attribute__((unused)))
{
return(0);
}
/*
Foo
SYNOPSIS
audit_null_notify()
thd connectioncontext
DESCRIPTION
*/
static void audit_null_notify(MYSQL_THD thd__attribute__((unused)),
unsigned intevent_class,
const void*event)
{
/*prone to races, oh well */
number_of_calls++;
if(event_class == MYSQL_AUDIT_GENERAL_CLASS)
{
const struct mysql_event_general *event_general=
(const struct mysql_event_general *) event;
switch (event_general->event_subclass)
{
case MYSQL_AUDIT_GENERAL_LOG:
number_of_calls_general_log++;
break;
case MYSQL_AUDIT_GENERAL_ERROR:
number_of_calls_general_error++;
break;
case MYSQL_AUDIT_GENERAL_RESULT:
number_of_calls_general_result++;
break;
default:
break;
}
}
}
/*
Plugin type-specific descriptor
*/
static struct st_mysql_auditaudit_null_descriptor=
{
MYSQL_AUDIT_INTERFACE_VERSION, /* interface version */
NULL, /*release_thd function */
audit_null_notify, /* notifyfunction */
{(unsigned long) MYSQL_AUDIT_GENERAL_CLASSMASK } /* class mask */
};
/*
Plugin status variables for SHOW STATUS
*/
static struct st_mysql_show_varsimple_status[]=
{
{"Audit_null_called", (char *) &number_of_calls, SHOW_INT },
{"Audit_null_general_log", (char *) &number_of_calls_general_log,SHOW_INT },
{"Audit_null_general_error", (char *)&number_of_calls_general_error,
SHOW_INT },
{"Audit_null_general_result", (char *)&number_of_calls_general_result,
SHOW_INT },
{0, 0, 0}
};
/*
Plugin library descriptor
*/
mysql_declare_plugin(audit_null)
{
MYSQL_AUDIT_PLUGIN, /*type */
&audit_null_descriptor, /*descriptor */
"NULL_AUDIT", /* name */
"Oracle Corp", /* author */
"Simple NULL Audit", /* description */
PLUGIN_LICENSE_GPL,
audit_null_plugin_init, /*init function (when loaded) */
audit_null_plugin_deinit, /*deinit function (when unloaded) */
0x0002, /*version */
simple_status, /*status variables */
NULL, /*system variables */
NULL
}
mysql_declare_plugin_end;
後記:
MySQL5.5對審計的支持使得我們可以更好的擴展插件功能,由於我們可以在多個地方進行審計,實際上相當於在服務器上給出了很多個斷點,我們可以據此發揮想象力……
1. 針對特定的SQL記錄到log,或發送警報給數據庫OWNER
2. 記錄每個數據庫甚至每個表的SQL執行量,結合I_S插件顯示出來
3. ……
MYSQL的插件的自由度非常高,但需要牢記的是,插件是工作在MYSQL的內存中的,需要仔細編寫插件並測試,否則可能造成嚴重的服務器Crash事件。
參考:
MySQL5.5.16源代碼
MySQL5.5官方文檔