SOFAMesh中的多協議通用解決方案x-protocol介紹系列(1) : DNS通用尋址方案

小螞蟻說:git

2018年上半年,螞蟻金服決定基於 Istio 訂製本身的 ServiceMesh 解決方案,並在6月底正式對外公佈了 SOFAMesh 
github

在 SOFAMesh 的開發過程當中,針對遇到的實際問題,咱們給出了一套名爲 x-protocol 的解決方案,本文將會對這個解決方案進行詳細的講解,後面會有更多內容,歡迎持續關注本系列文章。web


前言tomcat

x-protocol 的定位是雲原生、高性能、低侵入性的通用 Service Mesh 落地方案,依託 Kubernetes 基座,利用其原生的服務註冊和服務發現機制,支持各類私有 RPC 協議低成本、易擴展的接入,快速享受 Service Mesh 所帶來的紅利。服務器


具體解決的問題包括:架構

  • 多通信協議支持問題,減小開發工做量,簡單快捷的接入新協議app

  • 儘可能提高性能,提供更靈活的性能與功能的平衡點選擇,知足特定高性能場景負載均衡

  • 兼容現有 SOA 體系,提供經過接口進行訪問的方式,實現不修改業務代碼也能順利接入 Service Mesh框架

  • 支持單進程多服務的傳統 SOA 程序,能夠在微服務改造以前,先受益於 Service Mesh 帶來的強大功能less


在本系列文章中,咱們將對此進行詳細的講解,首先是「DNS通用尋址方案」。

SOFA 開源網站:

http://www.sofastack.tech/


背景和需求

SOA的服務模型


在 SOFAMesh 計劃支持的 RPC 框架中,SOFARPC、HSF、Dubbo 都是一脈相承的 SOA 體系,也都支持經典的SOA服務模型,一般稱爲」單進程多服務」,或者叫作」單進程多接口」。(備註:因爲服務一詞使用過於頻繁,下文都統一稱爲接口以便區分)


SOA 標準的服務註冊,服務發現和調用流程以下:



  1. 在單個 SOA 應用進程內,存在多個接口

  2. 服務註冊時,以接口爲單位進行屢次獨立的服務註冊

  3. 當客戶端進行調用時,按照接口進行服務發現,而後發起調用


當咱們試圖將這些 SOA 架構的應用搬遷到 ServiceMesh 時,就會遇到服務模型的問題:微服務是單服務模型,也就是一個進程裏面只承載一個服務。以 k8s 的服務註冊爲例,在單進程單服務的模型下,服務名和應用名能夠視爲一體,k8s 的自動服務註冊會將應用名做爲服務註冊的標示。


這就直接致使了 SOA 模型和微服務模型的不匹配問題:

  • SOA 以接口爲單位作服務註冊和服務發現,而微服務下是服務名

  • SOA 是」單進程多接口」,而微服務是」單進程單服務」


一步接一步的需求


  • 先上車後補票
    最理想的作法固然是先進行微服務改造,實現微服務拆分。可是考慮到現有應用數量衆多,咱們可能更願意在大規模微服務改造以前,先想辦法讓這些應用能夠運行在 ServiceMesh 下,提早受益於 ServiceMesh 帶來的強大功能。所以,咱們須要找到一個合適的方案,讓 ServiceMesh 支持沒有作微服務改造依然是」單進程多接口」形式的傳統 SOA 應用,所謂」先上車後補票」。

  • 不修改代碼
    考慮到原有的 SOA 應用,相互之間錯綜複雜的調用關係,最好不要修改代碼,即保持客戶端依然經過接口名來訪問的方式。固然,SOA 架構的客戶端 SDK 可能要進行改動,將原有的經過接口名進行服務發現再自行負載均衡進行遠程調用的方式,精簡爲標準的 ServiceMesh 調用(即走Sidecar),所以修改SDK依賴包和從新打包應用是不可避免。

  • 支持帶特殊字符的接口名
    k8s 的服務註冊,Service 名是不能攜帶」.「號的。而 SOA 架構下,接口名有時出於管理方便,有多是加了域名前綴,如「com.alipay.demo.interface-2」。爲了實現不修改原有代碼,就只能想辦法支持這種帶特殊字符的接口名。


