Одним из важным инструментом разработчика, в не зависимости от языка (и религиозных убеждений), является система контроля версий (VCS). И практически промышленным стандартом стала такая распределенная система как GIT. В повседневной работе мы (разработчики, DevOps инженеры, технические писатели и все причастные) используем ее чтобы нести людям добро и свет объединять усилия команд в работе над нашими проектами. И все давно уже выучили «на зубок» основные команды (если не выучили то бегом учить, тут есть отличная книжка) и превратили в рутину то что совсем недавно (олды тут?) казалось гениальным, сложным, а кому то магическим. А современные IDE еще больше нам упростили жизнь, спрятав от нас командную строку и git команды, заменив на возможность кликать мышкой. Но постойте, разве вам в детстве не было интересно понять как та или иная игрушка устроена внутри, как работает холодильник или мотор в папиных жигулях (олды все же тут?)? Так вот и мне стало интересно заглянуть под капот GITу. Конечно как и с любым сложным механизмом уровень этого «заглядывания» под капот может быть разным, кому то будет достаточно увидеть крышку мотора и отверстие куда лить незамерзайку, а кому то «заглянуть под капот» это дойти до марки стали используемой для изготовления той или иной детали жигулей. Поэтому давайте сразу обозначим уровень нашего погружения в этой статье. В статье мы рассмотрим в деталях что происходит когда мы делаем привычные нам «git clone/push», посмотрим как этот процесс устроен и какие есть в нем возможности. Сущности и процессы, которые конечно же останутся за рамками этого повествования, можно будет самостоятельно найти (ссылку я указал выше), ибо охватить такую обширную тему как Git, а тем более его подкапотное пространство, не представляется возможным за раз. Так что все кому интересно прошу под кат.
Итак, начнем с того, что заглядывать под капот мы будет по следующему плану:
Какие типы протоколов есть в Git.
Как происходит общение по протоколу ssh.
Рассмотрим возможности протокола Git.
Типы протоколов GIT
Git умеет работать с четырьмя сетевыми протоколами для передачи данных: локальный, HTTP, Secure Shell (SSH) и Git. В этой части мы обсудим только протокол SSH и HTTP, оставшиеся являются "нишевыми" и широкого распространения не имеют, но пару слов для общего представления скажем.
Локальный протокол
Базовым протоколом является Локальный протокол, для которого удалённый репозиторий — это другой каталог на диске. И это все что мы о нем скажем, потому что по сути этот протокол является архаизмом.
Протоколы HTTP
"Умный" HTTP
«Умный» протокол HTTP работает поверх стандартных HTTP/S портов и может использовать различные механизмы аутентификации HTTP, это часто проще для пользователя, чем что-то вроде SSH, так как можно использовать аутентификацию по логину/паролю вместо установки SSH-ключей. Наверное, сейчас он стал наиболее популярным способом использования Git, так как может использоваться и для анонимного доступа (если это разрешено конечно), и для отправки изменений с аутентификацией и шифрованием как протокол SSH. Вместо использования разных адресов URL для этих целей, можно использовать один URL адрес для всего. Если вы пытаетесь отослать изменения и репозиторий требует аутентификации (обычно так и есть), сервер может спросить логин и пароль. То же касается и доступа на чтение.
"Тупой" HTTP
Если сервер не отвечает на умный запрос Git по HTTP, клиент Git попытается откатиться на более простой Тупой HTTP-протокол. Тупой протокол ожидает, что голый (ой..) репозиторий Git будет обслуживаться веб-сервером как набор файлов. Прелесть тупого протокола HTTP — в простоте настройки. По сути, всё, что необходимо сделать — поместить голый репозиторий в корневой каталог любого веб-сервера, умеющего раздавать статику по HTTP/S. Теперь каждый может клонировать репозиторий, если имеет доступ к веб-серверу, на котором он был размещен. По сути такой протокол предназначен только для чтения, но в теории в этой одной задаче он может быть быстрее для высоко нагруженных git хостингов.
Git-протокол
Следующий протокол — Git-протокол. Вместе с Git поставляется специальный демон, который слушает отдельный порт (9418) и предоставляет сервис, схожий с протоколом SSH, но абсолютно без аутентификации. Чтобы использовать Git-протокол для репозитория, вы должны создать файл в этом репозитории git-export-daemon-ok
, иначе демон не будет работать с этим репозиторием. Соответственно, любой репозиторий в Git может быть либо доступен для клонирования всем, либо нет. Как следствие, обычно отправлять изменения по этому протоколу нельзя. Вы можете открыть доступ на запись, но из-за отсутствия аутентификации в этом случае кто угодно, зная URL вашего проекта, сможет его изменить. В общем, это редко используемая возможность.
Протокол SSH
Часто используемый транспортный протокол для самостоятельного хостинга Git ‑ это SSH. Причина этого в том, что доступ по SSH уже есть на многих серверах, а если его нет, то его очень легко настроить. К тому же, SSH ‑ протокол с аутентификацией, и благодаря его распространённости обычно легко настроить и использовать. Данные передаются зашифрованными по авторизованным каналам. Наконец, так же как и протоколы HTTP/S, Git и локальный протокол, SSH эффективен благодаря максимальному сжатию данных перед передачей.
Недостаток SSH в том, что, используя его, вы не можете обеспечить анонимный доступ к репозиторию (если это можно считать недостатком). Клиенты должны иметь доступ к машине по SSH, даже для работы в режиме только на чтение, что делает SSH неподходящим для проектов с открытым исходным кодом. Но в корпоративных условиях этого недостатка нет по определению.
Далее рассмотрим как происходит общение сервисов git по протоколу ssh, так как это общение точно такое же что и в случае HTTP.
Общение по протоколу SSH
Для общения по протоколу SSH чаще всего (нотариально заверенные 99,9%) используется «умный» протокол. «Умный» он потому что требует наличия на сервере специального процесса, знающего о структуре Git репозитория, умеющего выяснять, какие данные необходимо отправить клиенту и генерирующего отдельный pack‑файл с недостающими изменениями для него (тем самым оптимизируя пересылку данных). Работу умного протокола обеспечивают несколько процессов: два для отправки данных на сервер и два для загрузки с него.
Загрузка данных на сервер
Для загрузки данных на удалённый сервер используются процессы send-pack
и receive-pack
(на самом деле это команды утилиты git, но если вы «пошаритесь» у себя в системе, в которой установлен git, то найдете отдельный исполняемый файл: git-receive-pack
). Процесс send-pack
запускается на клиенте и подключается к receive-pack
на сервере. И выполняется это без какой либо магии, а все лишь используя возможность протокола ssh для удаленного запуска команды на сервере. То есть например вы хотите сделать отправку ваших локальных изменений в репозитории в удаленный репозиторий, ваша команда «git push origin» под капотом выполнит следующий вызов: «$ ssh -x git@server "git-receive-pack 'simplegit-progit.git'"
». Таким образом на стороне сервера произойдет запуск исполняемого файла git-receive-pack
и начнется общение между двумя этими процессами по определенном протоколу, о котором мы поговорим далее.
Процесс общения между git клиентом и git сервером построен на довольно специфичном git протоколе. Данные передаются пакетами. Каждый пакет начинается с 4-байтового шестнадцатеричного значения, определяющего его размер (включая эти 4 байта). Пакеты обычно содержат одну строку данных и завершающий символ переноса строки. Рассмотрим пример (все имена вымышлены, а совпадения случайны):
|
Первый пакет начинается с 00a5, что в десятичной системе равно 165 и означает, что размер пакета составляет 165 байт. Следующее значение в примере "ca82a6dff817ec66f4437202690a93763949" это SHA-1 коммита git объекта, а затем указана ссылка на него. После идет перечесление воможностей сервера, о них мы поговорим чуть позже (кстати в рамках этого протокола есть договоренность что возможности передаются только в первом пакете). Следующий пакет начинается с 0000, что говорит об окончании передачи списка ссылок сервером.
Теперь, когда send-pack
выяснил состояние сервера, он определяет коммиты, которые есть локально, но отсутствуют на сервере. Эту информацию процесс send-pack
передаёт процессу receive-pack
по каждой ссылке, которая подлежит отправке. Например, если мы обновляем ветку master
и добавляем ветку experiment
, ответ send-pack
будет выглядеть следующим образом:
|
Для каждой обновляемой ссылки Git посылает по строке, содержащей собственную длину, старый хеш, новый хеш и имя ссылки. В первой строке также посылаются возможности клиента, о возможностях мы расскажем далее более подробно. Хеш, состоящий из нулей, говорит о том, что раньше такой ссылки не было — мы ведь добавляете новую ветку experiment
. При удалении ветки всё было бы наоборот: нули были бы справа.
Затем клиент посылает pack-файл c объектами, которых нет на сервере. Наконец, сервер передаёт статус операции — успех или ошибка: 000eunpack ok
Скачивание данных.
Для получения данных из удаленного репозитория используются вторая пара процессов: fetch-pack
и upload-pack
(аналогично тоже можно найти отдельный исполняемый файл git-upload-pack). И так же аналогично отправке ваша команда "git clone" под капотом выполнит: "$ ssh -x git@server "git-upload-pack 'simplegit-progit.git'"
" и начнется процесс общения между этими процессами. Как только связь будет установлена то сервер отсылает клиенту следующее:
|
Это очень похоже на ответ receive-pack
, но только возможности другие. Вдобавок upload-pack
отсылает обратно ссылку HEAD (symref=HEAD:refs/heads/master
), чтобы клиент понимал, на какую ветку переключиться, если выполняется клонирование.
На данном этапе процесс fetch-pack
смотрит на имеющиеся в наличии объекты, а для недостающих объектов отвечает словом «want» с указанием SHA-1 необходимого объекта. Для каждого из имеющихся объектов процесс отправляет слово «have» с указанием SHA-1 объекта. В конце списка он пишет «done», что указывает процессу upload-pack
начать отправлять pack-файл с необходимыми данными:
|
То есть используя всего две команды "want" и "have" клиент и сервер могут договориться о том какие данные нужны клиенту, а какие данные у него уже есть, и таким образом сервер сможет подготовить и отправить для него не все данные, а только недостающие, тем самым сильно оптимизируя обмен данными.
Возможности протокола
Выше мы упомянули о том что в первом сообщении как при загрузке так и при скачивании данных передаются возможности которые поддерживает сервер и которые клиент может использовать для получения необходимого результата. Давайте рассмотрим их по ближе.
Напомним пример первого сообщения от сервера при скачивании данных:
|
В этом сообщении сервер перечисляет те возможности которые он поддерживает. Клиент прочитав такое сообщение в ответ может прислать сообщение со списком тех возможностей заявленных сервером, которые он хочет использовать. Рассмотрим указанные в примере возможности:
multi_ack - Данная возможность позволяет в процессе общения клиента и сервера оптимизировать процесс нахождения базового коммита (базовый коммит это коммит который есть как у клиента так и сервера и от которого можно достигнуть другого коммита или коммитов по дереву объектов). Например у нас есть следующее дерево git объектов (Зеленым изображены коммиты, которые есть на сервере, синим коммиты которые есть у клиента):
Допустим что клиент хочет получить коммиты X,Y (и соответственно недостающие предшествующие им коммиты). Клиент в процессе общения "говорит" - "have F,S" (на самом деле двумя сообщениями, но для краткости мы объединили в одно сообщение), но сервер про эти объекты ни чего не знает, так как они есть только у клиента. Тогда клиента "говорит" - "have E,R", про которые сервер так же не знает. Так процесс продолжается пока клиент не "скажет" - "have D", коммит D есть у сервера и сервер отвечает "ACK D continue", тем самым сообщая клиенту что "базовый" коммита найден (коммит D есть у клиента и у сервера, и из него достижим изначально запрошенный коммит Y) и что клиенту не нужно далее запрашивать коммиты этой ветви дерева CBA. Но так как клиент еще хочет коммит X то аналогичным образом происходит запрос для поиска "базового" коммита по ветви SRQ. Если бы сервер не поддерживал возможность multi_ack то потребовалось бы передавать CBA для поиска общего "базового" коммита для XY.
multi_ack_detailed - об этой возможности максимум что удалось выяснить что она позволяет серверу отвечать более детализировано, но в чем эта детализация заключается и на что она влияет не понятно.
thin-pack - возможность сервера и клиента принимать и обрабатывать "тонкие" пакеты. Git хранит данные в pack файле (в некоторых случая это может быть несколько файлов, но которые всегда можно объединить в один), который содержит в себе всю информацию обо всех git объектах. Но в целях оптимизации объема передаваемых данных между клиентом и сервером можно передавать не все объекты каждый раз, а лишь только те которых нет на принимающей стороне, клиенте или сервере. Такой пакет и называется "тонким". Но при этом нужно убедиться что сервер может отправлять "тонкие" pack файлы, а клиент готов их обрабатывать. Данная возможность основа скорости и эффективности работы Git.
side-band side-band-64k - это возможность позволяет серверу отправлять, а клиенту принимать данные с использование мультиплексирования данных, передаваемого pack файла, данных прогресса выполнения и данных об ошибках. Суть в том что, как уже мы говорили выше, данные передаются пакетами, в случае side-band размер пакета равен 1000 байт и 65520 байт в случае side-band-64k (сейчас конечно используется только side-band-64k). Как мы помним каждый пакет начинается с с 4-байтового шестнадцатеричного значения, определяющего его размер (включая эти 4 байта), далее следует 1 байт обозначающий код потока, и далее все оставшееся место в пакете занимают данные. Код потока как раз позволяет указать тип передаваемых данных.
Код |
Тип данных |
---|---|
1 |
Данные pack файла |
2 |
Данные прогресса выполнения операции |
3 |
Данные ошибок |
Благодаря этой возможности мы и видим процесс обработки нашей команды clone или push, промежуточные сообщения, а так же ошибки и предупреждения.
ofs-delta - эта возможность говорит о том что сервер и клиент могут использовать формат pack v2, который позволяет передавать и принимать объекты по позиции в pack файле, а не по идентификатору объекта, что позволяет повысить скорость обработки.
shallow - с помощью этой возможности клиент может, используя дополнительные команды "deepen", "shallow" and "unshallow" получать не полностью весь репозиторий, а только какое то ограниченное количество, срез на какою то глубину коммитом, или на дату. Именно эта возможность сервера используется клиентом когда мы в команде "git clone" указываем например флаг "–depth 1", что позволит нам получить репозиторий с состоянием на последний коммит нужной ветки. Тем самым не выкачивая полностью всю историю репозитория, что может быть очень полезным в различных CI/CD где как раз как правило нужно лишь последнее состояние какой то конкретной ветки проекта и можно сильно сократить объем скачиваемых данных и увеличить скорость например сборки и выкатки приложения на стенд.
no-progress - возможность по желанию клиента отключить получение типа данных с кодом 2 (выше в side-band side-band-64k мы говорили об этом типе). Когда мы запуская команду "git clone" с помощью флага "-q или --quiet" можем отказаться от получения сообщений о прогрессе выполнения команды, клиент отправит соответствующую команду серверу.
include-tag - возможность сервера автоматически включать в передаваемый pack файл объекты типа Tag, если таковые будут связаны с какими-либо другими объектами в передаваемом файле. Как правило эта возможность всегда используются, так как Tag часто используются в проектах.
symref=HEAD:refs/heads/master - с помощью этой возможности сервер сообщает клиенту на какую ссылку ссылается специальный указатель HEAD, и эту информацию использует клиент что бы понять на какую ветку по умолчанию нужно переключиться при операции "git clone".
agent - сервер с помощью этой возможности передает свою версию git, на что в ответ клиент может передать свою. Но на что либо обмен этой информации не влияет, а используется только исключительно для вывода в лог для возможного использования этих данных в целях отладки.
Заключение
В этой скромной статье нам удалось заглянуть под капот Git, в части общения по протоколу git между процессами клиента и сервера, происходящего когда мы хотим получить данные и когда хотим отправить свои изменения в удаленный репозиторий. При том что мы заглянули и рассмотрели не все аспекты, нюансы и сущности большого внутреннего мира Git, о чем я вас в самом начале предупредил. Но как говорится большое начинается с малого и осилит дорогу идущий. Настоятельно советую прочесть книгу о Git если вы им пользуетесь, а я уверен пользуетесь, иначе вы бы не дочитали до этих строк. Не поленюсь вставить еще раз ссылку на книгу.
Спасибо за внимание!
Комментарии (4)
AnROm
22.06.2024 09:00+2Статья - почти полная копия интернет-ресурсов. У вас оригинальная статья или перевод?
https://github.com/pawk/git-advanced/blob/master/protocols.md
brozes
22.06.2024 09:00+5Внесу свои пять килограмм боли с умным протоколом.
Протокол настолько "умный", что в отличии от старого "тупого" - не умеет даже в банальную докачку, в частности тех самых pack файлов, размер которых часто превышает сотни мбайт, а старый протокол тот же github например у себя отключил.
И если у вас вдруг интернет нестабильный или медленный, или git клиент используется через vpn/proxy - то можно реально посидеть, прежде чем склонируешь репу, ибо любой разрыв или потеря пары tcp пакетов приводит к rpc error xx, и нужно начинать все сначала или извращаться с --depth, т.к протоколом даже не предусмотрено восстановление загрузки.
В range bytes как оказывается с "умным" протоколом не умеет ни сервер, ни клиент, и они только планируют это добавить в будущем.
Daniel217D
Что за "умный" и "тупой" http? Первый раз о таком слышу
artptr86
https://git-scm.com/docs/gitprotocol-http