Penyeimbangan Beban NGINX yang disederhanakan dengan Loadcat

Diterbitkan: 2022-03-11

Aplikasi web yang dirancang untuk dapat diskalakan secara horizontal seringkali memerlukan satu atau lebih node penyeimbang beban. Tujuan utama mereka adalah untuk mendistribusikan lalu lintas masuk ke server web yang tersedia secara adil. Kemampuan untuk meningkatkan kapasitas keseluruhan aplikasi web hanya dengan meningkatkan jumlah node dan membuat penyeimbang beban beradaptasi dengan perubahan ini terbukti sangat berguna dalam produksi.

NGINX adalah server web yang menawarkan fitur penyeimbangan beban kinerja tinggi, di antara banyak kemampuan lainnya. Beberapa dari fitur tersebut hanya tersedia sebagai bagian dari model langganan mereka, tetapi versi gratis dan open source masih sangat kaya fitur dan dilengkapi dengan fitur penyeimbang beban yang paling penting di luar kotak.

Penyeimbangan Beban NGINX yang disederhanakan dengan Loadcat

Penyeimbangan Beban NGINX yang disederhanakan dengan Loadcat
Menciak

Dalam tutorial ini, kita akan menjelajahi mekanisme bagian dalam dari alat eksperimental yang memungkinkan Anda mengonfigurasi instans NGINX Anda dengan cepat untuk bertindak sebagai penyeimbang beban, mengabstraksikan semua detail seluk beluk file konfigurasi NGINX dengan menyediakan web- antarmuka pengguna berbasis. Tujuan artikel ini adalah untuk menunjukkan betapa mudahnya untuk mulai membangun alat semacam itu. Perlu disebutkan bahwa proyek Loadcat sangat terinspirasi oleh NodeBalancers Linode.

NGINX, Server, dan Hulu

Salah satu penggunaan NGINX yang paling populer adalah permintaan proxy terbalik dari klien ke aplikasi server web. Meskipun aplikasi web yang dikembangkan dalam bahasa pemrograman seperti Node.js dan Go dapat menjadi server web mandiri, memiliki proxy terbalik di depan aplikasi server yang sebenarnya memberikan banyak manfaat. Blok "server" untuk kasus penggunaan sederhana seperti ini dalam file konfigurasi NGINX dapat terlihat seperti ini:

 server { listen 80; server_name example.com; location / { proxy_pass http://192.168.0.51:5000; } }

Ini akan membuat NGINX mendengarkan pada port 80 untuk semua permintaan yang diarahkan ke example.com dan meneruskannya masing-masing ke beberapa aplikasi server web yang berjalan pada 192.168.0.51:5000. Kami juga dapat menggunakan alamat IP loopback 127.0.0.1 di sini jika server aplikasi web berjalan secara lokal. Harap perhatikan bahwa cuplikan di atas tidak memiliki beberapa penyesuaian yang jelas yang sering digunakan dalam konfigurasi proxy-terbalik, tetapi disimpan dengan cara ini untuk singkatnya.

Tetapi bagaimana jika kita ingin menyeimbangkan semua permintaan yang masuk antara dua instance dari server aplikasi web yang sama? Di sinilah arahan "upstream" menjadi berguna. Di NGINX, dengan arahan "upstream", dimungkinkan untuk menentukan beberapa node back-end di antaranya NGINX akan menyeimbangkan semua permintaan yang masuk. Sebagai contoh:

 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; } }

Perhatikan bagaimana kami mendefinisikan blok "upstream", bernama "node", yang terdiri dari dua server. Setiap server diidentifikasi oleh alamat IP dan nomor port yang mereka dengarkan. Dengan ini, NGINX menjadi penyeimbang beban dalam bentuknya yang paling sederhana. Secara default, NGINX akan mendistribusikan permintaan yang masuk secara round-robin, di mana yang pertama akan diproksikan ke server pertama, yang kedua ke server kedua, yang ketiga ke server pertama, dan seterusnya.

Namun, NGINX memiliki lebih banyak hal untuk ditawarkan dalam hal penyeimbangan beban. Ini memungkinkan Anda untuk menentukan bobot untuk setiap server, menandainya sebagai tidak tersedia untuk sementara, memilih algoritme penyeimbangan yang berbeda (mis. ada satu yang bekerja berdasarkan hash IP klien), dll. Semua fitur dan arahan konfigurasi ini didokumentasikan dengan baik di nginx.org . Selain itu, NGINX memungkinkan file konfigurasi diubah dan dimuat ulang dengan cepat tanpa gangguan.

Konfigurasi NGINX dan file konfigurasi sederhana membuatnya sangat mudah untuk menyesuaikannya dengan banyak kebutuhan. Dan sejumlah besar tutorial sudah ada di Internet yang mengajarkan Anda cara mengonfigurasi NGINX sebagai penyeimbang beban dengan tepat.

