當前位置:
首頁 > 科技 > 5分鐘了解TencentHub技術架構與DevOps實踐揭秘

5分鐘了解TencentHub技術架構與DevOps實踐揭秘

演講嘉賓:彭磊 | 騰訊雲高級工程師

TencentHub是一個集Docker鏡像、二進位文件、helmcharts於一體的倉庫存儲服務。那麼這一架構技術是如何基於Kubernetes 快速實現workflow引擎的呢?今天將為大家分享《TencentHub技術架構與DevOps落地實踐揭秘》,讓我們開始吧!

大家好,這次主要是給大家分享TencentHub技術架構和DevOps相關的一些東西,這兩點和大家日常開發結合會比較緊密,比較接地氣一點。我今天會主要分享三部分,第一,簡單聊一下TencentHub這個產品和DevOps的關係,我們如何去思考建設一個TencentHub鏡像倉庫+DevOps引擎。第二部分講講TencentHub總體的一些東西和Docker鏡像存儲在騰訊雲上是如何實現的,最後是我們的workflow引擎:為了支撐DevOps工作流去設計產品以及一些具體設計/實現細節。


TencentHub與DevOps?

什麼是DevOps?

我相信很多人對DevOps已經比較熟悉了,這個概念從2009年大火起來到現在已經過去了接近十年的時間。直擊DevOps的核心理念,下面這句話是我最贊同的對DevOps的描述:以業務敏捷為中心,去構造適應快速發布軟體的工具和文化。我們做DevOps,要去推崇DevOps,肯定不能忘掉我們的目標是快速交付軟體產品。在達到這個目標的過程當中,非常重要的落地就是我們需要工具去支撐DevOps,所以接下來分享TencentHub這樣一個以DevOps引擎為指導的工具。


什麼是TencentHub?

TencentHub是什麼?TencentHub核心有兩部分,第一,它是一個多功能的存儲倉庫,包含了Docker鏡像存儲功能以及helmcharts這樣的存儲,還有一些構建產物的存儲等等。第二,TencentHub是一個DevOps引擎,通過我們自己設計的workflow引擎對工作流進行編排,去幫助大家建立自己的DevOps流程,比如說構建、測試、審核、部署等等任務。當然,在前置環節,我們也將去對接騰訊雲提供的TGit代碼託管服務,最終給開發者提供一套完整的雲上Devops工具解決方案。


TencentHub技術架構

TencentHub – 總體架構

我簡單介紹一下TencentHub的總體架構,TencentHubd 的鏡像倉庫存儲是基於COS實現,因為COS可靠性非常高,這非常方便地給我們的鏡像存儲帶來高可靠保證。TencentHub核心還有一個自研workflow引擎,可以使用YAML定義DevOps流程,讓我們的DevOps本身達到Programmable,這對Devops的管理是很重要的。在TencentHub的Devops引擎裡面,我們使用容器去實現插件機制,用來封裝用戶自定義的DevOps任務,後面會詳細介紹。使用Kubernetes作為執行引擎,運行DevOps任務,目前是使用TKE來實現的。在Docker存儲倉庫里,我們也會加入Docker鏡像漏洞的安全掃描,後面會做一個的簡單介紹。


鏡像存儲

Registry Token Authentication Specification

左邊這個圖分為兩大部分。上面是Registry,下面是StorageEngine。Registry主要包含了組織、團隊、成員的管理,以及一些許可權的控制,同時還包括倉庫Repository的管理,負責Docker鏡像、文件、helmchart的存儲。我們所有存儲都是使用一個倉庫來管理,鏡像、文件等都會放在某一個倉庫下面。在最上面,我們提供了Webhook,可以幫助用戶對接自己的系統,監聽倉庫或者DevOps流程發生的一些事情。在Registry裡面還有Component組件,負責DevOps任務的存儲。下面這部分StorageEngine,是我們去解決統一的倉庫存儲層的問題,它是一個我們提供出來給全球的騰訊雲的機房都能使用TencentHub服務的機制。

TencentHub倉庫中最重要的是Docker鏡像的存儲。除了公共的鏡像存儲之外,TencentHub還支持私有的鏡像存儲。私有鏡像存儲需要通過登錄才能獲取或者上傳Docker鏡像。如何是想登錄認證呢?這是Docker官方的Registry用戶認證的規範,它並沒有納入到OCI規範裡面,只是在官方有一個文檔去說明流程是什麼樣的,Docker客戶端也是按這個流程去實現的,所以我們只需要在TencentHub後端按這個流程去實現自己的授權服務就可以了。

TencentHub pull 授權流程

在我們使用Docker進行push/pull的時候,本地的Docker客戶端會調到containerd,檢查當前這個操作是否有授權。當我們沒有帶上一個Token的時候,後端的存儲服務會返回401,並且攜帶一個授權服務的地址,告訴客戶端去這個地方拿授權碼。客戶端請求授權服務獲取將要進行操作的授權Token。

獲取Token支持兩種方式,一種是OAuth2密碼模式,你可以通過登錄之後,授權服務會給你下發一個key,它和你的密碼無關係,後續的通信都是使用Key獲取Token。

第二種方式是Basic Authentication方式,很多時候我們採用的都是這種方式。TencentHub實現的也是後者。這種方式會在Token客戶端留下帳號密碼,並不是太安全,我們後面可能會去支持OAuth2的密碼模式。

當客戶端發起一個請求到授權服務的時候,授權服務就根據當前的請求去生成一個授權的列表,封裝成Token返回客戶端,這個Token的格式對客戶端是透明的,可用使用任意自己的格式。我們這裡繼續保持了選擇JWT的格式,是因為我們把StorageEngine和Registry做了一個狀態的隔離,因為我們把需要的許可權都放到了Token裡面進行封裝,不需要有一個中心化的帶狀態的許可權校驗服務去連接兩者。最後當Token拿到之後,客戶端重新再去請求Registry,重新拉取它的Docker鏡像,或者進行數據的上傳/下載之類。

這裡用一個時序圖簡單說一下它的流程。首先可以看到,Docker服務端首先會去訪問/v2/這樣的路徑,客戶端會根據當前是否有攜帶Token來返回一個授權的地址,我們目前返回的是hub.tencentyun.com/token這個地址,客戶端就會自動訪問/token的URL,帶上當前的賬戶以及申請的許可權範圍。客戶端申請Token完成之後,就會進入Docker鏡像拉取流程。

OCI Distribution Specification

Docker鏡像是分層組織形式,每個Docker鏡像包含多個Layer和一個Config文件,每個Layer包含構建一個Docker鏡像時文件系統裡面出現的差異,Config文件裡面會包含當前這個Docker鏡像能運行的一些環境要求,例如OS,及一些配置入口命令,導出的一些埠之類。這些文件被一個Manifest文件引用起來,通過它可以找到這個Docker鏡像的所有內容。Docker是在V2版本設計了這樣一套規範,目前它也已經提交到OCI組織,成為一個Distribution Specification標準。

這樣的設計帶來很多好處。

首先,它的安全性有比較大的提高。鏡像中的所有文件都是content addressable 的設計,讓我們只需要拿到最後的Manifest去校驗,就可以發現整個數據的傳輸過程中是否有篡改。這在現在比較流行的區塊鏈中都採用類似的技術,是merkle tree的一種實現。

第二,它可以極大減少冗餘。每個構造的Docker鏡像都不會從空白文件開始,都是從各個發行版或者是比如CentOS的基礎鏡像開始,這些基礎鏡像存在非常多的基礎鏡像Layer都是相同的。它通過content addressable storage 的組織結構,不僅在客戶端減少存儲空間,也可以在遠端倉庫中減少存儲所需要的空間。

最後,它對緩存是非常友好的。因為整個Docker鏡像裡面不管哪一個Layer有改變,都會最終影響到Manifest的改變,所以Docker鏡像並不是重新修改一個Layer,而是重新生成的,我們去做緩存的時候就可以非常方便地在不同的環境下地部署我們的緩存,只需要使用content digest 作為Layer的key來引用Layer文件即可,Layer不會修改,只會生成新的或刪除老的Layer,緩存的管理就非常簡單。

distribution RESTful API

這裡再簡單補充一下關於distribution 規範的一些API。前面兩個不說,主要是第三個,每個Docker鏡像的上傳都會訪問到/v2/倉庫名/manifests/tags|digest地址,在這個地址上,它支持去GET、PUT、DELETE,去實現Docker鏡像在遠端倉庫的拉取/上傳/刪除的功能。剛才所說的Layer、Config文件,包括Menifests文件,也可以通過V2這個路徑下面的name+blobs+digest去獲取。更詳細的規範,可以看一下OCI組織已經成為標準的說明,下面有個URL。

distribution 架構

TencentHub鏡像的存儲核心還是利用了Docker官方的distribution來實現的,distribution的實現在這個圖裡面描述得比較清楚,最上面有API route層分發URL的規則。第二層會有一個許可權控制,我們在這裡做了一些改造,增加了TencentHub的hubtoken的實現。我們為什麼要去實現hubtoken,而不是用官方現在自己的GWT格式?

是因為TencentHub可能還會面臨著一些私有化部署的需求,或者說一些用戶在公有雲上面希望有自己獨立的存儲倉庫。但是我們上面的Registry和StorageEngine兩者之間並沒有中心化的授權服務,所以我們就通過在hubtoken裡面包含了一些不同租戶的身份識別信息,在上下文進行傳輸,所以就單獨改造了這裡。

在許可權控制下面會有API協議的函數處理,一些主要的業務邏輯,都在這裡實現。最終到下面storage的實現,它提供了一套存儲的插件機制,有一個標準的存儲介面,規定了文件上傳、文件移動等等介面,只要去實現就可以了。我們這裡也會實現GOS,是對COS的簡單封裝,後面會做一個GOS簡單介紹。

現在騰訊雲容器服務的倉庫是CCR,還沒有提到TencentHub上面來。CCR有兩個問題,第一是不同地域的鏡像是不通的,比如說我們在廣州上傳一個鏡像,想在矽谷拉取是拉取不到的,這是我們直接依賴了COS提供的分發能力,沒有去對它做封裝,COS是無法跨區域訪問的,所以會存在這個問題,這會帶來用戶體驗上的不一致,基於倉庫的觸發器、Docker鏡像構建功能,都會受到這個問題的影響,所有基於倉庫的服務都需要單獨又去部署一份,維護上也帶來了很大影響。所以我們就做了下面這一層。

我們在設計global object storage的時候,發現distribution協議的設計沒有事務的需求,第二是我們拉取多個鏡像,它對延時不太敏感,但是對吞吐量很敏感。因此,實現這個存儲,我們只對COS做了很簡單的封裝,提供中心化的服務,它類似於一個p2p的tracker,會去記錄它所有的文件分布在哪個區域的哪個COS裡面。比如說我們從廣州上傳了一個鏡像,他需要在矽谷去拉取,我們通過這個global object storage服務,會發現它在本地是沒有的。這個時候通過騰訊雲內部的專線,直接去廣州把它拉取過來。在拉的過程當中,它會同步寫到本地的COS和客戶端去,這樣客戶端也不用做很長的等待。當下一次矽谷再訪問該鏡像的時候,它就不會再跨區域去訪問這個數據,給我們減少了很多成本,最終也給用戶帶來很好的體驗。

Docker鏡像安全掃描

在Docker鏡像的存儲完成之後,我們還是提供了一個Docker鏡像的靜態掃描。它通過對比Docker鏡像包里所包含的軟體列表版本和漏洞資料庫裡面漏洞對應的軟體版本,得出一個差異,通過這個差異計算出當前Docker鏡像裡面可能會存在的漏洞風險。

Scanner服務是掃描的中心服務,我們借鑒了CoreOS的Claire來實現的,它會周期性地和幾大發行版的漏洞資料庫進行同步。Docker鏡像上傳完成之後,就會提交到掃描中心,逐個分析它的每個Layer,把每個Layer取出來,看裡面的軟體版本。當漏洞被發現的時候,Scanner發起一個通知,最終通知到前端的Registry服務,用戶可以通過webhook拿到這個消息。因為掃描只能獲得已知的漏洞,針對一些0day漏洞是沒有辦法的,當新漏洞被曝光出來之後,可能各大發行商會跟進,然後更新他們的漏洞資料庫,已經完成掃描的Docker鏡像就需要再去做對比,這個時候就需要我們開發人員比較小心,留意是否有影響到自己的漏洞。

前面基於Docker倉庫關於鏡像存儲方面的分享,也是TencentHub鏡像倉庫裡面最複雜的部分,關於artifact和heml chart這樣的文件存儲,我們就不做過多的實現,因為它的實現都類似。


workflow引擎設計與實現

如何設計一個通用的DevOps型解決方案?

我們為什麼要去做一個TencentHub的DevOps引擎呢?因為我們發現很多客戶在上雲的時候,平時遇到非常多重複性的操作工作,例如在騰訊雲UI上面做很多事情,沒辦法自動化或者騰訊雲的API還不夠好用等等各種問題。其實DevOps的自動化要求是很高的,我們不能加入大量的人工去操作,基於這個考慮,同時還要照顧到中長尾客戶的一些需求,我們最終決定要做一個DevOps的工具,去幫用戶來建立自己的DevOps流程。

這裡是我們考慮如何去做這個事情。

第一點,要實現DevOps這樣一個工具,首先要把DevOps任務編排起來,所以我們需要做到一個能盡量覆蓋到盡多客戶DevOps需求的編排邏輯。

第二,DevOps任務是千差萬別的,我們沒辦法去開發完所有的插件,去適配每個客戶自己的需求,他們的部署策略,他們的單元測試、編譯等等。所以我們需要把這些任務交給客戶自己去完成,需要設計一個插件機制,就是Component,後面會介紹。第三點,用戶的DevOps流程運行在TencentHub裡面,我們需要真正去執行它,但我們不想去發一個執行任務的集群,我們只需要只需要做很簡單的任務調度,然後交給成熟的集群管理組件完成。所以我們最終還是選擇kubernetes來做這個事情,也就是TKE,它為我們省去了很多運維工作,還有TKE的監控都可以復用。


DevOps任務編排 Workflow/Stage/Job

按業界通用的方法,workflow設計成三級結構,每個workflow包含多個stage,每個stage裡面會有很多job,job會有並行和串列的執行方式。stage有一個類型叫pause。為什麼會這樣一個類型呢?比如金融客戶的發布有很嚴格的審批流程,並不是說我修完代碼,提交到git server,代碼的提交,觸發workflow的執行,然後去線上部署,直接一股腦跑通了。這是不行的,在DevOps流程當中,在合適的時候是需要人工介入的,需要團隊中不同角色來決定流程是否繼續,因此我們設計可以暫停的stage,就是為了來適應這樣的場景。

具體的設計思想是,用戶可以去開發一個自己的DevOps Task任務邏輯(component),我們會把stage接下來繼續執行的調用介面或者取消介面通過環境變數傳到component裡面去,在component插件裡面,開發者可以通過郵件或者其他形式,把你需要審批的流程發給上級審批部門去察看,他如果通過之後,通過客戶自己的系統,點擊這個URL,回調到TencentHub裡面來,TencentHub就會根據你當前回調的URL是取消任務還是繼續執行任務,來決定這條workflow是繼續向下走還是終止。

Component的設計我們後面再詳細介紹。


Workflow 生命周期

workflow的設計生命周期需要簡單介紹一下。首先,workflow可以被觸發執行,這裡有三種方式。一種是把某一條workflow和代碼關聯起來,當提交代碼的時候,可以觸發這條workflow的執行。第二種是也可以和TencentHub的鏡像存儲關聯起來,比如說我們可能不需要代碼去觸發,我們想通過push一個鏡像,因為我們的代碼可能是在內部比較重要的地方去存儲,不會用到公有的github、gitlab,或者不想暴露git倉庫出來,就可以通過push一個鏡像去觸發。第三種是可以通過調API直接觸發某一條workflow。

workflow被觸發執行,就會在系統中把它置成一個pending狀態。因為每個客戶有配額限制,我們的scheduler會去檢查這個用戶的配置是否足夠,例如他如果有些比較長的workflow執行,我們會一直將它放在一個調度的狀態,當檢查通過之後,就會被投入到TKE當中去執行。但是它也不一定馬上會運行起來,因為TKE的資源也有可能會比較緊張,我們這裡提供的是公有雲服務,所有人都會訪問,所以資源可能會臨時不夠用,也會處於pending。我們會有一個狀態的檢測服務,不停的輪巡job的狀態,如果發現部分它已經脫離了pending,進入了其它狀態類型值,我們就會把它標定為running狀態。如果一個stage被標記為是一個可暫停的stage,這個stage里所有的job被執行完之後,我們會把它標記為已被暫停的狀態,這時候它需要等待外界的反饋,來決定這條workflow繼續如何走: 如果外界反饋說它要取消,workflow就會進入到整個流程的結束。

Job feature

首先,每個job都考慮成為一個類似於函數一樣,去處理輸入,在內部做一些自己的業務邏輯,最後通過我們定義的標準輸出,去輸出一些它處理完的信息。

第二,job可以從workflow全局環境變數中去讀取信息,做一些邏輯。這些環境變數不能修改,因為如果我們提出一個全局可修改的變數存儲,它會導致整個component的調試或者開發會非常麻煩。

第三,每個component肯定需要和外界打交道,我們workflow裡面會去提供cache和artifact指令,讓用戶可以非常方便地把container裡面構建出或者運行的一些結果保存到我們提供的外部存儲裡面。

第四,workflow沒有辦法去循環執行,只是一個DAG構成的關係圖,然後一條一條向前執行。這是我們設計workflow的一些考慮,沒有去違背這些,才去做接下來的開發。

Job Storage -- Artifacts & Cache

Cache,是用來在不同的job之間共享和傳遞一些數據,它可以是文件夾,也可以是文件。Artifacts是構建出的一些結果,可以保存在TencentHub倉庫裡面,在倉庫裡面有界面,也有API,可以進行查看或者拉取下來。Cache沒有去跨多個workflow實例。Cache的具體實現工程是對指定的文件/文件目錄進行壓縮,上傳到TencentHub的對象存儲裡面。當下面有一個Job依賴它的時候,又會在Component內部把它下載下來。整個實現就是在兩個hook中進行完成,兩個hook一個是Prestart,一個Poststop,後面會介紹我們如何去實現hook機制。


為什麼使用容器去做DevOps?

我們選擇了用容器來做DevOps,為什麼我們要選擇用容器來做這樣的插件機制呢?也是考慮了很久,討論了很久,核心是基於下面四個考慮。

? 第一,我們面對的所有用戶是公共服務,隔離性是我們要考慮的第一件事情,用容器可以非常方便的幫我們實現不同用戶的任務隔離,有些用戶可能對自己任務的安全性要求非常好,我們後期會考慮和CIS做結合,直接用Clear Container來提供更高的隔離性。

? 第二,復用,在不同的部門之外,它們的技術棧可能會有相似的,後台有一些公共的DevOps任務需要去使用,通過容器Docker鏡像可以非常方便的去共享這樣的邏輯。

? 第三,Docker標準非常固定,也已經非常成熟,通過我們前面對job特性的規範把它確定下來,它可以在本地直接去調試自己的Component,而不需要要建到一個workflow裡面去做這個事情。我們知道,如果我們用jenkins去搭一個複雜的workflow,編寫、調試腳本是比較麻煩的一件事情。

? 最後,我們考慮到需要平滑過渡客戶已有的DevOps流程,因為每個公司會積累很多已有的不管是否標準的DevOps任務,如果讓完全重新開發,代價是很高的。當我們用容器封裝的時候,只需要在Component封裝一個原來的任務,無論是用shell腳本,或者還是用Go寫的,只要提供一個環境,用容器封裝起來,就可以快速的把之前的邏輯挪過來。當然,這些遷移都不是完全透明的,需要去做開發,用容器封裝之後,我們相信這個工作量不會特別大。所以我們最終選擇了使用容器來作為DevOps的核心插件機制。

