图片 标记 软件
A previous company had a problem: our deploys were thousands of lines in size, took nearly an hour, and were massively risky. Engineers hated deploying, which led to a backed-up queue of dozens of pull requests awaiting deployment. Product managers reasonably wanted to wait to release things until it was all done, which meant our releases could contain thousands of lines of code, any one of which could cause a catastrophic crash. The result? Incomplete work was piling up, value wasting away without delivery.
以前的一家公司遇到了一个问题:我们的部署规模为数千条生产线,耗时近一个小时,而且风险巨大。 工程师讨厌部署,这导致备份队列中有数十个请求请求等待部署。 产品经理合理地希望等待发布,直到完成为止,这意味着我们的发布可能包含数千行代码,其中任何一行都可能导致灾难性的崩溃。 结果? 未完成的工作堆积如山,价值浪费而没有交付。
Something had to change.
某些事情不得不改变。
释放部署 (Releasing Deploys)
To help resolve these issues, one of the things I implemented was technology improvements that allowed us to separate the concept of a deploy from the concept of a release.
为了帮助解决这些问题,我实施的一件事是技术改进,使我们能够将部署的概念与发布的概念分开。
Previously, the deploy was the release, as soon as the code made it to production, it was being used by millions of users. This made deployments take on incredible risk.
以前,部署是发布版本,代码一经投入生产,便已被数百万用户使用。 这使部署承担了不可思议的风险。
This directly put engineers at odds with the product manager. Engineers wanted to minimize risk through smaller deploys, but product managers wanted to release working, comprehensive solutions. Instead of working together, our lack of technology capabilities created an environment with a combative dynamic.
这直接使工程师与产品经理发生冲突。 工程师希望通过较小的部署来最大程度地降低风险,但是产品经理希望发布可行的全面解决方案。 由于缺乏合作,我们缺乏技术能力,创造了具有战斗力的环境。
We wanted to ensure that our technology could support releasing functionality at a later time than when it was deployed. Put another way, we wanted to be able to send code to production without users being able to see it, and then allow the business and product managers to own the release of features on their schedule.
我们希望确保我们的技术可以在部署之后的较晚时间内支持发布功能。 换句话说,我们希望能够在用户看不到代码的情况下将代码发送到生产环境,然后允许业务和产品经理按计划拥有功能的发布。
To accomplish this separation, I pursued the development and integration of a centralized feature flagging system as a ninja project in-between my scheduled work.
为了实现这种分离,我在预定工作之间进行了忍者项目开发和集成集中功能标记系统。
什么是功能标志? (What Are Feature Flags?)
A feature flag system, also known as a feature toggler, boils down to storing whether a feature is enabled or not and then checking it when you need to. If the feature is enabled, allow whatever it is you are toggling. If not, hide it from users.
功能标记系统(也称为功能切换器)归结为存储是否启用了功能,然后在需要时进行检查。 如果启用了该功能,则无论您要切换什么,都应允许。 如果没有,请向用户隐藏。
Code-wise, it theoretically resolves to boolean check and the execution (or not) of an accompanying code path. An example:
在代码方面,从理论上讲,它解析为布尔检查和附带代码路径的执行(或不执行)。 一个例子:
run_code_path if flag
旗 (The flag)
The flag itself is a value that the code checks for in making its determination to run or not run a specific piece of code. This is the thing that the code would check for when making a runtime determination.
标志本身是代码在确定是否运行特定代码段时检查的值。 这是代码在确定运行时时将检查的内容。
代码路径 (The code path)
The code path is the code that is being toggled or disabled/enabled.
代码路径是正在切换或禁用/启用的代码。
Perhaps it checks whether a page component can be displayed or not. Maybe it adds an extra fee to a transaction or not. Maybe it isn’t even completed, and you are just putting the skeleton in place.
也许它检查页面组件是否可以显示。 也许它会为交易增加一笔额外费用。 也许它甚至还没有完成,而您只是将骨骼放置到位。
These various reasons for hiding the code will determine the longevity of the flag, how it is used, and where it is placed, but the end mechanics are the same.
隐藏代码的各种原因将决定标志的寿命,标志的使用方式以及标志的放置位置,但是最终机制是相同的。
支票 (The check)
The check is the code that determines whether the code should run or not. It’s the if-statement that will look at the flag and run the code path.
检查是确定代码是否应该运行的代码。 if语句将查看该标志并运行代码路径。
For us, we divided the system conceptually into two different kinds of checks: system-level checks and context-level checks.
对于我们来说,我们从概念上将系统分为两种不同的检查:系统级检查和上下文级检查。
System-level checks perform checks system-wide. If the check passed for one case, it would pass for all cases.
系统级检查在系统范围内执行检查。 如果支票通过了一种情况,它将通过所有情况。
Context-level checks perform checks within a specific context, such as within the scope of a User record. If the check passed for one context, it did not determine whether it would pass for another.
上下文级别检查在特定上下文(例如,用户记录的范围内)中执行检查。 如果检查通过了一个上下文,则不会确定是否通过另一个检查。
Finally, checks could be layered. By combining flags for system-level and context-level, we could toggle functionality for features on specific records, or records belonging to a specific user, or records belonging to users within a specific group, the possibilities are endless.
最后,检查可以分层。 通过组合系统级和上下文级的标志,我们可以切换特定记录,或属于特定用户的记录,或属于特定组内的用户的记录上的功能的功能,而这种可能性是无限的。
什么功能标记不是 (What Feature Flags Are Not)
One critical thing to point out is what feature flags specifically are not intended for.
需要指出的关键一件事是特定不打算使用哪些功能标志。
Because it is a boolean check, you can be tempted to leverage it in any situation where you would have a boolean check. However, just because you can doesn’t mean you should. Developers can fall into the trap of using it for account-specific authorization related logic just because it exists. It is important to not mix this area of concern.
由于它是布尔检查,因此在有布尔检查的任何情况下,您都可能想利用它。 但是,仅仅因为您可以做并不意味着您应该做。 仅仅因为它存在,开发人员就可能陷入将其用于特定于帐户的授权相关逻辑的陷阱。 重要的是不要混淆这个关注的领域。
You should never use a feature flag system as an account permission check, such as checking if a user is an administrator or not. Even if the base concept of checking a flag is the same, everything else is different enough to warrant separate consideration. Security is important, and concepts like authorization should be addressed as a primary concern within your system, not relegated to an unrelated feature flag infrastructure.
绝对不要将功能标记系统用作帐户权限检查,例如检查用户是否为管理员。 即使检查标志的基本概念相同,其他所有事物也足够不同,因此需要单独考虑。 安全性很重要,诸如授权之类的概念应作为系统内的主要问题解决,而不应归结为不相关的功能标志基础结构。
处理旧版工作 (Dealing With Legacy Work)
The product already had a few false starts in this area scattered throughout the codebase.
该产品在该领域已经有一些错误的开始,遍布整个代码库。
The most common feature flag implementations I saw in this product, as well as many others throughout my career, fell into these three camps:
我在该产品中看到的最常见的功能标志实现以及我整个职业生涯中的许多其他实现都属于以下三个阵营:
- A boolean on a record 记录中的布尔值
- A string array on a record 记录上的字符串数组
- An environment variable check 环境变量检查
布尔列 (The boolean column)
The boolean on a record is simple, add a boolean representing whether the feature is enabled, then check it:
记录中的布尔值很简单,添加一个表示该功能是否已启用的布尔值,然后检查它:
process_surcharge if @cause.surcharge_enabled?
The downside of this approach is that if you have dozens of features, or flags that can be shared across multiple kinds of records, you’d end up with just as many feature flag columns all over your system, and various checks for them.
这种方法的缺点是,如果您具有许多功能或可以在多种记录之间共享的标志,则最终会在整个系统中出现同样多的功能标志列,并对它们进行各种检查。
It would also clutter database tables with things specific to mechanics of how the system works, and not the domain. This can get unwieldy quickly.
它还会使数据库表混乱,其中包含系统工作原理的特定内容,而不是域的内容。 这会很快变得笨拙。
字符串数组 (The string array)
The second approach was clearly designed to solve the first downside of supporting multiple features.
显然,第二种方法旨在解决支持多种功能的第一个缺点。
process_surcharge if @cause.features.contains?('surcharge')
It solves one problem but still has the other downsides. It also then introduced a problem of being inconsistent with the already established feature flag pattern, which hadn’t been ported over to the new approach.
它解决了一个问题,但还有其他缺点。 然后,它还引入了一个问题,即与尚未建立的特征标记模式不一致,该特征标记模式尚未移植到新方法中。
Add in a couple of years of proliferation and we ended up with dozens of places where one or the other were being used.
再加上几年的扩散,我们最终在数十个使用一个或另一个的地方。
环境变量 (The environment variable)
Finally, system-wide features were flagged with environment variables.
最后,使用环境变量标记系统范围的功能。
process_surcharge if ENV['IS_SURCHARGE_ENABLED']
解决遗留约束 (Solving for legacy constraints)
Greenfield would allow us to do anything we wanted, but we live in a legacy world. Any new solution we built had to corral all of these various partial solutions to ensure there was only one true way of toggling a feature.
格林菲尔德将允许我们做任何我们想做的事情,但我们生活在一个古老的世界中。 我们构建的任何新解决方案都必须整合所有这些部分解决方案,以确保只有一种真正的方式来切换功能。
Otherwise, developer psychology would lead to developers continuing to create their own or copying one of the other feature toggling mechanisms due to the inconsistency.
否则,由于不一致,开发人员的心理会导致开发人员继续创建自己的模型或复制其他功能切换机制之一。
第一步 (First Steps)
We started by addressing the legacy constraints. We solved the problem of all of these disparate methods of feature checking by adding a layer of indirection and consolidating all of the differences within that layer.
我们从解决遗留约束开始。 我们通过添加一个间接层并整合了该层中的所有差异,解决了所有这些不同的特征检查方法的问题。
实施 (The implementation)
We created a service class called FeatureToggler
with a method called enabled?
that accepted a flag and a record:
我们创建了一个名为FeatureToggler
的服务类,并带有一个enabled?
的方法enabled?
接受了一个标志和一条记录:
class FeatureToggler
def enabled?(flag, record)
end
end
Because different kinds of records in our system had a different way to check whether a feature was enabled or not, we enumerated all of those ways within the enabled?
function:
由于系统中不同类型的记录使用不同的方法来检查功能是否已启用,因此我们列举了已启用功能中的所有这些方式enabled?
功能:
def enabled?(flag, record)
return record.features.contains?(flag) if record.instance_of?(User)
return record.send("#{flag}?") if record.instance_of?(Cause)
# ...and so on...
end
We then replaced all of the various if x.<y>_enabled?
, if z.features.contains?(a)
checks with calls to the new function.
然后,我们替换了所有所有的if x.<y>_enabled?
, if z.features.contains?(a)
检查是否有对新函数的调用。
The code movement screamed of violations of every kind from “don’t repeat yourself” to “single responsibility principle” to “breaking the rules of basic inheritance.”
代码运动大声疾呼,涉及从“不要重复自己”到“单一责任原则”到“违反基本继承规则”的各种违规行为。
It was all for the greater good, though. Sometimes you have to make the code a bit messier before you can clean it up. Like a sliding puzzle, you may need to do the “3 steps forward, 2 steps back” dance until you complete the refactor.
不过,这都是为了更大的利益。 有时,您必须使代码有些混乱,然后才能进行清理。 就像滑动拼图一样,您可能需要做“向前3步,向后2步”跳舞,直到完成重构为止。
Minor updates to our existing test suite ensured it continued to pass. The new consolidated interface actually made it possible to split out the testing of feature toggling functionality from other tests and creating a specific suite just for that, which made testing flagging much easier and more comprehensive.
对我们现有测试套件的较小更新确保其继续通过。 新的整合界面实际上使将功能切换功能的测试与其他测试分离开来,并为此创建了一个特定的套件,这使得测试标记变得更加容易和全面。
新数据模型 (A New Data Model)
Once the FeatureToggler
abstraction layer was in place, we wanted to transition to a new data model, one that was divorced from all of the other data models within our system.
放置FeatureToggler
抽象层之后,我们想过渡到新的数据模型,该模型与我们系统中的所有其他数据模型分离。
新数据模型 (The new data model)
![Image for post](https://miro.medium.com/max/9999/1*i6iVkX0mvRKcGODLbXuTuQ.png)
valuevalue
was the flag itself, the value we would check in our code to determine if a flag was running or not.
value value
是标志本身,我们将在代码中检查该值以确定标志是否正在运行。
statusstatus
was a string that represented whether the flag was enabled
or disabled
.
status status
是一个字符串,表示该标志是enabled
还是disabled
。
owner_id and owner_typeowner_id
and owner_type
were two fields that tracked the owning record in a polymorphic manner. If the owner was a Contract record with ID 2, owner_id
would be 2
and owner_type
would be Contract
.
owner_id和owner_type owner_id
和owner_type
是两个以多态方式跟踪拥有记录的字段。 如果所有者是ID为2的合同记录, owner_id
将为2
, owner_type
将为Contract
。
Having no owner meant that the flag was intended to be a system flag, not a context flag.
没有所有者意味着该标志旨在用作系统标志,而不是上下文标志。
过渡到新数据模型 (Transitioning to the New Data Model)
Transitioning was a bit more complex than we desired. Our transition plan required us to transition two aspects of flags: reads and writes.
过渡比我们期望的要复杂一些。 我们的过渡计划要求我们过渡标志的两个方面:读取和写入。
阅读很容易 (Reads were easy)
Reads were straightforward, we could insert a check in our new FeatureToggler
layer to look at the new data model:
读取非常简单,我们可以在新的FeatureToggler
层中插入检查以查看新的数据模型:
def enabled?(flag, record)
FeatureFlag.exists?(owner: record, enabled: :true, value: flag)
end
写作更具挑战性 (Writes were a bit more challenging)
Writes were made harder due to legacy constraints.
由于遗留的限制,写入变得更加困难。
In order to allow operations to toggle features on our old system, we had a CRUD interface that was data-model aware and generated via DSL. This meant that it was hard-coded and specifically knew whether it was inserting into an array, setting a boolean on a column on a specific record.
为了允许操作切换旧系统上的功能,我们有一个CRUD接口,该接口可以识别数据模型并通过DSL生成。 这意味着它是经过硬编码的,并且特别知道它是否要插入到数组中,并在特定记录的列上设置布尔值。
There was no way to abstract that behavior, we were severely limited in our ability to change it out. If we added a flag using the new data model, it would not reflect in the old one. If we added a flag in the old data model, it would not reflect in the new one. This meant flags could go out of sync. It was a potential source of significant confusion.
无法抽象这种行为,我们改变行为的能力受到严重限制。 如果我们使用新的数据模型添加一个标志,它将不会反映在旧的模型中。 如果我们在旧的数据模型中添加一个标志,它将不会反映在新的模型中。 这意味着标志可能不同步。 这是造成巨大混乱的潜在根源。
Legacy constraints were forcing us to head down the path of an all-or-nothing release, which was explicitly an anti-goal of our initiative.
遗留的约束迫使我们不得不放弃全部或全部放弃的道路,这显然是我们倡议的反目标。
We couldn’t have both systems being written-to in parallel, which meant we had to do a complete transition if we wanted to migrate writes.
我们无法将两个系统并行写入,这意味着如果要迁移写入,我们必须进行完整的转换。
解决方案:仅转换读取 (The solution: Only transition reads)
We decided to not migrate writes just yet and only migrate the reads. We kept the old data model as the source of truth for changes by propagating any changes made to them down to the new data model.
我们决定暂时不迁移写入,而仅迁移读取。 通过将对旧数据模型的所有更改传播到新数据模型,我们将旧数据模型作为更改的真实来源。
We wrote a migration to take all of the feature flags we had stored in the other various approaches and copied them to the new data model.
我们编写了一个迁移文件,以获取我们以其他各种方式存储的所有功能标记,并将其复制到新的数据模型中。
To keep the data between the old and the new data model in sync, we added an extra step to the existing writes through callbacks, ensuring we were also writing the same data to the new data model in the various locations flags were being written using the approaches.
为了使新旧数据模型之间的数据保持同步,我们通过回调向现有写入添加了一个额外的步骤,以确保我们还在使用方法。
Finally, we changed the reads of feature flags to point to the new system, but to also verify results via a quorum system that logged when the checks disagreed:
最后,我们更改了功能标记的读取以指向新系统,但同时也通过当检查不一致时记录的仲裁系统来验证结果:
def enabled?(flag, record)
if new_enabled?(flag, record) == old_enabled?(flag, record)
return new_enabled?(flag, record)
else
log_quorum_failure(flag, record)
return old_enabled?(flag, record)
end
end
We also decided to eat our own dog food and feature flag the feature flag check to determine whether to use the old system or the new system as a just-in-case. Talk about extra safe.
我们还决定吃自己的狗食,并通过特征标记检查特征标记,以决定是在旧情况下使用旧系统还是新系统。 谈论额外的安全。
By removing the requirement of transitioning writes, we greatly simplified the project and it allowed us to deliver a small piece of value rapidly. Sometimes the simplest thing is to do nothing.
通过消除转换写入的要求,我们极大地简化了项目,并允许我们快速交付一小部分价值。 有时最简单的事情是什么都不做。
完美的胜利 (Flawless victory)
We ran the system in production for a while, and it ran perfectly, we didn’t see any disagreements being logged, except for those we intentionally created as a test.
我们在生产环境中运行了一段时间,并且运行良好,我们没有看到任何分歧,除了我们故意创建的作为测试的分歧。
At that point, we switched over to using the new data model for all of our reads and started looking at transitioning writes.
到那时,我们切换到对所有读取使用新的数据模型,并开始研究转换写入。
过渡写作 (Transitioning Writes)
Because we had full confidence in the stability of the reads and the usefulness of the new system as a whole, we approached migrating writes with a whole lot more confidence.
因为我们对读取的稳定性和整个新系统的实用性完全有信心,所以我们对迁移的写入工作充满信心。
We had two data models in play, with the old data model being the source of truth for changes. Since changes to the old data model got synced to the new data model, this meant we were still reliant on all of the limitations of the old approach.
我们有两个数据模型,其中旧的数据模型是变更的真相。 由于对旧数据模型的更改已同步到新数据模型,因此这意味着我们仍然依赖于旧方法的所有局限性。
We needed to make our new data model the source of changes. Because our old system was reliant on an inflexible DSL for feature flag modification, this meant that we had to create a new user interface to provide the same functionality that was expected before we could actually transition writes over.
我们需要使我们的新数据模型成为变化的源泉。 因为我们的旧系统依赖于不灵活的DSL来进行功能标志修改,所以这意味着我们必须创建一个新的用户界面,以提供与实际过渡写入之前期望的功能相同的功能。
Turns out it was a piece of cake with the new data model. Operationally supporting the new flagging data model was as complex as inserting and reading records in a table. There were no special rules or gotchas. The interface literally boiled down to a table with some buttons, it was CRUD functionality in its purest form.
事实证明,使用新数据模型可谓小菜一碟。 在操作上支持新的标记数据模型就像在表中插入和读取记录一样复杂。 没有特别的规则或陷阱。 该界面从字面上简化为带有一些按钮的表,它是最纯粹形式的CRUD功能。
发行 (Releasing)
The actual release was pretty straightforward.
实际版本非常简单。
We provided a brief training to the main group of people who modified feature flags, wrote and shared a one-sheet to document it extensively, and then provided links to the new interface where the old interfaces were.
我们向主要的人进行了简短的培训,他们修改了功能标志,编写并共享了一张纸来广泛地记录它,然后提供了旧界面所在的新界面的链接。
迭代与改进 (Iterations and Improvements)
Our new system was fantastic.
我们的新系统很棒。
It allowed us to rapidly apply different kinds of feature flags or apply multiple flags within different contexts without the need for database migrations.
它使我们能够快速应用不同种类的功能标志或在不同上下文中应用多个标志,而无需进行数据库迁移。
Cascading flag checks were now easy, we could toggle functionality based on a specific record or for a specific user. We didn’t stop there. Now that we weren’t limited by legacy constraints, we could quickly add more functionality as time went on.
级联标志检查现在很容易,我们可以根据特定记录或特定用户切换功能。 我们没有到此为止。 现在我们不再受传统限制的限制,随着时间的流逝,我们可以快速添加更多功能。
描述性枚举 (Descriptive enumerations)
One problem with the old feature flag approach was that nobody knew what the flags actually did or meant.
旧功能标志方法的一个问题是没人知道标志的实际作用或含义。
Because many of them were boolean fields or string values without descriptions, it was difficult to tell exactly what the consequences of toggling them were. Unless you were already familiar with the system, you would never be able to find out.
由于它们中的许多是布尔型字段或没有描述的字符串值,因此很难准确说明切换它们的后果。 除非您已经熟悉该系统,否则您将永远无法找到答案。
When we transitioned them over, we preserved their unfortunate naming.
当我们过渡他们时,我们保留了他们不幸的名字。
For non-engineers, flags with values like bpfee
and gp
were indecipherable. It provided a terrible user experience and led to a lot of operational errors that ultimately made their way back to engineering in the form of bug reports and investigations, which we wanted to prevent.
对于非工程师,带有bpfee
和gp
bpfee
标志是不可bpfee
。 它提供了可怕的用户体验,并导致了许多操作错误,最终以错误报告和调查的形式最终回到了工程领域,我们希望避免这种情况。
We wanted to clearly describe what flags did and what they meant, so we introduced a data model to list what the available options were:
我们想清楚地描述标记的作用及其含义,因此我们引入了数据模型来列出可用的选项:
![Image for post](https://miro.medium.com/max/9999/1*hQzDKPbxgTpoJi_0PFvQGQ.png)
We changed our modification interface to decorate the list of feature flags with these details, providing significantly more clarity to anyone modifying the feature flags.
我们更改了修改界面,以用这些细节装饰功能标志列表,从而使任何修改功能标志的人都更加清楚。
They no longer had to see just:
他们不再只需要看到:
bpfee
They also saw what it meant:
他们还明白了这意味着什么:
Receipt Basis Points Fee (bpfee)Display fee percentages charged as basis points on the receipt.
The increased clarity led to significantly fewer errors and questions reaching us, resolving a large source of upstream issues.
清晰度的提高导致我们遇到的错误和问题明显减少,从而解决了上游问题的大量来源。
默认值 (Defaults)
Sometimes when a record gets created we want certain flags to be automatically enabled. By adding a column to the FeatureFlagOption
table, default_status
, we can achieve this:
有时,当创建记录时,我们希望某些标志被自动启用。 通过在FeatureFlagOption
表中添加一列default_status
,我们可以实现以下目的:
![Image for post](https://miro.medium.com/max/9999/1*3bQZgM7egXVG_NfTUKG8nA.png)
Now, whenever a record was created, we could pull up all of the flags where default_status
was enabled
and automatically enable the flags on that record.
现在,每当一个记录的创建,我们可以拉涵盖了所有的标志default_status
被enabled
,并自动启用该记录的标志。
分类 (Categorization)
Over time, you get a lot of different feature flags, all for different purposes.
随着时间的流逝,您会得到很多不同的功能标志,它们的目的都是不同的。
Some are flags intended for short-term use, typically to hide code until it is done or to mitigate risk by performing canary releases. Once the code is deployed, the flag should get removed. Some are flags intended for mid-term use, such as those used to perform experiments. Some flags are intended for long-term use. Features might be toggled based on the contract or plan the customer is on.
有些标志是供短期使用的,通常用于隐藏代码直到完成或通过执行canary发布来减轻风险。 部署代码后,应删除该标志。 一些是供中期使用的标志,例如用于执行实验的标志。 有些标志旨在长期使用。 可以根据合同或客户所在的计划来切换功能。
Each of these flags have different owners and usages, so it is important to be able to separate them. By adding a category
to FeatureFlagOption
, we are able to scope the enumeration and behavior even further:
这些标志中的每一个都有不同的所有者和用法,因此能够将它们分开很重要。 通过向FeatureFlagOption
添加category
,我们可以进一步对枚举和行为进行范围FeatureFlagOption
:
![Image for post](https://miro.medium.com/max/9999/1*vc42mFupyTOSciPm1qwaYQ.png)
实验性 (Experimentation)
Sometimes, we want flags that split groups into different cohorts for A/B testing of features. We could store the various parameters of experiments in the FeatureFlagOption
, to be sent to the A/B subsystem:
有时,我们希望使用标记将组分为不同的组,以进行功能的A / B测试。 我们可以将各种实验参数存储在FeatureFlagOption
,然后发送给A / B子系统:
![Image for post](https://miro.medium.com/max/9999/1*jkcJict1cUIc_BOA-xBzjQ.png)
While an A/B system is outside the scope of this post, I recommend checking out the split gem if you are using ruby.
虽然A / B系统不在本文讨论范围之内,但如果您使用的是Ruby,我建议您检查拆分后的宝石 。
性能 (Performance)
As usage of the feature flag system grows, you don’t want to be performing that many database reads. The abstraction layer provides a perfect place to put in a caching layer as well to ensure rapid lookups of feature flags.
随着功能标志系统的使用量增加,您不想执行那么多的数据库读取。 抽象层还提供了放置缓存层的理想位置,以确保快速查找功能标志。
As always, remember the words of the famed Donald Knuth:
一如往常,请记住著名的唐纳德·克努斯(Donald Knuth)的话:
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.
我们应该忘记效率低下的问题,例如大约97%的时间:过早的优化是万恶之源。
Ensure you have an actual need to improve performance and that the usage patterns are appropriate for your solution.
确保您确实有提高性能的需求,并且使用模式适合您的解决方案。
As another tip: don’t randomly add caching logic ad-hoc. It quickly gets out of hand when you try and figure out whether you are dealing with cached data or not. It is made monumentally more complex when caching layers on the controller, data, and database all start interacting with each other.
另一个提示:不要随意添加临时的缓存逻辑。 当您尝试弄清是否正在处理缓存的数据时,它很快就会失控。 当在控制器,数据和数据库上的所有缓存层开始相互交互时,它变得异常复杂。
Add caching thoughtfully and with centralized controls so you know exactly how stale the data is and how to force a refresh, if necessary.
考虑周到地添加缓存并使用集中式控件,因此您可以准确了解数据的陈旧程度,以及在必要时如何强制刷新。
客户端检查 (Client-side checks)
Our new technology platform greatly leveraged a lot of the feature flag infrastructure. We created a front-end service and component that showed/hid nested components based on the feature flag’s status.
我们的新技术平台极大地利用了许多功能标记基础结构。 我们创建了一个前端服务和组件,它们根据功能标志的状态显示/隐藏了嵌套的组件。
By exposing flag checks in an API and then consolidating all of its logic within the service and component, we were able to avoid having feature flag logic all over the front-end.
通过在API中公开标志检查,然后将其所有逻辑整合到服务和组件中,我们可以避免在整个前端使用特征标志逻辑。
效果 (The Effects)
The transition went incredibly smoothly, and it was shocking how much separating releases from deploys improved team collaboration and delivery effectiveness.
过渡过程非常顺利,令人震惊的是,发布与部署之间的分离程度提高了团队协作和交付效率。
Deploy sizes went down and frequency went up because developers could deploy code without causing production issues. This in turn decreased the change failure rate since deploys were smaller and bugs were easier to catch before they made it to production.
部署规模减小,频率提高,因为开发人员可以部署代码而不会引起生产问题。 这反过来降低了变更失败率,因为部署的规模较小,而且在投入生产之前,更容易发现错误。
Releases went more smoothly and adapted better to the business’ timetable because it didn’t require engineers to keep track of different branches of work and deal with integration challenges for tens of thousands of lines of code.
发布更加顺利,并且更适合业务时间表,因为它不需要工程师跟踪不同的工作分支并处理成千上万行代码的集成挑战。
It wasn’t a silver bullet to all of our problems, but it helped streamline processes and remove a lot of collaboration friction, paying off dividends.
这并不是解决我们所有问题的灵丹妙药,但它有助于简化流程并消除许多协作摩擦,并带来了回报。
经常问的问题 (Frequently Asked Questions)
我们为什么不与供应商合作? (Why didn’t we go with a vendor?)
There’s a lot of vendors that provide flag functionality like Optimizely or LaunchDarkly. Why didn’t we go with them, why reinvent the wheel? In a word: budget.
有很多供应商提供标志功能,例如Optimizely或LaunchDarkly。 我们为什么不跟他们一起去,为什么要重新发明轮子呢? 一言以蔽之:预算。
At the time, we were under significant budget restrictions, and it was difficult to get any additional spending approved. As engineering didn’t control our budget, we had to make do with what we had.
当时,我们受到严重的预算限制,因此很难批准任何额外的支出。 由于工程无法控制我们的预算,因此我们不得不按原样进行。
Even getting resources to work on technical debt like this was impossible, we had to ensure we made progress on engineering initiatives during the time we found in the “in-between:” the small slices of time between tickets, projects, and meetings.
即使不可能像这样获得资源来解决技术债务,我们也必须确保在“中间”期间发现工单方面取得进展,这些中间时间是票务,项目和会议之间的一小段时间。
We had to work within the context we had.
我们必须在已有的背景下工作。
我们为什么不带图书馆或宝石呢? (Why didn’t we go with a library or gem?)
We examined a lot of gems and realized that none of them suited our particular desires for future use cases, legacy constraints, or migration requirements. Several gems in particular would have required a big-bang approach to deployment, which was a level of risk we didn’t want to accept.
我们检查了许多宝石,并意识到它们都不适合我们对未来用例,遗留约束或迁移要求的特定需求。 尤其是一些瑰宝,需要采用大规模的部署方法,这是我们不愿接受的风险水平。
A few would have required a complete overhaul of how teams external to our department understood flagging, which expanded the scope of the changes we wanted to make and increased the delivery burden significantly by adding more stakeholders. We were already working on this as an unofficial ninja-project. We didn’t want to increase the odds of it never being delivered.
有几个要求彻底检查我们部门外部团队对标记的理解,这扩大了我们要进行的更改的范围,并通过增加更多的涉众而大大增加了交付负担。 我们已经作为一个非官方的忍者项目进行了这项工作。 我们不想增加从未交付的可能性。
Most of the gems we looked at would have required us to go the route we did anyways. We decided to kick the selection down the road and roll our own quickly, but hide all-access behind an interface so we could swap out the implementation in the future if resources freed up.
我们看过的大多数宝石都要求我们走我们无论如何所做的路线。 我们决定取消选择并Swift推出自己的选择,但将所有访问权限隐藏在一个界面后面,以便将来在释放资源的情况下可以交换实现。
We were already working on this as an unofficial ninja-project. We didn’t want to increase the odds of it never being delivered.
我们已经作为一个非官方的忍者项目进行了这项工作。 我们不想增加从未交付的可能性。
您为什么不立即转换所有内容? (Why didn’t you just transition everything over at once?)
An all-or-nothing approach was not desired for a few reasons.
由于某些原因,不希望采用全有或全无的方法。
Recall that this was a ninja project we were doing in-between actual work. This meant I had a few minutes here, an hour or two there to actually complete this migration. An all-or-nothing approach would have required significantly more focus than we could afford to allot.
回想一下,这是我们在实际工作之间进行的忍者项目。 这意味着我在这里花了几分钟,在那里花了一两个小时才能真正完成迁移。 一种全有或全无的方法将需要比我们分配的更多的精力。
We did introduce extra complexity in the migration by doing it in smaller pieces. However, we de-risked almost the entire initiative by doing so. Feature flags were used extensively within the system for a variety of reasons including contractual obligations, and an error in it would have been highly consequential.
通过在较小的部分中进行迁移,我们确实引入了额外的复杂性。 但是,这样做可以降低几乎整个计划的风险。 出于多种原因,功能标记在系统中得到了广泛使用,包括合同义务,而其中的错误将是高度后果。
Doing it in smaller pieces was absolutely worth the safety, even if it ultimately wasn’t needed in the end.
即使最终最终不需要这样做,也绝对值得将它做成小块。
翻译自: https://medium.com/better-programming/how-to-design-software-feature-flags-283c5f938171
图片 标记 软件