内容纲要

OpenResty 动态负载均衡

在之前提到的OpenResty/Nginx的负载均衡当中,当服务器启动之后,upstream中的上游服务器就是固定死的了,做不到动态的变更。这里面说到的变更,其实更多指的是增加机器。因为当上游服务器不可用时,upstream会自动将服务器摘除,但是当新增服务器时,upstream就做不到了。传统的负载均衡办法,就是能是修改配置,然后重启服务。下面介绍一下动态负载均衡的方式,一种是通过动态重启服务;另外一种是通过代码的方式动态拉取服务器列表。

Consul

Consul是一个分布式服务注册与发现系统。这里面使用Consul来管理上游服务器,当服务器启动时将其注册到注册中心去,当服务关闭时从注册中心列表中剔除。这里面需要注意一点的是:当上游服务器关闭时,Consul本身不会自动从列表中剔除,而是需要在服务器关闭前主动向Consul发起删除服务。

Consul有以下特性:

  • 服务注册:服务提供者可以通过HTTP或DNS的方式,将服务注册到Consul中去。
  • 服务发现:服务消费者可以通过HTTP或DNS的方式,获取服务的IP和PORT。
  • 故障检测:支持如TCP/HTTP等方式的健康检查机制,当服务出现故障时摘除服务。
  • K/V存储:使用key-value实现配置中心,使用HTTP长轮询实现变更配置。
  • 多数据中心:支持多数据中心,可以按照数据中心注册和发现服务。可以支持只消费本地机房的服务,多机房可以做到异地容灾。
  • Raft算法:Consul的一致性算法。

通过Consul可以获取到upstream中的上游服务器列表,下面要做的事情就是生成upstream中的模板了。这里就需要用到Consul-templete,它可以使用HTTP长轮询实现变更触发和配置更改。从而可以根据Consul服务器列表动态生成配置文件,然后去重新启动OpenResty/Nginx即可。

Consul+Consul-templete 方式

Consul+Consul-templete 就如上面所说的,是一种监听服务器列表变更,然后动态生成upstream模板,重启服务器。

Consul-Server

笔者使用的是MAC,下面所进行的操作都是基于MAC系统的。首先需要安装Consul如下:

brew install consul<br></br>

安装完成之后,可以通过如下命令启动Consul服务:

consul agent -dev<br></br>

启动完成之后,可以通过如下地址:localhost:8500/ui。访问Consul的Web界面:

OpenResty

  • 注册服务

可以使用HTTP的方式向Consul注册一个服务:

curl -X PUT http://localhost:8500/v1/agent/service/register -d '<br></br>{<br></br> ID: moguhu_server1,<br></br> Name: moguhu_server,<br></br> Tags: [dev],<br></br> Address: 127.0.0.1,<br></br> Port: 8081<br></br>}<br></br>'<br></br>
  • ID:代表要注册的服务的唯一标识
  • Name:表示一组服务的名称
  • Tags:服务标签,可以用于区分开发、测试环境
  • Address:服务的地址
  • Port:服务的端口

Consul-template

Consul-template的作用是生成upstream配置模板,安装命令如下:

brew install consul-template<br></br>

然后在nginx.conf同级目录下创建moguhu_server.ctmpl

upstream moguhu_server {<br></br> {{range service dev.moguhu_server@dc1}}<br></br> server {{.Address}}:{{.Port}};<br></br> {{end}}<br></br>}<br></br>

重启OpenResty脚本如下:reboot.sh

ps -ef | grep nginx | grep -v grep<br></br>if [ $? -ne 0 ]<br></br>then<br></br> sudo ~/software/openresty/nginx/sbin/nginx -p ~/hugege/code-sublime/01-zhtest -c ~/hugege/code-sublime/01-zhtest/config/nginx.conf<br></br> echo OpenResty start...<br></br>else<br></br> sudo ~/software/openresty/nginx/sbin/nginx -p ~/hugege/code-sublime/01-zhtest -c ~/hugege/code-sublime/01-zhtest/config/nginx.conf -s reload<br></br> echo Openresty restart...<br></br>fi<br></br>

