[Перевод] Как мы увеличили скорость работы git-сервера на 30 000%

Примечание переводчика: На Хабре и в нашем блоге о корпоративном IaaS мы много пишем об облачных технологиях, и рассматриваем интересные инфраструктурные проекты различных компаний. Ранее мы рассказывали о масштабировании Apache Storm в Spotify, а сегодня представляем вашему вниманию адаптированный перевод заметки проекта Clever Cloud о повышении производительности git-сервера.

79e205697d4d4102a46ddd11e0de1c74.jpg

В Clever Cloud git является важной частью процесса разработки. Годы шли, а наши запросы увеличивались (в особенности те, которые касались производительности). Я расскажу вам, как мы решали возникающие перед нами задачи.

Немного предыстории


Когда мы основали Clever Cloud, то для управления репозиториями Git выбрали gitolite. Эта утилита казалась нам готовым функциональным решением. Сначала мы использовали его для управления нашими внутренними репозиториями до выпуска продукта.

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

Управление конфигурацией gitolite


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

По достаточно очевидным причинам нам нужна была возможность автоматического обновления. Поэтому мы создали инструмент под названием etilotig, который должен был следить, чтобы конфигурация git всегда была актуальной. Сначала утилита загружает начальную конфигурацию из API, а затем «прослушивает» AMQP, чтобы обновить кэш и записать новую конфигурацию gitolite.

Такая схема работала достаточно долго, даже дольше чем мы ожидали — мы использовали её вплоть до 5 мая 2015 года.

Недостатки gitolite


Gitolite был отличным решением, но имел несколько значительных недостатков, которые нас не устраивали:

  • Как было сказано ранее, его конфигурация не могла изменяться динамически, поэтому приходилось прибегать к некоторым нетривиальным решениям;
  • Требовалось сохранять конфигурацию в API и gitolite одновременно;
  • Все репозитории создавались в одной директории;
  • Каждый раз, создавая новый репозиторий, gitolite выполнял полный проход по уже существующим, просматривая их на наличие перехватчиков, требующих обновления;
  • Переделка части конфигурации была совсем непростой задачей, поэтому с каждым изменением переписывался практически весь конфигурационный файл.


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

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

Новый etilotig


Хотя gitolite вполне справлялся с возложенными на него задачами, все-таки у него стали возникать проблемы с производительностью: в зависимости от времени и условий выполнения, создание нового репозитория могло занимать 5 минут (!). Такая задержка выходила далеко за рамки разумного, поэтому нам пришлось искать новое решение.

Идея была простой: полностью исключить gitolite и улучшить наш управляющий конфигурацией инструмент etilotig, чтобы он самостоятельно делал то, что требуется.

Список необходимых задач, был довольно короткий:

  • Управление SSH-ключами
  • Управление авторизацией
  • Создание репозиториев
  • Установка перехватчиков


Управление SSH-ключами


Когда etilotig управлял только конфигурацией, то он лишь перенаправлял SHH-ключи gitolite, который, в свою очередь, их обрабатывал. Теперь нам нужно было самостоятельно обрабатывать файл authorized_keys.

Каждый раз, когда добавлялся или удалялся SSH-ключ, мы создавали новый файл с ключами и заменяли им старый.
Каждая строка выглядела так:

command="AUTH_SCRIPT USER_ID",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty PUBLIC_KEY

Здесь AUTH_SCRIPT — это скрипт авторизации, о котором я скажу позднее, USER_ID — это id пользователя, а PUBLIC_KEY — его публичный ключ.
Скрипт авторизации вызывается с id пользователя в качестве первого параметра.

Это решает первую проблему в нашем небольшом списке.

Управление репозиториями


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

После этого в дело вступает небольшой bash-скрипт, создающий репозиторий. Теперь репозитории создаются глубже в иерархии файловой системы, чтобы в каждой директории их было как можно меньше. Скажем, мы имели: /data/app_18c6021b-0860-4f97-a08d-0663f45cf3f0.git, теперь имеем: /data/app_18/c6/02/app_18c6021b-0860-4f97-a08d-0663f45cf3f0.git, поэтому все работает намного быстрее.

#!/bin/bash

create_repo() {
    local repo_dir="${1}"

    mkdir -p "${repo_dir}"
    pushd "${repo_dir}" &>/dev/null
    git init --bare
    popd &>/dev/null
}

main() {
    local repo="${1}"
    local repo_dir
    
    repo_dir="${REPOS_DIR}/${repo:0:6}/${repo:6:2}/${repo:8:2}/${repo}"

    [[ -d "${repo_dir}"/hooks ]] || create_repo "${repo_dir}"
}

. "${HOME}"/.etilotig/.etilotigrc

main "${@}"


Еще у нас есть скрипт для установки перехватчиков.

#!/bin/bash

shopt -s nullglob

main() {
    local repo="${1}"
    local repo_dir
    
    repo_dir="${REPOS_DIR}/${repo:0:6}/${repo:6:2}/${repo:8:2}/${repo}"

    for hook in ${HOME}/.etilotig/hooks/*; do
        ln -sf "${hook}" "${repo_dir}"/hooks/
    done
}

. "${HOME}"/.etilotig/.etilotigrc

main "${@}"


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

Авторизация


Теперь нужно было решить вопрос с дублированием, то есть использовать конфигурацию только из API. Когда мы удалили из etilotig все, что относилось к управлению конфигурацией, его размер уменьшился более чем на 50%.

Управление авторизацией в etilotig реализовано довольно просто: при создании внутренней конфигурации генерируется скрипт на Perl, который сверяет SSH-ключ при каждой попытке подключения. Если ключ верный, то транзакция разрешается, иначе — запрещается.

Мы написали похожий скрипт, который выводит пользователям информацию о том, к каким репозиториям у них есть доступ. Для этого им нужно ввести что-то вроде ssh git@push.par.clever-cloud.com. Скрипт разрешает выполнение операции при попытке ввести git push/pull, если пользователь авторизован, и отклоняет, если — нет.

Он выглядит вот так (сюда добавлены еще несколько функций):

#!/bin/bash

sanity_check() {
    if [[ -z "${SSH_CONNECTION}" ]]; then
        echo "Who the hell are you?" >&2
        exit 1
    fi

    if [[ -z "${SSH_ORIGINAL_COMMAND}" ]]; then
        export SSH_ORIGINAL_COMMAND="info"
    fi
}

ask_for_info() {
    local userid="${1}"

    # make the request to the API to retrieve user info message
    echo "some info"
}

ask_for_authorization() {
    local userid="${1}"
    local appid="${2}"

    # make the request and return the HTTP status code here. 200 means authorized.
    echo 200
}

authorize() {
    local userid="${1}"
    local verb="${2}"
    local appid="${3}"
    local ret=1

    case "${verb}" in
        "git-receive-pack"|"git-upload-pack")
            local code

            code=$(ask_for_authorization "${userid}" "${appid}")

            [[ "${code}" == "200" ]] && ret=0
            ;;
    esac

    return "${ret}"
}

final_abort() {
    echo "What are you trying to achieve here?" >&2
    exit 2
}

main() {
    sanity_check

    local userid="${1}"
    local verb
    local repo
    local repo_dir

    verb=$(echo "${SSH_ORIGINAL_COMMAND}" | cut -d ' ' -f 1)
    repo=$(echo "${SSH_ORIGINAL_COMMAND}" | cut -d ' ' -f 2 | tr -d "'\"")
    [[ "${repo}" == /* ]] && repo=${repo:1}
    repo_dir="${REPOS_DIR}/${repo:0:6}/${repo:6:2}/${repo:8:2}/${repo}"

    if [[ "${verb}" == "info" ]]; then
        ask_for_info "${userid}"
    elif authorize "${userid}" "${verb}" "${repo}"; then
        export CC_USER="${userid}"
        export CC_NOTIFY_SCRIPT="${HOME}/.etilotig/send-push-event"
        exec "${verb}" "${repo_dir}"
    else
        final_abort
    fi
}

. "${HOME}"/.etilotig/.etilotigrc

main "${@}"


Теперь практически все готово. Два небольших перехватчика вверху нужны для того, чтобы позволить пользователям отправлять на сервер только ветку master и запускать развертывание по команде git push:

hooks/update
#!/bin/bash

main() {
    local rev="${1}"

    if [[ "${rev}" == refs/tags/* ]]; then
        exit 0
    fi

    if [[ "${rev}" != "refs/heads/master" ]]; then
        echo "You tried to push to a custom branch."
        echo "Only master is allowed."
        exit 1
    fi
}

main "${@}"
hooks/post-update
#!/bin/bash

sanity_check() {
    local rev="${1}"

    if [[ "${rev}" == refs/tags/* ]]; then
        exit 0
    fi
}

main () {
    local rev="${1}"

    sanity_check "${rev}"

    local repo=$(basename $(pwd))
    local appId=${repo/.git/}
    local commitId=$(git rev-parse "${rev}")

    "${CC_NOTIFY_SCRIPT}" "${appId}" "${commitId}" "${CC_USER}"

    echo "[SUCCESS] The application has successfully been queued for redeploy."
}

main "${@}"


Заключение


Вот и все, теперь у нас есть новый менеджер git-сервера, который неплохо справляется со своими задачами.

Насколько увеличилась производительность? Время обработки одного действия составляет менее секунды (было 3–5 минут), размер etilotig уменьшился на 50%, из-за удаления кодовой базы gitolite, а средний прирост производительности составил 30000%.

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

Gitolite — мертв, да здравствует etilotig!

© Habrahabr.ru