Nginx的负载均衡

Stella981
• 阅读 624

上篇blog讲述了加权轮询算法的原理、以及负载均衡模块中使用的数据结构,接着我们来看看加权轮询算法的具体实现。

 指令的解析函数

 如果upstream配置块中没有指定使用哪种负载均衡算法,那么默认使用加权轮询。

也就是说使用加权轮询算法,并不需要特定的指令,因此也不需要实现指令的解析函数。

而实际上,和其它负载均衡算法不同(比如ip_hash),加权轮询算法并不是以模块的方式实现的,

而是作为Nginx框架的一部分。

 初始化upstream块 

在执行ngx_http_upstream_module的init main conf函数时,会遍历所有upstream配置块,调用它们

事先指定的初始化函数。对于一个upstream配置块,如果没有指定初始化函数,则调用加权轮询算法

提供的upstream块初始化函数 - ngx_http_upstream_init_round_robin。 

来看下ngx_http_upstream_module。

  1. ngx_http_module_t ngx_http_upstream_module_ctx = {

  2. ...

  3. ngx_http_upstream_init_main_conf, /* init main configuration */

  4. ...

  5. };

  6. static char *ngx_http_upstream_init_main_conf(ngx_conf_t *cf, void *conf)

  7. {

  8. ...

  9. /* 数组的元素类型是ngx_http_upstream_srv_conf_t */

  10. for (i = 0; i < umcf->upstreams.nelts; i++) {

  11. /* 如果没有指定upstream块的初始化函数,默认使用round robin的 */

  12. init = uscfp[i]->peer.init_upstream ? uscfp[i]->peer.init_upstream :

  13. ngx_http_upstream_init_round_robin;

  14. if (init(cf, uscfp[i] != NGX_OK) {

  15. return NGX_CONF_ERROR;

  16. }

  17. }

  18. ...

  19. }

ngx_http_upstream_init_round_robin做的工作很简单:

指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据。

创建和初始化后端集群、备份集群。

  1. ngx_int_t ngx_http_upstream_init_round_robin (ngx_conf_t *cf, ngx_http_upstream_srv_conf_t *us)

  2. {

  3. ngx_url_t u;

  4. ngx_uint_t i, j, n, w;

  5. ngx_http_upstream_server_t *server;

  6. ngx_http_upstream_rr_peer_t *peer, **peerp;

  7. ngx_http_upstream_rr_peers_t *peers, *backup;

  8. /* 指定请求的负载均衡初始化函数,用于初始化per request的负载均衡数据 */

  9. us->peer.init = ngx_http_upstream_init_round_robin_peer;

  10. /* upstream配置块的servers数组,在解析配置文件时就创建好了 */

  11. if (us->servers) {

  12. server = us->servers->elts;

  13. n = 0;

  14. w = 0;

  15. /* 数组元素类型为ngx_http_upstream_server_t,对应一条server指令 */

  16. for (i = 0; i < us->servers->nelts; i++) {

  17. if (server[i].backup)

  18. continue;

  19. n += server[i].naddrs; /* 所有后端服务器的数量 */

  20. w += server[i].naddrs * server[i].weight; /* 所有后端服务器的权重之和 */

  21. }

  22. if (n == 0) { /* 至少得有一台后端吧 */

  23. ...

  24. return NGX_ERROR;

  25. }

  26. /* 创建一个后端集群的实例 */

  27. peers = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peers_t));

  28. ...

  29. /* 创建后端服务器的实例,总共有n台 */

  30. peer = ngx_pcalloc(cf->pool, sizeof(ngx_http_upstream_rr_peer_t) * n);

  31. ...

  32. /* 初始化集群 */

  33. peers->single = (n == 1); /* 是否只有一台后端 */

  34. peers->number = n; /* 后端服务器的数量 */

  35. peers->weight = (w != n); /* 是否使用权重 */

  36. peers->total_weight = w; /* 所有后端服务器权重的累加值 */

  37. peers->name = &us->host; /* upstream配置块的名称 */

  38. n = 0;

  39. peerp = &peers->peer;

  40. /* 初始化代表后端的结构体ngx_http_upstream_peer_t.

  41. * server指令后跟的是域名的话,可能对应多台后端.

  42. */

  43. for(i = 0; i < us->servers->nelts; i++) {

  44. if (server[i].backup)

  45. continue;

  46. for (j = 0; j < server[i].naddrs; j++) {

  47. peer[n].sockaddr = server[i].addrs[j].sockaddr; /* 后端服务器的地址 */

  48. peer[n].socklen = server[i].addrs[j].socklen; /* 地址的长度*/

  49. peer[n].name = server[i].addrs[j].name; /* 后端服务器地址的字符串 */

  50. peer[n].weight = server[i].weight;  /* 配置项指定的权重,固定值 */

  51. peer[n].effective_weight = server[i].weight; /* 有效的权重,会因为失败而降低 */

  52. peer[n].current_weight = 0; /* 当前的权重,动态调整,初始值为0 */

  53. peer[n].max_fails = server[i].max_fails; /* "一段时间内",最大的失败次数,固定值 */

  54. peer[n].fail_timeout = server[i].fail_timeout; /* "一段时间"的值,固定值 */

  55. peer[n].down = server[i].down; /* 服务器永久不可用的标志 */

  56. peer[n].server = server[i].name; /* server的名称 */

  57. /* 把后端服务器组成一个链表,第一个后端的地址保存在peers->peer */

  58. *peerp = &peer[n];

  59. peerp = &peer[n].next;

  60. n++;

  61. }

  62. }

  63. us->peer.data = peers; /* 保存后端集群的地址 */

  64. }

  65. /* backup servers */

  66. /* 创建和初始化备份集群,peers->next指向备份集群,和上述流程类似,不再赘述 */

  67. ...

  68. /* an upstream implicitly defined by proxy_pass, etc. */

  69. /* 如果直接使用proxy_pass指令,没有定义upstream配置块 */

  70. if (us->port == 0) {

  71. ...

  72. return NGX_ERROR;

  73. }

  74. ngx_memzero(&u, sizeof(ngx_url_t));

  75. u.host = us->host;

  76. u.port = us->port;

  77. /* 根据URL解析域名 */

  78. if (ngx_inet_resolve_host(cf->pool, &u) != NGX_OK) {

  79. ...

  80. return NGX_ERROR;

  81. }

  82. n = u.naddrs; /* 共有n个后端 */

  83. /* 接下来创建后端集群,并进行初始化,和上述流程类似,这里不再赘述 */

  84. ...

  85. return NGX_OK;

  86. }

 初始化请求的负载均衡数据

