golang

Проверка готовности приложения к работе в реальном ненадежном мире. Часть 5

  • суббота, 16 ноября 2024 г. в 00:00:09
https://habr.com/ru/companies/slurm/articles/858702/

Пятая и заключительная часть статьи, в которой Виталий Лихачёв, SRE в booking.com и спикер курса Слёрма «Golang-разработчик» рассказывает, о чём стоит подумать перед выкаткой сервиса в жестокий прод, где он может не справиться с нагрузкой или деградировать из-за резких всплесков при наплыве пользователей и по вечерам.

Статья состоит из 5 частей, которые выходят по очереди:

1. Надежность.

2. Масштабируемость/отказоустойчивость.

3. Resiliency/отказоустойчивость.

4. Безопасность. Процесс разработки. Процесс выкатки.

5. Наблюдаемость. Архитектура. Антипаттерны.

Наблюдаемость

Ключевые метрики

На уровне хостов, инфраструктуры, сервисов обязательно должны быть минимально достаточные метрики, позволяющие понять, как работает система.

И важно реализовать не только железные метрики (cpu, ram, rps), но и бизнес-метрики (кол-во заказов в минуту, кол-во смен пароля за час и т.д.), которые на уровне технических метрик не приносят понимания что происходит в системе с точки зрения конечного пользователя.

Стандартизация

Единообразные наименования метрик для любого сервиса. Зависит от той системы, что вы используете, потому что graphite и prometheus, например, используют разные форматы для метрик.

Посмотрим пример для graphite.

Структурированные имена:
Используйте иерархическую структуру имен, которая описывает метрику.

Например: service_name.metric_type.metric_name.
Разделители: Используйте точки (.) или подчеркивания (_) в качестве разделителей, чтобы улучшить читаемость.
Уникальность: Убедитесь, что каждое имя метрики уникально, чтобы избежать путаницы. Указание единиц измерения: Если это применимо, указывайте единицы измерения в имени метрики (например, response_time_ms).
Использование префиксов и суффиксов: Для группировки метрик используйте префиксы (например, http. для всех метрик, связанных с HTTP) или суффиксы (например, _count, _sum, _avg).

Алерты

Каждый алерт имеет четко описанную реакцию для дежурных. Алерт, для которого нет действий, которые можно выполнить, бесполезен и приводит к alert fatique. Его стоит удалить либо описать, что делать в случае такого алерта, чтобы решить проблему.

Визуализация

Дашборды понятны и четко выделяют важные метрики. Легко понять взаимосвязь метрик. Можно понять корреляцию между разными метриками. Метрик не слишком много, не слишком мало. Есть общий дашборд, показывающий важные бизнес метрики и более детальные дашборды для технических метрик.

Логирование

Динамическое логирование. Возможно включить более детальные логи на проде на лету. Возможно включить логи для определенных условий.

Примитивный пример как это можно делать. Конечно же должна быть более продуманная система, как это настраивать для нескольких реплик, для множества сервисов и в идеале через некий внутренний admin UI.

package main

import (
	"fmt"
	"net/http"
	"sync"
	"time"

	log "github.com/sirupsen/logrus"
)

// Логгер
var (
	mu       sync.Mutex
	logLevel log.Level = log.InfoLevel
)

func init() {
	// Установка формата логирования
	log.SetFormatter(&log.TextFormatter{
		FullTimestamp: true,
	})
	log.SetLevel(logLevel)
}

// HTTP-обработчик для изменения уровня логирования
func logLevelHandler(w http.ResponseWriter, r *http.Request) {
	level := r.URL.Query().Get("level")
	if level == "" {
		http.Error(w, "Level is required", http.StatusBadRequest)
		return
	}

	// Изменение уровня логирования
	mu.Lock()
	defer mu.Unlock()

	newLevel, err := log.ParseLevel(level)
	if err != nil {
		http.Error(w, "Invalid log level", http.StatusBadRequest)
		return
	}

	logLevel = newLevel
	log.SetLevel(logLevel)
	fmt.Fprintf(w, "Log level changed to %s", logLevel)
}

func main() {
	// Пример логирования
	log.Info("Starting application...")

	go func() {
		for {
			time.Sleep(time.Second)
			log.Info("info")
			log.Debug("debug")
		}
	}()

	// Настройка HTTP-сервера
	http.HandleFunc("/set-log-level", logLevelHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Наибольшую пользу от такой динамической настройки можно получить, если вы, например, заранее знаете, что хочется посмотреть флоу запроса, зная userId, который обращается в сервис. В таком случае, конечно же, придется писать свой обработчик, который, к примеру, получает контекст, достает из контекста userId и проверяет, нужно ли для него логировать на debug уровне.

Архитектура

Архитектурная диаграмма

Ссылки на другую документацию, графики, схемы и так далее. Всё, что поможет человеку со стороны быстро объяснить устройство сервиса. Примеры и больше деталей можно почитать у AWS.

Процесс внесения архитектурных изменений

Как только у вас есть важная часть системы, которая работает под нагрузкой и вы хотите внести новую фичу, может оказаться, что непродуманная выкатка функции в прод покажет, что нагрузку она не держит. В этом случае фича должна проходить через архитектурный комитет и написание дизайн документа с учетом ожидаемых нагрузок, причём не только сейчас, но и в будущем.

Подробнее

И как писать дизайн документы

Антипаттерны

БД не делится между сервисами

Классическая проблема разделения монолита на сервисы. Бывает сложность выделить зоны ответственности и так далее. Универсального решения нет, но главное — сервис один и БД принадлежит только ему. Конечно, у одного сервиса может быть несколько БД: например, PostgreSQL, Redis и ElasticSearch одновременно.

Разберем пример: почему бы такую простую сущность, как номер телефона пользователя на площадке объявлений не хранить в сущности самого пользователя в условном сервисе users?

Вокруг телефонов может быть построено (и будет построено) много логики:

  • Валидация принадлежности телефона пользователю

  • Инвалидация телефонов в случае взлома

  • Добавление нескольких телефонов в одну учетную запись

  • Хранение истории статусов номера телефона/аудит изменений номеров в общем смысле

  • Недопущение использования телефона из одной учетной записи в другой учетной записи

  • Разные типы телефонов (мобильные/домашние/etc.) и разные политики работы с ними (смс на домашний отправить нельзя)

  • Поддержка использования одного номера несколькими учетными записями

  • Миграция номеров с одной учетной записи в другую

  • И так далее

Список далеко не полный и полностью матчится с реальными кейсами, которые нужно реализовывать, поддерживать и т.д. В масштабах сотен миллионов пользователей одна такая БД вполне может занимать многие сотни гигабайт.

Правило одного хопа

Сервис не должен вызывать другие сервисы без необходимости. Если сервис сильно зависит от других сервисов, возможно архитектуру стоит пересмотреть и объединить сервисы в один. На бумаге это просто, но объединение сервисов в реальности может оказаться работой на многие месяцы.

Конечно же, антипаттернов больше, но здесь мы остановимся, спасибо что дочитали до конца. Надеюсь, было полезно.


Повысить навыки разработки на Go и собрать полноценный сервис для портфолио можно на курсе «Golang-разработчик».