GrowingIO 微服務 SaaS 與私有部署運行實踐

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GrowingIO 作爲國內領先的數據運營解決方案供應商,爲上千家企業提供基於用戶行爲的增長解決方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲滿足企業的不同側重與需求,GrowingIO 支持私有化和 SaaS 兩種部署方式。兩種部署方式的內容架構與交互設計不同,但相同的是產品底層邏輯,以及數據中心、用戶庫、產品分析、智能運營等產品線。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些產品線在研發環境中,被設計爲多個相互獨立的微服務。爲避免研發資源浪費,GrowingIO 需要確保同一批微服務可在不同部署模式的環境中均正常運行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"SaaS 與私有部署環境的差別"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要解決這個問題,我們需要找到 SaaS 與私有部署之間的差別。根據我們的經驗,總結了以下幾個主要的差別:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"雲生態的差別:"},{"type":"text","text":"SaaS 產品部署在公有云環境裏,能使用標準的接口管理資源(實例),也可以非常方便地使用公有云的產品生態去架構服務的高可用。在私有部署的環境裏面,資源的訪問方式,雲平臺的產品形態都不太一樣。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"數據隔離級別:"},{"type":"text","text":"SaaS 產品需要有非常完備且嚴格的多租戶數據隔離方案。在私有部署的環境裏面,數據隔離的級別會因爲客戶的需求有細微的區別。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"用戶訪問量:"},{"type":"text","text":"SaaS 需要滿足幾千家客戶業務需求,要支撐這些訪問的可靠性,需要有一個可靠的集羣。在私有部署的環境裏,服務只需要滿足 1 家客戶的業務需求,客戶對資源有一定的預算。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"運維方式不同:"},{"type":"text","text":"SaaS 產品有公司專業的 SRE 負責,他們有非常豐富的故障處理經驗,每個服務也有非常固定的上線流程。在私有環境裏,客戶可能不太熟悉服務的架構,服務升級會相對困難,處理故障不會那麼快速。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對上面的這些主要問題,我們在服務端分成 3 個方向去解決:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"減少服務個數"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"減少中間件依賴"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"插件化個性需求"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通過使用這 3 個方面的改進,我們能帶來的好處是:降低了服務對最小資源的限制,高可用方案的複雜度,運維的困難性以及滿足部分個性化的部署需求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"接下來,聊一聊 GrowingIO 具體是如何做到的:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1,減少服務個數 - 合併微服務獨立進程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"減少服務個數最簡單直接的方式是,將原本在 SaaS 上獨立運行的同構微服務,在私有部署的環境中讓它們合併在一個進程上運行。這種既能獨立又能合併運行的能力,在 Java 領域中已經有成熟的技術:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OSGi - Java 動態模型系統,使用標準化原語,將應用程序以組件的方式自由組合運行。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Servlet 容器技術 - 應用程序使用標準的 Servlet 接口,將自身以 web jar 的方式部署到像 Tomcat, Jetty 等 Servlet 容器中運行。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用上面任何一種方案,對於現有的系統都存在這幾個問題。首先,需要將現有的服務改造以實現 OSGi 或 Servlet 標準。另外,對於不能合併在一起運行的服務,希望能繼續使用 gRPC 的方式進行服務間的通訊,需要找到一種方式能讓它們支持發佈 gRPC 服務。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因此,我們需要設計一個模塊化系統,既能達成讓服務一起運行的目標,又只花少量的時間改造。爲了讓效果更有成效,對這個模塊化系統做了幾點要求:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用相同 HTTP 框架的服務(我們使用的框架有:akka-http, undertow, netty)共同使用一個 HTTP 端口"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所有服務共同使用一個 gRPC 端口"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"運行在同一個 JVM 進程上的服務間通訊由 gRPC 遠程調用切換成堆棧調用"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不同 JVM 之間的服務調用方式保持不變"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下圖所示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d7/d7dc2e0dcaf7064e96dccdebd44c5b1e.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"要實現這個模塊化系統,我們需要想清楚一個問題:服務的本質是什麼?在我看來,服務的本質不是一個 HTTP 或 gRPC 接口,服務的本質是真正的業務處理邏輯。HTTP 或 gRPC 只是對外通訊的一個手段而已。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當清楚服務的本質就是業務邏輯時,模塊化的定義就變得非常清晰。然後,我們構建這個模塊化的運行環境就能使得它們能在一起運行。要構建這個模塊化運行環境不必要知道模塊裏具體實現了什麼邏輯,只需要知道這個模塊初始化需要什麼,依賴了哪些服務,暴露的服務有哪些。根據這些具體的需求,能很清楚地用代碼表示:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"trait BoxerModule {\n // 打開調試模式\n def debug(): Unit\n\n def injector: Injector\n\n // 模塊是否暴露 HTTP 服務,以及使用的 HTTP 框架\n def httpEngine: Option[ServerEngine]\n\n // 模塊暴露的 gRPC 服務集合\n def grpcServices: Set[BindableService]\n\n // 模塊的後臺服務集合\n def workers: Seq[ServiceBuilder]\n\n // 模塊的啓動服務集合\n def bootstraps: Seq[ServiceBuilder]\n\n // 注入上下文信息\n def aware(context: ModuleContext): Unit\n\n}\n\ntrait ServiceBuilder {\n\n def build(injector: Injector): Service\n\n}\n\n// 模塊上下文信息\ntrait ModuleContext {\n\n // gRPC 進程內通道\n def grpcChannel: ManagedChannel\n\n // 判斷某個 gRPC 服務是否可以使用進程內調用\n def isInProcessServiceStub(stubClass: Class[_]): Boolean\n\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上面的代碼中,我們主要描述了以下幾個信息:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模塊的上下文信息"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模塊暴露的服務集合"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,要解決的是將多個服務(也就是 ModuleContext)合併在一起運行。在這之前,我們還需要解決幾個問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"用什麼樣的方式將多個服務合併在一起?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在同一個 JVM 內,怎麼保證模塊之間的既能相互調用,又能保持相互獨立?"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a1/a1c11d958991b05b5fd51608bac651f3.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖,Server A 與 Server B 都有 GlobalConfig 對象,它們的類路徑一致,包含的字段屬性也一致,但是它們字段屬性的值卻不一樣。如果將這兩個服務作爲依賴的方式引入到同一個項目裏面運行,這種方式雖然簡單但是沒有解決模塊之間的隔離型問題。會發生 A 會覆蓋 B 或者 B 覆蓋 A 的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那如何做到讓它們保持相互獨立呢?這時候,我們可以使用 JVM 的 ClassLoader 機制。在 JVM 中,兩個非父子關係的 ClassLoader 加載到的內容是相互隔離的。所以,如果我們將不同的模塊使用不同的 ClassLoader 加載就能讓它們保持相互獨立了。如下圖:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/13/133e6bab95f7485b246f29d91c8fe6e2.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上圖中的 ClassLoader 構造,可以使用下面的代碼實現:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"val classLoaderA = new URLClassLoader(serverA.libs, parent)\nval classLoaderB = new URLClassLoader(serverB.libs, parent)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模塊被加載成 ClassLoader 了,初始化模塊就變得簡單許多了。參考 Java 9 模塊化的設計,我們爲每個微服務加入了模塊描述文件,如下圖:我們在 growing-insight 服務裏面加入了 BoxerModuleInfo 文件描述模塊內容。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cf/cf8dfe0c73556207e9d7f68c9d639790.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上圖中的 SimpleBoxerModule 是默認的模塊內容描述,如果使用者用標準的模式開發服務,那麼他只需要創建一個 BoxerModuleInfo 文件並繼承 SimpleBoxerModule 就能完成模塊註冊。是使用方式及其簡單。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"class SimpleBoxerModule extends BoxerModule {\n\n private[this] var moduleContext: ModuleContext = _\n private[this] lazy val applicationContext = new ApplicationContextLoader(moduleContext).load(GlobalConfig.Server)\n\n override def debug(): Unit = {}\n\n override def injector: Injector = applicationContext.injector\n\n override def httpEngine: Option[ServerEngine] = applicationContext.httpEngineOpt\n\n override def grpcServices: Set[BindableService] = applicationContext.grpcServices\n\n override def hooks: Seq[ApplicationHook] = applicationContext.hooks\n\n override def workers: Seq[ServiceBuilder] = applicationContext.workers\n\n override def bootstraps: Seq[ServiceBuilder] = applicationContext.bootstraps\n\n override def aware(context: ModuleContext): Unit = moduleContext = context\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"根據模塊內容描述文件,用下面代碼的方式能將 ClassLoader 初始化成一個一個服務模塊:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"val loader = new ModuleAppClassLoader(urls, parent)\nval boxerModuleInfoClass = loader.loadClass(\"BoxerModuleInfo\")\nval module = boxerModuleInfoClass.newInstance().asInstanceOf[BoxerModule]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"得到了 Module 對象,就能依據它暴露的服務合併在一起發佈一個新的服務列表了。接下來,要解決的是如何讓原本通過遠程調用的 gRPC 服務轉換成進程內堆棧調用。這裏就不得不提在 BoxerModule 定義中的 ModuleContext 了。通過使用它的 isInProcessServiceStub 方法判斷某一個 gRPC 是否可以使用進程內調用,如果可以進程內調用,使用 grpcChannel 初始化 gRPC Stub 就能完成進程內調用了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上,是整個模塊化系統設計與實現的核心邏輯。不過還能在 ClassLoader 的架構上更進一步,達到上文中提到的減少資源使用的目標。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"服務與服務(即模塊與模塊)之間,除了業務邏輯以及需要隔離的類之外,剩下的框架部分絕大部分是一致的,框架部分的類沒有隔離的必要性。如下圖,將不需要隔離性的框架類通過 BoxerSuiteClassLoader 管理可以提高內存空間利用率,因爲 JVM 類加載器有雙親委派機制,ClassLoader 會優先從父 ClassLoader 加載類信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/9d/9d1776b86f67fff07a100be11360fde3.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2,減少中間件依賴 - 去掉中心化服務註冊中心"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 GrowingIO 的 SaaS 環境中,我們使用 Consul +"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Dryad("},{"type":"link","attrs":{"href":"https://link.zhihu.com/?target=https%3A//github.com/growingio/dryad","title":null},"content":[{"type":"text","text":"https://github.com/growingio/dryad"}],"marks":[{"type":"underline"}]},{"type":"text","text":" 我們開源的一個服務註冊發現與配置管理組件) 實現服務的註冊與發現。爲了降低私有部署的環境複雜性,去掉 Consul 集羣是一個有效的方式。因此選擇了類似於 nginx 中 upstream 的方式實現服務集羣的高可用。私有部署的用戶訪問流量比起 SaaS 上具有較強的可預測性,不需要經常性地做動態擴容。所以,使用這種方式並不會帶來特別高的維護成本。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3a/3ac88a5d0dccc46b52b90b826b6575c3.jpeg","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上圖所示,Server A 在本地維護了 Server B 與 Server C 的地址。在使用的過程中根據 Server Name 可以找到服務具體的地址。這一功能,已經在 Dryad 裏開源出來了。使用的過程如下代碼:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定義服務具體的地址"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"dryad {\n cluster {\n direct = true // 爲 true 則直接使用本地的集羣配置,爲 false 則通過 consul 的方式註冊與發現服務\n events = [ // events 是服務的名稱\n {\n address = \"10.0.0.1\"\n port = 8080\n },\n {\n address = \"10.0.0.2\"\n port = 8080\n }\n ]\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 io.growing.dryad.cluster.Cluster 的 roundRobin 接口獲取服務信息"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"val server = cluster.roundRobin(Schema.GRPC, \"events\")"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"3,個性化需求 - 插件化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"GrowingIO 服務的 SaaS 版本與私有部署版本使用的是同一個代碼分支,在面對私有部署版本中的個性化需求時,使用的是 “插件化” 的思想解決實現不一致的問題。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"即使用標準化的接口允許多個不同的實現滿足個性化需求。例如,在 SaaS 版本中爲了滿足多租戶數據隔離的需求,有一個專門的服務去管理各個用戶的數據作用域。在私有版本中,我們保留了支持多租戶數據隔離的這個功能,但是簡化了它的實現邏輯。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下圖,GrowingIO 使用接口 + 多個實現的方式滿足個性化需求:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"|- service // 接口定義\n | - impl // SaaS 上的標準實現\n | - extension // 私有部署上的個性化實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在服務啓動時,通過配置可以決定需要加載哪種實現,這個過程是自動完成的,不需要在開發過程中干預。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"extension {\n enabled = true // 是否按照拓展的方式加載實現\n key = \"extension\" // 加載的個性化需求地址(默認是 extension,如果有多個客戶的實現不一樣,修改 key 的值即可)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文,我們討論了 GrowingIO 是如何讓微服務支持 SaaS 與私有部署 2 種不同的環境的。要去解決它們之間的兼容問題,就需要找到它們之間的差別。很多時候,這 2 個環境的許多部分都是相反的,我們要在架構設計中自頂向下去包容這種矛盾,不能哪裏有問題就去解決哪裏,這樣只會讓程序越來越複雜。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章