文|八幡、朵拉
杭州塗鴉智能技術專家\
主要研究微服務與可觀測、消息引擎、任務調度、數據層中間件等領域。
本文 5389字 閲讀 15分鐘
當前塗鴉通過 Koupleless 的靜態合併部署能力,很好地解決了資源浪費問題。為了進一步提升研發效率,塗鴉與 Koupleless 正在開發動態合併部署能力,為公有云大規模合併部署打下基礎,也為未來更長遠的 Serverless 能力提供基座。
<p align=center>Koupleless 在塗鴉智能的落地效果探討</p>
背景
塗鴉智能是全球領先的雲平台服務提供商,致力於構建智慧解決方案的開發者生態,賦能萬物智能。基於全球公有云,塗鴉開發者平台實現了智慧場景和智能設備的互聯互通,承載着每日數以億計的設備請求交互;擁有億級海量數據併發處理能力,為用户提供高穩定性的不間斷計算服務。
塗鴉智能不僅服務於公有云用户,同時也為大客户提供混合雲及私有云解決方案,滿足不同層次客户的需求。因此,塗鴉需要同時關注公有云和私有云服務中潛在的問題。
公有云用户基數大、設備多、核心業務服務實例多,但也符合二八原則——20%的服務承擔了 80%的流量。還有大量的配套周邊管理服務及正處於發展中的業務等,雖然服務實例數量少,但為了保障業務的穩定性及可用性,至少得部署 2 個實例。
私有云在服務剛落地時,通常規模較小,隨着業務的逐步發展,接入的用户數和設備數才會隨之上漲。如果一開始就生搬硬套公有云的交付模式,將上百個服務部署在私有云客户環境中,基礎設施成本佔比太大,不僅造成了硬件資源成本浪費,同時也提升了運維複雜度。
綜合考慮公有云部分服務和私有云前期服務存在的硬件資源利用率低、彈性擴容慢等問題,塗鴉開始着手調研解決方案。
方案調研
首先看下內存佔用情況:
內存佔用分析
塗鴉絕大部分業務應用都是採用 Java 語言實現的,基於微服務架構,部署在 K8S 上。來看下內存佔用情況:
- 每個 POD 都需要啓動精簡的 Linux 系統,約幾十兆初始內存;
- K8S 節點一般會附帶一些 POD 監控、日誌收集 Agent,約幾十兆初始內存;
- Java Agent 之類的字節碼增強消耗的初始內存;
- JVM 類庫,Spring、Netty、Dubbo 等框架,內嵌 WEB 容器,佔用的 Metaspace 和堆棧空間;
- 業務應用代碼啓動佔用的 Metaspace 和堆棧空間;
- 業務流量處理產生的堆棧空間。
從上面分析可以看出,除了業務應用代碼本身和業務流量處理之外,其他的內存佔用當然是越少越好。一個簡單的控制枱應用內存佔用如下圖所示:
在微服務架構的視角下,Java 應用不但沒有百多兆的 JRE 和框架之類的基礎內存需要問題,更重要的是,單個微服務也不再需要再數十 GB 的內存。有了高可用的服務集羣,也無須追求單個服務 7×24 小時不間斷運行,在一天內隨着業務流量的波動、高峯和低谷,服務隨時可以進行彈性擴縮容。
但相應地,Java 的啓動時間相對較長、需要預熱才能達到最高性能等特點就顯得相悖於這樣的應用場景。在無服務架構中,矛盾則可能會更加突出,比起服務,一個函數的規模通常會更小,執行時間會更短。在這樣的場景下,我們看下有哪些方案可以降本增效:
⭕️ 擱淺方案:Native Image
⭕️無法支持全場景、Spring 支持不足、異常難預見、問題難排查。
提前編譯 (Ahead of Time Compilation,AOT) 可以減少即時編譯帶來的預熱時間,減少 Java 應用長期給用户帶來的“第一次運行慢”的不良體驗,讓用户能放心地進行很多全程序的分析行為,使用更大的優化措施。而隨着 Graal VM 技術的成熟,它能顯著降低內存佔用及啓動時間。由於 HotSpot 本身就會有一定的內存消耗 (通常約幾十 MB) ,根據 Oracle 官方給出的測試數據,運行在 Substrate VM 上的小規模應用,其內存佔用和啓動時間與運行在 HotSpot 相比有了 5 倍到 50 倍的下降。
但是提前編譯的壞處也很明顯,它破壞了 Java“一次編寫,到處運行”的承諾,必須為每個不同的硬件、操作系統去編譯對應的發行包。它也顯著降低了 Java 鏈接過程的動態性,要求加載的代碼必須在編譯期就是全部已知的,而不能再是運行期才確定,大多數運行期對字節碼的生成和修改操作也不再行得通。特別是在整個 Java 的生態系統中,數量龐大的第三方庫要一一進行適配。隨着 Graal VM 團隊與來自 Pivotal 的 Spring 團隊的緊密合作,解決了 Spring 全家桶在 Graal VM 上的運行適配問題。
為此,我們進行了 JDK17 升級,並將 SpringBoot 升至 3.x 版本,Dubbo 升至 3.x 版本。在驗證過程中出現了許多組件的兼容性問題,如 Apollo、Guava、Jedis、MyBatis 等,也進行了逐一解決。
雖然該方案在簡單的內部應用上驗證通過,並上線試運行了一段時間,取得了一定的效果。但業務應用使用的第三方庫數量眾多,達到數百個,在如 GroovyShell、BouncyCastle、Agent 等場景上還無法很好地支持。同時 Spring 的支持不足,如 Spring XML Bean 的構造參數或 properties 配置出現 TypedStringValue 類型無法識別,Spring-AOT 不支持 setter-inject 方式的循環依賴等,導致需要對應用進行較大調整和修改,另外會出現一些無法預見的異常,且出現問題時不方便定位排查。最後塗鴉還是決定暫時擱淺該方案。
✅ 使用中:Koupleless
Koupleless 是一種模塊化的 Serverless 技術解決方案,它能讓普通應用以比較低的代價演進為 Serverless 研發模式,讓代碼與資源解耦,輕鬆獨立維護,與此同時支持秒級構建部署、合併部署、動態伸縮等能力為用户提供極致的研發運維體驗,最終幫助企業實現降本增效。
不同模塊支持完全類隔離加載,對於應用開發來説相對透明,當然 Koupleless 也提供了更多插件機制,還有進程間跨 ClassLoader 的 JVM 調用能力。主要有如下優勢:
- 類隔離: 通過對業務應用的類隔離加載,原有業務系統幾乎無侵入支持合併部署;
- 插件機制: 通過插件機制解決依賴衝突問題;
- 基座和模塊: 可以將中間件和基礎框架下沉到基座,框架與中間件升級維護成本降低。通過進程內 JVM 調用替代遠程調用,節省網絡 IO 和序列化反序列化成本,提升性能和穩定性。見下圖:
- 靜態合併部署: 方便快速驗證,簡化部署,適合私有云環境;
- 動態合併部署: 支持模塊的熱更新,提高發布效率,降低啓動時間,適合公有云環境。
就內存節省效率方面來説,可以節省 POD 容器、Tomcat 容器、公共類庫加載 Metaspace 等內存資源,主要是節省啓動內存開銷。對於低流量業務,幾個應用合併部署到一個,資源利用更充分:10 個低流量 1C2G 應用,合併後僅需能一個 2C4G 就能搞定;單個應用突發的高流量可以有更大的水位池,穩定性也會提升。而對於高流量業務,雖然在內存方面優勢不大,但可以實現極速彈性,更有利於 Serverless 架構。
❌ 其他棄選方案
- 代碼合併: 簡單粗暴,降本優勢明顯,但同時也降低了開發和運維效率,不利於維護和業務快速發展
- 基於代碼組織的模塊化: 服務打成 jar 包引入,但沒有 ClassLoader 隔離,容易造成類庫依賴衝突,beanName 衝突等
- 基於 Tomcat 的多 WAR 包部署: 業務系統無任何侵入,也能一定程度上節省多個應用的 POD 和應用內置 Tomcat 內存。但脱離了主流的 DevOps 體系,需要針對這種部署方式提供額外支持,同時能節省的內存也非常有限。
綜合考慮,塗鴉決定使用 Koupleless 進行改造升級。
改造升級
Koupleless 是一種多應用的架構,而傳統的中間件可能只考慮了一個應用的場景,故在一些行為上無法兼容多應用共存的行為,會發生共享變量污染、ClassLoader 加載異常、Class 判斷不符合預期等問題。由此,在使用 Koupleless 中間件時,我們需要對一些潛在的問題做補丁,覆蓋掉原有中間件的實現,使開源的中間件和自研的組件也能兼容多應用的模式,涉及到以下的使用方式,可能需要多模塊化適配改造:
適配點
系統變量被共享
在使用的時候,需要考慮到全局共享的情況下是否會與別的模塊衝突,包括但不限於:Appid 、環境變量、System 配置等。
靜態變量、靜態單例、靜態緩存被共享
正常情況下公共代碼都是由基座進行加載,而基座的類加載器是唯一的,所以不同的模塊對靜態變量的操作都是施加在同一個對象上的,解決方案如下:
- 將公共包從基座引用改成每個應用單獨引入;
- 將靜態變量調整為按 ClassLoader 進行緩存,每次操作只對當前線程的 ClassLoader 對應的對象進行操作。
類找不到異常
一般存在於通過基座加載相應的類或者靜態調用的情況下。由於基座類加載器無法訪問到模塊的類加載器,所以在公共代碼中加載類時優先使用 Thread.currentThread().getContextClassLoader()
- XX Class Not Found
- XX Class No Defined
- ServiceLoader 異常
日誌適配
- logback 通過 condition 進行適配
'''
<if condition='property("sofa.ark.embed.enable").contains("true")'> <then> <springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="NO_APP_CONFIG"/> <property name="BASE_PATH" value="${user.home}/logs/${APP_NAME}"/> </then>
<else> <springProperty scope="context" name="loggingRoot" source="logging.file.path"/> <property name="BASE_PATH" value="${loggingRoot}/"/> </else>
</if>
'''
- log4j 通過 properties 進行適配
'''
<Property name="loggingRoot">${sys:user.home}/logs/${spring:tuya.sofa.ark.app:-}</Property>
'''
- log4j2 和 logback 日誌在同一個基座中混用
一般應用使用 logback,但是存在某些應用使用 log4j2 的情況,目前 Koupleless 不支持兩種應用放在一個基座中,可以考慮在 Koupleless 上調整日誌系統的判斷,或在 Koupleless 上調整由模塊決定某些 plugin 是否被加載。
健康檢查
提供基座應用的健康檢查實現:
- 靜態部署模式: 合併部署的所有應用的狀態都健康,健康檢查才會通過。
- 動態部署模式: 提供配置,讓用户自行決定模塊熱部署結果是否影響應用整體健康狀態 (默認配置為:不影響整體應用原本的健康狀態)
Web 容器共享
- 多 Host 模式:使用多個 port 進行區分。該模式的問題首先在於重複創建了 Tomcat 相關的資源,造成資源的浪費;其次是每個 Biz 有自己的端口,不利於整個 Ark 包應用整體對外提供服務。
- 單 Host 模式:Koupleless 提供了類似獨立 Tomcat 部署多 webapp 的方式。所有 Biz 共用同一個 Server 及 Host,每個 Biz 只創建自己的 Context,通過 Context 中的 contextPath 將自身接口與其它 Biz 接口做區分。
考慮到資源共享及整體性,我們採用了單 Host 模式。
實踐心得
- 將下沉的組件配置抽離到父 POM 中,方便統一管控;
- 基座的 AutoConfig 被模塊的依賴觸發,但是初始化的時候報錯,基座需要 exclude 相應的 AutoConfig。建議使用 Spring Boot 框架的自動裝配功能時將相應組件下沉到基座;
- ContextPath:可以通過參數動態配置,方便應用運行在單模塊獨佔基座和多模塊合併部署場景中;
- 模塊需要開啓 https 協議時:用於支持自定義證書場景,可以在模塊獲取到 web server 並主動添加一個新的 Connector。
DevOps 改造
測試應用驗證通過後,塗鴉團隊開始着手進行發佈系統的改造。由於靜態合併部署比較適合部署相對穩定不頻繁的場景 (如私有云) ,而公有云更適合動態合併部署模式。但為了給私有云交付打前戰,塗鴉一期先在公有云上實現靜態合併部署,快速試點驗證。而靜態合併部署是需要所有模塊和基座一起發佈的,為了儘可能不影響開發同學效率,我們同時支持單模塊獨佔基座和多模塊合併部署兩種發佈模式,方便在開發、日常、預發、線上等環境進行實地試運行,完整驗證業務鏈路。
首先按業務域及流量情況將應用拆分為基座或模塊:
然後發佈系統根據基座與模塊的映射關係,進行打包發佈。目前我們是通過 K8S 部署服務的,源代碼經過編譯打包成 Docker 鏡像再進行發佈,為複用原有流程,快速上線,模塊和基座都打包為鏡像。模塊由模塊負責人進行打包,基座由業務域負責人打包,並根據迭代節奏進行定期發佈。
InitContainert 先啓動模塊鏡像,再將模塊的 ark 包拷貝到/home/docker/module/biz目錄下:
通過-Dcom.alipay.sofa.ark.static.biz.dir 參數實現合併部署:
日誌目錄
日誌路徑由原來的 home/docker/logs,在合併模塊部署後調整為 home/docker/logs/appname/,日誌收集模板按此進行調整。std 輸出的日誌會打印在基座應用的 std 中,比如異常類的 printStackTrace,所以儘量避免使用 stdout 和 stderr。
域名切換
由於應用合併,原來的域名需要切換到新的域名上來。由單一應用過渡到合併部署模式的發佈過程中需要注意對等的流量切換。
有兩種域名切換方式:
1、調用方切換到新域名。 ✅優點:簡單,不需要路由。❌缺點:需要調用方配合調整,應用模塊與基座關係調整時同樣需要再次調整。
2、域名路由。 調用方還是訪問原域名,通過內部網關路由到新域名。✅優點:靈活。❌缺點:多一次調用。
我們最終採用路由的方式來進行域名切換。
上線實施
目前一期按業務域劃分為 18 個基座,60 多個模塊在一些數據中心** (如中國區、美西區等) **進行合併部署後穩定運行。
如某基座合併部署了 8 個模塊:
合併後,每個模塊由原來的 1C2G 變為 2C4G,節省資源 6C12G。
8 個模塊非合併部署前的啓動時間如下圖前 8 條記錄顯示,在 1-3 分鐘之間。
而靜態合併部署後的基座啓動時間見下圖最後一條記錄,約 2 分 45 秒。8 個模塊分別交付,每個 1-3 分鐘,統一成 2 分 45 秒一次性完成交付,大大提高了整體交付效率 。
總體來説,在內存方面,對於應用初始啓動內存佔總消耗內存比例較高、低流量的業務場景,應用合併部署的內存節省收益越大。60 個模塊共節省 70G 內存。線上 POD 數約有 6000 多個,預計可節省 6000 多 G 內存。相應地在 CPU 資源利用率方面,大部分業務應用都是 IO 密集型,而合併部署能將 CPU 較為充足地利用起來,線上 Pod 合併部署後能節省 3000 核。\
在調用性能和網絡 IO 優化上,大流量的業務場景從遠程調用變成本地調用,網絡 IO、序列化反序列的 CPU 開銷以及調用性能、穩定性上都能得到大幅度提升。
後續演進
當前我們完成的是靜態合併部署能力,很好地解決了資源浪費問題。為了進一步提升研發效率,我們正在開發動態合併部署能力,為公有云大規模合併部署打下基礎,也為未來更長遠的 Serverless 能力提供基座。
為了打造對開發者更友好的 Serverless 能力和平台:
1、我們需要增強模塊熱卸載能力。
2、進一步的模塊瘦身,將更多的通用組件下沉到基座中,減少公共類庫和框架加載和初始化運行開銷。不但節省內存,還能加速子模塊的啓動速度,如嘗試複用基座數據源和基座攔截器。
3、彈性伸縮,目前依賴應用的 CPU、內存等指標對進程進行彈性伸縮,而合併部署後,可以通過預熱基座的方式,讓模塊的擴縮容速度更快。另外由於模塊級別資源的監控指標較難獲取,也可以考慮 QPS 等其他維度的指標。
在整個調研、升級改造及實施的過程中,社區同學多次和我們進行線上和線下交流討論,提供了許多螞蟻內部和其他外部企業的最佳實踐, 給予了我們很多幫助與指導,在共同的努力下最終成功落地 Koupleless!
希望後續社區在模塊應用的卸載穩定性上有進一步的優秀表現,為我們的動態發佈打下堅實的基礎!也歡迎更多同學加入社區,一起參與共建!**
Koupleless Star 一下✨:
https://github.com/koupleless/koupleless