一般MQ的选型调研
需要考虑的问题
  • 性能: 每秒钟能抗住多少qps;
  • 功能: 是否支持延迟消息, 事务消息, 消息堆积, 死信队列, 消息回溯等;
  • 可靠性: 是否存在消息丢失现象, 是否支持集群部署;
  • 扩容: 集群是否支持线性扩容;
  • 业务: 根据自己的业务查看是否符合;
一般的可用选型
  • activeMQ: 传统的, 资历最老的mq, 因此运行稳定, 但是支持并发量太小, 不支持集群部署, 高可用方案实现复杂, 并且存在消息丢失现象, 因此适用于并发量较小, 且对少量消息丢失无严格要求的场景;
  • RabbitMQ: 可以保证数据不丢失, 也能保证高可用和集群部署, 能够支持每秒钟几万的并发, 也支持一些高级功能, 但是集群扩容比较麻烦, 另外rabbitmq是使用erlang语言开发, 如果要对源码进行修改, 定制化自己的消息功能, 会比较麻烦;
  • kafka: 专门为大数据设计的消息队列, 可以进行集群部署和线性扩容, 支持的并发量每秒钟可以达到几十万, 但是存在消息丢失的现象, 因此也不适用于对消息要求严格的场景, 另外kafka功能实现单一, 特殊场景不能实现, 要根据业务自己决定是否使用;
  • rocketMQ: 阿里消息系统, 经过双十一检验, 是一个优秀的消息中间件, 根据kafka的体系设计, 单机可以支撑十万并发, 可以进行集群部署以及线性扩容, 还支持很多高级功能, 但是rocketMQ的官方文档比较少, 使用成本会比较高;
RocketMQ基本架构图

image.png

  1. 支持集群部署, 可以部署多台服务器, 增加集群的并发能力;
  2. 使用分布式存储, 每个节点只存储一部分消息, 可以提高集群存储消息的数量;
  3. 主从架构: 集群中每个主节点可以设置从节点, 保证节点的高可用;
  4. 集群的协调组件nameServer: 负责接收集群中节点的注册信息, 客户端可以从nameServer中获取各个节点的路由信息(即每台机器的地址, 以及每条消息存储的位置);
nameServer
  • 支持集群部署, 保证集群的高可用;
  • broker是如何注册信息的: broker向所有的nameServer注册自己的信息, 这样可以保证客户端不论访问哪个nameServer都可以访问到broker信息;
  • 客户端获取nameServer上信息的方式: 客户端每个一段时间从nameServer上主动拉取信息;
  • nameServer如何感知broker存活状态: 心跳机制, broker每隔30秒会给nameServer发送心跳, namerServer每个10秒扫描broker的心跳, 如果某个broker在120秒内都没有发送心跳, 则认为他已经挂掉;
  • 如果nameServer挂掉, 客户端本地会缓存路由的信息, 不会影响访问broker;
broker主从架构
  • master和salve的数据同步机制
salve主动从master拉取数据
  • 客户端如何从master和salve选择哪个获取信息
1. client会先发送请求给master, master返回消息给client;
2. master会根据salve和自己负载情况告诉客户端下次去哪里请求数据;
3. salve挂掉, 会使master负载增加, 不影响集群基本运行;
4. master挂掉, 4.5之前salve是不能主动切换为master的, 需要人工干预;
5. 使用Dledger实现master和salve的自动切换;
6. 主从之间的消息怎么保证一致性: 可以使用同步复制和异步复制两种情况;
rocketmq的集群的设计

基本设计如下:
image.png

  • client和server之间的连接使用的是tcp的长连接, 可以避免频繁的开启和关闭连接;
topic概念
  • topic是一个数据集合, 不同类型的数据发送到不同的topic中, 客户端从不同的topic中获取数据;
  • topic中的数据时分布式存储在不同的节点上的;
  • producer如何发送数据: 首先从nameserver中获取路由信息, 根据一定的负载均衡算法向broker发送数据;
  • consumer获取数据: consumer获取数据的方式和producer类似, 上面已经有介绍;
伪rocketmq集群的部署
需要的服务器介绍
nameServer: 3台8核16G服务器;
broker: 3台24核48G服务器(负载最高, 需要配置较高);
web应用: 4核8G服务器;
具体部署
1. 构建Dledger
    git clone https://github.com/openmessaging/openmessaging-storage-dledger.git
    cd openmessaging-storage-dledger
    mvn clean install -DskipTests

2. 构建rocketmq
    git clone https://github.com/apache/rocketmq.git
    cd rocketmq
    git checkout -b store_with_dledger origin/store_with_dledger
    mvn -Prelease-all -DskipTests clean install -U
    cd distribution/target/apache-rocketmq
