使用Nginx实现HTTP动态负载均衡

时间:2024-04-03 17:44:21

Nginx中的负载均衡与反向代理中的负载均衡实现中,每次upstream列表有变更,都需要到服务器进行修改,首先是管理容易出现问题,而且对于upstream服务上线无法自动注册到Nginx upstream列表。因此,我们需要一种服务注册,可以将upstream动态注册到Nginx上,从而实现upstream服务的自动发现。

  Consul是一款开源的分布式服务注册与发现系统,通过HTTP API可以使得服务注册、发现实现起来非常简单,它支持如下特性。

  服务注册:服务实现者可以通过HTTP API或DNS方式,将服务注册到Consul。

  服务发现:服务消费者可以通过HTTP API或DNS方式,从Consul获取服务的IP和PORT。

  故障检测:支持如TCP、HTTP等方式的健康检查机制,从而当服务有故障时自动摘除。

  K/V存储:使用K/V存储实现动态配置中心,其使用HTTP长轮询实现变更触发和配置更改。

  多数据中心:支持多数据中心,可以按照数据中心注册和发现服务,即支持只消费本地机房服务,使用多数据中心集群还可以避免单数据中心的单点故障。

  Raft算法:Consul使用Raft算法实现集群数据一致性。

  通过Consul可以管理服务注册与发现,接下来需要有一个与Nginx部署在同一台机器的Agent来实现Nginx配置更改和Nginx重启功能。我们有Confd或者Consul-template两个选择,而Consul-template是Consul官方提供的,我们就选择它了。其使用HTTP长轮询实现变更触发和配置更改(使用Consul的watch命令实现)。也就是说,我们使用Consul-template实现配置模板,然后拉取Consul配置渲染模板来生成Nginx实际配置。

  除Consul外,还有一个选择是etcd3,其使用了gRPC和protobuf可以说是一个亮点。不过,etcd3目前没有提供多数据中心、故障检测、Web管理界面。

  1 Consul+Consul-template

  接下来,让我们看一下如何来实现Nginx动态配置。首先,下图是我们要实现的架构图。

  使用Nginx实现HTTP动态负载均衡

  1.首先,upstream服务启动,我们通过管理后台向Consule注册服务。

  2.我们需要在Nginx机器上部署并启动Consul-template Agent,其通过长轮询监听服务变更。

  3.Consul-template监听到变更后,动态修改upstream列表。

  4.Consul-template修改完upstream列表后,调用重启Nginx脚本重启Nginx。

  整个实现过程还是比较简单的,不过,实际生产环境要复杂得多。我们使用了Consul0.7.0和Consul-template0.16.0来实现。

  Consul-Server

  首先我们要启动Consul-Server。

  ./consul agent -server -bootstrap-expect 1-data-dir /tmp/consul -bind 0.0.0.0-client 0.0.0.0

  此处需要使用data-dir指定Agent状态存储位置,bind指定集群通信的地址,client指定客户端通信的地址(如Consul-template与Consul通信)。在启动时还可以使用-ui-dir ./ui/指定Consul Web UI目录,实现通过Web UI管理Consul,然后访问如http://127.0.0.1:8500即可看到控制界面。

  使用Nginx实现HTTP动态负载均衡

  使用如下HTTP API注册服务。

  curl -X PUT http://127.0.0.1:8500/v1/catalog/register -d'{"Datacenter": "dc1", "Node":"tomcat", "Address":"192.168.1.1","Service": {"Id" :"192.168.1.1:8080", "Service": "item_jd_tomcat","tags": ["dev"], "Port": 8080}}'

  curl -X PUT http://127.0.0.1:8500/v1/catalog/register -d'{"Datacenter": "dc1", "Node":"tomcat", "Address":"192.168.1.2","Service": {"Id" :"192.168.1.1:8090", "Service": "item_jd_tomcat","tags": ["dev"], "Port": 8090}}'

  Datacenter指定数据中心,Address指定服务IP,Service.Id指定服务唯一标识,Service.Service指定服务分组,Service.tags指定服务标签(如测试环境、预发环境等),Service.Port指定服务端口。

  通过如下HTTP API摘除服务。

  curl -X PUThttp://127.0.0.1:8500/v1/catalog/deregister -d '{"Datacenter":"dc1", "Node": "tomcat", "ServiceID" :"192.168.1.1:8080"}'

  通过如下HTTP API发现服务。

  curlhttp://127.0.0.1:8500/v1/catalog/service/item_jd_tomcat

  可以看到,通过这几个HTTPAPI可以实现服务注册与发现。更多API请参考https://www.consul.io/docs/agent/http.html

  Consul-template

  接下来我们需要在Consul-template机器上添加一份配置模板item.jd.tomcat.ctmpl。

  upstream item_jd_tomcat {

  server 127.0.0.1:1111; #占位server,必须有一个server,否则无法启动

  {{range service"[email protected]"}}

  server {{.Address}}:{{.Port}} weight=1;

  {{end}}

  }

  service指定格式为:标签.服务@数据中心,然后通过循环输出Address和Port,从而生成Nginx upstream配置。

  启动Consul-template。

  ./consul-template -consul 127.0.0.1:8500

  -template./item.jd.tomcat.ctmpl:/usr/servers/nginx/conf/domains/item.jd.tomcat:"./restart.sh"

  使用consul指定Consul服务器客户端通信地址,template格式是“配置模板:目标配置文件:脚本”,即通过配置模板更新目标配置文件,然后调用脚本重启Nginx。

  直接通过Nginx include指令将/usr/servers/nginx/conf/domains/item.jd.tomcat包含到nginx.conf配置文件即可,restart.sh脚本代码如下所示。

  #!/bin/bash

  ps -fe | grep nginx | grep -v grep

  if [ $? -ne 0 ]

  then

  sudo /usr/servers/nginx/sbin/nginx

  echo "nginx start"

  else

  sudo /usr/servers/nginx/sbin/nginx -s reload

  echo "nginx reload"

  fi

  即如果Nginx没有启动,则启动,否则重启。

  Java服务

  建议配合Spring Boot+Consul Java Client实现,我们使用的Consul Java Client如下。

  <dependency>

  <groupId>com.orbitz.consul</groupId>

  <artifactId>consul-client</artifactId>

  <version>0.12.8</version>

  </dependency>

  如下代码是进行服务注册与摘除。

  public static void main(String[] args) {

  //启动嵌入容器(如Tomcat)

  SpringApplication.run(Bootstrap.class, args);

  //服务注册

  Consul consul = Consul.builder().withHostAndPort(HostAndPort.fromString ("192.168.61.129:8500")).build();

  final AgentClient agentClient = consul.agentClient();

  String service = "item_jd_tomcat";

  String address = "192.168.61.1";

  String tag = "dev";

  int port= 9080;

  final String serviceId = address + ":" + port;

  ImmutableRegistration.Builder builder = ImmutableRegistration.builder();

  builder.id(serviceId).name(service).address(address).port(port).addTags(tag);

  agentClient.register(builder.build());

  //JVM停止时摘除服务

  Runtime.getRuntime().addShutdownHook(new Thread() {

  @Override

  publicvoid run() {

  agentClient.deregister(serviceId);

  }

  });

  }

  在Spring Boot启动后进行服务注册,然后在JVM停止时进行服务摘除。

  到此我们就实现了动态upstream负载均衡,upstream服务启动后自动注册到Nginx,upstream服务停止时,自动从Nginx上摘除。

  通过Consul+Consul-template方式,每次发现配置变更都需要reload nginx,而reload是有一定损耗的。而且,如果你需要长连接支持的话,那么当reloadnginx时长连接所在worker进程会进行优雅退出,并当该worker进程上的所有连接都释放时,进程才真正退出(表现为worker进程处于worker process is shutting down)。因此,如果能做到不reload就能动态更改upstream,那么就完美了。对于社区版Nginx目前有三个选择:Tengine的Dyups模块、微博的Upsync和使用OpenResty的balancer_by_lua。微博使用Upsync+Consul实现动态负载均衡,而又拍云使用其开源的slardar(Consul + balancer_by_lua)实现动态负载均衡。

  2 Consul+OpenResty

  使用Consul注册服务,使用OpenResty balancer_by_lua实现无reload动态负载均衡,架构如下所示。

  使用Nginx实现HTTP动态负载均衡

  1.通过upstream server启动/停止时注册服务,或者通过Consul管理后台注册服务。

  2.Nginx启动时会调用init_by_lua,启动时拉取配置,并更新到共享字典来存储upstream列表;然后通过init_worker_by_lua启动定时器,定期去Consul拉取配置并实时更新到共享字典。

  3.balancer_by_lua使用共享字典存储的upstream列表进行动态负载均衡。

  dyna_upstreams.lua模块

  local http = require("socket.http")

  local ltn12 = require("ltn12")

  local cjson = require "cjson"

  local function update_upstreams()

  local resp = {}

  http.request{

  url="http://192.168.61.129:8500/v1/catalog/service/item_jd_tomcat",

  sink = ltn12.sink.table(resp)

  }

  resp = table.concat(resp)

  resp =cjson.decode(resp)

  local upstreams = {{ip="127.0.0.1", port=1111}}

  for i, vin ipairs(resp) do

  upstreams[i+1] = {ip=v.Address, port=v.ServicePort}

  end

  ngx.shared.upstream_list:set("item_jd_tomcat", cjson.encode (upstreams))

  end

  local function get_upstreams()

  local upstreams_str = ngx.shared.upstream_list:get("item_jd_tomcat")

  end

  local _M = {

  update_upstreams = update_upstreams,

  get_upstreams = get_upstreams

  }

  通过luasockets查询Consul来发现服务,update_upstreams用于更新upstream列表,get_upstreams用于返回upstream列表,此处可以考虑worker进程级别的缓存,减少因为json的反序列化造成的性能开销。

  还要注意我们使用的luasocket是阻塞API,因为截至本书出版时,OpenResty在init_by_lua和init_worker_by_lua不支持Cosocket(未来会添加支持),所以我们只能使用luasocket,但是,注意这可能会阻塞我们的服务,使用时要慎重。

  init_*_by_lua配置

  #存储upstream列表的共享字典

  lua_shared_dict upstream_list 10m;

  #Nginx Master进程加载配置文件时执行,用于第一次初始化配置

  init_by_lua_block {

  localdyna_upstreams = require "dyna_upstreams";

  dyna_upstreams.update_upstreams();

  }

  #Nginx Worker进程调度,使用ngx.timer.at定时拉取配置

  init_worker_by_lua_block {

  local dyna_upstreams = require"dyna_upstreams";

  local handle = nil;

  handle =function ()

  --TODO:控制每次只有一个worker执行

  dyna_upstreams.update_upstreams();

  ngx.timer.at(5, handle);

  end

  ngx.timer.at(5, handle);

  }

  init_worker_by_lua是每个Nginx Worker进程都会执行的代码,所以实际实现时可考虑使用锁机制,保证一次只有一个人处理配置拉取。另外ngx.timer.at是定时轮询,不是走的长轮询,有一定的时延。有个解决方案,是在Nginx上暴露HTTP API,通过主动推送的方式解决。

  使用Nginx实现HTTP动态负载均衡

  Agent可以长轮询拉取,然后调用HTTPAPI推送到Nginx上,Agent可以部署在Nginx本机或者远程。

  对于拉取的配置,除了放在内存里,请考虑在本地文件系统中存储一份,在网络出问题时作为托底。

  upstream配置

  upstream item_jd_tomcat {

  server 0.0.0.1; #占位server

  balancer_by_lua_block {

  local balancer = require "ngx.balancer"

  local dyna_upstreams = require "dyna_upstreams";

  local upstreams = dyna_upstreams.get_upstreams();

  local ip_port = upstreams[math.random(1,table.getn(upstreams)) ]

  ngx.log(ngx.ERR, "current :=============", math.random(1,table.getn (upstreams)))

  balancer.set_current_peer(ip_port.ip, ip_port.port)

  }

  }

  获取upstream列表,实现自己的负载均衡算法,通过ngx.balancer API进行动态设置本次upstream server。通过balancer_by_lua除可以实现动态负载均衡外,还可以实现个性化负载均衡算法。

  最后,记得使用lua-resty-upstream-healthcheck模块进行健康检查。

