把數據庫放入Docker是一個好主意嗎?

對於無狀態的應用服務而言,容器是一個相當完美的開發運維解決方案。然而對於帶持久狀態的服務 —— 數據庫來說,事情就沒有那麼簡單了。生產環境的數據庫是否應當放入容器中,仍然是一個充滿爭議的問題。

站在開發者的角度上,我非常喜歡Docker,並始終相信Docker是未來軟件開發部署運維的標準方式,而Kubernetes則是事實上的下一代“操作系統”。但站在DBA的立場上,我認爲就目前而言,將生產環境數據庫放入Docker中仍然是一個餿主意。

 

一、 Docker解決什麼問題?

讓我們先來看一看Docker對自己的描述。

Docker用於形容自己的詞彙包括:輕量,標準化,可移植,節約成本,提高效率,自動,集成,高效運維。這些說法並沒有問題,Docker在整體意義上確實讓開發和運維都變得更容易了。因而可以看到很多公司都熱切地希望將自己的軟件與服務容器化。但有時候這種熱情會走向另一個極端:將一切軟件服務都容器化,甚至是生產環境的數據庫

容器最初是針對無狀態的應用而設計的,在邏輯上,容器內應用產生的臨時數據也屬於該容器的一部分。用容器創建起一個服務,用完之後銷燬它。這些應用本身沒有狀態,狀態通常保存在容器外部的數據庫裏,這是經典的架構與用法,也是容器的設計哲學。但當用戶想把數據庫本身也放到容器中時,事情就變得不一樣了:數據庫是有狀態的,爲了維持這個狀態不隨容器停止而銷燬,數據庫容器需要在容器上打一個洞,與底層操作系統上的數據卷相聯通。這樣的容器,不再是一個能夠隨意創建,銷燬,搬運,轉移的對象,而是與底層環境相綁定的對象。因此,應用使用容器的諸多優勢,對於數據庫容器來說都不復存在。

 

 

二、 可靠性

讓軟件跑起來,和讓軟件可靠地運行是兩回事。數據庫是信息系統的核心,在絕大多數場景下屬於關鍵(Critical)應用,就是出了問題會要命的應用。這與我們的日常經驗相符:Word/Excel/PPT這些辦公軟件如果崩了強制重啓即可,沒什麼大不了的;但正在編輯的文檔如果丟了、髒了、亂了,那纔是真的災難。數據庫亦然,對於不少公司,特別是互聯網公司來說,如果數據庫被刪了又沒有可用備份,基本上可以宣告關門大吉了。

可靠性(Reliability)是數據庫最重要的屬性。可靠性是系統在困境(硬件故障、軟件故障、人爲錯誤)中仍可正常工作(正確完成功能,並能達到期望的性能水準)的能力。可靠性意味着容錯(fault-tolerant)與韌性(resilient),它是一種安全屬性,並不像性能與可維護性那樣的活性屬性直觀可衡量。它只能通過長時間的正常運行來證明,或者某一次故障來否證。安全生產重於泰山,數據庫被刪,被攪亂,被脫庫後再捶胸頓足是沒有意義的。

回頭再看一看Docker對自己的特性描述中,並沒有包含“可靠”這個對於數據庫至關重要的屬性。

在裸機上部署數據庫可謂自古以來的實踐,通過幾十年的持續工作,它很好的證明了自己的可靠性。Docker雖爲DevOps帶來一場革命,但僅僅五年的歷史對於可靠性證明而言仍然是圖樣圖森破。對關乎身家性命的生產數據庫而言還遠遠不夠:因爲還沒有足夠的小白鼠去趟雷

想要提高可靠性,最重要的就是從故障中吸取經驗。故障是寶貴的經驗財富:它將未知問題變爲已知問題,是運維知識的表現形式。社區的故障經驗絕大多都基於裸機部署的假設,各式各樣的故障在幾十年裏都已經被人們踩了個遍。如果你遇到一些問題,大概率是別人已經踩過的坑,可以比較方便地處理與解決。同樣的故障如果加上一個“Docker”關鍵字,能找到的有用信息就要少的多。這也意味着當疑難雜症出現時,成功搶救恢復數據的概率要更低,處理緊急故障所需的時間會更長。