3. 修改配置文件
    一个是bin/runserver.sh,一个是bin/runbroker.sh,另外一个是bin/tools.sh
    在里面找到如下三行,然后将第二行和第三行都删了,同时将第一行的值修改为你自己的JDK的主目录
    [ ! -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)!"
4. 启动集群
    sh bin/dledger/fast-try.sh start(这个命令会在当前这台机器上启动一个NameServer和三个Broker)
    sh bin/mqadmin clusterList -n 127.0.0.1:9876(查看集群状态)
真实集群部署
首先是在三台NameServer的机器上,大家就按照上面的步骤安装好Java,构建好Dledger和RocketMQ,然后编辑对应的文件,设置好JAVA_HOME就可以了, 此时可以执行如下的一行命令就可以启动NameServer:
nohup sh mqnamesrv &

这个NameServer监听的接口默认就是9876;

完成一组Broker集群的部署
分别在上述三台为Broker准备的高配置机器上,安装好Java,构建好Dledger和RocketMQ,然后编辑好对应的文件, 接着就可以执行如下的命令:
nohup sh bin/mqbroker -c conf/dledger/broker-n0.conf & 

第一个Broker的配置文件是broker-n0.conf,
第二个broker的配置文件可以是broker-n1.conf,
第三个broker的配置文件可以是broker-n2.conf。

broker-n0.conf举例子:

# 这个是集群的名称,你整个broker集群都可以用这个名称
brokerClusterName = RaftCluster
# 这是Broker的名称,比如你有一个Master和两个Slave,那么他们的Broker名称必须是一样的,因为他们三个是一个分组,如果你有另外一组Master和两个Slave,你可以给他们起个别的名字,比如说RaftNode01
brokerName=RaftNode00
# 这个就是你的Broker监听的端口号,如果每台机器上就部署一个Broker,可以考虑就用这个端口号,不用修改
listenPort=30911
# 这里是配置NameServer的地址,如果你有很多个NameServer的话,可以在这里写入多个NameServer的地址
namesrvAddr=127.0.0.1:9876
# 下面两个目录是存放Broker数据的地方,你可以换成别的目录,类似于是/usr/local/rocketmq/node00之类的
storePathRootDir=/tmp/rmqstore/node00
storePathCommitLog=/tmp/rmqstore/node00/commitlog
# 这个是非常关键的一个配置,就是是否启用DLeger技术,这个必须是true
enableDLegerCommitLog=true
# 这个一般建议和Broker名字保持一致,一个Master加两个Slave会组成一个Group
dLegerGroup=RaftNode00
# 这个很关键,对于每一组Broker,你得保证他们的这个配置是一样的,在这里要写出来一个组里有哪几个Broker,比如在这里假设有三台机器部署了Broker,要让他们作为一个组,那么在这里就得写入他们三个的ip地址和监听的端口号
dLegerPeers=n0-127.0.0.1:40911;n1-127.0.0.1:40912;n2-127.0.0.1:40913
# 这个是代表了一个Broker在组里的id,一般就是n0、n1、n2之类的,这个你得跟上面的dLegerPeers中的n0、n1、n2相匹配
dLegerSelfId=n0
# 这个是发送消息的线程数量,一般建议你配置成跟你的CPU核数一样,比如我们的机器假设是24核的,那么这里就修改成24核
sendMessageThreadPoolNums=24

检测命令

sh bin/mqadmin clusterList -n 127.0.0.1:9876。
基本代码编写
依赖
18566900_1578388980.png

生产者
29069300_1578388980.png

消费者(待补充)
image.png

集群的监控和管理
构建
git clone https://github.com/apache/rocketmq-externals.git
cd rocketmq-externals/rocketmq-console
mvn package -DskipTests
# 进入target目录
java -jar rocketmq-console-ng-1.0.1.jar --server.port=8080 --rocketmq.config.namesrvAddr=127.0.0.1:9876
OS内核参数调整
echo 'vm.overcommit_memory=1'>>/etc/sysctl.conf
参数可以设置为0, 1, 2, 
0: os内核会检查内存, 如果内存足够的话, 就分配, 如果不够, 就拒绝分配;
1: 只要有内存就分配;

echo 'vm.max_map_count=655360'>>/etc/sysctl.conf
服务器打开线程数量, 默认值65536, 这个数量有时候会不够(高并发系统)

echo 'vm.swappiness=10'>>/etc/sysctl.conf
控制swap行为, 在os会申请一部分磁盘空间, 当都得进程不是很活跃的时候, 将他们放入swap区域, 然后空出他们原来占用的磁盘空间给其他程序
0: 尽量别把线程放入swap区域, 值越大就是代表要尽量将线程放入swap区域;
默认值是100: 偏高, 设置为10比较合适;

