Kubernetes+.NET Core 在非著名網際網路公司的落地實踐

語言: CN / TW / HK

容器化背景

本來生活網( benlai.com )是一家生鮮電商平臺,公司很早就停止了燒錢模式,開始追求盈利。既然要把利潤最大化,那就要開源節流,作為技術可以在省錢的方面想想辦法 。我們的生產環境是由 IDC 機房的 100 多臺物理機所組成,佔用率高達 95%,閒置資源比較多,於是我們考慮藉助 k8s 來重構我們的基礎設施,提高我們資源的利用率。

容器化專案團隊最初加上我就只有三個人,同時我們還有各自的工作任務要做,留給容器化的時間較少,因此我們要考慮如何快速的搭建容器平臺,避免走全部自研這條路,這對我們來說是個巨大的挑戰。在經歷了一年的容器化之旅後,分享下我們這一年所踩過的坑和獲得的經驗。

面臨的問題

在搭建 k8s 叢集前,有很多問題擺在我們面前:

l    人手不足,時間也不充裕,不能有太多自研的需求

l    我們目前的釋出是由測試人員完成的,不可能要求他們去寫一個 yaml 或執行 kubectl 做釋出,這個學習成本太高也容易出錯,因此我們必須構建一個使用者體驗良好的視覺化平臺給釋出人員使用

l    我們有大量的 .NET 專案,而 .NET 環境又依賴 Windows

l    ConfigMap/Secret 不支援版本控制,同時用來存業務配置也不是很方便

l    k8s 叢集和外部的通訊如何打通

容器平臺

作為小團隊去構建一個容器平臺,自研的工作量太大了。前期我們調研過很多視覺化平臺,比如 k8sdashboard 和 rancher 等等,但是要操作這些平臺得需要專業的 k8s 運維知識,這對我們的測試人員不太友好。後來我們嘗試了 KubeSphere( kubesphere.io )平臺,各方面都比較符合我們的需求,於是決定使用該平臺作為我們容器平臺的基礎,在這之上構建我們自己的釋出流程。感興趣的朋友可以去了解下這個平臺。

.NET 專案

我們的專案有 Java 也有 .NET 的,.NET 專案佔了 80% 以上。要支援.NET 意味著要支援 Windows。在我們去年開始啟動專案的時候,k8s 剛升級到 1.14 版本,是支援Windows 節點的第一個版本,同時功能也比較弱。經過實驗,我們成功對 .NET Framework 的程式進行了容器化,在不改程式碼的前提下執行在了 Windows 伺服器上,並通過 k8s 進行管理。不過我們也遇到了一些比較難處理的問題,使用下來的總結如下:

l    Kubernetes 版本必須是 1.14 版本或以上

l    大多數 Linux 容器若不做處理會自動排程到 Windows 節點上

l    Windows 基礎映象體積普遍比較大

l    必須使用 Windows Server 2019 及以上版本

l    k8s 關鍵性元件以 Windows 服務形式執行,而非 Pod

l    儲存和網路的支援有侷限性

l    部署步驟複雜,易出錯

我們調研了一段時間後決定放棄使用 Linux 和Windows 的混合叢集,因為這些問題會帶來巨大的運維成本,而且也無法避免 Windows 的版權費。

我們也考慮過把這些專案轉換成 Java,但其中包含大量的業務邏輯程式碼,把這些重構為 Java 會消耗巨大的研發和測試的人力成本,顯然這對我們來說也是不現實的。那麼有沒有一種方案是改動很少的程式碼,卻又能支援 Linux 的呢?答案很明顯了,就是把 .NET 轉 .NET Core。我們採用了這種方案,並且大多數情況能很順利的轉換一個專案而不需要修改一行業務邏輯程式碼。

當然這個方案也有它的侷限性,比如遇到如下情況就需要修改程式碼無法直接轉換:

l    專案裡使用了依賴 Windows API 的程式碼

l    引用的第三方庫無 .NET Core 版本

l    WCF 和 Web Forms 的程式碼

這些修改的成本是可控的,也是我們可以接受的。到目前為止我們已經成功轉換了許多 .NET 專案,並且已執行在 k8s 生產環境之上。

叢集暴露

由於我們是基於物理機部署也就是裸金屬(Bare Metal)環境,所以無論基於什麼平臺搭建,最終還是要考慮如何暴露 k8s 叢集這個問題。

暴露方式

l    LoadBalancer, 是 Kubernetes 官方推薦的暴露方式,很可惜官方支援的方式都需要部署在雲上。我們公司全部是裸機環境部署,無法使用雲方案。