Component像一個函數實現一樣,會有Input和Output,每個Component可以從另一個Component的Output去添加需要使用的一些輸出值,workflow會把它變成Component執行的容器的環境變數,注入到當前Component裡面。在Component容器進程裡面可以使用這些環境變數做一些邏輯,比如說克隆指定版本的代碼,或者發布上一個環節構建出的版本。Component的輸出直接放到標準輸出裡面,我們規定了每一行只要符合下面紅色地方標明的數據格式,我們就把它作為Component的輸出。選擇這樣不夠優美,但是非常實用的方式去實現,是考慮到如果使用外部的存儲比如redis對input/output 作為存儲,會對用戶編寫Component造成很大的影響:需要在Component裡面還要去調用外部存儲,保存輸出需要存儲一個Key,在下一個階段使用時又要用key到存儲里去取出來。雖然可能用一個第三方的存儲會帶來更豐富的數據結構方面的優勢,但是我們認為對客戶開發Component侵入性太大,所以我們直接用環境變數和標準輸出來做這個事情。


workflow引擎

workflow是在我們的TKE去執行的,選擇用DevOps做workflow引擎的執行集群,是基於這些特性去考慮的。

? 首先,Kubernetes的可靠性,TKE非常可靠,我們不用去擔心它的運維方面的事情。

? 第二,workflow跑的很多任務可能會佔用不同的資源,有可能一個固定的集群資源大小是不夠的,結合TKE的自動擴縮容,我們就非常方便的去在上層workflow變得很多時,自動為客戶提供構建的能力,不用我們人工又去介入。

? 第三,它的資源分配更靈活,並不是每個客戶自己的workflow都是很小的資源佔用,有些可能會做一些壓力測試之類,會佔用更大的資源。如果我們自己去實現一個workflow執行程序,就要考慮這些調度問題,所以我們最終選擇用kubernetes去做這個事情。

前面到使用Component作為插件的時候,使用到 Prestart和Poststop這兩個hook,在設計的時候,我們調研過是否能復用kubernetes pod 的Poststop。發現是不行的,Poststop是在pod的生命沒結束之前由外面的controller去結束一個pod的時候執行(這才會有機會在容器之內去運行指定的hook程序)。但如果像workflow這種Task是一次性執行然後自行退出的,kubernetes沒有辦法在進程正常前調用調用Poststop裡面指定的程序 – kubernetes不知道什麼時候Task會退出 。

所以我們最終實現了自己的方式: workflow把Component在kubernetes裡面運行變成pod前,把它的CMD替換成另一個程序CommandWrapper,這個程序採用靜態鏈接,保證可以在Linux上運行。CommandWrapper會在組件component任務被執行之前,會完成Cache和Artifact相應的邏輯,然後component會在子進程中運行,子進程退出後, CommandWrapper會繼續處理Cache和Artifact的上傳邏輯,最後,CommandWrapper程序會以子進程所退出狀態碼進行退出,如果是返回碼是0,workflow引擎就把它標記為是成功運行,然後會調度下一個job的運行。這就是我們去設計的Hook機制。

這裡簡單介紹如果workflow引擎如何獲取Task的Log。開發人員常常需要查看每個運行的workflow job的Log。可能workflow裡面會有Bug,或者想看它當前進行到什麼地步。Log的獲取沒有使用Docker driver,因為這在kubernetes裡面目前還沒有實現,即時kubernetes支持了docker的log driver,通過單獨的通道去收集Log也會帶來很大的複雜性,所以workflow引擎直接調用了kubernetes的API去獲取它的Log,因為kubernetes API的Log只要在調用沒退出之前,會一直把pod運行的Log從集群裡面讀出來,返回給調用方。Workflow引擎把整個Log存起來,放到COS裡面,或者最後把它通過websocket去寫到前端,讓用戶可以去實時查看。當然,後者我們還沒有實現,開發人員太少,但是我們預留了,可以這樣去做。

今天沒有給大家分享怎麼去搭建自己DevOps流程的例子,因為我相信這是千差萬別的,在不同的公司,不同的組織裡面,流程都不相同,我們沒有辦法枚舉出來。而是站在公共服務的提供商角度,考慮怎麼給用戶最大的便利去建立這樣一套工具。所以我今天的分享主要是關於DevOps的設計和一些實現細節,希望對大家會有一些幫助。

TencentHub這個產品目前已經在騰訊雲官網上開啟內測,邀請大家一起來體驗這個產品,也歡迎大家的反饋和吐槽。

https://cloud.tencent.com/product/thub

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 雲加社區 的精彩文章:

數據挖掘從入門到精通完全指南

TAG:雲加社區 |