功能開關(又稱功能旗標)

功能開關(通常也稱為功能旗標)是一種強大的技術,讓團隊可以在不變更程式碼的情況下修改系統行為。它們屬於各種使用類別,在實作和管理開關時,考慮此類別非常重要。開關會引入複雜度。我們可以使用智慧的開關實作做法和適當的工具來管理開關組態,以控制複雜度,但我們也應該限制系統中開關的數量。

2017 年 10 月 9 日



「功能切換」是一組模式,可以協助團隊快速且安全地向使用者提供新功能。在本文中,我們將從一個簡短的故事開始,說明功能切換在哪些典型場景中會有所幫助。然後,我們將深入探討細節,介紹具體的模式和實務,以協助團隊成功使用功能切換。

功能切換也稱為功能標記、功能位元或功能切換器。這些都是同一組技術的同義詞。在本文中,我將交替使用功能切換和功能標記。

開關故事

想像一下這個場景。您是多個團隊中的一個,負責開發一款複雜的城市規劃模擬遊戲。您的團隊負責核心模擬引擎。您被指派提升 Spline Reticulation 演算法的效率。您知道這需要大幅修改實作,將花費數週的時間。同時,團隊中的其他成員需要繼續進行程式碼庫中相關區域的持續工作。

如果您有可能,您想避免為這項工作進行分支,因為過去合併長時間分支的經驗非常痛苦。相反地,您決定整個團隊將繼續在主幹上工作,但負責 Spline Reticulation 改進的開發人員將使用功能切換,以防止其工作影響團隊的其他成員或破壞程式碼庫的穩定性。

功能旗標的誕生

以下是負責演算法的這對開發人員所做的第一項變更

之前

  function reticulateSplines(){
    // current implementation lives here
  }

這些範例都使用 JavaScript ES2015

之後

  function reticulateSplines(){
    var useNewAlgorithm = false;
    // useNewAlgorithm = true; // UNCOMMENT IF YOU ARE WORKING ON THE NEW SR ALGORITHM
  
    if( useNewAlgorithm ){
      return enhancedSplineReticulation();
    }else{
      return oldFashionedSplineReticulation();
    }
  }
  
  function oldFashionedSplineReticulation(){
    // current implementation lives here
  }
  
  function enhancedSplineReticulation(){
    // TODO: implement better SR algorithm
  }

這對開發人員已將目前的演算法實作移至 oldFashionedSplineReticulation 函式,並將 reticulateSplines 變成一個切換點。現在,如果有人正在開發新的演算法,他們可以透過取消註解 useNewAlgorithm = true 行來啟用「使用新演算法」功能

讓旗標動態化

過了幾個小時,這對開發人員準備透過模擬引擎的一些整合測試來執行他們的新演算法。他們也希望在同一個整合測試執行中執行舊演算法。他們需要能夠動態啟用或停用功能,這表示是時候從註解或取消註解 useNewAlgorithm = true 行的笨拙機制中脫離了

function reticulateSplines(){
  if( featureIsEnabled("use-new-SR-algorithm") ){
    return enhancedSplineReticulation();
  }else{
    return oldFashionedSplineReticulation();
  }
}

我們現在已引入一個 featureIsEnabled 函式,這是一個切換路由器,可用於動態控制哪個程式碼路徑是作用中的。有許多方法可以實作切換路由器,從簡單的記憶體內儲存到具備精緻使用者介面的高度精密的分布式系統。現在,我們將從一個非常簡單的系統開始

function createToggleRouter(featureConfig){
  return {
    setFeature(featureName,isEnabled){
      featureConfig[featureName] = isEnabled;
    },
    featureIsEnabled(featureName){
      return featureConfig[featureName];
    }
  };
}

請注意,我們正在使用 ES2015 的 方法簡寫

我們可以根據一些預設設定建立一個新的切換路由器,或許從設定檔中讀取,但我們也可以動態開啟或關閉一項功能。這允許自動化測試驗證切換功能的兩面

describe( 'spline reticulation', function(){
  let toggleRouter;
  let simulationEngine;

  beforeEach(function(){
    toggleRouter = createToggleRouter();
    simulationEngine = createSimulationEngine({toggleRouter:toggleRouter});
  });

  it('works correctly with old algorithm', function(){
    // Given
    toggleRouter.setFeature("use-new-SR-algorithm",false);

    // When
    const result = simulationEngine.doSomethingWhichInvolvesSplineReticulation();

    // Then
    verifySplineReticulation(result);
  });

  it('works correctly with new algorithm', function(){
    // Given
    toggleRouter.setFeature("use-new-SR-algorithm",true);

    // When
    const result = simulationEngine.doSomethingWhichInvolvesSplineReticulation();

    // Then
    verifySplineReticulation(result);
  });
});

準備發佈

更多時間過去,團隊相信他們的新演算法已具備完整功能。為了確認這一點,他們已經修改他們的高階自動化測試,以便在功能關閉和開啟時執行系統。團隊也希望進行一些手動探索性測試,以確保所有功能都能如預期般運作,畢竟,樣條網格化是系統行為中至關重要的一部分。