Loadcat: Alat Konfigurasi NGINX

Ada sesuatu yang menarik tentang program yang alih-alih melakukan sesuatu sendiri, konfigurasikan alat lain untuk melakukannya untuk mereka. Mereka tidak benar-benar melakukan banyak hal selain mungkin mengambil input pengguna dan menghasilkan beberapa file. Sebagian besar manfaat yang Anda peroleh dari alat tersebut sebenarnya adalah fitur alat lain. Tapi, mereka pasti membuat hidup lebih mudah. Saat mencoba menyiapkan penyeimbang beban untuk salah satu proyek saya sendiri, saya bertanya-tanya: mengapa tidak melakukan hal serupa untuk NGINX dan kemampuan penyeimbangan bebannya?

Loadcat lahir!

Loadcat, yang dibuat dengan Go, masih dalam masa pertumbuhan. Saat ini, alat ini memungkinkan Anda untuk mengonfigurasi NGINX hanya untuk penyeimbangan beban dan penghentian SSL. Ini menyediakan GUI berbasis web sederhana untuk pengguna. Daripada menelusuri fitur individual alat ini, mari kita intip apa yang ada di bawahnya. Namun berhati-hatilah, jika seseorang senang bekerja dengan file konfigurasi NGINX secara manual, mereka mungkin menemukan sedikit nilai dalam alat semacam itu.

Ada beberapa alasan di balik memilih Go sebagai bahasa pemrograman untuk ini. Salah satunya adalah Go menghasilkan binari terkompilasi. Ini memungkinkan kami untuk membangun dan mendistribusikan atau menyebarkan Loadcat sebagai biner terkompilasi ke server jarak jauh tanpa khawatir tentang penyelesaian dependensi. Sesuatu yang sangat menyederhanakan proses penyiapan. Tentu saja, biner mengasumsikan bahwa NGINX sudah diinstal dan file unit systemd ada untuknya.

Jika Anda bukan seorang insinyur Go, jangan khawatir sama sekali. Go cukup mudah dan menyenangkan untuk memulai. Selain itu, implementasinya sendiri sangat mudah dan Anda harus dapat mengikutinya dengan mudah.

Struktur

Go build tools memberlakukan beberapa batasan tentang bagaimana Anda dapat menyusun aplikasi Anda dan menyerahkan sisanya kepada pengembang. Dalam kasus kami, kami telah memecah hal-hal menjadi beberapa paket Go berdasarkan tujuannya:

  • cfg: memuat, mem-parsing, dan memberikan nilai konfigurasi
  • cmd/loadcat: paket utama, berisi titik masuk, dikompilasi menjadi biner
  • data: berisi "model", menggunakan penyimpanan kunci/nilai yang disematkan untuk kegigihan
  • feline: berisi fungsionalitas inti, misalnya pembuatan file konfigurasi, mekanisme reload, dll.
  • ui: berisi template, penangan URL, dll.

Jika kita melihat lebih dekat pada struktur paket, terutama di dalam paket feline, kita akan melihat bahwa semua kode khusus NGINX telah disimpan dalam sub-paket feline/nginx. Ini dilakukan agar kami dapat menjaga logika aplikasi lainnya tetap generik dan memperluas dukungan untuk penyeimbang beban lainnya (misalnya HAProxy) di masa mendatang.

Titik masuk

Mari kita mulai dari paket utama untuk Loadcat, yang terdapat di dalam “cmd/loadcatd”. Fungsi utama, titik masuk aplikasi, melakukan tiga hal.

 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) }

Untuk mempermudah dan membuat kode lebih mudah dibaca, semua kode penanganan kesalahan telah dihapus dari cuplikan di atas (dan juga dari cuplikan nanti di artikel ini).

Seperti yang Anda ketahui dari kode, kami memuat file konfigurasi berdasarkan flag baris perintah “-config” (yang defaultnya adalah “loadcat.conf” di direktori saat ini). Selanjutnya, kita menginisialisasi beberapa komponen, yaitu paket core feline dan database. Akhirnya, kami memulai server web untuk GUI berbasis web.

Konfigurasi

Memuat dan menguraikan file konfigurasi mungkin adalah bagian termudah di sini. Kami menggunakan TOML untuk mengkodekan informasi konfigurasi. Ada paket penguraian TOML yang rapi yang tersedia untuk Go. Kami membutuhkan informasi konfigurasi yang sangat sedikit dari pengguna, dan dalam banyak kasus kami dapat menentukan default yang waras untuk nilai-nilai ini. Struct berikut mewakili struktur file konfigurasi:

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

Dan, inilah tampilan khas file "loadcat.conf":

 [core] address=":26590" dir="/var/lib/loadcat" driver="nginx" [nginx] mode="systemd" [nginx.systemd] service="nginx.service"

