KOA + egg.js 整合 kafka 訊息佇列

Knove發表於2018-11-08

Egg.js : 基於KOA2的企業級框架

Kafka:高吞吐量的分散式釋出訂閱訊息系統

本文章將整合egg + kafka + mysql 的日誌系統例子

系統要求:日誌記錄,通過kafka進行訊息佇列控制

思路圖:

KOA + egg.js 整合 kafka 訊息佇列
這裡消費者和生產者都由日誌系統提供

λ.1 環境準備

①Kafka

官網下載kafka後,解壓

啟動zookeeper:

bin/zookeeper-server-start.sh config/zookeeper.properties
複製程式碼

啟動Kafka server

這裡config/server.properties中將num.partitions=5,我們設定5個partitions

bin/kafka-server-start.sh config/server.properties
複製程式碼

② egg + mysql

根據腳手架搭建好egg,再多安裝kafka-node,egg-mysql

mysql 使用者名稱root 密碼123456

λ.2 整合

  1. 根目錄新建app.js,這個檔案在每次專案載入時候都會運作
'use strict';
 
const kafka = require('kafka-node');
 
module.exports = app => {
  app.beforeStart(async () => {
    const ctx = app.createAnonymousContext();
 
    const Producer = kafka.Producer;
    const client = new kafka.KafkaClient({ kafkaHost: app.config.kafkaHost });
    const producer = new Producer(client, app.config.producerConfig);
 
    producer.on('error', function(err) {
      console.error('ERROR: [Producer] ' + err);
    });
 
    app.producer = producer;
 
    const consumer = new kafka.Consumer(client, app.config.consumerTopics, {
      autoCommit: false,
    });
 
    consumer.on('message', async function(message) {
      try {
        await ctx.service.log.insert(JSON.parse(message.value));
        consumer.commit(true, (err, data) => {
          console.error('commit:', err, data);
        });
      } catch (error) {
        console.error('ERROR: [GetMessage] ', message, error);
      }
    });
 
    consumer.on('error', function(err) {
      console.error('ERROR: [Consumer] ' + err);
    });
  });
};
複製程式碼

上述程式碼新建了生產者、消費者。

生產者新建後載入進app全域性物件。我們將在請求時候生產訊息。這裡只是先新建例項

消費者獲取訊息將訪問service層的insert方法(資料庫插入資料)。

具體引數可以參考kafka-node官方API,往下看會有生產者和消費者的配置引數。

  1. controller · log.js

這裡獲取到了producer,並傳往service層

'use strict';
 
const Controller = require('egg').Controller;
 
class LogController extends Controller {
  /**
   * @description Kafka控制日誌資訊流
   * @host /log/notice
   * @method POST
   * @param {Log} log 日誌資訊
   */
  async notice() {
    const producer = this.ctx.app.producer;
    const Response = new this.ctx.app.Response();
 
    const requestBody = this.ctx.request.body;
    const backInfo = await this.ctx.service.log.send(producer, requestBody);
    this.ctx.body = Response.success(backInfo);
  }
}
 
module.exports = LogController;
複製程式碼
  1. service · log.js

這裡有一個send方法,這裡呼叫了producer.send ,進行生產者生產

insert方法則是資料庫插入資料

'use strict';
 
const Service = require('egg').Service;
const uuidv1 = require('uuid/v1');
 
class LogService extends Service {
  async send(producer, params) {
    const payloads = [
      {
        topic: this.ctx.app.config.topic,
        messages: JSON.stringify(params),
      },
    ];
 
    producer.send(payloads, function(err, data) {
      console.log('send : ', data);
    });
 
    return 'success';
  }
  async insert(message) {
    try {
      const logDB = this.ctx.app.mysql.get('log');
      const ip = this.ctx.ip;
 
      const Logs = this.ctx.model.Log.build({
        id: uuidv1(),
        type: message.type || '',
        level: message.level || 0,
        operator: message.operator || '',
        content: message.content || '',
        ip,
        user_agent: message.user_agent || '',
        error_stack: message.error_stack || '',
        url: message.url || '',
        request: message.request || '',
        response: message.response || '',
        created_at: new Date(),
        updated_at: new Date(),
      });
 
      const result = await logDB.insert('logs', Logs.dataValues);
 
      if (result.affectedRows === 1) {
        console.log(`SUCEESS: [Insert ${message.type}]`);
      } else console.error('ERROR: [Insert DB] ', result);
    } catch (error) {
      console.error('ERROR: [Insert] ', message, error);
    }
  }
}
 
module.exports = LogService;
複製程式碼
  1. config · config.default.js

