Предлагаю вашему вниманию перевод небольшой статьи, в которой инженеры GitLab рассказывают как их приложение работает на Unicorn и что они делают с памятью, которая течет. Эту статью можно рассматривать как упрощенную версию уже переведенной на хабре статьи другого автора.

Unicorn


Для обработки HTTP запросов от git и пользователей GitLab использует Unicorn, Ruby сервер с prefork. Unicorn — это демон, написанный на Ruby и C, который может загружать и выполнять Ruby on Rails приложение, в нашем случае — GitLab Community Edition или GitLab Enterprise Edition.

Unicorn имеет мультипроцессную архитектуру для поддержки многоядерных систем (процессы могут выполняться параллельно на разных ядрах) и для отказоустойчивости (аварийно завершившийся процесс не приводит к завершению GitLab). При запуске основной процесс Unicorn загружает в память Ruby и Gitlab, после чего запускает некоторое количество рабочих процессов, которые наследуют этот «начальный» слепок памяти. Основной процесс Unicorn не обрабатывает входящие запросы — это делают рабочие процессы. Сетевой стек операционной системы получает входящие подключения и распределяет их по рабочим процессам.

В идеальном мире основной процесс один раз запускает пул рабочих процессов, которые затем обрабатывают входящие сетевые подключения до скончания веков. На практике рабочие процессы могут аварийно завершиться или же быть убиты по превышению времени ожидания. Если основной процесс Unicorn обнаруживает, что один из рабочих процессов обрабатывает запрос слишком долго, то он убивает этот процесс с помощью SIGKILL (kill -9). Вне зависимости от того, как завершился рабочий процесс, основной процесс заменит его на новый, который наследует все то же «начальное» состояние. Одна из особенностей Unicorn — возможность заменять дефектные рабочие процессы не обрывая сетевых подключений с запросами пользователей.

Пример таймаута рабочего процесса, который можно найти в unicorn_stderr.log. Идентификатор основного процесса 56227:

[2015-06-05T10:58:08.660325 #56227] ERROR -- : worker=10 PID:53009 timeout (61s > 60s), killing
[2015-06-05T10:58:08.699360 #56227] ERROR -- : reaped #<Process::Status: pid 53009 SIGKILL (signal 9)> worker=10
[2015-06-05T10:58:08.708141 #62538]  INFO -- : worker=10 spawned pid=62538
[2015-06-05T10:58:08.708824 #62538]  INFO -- : worker=10 ready


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

unicorn-worker-killer


В GitLab есть утечки памяти. Эти утечки проявляются в долго работающих процессах, в частности — в рабочих процессах, создаваемых Unicorn (при этом в основном процессе Unicorn таких утечек нет, так как он не обрабатывает запросов).

Для борьбы с этими утечками памяти GitLab использует unicorn-worker-killer, который модифицирует рабочие процессы Unicorn чтобы они проверяли использование памяти через каждые 16 запросов. Если объем используемой памяти рабочего процесса превышает установленный лимит, то процесс завершается и основной процесс Unicorn автоматически заменяет его на новый.

На самом деле это неплохой способ борьбы с утечками памяти, так как дизайн Unicorn позволяет не терять запрос пользователя при завершении рабочего процесса. Более того, unicorn-worker-killer завершает процесс между обработкой запросов, так что это никак не отражается на работе с ними.

Вот так в файле unicorn_stderr.log выглядит рестарт рабочего процесса по причине утечки памяти. Как можно видеть, процесс с идентификатором 125918 после самоанализа принимает решение завершиться. Пороговое значение памяти при этом составляет 254802235 байт, то есть порядка 250 мегабайт. GitLab использует в качестве порогового значения случайное число в диапазоне от 200 до 250 мегабайт. Основной процесс GitLab с идентификатором 117565 затем создает новый рабочий процесс с идентификатором 127549:

[2015-06-05T12:07:41.828374 #125918]  WARN -- : #<Unicorn::HttpServer:0x00000002734770>: worker (pid: 125918) exceeds memory limit (256413696 bytes > 254802235 bytes)
[2015-06-05T12:07:41.828472 #125918]  WARN -- : Unicorn::WorkerKiller send SIGQUIT (pid: 125918) alive: 23 sec (trial 1)
[2015-06-05T12:07:42.025916 #117565]  INFO -- : reaped #<Process::Status: pid 125918 exit 0> worker=4
[2015-06-05T12:07:42.034527 #127549]  INFO -- : worker=4 spawned pid=127549
[2015-06-05T12:07:42.035217 #127549]  INFO -- : worker=4 ready


Что еще бросается в глаза при изучении этого лога: рабочий процесс обработал всего 23 запроса, прежде чем завершиться из-за утечек памяти. На данный момент это является нормой для gitlab.com

Такой частый рестарт рабочих процессов на серверах GitLab может стать причиной беспокойства для сисадминов и devops, но на практике это чаще всего нормальное поведение.

Комментарии (15)


  1. Prototik
    10.11.2015 09:59
    +4

    рабочий процесс обработал всего 23 запроса

    Всё же, он обработал не всего 23 запроса, а вообще работал 23 секунды, за которые мог успеть обработать запросов гораздо больше. Если бы какое либо приложение на моем сервере бы рестартилось каждые 23 запроса (пусть и без разрывов связи) — я бы снёс такое приложение не задумываясь.

    Мой GitLab живёт по 3-4 часа на воркер и обрабатывает несколько сотен тысяч запросов за это время.


  1. slonopotamus
    10.11.2015 10:21
    +7

    Я извиняюсь, а какие такие религиозные причины мешают просто взять и починить memory leak'и?


    1. Caravus
      10.11.2015 10:41
      +2

      Я вот честно сказать тоже не понимаю. Везде где вижу упоминания про этот unicorn — везде пишут про проблемы с памятью, но при этом продолжают пользоваться. Мыши кололись но продолжали жрать кактус? Вместо того чтоб починить иточник проблемы — придумали костыли, которые решают проблему с памятью за счёт нагрузки на ЦП.


    1. FractalizeR
      10.11.2015 10:58

      Вероятно, это не так уж и просто. Не думаю, что просто так вот взяли и не починили.


    1. vladon
      10.11.2015 11:01

      Ruby?


      1. slonopotamus
        10.11.2015 11:06

        А что Ruby? Для Ruby нет инструментов для анализа memory dump'ов?


        1. vladon
          10.11.2015 11:09

          Ну, это ответ на «какие религиозные причины?».


    1. creker
      10.11.2015 11:47

      Пытались и неоднократно, как они пишут. Исправить не получилось до их пор, поэтому костыль.


  1. printercu
    10.11.2015 11:32
    +1

    Зачем еще один перевод? Еще остались пользователи юникорна, которые не знают про worker-killer? Нового ничего они не предлагают. Если пропустить через s/gitlab/other-unicorn-user/g, то всё так же останется правдой.


  1. tzlom
    10.11.2015 11:43

    Это не unicorn, это руби.Gitalab CE нельзя запустить на 5$ DO instance потому что не влазит в память при загрузке (конечно можно своп но сервер свопящийся уже на старте это грустно)


    1. IDMan
      10.11.2015 12:19
      +1

      Я запустил на 10$ инстансе Linode — это примерно вдвое лучше по характеристикам, и он постоянно валится в 500-ю ошибку, в логах «не хватает памяти», и спасает только перезапуск сервиса. Причем работаю на инстансе только я один, максимум двое. Такая требовательность поражает.


      1. Sky4eg
        10.11.2015 12:51

        В требованиях у GitLab от 2Gb памяти


        1. baldr
          10.11.2015 13:25

          А вот просто интересно — наюя ему столько? Это же не Java… Что он делает с этой памятью? Это же web-сервер, по большей части…


          1. norguhtar
            11.11.2015 08:59

            Вы таки не поверите, но для Java 2 гига это весьма вольготно :)


        1. IDMan
          10.11.2015 16:02

          2GB поражают еще больше :)