Seperti yang bisa kita lihat, ada kesamaan antara struktur file konfigurasi yang dikodekan TOML dan struct yang ditunjukkan di atasnya. Paket konfigurasi dimulai dengan mengatur beberapa default waras untuk bidang tertentu dari struct dan kemudian mem-parsing file konfigurasi di atasnya. Jika gagal menemukan file konfigurasi di jalur yang ditentukan, itu akan membuatnya, dan membuang nilai default di dalamnya terlebih dahulu.

 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 }

Data dan Kegigihan

Bertemu Bolt. Penyimpanan kunci/nilai tertanam yang ditulis dalam Go murni. Itu datang sebagai paket dengan API yang sangat sederhana, mendukung transaksi di luar kotak, dan sangat cepat.

Dalam data paket, kami memiliki struct yang mewakili setiap jenis entitas. Misalnya, kami memiliki:

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

… di mana instance Balancer mewakili penyeimbang beban tunggal. Loadcat secara efektif memungkinkan Anda untuk menyeimbangkan permintaan untuk beberapa aplikasi web melalui satu instance NGINX. Setiap penyeimbang kemudian dapat memiliki satu atau lebih server di belakangnya, di mana setiap server dapat menjadi simpul back-end yang terpisah.

Karena Bolt adalah penyimpanan nilai kunci, dan tidak mendukung kueri database tingkat lanjut, kami memiliki logika sisi aplikasi yang melakukan ini untuk kami. Loadcat tidak dimaksudkan untuk mengonfigurasi ribuan penyeimbang dengan ribuan server di masing-masingnya, jadi tentu saja pendekatan naif ini berfungsi dengan baik. Juga, Bolt bekerja dengan kunci dan nilai yang merupakan irisan byte, dan itulah sebabnya kami mengkodekan BSON struct sebelum menyimpannya di Bolt. Implementasi fungsi yang mengambil daftar struct Balancer dari database ditunjukkan di bawah ini:

 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 }

Fungsi ListBalancers memulai transaksi hanya-baca, mengulangi semua kunci dan nilai dalam ember "penyeimbang", mendekodekan setiap nilai ke instance struct Balancer dan mengembalikannya dalam array.

Menyimpan penyeimbang dalam ember hampir sama sederhananya:

 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) }) }

Fungsi Put memberikan beberapa nilai default ke bidang tertentu, mem-parsing sertifikat SSL terlampir dalam pengaturan HTTPS, memulai transaksi, mengkodekan instance struct dan menyimpannya di bucket terhadap ID penyeimbang.

Saat menguraikan sertifikat SSL, dua informasi diekstraksi menggunakan pengkodean/pem paket standar dan disimpan di SSLOptions di bawah bidang Pengaturan : nama DNS dan sidik jari.

Kami juga memiliki fungsi yang mencari server dengan penyeimbang:

 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 }

Fungsi ini menunjukkan betapa naifnya pendekatan kita sebenarnya. Di sini, kami secara efektif membaca seluruh ember "server" dan memfilter entitas yang tidak relevan sebelum mengembalikan array. Tetapi sekali lagi, ini berfungsi dengan baik, dan tidak ada alasan nyata untuk mengubahnya.

Fungsi Put untuk server jauh lebih sederhana daripada struct Balancer karena tidak memerlukan banyak baris pengaturan kode default dan bidang yang dihitung.

Mengontrol NGINX

Sebelum menggunakan Loadcat, kita harus mengkonfigurasi NGINX untuk memuat file konfigurasi yang dihasilkan. Loadcat menghasilkan file “nginx.conf” untuk setiap penyeimbang di bawah direktori dengan ID penyeimbang (string hex pendek). Direktori ini dibuat di bawah direktori "keluar" di cwd . Oleh karena itu, penting bagi Anda untuk mengonfigurasi NGINX untuk memuat file konfigurasi yang dihasilkan ini. Ini dapat dilakukan dengan menggunakan direktif “include” di dalam blok “http”:

Edit /etc/nginx/nginx.conf dan tambahkan baris berikut di akhir blok “http”:

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

Ini akan menyebabkan NGINX memindai semua direktori yang ditemukan di "/path/to/out/", mencari file bernama "nginx.conf" di dalam setiap direktori, dan memuat setiap direktori yang ditemukannya.

Dalam paket inti kami, kucing, kami mendefinisikan antarmuka Driver . Setiap struct yang menyediakan dua fungsi, Generate dan Reload , dengan tanda tangan yang benar memenuhi syarat sebagai driver.

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

Misalnya, struct Nginx di bawah paket feline/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") } }

Generate dapat dipanggil dengan string yang berisi path ke direktori output dan pointer ke instance struct Balancer . Go menyediakan paket standar untuk templating teks, yang digunakan driver NGINX untuk menghasilkan file konfigurasi NGINX akhir. Template terdiri dari blok "upstream" diikuti oleh blok "server", yang dibuat berdasarkan bagaimana penyeimbang dikonfigurasi:

 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'; } } `))

