如何在异步地将记录放入kinesis流中时确保排序?

时间:2017-11-06 14:10:21

标签: mysql node.js amazon-web-services amazon-kinesis

我正在编写一个应用程序,它读取MySQL bin日志并将更改推送到Kinesis流中。我的用例需要在 kinesis 流中完美排序mysql事件,我使用的是 putrecord 操作,而不是 putrecords ,还包括& #39;的 SequenceNumberForOrdering '键。但仍有一点失败,即重试逻辑。作为异步函数(使用aws的js sdk),如何在对kinesis执行写操作期间出现故障时确保顺序。

阻塞写入(阻止事件循环直到收到put记录的回调)太糟糕的解决方案?或者有更好的方法吗?

3 个答案:

答案 0 :(得分:1)

如果你想要完美的排序,那么你需要确保在插入下一个事件之前插入每个事件,所以是的,你必须等到一个put请求完成后再执行下一个。问题是你是否真的需要在所有事件中完美排序,或者你是否需要在某个子集中完美排序?因为您正在使用关系数据库,所以在同一个表中的行之间不太可能存在关系。您更有可能在表之间的行之间建立关系,因此您可以使用几个技巧来利用批量放置请求。

批量放置请求的问题在于它在请求中是无序的。由于bin日志为您提供了更改后行的完整映像,因此您实际上只关心每个主键的bin日志中的最新条目,因此您可以执行的操作是从中收集相对大量的事件。 bin日志,应按时间排序,按主键分组,然后仅从binlog记录中获取after_values映像,以获取每个主键组的最新记录。然后,您可以安全地为这些记录中的每一个使用批量放置请求,并确保在该密钥的最新记录之前,您不会意外地将给定密钥的陈旧记录放入流中。

这对于所有情况都是不够的,但在许多CDC(https://en.wikipedia.org/wiki/Change_data_capture)设置中,这足以准确地将数据复制到其他系统中。

假设您的bin日志中有以下记录(格式取自https://aws.amazon.com/blogs/database/streaming-changes-in-a-database-with-amazon-kinesis/):

{"table": "Users", "row": {"values": {"id": 1, "Name": "Foo User", "idUsers": 123}}, "type": "WriteRowsEvent", "schema": "kinesistest"}
{"table": "Users", "row": {"before_values": {"id": 1", "Name": "Foo User", "idUsers": 123}, "after_values": {"id": 1, "Name": "Bar User", "idUsers": 123}}, "type": "UpdateRowsEvent", "schema": "kinesistest"}
{"table": "Users", "row": {"values": {"id": 2, "Name": "User A", "idUsers": 123}}, "type": "WriteRowsEvent", "schema": "kinesistest"}
{"table": "Users", "row": {"before_values": {"id": 1", "Name": "Bar User", "idUsers": 123}, "after_values": {"id": 1, "Name": "Baz User", "idUsers": 123}}, "type": "UpdateRowsEvent", "schema": "kinesistest"}
{"table": "Users", "row": {"values": {"id": 3, "Name": "User C", "idUsers": 123}}, "type": "WriteRowsEvent", "schema": "kinesistest"}

在此示例中,主键id标识了三行。插入id=1行,然后更新两次,插入id=2行,插入id=3行。您需要分别处理每种类型的事件(写入,更新,删除),并仅收集每个ID的最新状态。因此,对于写入,您需要获取行的values,获取行的after_values更新,而deletes将行放入批处理删除。在这个例子中,唯一重要的三个条目是:

{"table": "Users", "row": {"values": {"id": 2, "Name": "User A", "idUsers": 123}}, "type": "WriteRowsEvent", "schema": "kinesistest"}
{"table": "Users", "row": {"before_values": {"id": 1", "Name": "Bar User", "idUsers": 123}, "after_values": {"id": 1, "Name": "Baz User", "idUsers": 123}}, "type": "UpdateRowsEvent", "schema": "kinesistest"}
{"table": "Users", "row": {"values": {"id": 3, "Name": "User B", "idUsers": 123}}, "type": "WriteRowsEvent", "schema": "kinesistest"}

这是因为它们是每个id的最新版本。您可以对包含这三个写入的批处理使用批量放置,并且在大多数情况下不必担心它们出现故障,除非您在单个表中的条目之间存在相互依赖关系或某些其他非常特定的要求。

如果您有删除,只需将它们放在批量放置记录后执行的单独批量删除中。在过去,我通过这种压缩和批处理程序看到了非常好的吞吐量改进。但同样,如果您确实需要阅读每个事件,而不仅仅是将最新数据复制到其他各个商店,那么这可能无效。

答案 1 :(得分:1)

在向流中添加记录时,不要尝试强制执行排序,而是在读取记录时对其进行排序。在您的用例中,每个binlog条目都有唯一的文件序列,起始位置和结束位置。所以订购它们并找出任何差距都是微不足道的。

如果在阅读时确实发现了空白,消费者将不得不等到他们被填满。但是,假设现在发生灾难性故障,所有记录应该在流中彼此接近,因此缓冲量应该是最小的。

通过在生产者方面强制执行排序,您将整体吞吐量限制为可以写入单个记录的速度。如果你能跟上实际的数据库变化,那就没关系。但是,如果你跟不上,即使消费者可能装载量很小,你也会在管道中出现越来越多的滞后。

此外,您只能在单个分片中强制执行订单,因此如果您的制作人需要摄取超过1 MB /秒(或> 1,000条记录/秒),那么您运气不好(根据我的经验,只有这样你就可以通过PutRecords达到1,000条记录/秒;如果你一次只写一条记录,你将获得大约20-30条请求/秒。

答案 2 :(得分:0)

通过使用内部FIFO队列,我能够实现完美的排序。我将每个事件都推送到FIFO队列中,该队列由递归函数读取,该函数在Kinesis流中推送事件(一次一个)。我也在每次成功的putRecord操作中将bin日志偏移存储在外部存储器中(在我的情况下为redis),如果对kinesis的任何写入失败,我可以重新启动服务器并从最后一个成功的偏移值开始再次读取。

对此解决方案或其他解决方案的任何建议都将受到高度赞赏。

这是我的递归函数的代码片段,它从fifo队列中读取。

const fetchAndPutEvent = () => {
let currentEvent = eventQueue.shift(); // dequeue from the fifo queue

if (currentEvent) {
    currentEvent = JSON.parse(currentEvent);
    // put in the kinesis stream with sequence number of last putRecord operation to achieve ordering of events
    return kinesis.putRecord(currentEvent, sequenceNumber, (err, result) => {
        if (err) {
            // in case of error while putting in kinesis stream kill the server and replay from the last successful offset
            logger.fatal('Error in putting kinesis record', err);
            return setTimeout(() => {
                process.exit(0);
            }, 10000);
        }
        try {
            //store the binlog offset and kinesis sequence number in an external memory
            sequenceNumber = result.SequenceNumber;
            let offsetObject = {
                binlogName: currentEvent.currentBinlogName,
                binlogPos: currentEvent.currentBinlogPos,
                sequenceNumber: sequenceNumber
            };
            redisClient.hmset(redisKey, offsetObject);
        }
        catch (ex) {
            logger.fatal('Exception in putting kinesis record', ex);
            setTimeout(function() {
                process.exit(0);
            }, 10000);
        }
        return setImmediate(function() {
            return fetchAndPutEvent();
        });
    });
}
else {
    // in case of empty queue just recursively call the function again
    return setImmediate(function() {
        return fetchAndPutEvent();
    });
}
};