微妙的現實是,如果沒有特殊理由,企業與個人通常並不願意分享故障方面的經驗。故障有損企業的聲譽:可能暴露一些敏感信息,或者是企業與團隊的垃圾程度。另一方面,故障經驗幾乎都是真金白銀的損失與學費換來的,是運維人員的核心價值所在,因此有關故障方面的公開資料並不多。

 

二、 額外失效點

開發關心Feature,而運維關注Bug。相比裸機部署而言,將數據庫放入Docker中並不能降低硬件故障,軟件錯誤,人爲失誤的發生概率。用裸機會有的硬件故障,用Docker一個也不會少。軟件缺陷主要是應用Bug,也不會因爲採用容器與否而降低,人爲失誤同理。相反,引入Docker會因爲引入了額外的組件,額外的複雜度,額外的失效點,導致系統整體可靠性下降

舉個最簡單的例子,dockerd守護進程崩了怎麼辦,數據庫進程就直接歇菜了。儘管這種事情發生的概率並不高,但它們在裸機上壓根不會發生

此外,額外組件引入的失效點可能並不止一個:Docker產生的問題並不僅僅是Docker本身的問題。當故障發生時,可能是單純Docker的問題,或者是Docker與數據庫相互作用產生的問題,還可能是Docker與操作系統,編排系統,虛擬機,網絡,磁盤相互作用產生的問題。可以參見官方PostgreSQL Docker鏡像的Issue列表:https://github.com/docker-library/postgres/issues?q=。

此外,彼之蜜糖,吾之砒霜。某些Docker的Feature,在特定的環境下也可能會變爲Bug。

 

三、 隔離性

Docker提供了進程級別的隔離性,通常來說隔離性對應用來說是個好屬性。應用看不見別的進程,自然也不會有很多相互作用導致的問題,進而提高了系統的可靠性。但隔離性對於數據庫而言不一定完全是好事。

一個真實案例在同一個數據目錄上啓動兩個PostgreSQL實例,或者在宿主機和容器內同時啓動了兩個數據庫實例。在裸機上第二次啓動嘗試會失敗,因爲PostgreSQL能意識到另一個實例的存在而拒絕啓動;但Docker因其隔離性第二個實例無法意識到宿主機或其他數據庫容器中的另一個實例。如果沒有配置合理的Fencing機制(例如通過宿主機端口互斥,pid文件互斥),兩個運行在同一數據目錄上的數據庫進程能把數據文件攪成一團漿糊。

數據庫需不需要隔離性?當然需要, 但不是這種隔離性。數據庫的性能很重要,因此往往是獨佔物理機部署。除了數據庫進程和必要的工具,不會有其他應用。即使放在容器中,也往往採用獨佔綁定物理機的模式運行。因此Docker提供的隔離性對於這種數據庫部署方案而言並沒有什麼意義;不過對雲數據庫廠商來說,這倒真是一個實用的Feature,用來搞多租戶超賣妙用無窮。

 

四、 工具

數據庫需要工具來維護,包括各式各樣的運維腳本,部署,備份,歸檔,故障切換,大小版本升級,插件安裝,連接池,性能分析,監控,調優,巡檢,修復。這些工具,也大多針對裸機部署而設計。這些工具與數據庫一樣,都需要精心而充分的測試。讓一個東西跑起來,與確信這個東西能持久穩定正確的運行,是完全不同的可靠性水準。

一個簡單的例子是插件,PostgreSQL提供了很多實用的插件,譬如PostGIS。假如想爲數據庫安裝該插件,在裸機上只要yum install然後create extension postgis兩條命令就可以。但如果是在Docker裏,按照Docker的實踐原則,用戶需要在鏡像層次進行這個變更,否則下次容器重啓時這個擴展就沒了。因而需要修改Dockerfile,重新構建新鏡像並推送到服務器上,最後重啓數據庫容器,毫無疑問,要麻煩的多。

