歡迎光臨
每天分享高質量文章

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

小螞蟻說:

2018年上半年,螞蟻金服決定基於 Istio 訂製自己的 ServiceMesh 解決方案,併在6月底正式對外公佈了 SOFAMesh,詳情可直接點擊之前的文章查看: 大規模微服務架構下的Service Mesh探索之路 

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


前言

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


具體解決的問題包括:

  • 多通訊協議支持問題,減少開發工作量,簡單快捷的接入新協議

  • 儘量提升性能,提供更靈活的性能與功能的平衡點選擇,滿足特定高性能場景

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

  • 支持單行程多服務的傳統 SOA 程式,可以在微服務改造之前,先受益於 Service Mesh 帶來的強大功能


在本系列文章中,我們將對此進行詳細的講解,首先是“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


 


赞(0)

分享創造快樂