1. 程式人生 > >一個Docker附加引數引起的網路服務異常

一個Docker附加引數引起的網路服務異常

1. 事件

背景

事發在線上環境,OpenStack由容器化部署方案Kolla提供,各主要軟體版本如下:

OpenStack

時間線

2017/3/17: 測試完成修復方案,上線更新;

2017/3/15: 找到root cause,確定觸發場景,確定修復方案;

2017/3/14: 找到neutron產生無限多tap的場景2,能夠在模擬條件下復現問題,與環境日誌記錄匹配;

2017/3/13: 找到neutron產生無限多tap的場景1,能夠在模擬條件下復現問題,但對比環境日誌並不完全匹配,而且模擬條件的出現概率非常低,排除;

……

2017/3/11

09:00:00: 在控制節點恢復網路服務(neutron-metadata-agent、neutron-dhcp-agent);

08:00:00: 自動化指令碼清理完成所有無效tap裝置,控制節點成功啟動neutron-*-agent,虛擬網路服務能力恢復;

……

2017/3/10

22:00:00: 執行指令碼自動化清理控制節點的無效tap裝置

21:30:00: 原控制節點所屬(6臺)虛擬機器恢復訪問在其他節點(nc05、nc03)恢復原控制節點的虛擬機器

21:00:00: 環境更新包功能還原在其他節點(nc05)成功啟動neutron-metadata-agent

20:30:00: 整體環境新建虛機分配網路還原在其他節點(nc05)成功啟動 neutron-dhcp-agent

18:30:00: 放棄在控制節點還原網路服務,嘗試在其他節點還原網路服務;

18:00:00: 嘗試刪除tap裝置,但進度較慢;嘗試在程式碼去掉neutron-openvsiwtch-agent中關於tap裝置的預讀過程,因涉及點太多放棄線上修改;

17:00:00: 發現控制節點存在10000+tap裝置,link狀態為DOWN(導致neutorn-openvswitch-agent啟動失敗) ;

16:40:00: 整體環境新建虛擬機器分配網路失效,處理人忙亂之中再次重啟neutron-dhcp-agent,發現迴圈錯誤,neutron-dhcp-agent重啟失敗;

16:30:00: 控制節點所屬(6臺)虛擬機器網路失效懷疑是控制節點流表未重新整理原因,開發同事嘗試重啟控制節點的neutron-openvsiwtch-agent重新整理,但重啟失敗;

16:25:00: 開發開始定位處理開發同事檢查虛擬機器建立成功,但新建虛擬機器無法獲取neutron-metadata服務介面,導致業務叢集配置失敗;

16:10:00: 線上環境更新包功能失效專案維護同事反映培訓環境更新業務應用叢集失效;

2. 分析

2.1.不正常的迴圈

經過內部環境反覆測試與現場日誌對比,發現實際產生10000多個tap裝置的迴圈位於neutron-dhcp-agent服務的定時同步功能函式中

下面這段整體邏輯由於一個設定namespace的異常而不斷重試迴圈:

namespace

1) Neutron-dhcp-agent迴圈監聽是否存在更新需求(need_resunc_reasons);

2) 每次迴圈延遲間隔conf.resync_interval秒;

3) setup_dhcp_port()方法中申請一個新的Port(新的tapid產生);

4) add_veth()方法建立veth裝置(新的tap裝置產生);

5) ensure_namespace()方法中確認namespace,如不存在則建立;

6) set_netns()方法設定tap裝置的network namespace,設定失敗;

7) 跳轉到第一步迴圈;

可以看到,這段邏輯本身有一定缺陷

1) 設定namespace失敗後,沒有正確的try…catch流程刪除之前建立的裝置,導致失敗的tap裝置積累越來越多

2) ensure_namespace()方法只檢查namespace是否存在,沒有深入檢查namespace許可權等可能導致後續設定失敗的屬性

接下來的問題是(可能也是neutron在最後一步不設防的原因):namespace是neutron-dhcp-agent程序自身建立的,tap裝置也是neutron-dhcp-agent程序自身建立的,為什麼設定時會失敗呢?

2.2.為什麼沒有許可權

容器服務的隔離與共享

容器

其中各容器共享主機的Network namespace,但每個容器具備非共享的Mount namespace;在各自獨立的Mount namespace中,共享主機/run/netns目錄,用於共享虛擬網路的network namespace操作入口。

Mount

而在Docker實現中,(共享)使用外部儲存空間、資料卷功能都最終會依賴mount系統呼叫,程式碼片段:

Docker對附加傳入的private、shared等不同屬性的處理,實際對應執行mount系統呼叫時傳入不同flags,不同的flags對應到不同的Mount Propagation Type:

MS_SHARED

This mount point shares mount and unmount events with other mount points that are members of its “peer group”. When a mount point is added or removed under this mount point, this change will propagate to the peer group, so that the mount or unmount will also take place under each of the peer mount points. Propagation also occurs in the reverse direction, so that mount and unmount events on a peer mount will also propagate to this mount point.

MS_PRIVATE

This is the converse of a shared mount point. The mount point does not propagate events to any peers, and does not receive propagation events from any peers.

MS_SLAVE

This propagation type sits midway between shared and private. A slave mount has a master—a shared peer group whose members propagate mount and unmount events to the slave mount. However, the slave mount does not propagate events to the master peer group.

MS_UNBINDABLE

This mount point is unbindable. Like a private mount point, this mount point does not propagate events to or from peers. In addition, this mount point can’t be the source for a bind mount operation.

找到原因

由於啟動容器時對/run目錄沒有使用MS_SHARED傳播型別,容器重啟後,之前建立的namespace檔案會因/run/netns產生的`peer group`內其他peer的引用計數不能正常刪除(Device or resource busy)

