bboyjing's blog

跟开涛学架构三【Nginx动态负载均衡】

本系列第一章节我们就知道了如何配置upstream。下面要学习的是如何不写死服务列表,而是通过服务注册、发现的方式来实现upstream列表的动态变更。书中是使用Consul来实现的,它是一款开源的分布式服务注册与发现系统。简单来说,其核心思想就是,upstream服务向Consul注册服务,Nginx能监听到Consul上注册的服务变更,然后修改upstream列表,最后重启Nginx。本人也没有使用过该产品,就一起来学习下吧。

了解Consul

首先得稍微了解下Consul,要不然玩儿不起来。

  1. 安装。本人直接使用brew install consul,如果是其他系统,可以去官网下载对应的安装包。
  2. 启动。浏览了下官网,感觉跟Zookeeper有点像。通过命令consul agent -dev启动,如果能正常打印出日志,表示安装成功了。这里就不研究Consul,有兴趣的同学自行去官网看文档。
  3. Web UI。Consul自带Web UI,而且-dev模式下是默认启动的,访问localhost:8500/ui即可。

注册服务

将服务注册到Consul。和Consul的交互可以使用HTTP协议,发送如下请求可以注册一个服务到Consul:

1
2
3
4
5
6
7
8
9
curl -X PUT http://localhost:8500/v1/agent/service/register -d '
{
"ID": "real_server_1",
"Name": "real_server",
"Tags": ["dev"],
"Address": "127.0.0.1",
"Port": 8081
}
'

成功之后登录控制台,会看到SERVICES下多出了一个real_server服务:
hunger_4
书中注册服务是请求的catalog API,但是注册完之后过会儿会被自动deregister,查了下文档,换成上述API了。另外,有一点和zookeeper不太一样的是,停止Consul后,之前注册的服务就消失了。不知道和目前测试用的是单节点有没关系,如果线上准备使用该工具,还得详细地看完文档再说。解释下上述API的几个参数:

  • ID:代表要注册的服务的唯一标识
  • Name:表示一组服务的名称
  • Tags:服务标签,可以用于区分开发、测试环境
  • Address:服务的地址
  • Port:服务的端口

还有一个real_server_2也要注册上去,改下参数ID和Port就行了,就不贴出来了。

Consul-template

Consul-template的作用是生成upstream的配置模板。这个不包含在consul中,是单独的工具,先安装brew install consul-template。接下来在conf目录下新建real_server.ctmpl文件,内容如下:

1
2
3
4
5
upstream real_server {
{{range service "dev.real_server@dc1"}}
server {{.Address}}:{{.Port}};
{{end}}
}

大概可以看出是遍历数据中心dc1中的Tags为dev,Name为real_server的服务。Consul启动的时候打印日志有一行Datacenter: 'dc1',这个就是数据中心。其余的占位符都对应注册服务发送的API请求参数。
还需要再新建一个Nginx重启脚本,同样再conf目录下新建restart.sh,并且给该文件执行权限:

1
2
3
4
5
6
7
8
9
ps -ef | grep nginx | grep -v grep
if [ $? -ne 0 ]
then
sudo nginx -c ./nginx.conf
echo "nginx start"
else
sudo nginx -s reload
echo "ngixn restart"
fi

最后在修改ngxin.conf,删除原来的upstream配置,通过includ引入配置文件

1
2
3
4
5
http {
...
include /Users/zhangjing/IdeaProjects/hunger/conf/real_server.conf
...
}

Consul-template配置完毕,先不启动,还差一步。对了,有一点先注意下,我们新建的模板是real_server.ctmpl,而nginx.conf中include的文件是real_server.conf。

Java服务