当收到一个请求后,一般使用的反向代理模块(upstream模块)为ngx_http_proxy_module,

其NGX_HTTP_CONTENT_PHASE阶段的处理函数为ngx_http_proxy_handler,在初始化upstream机制的

函数ngx_http_upstream_init_request中,调用在第二步中指定的peer.init,主要用于:

创建和初始化该请求的负载均衡数据块

指定r->upstream->peer.get,用于从集群中选取一台后端服务器(这是我们最为关心的)

指定r->upstream->peer.free,当不用该后端时,进行数据的更新(不管成功或失败都调用)

指定r->upstream->peer.tries,请求最多允许尝试这么多个后端

  1. ngx_int_t ngx_http_upstream_init_round_robin_peer(ngx_http_request_t *r, ngx_http_upstream_srv_conf_t *us)

  2. {

  3. ngx_uint_t n;

  4. ngx_http_upstream_rr_peer_data_t *rrp;

  5. /* 创建请求的负载均衡数据块 */

  6. rrp = r->upstream->peer.data;

  7. if (rrp == NULL) {

  8. rrp = ngx_palloc(r->pool, sizeof(ngx_http_upstream_rr_peer_data_t));

  9. if (rrp == NULL)

  10. return NGX_ERROR;

  11. r->upstream->peer.data = rrp; /* 保存请求负载均衡数据的地址 */

  12. }

  13. rrp->peers = us->peer.data; /*  upstream块的后端集群 */

  14. rrp->current = NULL;

  15. n = rrp->peers->number; /* 后端的数量 */

  16. /* 如果存在备份集群,且其服务器数量超过n */

  17. if (rrp->peers->next && rrp->peers->next->number > n) {

  18. n = rrp->peers->next->number;

  19. }

  20. /* rrp->tried指向后端服务器的位图,每一位代表一台后端的状态,0表示可用,1表示不可用。

  21. * 如果后端数较少,直接使用rrp->data作为位图。如果后端数较多,则需要申请一块内存。

  22. */

  23. if (n <= 8 *sizeof(uintptr_t)) {

  24. rrp->tried = &rrp->data;

  25. rrp->data = 0;

  26. } else {

  27. n = ( n + (8 * sizeof(uintptr_t) - 1)) / (8 * sizeof(uintptr_t)); /* 向上取整 */

  28. rrp->tried = ngx_pcalloc(r->pool, n * sizeof(uintptr_t));

  29. if (rrp->tried == NULL) {

  30. return NGX_ERROR;

  31. }

  32. }

  33. r->upstream->peer.get = ngx_http_upstream_get_round_robin_peer; /* 指定peer.get,用于从集群中选取一台后端服务器 */

  34. r->upstream->peer.free = ngx_http_upstream_free_round_robin_peer; /* 指定peer.free,当不用该后端时,进行数据的更新 */

  35. r->upstream->peer.tries = ngx_http_upstream_tries(rrp->peers); /* 指定peer.tries,是请求允许尝试的后端服务器个数 */

  36. ...

  37. return NGX_OK;

  38. }

  39. #define ngx_http_upstream_tries(p) ((p)->number + ((p)->next ? (p)->next->number : 0))

 选取一台后端服务器

 一般upstream块中会有多台后端,那么对于本次请求,要选定哪一台后端呢?

