微服务开发实战互联网架构DevOps实践

[翻译]功能切换(又称功能标志)

2019-06-21  本文已影响32人  Wales_Kuo

原文为martin fowler的个人网站中的:Feature Toggles (aka Feature Flags)

功能切换(通常也称为功能标志)是一种强大的技术,允许团队在不更改代码的情况下修改系统行为。功能切换可以分为多种使用类别,在实现和管理功能切换时必须考虑选用适合的类别。功能切换会带来一定的复杂度,对于这种复杂度我们可以通过有效的管理切换配置来控制,也可以通过限制系统中切换的数量以降低复杂度。

作者:

Pete Hodgson
Pete Hodgson是ThoughtWorks的顾问,他在过去的几年里帮助团队在可持续交付高质量软件方面变得非常出色。他特别热衷于基于互联网的移动端、Ruby、敏捷、软件的功能性方法和啤酒。
类似的文章有:连续交付应用架构

“功能切换”是一组模式,可以帮助团队快速而安全地向用户提供新功能。在本文中,我们将从一个短篇故事开始,介绍一些功能切换的典型的场景。然后本文将深入研究细节,介绍功能切换的特定的模式和实践。有了这些模式和实践团队将更加有效的使用功能切换。

功能切换(Feature Toggles)也被称为功能标志(Feature Flags)、功能位(Feature Bits)或功能翻转(Feature Flippers)。这些词都说明的是同一件事。在本文中,我将交替使用功能切换和功能标志。


一个切换的故事

想象一下这个场景:你正在参与一个复杂的城市规划模拟游戏的开发工作,这个游戏由几个团队分工开发。你的团队负责核心模拟引擎。您的任务是提高样条网格算法(Spline Reticulation algorithm)的效率。你现在需要花几周的时间对实现进行彻底检查。同时,你的团队中其他成员将需要继续在代码库进行其他部分的开发。

基于以前合并两个长期分支是的痛苦经历你不希望避免为此建立独立的代码分支。并且,为了整体团队的输出你决定整个团队仍将继续在主干分支上工作。同时在做样条网络算法改进的开发人员将使用功能切换来防止他们的工作影响团队的其余部分。

特征标志的诞生

这是研究算法的开发人员引入的第一个变化:

  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转换为一个切换点(Toggle Point)。现在,如果有人正在研究新算法,他们可以通过使用useNewAlgorithm=true来启用“新算法”。

标志动态化

过了一会,新算法开发人员准备通过一些集成测试用例来测试他们的新算法。并且不能影响旧算法的集成测试的运行。开发人员需要能够动态地启用或禁用该功能,这意味着注释或取消注释的已经不能适应现在的情况了。

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

我们现在介绍featureIsEnabled方法,一个切换路由器它可以用来动态控制代码执行路径。在现实中有很多种方法可以用来实现切换路由器,从简单的内存式到高度复杂的分布式系统,再到GUI。现在,我们将从一个非常简单的实现开始:

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

注意,这里使用了ES2015的method shorthand

可以基于一些默认配置(可能从配置文件中读取)创建一个新的切换路由器,但也可以动态地打开或关闭一个功能。这允许自动测试可以验证这两个算法:

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检测到)。团队现在可以自己打开cookie,并验证新功能。

金丝雀发布

基于目前的探索性试验,新的样条网络算法具有较好的效果。然而,由于它是游戏模拟引擎中如此关键的一部分,所以对于所有用户来说,仍然有一些人不愿意打开这个功能。团队决定使用他们的功能标志基础设施来执行一个金丝雀版本,只为他们总用户群中的一小部分打开新功能——一个“金丝雀”队列。

该团队通过向切换路由中添加用户群的概念来增强切换路由器的功能,用户群始终体验着一个功能的开启或关闭。一个金丝雀用户群是通过随机抽样1%的用户群创建的——可能使用用户ID的模件。这个金丝雀用户群将始终启用该功能,而其他99%的用户群仍使用旧算法。对两个组的关键业务指标(用户参与度、总收入等)进行监控,以确保新算法不会对用户行为产生负面影响。一旦团队确信新功能没有不良影响,他们就会修改切换配置,为整个用户群打开它。

