Sidecar на Go: позволь другому заниматься твоими проблемами
- суббота, 26 октября 2024 г. в 00:00:08
Привет, Хабр!
В распределённых системах каждая служба выполняет свою задачу: одна отвечает за логи, другая за обработку запросов, третья за безопасность. Но не всегда удобно нагружать основной сервис дополнительной логикой. Именно здесь хорошо вписывается Sidecar — отдельный контейнер или процесс, который берёт на себя часть инфраструктурных задач, разгружая основное приложение и позволяя сосредоточиться на главной бизнес-логике.
Сегодня мы рассмотрим реализацию Sidecar на Golang.
Что мы будем делать: создадим основной микросервис и рядом с ним Sidecar, который будет отвечать за простую задачу — логировать и проксировать запросы.
Начнём с простого HTTP сервера, который будет слушать на порту 8080 и возвращать простое сообщение.
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Main Service!")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Main Service running on port 8080")
http.ListenAndServe(":8080", nil)
}
Ничего сложного. Это основной сервис, который принимает HTTP запросы и отвечает на них.
Теперь создадим сервис Sidecar. В его обязанности будет входить логирование всех запросов, которые проходят через него, и проксирование на основной сервис.
package main
import (
"io"
"log"
"net/http"
)
func proxyHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Received request: %s %s", r.Method, r.URL.Path)
resp, err := http.Get("http://localhost:8080" + r.URL.Path)
if err != nil {
http.Error(w, "Error in Sidecar", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/", proxyHandler)
log.Println("Sidecar Service running on port 8081")
http.ListenAndServe(":8081", nil)
}
Здесь Sidecar получает запросы на 8081
порт, логирует их и проксирует на основной сервис, который работает на 8080
.
Запустим оба сервиса:
go run main_service.go
и в другой консоли:
go run sidecar_service.go
Теперь, если мы отправим HTTP запрос на localhost:8081
, мы увидим ответ от основного сервиса и запись в логах Sidecar:
curl localhost:8081
# Output: Hello from Main Service!
Предположим, есть микросервис, который обслуживает HTTP-запросы, и нужно добавить логирование всех входящих запросов, но без вмешательства в основной код сервиса. Используем Sidecar для этой задачи.
Основной сервис:
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Main Service!")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Main Service running on port 8080")
http.ListenAndServe(":8080", nil)
}
Sidecar для логирования запросов:
package main
import (
"log"
"net/http"
"io"
)
func proxyHandler(w http.ResponseWriter, r *http.Request) {
// Логируем запросы
log.Printf("Request: %s %s", r.Method, r.URL.Path)
// Прокси запрос на основной сервис
resp, err := http.Get("http://localhost:8080" + r.URL.Path)
if err != nil {
http.Error(w, "Error in Sidecar", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
// Копируем ответ основного сервиса обратно клиенту
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func main() {
http.HandleFunc("/", proxyHandler)
log.Println("Sidecar running on port 8081")
http.ListenAndServe(":8081", nil)
}
В этом примере Sidecar работает как прокси между клиентом и основным сервисом, логируя все запросы перед пересылкой их на основной сервис. Запросы отправляются на порт 8081
, где работает Sidecar, а затем проксируются на основной сервис, который работает на порту 8080
.
Задача: добавить кэширование для часто запрашиваемых данных, но не изменяя логику основного сервиса. Можно юзать Sidecar, который будет кэшировать ответы основного сервиса и выдавать закэшированные данные при повторных запросах.
Основной сервис:
package main
import (
"fmt"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
// Имитация длительной обработки
time.Sleep(2 * time.Second)
fmt.Fprintf(w, "Data from Main Service!")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Main Service running on port 8080")
http.ListenAndServe(":8080", nil)
}
Sidecar для кэширования:
package main
import (
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
)
// Простая структура для хранения кэша
type Cache struct {
data map[string]string
expiry map[string]time.Time
cacheLock sync.RWMutex
}
// Инициализация кэша
var cache = Cache{
data: make(map[string]string),
expiry: make(map[string]time.Time),
}
// Продолжительность хранения данных в кэше
const cacheDuration = 10 * time.Second
// Проверка наличия данных в кэше
func getFromCache(path string) (string, bool) {
cache.cacheLock.RLock()
defer cache.cacheLock.RUnlock()
data, found := cache.data[path]
if !found || time.Now().After(cache.expiry[path]) {
return "", false
}
return data, true
}
// Добавление данных в кэш
func saveToCache(path, response string) {
cache.cacheLock.Lock()
defer cache.cacheLock.Unlock()
cache.data[path] = response
cache.expiry[path] = time.Now().Add(cacheDuration)
}
// Прокси с кэшированием
func proxyHandler(w http.ResponseWriter, r *http.Request) {
// Проверяем кэш
if cachedData, found := getFromCache(r.URL.Path); found {
fmt.Fprintf(w, cachedData)
log.Printf("Served from cache: %s", r.URL.Path)
return
}
// Если в кэше данных нет, делаем запрос на основной сервис
resp, err := http.Get("http://localhost:8080" + r.URL.Path)
if err != nil {
http.Error(w, "Error in Sidecar", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
// Сохраняем в кэш
saveToCache(r.URL.Path, string(body))
// Возвращаем ответ клиенту
w.WriteHeader(resp.StatusCode)
w.Write(body)
}
func main() {
http.HandleFunc("/", proxyHandler)
log.Println("Sidecar with Caching running on port 8081")
http.ListenAndServe(":8081", nil)
}
В этом примере Sidecar кэширует ответы от основного сервиса на 10 секунд. При повторных запросах в течение этого времени клиент получает данные из кэша, а не от основного сервиса.
И помните, главное — не перегружать Sidecar и чётко понимать, где заканчиваются задачи основного сервиса и начинаются обязанности Sidecar.
28 октября пройдет открытый урок «Способы разделения микросервисов на компоненты». На практических примерах будет показано, как правильно структурировать микросервисную архитектуру для улучшения масштабируемости и управляемости систем. В том числе, разберем наиболее эффективные подходы к декомпозиции сервисов на основе доменных моделей и данных. Записаться на урок можно на странице курса "Software Architect".