这时候第三步中r->upstream->peer.get指向的函数就派上用场了:

采用加权轮询算法,从集群中选出一台后端来处理本次请求。 选定后端的地址保存在pc->sockaddr,pc为主动连接。

函数的返回值:

NGX_DONE:选定一个后端,和该后端的连接已经建立。之后会直接发送请求。

NGX_OK:选定一个后端,和该后端的连接尚未建立。之后会和后端建立连接。

NGX_BUSY:所有的后端(包括备份集群)都不可用。之后会给客户端发送502(Bad Gateway)。

  1. ngx_int_t ngx_http_upstream_get_round_robin_peer(ngx_peer_connection_t *pc, void *data)

  2. {

  3. ngx_http_upstream_rr_peer_data_t  *rrp = data; /* 请求的负载均衡数据 */

  4. ngx_int_t                      rc;

  5. ngx_uint_t                     i, n;

  6. ngx_http_upstream_rr_peer_t   *peer;

  7. ngx_http_upstream_rr_peers_t  *peers;

  8. ...

  9. pc->cached = 0;

  10. pc->connection = NULL;

  11. peers = rrp->peers; /* 后端集群 */

  12. ...

  13. /* 如果只有一台后端,那就不用选了 */

  14. if (peers->single) {

  15. peer = peers->peer;

  16. if (peer->down)

  17. goto failed;

  18. rrp->current = peer;

  19. } else {

  20. /* there are several peers */

  21. /* 调用ngx_http_upstream_get_peer来从后端集群中选定一台后端服务器 */

  22. peer = ngx_http_upstream_get_peer(rrp);

  23. if (peer == NULL)

  24. goto failed;

  25. ...

  26. }

  27. /* 保存选定的后端服务器的地址,之后会向这个地址发起连接 */

  28. pc->sockaddr = peer->sockaddr;

  29. pc->socklen = peer->socklen;

  30. pc->name = &peer->name;

  31. peer->conns++; /* 增加选定后端的当前连接数 */

  32. ...

  33. return NGX_OK;

  34. failed:

  35. /* 如果不能从集群中选取一台后端,那么尝试备用集群 */

  36. if (peers->next) {

  37. ...

  38. rrp->peers = peers->next;

  39. n = (rrp->peers->number + (8 * sizeof(uintptr_t) - 1))

  40. / (8 * sizeof(uintptr_t));

  41. for (i = 0; i < n; i++)

  42. rrp->tried[i] = 0;

  43. /* 重新调用本函数 */

  44. rc = ngx_http_upstream_get_round_robin_peer(pc, rrp);

  45. if (rc != NGX_BUSY)

  46. return rc;

  47. }

  48. /* all peers failed, mark them as live for quick recovery */

  49. for (peer = peers->peer; peer; peer = peer->next) {

  50. peer->fails = 0;

  51. }

  52. pc->name = peers->name;

  53. return NGX_BUSY;

  54. }