在项目中引入Consul Java Client可以实现服务的注册与摘除,那上面直接通过HTTP的注册方式就当了解下了。下面贴出核心代码:

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
@Configuration
public class ConsulConfiguration {
@Value("${server.port}")
private int port;
@Value("${spring.application.name}")
private String serviceId;
@PostConstruct
public void init() {
// 参数完全对应HTTP API
ImmutableRegistration.Builder builder = ImmutableRegistration.builder()
.id(serviceId)
.name("real_server")
.address("127.0.0.1")
.port(port)
.addTags("dev");
// 向Consul注册服务
Consul consul = Consul.builder().withHostAndPort(HostAndPort.fromString("127.0.0.1:8500")).build();
final AgentClient agentClient = consul.agentClient();
agentClient.register(builder.build());
//注册shutdown hook,停掉应用时从Consul摘除服务
Runtime.getRuntime().addShutdownHook(new Thread(() -> agentClient.deregister(serviceId)));
}
}

启动项目,然后登到Consul Web界面看一下,服务是否注册成功。另外,不要忘了把代码拷到real_server_2中,port和serviceId是取的配置文件中的值,所以不用改啥。另外,如果是真实环境的话serviceId可以取成ip地址+端口号,这样同一个应用的serviceId自然就不一样了。

测试

在整体测试之前,我们还不知道consul-template怎么用。下面就利用consul、real_server.ctmpl和Java服务,来先了测试下consul-template。

  1. 启动Consul:consul agent -dev
  2. real_server_1、real_server_2
  3. 在terminal中执行如下命令:
    1
    2
    3
    consul-template -consul-addr 127.0.0.1:8500 -template \
    "/Users/zhangjing/IdeaProjects/hunger/conf/real_server.ctmpl:\
    /Users/zhangjing/IdeaProjects/hunger/conf/real_server.conf"

成功之后会发现多了一个文件real_server.conf,其内容为:

1
2
3
4
upstream real_server {
server 127.0.0.1:8081;
server 127.0.0.1:8082;
}

其过程就是consul-template会监听指定的consul(-consul-addr指定的地址),监听的内容在real_server.ctmpl中定义。一旦相关内容有改动(此处指的是注册在consul上的服务),就会把更改后的内容输出到指定文件(此处是real_server.conf)。这里-template参数的格式为”模板源文件:将要写入的目标文件”。如果停掉其中一个服务,real_server.conf的内容会即时更新。可见达到实时更改配置的效果了。
有一点需要说明一下,consul-template是触发执行的,也就说监听的内容有改动才会触发命令。所以,有可能当时执行consul-template命令的时候什么事都不会发生,会误以为命令没有生效。这一点困扰了我一会儿,所以就提一下。

至此,相关知识点已经梳理完毕,下面整合起来测试下,列出测试步骤:

  1. 启动Consul:consul agent -dev
  2. 启动consul-template

    1
    2
    3
    4
    consul-template -consul-addr 127.0.0.1:8500 -template \
    "/Users/zhangjing/IdeaProjects/hunger/conf/real_server.ctmpl:\
    /Users/zhangjing/IdeaProjects/hunger/conf/real_server.conf:\
    sh /Users/zhangjing/IdeaProjects/hunger/conf/restart.sh"

    这里的命令稍有不同,在-template参数中加入了执行脚本

  3. 启动real_server_1、real_server_2,从输出可以看出nginx先被启动,然后又重启了:

    1
    2
    3
    4
    nginx start
    0 52600 1 0 5:36下午 ?? 0:00.00 nginx: master ...
    -2 52601 52600 0 5:36下午 ?? 0:00.00 nginx: worker process
    ngixn restart
  4. 请求Nginx:

    1
    2
    3
    4
    5
    6
    > curl localhost
    from real server 1%
    >curl localhost
    from real server 2%
    ...
    # 可以自行停止、启动real_server进行测试

这种实现方式会导致每次发现配置有变更,都需要reload nginx。书中还有另外一种方式,通过lua脚本的配合来实现无reload动态负载均衡,有兴趣的同学可以自行去看,本章节内容就到这里了。