Reload adalah fungsi lain pada struct Nginx yang membuat NGINX memuat ulang file konfigurasi. Mekanisme yang digunakan didasarkan pada bagaimana Loadcat dikonfigurasi. Secara default diasumsikan NGINX adalah layanan systemd yang berjalan sebagai nginx.service, sehingga [sudo] systemd reload nginx.service akan berfungsi. Namun alih-alih menjalankan perintah shell, ini membuat koneksi ke systemd melalui D-Bus menggunakan paket github.com/coreos/go-systemd/dbus.

GUI berbasis web

Dengan semua komponen ini di tempat, kami akan membungkus semuanya dengan antarmuka pengguna Bootstrap biasa.

Fitur penyeimbang beban NGINX, dibungkus dengan GUI sederhana

Fitur penyeimbang beban NGINX, dibungkus dengan GUI sederhana
Menciak

Untuk fungsionalitas dasar ini, beberapa penangan rute GET dan POST sederhana sudah cukup:

 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

Melewati setiap rute individu mungkin bukan hal yang paling menarik untuk dilakukan di sini, karena ini adalah halaman CRUD. Jangan ragu untuk mengintip kode ui paket untuk melihat bagaimana penangan untuk masing-masing rute ini telah diterapkan.

Setiap fungsi handler adalah rutin yang:

  • Mengambil data dari datastore dan merespons dengan template yang dirender (menggunakan data yang diambil)
  • Mem-parsing data formulir yang masuk, membuat perubahan yang diperlukan di penyimpanan data dan menggunakan paket kucing untuk membuat ulang file konfigurasi NGINX

Sebagai contoh:

 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) }

Semua fungsi ServeServerNewForm lakukan adalah mengambil penyeimbang dari datastore dan merender template, TplServerList dalam hal ini, yang mengambil daftar server yang relevan menggunakan fungsi Server pada penyeimbang.

Fungsi HandleServerCreate , selain itu, mem-parsing muatan POST yang masuk dari badan menjadi struct dan menggunakan data tersebut untuk membuat instance dan mempertahankan struct Server baru di datastore sebelum menggunakan paket kucing untuk membuat ulang file konfigurasi NGINX untuk penyeimbang.

Semua template halaman disimpan dalam file “ui/templates.go” dan file HTML template yang sesuai dapat ditemukan di bawah direktori “ui/templates”.

Mencobanya

Menyebarkan Loadcat ke server jauh atau bahkan di lingkungan lokal Anda sangat mudah. Jika Anda menjalankan Linux (64bit), Anda dapat mengambil arsip dengan biner Loadcat bawaan dari bagian Rilis repositori. Jika Anda merasa sedikit berjiwa petualang, Anda dapat mengkloning repositori dan mengkompilasi kode sendiri. Meskipun demikian, pengalaman dalam kasus tersebut mungkin sedikit mengecewakan karena mengkompilasi program Go tidak terlalu sulit. Dan jika Anda menjalankan Arch Linux, maka Anda beruntung! Sebuah paket telah dibangun untuk distribusi untuk kenyamanan. Cukup unduh dan instal menggunakan manajer paket Anda. Langkah-langkah yang terlibat diuraikan secara lebih rinci dalam file README.md proyek.

Setelah Anda mengkonfigurasi dan menjalankan Loadcat, arahkan browser web Anda ke "http://localhost:26590" (dengan asumsi itu berjalan secara lokal dan mendengarkan pada port 26590). Selanjutnya, buat penyeimbang, buat beberapa server, pastikan ada sesuatu yang mendengarkan pada port yang ditentukan, dan voila Anda harus memiliki permintaan masuk keseimbangan beban NGINX di antara server yang sedang berjalan.

Apa berikutnya?

Alat ini jauh dari sempurna, dan sebenarnya ini adalah proyek percobaan. Alat ini bahkan tidak mencakup semua fungsi dasar NGINX. Misalnya, jika Anda ingin men-cache aset yang dilayani oleh node back-end pada lapisan NGINX, Anda masih harus memodifikasi file konfigurasi NGINX secara manual. Dan itulah yang membuat hal-hal menarik. Ada banyak hal yang dapat dilakukan di sini dan itulah yang selanjutnya: mencakup lebih banyak lagi fitur penyeimbang beban NGINX - yang dasar dan bahkan mungkin yang ditawarkan NGINX Plus.

Cobalah Loadcat. Lihat kodenya, garpu, ubah, mainkan. Juga, beri tahu kami jika Anda telah membuat alat yang mengonfigurasi perangkat lunak lain atau telah menggunakan yang sangat Anda sukai di bagian komentar di bawah.