node.js中kafka的封装和高并发消费限流优雅降级以及egg-kafka的封装说明

2,771 阅读5分钟

HI!,你好,我是zane,zanePerfor是一款我开发的一个前端性能监控平台,现在支持web浏览器端和微信小程序端。

我定义为一款完整,高性能,高可用的前端性能监控系统,这是未来会达到的目的,现今的架构也基本支持了高可用,高性能的部署。实际上还不够,在很多地方还有优化的空间,我会持续的优化和升级。

开源不易,如果你也热爱技术,拥抱开源,希望能小小的支持给个star。

项目的github地址:github.com/wangweiange…

项目开发文档说明:blog.seosiwei.com/performance…


Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。
Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。
这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。
这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。
针对于zanePerfor这样的用户访问行为,页面性能监控系统,但又要求实时处理的限制,这是一个可行的解决方案。


zanePerfor中对于kafka的应用使用了kafka-node包,并在此基础上封装了egg-kafka插件。

zanePerfor初步的探索了kafka在node.js中的应用,以下内容主要讲解kafka在zanePerfor项目中的使用方式。

如果你对在node.js中使用kafka有更多的建议和心得,也希望能跟我一起分享。


zanePerfor项目中kafka应用介绍:

启用kafka配置说明:

// config/config.default.js
// kafka 配置 (report_data_type=kafka生效)
// 配置参考 https://www.npmjs.com/package/kafka-node
config.kafka = {
    client: {
        kafkaHost: 'localhost:9092',
    },
    producer: {
        web: {
            topic: 'zane_perfor_web',
            partition: 0, // default 0
            attributes: 0, // default: 0
            // timestamp: Date.now(),
        },
        wx: {
            topic: 'zane_perfor_wx',
        },
    },
    // consumer 和 consumerGroup消费任选其一即可
    // 优先选择consumer消费,两种消费配置任留一种即可
    consumer: {
        web: {
            topic: 'zane_perfor_web',
            offset: 0, // default 0
            partition: 0, // default 0
            isone: false, // 此参数默认不可更改
            total_limit: 10000, // 消息队列消费池限制数, 0:不限制 number: 限制条数 高并发时服务优雅降级方案
        },
        wx: {
            topic: 'zane_perfor_wx',
            isone: false,
            total_limit: 10000,
        },
    },
    consumerGroup: {
        web: { // ConsumerGroup(options, topics)
            topic: 'zane_perfor_web',
            groupId: 'WebPerformanceGroup',
            commitOffsetsOnFirstJoin: true,
        },
        wx: {
            topic: 'zane_perfor_wx',
            groupId: 'WxPerformanceGroup',
            commitOffsetsOnFirstJoin: true,
        },
    },
};

配置说明:

client参数说明:

client参数即为kafka-node中的KafkaClient参数,参考地址:www.npmjs.com/package/kaf…

producer生产者参数说明:

producer分web端和wx端配置

producer参数为kafka-node中的send参数,参考地址:www.npmjs.com/package/kaf…

consumer消费者参数说明:

consumer分web端和wx端配置

consumer参数为kafka-node中的consumer参数, 参考地址:www.npmjs.com/package/kaf…

consumerGroup消费者参数说明:

consumerGroup分web端和wx端配置

consumerGroup参数为kafka-node中的consumerGroup参数,参考地址:www.npmjs.com/package/kaf…

关于消费者说明

config配置中有consumer和consumerGroup配置,规则如下:

  • 如果consumer配置为真有限使用consumer配置
  • 如果想启用consumerGroup配置,则注释或者删除consumer配置即可


kafka生产消费逻辑实现:



核心代码实现:

一:生产者

kafka的性能非常强劲,能支持超高并发,因此所有客户端上报的信息都存储到消息队列中,限流策略只使用在消费端,生产端不做限流设置。

// app/controller/api/web/report.js
// 通过kafka 消息队列消费数据
    async saveWebReportDataForKafka(query) {
        // 生产者
        this.app.kafka.send(
            'web',
            JSON.stringify(query)
        );

        // 消费者
        if (!isKafkaConsumer && !this.app.config.kafka.consumer.web.isone) {
            this.ctx.service.web.reportTask.saveWebReportDatasForKafka();
            isKafkaConsumer = true;
            this.app.config.kafka.consumer.web.isone = true;
        }
    }
  • this.app.kafka.send是封装的插件egg-kafka中的方法,功能就是生产信息

  • if (!isKafkaConsumer && !this.app.config.kafka.consumer.web.isone)是为了保证订阅消息的方法只执行一次,后面一但有消息产生,会自动触发订阅函数进行数据消费消费。

