使用 Loadcat 簡化 NGINX 負載平衡

已發表: 2022-03-11

設計為可水平擴展的 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。 請注意,上面的代碼片段缺少一些在反向代理配置中經常使用的明顯調整,但為簡潔起見保持這種方式。

但是,如果我們想要平衡同一 Web 應用程序服務器的兩個實例之間的所有傳入請求,該怎麼辦? 這就是“上游”指令變得有用的地方。 在 NGINX 中,使用“upstream”指令,可以定義多個後端節點,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; } }

請注意我們如何定義一個名為“nodes”的“上游”塊,由兩個服務器組成。 每個服務器都由一個 IP 地址和它們正在偵聽的端口號標識。 有了這個,NGINX 就成為了最簡單形式的負載均衡器。 默認情況下,NGINX 將以循環方式分發傳入請求,其中第一個請求將代理到第一台服務器,第二個代理到第二台服務器,第三個代理到第一台服務器,依此類推。

然而,在負載平衡方面,NGINX 提供了更多功能。 它允許您為每個服務器定義權重,將它們標記為暫時不可用,選擇不同的平衡算法(例如,有一個基於客戶端 IP 哈希的算法)等。這些特性和配置指令都很好地記錄在 nginx.org . 此外,NGINX 允許動態更改和重新加載配置文件,幾乎沒有中斷。

NGINX 的可配置性和簡單的配置文件使它很容易適應許多需求。 Internet 上已經有大量教程教您如何將 NGINX 配置為負載均衡器。

Loadcat:NGINX 配置工具

程序有一些令人著迷的地方,而不是自己做某事,而是配置其他工具來為他們做這件事。 除了可能接受用戶輸入並生成一些文件之外,它們實際上並沒有做太多事情。 您從這些工具中獲得的大部分好處實際上是其他工具的功能。 但是,它們確實讓生活變得輕鬆。 在嘗試為我自己的一個項目設置負載均衡器時,我想知道:為什麼不對 NGINX 及其負載均衡功能做類似的事情呢?

負載貓誕生了!

使用 Go 構建的 Loadcat 仍處於起步階段。 目前,該工具僅允許您配置 NGINX 以進行負載平衡和 SSL 終止。 它為用戶提供了一個簡單的基於 Web 的 GUI。 讓我們看看下面的內容,而不是瀏覽該工具的各個功能。 但請注意,如果有人喜歡手動使用 NGINX 配置文件,他們可能會發現這樣的工具沒有什麼價值。

選擇 Go 作為編程語言有幾個原因。 其中之一是 Go 生成已編譯的二進製文件。 這使我們能夠構建和分發或部署 Loadcat 作為已編譯的二進製文件到遠程服務器,而無需擔心解決依賴關係。 大大簡化了設置過程的東西。 當然,二進製文件假定 NGINX 已經安裝並且存在一個 systemd 單元文件。

如果您不是 Go 工程師,請不要擔心。 Go 上手非常簡單有趣。 此外,實現本身非常簡單,您應該能夠輕鬆地跟進。

結構

Go 構建工具對如何構建應用程序施加了一些限制,其餘的留給開發人員。 在我們的例子中,我們根據它們的用途將它們分成了幾個 Go 包:

  • cfg:加載、解析和提供配置值
  • cmd/loadcat:主包,包含入口點,編譯成二進制
  • 數據:包含“模型”,使用嵌入式鍵/值存儲進行持久化
  • feline:包含核心功能,例如配置文件的生成、重新加載機制等。
  • ui:包含模板、URL 處理程序等。

如果我們仔細查看包結構,尤其是在 feline 包中,我們會注意到所有 NGINX 特定代碼都保存在子包 feline/nginx 中。 這樣做是為了讓我們可以保持應用程序邏輯的其餘部分通用,並在未來擴展對其他負載平衡器(例如 HAProxy)的支持。

入口點

讓我們從 Loadcat 的主包開始,它位於“cmd/loadcatd”中。 主函數,應用程序的入口點,做了三件事。

 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”)加載配置文件。 接下來,我們正在初始化幾個組件,即核心 feline 包和數據庫。 最後,我們正在為基於 Web 的 GUI 啟動一個 Web 服務器。

配置

加載和解析配置文件可能是這裡最簡單的部分。 我們使用 TOML 對配置信息進行編碼。 Go 有一個簡潔的 TOML 解析包。 我們需要用戶提供的配置信息非常少,並且在大多數情況下,我們可以確定這些值的合理默認值。 以下結構表示配置文件的結構:

 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 編碼的原因。 從數據庫中檢索Balancer 結構列表的函數的實現如下所示:

 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函數啟動一個只讀事務,遍歷“平衡器”存儲桶中的所有鍵和值,將每個值解碼為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 證書時,使用標準包 encoding/pem 提取兩條信息並存儲在SSLOptions中的Settings字段下: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 。 任何提供兩個函數GenerateReload並具有正確簽名的結構都可以作為驅動程序。

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

例如 feline/nginx 包下的 struct 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'; } } `))

ReloadNginx 結構上的另一個函數,它使 NGINX 重新加載配置文件。 使用的機制基於 Loadcat 的配置方式。 默認情況下,它假定 NGINX 是作為 nginx.service 運行的 systemd 服務,這樣[sudo] systemd reload nginx.service就可以工作。 但是,它不是執行 shell 命令,而是使用包 github.com/coreos/go-systemd/dbus 通過 D-Bus 建立到 systemd 的連接。

基於網絡的圖形用戶界面

有了所有這些組件,我們將用一個簡單的 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 有效負載解析為一個結構,並使用這些數據在數據存儲中實例化和持久化一個新的服務器結構,然後使用包 feline 為平衡器重新生成 NGINX 配置文件。

所有頁面模板都存儲在“ui/templates.go”文件中,相應的模板HTML文件可以在“ui/templates”目錄下找到。

試一試

將 Loadcat 部署到遠程服務器甚至本地環境中都非常容易。 如果您運行的是 Linux(64 位),則可以從存儲庫的 Releases 部分獲取包含預構建 Loadcat 二進製文件的存檔。 如果您覺得有點冒險,您可以克隆存儲庫並自己編譯代碼。 雖然,這種情況下的體驗可能有點令人失望,因為編譯 Go 程序並不是一個真正的挑戰。 如果你正在運行 Arch Linux,那麼你很幸運! 為方便起見,已為分發構建了一個包。 只需下載它並使用您的包管理器安裝它。 項目的 README.md 文件中更詳細地概述了所涉及的步驟。

配置並運行 Loadcat 後,將 Web 瀏覽器指向“http://localhost:26590”(假設它在本地運行並偵聽端口 26590)。 接下來,創建一個平衡器,創建幾個服務器,確保有東西在那些定義的端口上監聽,瞧,你應該讓 NGINX 負載平衡這些正在運行的服務器之間的傳入請求。

下一步是什麼?

這個工具遠非完美,事實上它是一個相當實驗性的項目。 該工具甚至沒有涵蓋 NGINX 的所有基本功能。 例如,如果你想在 NGINX 層緩存後端節點服務的資產,你仍然需要手動修改 NGINX 配置文件。 這就是讓事情變得令人興奮的原因。 這裡有很多可以做的事情,而這正是接下來要做的:涵蓋更多 NGINX 的負載平衡功能——基本的功能,甚至可能是 NGINX Plus 必須提供的功能。

試試 Loadcat。 檢查代碼,分叉它,改變它,玩它。 此外,如果您構建了配置其他軟件的工具或使用了您真正喜歡的工具,請在下面的評論部分告訴我們。