一些上述程式碼用到的配置引數具體在這裡,注這裡開了5個partition。

'use strict';
 
module.exports = appInfo => {
  const config = (exports = {});
 
  const topic = 'logAction_p5';
 
  // add your config here
  config.middleware = [];
 
  config.security = {
    csrf: {
      enable: false,
    },
  };
 
  // mysql database configuration
  config.mysql = {
    clients: {
      basic: {
        host: 'localhost',
        port: '3306',
        user: 'root',
        password: '123456',
        database: 'merchants_basic',
      },
      log: {
        host: 'localhost',
        port: '3306',
        user: 'root',
        password: '123456',
        database: 'merchants_log',
      },
    },
    default: {},
    app: true,
    agent: false,
  };
 
  // sequelize config
  config.sequelize = {
    dialect: 'mysql',
    database: 'merchants_log',
    host: 'localhost',
    port: '3306',
    username: 'root',
    password: '123456',
    dialectOptions: {
      requestTimeout: 999999,
    },
    pool: {
      acquire: 999999,
    },
  };
 
  // kafka config
  config.kafkaHost = 'localhost:9092';
 
  config.topic = topic;
 
  config.producerConfig = {
    // Partitioner type (default = 0, random = 1, cyclic = 2, keyed = 3, custom = 4), default 0
    partitionerType: 1,
  };
 
  config.consumerTopics = [
    { topic, partition: 0 },
    { topic, partition: 1 },
    { topic, partition: 2 },
    { topic, partition: 3 },
    { topic, partition: 4 },
  ];
 
  return config;
};
複製程式碼
  1. 實體類:

mode · log.js

這裡使用了 Sequelize

'use strict';
 
module.exports = app => {
  const { STRING, INTEGER, DATE, TEXT } = app.Sequelize;
 
  const Log = app.model.define('log', {
    /**
     * UUID
     */
    id: { type: STRING(36), primaryKey: true },
    /**
     * 日誌型別
     */
    type: STRING(100),
    /**
     * 優先等級(數字越高,優先順序越高)
     */
    level: INTEGER,
    /**
     * 操作者
     */
    operator: STRING(50),
    /**
     * 日誌內容
     */
    content: TEXT,
    /**
     * IP
     */
    ip: STRING(36),
    /**
     * 當前使用者代理資訊
     */
    user_agent: STRING(150),
    /**
     * 錯誤堆疊
     */
    error_stack: TEXT,
    /**
     * URL
     */
    url: STRING(255),
    /**
     * 請求物件
     */
    request: TEXT,
    /**
     * 響應物件
     */
    response: TEXT,
    /**
     * 建立時間
     */
    created_at: DATE,
    /**
     * 更新時間
     */
    updated_at: DATE,
  });
 
  return Log;
};

複製程式碼
  1. 測試Python指令碼:
import requests
 
from multiprocessing import Pool
from threading import Thread
 
from multiprocessing import Process
 
 
def loop():
    t = 1000
    while t:
        url = "http://localhost:7001/log/notice"
 
        payload = "{\n\t\"type\": \"ERROR\",\n\t\"level\": 1,\n\t\"content\": \"URL send ERROR\",\n\t\"operator\": \"Knove\"\n}"
        headers = {
        'Content-Type': "application/json",
        'Cache-Control': "no-cache"
        }
 
        response = requests.request("POST", url, data=payload, headers=headers)
 
        print(response.text)
 
if __name__ == '__main__':
    for i in range(10):
        t = Thread(target=loop)
        t.start()
複製程式碼
  1. 建表語句:
 
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
 
-- ----------------------------
-- Table structure for logs
-- ----------------------------
DROP TABLE IF EXISTS `logs`;
CREATE TABLE `logs`  (
  `id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL COMMENT '日誌型別',
  `level` int(11) NULL DEFAULT NULL COMMENT '優先等級(數字越高,優先順序越高)',
  `operator` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '操作人',
  `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '日誌資訊',
  `ip` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT 'IP\r\nIP',
  `user_agent` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '當前使用者代理資訊',
  `error_stack` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '錯誤堆疊',
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '當前URL',
  `request` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '請求物件',
  `response` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL COMMENT '響應物件',
  `created_at` datetime(0) NULL DEFAULT NULL COMMENT '建立時間',
  `updated_at` datetime(0) NULL DEFAULT NULL COMMENT '更新時間',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;
 
SET FOREIGN_KEY_CHECKS = 1;
複製程式碼

λ.3 後話

網上類似資料甚少,啃各種文件,探尋技術實現方式

喜歡請點個贊,若有疑問可以評論,非常樂意幫助解決問題

相關文章