作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
马哈茂德·里德万的头像

Mahmud Ridwan

Mahmud是一名软件开发人员,拥有多年的经验和效率诀窍, scalability, 稳定的解.

Expertise

Years of Experience

13

Share

设计为水平可伸缩的Web应用程序通常需要一个或多个负载平衡节点. 它们的主要目的是以公平的方式在可用的web服务器上分配传入的流量. 简单地通过增加节点数量和让负载平衡器适应这种变化来增加web应用程序的总体容量,这种能力在生产中被证明是非常有用的.

NGINX是一个提供高性能负载平衡功能的web服务器, 它还有许多其他功能. 其中一些功能仅作为其订阅模式的一部分可用, 但是免费和开源版本仍然具有非常丰富的功能,并且具有开箱即用的最基本的负载平衡功能.

简化NGINX负载平衡与Loadcat

简化NGINX负载平衡与Loadcat

在本教程中,我们将探索控件的内部机制 experimental tool 它允许你配置你的NGINX实例作为一个负载均衡器, 通过提供一个简洁的基于web的用户界面,抽象出NGINX配置文件的所有细节. 本文的目的是展示开始构建这样一个工具是多么容易. 值得一提的是,Loadcat项目很大程度上受到了Linode的启发 NodeBalancers.

NGINX,服务器和上游

NGINX最流行的用途之一是从客户端到web服务器应用程序的反向代理请求. 尽管web应用程序是用像Node这样的编程语言开发的.js和Go可以成为自给自足的web服务器, 在实际的服务器应用程序前面使用反向代理可以提供许多好处. 在NGINX配置文件中,一个简单用例的“server”块看起来像这样:

server {
    listen  80;
    server_name例子.com;

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

这将使NGINX监听端口80上指向示例的所有请求.Com并将它们每个传递给运行在192的web服务器应用程序.168.0.51:5000. 我们也可以使用环回IP地址127.0.0.如果web应用服务器在本地运行,则此处为1. 请注意,上面的代码片段缺少一些在反向代理配置中经常使用的明显调整, 但为了简洁起见,我一直这么说.

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

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

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

注意我们是如何定义一个名为“nodes”的“上游”块的,它由两个服务器组成. 每个服务器由IP地址和它们正在监听的端口号标识. 这样,NGINX就以最简单的形式成为了一个负载均衡器. By default, NGINX将以循环的方式分发传入的请求, 第一个将被代理到第一个服务器的位置, 第二个到第二个服务器, 第三个到第一个服务器,以此类推.

然而,当涉及到负载平衡时,NGINX提供了更多. 它允许您为每个服务器定义权重, 将它们标记为暂时不可用, 选择不同的平衡算法(例如.g. 有一个是基于客户端的IP哈希的),等等. 这些特性和配置指令都是 在nginx上有很好的文档.org. Furthermore, NGINX允许动态更改和重新加载配置文件,几乎没有中断.

NGINX的可配置性和简单的配置文件使得它很容易适应许多需求. 还有大量的教程 already exist 在互联网上教你如何配置NGINX作为负载均衡器.

Loadcat: NGINX配置工具

程序有一些迷人之处,而不是自己做一些事情, 配置其他工具来为他们做这件事. 除了接收用户输入并生成一些文件之外,它们实际上并没有做太多事情. 您从这些工具中获得的大多数好处实际上是其他工具的特性. 但是,它们确实让生活变得轻松. 在尝试为我自己的一个项目设置负载平衡器时, 我想知道:为什么不为NGINX和它的负载平衡能力做一些类似的事情呢?

Loadcat was born!

Loadcat,内置 Go但它仍处于起步阶段. 目前,该工具只允许您配置NGINX的负载平衡和SSL终止. 它提供了一个简单的基于web的 GUI for the user. 让我们来看看下面是什么,而不是浏览工具的各个功能. Be aware though, 如果有人喜欢手工处理NGINX配置文件, 他们可能觉得这样的工具没什么价值.

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

如果你不是 Go engineer,你完全不用担心. Go is quite easy and fun 首先. Moreover, 实现本身非常简单,您应该能够轻松地跟随.

Structure

Go build工具对如何构建应用程序施加了一些限制,并将其余部分留给开发人员. 在我们的例子中,我们根据它们的目的将它们分成了几个Go包:

  • Cfg:加载、解析并提供配置值
  • Cmd /loadcat:主包,包含入口点,编译成二进制文件
  • 数据:包含“模型”,使用嵌入式键/值存储进行持久化
  • 猫:包含核心功能,例如.g. 生成配置文件,重载机制等.
  • ui:包含模板、URL处理程序等.

如果我们仔细看一下包的结构, 尤其是猫科动物, 我们会注意到所有NGINX特定的代码都保存在子包猫科/ NGINX中. 这样做是为了保持应用程序逻辑的其余部分的通用性,并扩展对其他负载平衡器的支持.g. HAProxy).

