用Kafka在Rails中进行事件流处理教程

287 阅读11分钟

公司希望对实时处理和分享大量数据的需求做出快速反应,以获得洞察力并创造更有吸引力的客户体验。所以,传统的数据处理在当今世界已经不可行了。

为了实现这一目标,你需要尽可能快地处理大量的数据,然后将其发送到其他服务进行更多的处理。但在所有这些快速行动中,有必要在事件发生时通知消费者--我们可以使用事件流来实现这一点。

这是我们将要使用的GitHub中的repo

事件

在谈论事件流之前,我们先谈谈什么是事件。发生在应用程序中的事件可能与用户进程有关,也可能是影响业务的简单行为。

事件代表一种状态变化,而不是如何修改应用程序的问题。考虑一下这些例子。

  • 一个用户登录到一个服务中
  • 一个支付交易
  • 一个作家在博客中发表了一篇文章

在大多数情况下,一个事件会触发更多的事件;例如,当用户注册一项服务时,应用程序会向他们的设备发送通知,在数据库中插入记录,并发送一封欢迎邮件。

事件流

事件流是一种从事件源(如数据库)实时捕获数据的模式。事件流的主要部分如下:

  • 经纪人。负责存储事件的系统
  • 主题。一类事件
  • 生产者。向经纪人发送关于特定主题的事件
  • 消费者。读取事件
  • 事件。生产者想要传达给消费者的数据

在这一点上,谈论发布和订阅架构模式(pub/sub模式)是不可避免的;事件流是该模式的一个实现,但有这些变化:

  • 事件发生而不是消息。
  • 事件是有顺序的,通常是按时间排序。
  • 消费者可以从主题中的一个特定点读取事件。
  • 事件具有时间上的持久性。

生产者将一个新的事件发布到一个主题中时,流程就开始了(正如我们之前看到的,主题只是对特定类型的事件的分类)。然后,对某一特定类别的事件感兴趣的消费者订阅该主题。最后,经纪人确定主题的消费者并提供所需的事件。

事件流的优点

  • 解耦发布者和消费者之间没有依赖关系,因为他们不需要互相认识。此外,事件没有指定它们的行动,所以许多消费者可以得到相同的事件并执行不同的行动。

  • 低延迟事件是解耦的,让消费者随时利用它们;它可以在几毫秒内发生。

  • 独立性我们知道,发布者和消费者是独立的,所以不同的团队可以使用相同的事件进行其他行动或目的。

  • 容错性一些事件流平台帮助你处理消费者的故障;例如,消费者可以保存他们的位置,如果发生错误,可以从那里重新开始。

  • 实时处理反馈是实时收到的,所以用户不需要等待几分钟或几小时就能看到他们事件的反应。

  • 高性能事件平台由于延迟低,可以处理许多消息--例如,一秒钟内有数千个事件。

事件流的劣势

  • 监控一些事件流工具没有完整的监控工具;它们需要额外的工具来实现,如Datadog或New Relic。

  • 配置一些工具中的配置即使对有经验的人来说也是难以承受的。有许多参数,有时,你需要深入了解这个主题来实现它们。

  • 客户端库用Java以外的语言实现Kafka并不容易。有时候,客户端库不是最新的,显示不稳定,或者没有提供很多可供选择的。

最受欢迎的事件流工具之一是Apache Kafka。这个工具允许用户随时随地发送、存储和请求数据;让我们来谈谈它。

Apache Kafka

"Apache Kafka是一个开源的分布式事件流平台,被成千上万的公司用于高性能数据管道、流分析、数据集成和关键任务应用"。

作为专门为实时日志传输而设计的,Apache Kafka是需要以下内容的应用的理想选择:

  • 不同组件之间可靠的数据交换
  • 随着应用需求的变化,能够划分消息传递的工作负荷
  • 数据处理的实时传输

让我们在一个Rails应用程序中使用Kafka吧!

在Rails中使用Kafka

在Ruby中使用Kafka的最著名的宝石是Zendesk的ruby-kafka,它非常棒!但你仍然需要做所有的事情。不过,你还是需要手动完成所有的实现,这就是为什么我们有一些与ruby-kafka一起构建的 "框架"。它们也能帮助我们完成所有的配置和执行步骤。

Karafka是一个框架,用于简化基于Apache Kafka的Ruby应用开发。

要使用Kafka,必须要安装Java。因为Kafka也是一个Scala和Java应用,所以需要安装Zookeeper

在安装之前,我想先解释一下Zookeeper的情况。Zookeeper是Kafka必不可少的集中式服务;它在发生变化时发送通知,如创建新主题、代理崩溃、移除代理、删除主题等等。