若要對尚未驗證為可供一般使用之功能執行手動測試,我們需要能夠讓一般使用者在生產環境中關閉該功能,但能夠為內部使用者開啟該功能。有很多不同的方法可以達成此目標

  • 讓切換路由器根據切換設定做出決策,並讓該設定特定於環境。僅在預生產環境中開啟新功能。
  • 允許透過某種形式的管理員使用者介面在執行階段修改切換設定。使用該管理員使用者介面在測試環境中開啟新功能。
  • 教導切換路由器如何進行動態的逐次要求切換決策。這些決策會考量切換內容,例如尋找特殊 cookie 或 HTTP 標頭。切換內容通常用作識別提出要求使用者的代理。

(我們稍後會更深入探討這些方法,所以如果您對其中一些概念感到陌生,請別擔心。)

團隊決定採用逐次要求切換路由器,因為它提供了很大的彈性。團隊特別欣賞這將允許他們測試新演算法,而不需要一個獨立的測試環境。相反地,他們可以簡單地在生產環境中開啟演算法,但僅限於內部使用者(透過特殊 cookie 偵測)。團隊現在可以為自己開啟該 cookie,並驗證新功能是否如預期般執行。

金絲雀發佈

根據到目前為止進行的探索性測試,新的樣條網格化演算法看起來不錯。然而,由於它是遊戲模擬引擎中如此重要的部分,因此仍有一些猶豫是否要為所有使用者開啟此功能。團隊決定使用他們的特性標記基礎架構來執行 金絲雀釋出,僅為一小部分總使用者開啟新功能,也就是「金絲雀」群組。

團隊透過教授切換路由器使用者群組的概念來增強它,使用者群組是一群持續體驗某個功能始終為開啟或關閉的使用者。透過隨機抽樣 1% 的使用者基礎來建立一組金絲雀使用者,或許使用使用者 ID 的模數。這組金絲雀群組將持續開啟該功能,而其他 99% 的使用者基礎仍使用舊演算法。監控兩組的主要業務指標(使用者參與度、總收入等),以確信新演算法不會對使用者行為產生負面影響。一旦團隊確信新功能沒有不良影響,他們就會修改切換設定,替整個使用者基礎開啟它。

A/B 測試

團隊的產品經理瞭解此方法,並感到相當興奮。她建議團隊使用類似的機制來執行一些 A/B 測試。關於修改犯罪率演算法以考量污染程度是否會增加或減少遊戲的可玩性,一直存在長期的爭論。他們現在有能力使用資料解決爭議。他們計畫推出一個捕捉想法精髓的便宜實作,由功能旗標控制。他們將為相當大的一組使用者開啟該功能,然後研究這些使用者與「控制」群組相比的行為。此方法將允許團隊根據資料,而非河馬來解決有爭議的產品爭議。

此簡短場景旨在說明功能切換的基本概念,同時也強調此核心功能可以有多少不同的應用。現在我們已經看過一些這些應用程式的範例,讓我們深入探討。我們將探索不同類別的切換,並瞭解它們有何不同。我們將介紹如何撰寫可維護的切換程式碼,最後分享一些避免功能切換系統陷阱的做法。

開關類別

我們已經看過功能切換提供的基本設施,可以在一個可部署單元中發布替代程式碼路徑,並在執行階段選擇它們。上述場景也顯示此設施可以在各種情境中以各種方式使用。將所有功能切換都歸類為同一類別可能會很誘人,但這是一條危險的路徑。不同類別切換的設計力量大不相同,而且以相同方式管理所有切換可能會導致後續的痛苦。

功能切換可以根據兩個主要面向分類:功能切換的存續時間,以及切換決策必須有多動態。還有其他需要考慮的因素,例如誰將管理功能切換,但我認為長度和動態性是兩個重要的因素,有助於指導如何管理切換。

讓我們透過這兩個面向來考慮各種類型的切換,並了解它們的適用性。

發佈開關

發布切換允許將不完整且未測試的程式碼路徑發佈到生產環境,作為潛在程式碼,可能永遠不會啟用。

這些功能旗標用於為實踐持續交付的團隊啟用基幹開發。它們允許將正在進行中的功能簽入共享整合分支(例如主幹或基幹),同時仍允許隨時將該分支部署到生產環境。發布切換允許將不完整且未測試的程式碼路徑發佈到生產環境,作為潛在程式碼,可能永遠不會啟用。

產品經理也可以使用此相同方法的產品導向版本,以防止半完成的產品功能暴露給他們的最終使用者。例如,電子商務網站的產品經理可能不希望讓使用者看到新的預計送貨日期功能,該功能只適用於網站的其中一個送貨合作夥伴,而寧願等到該功能已針對所有送貨合作夥伴實作。產品經理可能還有其他原因不希望公開功能,即使它們已完全實作和測試。例如,功能發布可能與行銷活動協調。以這種方式使用發布切換是實作持續交付原則「將 [功能] 發布與 [程式碼] 部署分開」的最常見方式。

