个性化阅读
专注于IT技术分析

使用Loadcat简化NGINX负载均衡

本文概述

设计为水平可伸缩的Web应用程序通常需要一个或多个负载均衡节点。它们的主要目的是以公平的方式在可用的Web服务器上分配传入的流量。仅通过增加节点数量并让负载均衡器适应此更改来增加Web应用程序整体容量的能力就可以证明在生产中非常有用。

NGINX是一种Web服务器, 除其他许多功能外, 还提供高性能的负载均衡功能。其中一些功能仅作为其订阅模型的一部分提供, 但是免费和开放源代码版本仍然具有非常丰富的功能, 并且具有开箱即用的最重要的负载均衡功能。

使用Loadcat简化NGINX负载均衡

使用Loadcat简化NGINX负载均衡

鸣叫

在本教程中, 我们将探索一种实验工具的内部机制, 该工具可让你动态配置NGINX实例以充当负载均衡器, 并通过提供整洁的Web-基于用户的界面。本文的目的是说明开始构建这种工具非常容易。值得一提的是, Loadcat项目的灵感很大程度上来自Linode的NodeBalancers。

NGINX, 服务器和上游

NGINX最受欢迎的用途之一是将客户端的请求反向代理到Web服务器应用程序。尽管用诸如Node.js和Go之类的编程语言开发的Web应用程序可以是自给自足的Web服务器, 但是在实际的服务器应用程序之前使用反向代理可以带来很多好处。 NGINX配置文件中的一个简单用例的”服务器”块如下所示:

server {
    listen  80;
    server_name  example.com;

    location / {
        proxy_pass  http://192.168.0.51:5000;
    }
} 

这将使NGINX在端口80上侦听指向example.com的所有请求, 并将每个请求传递给以192.168.0.51:5000运行的某些Web服务器应用程序。如果Web应用程序服务器在本地运行, 我们也可以在此处使用回送IP地址127.0.0.1。请注意, 上面的代码片段缺少一些通常在反向代理配置中使用的明显调整, 但是为了简洁起见, 将其保留下来。

使用Loadcat简化NGINX负载均衡2

但是, 如果我们想平衡同一Web应用程序服务器的两个实例之间的所有传入请求, 该怎么办?这是”上游”指令变得有用的地方。在NGINX中, 使用”上游”指令, 可以定义多个后端节点, 其中NGINX将平衡所有传入请求。例如:

upstream nodes {
    server  192.168.0.51:5000;
    server  192.168.0.52:5000;
}
server {
    listen  80;
    server_name  example.com;

    location / {
        proxy_pass  http://nodes;
    }
}

注意我们如何定义一个由两个服务器组成的”上游”块, 称为”节点”。每个服务器都由它们正在侦听的IP地址和端口号标识。这样, NGINX成为最简单形式的负载均衡器。默认情况下, NGINX将以循环方式分发传入的请求, 第一个请求将代理到第一台服务器, 第二个请求代理到第二台服务器, 第三个请求代理到第一台服务器, 依此类推。

但是, 在负载均衡方面, NGINX提供了更多功能。它允许你为每台服务器定义权重, 将其标记为暂时不可用, 选择其他平衡算法(例如, 一种基于客户端IP哈希的平衡算法)等。这些功能和配置指令均已在nginx.org上进行了详细记录。 。此外, NGINX允许几乎不间断地动态更改和重新加载配置文件。

NGINX的可配置性和简单的配置文件使它很容易适应多种需求。 Internet上已经存在大量教程, 可以教你如何将NGINX配置为负载均衡器。

Loadcat:NGINX配置工具

程序令人着迷, 而不是自己做一些事, 而是配置其他工具来为他们做。除了采取用户输入并生成一些文件之外, 他们实际上并没有做其他事情。你从这些工具中获得的大多数好处实际上是其他工具的功能。但是, 它们无疑使生活变得轻松。在尝试为自己的项目之一设置负载均衡器时, 我想知道:为什么不对NGINX及其负载均衡功能执行类似的操作?

Loadcat诞生了!

