发布管理-发布到用户子集-对于公共面向网站如何运作

20

我曾经在某个地方读到过(很抱歉我不记得来源了)Facebook有每周二的发布日。他们先向内部员工发布新功能,然后再向一小部分外部用户发布,最后才向全世界发布。我相信Google也有类似的做法。

我主要使用Microsoft堆栈(TFS用于源代码控制,IIS、asp.net、sql server和大型数据)。当然是面向公众的网站,所以必须24x7x365在线。尽管我可以想象仅在一个服务器上发布我的api/dll并进行测试,但如果有数据库(存储过程签名、表模式更改)怎么办呢?目前我们正在对存储过程进行版本管理(新的将是mySPNameV2,而旧的将是mySPNameV1——两者采用不同的参数,因此需要重命名),新的API将使用SP-V2,而旧的API将继续使用SP-V1。

我看到了一些设计问题,但是否有更好的方法来解决这个问题呢?

编辑:我们只将新代码发布到一个服务器并进行测试,难点在于如何将多个并发应用程序版本的数据库模式更改抽象出来。


@AlexKey - 不,我们不是这样做的。我们按版本(实际上是发布和冲刺)存储它们,因此v1.sql将具有创建表,而v2.sql将具有修改表(添加列)。通过这种方式,我们可以为发布创建整个“状态”。您使用VS DB项目吗?您喜欢它吗? - ram
嗨@ram,我们目前在生产中没有使用Db项目,但我一直在进行实验。它们具有部署脚本等功能,这可能无法帮助存储每个用户版本的数据位置,但可能有助于部署这些版本。我对您得到的回复非常感兴趣,这是一个让我感到好奇的问题,我也想了解最佳实践的指导。 - Alex KeySmith
@AVee:是的,我同意,如果业务逻辑进入存储过程,我会很讨厌。而且,这正是我所面临的困境,我不能删除/更改任何旧的存储过程,并且必须在存储过程名称中或作为参数添加版本信息。我正在寻找一个更好的解决方案,可以扩展。你有什么建议吗? - ram
@ram 我没有投票关闭,但是为了回答你的问题,两个关闭投票都建议将其转移到Programmers.Stackexchange。通常这是因为问题是概念性或理论性的,没有实际的代码问题。 - Adam Wenger
@ram:坦白说,我真的没有什么建议。除了存储过程之外,你还会遇到模式更改的升级问题。唯一真正的解决方案是使用一个能够同时处理不同连接的不同模式版本的数据库系统。据我所知,目前并不存在这样的数据库,但我很愿意被证明是错误的。 - AVee
显示剩余5条评论
8个回答

6
在我们公司,几乎所有的重大发布都是分阶段进行的。我们通过在用户表中为每个新功能添加一个标志来实现这一点。默认情况下,此标志设置为false;随着我们将功能推出给更多的用户,我们只需在数据库表中切换标志即可。
这意味着在应用程序级别,我们必须确保在所有地方检查此标志。代码推送到所有服务器。进行数据库更改;但仍然只有一些用户看到新功能。
在数据库级别,我们确保对SP的任何更改都是“向后兼容”的。这是通过遵循一些简单的规则来完成的:
1. 将任何新参数添加到SP的参数列表的末尾。 2. 新参数应该有一个默认值。这样做是为了不破坏对SP的现有调用。 3. 如果必须更改SP的现有参数(或者如果必须更改参数的顺序),那么显然所有对SP的调用都会发生变化。但是,SP的编码方式是这样的,它支持已启用功能的用户以及未启用功能的用户。 4. 大多数表更改涉及添加新列。当我们必须修改现有列时,这是非常罕见的。所有新列都带有默认值或允许NULL。
至于API,我们的大多数参数都作为自定义对象(结构)传递。这样,我们可以添加一个新参数到API方法中,并仍然防止现有对API的调用失败。
此外,对于每个用户,我们存储他们正在使用的API版本。根据版本,用户进入不同的API URL。因此,当用户进行第一次API调用以进行身份验证时,我们会传递一个新的API URL(基于用户的API版本)。对于所有后续调用,他们应该调用新的URL。Salesforce.com在其API调用中也遵循此方式。

