网站安全狗 拦截301,导航 网站 分析,做网站新闻编辑,企业网站的一般要素包括哪些第四届阿里中间件性能挑战赛是由阿里巴巴集团发起#xff0c;阿里巴巴中间(Aliware)、阿里云天池联合举办#xff0c;是集团少有的工程性品牌赛事。大赛的初衷是为热爱技术的年轻人提供一个挑战世界级技术问题的舞台#xff0c;希望选手在追求性能极致的同时#xff0c;能深… 第四届阿里中间件性能挑战赛是由阿里巴巴集团发起阿里巴巴中间(Aliware)、阿里云天池联合举办是集团少有的工程性品牌赛事。大赛的初衷是为热爱技术的年轻人提供一个挑战世界级技术问题的舞台希望选手在追求性能极致的同时能深刻体会技术人的匠心精神用技术为全社会创造更大的价值。本文是亚军选手“做作业”的解题思路来自南京理工大学的95后开发者。 初赛部分一、赛题重述及理解初赛主要的考察点是实现一个高性能的http服务器一套优秀的负载均衡算法。而协议转换仅需要对协议进行详细了解即可实现属于基本技能考察。而服务的注册和发现参考demo中的实现即可属于比较常用的处理逻辑。针对高性能的http服务器想要在竞赛环境下取得较好的成绩使用常用通用http框架是较难取得高分的因为consumer agent和consumer共享一个docker运行环境在512连接测试环境下系统资源就已经很紧张了需要在舍弃一部分http特性的情况下才能将consumer性能发挥到极致。所以自己造轮子是一个比较合适的方案。Consumeragent的总体结构(所有逻辑基本集中在这)如图1所示。图1 Consumer Agent结构二、网络编程模型的选择高并发网络模型首选epoll在连接数增多的情况下其性能不会像select和poll一样明显下降是由于内核对其进行了特殊优化不是线性遍历所有连接。实际测试中发现因为请求是在收到响应后才会继续发出所以不仅要求高并发同时要求低延迟因为延迟叠加的原因也会导致QPS下降。而我们使用的常见的异步网络库libuv、libevent等都是one loop per thread的即一个线程一个事件循环这种设计具有较高的吞吐量但请求只能绑定到一个事件循环上一旦请求不均匀则会导致一核有难众核围观的情况对于动态均衡线程负载很不适合。基于此原因我决定自己造轮子让多个线程处理一个epoll循环让操作系统的线程调度均衡请求同时所有连接均绑定到一个事件循环上也便于处理。最终我选择使用多线程的et模式的epoll模型整体框架代码如图2所示。图2 多线程epoll框架整体框架非常简洁所有网络处理类仅需继承CepollCallback完成网络数据的读取和写入即可。图3 epoll回调抽象类三、程序整体流程Consumer端agent负责解析http请求转换为dubbo请求以udp形式发给provider agent处理provider agent收到udp后直接传给provider dubbo服务处理将结果以udp形式回传consumer agent处理后返回http形式结果。图4、5 CA/PA处理流程程序初始化的时候provider agent调用etcd接口将自己的服务注册consumer agent调用etcd接口获取到注册的provider agent地址由于采用udp协议通信故不用提前建立连接。Provider agent端也仅在第一个请求到达时建立到provider的tcp连接保证provider已经启动完毕可以接受请求。四、中间协议设计考虑到高吞吐量低延迟采用了udp协议请求为单包单请求数据格式为dubbo请求格式相应包为单包多相应数据格式为dubbo响应。即consumer agent端实现协议转换provider agent端实现udp转tcp。由于请求仅存在于一个包中所以无需考虑乱序情况直接将收到的数据包进行重组发到dubbo的tcp连接上提供最大的吞吐量和最低的延迟。具体处理操作如图3。图3 udp重组转发tcp同时为了减少系统调用可以一直取udp包当缓冲满后一次性写入tcp的缓冲中。同理在tcp转udp时可以将数据凑成接近MTU减少网络压力。如图4所示。图4 udp单包多相应还有很多工程上的技巧比如调整缓冲区大小等保证每次异步调用都能成功减少系统调用次数。五、负载均衡算法负载均衡采用计数方法实现每次请求发送到和配置负载比例差距最大的Provider上如果3台都超出200负载则进行请求排队当有请求返回时优先调度排队请求。整体如图5所示。图5 负载均衡算法六、创新点及工程价值1、consumer agent实现负载均衡和请求控制能够有效在consumer使用排队机制端遏制过载不把过载传递到网络传输上。2、两个agent间使用udp传输利用udp的低损耗因为是单包单请求即使乱序也能随意合并重组丢包不会造成协议出错仅造成超时出错非常适合这种实时性和吞吐量要求高的情况。3、在请求id中存储了调用者和时间戳能正确相应合适的http调用者的同时能够统计性能数据作为调参依据。4、多线程等待epoll的et模式能够充分挖掘网卡通信潜力不存在单线程拷贝速度比网络传输慢的情况同时省去了任务队列每个处理线程具备一定的CPU计算处理能力也能最小化响应时间使用自旋锁后保证一个连接的消息不会被多个线程处理健壮性有保障。5、设置合适的tcp缓冲区大小在不阻塞情况下保证每次write调用都会成功。复赛部分一、 赛题重述及理解复赛的主要考察点是单机实现100W规模的队列信息存储与读取具体考察如何在单进程空间中的实现1M个队列的信息存储如何设计合理的数据存储及索引结构在特定硬盘的情况下完成高性能的顺序写、随机读和顺序读功能。同时由于测评环境是4C8G的如何合理分配利用系统资源也是很重要的任何对队列存储的东西都会放大100W倍同时维护这么多数据需要占用更多的系统资源。针对上述考察点本队提出了一套完整的队列消息数据存储方案稀疏索引存储方案。程序开发迭代中的总共提出了3套写入方案2套读方案分别在Java和C平台上进行了实现。二、 整体系统架构系统的整体结构较为简单和清晰主要分为以下几个部分每个队列对应一个对象用于维护buffer等必要结构一个容器负责将队列名映射到队列对象文件管理对象负责维护读写文件负责整体读写状态切换和管理的控制模块。如图6所示。图6 复赛系统整体架构三、 消息存储方案考虑到顺序写、随机读、顺序读的使用场景参考文件系统以簇为单位的文件存储方式设计了以块为单位的队列消息存储方式。图7 队列消息存储方式考虑到不定长消息及超长消息的情况设计了变长整形消息体的结构记录单条消息同时支持跨块的消息。图8 消息数据存储方式每个块开头都是变长整形每个VarInt存储的都是目前还剩的消息体长度便于快速跳过或判断消息是否继续跨块这种结构支持任意长度的消息如果块剩余空间不足写VarInt则padding。考虑到节省磁盘空间在CPU充足的情况下可以采用消息数据压缩但是线上CPU紧张考虑到通用性并未使用特化压缩算法。实际测试中由于消息生成性能及CPU性能的问题未开启压缩功能但在程序中预留了压缩接口以适应各种场合。图9所示为预留的压缩接口。图9 预留的压缩接口四、 队列写入方案设计好了消息存储方案剩下的就是合适的消息读写方案了由于读取方案比较固定而写入由于系统资源的匮乏(4C8G)需要合理设计。1、mmap方案(Java实现)淘汰本方案使用引用计数机制控制mmap生命周期同时mmap的块存储在hash cache定长slot基于hash冲突淘汰的cache算法中cache也会增加引用计数这样在连续写文件时保证了较好cache命中率。实现结构如图10所示。图10 mmap方案优点完全不用flush操作因为读写都是引用mmap实现的。缺点由于page cache脏超过20%会block写入线程为了不阻塞线程队列块的大小必须非常小(512bytes1M队列512MB)性能较差。2、整体内存引用方案(Java实现)淘汰考虑到mmap直接作用在page cache上受脏页比例影响难以提升缓冲大小故考虑使用整体内存方案机制和mmap类似。同样使用引用计数机制控制写入缓冲区内存块的生命周期额外使用独立线程负责将缓冲区刷盘。该方法能够等待新缓冲区内存块有效保障不写入过载同时使用环形缓冲实现内存块及等待对象复用。使用2K大小block的java程序线上成绩为167w。方案整体流程如图11所示。图11整体内存引用方案优点没有额外拷贝异步写入不会阻塞。缺点由于内存限制存在写入线程等待刷盘数据释放block大小受限制(2K如果使用4K则会存在写入等待内存交换出来写入有波动性能不是最优)。3、分片内存池方案(C实现)最优成绩综合前2种方案后提出采用分片内存池iovec内存重组写入的方案。具体实现为将block分为更小的slice每次缓冲区申请一个slice写满一个block后用iovec提交。写入线程将提交的slice用iovec拼接成更大的part整体写入磁盘。写入后将slice释放回内存池便于后续队列缓冲使用。虽然均匀写入是一种糟糕的写入方式存在缓冲申请激增的情况但由于分片机制的存在每次激增为1M*slice的大小可以通过slice大小控制。具体流程如图1213所示。图12分片内存池方案优点更细粒度的内存分配和释放4K大小block情况下缓冲区能均匀移动不会阻塞写入和落盘同步进行能在有限内存中实现更大的block。图13 分片内存方案细节内部使用的ring buffer方案及代码如图14所示所有对象均实现了复用。图14 ring buffer实现细节五、 索引方案由于内存空间限制索引记录所有消息的偏移并不是个好方案。本系统方案采用了稀疏索引因为设计的消息存储结构保证块内消息能够自行区分仅记录块信息即可。只要能知道消息所在的块的位置和消息是该块的第几个即可定位到该消息。每个消息块仅需要记录3个信息块所在文件偏移(块号)块内消息数上个块消息是否跨块。通过索引定位到具体消息数据的方法也十分简单。1、通过从前往后累加每个块的消息数量快速定位到消息所在块2、通过索引查找到块所在文件偏移并完整读出块(4K读)3、通过是否跨块信息和目标消息ID确定需要跳过的消息数快速跳过无用消息。4、读取目标消息。具体流程如图15所示。图15 消息数据定位流程定位完成后可以连续读一旦发现长度超出当前块则说明读到跨块消息继续切到下一块读取即可。六、 随机读与连续读随机读取的消息落到一个块内有较大的概率故即使是读一个小范围的数据大部分情况能一次IO读取完成。针对连续读记录每次读取到的位置如果下次访问是从0或上次读取的位置则认为是连续消费对队列的下一个块发起readahead操作将其读入到page cache中实测可以提升40%性能。由于块的大小是4K连续读时不会读入任何无效数据操作系统的page cache能提供非常高的读取性能。七、 Map容器优化由于消息写入接口是以队列名对队列进行区分的队列名到队列对象的映射也是系统的瓶颈所在。针对数字后缀队列名进行特殊hash计算处理可以降低冲突概率。对于后缀计算hash冲突的情况使用正常哈希计算离散到32个加锁的unordered map中进行存储保证鲁棒性和性能。由于后缀的区分度高spin lock基本没有冲突100W插入比传统map快几百ms100W查找快几十ms。Suffix map结构如图16所示。图16 suffix map结构八、 读写状态切换程序一开始就设计有内存和磁盘协同工作的代码但由于缓冲占用内存过大使得后续随机读和连续读依赖page cache存在性能下降的问题采用了读之前将缓冲全部落盘的策略。在第一次读的时候block所有读线程落盘释放所有缓冲内存~5.5G然后开始读取操作。由于空余缓冲是全部填充padding的即使再写入队列数据也不会出错。同时整体状态可以切换回写状态(需重新申请缓冲内存)。九、 创新性与工程价值1、针对内存不足使用内存分片iovec重组写技术达到在小内存的情况下写内存和写硬盘同时进行的目的极大提升了写磁盘性能。线上测试写时存在CPU idle0现象说明生产已大于写盘速度。2、针对队列名称改进了map提出suffix map针对后缀进行hash现实情况下也大量存在编号结尾的队列名极大降低了hash碰撞几率测评数据中没有碰撞同时用自旋锁和多个细粒度的unordered_map处理了碰撞情况提升性能的时候保证了map的正确性鲁棒性。3、先写缓冲缓冲满了排队最后填满一个大block后刷盘极大利用了SSD顺序写入性能同时在队列不均匀时也能提供同样的高性能。4、变长整形消息存储及跨块消息超长消息的处理机制保证任意长度消息都能正确处理并有同样的高性能。5、稀疏索引只对块的偏移和消息数量做记录整个索引大小仅29*4blockId2number*1000000≈166MB完全可以存储在内存中。6、使用ring buffer分散写入和等待对象采用引用机制最后释放引用的线程负责提交该块到写入队列中工作期间没有任何对象创建和销毁建立在数组上充分利用cpu cache提升了性能。7、整体内存分配然后分片使用完成后整体释放降低了内存管理的cpu消耗。8、设计时考虑了缓冲和磁盘协同工作读不用将缓冲刷盘仅仅只用等待当前io完成正确性可以保证但由于占用太多内存性能不好故后期改为全刷盘操作。9、设计时候考虑了队列索引自增涨算法但由于tcmalloc分配内存不释放后面改为定长的了有宏控制开关。10、所有长度池大小等参数都可以拿宏修改可以修改适应大内存等各种情况。