l    NodePort,埠範圍一般是 30000 以上,每個埠只能對應一種服務。如果應用越來越多,那埠可能就不夠用了。它最大的問題是如果你暴露某一個節點給外部訪問,那麼這個節點會成為單點。如果你要做高可用,這幾個節點都暴露出去,前面一樣也要加一個負載均衡,這樣事情就複雜了。

l    Ingress,可以解決 NodePort 埠複用的問題,它一般工作在7層上可以複用 80 和 443 埠。使用 Ingress 的前提是必須要有 Ingress Controller 配合,而 Ingress Controller 同樣會出現你需要暴露埠並公開的問題。這時候如果你用 HostNetwork 或 HostPort 把埠暴露在當前的節點上,就存在單點問題;如果你是暴露多個節點的話,同樣需要在前面再加一個LB。

l    HostNetwork/HostPort,這是更暴力的方式,直接把 Pod 的埠繫結到宿主機的埠上。這時候埠衝突會是一個很大的問題,同時單點問題依舊存在。

裸金屬方案

我們分別試用了兩套方案 MetalLB( metallb.universe.tf )和 Porter( github.com/kubesphere/porter ),這兩個都是以 LoadBalancer 方式暴露叢集的。我們測試下來都能滿足需求。Porter 是 KubeSphere 的子專案,和 KubeSphere 平臺相容性更好,但是目前  Porter 沒有如何部署高可用的文件,我在這裡簡單分享下:

前置條件

l    首先你的路由器,必須是三層交換機,需要支援 BGP 協議。現在大多數路由裝置都會支援這個協議,所以這個條件一般都能滿足

l    其次叢集節點上不能有建立 BGP 通訊的其他服務。舉例來說,當使用 Calico 時,開啟了BGP模式。它的 BGP 埠執行在每個叢集節點,Calico 和 Porter 同時啟用BGP 協議,會有部分節點同時執行兩個元件與交換機建立 BGP 協議造成衝突。而 KubeSphere 預設安裝的 Calico 是 ipip 模式的,所以我們沒有遇到衝突問題

l    最後一定要有網路運維人員支援,配合你完成路由器配置以及瞭解整個網路拓撲結構。瞭解網路拓撲結構是非常重要的,否則會遇到很多問題

配置和部署

架構和邏輯

Porter有兩個外掛:Porter-Manager 和 Porter-Agent

Porter-Manager 是使用Deployment 部署到 Master 節點上的,但預設只部署1個副本,它負責同步 BGP 路由到物理交換機(單向 BGP 路由,只需將 kubernetes 私有路由釋出給交換機即可,無需學習交換機內物理網路路由)。還有一個元件,Porter-Agent,它以 DaemonSet 的形式在所有節點都部署一個副本,功能是維護引流規則。

高可用架構

部署好後,你可能會有疑問:

l    單個路由器掛了怎麼辦

l    單個 porter-manager 掛了怎麼辦

l    porter-manager 和路由器網路斷了怎麼辦

l    EIP 下一跳地址所在的節點掛了怎麼辦

l    某個 EIP 流量突然飆升,一個節點扛不住怎麼辦

一般路由器或交換機都會準備兩臺做 VSU (Virtual Switching Unit) 實現高可用,這個是網路運維擅長的,這裡不細講了。主要說下其他幾點怎麼解決

l    確保一個 EIP 有多條 BGP 路由規則,交換機和 Manager 是多對多部署的

l    確保交換機開啟等價路由(ECMP)

l    要做好故障演練和壓力測試

MetalLB 的高可用部署也是類似思路,雖然架構稍有不同,比如它和路由器進行 BGP 通訊的元件是 Speaker,對應 Porter 的 Manager;它與Porter 的區別在於高可用目前做的比較好;但是 Local 引流規劃不如 Porter,EIP 的下一跳節點必須是 BGP 對等體(鄰居)。

配置中心

Kubernetes 的 ConfigMap 和 Secret 在一定程度上解決了配置的問題,我們可以很輕鬆的使用它們進行更改配置而不需要重新生成映象,但我們在使用的過程中還是會遇到一些痛點:

1. 不支援版本控制

Deployment 和 StatefulSet 都有版本控制,我們可以使用 rollout 把應用回滾到老的版本,但是 ConfigMap/Secret 卻沒有版本控制,這會產生一個問題就是當我們的Deployment  要進行回滾時,使用的 ConfigMap 還是最新的,我們必須把 ConfigMap 改回上一個 Deployment 版本所使用的 ConfigMap 內容,再進行回滾,但是這樣的操作是很危險的,假設改完 ConfigMap 後有個 Pod 出了問題自動重啟了,又或者 ConfigMap 以檔案形式掛載到 Pod 中,都會造成程式讀取到了錯誤的配置。一個折中的做法是每個版本都關聯一個單獨的 ConfigMap。

2. 不太適合管理業務配置

一個應用有時候會需要加很多業務配置,在維護大量業務配置時,我們可能需要搜尋關鍵字、檢視某個 key 的備註、對 value 的格式進行校驗、批量更新配置等等,但是如果使用了ConfigMap ,我們就不得不再做一些工具來滿足這些需求,而這些需求對於配置的維護效率是有非常大的影響。

3. 熱更新

我們知道如果 ConfigMap 是以檔案形式進行掛載,那麼當修改了 ConfigMap 的值後,過一會所有的 Pod 裡相應的檔案都會變更,可是如果是以環境變數的方式掛載卻不會更新。為了讓我們的程式支援熱更新,我們需要把 ConfigMap 都掛載成檔案,而當配置有很多時麻煩就來了,如果 Key 和檔案是一對一掛載,那麼 Pod 裡會有很多小檔案;如果所有 Key 都放到一個檔案裡,那麼維護配置就成了噩夢。

4. 配置大小限制

ConfigMap 本身沒有大小限制,但是 etcd 有,預設情況下一個 ConfigMap 限制為 1MB,我們估算了下,有個別應用的配置加起來真有可能突破這個限制,為了繞過這個大小限制,目前也只有切割成多個 ConfigMap 的方法了。

為了解決這些痛點,我們綜合考慮了很多方案,最終決定還是使用一套開源的配置中心作為配置的源,先通過Jenkins 把配置的源轉換成 ConfigMap,以檔案形式掛載到 Pod 中進行釋出,這樣以上的問題都可以迎刃而解。

我們選擇了攜程的 Apollo( github.com/ctripcorp/apollo )作為配置中心 ,其在使用者體驗方面還是比較出色的,能滿足我們日常維護配置的需求。

微服務

在遷移微服務到 k8s 叢集的時候基本都會遇到一個問題,服務註冊的時候會註冊成 Pod IP,在叢集內的話沒有問題,在叢集外的服務可就訪問不到了。

我們首先考慮了是否要將叢集外部與 Pod IP 打通,因為這樣不需要修改任何程式碼就能很平滑的把服務遷移過來,但弊端是這個一旦放開,未來是很難收回來的,並且叢集內部的 IP 全部可訪問的話,等於破壞了 k8s 的網路設計,思考再三覺得這不是一個好的方法。

我們最後選擇了結合叢集暴露的方式,把一個微服務對應的 Service 設定成 LoadBalancer,這樣得到的一個 EIP 作為服務註冊後的 IP,手動註冊的服務只需要加上這個 IP 即可,如果是自動註冊的話就需要修改相關的程式碼。

這個方案有個小小的問題,因為一個微服務會有多個 Pod,而在遷移的灰度過程中,外部叢集也有這個微服務的例項在跑,這時候服務呼叫的權重會不均衡,因為叢集內的微服務只有一個 IP,通常會被看作是一個例項。因此如果你的業務對負載均衡比較敏感,那你需要修改這裡的邏輯。

呼叫鏈監控

我們一直使用的是點評的 CAT( github.com/dianping/cat )作為我們的呼叫鏈監控,但是要把 CAT 部署到 k8s 上比較困難,官方也無相關文件支援。總結部署 CAT 的難點主要有以下幾個:

l    CAT 每個例項都是有狀態的,並且根據功能不同,相應的配置也會不同,因此每個例項的配置是需要不一樣的,不能簡單的掛載 ConfigMap 來解決

l    CAT 每個例項需要繫結一個 IP 給客戶端使用,不能使用 Service 做負載均衡。

l    CAT 每個例項必須事先知道所有例項的 IP 地址

l    CAT 每個例項會在程式碼層面獲取自己的 IP,這時候獲取到的是可變的 Pod IP,與繫結的 IP 不一致,這就會導致叢集內部通訊出問題

為了把 CAT 部署成一個 StatefulSet 並且支援擴容,我們參考了 Kafka 的 Helm 部署方式,做了以下的工作:

l    我們為每個 Pod 建立了一個 Service,並且啟用 LoadBalancer 模式綁定了 IP,使每個 Pod 都會有一個獨立的對外 IP 地址,稱它為 EIP;

l    我們把所有例項的資訊都儲存在配置中心,並且為每個例項生成了不同的配置,比如有些例項是跑 Job,有些例項是跑監控的;