保持存储过程的向后兼容并不能解决问题。每个人都将使用新的存储过程(可能存在错误),而 OP 希望一部分用户使用新的存储过程。 - AVee
保持存储过程的向后兼容性会导致执行计划次优。 (注:假设存储过程体内没有动态SQL调用) - Nikhil S
不一定。如果额外的参数会导致完全不同的查询,我们将分支掉存储过程(即根据参数调用其他存储过程)或使用动态查询。 - Dharmendar Kumar 'DK'
“按功能标志”(per feature flag)的另一个优点是它允许您拥有差异化的付款结构(即不同的用户可以选择不同的功能,基于他们支付的费用)。例如:免费用户只有某些基本功能,银牌会员则有一些额外的功能,金牌会员则拥有所有功能。或者当引入新功能时,所有用户都可以在试用期内使用它。但是试用期过后,您需要为所需的功能付费。 - Dharmendar Kumar 'DK'
如果您不介意为哪些用户启用此功能,那么我建议您在(userId,featureId)上使用一些哈希,并决定将x%的用户视为已启用。这将是一致且高效的,因为您不需要进行数据库查询来确定特定用户是否启用了特定功能。它还将使您能够轻松了解由于错误而受到影响的用户数量。 - Moshe Bixenshpaner

3
去年我参加了QCon,当时Facebook的Web团队的一位成员谈到了这种方法。
你完全正确,实现这个方法需要处理代码异味。但是需要做的不仅仅是处理代码异味(实际上他们把那些代码异味称为门卫)。
首先,他们表示和存储数据的方式更加复杂,他们根本不使用外键或数据库的任何其他“高级”功能。据我所记,他们的数据与我们想要保持的“关系型”数据不太相似。
您可以添加门卫(如果(用户来自新西兰){使用具有新类的新版本} else {坚持旧版本})。
你可以想象在结构化模型的情况下会导致什么样的代码重复(例如,您将需要有2个用户,2个订单,2个订单详细信息)。撇开这一点,如果您不非常小心,还会发生很多回归。
据我所记,他们发布的频率比仅仅在星期二发布要高得多。我认为他们进行持续部署,每天都有代码上线,只是在星期二清理旧的门卫/模式更改,因为到那时他们已经将所有用户切换到新功能。
所以基本上:
1. 在新功能出现的地方添加门卫。 2. 维护两个版本的模式。 3. 添加更多用户以切换到新版本。 4. 清理旧版本。 5. 回到1(如果另一个团队还没有到那里)。
我意识到这个答案可能不够完整,但是我从他们的人那里听到了这个问题,并认为值得分享。

感谢分享,给你点个赞 ;) - Owais Qureshi

1

如果我理解你的意思正确,你想要两个机制使用相同的实时数据,但是不同的API版本。现在,假设你已经在使用双缓冲机制,我猜你实际的问题是在过渡期间使用实时表。

解决方案是让你的表包括V1和V2列(例如,一个用户表将包括来自两个API的字段)。注意:所有非公共字段必须具有默认值。

为了使其无缝工作,您应该为V1和V2创建视图,仅公开每个API版本的相关字段,并且您应该为视图而不是表进行开发(类似于开发接口而不是实现的概念)。

存储过程也是如此-所有非公共参数必须具有默认值等...


你能否详细解释一下双缓冲机制的含义? - ram
1
我现在有了更清晰的认识。使用基于JS的UI,将应用程序逻辑作为服务并进行抽象化,使用DB中的视图来抽象底层上下文。发布JS进行测试,并操作底层服务以根据版本获取干净的数据。 - ram

0

我的回答非常简单: 让流量进来,将[F5-负载均衡器]转到新部署的包(针对子集用户)仅在一个生产服务器上(此服务器具有新包),并连接到其他克隆的生产环境数据库。


0

我真的以为这个问题会得到更多的关注。考虑到你所说的和现有的答案,我认为你现有的解决方案是最直接和最容易管理的。正如你所说,有一些“设计气味”(我喜欢这个词),但它是最有意义的。

也许可以进一步结合一些建议的轻微修改和你自己的建议:

  • 保留你现有的数据库版本约定
  • 将特定版本发布到子域候选01.yoururl.com或特定服务器(如果你有服务器群)
  • 在用户/成员表上使用标志来指示应将用户重定向到哪个生产服务器或子域
    • 提供将某些用户重定向到发布候选服务器的能力
    • 不需要像DK的答案中提到的每个功能编码选项那样多的代码(你可以根据你想要的目标进行选择,但我认为最简单的路线在这里是最好的,并且会将用户重定向到应用程序的特定版本,而不是尝试按用户打开/关闭各个功能)

