bboyjing's blog

跟开涛学架构一【Nginx负载均衡】

最近在张开涛所著的《亿级流量网站架构核心技术》,粗粗浏览了下,核心思想是围绕着Nginx在转。如果平时没有一些运维实际经验的话,可能整本书看下来效果不是很好。要想吸收更多的知识,光看是没用的,还是得亲自动手操作。书中有不少代码片段,我会尝试尽量把案例都实现,组成可以跑通的完整测试项目,以达到学习的目的。本系列学习笔记的内容大多来自这本书,如果涉及到版权问题,请及时联系本人。下面就进入正题了。

环境准备

目前需要的环境是Java运行环境和Nginx。

Nginx

Nginx的话直接安装Openresty最新版(目前版本1.11.2.5),具体参照官网安装步骤。本机环境为Mac OS X,直接采用官方推荐的方式:

1
> brew install homebrew/nginx/openresty

安装完之后的目录在/usr/local/Cellar/openresty下,直接运行openresty命令即可:

1
2
3
4
5
6
7
> sudo openresty
> curl localhost
!DOCTYPE html>
<html>
<head>
<title>Welcome to OpenResty!</title>
...

如果还是想执行nginx命令的话,将脚本路径加入到的PATH变量。具体怎么加,按照自己的系统来,下面举个本机的例子:

1
2
3
> vim ~/.zshrc
export PATH="$PATH:/usr/local/Cellar/openresty/1.11.2.5/nginx/sbin"
> source ~/.zshrc

Java

Java环境的准备就不多说了,这里主要是提一下建测试项目,依然采用目前比较便捷的SpringBoot搭建后端项目。项目地址取名hunger,本系列涉及到的所有项目都在这个目录下。

Nginx常见用法

感觉这本书面向的人群需要具有目前主流架构的简单了解,尤其是Nginx。本人也是一边查资料一遍学,过程就不写出来了,还是以实践为主。

负载均衡/反向代理

其意思就是请求落到Nginx,Nginx代理了后端真正的服务地址,同时又能使流量均衡地发送过去。是通过upstream配置来实现该功能的。下面我们要做如下工作才能完成测试:

  1. 配置Nginx,并且重启。我们就直接在hungeru目录下创建conf/nginx.conf文件。参照官网,文件如下内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    worker_processes 1;
    error_log logs/error.log;
    events {
    worker_connections 1024;
    }
    http {
    server {
    listen 80;
    location / {
    default_type text/html;
    content_by_lua '
    ngx.say("<p>hello, world</p>")
    ';
    }
    }
    }

    尝试启动Nginx

    1
    2
    > sudo nginx -c ~/IdeaProjects/hunger/conf/nginx.conf
    会报错,提示没有logs文件夹,需要在/usr/local/Cellar/openresty/1.11.2.5/nginx下手动添加logs文件夹,再启动就好了。

    启动成功之后,访问localhost会成功输出hello, world。content_by_lua配置项属于Openresty范畴,如果不了解的话,需要自行再去学习了。

  2. 新建Java项目,我们在hunger目录下快速创建2个项目,过程就不写出来了。贴出核心代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RequestMapping(path = "/", method = RequestMethod.GET)
    public String handle() {
    return "from real server 1";
    }
    @RequestMapping(path = "/", method = RequestMethod.GET)
    public String handle() {
    return "from real server 2";
    }
  3. 模拟业务项目准备完毕之后,就可以来改Nginx配置了,主要是修改nginx.conf文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    http {
    upstream real_server {
    server localhost:8081;
    server localhost:8082;
    }
    server {
    listen 80;
    location / {
    proxy_pass http://real_server;
    }
    }
    }

上述配置完毕,将项目都启动,即可进行测试,稍微解释下上述配置文件。当有localhost:80的请求过来,nginx会反向代理到proxy_pass配置的upstream server,然后请求会平均地落到两台实际业务服务器上。测试结果如下:

1
2
3
4
5
> curl localhost
from real server 1%
> curl localhost
from real server 2%
...

负载均衡算法

负载均衡指的是如何将流量分配到upstream server,默认采用的是round-roobin,从上面的测也也可以看出来,请求是平均地落到real_server上。同时还支持其他几种算法:

  • weighte权重算法,通过给server配置weight可以实现基于权重的轮询,修改nginx.conf文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    upstream real_server {
    server localhost:8081 weight=1;
    server localhost:8082 weight=2;
    }
    # 测试结果
    > curl localhost
    from real server 1%
    > curl localhost
    from real server 2%
    > curl localhost
    from real server 2%
    ...

    上述配置的意思是每3个请求中的1个落到8081,另外2个落到8082。可以请求localhost:80来测试下。

  • ip_hash算法,根据客户端IP进行负载均衡,即相同的IP将负载均衡到同一个upstream,修改nginx.conf文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    upstream real_server {
    ip_hash;
    server localhost:8081;
    server localhost:8082;
    }
    # 经过测试,本机所有的请求都落在8081上,证明配置生效
    > curl localhost
    from real server 1%
    > curl localhost
    from real server 1%
    ...
  • hash key[consistent],对某个key进行哈希或者使用一致性哈希算法进行负载均衡。这里简单地根据请求uri进行负载均衡。修改java项目和nginx.conf来测试:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // Java项目新增接口
    @RequestMapping(path = "/{uri}", method = RequestMethod.GET)
    public String handleUri(@PathVariable String uri) {
    return "from real server 1, param is " + uri;
    }
    @RequestMapping(path = "/{uri}", method = RequestMethod.GET)
    public String handleUri(@PathVariable String uri) {
    return "from real server 2, param is " + uri;
    }
    # 修改nginx配置
    upstream real_server {
    hash $uri;
    server localhost:8081;
    server localhost:8082;
    }
    # 测试结果
    > curl localhost/a
    from real server 1, param is a
    > curl localshot/c
    from real server 2, param is c

