实现ODOO负载均衡集群我们主要需要做的是负载均衡器的搭建、负载均衡算法的实现、会话保持方案的实现、数据缓存以及共享存储方案的实现。
本文中我们使用yum在centos7上安装Nginx,首先通过rpm -ivh命令将Nginx源添加进来,然后#yum install nginx,即可成功安装Nginx。
Nginx官方文档中指出,Nginx支持轮询算法、最小连接算法、ip-hash算法3种负载均衡算法,但以上三种负载均衡算法都可以在配置中加入服务器节点的权重值来实现相应的带权重值的负载均衡算法,也就是说Nginx可实现6种负载均衡算法。
基本配置方法
在Centos7下,Nginx的配置文件默认为/etc/nginx/nginx.conf, Nginx作为负载均衡器最基本的配置方式为:
http {
upstream odoo_server {
server srv1.example.com;
server srv2.example.com;
server srv3.example.com;
}
server {
listen 80;
location / {
proxy_pass http://odoo_server;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
其中,odoo_server即ODOO集群节点服务器池,其中的3台服务器均运行了相同版本的ODOO实例。
在没有对负载均衡算法进行特殊配置的情况下,Nginx默认采用轮询算法。所有的请求按照平均分配的方式反向代理到odoo_server服务节点池中,Nginx默认应用Http负载均衡器来进行任务分配。
另一种配置方案是最小连接数优先算法,在完成某些请求需要花费较长时间的情况下,最小连接数优先算法表现的较为合适,它能更加合理地将请求分配到连接数较少的服务器节点上。
如果在Nginx的配置文件的服务器组中加入least_conn指令,Nginx将以最小连接数优先的方式来运行。
upstream odoo_server {
least_conn;
server odoo1;
server odoo2;
server odoo3;
}
通用哈希算法,客户端请求被发送至哪个服务器节点来处理取决于用户定义的一个关键词,这个关键词可以是文本、数字等的组合,还可以是来源IP或者端口。
upstream odoo_server {
hash $request_uri consistent;
server odoo1;
server odoo2;
server odoo3;
}
上面可选参数consistent用于开启ketama一致性哈希负载均衡。用户请求将会通过用户定义的关键词的哈希值均匀地分发到服务器节点。
2)服务器慢启动
服务器慢启动技术能防止刚刚恢复的服务器节点被大量连接覆盖而导致的超载进而失效。
在Nginx中,服务器慢启动技术能让集群中服务器节点在完全恢复并状态可用后权重逐步从0恢复到默认值。
具体配置方式:
upstream oe_server {
server odoo1 slow_start=30s;
server odoo2;
server odoo3;
}
实现本文中的动态负载均衡算法,主要需要解决动态获取并评价结点负载信息的问题和客户端与服务器结点长连接的问题,还需要在调度的同时实现服务器动态扩容的问题。负载调度我们是在Nginx的基础上修改而来的,为了尽可能少的修改Nginx的核心代码,对结点负载信息的获取以及动态扩容我们将在负载均衡结点上独立监控程序中实现,将最终结果输出到一个配置文件由Nginx来动态解析,该独立监控程序在监测到所有结点的负载都大于某个阈值的时候通过云平台SDK动态新建并初始化结点。
1)应用服务器性能监控及权值计算
本文中后端服务器均使用阿里云的云服务器ECS,云服务器(Elastic Compute Service,简称 ECS)是一种简单高效、处理能力可弹性伸缩的计算服务,获取ECS的系统参数需要安装相应的监控SDK。
安装aliyuncli:
pip install aliyuncli
pip install -Iv aliyun-python-sdk-cms==3.2.7
aliyuncli configure
安装cms-python-sdk的依赖:
pip install aliyun-python-sdk-core
pip install aliyun-python-sdk-cms
安装好相应的环境以后我们就可以通过调用相应的sdk来实现对云服务器的性能监控了。
第一步,需要初始化Client。
初始化Client时, 第一个参数是AccessKey,AccessKey和AccessSecret是访问阿里云API的一对钥匙;第二个参数是AccessSecret,AccessSecret相当于您的口令;第三个是默认RegionId, 实例代码如下:
from aliyunsdkcore import client
from aliyunsdkcms.request.v20160318 import QueryMetricListRequest
clt = client.AcsClient('your_access_key','your_access_secret','cn-hangzhou')
第二步, 初始化request
request = QueryMetricListRequest.QueryMetricListRequest()
request.set_accept_format('json')
request.set_Project('acs_slb')
request.set_Metric('ActiveConnection')
request.set_StartTime('2016-02-03T08:00:00Z')
request.set_Dimensions("{instanceId:'1527cf43124-cn-ningxiazhongwei'}")
request.set_Period('60')
第三步, 发起API调用
利用第一步初始化后的Client, 调用其do_action()方法, 将第二步中初始化好的request作为入参即可, 示例
result = clt.do_action(request)
print result
第四步,计算权重
由于获取服务器结点的性能信息时IO延迟较大,我们这里采用多线程的方式来同时获取各结点的负载信息,然后从根据第三章中推导出的权重计算公式来计算出各结点权重,并将权重值写入相应的配置文件。
部分代码如下:
def main():
filename = os.path.abspath(“filename”)
jobs = queue.Queue()
results = queue.Queue()
create_threads(limit,jobs,results,concurrency)
todo = add_jobs(filename, jobs)
process(todo, jobs, results, concurrency)
#将results以字典的形式写入预先定义好的配置文件
def create_threads(limit, jobs, results, concurrency):
for _ in range(concurrency):
thread = threading.Thread(target=worker, args=(limit, jobs, results))
thread.daemon = True
thread.start()
def worker(limit, jobs, results):
while True:
try:
server = jobs.get()
#这里执行第一步到第三步的代码来取得单一服务器结点的性能数据result
results.put(result)
2)动态扩容的实现
在负载均衡监控模块中需要通过监控各后端服务器的结点性能,如果发现所有后端服务器在一个时间段内长期处于高负荷状况,就需要增加新的结点到集群中来,用以提高集群的可用性。这种情况主要用在后端结点为虚拟机或者PaaS虚拟服务器环境下。我们通过相应的平台支持的开启虚拟机的调用来实现动态加入服务器结点。
动态扩容的实现在不同的环境下实际代码会有所差别,本文只需要在监控进程中遍历各服务器在一段时间内的负载状况,当负载大于某个阈值的时候启动一台新的虚拟主机,以此来实现动态扩容。
3)算法编程实现
通过上一节的代码,我们已经计算出各后端服务器结点的权重值,并已经动态地更新Nginx中新建的权重配置文件,现在我们只需要通过修改Nginx最小连接数算法的代码来实现权重数据的动态更新,就能在Nginx原有调度算法的基础上实现我们需要的动态负载均衡调度算法。
Nginx中主要包括了4个核心模块:core、event、http、mail,各模块下包含其自身的扩展代码,其中http模块定义了处理http请求和响应时需要做的一系列动作,http文件夹下包括了modules文件夹和一些系统本身的扩展,moudules文件夹中存放了一些额外的扩展。这里我们需要重点关注的是http文件夹中的upstream扩展,Nginx的http可分为3类:
Handler--接受请求,生成响应并返回;
Filter--对response进行修改操作;
Loadbalance--用于将负载分发到后端服务器处理。
upstream通过反向代理的方式作为负载均衡器使用时,通常采取轮询、加权轮询、ip-hash等负载调度算法。Upstream主要的几个回调函数,在请求处理的不同阶段会被调用。
在一个upstream配置块中,如果有least_conn指令,表示使用least connected负载均衡算法。
least_conn指令的解析函数为ngx_http_upstream_least_conn,主要做了:
指定初始化此upstream块的函数uscf->peer.init_upstream
指定此upstream块中server指令支持的属性。
详见附录A。
执行完指令的解析函数后,紧接着会调用所有HTTP模块的init main conf函数。
在执行ngx_http_upstream_module的init main conf函数时,会调用所有upstream块的初始化函数。
对于使用least_conn的upstream块,其初始化函数(peer.init_upstream)就是上一步中指定ngx_http_upstream_init_least_conn,它主要做了:
调用round robin的upstream块初始化函数来创建和初始化后端集群,保存该upstream块的数据指定per request的负载均衡初始化函数peer.init。
static ngx_int_t ngx_http_upstream_init_least_conn(ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)
{
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, cf->log, 0, "init least conn");
/* 使用round robin的upstream块初始化函数,创建和初始化后端集群 */
if (ngx_http_upstream_init_round_robin(cf, us) != NGX_OK)
return NGX_ERROR;
/* 重新设置per request的负载均衡初始化函数 */
us->peer.init = ngx_http_upstream_init_least_conn_peer;
return NGX_OK;
}
初始化请求的负载均衡数据 收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的ngx_http_upstream_init_request函数中,调用在第二步中指定的peer.init,主要用于初始化请求的负载均衡数据。
对于least_conn,peer.init实例为ngx_http_upstream_init_least_conn_peer,主要做了:
调用round robin的peer.init来初始化请求的负载均衡数据重新指定peer.get,用于从集群中选取一台后端服务器least_conn的per request负载均衡数据,和round robin的完全一样,都是一个ngx_http_upstream_rr_peer_data_t实例。
static ngx_int_t ngx_http_upstream_init_least_conn_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)
{
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "init least conn peer");
/* 调用round robin的per request负载均衡初始化函数 */
if (ngx_http_upstream_init_round_robin_peer(r, us) != NGX_OK)
return NGX_ERROR;
/* 指定peer.get,用于从集群中选取一台后端 */
r->upstream->peer.get = ngx_http_upstream_get_least_conn_peer;
return NGX_OK;
}
一般upstream块中会有多台后端,对于一次请求,要选定哪一台后端服务器,是负载均衡器最核心的功能,在Nginx中,通过第三步中的r->upstream->peer.get函数来实现:
采用least connected算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。
函数的返回值:
NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。
NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。
NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。
Upstream模块在处理用户请求时的函数调用关系如图4-1所示:
图 4-1 Nginx upstream函数调用关系
关键数据结构:
struct ngx_http_upstream_rr_peer_t {
struct sockaddr *sockaddr;
socklen_t socklen;
ngx_str_t name;
ngx_str_t server;
ngx_int_t current_weight;
ngx_int_t effective_weight;
ngx_int_t weight;
ngx_uint_t conns;
ngx_uint_t fails;
time_t accessed;
time_t checked;
ngx_uint_t max_fails;
time_t fail_timeout;
ngx_uint_t down; /* unsigned down:1; */
#if (NGX_HTTP_SSL)
void *ssl_session;
int ssl_session_len;
#endif
ngx_http_upstream_rr_peer_t *next;
#if (NGX_HTTP_UPSTREAM_ZONE)
ngx_atomic_t lock;
#endif
};
我们需要遍历配置文件并更新Nginx运行时的结点权重,就需要了解上面的数据结构,我们在定义动态权重配置文件时会以主机sockaddr及current_weight的键值对的形式保存,读取到内存后遍历更新peers数据结构。
在Nginx中,负责文件读取的函数存放在ngx_file.c和ngx.file.h中,后面我们会通过ngx_open_file()函数和ngx_read_file()函数来实现的自定义配置文件的读取。
王利萍[14]在基于Nginx服务器集群负载均衡技术的研究与改进中基于加权轮询算法的基础上做了修改,主要是在轮询模块中加入了后端结点动态参数获取及权重计算、更新的工作。类似的我们在ngx_http_upstream_least_conn_module.c文件中对Nginx函数ngx_http_upstream_get_least_conn_peer加入代码来使Nginx从自定义的配置文件中解析服务器的实时负载状况,然后通过最小连接数优先算法来从集群中选择相应的后端服务器。与之相区别的是我们加入的代码只需要加载由独立进程已经计算好的权重值,而不需要在调度器中访问后端服务器来获取参数以计算权重,进而避免了IO延迟所导致的请求调度性能的降低。
ngx_http_upstream_get_least_conn_peer函数主要是通过遍历peers来选择最小连接数的结点。
我们在循环判断前插入读取配置文件,以及更新权重的代码将从自定义的配置文件中读出的json数据,循环更新到peers结构体中,详见附录A。
3)长连接环境
ODOO的基础模块mail中,使用了长连接来保证服务器的最新消息到达客户端。所以在负载均衡这一中间环节,我们也应当开启长连接,在Nginx和后端服务器之间保持长连接还可以通过降低延迟来提升性能并能减少Nginx用光端口的可能性。
HTTP协议是基于TCP连接之上来发送HTTP请求和接受回复的,HTTP长连接允许重用TCP连接,用以避免频繁地建立、销毁TCP连接所带来的性能过载问题。
图 4-2 Nginx长连接示意图
NGINX保持了一个持久化连接的缓存区,即与后端应用服务器闲置的连接,Nginx利用已建立的TCP连接来替代新建TCP连接。这种方式降低了NGINX服务器与后端应用服务器之间事务处理延时并降低了短时间内NGINX服务器端口被耗尽的可能性。所以NGINX可以作为大流量环境下的负载均衡器。在一个很大峰值的流量下,缓存可以被清空,这时NGINX与后端服务器建立新的HTTP持久连接。
我们通过在配置文件中配置以下3个参数来实现长连接:
1. proxy_http_version, 一般设置为1.1,既强制为HTTP1.1版本;
2. proxy_set_header,
3. keepalive 对每台后端应用服务器可保持的最高闲置连接数。
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
upstream backend {
server odoo1;
server odoo2;
# maintain a maximum of 90 idle connections to each upstream server
keepalive 90;
}