1. 程式人生 > >騰訊Gaia平臺的Docker應用實踐

騰訊Gaia平臺的Docker應用實踐

本文由謝恆忠根據2016年1月24日@Container容器技術大會·北京站上陳純的演講《騰訊Gaia平臺的Docker應用實踐》整理而成。

大家好,我是騰訊資料平臺部的陳純,今天非常高興為大家介紹一下騰訊Gaia平臺Docker實踐。

首先我介紹一下Gaia平臺。Gaia平臺是騰訊資料平臺部資源排程和管理的系統,承載公司的離線業務、實時業務以及線上裝置service業務,最大單叢集達到8800臺,併發資源池個數達到2000個,服務於騰訊所有的事業群。

在2014年10月份我們正式上線對Docker型別的支援,通過Docker將Gaia雲平臺以更好用的方式呈現給各個業務。目前,Gaia平臺已經服務於公司內部遊戲雲、廣點通以及GPU深度學習等Docker類業務。

Gaia架構

下面我簡要介紹一下Gaia架構。如下圖:

騰訊Gaia平臺

Gaia架構

Gaia其實是我們基於Hadoop的YARN改造的一個Docker的排程系統。

首先它相比社群的YARN有哪些特點呢?社群的YARN可能在RM、NM都已經實現了無單點的設計,可以熱升級。在此基礎之上,我們自研了一個AM,負責所有Docker類作業的排程。然後我們對AM以及Docker也進行了一定的改造,讓它支援無單點的一個設計。右邊的圖中我們可以看到每個Slave節點上除了有NM之外,還有一個Docker程序,負責拉起所有的Docker作業,我們也實現了Docker的熱升級。除了對Master節點進行無單點改造之外,Gaia也為使用者的APP提供了本地重試和跨機重試兩種容災方式。

第二點就是Gaia除了對CPU記憶體以及網路出頻寬進行限制之外,還增加了對GPU和磁碟空間的隔離。

第三點是Gaia可以最大化的利用叢集的所有資源,在保證使用者最低資源使用量的情況下,在叢集有空閒資源時還能借叢集的空閒資源進行使用。

最後一點是我們自研的SFair排程器解決了排程器效率和擴充套件性的,目前排程器每秒最多可排程4K個Container例項。

當然我今天所演講的主要內容並不是對Gaia進行分析,我今天演講的內容主要是跟Docker相關的,因為我平常在組裡面也是負責Docker的研發。

Docker熱升級功能

Docker Daemon單點問題

首先我們來看一下Docker Daemon單點問題,相信所有使用Docker的人都會遇到這個問題。Docker Daemon在退出時會殺掉所有的Container,這一點對於線上服務來說完全不可接受。

其次Docker的坑也比較多,比如我們用的1.6.×版本:

第一個問題是我們遇到了Docker stats。Docker stats其實是用來監控Container實際使用資源的命令,當你的Container拉起來之後,用Docker stats監聽這個Container使用資源,當Container被回收,Stop之後,Docker stats命令由於它沒有正確的回收記憶體中的一些資料結構,會導致Docker Daemon crash,如果Docker Daemon crash,它就會把這個機器的所有Container都給殺掉。這個問題是我們遇到的,自己解決了,並且反饋到社群了。

第二個問題是Docker exec,Docker exec在Docker Daemon程式碼裡面沒有很好的做到同步,會引發一個NPE的異常,導致Docker Daemon crash。

第三個問題相信很多人都遇到過,在Docker 1.9.1版本之前,由於Container的stdout和stderr都會經過Docker Daemon進行快取,並最終寫入到磁碟的一個檔案中。當Container打的日誌量過大,或者速度過快,Docker Daemon來不及把這個日誌寫到檔案中的時候,就會導致Docker Daemon crash。

Docker熱升級功能設計

除了我這裡列舉的一些Docker Daemon的bug會導致Docker Daemon crash之外,我們日常也有對Docker Daemon進行升級的一個需求。總而言之,Docker Daemon的單點問題是一個痛點,所以我們就對Docker Daemon的熱升級功能進行了開發。要開發這樣一個功能,首先搞清楚Docker Daemon為什麼會在它停止的時候殺掉所有的Container,主要是受限於兩點:

第一使用者的程序是Docker Daemon子程序;

第二就是Container的IO流會經過Daemon快取,如果Docker Daemon掛掉的時候它不去殺掉所有的Container,在它被重新拉起來之後,它無法將這個IO流重新以原先的方式流經Daemon,這樣勢必會對它的Docker attach造成影響,所以現在一直都沒有支援這樣一個無單點的設計。

騰訊Gaia平臺

圖1

我們的做法也非常簡單,就是把原先的兩層程序父子關係變為三層(如圖1),在Docker Daemon程式碼裡面有一個monitor的元件,這個monitor的元件最開始是一個goroutine的方式,我們將這個goroutine的方式改為程序的方式,等待Container的執行結束。這樣在Docker Daemon掛掉的時候,它沒必要去殺掉Container,也沒必要去殺掉monitor,它只需要自己把自己的活幹完退出之後,monitor的程序它自己就會變成孤兒程序,從而託管給INIT程序,也就是程序號唯一的程序。這樣在Docker Daemon重啟之後,它就會從磁碟上載入所有Container的執行狀態,恢復所有的Container狀態。

下面講述這樣做對上層的排程系統的影響。一般排程系統拉起一個Container之後會使用Docker wait的命令去等待這個Container的執行結束,如果沒有熱升級功能的時候,client跟Daemon之間是通過http請求的方式通訊的,那Docker Daemon掛掉之後勢必會給client返回一個connection reset的response,這樣的話上層的排程系統就勢必會受到一些影響,對於這個client的狀態就無法感知了。

Docker crash不影響Docker wait

騰訊Gaia平臺

圖 2

我們的做法就是(如圖2)將這個wait的請求轉發到monitor程序,就是最開始還是client向Daemon請求說我要wait這個Container結束,那這個時候Docker Daemon發現自己已經開啟熱升級功能的情況下,它將這個請求返回一個305 redirect請求,把redirect請求返回給monitor,這時候客戶端收到這個response後,發現它是一個redirect ,它就會從location欄位中拿到當前要操作的這個container的monitor程序的地址,是一個IP加埠的一個形式。 client程序就會給monitor程序傳送一個wait請求,這時候monitor程序會等待container執行結束,當container執行結束之後會給client端返回response。可以看到改成這樣一種工作流之後,DockerDaemon整個wait的請求過程跟Docker Daemon已經沒有任何關係,它掛不掛其實對整個過程沒有任何的影響。

Docker熱升級功能實現

我接著介紹一下熱升級功能的實現。首先為了相容以前的方式,我們增加了一個開關,就是hot restart引數,並且將monitor的程式碼元件進行介面化設計,讓它支援以前的goroutine以及外部程序兩種方式。

其次由於Container結束時Docker Daemon可能是存活狀態,也可能是已經死掉的狀態,所以monitor程序在Container結束的時候會將它的啟動和結束事件首先持久化到磁碟,再通知Docker Daemon更新Container狀態。如果這時候Docker Daemon已經掛掉了,我們就重啟一定的次數,如果沒有通知成功,也不用繼續等待這個Daemon程序啟動就直接退出了,因為它原先已經把Container這個結束事件持久化到磁碟上了,當Docker Daemon重啟之後,它就可以從磁碟載入這個Container的狀態遷移檔案,從而可以正確的恢復Container的狀態。

第三比如說為了解決wait以及attach這些命令的問題,我們就將這些請求重定向到monitor程序進行處理,比如說attach請求的IO流,它以前是通過Daemon進行快取的,那現在就通過monitor程序進行快取,這樣Docker Daemon掛掉對於每個Container來說並沒有什麼太大的影響。

第四比如說網路狀態,現在Container Daemon的網路部分的程式碼已經全部遷出,遷出到一個新的工程叫做Libnetwork,Libnetwork中有很多的實體,比如說Libnetwork/Endpoint/Sandbox等有部分的狀態是儲存在記憶體中的,它原先自己會儲存一些global的網路的狀態,比如說global網路指的是一些Overlay網路,這種網路,它需要一些全域性資訊的儲存,所以它會存在global kv,global kv可能是zkEtcd或者是Consul ,我當時在做這個功能的時候就為Libnetwork引入了localstore功能,這個功能就會儲存一些本地的網路的狀態資訊,比如說bridge模式的一些狀態資訊。

Docker網路模式擴充套件

相信使用Docker的人都會遇到網路部分的問題,網路部分是一個比較頭疼的問題,也是每一家都會必然解決的問題。原先Docker Daemon提供了兩種方式:

第一種是Host方式,是完全沒有隔離的一個方式,它的優點是效能好,但是缺點也很明顯,就是沒有網路隔離,有埠衝突的問題。

其次Docker Daemon提供了一個bridge方式,也就是NAT的網路模式,這種網路模式提供了網路隔離的功能,它解決了埠衝突問題。但是Container IP是一個私有的IP,對外是不可見的。所以從另外一臺主機的Container想要直接訪問這個Container是不行的,必須得通過主機的一個對映之後的主機的一個IP,以及對映之後的埠去訪問這個Container。但是埠對映提高了業務遷移的成本,他們可能會需要去改程式碼,或者去改配製。並且NAT的這種方式對網路的IO效能比Host方式也低了接近10%。

使用者對網路的需求

看一下Gaia平臺在接入使用者業務的過程中遇到的一些網路方面的需求。比如說使用者可能會覺得埠衝突問題它不想改程式碼或者配置。第二就是可能有些業務會將本機的一個IP註冊到ZooKeeper上做服務發現,這時候如果是使用NAT方式的話,這個私有IP如果註冊到ZooKeeper上是完全沒有任何意義的。第三就是有些業務會對有許可權訪問自己服務的IP做限制,比如說做白名單限制,這時候在同一個主機上的Container互相訪問的時候,由於它的流量是從主機的Container首先發出,然後進入到global space,這時候會對它的IP進行原IP的替換,就是SNAT的過程。由於流量是從Docker 0進入的,所以它會將原IP替換成Docker 0的IP,也就是一個私有的IP,這時候在同組機的另外一個Container中看到的原IP就不是這個主機的IP,它是Docker 0的原IP,很顯然這個IP是不能加入到它的白名單裡面的,因為這是個私有IP,不能確定這個IP是從哪裡訪問過來的。

我們當初也做了一個改進,原先的SNAT的規則其實是MASQUERADE的規則,這個規則它會將原IP從哪個網絡卡進就會替換成哪個網絡卡的一個IP,後來我們加了一條規則是將它的原IP寫死了,改成主機的IP,這樣它在另外一個Container看到的是主機的IP,所以就不會有問題。

下面一點就是很多業務使用騰訊的TGW閘道器對外提供服務。我們在測試TGW對接NAT方式的時候發現報文是不通的,之所以不通是因為外面的流量訪問進來之後,它會首先做一個DNAT的過程,DNAT的過程會把目的IP轉化成Container的一個私有IP,之後當Container中的程序進行回包的時候,這時候其實走的是一個NAT的過程,因為它進來的時候發生了DNAT的過程,這個過程是發生在PREROUTING階段,然後出去的時候是一個逆過程,這個時候這個包的IP的替換會發生在路由決策之後,這樣在路由決策的時候destination IP還沒有替換,所以導致這個TCP/IP的協議無法對包進行封包,所以會導致通過TGW對接NAT的方式是不可行的。

下面一點是某些業務,比如說GPU業務,可能要求很好的網路效能,這個也是通過NAT的方式無法解決的。

固定IP網路模式

Docker

圖 3

下面就看我們對於網路的改進(如圖3)。前面很多公司也介紹了,大家會給Container分配一個內網IP,我們也是這麼做的。但是我們所不同的是我們在原先的網橋上面加了一個VLAN裝置,這個起到什麼怎麼呢?比如說有一些內網IP跟主機的IP不處於同一個VLAN的時候,如果直接分配到Container中,通過網橋橋接起來,這時候網路是不能通的,因為需要給它出的流量打上一個vlan tag它才能通,這樣我們就在原有的基礎之上加了一個VLAN裝置,這個VLAN裝置後面再橋接一個網橋,然後讓這些與主機的IP不同一個VLAN的IP的Container橋接到另外一個網橋上面, 這樣它出來都會打上這個vlan tag ,這樣就可以通了。這種方式相比NAT的方式,可能的IP對外可見,沒有埠對映帶來的遷移成本,也少了iptables或者是使用者態程序的一個轉發過程,所以效能略優於NAT的方式。 結合Gaia等上層排程系統可以實現IP漂移的功能,就是在Container發生遷移的過程中IP可以保持不變。

SR-IOV

下面介紹一下SR-IOV技術,SR-IOV技術是一個硬體虛擬化技術,它可以由一個網絡卡虛擬出多個功能介面,每個功能介面實際上是可以做一個網絡卡使用的。通過這種硬體取代核心的虛擬網路裝置的方式,可以極大的提高網路的效能,並且減少了物理機CPU的消耗。Docker 1.9.0版本支援網路外掛的方式,所以我們實現了一個SR-IOV的網路外掛,去非常方便的使用這種方式。

下面(圖4)的一個圖表就是我們測試使用SR-IOV之後所帶來的效能的提升。橫座標是測試的主要是在虛擬化比是1:1和1:4的情況下,網路流量包的大小是1個位元組、64個位元組和256個位元組的情況下SR-IOV的方式相比NAT方式所帶來的包量的提升。

Docker

圖 4

Docker Overlay網路模式

第三就是Overlay網路為我們提供了多租戶網路隔離功能,並且每個租戶可以分配一個獨立的虛擬的IP段。這種方式不需要分配一個內網的IP,也不需要依賴於NAT的方式,這種方式想必是以後必然會用上的一種多租戶的一個解決方案。但是Docker原生的Libnetwork在overlay driver為接入的Container預設連線了一個NAT網路對外提供服務,通過這個NAT的網路Container就不單可以訪問與自己位於一個Overlay的其它的Container,也可以訪問外網,因為它是通過NAT的方式去訪問的。但是很多Container並不一定需要訪問內網或者外網,所以我們給Libnetwork物件增加了一個internal開關,建立完全與外界隔離的一個網路。

Docker

圖 5

這個圖(圖5 )是位於不同主機上的5個Container,C1和C2構成一個Overlay網路,C3、C4、C5構成另外一個Overlay網路,這兩個網路彼此之間不同,如果說某些Container想要對外提供服務,這時候我們可以給它分配一個內網的IP,讓它對外提供服務。

Container資料儲存

接下來分享一下我們在儲存方面的工作。Container資料儲存相信也是使用Docker的人繞不開的一個問題。我們的做法就是在Container遷移後不需要保留的資料,就給它分配一個host volume進行儲存它的資料。如果這個Container遷移之後它的資料需要保留,給它分配一個Ceph RBD儲存,我們是使用了Ceph volume plugin為每個Container分配一個RBD的儲存目錄。

Container資源隔離

接下來介紹一下我們在資源隔離方面的一些工作。Docker本身對記憶體的控制是hard limit方式,就是Container的程序的記憶體如果超過它申請的,比如說5G的一個總的上限,它就會將這個Container給殺掉。但是這種方式就非常不方便,比如說使用者可能並不知道它的Container實際到峰值的時候使用到多少的記憶體,並且在很多情況下當Container超過它的記憶體上限的時候,這個機器的所有記憶體其實還有很多的空閒,這時候我們的做法是為這個機器上所有的Container設定一個總的流程上限,Hard limit。單個Container記憶體使用超出申請值,如果這個機器的所有Container沒有超出設定的總的Hard limit值時不會觸發kill,只有當總的記憶體超過Hard limit一定的百分比之後,才kill那些超出記憶體申請值最多的Container,這種方式可以大大降低叢集中Container被kill的機率。

網路出頻寬控制

下面一點是我們在做網路出頻寬的控制的時候發現,如果直接使用net_cls的方式對流量進行標誌的時候,資料包會經過bridge之後pid會發生丟失,這時候打的標記是沒有效果的。所以我們的做法就是在iptables的mangle表中對Container出流量按IP進行標記,這個時候我們就可以方便的使用TC工具對標記的流量進行限制。

容器中資源顯示問題

下面一個問題也是很多講師都講到的問題,是容器中自然顯示的問題,原先跑在物理機或者虛擬機器中的業務在使用騰訊網管系統記錄機器的資源使用情況,但是如果都遷移到Container中之後,這個時候Container中顯示的資源可能是主機的資源,那並不是它自己實際使用的資源。所以我們就需要解決這個問題,可能有一些團隊是通過修改核心的方式去解決的,但是修改核心可能還需要自己維護這些patch。我們的做法就是通過FUSE實現了一個使用者態的檔案系統,這個使用者態的檔案系統使用的是Cgroup的資料統計Container的實際資源使用,它可以為每個Container生成模擬的meminfo stats或者diskstats cpuuinfo檔案,然後在啟動Container的時候將這些檔案直接mount到Container中,這個程式是用go語言實現的,使得它非常方便的嵌入到Docker的程式碼中,整個過程對Docker使用者透明,這個連結( https://github.com/chenchun/cgroupfs)是我們做的這個功能的程式碼的地址。

文:陳純

文章出處:Docker