golang

Sidecar на Go: позволь другому заниматься твоими проблемами

  • суббота, 26 октября 2024 г. в 00:00:08
https://habr.com/ru/companies/otus/articles/852642/

Привет, Хабр!

В распределённых системах каждая служба выполняет свою задачу: одна отвечает за логи, другая за обработку запросов, третья за безопасность. Но не всегда удобно нагружать основной сервис дополнительной логикой. Именно здесь хорошо вписывается Sidecar — отдельный контейнер или процесс, который берёт на себя часть инфраструктурных задач, разгружая основное приложение и позволяя сосредоточиться на главной бизнес-логике.

Сегодня мы рассмотрим реализацию Sidecar на Golang.

Реализация Sidecar на Go

Что мы будем делать: создадим основной микросервис и рядом с ним 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!

Примеры применения Sidecar

Логирование и мониторинг трафика через Sidecar

Предположим, есть микросервис, который обслуживает 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

Задача: добавить кэширование для часто запрашиваемых данных, но не изменяя логику основного сервиса. Можно юзать 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".