接入层控制:nginx动态流量路由和负载均衡

原创声明:本文系作者原创,谢绝个人、媒体、公众号或网站未经授权转载,违者追究其法律责任。

任何团队面临着业务的增长,服务的路由场景及流量控制需求都会越来越复杂。通常会涉及到接入层路由、流量控制和应用服务层路由、甚至数据库路由等。而负载均衡又是另一个热门的技术话题,可以用在服务端各层。本文主要讲解分布式系统中接入层的动态路由以及围绕的负载均衡。

一、业务需要

业务早期,业务需求和发展规划简单,接入层用nginx+tomcat web 的简单结构就能满足需求:

这种结构下,nginx层流量控制和负载均衡没有特别复杂的要求,一般使用location、upstream、if、rewrite、reload等简单的nginx指令,实现部署或请求转发等功能。例如:

location ^~ /static/ {

root /webroot/static/;

}

 

location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {

root /webroot/res/;

}

 

upstream backend_aaa {

server 10.4.232.110:8080 weight=10;

}

 

location /abc {

proxy_pass http://backend_aaa; #路由到backend_bbb集群

}

 

location / {

proxy_pass http://tomcat:8080/

}

 

这种使用方法的优点是运行时性能高,接近原生nginx,缺点是功能受限、操作不精细、容易出错,以及转发规则固定,只能静态分流实现等。特别的,比如使用reload命令,业务初期,大部分情况类似如下使用:

#!/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

 

而reload是有一定损耗的。如果你需要长连接支持的话,那么当reload nginx时长连接所在worker进程会进行优雅退出,并当该worker进程上的所有连接都释放时,进程才真正退出(表现为worker进程处于worker process is shutting down)。因此,如果能做到不reload就能动态更改upstream,那么就更完美。如京东张开涛 "亿级流量架构"书籍中提到的,社区版Nginx目前有三个选择实现不用reload也能动态更改upstream:Tengine的Dyups模块、微博的Upsync和使用OpenResty的balancer_by_lua。微博使用Upsync+Consul实现动态负载均衡,而又拍云使用其开源的slardar(Consul + balancer_by_lua)实现动态负载均衡。

 

随着业务高速发展及用户量的增加,接入层的路由、流量控制等需求会变得越来越复杂,引用美团"Oceanus:美团HTTP流量定制化路由的实践"文中讲到的,复杂的接入层路由通常会有类似如下的常见场景:

 

  • 团购秒杀要灵活控制压测流量,实现线上服务单节点、各机房、各地域等多维度的压测。
  • 外卖业务要做流量隔离,把北方地域的流量转发到分组a,南方地域的流量转发到分组b。
  • 酒旅业务要对App新版本进行灰度,让千分之一的用户试用新版本,其他用户访问老版本。
  • QA部门要通过请求的自定义参数指定转发分组,构建稳定且高可用的测试环境。

 

因此,依靠原生的nginx指令很难精细的支持某些业务场景下的分流控制需求,所以很难作为解决公司级分流框架的有效手段。针对它们所存在的不足,定制高可扩展的动态分流框架就是一个很必要的定位,能实现动态支持各种业务场景的分流需求。

 

二、动态流量控制方案

动态流量处理中, consul+consul-template是一个开源的动态流量方案,这也是《亿级流量网站架构核心技术》中推荐的。在原有方案上,我对其细节做了一些重构,下图是它的实施架构:

大致过程:

1.upstream服务启动。我们可以通过管理后台手动或者改造server自动注册服务到consul。

2.在nginx机器上部署并启动consul-template 作为nginx agent,通过长轮询监听consul服务变更。

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

4.consul-template修改完upstream列表后,调用nginx重新加载nginx upstream配置(reload)。

5.nginx转发请求到新的upstream server;

接下来我们讲解各个节点的实施细节。

 

2.1 consul 及consul-template使用

以Consul1.2.3和Consul-template v0.19.5为例

