每个开发者都有一段时间会出现这样的想法,让你意识到需要从一个数据库迁移到另一个数据库。也许你开始用MongoDB,并且觉得它占用应用程序很小这点很棒,但它不能处理负载,让你意识到现在是大联盟时代。也许是时候咬紧牙关,把大项目数据库放在DynamoDB上了 (或Cassandra,或……)。
移动数据库是一个不小的工作,但它不会存在危险。
一个常见的误解是,Feature Flag是唯一有用的新功能或界面的变化。事实证明,对于做数据迁移来说,他们是非常有用的。Feature Flag对于数据迁移这件事来说变得很容易,但它更容易与实际测试,实时数据(为你的用户群的子集),如果出现错误是会很容易回滚该事务的。
注意,这里有个使用的例子:假定一个从来未更新过的数据模型,写入新的事件,并读取现有的时间,但是事件没有更新。基本的思想仍然适用于数据的更新,但是比较复杂。
计划
假设你有一些DAO代码,可以在数据库中读取和写入事件(最初MongoDB)。你所有的应用程序逻辑访问持久化数据都会通过此DAO。
第一步将使用相同的接口编写一个新的DAO,但DynamoDB能读和写。如果有不同的方式,你需要查询新的数据存储中的数据,这一点你需要面对。(对不起,Feature Flag不会帮你查询DynamoDB的灵活性,您可以查询MongoDB。)
Feature Flag 在你的数据库
现在,你有两个实现DAO的接口,一个是在Mongo的支持下,一个Dynamo。这让它开始变得更加有趣。
我们可以用一组四个Feature Flag来控制每个数据库独立读取和写入。例如,假设我们有此功能来保存事件:
func storeEvent(evt Event, account ld.User) {
if ldclient.toggle('events-mongo-write', account, true) {
mongoEventsDao.createEvent(evt)
}
if ldclient.toggle('events-dynamo-write', account, true) {
dynamoEventsDao.createEvent(evt)
}
}
注意,在这两个地方都可以保存相同的事件。这不是偶然!这样我们就可以开始向新的数据存储区写入事件,来维护旧的存储的完整性。
代码读取变得更有趣:
func findEventById(id string, account ld.User) Event {
// Check both 'read' flags
shouldReadMongo := ldclient.toggle('events-mongo-read', account, true)
shouldReadDynamo := ldclient.toggle('events-dynamo-read', account, true)
if shouldReadMongo && shouldReadDynamo { // compare results
mongoEvt := mongoEventsDao.findEventById(id)
dynamoEvt := dynamoEventsDao.findEventById(id)
if !reflect.DeepEqual(mongoEvt, dynamoEvt) {
logger.Error.Printf(
"Mongo and Dynamo events differ: mongo: %+v, dynamo: %+v",
mongoEvt,
dynamoEvt)
}
return mongoEvt
} else if shouldReadDynamo { // read from just dynamo
return dynamoEventsDao.findEventById(id)
} else { // read from just mongo
return mongoEventsDao.findEventById(id)
}
}func findEventById(id string, account ld.User) Event {
// Check both 'read' flags
shouldReadMongo := ldclient.toggle('events-mongo-read', account, true)
shouldReadDynamo := ldclient.toggle('events-dynamo-read', account, true)
if shouldReadMongo && shouldReadDynamo { // compare results
mongoEvt := mongoEventsDao.findEventById(id)
dynamoEvt := dynamoEventsDao.findEventById(id)
if !reflect.DeepEqual(mongoEvt, dynamoEvt) {
logger.Error.Printf(
"Mongo and Dynamo events differ: mongo: %+v, dynamo: %+v",
mongoEvt,
dynamoEvt)
}
return mongoEvt
} else if shouldReadDynamo { // read from just dynamo
return dynamoEventsDao.findEventById(id)
} else { // read from just mongo
return mongoEventsDao.findEventById(id)
}
}
所以,在这里我们检查了两个“读”的标志,如果两者都启用,那么我们从两个数据库中读取数据并比较结果。如果有一个不匹配,我们记录一个错误。这个实现有一个倾向现任数据存储(Mongo 在这个例子中):当它从这两个存储中读取时,它将从Mongo中读取返回值,它将在任何时间都能从Mongo中读取dynamo flag的值为false,它是没有意义去关闭和并从中读取两个标志。
被推出
现在,在Feature Flag里我们已经覆盖了数据访问功能,它可以计划被推出。您可以关闭每个人的读标志,并且慢慢地推出写标记。查看性能指标和错误日志。如果事情看起来不错,它会推给更多的用户,直到所有事件都被写入到数据库。
当你开心的写入性能时,你可以打开读取新的数据存储。打开读取用户的一小部分,再次查看性能指标和错误日志。具体来说,是在寻找“Mongo and Dynamo事件不同”的消息。但是,睡眠容易:即使你看到,它没有对用户产生任何的影响。他们仍在使用旧的数据存储中的数据。你可以解决任何问题。
一旦你向所有用户推出读取和写入,那么,你应该从你的旧数据存储中,将所有旧的数据迁移到你新的数据存储中 (一定要做这是等幂的,如果你迁移事件写入数据库将不会被复制)。
现在可以关闭旧数据库的读写标记,删除所有引用代码中的所有四个标记和你剩下的新数据库。
这不是在编小说
我没有编造这个故事来告诉你,Feature Flag是如何的强大。我们几个月前在LaunchDarkly上制作了这个迁移,并且非常顺利。有一个或两个错误,我们发现使用这种技术涉及了意想不到的用户数据。我们看到记录的信息,找到了问题的根源,很轻松搞定,完全不会影响我们的客户。