A/B测试

团队的产品经理了解这种方法,非常兴奋。她建议团队使用类似的机制来执行一些A/B测试。一直存在着一个争论,关于修改犯罪率算法后是否会增加或减少游戏的可玩性。他们现在有能力利用数据来解决辩论。他们计划推出一个廉价的实现,它捕获了这个想法的本质,并用一个特征标志进行控制。他们将为一个相当大的用户群打开该功能,然后研究这些用户的行为与“控制”用户群相比。这种方法将允许团队基于数据而不是HiPPOs来解决有争议的产品特性。

这个简短的场景旨在说明功能切换的基本概念,同时也强调这个核心功能可以有多少不同的应用程序。现在我们已经看到了这些应用程序的一些示例,让我们更深入地了解一下。我们将探讨不同类别的切换,看看是什么使它们不同。我们将讨论如何编写可维护的切换代码,并最终共享实践,以避免功能切换系统的一些缺陷。


切换的类别

我们已经看到了功能切换所提供的基本功能——能够在一个可部署单元中发布替代代码路径,并在运行时在它们之间进行选择。上面的场景还表明,该工具可以在不同的上下文中以不同的方式使用。将所有功能切换集中到同一个桶中是很有诱惑力的,但这是一条危险的路。不同种类的切换的设计是完全不同的,以同样的方式管理它们会导致非常痛苦。

功能切换可以通过两个主要维度来分析:功能切换的持续时间,切换决策的灵活性。还有其他的因素需要考虑——例如,谁来管理功能切换——但是我认为持续时长和灵活性是两大因素,可以帮助指导如何管理切换。

让我们通过这两个维度的透镜来考虑各种类型的切换,看看它们适合的场景。

发布toggles

这些是用于为持续交付的团队启用基于主干的开发的功能标志。它们允许将正在开发的特性被提交到共享的集成分支(例如主分支或主干),同时仍然允许该分支随时部署到生产环境中。释放切换允许将不完整和未测试的代码路径作为潜在代码(可能永远不会打开)发送到生产环境中。

发布toggles允许将不完整和未测试的代码路径作为潜在代码(可能永远不会打开)发布到生产环境中。

它们允许正在进行的特性被提交到共享的集成分支(例如主分支或主干),同时仍然允许该分支随时部署到生产环境中。释放切换允许将不完整和未测试的代码路径作为潜在代码(可能永远不会打开)发送到生产环境中。

产品经理也可以使用相同方法的以产品为中心的版本,以防止半完整的产品功能暴露给最终用户。例如,电子商务网站的产品经理可能不想让用户看到一个新的预计发货日期功能,该功能只适用于网站的一个发货合作伙伴,而宁愿等到所有发货合作伙伴都实现了该功能。产品经理可能还有其他原因不想公开特性,即使它们已经完全实现和测试。例如,功能发布可能与市场营销活动相协调。以这种方式使用发布切换是实现“将[功能]发布与[代码]部署分离”的连续交付原则的最常见方法。

发布切换本质上是可传递的。它们通常不会停留超过一到两周,尽管以产品为中心的切换可能需要保持更长时间。发布切换的切换决策通常是非常静态的。给定发布版本的每个切换决策都是相同的,通过使用切换配置更改推出新的发布来更改切换决策通常是完全可以接受的。

实验切换

实验切换用于执行多变量或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对象,它充当任何功能切换决策逻辑的集合点。我们为代码中的每个特定切换决策在此对象上创建一个决策方法-在本例中,“我们是否应在发票电子邮件中包含订单取消功能”由includeOrderCancellationEmail决策方法表示。现在,决定“逻辑”只是检查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来集中创建这些决策注入对象。这是一般依赖项注入模式的一个应用程序。如果在我们的代码库中有一个DI系统,那么我们可能会使用这个系统来实现这种方法。

避免条件判断

在我们的示例中,到目前为止,我们的切换点是使用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增强器——这个增强器没有任何效果,只是不做任何修改就将电子邮件返回。


切换配置

动态路由与动态配置

之前,我们将特性标志划分为那些通过部署给定代码的静态切换路由决策和那些在运行时决策的动态切换路由决策。需要注意的是,这两种方法都可以在运行时更改标志的决策。首先,像运维切换这样的东西可能会动态地 重新配置从响应到系统中断的ON到OFF。其次,一些类型的切换(如许可切换和实验切换)根据某些请求上下文(如哪个用户发出请求)为每个请求做出动态路由决策。前者通过重新配置是动态的,而后者则是固有的动态的。这些固有的动态切换可能会做出高度动态的 决策,但仍然具有 配置。但是这是非常静态的切换,可能只有通过重新部署才能改变。实验切换就是这种功能标志的一个例子——我们不需要在运行时修改实验的参数。事实上,这样做可能会使实验在统计上无效。

首选静态配置

如果功能标志的性质允许,最好通过源代码管理和重新部署来管理切换配置。通过源代码管理来管理切换配置给我们带来的好处与通过将源代码管理用于诸如基础结构和代码之类的事情所获得的好处相同。它可以允许切换配置与被切换的代码库共存,这提供了一个非常大的优势:切换配置将以与代码更改或基础结构更改完全相同的方式在您的持续交付Pipeline中移动。这就充分发挥了CD可重复构建的优势,这些构建在不同环境中以一致的方式进行验证。它还大大减少了特性标志的测试负担。在关闭和打开切换的情况下,不需要验证发布将如何执行,因为该状态已烘焙到发布中,并且不会被更改(至少对于较少的动态标志)。切换配置在源代码管理中并排运行的另一个好处是,我们可以很容易地看到以前版本中切换的状态,并在需要时轻松地重新创建以前的版本。

管理切换配置的方法

虽然静态配置是优选的,但是存在诸如运维切换的情况,其中需要更动态的方法。让我们来看看管理切换配置的一些选项,从简单但不太动态的方法到高度复杂但具有更多复杂性的方法。

硬编码切换配置

最基本的技术——也许是如此基本以至于不被认为是一个特性标志——就是简单地对代码块进行注释或取消注释。例如:

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

比评论方法更复杂的是使用预处理器的#ifdef功能(如果可用)。

因为这种类型的硬编码不允许动态重新配置切换,所以它只适用于我们愿意遵循部署代码的模式来重新配置标志的功能标志。

参数化切换配置

硬编码配置提供的构建时配置对于许多用例来说不够灵活,包括许多测试场景。一种简单的方法是通过命令行参数或环境变量指定切换配置,这种方法至少允许在不重新构建应用程序或服务的情况下重新配置功能标志。这是一种简单且久负盛名的切换方法,早在任何人将此技术称为特征切换或特征标记之前就已经存在。但是它也有局限性。在大量进程之间协调配置会变得很难,而对切换配置的更改需要重新部署,或者至少需要重新启动进程(也可能需要重新配置切换的人对服务器的访问权限)。

切换配置文件

另一种选择是从某种结构化文件中读取切换配置。这种方法通常会将配置切换为作为更通用的应用程序配置文件的一部分开始使用。

通过切换配置文件,你现在可以通过更改该文件而不是重新构建应用程序代码本身来重新配置功能标志。但是,尽管在大多数情况下,您不需要重新构建应用程序来切换功能,但仍可能需要执行重新部署以重新配置标志。

在应用程序配置数据库切换

一旦达到一定的规模,使用静态文件管理切换配置会变得很麻烦。通过文件修改配置相对比较麻烦。确保跨服务器组的一致性成为一项挑战,使更改更加一致。为了应对这种情况,许多组织将切换配置移动到某种类型的集中式存储中,通常是一个现有的应用程序数据库。这通常伴随着某种形式的管理用户界面的构建,它允许系统操作员、测试人员和产品经理查看和修改特性标志及其配置。

分布式切换配置

使用已经是系统体系结构的一部分的通用DB来存储切换配置是非常常见的;一旦引入特征标志并开始获得牵引力,它便是显而易见的去处。然而,现在有一种特殊用途的分层键值存储,它更适合于管理应用程序配置,比如ZooKeeper、ETCD或Consul。这些服务形成了一个分布式集群,为所有连接到集群的节点提供环境配置的共享源。配置可以在需要时进行动态修改,集群中的所有节点都会自动收到更改的通知——这是一个非常方便的额外功能。使用这些系统管理切换配置意味着我们可以在震源组中的每个节点上设置切换路由器,根据在整个震源组中协调的切换配置做出决策。

其中一些系统(如Consul)附带了一个管理用户界面,它提供了管理切换配置的基本方法。但是,在某些时候,通常会创建一个用于管理切换配置的小型自定义应用程序。

覆盖配置

到目前为止,我们的讨论已经假设,所有配置是由一个奇异的机制提供的。许多系统的实际情况更为复杂,配置的覆盖层来自不同的来源。使用切换配置时,通常会有一个默认配置和特定于环境的覆盖。这些覆盖可能来自一些简单的配置文件,或者一些复杂的配置文件,比如ZooKeeper集群。请注意,任何特定于环境的覆盖都与连续交付的理想背道而驰,即在整个交付管道中具有完全相同的位和配置流。通常实用主义要求使用一些特定于环境的覆盖,但是努力保持可部署单元和配置尽可能不受环境影响,将导致更简单、更安全的管道。当我们讨论测试功能切换系统时,我们将很快访问这个主题。

每个请求覆盖

环境特定配置的替代方法操作重写允许通过特殊的cookie、查询参数或HTTP头在每个请求的基础上重写切换的开/关状态。与完全配置覆盖相比,这有一些优势。如果一个服务是负载平衡的,那么您仍然可以确信无论您使用哪个服务实例,都将应用重写。您还可以在不影响其他用户的情况下覆盖生产环境中的功能标志,并且不太可能意外地将覆盖保留在适当的位置。如果每个请求覆盖机制使用持久cookie,那么测试您的系统的人可以配置他们自己的自定义切换覆盖集,该覆盖集将始终应用于他们的浏览器中。

这种按请求的方法的缺点是它会带来一种风险,即好奇的或恶意的最终用户可能会自己修改功能切换状态。一些组织可能会对这样的想法感到不安:一些未发布的特性可能会被足够确定的一方公开访问。加密签名您的覆盖配置是缓解这一问题的一个选项,但是不管这种方法如何,都会增加特性切换系统的复杂性和攻击面。

我在这篇文章 中详细阐述了基于cookie的覆盖的技术,并且还描述了一个Ruby实现 由我和一个思想工作的同事开源。


使用功能标记的系统

虽然功能切换绝对是一种有用的技术,但它也带来了额外的复杂性。有一些技术可以帮助在使用特征标记的系统时使生活更容易。

公开当前功能切换配置

将构建/版本号嵌入到部署的工件中,并在某个地方公开元数据,这样开发人员、测试人员或操作员就可以了解在给定环境中运行的特定代码,这始终是一种有用的实践。同样的想法也应该应用于功能标志。任何使用功能标志的系统都应该以某种方式让操作员发现切换配置的当前状态。在面向HTTP的SOA系统中,这通常通过某种元数据API端点或端点来实现。例如,请参见Spring Boot的Actuator endpoints

利用结构化切换配置文件

通常将基本切换配置存储在通过源代码管理管理的某种结构化的、人类可读的文件(通常为yaml格式)中。我们可以从这个文件中获得一些额外的好处。为每个切换包括一个人类可读的描述是非常有用的,特别是对于由核心交付团队以外的人员管理的切换。当您试图决定在生产中断事件期间是否启用操作切换时,您希望看到什么:basic-rec-algo“使用简单的推荐算法。这很快,在后端系统上产生的负载也更少,但是比我们的标准算法准确度要低吗?一些团队还选择在他们的切换配置文件中包含额外的元数据,例如创建日期、主要开发人员联系人,甚至是短期切换的到期日期。