發布切換本質上是過渡性的。它們通常不應持續超過一或兩週,儘管以產品為中心的切換可能需要保留較長時間。發布切換的切換決策通常非常靜態。給定發布版本的每個切換決策都將相同,並且透過推出具有切換組態變更的新發布版本來變更該切換決策通常完全可以接受。

實驗開關

實驗切換用於執行多變量或 A/B 測試。系統的每位使用者會被放入一個同儕群組,而在執行階段,切換路由器會根據使用者所在同儕群組,持續將使用者導向其中一個程式路徑。透過追蹤不同同儕群組的總體行為,我們可以比較不同程式路徑的效果。此技術通常用於對資料驅動的最佳化進行調整,例如電子商務系統的購買流程,或按鈕上的行動呼籲文字。

實驗切換需要維持在相同的組態中足夠長的時間,才能產生具有統計意義的結果。根據流量模式,可能表示數小時或數週的使用壽命。不太可能更長,因為系統的其他變更可能會使實驗結果失效。實驗切換本質上是高度動態的 - 每個傳入的要求很可能是代表不同的使用者,因此可能會與上一個要求的路由方式不同。

運作開關

這些旗標用於控制我們系統行為的運作層面。當推出效能影響不明的新功能時,我們可能會引入運作切換,以便系統操作員可以在需要時在生產環境中快速停用或降低該功能。

大多數運作切換的使用壽命會相對較短 - 一旦對新功能的運作層面有信心,就應該停用旗標。然而,系統擁有少數長期「終止開關」並不少見,這些開關允許生產環境的操作員在系統承受異常高負載時,優雅地降低非必要的系統功能。例如,當我們負載過重時,我們可能想要停用首頁上相對昂貴的推薦面板。我諮詢過一家線上零售商,他們維護運作切換,可以在高需求產品推出之前,故意停用網站主要購買流程中的許多非必要功能。這些類型的長期運作切換可以視為手動管理的斷路器

正如前面所提到的,這些旗標中的許多只會存在一小段時間,但少數關鍵控制項可能會無限期地保留給操作員。由於這些旗標的目的是讓操作員能夠快速對生產問題做出反應,因此需要極快地重新組態 - 為了切換運作切換而需要推出新版本不太可能讓操作人員感到滿意。

權限開關

為一組內部使用者開啟新功能 [是一個] 香檳早午餐 - 及早享用自己香檳的機會

這些標記用於變更特定使用者接收的功能或產品體驗。例如,我們可能有一組「高級」功能,我們僅針對付費客戶啟用。或者我們可能有一組「alpha」功能,僅供內部使用者使用,以及另一組「beta」功能,僅供內部使用者和 beta 使用者使用。我將這種為一組內部或 beta 使用者開啟新功能的技術稱為香檳早午餐,一個提早「暢飲自己的香檳」的機會。

香檳早午餐在許多方面類似於金絲雀發布。兩者之間的區別在於,金絲雀發布功能會公開給隨機選取的使用者群組,而香檳早午餐功能會公開給特定使用者群組。

當用於管理僅公開給高級使用者的功能時,與其他類別的功能標記相比,權限標記的使用壽命可能非常長,長達數年。由於權限是特定於使用者的,因此權限標記的切換決策將永遠是逐次請求進行,這使得它成為一個非常動態的標記。

管理不同類別的開關

現在我們有了標記分類方案,我們可以討論動態性和長壽性的這兩個面向如何影響我們使用不同類別的功能標記。

靜態與動態開關

進行執行時期路由決策的標記必定需要更精密的標記路由器,以及針對這些路由器進行更複雜的設定。

對於簡單的靜態路由決策,標記設定可以是每個功能的簡單開啟或關閉,而標記路由器僅負責將該靜態開啟/關閉狀態傳遞到標記點。如前所述,其他類別的標記更具動態性,並且需要更精密的標記路由器。例如,實驗標記的路由器會動態為特定使用者做出路由決策,也許會使用某種基於該使用者 ID 的一致群組演算法。此標記路由器不會從設定中讀取靜態標記狀態,而是需要讀取某種類型的群組設定,定義實驗群組和控制群組應有多大等事項。該設定將用作群組演算法的輸入。

我們稍後將深入探討管理此標記設定的不同方法。

長駐開關與暫態開關

我們也可以將我們的切換類別區分為本質上是暫時性的與長期的,並且可能持續數年。此區別應對我們實作功能的切換點方法有很大的影響。如果我們新增一個幾天後就會移除的版本切換,那麼我們可能會使用在切換路由器上執行簡單的 if/else 檢查的切換點。這是我們先前在樣條網格化範例中所做的

function reticulateSplines(){
  if( featureIsEnabled("use-new-SR-algorithm") ){
    return enhancedSplineReticulation();
  }else{
    return oldFashionedSplineReticulation();
  }
}

然而,如果我們正在建立一個新的權限切換,其切換點預計會持續很長一段時間,那麼我們肯定不希望透過隨意散佈 if/else 檢查來實作這些切換點。我們需要使用更易於維護的實作技術。

實作技術

功能旗幟似乎會產生相當混亂的切換點程式碼,而這些切換點也傾向於在整個程式碼庫中擴散。在程式碼庫中的任何功能旗幟中控制此趨勢非常重要,如果旗幟會長期存在,則至關重要。有一些實作模式和實務有助於減少此問題。

將決策點與決策邏輯解耦

功能切換的一個常見錯誤是將做出切換決定的位置(切換點)與決策背後的邏輯(切換路由器)結合在一起。我們來看一個範例。我們正在開發下一代電子商務系統。我們的新功能之一將允許使用者透過按一下訂單確認電子郵件(又稱發票電子郵件)中的連結來輕鬆取消訂單。我們使用功能旗幟來管理所有下一代功能的推出。我們的初始功能標記實作如下

invoiceEmailer.js

  const features = fetchFeatureTogglesFromSomewhere();

  function generateInvoiceEmail(){
    const baseEmail = buildEmailForInvoice(this.invoice);
    if( features.isEnabled("next-gen-ecomm") ){ 
      return addOrderCancellationContentToEmail(baseEmail);
    }else{
      return baseEmail;
    }
  }

在產生發票電子郵件時,我們的 InvoiceEmailler 會檢查 next-gen-ecomm 功能是否已啟用。如果是,則電子郵件發送器會在電子郵件中新增一些額外的訂單取消內容。

雖然這看起來像是一個合理的做法,但它非常脆弱。關於是否在我們的發票電子郵件中包含訂單取消功能的決定,直接連接到相當廣泛的 next-gen-ecomm 功能,更不用說使用神奇字串了。為什麼發票電子郵件程式碼需要知道訂單取消內容是下一代功能組的一部分?如果我們想在不公開訂單取消的情況下開啟下一代功能的某些部分會發生什麼情況?反之亦然?如果我們決定只對某些使用者推出訂單取消怎麼辦?隨著功能的開發,這些類型的「切換範圍」變更很常見。另外請記住,這些切換點傾向於在整個程式碼庫中擴散。由於我們目前的方法,由於切換決策邏輯是切換點的一部分,因此對該決策邏輯的任何變更都需要追蹤程式碼庫中已散布的所有這些切換點。

幸運的是,軟體中的任何問題都可以透過新增一層間接解決。我們可以像這樣將切換決策點與該決策背後的邏輯分離

featureDecisions.js

  function createFeatureDecisions(features){
    return {
      includeOrderCancellationInEmail(){
        return features.isEnabled("next-gen-ecomm");
      }
      // ... additional decision functions also live here ...
    };
  }

invoiceEmailer.js

  const features = fetchFeatureTogglesFromSomewhere();
  const featureDecisions = createFeatureDecisions(features);

  function generateInvoiceEmail(){
    const baseEmail = buildEmailForInvoice(this.invoice);
    if( featureDecisions.includeOrderCancellationInEmail() ){
      return addOrderCancellationContentToEmail(baseEmail);
    }else{
      return baseEmail;
    }
  }

我們引入了 FeatureDecisions 物件,它充當任何功能切換決策邏輯的收集點。我們為程式碼中的每個特定切換決策在這個物件上建立一個決策方法,在本例中,「我們是否應該在發票電子郵件中包含訂單取消功能」由 includeOrderCancellationInEmail 決策方法表示。目前,決策「邏輯」是一個平凡的傳遞,用於檢查 next-gen-ecomm 功能的狀態,但現在隨著該邏輯的演進,我們有一個單一的地方來管理它。每當我們想要修改特定切換決策的邏輯時,我們都有單一的地方可以去。我們可能希望修改決策的範圍,例如由哪個特定功能標誌控制決策。或者,我們可能需要修改決策的原因,從由靜態切換設定檔驅動轉變為由 A/B 測試或運作考量(例如我們某些訂單取消基礎架構中斷)驅動。在所有情況下,我們的發票電子郵件發送器都可以完全不知道該切換決策是如何或為何做出的。

決策反轉

在先前的範例中,我們的發票電子郵件發送器負責詢問功能標記基礎架構應如何執行。這表示我們的發票電子郵件發送器有一個額外的概念需要了解,即功能標記,以及一個額外的模組與其相結合。這使得發票電子郵件發送器更難以獨立使用和思考,包括更難測試。隨著時間推移,功能標記在系統中變得越來越普遍,我們將看到越來越多的模組與功能標記系統結合,作為全域相依性。這並非理想情況。

在軟體設計中,我們通常可以透過套用控制反轉來解決這些結合問題。在這個案例中也是如此。以下說明我們如何將發票電子郵件發送器與功能標記基礎架構解耦

invoiceEmailer.js

  function createInvoiceEmailler(config){
    return {
      generateInvoiceEmail(){
        const baseEmail = buildEmailForInvoice(this.invoice);
        if( config.includeOrderCancellationInEmail ){
          return addOrderCancellationContentToEmail(email);
        }else{
          return baseEmail;
        }
      },
  
      // ... other invoice emailer methods ...
    };
  }

