Обзор архитектуры Tonica
Этот документ объясняет внутреннюю архитектуру и принципы проектирования фреймворка Tonica.
Философия дизайна
Tonica построен на нескольких основных принципах:
- Proto-First: API определяются с использованием Protocol Buffers, обеспечивая типобезопасность и контрактный дизайн
- Соглашения вместо конфигурации: Разумные значения по умолчанию с возможностями для настройки
- Модульность: Используйте только то, что нужно - запускайте как единый сервис или отдельные компоненты
- Наблюдаемость по умолчанию: Встроенные метрики, трассировка и логирование
- Готовность к production: Корректное завершение, проверки работоспособности и обработка ошибок из коробки
Высокоуровневая архитектура
Основные компоненты
1. Ядро приложения (App)
Структура App - это сердце Tonica. Она управляет жизненным циклом приложения и координирует все компоненты.
Обязанности:
- Инициализация и настройка всех подсистем
- Управление реестром компонентов
- Координация корректного завершения
- Предоставление доступа к общим ресурсам (логгер, метрики и т.д.)
Ключевые поля:
type App struct {
Name string
cfg *config.Config
logger *log.Logger // Standard logger
router *gin.Engine // HTTP router
metricRouter *gin.Engine // Metrics endpoint
metricsManager *metrics.Manager
registry *Registry
shutdown *Shutdown
customRoutes []RouteMetadata // Custom route definitions
}
Жизненный цикл:
NewApp() → Configure() → RegisterComponents() → Run() → Shutdown()
2. Реестр (Registry)
Реестр - это центральный репозиторий для всех компонентов приложения (сервисы, воркеры, консьюмеры).
Назначение:
- Сохранение и получение сервисов по имени
- Сохранение и получение воркеров по имени
- Сохранение и получение консьюмеров по имени
- Предотвращение дублирующихся регистраций
- Обеспечение обнаружения компонентов во время запуска
Реализация:
type Registry struct {
services map[string]*Service
workers map[string]*Worker
consumers map[string]*Consumer
mu sync.RWMutex // Thread-safe access
}
Потокобезопасность: Все операции реестра защищены мьютексом чтения-записи, что позволяет безопасный конкурентный доступ.
3. Слой сервисов
Сервисы обрабатывают gRPC и REST API запросы.
Структура сервиса:
type Service struct {
name string
grpcServer *grpc.Server
db *DB
redis *Redis
}
Поток запросов:
Возможности:
- Автоматическая генерация REST API через gRPC-Gateway
- Генерация OpenAPI спецификации из proto аннотаций
- Встроенный middleware (CORS, логирование, метрики)
- Поддержка пользовательских маршрутов
4. Слой воркеров
Воркеры обрабатывают фоновые задачи с использованием Temporal.
Структура воркера:
type Worker struct {
name string
taskQueue string
client client.Client
worker worker.Worker
}
Поток выполнения:
Возможности:
- Регистрация активностей
- Регистрация workflow
- Автоматические повторные попытки и обработка ошибок
- Интеграция распределенной трассировки
5. Слой консьюмеров
Консьюмеры обрабатывают сообщения из Kafka, PubSub или других очередей сообщений.
Структура консьюмера:
type Consumer struct {
name string
topic string
consumerGroup string
client storage.Client
handler func(context.Context, *pubsub.Message) error
}
Поток обработки:
Возможности:
- Отмена на основе контекста
- Автоматическая обработка ошибок и логирование
- Корректное завершение (ожидает завершения обработки сообщения)
- Поддержка групп консьюмеров
6. Инфраструктурный слой
База данных (Bun ORM)
Tonica использует Bun в качестве ORM, поддерживая PostgreSQL, MySQL и SQLite.
Управление соединениями:
type DB struct {
driver string
dsn string
db *bun.DB // Cached connection
}
func (d *DB) GetClient() *bun.DB {
if d.db != nil {
return d.db // Return cached connection
}
// Create new connection
// ...
}
Возможности:
- Пулинг соединений
- Автоматическое определение драйвера
- Поддержка миграций (через Bun)
- Построитель запросов
Redis
Обертка клиента Redis с кэшированием соединений.
Управление соединениями:
type Redis struct {
addr string
password string
database int
conn *redis.Client // Cached connection
}
func (r *Redis) GetClient() *redis.Client {
if r.conn == nil {
r.conn = redis.NewClient(&redis.Options{
Addr: r.addr,
Password: r.password,
DB: r.database,
})
}
return r.conn
}
Варианты использования:
- Кэширование
- Хранение сессий
- Ограничение скорости
- Распределенные блокировки
7. Слой наблюдаемости
Трассировка OpenTelemetry
Автоматическая распределенная трассировка для всех запросов.
Точки инструментации:
- HTTP запросы (входящие/исходящие)
- gRPC вызовы (входящие/исходящие)
- Запросы к базе данных
- Операции Redis
- Пользовательские span'ы (определяемые пользователем)
Конфигурация:
OTEL_ENABLED=true
OTEL_ENDPOINT=localhost:4317
OTEL_SERVICE_NAME=myservice
Метрики Prometheus
Встроенный сбор и экспорт метрик.
Метрики по умолчанию:
app_info- Метаданные приложения- Метрики HTTP запросов (длительность, количество, статус)
- Метрики gRPC запросов
- Метрики Go runtime (горутины, память, GC)
Пользовательские метрики:
counter := app.GetMetricManager().NewCounter("orders_total", "Total orders")
gauge := app.GetMetricManager().NewGauge("active_connections", "Active connections")
histogram := app.GetMetricManager().NewHistogram("request_duration", "Request duration")
Эндпоинт метрик:
http://localhost:9090/metrics
Структурированное логирование
Использует стандартный log/slog Go для структурированного логирования.
Уровни логов:
DEBUG- Детальная отладочная информацияINFO- Общие информационные сообщенияWARN- Предупреждающие сообщенияERROR- Сообщения об ошибках
Использование:
import "log/slog"
// Use slog for structured logging
slog.Info("User created", "user_id", userID, "email", email)
slog.Error("Failed to process order", "order_id", orderID, "error", err)
// Or use app's logger (*log.Logger)
app.GetLogger().Printf("User created: %s", userID)
8. Координатор завершения
Координатор завершения обеспечивает корректное завершение работы всех компонентов.
Поток завершения:
Регистрация:
// Register HTTP server
app.shutdown.RegisterHTTPServer(httpServer)
// Register gRPC server
app.shutdown.RegisterGRPCServer(grpcServer)
// Register cleanup function
app.shutdown.RegisterCleanup(func(ctx context.Context) error {
// Close database connections, flush metrics, etc.
return db.Close()
})
Возможности:
- Параллельное завершение (быстро)
- Защита по таймауту (без зависаний)
- Агрегация ошибок
- Потокобезопасная регистрация
Режимы запуска
Tonica поддерживает четыре режима запуска, каждый из которых активирует разные компоненты:
ModeAio (Все-в-одном)
Запускает всё: gRPC + REST + Воркеры + Консьюмеры
ModeService
Запускает только gRPC и REST API
ModeWorker
Запускает только Temporal воркеры (+ метрики)
ModeConsumer
Запускает только консьюмеры сообщений (+ метрики)
См. Режимы запуска для детального сравнения.
Примеры потоков запросов
gRPC запрос
REST запрос (сгенерированный из Proto)
REST запрос (пользовательский маршрут)
Обработка задач воркером
Обработка сообщений консьюмером
Управление конфигурацией
Конфигурация следует этому порядку приоритетов (от высшего к низшему):
- Код - Опции, переданные в конструкторы
- Переменные окружения -
APP_NAME,DB_DSNи т.д. - Значения по умолчанию - Встроенные разумные значения
Пример:
// 1. Code (highest priority)
app := tonica.NewApp(tonica.WithName("myservice"))
// 2. Environment variable
// export APP_NAME="envservice"
// 3. Default (lowest priority)
// Falls back to "tonica-app" if nothing specified
См. Конфигурация для всех опций.
Стратегия обработки ошибок
Tonica использует многоуровневый подход к обработке ошибок:
Уровень 1: Ошибки приложения
if err != nil {
return nil, status.Error(codes.InvalidArgument, "invalid user ID")
}
Уровень 2: Ошибки middleware
Автоматически обрабатываются фреймворком:
- HTTP 500 для внутренних ошибок
- HTTP 4xx для ошибок клиента
- Логируются с контекстом
Уровень 3: Ошибки инфраструктуры
База данных, Redis и т.д.:
import "log/slog"
user, err := db.GetUser(ctx, userID)
if err != nil {
slog.Error("Failed to get user", "error", err, "user_id", userID)
return nil, status.Error(codes.Internal, "database error")
}
Уровень 4: Восстановление после паники
Автоматическое восстановление после паники в HTTP/gRPC обработчиках:
- Логирует панику со стеком вызовов
- Возвращает ошибку 500 клиенту
- Не приводит к падению приложения
Модель конкурентности
Гарантии потокобезопасности
App:
- Безопасно вызывать геттеры конкурентно
- Доступ к реестру защищен мьютексом
- Менеджер метрик потокобезопасен
Registry:
- Все операции используют RWMutex
- Разрешено несколько читателей
- Эксклюзивная блокировка на запись
Services/Workers/Consumers:
- Каждый запрос/задача/сообщение в отдельной горутине
- Нет общего изменяемого состояния (если не спроектировано явно)
- Отмена на основе контекста
Управление горутинами
HTTP сервер:
- Одна горутина на запрос
- Автоматически очищается после ответа
gRPC сервер:
- Одна горутина на поток
- Очищается при закрытии потока
Воркеры:
- Настраиваемое количество конкурентных активностей
- Управляются Temporal воркером
Консьюмеры:
- Одна горутина на консьюмер
- Обработка сообщений может быть последовательной или параллельной (выбор пользователя)
Соображения по производительности
Пулинг соединений
База данных:
// Bun handles connection pooling internally
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
Redis:
// Redis client has built-in connection pool
// Configured via redis.Options
&redis.Options{
PoolSize: 10,
MinIdleConns: 5,
}
Стратегия кэширования
- Уровень приложения - Кэши в памяти в вашем коде
- Redis - Распределенный кэш для многоэкземплярных развертываний
- База данных - Кэширование результатов запросов через Bun
Накладные расходы метрик
Сбор метрик легковесен:
- Инкремент счетчика: ~100нс
- Наблюдение гистограммы: ~500нс
- Незначительное влияние на производительность
Соображения безопасности
Аутентификация и авторизация
Не встроена - реализуйте как middleware:
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if !validateToken(token) {
c.AbortWithStatus(401)
return
}
c.Next()
}
}
app.GetRouter().Use(AuthMiddleware())
CORS
Встроенная поддержка CORS:
# Allow all origins (default)
# No configuration needed
# Restrict origins
export APP_CORS_ORIGINS="https://myapp.com,https://api.myapp.com"
TLS/HTTPS
Настройка на уровне развертывания:
- Используйте обратный прокси (nginx, Traefik) с терминацией TLS
- Или настройте TLS непосредственно в Gin роутере
Расширение Tonica
Пользовательский Middleware
HTTP Middleware:
app.GetRouter().Use(MyCustomMiddleware())
gRPC Interceptors:
grpcServer := grpc.NewServer(
grpc.UnaryInterceptor(MyInterceptor),
)
Пользовательские метрики
myCounter := app.GetMetricManager().NewCounter("my_metric", "Description")
myCounter.Inc()
Пользовательские проверки работоспособности
tonica.NewRoute(app).
GET("/health/detailed").
Handle(func(c *gin.Context) {
health := map[string]string{
"database": checkDB(),
"redis": checkRedis(),
"workers": checkWorkers(),
}
c.JSON(200, health)
})
Следующие шаги
- Режимы запуска - Узнайте, когда использовать каждый режим
- Конфигурация - Настройте ваше приложение
- Тестирование - Пишите тесты для ваших сервисов
- Лучшие практики - Паттерны для production