1. CTDB概述
CTDB是一個集羣TDB數據庫,可以被Samba或者其他的應用使用來存儲數據。如果一個應用是使用TDB來暫時存放數據,那麼這個應用可以很輕鬆的使用CTDB擴展爲集羣模式。CTDB提供與TDB相同的函數接口,並且是構建在多臺物理機器上的集羣。
特性:
- CTDB提供一個橫跨多個節點的並且數據一致、鎖一致的TDB數據庫;
- CTDB非常快速;
- 對於節點故障,CTDB將自動恢復和修復其所管理的所有TDB數據庫;
- CTDB是Samba3/4的一個核心組件;
- CTDB提供高可用特性,例如節點監控、節點切換、IP切換;
- CTDB爲其多個節點上的應用提供可靠的傳輸通道;
- CTDB提供可熱拔插的後端傳輸通道,目前實現了TCP和IB;
- CTDB可以提供爲應用指定特定的管理腳本,使得應用能夠實現高可用。
2. CTDB配置
CTDB的配置相對簡單,對於搭建一個三節點的CTDB配置步驟說明如下。
2.1 節點信息
節點名稱 | 節點IP | 說明 |
---|---|---|
node1 | 10.10.10.90 | CTDB節點 |
node2 | 10.10.10.91 | CTDB節點 |
node3 | 10.10.10.92 | CTDB節點 |
node4 | 10.10.10.99 | 共享存儲 |
說明:此處爲快速簡單的搭建ctdb集羣,採用單節點NFS共享,不考慮NFS共享的可靠性。
實際應用時,共享存儲應由高可靠的集羣擔當。
2.2搭建CTDB集羣
2.2.1 安裝軟件
在三個CTDB節點分別安裝ctdb,nfsd,samba,可以通過命令 yum install ctdb依次進行安裝;
2.2.2 創建共享存儲
在node4創建一個共享目錄/share,並設置權限777,在/etc/exports文件中添加如下:
/share *(sync,rw)
然後在節點4上執行如下命令:
# exoprtfs -rv
CTDB節點之間需要通過一個共享的存儲來實現其基於鎖機制的選舉過程。
2.2.3 掛載共享存儲
在三個CTDB節點上分別執行如下命令:
# mount -t nfs 10.10.10.99:/share /mnt
node1, node2,node3上都掛載了node4共享出來的目錄,這樣三個節點就可以訪問到一個相同的鎖文件了。
2.2.4 修改CTDB服務配置
在三個CTDB節點上修改如下文件:
~# vi /etc/sysconfig/ctdb
CTDB_RECOVERY_LOCK=/mnt/ctdb_lock
CTDB_MANAGES_SAMBA=yes
CTDB_MANAGES_WINBIND=yes
CTDB_MANAGES_NFS=yes
這個步驟中主要配置CTDB管理哪些應用。還有就是指定共享鎖文件的目錄。
2.2.5 修改CTDB節點配置
在三個CTDB節點上創建或者修改如下文件:
~# vi /etc/ctdb/nodes
10.10.10.91
10.10.10.92
10.10.10.90
2.2.6 修改CTDB IP配置
在三個CTDB節點上創建或者修改如下文件:
~# vi /etc/ctdb/public_addresses
10.0.0.1/24 eth0
其中eth0是節點上存在的並且在線的網卡。10.0.0.1就是配置給這個三個節點CTDB集羣對外提供業務的IP。
2.2.7 重啓CTDB服務
在三個CTDB節點上執行如下命令:
~# systemctl restart ctdb
2.2.8 查看CTDB服務狀態
在節點1上執行:
# ctdb status
Number of nodes:3
pnn:0 10.10.10.91 OK
pnn:1 10.10.10.92 OK
pnn:2 10.10.10.90 OK (THIS NODE)
Generation:1699238992
Size:3
hash:0 lmaster:0
hash:1 lmaster:1
hash:2 lmaster:2
Recovery mode:NORMAL (0)
Recovery master:1
[root@xenserver-yzulkyuc ~]#
集羣搭建完成。
說明:如果節點數量較少,可以將某一個CTDB節點作爲共享存儲,同時節點數量也可以爲1。但是將CTDB節點作爲共享存儲的話,需要將/etc/sysconfig/ctdb 文件中的CTDB_MANAGES_NFS設置位NO。
3. CTDB源碼分析
3.1 關鍵數據結構
3.1.1 ctdb
CTDB有兩個進程構成,ctdbd和recoveryd。在這個兩個進程進行各種事物處理時,一般都帶有一個參數:ctdb。該數據結構是包含了基本上所有邏輯所需或者相關的數據。在CTDBD啓動階段就是對ctdb數據結構的填充階段。
struct ctdb_context {
struct tevent_context *ev; //封裝的事件處理接口,後端使用select/poll/epoll 多路I/O複用機制
struct timeval ctdbd_start_time; //ctdbd啓動時間
struct timeval last_recovery_started;
struct timeval last_recovery_finished;
uint32_t recovery_mode; //根據此值可以判斷是否需要進行recovery
TALLOC_CTX *tickle_update_context;
TALLOC_CTX *keepalive_ctx;
TALLOC_CTX *check_public_ifaces_ctx;
struct ctdb_tunable_list tunable;
enum ctdb_freeze_mode freeze_mode;
struct ctdb_freeze_handle *freeze_handle;
bool freeze_transaction_started;
uint32_t freeze_transaction_id;
ctdb_sock_addr *address; //此ctdb節點的IP地址
const char *name;
const char *db_directory;
const char *db_directory_persistent;
const char *db_directory_state;
struct tdb_wrap *db_persistent_health;
uint32_t db_persistent_startup_generation;
uint64_t db_persistent_check_errors;
uint64_t max_persistent_check_errors;
const char *transport;
const char *recovery_lock;
uint32_t pnn; /* our own pnn */
uint32_t num_nodes;
uint32_t num_connected;
unsigned flags;
uint32_t capabilities;
struct reqid_context *idr;
struct ctdb_node **nodes; //集羣節點列表,索引爲vnn
struct ctdb_vnn *vnn; //公共IP列表和網卡
struct ctdb_interface *ifaces; /* list of local interfaces */
char *err_msg;
const struct ctdb_methods *methods; //啓動時註冊的tcp處理函數
const struct ctdb_upcalls *upcalls; //啓動時註冊的處理函數
void *private_data; /* private to transport */
struct ctdb_db_context *db_list;
struct srvid_context *srv;
struct ctdb_daemon_data daemon;
struct ctdb_statistics statistics;
struct ctdb_statistics statistics_current;
#define MAX_STAT_HISTORY 100
struct ctdb_statistics statistics_history[MAX_STAT_HISTORY];
struct ctdb_vnn_map *vnn_map;
uint32_t num_clients;
uint32_t recovery_master;
struct ctdb_client_ip *client_ip_list;
bool do_checkpublicip;
bool do_setsched;
const char *event_script_dir;
const char *notification_script;
const char *default_public_interface;
pid_t ctdbd_pid;
pid_t recoverd_pid;
enum ctdb_runstate runstate;
struct ctdb_monitor_state *monitor;
int start_as_disabled;
int start_as_stopped;
bool valgrinding;
uint32_t *recd_ping_count;
TALLOC_CTX *recd_ctx; /* a context used to track recoverd monitoring events */
TALLOC_CTX *release_ips_ctx; /* a context used to automatically drop all IPs if we fail to recover the node */
struct eventd_context *ectx;
TALLOC_CTX *banning_ctx;
struct ctdb_vacuum_child_context *vacuumers;
/* mapping from pid to ctdb_client * */
struct ctdb_client_pid_list *client_pids;
/* Used to defer db attach requests while in recovery mode */
struct ctdb_deferred_attach_context *deferred_attach;
/* if we are a child process, do we have a domain socket to send controls on */
bool can_send_controls;
struct ctdb_reloadips_handle *reload_ips;
const char *nodes_file;
const char *public_addresses_file;
struct trbt_tree *child_processes;
/* Used for locking record/db/alldb */
struct lock_context *lock_current;
struct lock_context *lock_pending;
};
3.1.2 初始化upcall
在上面一節中的ctdb結構中可以看到有這個upcall處理函數接口。初始化操作是在main()函數的入口處進行的。
ctdb->recovery_mode = CTDB_RECOVERY_NORMAL;
ctdb->recovery_master = (uint32_t)-1;
ctdb->upcalls = &ctdb_upcalls;
而ctdb_upcalls定義如下:
static const struct ctdb_upcalls ctdb_upcalls = {
.recv_pkt = ctdb_recv_pkt, //當有消息包傳入時調用此函數進行處理。
.node_dead = ctdb_node_dead, //當有節點出現故障或者離線時調用此函數,進行的操作是重啓。
.node_connected = ctdb_node_connected //當有連接請求發送時,調用此函數,更新連接統計信息。
};
3.1.3 創建unix socket fd
在啓動ctdbd守護進程過程中,創建了unix socket,並且將fd賦值給了3.1.1中的daemon.sd。並且開始監聽是否有連接請求。
ctdb->daemon.sd = socket(AF_UNIX, SOCK_STREAM, 0);
3.1.4 初始化tevent
tevent是ctdb中事件處理機制,底層採用select/poll/epoll多路I/O複用機制,對外提供統一的接口。初始化就是將接口進行統一處理。接口的統一入口就是3.1.1小節中提的ctdb->ev。
ctdb->ev = tevent_context_init(NULL);
初始化完成後,形成一個雙向鏈表,鏈表中存放的是不同的I/O多路複用接口,但是對於調用這來說,看的接口是相同的。
即ops的名稱是一致的,而對於不同的I/O複用就賦值了相對應的函數接口。
static const struct tevent_ops select_event_ops = {
.context_init = select_event_context_init,
.add_fd = select_event_add_fd,
.set_fd_close_fn = tevent_common_fd_set_close_fn,
.get_fd_flags = tevent_common_fd_get_flags,
.set_fd_flags = tevent_common_fd_set_flags,
.add_timer = tevent_common_add_timer_v2,
.schedule_immediate = tevent_common_schedule_immediate,
.add_signal = tevent_common_add_signal,
.loop_once = select_event_loop_once,
.loop_wait = tevent_common_loop_wait,
};
3.1.5 tcp傳輸通道初始化
在ctdbd守護進程啓動節點,初始化tcp傳輸通道
/*
initialise tcp portion of ctdb
*/
int ctdb_tcp_init(struct ctdb_context *ctdb)
{
struct ctdb_tcp *ctcp;
ctcp = talloc_zero(ctdb, struct ctdb_tcp);
CTDB_NO_MEMORY(ctdb, ctcp);
ctcp->listen_fd = -1;
ctcp->ctdb = ctdb;
ctdb->private_data = ctcp;
ctdb->methods = &ctdb_tcp_methods;
talloc_set_destructor(ctcp, tcp_ctcp_destructor);
return 0;
}
上面的代碼中的ctdb->methods初始化爲了&ctdb_tcp_methods,其定義如下:
static const struct ctdb_methods ctdb_tcp_methods = {
.initialise = ctdb_tcp_initialise,
.start = ctdb_tcp_start,
.queue_pkt = ctdb_tcp_queue_pkt,
.add_node = ctdb_tcp_add_node,
.connect_node = ctdb_tcp_connect_node,
.allocate_pkt = ctdb_tcp_allocate_pkt,
.shutdown = ctdb_tcp_shutdown,
.restart = ctdb_tcp_restart,
};
進行上述接口註冊後,就調用的ctdb_tcp_initialise進行初始了,在該函數中完成了兩件事情
- ctcp->listen_fd = socket(sock.sa.sa_family, SOCK_STREAM, IPPROTO_TCP);創建了一個socket server端,監聽的地址是當前ctdb節點的地址。監聽後,註冊了一個監聽事件,一旦有其他節點來連接當前ctdb節點,將調用註冊的函數:ctdb_listen_event,在該處理函數中將accept連接請求。並再次註冊一個讀事件到tevent中。也就是當客戶端有寫入數據時,當前ctdb將調用ctdb_tcp_read_cb進行數據包的處理。
in->queue = ctdb_queue_setup(ctdb, in, in->fd, CTDB_TCP_ALIGNMENT,
ctdb_tcp_read_cb, in, "ctdbd-%s", ctdb_addr_to_str(&addr));
- 給ctdb集羣的所有節點都創建了一個out_queue隊列,並且這個數據結構保存在ctdb->node[i]中。
3.1.6 unix socket accept函數
在3.1.3中已經創建了unix socket,此時註冊了一個accept事件,處理函數爲:ctdb_accept_client,事件類型爲TEVENT_FD_READ。在此處理函數中會accept連接請求。並將已經連接上的client信息存放在 ctdb->client_pids 鏈表當中。最後再次註冊一個讀請求處理函數ctdb_daemon_read_cb。該函數註冊的位置爲:ctdb->client_pids->queue->callback,此外ctdb->client_pids->queue->im= tevent_create_immediate(queue);理解起來應該是,如果已連接的這個unix socket上有讀事件的話,立刻處理。
3.1.7 啓動tcp傳輸通道
調用的函數爲ctdb_tcp_start(ctdb);該函數主要完成node節點之間的tcp連接。在前3.1.5節中說到每一個ctdb節點都在監聽自己的IP地址,而ctdb_tcp_start(ctdb)相當於把當前節點作爲client,去連接所有其他的ctdb節點。同樣,其他節點也會進行相同的操作處理。也就是最後會出現的結果是集羣的每一個節點都與剩餘節點存在鏈接。這個已連接的fd保存在ctdb->node[i]->private_data(ctdb_tcp_node)->fd。
同時註冊了兩個事件,一個是已建立連接可寫時的事件處理函數。第二個是定時連接處理函數,就是每隔一秒鐘去連接這個節點。
從已有的三節點ctdb環境中可以看到相關連接的信息:
[root]# netstat -anp | grep ctdbd
tcp 0 0 10.10.10.90:4379 0.0.0.0:* LISTEN 1006/ctdbd
tcp 0 0 10.10.10.90:50071 10.10.10.92:4379 ESTABLISHED 1006/ctdbd
tcp 0 0 10.10.10.90:4379 10.10.10.91:48353 ESTABLISHED 1006/ctdbd
tcp 0 0 10.10.10.90:4379 10.10.10.92:57112 ESTABLISHED 1006/ctdbd
tcp 0 0 10.10.10.90:57413 10.10.10.91:4379 ESTABLISHED 1006/ctdbd
從上面的連接信息判斷,node1(10.10.10.90)通過57413端口作爲client去connect了node2,通過50071端口去connect了node3。同樣node2通過48353端口connect了node1。node3通過57112端口去連接了node1。與上述連接邏輯一致。
3.2事件通知機制
事件通知機制是ctdb結構中最重要的一部分,事件通知機制將各個邏輯鏈接起來以完成相關的功能實現。
下面以 fde = tevent_add_fd(ctdb->ev, ctdb, ctdb->daemon.sd, TEVENT_FD_READ,ctdb_accept_client, ctdb);爲例來分析是如何將該事件監控起來並調用事件處理函數的。
在ctdb_start_daemon函數中,先後調用了tevent_add_fd以及tevent_loop_wait函數。
- tevent_add_fd,對於這個函數是tevent對外提供的接口,其實際調用的是epoll_event_add_fd函數(此處以epoll爲例,如果內核不支持epoll,那麼可能就會調用select或者poll相對應的函數。)在此函數中主要進行了2個邏輯處理
1. 將事件相關的參數和函數賦值到fde,並將fde加入到ctdb->ev->fd_events鏈表中;
2. 執行epoll_update_event(epoll_ev, fde)函數更新epoll_ev,具體就是將待添加的event通過epoll_ctl加入到epoll_ev->epoll_fd;
- tevent_loop_wait實際會調用epoll_event_loop_once,再調用epoll_event_loop,最終調用epoll_wait,查看已就緒事件,並調用相應的事件處理函數進行處理。
tevent_loop_wait會一直循環檢查是否有事件註冊了,如果有,就會不斷的循環去判斷;
*/
int tevent_common_loop_wait(struct tevent_context *ev,
const char *location)
{
/*
* loop as long as we have events pending
*/
while (tevent_common_have_events(ev)) { // 此函數會一直返回真
int ret;
ret = _tevent_loop_once(ev, location);
if (ret != 0) {
tevent_debug(ev, TEVENT_DEBUG_FATAL,
"_tevent_loop_once() failed: %d - %s\n",
ret, strerror(errno));
return ret;
}
}
tevent_debug(ev, TEVENT_DEBUG_WARNING,
"tevent_common_loop_wait() out of events\n");
return 0;
}
3.3 主循環流程
下面圖中所展示的是recoveryd進程的循環處理事務的過程。函數名稱爲main_loop。
A(檢查ctdbd是否存活)-->B(告訴ctdbd recoveryd是活動的)
B-->C(是否選舉中)
C-->D(是- 退出main_loop)
C-->E(不是- 獲取ctdbd debug level)
E-->F(獲取cdbd運行參數)
F-->G(獲取ctdbd運行狀態)
G-->H(獲取nodemap)
H-->I(獲取recovery mode)
I-->J(當前節點是否是stopped或者baned狀態)
J-->K(是- 設置recovery mode爲 active,並freezee db就是鎖住?)
K-->L(獲取所有節點所具備的角色能力,如下圖所示)
L-->M(查看recmaster,如果unknown, force elelction)
M-->N(選舉中,返回)
M-->O(查看是否有ip需要分配)
O-->P(查看是否所有節點都同意recmater node)
P-->Q(獲取vnnmap)
Q-->R(查看是否需要recovery)
R-->S(查看所有節點處於normal狀態)
S-->T(當前節點是否持有共享鎖,是的話,檢查一下鎖狀態)
T-->U(從所有其他節點獲取nodemap並比較所有nodemap是否一致,不一致的話執行do_recover)
U-->V(更新所有節點狀態標識)
V-->W(統計活動的node數量)
W-->X(active node情況與vnnmap記錄一致)
X-->Y(檢查所有節點是否有相同的vnn和相同的版本號)
3.4節點及節點節點間通信總結
3.4.1單個ctdb節點內進程之間的連接
ctdb有兩個進程,ctdbd和recoveryd,這個兩個進程之間通過unix socket進行消息的傳遞。一般recoveryd發送帶opcode的消息,ctdbd通過
3.4.2 ctdb節點之間連接
- 初始化函數ctdb_tcp_initialise中,爲當前節點創建了一個監聽ctdb address的socket,並且將fd保存在ctdb->private_data->listen_fd。其中private_data的數據結構是struct ctdb_tcp。也就是每一個ctdb節點啓動時都會創建並監聽本地節點的IP地址。
- 由於ctdb節點之間需要相互通信,因此在初始化過程中,將所有節點有關連接的信息都保存在ctdb中。ctdb->nodes[i]就可以遍歷所有節點關於連接的信息。而ctdb->nodes[i]->private_data->fd就是保存相應node的fd。其中private_data的數據結構爲struct ctdb_tcp_node。
- 基於上面步驟2中的描述,ctdb->nodes[i]->private_data->out_queue,就是初始化的一個隊列。但是初始化時ctdb->nodes[i]->private_data->fd值爲-1。
- 應該在main_loop 中getnodemap時會更新ctdb->nodes[i]->private_data->fd這個值(目前是猜測)
3.4.3 控制消息處理流程總結
下面以獲取node map的消息爲例來說明,如何從一個ctdb節點獲取到另一個ctdb節點的信息。
1. ret = ctdb_ctrl_getnodemap(ctdb, CONTROL_TIMEOUT(), pnn, rec, &rec->nodemap);執行程序的自然就是發起消息的節點,其中pnn就是目標節點;該控制消息是獲取nodemap;
2. ret = ctdb_control(ctdb, destnode, 0, CTDB_CONTROL_GET_NODEMAP, 0, tdb_null,mem_ctx, &outdata, &res, &timeout, NULL);其中destnode就是要目標node;
3. state = ctdb_control_send(ctdb, destnode, srvid, opcode, flags, data, mem_ctx,timeout, errormsg);
4. ret = ctdb_client_queue_pkt(ctdb, &(c->hdr));其中c->hdr中包含了函數調用中需要的參數;
5. ctdb_queue_send(struct ctdb_queue *queue, uint8_t *data, uint32_t length);
6. n = write(queue->fd, data, length2);即將數據包寫入了unix socket 的fd中;
7. unix socket fd的read 函數是在ctdb_accept_client 中設置的:ctdb_daemon_read_cb
8. daemon_incoming_packet(client, hdr);其中,hdr即最初發送的數據包;
9. 依據hdr中的hdr->operation判斷消息類別:CALL/MESSAGE/CONTROL
10. 調用CONTROL消息處理函數daemon_request_control_from_client
11. res = ctdb_daemon_send_control(client->ctdb, c->hdr.destnode,c->srvid, c->opcode, client->client_id,c->flags,data, daemon_control_callback,state);
12. ctdb_queue_packet(ctdb, &c->hdr);並添加了一個控制消息超時處理事件ctdb_control_timeout();
13. 語句node = ctdb->nodes[hdr->destnode]獲取到目標node信息;
14. ctdb->methods->queue_pkt(node, (uint8_t *)hdr, hdr->length);該函數爲初始化時註冊的;
15. ctdb_tcp_queue_pkt(struct ctdb_node *node, uint8_t *data, uint32_t length);從node的node->private_data中提取出tnode;
16. ctdb_queue_send(tnode->out_queue, data, length);其中tnode->out_queue->fd即是已連接到目標節點的fd。
17. n = write(queue->fd, data, length2);數據通過fd寫入到socket鏈接
18. 遠端node接收到請求;在遠端節點中從上述的第7步開始重複。
控制消息首先通過unix socket從recoveryd進程發送,ctdbd進程讀取unix socket上的數據,並對數據進行解析,如果獲取的是當前的節點信息就提供信息並返回,如果是需要獲取remote node的節點信息,就使用保存在node = ctdb->nodes[hdr->destnode]中的fd將消息發送給remote node。
static const struct ctdb_methods ctdb_tcp_methods = {
.initialise = ctdb_tcp_initialise,
.start = ctdb_tcp_start,
.queue_pkt = ctdb_tcp_queue_pkt,
.add_node = ctdb_tcp_add_node,
.connect_node = ctdb_tcp_connect_node,
.allocate_pkt = ctdb_tcp_allocate_pkt,
.shutdown = ctdb_tcp_shutdown,
.restart = ctdb_tcp_restart,
};