使用Go构建的Loadcat仍处于起步阶段。目前, 该工具允许你将NGINX配置为仅用于负载均衡和SSL终止。它为用户提供了一个基于Web的简单GUI。让我们不要窥视该工具的各个功能, 而让我们看一下其底层。但是请注意, 如果有人喜欢手工处理NGINX配置文件, 那么他们可能会在这种工具中发现很少的价值。

为此, 选择Go作为编程语言有几个原因。其中之一是Go生成编译的二进制文件。这使我们可以将Loadcat作为已编译的二进制文件构建和分发或部署到远程服务器, 而不必担心解决依赖关系。可以大大简化设置过程。当然, 该二进制文件假定已经安装了NGINX并且已经存在一个systemd单元文件。

如果你不是Go工程师, 请不要担心。开始使用Go很容易, 也很有趣。此外, 实现本身非常简单, 你应该可以轻松地进行后续操作。

结构体

Go构建工具对如何构造应用程序并将其余部分留给开发人员施加了一些限制。在我们的案例中, 我们根据它们的用途将它们分为几个Go软件包:

  • cfg:加载, 解析并提供配置值
  • cmd / loadcat:主程序包, 包含入口点, 编译为二进制文件
  • 数据:包含”模型”, 使用嵌入式键/值存储来保持持久性
  • 猫科动物:包含核心功能, 例如生成配置文件, 重新加载机制等
  • ui:包含模板, URL处理程序等。

如果我们仔细研究一下包的结构, 尤其是在feline包中, 我们会注意到所有NGINX特定的代码都保存在feline / nginx子包中。这样做是为了使我们可以保持其余应用程序逻辑的通用性, 并在将来扩展对其他负载均衡器(例如HAProxy)的支持。

入口点

让我们从” cmd / loadcatd”中的Loadcat主程序包开始。主要功能(应用程序的入口点)执行三件事。

func main() {
	fconfig := flag.String("config", "loadcat.conf", "")
	flag.Parse()
	cfg.LoadFile(*fconfig)

	feline.SetBase(filepath.Join(cfg.Current.Core.Dir, "out"))

	data.OpenDB(filepath.Join(cfg.Current.Core.Dir, "loadcat.db"))
	defer data.DB.Close()
	data.InitDB()

	http.Handle("/api", api.Router)
	http.Handle("/", ui.Router)

	go http.ListenAndServe(cfg.Current.Core.Address, nil)

	// Wait for an "interrupt" signal (Ctrl+C in most terminals)
}

为了简化程序并使代码易于阅读, 已将所有错误处理代码从上面的代码段(以及本文稍后的代码段)中删除。

从代码中可以看出, 我们正在基于” -config”命令行标志(在当前目录中默认为” loadcat.conf”)加载配置文件。接下来, 我们将初始化几个组件, 即核心猫科动物软件包和数据库。最后, 我们将启动基于Web的GUI的Web服务器。

组态

加载和解析配置文件可能是这里最简单的部分。我们正在使用TOML来编码配置信息。有一个精巧的TOML解析包可用于Go。我们几乎不需要用户提供任何配置信息, 在大多数情况下, 我们可以为这些值确定合理的默认值。以下结构代表配置文件的结构:

struct {
	Core struct {
		Address string
		Dir     string
		Driver  string
	}
	Nginx struct {
		Mode    string
		Systemd struct {
			Service string
		}
	}
}

并且, 典型的” loadcat.conf”文件如下所示:

[core]
address=":26590"
dir="/var/lib/loadcat"
driver="nginx"

[nginx]
mode="systemd"

[nginx.systemd]
service="nginx.service"

如我们所见, TOML编码的配置文件的结构与其上方显示的结构之间存在相似之处。配置程序包首先为结构的某些字段设置了一些合理的默认值, 然后在其上解析配置文件。如果无法在指定的路径中找到配置文件, 则会创建一个配置文件, 然后首先转储默认值。

func LoadFile(name string) error {
	f, _ := os.Open(name)
	if os.IsNotExist(err) {
		f, _ = os.Create(name)
		toml.NewEncoder(f).Encode(Current)
		f.Close()
		return nil
	}
	toml.NewDecoder(f).Decode(&Current)
	return nil
}

