kafka php swoole,kafka与swoole多进程消费

环境:docker

容器:kafka、zookeeper、nginx

语言:php(lumen框架)

扩展:rdkafka,swoole

本文适合对kafka有一些了解的人阅览,因为没有写关于kafka原理的东西,可能也写不清楚吧哈哈哈。本文的主题是根据设定的topic有多少个分区,为每一个分区开一个进程,该进程只消费该指定分区,即用swoole开多进程来消费kafka,保证分区内的消费顺序,是用来做数据库的binlog增量同步,使用canal+kafka,这里只写了关于消费的这一边。

首先进入kafka容器创建一个topic:

kafka-topics.sh --create --zookeeper zookeeper:2181 --replication-factor 1 --partitions 5 --topic mytest

821b96315d1b93e93e048b1af0dc90df.png

表示该topic有5个分区,1个备份(kafka的broker可以配置多个服务,变成一个集群,我这里只有一个broker所以备份只能配1个)。

查看topic列表:

kafka-topics.sh --zookeeper zookeeper:2181 --list

d97a0ef2e62ea05f0a246bcb29a4f2e9.png

就出现了刚刚创建的mytest。"__consumer_offsets"这个topic是kafka内置的topic,用来保存每个topic的每个分区当前消费到的offset。

由于kafka要删除topic或者消息记录挺麻烦的,这里删那里删要删好几处,所以我总是新建topic做新的测试。

查看topic详情:

kafka-topics.sh --zookeeper zookeeper:2181 --describe --topic mytest

cc3955e411d788a2eac4ea9a7ce6a457.png

生产者代码,就是个例子,增量同步生产的时候用的是canal,不过这个可以做一次性同步的生产者:

namespace App\Services;

class KafkaService

{

protected $producer;

protected $topic;

/**发送消息到kafka

* @param $playload

* @param $key

*/

public function produce($playload, $key)

{

$producer = $this->producer();

$topic = $producer->newTopic($this->topic);

//采用默认的随机方法选择分区,$key是用来选择分区的指标

$topic->produce(RD_KAFKA_PARTITION_UA, 0, $playload, $key);

while($producer->getOutQLen() > 0) {

$producer->poll(50);

}

}

/**生产者

* @return \RdKafka\Producer

*/

protected function producer()

{

if(!$this->producer) {

$producer = new \RdKafka\Producer();

$producer->addBrokers($this->getBroker());

$this->producer = $producer;

}

return $this->producer;

}

/**获取kafka server list

* @return mixed

*/

protected function getBroker()

{

return env('KAFKA_BROKERS', '127.0.0.1:9092');

}

/**设置topic

* @param $topic

*/

public function setTopic($topic)

{

$this->topic = $topic;

}

}

其实都是php rdkafka给出的官方例子。

获取topic有多少分区:

public function getPartitions()

{

$consumer = new \RdKafka\Consumer();

$consumer->addBrokers($this->getBroker());

$topic = $consumer->newTopic($this->topic);

$allInfo = $consumer->getMetadata(false, $topic, 60e3);

$topics = $allInfo->getTopics();

foreach($topics as $tp) {

$partitions = $tp->getPartitions();

break;

}

return $partitions;

}

Rdkafka提供了两种消费者,一种是high level消费者,一种是low level消费者,一开始我也没能理解这两种分别该用到什么场景下,两种都试着用了一下。看到官方例子里的注释,说high level有自动均衡机制,就是会自动分配分区给消费者,于是我就认为high level只用开一个进程就能消费到所有的分区,虽然的确可以这样。

High level消费者:

public function highlevelConsumer()

{

$conf = new \RdKafka\Conf();

//这里就是自动均衡分配分区给消费者的意思

$conf->setRebalanceCb(function(\RdKafka\KafkaConsumer $kafka, $err, array $partitions = null){

switch($err) {

case RD_KAFKA_RESP_ERR__ASSIGN_PARTITIONS:

$kafka->assign($partitions);

break;

case RD_KAFKA_RESP_ERR__REVOKE_PARTITIONS:

$kafka->assign(null);

break;

default:

throw new \Exception($err);

}

});

$conf->set('group.id', 'comsumer-group-'.$this->topic);

$conf->set('client.id', 'client-'.$this->topic);

$conf->set('bootstrap.servers', $this->getBroker());

$topicConf = new \RdKafka\TopicConf();

$topicConf->set('auto.offset.reset', 'smallest');

$conf->setDefaultTopicConf($topicConf);

$consumer = new \RdKafka\KafkaConsumer($conf);

$consumer->subscribe([$this->topic]);

while(true) {

$message = $consumer->consume(120 * 1000);

switch($message->err) {

case RD_KAFKA_RESP_ERR_NO_ERROR:

echo $this->topic.'-'.$message->partition.'-'.$message->offset."\n";

sleep(1);

//这里需要提交一下,使得更新broker上保存的offset,不然似乎offset不会自动提交

$consumer->commit($message);

break;

case RD_KAFKA_RESP_ERR__PARTITION_EOF:

case RD_KAFKA_RESP_ERR__TIMED_OUT:

break;

default:

throw new \Exception($message->errstr(), $message->err);

break;

}

}

}

所以关于消费者我选择的是low level的,因为它可以指定分区,一开始的目的是开多进程让每一个进程监听一个分区,这样就可以保证分区内是顺序执行。

low level消费者:

public function createConsumer($partionId)

