CoreOS那些事之系統升級
前段時間在DockerOne回復了一個關于 CoreOS 升級的提問。仔細琢磨來,這個問題還有不少可深入之處,因此有了此文,供已經在國內使用 CoreOS 的玩家們參考。
具有CoreOS特色的系統升級
CoreOS的設計初衷之一就是“解決互聯網上普遍存在的服務器系統及軟件由于沒有及時升級和應用補丁,造成已知漏洞被惡意利用導致的安全性問題”。因此,它的升級方式在各種Linux發型版中可以說是獨樹一幟的,特別是與主流的服務器端系統相比。
平滑升級
一方面來說,常用的服務器系統如RedHat、CentOS、Debian、Ubuntu甚至FreeBSD和Windows Server都存在明確的版本界限,要么不能支持直接在線升級至新的發行版本,要么(如Debian/Ubuntu和Windows 7以后的版本)雖能夠跨版本升級卻容易出現兼容性風險,一旦升級后出現故障往往面臨進退兩難的局面。
這個問題在一些新興的Linux發行版,如Arch Linux已經有了較先進的解決方法:將過去累計許多補丁再發行一次大版本的做法變為以月或更短周期的快速迭代更新
,并由系統本身提供平滑升級和回滾
的支持。這樣,用戶可以在任何時候、從任何版本直接更新至修了最新安全補丁的系統。然而,這些以 Arch 為代表的平滑升級系統還是帶來了一些更新系統后無法使用的事故,不妨在百度以“Arch 升級問題”關鍵字搜索會發現許多類似的抱怨。事實上,Arch的目標用戶主要是喜愛嘗鮮的Linux愛好者而不是服務器管理員或者服務端應用架構師。
那么平滑升級的思路是不是在服務器系統就走不通了呢。其實仔細分析平滑升級出現問題的原因,當中最關鍵的一個因素在于,系統設計時最多只能確保從一個干凈的系統順利升級的途徑,如果用戶對系統中的某些核心組件做了修改(比如將系統中的Python2升級成了Python3),它就不屬于操作系統設計者控制范圍內的工作了。這樣相當于設定了一個售后服務霸王條款(只是個比喻,這些Linux系統其實都是免費的):自行改裝,不予保修。
在過去,用戶要使用服務器系統,他就必然需要在上面安裝其他的提供對外服務軟件和程序,因此對系統本身有意無意的修改幾乎是無法避免的。這個問題直到近些年來應用容器(特別是Docker)的概念被大規模的推廣以后才出現了新的解決思路。而CoreOS就是通過容器巧妙的避開了用戶篡改系統的問題,提出了另一種解決思路:讓系統分區只讀,用戶通過容器運行服務
。不得不說,這簡直就是以一個霸王條款替代了另一個霸王條款,然而這個新的“條款”帶來的附加好處,使得它被對穩定性和安全性都要求很高的服務器領域而言接受起來要心安理得的多。
“反正許多東西都要自動化的,套個容器又何妨。” 恩,就這么愉快的決定了。
自動更新
另一方面來說,除了系統的大版本升級,平時的系統和關鍵軟件的小幅補丁更新也時常由于系統管理員的疏忽而沒有得到及時運用,這同樣是導致系統安全問題的一個重要因素(比如2014年BrowserStack中招的這個例子)。
這個解決思路就比較簡單了:自動更新
。這么簡單的辦法當然早就被人用過了。即便在操作系統層面還見得不多,在應用軟件上早都是爛熟的套路。那么,為了不落俗套,怎樣把自動更新做得創意一些呢。先來看看系統升級都會有哪些坑。
乍一看來,操作系統這個東東和普通應用在升級時候會遇到的問題還是有幾分相似。比如軟件正在使用的時候一般是不可以直接熱修補的,系統也一樣(Linux 4.0 內核已經在著手解決這個痛點了,因此它在未來可能會成為偽命題)。又比如軟件運行會有依賴,而系統的核心組件之間也是有依賴的,因此一旦涉及升級就又涉及了版本匹配問題。除此之外,它們之間還是有些不一樣的地方。比如許多應用軟件其實可以直接免安裝的,升級時候直接把新文件替換一下舊的就算完成了。操作系統要想免安裝,則需要些特別的技巧。
下面依次來說說CoreOS是怎樣應對這幾個坑的。
既然系統不能熱修補,就一定會牽扯到重啟的情況,這在服務器系統是比較忌諱的,為了避免系統重啟時對外服務中斷,CoreOS設計了服務自動遷移
的內置功能,由其核心組件Fleet提供。當然這個并不是一個完美的方案,相信未來還會有更具創意的辦法替代它的。
版本匹配的問題在應用軟件層面比較好的解決方法還是容器,即把所有依賴打包在一起部署,每次更新就更新整個容器的鏡像。同樣的思路用到操作系統上,CoreOS每次更新都是一次整體升級
,下載完整的系統鏡像,然后做MD5校驗,最后重啟一下系統,把內核與外圍依賴整個兒換掉。這樣帶來的額外好處是,每次升級必然是全部成功或者全部失敗,不會存在升級部分成功的尷尬情況。
要想免安裝軟件那樣直接重啟換系統會遇到什么問題呢?兩個方面,其一是,應用軟件是由操作系統托管和啟動的,可以通過系統來替換他的文件。那么操作系統自己呢,是由引導區的幾行啟動代碼帶動的,想在這么一畝三分地上提取鏡像、替換系統、還想搞快點別太花時間,額,那真是螺螄殼里做道場——排不出場面(還記得Window或者Mac電腦每次升級系統時候的等待界面么)。其二是,系統升級出問題是要能回滾的啊,不然怎么在生產環境用?即便不考慮啟動時替換文件所需要的時間,萬一更新過后啟動不起來,原來的系統又已經被覆蓋了,我天,這簡直是給自己埋了一個地雷。由此可見,想要實現快速安全的升級,在重啟后安裝更新的做法從啟動時間和回滾難度的兩個方面看都不是最佳的辦法。
為此,CoreOS又有一招絕活。進過CoreOS的主頁的讀者應該都見過上面這個A/B雙系統分區
的設計圖。正如圖中所示,CoreOS安裝時就會在硬盤上劃出兩塊獨立的系統分區(空間大致為每個1GB),并且每次只將其中一個在作為系統內核使用,而后臺下載好的新系統鏡像會在系統運行期間就部署到備用的那個分區上。重啟的時候只需要設計個邏輯切換兩個分區的主次分工即可,不到分分鐘就完成了升級的過程,要是真出現啟動失敗的情況,CoreOS會自動檢測到并切換回原來的能正常工作的分區。用事先部署好的分區直接替換啟動的方法避免重啟后臨時安裝更新,這種思路的轉換,確實有點神來之筆的意思。
說個題外話。之前有一次我和其他的CoreOS愛好者在Meetup活動時聊到對于雙系統分區的看法,當時大家得出較一致的結論是:既然還是必須重啟,用不用兩個分區用戶都沒有實際獲益,相比之下,“平滑升級才是賣點,雙分區只是噱頭”。我在《CoreOS實踐指南》系列里也曾表達過類似的觀點。一直到后來自己仔細反思了這種設計的巧妙,才發覺原先想法的片面性,實在貽笑大方。
這些方法說起來蠻輕松,若要真的實施出來,就不是拍拍腦袋那么容易了。縱觀Linux開源系統百家爭鳴,真正實現了這樣后臺更新設計的系統也僅CoreOS一枝獨秀。
升級參數配置
理解了CoreOS的自升級方式,繼續來說說與升級相關的配置。CoreOS系統升級有關的選項通常會在首次啟動服務器時通過 cloud-init
的 coreos.update
項指定,系統啟動后也可以在 /etc/coreos/update.conf
文件里修改。可配置的屬性包括三個:升級通道、升級策略和升級服務器。這三個屬性在DockerOne的回答中都已經提到,下面將在此基礎上再略作深化。
初始化升級配置
這是最常用的配置升級參數的方式,系統首次啟動時cloud-init將完成大多數節點和集群相關的初始化任務。與CoreOS升級有關的部分是coreos.update下面的三個鍵,其內容舉例如下:
coreos: update: reboot-strategy: best-effort group: alpha server: https://example.update.core-os.net
其中只有group一項是必須的,它指定了系統的升級通道。升級策略 reboot-strategy的默認值是best-effort,而升級服務器server的默認值是CoreOS的官方升級服務器。
修改升級配置
對于已經啟動的集群,可以在/etc/coreos/update.conf配置文件中對升級參數進行修改,其內容格式簡單明了。舉例如下:
GROUP=alpha REBOOT_STRATEGY=best-effort SERVER=https://example.update.core-os.net
同樣,大多數情況下用戶只會看到GROUP這一個值,因為只有它是必須的。其余的兩行可以沒有,此時會使用默認值代替。
需要注意的是:
- 每次修改完成以后需要執行
sudo systemctl restart update-engine
命令使配置生效 - 修改一個節點的配置并不會影響集群其他節點的升級配置,需要逐一單獨修改
- 最好讓集群中的節點使用相同的升級通道,方便管理,雖然混用通道一般不會直接導致問題
- 優先選擇用cloud-init。在初始化時就將系統參數設計好,減少額外修改的工作量
升級通道
升級通道間接的定義了CoreOS每次升級的目標版本號。這個思路大概是從Chrome瀏覽器借鑒來的,官方提供三個升級通道:Alpha(內測版)、Beta(公測版 )和 Stable(正式發行版)。舉個例子來說,如果用戶配置的是Alpha通道,那么他的每次更新就會升級到當前最新的內測系統版本上。內存版本類似于Chrome瀏覽器的所謂“開發版”,會第一時間獲得新的功能更新,穩定性一般還是蠻可以的,但不適合做為產品服務器,主要面向的對象是喜愛新鮮的開發者和玩家。公測版穩定性略高,也會比較快的獲得新功能的推送,適合作為項目開發測試環境把玩。正式發行版中的組件往往都不是最新版本的,但其穩定性最高,適合作為產品服務器使用。 CoreOS目前采用一個整數數字來表示版本號,數字越大則相對發布時間越新。
各通道發布更新的頻率依次為(見官方博客聲明):
- Alpha:每周星期四發布
- Beta:每兩周發布一次
- Stable:每個月發布一次
每個通道當前的系統版本號及內置組件版本號可以在這個網頁上查看到。
除了三個公開的通道,訂閱了CoreUpdate服務的用戶還可以定制升級自己的通道,但這個服務是付費的。此外,使用了企業版托管CoreOS系統的用戶也可以免費使用此功能,企業版的起步費用是10個節點以內 $100/月,見這個鏈接。還有另一個土豪企業版服務起步價是25個節點以內 $2100/月,差別就是提供額外的人工技術支持服務,果然技術人才是最貴的東東。
升級策略
升級策略主要與自動升級后的重啟更新方式有關。它的值可以是 best-effort(默認值)、 etcd-lock、 reboot 和off。其作用依次解釋如下:
- best-effort:如果Etcd運行正常則相當于 etcd-lock,否則相當于reboot
- etcd-lock:自動升級后自動重啟,使用LockSmith 服務調度重啟過程
- reboot:自動升級后立即自動重啟系統
- off:自動升級后等待用戶手工重啟
默認的方式是best-effort,通常它相當于etcd-lock策略,重啟過程會使用到CoreOS的LockSmith服務調度升級過程。主要是防止過多的節點同時重啟導致對外服務中斷和Etcd的Leader節點選舉無法進行。它的工作原理本身很簡單,通過在Etcd的 coreos.com/updateengine/rebootlock/semaphore
路徑可用看到它的全部配置:
$ etcdctl get coreos.com/updateengine/rebootlock/semaphore { "semaphore": 0, "max": 1, "holders": [ "010a2e41e747415ba51212fa995801dd" ] }
通過設定固定數量的鎖,只有獲得鎖的主機才能夠進行重啟升級,否則就繼續監聽鎖的變化。重啟升級后的節點會釋放它占用的鎖,從而通知其他節點開始下一輪獲取升級鎖的競爭。
除了直接修改Etcd的內容,CoreOS還提供了 locksmithctl
命令更直觀的查看LockSmith服務的狀態或設置升級鎖的數量。
查看升級鎖的狀態信息:
$ locksmithctl status Available: 0 <-- 剩余的鎖數量 Max: 1 <-- 鎖的總數 MACHINE ID 010a2e41e747415ba51212fa995801dd <-- 獲得鎖的節點
其中獲得鎖的節點就是已經已經下載部署好新版本系統,等待或即將重啟(與升級策略有關)的節點的Machine ID。用locksmithctl set-max
命令可用修改升級鎖數量(即允許同時重啟升級的節點數量):
$ locksmithctl set-max 3 Old: 1 New: 3
此時若再次用locksmithctl status
查看狀態就會看到 Max
的數量變成3了。
此外,locksmithctl unlock
命令可以將升級鎖從獲得鎖的節點上釋放,這個命令很少會用到,除非一個節點獲得鎖后由于特殊的原因無法重啟(例如磁盤錯誤等硬件故障),因而始終占用這個鎖。這種情況下才會需要手工釋放。
升級服務器
許多希望在內網中使用CoreOS的用戶都比較關心能否在內網搭建自己的升級服務器?答案是肯定的。
比較可惜的是,CoreOS 升級服務器是屬于CoreUpdate服務的一部分,也就是說,它是需要付費使用的。不過考慮到通常會在自己內網搭建服務器集群的大都是企業級用戶,收費也還算公道。
從文檔資料來看,CoreOS所用升級服務器協議與Google的ChromeOS升級服務器是完全兼容的,甚至可以相互替代。比較有趣的是,兩者都開源了各自的操作系統,但都沒有開源其升級服務器實現,這個中意思仿佛是如果讓用戶去自己架設升級服務器,誰來保證這些升級服務器的鏡像是最新的呢,那么自動升級提供的系統安全性的意義又何在了呢。
順帶說一句,在CoreOS的SDK中有一個 start_devserver工具 用于測試部署用戶自己構建的CoreOS鏡像(系統是開源的嘛),因此如果用戶直接下載官方鏡像提供給這個工具,應當是可以自己構建內網升級服務器的。但是官方文檔對這方面的介紹比較模糊,我暫且拋磚引玉了,待高人給出具體方案。
手動升級系統
CoreOS始終會自動在后臺下載和部署新版本系統,即使將升級策略設為off(這樣只是禁止自動重啟)。因此在絕大多數情況下,除非處于測試目的和緊急的版本修復,用戶是不需要手動觸發系統升級的。不過,大概是考慮到總是有新版本強迫癥用戶的需求(其實主要是系統測試的需求啦),CoreOS還是提供了手動更新的途徑。
查看當前系統版本
相比手動更新,用戶也許更想看到的僅僅是:現在的系統到底是部署的哪個版本啦。方法很簡單,查看一下etc目錄下面的 os-release
文件就可以了。
$ cat /etc/os-release NAME=CoreOS ID=coreos VERSION=607.0.0 VERSION_ID=607.0.0 BUILD_ID= PRETTY_NAME="CoreOS 607.0.0" ANSI_COLOR="1;32" HOME_URL="https://coreos.com/" BUG_REPORT_URL="https://github.com/coreos/bugs/issues"
這個文件實際上是一個軟鏈接,指向系統分區的 /usr/lib/os-release 文件,而后者是只讀分區的一部分,因此不用擔心這個文件中的內容會被外部篡改。
自動升級的頻率
CoreOS會在 啟動后10分鐘 以及之后的 每隔1個小時 自動檢測系統版本,如果檢查到新版本就會自動下載下來放到備用分區上,然后依據之前的那個升級策略決定是否自動重啟節點。OK,就這么簡單。
具體的升級檢測記錄可以通過 journalctl -f -u update-engine
命令查看到。
手動觸發升級
恩,下面這個命令是給升級強迫癥用戶準備滴。
命令非常簡單:update_engine_client -update
,如果提示 "Update failed" 則表示當前已經是最新版本(搞不懂CoreOS那班人為啥不弄個友好點的提示信息)。如果檢測到有新版本的系統則會立即將其下載和部署到備用系統分區上。
$ update_engine_client -update [0404/032058:INFO:update_engine_client.cc(245)] Initiating update check and install. [0404/032058:INFO:update_engine_client.cc(250)] Waiting for update to complete. LAST_CHECKED_TIME=1428117554 PROGRESS=0.000000 CURRENT_OP=UPDATE_STATUS_UPDATE_AVAILABLE NEW_VERSION=0.0.0.0 ... ... CURRENT_OP=UPDATE_STATUS_FINALIZING NEW_VERSION=0.0.0.0 NEW_SIZE=129636481 Broadcast message from locksmithd at 2015-04-04 03:22:56.556697323 +0000 UTC: System reboot in 5 minutes! LAST_CHECKED_TIME=1428117554 PROGRESS=0.000000 CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT NEW_VERSION=0.0.0.0 NEW_SIZE=129636481 [0404/032258:INFO:update_engine_client.cc(193)] Update succeeded -- reboot needed.
部署完成后,如果用戶的升級策略不是 off,系統會發送消息給所有登錄當前的用戶:“5分鐘后系統將重啟”。當然,你自己也會在5分鐘后被踢出SSH登錄,等再次登錄回來的時候,就會發現系統已經變成新的版本了。
更好的升級策略
在看到CoreOS的4種升級策略時候,不曉得讀者有沒發現一個問題。前3種策略都會讓新的系統版本下載部署后馬上重啟服務器,如果這個時候恰好是系統訪問的高峰期,即使重啟過程中,服務會自動遷移到其他的節點繼續運行,仍然可能會造成短暫的服務中斷的情況。而第4種策略索性等待管理員用戶來重啟系統完成升級,又引入了額外的人工干預,如果重啟不及時還會使得集群得不到必要的安全更新。
有沒有辦法既讓服務器不要在服務高峰期重啟,又不至于很長時間沒有更新呢?CoreOS給出了一種推薦的解決方法。我將它稱為第5種升級策略:基于定時檢測的自動重啟。
這種升級策略沒有在內置的選項當中,我們需要做些額外的工作:
- 將升級策略設置成 off
- 增加一個服務用來檢測備用分區是否已經部署新的系統版本,如果部署了就進行重啟
- 增加一個定時器在集群的低峰時段觸發執行上面那個服務
檢測和重啟服務
首先來看最關鍵的這個服務update-window.service
,它會去執行放在 /opt/bin 目錄下面的update-window.sh
腳本文件。
[Unit] Description=Reboot if an update has been downloaded [Service] ExecStart=/opt/bin/update-window.sh
這個腳本首先使用 update_engine_client -status
檢測了備份分區是否已經部署好了新版本的系統。如果發現新的版本已經部署好,就根據 Etcd 服務是否啟動來選擇通過 Locksmith 調度重啟節點(先獲取鎖然后重啟動)或延遲一個隨機的時間后重啟節點,這樣做的目的是防止太多節點在同一個時間重啟導致集群不穩定。
#!/bin/bash # If etcd is active, this uses locksmith. Otherwise, it randomly delays. delay=$(/usr/bin/expr $RANDOM % 3600 ) rebootflag='NEED_REBOOT' if update_engine_client -status | grep $rebootflag; then echo -n "etcd is " if systemctl is-active etcd; then echo "Update reboot with locksmithctl." locksmithctl reboot else echo "Update reboot in $delay seconds." sleep $delay reboot fi fi
定時觸發服務
接下來添加定時器update-window.timer
在集群訪問的低峰時段觸發前面那個服務,
[Unit] Description=Reboot timer [Timer] OnCalendar=*-*-* 05,06:00,30:00
這個定時器Unit文件的功能類似于一個crontab記錄,只不過對于用了Systemd啟動的系統比較推薦使用這樣的方式。上面的配置表示每天早上的5:00, 5:30, 6:00 和 6:30。
寫到 cloud-init 里
既然是每個節點都要有的東東,當然要放到cloud-init 配置里面。把上面的內容統統寫進去,看起來就是這個樣子的了:
#cloud-config coreos: update: reboot-strategy: off units: - name: update-window.service runtime: true content: | [Unit] Description=Reboot if an update has been downloaded [Service] ExecStart=/opt/bin/update-window.sh - name: update-window.timer runtime: true command: start content: | [Unit] Description=Reboot timer [Timer] OnCalendar=*-*-* 05,06:00,30:00 write_files: - path: /opt/bin/update-window.sh permissions: 0755 owner: root content: | #!/bin/bash # If etcd is active, this uses locksmith. Otherwise, it randomly delays. delay=$(/usr/bin/expr $RANDOM % 3600 ) rebootflag='NEED_REBOOT' if update_engine_client -status | grep $rebootflag; then echo -n "etcd is " if systemctl is-active etcd; then echo "Update reboot with locksmithctl." locksmithctl reboot else echo "Update reboot in $delay seconds." sleep $delay reboot fi fi exit 0
到這里,CoreOS升級相關的事兒已經侃得差不多了。不過總覺得還差點什么。
具有國內特色的CoreOS升級問題
在國內服務器用過CoreOS的用戶大約都會發現一個比較憂傷的現象:好像CoreOS的自動升級沒有生效捏?
相信不少用戶大概已經猜到原因了吧。為了驗證猜測,不妨做個手動升級試試。下面是我在國內的一個CoreOS集群上得到的結果:
$ update_engine_client -check_for_update [0328/091247:INFO:update_engine_client.cc(245)] Initiating update check and install. [0328/092033:WARNING:update_engine_client.cc(59)] Error getting dbus proxy for com.coreos.update1: GError(3): Could not get owner of name 'com.coreos.update1': no such name [0328/092033:INFO:update_engine_client.cc(50)] Retrying to get dbus proxy. Try 2/4 ... ... [0328/092053:INFO:update_engine_client.cc(50)] Retrying to get dbus proxy. Try 4/4 [0328/092103:WARNING:update_engine_client.cc(59)] Error getting dbus proxy for com.coreos.update1: GError(3): Could not get owner of name 'com.coreos.update1': no such name [0328/092103:ERROR:update_engine_client.cc(64)] Giving up -- unable to get dbus proxy for com.coreos.update1
看到最后輸出Giving up的時候,整個人都不好了。現在來說說怎么解決這個問題。
使用HTTP代理升級CoreOS
既然是訪問不到升級服務器,解決辦法就很干脆了:KX上網。
首先得找一個能用的墻外HTTP代理服務器,這個...大家各顯神通吧,記錄下找到的地址和端口,下面來配置通過代理升級服務器。
創建一個配置文件 /etc/systemd/system/update-engine.service.d/proxy.conf,內容為:
[Service] Environment=ALL_PROXY=http://your.proxy.address:port
將ALL_PROXY
的值換成實際的代理服務器地址,重啟一下update-engine服務即可:
sudo systemctl restart update-engine
這個工作也可以在cloud-config里面用write_files命令在節點啟動時候就完成:
#cloud-config write_files: - path: /etc/systemd/system/update-engine.service.d/proxy.conf content: | [Service] Environment=ALL_PROXY=http://your.proxy.address:port coreos: units: - name: update-engine.service command: restart
官方的服務器呢
其實一直有小道消息說,CoreOS公司已經在積極解決這個問題,預計2015年下半年會在國內架設專用的升級服務器。只能是期待一下了。
小結
“平滑而安全的滾動升級”和“無需干預的自動更新”既是CoreOS系統設計的初衷,也是一直是許多用戶青睞CoreOS的原因。特別是在需要長期運行的服務器集群上,這些特性不僅節省了手工安裝補丁和升級系統的成本,更避免了系統和核心軟件升級不及時帶來的安全性隱患。
希望這篇文章中介紹的內容能對大家理解CoreOS的系統升級相關問題提供一定參考和幫助。歡迎通過評論參與討論。
來自:http://www.infoq.com/cn/articles/coreos-system-upgrade