数据与持久性

认识博尔特。用纯Go编写的嵌入式键/值存储。它以具有非常简单的API的软件包的形式提供, 支持开箱即用的事务, 并且速度很快。

在数据包数据中, 我们具有表示每种实体类型的结构。例如, 我们有:

type Balancer struct {
	Id       bson.ObjectId
	Label    string
	Settings BalancerSettings
}

type Server struct {
	Id         bson.ObjectId
	BalancerId bson.ObjectId
	Label      string
	Settings   ServerSettings
}

…其中Balancer的一个实例代表单个负载均衡器。通过有效的Loadcat, 你可以通过一个NGINX实例平衡对多个Web应用程序的请求。然后, 每个平衡器可以在其后具有一个或多个服务器, 其中每个服务器可以是一个单独的后端节点。

由于Bolt是键值存储, 并且不支持高级数据库查询, 因此我们拥有应用程序侧逻辑来为我们执行此操作。 Loadcat并不是要配置成千上万个平衡器, 每个平衡器中都有成千上万的服务器, 因此自然地, 这种幼稚的方法就可以正常工作。而且, Bolt可以使用作为字节片的键和值, 这就是为什么我们在将结构存储在Bolt中之前对其进行BSON编码的原因。从数据库检索平衡器结构列表的函数的实现如下所示:

func ListBalancers() ([]Balancer, error) {
	bals := []Balancer{}
	DB.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("balancers"))
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			bal := Balancer{}
			bson.Unmarshal(v, &bal)
			bals = append(bals, bal)
		}
		return nil
	})
	return bals, nil
}

ListBalancers函数启动一个只读事务, 遍历” balancers”存储桶中的所有键和值, 将每个值解码为Balancer结构的实例, 并将它们返回到数组中。

在桶中存储平衡器几乎同样简单:

func (l *Balancer) Put() error {
	if !l.Id.Valid() {
		l.Id = bson.NewObjectId()
	}
	if l.Label == "" {
		l.Label = "Unlabelled"
	}
	if l.Settings.Protocol == "https" {
		// Parse certificate details
	} else {
		// Clear fields relevant to HTTPS only, such as SSL options and certificate details
	}
	return DB.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("balancers"))
		p, err := bson.Marshal(l)
		if err != nil {
			return err
		}
		return b.Put([]byte(l.Id.Hex()), p)
	})
}

Put函数将某些默认值分配给某些字段, 在HTTPS设置中解析附加的SSL证书, 开始交易, 对结构实例进行编码, 并将其存储在存储桶中, 以平衡器的ID为准。

解析SSL证书时, 使用标准软件包编码/ pem提取两条信息, 并将其存储在SSLOptions中的”设置”字段下:DNS名称和指纹。

我们还有一个功能, 可以通过均衡器查找服务器:

func ListServersByBalancer(bal *Balancer) ([]Server, error) {
	srvs := []Server{}
	DB.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("servers"))
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			srv := Server{}
			bson.Unmarshal(v, &srv)
			if srv.BalancerId.Hex() != bal.Id.Hex() {
				continue
			}
			srvs = append(srvs, srv)
		}
		return nil
	})
	return srvs, nil
}

此功能显示了我们的方法实际上是多么幼稚。在这里, 我们有效地读取了整个”服务器”存储桶, 并在返回数组之前过滤掉了不相关的实体。但话又说回来, 这很好, 并且没有真正的理由进行更改。

服务器的Put函数比Balancer结构简单得多, 因为它不需要太多行代码来设置默认值和计算字段。

控制NGINX

在使用Loadcat之前, 我们必须配置NGINX来加载生成的配置文件。 Loadcat通过平衡器的ID(较短的十六进制字符串)为目录下的每个平衡器生成” nginx.conf”文件。这些目录在cwd的” out”目录下创建。因此, 配置NGINX以加载这些生成的配置文件非常重要。可以使用” http”块中的” include”指令来完成此操作:

编辑/etc/nginx/nginx.conf并在” http”块的末尾添加以下行:

http {
    include  /path/to/out/*/nginx.conf;
}

这将导致NGINX扫描” / path / to / out /”下的所有目录, 在每个目录中查找名为” nginx.conf”的文件, 并加载找到的每个目录。

在我们的核心程序包feline中, 我们定义了一个接口Driver。提供具有正确签名的两个函数(生成和重新加载)的任何结构都可以作为驱动程序。

type Driver interface {
	Generate(string, *data.Balancer) error
	Reload() error
}

例如, 在feline / nginx包下的结构Nginx:

type Nginx struct {
	sync.Mutex

	Systemd *dbus.Conn
}

func (n Nginx) Generate(dir string, bal *data.Balancer) error {
	// Acquire a lock on n.Mutex, and release before return

	f, _ := os.Create(filepath.Join(dir, "nginx.conf"))
	TplNginxConf.Execute(f, /* template parameters */)
	f.Close()

	if bal.Settings.Protocol == "https" {
		// Dump private key and certificate to the output directory (so that Nginx can find them)
	}

	return nil
}

func (n Nginx) Reload() error {
	// Acquire a lock on n.Mutex, and release before return

	switch cfg.Current.Nginx.Mode {
	case "systemd":
		if n.Systemd == nil {
			c, err := dbus.NewSystemdConnection()
			n.Systemd = c
		}

		ch := make(chan string)
		n.Systemd.ReloadUnit(cfg.Current.Nginx.Systemd.Service, "replace", ch)
		<-ch

		return nil

	default:
		return errors.New("unknown Nginx mode")
	}
}

可以使用包含输出目录路径和Balancer结构实例的指针的字符串调用Generate。 Go提供了用于文本模板化的标准程序包, NGINX驱动程序将其用于生成最终的NGINX配置文件。该模板由一个”上游”块和一个”服务器”块组成, 该块根据平衡器的配置方式生成:

var TplNginxConf = template.Must(template.New("").Parse(`
upstream {{.Balancer.Id.Hex}} {
	{{if eq .Balancer.Settings.Algorithm "least-connections"}}
		least_conn;
	{{else if eq .Balancer.Settings.Algorithm "source-ip"}}
		ip_hash;
	{{end}}
	{{range $srv := .Balancer.Servers}}
		server  {{$srv.Settings.Address}} weight={{$srv.Settings.Weight}} {{if eq $srv.Settings.Availability "available"}}{{else if eq $srv.Settings.Availability "backup"}}backup{{else if eq $srv.Settings.Availability "unavailable"}}down{{end}};
	{{end}}
}
server {
	{{if eq .Balancer.Settings.Protocol "http"}}
		listen  {{.Balancer.Settings.Port}};
	{{else if eq .Balancer.Settings.Protocol "https"}}
		listen  {{.Balancer.Settings.Port}} ssl;
	{{end}}
	server_name  {{.Balancer.Settings.Hostname}};
	{{if eq .Balancer.Settings.Protocol "https"}}
		ssl                  on;
		ssl_certificate      {{.Dir}}/server.crt;
		ssl_certificate_key  {{.Dir}}/server.key;
	{{end}}
	location / {
		proxy_set_header  Host $host;
		proxy_set_header  X-Real-IP $remote_addr;
		proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header  X-Forwarded-Proto $scheme;
		proxy_pass  http://{{.Balancer.Id.Hex}};
		proxy_http_version  1.1;
		proxy_set_header  Upgrade $http_upgrade;
		proxy_set_header  Connection 'upgrade';
	}
}
`))

重新加载是Nginx结构上的另一个功能, 它使NGINX重新加载配置文件。使用的机制基于如何配置Loadcat。默认情况下, 它假定NGINX是作为nginx.service运行的systemd服务, 这样, 可以使systemd reload nginx.service正常工作。但是, 它没有执行shell命令, 而是使用github.com/coreos/go-systemd/dbus软件包通过D-Bus建立了与systemd的连接。

基于Web的GUI

在所有这些组件都准备就绪后, 我们将用一个简单的Bootstrap用户界面将其包装起来。

NGINX负载均衡功能,包装在简单的GUI中

NGINX负载均衡功能, 包装在简单的GUI中