{

$conf = new \RdKafka\Conf();

//指定消费组

$conf->set('group.id', 'comsumer-group-'.$this->topic);

$conf->set('bootstrap.servers', $this->getBroker());

$rk = new \RdKafka\Consumer($conf);

$topicConf = new \RdKafka\TopicConf();

//自动提交offset间隔时间

$topicConf->set('auto.commit.interval.ms', 100);

//保存offset的方式,可以选择broker或者file

$topicConf->set('offset.store.method', 'broker');

//如果没有检测到有保存的offset,就从最小开始

$topicConf->set('auto.offset.reset', 'smallest');

$topic = $rk->newTopic($this->topic, $topicConf);

$topic->consumeStart($partionId, RD_KAFKA_OFFSET_STORED);

while(true) {

$message = $topic->consume($partionId, 120 * 10000);

switch($message->err) {

case RD_KAFKA_RESP_ERR_NO_ERROR:

echo $this->topic.'-'.$message->partition.'-'.$message->offset."\n";

sleep(1);

break;

case RD_KAFKA_RESP_ERR__PARTITION_EOF:

case RD_KAFKA_RESP_ERR__TIMED_OUT:

break;

default:

throw new \Exception($message->errstr(), $message->err);

break;

}

}

}

然而研究了这么久也还是没弄清楚这两种情况下消费组里的消费者是否存在,是一个还是多个?

接下来把swoole用上,这里用的是laravel的command类和swoole_process。

namespace App\Console\Commands;

use App\Services\KafkaService;

use Illuminate\Console\Command;

use \swoole_process;

class DataSyncCommand extends Command

{

protected $signature = 'import:datasync {topic}';

protected $description = '多进程队列';

public $mpid = 0;

public $works = [];

public $kafka;

public $processName;

public function handle(KafkaService $kafka)

{

$topic = $this->argument('topic');

$this->kafka=$kafka;

$this->kafka->setTopic($topic);

$this->processName = 'kafka-queue:'.$topic;

swoole_set_process_name($this->processName.'-master');

$this->mpid = posix_getpid();

$partitions = $this->kafka->getPartitions();

foreach($partitions as $part) {

$partionId = $part->getId();

$this->createProcess($partionId);

}

$this->processWait();

}

public function createProcess($partionId)

{

$process = new swoole_process(function(swoole_process $worker) use ($partionId){

swoole_set_process_name($this->processName.'-'.$partionId);

$this->kafka->createConsumer($partionId); //low level

//$this->kafka->highlevelConsumer(); //high level

}, false, false);

$pid = $process->start();

$this->works[$partionId] = $pid;

return $pid;

}

public function checkMpid(&$worker)

{

if(!swoole_process::kill($this->mpid, 0)) {

$worker->exit();

}

}

public function rebootProcess($ret)

{

$pid = $ret['pid'];

$index = array_search($pid, $this->works);

if($index !== false) {

$index = intval($index);

$new_pid = $this->createProcess($index);

return;

}

throw new \Exception('rebootProcess Error: no pid');

}

public function processWait()

{

while(1) {

if(count($this->works)) {

$ret = swoole_process::wait();

if($ret) {

$this->rebootProcess($ret);

}

}else {

break;

}

}

}

}

从懵懂到略懂经过了一系列头发都要掉光的研究,发现了一些结论:

在low level中保存offset的方式有两种,一种是存本地文件,可以指定存放路径,此时指定"group.id"是不起作用的,消费后在kafka容器里查看消费组列表是没有这个消费组的。另一种是存在broker上,这种方式可以看到指定的消费组出现了,但是消费时组内是没有消费者的,如下:

696b9ecb06b12b12d4d07ee6d51bddb8.png

所以我猜low level的消费者是不支持kafka消费者的。

但是这样子的话岂不是没有将kafka物尽其用?看了那么多kafka相关文章,都没有看到消费者到底是怎么操作才能出现在消费组里,一段时间只知道如何去查看topic里的消息,即执行消费命令,如下:

kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic mytest --from-beginning

b536957321dc3b8b6bba61a10d1b2614.png

然而,一直都没有想到如果此时再查看消费组详情是什么样子,但由于上面这个命令没有指定消费组,所以kafka会自动生成一个消费组来消费,这样不方便查看,所以先指定一个消费组吧:

kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic mytest --from-beginning --group testGroup

此时再开一个shell框来看消费组列表,里面就已经有了刚刚指定的testGroup:

008db92f5fc6be2d1b04344b6166b98b.png

保持刚刚的消费窗口不要撤销命令,此时查看消费组详情会出现什么?

a6b4fbcf21945f19818e927770130e06.png

有具体的consumer-id了!那如果再开一个窗口,再执行一次消费命令呢?注意要指定同一个消费组名称,这样才是同一个消费组在消费,但是此时可能不会出现消息列表,因为topic里的消息已经被刚刚那个消费者消费掉了,同一个消费组里的消费者不会重复消费消息,那么此时再查看消费组详情呢:

1cd8eab4b20cbcf1929856f47f8f79f0.png

可以看到有两个不同的consumer-id了!于是我就设想,rdkafka的high level消费者是不是也是这样用的,因为它有自动rebalance的机制。那么就用swoole根据topic分区数开启相应数量的消费者来试试看。

向topic里再发送一些消息:

d828f526b7a0f22f545c993ab9960495.png

用laravel的artistan开启消费:

085042a9dbc487ea8537689dd560e237.png

查看进程:

c92f9d1563031a50a62d6fba218808d1.png

然后查看消费组详情:

a360b6d6da39ce688974a4065b9f6675.png

成功啦!可以看到每一个分区的consumer-id都是不同的。到此,关于kafka的消费组和消费者总算是研究清楚了,但是swoole这边肯定还有很多工作需要完善。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值