Pull to refresh
813.2
Яндекс
Как мы делаем Яндекс

Кот в мешке: как приручить дикий бинарник

Level of difficultyMedium
Reading time26 min
Views10K

Всем привет. Меня зовут Василий. Я работаю SRE в Яндекс Маркете. Недавно у нас прошли тренировки по DevOps от Young&&Yandex. Сегодня я разберу финальное задание, как и обещал участникам тренировок. Оно состоит в том, чтобы развернуть инсталляцию приложения из готового бинарника, которая будет соответствовать SLA из ТЗ. Выглядит предельно просто, но только на первый взгляд. Под катом — один из вариантов обхода всех подводных камней, которые притаились в задании.

Для начала расскажу о самих тренировках для тех, кто о них не слышал. Если кратко, то это активные четыре недели с лекциями и заданиями, по итогам которых все участники получают ценные знания и опыт, самые успешные — призы и фасттрек на стажировку, а организаторы — возможность закрыть какое-то количество вакансий.

В этот раз помимо тренировок по алгоритмам были организованы тренировки по ML и DevOps. То есть это наш первый опыт такого мероприятия для девопсов. Что-то мы делали с оглядкой на КИТ, но фактически пришлось всё создавать с нуля и в сжатые сроки. Помимо подготовки лекций и домашних заданий к ним нам требовалось оценить участников и выделить среди них лучших. В итоге домашние задания к лекциям стали необязательными, но в конце всех ждал объёмный финальный проект.

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

Итак, за дело!

Запускаем приложение

У нас есть бинарник приложения (и он кусается). Называется bingo. Изначально это сокращение от «бинарник гошный» с прозрачным намёком на некоторые особенности его поведения. А ещё можно восклицать «Бинго!» после решения очередной сложной загадки.
Наша задача — развернуть приложение в соответствии с ТЗ.

SLA
  • Отказоустойчивость: сервис должен быть развернут на двух нодах, отказ любой из них должен быть незаметен пользователю. Допускается просадка по RPS до стабильного значения в момент отказа любой из нод. При живости обеих нод, инсталляция обязана выдерживать пиковую нагрузку. Также нужно обеспечить восстановление работоспособности любой отказавшей ноды быстрее, чем за минуту.

  • Сервис должен переживать пиковую нагрузку в 120 RPS в течение 1 минуты, стабильную в 60 RPS.

  • Запросы POST /operation {"operation": <operation_id: integer>} должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат быстрее чем за 400 миллисекунд в 90% случаев при 120 RPS, гарантируя не более 1% ошибок.

  • Запросы GET /db_dummy должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат быстрее чем за 400 миллисекунд в 90% случаев при 120 RPS, гарантируя не более 1% ошибок.

  • Запросы GET /api/movie/{id} должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат быстрее чем за 400 миллисекунд в 90% случаев при 120 RPS, гарантируя не более 1% ошибок.

  • Запросы GET /api/customer/{id} должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат быстрее чем за 400 миллисекунд в 90% случаев при 120 RPS, гарантируя не более 1% ошибок.

  • Запросы GET /api/session/{id} должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат быстрее чем за 400 миллисекунд в 90% случаев при 120 RPS, гарантируя не более 1% ошибок.

  • Запросы GET /api/movie должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат, гарантируя не более 1% ошибок. Требований по времени ответа нет, планируем делать не более одного такого запроса одновременно.

  • Запросы GET /api/customer должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат, гарантируя не более 1% ошибок. Требований по времени ответа нет, планируем делать не более одного такого запроса одновременно.

  • Запросы GET /api/session должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат, гарантируя не более 5% ошибок. Требований по времени ответа нет, планируем делать не более одного такого запроса одновременно.

  • Запросы POST /api/session должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат, гарантируя не более 1% ошибок. Требований по времени ответа и RPS нет.

  • Запросы DELETE /api/session/{id} должны возвращать незакешированный ответ. Сервер должен обрабатывать такие запросы и отдавать результат, гарантируя не более 1% ошибок. Требований по времени ответа и RPS нет.

  • Задача со звёздочкой 1: сделать так, чтобы сервис работал на отдельном домене и по HTTPS-протоколу, и по HTTP без редиректа на HTTPS (допускается самоподписанный сертификат).

  • Задача со звёздочкой 2: сделать HTTP3.

  • Задача со звёздочкой 3: сделать так, чтобы запросы GET /long_dummy возвращали ответ не старше 1 минуты и отвечали быстрее чем за 1 секунду в 75% случаев.

  • Задача со звёздочкой 4: желательно обеспечить наблюдаемость приложения: графики RPS и ошибок по каждому эндпоинту.

  • Задача со звёздочкой 5: автоматизировать развёртывание при помощи DevOps-инструментов, с которыми вы успели познакомиться ранее.

Из ТЗ становится понятно, что приложение обрабатывает HTTP-запросы. Ещё там прописаны требования по RPS, скорости ответа и максимальному проценту ошибок на различные HTTP-эндпоинты. В остальном у нас есть большое пространство для манёвра. Кроме того, стек технологий, который можно использовать, заданием не ограничен.

Скачиваем бинарник и внимательно смотрим на него.

vgbadaev@vgbadaev-ux1:~/Bingo$ file bingo
bingo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=7PHOtgNzLcwX7-Bzugf1/EdQYZcMp80WIrKIvs4C9/cCxp-3U9Pg89ljURta9o/hFV8Si2Caw7pBesBwdFI, with debug_info, not stripped

Файл — это ELF под архитектуру AMD64. И теперь мы знаем, что он написан на Go, да ещё и не пострипан (not stripped). Можно заглянуть внутрь поглубже (go tool objdump ./bingo | less) и разыскать там некоторые подсказки. Но и не стоит тратить слишком много времени на ковыряние в бинарнике — все ответы в нём не найдешь, да и задание не про то.

Попробуем запустить.

vgbadaev@vgbadaev-ux1:~/Bingo$ ./bingo
Hello world
vgbadaev@vgbadaev-ux1:~/Bingo$ echo $?
0
vgbadaev@vgbadaev-ux1:~/Bingo$ 

Неужели это какая-то ошибка, и нам дали самый обычный “Hello world”? Весьма удивительно. Кстати судя по чату, какая-то часть участников именно в этом месте и застряла.

Попробуем вызвать help.

vgbadaev@vgbadaev-ux1:~/Bingo$ ./bingo --help
bingo

Usage:
   [flags]
   [command]

Available Commands:
  completion           Generate the autocompletion script for the specified shell
  help                 Help about any command
  prepare_db           prepare_db
  print_current_config print_current_config
  print_default_config print_default_config
  run_server           run_server
  version              version

Flags:
  -h, --help   help for this command

Use " [command] --help" for more information about a command.

Уже интереснее. Кто-то, наверное, невнимательно чистил код после копипасты. Похоже, что бинарник умеет готовить автокомплиты, как многие гошные бинарники, например kubectl. Вероятно, авторы использовали ту же самую библиотеку для работы с CLI.

Тут же видим несколько полезных команд:

  • prepare_db — наверное, это и есть то самое «правильно попросить» из ТЗ, но оно пригодится позже. Сейчас нужно разобраться с конфигом.

  • print_default_config — судя по названию, она должна вывести дефолтный конфиг.

Попробуем.

vgbadaev@vgbadaev-ux1:~/Bingo$ ./bingo print_default_config
student_email: test@example.com
postgres_cluster:
  hosts:
  - address: localhost
    port: 5432
  user: postgres
  password: postgres
  db_name: postgres
  ssl_mode: disable
  use_closest_node: false

Итак, конфиг принимает целых два параметра: строковый email студента и объект postgres_cluster. По дефолту этот самый кластер состоит из одной ноды. Отлично, теперь мы знаем, что нужно подготовить кластер Postgres хотя бы из одной ноды, а также конфиг по образцу выше. Воспользуемся быстрым способом поднять Postgres локально — Docker Compose.

docker-compose.yaml
---

version: "3.8"

services:
  postgres:
    image: "postgres:15"
    container_name: "postgres"
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: "512mb"
        reservations:
          memory: "256mb"
    shm_size: "128mb"
    user: "postgres"
    command:
      - "postgres"
      - "-c"
      - "log_statement=all"
      - "-c"
      - 'max_connections=100'
    healthcheck:
      test: [ "CMD", "pg_isready", "-d", "postgres", "-U", "postgres" ]
      interval: "10s"
      timeout: "2s"
      retries: 3
      start_period: "15s"
    environment:
      POSTGRES_PASSWORD: "postgres"
      POSTGRES_USER: "postgres"
      POSTGRES_DB: "postgres"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "pgdata:/var/lib/postgresql/data:rw"
    ports:
      - "127.0.0.1:5432:5432/tcp"
    networks:
      - "bingo-network"
    logging: &logging
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    restart: "unless-stopped"

volumes:
  pgdata:

networks:
  bingo-network:
    driver: "bridge"
    name: "bingo-network"
    external: false

Я стащил эту заготовку из своего проекта, где этот yaml использовался для локальной разработки. Из избыточного многословия в сравнении с обычным docker run postgres получаем ограничение контейнера по ресурсам, ротацию логов, volume, хэлсчеки, проброс порта на локалхост и возможность видеть запросы в stdout. Последнее очень полезно, когда нужно знать, какие запросы получает база.

docker-compose up — и база поднята. Теперь нужно подготовить конфиг самого приложения. Он будет выглядеть вот так:

student_email: vgbadaev@example.com
postgres_cluster:
  hosts:
  - address: localhost
    port: 5432
  user: postgres
  password: postgres
  db_name: postgres
  ssl_mode: disable
  use_closest_node: false

В help не оказалось опций или аргументов, задающих путь к файлу с конфигом, поэтому для начала положим его рядом с самим бинарником. Для удобства назовем его config.yml. При помощи команды print_current_config пробуем понять, видит ли бинарник конфиг рядом. Запускаем и получаем панику с сообщением failed to read config data. Видимо, настало время вспомнить лекцию с тренировок, где Андрей Мичурин рассказал об инструментах отладки black box.

Кажется, что стоит попробовать strace.

vgbadaev@vgbadaev-ux1:~/Bingo$ strace -e file ./bingo print_current_config
execve("./bingo", ["./bingo", "print_current_config"], 0x7ffe11e4ab78 /* 46 vars */) = 0
openat(AT_FDCWD, "/sys/kernel/mm/transparent_hugepage/hpage_pmd_size", O_RDONLY) = 3
--- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=120382, si_uid=1000} ---
--- SIGURG {si_signo=SIGURG, si_code=SI_TKILL, si_pid=120382, si_uid=1000} ---
openat(AT_FDCWD, "/opt/bingo/config.yaml", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
panic: failed to read config data
...

Бинго! Приложение ищет конфиг-файл по пути /opt/bingo/config.yaml. Положим конфиг по этому пути.

vgbadaev@vgbadaev-ux1:~/Bingo$ sudo mkdir -p /opt/bingo/ && sudo chown vgbadaev:vgbadaev /opt/bingo/ && mv config.yml /opt/bingo/config.yaml
vgbadaev@vgbadaev-ux1:~/Bingo$ ./bingo print_current_config
student_email: vgbadaev@example.com
postgres_cluster:
  hosts:
  - address: localhost
    port: 5432
  user: postgres
  password: postgres
  db_name: postgres
  ssl_mode: disable
  use_closest_node: false

Отлично, нам удалось скормить приложению конфиг! Настало время попробовать наполнить базу c помощью команды ./bingo prepare_db.

Наполняем базу и разбираемся с логами

Наблюдаем за консолями. Видим, как создались таблицы, и в базу добавляются многие тысячи строк. Ждем завершения (в моём случае процесс успешно завершился за 15 минут). Теперь можно подключиться к базе в контейнере, выполнив docker-compose exec postgres psql, и посмотреть, что приложение в ней насоздавало. У некоторых участников уже на этом этапе возникли вопросы к подготовке базы и зачесались руки навести красоту. Я же вернусь к этому вопросу позже.

Самое время запустить сервер ./bingo run_server — снова паника, на этот раз с сообщением failed to build logger. Расчехляем strace и узнаём путь к файлу с логами (в моём случае это /opt/bongo/logs/143f38a3cc/main.log). Создаём нужную директорию и даём текущему пользователю права sudo mkdir -p /opt/bongo/logs/143f38a3cc/ && sudo chown -R vgbadaev:vgbadaev /opt/bongo/logs. Снова запускаем ./bingo run_server. Что-то происходит… Спустя полминуты в консоли поздравления с запуском сервера и первый секретный код.

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

Что же делать с логами? Есть соблазн на месте файла с логами сделать симлинк на /dev/null (немало участников так и сделали), но ТЗ намекает на то, что логи нам потребуются. Скорее всего, в логах можно найти что-то полезное, но сейчас это довольно тяжело. Можно пытаться вычитывать глазами. А ещё можно накидать какой-нибудь скрипт на коленке, который прочтёт логи и выведет нам их очищенный вариант. Ну или можно взять какую-нибудь готовую тулзу для парсинга логов и подготовить конфиг для неё, потом его можно будет хотя бы частично переиспользовать в проде.

Первый вариант можно немного улучшить через grep -v, но даже так приходится читать много мусора. Второй вариант может быть полезным только ограниченно для ситуации «здесь и сейчас прочитать, что же делает приложение». Поэтому остановимся на третьем. Мой выбор пал на fluentd, так как у меня есть некоторый опыт работы с ним. Ставить его на ноутбук желанием не горю, поэтому снова воспользуюсь докером. Подготовим конфиг файл fluent.conf.

<source>
    @type tail
    path /log/*.log
    pos_file /log/bingo.log.pos
    follow_inodes true
    read_from_head true
    tag bingo
    <parse>
        @type json
    </parse>
</source>

<filter bingo>
    @type grep
    <exclude>
        key level
        pattern debug
    </exclude>
</filter>

<filter bingo>
    @type record_transformer
    remove_keys runtime_version,runtime_numCPU,runtime_goos,runtime_goarch,runtime_compiler,os_hostname,os_uid,os_ppid,os_pid,os_gid,os_env,os_pagesize,log_path,app_version,app_name,http_request_header,stacktrace,caller
</filter>

<match bingo>
    @type stdout
</match>

Допишем в docker-compose-файл секцию с контейнером fluentd.


  fluentd:
    image: "fluent/fluentd:v1.16-1"
    container_name: "fluentd"
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: "128mb"
        reservations:
          memory: "64mb"
    entrypoint: [ ]
    user: "root"
    command:
      - "fluentd"
      - "--config"
      - "/etc/fluent/fluent.conf"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./:/etc/fluent/:ro"
      - "/opt/bongo/logs/143f38a3cc/:/log:rw"
    network_mode: "none"
    logging: *logging
    restart: "unless-stopped"

Запускаем контейнеры, запускаем ./bingo run_server — получаем очищенные логи запуска.

fluentd     | 2024-01-05 17:00:21.492885097 +0300 bingo: {"level":"info","timestamp":"2024-01-05T17:00:18.279+0300","msg":"Prepare app."}
fluentd     | 2024-01-05 17:00:21.493068563 +0300 bingo: {"level":"info","timestamp":"2024-01-05T17:00:18.292+0300","msg":"Run initialization request."}
fluentd     | 2024-01-05 17:00:48.493112660 +0300 bingo: {"level":"error","timestamp":"2024-01-05T17:00:48.293+0300","msg":"Failed initialization request."}
fluentd     | 2024-01-05 17:00:48.493446398 +0300 bingo: {"level":"info","timestamp":"2024-01-05T17:00:48.294+0300","msg":"Run server."}

К сожалению, в этих логах приложение не сообщило о том, на каком порте оно запустилось. Но тут есть кое-что интересное. Согласно этим логам, приложение при запуске делает какой-то инициализирующий запрос. Этот запрос через 30 секунд завершается неуспешно. Но это не мешает серверу запуститься.

А что если получится этот запрос починить или хотя бы сфэйлить быстрее? Наверное, это какой-то большой запрос в базу. Заглядываем в консоль с запущенной базой, но там ничего нет кроме регулярно мелькающих SELECT NOT pg_is_in_recovery(). Думаю, сейчас не стоит тратить на это время, но стоит запомнить — в будущем может пригодиться.

Обратимся за помощью к lsof, чтобы узнать, какой порт слушает приложение.

vgbadaev@vgbadaev-ux1:~/Bingo$ lsof -i -nP | grep bingo
bingo     7771 vgbadaev    8u  IPv4 180027      0t0  TCP 127.0.0.1:43430->127.0.0.1:5432 (ESTABLISHED)
bingo     7771 vgbadaev    9u  IPv4 184736      0t0  TCP 10.211.5.17:39716->8.8.8.8:80 (SYN_SENT)

Упс! А что это? 5432 — это Postgres. А вот другая строка вызывает вопросы. Я ожидал здесь увидеть порт, который приложение слушает, но похоже, что поспешил и увидел тот самый initialization request, висящий в статусе SYN_SENT. Но зачем приложение идёт на 80-й порт общеизвестного DNS-сервера? Есть ли там вообще что-то?

vgbadaev@vgbadaev-ux1:~/Bingo$ nc -vzw2 8.8.8.8 80
nc: connect to 8.8.8.8 port 80 (tcp) timed out: Operation now in progress
vgbadaev@vgbadaev-ux1:~/Bingo$ 

Похоже, что 80-й порт там никто не слушает, именно поэтому соединение и висит в SYN_SENT. Очень интересно, что же там такое в запросе. Tcpdump здесь не помощник, потому что кроме tcp syn он ничего не покажет. А нам хочется увидеть сам запрос. Для этого перенаправим его в локалхост средствами iptables и дадим ему фэйковый 200 ОК через nc.

vgbadaev@vgbadaev-ux1:~/Bingo$ sudo iptables --table nat --insert OUTPUT --proto tcp --destination 8.8.8.8 --destination-port 80 --jump DNAT --to-destination 127.0.0.1:8080
vgbadaev@vgbadaev-ux1:~/Bingo$ echo "HTTP/1.1 200 OK
Connection: close

" | nc -l 8080 &
[1] 724182
vgbadaev@vgbadaev-ux1:~/Bingo$ ./bingo run_server

Congratulations.
You were able to figure out why
the application is slow to start and fix it.
Here's a secret code that confirms that you did it.
--------------------------------------------------
code:         google_dns_is_not_http
--------------------------------------------------
    
GET / HTTP/1.1
Host: 8.8.8.8
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip


My congratulations.
You were able to start the server.
Here's a secret code that confirms that you did it.
--------------------------------------------------
code:         yoohoo_server_launched
--------------------------------------------------
    
^C[1]+  Done                    echo "HTTP/1.1 200 OK
Connection: close

" | nc -l 8080
vgbadaev@vgbadaev-ux1:~/Bingo$ 

Бинго! Приложение быстро запустилось и сообщило, что это была недокументированная задача от организаторов тренировок, подарив ещё один секретный код.

Полагаю, что достаточно будет просто быстрее фэйлить этот запрос

sudo iptables --table nat --delete OUTPUT 1
sudo iptables --insert OUTPUT --proto tcp --destination 8.8.8.8 --destination-port 80 --jump REJECT

Пробуем запустить сервер. В логах приложение ругается на фэйл запроса, но запускается быстро и по-прежнему показывает секретный код. Это хорошее решение проблемы.

Пара интересных фактов. Изначально в этом месте планировался обычный sleep, чтобы имитировать медленно стартующее приложение. Но потом возникла идея дать возможность починить slow start. Многие участники, как и ожидалось, не смогли дойти до такого решения.

А ещё по легенде задания у нас есть Петя-тестировщик, который проверяет работоспособность стенда. Метод в бинарнике, через который он проверял быстрый запуск, делался в последний момент и содержал уже настоящий баг. Из-за этого соотвествующий Петин тест практически всегда светился зелёным (секретный код это дублировал, поэтому на финальный рейтинг это не повлияло).

А кто такой Петя на самом деле?

Петя — это коллективный псевдоним организаторов тренировок. В одни руки проделать такой объём работы было бы очень сложно. А ещё Петя — это герой мема «Петя умный. Будь как Петя».

Копать причины медленного запуска ошибочно зелёный тест вовсе не мотивировал. Но при этом быстрый запуск приложения мог прощать некоторые ошибки на балансировщике. Именно поэтому дальнейший разбор будет исходить из того, что запуск приложения всё-таки медленный.

Вернёмся к lsof, которым мы планировали найти прослушиваемый приложением порт.

vgbadaev@vgbadaev-ux1:~/Bingo$ lsof -i -nP | grep bingo
bingo     7771 vgbadaev    8u  IPv4 180027      0t0  TCP 127.0.0.1:43430->127.0.0.1:5432 (ESTABLISHED)
bingo     7771 vgbadaev    9u  IPv6 180900      0t0  TCP *:22102 (LISTEN)
vgbadaev@vgbadaev-ux1:~/Bingo$ 

Установленное соединение на 127.0.0.1:5432 — это Postgres. Само приложение слушает порт 22102 (в моём случае). Проверяем этот порт браузером и забираем очередной секретный код.

Контейнер с приложением

Пока гуглились и писались конфиги fluentd, пока рождались в голове идеи, как заглянуть в инициализирующий запрос, пока освежались в памяти мануалы iptables путём повторного их чтения с перерывами на чай и «папа, помоги собрать Lego» — bingo молотил в консоли вхолостую. Его пришлось несколько раз перезапускать. Спустя 20–30 минут после запуска он то выпадал в панику, то прибивался ООМом. И даже умудрился за всё время забить логами десяток гигабайт на диске! Значит, при подготовке контейнера и в дальнейших шагах нужно будет учесть эти особенности приложения.

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

Отдельной дилеммой стал вопрос с логами. Можно сделать симлинк файла с логами на stdout процесса (/proc/${PID}/fd/1 в случае с правильно собранным образом PID=1). Это переложит проблему логов на container runtime и тем самым даже частично упростит её решение. Можно сделать отдельный раздел под логи, а при помощи сайдкаров их ротировать, парсить и чистить.

Наиболее правильным решением мне показалось сделать первый вариант в качестве дефолтного, а второй включать через переменную окружения. Получился вот такой dockerfile:

FROM debian:12-slim as production

ARG LOG_DIRECTORY="${LOG_DIRECTORY:-/opt/bongo/logs/143f38a3cc/}"
ARG BINGO_PORT="${BINGO_PORT:-22102/tcp}"

ENV LOG_DIRECTORY="${LOG_DIRECTORY}" \
    BINGO_PORT="${BINGO_PORT}" \
    LOG_TO_STDOUT='true'

RUN apt-get update \
    && apt-get install -y \
        "curl" \
    && rm -rf "/var/lib/apt/lists/*"

RUN mkdir -p "${LOG_DIRECTORY}" \
    && chown "nobody" "${LOG_DIRECTORY}" \
    && chmod 700 "${LOG_DIRECTORY}"
VOLUME "${LOG_DIRECTORY}"

USER nobody
EXPOSE "${BINGO_PORT}"
WORKDIR "/opt/bingo"

COPY --chmod=555 \
    "bingo" \
    "entrypoint.sh" \
    ./

ENTRYPOINT ["./entrypoint.sh"]
CMD ["./bingo", "run_server"]
HEALTHCHECK --interval=2s --timeout=1s --retries=1 --start-period=32s \
    CMD curl -sf "http://localhost:22102/ping" || exit 1

С вот таким entrypoint.sh:

#!/bin/bash

if [ "${LOG_TO_STDOUT}" = 'true' ] && [ ! -f "${LOG_DIRECTORY}/main.log" ]; then
    ln -s '/proc/1/fd/1' "${LOG_DIRECTORY}/main.log"
fi

exec "$@"

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

docker-compose.yaml
---

version: "3.8"

services:
  postgres:
    image: "postgres:15"
    container_name: "postgres"
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: "512mb"
        reservations:
          memory: "256mb"
    shm_size: "128mb"
    user: "postgres"
    command:
      - "postgres"
      - "-c"
      - "log_statement=all"
      - "-c"
      - 'max_connections=100'
    healthcheck:
      test: [ "CMD", "pg_isready", "-d", "postgres", "-U", "postgres" ]
      interval: "10s"
      timeout: "2s"
      retries: 3
      start_period: "15s"
    environment:
      POSTGRES_PASSWORD: "postgres"
      POSTGRES_USER: "postgres"
      POSTGRES_DB: "postgres"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "pgdata:/var/lib/postgresql/data:rw"
    networks:
      - "bingo-network"
    logging: &logging
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    restart: "unless-stopped"

  bingo-1: &bingo
    image: "bingo:latest"
    container_name: "bingo-1"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: "256mb"
        reservations:
          memory: "256mb"
    depends_on:
      postgres:
        condition: "service_healthy"
    build:
      context: "./"
      dockerfile: "Dockerfile"
      target: "production"
    environment:
      LOG_TO_STDOUT: "false"
    read_only: true
    ports:
      - "127.0.0.1:80:22102/tcp"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./config.yaml:/opt/bingo/config.yaml:ro"
      - "bingo-1-logs:/opt/bongo/logs/143f38a3cc/:rw"
    networks:
      - "bingo-network"
    logging: *logging
    restart: "unless-stopped"

  fluentd-1: &fluentd
    image: "fluent/fluentd:v1.16-1"
    container_name: "fluentd-1"
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: "128mb"
        reservations:
          memory: "64mb"
    depends_on:
      bingo-1:
        condition: "service_started"
    entrypoint: [ ]
    user: "root"
    command:
      - "fluentd"
      - "--config"
      - "/etc/fluent/fluent.conf"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./fluent.conf:/etc/fluent/fluent.conf:ro"
      - "bingo-1-logs:/log:rw"
    network_mode: "none"
    logging: *logging
    restart: "unless-stopped"

volumes:
  pgdata:
  bingo-1-logs:

networks:
  bingo-network:
    driver: "bridge"
    name: "bingo-network"
    external: false

А нужно ли было делать базу отказоустойчивой? Фокус задачи был, прежде всего, на решении проблем, зашитых в бинарник. Но это не помешало некоторым участникам сделать её отказоустойчивой, а нам накинуть за это дополнительные баллы при проверке отчётов. Равно как и single node-база не помешала участникам попасть в топ-10. Простой способ развернуть Postgres из более чем одной ноды даже на этом локальном стенде — образ bitnami/postgresql-repmgr.

Проверяем запросы

Итак, одна нода локально запущена, и можно проверить, насколько она выполняет она ТЗ. Для этого пройдусь по пунктам, где указаны HTTP-запросы и требования к ним. В некоторых пунктах не совсем понятно, как должно выглядеть тело запроса, поэтому начну с тех, в которых поменьше требований, а именно GET /api/movie, GET /api/customer и GET /api/session.

ТЗ требует незакешированный ответ и выставляет SLA только по проценту ошибок. С первыми двумя порядок. А вот GET /api/session стабильно завершается с таймаутом и 504-м статусом. В самом начале, когда я поднимал базу в Docker Compose, написал, что эта база будет выводить SQL-запросы в консоль. Для этого в docker-compose-файле есть строка с log_statement=all. Осталось снова сделать запрос GET /api/session к bingo и поискать в консоли с docker-compose соотвествующий SQL-запрос. Он сильно выделяется на фоне постоянных SELECT NOT pg_is_in_recovery().

SELECT sessions.id, sessions.start_time, customers.id, customers.name, customers.surname, customers.birthday, customers.email, movies.id, movies.name, movies.year, movies.duration FROM sessions INNER JOIN customers ON sessions.customer_id = customers.id INNER JOIN movies ON sessions.movie_id = movies.id ORDER BY movies.year DESC, movies.name ASC, customers.id, sessions.id DESC LIMIT 100000;

Запрос выглядит тяжёленьким: в нём целых два джойна на немаленьких таблицах, сортировка по нескольким полям. И по всем трём участвующих таблицам нет ни единого индекса. Похоже, что авторы в спешке совсем забыли про них. Нужно исправлять это недоразумение.

Сделаю поля id первичными ключами. Создам индексы по полям customer_id и movie_id в таблице sessions, поскольку они используются при джойне. И создам составной индекс для таблицы movies по полям year, name. По итогу получится вот такой SQL скрипт:

ALTER TABLE customers ADD PRIMARY KEY (id);
ALTER TABLE movies ADD PRIMARY KEY (id);
ALTER TABLE sessions ADD PRIMARY KEY (id);
CREATE INDEX IF NOT EXISTS sessions_customer_id_index ON sessions (customer_id);
CREATE INDEX IF NOT EXISTS sessions_movie_id_index ON sessions (movie_id);
CREATE INDEX IF NOT EXISTS movies_year_name_index ON movies (year DESC, name ASC);

Проверяем: запрос больше не таймаутит.

Следующая дилемма: нужно ли оптимизировать таким образом все запросы или только те, что не укладываются в SLA без индексов? Индексы — штука небесплатная. В моём случае мне потребовалось добавить ещё индексы для таблицы customers, остальное укладывалось в SLA. Нужно ли это делать, во многом зависит от ресурсов, на которых развёрнута база, и от производительности железа под этими ресурсами. Преждевременная оптимизация — грех, но строго говоря в случае с bingo избыточные индексы простительны, потому что ни один из изменяющих базу методов не снабжён SLA по времени или по RPS. Таким образом в этом месте при оценке работ мы опирались на выполнение SLA и сам факт обнаружения участником проблемы с индексами.

Отказоустойчивость

Пятисотки?

Пока я разбирался с индексами, bingo преподнёс новый сюрприз — приложение стало пятисотить. Docker честно пометил контейнер как unhealthy — это хоть и подсветило, но никак не полечило проблему. Перезапуск контейнера вернул состояние healthy, а само приложение перестало пятисотить.

Значит, bingo может внезапно начать отвечать на все запросы пятисотками. А значит, нужно найти решение получше, чем перезапуск на «мясном» приводе. Нормальный оркестратор решает эту проблему из коробки, но сейчас речь о голом докере.

Первая мысль: чем-то регулярно дёргать команду docker container restart $( docker container ls --quiet --filter 'health=unhealthy' --filter 'name=^bingo\-\d+$' ) --signal SIGTERM --time 15. Этот однострочник можно даже переписать в нормальный bash-скрипт и обернуть в таймер systemd. Но сильно не хочется городить такие конструкции на локальной машине. Да и было бы очень классно, если бы эта конструкция демонтировалась автоматически вместе с остановкой стенда. И такое решение есть: контейнер willfarrell/docker-autoheal.

В docker-compose добавим label "autoheal=true" для сервиса bingo-1 и сервис из образа willfarrell/docker-autoheal.

  autoheal:
    image: "willfarrell/autoheal:latest"
    container_name: "autoheal"
    deploy:
      resources:
        limits:
          cpus: '0.05'
          memory: "16mb"
        reservations:
          memory: "8mb"
    privileged: true
    environment:
      AUTOHEAL_CONTAINER_LABEL: "autoheal"
      AUTOHEAL_START_PERIOD: "0"
      AUTOHEAL_INTERVAL: "5"
      AUTOHEAL_DEFAULT_STOP_TIMEOUT: "15"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "/var/run/docker.sock:/var/run/docker.sock"
    network_mode: "none"
    logging: *logging
    restart: "unless-stopped"

Эта конструкция будет перезапускать unhealthy-контейнеры с соответствующим лейблом.

Как нужно было бороться с тем, что обе ноды падают одновременно? Я такое у себя видел!

С запущенным бинарником действительно что-то интересное происходило спустя случайное время (минимум 20, максимум 30 минут) после запуска. Само приложение запускалось 30 секунд. То есть было возможно увидеть обе ноды неработоспособными одновременно. Разумеется, мы это предусмотрели. Помимо механизма рандомных падений в bingo есть механизм их выключения и механизм падения по требованию. Эти механизмы и использовались в Петиных тестах для того, чтобы падения были только плановыми. Именно поэтому в ТЗ явно было написано про две ноды.

Проверяем RPS

Настало время проверить пункты ТЗ, где есть требования по RPS. Покажу на примере GET /db_dummy.

vgbadaev@vgbadaev-ux1:~/Bingo$ wrk -c 10 --latency -d 10s http://localhost/db_dummy
Running 10s test @ http://localhost/db_dummy
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   103.91ms    6.37ms 165.29ms   94.69%
    Req/Sec    48.21      4.58    50.00     84.85%
  Latency Distribution
     50%  102.57ms
     75%  102.88ms
     90%  103.20ms
     99%  129.14ms
  960 requests in 10.02s, 219.38KB read
Requests/sec:     95.82
Transfer/sec:     21.90KB
vgbadaev@vgbadaev-ux1:~/Bingo$ wrk -c 15 --latency -d 10s http://localhost/db_dummy
Running 10s test @ http://localhost/db_dummy
  2 threads and 15 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   149.30ms   88.24ms 602.47ms   87.96%
    Req/Sec    49.64     10.03    70.00     64.14%
  Latency Distribution
     50%  102.89ms
     75%  154.80ms
     90%  280.88ms
     99%  496.45ms
  988 requests in 10.01s, 225.77KB read
Requests/sec:     98.68
Transfer/sec:     22.55KB
vgbadaev@vgbadaev-ux1:~/Bingo$

Поигравшись с числом одновременных запросов, приходим к выводу, что один инстанс приложения не может обработать более 100 RPS (рост числа параллельных запросов после определённого порога приводит только к росту таймингов). А это значит, чтобы держать 120 RPS в пике, нам потребуется минимум два инстанса, что и просили сделать в ТЗ.

Настало время заскейлить приложение. А ещё придётся решить вопрос с распределением трафика по нодам. Добавлю bingo-2 с соответствующими volume и fluentd-сайдкаром по аналогии с bingo-1, уберу с bingo проброс портов на хост и добавлю контейнер с nginx.

Я бы не стал для этих целей использовать nginx, а взял бы что-нибудь с активными хэлсчеками. Но довольно много участников взяли именно nginx и допустили одну и ту же ошибку. Вследствие чего тест с названием «Отказоустойчивость 3» потрепал всем много нервов. Так что я обязан разобрать эту ошибку — и беру nginx.

Конфиг nginx для демонстрации предельно минималистичен.

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
  worker_connections  1024;
}

http {
  upstream app {
    server bingo-1:22102;
    server bingo-2:22102;
  }

  server {
    listen 80;

    location / {
      proxy_pass http://app;
    }
  }
}

Проверим, как себя поведёт инсталляция.

vgbadaev@vgbadaev-ux1:~/Bingo$ wrk -c 10 --latency -d 30s http://localhost/ping
Running 30s test @ http://localhost/ping
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    54.15ms   40.72ms 259.33ms   69.97%
    Req/Sec   100.00     23.00   200.00     76.50%
  Latency Distribution
     50%   52.23ms
     75%   77.79ms
     90%  104.32ms
     99%  181.01ms
  5988 requests in 30.04s, 1.12MB read
Requests/sec:    199.37
Transfer/sec:     38.35KB

Not Bad. Вопрос с RPS решает. Повторим эксперимент, но с рестартом одного из контейнеров.

vgbadaev@vgbadaev-ux1:~/Bingo$ docker container restart bingo-2 && wrk -c 10 --latency -d 30s http://localhost/ping
bingo-2
Running 30s test @ http://localhost/ping
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   103.27ms   57.59ms 796.80ms   86.77%
    Req/Sec    50.03     11.95   101.00     85.79%
  Latency Distribution
     50%  103.82ms
     75%  106.00ms
     90%  156.39ms
     99%  307.18ms
  2997 requests in 30.05s, 488.77KB read
Requests/sec:     99.75
Transfer/sec:     16.27KB
vgbadaev@vgbadaev-ux1:~/Bingo$ 

Ещё лучше. Внезапная остановка одного бэкенда теперь тоже не страшна. Но что будет, если один из бэкендов начнёт пятисотить, как это и происходило в проверке, названной «Отказоустойчивость 3»? Ведь уже выяснилось, что bingo так делает.

Продемонстрирую, как ведет себя конфиг в такой ситуации. Определю у себя в bash функцию start500 с секретным содержимым, которое заставит пятисотить один из бэкендов, и заменю в предыдущем эксперименте рестарт контейнера на её вызов.

А что за секретное содержимое такое?

Самый обычный curl, который заставит получившую его ноду сначала пятисотить на /ping, а потом и на всё остальное. Да, bingo даже немного подыгрывал участникам, делая эту подлянку только после предупреждения через healthcheck. Само содержимое запроса не раскрываю, для решения задачи оно не требуется.

Я описал этот эксперимент для наглядной демонстрации распространённой ошибки, ради чего и брал nginx. Есть и другие способы воспроизвести это поведение nginx без bingo и секретных функций, но я предпочел не раздувать этот раздел сборкой отдельного демонстрационного стенда.

vgbadaev@vgbadaev-ux1:~/Bingo$ start500 && wrk -c 10 --latency -d 30s http://localhost/ping
now 500
Running 30s test @ http://localhost/ping
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    84.84ms   69.52ms   1.14s    73.73%
    Req/Sec    62.74     28.25   220.00     80.50%
  Latency Distribution
     50%  102.74ms
     75%  104.28ms
     90%  155.25ms
     99%  274.55ms
  3763 requests in 30.05s, 628.24KB read
  Non-2xx or 3xx responses: 573
Requests/sec:    125.23
Transfer/sec:     20.91KB
vgbadaev@vgbadaev-ux1:~/Bingo$ 

Autoheal сделал своё дело — без него была бы пролита половина трафика. Но тем не менее в SLA «не более 1% ошибок» в этом эксперименте уложиться бы не вышло.
Ответ на вопрос, почему так происходит, кроется в дефолтном значении директивы proxy_next_upstream., Eсли nginx получил любой ответ с бэкенда, он не будет ретраить без переопределения этого значения в конфиге.

Добавим в конфиг строчку proxy_next_upstream error timeout http_500; и повторим.

vgbadaev@vgbadaev-ux1:~/Bingo$ start500 && wrk -c 10 --latency -d 30s http://localhost/ping
now 500
Running 30s test @ http://localhost/ping
  2 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    96.43ms   53.68ms 564.27ms   73.05%
    Req/Sec    53.28     18.77   141.00     79.30%
  Latency Distribution
     50%  103.67ms
     75%  104.93ms
     90%  156.01ms
     99%  261.48ms
  3196 requests in 30.05s, 521.22KB read
Requests/sec:    106.37
Transfer/sec:     17.35KB
vgbadaev@vgbadaev-ux1:~/Bingo$

Теперь порядок. Как я уже писал выше, у nginx в бесплатной версии нет активных хэлсчеков. То есть решение о признании бэкенда поломанным принимается на основании боевых ответов. А починенным бэкенд считается по прошествии fail_timeout, что мне категорически не нравится.

Чтобы обойти эту проблему, можно использовать другой балансировщик вместо или вместе с nginx.

Почитать подробнее про один из таких вариантов можно в статье, а обзорно про балансировщики — в другой статье. И это далеко не все возможные варианты решения. Некоторые участники вполне успешно использовали в своих решениях балансировщики, про которые я раньше и не слышал.

Задачи со звёздочкой

Наша локальная инсталляция уже довольно хороша, но она пока работает только с голым HTTP. Нужно добавить TLS и кеширование. Подготовим ключ и сертификат командой mkdir certs && sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout certs/nginx-selfsigned.key -out certs/nginx-selfsigned.crt. Сгенерируем dhparam командой sudo openssl dhparam -out certs/dhparam.pem 4096. Добавим в конфиг nginx location с кешированием, SSL и прицепом HTTP3.

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
  worker_connections 1024;
}

http {
  proxy_cache_path /var/cache/nginx keys_zone=app_long_dummy_cache:1m;

  upstream app {
    server bingo-1:22102;
    server bingo-2:22102;
  }

  server {
    listen 80 default_server fastopen=50 reuseport;
    listen 443 default_server ssl http2 fastopen=50 reuseport;
    listen 443 quic reuseport;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers kEECDH+AESGCM+AES128:TLS-CHACHA20-POLY1305-SHA256:kEECDH+AES128:kRSA+AESGCM+AES128:kRSA+AES128:!DES-CBC3-SHA:!RC4:!aNULL:!eNULL:!MD5:!EXPORT:!LOW:!SEED:!CAMELLIA:!IDEA:!PSK:!SRP:!SSLv2:!SSLv3;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:8m;
    ssl_session_timeout 10m;
    ssl_dhparam /etc/nginx/certs/dhparam.pem;
    ssl_certificate /etc/nginx/certs/nginx-selfsigned.crt; 
    ssl_certificate_key /etc/nginx/certs/nginx-selfsigned.key;

    add_header Alt-Svc 'h3=":443"; ma=86400';

    proxy_connect_timeout 100ms;
    proxy_next_upstream error timeout http_500;
    proxy_next_upstream_tries 2;

    location = /long_dummy {
      proxy_pass http://app;
      proxy_cache app_long_dummy_cache;
      proxy_cache_valid 200 1m;
    }

    location / {
      proxy_pass http://app;
    }
  }
}

Поправим docker-compose.yaml, прокинув новую директорию и 443 udp- и tcp-порты в контейнер с nginx. По итогу файл примет следующий вид:

docker-compose.yaml
---

version: "3.8"

services:
  postgres:
    image: "postgres:15"
    container_name: "postgres"
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: "512mb"
        reservations:
          memory: "256mb"
    shm_size: "128mb"
    user: "postgres"
    command:
      - "postgres"
      - "-c"
      - "log_statement=all"
      - "-c"
      - 'max_connections=100'
    healthcheck:
      test: [ "CMD", "pg_isready", "-d", "postgres", "-U", "postgres" ]
      interval: "10s"
      timeout: "2s"
      retries: 3
      start_period: "15s"
    environment:
      POSTGRES_PASSWORD: "postgres"
      POSTGRES_USER: "postgres"
      POSTGRES_DB: "postgres"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "pgdata:/var/lib/postgresql/data:rw"
    networks:
      - "bingo-network"
    logging: &logging
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    restart: "unless-stopped"

  bingo-1: &bingo
    image: "bingo:latest"
    container_name: "bingo-1"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: "256mb"
        reservations:
          memory: "256mb"
    depends_on:
      postgres:
        condition: "service_healthy"
    build:
      context: "./"
      dockerfile: "Dockerfile"
      target: "production"
    environment:
      LOG_TO_STDOUT: "false"
    read_only: true
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./config.yaml:/opt/bingo/config.yaml:ro"
      - "bingo-1-logs:/opt/bongo/logs/143f38a3cc/:rw"
    networks:
      - "bingo-network"
    labels:
      - "autoheal=true"
      - "autoheal.stop.timeout=10"
    logging: *logging
    restart: "unless-stopped"

  fluentd-1: &fluentd
    image: "fluent/fluentd:v1.16-1"
    container_name: "fluentd-1"
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: "128mb"
        reservations:
          memory: "64mb"
    depends_on:
      bingo-1:
        condition: "service_started"
    entrypoint: [ ]
    user: "root"
    command:
      - "fluentd"
      - "--config"
      - "/etc/fluent/fluent.conf"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./fluent.conf:/etc/fluent/fluent.conf:ro"
      - "bingo-1-logs:/log:rw"
    network_mode: "none"
    logging: *logging
    restart: "unless-stopped"

  bingo-2:
    <<: *bingo
    container_name: "bingo-2"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./config.yaml:/opt/bingo/config.yaml:ro"
      - "bingo-2-logs:/opt/bongo/logs/143f38a3cc/:rw"

  fluentd-2:
    <<: *fluentd
    container_name: "fluentd-2"
    depends_on:
      bingo-2:
        condition: "service_started"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./fluent.conf:/etc/fluent/fluent.conf:ro"
      - "bingo-2-logs:/log:rw"

  nginx:
    image: "nginx:1.25"
    container_name: "nginx"
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: "64mb"
        reservations:
          memory: "64mb"
    depends_on:
      bingo-1:
        condition: "service_started"
      bingo-2:
        condition: "service_started"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "./nginx.conf:/etc/nginx/nginx.conf:ro"
      - "./certs:/etc/nginx/certs:ro"
    networks:
      - "bingo-network"
    ports:
      - "127.0.0.1:80:80/tcp"
      - "127.0.0.1:443:443/tcp"
      - "127.0.0.1:443:443/udp"
    logging: *logging
    restart: "unless-stopped"

  autoheal:
    image: "willfarrell/autoheal:latest"
    container_name: "autoheal"
    deploy:
      resources:
        limits:
          cpus: '0.05'
          memory: "16mb"
        reservations:
          memory: "8mb"
    privileged: true
    environment:
      AUTOHEAL_CONTAINER_LABEL: "autoheal"
      AUTOHEAL_START_PERIOD: "0"
      AUTOHEAL_INTERVAL: "5"
      AUTOHEAL_DEFAULT_STOP_TIMEOUT: "15"
    volumes:
      - "/etc/localtime:/etc/localtime:ro"
      - "/var/run/docker.sock:/var/run/docker.sock"
    network_mode: "none"
    logging: *logging
    restart: "unless-stopped"

volumes:
  pgdata:
  bingo-1-logs:
  bingo-2-logs:

networks:
  bingo-network:
    driver: "bridge"
    name: "bingo-network"
    external: false

Прогоняем Петины тесты — всё зелёное. Проверяем HTTP3 docker run --rm -it --network bingo-network ymuski/curl-http3 curl --http3 -kv https://nginx/ping — работает.

К этому моменту мне удалось показать все сюрпризы, зашитые в bingo, и наиболее популярные способы работы с ними. Развёртывание приложения в облаке, автоматизацию развёртывания, наблюдаемость и даже ротацию логов я оставлю за скоупом разбора.
Многие просили у нас исходники, но мы приняли решение пока этого не делать. Мы много трудились при подготовке этого задания, поэтому хотим оставить себе возможность переиспользовать результаты в будущем.

Автор, тема не раскрыта! Как всё-таки нужно было разворачивать приложение? Я бы хотел взглянуть на эталонное решение.

На усмотрение участника. Выше я принял и обосновал решение поместить бинарник в контейнер. Как можно было заметить, это решение местами сильно облегчает жизнь. Сам контейнер bingo получился stateless, а значит, вполне хорошо годится к запуску в k8s. Это было бы отличным решением, будь у нас готовый кластер. Нормальный кластер потребует значительных в сравнении с bingo ресурсов: как железных, так и инженерочасов. Можно заиспользовать k3s. Docker swarm тоже выглядит приемлемым для этой задачи вариантом.

Я не рассматривал Nomad, потому что у меня нет большой экспертизы в нём, но подозреваю, что он тоже применим для этой задачи. Подходящим вариантом я считаю и запуск контейнеров на хостах без использования оркестраторов. При этом даже использование контейнеров необязательно.

Что касается эталонного решения, то у каждого из вариантов, перечисленных (и не перечисленных) выше, есть свои плюсы и минусы. Было бы совершенно неправильным с нашей стороны объявить один из них эталонным. Во-первых, эту статью наверняка будут читать участники следующих тренировок, и я бы не хотел таким образом склонять их к какому бы то ни было варианту. Во-вторых, среди этих вариантов нет однозначно лучшего. А это значит, что вопрос весьма холиварный. Я бы не хотел в этом месте разжигать горячий спор, ведь статья совсем не об этом.

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

Tags:
Hubs:
Total votes 41: ↑41 and ↓0+41
Comments13

Useful links

Что ты такое, dhclient?

Reading time19 min
Views37K
Total votes 223: ↑222 and ↓1+221
Comments61

Information

Website
www.ya.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия