天天看點

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

小螞蟻說:

2018年上半年,螞蟻金服決定基于 Istio 訂制自己的 ServiceMesh 解決方案,并在6月底正式對外公布了 SOFAMesh 。

在 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 标準的服務注冊,服務發現和調用流程如下:

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

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:

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

當部署在 k8s 下的某個充當用戶端的應用發起請求時,如圖中的 HTTP GET請求,目标 URL 位址為 “

http://userservice/id/100022

1"。請求的尋址方式和過程如下:

  • 首先進行域名解析,分别嘗試解析“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。

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

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

重點關注請求和應答封包的源位址和目标位址:

用戶端發出的請求,為“用戶端到 ClusterIP”

kube-proxy 攔截到請求後,将請求修改為“用戶端到伺服器端”

伺服器端收到請求時,表現為“用戶端到伺服器端”,ClusterIP 被kube-proxy 屏蔽

伺服器端發送應答,因為收到的請求看似來自用戶端,是以應答封包為”伺服器端到用戶端”

應答封包被 kube-proxy 攔截,将應答修改為 “ClusterIP到伺服器端”

用戶端收到應答,表現為“ClusterIP 到伺服器端”,伺服器端 IP 被 kube-proxy 屏蔽

kube-proxy 在用戶端和伺服器端之間攔截并修改請求和應答的封包,聯通兩者,但各自屏蔽了一些資訊:

在用戶端看來它是在和 ClusterIP 互動,userservice 的具體伺服器端執行個體對用戶端是無感覺的

在伺服器端看來,用戶端是直接在和它互動,ClusterIP 的存在對伺服器端是無感覺的

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

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

在攔截并修改請求封包之後,kube-proxy 會儲存封包修改的5元組對應關系(5元組指源 IP 位址,源端口,協定,目的地 IP 位址,目的地端口)

在收到應答封包後,根據應答封包中的5元組,在儲存的5元組對應關系中,找到對應資訊,得到原有的 ClusterIP 和端口,然後修改應答封包

總結,通過上述k8s下的尋址方式,用戶端隻需發送帶簡單尋址資訊的請求(如 “

1" 中的“userservice” ),就可以尋址到正确的伺服器端。這期間有兩個關注點:

通過 DNS,建立了域名和 ClusterIP 的關系。

對于用戶端,這是它能看到的内容,非常的簡單,域名、DNS 是非常容易使用的。

而通過 kube-proxy 的攔截和轉發,又打通了ClusterIP 和伺服器端實際的Pod IP

對于用戶端,這些是看不到的内容,不管有多複雜,都是k8s在底層完成,對用戶端,或者說使用者透明。

以用戶端的視角看來,這個DNS尋址方式非常的簡單直白:

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

Istio的 DNS 尋址方式

Istio的請求尋址方式和普通 kubernetes 非常相似,原理相同,隻是 kube-proxy被 sidecar 取代,然後 sidecar 的部署方式是在 pod 内部署,而且用戶端和伺服器端各有一個 sidecar。其他基本一緻,除了圖中紅色文本的部分:

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

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:

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

domains中,除了列出域名外,還有一個特殊的IP位址,這個就是 k8s 服務的 ClusterIP!是以,Sidecar可以通過前面傳遞過來的 ClusterIP 在這裡進行路由比對(當然也可以從封包中擷取 destination 然後通過域名比對)。

總結,Istio 延續了 k8s 的尋址方式,用戶端同樣隻需發送帶簡單尋址資訊的請求,就可以尋址到正确的伺服器端。這期間同樣有兩個關注點:

通過DNS,建立了域名和ClusterIP的關系。

通過 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 :

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

隻需要在DNS記錄中,增加接口到 ClusterIP 的映射,然後就可以完全延續Istio的标準做法!其他的步驟,如域名解析到ClusterIP,iptables攔截并傳遞ClusterIP,sidecar讀取ClusterIP并比對路由,都完全可以重用原有方案。

具體實作方案

實作時,我們選擇了使用 CoreDNS 作為 k8s 的 DNS 解決方案,然後通過 Service Controller 操作 CoreDNS 的記錄來實作 DNS 解析。

為了收集到 SOA 應用的接口資訊,我們還提供了一個 Register Agent 給 Service Controller 收集資訊。

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

詳細的實作方案,不在本文中重複講述,請參閱我們之前的分享文章 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 上,滿足以下條件:

有正常注冊為 k8s Service,配置設定有 ClusterIP

為實體(或者更細分的子實體)配置設定域名或子域名,然後添加到 DNS,解析到 ClusterIP

那麼我們的 DNS 通用尋址方案,就可以工作,進而将請求正确的轉發到目的地。而在此基礎上,Service Mesh 所有的強大功能都可以為這些實體所用,實作我們前面的目标:在不修改代碼不做微服務改造的情況下,也能提前受益于 Service Mesh 帶來的強大服務治理功能。

繼續閱讀