Entry Point

让我们从Loadcat的主包开始,在“cmd/loadcatd”中找到. main函数是应用程序的入口点,它做三件事.

func main() {
	fconfig := flag.字符串(“配置”、“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)

	//等待“中断”信号(大多数终端使用Ctrl+C)
}

为了使事情简单,使代码更容易阅读, 所有错误处理代码都已从上面的代码段(以及本文后面的代码段)中删除。.

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

Configuration

加载和解析配置文件可能是这里最简单的部分. 我们使用TOML对配置信息进行编码. Go有一个简洁的TOML解析包. 我们只需要用户提供很少的配置信息, 在大多数情况下,我们可以为这些值确定相同的默认值. The following struct 表示配置文件的结构:

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

下面是一个典型的“装载猫”.Conf”文件可能看起来像:

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

[nginx]
mode="systemd"

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

As we can see, xml编码的配置文件的结构与 struct shown above it. 配置包首先为属性的某些字段设置一些相同的默认值 struct 然后在上面解析配置文件. 如果在指定的路径上找不到配置文件, it creates one, 并首先将默认值转储到其中.

函数LoadFile(name string)错误{
	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
}

数据和持久性

Meet Bolt. 一个用纯Go编写的嵌入式键/值存储. 它是一个带有非常简单API的包,支持开箱即用的事务 disturbingly fast.

在包数据中,我们有 structs 表示每种类型的实体. 例如,我们有:

类型平衡器结构{
	Id       bson.ObjectId
	Label    string
	设置BalancerSettings
}

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

其中的一个实例 Balancer 表示单个负载平衡器. Loadcat有效地允许你通过一个NGINX实例来平衡多个web应用程序的请求. 每个平衡器可以有一个或多个服务器, 每个服务器可以是一个单独的后端节点.

因为Bolt是一个键值存储, 并且不支持高级数据库查询, 我们有应用程序端逻辑为我们做这些. Loadcat不是用来配置数千个平衡器,每个平衡器中有数千个服务器, 所以这种朴素的方法自然很有效. 此外,Bolt处理作为字节片的键和值,这就是为什么我们对 structs 然后储存在Bolt中. 的列表函数的实现 Balancer structs 数据库中的数据如下所示:

函数listbalancer () ([]Balancer, error) {
	bals:= []Balancer{}
	DB.View(func(tx *bolt.Tx) error {
		b := tx.桶([]字节(“平衡器”))
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			bal := Balancer{}
			bson.Unmarshal(v, &bal)
			Bals = append(Bals, Bals)
		}
		return nil
	})
	return bals, nil
}

ListBalancers 函数启动只读事务, 遍历“balancer”桶中的所有键和值, 将每个值解码为的实例 Balancer struct 并以数组的形式返回.

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

function (l *Balancer) Put() error {
	if !l.Id.Valid() {
		l.Id = bson.NewObjectId()
	}
	if l.Label == "" {
		l.标签= "未标示"
	}
	if l.Settings.协议== "http" {
		//解析证书详细信息
	} else {
		//清除仅与HTTPS相关的字段,如SSL选项和证书详细信息
	}
	return DB.更新(func (tx *螺栓.Tx) error {
		b := tx.桶([]字节(“平衡器”))
		p, err := bson.Marshal(l)
		if err != nil {
			return err
		}
		return b.Put([]byte(l.Id.Hex()), p)
	})
}

The Put 函数为某些字段分配一些默认值, 在HTTPS设置中解析附加的SSL证书, 开始事务, encodes the struct 实例,并根据平衡器的ID将其存储在桶中.

在解析SSL证书时,将使用 标准包编码/pem and stored in SSLOptions under the Settings 字段:DNS名称和指纹.

我们也有一个功能,查找服务器的平衡器:

函数ListServersByBalancer(Balancer *Balancer)([]服务器,错误){
	srvs := []Server{}
	DB.View(func(tx *bolt.Tx) error {
		b := tx.桶([]字节(“服务器”))
		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
}

这个函数显示了我们的方法是多么幼稚. Here, 我们正在有效地读取整个“servers”存储桶,并在返回数组之前过滤掉不相关的实体. 但话说回来,这工作得很好,没有真正的理由去改变它.

The Put 函数对于服务器来说要简单得多 Balancer struct 因为它不需要那么多行代码来设置默认值和计算字段.

Controlling NGINX

在使用Loadcat之前,我们必须配置NGINX来加载生成的配置文件. Loadcat生成“nginx”.根据平衡器的ID(一个短十六进制字符串)为目录下的每个平衡器创建一个“conf”文件。. 这些目录是在“输出”目录下创建的 cwd. 因此,配置NGINX来加载这些生成的配置文件是很重要的. 这可以使用" http "块中的" include "指令来完成:

编辑/etc/nginx/nginx.配置并在" http "块的末尾添加以下行:

http {
    包括/道路/ / / * / nginx.conf;
}

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

在我们的核心包猫科中,我们定义了一个接口 Driver. Any struct 它提供了两个功能, Generate and Reload,并且签名正确,才有资格成为司机.

type驱动接口{
	生成数据(字符串,*.Balancer) error
	Reload() error
}

例如,结构体 Nginx 在cat /nginx包下:

type Nginx struct {
	sync.Mutex

	Systemd *dbus.Conn
}

函数c (n Nginx)生成(dir字符串,bal *data.Balancer) error {
	//获取n的锁.互斥,并在返回前释放

	f, _ := os.Create(filepath.Join(dir, "nginx.conf"))
	TplNginxConf.执行(f, /*模板参数*/)
	f.Close()

	if bal.Settings.协议== "http" {
		//转储私钥和证书到输出目录(以便Nginx可以找到它们)
	}

	return nil
}

函数c (n Nginx) Reload() error {
	//获取n的锁.互斥,并在返回前释放

	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.服务,“更换”,ch)
		<-ch

		return nil

	default:
		return errors.New(“未知Nginx模式”)
	}
}

Generate 可以用包含输出目录路径的字符串和指向 Balancer struct instance. Go提供了一个标准的文本模板包, NGINX驱动程序使用哪个来生成最终的NGINX配置文件. 模板由“上游”块和“服务器”块组成, 根据平衡器的配置方式生成:

var TplNginxConf = template.Must(template.New("").Parse(`
upstream {{.Balancer.Id.Hex}} {
	{{if eq .Balancer.Settings.“最少连接算法"}}
		least_conn;
	{{else if eq .Balancer.Settings.算法“源ip”}}
		ip_hash;
	{{end}}
	{{range $srv := .Balancer.Servers}}
		server  {{$srv.Settings.地址}}= {{$ srv重量.Settings.权重}}{{如果eq $srv.Settings.可用性“可用”}}{{如果eq $srv.Settings.可用性"backup"}}backup{{else if eq $srv.Settings.可用性“不可用”}}{{结束}};
	{{end}}
}
server {
	{{if eq .Balancer.Settings.Protocol "http"}}
		listen  {{.Balancer.Settings.Port}};
	{{else if eq .Balancer.Settings.Protocol "http"}}
		listen  {{.Balancer.Settings.Port}} ssl;
	{{end}}
	server_name  {{.Balancer.Settings.Hostname}};
	{{if eq .Balancer.Settings.Protocol "http"}}
		ssl                   上;
		ssl_certificate {{.Dir}}/server.crt;
		ssl_certificate_key {{.Dir}}/server.key;
	{{end}}
	location / {
		proxy_set_header $ Host;
		proxy_set_header X-Real-IP $remote_addr
		$proxy_add_x_forwarded_for;
		proxy_set_header x - forward - proto $scheme;
		proxy_pass http:// {{.Balancer.Id.Hex}};
		proxy_http_version 1.1;
		proxy_set_header升级$http_upgrade
		proxy_set_header连接'upgrade';
	}
}
`))