它的主要任务是管理Kafka经纪人,维护一个带有各自元数据的列表,并促进健康检查机制。此外,它还有助于为不同分区的主题选择领先的经纪人。

要求

对于MacOS:

现在,让我们用以下命令安装Java和Zookeeper。

brew install java
brew install zookeeper

然后,我们可以继续安装Kafka运行这个。

brew install kafka

一旦我们安装了Kafka和Zookeeper,就有必要这样启动服务。

brew services start zookeeper
brew services start kafka

对于Windows和Linux:

说明:

  1. 安装Java
  2. 下载Zookeeper

设置Rails

像往常一样创建一个简单的Rails应用程序。

rails new karafka_example

并在Gemfile中添加karafka gem。

gem 'karafka'

然后,运行bundle install ,安装最近添加的gem,别忘了运行下面的命令来获取所有Karafka的东西。

bundle exec karafka install

该命令应该会生成一些有趣的文件:第一个是根目录下的karafka.rbapp/consumers/application_consumer.rb ,和app/responders/application_responder.rb

Karafka初始化器

karafka.rb 文件就像一个与Rails config分离的初始化程序。它允许你配置Karafka应用,并绘制一些路由,在API方面与Rails应用路由类似。但在这里,它是针对主题和消费者的。

生产者

生产者负责创建事件,我们可以把它们添加到app/responders 。现在,让我们为用户做一个简单的生产者。

# app/responders/users_responder.rb

class UsersResponder < ApplicationResponder
  topic :users

  def respond(event_payload)
    respond_to :users, event_payload
  end
end

消费者

消费者负责读取所有从生产者那里发出的事件/消息。这只是一个记录收到的消息的消费者。

# app/consumers/users_consumer.rb

class UsersConsumer < ApplicationConsumer
  def consume
    Karafka.logger.info "New [User] event: #{params}"
  end
end

我们使用params 来获取事件。但如果你要分批读取事件,并且你的配置config.batch_fetching 为真,你应该使用params_batch

测试

要运行我们的Karafka服务(将听到事件的那个),请进入控制台,打开一个新标签,进入Rails项目,然后运行。

bundle exec karafka server

成功事件

现在,打开另一个控制台标签,转到Rails项目,然后输入这个。

rails c

在那里,让我们用我们的应答器创建一个事件。

> UsersResponder.call({ event_name: "user_created", payload: { user_id: 1 } })

如果你检查Rails控制台,在事件创建后我们会收到这样的消息。

Successfully appended 1 messages to users/0 on 192.168.1.77:9092 (node_id=0)
=> {"users"=>[["{\"event_name\":\"user_created\",\"payload\":{\"user_id\":1}}", {:topic=>"users"}]]}

而在Karafka服务标签中,你会看到这样的内容。

New [User] event: #<Karafka::Params::Params:0x00007fa76f0316c8>
Inline processing of topic users with 1 messages took 0 ms
1 message on users topic delegated to UsersConsumer
[[karafka_example] {}:] Marking users/0:1 as processed
[[karafka_example] {}:] Committing offsets: users/0:2
[[karafka_example] {}:] [offset_commit] Sending offset_commit API request 28 to 192.168.1.77:9092

但如果你只想要消息的有效载荷,你可以在你的消费者中添加params.payload ,你会有这样的东西。

Params deserialization for users topic successful in 0 ms
New [User] event: {"event_name"=>"user_created", "payload"=>{"user_id"=>1}}
Inline processing of topic users with 1 messages took 1 ms
1 message on users topic delegated to UsersConsumer

失败事件

你可以运行下面的命令创建一个带有一些属性的用户模型,比如email,first_namelast_name

rails g model User email first_name last_name

然后,你可以用这个命令运行迁移。

rails db:migrate

现在,添加一些验证,像这样。

class User < ApplicationRecord
  validates :email, uniqueness: true
end

最后,我们可以改变消费者。

class UsersConsumer < ApplicationConsumer
  def consume
    Karafka.logger.info "New [User] event: #{params.payload}"
    User.create!(params.payload['user'])
  end
end

所以,让我们创建两个具有相同电子邮件的事件。

UsersResponder.call({ event_name: "user_created", user: { user_id: 1, email: 'batman@mail.com', first_name: 'Bruce', last_name: 'Wayne' } } )

UsersResponder.call({ event_name: "user_created", user: { user_id: 2, email: 'batman@mail.com', first_name: 'Bruce', last_name: 'Wayne' } } )

这样,第一个事件就在数据库中创建了。

