前语


我最近很喜欢去看天池的竞赛,虽然过往许多年前了,18年吧那一期,刚好我大三,其时我比较少重视这些竞赛。为什么这类我会遽然重视呢? 由于无论是日常面试那些高大上的常识理论,仍是咱们日常了解的东西,它都没有一个运用场景来真正落地实践,比方说高并发,许多都是日常几十qps,我上家商城能达到上w的qps,之前阿里边试官搞过淘宝活动,大约上千万并发。

理论是这么一个理论,可是每个层级、不同的场景,它对应的技能计划也是不相同的,这才是实践的意义。

第四届阿里中间件竞赛


链接:tianchi.aliyun.com/competition…

  • 复赛 **:《Apache RocketMQ音讯缓存设计》

    **赛题描绘:
    Apache RocketMQ作为的一款分布式的音讯中间件,历年双十一承载了万亿级的音讯流通,为事务方供给高功能低推迟的安稳可靠的音讯服务。随着事务的逐渐发展和云上的输出,单机行列数量的逐渐添加,给RocketMQ带来了新的应战。复赛的标题要求设计一个单机百万行列以上的存储引擎,单机内存有限,需求充分利用数据结构与存储技能,最大化吞吐量

    标题描绘:
    持续更新地址:code.aliyun.com/middlewarer…

  • 2.1 标题内容
    完成一个进程内的行列引擎,单机可支撑100万行列以上。

  • 2.2 语言限定
    JAVA和C++

  • 3. 程序方针
    仔细阅读demo项目中的QueueStore,DefaultQueueStoreImpl,DemoTester三个类。

  • 你的coding方针是重写DefaultQueueStoreImpl,并完成以下接口: abstract void put(String queueName, String message); abstract Collection get(String queueName, long offset, long num);

  • 4.参赛办法说明
    在阿里天池找到”中间件功能应战赛”,并报名参加
    在code.aliyun.com注册一个账号,并新建一个库房名,并将大赛官方账号middlewarerace2018添加为项目成员,权限为reporter
    fork或者拷贝本库房的代码到自己的库房,并完成自己的逻辑
    在天池提交成果的入口,提交自己的库房git地址,等候评测成果
    坐等每天10点排名更新
    4. 测验环境描绘
    测验环境为4c8g的ECS,限定运用的最大JVM巨细为4GB(-Xmx4g)。带一块500G左右巨细的SSD磁盘。

  • 5. 程序校验逻辑
    校验程序分为三个阶段: 1.发送阶段 2.索引校验阶段 3.次序消费阶段 请具体阅读DemoTester以了解评测程序的逻辑。

  • 5.1. 程序校验规划说明
    1.各个阶段线程数在20 ~ 30左右 2.发送阶段:音讯巨细在50字节左右,音讯条数在20亿条左右,也即发送总数据在100G左右 3.索引校验阶段:会对一切行列的索引进行随机校验;均匀每个行列会校验1~2次; 4.次序消费阶段:选择20%的行列进行悉数读取和校验; 5.发送阶段最大耗时不能超过1800s;索引校验阶段和次序消费阶段加在一起,最大耗时也不能超过1800s;超时会被判断为评测失利。

  • 6. 排名规则
    在成果校验100%正确的前提下,依照均匀tps从高到低来排名

  • 7. 第二/三方库规约
    仅允许依赖JavaSE 8 包含的lib
    能够参考他人的完成,拷贝少量的代码
    咱们会对排名靠前的代码进行review,假如发现大量拷贝他人的代码,将扣分

    8.做弊说明
    一切音讯都应该进行按实际发送的信息进行存储,能够压缩,但不能假造。 假如发现有做弊行为,比方经过hack评测程序,绕过了有必要的评测逻辑,则程序无效,且取消参赛资格。

实践


初始代码

第一次功能测验

首先咱们拉取代码,然后跑了一下测验

第四届阿里中间件比赛-mq优化

代码

第四届阿里中间件比赛-mq优化

思路便是典型的map,依据topic来保存message,咱们知道每个topic下面音讯都是次序性,为了保证线程安全性加上了synchronized

优化思路

这个我从之前抽奖项目的灵感来的,之前咱们抽奖也是一把大锁,然后里边逻辑咔咔咔搞,然后开释。那这样流量一进来,大家都卡在那,所以考虑到锁粒度细化。

这里咱们从put办法开始,你说有必要每个操作都锁住吗,显着不合适的,咱们只有topic对应的message才需求上锁,所以细化到topic等级。别的呢,get请求也没有必要上锁,由于它是按次序去拉数据的,比方说我有个水管,每秒加10米,你自己有个计算器,每秒过来度这水管从第3节到第5节到数字,加不加锁都无所谓。

