本文由逍遥子撰写,转发请标注原址:
http://blog.csdn.net/houjixin/article/details/46413583
或
http://houjixin.blog.163.com/blog/static/3562841020155835146428/#
原版的mosquito在移动互联网情况下,其性能不高,实际运营时一个mosquito实例能支持2万连接就不错了;mosquitto在网络状态不好的情况下,随着用户量的上升,其对cpu消耗将大幅增加,主要的CPU主要消耗在以下几个方面:
(1)Poll机制的缺陷;
(2)Mosquitto内部订阅树机制的缺陷;
(3)其他消息发送,数据结构管理方面的缺陷;
本节将针对这些缺陷提出相应的优化策略和方法。
7.1、poll优化
7.1.1、优化原因
在mosquitto原始程序中,核心处理流程是对所有的socket进行监听处理,该部分功能主要使用poll来完成,但是它的效率较差,尤其当在线活动用户较少的情况下,性能更差,这一定程序上影响了mosquitto性能,poll效率的低下主要是由于下面3个原因:
1) poll在每次监听端口之前,都需要重新注册所有需要监听的socket;
2) poll返回的结果中,只会修改有事件发生的socket对应的poll结构体,因此,在使用时需要对所有注册的socket对应的poll结构体进行扫描,才能判断出那些socket有事件发生。
3) 在内部实现上,poll需要查询所有注册的socket以确定其是否有事件发生。
Poll的上述问题是是其自身实现方式造成的,很难进行优化。针对poll的这些问题,linux实现了一个更高效的实现方式:epoll,相对而言,epoll则不需要poll这些复杂操作,epoll具有以下3个优点:
1) epoll中,只需要将被监听的socket注册进epoll一次,后续epoll就会监听它,而不需要每次监听之前重新注册。
2) epoll返回结果包含了所有有事件发生的socket,因此处理过程中,扫描所有这些有事件发生的socket即可,而不需要扫描所有注册的socket。
3) 在内部实现上,epoll不需查询所有注册的socket,它内部是:所有有事件发生的socket自己将自己挂载epoll的就绪队列上,epoll只需返回就绪队列中的socket即可。
因此,本次优化首先选择对poll进行优化,主要使用epoll替换poll,以提升系统的执行效率。
7.1.2、优化方案:
Mosquitto中对poll的使用主要集中在文件loop.c的在函数mosquitto_main_loop中,因此本次修改将使用一个新的函数epoll_mosquitto_main_loop来代替它。另外,在使用方法上,epoll的监听函数epoll_wait返回所有就绪socket,而mosquitto程序中需要知道socket对应的conetxt才能完成业务处理,因此,为了支持epoll,需要增加一个hash表t_fd2context来完成socket到其对应conetxt的映射。
Mosquito的核心处理逻辑主要在函数mosquitto_main_loop中,该函数主要完成了以下功能:
1) 更新系统topic的信息;
2) 将所有监听socket放入poll结构体pollfds中;
3) 扫描所有context,完成下列工作:
如果context有消息发送,则将消息按照mqtt协议发送出去;
检查context是否超时;
将context的socket放入poll的结构体pollfds中。
4) 扫描所有conetxt,如果context中有消息,则更新消息的时间戳;
5) 调用poll,轮询poll的结构体pollfds所有的socket;
6) 处理poll的结果,需完成下面两个工作:
扫描所有的context,查看其对应的socket是否有事件发生,如果有,则进行读写处理;
处理所有的监听socket,如果有新的连接进入,则为之创建对应的context
7) 进行重新加载配置文件等可选择操作。
在poll的工作过程中,上述操作将会循环执行,使用epoll优化之后的mosquitto的核心流程将会有所改变,为:
1) 向epoll中注册监听端口,该步骤在循环之前执行;下面2)之后的步骤将会放入循环执行。
2) 更新系统topic的信息;
3) 根据策略扫描全部context,完成下面两个工作:
回收context,并将该context的索引放入空闲索引数组中;
检查超时,如果超时,则将该context与其socket的映射从hash表t_fd2context中删除;
4) 调用epoll的epoll_wait函数获取所有的就绪socket;
5) 对所有的就绪socket进行处理,主要完成下面的工作:
如果就绪的socket是监听接口,则对监听接口进行处理,为每个新进来的业务socket建立context,并将其注册进epoll;
如果不是监听socket,则从socket到cotext的hash表t_fd2context中找到该socket对应的context,然后完成相应的处理,如果找不到,则断开此socket的连接。
6) 进行重新加载配置文件等可选择操作。
由于epoll和poll的工作方式不同,因此需要对上述流程进行修改以使其能适应epoll的要求,主要修改之处包括:
1) Socket注册方式;poll中每次循环都需要将socket重新注册入poll,而epoll只需要开始注册一次即可;
2) 返回结果处理;poll中需要对所有注册的socket结构体进行扫描,才能判断某个socket是否有数据处理;epoll直接返回就绪socket,因此无需全部遍历注册的socket结构体。
3) 增加socket到对应context的映射,由于epoll直接返回就绪socket,而mosquitto中需要找到该socket对应的context才能进行上层应用的处理,因此需要增加一个hash表完成socket到context的映射。
4) 修改消息发送部分的功能
7.1.3、具体实现
Epoll的优化也采用原来的单线程结构,并使用一个大循环“while”完成对任务的处理,该大循环被放在函数epoll_mosquitto_main_loop中,该函数与函数mosquitto_main_loop(该函数是使用poll时的主要业务处理)的参数完全相同,并在函数mosquitto_main_loop中调用epoll_mosquitto_main_loop,因此程序中原来调用poll的主业务处理函数mosquitto_main_loop的地方将会被转向调用epoll的主业务处理函数epoll_mosquitto_main_loop,从而实现对epoll功能的调用,系统的流程如下图5-1所示。
图7-1 使用epoll之后的系统流程图
1、socket注册
epoll在使用时只需将待监控的socket加入一次即可,这一点与poll在使用方法上有差别,在mosquitto程序中,需要epoll监控的socket包括监听socket和业务socket;Socket的注册过程由函数reg_socket完成,注册socket的监听类型为EPOLLIN,采用默认的水平触发模式。
1) 监听socket,此类型socket负责接收客户端的新连接,例如程序中默认的1883端口对应的socket,客户端将使用该端口连接到mosquitto。在函数epoll_mosquitto_main_loop的主循环开始直接之前完成注册,只进行一次注册,后续不再重复注册;
2) 业务socket,该类型socket负责完成客户端和mosquitto之间的业务数据的传输;业务socket将在新连接进入时完成注册,此过程在函数epoll_loop_handle_result中完成。
2、Epoll的事件处理
Epoll事件处理将由函数epoll_loop_handle_result来完成,在该函数中将循环扫描epoll返回的所有就绪socket,并对每个就绪的socket进行处理,处理的方式为:
1) 如果为监听socket,则首先调用mqtt3_socket_accept函数对新连接进来的socket进行处理,处理过程包括:为socket创建对应的context等;其次将socket注册入epoll。
2) 如果监听端口为业务socket,则读取该结构体上的数据,然后对数据进行处理。
图7-2 epoll事件处理流程