再比如說監控,在傳統的裸機部署模式下,機器的各項指標是數據庫指標的重要組成部分。容器中的監控與裸機上的監控有很多微妙的區別。不注意可能會掉到坑裏。例如,CPU各種模式的時長之和,在裸機上始終會是100%,但這樣的假設在容器中就不一定總是成立了。再比方說依賴/proc文件系統的監控程序可能在容器中獲得與裸機上涵義完全不同的指標。雖然這類問題最終都是可解的(例如把Proc文件系統掛載到容器內),但相比簡潔明瞭的方案,沒人喜歡複雜醜陋的workaround。

類似的問題包括一些故障檢測工具與系統常用命令,雖然理論上可以直接在宿主機上執行,但誰能保證容器裏的結果和裸機上的結果有着相同的涵義?更爲棘手的是緊急故障處理時,一些需要臨時安裝使用的工具在容器裏沒有,外網不通,如果再走Dockerfile→Image→重啓這種路徑毫無疑問會讓人抓狂。

把Docker當成虛擬機來用的話,很多工具大抵上還是可以正常工作的,不過這樣就喪失了使用的Docker的大部分意義,不過是把它當成了另一個包管理器用而已。有人覺得Docker通過標準化的部署方式增加了系統的可靠性,因爲環境更爲標準化更爲可控。這一點不能否認。私以爲,標準化的部署方式雖然很不錯,但如果運維管理數據庫的人本身瞭解如何配置數據庫環境,將環境初始化命令寫在Shell腳本里和寫在Dockerfile裏並沒有本質上的區別。

 

五、 可維護性

軟件的大部分開銷並不在最初的開發階段,而是在持續的維護階段,包括修復漏洞、保持系統正常運行、處理故障、版本升級,償還技術債、添加新的功能等等。可維護性對於運維人員的工作生活質量非常重要。應該說可維護性是Docker最討喜的地方:Infrastructure as code。可以認爲Docker的最大價值就在於它能夠把軟件的運維經驗沉澱成可複用的代碼,以一種簡便的方式積累起來,而不再是散落在各個角落的install/setup文檔。在這一點上Docker做的相當出色,尤其是對於邏輯經常變化的無狀態應用而言。Docker和K8s能讓用戶輕鬆部署,完成擴容,縮容,發佈,滾動升級等工作,讓Dev也能幹Ops的活,讓Ops也能幹DBA的活。

 

六、 環境配置

如果說Docker最大的優點是什麼,那也許就是環境配置的標準化了。標準化的環境有助於交付變更,交流問題,復現Bug。使用二進制鏡像(本質是物化了的Dockerfile安裝腳本)相比執行安裝腳本而言更爲快捷,管理更方便。一些編譯複雜,依賴如山的擴展也不用每次都重新構建了,這些都是很爽的特性。

不幸的是,數據庫並不像通常的業務應用一樣來來去去更新頻繁,創建新實例或者交付環境本身是一個極低頻的操作。同時DBA們通常都會積累下各種安裝配置維護腳本,一鍵配置環境也並不會比Docker慢多少。因此在環境配置上Docker的優勢就沒有那麼顯著了,只能說是Nice to have。當然,在沒有專職DBA時,使用Docker鏡像可能還是要比自己瞎折騰要好一些,因爲起碼鏡像中多少沉澱了一些運維經驗。

通常來說,數據庫初始化之後連續運行幾個月幾年也並不稀奇。佔據數據庫管理工作主要內容的並不是創建新實例與交付環境,主要還是日常運維的部分。不幸的是在這一點上Docker並沒有什麼優勢,反而會產生一些麻煩。

 

七、 日常運維

Docker確實能極大地簡化來無狀態應用的日常維護工作,諸如創建銷燬,版本升級,擴容等,但能延伸到數據庫上嗎?

