16
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Eureka, Config, Circuit Breaker: 3 причины, почему я сплю

Время чтения 42 минуты
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
16
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

«Микросервисы — это когда сервер падает не целиком, а по частям. И ты должен угадать, какая часть упала сегодня».
Из разговора с коллегой в курилке, 3 утра, продакшен лежит

Eureka, Config, Circuit Breaker: 3 причины, почему я сплю

Как сервисы не теряют друг друга в потемках

Раньше я думал, что микросервисы — это когда всё работает. Теперь я знаю: это когда всё делает вид, что работает, а ты проверяешь логи в три часа ночи.

У меня три сервиса: Auth, Gateway и Backend. Каждый крутится на своем порту. Backend знал, что Auth живет на 8082. Он помнил это как номер телефона, который вбит прямо в пальцы. И всё было бы нормально, пока я не подумал: а что будет, если поднять вторую реплику Auth?

Вот тут и начнется самое интересное. Коннекты будут рваться. Тайм-ауты. Backend будет стучаться по старому адресу, а там только одна реплика или вообще никто не живет. Я буду сидеть и смотреть в монитор, как смотритель зоопарка, у которого разбежались звери.

Требуется решение. И… да… меня осенило! Эврика! Ну, то есть Eureka, или телефонный справочник для сервисов.

Хардкодить адреса в конфигах — это как писать номер телефона на руке перед встречей: вспотел — и всё поплыло. Auth может переехать на другой порт, или мы можем распилить его на два сервиса. И что тогда, писать в конфигах «второй Auth в подвале, пароль — „помидоры“»?

Netflix придумал Eureka как раз для такого. Это одна точка, где все регистрируются при старте, как в домовой книге. Каждый сервис при запуске говорит: «Я здесь, мой порт такой-то», — а Eureka записывает.

Как это выглядит в коде. Добавляем модуль discovery. Внутри — три строчки:


java
@SpringBootApplication
@EnableEurekaServer
public class DiscoveryApplication {
public static void main(String... args) {
     SpringApplication.run(DiscoveryApplication.class, args);
}
}

 

И минимальный application.yml для сервера:


yaml
server:
  port: 8761
 
eureka:
  client:
register-with-eureka: false  # сервер не регистрирует сам себя
    fetch-registry: false
  server:
enable-self-preservation: false  # для разработки
eviction-interval-timer-in-ms: 5000  # проверяет умерших каждые 5 секунд

 

Всё. Теперь это центр. Висит на порту 8761 и знает, кто жив, а кто притворяется.

А теперь как подключается клиент. В pom.xml любого сервиса:

xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

 

И в его application.yml:


yaml
spring:
  application:
name: ollama-backend  # это имя будет видно в Eureka
 
eureka:
  client:
service-url:
   defaultZone: http://localhost:8761/eureka/
  instance:
lease-renewal-interval-in-seconds: 10   # говорит "я жив" каждые 10 сек.
lease-expiration-duration-in-seconds: 30 # если 30 сек. тишины — я умер
prefer-ip-address: true

 

Раньше Gateway обращался к Backend напрямую: uri: http://localhost:8080. Теперь он пишет так:


yaml
spring:
  cloud:
gateway:
   routes:
     - id: backend_route
       uri: lb://ollama-backend 
       predicates:
         - Path=/api/generate/**
       filters:
         - StripPrefix=1
         - name: Retry
           args:
             retries: 3
             statuses: INTERNAL_SERVER_ERROR

 

lb:// значит «посмотри в Eureka, кто такой ollama-backend, и дай мне его адрес». Если сервисов три, Gateway сам раскидает запросы между ними. Ни строчки кода для балансировки. Просто дописываешь конфиг и сидишь, моргаешь.

Куда прятать конфиги, чтобы не сойти с ума

Дальше всплыла вторая проблема: конфиги. У каждого сервиса — свой application.yml: пароли, ключи, тайм-ауты. Когда сервисов три — еще ладно. Когда пять — уже тошнит. А когда на проде пароль один, на стейджинге другой, а локально вообще третий?

Можно коммитить и раскатывать разные ветки или открыть три терминала и править конфиги руками. Но это бесит.

Решение — Config Server. Еще один модуль (или микросервис, кому как нравится):


java
@SpringBootApplication
@EnableConfigServer
public class ConfigApplication {
public static void main(String[] args) {
     SpringApplication.run(ConfigApplication.class, args);
}
}

 

И его application.yml:


yaml
server:
  port: 8888
 
spring:
  cloud:
config:
   server:
     git:
       uri: https://github.com/yourcompany/ollama-configs
       default-label: main
       search-paths: '{application}'  # ищет в папке с именем сервиса
 
encrypt:
  key: my-super-secret-key-32-chars-long!!!  # для шифрования паролей

 

Теперь конфиги лежат в отдельном Git-репозитории — просто папка ollama-configs с файлами backend.yml, auth.yml, gateway.yml. Сервисы при старте идут туда и читают свои настройки из Git.

Как клиент тянет конфиги. В каждом сервисе появляется bootstrap.yml (грузится до application.yml):


yaml
spring:
  application:
name: ollama-backend
  cloud:
config:
   uri: http://localhost:8888
   fail-fast: true   # если Config Server не отвечает — сервис даже не стартует
      retry:
     max-attempts: 6
     initial-interval: 1000

 

А в Git-репозитории лежит, например, ollama-backend.yml:


yaml
ollama:
  url: http://ollama:11434
  timeout: 30000
  model: llama3.2
 
postgres:
  url: jdbc:postgresql://db:5432/ollama
  username: ollama_user
  password: '{cipher}7a8b3c9d...'  # зашифрованный пароль

Пароли шифруются простой командой:


bash
curl localhost:8888/encrypt -d POSTGRES_PASSWORD
# вернет: 7a8b3c9d...

 

И самое крутое: если меняешь конфиг в Git и дергаешь /actuator/refresh, сервис подхватывает изменение сам, без перезагрузки и пересборки контейнера.


bash
curl -X POST http://localhost:8090/actuator/refresh

 

Ответ покажет, что именно изменилось:


json
["ollama.timeout", "postgres.url"]

 

Поменял тайм-аут и ключ шифрования на лету. Система даже не икнула.

Можно еще прикрутить Spring Cloud Bus с RabbitMQ — тогда изменения разлетаются по сервисам сами. Достаточно добавить зависимость и дернуть /actuator/busrefresh на любом сервисе — и все подтянут новые конфиги. Но это уже для полного дзена.

А если нейросеть зависла? (а она зависнет)

Теперь о грустном. Ollama — штука непредсказуемая. Может работать, может не работать. Может на вопрос «Сколько будет 2 + 2?» думать 40 секунд и выдать инструкцию по сборке велосипеда, причем половина ответа будет написана иероглифами.

Когда она зависает, всё встает. Пользователь жмет Enter, запрос висит, поток залипает. Двести таких потоков — и процессор уже дымится. Это называется каскадный отказ: одна часть легла — и за ней посыпались остальные.

Circuit Breaker решает эту проблему. Идея старая, из электрики: если ток слишком сильный, цепь размыкается. У нас ток — это ошибки.

Как это пишется в коде. Добавляем зависимость:


xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

 

Оборачиваем вызов к Ollama:


java
@Service
@Slf4j
public class OllamaService {
 
@CircuitBreaker(
     name = "ollama",
     fallbackMethod = "handleOllamaFailure"
)
public String generate(String prompt) {
     var response = restTemplate.postForObject(
         "http://localhost:11434/api/generate",
         request,
         OllamaResponse.class
     );
     return response.getResponse();
}
public String handleOllamaFailure(String prompt, Throwable t) {
     log.error("Circuit breaker сработал: {}", t.getMessage());
    
     return switch (t.getClass().getSimpleName()) {
         case "TimeoutException" -> " Модель думает слишком долго. Попробуйте проще.";
         case "HttpServerErrorException" -> "🔌 У Ollama внутренняя ошибка. Уже чиним.";
         case "ResourceAccessException" -> "🌐 Ollama не отвечает. Проверьте сервер.";
         default -> "🤖 Нейросеть временно недоступна. Загляните через минуту.";
        };
}
}

 

И конфиг:


yaml
resilience4j:
  circuitbreaker:
instances:
   ollama:
     sliding-window-type: COUNT_BASED
     sliding-window-size: 10
     minimum-number-of-calls: 5
     failure-rate-threshold: 50
     wait-duration-in-open-state: 15s
     permitted-number-of-calls-in-half-open-state: 3
     slow-call-duration-threshold: 30s
     slow-call-rate-threshold: 70
     record-exceptions:
       - org.springframework.web.client.HttpServerErrorException
       - java.net.SocketTimeoutException

 

Работает так: смотрим на последние запросы. Если половина провалилась, цепь рвется на 10 секунд (или на 15, как в примере выше). Никто даже не пытается дозвониться до Ollama. Сразу уходят в fallback. Пользователь видит вежливое сообщение вместо тайм-аута. Сервер не падает.

Кроме того, можно посмотреть, в каком состоянии Circuit Breaker прямо сейчас:


bash
# Текущее состояние
curl http://localhost:8080/actuator/health
 
# Все события переключения
curl http://localhost:8080/actuator/circuitbreakerevents/STATE_TRANSITION

 

Когда всё сломалось, в /health придет:


json
{
  "status": "UP",
  "components": {
"circuitBreakers": {
   "status": "UP",
   "details": {
     "ollama": "OPEN"  // цепь разомкнута
   }
    }
  }
}

Я сплю.

Схема

Вот что должно получиться:


text
┌─────────┐  ┌─────────────────┐
│ БРАУЗЕР │── GATEWAY (:8081)
└─────────┘  │                 
             └────────┬────────┘
                      │ "lb://ollama-backend", "lb://ollama-auth"
                      
             ┌─────────────────┐
              EUREKA          │
              (:8761)         │
             └────────┬────────┘
                      
         ┌────────────┼────────────┐
         │            │            
         │ (регистр)  │ (регистр)  │
         ▼            ▼            
  ┌────────────┐ ┌────────────┐    
  │   AUTH     │ │  BACKEND   │    
  └─────┬──────┘ └─────┬──────┘    
        │              │           
        │ (конфиг)     │ (конфиг)  │
        ▼              ▼           
  ┌────────────────────────────┐  (Gateway тоже может тянуть конфиги)
  │     CONFIG SERVER (:8888)  │   │
  │          ┌─────┐           │   │
  │          │ GIT │           │───┘
  │          └─────┘           
  └────────────────────────────┘
                  

Auth и Backend могут менять порты — никто не заметит. Eureka в курсе, Gateway спрашивает у Eureka, Config Server стоит в стороне и не лезет.

Всё вместе в Docker Compose для локальной разработки:


yaml
services:
  eureka:
build: ./discovery
ports:
   - "8761:8761"
 
  config-server:
build: ./config-server
ports:
   - "8888:8888"
 
  backend:
build: ./backend
depends_on:
   - eureka
   - config-server
environment:
   - EUREKA_CLIENT_SERVICEURL_DEFAULTZONE=http://eureka:8761/eureka/
   - SPRING_CLOUD_CONFIG_URI=http://config-server:8888
deploy:
   replicas: 2   # две реплики — Eureka знает обе
 
  gateway:
    build: ./gateway
    ports:
      - "8081:8081"

 

Что я понял

Eureka — это когда ты не хранишь адреса в голове, а спрашиваешь у справочной.

Config Server — это когда правки на проде вносятся через коммит в Git, а не через kubectl edit или docker exec дрожащими руками.

Circuit Breaker — это когда твой сервис не умирает от того, что соседний лег.

Всё это — Spring Cloud, просто набор для решения проблем, которые у вас появятся, если вы разобьете монолит на части.

P.S. Что я забыл и теперь жалею:

  • Bulkhead — отдельный пул потоков для каждого сервиса, чтобы один медленный сервис не забил все потоки Gateway.
  • Rate limiter — чтобы никто не дергал нейросеть 1000 раз в секунду через Swagger UI.
  • Retry с экспоненциальной задержкой вместо линейной.

Но это уже в следующих статьях, которые я буду писать в 4 утра, когда прод снова ляжет… Да, и мне будет не лень.

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники