《騰訊Gaia平臺的Docker應(yīng)用實踐》要點:
本文介紹了騰訊Gaia平臺的Docker應(yīng)用實踐,希望對您有用。如果有疑問,可以聯(lián)系我們。
本文由謝恒忠根據(jù)2016年1月24日@Container容器技術(shù)大會·北京站上陳純的演講《騰訊Gaia平臺的Docker應(yīng)用實踐》整理而成.
大家好,我是騰訊數(shù)據(jù)平臺部的陳純,今天非常高興為大家介紹一下騰訊Gaia平臺Docker實踐.
首先我介紹一下Gaia平臺.Gaia平臺是騰訊數(shù)據(jù)平臺部資源調(diào)度和管理的系統(tǒng),承載公司的離線業(yè)務(wù)、實時業(yè)務(wù)以及在線設(shè)備service業(yè)務(wù),最大單集群達(dá)到8800臺,并發(fā)資源池個數(shù)達(dá)到2000個,服務(wù)于騰訊所有的事業(yè)群.
在2014年10月份我們正式上線對Docker類型的支持,通過Docker將Gaia云平臺以更好用的方式呈現(xiàn)給各個業(yè)務(wù).目前,Gaia平臺已經(jīng)服務(wù)于公司內(nèi)部游戲云、廣點通以及GPU深度學(xué)習(xí)等Docker類業(yè)務(wù).
下面我簡要介紹一下Gaia架構(gòu).如下圖:
Gaia架構(gòu)
Gaia其實是我們基于Hadoop的YARN改造的一個Docker的調(diào)度系統(tǒng).
首先它相比社區(qū)的YARN有哪些特點呢?社區(qū)的YARN可能在RM、NM都已經(jīng)實現(xiàn)了無單點的設(shè)計,可以熱升級.在此基礎(chǔ)之上,我們自研了一個AM,負(fù)責(zé)所有Docker類作業(yè)的調(diào)度.然后我們對AM以及Docker也進(jìn)行了一定的改造,讓它支持無單點的一個設(shè)計.右邊的圖中我們可以看到每個Slave節(jié)點上除了有NM之外,還有一個Docker進(jìn)程,負(fù)責(zé)拉起所有的Docker作業(yè),我們也實現(xiàn)了Docker的熱升級.除了對Master節(jié)點進(jìn)行無單點改造之外,Gaia也為用戶的APP提供了本地重試和跨機(jī)重試兩種容災(zāi)方式.
第二點就是Gaia除了對CPU內(nèi)存以及網(wǎng)絡(luò)出帶寬進(jìn)行限制之外,還增加了對GPU和磁盤空間的隔離.
第三點是Gaia可以最大化的利用集群的所有資源,在保證用戶最低資源使用量的情況下,在集群有空閑資源時還能借集群的空閑資源進(jìn)行使用.
最后一點是我們自研的SFair調(diào)度器解決了調(diào)度器效率和擴(kuò)展性的,目前調(diào)度器每秒最多可調(diào)度4K個Container實例.
當(dāng)然我今天所演講的主要內(nèi)容并不是對Gaia進(jìn)行分析,我今天演講的內(nèi)容主要是跟Docker相關(guān)的,因為我平常在組里面也是負(fù)責(zé)Docker的研發(fā).
首先我們來看一下Docker Daemon單點問題,相信所有使用Docker的人都會遇到這個問題.Docker Daemon在退出時會殺掉所有的Container,這一點對于在線服務(wù)來說完全不可接受.
其次Docker的坑也比較多,比如我們用的1.6.×版本:
第一個問題是我們遇到了Docker stats.Docker stats其實是用來監(jiān)控Container實際使用資源的命令,當(dāng)你的Container拉起來之后,用Docker stats監(jiān)聽這個Container使用資源,當(dāng)Container被回收,Stop之后,Docker stats命令由于它沒有正確的回收內(nèi)存中的一些數(shù)據(jù)結(jié)構(gòu),會導(dǎo)致Docker Daemon crash,如果Docker Daemon crash,它就會把這個機(jī)器的所有Container都給殺掉.這個問題是我們遇到的,自己解決了,并且反饋到社區(qū)了.
第二個問題是Docker exec,Docker exec在Docker Daemon代碼里面沒有很好的做到同步,會引發(fā)一個NPE的異常,導(dǎo)致Docker Daemon crash.
第三個問題相信很多人都遇到過,在Docker 1.9.1版本之前,由于Container的stdout和stderr都會經(jīng)過Docker Daemon進(jìn)行緩存,并最終寫入到磁盤的一個文件中.當(dāng)Container打的日志量過大,或者速度過快,Docker Daemon來不及把這個日志寫到文件中的時候,就會導(dǎo)致Docker Daemon crash.
除了我這里列舉的一些Docker Daemon的bug會導(dǎo)致Docker Daemon crash之外,我們?nèi)粘R灿袑ocker Daemon進(jìn)行升級的一個需求.總而言之,Docker Daemon的單點問題是一個痛點,所以我們就對Docker Daemon的熱升級功能進(jìn)行了開發(fā).要開發(fā)這樣一個功能,首先搞清楚Docker Daemon為什么會在它停止的時候殺掉所有的Container,主要是受限于兩點:
第一用戶的進(jìn)程是Docker Daemon子進(jìn)程;
第二就是Container的IO流會經(jīng)過Daemon緩存,如果Docker Daemon掛掉的時候它不去殺掉所有的Container,在它被重新拉起來之后,它無法將這個IO流重新以原先的方式流經(jīng)Daemon,這樣勢必會對它的Docker attach造成影響,所以現(xiàn)在一直都沒有支持這樣一個無單點的設(shè)計.
圖1
我們的做法也非常簡單,就是把原先的兩層進(jìn)程父子關(guān)系變?yōu)槿龑?如圖1),在Docker Daemon代碼里面有一個monitor的組件,這個monitor的組件最開始是一個goroutine的方式,我們將這個goroutine的方式改為進(jìn)程的方式,等待Container的運行結(jié)束.這樣在Docker Daemon掛掉的時候,它沒必要去殺掉Container,也沒必要去殺掉monitor,它只需要自己把自己的活干完退出之后,monitor的進(jìn)程它自己就會變成孤兒進(jìn)程,從而托管給INIT進(jìn)程,也就是進(jìn)程號唯一的進(jìn)程.這樣在Docker Daemon重啟之后,它就會從磁盤上加載所有Container的運行狀態(tài),恢復(fù)所有的Container狀態(tài).
下面講述這樣做對上層的調(diào)度系統(tǒng)的影響.一般調(diào)度系統(tǒng)拉起一個Container之后會使用Docker wait的命令去等待這個Container的運行結(jié)束,如果沒有熱升級功能的時候,client跟Daemon之間是通過http請求的方式通信的,那Docker Daemon掛掉之后勢必會給client返回一個connection reset的response,這樣的話上層的調(diào)度系統(tǒng)就勢必會受到一些影響,對于這個client的狀態(tài)就無法感知了.
圖 2
我們的做法就是(如圖2)將這個wait的請求轉(zhuǎn)發(fā)到monitor進(jìn)程,就是最開始還是client向Daemon請求說我要wait這個Container結(jié)束,那這個時候Docker Daemon發(fā)現(xiàn)自己已經(jīng)開啟熱升級功能的情況下,它將這個請求返回一個305 redirect
請求,把redirect請求返回給monitor,這時候客戶端收到這個response后,發(fā)現(xiàn)它是一個redirect ,它就會從location字段中拿到當(dāng)前要操作的這個container的monitor進(jìn)程的地址,是一個IP加端口的一個形式. client進(jìn)程就會給monitor進(jìn)程發(fā)送一個wait請求,這時候monitor進(jìn)程會等待container運行結(jié)束,當(dāng)container運行結(jié)束之后會給client端返回response.可以看到改成這樣一種工作流之后,DockerDaemon整個wait的請求過程跟Docker Daemon已經(jīng)沒有任何關(guān)系,它掛不掛其實對整個過程沒有任何的影響.
我接著介紹一下熱升級功能的實現(xiàn).首先為了兼容以前的方式,我們增加了一個開關(guān),就是hot restart參數(shù),并且將monitor的代碼組件進(jìn)行接口化設(shè)計,讓它支持以前的goroutine以及外部進(jìn)程兩種方式.
其次由于Container結(jié)束時Docker Daemon可能是存活狀態(tài),也可能是已經(jīng)死掉的狀態(tài),所以monitor進(jìn)程在Container結(jié)束的時候會將它的啟動和結(jié)束事件首先持久化到磁盤,再通知Docker Daemon更新Container狀態(tài).如果這時候Docker Daemon已經(jīng)掛掉了,我們就重啟一定的次數(shù),如果沒有通知成功,也不用繼續(xù)等待這個Daemon進(jìn)程啟動就直接退出了,因為它原先已經(jīng)把Container這個結(jié)束事件持久化到磁盤上了,當(dāng)Docker Daemon重啟之后,它就可以從磁盤加載這個Container的狀態(tài)遷移文件,從而可以正確的恢復(fù)Container的狀態(tài).
第三比如說為了解決wait以及attach這些命令的問題,我們就將這些請求重定向到monitor進(jìn)程進(jìn)行處理,比如說attach請求的IO流,它以前是通過Daemon進(jìn)行緩存的,那現(xiàn)在就通過monitor進(jìn)程進(jìn)行緩存,這樣Docker Daemon掛掉對于每個Container來說并沒有什么太大的影響.
第四比如說網(wǎng)絡(luò)狀態(tài),現(xiàn)在Container Daemon的網(wǎng)絡(luò)部分的代碼已經(jīng)全部遷出,遷出到一個新的工程叫做Libnetwork,Libnetwork中有很多的實體,比如說Libnetwork/Endpoint/Sandbox等有部分的狀態(tài)是保存在內(nèi)存中的,它原先自己會存儲一些global的網(wǎng)絡(luò)的狀態(tài),比如說global網(wǎng)絡(luò)指的是一些Overlay網(wǎng)絡(luò),這種網(wǎng)絡(luò),它需要一些全局信息的保存,所以它會存在global kv,global kv可能是zkEtcd或者是Consul ,我當(dāng)時在做這個功能的時候就為Libnetwork引入了localstore功能,這個功能就會存儲一些本地的網(wǎng)絡(luò)的狀態(tài)信息,比如說bridge模式的一些狀態(tài)信息.
相信使用Docker的人都會遇到網(wǎng)絡(luò)部分的問題,網(wǎng)絡(luò)部分是一個比較頭疼的問題,也是每一家都會必然解決的問題.原先Docker Daemon提供了兩種方式:
第一種是Host方式,是完全沒有隔離的一個方式,它的優(yōu)點是性能好,但是缺點也很明顯,就是沒有網(wǎng)絡(luò)隔離,有端口沖突的問題.
其次Docker Daemon提供了一個bridge方式,也就是NAT的網(wǎng)絡(luò)模式,這種網(wǎng)絡(luò)模式提供了網(wǎng)絡(luò)隔離的功能,它解決了端口沖突問題.但是Container IP是一個私有的IP,對外是不可見的.所以從另外一臺主機(jī)的Container想要直接訪問這個Container是不行的,必須得通過主機(jī)的一個映射之后的主機(jī)的一個IP,以及映射之后的端口去訪問這個Container.但是端口映射提高了業(yè)務(wù)遷移的成本,他們可能會需要去改代碼,或者去改配制.并且NAT的這種方式對網(wǎng)絡(luò)的IO性能比Host方式也低了接近10%.
看一下Gaia平臺在接入用戶業(yè)務(wù)的過程中遇到的一些網(wǎng)絡(luò)方面的需求.比如說用戶可能會覺得端口沖突問題它不想改代碼或者配置.第二就是可能有些業(yè)務(wù)會將本機(jī)的一個IP注冊到ZooKeeper上做服務(wù)發(fā)現(xiàn),這時候如果是使用NAT方式的話,這個私有IP如果注冊到ZooKeeper上是完全沒有任何意義的.第三就是有些業(yè)務(wù)會對有權(quán)限訪問自己服務(wù)的IP做限制,比如說做白名單限制,這時候在同一個主機(jī)上的Container互相訪問的時候,由于它的流量是從主機(jī)的Container首先發(fā)出,然后進(jìn)入到global space,這時候會對它的IP進(jìn)行原IP的替換,就是SNAT的過程.由于流量是從Docker 0進(jìn)入的,所以它會將原IP替換成Docker 0的IP,也就是一個私有的IP,這時候在同組機(jī)的另外一個Container中看到的原IP就不是這個主機(jī)的IP,它是Docker 0的原IP,很顯然這個IP是不能加入到它的白名單里面的,因為這是個私有IP,不能確定這個IP是從哪里訪問過來的.
我們當(dāng)初也做了一個改進(jìn),原先的SNAT的規(guī)則其實是MASQUERADE的規(guī)則,這個規(guī)則它會將原IP從哪個網(wǎng)卡進(jìn)就會替換成哪個網(wǎng)卡的一個IP,后來我們加了一條規(guī)則是將它的原IP寫死了,改成主機(jī)的IP,這樣它在另外一個Container看到的是主機(jī)的IP,所以就不會有問題.
下面一點就是很多業(yè)務(wù)使用騰訊的TGW網(wǎng)關(guān)對外提供服務(wù).我們在測試TGW對接NAT方式的時候發(fā)現(xiàn)報文是不通的,之所以不通是因為外面的流量訪問進(jìn)來之后,它會首先做一個DNAT的過程,DNAT的過程會把目的IP轉(zhuǎn)化成Container的一個私有IP,之后當(dāng)Container中的進(jìn)程進(jìn)行回包的時候,這時候其實走的是一個NAT的過程,因為它進(jìn)來的時候發(fā)生了DNAT的過程,這個過程是發(fā)生在PREROUTING階段,然后出去的時候是一個逆過程,這個時候這個包的IP的替換會發(fā)生在路由決策之后,這樣在路由決策的時候destination IP還沒有替換,所以導(dǎo)致這個TCP/IP的協(xié)議無法對包進(jìn)行封包,所以會導(dǎo)致通過TGW對接NAT的方式是不可行的.
下面一點是某些業(yè)務(wù),比如說GPU業(yè)務(wù),可能要求很好的網(wǎng)絡(luò)性能,這個也是通過NAT的方式無法解決的.
圖 3
下面就看我們對于網(wǎng)絡(luò)的改進(jìn)(如圖3).前面很多公司也介紹了,大家會給Container分配一個內(nèi)網(wǎng)IP,我們也是這么做的.但是我們所不同的是我們在原先的網(wǎng)橋上面加了一個VLAN設(shè)備,這個起到什么怎么呢?比如說有一些內(nèi)網(wǎng)IP跟主機(jī)的IP不處于同一個VLAN的時候,如果直接分配到Container中,通過網(wǎng)橋橋接起來,這時候網(wǎng)絡(luò)是不能通的,因為需要給它出的流量打上一個vlan tag它才能通,這樣我們就在原有的基礎(chǔ)之上加了一個VLAN設(shè)備,這個VLAN設(shè)備后面再橋接一個網(wǎng)橋,然后讓這些與主機(jī)的IP不同一個VLAN的IP的Container橋接到另外一個網(wǎng)橋上面, 這樣它出來都會打上這個vlan tag ,這樣就可以通了.這種方式相比NAT的方式,可能的IP對外可見,沒有端口映射帶來的遷移成本,也少了iptables或者是用戶態(tài)進(jìn)程的一個轉(zhuǎn)發(fā)過程,所以性能略優(yōu)于NAT的方式. 結(jié)合Gaia等上層調(diào)度系統(tǒng)可以實現(xiàn)IP漂移的功能,就是在Container發(fā)生遷移的過程中IP可以保持不變.
下面介紹一下SR-IOV技術(shù),SR-IOV技術(shù)是一個硬件虛擬化技術(shù),它可以由一個網(wǎng)卡虛擬出多個功能接口,每個功能接口實際上是可以做一個網(wǎng)卡使用的.通過這種硬件取代內(nèi)核的虛擬網(wǎng)絡(luò)設(shè)備的方式,可以極大的提高網(wǎng)絡(luò)的性能,并且減少了物理機(jī)CPU的消耗.Docker 1.9.0版本支持網(wǎng)絡(luò)插件的方式,所以我們實現(xiàn)了一個SR-IOV的網(wǎng)絡(luò)插件,去非常方便的使用這種方式.
下面(圖4)的一個圖表就是我們測試使用SR-IOV之后所帶來的性能的提升.橫坐標(biāo)是測試的主要是在虛擬化比是1:1和1:4的情況下,網(wǎng)絡(luò)流量包的大小是1個字節(jié)、64個字節(jié)和256個字節(jié)的情況下SR-IOV的方式相比NAT方式所帶來的包量的提升.
圖 4
第三就是Overlay網(wǎng)絡(luò)為我們提供了多租戶網(wǎng)絡(luò)隔離功能,并且每個租戶可以分配一個獨立的虛擬的IP段.這種方式不需要分配一個內(nèi)網(wǎng)的IP,也不需要依賴于NAT的方式,這種方式想必是以后必然會用上的一種多租戶的一個解決方案.但是Docker原生的Libnetwork在overlay driver為接入的Container默認(rèn)連接了一個NAT網(wǎng)絡(luò)對外提供服務(wù),通過這個NAT的網(wǎng)絡(luò)Container就不單可以訪問與自己位于一個Overlay的其它的Container,也可以訪問外網(wǎng),因為它是通過NAT的方式去訪問的.但是很多Container并不一定需要訪問內(nèi)網(wǎng)或者外網(wǎng),所以我們給Libnetwork對象增加了一個internal開關(guān),創(chuàng)建完全與外界隔離的一個網(wǎng)絡(luò).
圖 5
這個圖(圖5 )是位于不同主機(jī)上的5個Container,C1和C2構(gòu)成一個Overlay網(wǎng)絡(luò),C3、C4、C5構(gòu)成另外一個Overlay網(wǎng)絡(luò),這兩個網(wǎng)絡(luò)彼此之間不同,如果說某些Container想要對外提供服務(wù),這時候我們可以給它分配一個內(nèi)網(wǎng)的IP,讓它對外提供服務(wù).
接下來分享一下我們在存儲方面的工作.Container數(shù)據(jù)存儲相信也是使用Docker的人繞不開的一個問題.我們的做法就是在Container遷移后不需要保留的數(shù)據(jù),就給它分配一個host volume進(jìn)行存儲它的數(shù)據(jù).如果這個Container遷移之后它的數(shù)據(jù)需要保留,給它分配一個Ceph RBD存儲,我們是使用了Ceph volume plugin為每個Container分配一個RBD的存儲目錄.
接下來介紹一下我們在資源隔離方面的一些工作.Docker本身對內(nèi)存的控制是hard limit方式,就是Container的進(jìn)程的內(nèi)存如果超過它申請的,比如說5G的一個總的上限,它就會將這個Container給殺掉.但是這種方式就非常不方便,比如說用戶可能并不知道它的Container實際到峰值的時候使用到多少的內(nèi)存,并且在很多情況下當(dāng)Container超過它的內(nèi)存上限的時候,這個機(jī)器的所有內(nèi)存其實還有很多的空閑,這時候我們的做法是為這個機(jī)器上所有的Container設(shè)置一個總的流程上限,Hard limit.單個Container內(nèi)存使用超出申請值,如果這個機(jī)器的所有Container沒有超出設(shè)置的總的Hard limit值時不會觸發(fā)kill,只有當(dāng)總的內(nèi)存超過Hard limit一定的百分比之后,才kill那些超出內(nèi)存申請值最多的Container,這種方式可以大大降低集群中Container被kill的幾率.
下面一點是我們在做網(wǎng)絡(luò)出帶寬的控制的時候發(fā)現(xiàn),如果直接使用net_cls的方式對流量進(jìn)行標(biāo)志的時候,數(shù)據(jù)包會經(jīng)過bridge之后pid會發(fā)生丟失,這時候打的標(biāo)記是沒有效果的.所以我們的做法就是在iptables的mangle表中對Container出流量按IP進(jìn)行標(biāo)記,這個時候我們就可以方便的使用TC工具對標(biāo)記的流量進(jìn)行限制.
下面一個問題也是很多講師都講到的問題,是容器中自然顯示的問題,原先跑在物理機(jī)或者虛擬機(jī)中的業(yè)務(wù)在使用騰訊網(wǎng)管系統(tǒng)記錄機(jī)器的資源使用情況,但是如果都遷移到Container中之后,這個時候Container中顯示的資源可能是主機(jī)的資源,那并不是它自己實際使用的資源.所以我們就需要解決這個問題,可能有一些團(tuán)隊是通過修改內(nèi)核的方式去解決的,但是修改內(nèi)核可能還需要自己維護(hù)這些patch.我們的做法就是通過FUSE實現(xiàn)了一個用戶態(tài)的文件系統(tǒng),這個用戶態(tài)的文件系統(tǒng)使用的是Cgroup的數(shù)據(jù)統(tǒng)計Container的實際資源使用,它可以為每個Container生成仿真的meminfo stats或者diskstats cpuuinfo文件,然后在啟動Container的時候?qū)⑦@些文件直接mount到Container中,這個程序是用go語言實現(xiàn)的,使得它非常方便的嵌入到Docker的代碼中,整個過程對Docker用戶透明,這個鏈接(?https://github.com/chenchun/cgroupfs)是我們做的這個功能的代碼的地址.
文:陳純
文章出處:Docker
轉(zhuǎn)載請注明本頁網(wǎng)址:
http://www.snjht.com/jiaocheng/4470.html