鸣叫

对于这些基本功能, 一些简单的GET和POST路由处理程序就足够了:

GET /balancers
GET /balancers/new
POST /balancers/new
GET /balancers/{id}
GET /balancers/{id}/edit
POST /balancers/{id}/edit
GET /balancers/{id}/servers/new
POST /balancers/{id}/servers/new
GET /servers/{id}
GET /servers/{id}/edit
POST /servers/{id}/edit

遍历每条单独的路线可能不是最有趣的事情, 因为这些几乎都是CRUD页面。完全可以自由浏览ui包代码, 以了解如何实现这些路由的处理程序。

每个处理程序函数都是一个例程, 可以:

  • 从数据存储中获取数据并使用渲染的模板进行响应(使用获取的数据)
  • 解析传入的表单数据, 在数据存储中进行必要的更改, 并使用feline包重新生成NGINX配置文件

例如:

func ServeServerNewForm(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"]))

	TplServerNewForm.Execute(w, struct {
		Balancer *data.Balancer
	}{
		Balancer: bal, })
}

func HandleServerCreate(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	bal, _ := data.GetBalancer(bson.ObjectIdHex(vars["id"]))

	r.ParseForm()
	body := struct {
		Label    string `schema:"label"`
		Settings struct {
			Address string `schema:"address"`
		} `schema:"settings"`
	}{}
	schema.NewDecoder().Decode(&body, r.PostForm)

	srv := data.Server{}
	srv.BalancerId = bal.Id
	srv.Label = body.Label
	srv.Settings.Address = body.Settings.Address
	srv.Put()
	
	feline.Commit(bal)

	http.Redirect(w, r, "/servers/"+srv.Id.Hex()+"/edit", http.StatusSeeOther)
}

ServeServerNewForm函数的全部作用是从数据存储中获取一个平衡器并呈现一个模板(在本例中为TplServerList), 该模板使用平衡器上的Servers函数检索相关服务器的列表。

另一方面, HandleServerCreate函数将来自主体的传入POST有效负载解析为一个结构, 并使用这些数据在数据包中实例化新的Server结构并将其持久化, 然后再使用feline软件包为平衡器重新生成NGINX配置文件。

所有页面模板都存储在” ui / templates.go”文件中, 相应的模板HTML文件可在” ui / templates”目录下找到。

尝试一下

将Loadcat部署到远程服务器甚至在你的本地环境中都非常容易。如果你使用的是Linux(64位), 则可以从存储库的”发行版”部分中获取带有预构建的Loadcat二进制文件的存档。如果你有点冒险, 可以克隆存储库并自己编译代码。虽然, 在那种情况下的经验可能有点令人失望, 因为编译Go程序并不是真正的挑战。如果你正在运行Arch Linux, 那么你很幸运!为了方便分发, 已构建了一个软件包以供分发。只需下载并使用软件包管理器进行安装即可。该项目的README.md文件中更详细地概述了涉及的步骤。

一旦配置并运行了Loadcat, 将你的Web浏览器指向” http:// localhost:26590″(假设它在本地运行并在端口26590上侦听)。接下来, 创建一个均衡器, 创建几个服务器, 确保正在侦听那些已定义的端口, 瞧, 你应该让NGINX负载均衡那些正在运行的服务器之间的传入请求。

下一步是什么?

该工具远非完美, 实际上它是一个实验项目。该工具甚至无法涵盖NGINX的所有基本功能。例如, 如果要缓存NGINX层的后端节点服务的资产, 则仍然必须手动修改NGINX配置文件。这就是使事情变得令人兴奋的原因。接下来有很多事情可以做, 而这正是下一步:覆盖NGINX的更多负载均衡功能-基本功能甚至NGINX Plus必须提供的功能。

尝试一下Loadcat。签出代码, 对其进行分叉, 更改, 使用。另外, 请让我们知道你是否构建了可配置其他软件的工具, 或者是否在下面的评论部分使用了你真正喜欢的工具。

赞(0)
未经允许不得转载:srcmini » 使用Loadcat简化NGINX负载均衡

评论 抢沙发

评论前必须登录!