所以咱们定了两个方向,一个是put锁细化到topic等级,一个是get请求去锁。

第四届阿里中间件比赛-mq优化

第四届阿里中间件比赛-mq优化

第二次功能测验

第四届阿里中间件比赛-mq优化

tps提高了许多,那咱们继续优化,咱们将代码丢给gpt同学剖析下还有哪些优化

第四届阿里中间件比赛-mq优化

我敏锐的看到了第3点,便是subList在大列表查询的时分功能比较差的,里边提到了分片,那咱们就朝着分片的思路来搞搞吧~

优化

message内容分片存储,怎样分片呢?很场景便是依据什么id然后取余,然后均匀落到表,可是咱们场景不相同,这些message需求次序存储的,让我想起二维数组List<List<byte[]>>,那咱们就开干吧~

第四届阿里中间件比赛-mq优化

首先咱们定义了2个要害东西,一个是二维音讯表,然后是每个列表最多储存量,比方说800,我有1000条数据,便是两组,第一组800条,第二组200条,以此递加。

第四届阿里中间件比赛-mq优化

put办法咱们加上可重入锁,然后塞入音讯,咱们看下里边怎样分组塞入的

public void add(List<List<byte[]>> lists, byte[] element) {
    int lastListIndex = lists.size() - 1;
    if (lists.isEmpty() || lists.get(lastListIndex).size() == length) {
        lists.add(new ArrayList<>(length / 10));
        lastListIndex++;
    }
    List<byte[]> lastList = lists.get(lastListIndex);
    lastList.add(element);
}

逻辑:假如这个数组为空,或者当时数组巨细等于最大的数量800,那么给它加上一组,然后index+1。很好了解吧,比方说饭馆坐满了,你来了2个人,那老板加多一桌子。

然后咱们往行列里边塞数据即可,这个简略吧,其实比较复杂的是get办法。

第四届阿里中间件比赛-mq优化

public Collection<byte[]> get(List<List<byte[]>> lists, long offset, long num) {
    long firstIndex = offset / length;
    int listSize = lists.size() == 0 ? 0 : lists.size() - 1;
    List<byte[]> list = new ArrayList<>(10);
    if (firstIndex > listSize) {
        return list;
    }
    long maxOffset = offset + num;
    long lastIndex = maxOffset / length;
    if (lastIndex >= listSize) {
        lastIndex = listSize;
    }
    long first = firstIndex;
    while (first <= lastIndex) {
        if (first == firstIndex && firstIndex == lastIndex) {
            list.addAll(lists.get((int) first).subList((int) offset % length, (int) (maxOffset == 0 ? 0 : maxOffset % length == 0 ? lists.get((int) first).size() : (maxOffset % length > lists.get((int) first).size() ? lists.get((int) first).size() : maxOffset % length))));
            break;
        }
        if (first == firstIndex) {
            list.addAll(lists.get((int) first).subList((int) offset % length, length));
        } else if (first < lastIndex) {
            list.addAll(lists.get((int) first));
        } else {
            list.addAll(lists.get((int) first).subList(0, (int) (maxOffset == 0 ? 0 : maxOffset % length == 0 ? lists.get((int) first).size() : maxOffset % length)));
        }
        first++;
    }
    return list;
}

首先咱们要知道offset在哪个组里边对吧,然后offset+num又在哪个组,然后咱们对极端状况做了处理,比方说你拿到组数都 超出了最终一组数量,那就要兜底下。

然后咱们知道first、last第几组之后开始遍历,有许多状况,比方说两个值都相同在一组,或者说last在最终一组,咱们在里边许多判断便是为了解决这些状况的,写法比较丑了点,哈哈。

第三次功能测验

第四届阿里中间件比赛-mq优化

第四届阿里中间件比赛-mq优化

在咱们对message分片之后,写入跟读取功能都有很高的提高,还有优化吗?

优化

其实还有的便是数组初始巨细,咱们都知道list扩容是仿制过去,比较消耗功能的,所以咱们在new的时分给了初始的巨细,可是比较有趣的是比方list会存放大约1000条数据,我假如写1000的话功能反而差,假如折中500,功能好一点,这个有点古怪的。

第四届阿里中间件比赛-mq优化

总结


经过对实践的实践经验,了解到理论虽然很重要,可是真正将它转化为一个落地运用的技能计划才是最重要的。咱们参加的一个代码功能优化的进程,包括开始的map的完成和细粒度锁优化,以及后来的分片存储优化。最终经过优化后的代码,明显提高了系统的 TPS。