featureAwareFactory.js

  function createFeatureAwareFactoryBasedOn(featureDecisions){
    return {
      invoiceEmailler(){
        return createInvoiceEmailler({
          includeOrderCancellationInEmail: featureDecisions.includeOrderCancellationInEmail()
        });
      },
  
      // ... other factory methods ...
    };
  }

現在,我們的 InvoiceEmailler 不再接觸 FeatureDecisions,而是透過 config 物件在建構時將這些決定注入其中。InvoiceEmailler 現在完全不了解功能標記。它只知道其行為的某些面向可以在執行階段進行設定。這也讓測試 InvoiceEmailler 的行為變得更容易,我們可以透過在測試期間傳遞不同的設定選項,來測試它產生電子郵件的方式,無論是否包含訂單取消內容

describe( 'invoice emailling', function(){
  it( 'includes order cancellation content when configured to do so', function(){
    // Given 
    const emailler = createInvoiceEmailler({includeOrderCancellationInEmail:true});

    // When
    const email = emailler.generateInvoiceEmail();

    // Then
    verifyEmailContainsOrderCancellationContent(email);
  };

  it( 'does not includes order cancellation content when configured to not do so', function(){
    // Given 
    const emailler = createInvoiceEmailler({includeOrderCancellationInEmail:false});

    // When
    const email = emailler.generateInvoiceEmail();

    // Then
    verifyEmailDoesNotContainOrderCancellationContent(email);
  };
});

我們還導入了一個 FeatureAwareFactory 來集中建立這些已注入決策的物件。這是通用相依性注入模式的應用。如果我們的程式碼庫中啟用了相依性注入系統,我們可能會使用該系統來實作此方法。

避免條件式

在我們的範例中,我們的切換點是使用 if 陳述式實作的。對於一個簡單、短暫的切換,這可能很有道理。然而,在功能需要多個切換點,或您預期切換點會長期存在的情況下,不建議使用點狀條件式。一個更易於維護的替代方案是使用某種策略模式實作替代程式碼路徑

invoiceEmailler.js

  function createInvoiceEmailler(additionalContentEnhancer){
    return {
      generateInvoiceEmail(){
        const baseEmail = buildEmailForInvoice(this.invoice);
        return additionalContentEnhancer(baseEmail);
      },
      // ... other invoice emailer methods ...
  
    };
  }

featureAwareFactory.js

  function identityFn(x){ return x; }
  
  function createFeatureAwareFactoryBasedOn(featureDecisions){
    return {
      invoiceEmailler(){
        if( featureDecisions.includeOrderCancellationInEmail() ){
          return createInvoiceEmailler(addOrderCancellationContentToEmail);
        }else{
          return createInvoiceEmailler(identityFn);
        }
      },
  
      // ... other factory methods ...
    };
  }

在此,我們透過允許我們的發票電子郵件發送器使用內容增強功能進行設定,來套用策略模式。FeatureAwareFactory 在建立發票電子郵件發送器時會選取策略,並由其 FeatureDecision 引導。如果電子郵件中應包含訂單取消,則它會傳入一個增強器函數,將該內容新增到電子郵件中。否則,它會傳入一個 identityFn 增強器,該增強器沒有任何效果,只會將電子郵件傳回,而不會進行修改。

開關組態

動態路由與動態組態

稍早,我們將功能旗標區分為:對於給定的程式碼部署而言,其切換路由決策基本上是靜態的,與其決策在執行階段會動態變化的那些。重要的是要注意,旗標的決策在執行階段可能會以兩種方式變更。首先,類似 Ops 切換之類的東西可能會動態地從開啟重新設定為關閉,以回應系統中斷。其次,某些類別的切換,例如權限切換和實驗切換,會根據某些要求內容(例如哪個使用者提出要求)為每個要求做出動態路由決策。前者是透過重新設定而動態的,而後者本質上是動態的。這些本質上動態的切換可能會做出高度動態的決策,但仍具有相當靜態的設定,可能只能透過重新部署進行變更。實驗切換就是這種類型功能旗標的一個範例,我們並不需要在執行階段修改實驗的參數。事實上,這麼做可能會使實驗在統計上無效。

優先使用靜態組態

如果功能旗標的性質允許,則最好透過原始碼控制和重新部署來管理切換設定。透過原始碼控制來管理切換設定,我們可以獲得與將原始碼控制用於基礎架構即程式碼等事物時相同的優點。它可以讓切換設定與被切換的程式碼庫並存,這會帶來很大的好處:切換設定將會以與程式碼變更或基礎架構變更完全相同的方式,在您的持續傳遞管道中移動。這能發揮持續傳遞的全部優點,也就是在各種環境中以一致的方式驗證可重複的建置。它也能大幅降低功能旗標的測試負擔。驗證版本在切換關閉和開啟時會如何執行,需求較低,因為該狀態已內建到版本中,而且不會變更(至少對於較不動態的旗標而言)。切換設定在原始碼控制中並存的另一個好處是,我們可以輕鬆查看先前版本中切換的狀態,而且在需要時可以輕鬆重新建立先前版本。

管理開關組態的方法

雖然靜態配置較為理想,但在某些情況下,例如 Ops 切換,則需要更動態的方法。讓我們來看看一些管理切換配置的選項,這些選項的範圍從簡單但較不具動態性的方法到一些高度複雜但伴隨著許多額外複雜性的方法。

硬編碼開關組態

最基本的技術(可能如此基本以至於不被視為功能旗標)就是簡單地註解或取消註解程式碼區塊。例如

function reticulateSplines(){
  //return oldFashionedSplineReticulation();
  return enhancedSplineReticulation();
}

比註解方法稍微複雜一點的是使用預處理器的 #ifdef 功能(如果可用)。

由於這種硬編碼類型不允許動態重新配置切換,因此它只適用於我們願意遵循部署程式碼模式以重新配置旗標的功能旗標。

參數化開關組態

硬編碼配置提供的建置時間配置對於許多使用案例來說不夠靈活,包括許多測試場景。一種簡單的方法,至少允許重新配置功能旗標而無需重新建置應用程式或服務,就是透過命令列引數或環境變數指定切換配置。這是一種簡單且歷久不衰的切換方法,早在任何人將此技術稱為功能切換或功能標記之前就已經存在。然而它有其限制。在大量程序中協調配置可能會變得難以控制,而對切換配置的變更需要重新部署或至少重新啟動程序(而且重新配置切換的人員可能也需要伺服器的特權存取權)。

開關組態檔案

另一個選項是從某種結構化檔案中讀取切換配置。這種切換配置方法通常會作為更通用的應用程式設定檔的一部分而開始。

使用切換設定檔,您現在可以透過變更檔案,而不是重新建置應用程式碼本身,來重新設定功能旗標。然而,儘管在大部分情況下您不需要重新建置應用程式來切換功能,您可能仍然需要重新部署才能重新設定旗標。

應用程式資料庫中的開關組態

一旦達到一定規模,使用靜態檔案來管理切換設定可能會變得繁瑣。透過檔案修改設定相對繁瑣。確保伺服器機隊的一致性成為一項挑戰,讓變更保持一致性更是如此。為了應對此問題,許多組織將切換設定移至某種類型的集中式儲存,通常是現有的應用程式資料庫。這通常會伴隨著建置某種形式的管理員 UI,讓系統操作員、測試人員和產品經理可以檢視和修改功能旗標及其設定。

分散式開關組態

使用已經是系統架構一部分的一般用途資料庫來儲存切換設定非常常見;這是引入功能旗標並開始獲得關注後一個顯而易見的去處。然而,現今有一種特殊用途的階層式鍵值儲存,更適合管理應用程式設定,例如 Zookeeper、etcd 或 Consul 等服務。這些服務形成一個分散式叢集,為連接到叢集的所有節點提供環境設定的共用來源。設定可以在需要時動態修改,而且叢集中的所有節點都會自動收到變更通知,這是一個非常方便的附加功能。使用這些系統管理切換設定表示我們可以在機隊中的每個節點上使用切換路由器,根據整個機隊協調的切換設定做出決策。

其中一些系統(例如 Consul)附帶一個管理員 UI,提供管理切換設定的基本方式。然而,在某些時候,通常會建立一個小型自訂應用程式來管理切換設定。

覆寫組態

到目前為止,我們的討論假設所有設定都由單一機制提供。許多系統的實際情況更複雜,具有來自不同來源的設定覆寫層。使用切換設定,通常會有預設設定以及特定於環境的覆寫。這些覆寫可能來自像額外設定檔一樣簡單的東西,或像 Zookeeper 叢集一樣複雜的東西。請注意,任何特定於環境的覆寫都與持續傳遞的理想相悖,即在整個傳遞管線中具有完全相同的位元和設定流動。實用主義通常要求使用一些特定於環境的覆寫,但盡可能讓您的可部署單元和設定與環境無關,將導致更簡單、更安全的管線。當我們討論測試功能切換系統時,我們將很快重新探討這個主題。

每個請求的覆寫

環境特定組態覆寫的另一種替代方法是允許透過特殊 cookie、查詢參數或 HTTP 標頭,在每個請求的基礎上覆寫開關的開啟/關閉狀態。這比完整的組態覆寫有幾個優點。如果服務是負載平衡的,您仍然可以確信無論您使用哪個服務執行個體,都會套用覆寫。您還可以在不影響其他使用者的情況下,在生產環境中覆寫功能標記,而且您不太可能意外地保留覆寫。如果每個請求的覆寫機制使用持續性 cookie,則測試您系統的人員可以設定他們自己的自訂開關覆寫組,這些組會持續套用在他們的瀏覽器中。

這種每個請求方法的缺點是,它引入了好奇或惡意的最終使用者可能會自行修改功能開關狀態的風險。有些組織可能不喜歡某些未發布功能可能對決心夠堅定的某一方公開存取的想法。對您的覆寫組態進行加密簽署是一種減輕這種疑慮的選項,但無論如何,這種方法都會增加您功能切換系統的複雜性 - 和攻擊面。

我在 這篇文章 中詳細說明了基於 cookie 的覆寫技術,並且還 描述了我和 Thoughtworks 同事共同開放原始碼的 Ruby 實作。

使用具功能旗標的系統

雖然功能切換絕對是有用的技術,但它也會帶來額外的複雜性。在使用功能標記系統時,有一些技巧可以幫助簡化工作。

公開目前的開關組態

將建置/版本號碼嵌入已部署的成品,並在某處公開該元資料,以便開發人員、測試人員或操作員可以找出特定程式碼在給定環境中執行的內容,這一直是有用的做法。相同的概念應該應用於功能標記。任何使用功能標記的系統都應該公開某種方式,讓操作員可以找出開關組態的目前狀態。在以 HTTP 為導向的 SOA 系統中,這通常是透過某種元資料 API 端點或端點來完成的。例如,請參閱 Spring Boot 的 Actuator 端點

善用結構化的開關組態檔案

將基本切換設定儲存在某種結構化、人類可讀取的檔案中(通常採用 YAML 格式)並透過原始碼控制進行管理,是常見的做法。我們可以從此檔案中衍生出一些額外的好處。為每個切換包含人類可讀取的說明,出乎意料地有用,特別是對於由核心傳遞團隊以外的人員管理的切換。在生產中斷事件期間,當您嘗試決定是否啟用 Ops 切換時,您會希望看到什麼:basic-rec-algo 還是 「使用簡化的推薦演算法。這很快,而且會減少後端系統的負載,但準確度遠低於我們的標準演算法。」?有些團隊也會選擇在切換設定檔中包含額外資料,例如建立日期、主要開發人員聯絡人,甚至是預計為短期存在的切換的到期日。

以不同的方式管理不同的開關

如前所述,功能切換有各種不同特性的類別。應接受這些差異,並以不同的方式管理不同的切換,即使所有不同的切換都可能使用相同的技術機制進行控制。

讓我們重新檢視我們之前電子商務網站的範例,其首頁有一個推薦產品區段。最初,我們可能在開發時將該區段置於發布切換之後。然後,我們可能已將其移至實驗切換之後,以驗證它有助於推動營收。最後,我們可能會將其移至 Ops 切換之後,以便在負載極端時將其關閉。如果我們遵循先前關於從切換點中分離決策邏輯的建議,則切換類別的這些差異根本不應對切換點程式碼產生任何影響。

然而,從功能旗幟管理的角度來看,這些轉換絕對應該產生影響。作為從發布切換過渡到實驗切換的一部分,切換的設定方式將會改變,而且可能會移至不同的區域,或許是移至管理員 UI,而不是原始碼控制中的 yaml 檔案。產品人員現在可能會管理設定,而不是開發人員。同樣地,從實驗切換過渡到 Ops 切換將表示切換的設定方式、該設定所在的位置以及管理設定的人員將發生另一項變更。

功能開關會引入驗證複雜度

使用功能標記系統,我們的持續傳遞流程會變得更複雜,特別是在測試方面。當同一個成品在 CD 管道中移動時,我們通常需要測試多個程式碼路徑。為了說明原因,想像我們正在運送一個系統,如果切換開啟,它可以使用新的最佳化稅額計算演算法,否則繼續使用我們現有的演算法。在給定的可部署成品在我們的 CD 管道中移動時,我們無法知道切換是否會在生產中某個時間點開啟或關閉,畢竟,這就是功能旗幟的重點。因此,為了驗證所有最終可能在生產中運作的程式碼路徑,我們必須在兩個狀態下測試我們的成品:切換開啟和關閉。

我們可以看到,在播放中使用單一切換會引入至少加倍我們部分測試的要求。在播放中使用多個切換時,我們會遇到指數爆炸的可能切換狀態。驗證這些狀態中的每個狀態的行為將是一項艱鉅的任務。這可能會導致一些人對專注於測試的人員對功能標記產生一些健康的懷疑。

幸運的是,情況並不像某些測試人員最初想像的那麼糟糕。雖然功能標記的候選版本確實需要使用一些切換配置進行測試,但沒有必要測試 *每個* 可能的組合。大多數功能標記不會相互作用,而且大多數版本不會涉及對多於一個功能標記的配置進行更改。

一個好的慣例是在功能標記關閉時啟用現有或舊行為,在功能標記開啟時啟用新的或未來的行為。

那麼,團隊應該測試哪些功能切換配置?最重要的是測試您預計在生產中啟用的切換配置,這意味著當前的生產切換配置加上您打算發布並切換為開啟的任何切換。在您打算發布的那些切換也切換為關閉的情況下,測試備用配置也很明智。為了避免在未來的版本中出現任何意外的回歸,許多團隊還會對所有切換都切換為開啟的情況執行一些測試。請注意,只有當您堅持使用切換語義慣例時,此建議才有意義,其中在功能關閉時啟用現有或舊行為,在功能開啟時啟用新的或未來的行為。

如果您的功能標記系統不支援執行時期配置,那麼您可能必須重新啟動您正在測試的程序以切換切換,或者更糟的是,將人工製品重新部署到測試環境中。這可能會對驗證程序的週期時間產生非常不利的影響,而這反過來又會影響 CI/CD 提供的所有重要回饋迴路。為了避免此問題,請考慮公開一個端點,允許動態重新配置功能標記的內存。當您使用像實驗切換這樣的東西時,這些類型的覆寫變得更加必要,因為在切換的兩個路徑中進行練習更加繁瑣。

動態重新配置特定服務實例的能力是一個非常尖銳的工具。如果使用不當,它會在共享環境中造成很多痛苦和混亂。此功能應僅由自動化測試使用,並且可能作為手動探索性測試和除錯的一部分。如果需要在生產環境中使用更通用的切換控制機制,最好使用如上文切換配置部分中所討論的真實分散式配置系統來建置。

放置開關的位置

邊緣的開關

對於需要每個請求上下文的切換類別(實驗切換、許可切換),將切換點放置在系統的邊緣服務中是有意義的 - 也就是說,向最終使用者呈現功能的公開 Web 應用程式。這是使用者的個別請求首次進入您的網域的地方,因此您的切換路由器擁有最多的可用內容,可以根據使用者及其請求做出切換決策。將切換點放置在系統邊緣的一個好處是,它可以將繁瑣的條件切換邏輯排除在系統的核心之外。在許多情況下,您可以將切換點放置在您呈現 HTML 的地方,就像在這個 Rails 範例中

someFile.erb

  <%= if featureDecisions.showRecommendationsSection? %>
    <%= render 'recommendations_section' %>
  <% end %>

當您控制對尚未準備好發布的新使用者介面功能的存取時,在邊緣放置切換點也很有意義。在此背景下,您可以再次使用切換來控制存取,該切換只顯示或隱藏使用者介面元素。舉例來說,您也許正在建立使用 Facebook 登入應用程式的功能,但尚未準備好向使用者推出。此功能的實作可能涉及架構中各個部分的變更,但您可以使用使用者介面層中的簡單功能切換控制功能的公開,該切換會隱藏「使用 Facebook 登入」按鈕。

有趣的是,請注意,對於這些類型的功能標幟,大部分未發布的功能本身實際上可能公開,但位於使用者無法發現的網址。

核心的開關

有其他類型的較低層級切換必須放置在架構中更深的位置。這些切換通常本質上是技術性的,並控制某些功能在內部如何實作。一個範例是「發布切換」,它控制是否在第三方 API 前面使用新的快取基礎架構,或只是將要求直接路由到該 API。在這些案例中,將這些切換決策定位在功能正在切換的服務中是唯一明智的選項。

管理功能開關的持有成本

功能標幟有快速倍增的趨勢,特別是在首次引入時。它們很實用且建置成本低,因此經常建立很多。然而,切換確實會產生持有成本。它們要求您在程式碼中引入新的抽象或條件式邏輯。它們也引入了顯著的測試負擔。Knight Capital Group 的 4.6 億美元錯誤 是一個警示故事,說明當您無法正確管理功能標幟時(以及其他事項)可能會出什麼問題。

精明的團隊將其功能切換視為具有持有成本的庫存,並努力將該庫存維持在最低。

精明的團隊將其程式碼庫中的功能切換視為具有持有成本的庫存,並尋求將該庫存保持在最低限度。為了讓功能旗標的數量保持在可控範圍內,團隊必須主動移除不再需要的功能旗標。有些團隊會訂定一個規則,即在首次引入發佈切換時,務必將切換移除任務新增到團隊待辦事項中。其他團隊則會為其切換設定「到期日」。有些團隊甚至會建立「定時炸彈」,如果功能旗標在到期日後仍然存在,就會導致測試失敗(甚至拒絕啟動應用程式!)。我們也可以採用精實方法來減少庫存,限制系統在任何時間點允許擁有的功能旗標數量。一旦達到該限制,如果有人想要新增新的切換,他們首先需要執行移除現有旗標的工作。


致謝

感謝 Brandon Byars 和 Max Lincoln 提供詳細的回饋和建議,以撰寫本文的早期草稿。非常感謝 Martin Fowler 的支持、建議和鼓勵。感謝我的同事 Michael Wongwaisayawan 和 Leo Shaw 進行編輯審查,以及感謝 Fernanda Alcocer 讓我的圖表看起來不那麼醜陋。

重大修訂

2017 年 10 月 9 日:將功能旗標標示為同義詞

2016 年 2 月 8 日:最後部分:完成文章發佈

2016 年 2 月 5 日:第 7 部分:使用切換系統

2016 年 2 月 2 日:第 6 部分:切換設定

2016 年 1 月 28 日:第 5 部分:實作技術

2016 年 1 月 27 日:第 4 部分:管理不同類別的切換

2016 年 1 月 22 日:第 3 部分:操作和權限切換

2016 年 1 月 21 日:第二部分 - 發佈和實驗切換

2016 年 1 月 19 日:發佈第一部分 - 切換故事