在私有云的容器化過程當中,咱們並非白手起家開始的。而是接入了公司已經運行了多年的多個系統,包括自動編譯打包,自動部署,日誌監控,服務治理等等系統。在容器化以前,基礎設施主要以物理機和虛擬機爲主。所以,咱們私有云落地的主要工做是基礎設施容器化,同時在應用的運維方面,兼用了以前的配套系統。利用以前的歷史系統有利有弊,這些後面再談。在這裏我主要同你們分享一下在容器化落地實踐中的一些經驗和教訓。前端
當咱們向別人講述什麼是容器的時候,經常用虛擬機做類比。在給用戶進行普及的時候,咱們能夠告訴他,容器是一種輕量級的虛擬機。可是在真正的落地實踐的時候,咱們要讓用戶明白這是容器,而不是虛擬機。這二者是有本質的區別的。java
虛擬機的本質上是模擬。經過模擬物理機上的硬件,向用戶提供諸如CPU、內存等資源。所以虛擬機上能夠且必須安裝獨立的操做系統,系統內核與物理機的系統內核無關。所以一臺物理機上有多個虛擬機時,一個虛擬機操做系統的崩潰不會影響到其餘虛擬機。而容器的本質是通過隔離與限制的linux進程。容器實際使用的仍是物理機的資源,容器之間是共享了物理機的linux內核。這也就意味着當一個容器引起了內核crash以後,會殃及到物理機和物理機上的其餘容器。從這個角度來講,容器的權限和安全級別沒有虛擬機高。可是反過來講,由於可以直接使用CPU等資源,容器的性能會優於物理機。node
容器之間的隔離性依賴於linux提供的namespace。namespace雖然已經提供了較多的功能,可是,系統的隔離不可能如虛擬機那麼完善。一個最簡單的例子,就是一個物理機上的不一樣虛擬機能夠設置不一樣的系統時間。而同一個物理機的容器只能共享系統時間,僅僅能夠設置不一樣的時區。linux
另外,對於容器資源的限制是經過linux提供的cgroup。在容器中,應用是能夠感知到底層的基礎設施的。並且因爲沒法充分隔離,從某種程度上來講,容器能夠看到宿主機上的全部資源,但實際上容器只能使用宿主機上的部分資源。nginx
我舉個例子來講。一個容器的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崩潰。redis
這裏我舉一個實際的例子。docker
這是咱們在線上的一個實際問題,主要現象是垃圾回收偏長。具體的問題記錄和解決在開濤的博客中有詳細記錄使用Docker容器時須要更改GC併發參數配置。centos
這裏主要轉載下問題產生的緣由。安全
一、由於容器不是物理隔離的,好比使用Runtime.getRuntime().availableProcessors() ,會拿到物理CPU個數,而不是容器申請時的個數,網絡
二、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的選擇問題。當時使用的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的穩定版本仍是1.2和1.3。可是隨着docker的火熱,版本迅速迭代。誠然,新的版本增長了不少的新功能。可是也可能引入了一些新的bug。咱們使用docker的基本理念是新技術不必定都步步跟上。更多的是考慮實際狀況,版本儘可能的穩定。落地是第一要務。並且咱們的容器平臺1.0更偏向於基礎設施層,容器儘可能不進行重啓和遷移。由於業務混合部署在集羣中,當一個宿主機上的docker須要升級,則容器須要遷移,那麼可能涉及到多個業務方,要去與各個業務方的運維、研發等進行溝通協調,至關的費時費力。咱們在充分測試以後,定製了本身的docker版本,並穩定使用。docker提供的功能足夠,儘可能再也不升級,新增功能咱們經過其餘的方式進行實現。
這裏所指的監控主要指對資源,如CPU、內存等的監控。如前文中所述,因爲容器的隔離性不足,所以容器中使用top
等看到的信息,也不徹底是容器內的信息,也包含有宿主機的信息。若是用戶直接在容器中使用工具獲取資源監控信息,容易被這些信息誤導,沒法準確判斷資源的使用狀況。所以咱們本身研發完成了一套容器平臺監控系統,負責容器平臺的資源監控,對於物理機和容器的資源使用狀況進行採集和整理,統一在前端呈現給用戶。並集成了資源告警、歷史查詢等功能。
因爲容器平臺系統,包含了多個子系統,以及許多的模塊。系統龐大,就涉及到了諸多的配置、狀態檢查等等。咱們對應開發了一套巡檢系統,用於巡查一些關鍵信息等。巡檢系統一方面定時巡查,以便覈實各個組件的工做狀態;另外一方面,當出現情況時,使用巡檢系統對於定位出現問題的節點,分析問題產生的緣由有極多的幫助。
在沒有容器化以前,公司內部逐漸造成了本身的一套工具鏈,諸如編譯打包、自動部署、統一監控等等。這些已經爲部署在物理機和虛擬機中的應用提供了穩定的服務。所以在咱們的私有云落地實踐中,咱們儘量的兼容了公司的工具鏈,在製做鏡像時,將原有的工具鏈也都打入了鏡像中。真正實現了業務的平滑遷移。
固然,打包了諸多的工具,隨之帶來的就是鏡像的龐大,鏡像的體積也不可遏制的從幾百兆增加到了GB級別。而藉助於工具鏈的標準化,鏡像的種類就被縮減爲了幾種。考慮到建立容器的速度,咱們採用了鏡像預分發的方式,將最新版本的鏡像及時推送到計算節點上,雖然多佔用了一些磁盤空間,可是有效防止了容器集中建立時,鏡像中心的網絡、磁盤讀寫成爲瓶頸的問題。
使用已有工具鏈也意味着喪失了docker容器的一些優良特性。好比應用的發包升級上線仍然經過既有的自動部署系統,而沒法利用docker的鏡像分層。工具鏈之間的壁壘也制約了平臺的集成能力,難於實現一鍵部署的效果。容器的彈性和遷移也只能以一個空殼容器自己的伸縮遷移體現,而不是應用層級的伸縮遷移。
彈性主要包括橫向伸縮和縱向伸縮。橫向伸縮主要是指調整應用容器的數量,這個主要經過建立/銷燬容器進行實現。縱向伸縮主要是對單個容器的資源規格進行改變。由於容器對於CPU和內存的限制,主要是經過cgroup實現的。所以縱向的伸縮主要是經過修改cgroup中對應的值進行。
容器的遷移仍是冷遷移的方式呈現。因爲公司相關業務的要求,容器的IP要儘可能保持不變。所以咱們在neutron中作了定製,能夠在遷移後保證容器的ip地址不變,這樣對外啓動後呈現的服務不會有變化。
目前運行有大量容器,部署在多個機房,分爲多個集羣。如此大規模的容器運維,實際集羣的運維人員並很少。主要緣由是對於運維的權限進行了分割。對於物理機、容器生命週期的管理,由集羣的運維負責。而各個線上應用的運維,由各個應用配合的垂直運維(又稱爲應用運維)負責。通常問題,如物理機下線,由於涉及到應用下的該實例須要下線,由集羣運維查看該物理機上的容器所屬的應用,須要通知垂直運維,配合容器遷移,然後從新上線提供服務。二新增機器或者集羣,對機器部署了容器平臺系統後,便可交付集羣運維,用以建立容器實例,並進而根據應用的申請,分配給各個應用。相較於一個應用的平臺,不少操做還有一些手工的成分,所以還須要投入至關的人力在集羣管理上。