Впровадження реєстру сервісів у Zookeeper
Фото: taichi nakamura на Unsplash
При створенні веб-сервісу ми можемо мати кілька екземплярів сервісу, ідея полягає в тому, щоб наш додаток міг масштабуватися горизонтально. Клієнт на upstream стороні буде забезпечувати рівномірний виклик downstream сервісів. Ці терміни можуть використовуватися для позначення балансування навантаження.
Існує кілька інструментів для виявлення сервісів, і в цій статті я розповім, як використовувати Zookeeper, використовуючи його концепції Znode та Ephemeral Node, щоб створити власний простий механізм балансування навантаження за допомогою круглого-robin.
Ключові концепції:
- Буде два проекти на Go: Сервіс (go-zookeeper-registry-service) та Клієнт (go-zookeeper-registry-client)
- Сервісний додаток буде мати HTTP-ендпоінт (“/hello”), до якого підключатиметься клієнт
- У сервісному додатку створюється еферентний вузол під попередньо визначеним Znode (go-zookeeper-registry-service), з унікальним ім'ям для кожного запущеного додатку та метаданими, що включають хост і порт, призначений для додатка
- У клієнтському додатку буде перелік усіх дочірніх еферентних вузлів go-zookeeper-registry-service, і клієнт вибиратиме один із них (на основі круглого-robin)
- Клієнт отримає метадані вибраного вузла і підключиться до хоста та порту, зазначених у метаданих вибраного дочірнього вузла. Це працюватиме як механізм балансування навантаження
Попередні вимоги
1.
Запуск сервера Zookeeper
Код
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/samuel/go-zookeeper/zk"
)
func main() {
if len(os.Args) < 2 {
log.Fatalf("Необхідно вказати номер порту як перший аргумент")
}
port, err := strconv.Atoi(os.Args[1])
if err != nil {
log.Fatalf("Невірний номер порту: %v", err)
}
conn := connectToZookeeper()
defer conn.Close()
parentPath := "/go-zookeeper-registry-service"
createParentZnodeIfNotExists(conn, parentPath)
instancePath := createEphemeralZnode(conn, parentPath, port)
serviceName := instancePath[len(parentPath)+1:]
go watchZnode(conn, parentPath)
go startHTTPServer(port, serviceName)
waitForShutdown()
}
func connectToZookeeper() *zk.Conn {
conn, _, err := zk.Connect([]string{"localhost:2181"}, time.Second)
if err != nil {
log.Fatalf("Не вдалося підключитися до Zookeeper: %v", err)
}
return conn
}
func createParentZnodeIfNotExists(conn *zk.Conn, parentPath string) {
exists, _, err := conn.Exists(parentPath)
if err != nil {
log.Fatalf("Не вдалося перевірити наявність znode: %v", err)
}
if !exists {
_, err = conn.Create(parentPath, []byte{}, 0, zk.WorldACL(zk.PermAll))
if err != nil {
log.Fatalf("Не вдалося створити батьківський znode: %v", err)
}
fmt.Printf("Створено батьківський znode: %s\n", parentPath)
}
}
func createEphemeralZnode(conn *zk.Conn, parentPath string, port int) string {
timestamp := time.Now().Unix()
instancePath := fmt.Sprintf("%s/%d", parentPath, timestamp)
data := map[string]interface{}{
"port": port,
"host": "127.0.0.1",
}
dataBytes, err := json.Marshal(data)
if err != nil {
log.Fatalf("Не вдалося перетворити дані в JSON: %v", err)
}
_, err = conn.Create(instancePath, dataBytes, zk.FlagEphemeral, zk.WorldACL(zk.PermAll))
if err != nil {
log.Fatalf("Не вдалося створити еферентний znode: %v", err)
}
fmt.Printf("Створено еферентний znode: %s з даними: %s\n", instancePath, string(dataBytes))
return instancePath
}
func watchZnode(conn *zk.Conn, parentPath string) {
for {
children, _, ch, err := conn.ChildrenW(parentPath)
if err != nil {
log.Printf("Не вдалося відслідковувати znode: %v", err)
time.Sleep(time.Second)
continue
}
fmt.Printf("Поточні znodes: %v\n", children)
event := <-ch
fmt.Printf("Подія znode: %v\n", event)
}
}
func waitForShutdown() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
fmt.Println("Завершення роботи збережено...")
fmt.Println("Сервіс зупинено.")
}
func startHTTPServer(port int, serviceName string) {
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Привіт від сервісу %s", serviceName)
})
addr := fmt.Sprintf(":%d", port)
fmt.Printf("Запуск HTTP сервера на порту %d\n", port)
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatalf("Не вдалося запустити HTTP сервер: %v", err)
}
}
Запустіть це за допомогою цієї команди
> go run .
//наприклад: go run . 8001
Запустіть цей додаток як кілька сервісів, відрізняючи їх за портами
приклад:
// термінал 1
> go run . 8001
// термінал 2
> go run . 8002
// термінал 3
> go run .
8003
Тепер запустіть клієнтський додаток, код виглядатиме так:
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"github.com/samuel/go-zookeeper/zk"
)
const (
zkServers = "127.0.0.1:2181"
znodePath = "/go-zookeeper-registry-service"
sleepDuration = 1 * time.Second
)
type NodeData struct {
Host string `json:"host"`
Port int `json:"port"`
}
func main() {
conn, _, err := zk.Connect([]string{zkServers}, time.Second)
if err != nil {
log.Fatalf("Не вдалося підключитися до Zookeeper: %v", err)
}
defer conn.Close()
var currentIndex int
for {
children, err := getChildren(conn)
if err != nil {
log.Println(err)
time.Sleep(sleepDuration)
continue
}
if len(children) == 0 {
log.Println("Не знайдено жодного вузла в znode")
time.Sleep(sleepDuration)
continue
}
nodeData, err := getNodeData(conn, children[currentIndex%len(children)])
if err != nil {
log.Println(err)
time.Sleep(sleepDuration)
continue
}
if err := makeHTTPRequest(nodeData); err != nil {
log.Println(err)
time.Sleep(sleepDuration)
continue
}
currentIndex++
time.Sleep(sleepDuration)
}
}
func getChildren(conn *zk.Conn) ([]string, error) {
children, _, err := conn.Children(znodePath)
if err != nil {
return nil, fmt.Errorf("не вдалося отримати дочірні елементи znode: %v", err)
}
return children, nil
}
func getNodeData(conn *zk.Conn, node string) (*NodeData, error) {
data, _, err := conn.Get(znodePath + "/" + node)
if err != nil {
return nil, fmt.Errorf("не вдалося отримати дані вузла %s: %v", node, err)
}
var nodeData NodeData
if err := json.Unmarshal(data, &nodeData); err != nil {
return nil, fmt.Errorf("не вдалося розпарсити дані вузла: %v", err)
}
return &nodeData, nil
}
func makeHTTPRequest(nodeData *NodeData) error {
url := fmt.Sprintf("http://127.0.0.1:%d/hello", nodeData.Port)
resp, err := http.Get(url)
if err != nil {
return fmt.Errorf("HTTP запит не вдався: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("не вдалося прочитати тіло відповіді: %v", err)
}
log.Printf("Відповідь від %s: %s", url, body)
return nil
}
> go run .
Цей код буде надсилати запит до HTTP сервісу кожну секунду, і ви побачите, що запити відправляються на різні порти по принципу round-robin.
Перекладено з: Simple Service Discovery In Go with Zookeeper