New [User] event: {"event_name"=>"user_created", "user"=>{"user_id"=>1, "email"=>"batman@mail.com", "first_name"=>"Bruce", "last_name"=>"Wayne"}}
[[karafka_example] {users: 0}:] [fetch] Received response 2 from 192.168.1.77:9092
[[karafka_example] {users: 0}:] Fetching batches
[[karafka_example] {users: 0}:] [fetch] Sending fetch API request 3 to 192.168.1.77:9092
[[karafka_example] {users: 0}:] [fetch] Waiting for response 3 from 192.168.1.77:9092
  TRANSACTION (0.1ms)  BEGIN
  ↳ app/consumers/users_consumer.rb:14:in `consume'
  User Create (9.6ms)  INSERT INTO "users" ("user_id", "email", "first_name", "last_name", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["user_id", "1"], ["email", "batman@mail.com"], ["first_name", "Bruce"], ["last_name", "Wayne"], ["created_at", "2021-03-10 04:29:14.827778"], ["updated_at", "2021-03-10 04:29:14.827778"]]
  ↳ app/consumers/users_consumer.rb:14:in `consume'
  TRANSACTION (5.0ms)  COMMIT
  ↳ app/consumers/users_consumer.rb:14:in `consume'
Inline processing of topic users with 1 messages took 70 ms
1 message on users topic delegated to UsersConsumer

但第二个会失败,因为我们有一个验证,说电子邮件是唯一的。如果你试图用现有的电子邮件添加另一条记录,你会看到这样的情况。

New [User] event: {"event_name"=>"user_created", "user"=>{"user_id"=>2, "email"=>"batman@mail.com", "first_name"=>"Bruce", "last_name"=>"Wayne"}}
[[karafka_example] {users: 0}:] [fetch] Received response 2 from 192.168.1.77:9092
[[karafka_example] {users: 0}:] Fetching batches
[[karafka_example] {users: 0}:] [fetch] Sending fetch API request 3 to 192.168.1.77:9092
[[karafka_example] {users: 0}:] [fetch] Waiting for response 3 from 192.168.1.77:9092
  TRANSACTION (0.2ms)  BEGIN
  ↳ app/consumers/users_consumer.rb:14:in `consume'
  User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = $1 LIMIT $2  [["email", "batman@mail.com"], ["LIMIT", 1]]
  ↳ app/consumers/users_consumer.rb:14:in `consume'
  TRANSACTION (0.2ms)  ROLLBACK
  ↳ app/consumers/users_consumer.rb:14:in `consume'
[[karafka_example] {users: 0}:] Exception raised when processing users/0 at offset 42 -- ActiveRecord::RecordInvalid: Validation failed: Email has already been taken

你可以看到最后一行的错误ActiveRecord::RecordInvalid: Validation failed: Email has already been taken 。但这里有趣的是,Kafka会尝试处理这个事件,一次又一次。即使你重新启动Karafka服务器,它也会尝试处理最后一个事件。Kafka是如何知道从哪里开始的呢?

如果你看到你的控制台,在出错之后,你会看到这个。

[[karafka_example] {users: 0}:] Exception raised when processing users/0 at offset 42

它会告诉你哪个偏移量被处理了:在这个例子中,是偏移量42。所以,如果你重新启动Karafka服务,它将在那个偏移量上启动。

[[karafka_example] {}:] Committing offsets with recommit: users/0:42
[[karafka_example] {users: 0}:] Fetching batches

它仍然会失败,因为我们的用户模型里有电子邮件验证。在这一点上,停止Karafka服务器,删除或注释该验证,并再次启动你的服务器;你会看到事件被成功处理。

