1. <rp id="irfrr"></rp>
      <button id="irfrr"><acronym id="irfrr"><input id="irfrr"></input></acronym></button>

      1. <em id="irfrr"><acronym id="irfrr"><u id="irfrr"></u></acronym></em><dd id="irfrr"><track id="irfrr"></track></dd>
        1. 勇士,你可曾好奇過 Git 和極狐GitLab 是如何工作的?現在,拿起你心愛的 IDE,和我們一起踏上探索之旅吧!

          基礎知識

          在開始旅程之前,我們需要做三分鐘的知識儲備,計時開始!

          Git 倉庫內幕

          使用了 Git 的項目都會在其根目錄有個 .git 文件夾(隱藏),它承載了 Git 保存的所有信息,下面是我們這次關注的部分:

          .git
          ├── HEAD # 當前工作空間處于的分支(ref)
          ├── objects # git對象,git根據這些對象可以重建出倉庫的全部commit及當時的全部文件
          │   ├── 20 # 稀疏對象,基于對象hash的第一個字節按文件夾分片,避免某個目錄有太多的文件
          │   │   └── 7151a78fb5e2d99f1185db7ebbd7d883ebde6c
          │   ├── 43 # 另一組稀疏對象
          │   │   └── 49b682aeaf8dc281c7a7c8d8460f443835c0c2
          │   └── pack # 壓縮過的對象
          └── refs # 分支,文件內容是commit的hash
              ├── heads
              │   ├── feat
              │   │   └── hello-world # 某個feature分支
              │   └── main # 主分支
              ├── remotes
              │   └── origin
              │       └── HEAD # 本地記錄的遠端分支
              └── tags # 標簽,文件內容是commit的hash
          
          git-data-model

          紅色部分由 refs 提供,其余部分全部由 objects 提供,commit 對象(黃色)指向保存文件結構的 tree 對象(藍色),后者再指向各個文件對象(灰色)

          Git 服務端只會存儲 .git 文件夾內的信息(稱為 bare repository,裸倉庫),git clone 是從遠端拉取這些信息到本地再重建倉庫位于 HEAD 的狀態的操作,而 git push 是把本地的 ref 及其相關 commit 對象、tree 對象和文件對象發送到遠端的操作。

          Git 在通過網絡傳輸對象時會將其壓縮,壓縮后的對象稱為 packfile。

          Git 傳輸協議

          讓我們按時間先后順序理理 git push 時發生了什么:

          1. 用戶在客戶端上運行 git push

          2. 客戶端的 Git 的 git-send-pack 服務帶上倉庫標識符,調用服務端的 git-receive-pack 服務

          3. 服務端返回目前服務端倉庫各個 ref 所處的 commit hash,每個 hash 記為 40 位 hex 編碼的文本,它們長這樣:

          001f# service=git-receive-pack
          000000c229859bcc73cdab4db2b70ed681077a5885f80134 refs/heads/main\x00report-status report-status-v2 delete-refs side-band-64k quiet atomic ofs-delta push-options object-format=sha1 agent=git/2.37.1.gl1
          0000
          

          我們可以看到,服務端的 main 分支位于 229859bcc73cdab4db2b70ed681077a5885f80134(忽略前面的協議內容)。

          1. 客戶端根據返回的 ref 情況,找出那些自己有但是服務端沒有的 commit,把即將變更的 ref 告知服務端:
          009f0000000000000000000000000000000000000000 8fa91ae7af0341e6524d1bc2ea067c99dff65f1c refs/heads/feat/hello-world
          

          上面這個例子中,我們正在推送一個新分支 feat/hello-world,它現在指向 8fa91ae7af0341e6524d1bc2ea067c99dff65f1c,由于它是個新分支,以前的指向記為0000000000000000000000000000000000000000。

          1. 客戶端將相關 commit 及其 tree 對象、文件對象打包壓縮為 packfile,發送到服務端,packfile 是二進制:
          report-status side-band-64k agent=git/2.20.10000PACK\x00\x00\x00\x02\x00\x00\x00\x03\x98\x0cx\x9c\x8d\x8bI
          \xc30\x0c\x00\xef~\x85\xee\x85"[^$(\xa5_\x91m\x85\xe6\xe0\xa4\x04\xe7\xff]^\xd0\xcb0\x87\x99y\x98A\x11\xa5\xd8\xab,\xbdSA]Z\x15\xcb(\x94|4\xdf\x88\x02&\x94\xa0\xec^z\xd86!\x08'\xa9\xad\x15j]\xeb\xe7\x0c\xb5\xa0\xf5\xcc\x1eK\xd1\xc4\x9c\x16FO\xd1\xe99\x9f\xfb\x01\x9bn\xe3\x8c\x01n\xeb\xe3\xa7\xd7aw\xf09\x07\xf4\\\x88\xe1\x82\x8c\xe8\xda>\xc6:\xa7\xfd\xdb\xbb\xf3\xd5u\x1a|\xe1\xde\xac\xe29o\xa9\x04x\x9c340031Q\x08rut\xf1u\xd5\xcbMap\xf6\xdc\xd6\xb4n}\xef\xa1\xc6\xe3\xcbO\xdcp\xe3w\xb10=p\xc8\x10\xa2(%\xb1$U\xaf\xa4\xa2\x84\xa1T\xe5\x8eO\xe9\xcf\xd3\x0c\\R\x7f\xcf\xed\xdb\xb9]n\xd1\xea3\xa2\x00\xd3\x86\x1db\xbb\x02x\x9c\x01+\x00\xd4\xff2022\xe5\xb9\xb4 09\xe6\x9c\x88 01\xe6\x97\xa5 \xe6\x98\x9f\xe6\x9c\x9f\xe5\x9b\x9b 15:52:13 CST
          \xa4d\x11\xa1\xe8\x86\xdeQ\x90\xb1\xe0Z\xfd\x7f\x91\x90\xc3\xd6\x17\xe8\x02&K\xd0
          
          1. 服務端解包 packfile,更新 ref,返回處理結果:
          003a\x01000eunpack ok
          0023ok refs/heads/feat/hello-world
          

          Git 傳輸協議可以由 SSH 或者 HTTP(S) 承載。

          還是挺直接的,對吧?

          極狐GitLab 的組成部分

          極狐GitLab 是一個常用的 Git 代碼托管服務,同時支持協作開發、任務跟蹤、CI/CD 等功能。

          極狐GitLab 的服務并不是一個單體,我們以大版本 15 為例,和 git push 有關的組件有下面這些:

          gitlab-high-level-architecture.png

          開始 git push!

          三分鐘過得真快!現在你已經掌握了基礎,讓我們開始征途吧!

          你喜歡SSH?

          如果你的遠端地址是 git@jihulab.example.com:user/repo.git 這樣的,那么你在用 SSH 與 極狐GitLab 進行通訊。在你執行 git push 時,本質上,你的 Git 客戶端的 upload-pack 服務在執行下列命令:

          ssh -x git@jihulab.example.com "git-receive-pack 'user/repo.git'"
          

          這里面有挺多問題值得說道的:

          這兩個問題由極狐GitLab Shell 的 gitlab-sshd 來解決。它是個定制化的 SSH Daemon,和一般的 sshd 講同樣的 SSH 協議,客戶端沒法分清它們??蛻舳嗽谧?SSH 握手時會提供自己的公鑰,gitlab-sshd 會調用 Rails 的內部 API GET /api/v4/internal/authorized_keys 查詢公鑰是否在極狐GitLab 注冊過并返回對應公鑰 ID(可定位到用戶),同時校驗 SSH 握手的簽名是否由同一份公鑰對應的私鑰生成。

          另外,gitlab-sshd 限制了客戶端可以運行的命令,其實,它在使用用戶運行的命令來匹配自己應該運行哪個方法,沒有對應方法的命令都會被拒絕。

          可惜,看來我們是沒法通過 SSH 在極狐GitLab 的服務器上運行 bash 或者 rm -rf / 了。┑( ̄Д  ̄)┍

          說點有趣的,早期極狐GitLab 當真使用 sshd 來響應 Git 請求。為了解決上面這兩個問題,他們這么寫 authorized_keys

          # Managed by gitlab-rails
          command="/bin/gitlab-shell key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-
          rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7
          Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=
          command="/bin/gitlab-shell key-2",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-
          rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1026k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7
          Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=
          

          對,你沒猜錯,整個極狐GitLab 的用戶公鑰都會被放到這個文件里,它可能會上百 MB 的大??!樸實無華!

          Command 參數覆蓋了每次 SSH 客戶端想運行的命令,讓 sshd 啟動 gitlab-shell,啟動參數是公鑰 ID. gitlab-shell 可以在由 sshd 設定的環境變量 SSH_ORIGINAL_COMMAND 獲取到客戶端原本想執行的命令,進而運行相關方法。

          由于 sshd 在匹配 authorized_keys 時用的是線性檢索,在 authorized_keys 很大時,先注冊的用戶(公鑰在文件的前面)的匹配優先級會被后注冊的用戶高很多,換句話說,老用戶的 SSH 鑒權要比新用戶的快,而且是可察覺的快。(真·老用戶福利)

          git-push-joke

          如今 gitlab-sshd 依賴的 Rails API 背后是 Postgres 索引,這個 bug(feature?)不復存在。

          通過用戶身份驗證后,gitlab-sshd 會檢查用戶對目標倉庫是否有寫權限(POST /api/v4/internal/allowed),同時獲知這個倉庫在哪一個 Gitaly 實例,以及用戶 ID 和倉庫信息。

          最后,gitlab-sshd 會調用對應的 Gitaly 實例的 SSHReceivePack 方法,在 Git 客戶端(SSH)與 Gitaly(GRPC)之間作為中繼和翻譯。

          最后兩步 gitlab-shell 的行為和 gitlab-sshd 是一樣的。

          從宏觀視角看,經由 SSH 的 git push 是這樣的:

          1. 用戶執行 git push;
          2. Git 客戶端通過 SSH 鏈接到 gitlab-shell;
          3. gitlab-shell 使用客戶端公鑰調用 GET /api/v4/internal/authorized_keys 獲得公鑰 ID,進行 SSH 握手;
          4. gitlab-shell 使用公鑰 ID 和倉庫地址調用 POST /api/v4/internal/allowed,確認用戶有到倉庫的寫權限;
          5. API 返回:Gitaly 地址和鑒權 token、repo 對象、鉤子回調信息(邏輯用戶名 GL_ID、邏輯項目名 GL_REPOSITORY);
          6. gitlab-shell 用上列信息調用 Gitaly 的 SSHReceivePack 方法,成為客戶端和 Gitaly 的中繼;
          7. Gitaly 在適當的工作目錄運行 git-receive-pack,并且預先設定好環境變量 GITALY_HOOKS_PAYLOAD,其中包含 GL_ID, GL_REPOSITORY 等;
          8. 服務端 Git 嘗試更新 refs,運行 Git hooks;
          9. 完成。

          Gitaly 和 refs 更新我們稍后會聊到。

          你更喜歡HTTP(S)?

          HTTP(S)的遠端地址形如 https://gitlab.example.com/user/repo.git. 和SSH不一樣,HTTP請求是無狀態的,而且總是一問一答。在你執行git push時,Git客戶端會按順序和兩個接口打交道:

          003a\x01000eunpack ok
          0023ok refs/heads/feat/hello-world
          00000085\x02
          To create a merge request for feat/hello-world, visit:
            https://gitlab.example.com/user/repo/-/merge_requests/new?merge_request%5Bs0029\x02ource_branch%5D=feat%2Fhello-world
          0000
          

          上述兩個請求會被Workhorse截獲,每次它都做這兩件事:

          1. 把請求原樣發到Rails,后者會返回鑒權結果、用戶ID、倉庫對應Gitaly實例信息(有點怪,對吧?Rails的info/refs和git-receive-pack接口居然是用來鑒權的,我猜這后面多少有些歷史原因)
          2. Workhorse根據上一步Rails返回的信息,建立與Gitaly的連接,在客戶端和Gitaly之間充當中繼。

          總結一下,經由HTTP(S)的git push是這樣的:

          1. 用戶執行 git push
          2. Git客戶端調用GET https://gitlab.example.com/user/repo.git/info/refs?service=git-receive-pack,帶上對應的authorization header
          3. Workhorse截獲請求,原樣發送請求到Rails,獲得鑒權結果、用戶ID、倉庫對應Gitaly實例信息
          4. Workhorse根據上一步Rails的返回信息,調用Gitaly的GRPC服務InfoRefsReceivePack,在客戶端和Gitaly之間充當中繼
          5. Gitaly在適當的工作目錄運行git-receive-pack,返回refs信息
          6. Git客戶端調用POST https://gitlab.example.com/user/repo.git/git-receive-pack
          7. Workhorse截獲請求,原樣發送請求到Rails,獲得鑒權結果、用戶ID、倉庫對應Gitaly實例信息
          8. Workhorse根據上一步Rails的返回信息,調用Gitaly的GRPC服務PostReceivePack,在客戶端和Gitaly之間充當中繼
          9. Gitaly在適當的工作目錄運行git-receive-pack,并且預先設定好環境變量GITALY_HOOKS_PAYLOAD,其中包含GL_ID, GL_REPOSITORY等
          10. 服務端Git嘗試更新refs,運行Git hooks
          11. 完成

          Gitaly 和 Git Hooks

          呼…說完了前面的連接層和權限控制,我們終于得以接近極狐GitLab 的 Git 核心,Gitaly。

          gitaly-logo

          Gitaly 這個名字其實是在玩梗,致敬了 Git 和俄羅斯小鎮 Aly,后者在 2010 年俄羅斯人口普查中得出的常住人口是 0,Gitaly 的工程師希望 Gitaly 的大部分操作的磁盤 IO 也是 0。

          軟件工程師的梗實在是太生硬了,一般人恐怕吃不下……

          Gitaly 負責極狐GitLab 倉庫的存儲和操作,它通過 fork/exec 運行本地的 Git 二進制程序,采用 cgroups 防止單個 Git 吃掉太多 CPU 和內存。倉庫存儲在本地,路徑形如/var/opt/gitlab/git-data/repositories/@hashed/b1/7e/b17ef6d19c7a5b1ee83b907c595526dcb1eb06db8227d650d5dda0a9f4ce8cd9.git,早期極狐GitLab/Gitaly 也使用 #{namespace}/#{project_name}.git 的形式,但是 namespaceproject_name 都可以被用戶修改,這帶來了額外的運行開銷。

          git push 對應 Gitaly 的 SSHReceivePack(SSH)和 PostReceivePack(HTTPS)方法,它們的底部都是 Git 的 git-receive-pack,也就是說,最核心的 refs 和 object 更新由 Git 二進制來完成。git-receive-pack 提供了鉤子使得這個過程能夠被 Gitaly 介入,這里面還牽扯 Rails,一個單邊的請求(不含返回)流程大概像下面這樣:

          gitaly-workflow

          Gitaly在啟動git-receive-pack時會通過環境變量GITALY_HOOKS_PAYLOAD傳入一個Base64編碼的JSON,其中有倉庫信息、Gitaly Unix Socket地址和鏈接token、用戶信息、要執行的哪些Hook(對于git push,總是下面這幾個),并且設定Git的core.hooksPath參數到Gitaly自己在程序啟動時準備好的一個臨時文件夾,那里的所有Hook文件都符號鏈接到了gitaly-hooks上。

          gitaly-hooks在被git-receive-pack啟動后從環境變量讀取GITALY_HOOKS_PAYLOAD,通過Unix Socket和GRPC連接回Gitaly,告知Gitaly目前執行的Hook,以及Git提供給Hook的參數。

          pre-receive hook

          這個鉤子會在 Git 收到 git push 時觸發一次,在調用 gitlab-hooks 時,Git 會向其標準輸入中寫入變更信息,即“某個 ref 想從 commit hash A 更新到 commit hash B”,一行一個:

          <舊commit ref hash> SP <新commit ref hash> SP <ref名字> LF
          

          其中 SP 是空格,LF 是換行符。

          上述信息回到 Gitaly 之后,Gitaly 會依次調用 Rails 的兩個接口:

          如果 POST /api/v4/internal/allowed 返回錯誤,Gitaly 會將錯誤返回給 gitaly-hooks,gitaly-hooks 會在標準錯誤中寫入錯誤信息并且退出,退出碼非 0. 錯誤信息會被 git-receive-pack 收集后再寫入到標準錯誤,gitaly-hooks 非 0 的退出碼會使得 git-receive-pack 停止處理當前的 git push 而退出,退出碼同樣非 0,控制權回到 Gitaly,后者收集 git-receive-pack 的標準錯誤輸出,回復 GRPC 響應到 Workhorse/Gitlab-Shell.

          細心的同學可能會問,Hooks 在運行的時候,相關的 object 肯定已經上傳到服務端了,這時停下來這部分懸空的 object 如何處理呢?

          其實沒有處理完的 git push 對應的 object 會被先寫入到隔離環境中,它們獨立存儲在 objects 下的一個子文件夾,形如 incoming-8G4u9v,這樣如果 Hooks 認為這個 push 有問題,相關的資源就能容易地得到清理了。

          update hook

          這個鉤子會在 Git 實際更新 ref 的前一刻觸發,每個 ref 觸發一次,入參從命令行參數傳入:要更新的 ref、舊 commit hash、新 commit hash。目前這個鉤子不會與 Rails 互動。

          極狐GitLab 同時支持自定義 Git Hooks,pre-receive hook, update hook 和 post-receive hook 都支持,這個操作在 gitlab-hooks 通知 Gitaly 鉤子運行時在 Gitaly 中完成。此刻就是觸發自定義 update hook 的時候。

          a-picture-of-a-hook

          圖中的這個鉤子和計算機科學有著歷史悠久的聯系……咳咳,好吧我編不下去了,我只是擔心你看到這里已經要睡著了,找張圖片讓你放松一下~

          post-receive hook

          在所有 refs 都得到更新后,Git 會執行一次 post-receive 鉤子,它獲得的參數與pre-receive鉤子相同。

          Gitaly 收到 gitaly-hooks 的提醒后,會調用 Rails 的 POST /api/v4/internal/post_receive,Rails 會在這時干很多事:

          其中有的操作是異步的,被交給 SideKiq 調度。

          結語

          現在,你已經從客戶端到服務端走完了 git push 全程,真是一次偉大的旅程!

          勇士,下圖就是你的通關寶藏!

          tong-guan-bao-zang

          30天免費試用極狐GitLab旗艦版

          極狐GitLab不僅是源代碼管理或CI/CD工具,它是一個覆蓋完整軟件開發生命周期和DevOps的開放式一體化平臺。

          免費試用
          Git 為 Software Freedom Conservancy 的注冊商標,GitLab 為 GitLab B.V.的注冊商標,我們已獲授權使用“極狐GitLab”。
          Copyright ? 2022 極狐信息技術(湖北)有限公司   |  
          鄂ICP備2021008419號-1   |   鄂公網安備42018502006137號

          免費試用極狐GitLab 30天

          有疑問? 聯系我們

          Gitlab x icon svg
          激情文学亚洲综合激情
          1. <rp id="irfrr"></rp>
              <button id="irfrr"><acronym id="irfrr"><input id="irfrr"></input></acronym></button>

              1. <em id="irfrr"><acronym id="irfrr"><u id="irfrr"></u></acronym></em><dd id="irfrr"><track id="irfrr"></track></dd>