作者:十眠
流量路由,顧名思義就是將具有某些屬性特徵的流量,路由到指定的目標。流量路由是流量治理中重要的一環,本節內容將會介紹流量路由常見的場景、流量路由技術的原理以及實現。
流量路由的業務場景
我們可以基於流量路由標準來實現各種業務場景,如標籤路由、金絲雀發佈、同機房優先路由等。
- 標籤路由
標籤路由是按照標籤爲維度對目標負載進行劃分,符合條件的流量匹配至對應的目標,從而實現標籤路由的能力。當然基於標籤路由的能力,賦予標籤各種含義我們就可以實現各種流量路由的場景化能力。
- 金絲雀發佈
金絲雀發佈是一種降低在生產中引入新軟件版本的風險的技術,方法是在將更改推廣到整個基礎架構並使其可供所有人使用之前,緩慢地將更改推廣到一小部分用戶。金絲雀發佈是一種在黑與白之間,能夠平滑過渡的一種發佈方式。讓一部分用戶繼續用舊版本,一部分用戶開始用新版本,如果用戶對新版本沒有什麼反對意見,那麼逐步擴大範圍,把所有用戶都遷移到新版本上面來。一直都有聽說,安全生產三板斧的概念:可灰度、可觀測、可回滾。那麼灰度發佈能力就是幫助企業軟件做到快速迭代驗證的必備能力。在K8s中金絲雀發佈的最佳實踐如下:第一步:新建灰度 Deployment,部署新版本的鏡像,打上新版本的標籤。第二步:配置針對新版本的標籤路由規則。第三步:驗證成功,擴大灰度比例。第四步:若驗證成功,將穩定版本的應用更新成最新鏡像;若驗證失敗,把灰度的 Deployment 副本數調整到 0 或刪除該 Deployment。
- 全鏈路灰度
當企業的發展,微服務的數量會逐漸增多。在有一定規模的一定數量的微服務情況下,一次發版可能涉及到的服務數量會比較多,微服務鏈路也相當較長。全鏈路灰度可以保證特定的灰度流量可以路由到所有涉及到的灰度版本中。
- 同可用區優先路由
當企業的對穩定性的要求變高時,企業的應用會選擇部署在多個可用區中提高應用的可用性,避免某個可用區出現問題後導致影響應用的可用性。當應用在不同的可用區部署時,應用間跨可用區調用可能會被因爲遠距離調用造成的網絡延遲影響,同可用區優先路由會讓我們的Consumer應用優先調用當前可用區內的Provider應用,可以很好地減少這種遠距離調用造成的影響,同時當某個可用區出現問題後,我們只需在流量入口處將當前可用區的流量隔離掉,其他可用區的流量不會訪問至當前可用區的節點,可以很好地控制某個可用區出現問題後的影響面。
流量路由能力實現的場景衆多,上面只是列舉了一些典型的場景,下面我們將從流量路由原理入手,剖析流量路由的實現與技術細節。
流量路由原理
需要實現上述所提的流量路由的場景,那麼對於Consumer應用來說,同一個 Provider 應用的不同節點之間是有一些特殊的標識。金絲雀發佈場景來說,新版本代碼所部署的節點需要被標上成新版本的標識;同機房優先路由來說,Provider節點要被標識上機房的信息;全鏈路灰度場景來說,灰度環境的節點需要被帶上灰度標。因此,我們需要在Provider服務註冊的過程中,就在註冊到註冊中心的地址信息中帶上治理場景所需的標識。
- 節點打標
首先介紹一下節點打標的能力,我們先看看 Apache Dubbo 的設計,其中 Dubbo 服務節點的地址信息使用 URL 模型來承載。
class URL implements Serializable {
protected String protocol;
// by default, host to registry
protected String host;
// by default, port to registry
protected int port;
protected String path;
private final Map<String, String> parameters;
}
舉個簡單的例子,假如 Consumer 收到這樣一條 dubbo://10.29.0.102:20880/GreetingService?tag=gray&az=az_1 地址信息,表示 GreetingService 服務使用的是 dubbo 協議,服務綁定的 ip 與 port 分別爲 10.29.0.102 跟 20880,該地址攜帶上了 tag=gray、az=az_1 這樣兩條元數據信息,分別表示當前節點的標籤爲灰度,當前節點所處的可用區(az:Availability Zone 爲雲上的機房的可用區概念)爲 az_1 。那麼節點打標的能力其實就比較明確了,我們在服務提供者向註冊中心註冊服務地址之前,我們在當前服務提供者的地址信息上增加需要增加的元數據信息比如 verion = gray
,比如在 Apache Dubbo 的 URL 中增加 paramters 信息,一般來說元數據信息都是 k-v 的 map 結構,這樣框架向註冊中心註冊該節點時會爲其添加需要的標籤信息verison=gray
。
相似的, Spring Cloud 中通過表示服務節點信息的抽象
public class Server {
public static interface MetaInfo {
...
}
private String host;
private int port = 80;
...
}
Sentinel2.0 希望作爲流量治理能力的實現,考慮到會被較多框架即成,因此需要考慮到各個框架的通用點以及本身設計的易用性,Sentinel2.0 中使用 Instance 模型表示服務節點信息的抽象,並且在其中保留了原有類型的引用。
public class Instance {
private String host;
private Integer port;
private Map<String, String> metadata;
private Object targetInstance;
}
其中 metadata 用來存儲用於服務治理的元數據,比如AZ標、版本標籤等等。
- 流量路由
到目前爲止,我們算是搞明白了 Consumer 收到的 Provider 的地址列表長什麼樣子。假設 Consumer 收到了 如下圖所示 GreetingService 服務的6條地址,那麼我們該如何進行選擇呢?
算是進入到正題,我們看一下 Sentinel2.0 是如何實現流量路由能力的。
目前我們在 Sentinel2.0 中分別抽象了InstanceManager、RouterFilter 以及 LoadBalancer 三個對象,並通過 ClusterManager 將它們管理起來。其中 InstanceManager 將地址列表按需進行存儲與管理,RouterFilter做爲流量路由能力實現的主體,LoadBalancer做爲負載均衡能力實現的主體。
Dubbo 在收到註冊中心同步過來的 Provider URL 之後會生成對應的 Invoker ,Invoker 列表我們可以理解爲就是可以調用的 Provider 節點列表的抽象。流量路由則是需要將傳入的 Invoker 列表按照路由規則進行路由篩選,篩選出符合路由規則的服務提供者,即符合路由規則的 Invoker 列表。我們如何可以通過 Sentinel2.0 的抽象來實現流量路由的能力呢?當地址通知下來後,我們需要通過 instanceManager#storeInstances 將地址列表進行緩存。
@Override
public void notify(BitList<Invoker<T>> invokers) {
super.notify(invokers);
instanceManager.storeInstances(invokersToInstances(invokers));
}
在流量路由處,我們則調用 clusterManager#route 實現地址路由。
@Override
protected BitList<Invoker<T>> doRoute(BitList<Invoker<T>> invokers, URL url, Invocation invocation, boolean needToPrintMessage, Holder<RouterSnapshotNode<T>> routerSnapshotNodeHolder, Holder<String> messageHolder) throws RpcException {
TrafficContext trafficContext = getTrafficContext(invocation);
List<Instance> instances = clusterManager.route(trafficContext);
return instancesToInvokers(instances);
}
其中 ClusterManager 會將路由執行的邏輯交給 RouterFiler.route 進行執行。
public List<Instance> route(TrafficContext context) {
List<Instance> instances = instanceManager.getInstances();
for (RouterFilter routerFilter : routerFilterList) {
instances = routerFilter.filter(instances, context);
}
return instances;
}
每個 RouterFilter 服務路由都可以包含一條路由規則,路由規則決定了服務消費者的調用目標,即規定了服務消費者可調用哪些服務提供者;一次微服務調用的地址列表可以由多個 RouterFilter 服務路由共同影響,比如我們希望當前的 Consumer 流量訪問到在同時符合灰度發佈以及同可用區優先調用路由規則的節點上。我們可以按照需求增加路由鏈中的 RouterFilter,並且路由鏈的 Route 方法是循環調用每個 RouterFilter 的 Route 方法。並且上一個 Router 的輸出 Invoker 列表會做爲下一個 Router 的輸入。介紹到這裏,大家可能對下圖會有一個更加深刻的理解了。
路由的整體模型大家已經理解了,我們來重點看一下具體的 RouterFilter 服務路由是如何實現的。
public interface RouterFilter {
List<Instance> filter(List<Instance> instanceList, TrafficContext context) throws TrafficException;
}
RouterFilter 的 Route 方法會在每次請求調用時被執行,Route 方法有關鍵的兩個入參 InstanceList 跟 TrafficContext,instanceList 是可調用的服務提供者節點列表的抽象。TrafficContext 是當前調用流量的請求上下文的抽象,我們可以從中讀到請求中攜帶着的 RouterFilter 所關心的一些元數據(比如當前請求的AZ信息、請求參數中指定 key 的值等內容)。Route 方法會在每次調用時候根據請求中的上下文信息結合路由規則計算出當前請求需要匹配的目標節點特徵,並遍歷當前的地址列表,根據目標節點特徵進行地址過濾。篩選出目標節點的地址列表,是輸入地址列表的子集,然後傳遞給下一個 RouterFilter。
RouterFilter 的 Route 方法邏輯的僞代碼如下:
@Override
public List<Instance> filter(List<Instance> instanceList, TrafficContext context) throws TrafficException {
List<Instance> targetInstances = new ArrayList<>();
for (Instance instance : instanceList) {
if (trafficRouteMatch(instance, context)) {
targetInstances.add(instance);
}
}
return targetInstances;
}
instanceList 爲輸入地址列表,targetInstances 爲輸出地址列表即當前 Router 服務路由的結果。
Sentinel2.0流量路由規劃
Sentinel2.0 將基於 OpenSergo 流量路由規則實現基本的流量路由能力,支持多種流量路由策略、負載均衡策略、虛擬工作負載等。Sentinel2.0 期望支持 Http、RPC、SQL等微服務各種流量的路由能力,並且可以快速被各主流微服務框架所集成。