[[karafka_example] {}:] Committing offsets with recommit: users/0:42
[[karafka_example] {}:] [offset_commit] Sending offset_commit API request 5 to 192.168.1.77:9092
[[karafka_example] {}:] [offset_commit] Waiting for response 5 from 192.168.1.77:9092
[[karafka_example] {}:] [offset_commit] Received response 5 from 192.168.1.77:9092
Params deserialization for users topic successful in 0 ms
New [User] event: {"event_name"=>"user_created", "user"=>{"user_id"=>2, "email"=>"batman@mail.com", "first_name"=>"Bruce", "last_name"=>"Wayne"}}
  TRANSACTION (0.2ms)  BEGIN
  ↳ app/consumers/users_consumer.rb:14:in `consume'
  User Create (3.8ms)  INSERT INTO "users" ("user_id", "email", "first_name", "last_name", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5, $6) RETURNING "id"  [["user_id", "2"], ["email", "batman@mail.com"], ["first_name", "Bruce"], ["last_name", "Wayne"], ["created_at", "2021-03-10 04:49:37.832452"], ["updated_at", "2021-03-10 04:49:37.832452"]]
  ↳ app/consumers/users_consumer.rb:14:in `consume'
  TRANSACTION (5.5ms)  COMMIT
  ↳ app/consumers/users_consumer.rb:14:in `consume'
Inline processing of topic users with 1 messages took 69 ms
1 message on users topic delegated to UsersConsumer
[[karafka_example] {}:] Marking users/0:43 as processed

最后,你可以在最后一行看到这个消息:Marking users/0:43 as processed

回调,心跳,和提交

回调

这是Karafka提供的很酷的东西:你可以在你的消费者中使用回调。要做到这一点,你只需要导入模块并使用它们。然后,打开你的UserConsumer ,添加这个:

class UsersConsumer < ApplicationConsumer
  include Karafka::Consumers::Callbacks

  before_poll do
    Karafka.logger.info "*** Checking something new for #{topic.name}"
  end

  after_poll do
    Karafka.logger.info '*** We just checked for new messages!'
  end

  def consume
    Karafka.logger.info "New [User] event: #{params.payload}"
    User.create!(params.payload['user'])
  end
end

轮询是一种媒介,我们通过它来获取基于当前分区偏移的记录。所以,那些回调before_pollafter_poll ,就像它们的名字一样,在那一刻被执行。我们只是在记录一条信息,你可以在你的Karafka服务器中看到它们--一个在获取之前,另一个在获取之后。

*** Checking something new for users
[[karafka_example] {}:] No batches to process
[[karafka_example] {users: 0}:] [fetch] Received response 325 from 192.168.1.77:9092
[[karafka_example] {users: 0}:] Fetching batches
[[karafka_example] {users: 0}:] [fetch] Sending fetch API request 326 to 192.168.1.77:9092
[[karafka_example] {users: 0}:] [fetch] Waiting for response 326 from 192.168.1.77:9092
*** We just checked for new messages!

心跳

心跳是我们作为消费者向Kafka表示我们还活着的方式;否则,Kafka会认为消费者已经死了。

在Karafka中,我们有一个默认的配置,在一段时间内做这件事;它是kafka.heartbeat_interval ,默认是10秒。你可以在你的Karafka服务器中看到这个心跳声:

*** Checking something new for users
[[karafka_example_example] {}:] Sending heartbeat...
[[karafka_example_example] {}:] [heartbeat] Sending heartbeat API request 72 to 192.168.1.77:9092
[[karafka_example_example] {}:] [heartbeat] Waiting for response 72 from 192.168.1.77:9092
[[karafka_example_example] {}:] [heartbeat] Received response 72 from 192.168.1.77:9092
*** We just checked for new messages!

通过Sending heartbeat... ,Kafka知道我们是活着的,我们是其消费者组的有效成员。同时,我们可以消费更多的记录。

承诺

将一个偏移量标记为已消耗,称为提交一个偏移量。在Kafka中,我们通过写到Kafka的内部主题(称为offsets主题)来记录偏移量的提交。只有当一个消息的偏移量被提交到偏移量主题时,它才被认为是被消耗了。

Karafka有一个配置,可以每次自动进行这种提交;这个配置是kafka.offset_commit_interval ,其值默认为10秒。有了这个,Karakfa就会每10秒进行一次偏移量提交,你可以在你的Karafka服务器中查看该消息。

*** Checking something new for users
[[karafka_example] {}:] No batches to process
[[karafka_example] {users: 0}:] [fetch] Received response 307 from 192.168.1.77:9092
[[karafka_example] {users: 0}:] Fetching batches
[[karafka_example] {users: 0}:] [fetch] Sending fetch API request 308 to 192.168.1.77:9092
[[karafka_example] {users: 0}:] [fetch] Waiting for response 308 from 192.168.1.77:9092
[[karafka_example] {}:] Committing offsets: users/0:44
[[karafka_example] {}:] [offset_commit] Sending offset_commit API request 69 to 192.168.1.77:9092
[[karafka_example] {}:] [offset_commit] Waiting for response 69 from 192.168.1.77:9092
[[karafka_example] {}:] [offset_commit] Received response 69 from 192.168.1.77:9092
*** We just checked for new messages!

Committing offsets: users/0:44 ,告诉我们它在提交哪个偏移量;在我的例子中,它告诉Kafka,它可以提交主题0的偏移量44。这样一来,如果我们的服务发生了问题,Karafka就可以重新开始处理那个偏移量的事件。

总结

事件流帮助我们更快,更好地利用数据,并设计更好的用户体验。事实上,许多公司都在使用这种模式来沟通他们所有的服务,并能够对不同的事件做出实时反应。正如我之前提到的,除了Karafka之外,还有其他的替代品,你可以和Rails一起使用。你已经掌握了基础知识;现在,请你自由地尝试一下吧。