ngx_http_upstream_get_peer用于从集群中选取一台后端服务器。

  1. static ngx_http_upstream_rr_peer_t *ngx_http_upstream_get_peer(ngx_http_upstream_rr_peer_data_t *rrp)

  2. {

  3. time_t                        now;

  4. uintptr_t                     m;

  5. ngx_int_t                     total;

  6. ngx_uint_t                    i, n, p;

  7. ngx_http_upstream_rr_peer_t  *peer, *best;

  8. now = ngx_time();

  9. best = NULL;

  10. total = 0;

  11. ...

  12. /* 遍历集群中的所有后端 */

  13. for (peer = rrp->peers->peer, i = 0;

  14. peer;

  15. peer = peer->next, i++)

  16. {

  17. n = i / (8 * sizeof(uintptr_t));

  18. m = (uintptr_t) 1 << i % (8 * sizeof(uintptr_t));

  19. /* 检查该后端服务器在位图中对应的位,为1时表示不可用 */

  20. if (rrp->tried[n] & m)

  21. continue;

  22. /* 永久不可用的标志 */

  23. if (peer->down)

  24. continue;

  25. /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,那么不允许使用此后端了 */

  26. if (peer->max_fails

  27. && peer->fails >= peer->max_fails

  28. && now - peer->checked <= peer->fail_timeout)

  29. continue;

  30. peer->current_weight += peer->effective_weight; /* 对每个后端,增加其当前权重 */

  31. total += peer->effective_weight; /* 累加所有后端的有效权重 */

  32. /* 如果之前此后端发生了失败,会减小其effective_weight来降低它的权重。

  33. * 此后在选取后端的过程中,又通过增加其effective_weight来恢复它的权重。

  34. */

  35. if (peer->effective_weight < peer->weight)

  36. peer->effective_weight++;

  37. /* 选取当前权重最大者,作为本次选定的后端 */

  38. if (best == NULL || peer->current_weight > best->current_weight) {

  39. best = peer;

  40. p = i;

  41. }

  42. }

  43. if (best == NULL) /* 没有可用的后端 */

  44. return NULL;

  45. rrp->current = best; /* 保存本次选定的后端 */

  46. n = p / (8 * sizeof(uintptr_t));

  47. m = (uintptr_t) 1 << p % (8 * sizeof(uintptr_t));

  48. /* 对于本次请求,如果之后需要再次选取后端,不能再选取这个后端了 */

  49. rrp->tried[n] |= m;

  50. best->current_weight -= total; /* 选定后端后,需要降低其当前权重 */

  51. /* 更新checked时间 */

  52. if (now - best->checked > best->fail_timeout)

  53. best->checked = now;

  54. return best;

  55. }

 释放一台后端服务器

当不再使用一台后端时,需要进行收尾处理,比如统计失败的次数。

这时候会调用第三步中r->upstream->peer.get指向的函数。函数参数state的取值:

0,请求被成功处理

NGX_PEER_FAILED,连接失败

NGX_PEER_NEXT,连接失败,或者连接成功但后端未能成功处理请求

一个请求允许尝试的后端数为pc->tries,在第三步中指定。当state为后两个值时:

如果pc->tries不为0,需要重新选取一个后端,继续尝试,此后会重复调用r->upstream->peer.get。

如果pc->tries为0,便不再尝试,给客户端返回502错误码(Bad Gateway)。

  1. void ngx_http_upstream_free_round_robin_peer(ngx_peer_connection_t *pc, void *data,

  2. ngx_uint_t state)

  3. {

  4. ngx_http_upstream_rr_peer_data_t  *rrp = data; /* 请求的负载均衡数据 */

  5. time_t                       now;

  6. ngx_http_upstream_rr_peer_t  *peer;

  7. ...

  8. peer = rrp->current; /* 当前使用的后端服务器 */

  9. if (rrp->peers->single) {

  10. peer->conns--; /* 减少后端的当前连接数 */

  11. pc->tries = 0; /* 不能再继续尝试了 */

  12. return;

  13. }

  14. /* 如果连接后端失败了 */

  15. if (state & NGX_PEER_FAILED) {

  16. now = ngx_time();

  17. peer->fails++; /* 一段时间内,已经失败的次数 */

  18. peer->accessed = now; /* 最近一次失败的时间点 */

  19. peer->checked = now; /* 用于检查是否超过了“一段时间” */

  20. /* 当后端出错时,降低其有效权重 */

  21. if (peer->max_fails)

  22. peer->effective_weight -= peer->weight / peer->max_fails;

  23. /* 有效权重的最小值为0 */

  24. if (peer->effective_weight < 0)

  25. peer->effective_weight = 0;

  26. } else {

  27. /* mark peer live if check passed */

  28. /* 说明距离最后一次失败的时间点,已超过fail_timeout了,清零fails */

  29. if (peer->accessed < peer->checked)

  30. peer->fails = 0;

  31. }

  32. peer->conns--; /* 更新后端的当前连接数 */

  33. if (pc->tries)

  34. pc->tries--;  /* 对于一个请求,允许尝试的后端个数 */

  35. }

判断后端是否可用

相关的变量的定义

ngx_uint_t fails; /* 一段时间内,已经失败的次数 */

time_t accessed; /* 最近一次失败的时间点 */

time_t checked; /* 用于检查是否超过了“一段时间” */

ngx_uint_t max_fails; /* 一段时间内,允许的最大的失败次数,固定值 */

