使用容器的正確方式,Docker在雪球的技術(shù)實(shí)踐
原創(chuàng)【51CTO.com原創(chuàng)稿件】雪球目前擁有一千多個(gè)容器,項(xiàng)目數(shù)量大概有一百多個(gè),規(guī)模并不是很大。但是得益于容器技術(shù),雪球部署的效率非常高,雪球的開(kāi)發(fā)人員只有幾十個(gè),但是每個(gè)月的發(fā)布次數(shù)高達(dá)兩千多次。
2018 年 5 月 18-19 日,由 51CTO 主辦的全球軟件與運(yùn)維技術(shù)峰會(huì)在北京召開(kāi)。在“開(kāi)源與容器技術(shù)”分會(huì)場(chǎng),雪球 SRE 工程師董明鑫帶來(lái)了《容器技術(shù)在雪球的實(shí)踐》的主題分享。
本文主要分為如下三個(gè)方面跟大家分享雪球在業(yè)務(wù)中引入和使用容器技術(shù)的心路歷程:
- 為什么要引入 Docker
- Docker 在雪球的技術(shù)實(shí)踐
- 后續(xù)演進(jìn)
雪球是一個(gè)投資者交流的社區(qū),用戶可以在上面買賣股票,代銷基金等各種金融衍生業(yè)務(wù),同時(shí)也可以通過(guò)雪盈證券來(lái)進(jìn)行滬、深、港、美股的交易。
為什么要引入 Docker
隨著業(yè)務(wù)的發(fā)展,不同的社區(qū)業(yè)務(wù)之間所受到影響的概率正在逐漸升高,因此我們希望各個(gè)業(yè)務(wù)之間既能夠不被打擾,又能在資源上、機(jī)器間、甚至網(wǎng)絡(luò)上根據(jù)監(jiān)管的要求予以不同層面的隔離。
早在 2014 年時(shí),我們就發(fā)現(xiàn)容器技術(shù)具有本身鏡像小、靈活、啟動(dòng)速度快等特點(diǎn),而且在性能上比較適合于我們當(dāng)時(shí)物理機(jī)不多的規(guī)模環(huán)境。
相比而言,傳統(tǒng)的虛擬化技術(shù)不但實(shí)現(xiàn)成本高,而且性能損耗也超過(guò) 10%。因此,基于對(duì)鏡像大小、啟動(dòng)速度、性能損耗、和隔離需求的綜合考慮,我們選用了兩種容器引擎:LXC 和 Docker。
我們把 MySQL 之類有狀態(tài)的服務(wù)放在 LXC 里;而將線上業(yè)務(wù)之類無(wú)狀態(tài)的服務(wù)放在 Docker 中。
容器使用方法
眾所周知,Docker 是以類似于單機(jī)的軟件形態(tài)問(wèn)世的,最初它的宣傳口號(hào)是:Build/Ship/Run。
因此它早期的 Workflow(流程)是:
- 在一臺(tái) Host 主機(jī)上先運(yùn)行 Docker Build。
- 然后運(yùn)用 Docker Pull,從鏡像倉(cāng)庫(kù)里把鏡像拉下來(lái)。
- ***使用 Docker Run,就有了一個(gè)運(yùn)行的 Container。
需要解決的問(wèn)題
上述的流程方案伴隨著如下有待解決的問(wèn)題:
- 網(wǎng)絡(luò)連通性,由于是單機(jī)軟件,Docker 最初默認(rèn)使用的是 Bridge 模式,不同宿主機(jī)之間的網(wǎng)絡(luò)并不相通。因此,早期大家交流最多的就是如何解決網(wǎng)絡(luò)連通性的問(wèn)題。
- 多節(jié)點(diǎn)的服務(wù)部署與更新,在上馬該容器方案之后,我們發(fā)現(xiàn)由于本身性能損耗比較小,其節(jié)點(diǎn)的數(shù)量會(huì)出現(xiàn)爆炸式增長(zhǎng)。
因此,往往會(huì)出現(xiàn)一臺(tái)物理機(jī)上能夠運(yùn)行幾十個(gè)節(jié)點(diǎn)的狀況。容器節(jié)點(diǎn)的絕對(duì)數(shù)量會(huì)比物理節(jié)點(diǎn)的數(shù)量大一個(gè)數(shù)量級(jí),甚至更多。那么這么多節(jié)點(diǎn)的服務(wù)部署與更新,直接導(dǎo)致了工作量的倍數(shù)增加。
- 監(jiān)控,同時(shí),我們需要為這么多節(jié)點(diǎn)的運(yùn)行狀態(tài)采用合適的監(jiān)控方案。
Docker 在雪球的技術(shù)實(shí)踐
網(wǎng)絡(luò)模式
首先給大家介紹一下我們?cè)缙诘木W(wǎng)絡(luò)解決方案:在上圖的左邊,我們默認(rèn)采用的是 Docker 的 Bridge 模式。
大家知道,默認(rèn)情況下 Docker 會(huì)在物理機(jī)上創(chuàng)建一個(gè)名為 docker0 的網(wǎng)橋。
每當(dāng)一個(gè)新的 Container 被創(chuàng)建時(shí),它就會(huì)相應(yīng)地創(chuàng)建出一個(gè) veth,然后將其連到容器的 eth0 上。
同時(shí),每一個(gè) veth 都會(huì)被分配到一個(gè)子網(wǎng)的 IP 地址,以保持與相同主機(jī)里各個(gè)容器的互通。
由于在生產(chǎn)環(huán)境中不只一張網(wǎng)卡,因此我們對(duì)它進(jìn)行了改造。我們產(chǎn)生了一個(gè)“網(wǎng)卡綁定”,即生成了 bond0 網(wǎng)卡。我們通過(guò)創(chuàng)建一個(gè) br0 網(wǎng)橋,來(lái)替換原來(lái)的 docker0 網(wǎng)橋。
在該 br0 網(wǎng)橋中,我們所配置的網(wǎng)段和物理機(jī)所處的網(wǎng)段是相同的。由于容器和物理機(jī)同處一個(gè)網(wǎng)段,因此核心上聯(lián)的交換機(jī)能夠看到該容器和不同宿主機(jī)的 MAC 地址。這就是一個(gè)網(wǎng)絡(luò)二層互通的解決方案。
該網(wǎng)絡(luò)模式具有優(yōu)劣兩面性:
- 優(yōu)點(diǎn):由于在網(wǎng)絡(luò)二層上實(shí)現(xiàn)了連接互通,而且僅用到了內(nèi)核轉(zhuǎn)發(fā),因此整體性能非常好,與物理機(jī)真實(shí)網(wǎng)卡的效率差距不大。
- 缺點(diǎn):管理較為復(fù)雜,需要我們自己手動(dòng)的去管理容器的 IP 和 MAC 地址。
由于整體處于網(wǎng)絡(luò)大二層,一旦系統(tǒng)達(dá)到了一定規(guī)模,網(wǎng)絡(luò)中的 ARP 包會(huì)產(chǎn)生網(wǎng)絡(luò)廣播風(fēng)暴,甚至?xí)及l(fā)出現(xiàn) PPS(Package Per Second)過(guò)高,網(wǎng)絡(luò)間歇性不通等奇怪的現(xiàn)象。
由于處于底層網(wǎng)絡(luò)連接,在實(shí)現(xiàn)網(wǎng)絡(luò)隔離時(shí)也較為復(fù)雜。
服務(wù)部署
對(duì)于服務(wù)的部署而言,我們最初沿用虛擬機(jī)的做法,將容器啟動(dòng)起來(lái)后就不再停下了,因此:
- 如果節(jié)點(diǎn)需要新增,我們就通過(guò) Salt 來(lái)管理機(jī)器的配置。
- 如果節(jié)點(diǎn)需要更新,我們就通過(guò) Capistrano 進(jìn)行服務(wù)的分發(fā),和多個(gè)節(jié)點(diǎn)的部署操作,變更容器中的業(yè)務(wù)程序。
其中,優(yōu)勢(shì)為:
- 與原來(lái)的基礎(chǔ)設(shè)施相比,遷移的成本非常低。由于我們通過(guò)復(fù)用原來(lái)的基礎(chǔ)設(shè)施,直接將各種服務(wù)部署在原先的物理機(jī)上進(jìn)行,因此我們很容易地遷移到了容器之中。
而對(duì)于開(kāi)發(fā)人員來(lái)說(shuō),他們看不到容器這一層,也就如同在使用原來(lái)的物理機(jī)一樣,毫無(wú)“違和感”。
- 與虛擬機(jī)相比,啟動(dòng)比較快,運(yùn)行時(shí)沒(méi)有虛擬化的損耗。
- 最重要的是一定程度上滿足了我們對(duì)于隔離的需求。
而劣勢(shì)則有:
- 遷移和擴(kuò)容非常繁瑣。例如:當(dāng)某個(gè)服務(wù)需要擴(kuò)容時(shí),我們就需要有人登錄到該物理機(jī)上,生成并啟動(dòng)一個(gè)空的容器,再把服務(wù)部署進(jìn)去。此舉較為低效。
- 缺乏統(tǒng)一的平臺(tái)進(jìn)行各種歷史版本的管理與維護(hù)。我們需要通過(guò)文檔來(lái)記錄整個(gè)機(jī)房的容器數(shù)量,和各個(gè)容器的 IP/MAC 地址,因此出錯(cuò)的可能性極高。
- 缺少流程和權(quán)限的控制。我們基本上采用的是原始的管控方式。
自研容器管理平臺(tái)
面對(duì)上述缺點(diǎn),我們需要自行研發(fā)一個(gè)容器管理平臺(tái),去管理各種物理機(jī)、容器、IP 與 MAC 地址、以及進(jìn)行流程控制。
因此我們變更的整套發(fā)布流程為:
- 由開(kāi)發(fā)人員將代碼提交到代碼倉(cāng)庫(kù)(如 Github)之中。
- 觸發(fā)一個(gè) Hook 去構(gòu)建鏡像,在構(gòu)建的同時(shí)做一些 CI(持續(xù)集成),包括靜態(tài)代碼掃描和單測(cè)等。
- 將報(bào)告附加到鏡像的信息里,并存入鏡像倉(cāng)庫(kù)中。
- 部署測(cè)試環(huán)境。
- 小流量上線,上線之后,做一些自動(dòng)化的 API Diff 測(cè)試,以判斷是否可用。
- 繼續(xù)全量上線。
鏡像構(gòu)建
有了容器管理平臺(tái),就會(huì)涉及到鏡像的自動(dòng)構(gòu)建。和業(yè)界其他公司的做法類似,我們也使用的是基于通用操作系統(tǒng)的鏡像。
然后向鏡像中添加那些我們公司內(nèi)部會(huì)特別用到的包,得到一個(gè)通用的 base 鏡像,再通過(guò)分別加入不同語(yǔ)言的依賴,得到不同的鏡像。
每次業(yè)務(wù)版本發(fā)布,將代碼放入相應(yīng)語(yǔ)言的鏡像即可得到一個(gè)業(yè)務(wù)的鏡像。構(gòu)建鏡像的時(shí)候需要注意盡量避免無(wú)用的層級(jí)和內(nèi)容,這有助于提升存儲(chǔ)和傳輸效率。
系統(tǒng)依賴
我們的這一整套解決方案涉及到了如下周邊的開(kāi)源項(xiàng)目與技術(shù):
負(fù)載均衡
由于會(huì)頻繁發(fā)生節(jié)點(diǎn)的增減,我們?cè)撊绾瓮ㄟ^(guò)流量的調(diào)度和服務(wù)的發(fā)現(xiàn),來(lái)實(shí)現(xiàn)自動(dòng)加入負(fù)載均衡呢?對(duì)于那些非 Http 協(xié)議的 RPC,又該如何自動(dòng)安全地摘掉某個(gè)節(jié)點(diǎn)呢?
我們?cè)诖耸褂昧?Nginx+Lua(即 OpenResty),去實(shí)現(xiàn)邏輯并動(dòng)態(tài)更改 Upstream。
當(dāng)有節(jié)點(diǎn)啟動(dòng)時(shí),我們就能夠?qū)⑺詣?dòng)注冊(cè)與加入;而當(dāng)有節(jié)點(diǎn)被銷毀時(shí),也能及時(shí)將其摘掉。
同時(shí),我們?cè)趦?nèi)部使用了 Finagle 作為 RPC 的框架,并通過(guò) ZooKeeper 實(shí)現(xiàn)了服務(wù)的發(fā)現(xiàn)。
日志收集
由于節(jié)點(diǎn)眾多,我們需要進(jìn)行各種日志的收集。在此,我們大致分為兩類收集方式:
- 一類是 Nginx 這種不易侵入代碼的,我們并沒(méi)有設(shè)法去改變?nèi)罩镜牧飨?,而是讓它直?ldquo;打”到物理機(jī)的硬盤上,然后使用 Flume 進(jìn)行收集,傳輸?shù)?nbsp; Kafka 中。
- 另一類是我們自己的業(yè)務(wù)。我們實(shí)現(xiàn)了一個(gè) Log4 Appender,把日志直接寫到 Kafka,再?gòu)?Kafka 轉(zhuǎn)寫到 ElasticSearch 里面。
網(wǎng)絡(luò)模式
在該場(chǎng)景下,我們采用的是上述提到的改進(jìn)后的 Bridge+Host 模式。
監(jiān)控系統(tǒng)
監(jiān)控系統(tǒng)由上圖所示的幾個(gè)組件所構(gòu)成。它將收集(Collector)到的不同監(jiān)控指標(biāo)數(shù)據(jù),傳輸?shù)?Graphite 上,而 Grafana 可讀取 Graphite 的信息,并用圖形予以展示。
同時(shí),我們也根據(jù)內(nèi)部業(yè)務(wù)的適配需要,對(duì)報(bào)警組件 Cabot 進(jìn)行了改造和定制。
此時(shí)我們的平臺(tái)已經(jīng)與虛擬機(jī)的用法有了較大的區(qū)別。如上圖所示,主要的不同之處體現(xiàn)在編譯、環(huán)境、分發(fā)、節(jié)點(diǎn)變更,流程控制、以及權(quán)限控制之上。我們的用法更具自動(dòng)化。
由于是自行研發(fā)的容器管理平臺(tái),這給我們帶來(lái)的直接好處包括:
- 流程與權(quán)限的控制。
- 代碼版本與環(huán)境的固化,多個(gè)版本的發(fā)布,鏡像的管理。
- 部署與擴(kuò)容效率的大幅提升。
但是其自身也有著一定的缺點(diǎn),包括:
- 在流程控制邏輯,機(jī)器與網(wǎng)絡(luò)管理,以及本身的耦合程度上都存在著缺陷。因此它并不算是一個(gè)非常好的架構(gòu),也沒(méi)能真正實(shí)現(xiàn)“高內(nèi)聚低耦合”。
- 由于是自研的產(chǎn)品,其功能上并不完善,沒(méi)能實(shí)現(xiàn)自愈,無(wú)法根據(jù)新增節(jié)點(diǎn)去自動(dòng)選擇物理機(jī)、并自動(dòng)分配與管理 IP 地址。
引入 Swarm
2015 年,我們開(kāi)始著手改造該容器管理平臺(tái)。由于該平臺(tái)之前都是基于 DockerAPI 構(gòu)建的。
而 Swarm 恰好能對(duì) Docker 的原生 API 提供非常好的支持,因此我們覺(jué)得如果引入 Swarm 的話,對(duì)于以前代碼的改造成本將會(huì)降到***。
那么我們?cè)撊绾螌?duì)原先的網(wǎng)絡(luò)二層方案進(jìn)行改造呢?如前所述,我們一直實(shí)現(xiàn)的是讓容器的 IP 地址與物理機(jī)的 IP 地址相對(duì)等。
因此并不存在網(wǎng)絡(luò)不通的情況。同時(shí),我們的 Redis 是直接部署在物理機(jī)上的。
所以依據(jù)上圖中各個(gè)列表的對(duì)比,我們覺(jué)得 Calico 方案更適合我們的業(yè)務(wù)場(chǎng)景。
因此,我們?cè)谏蠈邮褂?Rolling 來(lái)進(jìn)行各種流程的操作,中下層則用 Swarm+Calico 來(lái)予以容器和網(wǎng)絡(luò)的管理。
Calico 使用的是 DownwardDefault 模式,該模式通過(guò)運(yùn)用 BGP 協(xié)議,來(lái)實(shí)現(xiàn)對(duì)于不同機(jī)器之間路由信息的分發(fā)。
在默認(rèn)情況下,Calico 是 Node 與 Node 之間的 Mesh 方式,即:任意兩個(gè) Node 之間都有著 BGP 連接。
當(dāng)我們?cè)谝慌_(tái)物理機(jī)上啟動(dòng)了某個(gè)容器之后,它就會(huì)添加一條包含著從容器 IP 地址到物理機(jī)的路由信息。
由于多臺(tái)物理機(jī)同處一個(gè) Mesh,那么每一臺(tái)機(jī)器都會(huì)學(xué)習(xí)到該路由信息。而隨著我們系統(tǒng)規(guī)模的逐漸增大,每一臺(tái)物理機(jī)上的路由表也會(huì)相應(yīng)地增多,這就會(huì)影響到網(wǎng)絡(luò)的整體性能。
因此我們需要采用這種 Downward Default 部署模式,使得不必讓每臺(tái)物理機(jī)都擁有全量的路由表,而僅讓交換機(jī)持有便可。
眾所周知,BGP 會(huì)給每一臺(tái)物理機(jī)分配一個(gè) AS(自治域是 BGP 中的一個(gè)概念)號(hào),那么我們就可以給各臺(tái)物理機(jī)都分配相同的 AS 號(hào)。
而給它們的上聯(lián)交換機(jī)分配另一個(gè) AS 號(hào),同時(shí)也給核心交換機(jī)再分配第三種 AS 號(hào)。
通過(guò)此法,每一臺(tái)物理機(jī)只會(huì)和自己上聯(lián)的交換機(jī)做路由分發(fā),那么當(dāng)有一個(gè)新的節(jié)點(diǎn)啟動(dòng)之后,我們便可以將這條路由信息插入到該節(jié)點(diǎn)自己的路由表中,然后再告知與其相連的上聯(lián)交換機(jī)。
上聯(lián)交換機(jī)在學(xué)習(xí)到了這條路由之后,再進(jìn)一步推給核心交換機(jī)。
總結(jié)起來(lái),該模式的特點(diǎn)是:
- 單個(gè)節(jié)點(diǎn)不必知道其他物理機(jī)的相關(guān)信息,它只需將數(shù)據(jù)包發(fā)往網(wǎng)關(guān)便可。因此單臺(tái)物理機(jī)上的路由表也會(huì)大幅減少,其數(shù)量可保持在“單機(jī)上的容器數(shù)量 +一個(gè)常數(shù)(自行配置的路由)”的水平上。
- 每個(gè)上聯(lián)交換機(jī)只需掌握自己機(jī)架上所有物理機(jī)的路由表信息。
- 核心交換機(jī)則需要持有所有的路由表。而這恰是其自身性能與功能的體現(xiàn)。
當(dāng)然,該模式也帶來(lái)了一些不便之處,即:對(duì)于每一個(gè)數(shù)據(jù)流量而言,即使目標(biāo) IP 在整個(gè)網(wǎng)絡(luò)中并不存在,它們也需要一步一步地向上查詢直到核心交換機(jī)處,***再判斷是否真的需要丟棄該數(shù)據(jù)包。
后續(xù)演進(jìn)
在此之后,我們也將 DevOps 的思想和模式逐步引入了當(dāng)前的平臺(tái)。具體包括如下三個(gè)方面:
- 通過(guò)更加自助化的流程,來(lái)解放運(yùn)維。讓開(kāi)發(fā)人員自助式地創(chuàng)建、添加、監(jiān)控他們自己的項(xiàng)目,我們只需了解各個(gè)項(xiàng)目在平臺(tái)中所占用的資源情況便可,從而能夠使得自己的精力更加專注于平臺(tái)的開(kāi)發(fā)與完善。
- 如今,由于 Kubernetes 基本上已成為了業(yè)界的標(biāo)準(zhǔn),因此我們逐步替換了之前所用到的 Swarm,并通過(guò) Kubernetes 來(lái)實(shí)現(xiàn)更好的調(diào)度方案。
- 支持多機(jī)房和多云環(huán)境,以達(dá)到更高的容災(zāi)等級(jí),滿足業(yè)務(wù)的發(fā)展需求,并完善集群的管理。
上圖展示了一種嵌套式的關(guān)系:在我們的每一個(gè) Project 中,都可以有多個(gè) IDC。
而每個(gè) IDC 里又有著不同的 Kubernetes 集群。同時(shí)在每一個(gè)集群里,我們?yōu)槊恳粋€(gè)項(xiàng)目都分配了一個(gè) Namespace。
根據(jù)不同的環(huán)境,這些項(xiàng)目的 Namespace 會(huì)擁有不同的 Deployment。例如想要做到部署與發(fā)布的分離,我們就相應(yīng)地做了多個(gè) Deployment,不同的 Deployment 標(biāo)示不同的環(huán)境。
默認(rèn)將流量引入***個(gè) Deployment,等到第二個(gè) Deployment 被部署好以后,需要發(fā)布的時(shí)候,我們?cè)僦苯影蚜髁?ldquo;切”過(guò)去。
同時(shí),鑒于我們的平臺(tái)上原來(lái)就已經(jīng)具有了諸如日志、負(fù)載均衡、監(jiān)控之類的解決方案。
而 Kubernetes 本身又是一個(gè)較為全面的解決方案,因此我們以降低成本為原則,謹(jǐn)慎地向 Kubernetes 進(jìn)行過(guò)渡,盡量保持平臺(tái)的兼容性,不至讓開(kāi)發(fā)人員產(chǎn)生“違和感”。
如今,我們的容器只有一千多個(gè),項(xiàng)目數(shù)量大概有一百多個(gè)。但是我們?cè)诓渴鹦史矫娴奶嵘€是非常顯著的,我們的幾十個(gè)開(kāi)發(fā)人員每個(gè)月所發(fā)布的次數(shù)就能達(dá)到兩千多次,每個(gè)交易日的日志量大概有 1.5T。
董明鑫,雪球網(wǎng)運(yùn)維開(kāi)發(fā)架構(gòu)師,曾就職百度,2014 年加入雪球,目前主要負(fù)責(zé)保障雪球穩(wěn)定性、提升資源利用率及提高開(kāi)發(fā)效率等方面。關(guān)注容器生態(tài)圈的技術(shù)發(fā)展。
【51CTO原創(chuàng)稿件,合作站點(diǎn)轉(zhuǎn)載請(qǐng)注明原文作者和出處為51CTO.com】