以不同方式管理不同的切换

如前所述,有不同类型的特征切换具有不同的特征。这些差异应该被接受,不同的开关以不同的方式管理,即使所有不同的开关可以使用相同的技术机器进行控制。

让我们回顾一下我们以前的电子商务网站的例子,它在主页上有一个推荐的产品部分。最初,我们可能在开发过程中将该部分置于发布切换之后。然后,我们可能会将其转移到实验切换的后面,以验证它是否有助于推动收入增长。最后,我们可以把它移到操作开关后面,这样我们可以在极端负载下关闭它。如果我们遵循了前面关于从切换点去耦合决策逻辑的建议,那么切换类别中的这些差异应该对切换点代码没有任何影响。

但是,从功能标记管理的角度来看,这些转换绝对应该有影响。作为从释放切换到实验切换的一部分,切换的配置方式将发生变化,并可能移动到其他区域-可能是进入管理用户界面,而不是源代码管理中的yaml文件。产品人员现在可能会管理配置,而不是开发人员。同样,从“实验切换”到“操作切换”的转换意味着切换配置方式、该配置所在位置以及配置管理者的另一个变化。

功能切换会带来验证复杂性

有了特征标记的系统,我们的持续交付过程变得更加复杂,特别是在测试方面。我们通常需要在同一工件通过CD管道时测试多个代码路径。为了说明原因,假设我们正在运输一个系统,该系统可以使用新的优化税务计算算法(如果打开了切换),或者继续使用我们现有的算法。当一个给定的可部署工件在我们的CD管道中移动时,我们不知道在生产环境中是否会在某个时间点打开或关闭切换—毕竟这就是特性标记的全部意义。因此,为了验证所有可能最终在生产中使用的代码路径,我们必须在两种状态下对工件进行测试:打开和关闭切换。

我们可以看到,在使用一个切换时,这引入了一个要求,至少在某些测试上加倍。在多个切换中,我们有可能切换状态的组合爆炸。验证这些状态的行为将是一项艰巨的任务。这可能导致一些有测试焦点的人对特性标志持健康的怀疑态度。

令人欣慰的是,情况并不像一些测试人员最初想象的那样糟糕。虽然一个功能标记的候选发布版本需要用一些切换配置进行测试,但是不需要测试每个可能的组合。大多数功能标志不会相互作用,大多数版本不会涉及对多个功能标志配置的更改。

一个很好的约定是,当特征标志关闭时启用现有的或遗留的行为,并在其上启用新的或将来的行为。

那么,哪种特性切换配置应该进行团队测试?最重要的是测试切换配置,您希望它在生产中成为实时的,这意味着当前的生产切换配置加上任何切换,您打算释放翻转。测试回退配置也是明智的,在这种配置中,您想要释放的那些切换也会被关闭。为了避免在将来的版本中出现任何意外的回归,许多团队也会在打开所有切换的情况下执行一些测试。请注意,只有当您坚持使用切换语义的约定时,此建议才有意义,即在关闭某个功能时启用现有或遗留行为,在打开某个功能时启用新的或未来的行为。

那么,哪种特性切换配置应该进行团队测试?最重要的是测试切换配置,您希望它在生产中成为实时的,这意味着当前的生产切换配置加上任何切换,您打算释放翻转。测试回退配置也是明智的,在这种配置中,您想要释放的那些切换也会被关闭。为了避免在将来的版本中出现任何意外的回归,许多团队也会在打开所有切换的情况下执行一些测试。请注意,只有当您坚持使用切换语义的约定时,此建议才有意义,即在关闭某个功能时启用现有或遗留行为,在打开某个功能时启用新的或未来的行为。

动态地重新配置特定服务实例的能力是一个非常敏锐的工具。如果使用不当,在共享环境中会造成很多痛苦和困惑。此工具只能用于自动测试,也可能是手动探索性测试和调试的一部分。如果需要在生产环境中使用更通用的切换控制机制,最好使用真正的分布式配置系统来构建,如上面的切换配置部分所讨论的那样。

