负载均衡和反向代理
一般来说负载均衡我们比较关心一下几点:
上游服务器配置
: 使用 upstream server 配置上游服务器负载均衡算法
: 配置多个上游服务器时的负载均衡机制失败重试机制
: 配置当超时或上游服务器不存活时,是否需要重试其他上游服务器服务器心跳检查
: 上游服务器的检查心跳/心跳检查
nginx 作为负载均衡器/反向代理服务器如下图所示:
upstream 配置
- 给 nginx 配置上游服务器,即负载均衡到的真是处理业务的服务器,通过 http 指令下配置 upstream 即可。
upstream dao{
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
upstream server 的主要配置如下:
IP 地址和端口
:配置上游服务器的 IP 和端口权重
: weight 用来配置权重,默认都是 1,权重越高分配给这台服务器的请求就越多(如上配置中每三次请求其中一个是转发给 9080 ,其余两个转发给9090),需要根据服务器实际处理能力设置权重.
- 配置如下 proxy_pass 来处理用户请求
location /{
proxy_pass http://dao
}
当访问 nginx 时,会将请求反向代理到 dao 配置的 upstream server。
负载均衡算法
负载均衡是用来解决用户请求到来时如何选择 upstream server 进行处理,默认采用 round-robin(轮询),除此之外还支持其它的负载均衡算法,如下所示。
- round-robin: 是 nginx 默认的负载均衡算法,即以轮询的方式将请求转发到上游服务器,通过配合 weight 配置实现服务器权重的轮询。
- ip_hash:根据客户 IP 进行负载均衡,即相同的 IP 将负载均衡到同一个 upstream server,配置如下:
upstream dao{
ip_hash;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
hash key [consistent]
:对某一个 key 进行哈希或者使用一致性哈希算法进行负载均衡。使用 hash 算法存在的问题是,当添加或者删除一台服务器时,将导致很多 key 被重新负载均衡到不同的服务器(这样会导致服务端会出现问题);因此,建议考虑使用一致性哈希算法,这样就算添加或删除一台服务器,也只有少算的 key 被重新负载均衡到不同的服务器。
哈希算法
:以请求的 uri 进行负载均衡,当然也可以使用 nginx 变量,因此可以实现很复杂的算法。
upstream dao{
hash $uri;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
一致性哈希算法
:consistent_key 动态指定
upstream nginx_local_server{
hash $consistent_key consistent;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
}
如果 location 指定了一致性哈希 key,此外会优先考虑请求参数 cat(类目),如果没有,则再根据请求的 uri 进行负载均衡。
location / {
set $consistent_key $arg_cat;
if($consistent_key = ""){
set $consistent_key $request_uri;
}
}
不过我们更倾向于通过 lua 设置一致性哈希 key。
set_by_lua_file $consistent_key “lua_balancing.lua”;
lua_balancing.lua 代码如下:
local consistent_key = args.cat
if not consistent_key or consistent_key == '' then
consistent_key = ngx_var.request_uri
end
local value = balancing_cache:get(consistent_key)
if not value then
success,err = balancing_cache:set(consistent_key,1,60)
else
newval,err=balancing_cache:incr(consistent,1)
end
如果某一个分类请求量太大,上游服务器可能处理不了这么多请求,此时可以在一致性哈希 key 后加上递增的计数以实现类似轮询的算法。
if newval > 5000 then
consisitent_key = consistent_key .. '_' .. newval
end
least_conn
: 将请求负载均衡到最少活跃连接的上游服务器。如果配置来的服务器较少,则将转而使用基于权重的轮询算法。Nginx 商业版还提供了 least_time
:基于最小平均响应时间进行负载均衡。
失败重试
主要有两部分配置: upstream server 和 proxy_pass。
upstream dao{
server 192.168.61.1:9080 max_fails=2 fail_timeout=10s weight=1;
server 192.168.61.1:9090 max_fails=2 fail_timeout=10s weight=1;
}
通过配置上游服务器的 max_fails 和 fail_timeout,来指定每一个上游服务器,当 fail_timeout 时间内失败了 max_fails 次请求,则认为该上游服务器不可用/不存活,然后将摘掉该上游服务器,fail_timeout 时间后会将该服务器加入到存活上游服务器列表进行重试。
location /test{
proxy_connect_timeout 5s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
proxy_next_upstream error timeout;
proxy_next_upstream_timeout 10s;
proxy_next_upstream_tries 2;
proxy_pass http://dao;
add_header upstream_addr $upstream_addr;
}
然后进行 proxy_next_upstream 相关配置,当遇到错误时,会重试下一台上游服务器。
健康检查
nginx 对上游服务器的健康检查默认采用的是惰性策略,nginx 商业版提供了 health_check 进行主动健康检查,也可以集成 nginx_stream_check_module 模块进行主动健康检查。
nginx_stream_check_module 支持 TCP 心跳和 HTTP 心跳来实现健康检查。
- TCP 心跳检查
upstream dao{
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
check interval=3000 rise=1 fall=3 timeout=2000 type=tcp;
}
该处使用 TCP 进行心跳检测,下面对参数进行一些解释说明
interval
: 检测间隔时间,此处配置了每隔3秒检测一次fall
: 检测失败多少次后,上游服务器被标识为不存活rise
: 检测成功多少次后,上游服务器被标识为存活,并可以处理请求timeout
: 检测请求超时时间配置
- HTTP 心跳检查
upstream dao{
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2;
check interval=3000 rise=1 fall=3 timeout=2000 type=http;
check_http_send "HEAD /status HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}
HTTP 心跳检查有如下两个需要额外配置。
check_http_send
: 即检查时发的 HTTP 请求内容。check_http_expect_alive
: 当上游服务器返回匹配的响应状态码时,则认为上游服务器存活。
其他配置
- 域名上游服务器
upstream dao{
server wwjd.fun;
server xu.wwjd.fun
}
在 nginx 社区版中,是在 nginx 解析配置文件的阶段将域名解析成 IP 地址记录到 upstream 上,当这两个域名对应的 IP 地址发生变化时,该 upstream 不会更新。nginx 商业版才支持动态更新。
不过,proxy_pass http://wwjd.fun 是支持动态域名解析的。
- 备份上游服务器
upstream dao{
hash $uri;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2 backup;
}
将 9090 端口上游服务器配置为备上游服务器,当所有上游服务器都不存活时,请求会转发给备上游服务器。
如通过缩容上游服务器进行压测,要摘掉一些上游服务器进行压测,单位了保险起见会配置一些备上游服务器,当压测的上游服务器都挂掉时,流量可以转发到备上游服务器,从而不影响用户请求处理。
- 不可用上游服务器
upstream dao{
hash $uri;
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2 down;
}
9090 端口上游服务器配置为永久不可用,当测试或者机器出现故障时,暂时通过该配置临时摘掉机器。
长连接
这里主要说如何配置 nginx 与上游服务器的长连接,可以通过 keepalive 指令配置长连接数。
upstream dao{
server 192.168.61.1:9080 weight=1;
server 192.168.61.1:9090 weight=2 backup;
keepalive 100;
}
通过该指令配置每个 worker 进程与上游服务器可缓存的空闲连接的最大数量。当超过这个数量时,最近最少使用的连接将被关闭。 keepalive 指令不限制 worker 进程与上游服务器的总连接。
如果想跟上游服务器建立长连接,可别忘了下面的配置
location / {
# 支持 keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://dao;
}
如果是 http/1.0,则需要配置发送 “Connection:Keep-Alive”请求头。上游服务器不要忘记开启长连接支持。
然后我们看看 nginx 是如何实现 keepalive 的(ngx_http_upstream_keepalive_module),获取连接时的部分代码。
ngx_http_upstream_get_peer(ngx_peer_connection_t *pc,void *data){
//1.首先询问负载均衡使用哪台服务器(IP和端口)
rc = kp->original_ get_ peer(pc, kp->data) ;
cache = &kp->conf->cache;
//2.轮询“空闲连接池”
for (q = ngx_ queue_ head (cache) ;q!= ngx_ queue_ sentinel (cache) ;q = ngx_ queue_ next(q) )
{
item = ngx_ queue_ data(q, ngx_ http_ upstream_ keepalive_ cache_ t, queue) ;
C = item->connection;
//2.1.如果“空闲连接池”缓存的连接IP和端口与负载均衡到的IP和端口相同,则使用此连接
if (ngx_ memn2cmp((u_ char *) &item->sockaddr, (u_ char *) pc->sockaddr,item->socklen, pc->socklen) == 0) {
//2.2从“空闲连接池”移除此连接并压入“释放连接池”栈顶
ngx_ queue_ remove (q) ;
ngx_ queue_ insert_ head(&kp->conf->free,q) ;
goto found;
}
}
// 3. 如果 “空闲连接池” 没有可用的长连接,将穿件短连接
return NGX_OK;
}
释放连接时的部分代码如下。
ngx_ http_ upstream free_ keepalive_ peer (ngx_ peer_ connection_ t *pc,void *data, ngx_ uint_ t state)
{
//当前要释放的连接
c = pc->connection;
//1.如果“释放连接池”没有待释放连接,那么需要从“空闲连接池”腾出一个空间给新的连接使用(这种情况存在于创建连接数超出了连接池大小时,这就会出现震荡)
if (ngx_ queue_ empty (&kp->conf->free)) {
q = ngx_ queue_ last (&kp->conf->cache) ;
ngx_ queue_ remove(q) ;
item = ngx_ queue_ data (q, ngx_ http_ upstream_ keepalive_ cache_ t,queue);
ngx_ http_ upstream keepalive_ close (item->connection) ;
} else {
//2. 从“释放连接池”释放一个连接
q = ngx_ queue_ head (&kp->conf->free) ;
ngx_ queue_ remove(q) ;
item = ngx_ queue_ data(q, ngx_ http_ upstream_ keepalive_ cache_ t,queue) ;
}
//3.将当前连接压入“空闲连接池”栈顶供下次使用
ngx_ queue_ insert_ head (&kp->conf->cache, q) ;
item->connection = c;
}
总长连接数是“空闲连接池”+“释放连接池”的长连接总数。首先,长连接配置不会限制 Worker 进程可以打开的总连接数(超了的作为短连接)。另外,连接池一定要根据实际场景合理进行设置。
- 空闲连接池太小,连接不够用,需要不断建连接。
- 空闲连接池太大,空闲连接太多,还没使用就超时。
另外,建议只对小报文开启长连接。
http 反向代理示例
反向代理除了实现负载均衡之外,还提供如缓存来减少上游服务器的压力。
- 全局配置
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 512 4k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 256k;
proxy_cache_lock on;
proxy_cache_lock_timeout 200ms;
proxy_connect_timeout 3s;
proxy_read_timeout 5s;
proxy_send_timeout 5s;
开启 proxy buffer,缓存内容将存放 tmpfs (内存文件系统)以提升性能,设置超时时间。
- location 配置
location ~* ^/dao/(.*)$ {
# 设置一致性哈希负载均衡 key
set_by_lua_file $consistent_key "/dao/lua/lua_balancing_dao.properties";
# 失败重试配置
proxy_next_upstream error timeout http_500 http_502 http_504;
proxy_next_upstream_timeout 2s;
proxy_next_upstream_tries 2;
# 请求上游服务器使用服务器使用 GET 方法(不管是什么方式请求)
proxy_method GET;
# 不给上游服务器传递请求体
proxy_pass_request_body off;
# 设置上游服务器的哪些响应头不发送给客户端
proxy_hide_header Vary;
# 支持 keep-alive
proxy_http_version 1.1;
proxy_set_header Connection "";
# 给上游服务器传递 X-Forwarded-For、Referer、Cookie 和 Host (按需传递)
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header Referer $http_referer;
proxy_set_header Cookie $http_cookie;
proxy_set_header Host $host;
}
我们开启了 proxy_pass_request_body 和proxy_pass_request_headers,禁止向上游服务器传递请求头和内容体,从而使得上游服务器不受请求头攻击,也不需要解析;如果需要传递,则使用 proxy_set_header 按需传递即可。
我们还可以通过如下配置来开启 gzip 支持,减少网络传输的数据包大小。
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
#gzip_http_version 1.0;
gzip_comp_level 2;
gzip_proxied any;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
gzip_vary off;
gzip_disable "MSIE [1-6]\.";
gzip
:开启 Gzipgzip_min_length
:不压缩临界值,大于 1K 的才压缩,一般不用改gzip_buffers
:buffer 就是缓存,不用改gzip_http_version
:用了反向代理的话,末端通信是 HTTP/1.0,有需求的应该也不用看我这科普文了;有这句的话注释了就行了,默认是 HTTP/1.1gzip_comp_level
:压缩级别,1-10,数字越大压缩的越好,时间也越长,看心情随便改吧gzip_proxied
:Nginx作为反向代理的时候启用,根据某些请求和应答来决定是否在对代理请求的应答启用gzip压缩,是否压缩取决于请求头中的“Via”字段,指令中可以同时指定多个不同的参数,意义如下:
expired
- 启用压缩,如果header头中包含 “Expires” 头信息no-cache
- 启用压缩,如果header头中包含 “Cache-Control:no-cache” 头信息no-store
- 启用压缩,如果header头中包含 “Cache-Control:no-store” 头信息private
- 启用压缩,如果header头中包含 “Cache-Control:private” 头信息no_last_modified
- 启用压缩,如果header头中不包含 “Last-Modified” 头信息no_etag
- 启用压缩 ,如果header头中不包含 “ETag” 头信息auth
- 启用压缩 , 如果header头中包含 “Authorization” 头信息any
- 无条件启用压缩
gzip_types text/plain
:进行压缩的文件类型,缺啥补啥就行了,JavaScript 有两种写法,最好都写上吧,总有人抱怨 js 文件没有压缩,其实多写一种格式 application/javascript 就行了gzip_vary
:跟 Squid 等缓存服务有关,on 的话会在 Header 里增加 “Vary: Accept-Encoding”,我不需要这玩意,自己对照情况看着办吧gzip_disable
:IE6 对 Gzip 不怎么友好,不给它 Gzip 了
http 动态负载均衡
如上的负载均衡实现中,每次 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 界面。
Consul + Consul-template
让我们看看如何实现 nginx 的动态配置。首先,下图是我们要实现的架构图。
首先,upstream 服务启动,我们通过管理后台向 Consul 注册服务。
我们需要在 nginx 机器上部署并启动 Consul-template Agent,其通过长轮询监听服务变更。
Consul-template 监听变更后,动态修改 upstream 列表。
Consul-template 修改完 upstream 列表后,调用重启 nginx 脚本重启 nginx。
整个实现过程还是比较简单的,不过实际生产环境要复杂得多。一般使用 Consul 0.7.0 和 Consul-template 0.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 指定 Consul Web UI 目录,实现通过 Web UI 管理 Consul,然后访问如 http://127.0.0.1:8500 即可看到控制界面。
使用如下 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 指定服务分组,Server.tags 指定服务标签(如测试环境,预发环境等),Service.Port 指定服务器端口。
通过如下 HTTP API 摘除服务。
curl -x PUT http://127.0.0.1:8500/v1/catalog/deregister -d '{"Datacenter": "dc1","Node": "tomcat", "serviceID":"192.168.1.1:8080"}'
通过如下 HTTP API 发现服务。
curl http://127.0.0.1:8500/v1/catalog/service/item_jd_tomcat
可以看到,通过这几个 HTTP API 可以实现服务注册与发现。更多 API 请参考 consul 文档中心 。
- Consul-template
接下来我们需要在 Consul-template 机器上添加一份配置模板 item.jd.tomcat.ctmpl.
upstream item_jd_tomcat{
server 127.0.0.1:111;
# 占位 server,必须有一个 server,否则无法启动
{{range service "dev.item_jd_tomcat@dc1"}}
server {{.Assress}}:{{.Port}} weight=1;
{{end}}
}
service指定格式为:标签。服务@数据中心,然后通过循环输出 Address 和 Port,从而产生 nginx upstream 配置。
启动 Consul-template 命令如下:
#!/bin/bash
ps -ef|grep nginx |grep -v grep
if [ $? -ne 0 ]
then
sudo /usr/server/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){
// 启动嵌入容器
SpringApplication.run(Bootstrap.class,args);
// 服务注册
Consul consul = Consu.builder().withHostAndPort(HostAndPort.fromString("192.168.61,129:8500")).build();
final AgentClient agentClient = consul.gentClient();
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
public void run(){
agentClient.deregister(serviceId);
}
});
}
在 Spring Boot 启动后进行服务注册,然后在 JVM 停止时进行服务摘除。
到此我们就实现了动态 upstream 负载均衡,upstream 服务启动后自动注册到 nginx,upstream 服务停止时,自动从 nginx 上摘除。
通过 Consul+Consul-template 方式,每次发现配置变更都需要 reload nginx,而 reload 是有一定损耗的。而且,如果你需要长连接支持的话,那么当 reload nginx 时长连接所在的 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) 实现动态负载均衡的。
Consul + OpenResty
使用 Consul 注册服务,使用 Openresty balancer_by_lua 实现无 reload 动态负载均衡,架构如下所示。
-
通过 upstream server 启动/停止时注册服务,或者通过 Consul 管理后台注册服务
-
Nginx 启动时会调用 init_by_lua,启动时拉取配置,并更新到共享字典来存储 upstream 列表;通过 init_worker_by_lua 启动定时器,定期去 Consul 拉取配置并实时更新到共享字典
-
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, v in 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 {
local dyna_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, 通过主动推送的方式解决。
Agent 可以长轮询拉取,然后调用 HTTP API 推送到 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 四层负载均衡
Nginx 1.9.0 版本起支持四层负载均衡,从而使得 Nginx 变得更加强大。目前,四层软件负载均衡器用得比较多的是 HaProxy;而 Nginx 也支持四层负载均衡,一-般场景我们使用 Nginx 一站式解决方案就够了。本部分将以 TCP 四层负载均衡进行示例讲解。
静态负载均衡
在默认情况下,ngx_stream_core_module 是没有启用的,需要在安装 nginx 时,添加 --with-stream 配置参数启用。
./configure --prefix=/usr/servers --with-stream
- stream 指令
我们配置 HTTP 负载均衡时,都是配置在 http 指令下,而四层负载均衡是配置在 stream 指令下。
stream{
upstream mysql_dao{
···
}
server{
···
}
}
- upstream 配置
类似于 http upstream 配置,配置如下
upstream mysql_dao{
server 192.168.0.10:3306 max_fail_tiimeout=10s weight=1;
server 192.168.0.11:3306 max_fail_tiimeout=10s weight=1;
least_conn;
}
进行失败重试。惰性健康检查、负载均衡算法相关配置。与 HTTP 负载均衡配置类似,不再重复解释。此处我们配置实现了两个数据库服务器的 TCP 负载均衡。
- server 配置
server {
#监听端口
listen 3308;
#失败重试
proxy_next_upstream on;
proxy_next_upstream_timeout 0;
proxy_next_upstream_tries 0 ;
#超时配置
proxy_connect_timeout 1s;
proxy_timeout 1m;
#限速配置
proxy_upload_rate 0;
proxy_download_rate 0;
#上游服务器
proxy_pass mysq1_dao;
}
listen 指令指定监听的端口,默认 TCP 协议,如果需要 UDP,则可以配置“listen 3308 udp”。
proxy_next_upstream* 与之前讲过的 HTTP 负载均衡类似,不再重复解释。proxy_connect_timeout 配置与上游服务器连接超时时间,默认60s。proxy_timeout 配置与客户端或上游服务器连接的两次成功读/写操作的超时时间,如果超时,将自动断开连接,即连接存活时间,通过它可以释放那些不活跃的连接,默认10分钟。proxy_upload_rate 和 proxy_download_rate 分别配置从客户端读数据和从上游服务器读数据的速率,单位为每秒字节数,默认为0,不限速。
Nginx 的 3308 端口,访问我们的数据库服务器了。
目前的配置都是静态配置,像数据库连接–般都是使用长连接,如果重启 Nginx 服务器,则会看到如下 Worker 进程一直不退出。
Nobody 10268 ..... nginx: worker process is shutting down
这是因为 Worker 维持的长连接一直在使用,所以无法退出,解决办法只能是杀掉该进程。
当然,一般情况下是因为需要动态添加/删除上游服务器,才需要重启 Nginx, 像 HTTP 动态负载均衡那样。如果能做到动态负载均衡,则一大部分问题就解决了。一 个选择是购买 Nginx 商业版,另一个选择是使用 nginx-stream-upsync-module,目前,OpenResty 提供的 stream-lua-nginx-module 尚未实现 balancer_by_lua 特性,因此暂时无法使用。当前开源选择可以使用 nginx-stream-upsync-module。
动态负载均衡
nginx-stream-upsync-module 有一个兄弟 nginx-upsync-module,其提供了 HTTP 七层动态负载均衡,动态更新上游服务器不需要 reload nginx。 当前最新版本是基于 Nginx1.9.10 开发的,因此兼容 1.9.10+ 版本。 其提供了基于 consul 和 etcd 进行动态更新上游服务器实现。本部分基于 Nginx 1.9.10 版本和 consul 配置中心进行演示。
首先,需要下载并添加 nginx-stream-upsync-module 模块最新版本。
./configure --prefix=/usr/servers --with-stream --add-module=./nginx-stream-upsync-module
- upstream 配置
upstream mysq1_ dao {
server 127.0.0.1:1111;
#占位 server
upsync 127.0.0.1: 8500/v1/kv/upstreams/mysql dao upsync_timeout = 6m
upsync_interval=500ms upsync_type=consul strong_dependency=off;
upsync_dump_path /usr/ servers/ nginx/conf/mysq1_dao.conf;
}
upsync 指令指定从 consul 哪个路径拉取上游服务器配置; upsync_timeout 配置从 consul 拉取上游服务器配置的超时时间;upsync_interval 配置从 consul 拉取上游服务器配置的间隔时间;upsync_type 指定使用 consul 配置服务器;strong_dependency 配置 nginx 在启动时是否强制依赖配置服务器,如果配置为 on,则拉取配置失败时 nginx 启动同样失败。
upsync_dump_path 指定从 consul 拉取的上游服务器后持久化到的位置,这样即使 consul 服务器出问题了,本地还有一个备份。
- 从 Consul 添加上游服务器
curl -X PUT -d "{\"weight\":1, \"max_ fails\":2, \"fail_ timeout\":10}" http://127.0.0.1:8500/v1/kv/ upstreams/mysql_dao/10.0.0.24:3306
curl -X PUT -d "{\"weight\":1, \"max_ fails\":2, \"fail_ timeout\":10}" http://127.0.0.1:8500/v1/ kv/upstreams/mysql_dao/ 192.168.0.11:3306
- 从 Consul 删除上游服务器
curl -X DELETE http://127.0.0.1 :8500/v1/kv/upstreams/mysql_ dao/192.168.0.11 :3306
- upstream_show
server {
listen 1234;
upstream_show;
}
配置 upstream_show 指令后,可以通过 curl http://127.0.0.1:1234/upstream_show 来查看当前动态负载均衡上游服务器列表。
到此动态负载均衡就配置完成了,我们已讲解完动态添加/删除上游服务器。在实际使用时,请进行压测来评测其稳定性。在实际应用中,更多的是用 HaProxy 进行四层负载均衡,因此,还是要根据自己的场景来选择方案。