大廠都在玩的容器技術到底是什么?
引言
著名雜志《經濟學人》曾經評價“沒有集裝箱,就沒有全球化”,可以說集裝箱的出現重塑了現代貨運體系,實現了交通運輸行業的標準化,有效降低物流運輸成本,極大提升了貨物轉運效率。而在云原生領域,容器就相當于集裝箱,它使得軟件發布以及軟件運行隔離實現標準化,引領了云原生基礎設施的跨越式發展。從某種意義上來說,容器技術重塑了整個軟件供應鏈。今天就和大家聊聊各個大廠都在玩的容器技術到底是什么。
為什么需要容器技術
在正式介紹容器技術之前,我們先來看下軟件領域為什么需要容器技術。一項新技術的出現必定是為了解決當下遇到的的某項具體問題或者說更加提高現有軟件運行效率。那我們就來分析下在容器技術出現之前,軟件領域到底面臨什么樣的問題。
在很早很早之前,我們部署服務的時候都是直接部署在硬件服務器上。如果想對服務進行擴容就必須要購買服務器,然后再進行應用部署以及各種繁瑣的環境配置以及服務配置,由于都是人工操作所以還特別容易出錯,不僅浪費時間還很費程序猿,因此服務部署以及遷移效率都極其低下。在互聯網早期的時候,用戶數以及業務體量還不是很大,人工操作還能夠應付得過來。但是隨著業務規模不斷發展以及用戶數的爆炸式增長,這樣的軟件服務生產方式已經無法滿足業務高速發展的需求。將應用服務直接部署在服務器上主要有以下三方面的問題。
服務互干擾
一臺服務器一般不會只部署一個服務應用,都是部署多個服務應用。但是由于這些服務都是公用服務器中的CPU、內存、硬盤以及網絡IO等服務器資源,那么必定就會存在資源互相爭用、服務互相影響的情況。
資源利用低
業務是存在高峰期和低谷期的,對于電商平臺來說,一般深夜的屬于業務低谷期,這個時候的服務器的資源利用率相比業務高峰期的時候要低很多。因此在業務低谷期,實際服務器的資源利用率比較低,不能物盡其用。
遷移擴展難
原有的服務器數量不足以應對高速發展的業務時,就需要不斷的進行服務器實例擴充,但是由于服務直接部署在服務器中,在進行服務遷移擴展的時候,需要各種依賴庫、環境配置以及網絡配置等,步驟復雜,擴展困難。
正是軟件領域面臨這么多問題,因此大神們才會發揮他們的聰明才智不斷推進技術發展。因此大神們就設想如果有一種部署方式可以實現差別的服務構建,那就可以解決服務部署的各種配置問題。如果有一種技術可實現真正的資源隔離,進程之間互相不影響,這樣就可以解決互相影響的問,那將是多么美好的一件事情。這些美好的技術設想實際就是容器技術發展的原動力。當然技術的發展并不是一蹴而就的,總是隨著時間的推移不斷進行完善。
容器技術的思想最早可以追溯到1979年,這一年Unix版本V7發布,在這個版本中作者發明了chroot系統調用,通過它可以實現改變一個進程及其子進程的根目錄到另外一個目錄下,為進程指定一個單獨的、新的文件系統上下文環境,可見在很早的時候Unix的大神們已經有了進行進程隔離的意識和思想了。
那么到底什么叫進程隔離呢?舉個栗子大家一看就明白,相信很多同學都使用過tomcat這個web容器,我們可以在tomcat中部署war服務。假設我們有3 個服務都部署在了1個tomcat實例中,假如我們需要重啟其中的某個服務,我們就需要重啟整個tomcat,那么tomact中的3個服務都會被重啟。重啟一個服務影響其他2個服務,服務操作存在高度的耦合。但是如果我們把三個服務部署到三個不同的tocmat容器實例中,那么重啟任何一個服務都不會影響到其他兩個服務,實現了服務的獨立管理。
通過部署多個實例,我們實現了服務之間的進程隔離,而進程擁有獨立的地址空間以及執行上下文。但是這種形式的獨立管理并不是真正意義上的獨立管理,為什么這么說呢?因為實際上他們還是共用服務器的CPU、內存以及IO等服務器資源。假如Tomcat1占用的服務器內存高了,那么剩余給Tomcat2以及Tomcat2的內存分配就相對來說會變少。因此實際上這三個Tomcat雖然是獨立的進程但還是會相互影響。有沒有辦法實現真正的獨立,不互相影響呢?
實際上實現資源隔離的方式大概有硬件虛擬化、OS虛擬化以及硬件分區等幾種常見的實現方式。但是綜合各方面的表現,OS虛擬化成為后期容器技術發展的主流技術路線。
容器技術的解決的核心問題就是實現軟件運行時的環境隔離,通過容器構建一個標準的、無差別的服務運行環境,這樣就不會因為環境、配置以及依賴等原因造成的在這臺服務器上好好的,在另外的一臺服務器上又不行的尷尬問題。2008年的時候,通過將Cgroups的資源管理能力以及Namespace的視圖隔離能力糅合在一起,Linux Container被合入linux主線,Linux Container是Linux系統提供的容器技術,能提供輕量級的虛擬化能力,能夠進行隔離進程和資源。通過這種OS層面的虛擬化技術,實際上也就是解決了容器的核心問題即為如何實現服務運行時的隔離。因此可以說Linux Container是后期實現Docker技術的基礎。
在2013年,Docker正式發布。Docker是基于Linux Container技術發展而來的,它的口號是:“Build,Ship and Run Any App,Anywhere”。Docker創新構建了一種全新的軟件打包、軟件分發以及軟件運行的機制,它通過容器鏡像,將應用服務本身以及運行服務所需要的環境、配置、資源文件以及依賴庫等都打包成一個唯一版本的軟件鏡像包。往后在任何地方運行的服務都是基于這個軟件鏡像包來進行構建和運行的,真正解決了如何高效發布軟件以及如何高效運行軟件的兩大核心問題。關于Docker,后面會有專門的文章進行介紹。
類似下圖這種虛擬機與容器的對比圖相信大家都看過,左邊的部分就是虛擬機的大致原理,實際上是通過Hypervisor實現服務器硬件資源的虛擬化,從而在服務器中模擬出來具備CPU、內存、硬盤等完整計算機硬件基礎設施同時還有Guest OS,簡單理解就是在服務器中派生出了新的虛擬服務器。用戶的應用服務進程都是運行在這些虛擬出來的計算機資源當中的。同一臺服務器可以同時運行多個操作系統,各個操作系統之間是相互隔離的,雖然安全性隔離性都很完備,但是大家應該能看得出來,硬件虛擬化需要額外的性能開銷,因此它是一種非常重的資源隔離技術。
容器技術原理
前面和大家簡要介紹了容器技術的發展,我們都知道了容器最核心的是實現了應用服務資源隔離。那么到底容器是如何實現資源隔離的呢?實際上它依賴了Namespace(命名空間)、Cgroups(控制組)底層Linux內核技術。
Namespace?
我們先來看下wiki中關于Linux Namespace的描述:
Namespaces are a feature of the Linux kernel that partitions kernel resources such that one set of processes sees one set of resources while another set of processes sees a different set of resources. The feature works by having the same namespace for a set of resources and processes, but those namespaces refer to distinct resources。
這描述看上去就很繞,總結一下就是Linux Namespace是Linux kernel提供的一種進行資源隔離的底層能力。通過Namespace實現對服務器全局資源的封裝隔離,使得不同Namespace中的進程互相獨立,彼此透明。
如下圖所示,在一臺宿主服務器當中,Linux的Namespace實際就是Linux內核中資源隔離的實現方式。玩過Docker的同學都知道,到我們run了一個docker鏡像之后,在服務器中就會產生一個docker容器,當我們進入到容器里面去之后,使用ps命令查看,我們會驚奇的發現容器中運行的服務pid=1。相當于這個服務是容器內的第一號進程。而如果我們在服務器中運行這個服務,操作系統會給這個服務進程分配一個全局唯一的進程號,假設是34134。同樣是這個程序在服務器中運行pid是34134,但是在Docker容器中的pid卻是1。這是怎么回事呢?
這種隔離技術就是Namspace機制,通過Namespace構建了一個全新的運行環境,與其他運行環境互相透明,讓服務在這個小空間里面自封為王。實際上是Linux kernel內核提供的系統調用函數clone()。通過clone函數創建的新進程會在一個全新的進程空間當中。因此實際上容器的本質還是進程,只不過是一種特殊的進程,在創建它的時候指定了一些參數,使得容器只能訪問到當前Namespace內的文件、IO等資源。
int pid = clone(main_function, stack_size, SIGCHLD, NULL);
Namespace除了PID還實現了其他不同資源級別的隔離。
名稱 宏定義 隔離內容
IPC CLONE_NEWIPC System V IPC, POSIX message queues (since Linux 2.6.19)
Network CLONE_NEWNET Network devices, stacks, ports, etc. (since Linux 2.6.24)
Mount CLONE_NEWNS Mount points (since Linux 2.4.19)
PID CLONE_NEWPID Process IDs (since Linux 2.6.24)
User CLONE_NEWUSER User and group IDs (started in Linux 2.6.23 and completed in Linux 3.8)
UTS CLONE_NEWUTS Hostname and NIS domain name (since Linux 2.6.19)
Cgroups
Namespace機制幫助我們解決了資源隔離的問題,那么僅僅只是隔離對于容器運行來說就夠了嗎?雖然在容器內部應用服務可能是個王者,資源都是他獨享的,但是映射到服務器內核當中的真實的進程后它就是個各個普普通通的青銅,需要和其他青銅共享計算機各類資源。顯然這不是我們想要的容器的效果。而Linux Cgroups技術就是幫助我們設置資源限制的功能。Linux Cgroups即Linux Control Group就是限制一個進程組能夠使用的資源上限,包括 CPU、內存、磁盤、網絡帶寬等等。
在Linux操作系統中,Cgroups的能力通過內核的文件系統操作接口暴露出來的,它是以文件和目錄的方式組織在操作系統的/sys/fs/cgroup路徑下。在Linux服務器中輸入mount -t cgroup,可以看到如下的目錄文件結構。
從上圖中我們可以看到,在/sys/fs/cgroup目錄下有很多關于資源的子目錄或者說子系統,如cpucet、cpu以及memory等。這些目錄都是當前服務器可以被Cgroups進行限制的資源類別。而在子系統對應的資源種類下,你就可以看到該類資源具體可以被限制的方法。我們可以查看memory下面的配置文件,ls /sys/fs/cgroup/memory。
我們再看下Docker的源碼:
// New creates and initializes a new containerd server
func New(ctx context.Context, config *Config) (*Server, error) {
//...
if err := apply(ctx, config); err != nil {
return nil, err
}
//...
}
// apply sets config settings on the server process
func apply(ctx context.Context, config *Config) error {
if config.OOMScore != 0 {
log.G(ctx).Debugf("changing OOM score to %d", config.OOMScore)
if err := sys.SetOOMScore(os.Getpid(), config.OOMScore); err != nil {
log.G(ctx).WithError(err).Errorf("failed to change OOM score to %d", config.OOMScore)
}
}
if config.Cgroup.Path != "" {
cg, err := cgroups.Load(cgroups.V1, cgroups.StaticPath(config.Cgroup.Path))
if err != nil {
if err != cgroups.ErrCgroupDeleted {
return err
}
if cg, err = cgroups.New(cgroups.V1, cgroups.StaticPath(config.Cgroup.Path), &specs.LinuxResources{}); err != nil {
return err
}
}
if err := cg.Add(cgroups.Process{
Pid: os.Getpid(),
}); err != nil {
return err
}
}
return nil
}
通過代碼我么可以看得出來在創建一個Docker容器的時候,調用了apply函數,在這個apply函數中會進行cgourp的加載,而后通過cg.Add將創建的容器id添加到控制組的task文件中。
總結
本文主要對容器技術的發展進行了簡單回顧,從很早之前服務部署以及應用方面存在的不足出發,闡述了容器技術的出現到底解決了什么問題,同時和大家分享了容器技術的本質以及原理,相信通過本文大家可以對容器技術有一個基本的感受,后續再和大家繼續分享云原生技術體系。