umount(“/run/netns/ns1”, MNT_DETACH) = -1 EINVAL (Invalid argument)

unlink(“/run/netns/ns1”)            = -1 EBUSY (Device or resource busy)

刪除失敗帶來的後續結果是,由於檔案系統層的namespace檔案沒有完全刪除,而實際的networknamespace已經釋放,所以這個“半刪除”的namespace從使用者態程式的角度就呈現出這樣的狀態:能夠檢視到namespace,對應2.1第5)步中ensure_namespace()操作正常,但set操作時會失敗,對應2.1第6)步set_ns()操作失敗

setns(4, 1073741824)                    = -1 EINVAL (Invalid argument)

write(2, “seting the network namespace \”ns”…, 60seting the network namespace “ns1” failed: Invalid argument

解決方法

修改neutron-*-agent容器的啟動引數:

-v /run/netns:/run/netns:shared  -v /run:/run:rw

+ -v /run/netns:/run/netns:shared  -v /run:/run:rw:shared

2.3.確定觸發場景

模擬再現故障

1) 建立網路和子網

neutron net-create –shared –provider:network_type vlan –provider:physical_network physnet1 –provider:segmentation_id 108 vlan108

neutron subnet-create –name subnet108 vlan108 192.168.0.0/24

2) 重啟neutron_dhcp_agent容器

docker restart neutron_dhcp_agent

這時neutron_dhcp_agent所建立的namespace已被docker daemon和其他容器引用,許可權已轉移不允許neutron_dhcp_agent刪除

3) 刪除該網路的子網

neutron subnet-delete subnet108

neutron刪除子網成功,但後臺實際刪除namespace失敗,而且namespace名稱空間已釋放,但檔案系統介面任然存在,處於“半刪除”狀態,直接刪除網路不會觸發後續異常。

4) 為該網路重新建立子網

neutron subnet-create –name subnet108 vlan108 172.16.0.0/24

neutron建立子網成功,這時後臺應當重新建立namespace,但由於上一步刪除namespace動作失敗導致namespace非正常殘留,所以這裡跳過建立namespace動作,接下來為tap裝置設定namespace的動作失敗,開始進入2.1描述的迴圈狀態。

用Docker模擬區域性故障

1) 啟動兩個容器共享可操作network namespace檔案系統

docker run -d –name testa -it –v /run:/run:rw -v /run/netns:/run/netns:shared –privileged –net=host nova-compute:latest bash

docker run -d –name testb -it –v /run:/run:rw -v /run/netns:/run/netns:shared –privileged –net=host nova-compute:latest bash

2) 在容器A中建立namespace ns1

docker exec -u root testa ip netns add ns1

3) 重啟容器A

docker restart testa

4) 使用容器A刪除之前建立的namespace

docker exec -u root testa ip netns del ns1

Cannot remove namespace file “/var/run/netns/ns1”: Device or resource busy

5) 使用容器A設定namespace

docker exec -u root testa ip netns exec ns1 ip a

seting the network namespace “ns1” failed: Invalid argument

3. 小結

3.1.經驗總結

Mount陷阱

每個Mount namespace有自己獨立的檔案系統檢視,但是這種隔離性同時也帶來一些問題:比如,當系統載入一塊新的磁碟時,在最初的實現中每個namespace必須單獨掛載磁碟。為此核心在2.6.15引入了shared subtrees feature:“The key benefit of shared subtrees is to allow automatic, controlled propagation of mount and unmount events between namespaces. This means, for example, that mounting an optical disk in one mount namespace can trigger a mount of that disk in all other namespaces.” 每個掛載點都會標記Propagation type,用於決定在當前掛載點下建立/刪除(子)掛載點時,是否傳播到別的掛載點。功能同樣帶來潛在的複雜,如2.2描述的許可權傳播轉移出乎使用者的預料。

目前容器技術的儲存管理和使用最終都依賴Mount系統呼叫,後續的使用場景需注意。

應該溫柔的重試

如果neutron的重試機制“聰明”一點,就不會累計產生越來越多的tap裝置,也就不會造成實際使用者可見的網路異常。更好的方式是採用隨機化、指數型遞增的重試周期,有時候系統出現的一個小故障可能會導致重試請求同時出現,這些請求可能會逐漸放大故障。在不同場景應該考慮限制某個請求的重試次數或者程序整體在單位時間內的重試配額。

謹慎對待重啟

我們常使用重啟服務“快刀斬亂麻”,解決一般性問題,但從這件事的處理經過來看,兩次重啟服務使問題影響範圍不斷擴大,而且在其他很多場景下,重啟服務時程式會重新讀取外部資源,也常常會暴露出很多已經潛在、但尚未產生影響的問題。所以,在生產環境應該避免草率重啟,而且還應在服務可中斷時間有計劃的演練重啟,以便提前發現、解決未來被動重啟時才會暴露的問題。

容器化的利與弊

這次事故由容器啟動引數的不正確使用引起,但同樣因為容器利於部署應用的特性,現場較快的在其他節點部署恢復了網路服務,減少了業務中斷時間。

3.2.事件還原

容器化

附錄參考:

Docker基礎技術——Linux Namespace http://coolshell.cn/articles/17010.html

Mount namespace and mount propagation http://hustcat.github.io/mount-namespace-and-mount-propagation/

Shared Subtrees https://lwn.net/Articles/159077/

Mount namespaces and shared trees https://lwn.net/Articles/689856/

Mount namespaces, mount propagation, and unbindable mountshttps://lwn.net/Articles/690679/

作者:崔昊之,雲技術開發者和愛好者,Openstack化石玩家,Docker社群committer,就職於中電科華雲,希望和大家分享雲技術實踐過程中有趣的事兒。

文章來自微信公眾號:雲計術實踐