echo 'vm.ulimit -n 1000000'>>/etc/sysctl.conf
最大文件链接数, 默认值1024, 网络通信时打开文件数量;
rocketmq的jvm参数调整
目录: rocketmq/distribution/target/pache-rocketmq/bin目录, runbroker.sh
参数解释
# -Xms8g -Xms8g -Xmn4g: 
默认堆大小为8g, 新生代大小4g, 根据机器配置, 此处使用堆20g, 新生代10g

# -XX:UseG1GC -XX:G1HeapRegionSize=16G
使用G1垃圾回收器, region的区域大小是16g

# -XX:G1ReservePercent=25
老年代预留空间为25%, 数值偏小, 设置为10%

# -XX:InitialtingHeapOccupancyPercent=30
当堆内存使用率达到30%时触发垃圾回收, 默认值是45, 调低, 可以降低jc频率, 但是每次回收的对象会比较多;

# -verbose:gc.....
gc日志

# -XX:SoftRefLRUPolicyMSPerMB=0
默认为0, 此时会频繁回收一些软引用, 建议调整为1000

#-XX:-OmitStackTraceInFastThrow
设置这个参数就是禁止jvm抛出堆栈异常信息, 而是将他们打印在日志里面;

#-XX:+AlwaysPreTouch: 
不使用时, jvm默认不是一开始就分配给我们指定的内存, 而是在需要的时候分配, 使用这个参数, 强制jvm一开始就分配指定内存;
Rocketmq核心参数调整
目录: rocketmq/distribution/target/pache-rocketmq/conf/dledger
sendMessageThreadPoolNums=16
rocketmq默认的发送消息的线程数, 可以设置为cpu数量, 或稍大一点
rocketmq集群压测

方法: 可以布置多个消费者, 和多个生产者, 然后在生产者中启动多个线程不多的去发送消息

观察: 服务器的负载情况, cpu, 网络读写, 内存, 磁盘使用等, 控制台上观察rocketmq的tps, 主要是平衡rocketmq的并发量和服务器性能;

Rocketmq使用细节
同步消息
SendResult result = producer.send(message);
// 同步发送, 代码会停在这里不能继续向下执行, 直到mq返回结果
异步消息
producer.send(message, new SendCallBack() {
    onSuccess(){}
    onException(){}
})
单向消息
producer.sendOneWay(msg)
// 发送给mq之后就什么都不管了, 接着往下执行, 效率高, 安全性低
push消费模式
new DefaultMQPushConsumer()
consumer主动去拉取消息
pull消费
new DefaultMQPullConsumer()
mq主动把消息发送给consumer进行消费
秒杀系统的构建(使用mq)

主要的架构设计是: 页面数据静态化+多级缓存

1. 将页面中使用的数据提前提取出来, 放入到缓存中(redis), 避免频繁的请求数据库;
2. 使用mq进行流量削峰, 页面的所有请求都发送给mq, 后台系统从mq中进行消费;
3. 进行计数使用redis进行计数, 当商品抢完之后, 返回客户端已抢完消息, 后台不再进行处理;
4. 多级缓存: nginx + redis(nginx可以基于lua脚本实现本地缓存)
topic, messegeQueue和broker之间的关系
1. topic是主题, 和kafak中的topic是一个概念;
2. messageQueue是队列, 一个topic会包含很多队列, 类似于kafka中partition, 
也是数据的分片, 用来实现分布式存储;
3. 一个topic可以分散在多个broker上, 但是一个messageQueue只能存在于一个broker上;

这种设计, 就是为了实现rocketmq数据的海量存储, 和支持高并发的, 另外, 
如果mq中master挂掉, 就没有master可以写入了, 此时java api中可以社会
setLatencyFaultEnable(), 这样第一次请求失败后, 就会在接下来一段时
间内不会访问这个master;
mq消息的持久化存储(类似kafka)

消息的存储机制决定了mq的吞吐量

1. commitLog顺序写入机制: 生产者最终会把消息写入磁盘上的commitLog
文件, 写入时顺序的, 每个commitLog文件默认大小是1G, 超过这个值之后会
另起一个commitLog文件;
2. mq中消息存储路径/comsumerqueue/{topic}/{queueid}/{fileName},
topic就是主题, queueId就是messageQueue, fileName就是comsumerQueue
文件, comsumerQueue中存储的是消息在commitlog中偏移量, 长度等;
3. 如何让commitLog消息写入接近内存的读写: 顺序写提高性能, 另外, 使用
系统pageCache, 消息写入时, 并不是直接写入物理磁盘, 而是先写入到page
Cache中, 一定时间后, 有系统的后台线程将pageCache中的内容刷进磁盘, 这
样就会带来消息的安全性问题;
同步刷盘和异步刷盘
同步: producer在消息被刷到磁盘后再得到返回结果;
异步: 只要写到mq中就得到返回结果, 不需要刷盘;
可以在系统中做配置
基于Dledger技术的主从同步原理
  • 实际上就是将原来的commitLog文件交给Dledger管理, 而comsumerQueue基于这个commitLog来构建;
  • master选举: 基于raft协议, 每个broker都会先选举自己, 并将选举结果发送给其他broker, 因此第一轮选举失败, 然后所有的broker会随机睡眠一段时间, 先醒来的broker还是会投自己的票, 然后发送给其他broker, 其他broker接受到请求之后, 发现已经有人投了票, 就直接投哪个broker, 当超过半数的broker投了这个服务器之后, 这个broker就是master;
  • 数据同步, 基于raft的二阶提交, 和zookeeper的提交模式类似;
消费者如何消费数据
消费者组概念

一个消费者组中, 可以有多个消费者;

集群模式和广播模式
  • 集群模式下, 一个消费者组中, 只有一个消费者能消费到消息(默认模式);
  • 广播模式下, 消费者组中的消费者都可以消费到消息;
  • 消费者和messageQueue关系: 最多是一对一关系, 如果消费者数量超出messageQueue, 多余的消费者不会获取到消息, 如果少于message, 则会均匀分配;
消费者的扩容: broker会进行rebalance, 重新分配messageQueue和comsumer的关系;

一个效率问题: 不论是commitLog和ConsumerQueue在被消费数据时, 都是先从os cache中先获取数据, consumerQueue中数据比较少, 基本上可以全部存入os cache中, 读取速度比较快, commitLog则是数据量比较, os cache中和磁盘上都有, 当消费者消费比较快时, 可以从os cache中拉取数据, 但是有大量数据堆积的时候, 数据会被存入磁盘, 此时在拉取, 会走磁盘, 效率较低;

基于netty扩展高性能网络架构
  • 线程模型上: 如图:

image.png
每个线程组负责一部分工作, 相互不会影响, 一个socketChanel的请求阻塞, 不会影响其他的;

  • 零拷贝

使用mmap技术和os cache技术实现零拷贝, mmap就是在虚拟内存和和磁盘之间建立映射关系, 不需要再次进行用户内核的操作(web系统就是jvm的堆内存)

生产案例应用
消息零丢失方案
  • 系统向mq推送消息环节: 使用half消息, 代码实现如下:

image.png
image.png

  • mq内部因为os cache缓存和磁盘损坏导致的消息丢失
    异步刷盘: 消息只是进入到mq中的os cache中, 没有被写入到磁盘(影响安全性);
    同步刷盘: 就是消息已经被写入到os cache中(影响效率);
    配置: flushDiskType, SYNC_FLUSH(同步刷盘), ASYNC_FLUSH(异步刷盘)
    主从同步机制: 保证磁盘损坏的问题;
  • 客户端异常导致消息丢失
使用手动提交的方式
消息重复消费问题

重复消息产生的一个场景: 客户端调用订单系统, 支付了一个订单, 此时订单系统已经处理了该订单, 但是因为其他问题还未返回给客户端响应, 客户端此时有重新调用了订单系统的接口, 因此产生了两条一样的消息;

解决方法就是使用消息幂等机制, 判断消息是否已经被消费过了, 可以使用redis实现;

数据库宕机, 消费消息失败后的重试机制
  • 返回RECONSUMER_LATER状态, 让mq重试, 最多重试16次, 没重试一次就会增大下次重试的时间间隔, 最大间隔2h;
  • 死信队列: 所有重试都失败后会进入死信队列;
消费消息乱序问题

主要原因是mq只保证在一个messageQueue中的消费时有序的, 但是不保证在不同的messageQueue中的消费时有序的(类似于kafka);

  • 让业务上需要顺序的消息发送到同一个messageQueue中去:

image.png

  • 顺序消费

image.png

延迟消息的实现

image.png

消息追踪
  • 配置文件中traceTopicEnable=true
  • 构建producer时: new DefaultMQProducer("...", true);
  • 消费者同理;
消息堆积解决方案
  • consumer少于messageQueue时, 可以增加consuemr的数量进行消费;
  • consumer数量等于messageQueue时, 修改consuemr的代码, 将消息写入一个新的topic中, 设置topic的messageQueue数量多一点, 让其他一些消费者消费新的topic中的消息;
  • 具体的业务场景用不同的方案解决, 总之就是在保证业务正确的前提先,尽快消费消息;
权限配置(待补充)

红番茄
7 声望2 粉丝

« 上一篇
多线程笔记
下一篇 »
JVM笔记