除此之外,非常好的问题!噢,是的,当所有准备就绪时,只需翻转fluger开关以启用双缓冲机制,你就可以开始了。


0
也许我在这里过于简化了,但为什么不进一步实例化数据库呢?
当你正在对“测试”的用户子集进行操作时,无论是内部、外部的子集还是全球的子集,你实际上是在执行类似于 beta 测试的操作吗?这难道不需要一个具有存储过程 beta 版本的不同数据库实例来完成吗?只需要在 web 配置文件中指向连接字符串即可。
也许这里的限制是服务器成本和它们上面的“大数据”,我承认我可能忽略了这些问题,或者问题真正关乎存储过程的版本控制等等。它们不能像模式更改一样被源代码控制吗?
我可能提出了更多问题而非回答。

如果存在标识键,你将如何合并数据? - ram
就内部用户而言,我想你可以丢弃测试数据,但你希望外部用户使用“beta”实例并保留他们创建的所有内容。我猜你需要使用代理键进行跟踪,并使用一些GUID操作来确保唯一性。 - Neil Wood

0

选项1:

为半公开的开发站点创建一个(子)域名。 根据手动设置的 Cookie(员工等)接受某些用户在 devsite 上进行私人测试。

设置某些用户的 cookie 的主域(当时间在时间 X 和时间 Y 之间时设置 cookie)< 根据您的流量而定。 如果您每小时获得 1000 位(独特的)访问者,并且希望有 10% 访问 dev 域,那么确保 delta 时间为 6 分钟。 您可以在需要让用户重定向到正常网站时简单删除 cookie。(确保将整个传入的 URL 重定向以防止损坏的书签。)

选项2:

将一定比例的流量负载平衡到运行新应用程序的服务器上。

数据库

1:在开发过程中与实时数据库交互<定期备份+经验丰富的开发人员=安全

2:查看主从复制,以创建您的 DB 的“live”阴影副本


2
-1 对于“在开发过程中与实时数据库进行交互”是绝对不可接受的。这就像玩俄罗斯轮盘,只是时间问题,迟早会出现问题。 - Joel C

0
在我看来,最简单的方法是通过使用安全性来确定用户获取哪个页面的页面集合来分离功能。除了安全性和其他确保用户无法直接访问他们没有权限的页面的手段(例如站点地图等),您还可以在数据库中存储功能列表以及具有访问该功能的用户或角色:
Create Table Features
    (
    Code varchar(10) not null Primary Key
    , StartPage nvarchar(max) not null
    , Description nvarchar(max) not null
    )

Create Table UserFeatures 
    ( 
    UserId ... not null
    , FeatureCode varchar(10) References Features ( Code )
    )

首先,我使用文本代码作为特征主键而不是像IDENTITY列或GUID这样的代理键的原因是只有系统会查询特征。用户永远不会有任意添加特征的能力。因此,查询...Where FeatureCode = 'AdvancedEntry'比查询...Where FeatureId = 13使您的代码更清晰、更易于阅读。
其次,在这种方法中,页面本身的代码将确定调用哪个过程。然而,如果这些特性仅涉及附加信息字段,则可能意味着相当多的重复。
因此,如果这些特性与现有代码库和表示层紧密集成(这也是版本控制如此困难的原因),另一种方法是在Features表中存储应该使用的存储过程的名称。您的代码将查询连接上述表并返回应该使用的存储过程名称。对于参数,您可以在数据库中存储参数化调用(例如exec Schema.Foo @bar, @gamma, @beta),并在执行查询时简单地检查该字符串是否包含给定参数,如果包含,则添加该参数值:
if ( ProcTemplate.Contains( "@bar")
    commandInstance.Parameters.AddWithValue( "@bar", barValue );
if ( ProcTemplate.Contains( "@gamma")
    commandInstance.Parameters.AddWithValue( "@gamma", gammaValue );
...

如果您将功能映射到用户角色或组,则需要制定“优先”规则,以确定在返回多个功能的情况下应使用哪个功能。在这种方法中,您将保留现有的存储过程,除非模式需要更改存储过程(例如,删除列)。此方法的额外结果是,您可以向Features表添加日期,以确定何时应上线新功能。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接