time_t fail_timeout; /* “一段时间”的长度,固定值 */

  1. ngx_http_upstream_get_peeer

  2. /* 在一段时间内,如果此后端服务器的失败次数,超过了允许的最大值,

  3. * 那么在此后的一段时间内不允许使用此后端了。

  4. */

  5. if (peer->max_fails && peer->fails >= peer->max_fails &&

  6. now - peer->checked <= peer->fail_timeout)

  7. continue;

  8. ...

  9. /* 选定本后端了 */

  10. if (now - best->checked > best->fail_timeout)

  11. best->checked = now;

  12. ngx_http_upstream_free_round_robin_peer

  13. if (state & NGX_PEER_FAILED) {

  14. peer->fails++;

  15. peer->accessed = now;

  16. peer->checked = now;

  17. ...

  18. } else if (peer->accessed < peer->checked)

  19. peer->fails = 0;

相关变量的更新

accessed:释放peer时,如果发现后端出错了,则更新为now。

checked:释放peer时,如果发现后端出错了,则更新为now。选定该peer时,如果now - checked > fail_timeout,则更新为now。

fails:释放peer时,如果本次成功了且accessed < checked,说明距离最后一次失败的时间点,已超过fail_timeout了,清零fails。

上述变量的准备定义

fails并不是“一段时间内”的失败次数,而是两两间时间间隔小于“一段时间”的连续失败次数。

max_fails也不是“一段时间内”允许的最大失败次数,而是两两间的时间间隔小于“一段时间”的最大失败次数。

举例说明,假设fail_timeout为10s,max_fails为3。

10s内失败3次,肯定会导致接下来的10s不可用。

27s内失败3次,也可能导致接下来的10s不可用,只要3次失败两两之间的时间间隔为9s。

下图用来简要说明

 Nginx的负载均衡

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
2年前
LNMP架构之负载均衡及HTTPS相关配置
本文索引:Nginx负载均衡ssl原理生成ssl密钥对Nginx配置sslNginx负载均衡负载均衡原理上就是代理,只不过通过设置多个代理服务器来实现多用户访问时的负载均衡。同时也可以在某个代理服务器无法访问时,切换到另外的代理服务器,从而实现访问不间断的目的。下面以qq.com为例
Stella981 Stella981
2年前
QPS 提升60%,揭秘阿里巴巴轻量级开源 Web 服务器 Tengine 负载均衡算法
前言在阿里七层流量入口接入层(ApplicationGateway)场景下,Nginx官方的SmoothWeightedRoundRobin(SWRR)负载均衡算法已经无法再完美施展它的技能。Tengine通过实现新的负载均衡算法VirtualNodeSmoothWeightedRoundRobin(VNSWRR)不
Easter79 Easter79
2年前
SpringCloud全家桶学习之客户端负载均衡及自定义负载均衡算法
一、Ribbon是什么?  SpringCloudRibbon是基于NetflixRibbon实现的一套客户端 负载均衡的工具(这里区别于nginx的负载均衡)。简单来说,Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法,将Netflix中间服务连接在一起。Ribbon客户端组件
Stella981 Stella981
2年前
SOFA 源码分析 — 负载均衡和一致性 Hash
!(https://oscimg.oschina.net/oscnet/76a9ee48bb4c7f7b344343922f049224d4d.png)前言SOFA内置负载均衡,支持5种负载均衡算法,随机(默认算法),本地优先,轮询算法,一致性hash,按权重负载轮询(不推荐,已被标注废弃)。一起看看他们的实现(重点还是一致性
Stella981 Stella981
2年前
Dubbo的负载均衡算法
\toc\1简介Dubbo提供了4种负载均衡机制:权重随机算法:RandomLoadBalance最少活跃调用数算法:LeastActiveLoadBalance一致性哈希算法:ConsistentHashLoadBalance加权轮询算法:RoundRobinLoadBalan
Stella981 Stella981
2年前
Nginx相关
Nginx篇WindowsNginx配置使用Nginx负载均衡模式1)、轮询——1:1轮流处理请求(默认)每个请求按时间顺序逐一分配到不同的应用服务器,如果应用服务器down掉,自动剔除,剩下的继续轮询。2)、权重——you
解密负载均衡技术和负载均衡算法
什么是负载均衡技术负载均衡器是一种软件或硬件设备,它起到了将网络流量分散到一组服务器的作用,可以防止任何一台服务器过载。负载均衡算法就是负载均衡器用来在服务器之间分配网络流量的逻辑(算法是一组预定义的规则),有时候也叫做负载均衡的类型。负载均衡