consul

  • ./consul agent -server -bootstrap-expect 1-data-dir /tmp/consul -bind 0.0.0.0 -client 0.0.0.0 是consul启动命令。data-dir指定agent的状态存储位置,bind指定集群通信的地址,client指定客户端通信的地址(如consul-template与consul通信)。启动时还可以使用-ui-dir ./ui/指定Consul Web UI目录,实现通过Web UI管理consul,然后访问如http://127.0.0.1:8500即可看到控制界面。

 

  • 到consul中注册服务:

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

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

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

 

  • consul中摘除服务:

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

 

  • consul发现服务:

curl http://127.0.0.1:8500/v1/catalog/service/oilcard_tomcat

上面是常见的consul api,更多可参考: https://www.consul.io/docs/agent/http.html

 

consul-template

  • template配置

consul-template nginx代理机器上添加一份consul-template模板 oilcard.tomcat.ctmpl。

upstream oilcard_tomcat {

server 127.0.0.1:1111;

{{range service"dev.item_jd_tomcat@dc1"}}

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

{{end}}

}

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

consul-template模板语言使用可以多参考官方文档

 

  • 启动consul-template

./consul-template -consul 127.0.0.1:8500 -template ./oilcard.tomcat.ctmpl:/usr/servers/nginx/conf/domains/oilcard.tomcat.conf: "./restart.sh" 2>&1 >/opt/consul-template.log & ;

使用consul指定consul服务器客户端通信地址,template格式是“配置模板:目标配置文件:脚本/命令”,即通过配置模板更新目标配置文件,然后调用脚本重启nginx。使用nginx include指将/usr/servers/nginx/conf/domains/oilcard.tomcat.conf包含到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

 

  • java程序自动注册到consul中

让程序自动注册到consul中能让服务很好的达到高可用和质量检测,当然也可以通过consul admin手工注册管理。

本文的自动注册使用Spring Boot+Consul Java Client实现,代码细节如下:

<dependency>

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

<artifactId>consul-client</artifactId>

<version>0.12.8</version>

</dependency>

如下spring boot启动代码是进行服务注册与摘除:

public static void main(String[] args) {

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 = "oilcard_tomcat";

String address = "192.168.1.1";

String tag = "dev";

int port= 8090;

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);

}

});

}

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

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

通过consul+consul-template方式,每次配置变更都需要reload nginx,如本文开始所述,reload耗损性能,接下来我们使用consul + balancer_by_lua 实现动态upstream。

 

consul+openresty

使用consul注册服务,使用openresty balancer_by_lua实现无reload动态负载均衡,架构如下:

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

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

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

 

将更新、获取upstream 抽象成dyna_upstreams模块:

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.100:8500/v1/catalog/service/oilcard_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

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

}

 

update_upstreams用于更新upstream列表,get_upstreams用于返回upstream列表,注意因为luasocket是阻塞API这可能会阻塞我们的服务,使用时要慎重。

 

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,通过主动推送的方式解决,架构图如下。

consul template可以长轮询拉取,然后调用HTTP API推送到nginx上,对于拉取的配置,除了放在内存里,请考虑在本地文件系统中存储一份,在网络出问题时作为备份使用。

 

oilcard.tomcat.conf upstream配置balancer_by_lua

upstream oilcard_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模块进行健康检查。

 

三、总结

本文一开始讲述了动态流量及路由控制的业务必要性,接着以开源的consul+consul-template+nginx+openresty+lua为例,讲解了具体怎样实施动态路由方案。稳定的接入层方案很需要时间不断来检验。希望本文帮助到你。

 

注:开源的consul+consul-template方案主要从《亿级流量网站架构核心技术》中借鉴而来,特此申明。

 

参考:

1.https://tech.meituan.com/Oceanus_Custom_Traffic_Routing.html

2.https://mp.weixin.qq.com/s/O9LELywlYUkSrdcNaooNAw?

3.https://www.consul.io/api/index.html

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值