Nginx中的负载均衡与反向代理中的负载均衡实现中,每次upstream列表有变更,都需要到服务器进行修改,首先是管理容易出现问题,而且对于upstream服务上线无法自动注册到Nginx upstream列表。因此,我们需要一种服务注册,可以将upstream动态注册到Nginx上,从而实现upstream服务的自动发现。

  Consul是一款开源的分布式服务注册与发现系统,通过HTTP API可以使得服务注册、发现实现起来非常简单,它支持如下特性。

  服务注册:服务实现者可以通过HTTP API或DNS方式,将服务注册到Consul。

  服务发现:服务消费者可以通过HTTP API或DNS方式,从Consul获取服务的IP和PORT。

  故障检测:支持如TCP、HTTP等方式的健康检查机制,从而当服务有故障时自动摘除。

  K/V存储:使用K/V存储实现动态配置中心,其使用HTTP长轮询实现变更触发和配置更改。

  多数据中心:支持多数据中心,可以按照数据中心注册和发现服务,即支持只消费本地机房服务,使用多数据中心集群还可以避免单数据中心的单点故障。

  Raft算法:Consul使用Raft算法实现集群数据一致性。

  通过Consul可以管理服务注册与发现,接下来需要有一个与Nginx部署在同一台机器的Agent来实现Nginx配置更改和Nginx重启功能。我们有Confd或者Consul-template两个选择,而Consul-template是Consul官方提供的,我们就选择它了。其使用HTTP长轮询实现变更触发和配置更改(使用Consul的watch命令实现)。也就是说,我们使用Consul-template实现配置模板,然后拉取Consul配置渲染模板来生成Nginx实际配置。

  除Consul外,还有一个选择是etcd3,其使用了gRPC和protobuf可以说是一个亮点。不过,etcd3目前没有提供多数据中心、故障检测、Web管理界面。

  1 Consul+Consul-template

  接下来,让我们看一下如何来实现Nginx动态配置。首先,下图是我们要实现的架构图。

  使用Nginx实现HTTP动态负载均衡

  1.首先,upstream服务启动,我们通过管理后台向Consule注册服务。

  2.我们需要在Nginx机器上部署并启动Consul-template Agent,其通过长轮询监听服务变更。

  3.Consul-template监听到变更后,动态修改upstream列表。

  4.Consul-template修改完upstream列表后,调用重启Nginx脚本重启Nginx。

  整个实现过程还是比较简单的,不过,实际生产环境要复杂得多。我们使用了Consul0.7.0和Consul-template0.16.0来实现。

  Consul-Server

  首先我们要启动Consul-Server。

  ./consul agent -server -bootstrap-expect 1-data-dir /tmp/consul -bind 0.0.0.0-client 0.0.0.0

  此处需要使用data-dir指定Agent状态存储位置,bind指定集群通信的地址,client指定客户端通信的地址(如Consul-template与Consul通信)。在启动时还可以使用-ui-dir ./ui/指定Consul Web UI目录,实现通过Web UI管理Consul,然后访问如http://127.0.0.1:8500即可看到控制界面。

  使用Nginx实现HTTP动态负载均衡

  使用如下HTTP API注册服务。

  curl -X PUT http://127.0.0.1:8500/v1/catalog/register -d'{"Datacenter": "dc1", "Node":"tomcat", "Address":"192.168.1.1","Service": {"Id" :"192.168.1.1:8080", "Service": "item_jd_tomcat","tags": ["dev"], "Port": 8080}}'

  curl -X PUT http://127.0.0.1:8500/v1/catalog/register -d'{"Datacenter": "dc1", "Node":"tomcat", "Address":"192.168.1.2","Service": {"Id" :"192.168.1.1:8090", "Service": "item_jd_tomcat","tags": ["dev"], "Port": 8090}}'

  Datacenter指定数据中心,Address指定服务IP,Service.Id指定服务唯一标识,Service.Service指定服务分组,Service.tags指定服务标签(如测试环境、预发环境等),Service.Port指定服务端口。

  通过如下HTTP API摘除服务。

  curl -X PUThttp://127.0.0.1:8500/v1/catalog/deregister -d '{"Datacenter":"dc1", "Node": "tomcat", "ServiceID" :"192.168.1.1:8080"}'

  通过如下HTTP API发现服务。

  curlhttp://127.0.0.1:8500/v1/catalog/service/item_jd_tomcat

  可以看到,通过这几个HTTPAPI可以实现服务注册与发现。更多API请参考https://www.consul.io/docs/agent/http.html

  Consul-template

  接下来我们需要在Consul-template机器上添加一份配置模板item.jd.tomcat.ctmpl。

  upstream item_jd_tomcat {

  server 127.0.0.1:1111; #占位server,必须有一个server,否则无法启动

  {{range service"[email protected]"}}

  server {{.Address}}:{{.Port}} weight=1;

  {{end}}

  }

  service指定格式为:标签.服务@数据中心,然后通过循环输出Address和Port,从而生成Nginx upstream配置。

  启动Consul-template。

  ./consul-template -consul 127.0.0.1:8500

  -template./item.jd.tomcat.ctmpl:/usr/servers/nginx/conf/domains/item.jd.tomcat:"./restart.sh"

  使用consul指定Consul服务器客户端通信地址,template格式是“配置模板:目标配置文件:脚本”,即通过配置模板更新目标配置文件,然后调用脚本重启Nginx。

  直接通过Nginx include指令将/usr/servers/nginx/conf/domains/item.jd.tomcat包含到nginx.conf配置文件即可,restart.sh脚本代码如下所示。

  #!/bin/bash

  ps -fe | grep nginx | grep -v grep

  if [ $? -ne 0 ]

  then

  sudo /usr/servers/nginx/sbin/nginx

  echo "nginx start"

  else

  sudo /usr/servers/nginx/sbin/nginx -s reload

  echo "nginx reload"

  fi

  即如果Nginx没有启动,则启动,否则重启。

  Java服务

  建议配合Spring Boot+Consul Java Client实现,我们使用的Consul Java Client如下。

  <dependency>

  <groupId>com.orbitz.consul</groupId>

  <artifactId>consul-client</artifactId>

  <version>0.12.8</version>

  </dependency>

  如下代码是进行服务注册与摘除。

  public static void main(String[] args) {

  //启动嵌入容器(如Tomcat)

  SpringApplication.run(Bootstrap.class, args);

  //服务注册

  Consul consul = Consul.builder().withHostAndPort(HostAndPort.fromString ("192.168.61.129:8500")).build();

  final AgentClient agentClient = consul.agentClient();

  String service = "item_jd_tomcat";

  String address = "192.168.61.1";

  String tag = "dev";

  int port= 9080;

  final String serviceId = address + ":" + port;

  ImmutableRegistration.Builder builder = ImmutableRegistration.builder();

  builder.id(serviceId).name(service).address(address).port(port).addTags(tag);

  agentClient.register(builder.build());

  //JVM停止时摘除服务

  Runtime.getRuntime().addShutdownHook(new Thread() {

  @Override

  publicvoid run() {

  agentClient.deregister(serviceId);

  }

  });

  }

  在Spring Boot启动后进行服务注册,然后在JVM停止时进行服务摘除。

  到此我们就实现了动态upstream负载均衡,upstream服务启动后自动注册到Nginx,upstream服务停止时,自动从Nginx上摘除。

  通过Consul+Consul-template方式,每次发现配置变更都需要reload nginx,而reload是有一定损耗的。而且,如果你需要长连接支持的话,那么当reloadnginx时长连接所在worker进程会进行优雅退出,并当该worker进程上的所有连接都释放时,进程才真正退出(表现为worker进程处于worker process is shutting down)。因此,如果能做到不reload就能动态更改upstream,那么就完美了。对于社区版Nginx目前有三个选择:Tengine的Dyups模块、微博的Upsync和使用OpenResty的balancer_by_lua。微博使用Upsync+Consul实现动态负载均衡,而又拍云使用其开源的slardar(Consul + balancer_by_lua)实现动态负载均衡。

  2 Consul+OpenResty

  使用Consul注册服务,使用OpenResty balancer_by_lua实现无reload动态负载均衡,架构如下所示。

  使用Nginx实现HTTP动态负载均衡

  1.通过upstream server启动/停止时注册服务,或者通过Consul管理后台注册服务。

  2.Nginx启动时会调用init_by_lua,启动时拉取配置,并更新到共享字典来存储upstream列表;然后通过init_worker_by_lua启动定时器,定期去Consul拉取配置并实时更新到共享字典。

  3.balancer_by_lua使用共享字典存储的upstream列表进行动态负载均衡。

  dyna_upstreams.lua模块

  local http = require("socket.http")

  local ltn12 = require("ltn12")

  local cjson = require "cjson"

  local function update_upstreams()

  local resp = {}

  http.request{

  url="http://192.168.61.129:8500/v1/catalog/service/item_jd_tomcat",

  sink = ltn12.sink.table(resp)

  }

  resp = table.concat(resp)

  resp =cjson.decode(resp)

  local upstreams = {{ip="127.0.0.1", port=1111}}

  for i, vin ipairs(resp) do

  upstreams[i+1] = {ip=v.Address, port=v.ServicePort}

  end

  ngx.shared.upstream_list:set("item_jd_tomcat", cjson.encode (upstreams))

  end

  local function get_upstreams()

  local upstreams_str = ngx.shared.upstream_list:get("item_jd_tomcat")

  end

  local _M = {

  update_upstreams = update_upstreams,

  get_upstreams = get_upstreams

  }

  通过luasockets查询Consul来发现服务,update_upstreams用于更新upstream列表,get_upstreams用于返回upstream列表,此处可以考虑worker进程级别的缓存,减少因为json的反序列化造成的性能开销。

  还要注意我们使用的luasocket是阻塞API,因为截至本书出版时,OpenResty在init_by_lua和init_worker_by_lua不支持Cosocket(未来会添加支持),所以我们只能使用luasocket,但是,注意这可能会阻塞我们的服务,使用时要慎重。

  init_*_by_lua配置

  #存储upstream列表的共享字典

  lua_shared_dict upstream_list 10m;

  #Nginx Master进程加载配置文件时执行,用于第一次初始化配置

  init_by_lua_block {

  localdyna_upstreams = require "dyna_upstreams";

  dyna_upstreams.update_upstreams();

  }

  #Nginx Worker进程调度,使用ngx.timer.at定时拉取配置

  init_worker_by_lua_block {

  local dyna_upstreams = require"dyna_upstreams";

  local handle = nil;

  handle =function ()

  --TODO:控制每次只有一个worker执行

  dyna_upstreams.update_upstreams();

  ngx.timer.at(5, handle);

  end

  ngx.timer.at(5, handle);

  }

  init_worker_by_lua是每个Nginx Worker进程都会执行的代码,所以实际实现时可考虑使用锁机制,保证一次只有一个人处理配置拉取。另外ngx.timer.at是定时轮询,不是走的长轮询,有一定的时延。有个解决方案,是在Nginx上暴露HTTP API,通过主动推送的方式解决。

  使用Nginx实现HTTP动态负载均衡

  Agent可以长轮询拉取,然后调用HTTPAPI推送到Nginx上,Agent可以部署在Nginx本机或者远程。

  对于拉取的配置,除了放在内存里,请考虑在本地文件系统中存储一份,在网络出问题时作为托底。

  upstream配置

  upstream item_jd_tomcat {

  server 0.0.0.1; #占位server

  balancer_by_lua_block {

  local balancer = require "ngx.balancer"

  local dyna_upstreams = require "dyna_upstreams";

  local upstreams = dyna_upstreams.get_upstreams();

  local ip_port = upstreams[math.random(1,table.getn(upstreams)) ]

  ngx.log(ngx.ERR, "current :=============", math.random(1,table.getn (upstreams)))

  balancer.set_current_peer(ip_port.ip, ip_port.port)

  }

  }

  获取upstream列表,实现自己的负载均衡算法,通过ngx.balancer API进行动态设置本次upstream server。通过balancer_by_lua除可以实现动态负载均衡外,还可以实现个性化负载均衡算法。

  最后,记得使用lua-resty-upstream-healthcheck模块进行健康检查。