對傳統應用進行容器化改造

HitTwice發表於2018-08-03

本文由 陳計節 翻譯自 FP Complete 網站上的文章 CONTAINERIZING A LEGACY APPLICATION: AN OVERVIEW,原作者 Emanuel Borsboom。

來源:微信公眾號(諾普部落格)

原文連結: https://mp.weixin.qq.com/s/0yWIuIwarLiml4MD0pDxMg

以下為譯文全文,如需閱讀英文原文,請轉到文末獲取連結:

本文接下來簡要介紹什麼是容器化,要在 Docker 容器中執行傳統應用的緣由,容器化的過程,其間可能遇到的問題,在用容器部署之後的其他步驟等。這將明顯減輕部署工作的壓力,並讓應用朝著零停機部署和橫向縮放的方向前進。

注:本文專注在簡化應用的部署過程,並不包含需要對應用重新設計的內容,比如高可用和橫向擴充套件。


概念



什麼是“傳統”應用?

並沒有一個特定的定義能夠描述所有的傳統應用,但它們有一些共同的特性:

  • 使用本地檔案系統來持久化儲存,資料檔案和應用的檔案混合在一起。

  • 在同一個伺服器上執行很多服務,比如 MySQL 資料庫,Redis 伺服器,nginx web 伺服器,一個 Ruby on Rails 應用,以及一大堆定時任務

  • 使用大雜燴式的指令碼和手工流程進行安裝和升級(文件也很簡陋)。

  • 配置是儲存在檔案裡的,通常散落在多個位置,並與應用的檔案混在一起。

  • 程式間的通訊是藉助本地檔案系統進行的(比如在磁碟上放一個檔案,另一個程式來讀取),而不是TCP/IP。

  • 按照單個伺服器上只執行一個應用的示例的方式來設計的。


傳統應用的缺點

  • 自動化部署很困難。

  • 如果需要執行應用的多個不同的例項,很難讓多個例項在同一個伺服器上“共存”。

  • 如果伺服器停機,由於需要手工流程所以需要較長的時間來恢復。

  • 部署新版本的過程基本是手動的,或者大部分是手動的,難以回滾。

  • 很有可能測試環境與生產環境有較大差異,導致一些生產環境問題不能在測試期間發現。

  • 很難透過增加新的例項來進行橫向擴充套件。

什麼是容器化?

將應用“容器化”的過程,就是讓應用能夠執行在 Docker 容器或類似技術中,它們能將作業系統環境和應用封裝在一起(完整的系統映象)。由於容器能給應用提供近似於完整系統的環境,這就為在不修改,或者少量修改應用的情況下,對應用的部署進行現代化改造提供了一種思路。這也是應用的架構持續能保持“雲友好”的基礎。

容器化的好處

  • 部署容易多了:使用新的容器映象直接替換整個老版本。

  • 自動化部署也相對容易,甚至可以完全由 CI(Continuous Integration, 持續整合)來驅動。

  • 部署失敗時的回滾只要切換到之前的映象。

  • 應用升級非常容易,因為現在沒有可能出錯的“中間步驟”了(不管它是否影響整個部署過程的成功)。

  • 相同的容器映象可以在不同的環境中充分測試,再直接部署到生產環境。這可以確保測試態與生產態的產品是完全一致的。

  • 系統更容易從當機中恢復,因為可以迅速在新硬體資源上啟動裝有這個應用的新容器,並附加到同一資料來源上。

  • 開發人員能在本地以容器的形式,在更逼真的環境裡測試新功能。

  • 硬體資源的利用更高效,在單一主機上現在可以執行多個容器應用,而以前不能。

  • 容器化是支援零停機升級、金絲雀部署、高可用和橫向擴充套件的堅實基礎。

容器化之外的選擇

  • 用 Puppet 和 Chef 之類的配置管理工具,能解決一部分的“傳統”問題,比如環境一致性等。但它們不能支援“原子”部署,以及對應用+環境的完整回滾。而一種無法方便回滾的部署方案,仍然會在部署中途充滿風險。

  • 虛擬機器映象是能實現部分上述能力的另一種方法,而且在有些情形中,相對於容器,使用完整的虛機進行“原子地”部署會更合適。但使用虛機的主要問題是,它對硬體的利用率更低效。因為虛機需要一些獨佔的資源(CPU、記憶體和磁碟等),而容器之間可以共享主機的資源。


如何容器化



一、準備工作

列出儲存資料的檔案系統位置

由於部署新版本應用是透過替換 Docker 映象實現的,所以任何持久化的資料都應該儲存在容器之外。如果運氣不錯的話,可能遇到應用已經將所有資料都寫入了特定位置,不過多數傳統應用常將它們的資料往磁碟上到處亂寫,還有可能與應用本身的檔案混在一起。Docker 的可載入儲存卷(volume)讓主機的檔案系統能暴露給容器用作特定路徑,這樣資料可以在容器之間留存。所以,我們無論是哪種情況,我們都需要列出用於儲存資料的位置。