參考 Kubernetes 和 Istio

在進一步討論解決方案以前,咱們先來看一下kubernetes 和 istio 中的標準請求尋址方式。


(備註:過程稍顯複雜,涉及到k8s/istio的一些底層細節。可是瞭解這個過程對後續的理解很是重要,也能夠幫助你們瞭解k8s和k8s的工做原理,強烈推薦閱讀。)


k8s下的DNS尋址方式


在k8s下,如圖所示,假定咱們部署了一個名爲 userservice 的應用,有三個實例,分別在三個 pod 中。則應用部署以後,k8s 會爲這個應用分配 ClusterIP 和域名,並在DNS中生成一條DNS記錄,將域名映射到ClusterIP:



當部署在 k8s 下的某個充當客戶端的應用發起請求時,如圖中的 HTTP GET請求,目標 URL 地址爲 「http://userservice/id/1000221"。請求的尋址方式和過程以下:

  • 首先進行域名解析,分別嘗試解析「userservice」/「userservie.default.svc.cluster.local」等域名,獲得 ClusterIP

  • 而後客戶端發出請求的報文,目標地址爲ClusterIP,源地址爲當前客戶端所在的 pod IP(簡單起見,端口先忽略)

  • 請求報文隨即被 kube-proxy 攔截,kube-proxy 根據 ClusterIP,拿到ClusterIP 對應的多個實際服務實例所在的 pod ip,取其中一個,修改目標地址爲這個pod IP

  • 請求報文最終就被髮送到服務實例所在的pod IP


應答回來的方式相似,userservice發出的應答報文會被 kube-proxy 攔截並修改成發送到客戶端所在的 pod IP。


咱們詳細看一下請求和應答全稱的四個請求包的具體內容(簡單起見繼續忽略端口):



重點關注請求和應答報文的源地址和目標地址:

  • 客戶端發出的請求,爲「客戶端到 ClusterIP」

  • kube-proxy 攔截到請求後,將請求修改成「客戶端到服務器端」

  • 服務器端收到請求時,表現爲「客戶端到服務器端」,ClusterIP 被kube-proxy 屏蔽

  • 服務器端發送應答,由於收到的請求看似來自客戶端,所以應答報文爲」服務器端到客戶端」

  • 應答報文被 kube-proxy 攔截,將應答修改成 「ClusterIP到服務器端」

  • 客戶端收到應答,表現爲「ClusterIP 到服務器端」,服務器端 IP 被 kube-proxy 屏蔽


kube-proxy 在客戶端和服務器端之間攔截並修改請求和應答的報文,聯通二者,但各自屏蔽了一些信息:

  • 在客戶端看來它是在和 ClusterIP 交互,userservice 的具體服務器端實例對客戶端是無感知的

  • 在服務器端看來,客戶端是直接在和它交互,ClusterIP 的存在對服務器端是無感知的


更深刻一步,看 kube-proxy 在兩個攔截和修改報文中的邏輯處理關係,即kube-proxy是如何在收到應答時正確的找回原有的 ClusterIP:



  1. 在攔截並修改請求報文以後,kube-proxy 會保存報文修改的5元組對應關係(5元組指源 IP 地址,源端口,協議,目的地 IP 地址,目的地端口)

  2. 在收到應答報文後,根據應答報文中的5元組,在保存的5元組對應關係中,找到對應信息,獲得原有的 ClusterIP 和端口,而後修改應答報文


