Просте виявлення сервісів у Go з використанням Zookeeper

Впровадження реєстру сервісів у Zookeeper

pic

Фото: taichi nakamura на Unsplash

При створенні веб-сервісу ми можемо мати кілька екземплярів сервісу, ідея полягає в тому, щоб наш додаток міг масштабуватися горизонтально. Клієнт на upstream стороні буде забезпечувати рівномірний виклик downstream сервісів. Ці терміни можуть використовуватися для позначення балансування навантаження.

Існує кілька інструментів для виявлення сервісів, і в цій статті я розповім, як використовувати Zookeeper, використовуючи його концепції Znode та Ephemeral Node, щоб створити власний простий механізм балансування навантаження за допомогою круглого-robin.

Ключові концепції:

  1. Буде два проекти на Go: Сервіс (go-zookeeper-registry-service) та Клієнт (go-zookeeper-registry-client)
  2. Сервісний додаток буде мати HTTP-ендпоінт (“/hello”), до якого підключатиметься клієнт
  3. У сервісному додатку створюється еферентний вузол під попередньо визначеним Znode (go-zookeeper-registry-service), з унікальним ім'ям для кожного запущеного додатку та метаданими, що включають хост і порт, призначений для додатка
  4. У клієнтському додатку буде перелік усіх дочірніх еферентних вузлів go-zookeeper-registry-service, і клієнт вибиратиме один із них (на основі круглого-robin)
  5. Клієнт отримає метадані вибраного вузла і підключиться до хоста та порту, зазначених у метаданих вибраного дочірнього вузла. Це працюватиме як механізм балансування навантаження

Попередні вимоги

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.

pic

Перекладено з: Simple Service Discovery In Go with Zookeeper

Leave a Reply

Your email address will not be published. Required fields are marked *