在哪里放置开关

在边缘切换

对于需要每个请求上下文的切换类别(实验切换、许可切换),在系统的边缘服务中放置切换点是有意义的,即公开的向最终用户提供功能的Web应用程序。这是用户的单个请求首先进入您的域的地方,因此切换路由器具有根据用户及其请求做出切换决策的最多上下文。将切换点放置在系统边缘的一个附带好处是,它可以将有条件的切换逻辑保持在系统核心之外。在许多情况下,您可以将切换点放在呈现HTML的位置,如下面的Rails示例所示:

someFile.erb

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

在边缘放置切换点在控制对尚未准备好启动的面向用户的新功能的访问时也是有意义的。在此上下文中,您可以再次使用一个只显示或隐藏UI元素的切换来控制访问。例如,您可能正在构建使用Facebook登录应用程序但还没有准备好向用户推广它。此功能的实现可能涉及到体系结构的各个部分的更改,但是您可以在UI层使用一个简单的功能切换来控制功能的暴露,该切换隐藏了“使用Facebook登录”按钮。

有趣的是,有了这些类型的功能标志,大部分未发布的功能本身可能实际上是公开的,但位于用户无法发现的URL上。

切换核心

还有其他类型的低级切换,必须在体系结构中放置得更深。这些切换通常本质上是技术性的,并且控制一些功能是如何在内部实现的。一个例子是一个发布切换,它控制是在第三方API前面使用一个新的缓存基础设施,还是直接将请求路由到该API。在这些情况下,将这些切换决策本地化到正在切换功能的服务中是唯一明智的选择。

管理特征切换的携带成本

特征标志具有快速增加的趋势,尤其是在首次引入时。它们是有用的和廉价的创造,所以经常创造很多。然而,拨动确实伴随着携带成本。它们要求您在代码中引入新的抽象或条件逻辑。它们还引入了一个重要的测试负担。Knight Capital Group的$4.6亿美元的错误是一个警告故事,说明当您不能正确管理您的功能标志时会发生什么错误(以及其他事情)。

精明的团队将他们的功能切换视为带有账面成本的库存,并尽可能地保持库存的低水平。

精明的团队将代码库中的功能切换视为库存,这会带来账面成本,并寻求尽可能降低库存。为了保持功能标志的数量可控,团队必须主动删除不再需要的功能标志。有些团队有一个规则,每当首次引入发布切换时,总是将切换移除任务添加到团队的积压工作中。其他的团队把“到期日期”放在他们的开关上。有些人甚至创建了“定时炸弹”,它将无法通过测试(甚至拒绝启动应用程序!)如果功能标志在其到期日期之后仍然存在。我们还可以应用精益方法来减少库存,对系统在任何时候都允许拥有的特性标志的数量设置限制。一旦达到这个限制,如果有人想添加一个新的切换,他们将首先需要做的工作,以删除现有的标志。


Share:

twitter facebook google

如果你觉得这篇文章有用,请分享。感谢您的反馈和鼓励。

关于类似主题的文章…

…查看以下标签:
持续交付
应用架构


致谢

感谢BrandonByars和Max Lincoln为本文的早期草稿提供了详细的反馈和建议。感谢马丁·福勒的支持、建议和鼓励。感谢我的同事Michael Wongwaisayawan和Leo Shaw的编辑评论,感谢Fernanda Alcocer让我的图表看起来不那么难看。

重大修订

2017年10月09日:突出显示功能标志作为同义词
2016年02月08日:最后部分:发表完整文章
2016年02月05日:第7部分:使用切换系统
2016年02月02日:第6部分:切换配置
2016年01月28日:第5部分:实施技术
2016年01月27日:第4部分:管理不同类别的切换
2016年01月22日:第3部分:操作和许可切换
2016年01月21日:第二期-发布和实验切换
2016年01月19日:出版了第一期:一个切换故事

上一篇下一篇

猜你喜欢

热点阅读