第四届阿里中间件性能挑战赛 复赛心得分享

本次只分享复赛,赛题介绍见链接:第四届阿里中间件性能挑战赛
复赛成绩是第8名,得到了去阿里总部答辩的机会,至少1W元的奖金到手。
中间件比赛排名
总结下,复赛是另外一番的体验,队友的合作,一起通宵奋斗,最终得到了一个比较好的成绩。
题目:使用 Java 或者 C++ 实现一个进程内的队列引擎,单机可支持 100 万队列以上。

public abstract class QueueStore {
    abstract void put(String queueName, byte[] message);
    abstract Collection<byte[]> get(String queueName, long offset, long num);
}

编写如上接口的实现。
put 方法将一条消息写入一个队列,这个接口需要是线程安全的,评测程序会并发调用该接口进行 put,每个queue 中的内容按发送顺序存储消息(可以理解为 Java 中的 List),同时每个消息会有一个索引,索引从 0 开始,不同 queue 中的内容,相互独立,互不影响,queueName 代表队列的名称,message 代表消息的内容,评测时内容会随机产生,大部分长度在 58 字节左右,会有少量消息在 1k 左右。

get 方法从一个队列中读出一批消息,读出的消息要按照发送顺序来,这个接口需要是线程安全的,也即评测程序会并发调用该接口进行 get,返回的 Collection 会被并发读,但不涉及写,因此只需要是线程读安全就可以了,queueName 代表队列的名字,offset 代表消息的在这个队列中的起始索引,num 代表读取的消息的条数,如果消息足够,则返回 num 条,否则只返回已有的消息即可,若消息不足,则返回一个空的集合。

评测程序介绍

  • 发送阶段:消息大小在 58 字节左右,消息条数在 20 亿条左右,即发送总数据在 100G 左右,总队列数 100w。实际是所有数据58字节,20Y数据。

  • 索引校验阶段:会对所有队列的索引进行随机校验;平均每个队列会校验1~2次;(随机消费),实际随机校验100W次

  • 顺序消费阶段:挑选 20% 的队列进行全部读取和校验; (顺序消费)实际是10%。

  • 发送阶段最大耗时不能超过 1800s;索引校验阶段和顺序消费阶段加在一起,最大耗时也不能超过 1800s;超时会被判断为评测失败。

  • 各个阶段线程数在 20~30 左右,实际是10
    测试环境为 4c8g 的 ECS,限定使用的最大 JVM 大小为 4GB(-Xmx 4g)。带一块 300G 左右大小的 SSD 磁盘。对于 Java 选手而言,可使用的内存可以理解为:堆外 4g 堆内 4g。

赛题剖析

根据需求可以把整个题目拆分为3个部分,写入、随机度、顺序读。写入的设计就影响了接下来的读。

1. 写入

写入的话是100W个队列,平均每个队列2K个消息。首先你能使用的工具有哪些呢。写入的话主要有:FileChannel、channel.map、RandomAccessFile还有一个阻塞IO。鉴于初赛的经验首先排除掉了阻塞IO,剩下3个。Channel.map方式的话需要映射一块硬盘区域,我们只需要写入并不需要读,并且建议写到用来读取,不建议写入。RandomAccessFile经过测试和FileChannel的性能相近,但是更灵活方便一些。所以Api的话我们采用了FileChannel。
接下来就是写入方案了。

  • 同一队列的数据放在一起,当读取一个队列的消息会很方便,多条消息同时读出来,查一次索引即可。一个队列一个文件,这样需要100W个文件描述符,直接卡死,方案不可行。
    一个文件里面指定位置放该队列,扩展性能太差,当队列长度不均匀或者队列长度位置,需要根据最大长度或者留有空余的方式会浪费很多空间不可取。
  • 每个数据加一个索引,来了就写入,避免了空间的浪费,扩展性也很好,但是这样同一队列数据会分散存储,读多条数据需要多次操作IO,尤其当分散特别远时,根据IOPS的定义读效率会特别低。
  • 1、2方案折中,为每个队列做缓存,当缓存达到一定时才会落盘,为这一块缓存块做索引。这样扩展灵活,节省空间,并且可以做到批写入,写入很快,并且读一次索引可以读取多条消息,既减少读取索引次数,又减少了IO操作次数。
    所以方案上采取了3的方式,此中还有很多细节不在此详述。

2. 随机读取

首先阐述一个概念,硬盘的读写速率最直观的是MB/S,但是这和查询块的大小有关,具体关系如下表:
这里写图片描述
因为随机读取比较特殊,每次读取和上一次的位置间隔较大,所以根据IOPS特性,在随机读取阶段尽可能的读取最少的数据。此处我们选取了1K,如果在预知数据大小的情况下尽可能的小,减少IO操作次数


3. 顺序读取

顺序读取阶段,同一时刻查询的数据物理间隔较近,最好的方式是把读过的数据都缓存下来,但是缓存大小是有限的,所以此处可以参考LRU,释放掉最先读取过的缓存。但是此处可能存在饿死现象:即,当有一个队列读取的过快,有一个队列读取的过慢,那么读取慢的队列可能会饿死,一直跑的很慢,拖慢真个时间。不过到最后才注意到该问题。但这个问题是有改进方法的,也就是采用上车的概念,检测CPU资源,发现CPU利用率很低不到5%,也就是消费线程很快,硬盘读取很慢,此处我们可以采用一个上车的方法。开启一个异步线程,然后改异步线程一直去读,读完后放到缓存中。消费线程去查看是不是该队列数据已经加载到缓存,如果没有就去阻塞。这样所有的读取读完的最长时间取决于该异步线程的读取速率。并且适用于所有数据顺序读。

最终20亿数据,共计100G数据,我们的写在600秒左右,随机索引100W次,耗时130秒所有,读取耗时300秒左右。最终tps定格在了212W。

展开阅读全文

没有更多推荐了,返回首页