l    我們會預先規劃好 EIP,並把這些 EIP 列表通過動態生成配置的方式傳給每個例項;

l    最後我們給每個例項裡塞了一個特殊的檔案,這個檔案裡存的是當前這個例項所繫結的 EIP 地址。接著我們修改了 CAT 的程式碼,把所有獲取本地 IP 地址的程式碼改成了讀取這個特定檔案裡的 IP 地址,以此欺騙每個例項認為這個 EIP 就是它自己的本地 IP。

擴容很簡單,只需要在配置中心裡新增一條例項資訊,重新部署即可。

CI/CD

由於 KubeSphere 平臺集成了 Jenkins,因此我們基於 Jenkins 做了很多 CI/CD 的工作。

釋出流程

起初我們為每個應用都寫了一個 Jenkinsfile,裡面的邏輯有拉取程式碼、編譯應用、上傳映象到倉庫和釋出到 k8s 叢集等。接著我為了結合現有的釋出流程,通過 Jenkins 的動態引數實現了完全釋出、製作映象、釋出配置、上線應用、回滾應用這樣五種流程。

處理配置

由於前面提到了 ConfigMap 不支援版本控制,因此配置中心拉取配置生成 ConfigMap 的事情就由 Jenkins 來實現了。我們會在 ConfigMap 名稱後加上當前應用的版本號,將該版本的 ConfigMap 關聯到 Deployment 中。這樣在執行回滾應用時 ConfigMap 也可以一起回滾。同時 ConfigMap 的清理工作也可以在這裡完成。

複用程式碼

隨著應用的增多,Jenkinsfile 也越來越多,如果要修改一個部署邏輯將會修改全部的 Jenkinsfile,這顯然是不可接受的,於是我們開始優化 Jenkinsfile。

首先我們為不同型別的應用建立了不同的 yaml 模板,並用模板變數替換了裡面的引數。接著我們使用了 Jenkins Shared Library 來編寫通用的 CI/CD 邏輯,而 Jenkinsfile 裡只需要填寫需要執行什麼邏輯和相應的引數即可。這樣當一個邏輯需要變更時,我們直接修改通用庫裡的程式碼就全部生效了。

資料落地

隨著越來越多的應用接入到容器釋出中,不可避免的要對這些應用的釋出及部署上線的釋出效率、失敗率、釋出次數等指標進行分析;其次我們當前的流程雖然實現了 CI/CD 的流程程式碼複用,但是很多引數還是要去改對應應用的 Jenkinsfile 進行調整,這也很不方便。於是我們決定將所有應用的基本資訊、釋出資訊、版本資訊、編譯資訊等資料存放在指定的資料庫中,然後提供相關的 API,Jenkinsfile 可以直接呼叫對應的釋出介面獲取應用的相關釋出資訊等;這樣後期不管是要對這些釋出資料分析也好,還是要檢視或者改變應用的基本資訊、釋出資訊、編譯資訊等都可以遊刃有餘;甚至我們還可以依據這些介面打造我們自己的應用管理介面,實現從研發到構建到上線的一體化操作。

叢集穩定性

我們在構建我們的測試環境的時候,由於伺服器資源比較匱乏,我們使用了線上過保的機器作為我們的測試環境節點。在很長一段時間裡,伺服器不停的宕機,起初我們以為是硬體老化引起的,因為在主機告警螢幕看到了硬體出錯資訊。直到後來我們生產環境很新的伺服器也出現了頻繁宕機的問題,我們就開始重視了起來,並且嘗試去分析了原因。

後來我們把 CentOS 7 的核心版本升級到最新以後就再也沒發生過宕機了。所以說核心的版本與 k8s 和 Docker 的穩定性是有很大的關係。同樣把 k8s 和 Docker 升級到一個穩定的版本也是很有必要的。

未來展望

我們目前對未來的規劃是這樣的:

l    支援多叢集部署

l    支援容器除錯

l    微服務與 istio 的結合

l    應用管理介面

編後:每一家公司都有自己經歷的階段,如何可拆解、因地制宜解決問題就考驗選擇。首先是活在當下,同時還要考慮一下未來的一段時期。採用開源軟體可以大大享受社群紅利,但在做整合的過程中不得不面對各自適配、取捨。採用更主流的、統一的技術棧可能會在簡約方面獲益。為作者的探索點贊!

另外,面對這個case,是  .NET Core 救了 .NET 專案呢, 還是初期採用  .NET 的坑呢,已經說不清楚了。噹噹、京東、蘑菇街都經歷了 .NET 或者php轉java.....