失败重试

通过upstream server的max_fails和fail_timeout来实现该功能。意思是在fail_timeout时间内失败了max_fails次请求后,则认为该上游服务器不可用,然后将该服务地址踢除掉。fail_timeout时间后会再次将该服务器加入存活列表,进行重试。修改nginx.conf文件:

1
2
3
4
upstream real_server {
server localhost:8081 max_fails=2 fail_timeout=60s;
server localhost:8082 max_fails=2 fail_timeout=60s;
}

下面我们来测试下:

  1. 停掉real_server_2
  2. 请求localhost,并查看输出以及nginx error.log

    1
    2
    3
    4
    > curl localhost
    from real server 1%
    > tail -f /usr/local/Cellar/openresty/1.11.2.5/nginx/logs/error.log
    ...Connection refused...upstream: "http://127.0.0.1:8082/"...
  3. 重启real_server_2后会再次连上

通过wireshark截取请求可以看出,确实在判断出real_servert_2不在线之后,fail_timeout时间内没有对它再请求,以此可以作为nginx把服务器地址踢掉的证据。另外,翻了一下书本,后面有一章专门讲超超时与重试机制,到那时候再详细看看。

健康检查

Nginx对上游服务器的健康检查默认采用的是惰性策略,书中的例子是通过集成nginx_upstream_check_module模块来进行主动健康检查。但是Openresty本身自带了一个叫lua-resty-upstream-healthcheck的模块,可以提供这样的功能。我们就直接使用该模块做测试,下面开始可能会比较多地涉及Openresty的相关内容,建议先看下开涛老师的一个系列博客入门。假定我们已经有一点基础了,就来试下吧:

  1. 在conf目录下新建一个名为lua的文件夹,以后所有的的lua文件都放到conf/lua目录下,然后在lua目录下新建healthcheck.lua

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    -- 引入healthcheck模块
    local hc = require "resty.upstream.healthcheck"
    healthcheck = function()
    local ok, err = hc.spawn_checker {
    shm = "healthcheck",
    upstream = "real_server",
    type = "http",
    http_req = "GET / HTTP/1.0\r\nHost: real_server\r\n\r\n",
    interval = 2000,
    timeout = 5000,
    fall = 3,
    rise = 2,
    valid_statuses = {200, 302},
    concurrency = 1,
    }
    if not ok then
    ngx.log(ngx.ERR, "=====> failed to spawn health checker: ", err)
    return
    end
    end
    -- 调用上面定义的function
    healthcheck()
  2. 修改nginx.conf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    http {
    # 共享全局变量,在所有worker间共享
    lua_shared_dict healthcheck 1m;
    # 关闭cosocket错误日志
    lua_socket_log_errors off;
    # 用于启动一些定时任务,比如心跳检查,定时拉取服务器配置等等
    # 此处的任务数量 == Worker进程数
    init_worker_by_lua_file /Users/zhangjing/IdeaProjects/hunger/conf/lua/healthcheck.lua;
    upstream real_server {
    server localhost:8081;
    server localhost:8082;
    }
    server {
    ...
    location /status {
    access_log off;
    allow 127.0.0.1;
    deny all;
    content_by_lua_block {
    local hc = require "resty.upstream.healthcheck"
    ngx.say("Nginx Worker PID: ", ngx.worker.pid())
    ngx.print(hc.status_page())
    }
    }
    }
    }
  3. 测试,启动real_server_1、real_server_2、nginx

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    > curl localhost/status
    Nginx Worker PID: 59109
    Upstream real_server
    Primary Peers
    127.0.0.1:8081 up
    [::1]:8081 up
    127.0.0.1:8082 up
    [::1]:8082 up
    Backup Peers
    # 停掉real_server_2
    > curl localhost/status
    Nginx Worker PID: 59109
    Upstream real_server
    Primary Peers
    127.0.0.1:8081 up
    [::1]:8081 up
    127.0.0.1:8082 DOWN
    [::1]:8082 DOWN
    Backup Peers

其他配置项

  • 备份上游服务器,通过backup来指定,当所有主服务器都不存活时,请求会发给备服务器
  • 不可用上游服务器,通过down来指定,表示该上游服务器永久不可用

这两个配置项比较简单,就不测试了。另外,这一章节就学到这里吧。