總結,經過上述k8s下的尋址方式,客戶端只需發送帶簡單尋址信息的請求(如 「http://userservice/id/1000221" 中的「userservice」 ),就能夠尋址到正確的服務器端。這期間有兩個關注點:

  1. 經過 DNS,創建了域名和 ClusterIP 的關係。
    對於客戶端,這是它能看到的內容,很是的簡單,域名、DNS 是很是容易使用的。

  2. 而經過 kube-proxy 的攔截和轉發,又打通了ClusterIP 和服務器端實際的Pod IP
    對於客戶端,這些是看不到的內容,無論有多複雜,都是k8s在底層完成,對客戶端,或者說使用者透明。


以客戶端的視角看來,這個DNS尋址方式很是的簡單直白:


Istio的 DNS 尋址方式

Istio的請求尋址方式和普通 kubernetes 很是類似,原理相同,只是 kube-proxy被 sidecar 取代,而後 sidecar 的部署方式是在 pod 內部署,並且客戶端和服務器端各有一個 sidecar。其餘基本一致,除了圖中紅色文本的部分:



  • iptables 在劫持流量時,除了將請求轉發到 localhost 的 Sidecar 處外,還額外的在請求報文的 TCP options 中將 ClusterIP 保存爲 original dest。

  • 在 Sidecar (Istio 默認是 Envoy)中,從請求報文 TCP options 的 original dest 處獲取 ClusterIP


經過TCP options 的 original dest,iptables就實現了在劫持流量到Sidecar的過程當中,額外傳遞了 ClusterIP 這個重要參數。Istio爲何要如此費力的傳遞這個 ClusterIP 呢?


看下圖就知道了,這是一個 Virtual Host 的示例, Istio 經過 Pilot 將這個規則發送給 Sidecar/Envoy ,依靠這個信息來匹配路由請求找處處理請求的cluster:



domains中,除了列出域名外,還有一個特殊的IP地址,這個就是 k8s 服務的 ClusterIP!所以,Sidecar能夠經過前面傳遞過來的 ClusterIP 在這裏進行路由匹配(固然也能夠從報文中獲取 destination 而後經過域名匹配)。


總結,Istio 延續了 k8s 的尋址方式,客戶端一樣只需發送帶簡單尋址信息的請求,就能夠尋址到正確的服務器端。這期間一樣有兩個關注點:

  1. 經過DNS,創建了域名和ClusterIP的關係。

  2. 經過 ClusterIP 和 Pilot 下發給 Virtual Host 的配置,Sidecar 能夠完成路由匹配,將 ClusterIP 和目標服務器關聯起來
    一樣,對於客戶端,這些是看不到的內容。


所以,以客戶端的視角看來,Isito的這個DNS尋址方式一樣的簡單直白!



DNS通用尋址方案具體介紹

解決問題的思路


在詳細講述了 k8s 和 istio 的 DNS 尋址方案以後,咱們繼續回到咱們的主題,咱們要解決的問題:


如何在不修改代碼,繼續使用接口的狀況下,實如今 Service Mesh 上運行現有的 Dubbo/HSF/SOFA 等傳統 SOA 應用?

這裏有一個關鍵點:k8s 的服務註冊是以基於 Service 或者說基於應用(app name),而咱們的客戶端代碼是基於接口的。所以,在 Virtual Host 進行路由匹配時,是不能經過域名匹配的。固然,這裏理論上還有一個思路,就是將接口註冊爲 k8s Service。可是,還記得要支持接口特殊字符的需求嗎?帶點號的接口名,k8s 是不能接受它做爲 Service Name 的,直接堵死了將接口名註冊到 k8s Service 的道路。



這樣,咱們就只有一條路能夠走了:效仿 istio 的作法,經過 ClusterIP 匹配!


而要將接口名(如」com.alipay.demo.interface-1」)和 ClusterIP 關聯,最簡單直接的方式就是打通DNS



只須要在DNS記錄中,增長接口到 ClusterIP 的映射,而後就能夠徹底延續Istio的標準作法!其餘的步驟,如域名解析到ClusterIP,iptables攔截並傳遞ClusterIP,sidecar讀取ClusterIP並匹配路由,都徹底能夠重用原有方案。


具體實現方案


實現時,咱們選擇了使用 CoreDNS 做爲 k8s 的 DNS 解決方案,而後經過 Service Controller 操做 CoreDNS 的記錄來實現 DNS 解析。


爲了收集到 SOA 應用的接口信息,咱們還提供了一個 Register Agent 給 Service Controller 收集信息。



詳細的實現方案,不在本文中重複講述,請參閱咱們以前的分享文章 SOFAMesh 的通用協議擴展 中的 DNS 尋址方案一節。


(備註:暫時修改 CoreDNS 記錄的方式是直接修改 CoreDNS 的底層數據,不夠優雅。將來將修改成經過 CoreDNS 的 Dynamic updates API 接口進行,不過 CoreDNS 的這個API還在開發中,須要等待完成。)


單進程多接口問題的解決


上面的解決方案,在解決經過接口實現訪問的同時,也將」單進程多接口」的問題一塊兒解決了:

  • 原 SOA 應用上 k8s 時,能夠註冊爲標準的 k8s Service,獲取 ClusterIP。此時使用應用名註冊,和接口無關。

  • 經過操做 CoreDNS,咱們將該 SOA 應用的各個接口都添加爲 DNS 記錄,指向該應用的 ClusterIP

  • 當客戶端代碼使用不一樣的接口名訪問時,DNS解析出來的都是同一個 ClusterIP,後續步驟就和接口名無關了


欠缺微服務改造帶來的限制


須要特別指出的是,DNS 通用尋址方案雖然能夠解決使用接口名訪問和支持單進程多接口的問題,可是這種方案只是完成了「尋址」,也就是打通端到端的訪問通道。因爲應用沒有進行微服務改造,部署上是依然一個應用(體現爲一個進程,在 k8s 上體現爲一個 Service)中包含多個接口,本質上:

  • 服務註冊依然是以應用名爲基礎,對應的 k8s service 和 service 上的 label也是應用級別

  • 所以提供的服務治理功能,也是以 k8s 的 Service 爲基本單位,包括灰度,藍綠,版本拆分等全部的 Vesion Based Routing 功能

  • 這意味着,只能進行應用級別的服務治理,而不能繼續細分到接口級別


這個限制來源於應用沒有進行微服務改造,沒有按照接口將應用拆分爲多個獨立的微服務,所以沒法獲得更小的服務治理粒度。這也就是我在2018年上半年,螞蟻金服決定基於 Istio 訂製本身的 ServiceMesh 解決方案,在6月底對外公佈了 SOFAMesh,詳情請見以前的文章: 大規模微服務架構下的Service Mesh探索之路 。


DNS通用尋址方案總結

咱們將這個方案稱爲「DNS通用尋址方案」,是由於這個方案真的很是的通用,體如今如下幾個方面:

  • 對使用者來講,經過域名和 DNS 解析的方式來訪問,是很是簡單直白而易於接受的,同時也是普遍使用的,適用於各類語言、平臺、框架。

  • 這個方案延續了 k8s 和 istio 的作法,保持了一致的方式方式,對用戶提供了相同的體驗

  • 這個尋址方案,不只僅能夠用於 Dubbo、SOFA、HSF 等 RPC 框架往 Service Mesh 的遷移,也能夠適用於基於 HTTP/REST 協議的 SOA 應用,甚至最傳統的 web 應用(例如 tomcat 下部署多個 war 包)遷移到Service Mesh

  • 咱們也在考慮在將來的 Serverless 項目中,將 Function 的尋址也統一到這套方案中,而無須要求每一個 Function 都進行一次服務註冊


歸納的說,有了這套 DNS 通用尋址方案,無論須要尋址的實體是什麼形態,只要它部署在 Service Mesh 上,知足如下條件:

  1. 有正常註冊爲 k8s Service,分配有 ClusterIP

  2. 爲實體(或者更細分的子實體)分配域名或子域名,而後添加到 DNS,解析到 ClusterIP


那麼咱們的 DNS 通用尋址方案,就能夠工做,從而將請求正確的轉發到目的地。而在此基礎上,Service Mesh 全部的強大功能均可覺得這些實體所用,實現咱們前面的目標:在不修改代碼不作微服務改造的狀況下,也能提早受益於 Service Mesh 帶來的強大服務治理功能。


長按關注,獲取分佈式架構乾貨

歡迎你們共同打造 SOFAStack https://github.com/alipay

相關文章
相關標籤/搜索