數據庫容器不可能像應用容器一樣隨意銷燬創建,重啓遷移。因而Docker並不能對數據庫的日常運維的體驗有什麼提升,真正有幫助的倒是諸如ansible之類的工具。而對於日常運維而言,很多操作都需要通過docker exec的方式將腳本透傳至容器內執行。底下跑的還是一樣的腳本,只不過用docker-exec來執行又額外多了一層包裝,這就有點脫褲子放屁的意味了。

此外,很多命令行工具在和Docker配合使用時都相當尷尬。譬如docker exec會將stderrstdout混在一起,讓很多依賴管道的命令無法正常工作。以PostgreSQL爲例,在裸機部署模式下,某些日常ETL任務可以用一行bash輕鬆搞定:

psql <src-url> -c 'COPY tbl TO STDOUT' |\
psql <dst-url> -c 'COPY tdb FROM STDIN'

但如果宿主機上沒有合適的客戶端二進制程序,那就只能這樣用Docker容器中的二進制:

docker exec -it srcpg gosu postgres bash -c "psql -c \"COPY tbl TO STDOUT\" 2>/dev/null" |\ docker exec -i dstpg gosu postgres psql -c 'COPY tbl FROM STDIN;'

當用戶想爲容器裏的數據庫做一個物理備份時,原本很簡單的一條命令現在需要很多額外的包裝:dockergosubashpg_basebackup

docker exec -i postgres_pg_1 gosu postgres bash -c 'pg_basebackup -Xf -Ft -c fast -D - 2>/dev/null' | tar -xC /tmp/backup/basebackup

如果說客戶端應用psql|pg_basebackup|pg_dump還可以通過在宿主機上安裝對應版本的客戶端工具來繞開這個問題,那麼服務端的應用就真的無解了。總不能在不斷升級容器內數據庫軟件版本時每次一併把宿主機上的服務器端二進制版本升級了吧?

另一個Docker喜歡講的例子是軟件版本升級:例如用Docker升級數據庫小版本,只要簡單地修改Dockerfile裏的版本號,重新構建鏡像然後重啓數據庫容器就可以了。沒錯,至少對於無狀態的應用來說這是成立的。但當需要進行數據庫原地大版本升級時問題就來了,用戶還需要同時修改數據庫狀態。在裸機上一行bash命令就可以解決的問題,在Docker下可能就會變成這樣的東西:https://github.com/tianon/docker-postgres-upgrade

如果數據庫容器不能像AppServer一樣隨意地調度,快速地擴展,也無法在初始配置,日常運維,以及緊急故障處理時相比普通腳本的方式帶來更多便利性,我們又爲什麼要把生產環境的數據庫塞進容器裏呢?

Docker和K8s一個很討喜的地方是很容易進行擴容,至少對於無狀態的應用而言是這樣:一鍵拉起起幾個新容器,隨意調度到哪個節點都無所謂。但數據庫不一樣,作爲一個有狀態的應用,數據庫並不能像普通AppServer一樣隨意創建,銷燬,水平擴展。譬如,用戶創建一個新從庫,即使使用容器,也得從主庫上重新拉取基礎備份。生產環境中動輒幾TB的數據庫,用萬兆網卡也需要個把鐘頭才能完成,也很可能還是需要人工介入與檢查。相比之下,在同樣的操作系統初始環境下,運行現成的拉從庫腳本與跑docker run在本質上又能有什麼區別?畢竟時間都花在拖從庫上了。

