• rocketmq那些事儿之入门基础


    分布式消息队列中间件作为高并发系统的核心组件之一,能够帮助业务系统解构提升开发效率和系统稳定性,其复杂性可见一斑,作为核心组件,有必要去深入了解学习

    前言

    分布式消息队列中间件主要具有以下优势:

    • 削峰填谷(主要解决瞬时写压力大于应用服务能力导致消息丢失、系统奔溃等问题)
    • 系统解耦(解决不同重要程度、不同能力级别系统之间依赖导致一死全死)
    • 提升性能(当存在一对多调用时,可以发一条消息给消息系统,让消息系统通知相关系统)
    • 蓄流压测(线上有些链路不好压测,可以通过堆积一定量消息再放开来压测)

    笔者为什么要深入rocketmq,一方面是因为公司目前已经在线上使用了rocketmq,另一方面也是因为笔者是主要的维护人员,需要对其有个深入的了解,并且测试环境的集群之前出现了问题,虽然最终解决,但是发生的原因依旧没有找到,感觉还是需要去深入了解下,对架构和源码进行一个整体的学习以应对之后可能出现的问题

    rocketmq的相关中文文档在github上应该算非常详细了,初学者应该经常去看一看,每一句思考下,能收获不少,而且其中涉及到不少概念,还是需要去理解的,不明白的话也无法深入的学习下去,地址如下:

    https://github.com/apache/rocketmq/tree/master/docs/cn

    对于与其他中间件的比较和起源发展可参考阿里中间件博客了解,更多的知识可Google了解,地址如下:

    http://jm.taobao.org/tags/RocketMQ/

    特点


    设计
    • rocketmq是一个队列模型的消息中间件,具有高性能、高可靠、高实时、分布式特点。
    • Producer、Consumer、队列都可以分布式。
    • Producer向一些队列轮流发送消息,队列集合称为Topic,Consumer如果做广播消费,则一个consumer实例消费这个Topic对应的所有队列,如果做集群消费,则多个Consumer实例平均消费这个topic对应的队列集合。
    • 能够保证严格的消息顺序
    • 提供丰富的消息拉取模式
    • 高效的订阅者水平扩展能力
    • 实时的消息订阅机制
    • 亿级消息堆积能力
    • 较少的依赖

    基础概念

    • 消息模型(Message Model)

    RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个 Broker 可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。

    • 消息生产者(Producer)

    负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。同步和异步方式均需要Broker返回确认信息,单向发送不需要。

    • 消息消费者(Consumer)

    负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息、并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费、推动式消费。

    • 主题(Topic)

    表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。

    • 代理服务器(Broker Server)

    消息中转角色,负责存储消息、转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。

    • 名字服务(Name Server)

    名称服务充当路由消息的提供者。生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表。多个Namesrv实例组成集群,但相互独立,没有信息交换。

    • 拉取式消费(Pull Consumer)

    Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉消息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。

    • 推动式消费(Push Consumer)

    Consumer消费的一种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。

    • 生产者组(Producer Group)

    同一类Producer的集合,这类Producer发送同一类消息且发送逻辑一致。如果发送的是事物消息且原始生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。

    • 消费者组(Consumer Group)

    同一类Consumer的集合,这类Consumer通常消费同一类消息且消费逻辑一致。消费者组使得在消息消费方面,实现负载均衡和容错的目标变得非常容易。要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。RocketMQ支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。

    • 集群消费(Clustering)

    集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。

    • 广播消费(Broadcasting)

    广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

    • 普通顺序消息(Normal Ordered Message)

    普通顺序消费模式下,消费者通过同一个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。

    • 严格顺序消息(Strictly Ordered Message)

    严格顺序消息模式下,消费者收到的所有消息均是有顺序的。

    • 消息(Message)

    消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。

    • 标签(Tag)

    为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。标签能够有效地保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

    物理部署结构


    设计

    Name Server是一个几乎无状态节点,可集群部署,节点之间无任何信息同步

    Broker部署相对复杂,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave的对应关系通过指定相同的BrokerName,不同的BrokerId来定义,BrokerId为0表示Master,非0表示Slave。Master也可以部署多个。每个Broker与Name Server集群中的所有节点建立长连接,定时注册Topic信息到所有Name Server。

    Producer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master建立长连接,且定时向Master发送心跳。Producer完全无状态,可集群部署。

    Consumer与Name Server集群中的其中一个节点(随机选择)建立长连接,定期从Name Server取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳。Consumer既可以从Master订阅消息,也可以从Slave订阅消息,订阅规则由Broker配置决定

    构建脚本

    rocketmq版本号:4.5.2

    源码distribution的bin目录下已经提供了创建脚本,构建分布式集群前我们先进行下分析,便于之后源码的分析实现

    部署当然是两个部分代理服务器(Broker Server)和名字服务(Name Server),脚本对应的是mqnamesrv和mqbroker,同时运维管理通过mqadmin完成,分别进行分析下

    mqnamesrv

    mqnamesrv启动脚本如下,主要是处理ROCKETMQ_HOME目录

    if [ -z "$ROCKETMQ_HOME" ] ; then
      ## resolve links - $0 may be a link to maven's home
      # 执行脚本命令参数 sh 之后地址串
      PRG="$0"
    
      # need this for relative symlinks
      while [ -h "$PRG" ] ; do
        ls=`ls -ld "$PRG"`
        link=`expr "$ls" : '.*-> (.*)$'`
        if expr "$link" : '/.*' > /dev/null; then
          PRG="$link"
        else
          PRG="`dirname "$PRG"`/$link"
        fi
      done
    
      # 当前执行命令所在目录
      saveddir=`pwd`
      # 当前脚本所在目录的上一级目录
      ROCKETMQ_HOME=`dirname "$PRG"`/..
      # make it fully qualified
      # mq主目录
      ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd`
      # 回到当前命令执行目录
      cd "$saveddir"
    fi
    
    # 定义环境变量
    export ROCKETMQ_HOME
    
    # runserver.sh执行NamesrvStartup启动类 $@传参
    sh ${ROCKETMQ_HOME}/bin/runserver.sh org.apache.rocketmq.namesrv.NamesrvStartup $@
    

    调用runserver.sh通过NamesrvStartup来完成启动

    
    #===========================================================================================
    # Java Environment Setting
    #===========================================================================================
    # 错误退出
    error_exit ()
    {
        echo "ERROR: $1 !!"
        exit 1
    }
    
    # 判断是否安装java运行环境
    [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
    [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
    [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"
    
    # 设置环境变量
    export JAVA_HOME
    export JAVA="$JAVA_HOME/bin/java"
    # bin同级目录
    export BASE_DIR=$(dirname $0)/..
    # 设置CLASSPATH,conf配置文件目录也放入
    export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH}
    
    #===========================================================================================
    # JVM Configuration
    #===========================================================================================
    # The RAMDisk initializing size in MB on Darwin OS for gc-log
    DIR_SIZE_IN_MB=600
    
    # gc日志目录设置 根据不同操作系统设置GC_LOG_DIR
    choose_gc_log_directory()
    {
        case "`uname`" in
            Darwin)
                if [ ! -d "/Volumes/RAMDisk" ]; then
                    # create ram disk on Darwin systems as gc-log directory
                    DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null
                    diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null
                    echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS."
                fi
                GC_LOG_DIR="/Volumes/RAMDisk"
            ;;
            *)
                # check if /dev/shm exists on other systems
                if [ -d "/dev/shm" ]; then
                    GC_LOG_DIR="/dev/shm"
                else
                    GC_LOG_DIR=${BASE_DIR}
                fi
            ;;
        esac
    }
    
    # 执行方法
    choose_gc_log_directory
    
    # 设置JVM参数,通过Djava.ext.dirs扩展目录将lib下的rocketmq相关jar包导入
    # 堆4g 年轻代2g 默认jdk8以上环境 元数据空间128m 最大320m
    # 老年代CMS垃圾收集器 禁掉ParNewGC 新生代使用SerialGC
    # 打印gc日志
    # 设置日志文件个数大小
    # 禁止重复大量异常打印堆栈信息
    # 禁止使用大页面内存
    # 设置扩展包路径和classpath路径
    
    JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
    JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8  -XX:-UseParNewGC"
    JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails"
    JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
    JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
    JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages"
    JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib"
    #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
    JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
    JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"
    
    # 执行java 命令操作 可通过ps命令查看完整信息
    $JAVA ${JAVA_OPT} $@
    

    最终通过java命令启动

    mqbroker

    mqbroker启动脚本说明与mqnamesrv启动脚本类似,参考mqnamesrv

    if [ -z "$ROCKETMQ_HOME" ] ; then
      ## resolve links - $0 may be a link to maven's home
      PRG="$0"
    
      # need this for relative symlinks
      while [ -h "$PRG" ] ; do
        ls=`ls -ld "$PRG"`
        link=`expr "$ls" : '.*-> (.*)$'`
        if expr "$link" : '/.*' > /dev/null; then
          PRG="$link"
        else
          PRG="`dirname "$PRG"`/$link"
        fi
      done
    
      saveddir=`pwd`
    
      ROCKETMQ_HOME=`dirname "$PRG"`/..
    
      # make it fully qualified
      ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd`
    
      cd "$saveddir"
    fi
    
    export ROCKETMQ_HOME
    
    # runbroker.sh执行BrokerStartup启动类 $@传参
    sh ${ROCKETMQ_HOME}/bin/runbroker.sh org.apache.rocketmq.broker.BrokerStartup $@
    

    调用runbroker.sh通过BrokerStartup启动

    
    #===========================================================================================
    # Java Environment Setting
    #===========================================================================================
    error_exit ()
    {
        echo "ERROR: $1 !!"
        exit 1
    }
    
    [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
    [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
    [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"
    
    export JAVA_HOME
    export JAVA="$JAVA_HOME/bin/java"
    export BASE_DIR=$(dirname $0)/..
    export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH}
    
    #===========================================================================================
    # JVM Configuration
    #===========================================================================================
    # The RAMDisk initializing size in MB on Darwin OS for gc-log
    DIR_SIZE_IN_MB=600
    
    choose_gc_log_directory()
    {
        case "`uname`" in
            Darwin)
                if [ ! -d "/Volumes/RAMDisk" ]; then
                    # create ram disk on Darwin systems as gc-log directory
                    DEV=`hdiutil attach -nomount ram://$((2 * 1024 * DIR_SIZE_IN_MB))` > /dev/null
                    diskutil eraseVolume HFS+ RAMDisk ${DEV} > /dev/null
                    echo "Create RAMDisk /Volumes/RAMDisk for gc logging on Darwin OS."
                fi
                GC_LOG_DIR="/Volumes/RAMDisk"
            ;;
            *)
                # check if /dev/shm exists on other systems
                if [ -d "/dev/shm" ]; then
                    GC_LOG_DIR="/dev/shm"
                else
                    GC_LOG_DIR=${BASE_DIR}
                fi
            ;;
        esac
    }
    
    choose_gc_log_directory
    
    # 设置JVM参数,通过Djava.ext.dirs扩展目录将lib下的rocketmq相关jar包导入
    # 堆8g 年轻代4g
    # 使用G1收集器,Region大小16m,预留内存25,内存占用达到整个堆百分之30的时候开启一个GC周期,软引用存活对象不用则立即清除
    # 打印gc日志
    # 设置日志文件个数大小
    # 禁止重复大量异常打印堆栈信息
    # 预分配所有内存
    # 最大堆外内存15g
    # 禁止使用大页面内存,禁止使用偏向锁
    # 设置扩展包路径和classpath路径
    
    JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"
    JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0"
    JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_broker_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintAdaptiveSizePolicy"
    JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
    JAVA_OPT="${JAVA_OPT} -XX:-OmitStackTraceInFastThrow"
    JAVA_OPT="${JAVA_OPT} -XX:+AlwaysPreTouch"
    JAVA_OPT="${JAVA_OPT} -XX:MaxDirectMemorySize=15g"
    JAVA_OPT="${JAVA_OPT} -XX:-UseLargePages -XX:-UseBiasedLocking"
    JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${JAVA_HOME}/jre/lib/ext:${BASE_DIR}/lib"
    #JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
    JAVA_OPT="${JAVA_OPT} ${JAVA_OPT_EXT}"
    JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"
    
    # numa设置,涉及到了底层部分,不过最终都是要执行java命令的
    numactl --interleave=all pwd > /dev/null 2>&1
    if [ $? -eq 0 ]
    then
    	if [ -z "$RMQ_NUMA_NODE" ] ; then
    		numactl --interleave=all $JAVA ${JAVA_OPT} $@
    	else
    		numactl --cpunodebind=$RMQ_NUMA_NODE --membind=$RMQ_NUMA_NODE $JAVA ${JAVA_OPT} $@
    	fi
    else
    	$JAVA ${JAVA_OPT} $@
    fi
    

    mqadmin

    mqadmin启动脚本说明与mqnamesrv启动脚本类似,参考mqnamesrv

    if [ -z "$ROCKETMQ_HOME" ] ; then
      ## resolve links - $0 may be a link to maven's home
      PRG="$0"
    
      # need this for relative symlinks
      while [ -h "$PRG" ] ; do
        ls=`ls -ld "$PRG"`
        link=`expr "$ls" : '.*-> (.*)$'`
        if expr "$link" : '/.*' > /dev/null; then
          PRG="$link"
        else
          PRG="`dirname "$PRG"`/$link"
        fi
      done
    
      saveddir=`pwd`
    
      ROCKETMQ_HOME=`dirname "$PRG"`/..
    
      # make it fully qualified
      ROCKETMQ_HOME=`cd "$ROCKETMQ_HOME" && pwd`
    
      cd "$saveddir"
    fi
    
    export ROCKETMQ_HOME
    # 通过tools.sh脚本执行MQAdminStartup
    sh ${ROCKETMQ_HOME}/bin/tools.sh org.apache.rocketmq.tools.command.MQAdminStartup $@
    

    通过tools.sh执行MQAdminStartup完成运维命令的实现

    error_exit ()
    {
        echo "ERROR: $1 !!"
        exit 1
    }
    
    [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=$HOME/jdk/java
    [ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
    [ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"
    
    export JAVA_HOME
    export JAVA="$JAVA_HOME/bin/java"
    export BASE_DIR=$(dirname $0)/..
    export CLASSPATH=.:${BASE_DIR}/conf:${CLASSPATH}
    
    #===========================================================================================
    # JVM Configuration
    #===========================================================================================
    
    # 基础的jvm配置
    # 堆大小配置1g,年轻代配置256m,元数据大小配置128m
    
    JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m"
    JAVA_OPT="${JAVA_OPT} -Djava.ext.dirs=${BASE_DIR}/lib:${JAVA_HOME}/jre/lib/ext:${JAVA_HOME}/lib/ext"
    JAVA_OPT="${JAVA_OPT} -cp ${CLASSPATH}"
    
    # 执行java命令
    $JAVA ${JAVA_OPT} $@
    

    总结

    本文主要是对rocketmq进行入门基础的学习,初学者应该多理解,因为涉及到了很多概念,以及其架构实现,多看几遍,多思考下,应该能够了解

    为了接下来更好的学习,必须要进行分布式集群的搭建,在搭建集群之前有必要去学习下每个启动脚本启动的过程,启动类的入口,不是很复杂,其中涉及到了jvm的设置,如果不是很明白,可以先大概了解就好,重点在于理解如何通过脚本启动的

    以上内容如有问题欢迎指出,笔者验证后将及时修正,谢谢

    参考资料:

    作者:freeorange
    个人博客网站:https://www.gclearning.cn/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文连接,否则保留追究法律责任的权利。
  • 相关阅读:
    Spring Boot面试杀手锏————自动配置原理
    session在什么时候创建,以及session一致性问题
    IaaS、PaaS、SaaS、DaaS都是什么?现在怎么样了?终于有人讲明白了
    FaaS,未来的后端服务开发之道
    架构师必须了解的30条设计原则
    vuejs2.0使用Sortable.js实现的拖拽功能( 转)
    Spring配置中的"classpath:"与"classpath*:"的区别研究(转)
    Java静态类
    CGLIB(Code Generation Library)详解
    hive--udf函数(开发-4种加载方式)
  • 原文地址:https://www.cnblogs.com/freeorange/p/11921895.html
Copyright © 2020-2023  润新知