公司希望对实时处理和分享大量数据的需求做出快速反应,以获得洞察力并创造更有吸引力的客户体验。所以,传统的数据处理在当今世界已经不可行了。
为了实现这一目标,你需要尽可能快地处理大量的数据,然后将其发送到其他服务进行更多的处理。但在所有这些快速行动中,有必要在事件发生时通知消费者--我们可以使用事件流来实现这一点。
这是我们将要使用的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:
说明:
设置Rails
像往常一样创建一个简单的Rails应用程序。
rails new karafka_example
并在Gemfile中添加karafka gem。
gem 'karafka'
然后,运行bundle install ,安装最近添加的gem,别忘了运行下面的命令来获取所有Karafka的东西。
bundle exec karafka install
该命令应该会生成一些有趣的文件:第一个是根目录下的karafka.rb ,app/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_name 和last_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_poll 和after_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一起使用。你已经掌握了基础知识;现在,请你自由地尝试一下吧。