Để hỗ trợ trình bày các thuật toán chi tiết, cần phải định nghĩa một số biến cấu trúc chương trình, đó là listener, proxy và server.
Con trỏ cấu trúc listener được khai báo trong bộ cân bằng tải nhằm lưu trữ thông tin của listener bao gồm listen socket, địa chỉ listen và một con trỏ next chỉ đến địa chỉ tiếp theo hoặc chỉ vào NULL.
Vậy thì socket là gì? Một socket bao gồm địa chỉ IP kết hợp với cổng (port) nhằm giúp local DNS xác định đích của một ứng dụng đang chạy trên máy server.
Chúng ta biết rằng mỗi máy tính khi giao tiếp trong mạng sẽ có một địa chỉ IP duy nhất dùng để xác định danh tính của máy tính này. Tuy nhiên máy tính này lại chạy nhiều ứng dụng khác nhau, giả sử như một server đang cài đặt cả web-server và DNS server, vậy làm cách nào để người dùng, đúng hơn là local DNS biết được là mình sẽ tương tác với ứng dụng nào. Lúc này chúng ta sẽ cần khái niệm cổng, mỗi ứng dụng trên server sẽ chạy ở các port khác nhau. Khi người dùng muốn truy cập vào dịch vụ web, trong gói tin gửi đi sẽ bao gồm địa chỉ IP của máy server và cổng đang chạy ứng dụng web (thường là cổng 80). Ngược lại nếu muốn truy cập vào DNS server, gói tin gửi đi sẽ là địa chỉ IP và cổng 53.
struct listener {
int fd; // the listen socket
struct sockaddr_storage addr;// the address we listen to struct listener *next; // next address or NULL
};
Con trỏ cấu trúc server lưu giữ tất cả các thông tin về các server, bao gồm socket (địa chỉ IP và cổng của server đó), các thông số về số lượng session hiện tại, số lượng session lớn nhất, số lượng session đã kết nối, số lượng kết nối đang chờ để được phục vụ, trọng số của server và lưu giữ thông tin về proxy mà nó thuộc về.
Thêm nữa, nó lưu thông tin về trạng thái của server trong biến “state”, bao gồm nhiều trạng thái khác nhau, chẳng hạn như “đang chạy” (running), backup, cần kiểm tra (checked)…
struct server {
struct server *next;
int state; /* server state (SRV_*) */
int cklen; /* the len of the cookie, to speed up checks */
char *cookie; /* the id set in the cookie */
char *id; /* just for identification */
struct list pendconns; /* pending connections */
int nbpend, nbpend_max; /* number of pending connections */
struct task *queue_mgt; /* the task associated to the queue processing */
struct sockaddr_in addr; /* the address to connect to */
struct sockaddr_in source_addr; /* the address to which we want to bind for connect() */
short check_port; /* the port to use for the health checks */
int health; /* 0->rise-1 = bad; rise->rise+fall-1 = good */
int rise, fall; /* time in iterations */
int inter; /* time in milliseconds */
int result; /* 0 = connect OK, -1 = connect KO */
unsigned char uweight, eweight; /* user-specified weight-1, and effective weight-1 */
unsigned int wscore; /* weight score, used during srv map computation */
int cur_sess, cur_sess_max; /* number of currently active sessions (including syn_sent) */
unsigned int cum_sess; /* cumulated number of sessions really sent to this server */
unsigned int maxconn, minconn; /* max # of active sessions (0 = unlimited), min# for dynamic limit. */
unsigned failed_checks, down_trans; /* failed checks and up-down transitions */
unsigned failed_conns, failed_resp; /* failed connect() and responses */
unsigned failed_secu; /* blocked responses because of security concerns */
struct proxy *proxy; /* the proxy this server belongs to */
};
Con trỏ cấu trúc proxy lưu giữ thông tin về các proxy đang hoạt động trong hệ thống. Đây là một cấu trúc phức tạp, trong khuôn khổ thuật toán sẽ thảo luận dưới đây, chúng ta sẽ chỉ quan tâm đến một số biến cần thiết. Bao gồm con trỏ cấu trúc server “struct server **srv_map” dùng để chứa tập các server mà proxy đang kết nối tới, tên và độ dài của cookie (dành để đọc cookie do người truy cập gửi đến hoặc server trả về), “srv_rr_idx” dùng làm biến đếm để chọn server trong srv_map theo thuật toán RR.
struct proxy {
struct listener *listen; /* the listen addresses and sockets */
………
struct server *srv; /* known servers */
int srv_act, srv_bck; /* # of running servers */
int tot_wact, tot_wbck; /* total weights of active and backup servers */
struct server **srv_map; /* the server map used to apply weights */
int srv_map_sz; /* the size of the effective server map */
char *cookie_name; /* name of the cookie to look for */
int cookie_len; /* strlen(cookie_name), computed only once */
int srv_rr_idx; /* next server to be elected in round robin mode */
unsigned int wscore; /* weight score, used during srv map computation */
………}
Khi Haproxy bắt đầu hoạt động, nó sẽ đọc file cấu hình của hệ thống. Trong quá trình đọc file cấu hình, Haproxy sẽ xác định xem có bao nhiêu server, server nào là hoạt động, server nào là backup. Cứ mỗi server sẽ được cấp phát một vùng bộ nhớ và lưu trữ vào srv_map tùy theo trạng thái của nó.
curproxy->srv_map = (struct server **)calloc(act, sizeof(struct server *));
Trong đó curproxy là proxy hiện tại đang sử dụng. Cứ mỗi khi lưu một server mới vào hệ thống, chương trình sẽ vẽ lại bản đồ server cũng như tổng trọng số của chúng. Việc vẽ lại bản đồ và tính lại tổng trọng số rất quan trọng, nó đảm bảo cho thuật toán RR hoạt động đúng khi các server có trọng số khác nhau.
static void recount_servers(struct proxy *px){…}
static void recalc_server_map(struct proxy *px){…}
Cho dù trọng số của server là bao nhiêu, server đầu tiên được lưu vào hệ thống sẽ luôn là server được gọi khi người dùng lần đầu tiên truy cập. Nó đảm bảo rằng trong trường hợp chúng ta cần server backup, nó sẽ gọi đến server đầu tiên.
2.1 Thuật toán weighted round robin (WRR)
Như đã mô tả ở trong chương 2 của đồ án này, tư tưởng chính của thuật toán WRR là phân tải xoay vòng giữa các server. Giả sử như chúng ta có 3 server A, B, C với trọng số lần lượt là 1, 2, 3. Thuật toán sẽ phân tải vào server theo thứ tự ABBCCC. Điều này được chỉ định trong srv_map khi chúng ta lưu cấp phát bộ nhớ cho một server mới và lưu nó vào srv_map. Biến srv_rr_idx được dùng để chọn server tiếp theo trong thuật toán.
Thuật toán RR được cài đặt như sau:
static inline struct server
*get_server_rr_with_conns(struct proxy *px) { int newidx;
/* Biến này dùng để đặt giá trị mới cho srv_rr_idx sau khi chọn được server */
struct server *srv;
/*Kiểm tra nếu kích thước của srv_map bằng không, nghĩa là không tồn tại server nào đang hoạt động, thuật toán trả về null */
if (px->srv_map_sz == 0) return NULL;
/*Nếu như giá trị của srv_rr_idx nhỏ hơn 0, hoặc vượt quá kích thước srv_map_sz, nghĩa là đã đi đến cuối server map chúng ta sẽ cập nhập giá trị cho nó bằng 0
*/
if(px->srv_rr_idx < 0 || px->srv_rr_idx >= px-
>srv_map_sz)
px->srv_rr_idx = 0;
/*Gán giá trị của srv_rr_idx cho newidx */
newidx = px->srv_rr_idx;
/*Thực hiện vòng lặp cho đến khi lấy được server phù hợp lẽ ra ở đây vì newidx = pr->srv_rr_idx, chúng ta sẽ lấy được luôn server thỏa mãn, tuy nhiên cần phải loại trừ trường hợp server đó đã đầy, hoặc số kết nối hiện tại lớn hơn số kết nối cho phép */
do {
srv = px->srv_map[newidx++];
if (!srv->maxconn || srv->cur_sess <
srv_dynamic_maxconn(srv)) {
px->srv_rr_idx = newidx;
return srv;
}
if (newidx == px->srv_map_sz) newidx = 0;
} while (newidx != px->srv_rr_idx);
return NULL;
}
Thuật toán WRR đơn giản và chạy khá ổn định trong phần mềm Haproxy, tuy nhiên nó cũng có những nhược điểm mà không thể khắc phục được.
Thứ nhất, vì nó phân phối tải theo hình thức xoay vòng, nên một người dùng đến với website có thể được đẩy đến các server khác nhau, điều này là không nên xảy ra, vì khi người dùng đã được đưa vào một server, nghĩa đã được thiết lập kết nối với server đó, tiếp tục làm việc với server này ở các yêu cầu tiếp theo sẽ giúp người dùng không phải tải lại một số các đối tượng (chẳng hạn như đã được tải về ở yêu cầu trước), hơn nữa, sẽ giúp người dùng không phải thực hiện lại việc kết nối server, điều này đặc biệt quan trọng khi web-server có yêu cầu về bảo mật, chẳng hạn như kiểm tra SSL key. Nếu yêu cầu của một người dùng được đưa vào các server khác nhau, việc kiểm tra liên tục SSL key sẽ mất rất nhiều thời gian, làm tăng thời gian đáp ứng người dùng.
Chúng ta có thể khắc phục nhược điểm này bằng 2 cách. Cách thứ nhất là sử dụng cookie như đã nói ở phần cookie của chương 2. Cách thứ 2 là sử dụng một hàm băm theo địa chỉ IP của người dùng. Khi bộ cân bằng tải nhận yêu cầu, nó sẽ băm địa chỉ IP của người dùng. Cùng một giá trị băm sẽ được cho vào 1 server. Tuy nhiên phương pháp này đòi hỏi người dùng phải có IP tĩnh.
Nhược điểm thứ 2 là vì phân phối theo kiểu xoay vòng, nên có thể sẽ xảy ra trường hợp một server phải phục vụ rất nhiều người dùng, trong khi server khác lại nhàn rỗi. Điều này có thể khắc phục bằng cách sử dụng thuật toán Least Connections (LC)
2.2 Thuật toán least connections
Như đã đề cập ở chương 2, thuật toán LC sẽ phân tải dựa trên số kết nối hiện tại đến với server. Mỗi proxy sẽ lưu thông tin của tất cả các server của nó. Trong mỗi server đều có biến “cur_sess” lưu lại số session active của server. Chúng ta sẽ sử dụng một vòng lặp kiểm tra biến này, và lấy server có giá trị cur_sess thấp chia cho trọng số của mỗi server nhất để gửi yêu cầu tiếp theo.
Thực vậy, giả sử 2 server A, B có trọng số lần lượt là 1 và 2. Thuật toán sẽ đảm bảo cho số session active ở B luôn xấp xỉ gấp đôi số session active ở A, vì chúng ta đang so sánh “số session active / trọng số server”.
Vì việc kiểm tra số lượng kết nối không phải được thực hiện ngay lập tức.
Trong một hệ thống có tải cao, nếu một server là đang có ít active session nhất, nó sẽ nhận được yêu cầu, sau đó nó sẽ nhận được liên tục yêu cầu, do số lượng active session chưa kịp cập nhập. Vì vậy, trong thuật toán này, chúng ta cần tránh phân 2 yêu cầu liên tiếp vào cùng một server (chúng ta đang xét với bài toán không có cookie, nghĩa là 2 kết nối đến từ 2 clients khác nhau). Nghĩa là server vừa được phân tải sẽ trở thành server cần tránh trong lần phân tải tiếp theo. Ở đây chúng ta sử dụng biến “struct server *srvtoavoid”.
if(srv != srvtoavoid){
… }
Sau khi chọn được server, chúng ta sẽ cập nhập lại giá trị srvtoavoid bằng một biến static
Static struct server *pre_server;
…
Pre_server = t;
Dưới đây là hàm thực thi thuật toán least connections:
static inline struct server *get_server_lc(struct proxy
*px, struct server *srvtoavoid) {
int s; /*Sử dụng để lấy số kết nối ít nhất của mỗi server dựa trên trọng số của chúng */
struct server *srv, *t;
/* Kiểm tra nếu kích thước của srv_map bằng không, nghĩa là không tồn tại server nào đang hoạt động, thuật toán trả về null */
if (px->srv_map_sz == 0) return NULL;
t = NULL;
s = 10000000;
/* Thực hiện cho đến khi tìm được server phù hợp, tăng giá trị i và chọn server nào có ít kết nối nhất dựa trên biến cur_sess và trọng số */
for(srv = px->srv; srv != NULL; srv = srv->next) { if(srv != srvtoavoid) {
if (!srv->maxconn || srv->cur_sess <
srv_dynamic_maxconn(srv)){
if (s > (srv->cur_sess / srv->eweight )
|| t==NULL) {
t = srv;
s = srv->cur_sess / srv->eweight;
} }
} }
pre_server = t;
return t;
}
2.3 Một số cải tiến
Thuật toán LC mới chỉ chú trọng vào một biến đếm, là số lượng kết nối đến server để thực hiện việc phân tải. Tuy nhiên vẫn còn rất nhiều thông số khác để đo hiệu năng của một server ngoài chỉ số kết nối. Hai server có thể đang có số kết nối tương đương nhưng một server lại phải chịu tải lớn hơn hẳn, do các công việc của server này có thể nặng tải hơn yêu cầu của server kia. Thuật toán RR cũng có khả năng xảy ra hiện tượng này.
Giả sử như một server đang chạy thấy mình đang phải chịu tải quá nặng, nó sẽ tự kiểm tra thông số này bằng cách kiểm tra tải của CPU, dung lượng RAM đang sử dụng…Nó sẽ gửi một thông báo đến bộ cân bằng tải báo rằng mình đang chịu tải quá cao. Chúng ta cần thiết kế một chương trình cài đặt trên từng server để kiểm tra thông số này, sau đó gửi về bộ cân bằng tải. Tại sao lại không thể cài đặt tích hợp vào bộ cân bằng tải? Vì bộ cân bằng tải sẽ không đo được tải của từng server, đặc biệt là khi các server ở xa nhau.
Server sẽ gửi đi một tin nhắn chẳng hạn như:
sendmessage( proxy_address, proxy_port, srv_add, srv_port, srvoverload);
Bộ cân bằng tải sẽ nhận tín hiệu này:
getMessage (srv_add, srv_port, srv_load);
Nếu tải của server lớn hơn một giá trị nào đó, chẳng hạn 85%, bộ cân bằng tải sẽ loại nó ra khỏi danh sách các server của proxy, tuy vậy phải giữ toàn bộ thông tin về nó trong proxy như vậy khi chúng ta thực hiện thuật toán sẽ không gửi thêm kết nối đến server này, nhưng các nhiệm vụ mà nó đang thực hiện vẫn được làm.
Do đó phải khai báo trong proxy một con trỏ cấu trúc server, lưu thông tin về các server bị quá tải. Thêm vào cấu trúc này mỗi khi cần
struct server *overload_srv;
Đẩy server bị quá tải ra khỏi tập server.
for(srv = px->svr; srv!=NULL; svr = srv->next) {
//Compare srv id and overload server id if(strcmp(srv->next->id, id))
//overload server {
temp = srv->next;
srv->next = temp->next;
temp->next = NULL;
} }
Sau đó cho nó vào overload_server.
temp->next = px->overload_srv;
px->overload_srv = temp;
temp->proxy = px;
Khi tải của server xuống dưới một giá trị nào đó, chẳng hạn 50%, nó sẽ cần phải gửi một tin nhắn thông báo là tải của nó đã nhẹ hơn, chúng ta sẽ lại cho nó vào trong danh sách các server của proxy.
sendmessage( proxy_address, proxy_port, srv_add, srv_port, srvOK);
Quá trình ở đây sẽ diễn ra ngược lại so với quá trình trên. Chúng ta sẽ đưa server ra khỏi danh sách các server đang bị overload và thêm nó vào trong danh sách các server đang chạy của proxy
Vì trường hợp quá tải ít khi xảy ra, do đó số lượng cũng như thời điểm mà server gửi message về cho bộ cân bằng tải sẽ không quá nhiều để có thể gây ra quá tải cho bộ cân bằng tải. Ngược lại thuật toán sẽ đảm bảo chúng ta không gửi thêm kết nối đến các server quá tải trong hệ thống, nhằm tránh trường hợp các server bị treo do phải phục vụ quá khả năng của nó.
Một số cải tiến khác chưa có điều kiện để thực thi do nguồn lực và thời gian có hạn, NVLV xin được trình bày trong phần định hướng của đồ án này