二:消费者

// app/service/web/report_task.js
// kafka 消费信息
    async saveWebReportDatasForKafka() {
        if (this.kafkaConfig.consumer) {
            this.app.kafka.consumer('web', message => {
                this.consumerDatas(message);
            });
        } else if (this.kafkaConfig.consumerGroup) {
            this.app.kafka.consumerGroup('web', message => {
                this.consumerDatas(message);
            });
        }
    }


  • this.app.kafka.consumer 单独消费,egg-kafka中暴露的方法

  • this.app.kafka.consumerGroup 以分组的方式消费消息

  • 优先使用consumer消费,其次使用consumerGroup进行消费


egg-kafka插件封装说明

为了更好、更方便的使用kafka,项目中对node-kafka进行了一层封装。

详细请参考:/lib/plugin/egg-kafka/lib/kafka.js

send代码实现如下:

send(type, data) {
        assert(type, '[egg-kafka] type is must required.');
        if (!data) return;
        let producer = this.app.config.kafka.producer[type] || {};
        let producers = [];
        if (typeof (data) === 'string') {
            producer.messages = data;
            producers = [ producer ];
        } else if (Object.prototype.toString.call(data) === '[object Object]') {
            producer = Object.assign({}, producer, data);
            producers = [ producer ];
        } else if (Object.prototype.toString.call(data) === '[object Array]') {
            for (let i = 0; i < data.length; i++) {
                data[i] = Object.assign({}, producer, data[i]);
            }
            producers = data;
        }
        this.producer.send(producers, (err, data) => {
            if (err) assert(err, '[egg-kafka] err. errmsg ${err}');
            console.log(data);
        });
    }

send有两个参数,第一个参数type为发送类型,有web、wx两个值可以选择。

对data做了一定的判断,send调用可以有以下几种方式:

// 消息为String
this.app.kafka.send('web','hello world!');
// 消息为Object
this.app.kafka.send('web',{ topic:'test', messages:'hello world!' });
// 消息为Array
this.app.kafka.send('web',[{ topic: 'test', messages: 'hi', partition: 0}]);


consumer方法代码实现:

consumer(type = 'web', fn) {
    assert(type, '[egg-kafka] consumers type argument must be required');
    const kafkaConfig = this.app.config.kafka;
    const consumer = kafkaConfig.consumer[type] || {};
    const consumers = Array.isArray(consumer) ? consumer : [ consumer ];
    const Consumer = kafka.Consumer;
    const _consumer = new Consumer(
        this.client,
        consumers,
        {
            autoCommit: true,
        }
    );
    _consumer.on('error', err => {
        this.app.coreLogger.error(`[egg-kafka] consumer have error ${err}`);
    });
    _consumer.on('message', message => {
        fn && fn(message);
    });
}


consumerGroup代码实现:

consumerGroup(type = 'web', fn) {
    assert(type, '[egg-kafka] consumers type argument must be required');
    const kafkaConfig = this.app.config.kafka;
    const kafkaHost = kafkaConfig.client.kafkaHost;
    const consumerOption = kafkaConfig.consumerGroup[type] || {};
    const topic = consumerOption.topic;
    consumerOption.kafkaHost = kafkaHost;
    const ConsumerGroup = kafka.ConsumerGroup;
    const _consumer = new ConsumerGroup(consumerOption, topic);
    _consumer.on('error', err => {
        this.app.coreLogger.error(`[egg-kafka] consumer have error ${err}`);
    });
    _consumer.on('message', message => {
        fn && fn(message);
    });
}


消费限流策略:

由于kafka性能及其强悍,因此zanePerfor只对消费进行限流



代码实现:

设置消费池数量

// config.default.js
{
	topic: 'zane_perfor_web',
	offset: 0, // default 0
	partition: 0, // default 0
	isone: false, // 此参数默认不可更改
	total_limit: 10000, // 消息队列消费池限制数, 0:不限制 number: 限制条数 高并发时服务优雅降级方案		
}


kafka连接池数量判断

// app/service/web/report_task.js 中 getWebItemDataForKafka 方法
// kafka 连接池限制
const msgtab = query.time + query.ip;
if (this.kafkatotal && this.kafkalist.length >= this.kafkatotal) return;
this.kafkalist.push(msgtab);


数据消费完成之后删除消费标识

// app/service/web/report_task.js 中 getWebItemDataForKafka 方法
this.savePages(item, system.slow_page_time, () => {
    // 释放消费池
    const index = this.kafkalist.indexOf(msgtab);
    if (index > -1) this.kafkalist.splice(index, 1);
});


至此实现了egg.js中对kafka的应用和封装。