工业级的大规模分布式系统,都不会采取特别简单的代码和模式,那样性能很低下。这里都有大量的并发优化、网络IO优化、内存优化、磁盘读写优化的架构设计、生产方案在里面
并发优化:rocketmq消费端线程池并发消费msg
网络IO优化:hadoop的大文件上传+内存缓冲+package数据包机制+内存队列异步发送机制
内存优化:rocketmq每次预热1G内存空间并进行内存锁定,防止内存空间被置换出去
磁盘读写优化:rocketmq的批量刷盘+顺序写+内存映射
- 通过加锁来控制好标志位的状态流转
- 通过标志位的不同的流转状态,来控制每个线程有不同的行为
- 如果是需要各个线程协作处理的逻辑,就采用最简洁的wait/notify机制
本篇editLog批量刷盘,和前面文章的内存队列合并请求,如出一辙,本质都是将客户端发送过来的请求数据在本机内存队列中攒一拨儿,然后隔段时间,一起批量处理一批
比如面试题,实现每秒1w的插入数据库,不能使用redis和mq,不能分库分表
方式一:订单缓存在内存队列中,再用线程池异步消费内存队列,进行批量插入入库,本地测试过1w/s没有问题,再用mysql的hash分区或者key分区,这里异常情况怎么保证订单不丢,线程池大小配置多少,线程池满后拒绝策略应该做哪些事情
方式二:多线程内存队列数据写多个固定大小的txt。在异步单线程读txt批量写库。没有内存溢出风险和数据库压力,标准的漏斗型io流,同时保证了防丢。写txt文件时,还可以使用mmap内存映射直接写文件
实际上,方式二就是rocketmq的实现方式,把rocketmq的broker的写入原理搞清楚了,玩这道题轻轻轻松
/*
* 通过文件通道,获取到大小为1G的 mappedByteBuffer(虚拟内存地址空间)
* */
this.fileChannel = new RandomAccessFile(new File(filePath), "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
创建出固定fileSize大小的内存映射文件
刷盘线程
使用独立的单线程,进行内存队列的刷盘动作,比如每次从队列中刷1000条数据到磁盘,刷完以后立马检查队列是否仍有超过1000条数据,如果有则立马进行下一轮刷1000条,如果不到1000条,则sleep 10ms,然后再检查当前队列中的数据量
- 涉及业务功能和三方有交互的时候,最好不要仅仅依赖单向的接收或调用,虽然可以以接受别人推送为主,但当推送阻塞时,提前约定好的备用的类似主动拉取的接口就要出马了
- 上游不给你推,系统处理就是被动的,而且你如果反调别人的接口去查询的话,你得和人商量好,让人给你提供接口,而且人家接口的并发上限啥的,也得提前确认好
- 如果上游给你推送了你,你的系统本身处理不过来,也分情况,一种是代码本身处理慢,可以改多线程,或者增加消费实例,但如果瓶颈是你的数据库响应速度,那增加服务实例没用还可能适得其反,这时候要么加缓存 ,要么做分库分表之类的针对数据库层的处理
兄弟,用大白话告诉你小白都能看懂的Hadoop架构原理【石杉的架构笔记】
hadoop架构原理
edit_log高性能写入原理
分布式存储系统NameNode框架实现
创建目录
editslog的全局txid机制和双缓冲机制
基于synchronized实现editslog的分段加锁
实现分段加锁,就是为了保证锁的最细粒度,将最耗时的批量刷磁盘操作,排除在加锁范围之外
基于wait/notify实现editslog批量刷磁盘
但凡涉及到批量刷磁盘的,必然就需要用到内存缓冲,并且是双缓冲来进行读写分离
多个客户端请求线程,并发的往缓冲中写入数据,然后此时用哪个线程来负责将缓冲中积累的多条数据一起刷写到磁盘中去,有两种方式:
- 单独起一个定时轮询线程,每一定时间间隔执行一次双缓冲的交换,并将读缓存中的数据一起刷写进磁盘
- 多个客户端请求线程中随机选择一个,来间隔执行一次双缓冲的交换,并将读缓存中的数据一起刷写进磁盘
方式一,也就是rocketmq的多条commitlog消息同步刷盘时采用的方式
方式二,也就是hadoop的editslog刷写磁盘采用的方式
核心流程
每个客户端请求线程一进来,先获取锁,然后在同步块内生成txid并存入ThreadLocal
执行一个单独的刷盘方法,
弄一个volatile的标志位isFlushRunning,标识当前是否已经有了一个线程,正在执行一批缓冲的刷盘
弄一个volatile的标志位maxFlushTxid,标识上面的刷盘线程准备刷的这批数据中的最大的那个txid
进入这个单独的刷盘方法内的线程,会被分为三种情况
- 线程1,进入该方法,发现isFlushRunning为false,表示当前没有线程在执行刷盘,那么当前线程就在同步块内将isFlushRunning置为true,设置maxFlushTxid为当前缓冲中的最大txid,然后释放锁,并在释放锁后开始真正的批量刷盘操作,刷盘完成后在同步块内执行notifyAll()
- 线程2、3,进入该方法,发现isFlushRunning已经为true,且当前线程ThreadLocal
中存的txid小于等于maxFlushTxid,则当前线程直接return返回 - 线程4、5、6,进入该方法,发现isFlushRunning已经为true,且当前线程ThreadLocal
中存的txid大于maxFlushTxid,则当前线程直接在同步块中wait两秒,等待线程一执行完刷盘唤醒它们。比如线程1唤醒线程4、5、6后,重新抢到锁的是线程5,线程5又会开始重新执行上面的线程1的工作
参考视频链接
46_案例实战:分布式存储系统案例背景引入_哔哩哔哩_bilibili
Hadoop其他优化
基于内存里的chunk缓冲机制、packet数据包机制、内存队列异步发送机制。绝对不会有任何网络传输的卡顿,导致大文件的上传速度变慢
【性能优化的秘密】Hadoop如何将TB级大文件的上传性能优化上百倍?【石杉的架构笔记】