Reload 另一个函数开了吗 Nginx struct 这使得NGINX重新加载配置文件. 所使用的机制取决于Loadcat的配置方式. 默认情况下,它假定NGINX是作为NGINX运行的系统服务.service, such that [sudo] systemd reload nginx.service would work. 但是,不是执行shell命令, 通过d总线与系统建立连接 package github.com/coreos/go-systemd/dbus.

Web-based GUI

有了所有这些组件,我们将用一个普通的Bootstrap用户界面将其包装起来.

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

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

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

GET /balancers
GET /balancers/new
POST /平衡器/新
得到平衡器/ {id}
GET /平衡器/ {id} /编辑
POST /平衡器/ {id} /编辑
GET /平衡器/ {id} /服务器/新
POST /平衡器/ {id} /服务器/新
GET /servers/{id}
/服务器/ {id} /编辑
POST /服务器/ {id} /编辑

在这里,仔细检查每一条路线可能不是最有趣的事情, 因为这些几乎都是CRUD页面. 你可以随便看一眼 package ui code 查看这些路由的处理程序是如何实现的.

每个handler函数都是一个例程,它要么:

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

For example:

函数ServeServerNewForm.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	bal, _ := data.GetBalancer(bson.ObjectIdHex (var (" id ")))

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

函数HandleServerCreate(.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	bal, _ := data.GetBalancer(bson.ObjectIdHex (var (" id ")))

	r.ParseForm()
	body := struct {
		标签字符串' schema: ' Label ' '
		Settings struct {
			地址字符串' schema:" Address " '
		}的模式:“设置”
	}{}
	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.重定向(w, r, "/servers/"+srv.Id.十六进制()+ /编辑,http.StatusSeeOther)
}

All ServeServerNewForm 函数的作用是从数据存储中获取平衡器并呈现模板, TplServerList 方法检索相关服务器的列表 Servers 平衡器上的功能.

HandleServerCreate 函数将从主体传入的POST有效负载解析为 struct 并使用这些数据实例化和持久化一个新的 Server struct 在使用包猫科重新生成平衡器的NGINX配置文件之前,在数据存储中.

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

Trying It Out

将Loadcat部署到远程服务器甚至本地环境非常简单. 如果您运行的是Linux(64位), 您可以从存储库中获取带有预构建Loadcat二进制文件的归档文件 Releases section. 如果您觉得有点冒险,您可以克隆存储库并自己编译代码. 虽然,这种情况下的经验可能有点 disappointing 因为编译Go程序并不是真正的挑战. 如果您正在运行Arch Linux,那么您很幸运! 为方便起见,已经为发行版构建了一个包. Simply download it 然后使用包管理器安装它. 所涉及的步骤在项目的 README.md file.

配置并运行Loadcat之后, 将web浏览器指向“http://localhost:26590”(假设它在本地运行并在端口26590上侦听). Next, create a balancer, 创建一对服务器, 确保在这些定义的端口上有监听, 瞧,你应该有NGINX负载平衡这些运行的服务器之间的传入请求.

What’s Next?

这个工具远非完美,实际上它是一个实验性项目. 该工具甚至没有涵盖NGINX的所有基本功能. For example, 如果你想在NGINX层缓存后端节点提供的资源, 你仍然需要手工修改NGINX的配置文件. 这就是事情令人兴奋的地方. 这里有很多可以做的,这正是下一步要做的:覆盖更多的NGINX的负载平衡特性——基本的,甚至可能是NGINX Plus必须提供的.

Give Loadcat a try. 检查代码,分叉它,修改它,试用它. Also, 如果你已经构建了一个配置其他软件的工具,或者已经使用了一个你真正喜欢的工具,请在下面的评论部分告诉我们.

聘请Toptal这方面的专家.
Hire Now
马哈茂德·里德万的头像
Mahmud Ridwan

Located in 达卡,达卡区,孟加拉国

Member since January 16, 2014

About the author

Mahmud是一名软件开发人员,拥有多年的经验和效率诀窍, scalability, 稳定的解.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

Expertise

Years of Experience

13

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

世界级的文章,每周发一次.

订阅意味着同意我们的 privacy policy

Toptal Developers

Join the Toptal® community.