多云架構(gòu)落地設(shè)計(jì)和實(shí)施方案
“不要把雞蛋放在同一個(gè)籃子里”是一條知名的商業(yè)準(zhǔn)則,在云平臺(tái)選擇上,很多公司也遵循這樣的準(zhǔn)則。基于多云平臺(tái)構(gòu)筑“業(yè)務(wù)中臺(tái)”并不是一件簡(jiǎn)單的事情,需要構(gòu)建一種快速繼承、可持續(xù)迭代的路徑,幫助整體方案落地。本文以實(shí)際項(xiàng)目案例為例,分析項(xiàng)目的架構(gòu)設(shè)計(jì)、實(shí)施步驟,以及多云架構(gòu)面臨的挑戰(zhàn)和機(jī)遇。
總體思路
不同云廠商提供的云服務(wù)不盡相同,相同的云服務(wù)在功能、性能上也會(huì)有或多或少的差異。越是深度使用某個(gè)云廠商的云服務(wù),越是難于遷移到其他云廠商。選擇自己構(gòu)建云服務(wù),則技術(shù)門檻,維護(hù)成本很高。確定多云架構(gòu)以后,首先需要在技術(shù)棧的選型上做好折中。一個(gè)基本的原則是通過(guò)業(yè)務(wù)架構(gòu)的靈活性,去適配不同的云廠商,盡可能的使用云廠商提供的優(yōu)秀特性,提升運(yùn)行于該云平臺(tái)的業(yè)務(wù)系統(tǒng)的可靠性,提升整體業(yè)務(wù)的競(jìng)爭(zhēng)力。
上面的思路和一些客戶常見(jiàn)的思路有顯著差別。有些客戶選擇采用開(kāi)源軟件,搭建自己的 PaaS 平臺(tái);有些客戶則完全采用云廠商的技術(shù)棧,開(kāi)發(fā)兩套業(yè)務(wù)系統(tǒng)。這兩種方式是兩個(gè)極端,前者開(kāi)發(fā)和運(yùn)維難度高,往往由于技術(shù)風(fēng)險(xiǎn)評(píng)估不足,項(xiàng)目無(wú)法如期交付,或者產(chǎn)品競(jìng)爭(zhēng)力太弱,沒(méi)有云廠商提供的服務(wù)好。后者則需要維護(hù)兩套系統(tǒng),代碼重復(fù)度高,還會(huì)被云廠商完全綁定,失去談判的籌碼,業(yè)務(wù)發(fā)展靈活性降低。還有些客戶期望云廠商提供足夠兼容性的框架支持,在不改造現(xiàn)有業(yè)務(wù)系統(tǒng)的邏輯的情況下實(shí)現(xiàn)多云部署,云廠商這方面的努力通常由于客戶系統(tǒng)的復(fù)雜性和多樣性得不到落地。
開(kāi)發(fā)框架選擇和架構(gòu)設(shè)計(jì)
開(kāi)發(fā)框架設(shè)計(jì)是多云架構(gòu)的核心,也是抽象程度最高的部分。華為云主推 ServiceComb 作為微服務(wù)框架,阿里云主推 HSF、Dubbo 作為微服務(wù)框架,其他國(guó)外的云廠商,多數(shù)選擇 Spring Cloud 作為微服務(wù)框架,另外還有其他不同的語(yǔ)言和框架選擇。雖然 mesher 等技術(shù)為多語(yǔ)言協(xié)同工作提供了良好的支持,但是為了最大限度的利用框架特性,幫助快速構(gòu)建穩(wěn)定可靠的業(yè)務(wù)系統(tǒng),選擇一個(gè)微服務(wù)開(kāi)發(fā)框架仍然是必不可少的。
基于總體思路,多云架構(gòu)期望在華為云上使用 ServiceComb 運(yùn)行時(shí),在阿里云上使用 HSF 運(yùn)行時(shí),并且支持 Spring Cloud 運(yùn)行時(shí)。完成這個(gè)目標(biāo),首先需要對(duì)微服務(wù)運(yùn)行框架的運(yùn)行時(shí)和主要組成部分有所了解。對(duì)于多數(shù)中臺(tái)系統(tǒng),對(duì)于框架運(yùn)行時(shí)的依賴,一般都是 RPC 框架,以及基于 RPC 框架做的服務(wù)治理能力,包括服務(wù)注冊(cè)發(fā)現(xiàn)、熔斷容錯(cuò)、限流等機(jī)制。將業(yè)務(wù)邏輯核心代碼,與微服務(wù)框架能力進(jìn)行解耦,是設(shè)計(jì)的第一步。
上面的圖形展現(xiàn)了基本的邏輯架構(gòu)。
業(yè)務(wù)核心:技術(shù)選型上使用 Spring、Spring Boot。ServiceComb、HSF 和 Spring Cloud 等微服務(wù)框架的技術(shù)底座,都可以基于 Spring、 Spring Boot 技術(shù)棧來(lái)構(gòu)建。
在邏輯架構(gòu)下,需要將微服務(wù)代碼進(jìn)行分層,包含下面三個(gè)主要目錄:
- microservice-api:定義微服務(wù)的接口。該目錄包含接口定義(interface)、數(shù)據(jù)結(jié)構(gòu)定義(models)。為了支持不同的微服務(wù)框架,對(duì)于接口定義和數(shù)據(jù)結(jié)構(gòu)定義會(huì)有一定的要求。
- microservice-service:業(yè)務(wù)邏輯實(shí)現(xiàn)代碼。
- microservice-endpoint-servicecomb:發(fā)布為 ServiceComb 的微服務(wù)項(xiàng)目。
- microservice-endpoint-hsf:發(fā)布為 HSF 的微服務(wù)項(xiàng)目。
- microservice-endpoint-springcloud:發(fā)布為 Spring Cloud 的微服務(wù)項(xiàng)目。
這個(gè)代碼分層實(shí)施的核心關(guān)鍵是 api 設(shè)計(jì),以及業(yè)務(wù)邏輯實(shí)現(xiàn)和服務(wù)發(fā)布解耦。api 設(shè)計(jì)需要滿足不同的微服務(wù)框架的設(shè)計(jì)要求。這里涉及到 RPC 編解碼的基礎(chǔ)。RPC 編解碼通常分為語(yǔ)言無(wú)關(guān)(跨平臺(tái))和語(yǔ)言相關(guān)(不跨平臺(tái))。比如 HSF、Dubbo 的缺省編解碼是語(yǔ)言有關(guān)的,只能夠支持 JAVA 程序之間的通信,ServiceComb 缺省采用 Jackson 編解碼,或者 protobuffer 編解碼,這兩種方式都基于 Open API 2.0 進(jìn)行定義,可以做到語(yǔ)言無(wú)關(guān),Spring Cloud 則相對(duì)復(fù)雜一些,它是一種混合型的編碼格式,可以通過(guò)靈活的序列化定制,滿足多樣性需要,也可以嚴(yán)格遵循 Jackson 和 Open API 標(biāo)準(zhǔn)。通常語(yǔ)言無(wú)關(guān)的編解碼可以完全包含語(yǔ)言無(wú)關(guān)的編解碼要求,因此 api 定義的時(shí)候,需要做到語(yǔ)言無(wú)關(guān),才能夠保證 api 能夠在不同的微服務(wù)開(kāi)發(fā)框架下得到最好的實(shí)現(xiàn)。基于本文是描述工程實(shí)踐,不詳細(xì)探討關(guān)于跨平臺(tái)設(shè)計(jì)的原理,下面列出一些接口設(shè)計(jì)的準(zhǔn)則:
- 使用簡(jiǎn)單的類型(比如 Integer, String, Boolean 等)定義參數(shù)或者返回值。或者使用包含簡(jiǎn)單類型的,符合 Java Bean 規(guī)范的 POJO Bean 定義參數(shù)或者返回值。
- 盡可能不使用 interface, abstract class, 存在多個(gè)實(shí)現(xiàn)的基類,模板類作為參數(shù)或者返回值。
- 不使用運(yùn)行環(huán)境強(qiáng)相關(guān)的對(duì)象作為接口參數(shù)或者返回值。比如 HttpServletRequest,RpcContext,InvocationContext,ResonseEntity 等。
上面的原則從使用者的視角來(lái)看,是非常容易理解的。接口語(yǔ)義清晰,沒(méi)有歧義,直接通過(guò)接口定義就能夠理解接口的參數(shù)個(gè)數(shù)以及如何傳遞,不需要提供額外的文檔或者查看源代碼。有利于通過(guò)接口定義生成文檔、swagger 定義。
在實(shí)際項(xiàng)目中,開(kāi)發(fā)者需要理解 microservice-api 是微服務(wù)之間的接口定義,接口設(shè)計(jì)需要考慮數(shù)據(jù)的序列化和反序列化。這個(gè)不同于內(nèi)部接口設(shè)計(jì)。為了降低業(yè)務(wù)實(shí)現(xiàn)邏輯的重復(fù)度,增強(qiáng)內(nèi)聚性,內(nèi)部接口設(shè)計(jì)會(huì)更多的使用抽象、繼承進(jìn)行邏輯封裝。內(nèi)部接口的數(shù)據(jù)結(jié)構(gòu),還會(huì)包含一些額外的控制邏輯。比如數(shù)據(jù)庫(kù)訪問(wèn)層的數(shù)據(jù)結(jié)構(gòu),提供懶加載等機(jī)制,當(dāng)訪問(wèn)到 getter 方法的時(shí)候,實(shí)際上調(diào)用的是代理對(duì)象的 getter 方法。因此,需要有一些數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換邏輯,將內(nèi)部的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為外部接口的數(shù)據(jù)結(jié)構(gòu),以保持服務(wù)之間接口和內(nèi)部接口的界限清晰,防止將內(nèi)部數(shù)據(jù)結(jié)構(gòu)作為參數(shù)或者返回值,導(dǎo)致內(nèi)部信息泄露,造成不可預(yù)期的處理結(jié)果。
api 示例
- interface LoginService {
- SessionInfo login(String username, String password);
- }
- public class SessionInfo {
- private String sessionId;
- private String username;
- }
service 示例
- @Service
- @Primary
- public class LoginServiceImpl implements LoginService {
- public SessionInfo login(String username, String password) {
- // do login
- }
- }
ServiceComb Endpoint 示例
服務(wù)端:
- @RpcSchema(schemaId = “LoginServiceEndpoint”)
- public class LoginServiceEndpoint implements LoginService {
- @Autowired
- private LoginService service;
- public SessionInfo login(String username, String password) {
- return service.login(username, password);
- }
- }
客戶端:
- @Bean
- public LoginService getLoginService() {
- return Invoker.createProxy(SERVICE_NAME, "LoginServiceEndpoint", LoginService.class);
- }
或者
- @RpcReference(microserviceName=SERVICE_NAME, schemaId=”LoginServiceEndpoint”)
- private LoginService loginService;
HSF Endpoint 示例
服務(wù)端:
- @HSFProvider(serviceInterface = LoginService.class,serviceVersion = "1.0.0")
- public class LoginServiceEndpoint implements LoginService {
- @Autowired
- private LoginService service;
- public SessionInfo login(String username, String password) {
- return service.login(username, password);
- }
- }
客戶端:
- @HSFConsumer
- private LoginService loginService;
從上面的代碼示例看,Endpoint 層需要做到盡可能邏輯簡(jiǎn)單,實(shí)現(xiàn)邏輯全部交給 Service 層,只包含接口聲明,或者包含必要的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換邏輯。上面的方式演示了 RPC 的接口聲明,還可以加上 REST 標(biāo)簽(Spring MVC,或者 JAX RS),來(lái)支持 REST 接口。
遺留系統(tǒng)的改造策略
大部分公司都會(huì)存在現(xiàn)有系統(tǒng)運(yùn)行于某一個(gè)云上,把現(xiàn)有系統(tǒng)推倒重來(lái),進(jìn)行全新設(shè)計(jì),通常不是一個(gè)好主意。遺留系統(tǒng)的改造需要遵循持續(xù)迭代,繼承性改造的思路。
以阿里云系統(tǒng)改造為華為云系統(tǒng)為例,將原來(lái)基于 HSF 開(kāi)發(fā)的微服務(wù)應(yīng)用改造為基于 ServcieComb 開(kāi)發(fā)的微服務(wù)應(yīng)用。
按照前面的總體思路,首先需要將原來(lái)項(xiàng)目強(qiáng)依賴于 HSF 的代碼部分分離出來(lái),建立下面的目錄結(jié)構(gòu):
- microservice-api:定義微服務(wù)的接口。該目錄包含接口定義(interface)、數(shù)據(jù)結(jié)構(gòu)定義(models)。為了支持不同的微服務(wù)框架,對(duì)于接口定義和數(shù)據(jù)結(jié)構(gòu)定義會(huì)有一定的要求。
- microservice-service:業(yè)務(wù)邏輯實(shí)現(xiàn)代碼。
- microservice-endpoint-hsf:發(fā)布為 HSF 的微服務(wù)項(xiàng)目。
這個(gè)過(guò)程相對(duì)而言是簡(jiǎn)單的,不涉及任何業(yè)務(wù)邏輯的調(diào)整,只是代碼目錄結(jié)構(gòu)的變化和 POM 依賴關(guān)系的調(diào)整。調(diào)整完成后,可以對(duì)現(xiàn)有功能進(jìn)行簡(jiǎn)單自動(dòng)化驗(yàn)證,保證項(xiàng)目自動(dòng)化測(cè)試用例能夠通過(guò)。調(diào)整后的項(xiàng)目功能和遺留系統(tǒng)是一致的,擁有一個(gè)始終無(wú)損的運(yùn)行系統(tǒng),對(duì)于功能比較、問(wèn)題發(fā)現(xiàn)都是非常有幫助的。
項(xiàng)目調(diào)整后,就可以增加華為云的項(xiàng)目:
- microservice-endpoint-servicecomb:發(fā)布為 ServiceComb 的微服務(wù)項(xiàng)目。
這個(gè)項(xiàng)目可以復(fù)制 microservice-endpoint-hsf,將 POM 依賴改為 ServiceComb 的內(nèi)容,并將發(fā)布的接口(Endpoint)調(diào)整為 ServiceComb 的發(fā)布方式。
項(xiàng)目調(diào)整后,就可以一份代碼構(gòu)建出兩個(gè)可執(zhí)行 jar 包,兩個(gè) jar 包分別在華為云和阿里云部署。
這個(gè)過(guò)程的工作量需要通過(guò)原來(lái)代碼本身的復(fù)雜度、api 接口的規(guī)范性、代碼規(guī)模等進(jìn)行評(píng)估。如果原來(lái)代碼結(jié)構(gòu)復(fù)雜,api 定義不規(guī)范,工作量會(huì)顯著增加。對(duì)于良好組織的代碼,這個(gè)過(guò)程則會(huì)非常容易。實(shí)際改造的一些項(xiàng)目,有些一天時(shí)間即可完成,有些則花費(fèi)了一、兩個(gè)月時(shí)間。獲取到產(chǎn)品的業(yè)務(wù)結(jié)構(gòu)、代碼規(guī)模和通過(guò)簡(jiǎn)單的識(shí)別現(xiàn)有代碼的技術(shù)棧和開(kāi)發(fā)方式,能夠幫助有效的評(píng)估工作量。
遺留系統(tǒng)改造的核心工作在于 microservice-api 中接口定義的梳理。由于 HSF 不是一個(gè)跨語(yǔ)言的開(kāi)發(fā)框架,因此在接口定義里面使用的數(shù)據(jù)結(jié)構(gòu) ServiceComb 可能存在不支持的情況。采用一個(gè)支持跨語(yǔ)言的框架的接口定義作為基準(zhǔn)(一般的,可以將接口定義通過(guò) IDL、WSDL 或者 Open API 描述的接口定義,比如 gRPC、WebService、ServiceComb,),其他框架都實(shí)現(xiàn)這個(gè)接口,是微服務(wù)架構(gòu)適配的核心關(guān)鍵。
數(shù)據(jù)庫(kù)適配
業(yè)務(wù)系統(tǒng)都會(huì)使用數(shù)據(jù)庫(kù)。各個(gè)云廠商支持的數(shù)據(jù)庫(kù)形式多樣,有 MySQL、Postgre、Oracle 等,此外還有分布式數(shù)據(jù)庫(kù),以及分庫(kù)分表等特性,數(shù)據(jù)倉(cāng)庫(kù)支持等。雖然在連接池管理、事務(wù)管理、ORM 框架等方面有大量的開(kāi)源開(kāi)發(fā)框架,比如 dbcp、spring transaction、MyBatis 等,它們?nèi)匀粵](méi)法覆蓋所有的應(yīng)用場(chǎng)景。適配多云架構(gòu)對(duì)于業(yè)務(wù)代碼開(kāi)發(fā)有如下建議:
盡可能使用符合 ANSI SQL Standard 的 SQL 語(yǔ)句。比如 MySQL 在大小寫、Column 引用、Limit、Group by 語(yǔ)句等方面都有自己的特殊用法。為了更好的適配多云數(shù)據(jù)庫(kù),應(yīng)該避免使用特殊用法,除非對(duì)于業(yè)務(wù)價(jià)值具備明顯的價(jià)值,而且沒(méi)有對(duì)等的解決方案。
選用一個(gè)被廣泛采用的 ORM 框架。比如 MyBatis、JPA 等。這些框架被廣泛支持,對(duì)于多數(shù)據(jù)庫(kù)支持得到比較充分的驗(yàn)證。在使用數(shù)據(jù)庫(kù)有差異的地方,提供了非常良好的擴(kuò)展機(jī)制。比如使用 MyBatis,可以使用 DatabaseProvider 接口,來(lái)屏蔽語(yǔ)法差異。在使用 JDBC 或者 JDBCTempate 拼接 URL 的地方,則可以通過(guò)接口的不同實(shí)現(xiàn),返回不同的 SQL 語(yǔ)句。
- <if test="_databaseId == 'mysql' ">
- limit #{stratRow},#{rowCount}
- </if>
- <if test="_databaseId == 'postgresql' ">
- limit #{rowCount} offset #{stratRow}
- </if>
分布式數(shù)據(jù)庫(kù)、分庫(kù)分表、分析型數(shù)據(jù)庫(kù)對(duì)于業(yè)務(wù)開(kāi)發(fā)存在不同的限制。比如對(duì)于唯一索引使用的限制、對(duì)于分庫(kù)分表鍵的修改限制等。對(duì)于這些場(chǎng)景,盡可能符合這些限制,調(diào)整業(yè)務(wù)實(shí)現(xiàn)方式,避免為了滿足某個(gè)不重要的場(chǎng)景需要,陷入一些分布式場(chǎng)景無(wú)法解決的問(wèn)題當(dāng)中去,而無(wú)法適配其他云廠商的數(shù)據(jù)庫(kù)。
緩存適配
各個(gè)廠商均支持 Redis 作為緩存,但是在 Redis 發(fā)展路徑上,有不同的分支。一個(gè)分支是 Proxy 集群模式,一個(gè)分支是 Redis 的原生集群方式。Redis 提供的客戶端 API 也存在兩套,一個(gè)是 Jedis,一個(gè)是 JedisCluster,兩套支持的命令集合不盡相同。比如 Proxy 集群模式能夠非常好的支持所有的 Jedis 命令,而 Redis 的原生集群方式只支持 JedisCluster 命令。很多客戶常用的 pipeline 指令,在 Redis 原生集群方式下不支持。
Proxy 集群能夠更好的屏蔽底層服務(wù)的差異,在沒(méi)有特殊需要的情況下,建議用戶使用 Proxy 集群模式,云廠商需要通過(guò) Proxy 集群模式提供對(duì)于 Jedis 不同指令的支持。用戶也可以選擇 Redis 的原生集群,這個(gè)在不同的云產(chǎn)生也都提供了支持,用戶可以在業(yè)務(wù)場(chǎng)景上避免使用原生集群不支持的命令,這樣就可以在多云環(huán)境上部署。
總結(jié)起來(lái),緩存的多云支持的最佳實(shí)踐:及時(shí)升級(jí) Client API 版本,使用比較新的 Client API,并且只使用 JedisCluster 提供的指令集合。
消息中間件適配
相對(duì)于數(shù)據(jù)庫(kù)和緩存,消息中間件的適配的標(biāo)準(zhǔn)性更弱一點(diǎn)。雖然早期 JAVA 提出了 JMS 等標(biāo)準(zhǔn),但是目前主流的消息中間件都沒(méi)有提供 JMS 接口的實(shí)現(xiàn)。阿里的 RocketMQ、華為基于 Kafka 的 DMS 的接口均不一致。而且消息中間件的服務(wù)質(zhì)量并不一樣,比如在消息有序投遞、重復(fù)投遞等方面是通過(guò)消息中間件配置提供的,而不受客戶端接口控制。有些客戶的業(yè)務(wù)邏輯依賴于特定消息中間件的機(jī)制,因此需要對(duì)消息中間件使用的接口、接口行為進(jìn)行抽象,分析其他云廠商的中間件能否提供對(duì)應(yīng)的接口實(shí)現(xiàn)并滿足對(duì)應(yīng)的服務(wù)質(zhì)量要求,有時(shí)候需要業(yè)務(wù)代碼做一些補(bǔ)償,以彌補(bǔ)不同中間件服務(wù)質(zhì)量的差異。
其他中間件適配
其他中間件包括日志服務(wù)器、定時(shí)任務(wù)服務(wù)、對(duì)象存儲(chǔ)服務(wù)等等。適配的總體思路一致,這里不詳細(xì)描述里面的細(xì)節(jié)。
多云架構(gòu)的挑戰(zhàn)和建議
和做協(xié)議標(biāo)準(zhǔn)一樣,多云架構(gòu)除了給客戶帶來(lái)商業(yè)上的靈活性,還會(huì)促進(jìn)業(yè)務(wù)系統(tǒng)軟件架構(gòu)的改善,提升整體的軟件質(zhì)量。因?yàn)槎嘣萍軜?gòu)需要對(duì)使用的技術(shù)點(diǎn)進(jìn)行權(quán)衡,技術(shù)選型上更多采用相對(duì)中立和標(biāo)準(zhǔn)的方案,這些方案被廣泛驗(yàn)證,相對(duì)于使用一些私有特性來(lái)說(shuō),更加穩(wěn)定,同時(shí)可以促進(jìn)項(xiàng)目開(kāi)發(fā)人員之間的技術(shù)交流和能力繼承。在給客戶做多云架構(gòu)落地的實(shí)踐中,通過(guò)架構(gòu)交流,幫助客戶梳理了整體的架構(gòu)演進(jìn)方向,還發(fā)現(xiàn)了很多歷史問(wèn)題,對(duì)于客戶的代碼質(zhì)量提升起到了很大作用。
多云架構(gòu)也面臨特別多的挑戰(zhàn)。首先是短期內(nèi)企業(yè)維護(hù)成本的增加和技術(shù)成本的增加,需要投入專家解決前期的架構(gòu)適配問(wèn)題,為系統(tǒng)持續(xù)演進(jìn)搭好框架。多云系統(tǒng)運(yùn)行,還會(huì)增加業(yè)務(wù)數(shù)據(jù)同步,不同云上系統(tǒng)如何進(jìn)行協(xié)同的問(wèn)題。只有當(dāng)客戶多云系統(tǒng)相對(duì)獨(dú)立,沒(méi)有數(shù)據(jù)共享和業(yè)務(wù)交互的場(chǎng)景才比較簡(jiǎn)單。多云系統(tǒng)還會(huì)影響到客戶的交付效率,不同云的持續(xù)交付方式存在較大的差異,運(yùn)維人員的體驗(yàn)不同,會(huì)降低日常的效率。
因此,多云架構(gòu)更多的是準(zhǔn)備“備胎”,客戶的主要業(yè)務(wù)還是以單一云廠商提供。多云架構(gòu)給客戶對(duì)比不同云廠商的服務(wù)質(zhì)量提供了非常準(zhǔn)確的參考,客戶能夠更加準(zhǔn)確的選擇優(yōu)質(zhì)的供應(yīng)商。
微服務(wù)框架層的抽象是做多云架構(gòu)最難抽象的部分,也是多數(shù)開(kāi)發(fā)者最難把握的一層。ServiceComb 微服務(wù)開(kāi)發(fā)框架作為參考接口規(guī)范,可以非常好的適配到 HSF、Spring Cloud 等框架。在接口定義的時(shí)候,可以采用它作為基線,先測(cè)試 ServiceComb,再測(cè)試 HSF 和 Spring Cloud 等適配,對(duì)于新接口開(kāi)發(fā)、歷史系統(tǒng)遷移都是一個(gè)非常好的中間產(chǎn)品。