使用Docker放生產數據庫的一個尷尬之處就在於,數據庫是有狀態的,而且爲了建立這個狀態需要額外的工序。通常來說設置一個新PostgreSQL從庫的流程是,先通過pg_baseback建立本地的數據副本,然後再在本地數據目錄上啓動postmaster進程。然而容器是和進程綁定的,一旦進程退出容器也隨之停止。因此爲了在Docker中擴容一個新從庫:要麼需要先後啓動pg_baseback容器拉取數據,再在同一個數據捲上啓動postgres兩個容器;要麼需要在創建容器的過程中就指定好複製目標並等待幾個小時的複製完成;要麼在postgres容器中再使用pg_basebackup偷天換日替換數據目錄。無論哪一種方案都是既不優雅也不簡潔。因爲容器的這種進程隔離抽象,對於數據庫這種充滿狀態的多進程,多任務,多實例協作的應用存在抽象泄漏,它很難優雅地覆蓋這些場景。當然有很多折衷的辦法可以打補丁來解決這類問題,然而其代價就是大量非本徵複雜度,最終受傷的還是系統的可維護性。

總的來說,不可否認Docker對於提高系統整體的可維護性是有幫助的,只不過針對數據庫來說這種優勢並不顯著:容器化的數據庫能簡化並加速創建新實例或擴容的速度,但也會在日常運維中引入一些麻煩和問題。不過,我相信隨着Docker與K8s的進步,這些問題最終都是可以解決克服的。

 

八、 性能

性能也是人們經常關注的一個維度。從性能的角度來看,數據庫的基本部署原則當然是離硬件越近越好,額外的隔離與抽象不利於數據庫的性能:越多的隔離意味着越多的開銷,即使只是內核棧中的額外拷貝。對於追求性能的場景,一些數據庫選擇繞開操作系統的頁面管理機制直接操作磁盤,而一些數據庫甚至會使用FPGA甚至GPU加速查詢處理。

實事求是地講,Docker作爲一種輕量化的容器,性能上的折損並不大,這也是Docker相比虛擬機的優勢所在。但毫無疑問的是,將數據庫放入Docker只會讓性能變得更差而不是更好。

 

九、 總結

容器技術與編排技術對於運維而言是非常有價值的東西,它實際上彌補了從軟件到服務之間的空白,其願景是將運維的經驗與能力代碼化模塊化。容器技術將成爲未來的包管理方式,而編排技術將進一步發展爲“數據中心分佈式集羣操作系統”,成爲一切軟件的底層基礎設施Runtime。當越來越多的坑被踩完後,人們可以放心大膽的把一切應用,有狀態的還是無狀態的都放到容器中去運行。但現在,起碼對於數據庫而言,還只是一個美好的願景。

最後需要再次強調的是,以上討論僅限於生產環境數據庫。換句話說,對於開發環境而言,我其實是很支持將數據庫放入Docker中的,畢竟不是所有的開發人員都知道怎麼配置本地測試數據庫環境,使用Docker交付環境顯然要比一堆手冊簡單明瞭的多。對於生產環境的無狀態應用,甚至一些帶有衍生狀態的不甚重要衍生數據系統(譬如Redis緩存),Docker也是一個不錯的選擇。但對於生產環境的核心關係型數據庫而言,如果裏面的數據真的很重要,使用Docker前還望三思:我願意當小白鼠嗎?出了疑難雜症我能Hold住嗎?真搞砸了這鍋我背的動嗎?

任何技術決策都是一個利弊權衡的過程,譬如這裏使用Docker的核心權衡可能就是犧牲可靠性換取可維護性。確實有一些場景,數據可靠性並不是那麼重要,或者說有其他的考量:譬如對於雲計算廠商來說,把數據庫放到容器裏混部超賣就是一件很好的事情:容器的隔離性,高資源利用率,以及管理上的便利性都與該場景十分契合。這種情況下將數據庫放入Docker中也許就是利大於弊的。但對於多數的場景而言,可靠性往往都是優先級最高的的屬性,犧牲可靠性換取可維護性通常並不是一個可取的選擇。更何況實際很難說運維管理數據庫的工作會因爲用了Docker而輕鬆多少:爲了安裝部署一次性的便利而犧牲長久的日常運維可維護性,並不是一個很好的生意。

綜上所述,我認爲就目前對於普通用戶而言,將生產環境的數據庫放入容器中恐怕並不是一個明智的選擇。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章