前言
在私有雲的容器化過程中,我們並不是白手起家開始的。而是接入了公司已經運行了多年的多個系統,包括自動編譯打包,自動部署,日志監控,服務治理等等系統。在容器化之前,基礎設施主要以物理機和虛擬機為主。因此,我們私有雲落地的主要工作是基礎設施容器化,同時在應用的運維方面,兼用了之前的配套系統。利用之前的歷史系統有利有弊,這些后面再談。在這里我主要同大家分享一下在容器化落地實踐中的一些經驗和教訓。
容器與虛擬機
當我們向別人講述什么是容器的時候,常常用虛擬機作類比。在給用戶進行普及的時候,我們可以告訴他,容器是一種輕量級的虛擬機。但是在真正的落地實踐的時候,我們要讓用戶明白這是容器,而不是虛擬機。這兩者是有本質的區別的。
虛擬機的本質上是模擬。通過模擬物理機上的硬件,向用戶提供諸如CPU、內存等資源。因此虛擬機上可以且必須安裝獨立的操作系統,系統內核與物理機的系統內核無關。因此一台物理機上有多個虛擬機時,一個虛擬機操作系統的崩潰不會影響到其他虛擬機。而容器的本質是經過隔離與限制的linux進程。容器實際使用的還是物理機的資源,容器之間是共享了物理機的linux內核。這也就意味着當一個容器引發了內核crash之后,會殃及到物理機和物理機上的其他容器。從這個角度來說,容器的權限和安全級別沒有虛擬機高。但是反過來說,因為能夠直接使用CPU等資源,容器的性能會優於物理機。
容器之間的隔離性依賴於linux提供的namespace。namespace雖然已經提供了較多的功能,但是,系統的隔離不可能如虛擬機那么完善。一個最簡單的例子,就是一個物理機上的不同虛擬機可以設置不同的系統時間。而同一個物理機的容器只能共享系統時間,僅僅可以設置不同的時區。
另外,對於容器資源的限制是通過linux提供的cgroup。在容器中,應用是可以感知到底層的基礎設施的。而且由於無法充分隔離,從某種程度上來說,容器可以看到宿主機上的所有資源,但實際上容器只能使用宿主機上的部分資源。
我舉個例子來說。一個容器的CPU,綁定了0和1號核(通過cpuset設置)。但是如果應用是去讀取的/proc/cpuinfo
的信息,作為其可以利用的CPU資源,則將會看到宿主機的所有cpu的信息,從而導致使用到其他的沒有綁定的CPU(而實際由於cpuset的限制,容器的進程是不能使用除0和1號之外的CPU的)。類似的,容器內/proc/meminfo
的信息也是宿主機的所有內存信息(比如為10G)。如果應用也是從/proc/meminfo
上獲取內存信息,那么應用會以為其可用的內存總量為10G。而實際,通過cgroup對於容器設置了1G的最高使用量(通過mem.limit_in_bytes)。那么應用以為其可以利用的內存資源與實際被限制使用的內存使用量有較大出入。這就會導致,應用在運行時會產生一些問題,甚至發生OOM崩潰。
這里我舉一個實際的例子。
這是我們在線上的一個實際問題,主要現象是垃圾回收偏長。具體的問題記錄和解決在開濤的博客中有詳細記錄使用Docker容器時需要更改GC並發參數配置。
這里主要轉載下問題產生的原因。
1、因為容器不是物理隔離的,比如使用Runtime.getRuntime().availableProcessors() ,會拿到物理CPU個數,而不是容器申請時的個數,
2、CMS在算GC線程時默認是根據物理CPU算的。
這個原因在根本上來說,是因為docker在創建容器時,將宿主機上的諸如/proc/cpuinfo
,/proc/meminfo
等文件掛載到了容器中。使得容器從這些文件中讀取了相關信息,誤以為其可以利用全部的宿主機資源。而實際上,容器使用的資源受到了cgroup的限制。
上面僅僅舉了一個java的例子。其實不僅僅是java,其他的語言開發出來的應用也有類似的問題。比如go上runtime.GOMAXPROCS(runtime.NumCPU())
,nodejs的Environment::GetCurrent()
等,直接從容器中讀取了不准確的CPU信息。又比如nginx設置的cpu親和性綁定worker_cpu_affinity
。也可能綁定了不准確的CPU。
解決的渠道一般分為兩種:
一種是逢山開路遇水搭橋,通過將容器的配置信息,比如容器綁定的cpu核,容器內存的限制等,寫入到容器內的一個標准文件container_info
中。應用根據container_info
中的資源信息,調整應用的配置來解決。比如修改jvm的一些參數,nginx的修改綁定的cpu編號等。
在docker后來的版本里,容器自己的cgroup會被掛載到容器內部,也就是說容器內部可以直接通過訪問
/sys/fs/cgroup
中對應的文件獲取容器的配置信息。就不必再用寫入標准文件的方式了。
另一種是增強容器的隔離性,通過向容器提供正確的諸如/proc/cpuinfo
,/proc/meminfo
等文件。lxcfs
項目正是致力於此方式。
我們使用的是前一種方式。前一種方式並不能一勞永逸的解決的所有問題,需要對於接入的應用進行分析,但是使用起來更為穩定。
graph driver的坑
在容器化落地的實踐過程中,難免會遇到很多坑。其中一個坑是就是graph driver的選擇問題。當時使用的centos的devicemapper,遇到了內核crash的問題。這個問題在當時比較棘手,我們一開始沒能解決,於是我們自己寫了一版graph driver,命名為vdisk。這個vdisk主要是通過稀疏文件來模擬union filesystem的效果。這個在實際使用中,會減慢創建速度,但是益處是帶來了極高的穩定性,而且不再有dm的data file的預設磁盤容量的限制了。因為容器創建后,還需要啟動公司的工具鏈,運維確認,然后切流量等才能完成上線,所以在實際中,該方式仍然有着極好的效果。
不過我們沒有放棄dm,在之后我們也解決了dm帶來的內核crash問題,這個可以參見蘑菇街對此的分享記錄使用Docker時內核隨機crash問題的分析和解決。配置nodiscard雖然解決了內核crash的問題,但是在實際的實踐中,又引入了一個新的問題,就是容器磁盤超配的問題。就比如dm的data是一個稀疏問題,容量為10G,現在有5個容器,每個容器磁盤預設空間是2G。但是容器文件實際占用只有1G。這時理論上來說,創建第6個2G的容器是沒問題的。但是如果這個5個容器,雖然實際文件只占用1G,但是容器中對於某個大文件進行反復的創建、刪除(如redis的aof),則在dm的data中,會實際占用2G的空間。這時創建第6個容器就會因為dm的data空間不足而無法創建。這個問題從dm角度來說不好直接解決,在實際操作中,通過與業務的溝通,將這種頻繁讀寫的文件放置到外掛的volume中,從而解決了這個問題。
從這個坑中,我們也對於后來的業務方做了建議,鏡像和根目錄盡量只存只讀和少量讀寫的文件,對於頻繁讀寫或大量寫的文件,盡量使用外掛的volume。當然,我們對外掛的volume也做了一些容量和寫速度的限制,以免業務之間互相影響。規范業務對於容器的使用行為。
docker版本選擇
在開始研究docker時,docker的穩定版本還是1.2和1.3。但是隨着docker的火熱,版本迅速迭代。誠然,新的版本增加了很多的新功能。但是也可能引入了一些新的bug。我們使用docker的基本理念是新技術不一定都步步跟上。更多的是考慮實際情況,版本盡量的穩定。落地是第一要務。而且我們的容器平台1.0更偏向於基礎設施層,容器盡量不進行重啟和遷移。因為業務混合部署在集群中,當一個宿主機上的docker需要升級,則容器需要遷移,那么可能涉及到多個業務方,要去與各個業務方的運維、研發等進行溝通協調,相當的費時費力。我們在充分測試之后,定制了自己的docker版本,並穩定使用。docker提供的功能足夠,盡量不再升級,新增功能我們通過其他的方式進行實現。
監控與巡檢
這里所指的監控主要指對資源,如CPU、內存等的監控。如前文中所述,由於容器的隔離性不足,因此容器中使用top
等看到的信息,也不完全是容器內的信息,也包含有宿主機的信息。如果用戶直接在容器中使用工具獲取資源監控信息,容易被這些信息誤導,無法准確判斷資源的使用情況。因此我們自己研發完成了一套容器平台監控系統,負責容器平台的資源監控,對於物理機和容器的資源使用情況進行采集和整理,統一在前端呈現給用戶。並集成了資源告警、歷史查詢等功能。
由於容器平台系統,包含了多個子系統,以及許多的模塊。系統龐大,就涉及到了諸多的配置、狀態檢查等等。我們對應開發了一套巡檢系統,用於巡查一些關鍵信息等。巡檢系統一方面定時巡查,以便核實各個組件的工作狀態;另一方面,當出現狀況時,使用巡檢系統對於定位出現問題的節點,分析問題產生的原因有極多的幫助。
標准化的工具
在沒有容器化之前,公司內部逐漸形成了自己的一套工具鏈,諸如編譯打包、自動部署、統一監控等等。這些已經為部署在物理機和虛擬機中的應用提供了穩定的服務。因此在我們的私有雲落地實踐中,我們盡可能的兼容了公司的工具鏈,在制作鏡像時,將原有的工具鏈也都打入了鏡像中。真正實現了業務的平滑遷移。
當然,打包了諸多的工具,隨之帶來的就是鏡像的龐大,鏡像的體積也不可遏制的從幾百兆增長到了GB級別。而借助於工具鏈的標准化,鏡像的種類就被縮減為了幾種。考慮到創建容器的速度,我們采用了鏡像預分發的方式,將最新版本的鏡像及時推送到計算節點上,雖然多占用了一些磁盤空間,但是有效防止了容器集中創建時,鏡像中心的網絡、磁盤讀寫成為瓶頸的問題。
使用已有工具鏈也意味着喪失了docker容器的一些優良特性。比如應用的發包升級上線仍然通過既有的自動部署系統,而無法利用docker的鏡像分層。工具鏈之間的壁壘也制約了平台的集成能力,難於實現一鍵部署的效果。容器的彈性和遷移也只能以一個空殼容器本身的伸縮遷移體現,而不是應用層級的伸縮遷移。
彈性與遷移
彈性主要包括橫向伸縮和縱向伸縮。橫向伸縮主要是指調整應用容器的數量,這個主要通過創建/銷毀容器進行實現。縱向伸縮主要是對單個容器的資源規格進行改變。因為容器對於CPU和內存的限制,主要是通過cgroup實現的。因此縱向的伸縮主要是通過修改cgroup中對應的值進行。
容器的遷移還是冷遷移的方式呈現。由於公司相關業務的要求,容器的IP要盡量保持不變。因此我們在neutron中做了定制,可以在遷移后保證容器的ip地址不變,這樣對外啟動后呈現的服務不會有變化。
容器的運維
目前運行有大量容器,部署在多個機房,分為多個集群。如此大規模的容器運維,實際集群的運維人員並不多。主要原因是對於運維的權限進行了分割。對於物理機、容器生命周期的管理,由集群的運維負責。而各個線上應用的運維,由各個應用配合的垂直運維(又稱為應用運維)負責。一般問題,如物理機下線,因為涉及到應用下的該實例需要下線,由集群運維查看該物理機上的容器所屬的應用,需要通知垂直運維,配合容器遷移,而后重新上線提供服務。二新增機器或者集群,對機器部署了容器平台系統后,即可交付集群運維,用以創建容器實例,並進而根據應用的申請,分配給各個應用。相較於一個應用的平台,很多操作還有一些手工的成分,因此還需要投入相當的人力在集群管理上。