然后nginx.conf配置如下:

include /Users/xuefeihu/hugege/code-sublime/01-zhtest/config/moguhu_server.conf;<br></br>...<br></br>server {<br></br> listen 80;<br></br> server_name localhost;<br></br> charset utf-8;<br></br> location / {<br></br> proxy_pass http://moguhu_server;<br></br> }<br></br>}<br></br>

上游服务器

上游服务器upstream中使用的是Spring Boot实现的,其核心代码如下所示:

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

笔者在实验时,Consul版本的问题,造成在JVM停止时,没有执行删除服务的操作。因此附上下面的pom依赖

<dependencies><br></br>       <dependency><br></br>         <groupId>org.springframework.boot</groupId><br></br>            <artifactId>spring-boot-starter-web</artifactId><br></br>       </dependency><br></br>        <dependency><br></br>         <groupId>com.alibaba</groupId><br></br>         <artifactId>fastjson</artifactId><br></br>          <version>1.2.17</version><br></br>      </dependency><br></br>        <dependency><br></br>         <groupId>com.orbitz.consul</groupId><br></br>           <artifactId>consul-client</artifactId><br></br>         <version>1.2.1</version><br></br>       </dependency><br></br>        <dependency><br></br>         <groupId>com.google.guava</groupId><br></br>            <artifactId>guava</artifactId><br></br>         <version>25.1-jre</version><br></br>        </dependency><br></br>        <dependency><br></br>         <groupId>com.squareup.okhttp3</groupId><br></br>            <artifactId>okhttp</artifactId><br></br>            <version>3.10.0</version><br></br>      </dependency><br></br></dependencies><br></br>

测试验证

1、启动Consul

consul agent -dev<br></br>

2、启动Consul-template

consul-template -consul-addr 127.0.0.1:8500 -template \<br></br>/Users/xuefeihu/hugege/code-sublime/01-zhtest/config/moguhu_server.ctmpl:\<br></br>/Users/xuefeihu/hugege/code-sublime/01-zhtest/config/moguhu_server.conf:\<br></br>sh /Users/xuefeihu/hugege/code-sublime/01-zhtest/config/reboot.sh<br></br>

3、启动2台upstream服务器

然后你会发现在nginx.conf的同级目录下生成了moguhu_server.conf文件,内容如下:

upstream real_server {<br></br> server 127.0.0.1:8081;<br></br> server 127.0.0.1:8082;<br></br>}<br></br>

当手动停掉一台服务器时,配置又会变更为如下:

upstream real_server {<br></br> server 127.0.0.1:8081;<br></br>}<br></br>

此时reboot.sh脚本会自动触发执行,如下所示:

OpenResty

Consul+Lua 方式

上面的方式实现动态负载均衡在配置较多的时候会有一些问题,比如配置较多时,OpenResty重启的速度就会变慢。所以通过Lua脚本的方式可以规避掉重启这一步骤。

使用Lua实现时,与上面的组件相比Consul-templete就不需要了。通过Consul的http://127.0.0.1:8500/v1/catalog/service/moguhu_server接口就可以获取到服务的列表,如下所示:

[<br></br> {<br></br> ID: 5d452e6b-71e0-67ae-5169-c7e6342da53b,<br></br> Node: Jacks-MacBook-Air.local,<br></br> Address: 127.0.0.1,<br></br> Datacenter: dc1,<br></br> TaggedAddresses: {<br></br> lan: 127.0.0.1,<br></br> wan: 127.0.0.1<br></br> },<br></br> NodeMeta: {<br></br> consul-network-segment: <br></br> },<br></br> ServiceID: csi,<br></br> ServiceName: moguhu_server,<br></br> ServiceTags: [<br></br> dev<br></br> ],<br></br> ServiceAddress: 127.0.0.1,<br></br> ServiceMeta: {},<br></br> ServicePort: 8081,<br></br> ServiceEnableTagOverride: false,<br></br> CreateIndex: 10,<br></br> ModifyIndex: 10<br></br> },<br></br> {<br></br> ID: 5d452e6b-71e0-67ae-5169-c7e6342da53b,<br></br> Node: Jacks-MacBook-Air.local,<br></br> Address: 127.0.0.1,<br></br> Datacenter: dc1,<br></br> TaggedAddresses: {<br></br> lan: 127.0.0.1,<br></br> wan: 127.0.0.1<br></br> },<br></br> NodeMeta: {<br></br> consul-network-segment: <br></br> },<br></br> ServiceID: oc,<br></br> ServiceName: moguhu_server,<br></br> ServiceTags: [<br></br> dev<br></br> ],<br></br> ServiceAddress: 127.0.0.1,<br></br> ServiceMeta: {},<br></br> ServicePort: 8082,<br></br> ServiceEnableTagOverride: false,<br></br> CreateIndex: 12,<br></br> ModifyIndex: 12<br></br> }<br></br>]<br></br>

这一方式当中主要就是OpenResty里面的相关配置。

OpenResty 配置

upstreams.lua

local http = require socket.http<br></br>local ltn12 = require ltn12<br></br>local cjson = require cjson<br></br>local _M = {}<br></br>_M._VERSION=0.1<br></br>function _M:update_upstreams()<br></br>  local resp = {}<br></br>    http.request{<br></br>   url = http://127.0.0.1:8500/v1/catalog/service/moguhu_server, sink = ltn12.sink.table(resp)<br></br>   }<br></br>  local resp = cjson.decode(resp)<br></br>    local upstreams = {}<br></br>   for i, v in ipairs(resp) do<br></br>    upstreams[i+1] = {ip=v.Address, port=v.ServicePort}<br></br>    end<br></br>    ngx.shared.upstream_list:set(moguhu_server, cjson.encode(upstreams))<br></br>end<br></br>function _M:get_upstreams()<br></br> local upstreams_str = ngx.shared.upstream_list:get(moguhu_server)<br></br> return upstreams_str<br></br>end<br></br>return _M<br></br>

nginx.conf

lua_shared_dict upstream_list 10m;<br></br># 第一次初始化<br></br>init_by_lua_block {<br></br> local upstreams = require upstreams;<br></br> upstreams.update_upstreams();<br></br>}<br></br># 定时拉取配置<br></br>init_worker_by_lua_block {<br></br> local upstreams = require upstreams;<br></br> local handle = nil;<br></br> handle = function ()<br></br> --TODO:控制每次只有一个worker执行<br></br> upstreams.update_upstreams();<br></br> ngx.timer.at(5, handle);<br></br> end<br></br> ngx.timer.at(5, handle);<br></br>}<br></br>...<br></br>upstream moguhu_server {<br></br> server 0.0.0.1; #占位server<br></br> balancer_by_lua_block {<br></br> local balancer = require ngx.balancer;<br></br> local upstreams = require upstreams;<br></br><br></br> local tmp_upstreams = upstreams.get_upstreams();<br></br> local ip_port = tmp_upstreams[math.random(1, table.getn(tmp_upstreams))];<br></br> ngx.log(ngx.ERR, current :=============, math.random(1, table.getn(tmp_upstreams)));<br></br> balancer.set_current_peer(ip_port.ip, ip_port.port);<br></br> }<br></br>}<br></br>server {<br></br> listen 80;<br></br> server_name localhost;<br></br> charset utf-8;<br></br> location / {<br></br> proxy_pass http://moguhu_server;<br></br> }<br></br>}<br></br>

上面通过balancer_by_lua_block去动态的设置了,upstream的服务器列表。然后启动OpenResty就可以了。


参考:《亿级流量网站架构核心技术》

发表评论

电子邮件地址不会被公开。 必填项已用*标注