Все новые проекты, включая разработанные на 1c-bitrix, мы в компании разворачиваем в Kubernetes. Изначально это кажется довольно сложным процессом, но, при должном подходе все проблемы в итоге решаемы и можно с уверенностью сказать — проекты на битриксе корректно разворачиваются и работают в инфраструктуре k8s.

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

Зачем вообще переезжать?

Миграция в «облака» дала нам несколько преимуществ по сравнению с классической схемой размещения проектов:

  • Инфраструктура описана как код. Кластер, образы, процессы CI\CD описаны в виде текста и дают возможность быстро изменять конфигурацию или поднимать проект
  • Масштабируемость. У нас есть возможность практически неограниченного горизонтального масштабирования приложения
  • Отказоустойчивость
  • Одинаковая конфигурация сред production, stage, local

В статье мы исходим из некоторых предпосылок, необходимых для успешного старта:

  • У вас есть DevOps, который поможет в конфигурировании кластера.
  • Максимально используем SaaS-сервисы — инстанс Mysql, хранилище S3, Git, EFK.
  • Внутри кластера развернут Redis.
  • Менеджеры сайта не редактируют файлы на prod-окружении, все изменения идут изнутри наружу, но не наоборот.
  • Весь код сайта, включая ядро, лежит в репозитории.
  • Битрикс обновлен до последней версии.

Итак, что нужно сделать и с чем придется столкнуться.

Кэш и сессии

Приложение в Kubernetes должно быть stateless, а значит нам нужно уйти от хранения сессий и кэша из файловой системы контейнеров.

Для этого в битриксе есть все необходимые средства — сессии и кэш мы можем перенести в Redis путем простого изменения конфига .settings.php. Подразумевается, что внутри кластера вы уже развернули и подготовили к работе Redis.

'session' => [
        'value' => [
            'mode' => 'default',
            'handlers' => [
                'general' => [
                    'type' => 'redis',
                    'port' => 6379,
                    'host' => 'redis.infra.svc.cluster.local', // хост redis
                    'serializer' => Redis::SERIALIZER_IGBINARY,
                    'persistent' => false,
                    'failover' => RedisCluster::FAILOVER_DISTRIBUTE,
                    'timeout' => null,
                    'read_timeout' => null,
                ],
            ],
        ]
    ],
    'cache' => [
        'value' => [
            'type' => [
                'class_name' => Bitrix\Main\Data\CacheEngineRedis::class,
                'extension' => 'redis'
            ],
            'redis' => [
                'port' => 6379,
                'host' => 'redis.infra.svc.cluster.local',
            ],
            'sid' => 'cache-' // если один redis будет использоваться на нескольких копиях проекта, например, stage-площадках, в sid нужно подмешать уникальный идентификатор - адрес сайта или подобное
        ],
    ],

Подробнее о вариантах хранения кэша и сессий вне файловой системы хорошо описано в документации.

Единственная сложность, которая может возникнуть при переносе в Redis — образ php должен быть собран с поддержкой igbinary. Например, так:

FROM php:8.0-fpm

# ...

# для корректной работы redis в битриксе собираем его с поддержкой igbinary
RUN pecl install igbinary && \
    # compile Redis with igbinary support
    pecl bundle redis && cd redis && phpize && ./configure --enable-redis-igbinary && make && make install && \
    docker-php-ext-enable igbinary redis && \
    docker-php-source delete && \
    rm -rf /var/www/*

RUN apt-get clean && apt-get autoclean

Статика и upload

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

Это же хранилище необходимо подключить в админке битрикса как место для загрузки статики через API\админку — документация.

Также следует взять за правило разработки отказ от загрузки файлов «навсегда» в файловую структуру сайта. Все сохраняем сразу в облако через метод SaveFile.

Проблема реплик и upload

При использовании количества реплик приложения больше одной (replicas: n) — есть проблема с загрузкой изображений через админку.

Битрикс «предзагружает» картинки перед сохранением сущности через админку. Причем делает это всегда в файловую систему сайта. Т.е. вы можете добавить картинку через админку, она загрузится в директорию TMP контейнера php, а хит сохранения обработает другой под, где этой картинки нет.

Решить эту проблему можно 2 способами:

  1. использовать постоянный том для директории TMP битрикса, по умолчанию это /upload/
  2. для работы в админке настроить sticky session, когда все запросы при работе с админкой будут роутиться в один и тот же под

Второй метод реализуется добавлением в конфиг развертывания ingress этих строк:

  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "bitrix"
    nginx.ingress.kubernetes.io/session-cookie-expires: "172800"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "172800"
    nginx.ingress.kubernetes.io/session-cookie-path: "/bitrix"

Kubernetes сам «прилипнет» к поду на основании куки bitrix в адресах, начинающихся с /bitrix.

Проблема размера ядра битрикса

Ядро установленного битрикса может занимать больше 1Gb места и содержать десятки тысяч файлов. Это доставляет множество проблем — время сборки образов, их размер, время развертывания.

Ниже несколько вариантов, каким образом можно организовать хранение ядра в k8s.

Вынести ядро в pv

Идея проста и понятна — создаем постоянное хранилище (например, CephFS или Glusterfs) и отгружаем туда все файлы из ядра. Затем его можно подключить ко всем репликам и контейнерам проекта.

Разнести ядро и код приложения в разные контейнеры

Идея такая — как можно сильнее уменьшить директорию /bitrix/ и монтировать ее ко всем подам во время деплоя.

Для этого нужно:

  • деинсталлировать лишние модули через админку
  • создать образ, который содержит только файлы ядра без ненужных модулей
  • при разворачивании проекта файлы ядра копировать в emptyDir-volume и монтировать ко всем контейнерам приложения
  • пересобирать образ только в случае изменения ядра или обновления битрикса

Чтобы создать образ без лишних файлов, нужно в директорию /bitrix/ положить файл .dockerignore с перечислением всего, что нужно исключить и запускать сборку в контексте этой директории.

Пример .dockerignore

/wizards/*
/modules/ldap
/modules/sender
/modules/pull
/modules/wiki
/modules/bizproc
/modules/blog
/modules/statistic
...

Пример deployment.yaml для монтирования ядра битрикса во все контейнеры

spec:
  template:
    spec:
      containers:
      - image: nginx:master_af78bc74
        volumeMounts:
        - mountPath: /var/www/public/bitrix
          name: bitrix-volume
      - image: phpfpm:master_af78bc74
        volumeMounts:
        - mountPath: /var/www/public/bitrix
          name: bitrix-volume
      initContainers:
      # перед запуском контейнеров готовим файлы ядра
      - args:
        - set -ex; cp -r /var/www/public/bitrix/. /tmp/
        command:
        - /bin/sh
        - -ce
        image: bitrix:latest # наш образ с битриксом
        name: prepare-dir
        resources: {}
        volumeMounts:
        - mountPath: /tmp
          name: bitrix-volume
      volumes:
      - emptyDir: {}
        name: bitrix-volume

Собирать «полные» образы приложения

На мой взгляд, это самый правильный и нативный для kubernetes путь деплоя приложения, если вас не пугает размер каждого образа под гигабайт:)

Чтобы все собиралось сильно быстрее, нужно создать образы php и nginx с ядром битрикса по вышеописанному методу, они будут обновляться крайне редко. А все production-образы собирать уже на их основе, добавляя кодовую базу приложения.

Параллельные запуски и мьютексы

Kubernetes не гарантирует запуск только одной копии пода, даже если указано количество реплик = 1. Это же касается команд, выполняемых через CronJob

A cron job creates a job object about once per execution time of its schedule. We say «about» because there are certain circumstances where two jobs might be created, or no job might be created. We attempt to make these rare, but do not completely prevent them. Therefore, jobs should be idempotent.

Задание cron создает объект задания примерно один раз за время выполнения своего расписания. Мы говорим «примерно», потому что при определенных обстоятельствах могут быть созданы два задания одновременно или ни одного. Мы пытаемся сделать подобные случаи редкими, но не предотвращаем полностью. Поэтому задания должны быть идемпотентными.

https://kubernetes.io/docs/concepts/workloads/controllers/cron-jobs/#cron-job-limitations

По этой причине во все скрипты\демоны\задания cron, требующие выполнения в единственном экземпляре, необходимо добавить защиту от параллельного запуска.

В 1с-битрикс есть нативная поддержка блокировок через базу данных:

use Bitrix\Main\Application;

// блокировка параллельного запуска 2х и более экземпляров скрипта
if (!Application::getConnection()->lock('unique_identifier')) {
    Log::debug('Не удалось получить блокировку для test', ['type' => 'lock error']);
    return;
}

Обработка SigTerm и SigQuit

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

Для этого в главный процесс каждого из контейнеров отправляется SigTerm (SigQuit) и через некоторое время (по умолчанию 30 секунд) SigKill для всего пода. Приложение должно уметь реагировать на эту команду и завершать выполнение, когда это необходимо.

В php обработка сигналов реализуется довольно просто. На примере cron-скрипта в стиле bitrix

<?php

declare(ticks=1);

use App\Facades\Log;

const NO_KEEP_STATISTIC = true;
const NOT_CHECK_PERMISSIONS = true;
const NEED_AUTH = false;
const NO_AGENT_CHECK = true;

$_SERVER["DOCUMENT_ROOT"] = __DIR__ . "/../../..";
$DOCUMENT_ROOT = $_SERVER["DOCUMENT_ROOT"];

require_once($_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php');

$service = new class () {
    /**
     * Флаг необходимости завершить выполнение
     * @var bool
     */
    private bool $stop = false;

    /**
     * Callback для получения сигнала
     * @param $code
     * @return void
     */
    public function stopHandler($code): void
    {
        Log::debug('Signal code: ' . $code);
        $this->stop = true;
    }

    /**
     * @return void
     */
    public function execute(): void
    {
        Log::debug('Pid: ' . getmypid());
        while (true) {
            if ($this->stop) {
                Log::debug("Я завершаю работу: " . date('H:i:s'));
                exit;
            }
            Log::debug("Я выполняюсь: " . date('H:i:s'));
            sleep(5);
        }
    }
};
/**
 * Установка обработчика на 3 сигнала - SIGTERM, SIGINT, SIGQUIT
 */
pcntl_signal(SIGTERM, [$service, 'stopHandler']);
pcntl_signal(SIGINT, [$service, 'stopHandler']);
pcntl_signal(SIGQUIT, [$service, 'stopHandler']);

$service->execute();

Запускаем в консоли скрипт и отправляем ему команду TERM

root@97a5e5b0a0c0:/var/www/public# php -f /var/www/public/local/php_interface/cron/test.php 
Pid: 23
Я выполняюсь: 21:19:36
Я выполняюсь: 21:19:41
Signal code: 15
Я завершаю работу: 21:19:45
root@97a5e5b0a0c0:/var/www/public# kill -15 23

Логирование

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

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

Поэтому в контейнеризованных приложениях принять отправлять логи в потоки stdout и stderr. В этом случае их можно будет посмотреть через kubectl logs или настроить, например, EFK — отправку в Elastic-Kibana через Fluentd (но этим займется ваш девопс:)

Битрикс по понятным причинам не умеет писать в стандартные потоки вывода. Чтобы не изобретать велосипед, будет полезно сразу установить нормальную систему логирования — пакет Monolog через composer.

Использовать его максимально просто:

<?php

use Monolog\Logger;

...

$logger = new Logger('mylogger');
$stream = new Monolog\Handler\StreamHandler('php://stdout', Monolog\Logger::DEBUG);
$stream->setFormatter(new Monolog\Formatter\LineFormatter("%datetime% > %level_name% > %message% %context% %extra%\n"));
$logger->pushHandler($stream);

$logger->debug('Debug message', ['alice' => 'bob']);

В логах контейнера это будет выглядеть как 2022-09-02T22:07:23.274666+03:00 > DEBUG > Debug message {"alice":"bob"} []

Установка обновлений

К сожалению, у 1c-bitrix нет механизма миграций и устанавливать обновления принято прямо на production-сервер сайта.

Нам не удалось придумать 100% хороший способ обновления сайта, но есть вариант с небольшим временем простоя:

  • тестируем обновление в режиме разработки на локальном или stage окружении
  • делаем бэкап production базы данных
  • закрываем публичную часть боевого сайта от посетителей
  • из локального окружения, где ядро и код сайта под git’ом, подключаемся к production БД
  • делаем обновление
  • коммитим файлы ядра и деплоим проект

Второй вариант, более рискованный, но без простоя:

  • тестируем обновление в режиме разработки на локальном или stage окружении
  • делаем бэкап production базы данных
  • уменьшаем количество реплик сайта до 1
  • делаем обновление в production-среде
  • делаем обновление из локального окружения, где ядро и код сайта под git’ом
  • коммитим файлы ядра и деплоим проект, возвращая нужное количество реплик

Еще один способ обновления описан на странице компании southbridge через специальный под с доступом к git’у и модулем в админке.


На этом все:)

Как оказалось, никаких сверхъестественных сложностей в процессе подготовки битрикса к k8s не возникает и все можно решить довольно простыми способами.

Удачных переездов!

Добавить комментарий

Ваш адрес email не будет опубликован.