company_banner
6 ноября 2013 в 12:56

GitPHP в Badoo

Badoo — это проект с гигантским git-репозиторием, в котором есть тысячи веток и тегов. Мы используем сильно модифицированный GitPHP (http://gitphp.org) версии 0.2.4, над которой сделали множество надстроек (включая интеграцию с нашим workflow в JIRA, организацию процесса ревью и т.д.). В целом нас этот продукт устраивал, пока мы не стали замечать, что наш основной репозиторий открывается более 20 секунд. И сегодня мы расскажем о том, как мы исследовали производительность GitPHP и каких результатов добились, решая эту проблему.

Расстановка таймеров


При разработке badoo.com в девелоперском окружении мы используем весьма простую debug-панель для расстановки таймеров и отладки SQL-запросов. Поэтому первым делом мы переделали ее в GitPHP и стали измерять время выполнения участков кода, не учитывая вложенные таймеры. Вот так выглядит наша debug-панель:



В первой колонке находится имя вызываемого метода (или действия), во второй — дополнительная информация: аргументы для запуска, начало вывода команды и trace. В последнем столбце находится потраченное на вызов время (в секундах).

Вот небольшая выдержка из реализации самих таймеров:

<?php
class GitPHP_Log {
// ...
    public function timerStart() {
        array_push($this->timers, microtime(true));
    }

    public function timerStop($name, $value = null) {
        $timer = array_pop($this->timers);
        $duration = microtime(true) - $timer;
        // Вычтем потраченное время из всех таймеров, которые включают этот таймер
        foreach ($this->timers as &$item) $item += $duration;
        $this->Log($name, $value, $duration);
    }
// ...
}

Использование такого API очень простое. В начале измеряемого кода вызывается timerStart(), в конце — timerStop() с именем таймера и опциональными дополнительными данными:

<?php
$Log = new GitPHP_Log;
$Log->timerStart();

$result = 0;
$mult = 4;
for ($i = 1; $i < 1000000; $i+=2) {
    $result += $mult / $i;
    $mult = -$mult;
}

$Log->timerStop("PI computation", $result);

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

Для более легкой отладки кода внутри Smarty мы сделали «автотаймеры». Они позволяют легко измерять время, потраченное на работу методов с множеством точек выхода (много мест, где выполняется return):

<?php
class GitPHP_DebugAutoLog {
        private $name;
        public function __construct($name) {
                $this->name = $name;
                GitPHP_Log::GetInstance()->timerStart();
        }

        public function __destruct() {
                GitPHP_Log::GetInstance()->timerStop($this->name);
        }
}

Использовать такой класс очень просто: нужно вставить $Log = new GitPHP_DebugAutoLog(‘timer_name’); в начало любой функции или метода, и при выходе из функции будет автоматически измерено время ее исполнения:

<?php
function doSomething($a) {
    $Log = GitPHP_DebugAutoLog('doSomething');
    if ($a > 5) {
        echo "Hello world!\n";
        sleep(5);
        return;
    }
    sleep(1);
}

Тысячи вызовов git cat-file -t <commit>


Благодаря расставленным таймерам мы быстро смогли найти, где GitPHP версии 0.2.4 тратил большую часть времени. На каждый тег в репозитории делался один вызов git cat-file -t только для того, чтобы узнать тип коммита, и является ли этот коммит «легковесным тегом» (http://git-scm.com/book/en/Git-Basics-Tagging#Lightweight-Tags). Легковесные теги в Git — это тип тега, который создается по умолчанию и содержит ссылку на конкретный коммит. Поскольку в нашем репозитории никакие другие типы тегов не присутствовали, мы просто убрали эту проверку и сэкономили пару тысяч вызовов git cat-file -t, занимавших около 20 секунд.

Как так получилось, что GitPHP нужно было для каждого тега в репозитории узнавать, является ли он «легковесным»? Все довольно просто.

На всех страницах GitPHP рядом с коммитом выводятся ветки и теги, которые на него указывают:



Для этого в классе GitPHP_TagList есть метод, который отвечает за получение списка тегов, ссылающихся на указанный коммит:

<?php
class GitPHP_TagList extends GitPHP_RefList {
// ...
        public function GetCommitTags($commit) {
                if (!$commit) return array();
                $commitHash = $commit->GetHash();
                if (!$this->dataLoaded) $this->LoadData();
                $tags = array();
                foreach ($this->refs as $tag => $hash) {
                        if (isset($this->commits[$tag])) {
                                // ...
                        } else {
                                $tagObj = $this->project->GetObjectManager()->GetTag($tag, $hash);
                                $tagCommitHash = $tagObj->GetCommitHash();
                                // ...
                                if ($tagCommitHash == $commitHash) {
                                        $tags[] = $tagObj;
                                }
                        }
                }
                return $tags;
        }
// ...
}

Т.е. для каждого коммита, для которого нужно получить список тегов, выполняется следующее:

  1. При первом вызове загружается список всех тегов в репозитории (вызов LoadData()).
  2. Перебирается список всех тегов.
  3. Для каждого тега загружается соответствующий ему объект.
  4. Вызывается GetCommitHash() у объекта тега и полученное значение сравнивается с искомым.

Помимо того что можно сначала составить карту вида array( commit_hash => array(tags) ), нужно обратить внимание на метод GetCommitHash(): он вызывает метод Load($tag), который в реализации с использованием внешней утилиты Git делает следующее:

<?php
class GitPHP_TagLoad_Git implements GitPHP_TagLoadStrategy_Interface {
// ...
        public function Load($tag) {
// ...
                $args[] = '-t';
                $args[] = $tag->GetHash();
                $ret = trim($this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args));
                
                if ($ret === 'commit') {
// ...
                        return array(/* ... */);
                }
// ...
                $ret = $this->exe->Execute($tag->GetProject()->GetPath(), GIT_CAT_FILE, $args);
// ...
                return array(/* ... */);
        }
}

Т.е. чтобы показать, какие ветки и теги входят в какой-либо коммит, GitPHP загружает список всех тегов и вызывает git cat-file -t для каждого из них. Неплохо, Кристофер, так держать!

Сотни вызовов git rev-list --max-count=1 … <commit>


Аналогичная ситуация и с информацией о коммите. Чтобы загрузить дату, сообщение коммита, автора и т.д, каждый раз вызывался git rev-list --max-count=1 … <commit>. Эта операция тоже не является бесплатной:

<?php
class GitPHP_CommitLoad_Git extends GitPHP_CommitLoad_Base {
        public function Load($commit) {
// ...
                /* get data from git_rev_list */
                $args = array();
                $args[] = '--header';
                $args[] = '--parents';
                $args[] = '--max-count=1';
                $args[] = '--abbrev-commit';
                $args[] = $commit->GetHash();
                $ret = $this->exe->Execute($commit->GetProject()->GetPath(), GIT_REV_LIST, $args);
// ...
                return array(
// ...
                );
        }
// ...
}

Решение: пакетная загрузка коммитов (git cat-file --batch)


Для того чтобы не делать много одиночных обращений к git cat-file, Git позволяет загружать сразу много коммитов с использованием опции --batch. При этом он принимает список коммитов в stdin, а результат записывает в stdout. Соответственно, можно сначала записать в файл все хэши коммитов, которые нам нужны, запустить git cat-file --batch и загрузить сразу все результаты.

Вот пример кода, который это делает (код приведен для версии GitPHP 0.2.4 и операционных систем семейства *nix):

<?php
class GitPHP_Project {
// ...
    public function BatchReadData(array $hashes) {
        if (!count($hashes)) return array();
        $outfile = tempnam('/tmp', 'objlist');
        $hashlistfile = tempnam('/tmp', 'objlist');
        file_put_contents($hashlistfile, implode("\n", $hashes));
        $Git = new GitPHP_GitExe($this);
        $Git->Execute(GIT_CAT_FILE, array('--batch', ' < ' . escapeshellarg($hashlistfile), ' > ' . escapeshellarg($outfile)));
        unlink($hashlistfile);
        $fp = fopen($outfile, 'r');
        unlink($outfile);

        $types = $contents = array();
        while (!feof($fp)) {
            $ln = rtrim(fgets($fp));
            if (!$ln) continue;
            list($hash, $type, $n) = explode(" ", rtrim($ln));
            $contents[$hash] = fread($fp, $n);
            $types[$hash] = $type;
        }

        return array('contents' => $contents, 'types' => $types);
    }
// ...
}

Мы стали использовать эту функцию для большей части страниц, где показывается информация о коммитах (т.е. мы собираем список коммитов и загружаем их все одним вызовом git cat-file --batch). Такая оптимизация сократила среднее время загрузки страницы с 20 с лишним секунд до 0,5 секунды. Таким образом мы решили проблему медленной работы GitPHP в нашем проекте.

Open-source: оптимизации GitPHP 0.2.9 (master)


Немного подумав, мы поняли, что можно было не переписывать весь код для использования git cat-file --batch. Хоть это и не отражено в документации, эта команда позволяет загружать информацию по одному коммиту за раз, не теряя в производительности! Во время работы производится чтение по одной строке из стандартного ввода и отправка результатов в стандартный вывод без их буферизации. Это означает, что мы можем открыть git cat-file --batch через proc_open() и получать результаты немедленно, без переделывания архитектуры!

Вот выдержка из реализации (для удобства чтения обработка ошибок убрана):

<?php
// ...
class GitPHP_GitExe implements GitPHP_Observable_Interface {
// ...
        public function GetObjectData($projectPath, $hash) {
                $process = $this->GetProcess($projectPath);
                $pipes = $process['pipes'];
                $data = $hash . "\n";
                fwrite($pipes[0], $data);
                fflush($pipes[0]);

                $ln = rtrim(fgets($pipes[1]));
                $parts = explode(" ", rtrim($ln));
                list($hash, $type, $n) = $parts;
                $contents = '';
                while (strlen($contents) < $n) {
                        $buf = fread($pipes[1], min(4096, $n - strlen($contents)));
                        $contents .= $buf;
                }

                return array(
                        'contents' => $contents,
                        'type' => $type,
                );
        }
// ...
}

Учитывая, что мы теперь можем очень быстро загружать содержимое объектов, не делая каждый раз вызов команды git, получить большой прирост производительности стало просто: достаточно лишь поменять все вызовы git cat-file и git rev-list на вызов нашей оптимизированной функции.

Мы собрали все изменения в один коммит и отправили pull-request разработчику GitPHP. Через какое-то время патч приняли! Вот этот коммит:

source.gitphp.org/projects/gitphp.git/commitdiff/3c87676b3afe4b0c1a1f7198995cecc176200482

Автором были внесены некоторые исправления в код (отдельными коммитами), и сейчас в ветке master находится значительно ускоренная версия GitPHP! Для использования оптимизаций требуется выключить «режим совместимости», то есть поставить $compat = false; в конфигурации.

Юрий youROCK Насретдинов, PHP-разработчик, Badoo
Евгений eZH Махров, QA-инженер, Badoo
Автор: @Badoo
Badoo
рейтинг 415,46
Похожие публикации

Вакансии компании Badoo

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

  • –7
    проект с гигантским git-репозиторием, в котором есть тысячи веток и тегов

    Удалить ненужное не?
    • +10
      Дело в том, что у нас используется по ветке на тикет, и в каждый момент времени количество открытых тикетов достигает тысяч (какое-то количество тикетов просто открыто и ещё не зарезолвлено, какое-то — в QA, разработчиков у нас около 100 :)).
      • –3
        И зачем вам такие приблуды: GitPHP и самописные профайлеры? Git создан для коммандной строки. А для профилирования есть Xdebug и KCachegrind
        • +6
          Одна из основных вещей, для которой мы используем GitPHP — это процесс ревью кода. В нашей версии добавлена функциональность, позволяющая в удобном виде смотреть дифф по ветке (заточенная под наш workflow, то есть, задачи стартуют от мастера и «живут» в своих ветках).

          Таймеры а-ля pinba позволяют легко отлаживать времена обращений к внешним сервисам. Внутри компании для профилирования мы всё же предпочитаем использовать XHProf, но по умолчанию страницы не профилируются. Такой простенький профилировщик на каждой странице позволяет легко видеть узкие места, когда дело касается вызова утилиты git. XHProf или XDebug выдают несравненно больше информации, которую нужно отдельно анализировать. Pinba-like таймеры и XHProf не отменяют друг друга.
          • –1
            Просмотреть все изменения в ветке some_feature можно командой git diff master...some_feature.

            git difftool — возможность ревью. Можно использовать любой графический инструмент слияния и просмотра изменений от meld и до kdiff3. Возможности PHPGit и рядом не стояли.

            Преждевременная оптимизация источник всех бед (с). Оптимизацией нужно заниматься используя соотвествующий инструментарий, в нужное время и нужными людьми.
            • +1
              Безусловно diff с тремя точками покажет изменения по ветке, в оригинальном GitPHP этой возможности нет, вот мы и добавили один простой вызов.

              Нас вполне устраивает вид unified диффа в браузере + наша подсветка изменений символов в строке. Каждый сотрудник использует удобную ему операционную систему и GitPHP работает везде одинаково и требует только браузер, также никто не запрещает использовать другие средства, такие как meld или встроеный в phpstorm diff viewer. Но это только просмотр, в нашей версии есть возможность тут же написать комментарии и по окончании ревью уведомить разработчика и возможно обновить статус задачи в баг-трекере.

              GitPHP очень простой инструмент и делает не больше чем вызов программы git с нужными параметрами.
              Необходимость оптимизации настала, очень тоскливо ждать загрузки страницы с бранчдиффом по 20+ секунд. Инструмент уже был встроен в GitPHP, мы его только улучшили и поделились с автором.
      • –7
        У меня, честно говоря, все эти истории про бадушную инфраструктуру вызывают всего одну мысль — «Мыши плакали, кололись, но продолжали жрать фичебранчи, вместо того чтобы организовать нормальный continious delivery».
      • –1
        Хорошо. Допустим вы на каждый чих заводите тикет и ветку. Это нормально. Стандартная процедура для большой компании, где все проверяется долго и нудно.

        С ваших слов я примерно прикидываю что у ва 2000-3000 веток на 100 разработчиков, то есть в среднем 20-30 на одного. Пусть 20. 20 мелких задач, 20 веток. Я не верю что 20 задач у разработчика никак не связаны с собой. Так не бывает, мы живем в реальном мире, тикеты приходят пачками на один тип задачи. В рамках одной ветки(задачи) они и делаются. И все это в одной ветке и тестируется и выкатывается.

        А вся эта возьня с ветками просто похоже на видимость работы. Любой разработчик иногда прикрывается тикетами, не надо этого скрывать, это нормально. Просто со временем видимость работы, которая создается иммено тикетами превращается в работу и человек думает что он правда работает. Хотя на самом деле это все не так. Разработчику любого уровня всего-то и надо — просто х@$рить код.
        • +3
          Разделение на мелкие ветки не означает, что мы не можем, скажем, мержить эти ветки между собой, если задачи между собой сильно связаны :). Базовый паттерн «ветка на фичу» позволяет комбинировать ветки, как нам удобно, потому что они уже разбиты на довольно мелкие кусочки. Если задачи между собой связаны по коду, то мы выкладываем сразу все эти задачи, в этом вы правы.

          Я считаю, что вводить ограничения на количество веток (или тикетов) только из соображений производительности только _одного_ из инструментов, который мы используем — это как-то странно :).

          В целом после перехода с SVN (и разработки в trunk) на фиче-ветки стало намного проще отслеживать статус задач и тестировать сами задачи. Проблемы с производительностью мы способны решить сами :). Архитектура GitPHP весьма простая и легко поддается оптимизации, благо опыт у каждого из наших PHP-разработчиков в этом более, чем достаточный.

          В команде мобильной разработки, например, у нас используется немного другой workflow, о котором мы рассказывали на HighLoad++ (см. доклад Владислава Чернова). Для веба наш текущий workflow нас устраивает.
          • –6
            Feature Branching is a poor man's modular architecture, instead of building systems with the ability to easy swap in and out features at runtime/deploytime they couple themselves to the source control providing this mechanism through manual merging.

            — Dan Bodart

            Давайте будем честными.

            ИМХО, скорее всего у вас сильносвязанный код с плохо изолированными модулями и поэтому вы изолируете изменения в отдельных ветках, потому что не можете честно включать/выключать фичи при деплое (как это делает гитхаб, например). А еще вы скорее всего толком не рефакторите, так как при любом серьезном рефакторинге вы будете постоянно влетать в семантические конфликты и вся автоматизация мержей не будет давать никакого профита. Ну и, скорее всего, вы регулярно ловите интеграционные баги в, казалось бы, уже оттестированных задачах на продакшне.

            Ни в коем разе не отменяя ваш вклад в открытые продукты, вместо разруливания реальных проблем — вы тратите ресурсы на прикрывание чьих-то организационных и архитектурных факапов автоматизируя бардак. Да еще и гордитесь этим ;(

            Комментатор выше абсолютно прав — это видимость работы.
    • +19
      не тру. репо должен шевелиться с любым вероятным кол-вом веток и тегов. Badoo молодцы, спасибо за вклад в Open Source!
      • –2
        Лень всему оправдание.
  • +1
    Я уж было удивился, что в китайском поисковике badoo работают наши программисты. Потом сходил в google и понял что поисковик — это baidu.
    • +6
      спасибо! до сих пор думал, что это одна и та же компания.
    • +1
      Только аккуратней, а то вдруг все-таки в поисковике тоже работают наши! обидятся)
      • 0
        А на что обижаться? Я же не сказал что это плохо, я просто удивился.
      • 0
        Хотя конечно работать в «байде» это прикольно.
  • 0
    У меня к badoo такой личный вопрос, не относящийся к топику:

    Когда рапортуешь, что в некой анкете фотография чужая (обычно знаменитости), требуется указать имя изображённого. Для этого приходится через задницу выуживать фото (простые методы вами заблокированы), чтобы впихнуть в google images или tineye и, наконец, идентифицировать эту знаменитость.

    В результате badoo никаких действий по поводу данной анкеты не предпринимает.

    Вопрос: нахрена это всё в таком случае?!
    • 0
      Подскажите ваш аккаунт (в пм) — посмотрим кого вы репортили и почему с ним ничего не сделали.
    • +1
      ты решил на меня теперь здесь пожаловаться??? мои это фотки, я отвечаю!!!
    • 0
      На самом деле можно просто написать слово знаменитость, если уж вы не помните имя — это не критично, а далее наши модераторы проверят фотку на принадлежность какой-либо знаменитости и уберут её с сайта.
      Но в скором времени не нужно будет вводить никакой текст. Так что всё меняется, мы развиваемся и стараемся упрощать подобные процессы.

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

Самое читаемое Разработка