現在你可以考慮考慮讓應用裡所有輸出的資料寫入到檔案系統的同一目錄去了,這樣能明顯簡化容器化版本的部署工作。不過,如果修改應用難以達成,這也並不是必須的。

找出會隨部署環境變化的配置資料

為了確保一致性,同一個映象要在多套環境中使用(比如,測試和生產),因此必須要列出所有在不同環境中會變化的配置值,在啟動容器的時刻再設定值。容器中的程式到時候可以從環境變數,或者從配置檔案中獲取這些配置的值。

你可以現在就考慮修改應用並支援從環境變數中讀取配置,以便簡化容器化的過程。同樣的,如果不好修改應用,這也是不一定是必要的。

找出容易移出去的服務

在同一機器上,我們的應用可能要依賴一些其他服務,它們如果獨立性比較高、使用 TCP/IP 通訊,就很容易能移出去。舉例來說,如果在同一機器上執行 MySQL 或 PostgreSQL 資料庫,或者類似 Redis 的快取,那就容易移出去了。可能同時還需要調整配置,才能支援指定機器名(hostname)和埠(port)而不是直接認為應用執行在 localhost。

二、建立容器映象

建立用於安裝應用的 Dockerfile

如果已經有基於指令碼或者 Chef、Puppet 之類的配置管理工具的自動化安裝能力,那這個過程就很簡單了。挑選一個喜歡的系統映象、安裝所有依賴,然後執行自動化指令碼就行了。

如果目前的安裝過程是手動的,就需要寫一些指令碼了。不過,由於映象的狀態是已知的,在這兒編寫指令碼要比基於可能存在不一致性的原生系統來的容易。

如果提前找出了要移出去的服務,那麼在指令碼里就不應該安裝它們了。

下面是一個簡單的示例 Dockerfile:

# 基於官方 Ubuntu 16.04 Docker 映象
FROM ubuntu:16.04
# 安裝所依賴的 Ubuntu 軟體包
RUN apt-get install -y <REQUIRED UBUNTU PACKAGES> \ 
   && apt-get clean \ 
   && rm -rf /var/lib/apt/lists/*
# 將應用的檔案複製到映象裡
ADD . /app
# 執行安裝指令碼
RUN /app/setup.sh
# 切換到應用的目錄
WORKDIR /app
# 指定應用的啟動指令碼
COMMAND /app/start.sh

製作用於配置的啟動指令碼

如果應用已經在使用環境變數中讀取配置值了,那這一步可以跳過了。如果要從檔案裡讀取特定環境相關的配置值,那啟動指令碼就要能從環境變數裡讀取配置值,並將這些值更新到配置檔案中去。

這裡有一個啟動指令碼的例子:

#!/usr/bin/env bash
set -e
# 把環境變數 $MYAPPCONFIG 的值新增到配置檔案中
cat >>/app/config.txt <<END
my_app_config = "${MYAPPCONFIG}"
END
# 用環境變數 $MYAPPARG 作為應用的啟動引數
/app/bin/my-app --my-arg="${MYAPPARG}"

推送映象

映象生成之後(使用 docker build),需要推送到 Docker 倉儲(Registry)中才能從部署機器上拉取到(如果要在生成映象的同一臺機器上執行,就不需要)。

可以使用 Docker Hub 來儲存映象(用付費賬號可以建立私有倉庫),大多數雲服務商也提供容器倉儲(比如 Amazon ECR)。

給映象設定標籤(比如 docker tag myimage mycompany/myimage:mytag)之後,就可以推送到倉庫了(比如 docker push mycompany/myimage:mytag)。每次在應用新版本生成映象時打上新的標籤,這樣既能明確當前所執行的版本,還能保留舊版本的映象以便回滾。

三、如何部署

部署容器是個很大的話題,接下來只關注直接使用 docker 命令執行容器的部分。在現實世界中,應該考慮使用 docker-compose(對於所有容器都執行在同一機器上的簡單情形)和 Kubernetes (在叢集中編排容器)之類的工具。

被移出來的服務

提前移出來的服務可以執行在單獨的 Docker 容器中,然後連結(link)到我們的應用所在容器。另外,還可以用雲上託管的服務。舉個例子,在 AWS 上,可以使用 RDS 作為資料庫、用 Elasticache 作為快取,這樣可以極大地簡化你的工作,因為他們能為你解決後期維護,高可用和備份等需求。

執行 Postgres 資料庫容器的例子:

docker run -d \
    --name db \
    -v /usr/local/postgresql/data:/var/lib/postgresql/data \
    postgres

容器化之後的應用

要在 Docker 容器中執行一個應用,只要用一個命令列:

docker run -d \
    -p 8080:80 \
    --name myapp \
    -v /usr/local/myappdata:/var/lib/myappdata \
    -e MYAPPCONFIG=myvalue \
    -e MYAPPARG=myarg \
    --link db:db \
    myappimage:mytag

其中的 -p 引數將容器裡的 80 埠公開並對映到主機上的 8080 埠,-v 引數設定要在容器里載入的、用於持久化資料的儲存卷(格式是 主機上的路徑:容器中的路徑)-e 引數設定一個用於配置的環境變數值(這些引數可以指定多次,從而設定多個卷和環境變數),而 --link引數將資料庫所在容器以連結的方式傳入,這樣應用就可以與資料庫通訊了。容器會根據 Dockerfile 中的 COMMAND 指令指定的指令碼來啟動。

對應用進行升級

如果要升級到應用的新版本,只要停掉舊版的容器(比如 docker rm -f myapp),並用新的映象標籤啟動新的容器就可以了(可能有短暫的停機時間)。回滾操作也類似,只要換用舊版的映象標籤。


更多相關考量



“init” 程式(PID 1)

傳統應用通常有多個程式,如果沒有 “init” 守護程式(PID 1)的清理,就容易出現孤兒程式(orphan processes)發生累積的情況了。Docker 預設並不提供這樣的守護程式,所以推薦自己用 ENTRYPOINT 在 Dockerfile 裡新增一個。dumb-init 是眾多初始守護程式中的比較輕量級的一個。phusion/baseimage 是一個包含 init 初始守護程式和其他一些服務的全功能基準映象。 請檢視我們部落格上關於這個主題的文章:Docker 守護程式:PID-1, 孤兒程式, 殭屍程式和訊號。

守護程式和定時任務

在使用 Docker 容器時,一般只會在每個容器中執行一個程式。理想情況下,所有守護程式和定時任務都應該移到其他容器中去,不過對於傳統應用來,這也不一定都行得通,主要是經常要求對應用進行重新設計。要執行多個程式也不是一定不行,但確實會需要一些額外的一些配置,因為標準的基準映象裡並不包含程式管理和排程能力。小型程式管理程式,比如 runit,比 systemd 之類的完整功能的子系統更適合在容器中用。phusion/baseimage 是一個包含 runit 和定時能力和其他一些服務的全功能基準映象。

儲存卷的許可權

在容器裡,所有程式通常都以 root 身份執行(不過也不是必須的)。傳統的應用對使用者的需求通常複雜一些,可能要用其他使用者來執行(或者用不同的使用者執行多個程式)。這可能給儲存卷的使用帶來一些麻煩,因為 Docker 預設讓載入的卷的所有權指向 root,也就是說非 root 程式就不能寫入到這些捲了。有兩個方法可以解決這個問題:

第一種方式是在在建立容器之前,先在主機上建立好目錄,由有正確的 UID/GID 的使用者持有所有權。注意,由於容器裡和主機上的使用者不能匹配,所以需要用容器裡使用者的 UID/GID,而不僅僅是使用者名稱要一致。

另一種方式是在容器裡,在啟動過程中調整載入點的所有權。這就需要在切換到用來啟動應用的非 root 使用者之前,還在以 root 身份執行期間處理。

資料庫遷移

資料庫結構遷移在部署工作中經常是一大挑戰,因為資料庫結構通常與應用是嚴格耦合的,這對遷移的時機提出了要求,而且這也讓回滾到舊版本變得更難,因為資料庫遷移並不一定容易回滾。

完成這種遷移的方法是引入一個過渡步驟。如果需要對資料庫結構做出與舊版本不相容的變更,那就將這個變更分為兩次部署。比如,如果想將資料移到另一處,兩個步驟是:

將資料同時寫入舊的位置和的位置,並只從新的位置讀取。這意味著,如果把應用回滾到前一個版本,在回滾之前新產生的新資料是不會丟的。

不再向舊的位置寫入資料。 要注意的是,如果希望部署期間沒有停機時間,就意味著在同一時間會有應用的多個版本在執行,相應的也會帶來更多挑戰。

資料備份

對容器化的應用進行備份通常比較簡單。資料檔案可以從主機上備份,而不需要擔心資料會與應用程式的檔案混在一起,因為它們已經嚴格地分開了。如果將資料庫遷移到了像 RDS 這樣的託管服務,他們就會處理好備份(至少自己的工作會簡化一些)。

遷移已有資料

在生產環境中,要把現有應用遷向容器化的版本,就需要對舊的已有資料進行遷移。這個工作往往因地制宜,不過最簡單的就是停掉舊版本,把資料備份直接恢復給新版本用。這個過程應該提前做好,也不可避免地會需要一定的停機時間。


結論



雖然提前需要做一些工作,對傳統的應用進行容器化的過程會幫助我們更好地對它進行管控和自動化,能把部署的壓力降到最低。它給對應用進行現代化改造提供了一個明確的路徑,並能支援零停機部署、高可用和橫向擴充套件。

除了從零開始構建容器化應用,FP Complete 已經多次開展了上述實踐。如果你想了解邁向現代化、無壓力部署之路,可以瞭解一下我們的 DevOps 和諮詢服務,或直接聯絡我們。

英文原文(請手動複製): https://www.fpcomplete.com/blog/2017/01/containerize-legacy-app

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31473948/viewspace-2182716/,如需轉載,請註明出處,否則將追究法律責任。

相關文章