Load balancers memainkan peran penting dalam arsitektur web. Mereka memungkinkan Anda untuk mendistribusikan beban di beberapa backend, sehingga meningkatkan skalabilitas. Dan karena kami memiliki beberapa backend yang dikonfigurasi, layanan menjadi sangat tersedia, karena jika terjadi kegagalan pada satu server, penyeimbang dapat memilih server lain yang berfungsi.
Setelah bermain dengan penyeimbang profesional seperti NGINX, saya mencoba membuat penyeimbang sederhana untuk bersenang-senang. Saya menulisnya di Go, itu adalah bahasa modern yang mendukung paralelisme penuh. Pustaka standar di Go memiliki banyak fitur dan memungkinkan Anda untuk menulis aplikasi berkinerja tinggi dengan kode lebih sedikit. Selain itu, untuk kemudahan distribusi, ini menghasilkan biner tunggal yang terhubung secara statis.
Cara kerja penyeimbang kami
Algoritma yang berbeda digunakan untuk mendistribusikan beban di antara backend. Sebagai contoh:
- Round Robin - beban didistribusikan secara merata, dengan mempertimbangkan kekuatan komputasi yang sama dari server.
- Robin Round Tertimbang - Tergantung pada kekuatan pemrosesan, server dapat diberi bobot yang berbeda.
- Least Connections - beban didistribusikan di seluruh server dengan paling sedikit koneksi aktif.
Di penyeimbang kami, kami menerapkan algoritma paling sederhana - Round Robin.
Seleksi di Round Robin
Algoritma Round Robin sederhana. Ini memberi semua pemain kesempatan yang sama untuk menyelesaikan tugas.
Pilih server di Round Robin untuk menangani permintaan yang masuk.Seperti yang ditunjukkan dalam ilustrasi, algoritma memilih server dalam lingkaran, secara siklis. Tetapi kita tidak dapat memilihnya secara
langsung , bukan?
Dan jika server berbohong? Kami mungkin tidak perlu mengirimkan lalu lintas ke sana. Artinya, server tidak dapat digunakan secara langsung sampai kami membawanya ke keadaan yang diinginkan. Hal ini diperlukan untuk mengarahkan lalu lintas hanya ke server-server yang aktif dan berjalan.
Tentukan strukturnya
Kita perlu melacak semua detail yang terkait dengan backend. Anda perlu tahu apakah dia masih hidup, dan melacak URL. Untuk melakukan ini, kita dapat mendefinisikan struktur berikut:
type Backend struct { URL *url.URL Alive bool mux sync.RWMutex ReverseProxy *httputil.ReverseProxy }
Jangan khawatir, saya akan menjelaskan arti bidang di Backend.
Sekarang di balancer Anda perlu melacak semua backend. Untuk melakukan ini, Anda bisa menggunakan Slice dan penghitung variabel. Definisikan di ServerPool:
type ServerPool struct { backends []*Backend current uint64 }
Menggunakan ReverseProxy
Seperti yang telah kami tentukan, esensi dari penyeimbang adalah dalam mendistribusikan lalu lintas ke server yang berbeda dan mengembalikan hasil kepada klien. Seperti yang dijelaskan dalam dokumentasi Go:
ReverseProxy adalah penangan HTTP yang menerima permintaan masuk dan mengirimkannya ke server lain, mem-proxy tanggapan kembali ke klien.Tepat seperti yang kita butuhkan. Tidak perlu menemukan kembali roda. Anda cukup mengalirkan permintaan kami melalui
ReverseProxy
.
u, _ := url.Parse("http://localhost:8080") rp := httputil.NewSingleHostReverseProxy(u)
Menggunakan
httputil.NewSingleHostReverseProxy(url)
Anda dapat menginisialisasi
ReverseProxy
, yang akan menyiarkan permintaan ke
url
diteruskan. Dalam contoh di atas, semua permintaan dikirim ke localhost: 8080, dan hasilnya dikirim ke klien.
Jika Anda melihat tanda tangan dari metode ServeHTTP, maka Anda dapat menemukan tanda tangan dari penangan HTTP di dalamnya. Oleh karena itu, Anda dapat meneruskannya ke
HandlerFunc
di
http
.
Contoh lain ada di
dokumentasi .
Untuk penyeimbang kami, Anda dapat memulai
ReverseProxy
dengan
URL
terkait di
Backend
sehingga ReverseProxy merutekan permintaan ke
URL
.
Proses pemilihan server
Selama pemilihan server berikutnya, kita perlu melewati server yang mendasarinya. Tetapi Anda perlu mengatur penghitungan.
Banyak klien akan terhubung ke penyeimbang, dan ketika masing-masing dari mereka meminta node berikutnya untuk mentransfer lalu lintas, kondisi balapan dapat terjadi. Untuk mencegah hal ini, kita dapat memblokir
ServerPool
dengan
mutex
. Tapi itu akan mubazir, selain itu kita tidak ingin memblokir
ServerPool
. Kami hanya perlu menambah penghitung satu per satu.
Solusi terbaik untuk memenuhi persyaratan ini adalah peningkatan atom. Go mendukungnya dengan paket
atomic
.
func (s *ServerPool) NextIndex() int { return int(atomic.AddUint64(&s.current, uint64(1)) % uint64(len(s.backends))) }
Kami secara atomik meningkatkan nilai saat ini dengan satu dan mengembalikan indeks dengan mengubah panjang array. Ini berarti bahwa nilai harus selalu berada pada rentang dari 0 hingga panjang array. Pada akhirnya, kami akan tertarik pada indeks tertentu, bukan seluruh penghitung.
Memilih server langsung
Kami sudah tahu bahwa permintaan kami diputar secara siklis di semua server. Dan kita hanya perlu melewati idle.
GetNext()
selalu mengembalikan nilai mulai dari 0 hingga panjang array. Kapan saja, kita bisa mendapatkan node berikutnya, dan jika tidak aktif, kita perlu mencari lebih jauh melalui array sebagai bagian dari loop.
Kami mengulang melalui array.Seperti yang ditunjukkan dalam ilustrasi, kami ingin beralih dari simpul berikutnya ke akhir daftar. Ini bisa dilakukan menggunakan
next + length
. Tetapi untuk memilih indeks, Anda harus membatasi itu ke panjang array. Ini dapat dengan mudah dilakukan menggunakan operasi modifikasi.
Setelah kami menemukan server yang berfungsi selama pencarian, itu harus ditandai sebagai saat ini:
Menghindari kondisi balapan di struktur Backend
Di sini Anda perlu mengingat masalah penting. Struktur
Backend
berisi variabel yang beberapa goroutine dapat modifikasi atau kueri secara bersamaan.
Kita tahu bahwa goroutine akan membaca variabel lebih dari menulis padanya. Oleh karena itu, untuk membuat serial akses ke
Alive
kami memilih
RWMutex
.
Menyeimbangkan permintaan
Sekarang kita dapat merumuskan metode sederhana untuk menyeimbangkan permintaan kita. Ini akan gagal hanya jika semua server jatuh.
Metode ini dapat diteruskan ke server HTTP hanya sebagai
HandlerFunc
.
server := http.Server{ Addr: fmt.Sprintf(":%d", port), Handler: http.HandlerFunc(lb), }
Kami merutekan lalu lintas hanya ke server yang berjalan
Penyeimbang kami memiliki masalah serius. Kami tidak tahu apakah server sedang berjalan. Untuk mengetahuinya, Anda perlu memeriksa server. Ada dua cara untuk melakukan ini:
- Aktif: menjalankan permintaan saat ini, kami menemukan bahwa server yang dipilih tidak merespons, dan menandainya sebagai siaga.
- Pasif: Anda dapat melakukan ping server pada beberapa interval dan memeriksa status.
Secara aktif memeriksa server yang sedang berjalan
Jika ada kesalahan
ReverseProxy
memulai fungsi callback
ErrorHandler
. Ini dapat digunakan untuk mendeteksi kegagalan:
proxy.ErrorHandler = func(writer http.ResponseWriter, request *http.Request, e error) { log.Printf("[%s] %s\n", serverUrl.Host, e.Error()) retries := GetRetryFromContext(request) if retries < 3 { select { case <-time.After(10 * time.Millisecond): ctx := context.WithValue(request.Context(), Retry, retries+1) proxy.ServeHTTP(writer, request.WithContext(ctx)) } return }
Dalam mengembangkan penangan kesalahan ini, kami menggunakan kemampuan penutupan. Ini memungkinkan kami untuk menangkap variabel eksternal seperti URL server ke dalam metode kami. Pawang memeriksa retry counter, dan jika kurang dari 3, maka kami kembali mengirim permintaan yang sama ke server yang sama. Ini karena, karena kesalahan sementara, server dapat membatalkan permintaan kami, tetapi segera menjadi tersedia (server mungkin tidak memiliki soket gratis untuk klien baru). Jadi, Anda perlu mengatur timer tunda untuk upaya baru setelah sekitar 10 ms. Dengan setiap permintaan, kami meningkatkan upaya balasan.
Setelah kegagalan setiap upaya, kami menandai server sebagai siaga.
Sekarang Anda perlu menetapkan server baru untuk permintaan yang sama. Kami akan melakukan ini menggunakan penghitung upaya menggunakan paket
context
. Setelah meningkatkan penghitungan upaya, kami meneruskannya ke
lb
untuk memilih server baru untuk memproses permintaan.
Kami tidak dapat melakukan ini tanpa batas, jadi kami akan memeriksa di
lb
apakah jumlah upaya maksimum telah tercapai sebelum melanjutkan dengan pemrosesan permintaan.
Anda bisa mendapatkan counter upaya dari permintaan, jika itu mencapai maksimum, maka kami mengganggu permintaan.
Ini adalah implementasi rekursif.
Menggunakan paket konteks
Paket
context
memungkinkan Anda untuk menyimpan data yang berguna dalam permintaan HTTP. Kami akan secara aktif menggunakan ini untuk melacak data yang terkait dengan permintaan -
Attempt
dan
Retry
penghitung.
Pertama, Anda perlu mengatur kunci untuk konteksnya. Disarankan untuk tidak menggunakan string, tetapi nilai numerik yang unik. Go memiliki kata kunci
iota
untuk implementasi konstanta tambahan, yang masing-masing berisi nilai unik. Ini adalah solusi yang bagus untuk mendefinisikan kunci numerik.
const ( Attempts int = iota Retry )
Anda kemudian dapat mengekstrak nilainya, seperti yang biasa kita lakukan dengan
HashMap
. Nilai default dapat bergantung pada situasi saat ini.
Validasi Server Pasif
Pemeriksaan pasif mengidentifikasi dan memulihkan server yang jatuh. Kami melakukan ping pada interval tertentu untuk menentukan status mereka.
Untuk melakukan ping, cobalah membuat koneksi TCP. Jika server merespons, kami menandainya berfungsi. Metode ini dapat diadaptasi untuk memanggil titik akhir tertentu seperti
/status
. Pastikan untuk menutup koneksi setelah dibuat untuk mengurangi beban tambahan di server. Jika tidak, ia akan mencoba mempertahankan koneksi ini dan pada akhirnya akan menghabiskan sumber dayanya.
Sekarang Anda dapat mengulangi server dan menandai statusnya:
Untuk menjalankan kode ini secara berkala, Anda dapat menjalankan timer di Go. Ini akan memungkinkan Anda untuk mendengarkan acara di saluran.
Dalam kode ini, saluran
<-tC
akan mengembalikan nilai setiap 20 detik.
select
memungkinkan Anda untuk menentukan acara ini. Dengan tidak adanya situasi
default
, menunggu sampai setidaknya satu kasus dapat dieksekusi.
Sekarang jalankan kodenya di goroutine terpisah:
go healthCheck()
Kesimpulan
Dalam artikel ini, kami memeriksa banyak pertanyaan:
- Algoritma Round Robin
- ReverseProxy dari perpustakaan standar
- Mutex
- Operasi atom
- Sirkuit pendek
- Telepon balik
- Operasi pemilihan
Ada banyak cara untuk meningkatkan penyeimbang kita. Sebagai contoh:
- Gunakan heap untuk mengurutkan server langsung untuk mengurangi cakupan pencarian.
- Kumpulkan statistik.
- Menerapkan algoritma round-robin tertimbang dengan jumlah koneksi paling sedikit.
- Tambahkan dukungan untuk file konfigurasi.
Dan sebagainya.
Kode sumber ada di
sini .