前言

这是这个项目的 Github 地址 DAWN0ER/WordCount

这个项目发起前我还没有实习,基本就是头铁一顿乱写,功能可以实现就行,然后就写的十分不规范和诡异,比如RPC 没有防腐层,Dao 层直接和 Mapper 交互,而没有中间一层包装器,redis 和 kafka 也是直接交互,做的十分粗糙,自己有点看不下去了,就打算重构整个项目,然后就发生悲剧了。

技术选型

架构:SSM(SpringBoot,Spring MVC,Mybatis)

RPC框架:Dubbo

注册中心:Zookeeper (不用 Nacos 了,感觉有点大)

定时任务:Quartz

消息队列:Kafka(可以考虑用 Kraft 模式,不过都有 Zookeeper 了就直接用 Zookeeper)

数据库:MySQL

架构设计

StreamMapReduce.png

WordCount 服务层

  • 文件存储:
    • 文件上传,分区块储存到 MySQL,区块大小要求:一个 Worker 能处理的大小
      • TODO: 引入多数据源(业务数据源和 Quartz 数据源)
    • 文件下载,分区块存储的能够一次性全部下载完成
      • TODO: 如果文件真的很大的话,需要考虑用其他的 S3 存储方案了,暂时这样
  • 分词计数
    • 发起对指定文件的计数任务
      • 添加一个延时守护线程,用来多次轮询一个任务的完成情况并同步
      • 需要在 Redis 中队 BitMap 设置超时时间
    • 获取指定文件前K个热点词
      • TODO 暂时使用 ORDER BY 后续可以性能优化
    • 获取指定文件指定词(可多个)的计数

Worker 集群

  • 依靠 Dubbo 自己的负载均衡策略(默认随机均衡)
  • 通过 RPC 调用发起分词计数任务
  • 作为 Kafka 消息的生产者
  • 采用 Kafka 的 Partition 特性实现 Shuffle,指定 Partition 发送对应消息
  • 不同 Hash 的负载均衡

Reducer 集群

  • Kafka 消费者集群对词进行整合,同步进度

实现方案

Restful URL

/api/v2/file

功能方法路径RequestBody
上传文件POST/upload@RequestParam("file") MultipartFile file
下载文件GET/download/{fileUid}-

/api/v2/word-count

功能方法路径RequestBody
开启计数POST/count@RequestBody int fileUid
获取进度GET/progress/{taskId}-
topK热点词GET/file/{fileUid}/top/{K}-
获取指定words的计数GET/file/{fileUid}/words@RequestBody List<String> words

RPC接口

public interface WorkerService {
    /**
     * 同步计数
     * @param chunkCountTaskDto 入参
     * @return 返回 map 到的 partition 数量
     */
    Integer countWordsOfChunk(ChunkCountTaskDto chunkCountTaskDto);

    /**
     * 异步完成计数
     * @param chunkCountTaskDto 入参
     */
    void countWordOfChunkAsync(ChunkCountTaskDto chunkCountTaskDto);

消息队列

topic: word_count

数据库

CREATE TABLE IF NOT EXISTS file_info
(
    id           BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
    file_uid     INT(10) UNSIGNED    NOT NULL COMMENT '文件标识id',
    file_name    VARCHAR(128)        NOT NULL COMMENT '文件名',
    chunk_num    INT(10)             NOT NULL COMMENT '分区数量',
    status       TINYINT             NOT NULL DEFAULT 0 COMMENT '文件状态(1-初始化, 2-已存储, 3-损坏, 4-任务中, 5-任务完成)',
    created_time DATETIME            NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_time DATETIME            NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    
    PRIMARY KEY pk (id),
    UNIQUE KEY file_uid_key (file_uid)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  AUTO_INCREMENT = 1 COMMENT '文件基础信息表';

CREATE TABLE IF NOT EXISTS file_chunks
(
    id       BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
    file_uid INT(10) UNSIGNED    NOT NULL COMMENT '文件标识id',
    chunk_id INT(10)             NOT NULL COMMENT '分区id',
    context  TEXT                NOT NULL COMMENT '分区主要内容',
    
    PRIMARY KEY id_key (id),
    UNIQUE KEY file_uid_chunk_id (file_uid, chunk_id)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  AUTO_INCREMENT = 1 COMMENT '文件存储表';

CREATE TABLE IF NOT EXISTS word_count
(
    id       BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
    file_uid INT(10) UNSIGNED    NOT NULL COMMENT '文件标识id',
    word     CHAR(20)            NOT NULL COMMENT '分词',
    cnt      INT                 NOT NULL COMMENT '计数',
    
    PRIMARY KEY id (id),
    UNIQUE KEY file_word (file_uid, word)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  AUTO_INCREMENT = 1 COMMENT '分词计数表';

CREATE TABLE IF NOT EXISTS word_count_task
(
    id         BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
    task_id	   BIGINT(20) UNSIGNED NOT NULL COMMENT '任务标识id',
    file_uid   INT(10) UNSIGNED NOT NULL COMMENT '文件标识id',
    status     INT NOT NULL DEFAULT 0 COMMENT '1-待完成,2-已完成,3-异常',
    created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_time  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

    PRIMARY KEY pk (id),
    UNIQUE KEY task_id_key (task_id),
    KEY file_uid_key (file_uid)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  AUTO_INCREMENT = 1 COMMENT '分词统计任务表';

可能存在的坑点

  • Dubbo 测试的 Mock 问题
  • Quartz 的多数据源问题
  • RPC 服务的循环依赖问题
  • Redis 粗颗粒锁更新 BitMap
  • 重构就是噩梦

重构后的感悟

谁爱重构谁去重构去吧,虽然每次看到自己以前写的这些 Shit 真的很难受,但重写所有功能让人感觉更加难绷,过去垃圾代码的回旋镖终究是回到了自己身上。

关于程序员的东西也思考了很多,最后还是觉得自己应该是一个以业务场景为驱动的研发工程师,技术终究是用不完也学不完的,但技术是为了业务服务的,做业务的程序员死磕技术上的各种效能和比较没有意义,只有放在场景里面才有效,去学架构设计和业务场景的开发吧,同时也不能忘记各种新的知识的学习。