Envoy源码分析之Dispatcher

Dispatcher

在Envoy的代码中Dispatcher是随处可见的,可以说在Envoy中有着举足轻重的地位,一个Dispatcher就是一个EventLoop,其承担了任务队列、网络事件处理、定时器、信号处理等核心功能。在Envoy threading model这篇文章所提到的EventLoop(Each worker thread runs a “non-blocking” event loop)指的就是这个Dispatcher对象。这个部分的代码相对较独立,和其他模块耦合也比较少,但重要性却不言而喻。下面是与Dispatcher相关的类图,在接下来会对其中的关键概念进行介绍。

Dispatcher 和 Libevent

Dispatcher本质上就是一个EventLoop,Envoy并没有重新实现,而是复用了Libevent中的event_base,在Libevent的基础上进行了二次封装并抽象出一些事件类,比如FileEventSignalEventTimer等。Libevent是一个C库,而Envoy是C++,为了避免手动管理这些C结构的内存,Envoy通过继承unique_ptr的方式重新封装了这些libevent暴露出来的C结构。

template <class T, void (*deleter)(T*)>
class CSmartPtr : public std::unique_ptr<T, void (*)(T*)> {
public:CSmartPtr() : std::unique_ptr<T, void (*)(T*)>(nullptr, deleter) {}CSmartPtr(T* object) : std::unique_ptr<T, void (*)(T*)>(object, deleter) {}
};

通过CSmartPtr就可以将Libevent中的一些C数据结构的内存通过RAII机制自动管理起来,使用方式如下:

extern "C" {
void event_base_free(event_base*);
}struct evbuffer;
extern "C" {
void evbuffer_free(evbuffer*);
}
.....
typedef CSmartPtr<event_base, event_base_free> BasePtr;
typedef CSmartPtr<evbuffer, evbuffer_free> BufferPtr;
typedef CSmartPtr<bufferevent, bufferevent_free> BufferEventPtr;
typedef CSmartPtr<evconnlistener, evconnlistener_free> ListenerPtr;

在Libevent中无论是定时器到期、收到信号、还是文件可读写等都是事件,统一使用event类型来表示,Envoy中则将event作为ImplBase的成员,然后让所有的事件类型的对象都继承ImplBase,从而实现了事件的抽象。

class ImplBase {
protected:~ImplBase();event raw_event_;
};

SignalEvent

SignalEvent的实现很简单,通过evsignal_assign来初始化事件,然后通过evsignal_add添加事件使事件成为未决状态(关于Libevent事件状态见附录)。

class SignalEventImpl : public SignalEvent, ImplBase {
public:// signal_num: 要设置的信号值// cb: 信号事件的处理函数SignalEventImpl(DispatcherImpl& dispatcher, int signal_num, SignalCb cb);
private:SignalCb cb_;
};SignalEventImpl::SignalEventImpl(DispatcherImpl& dispatcher, int signal_num, SignalCb cb) : cb_(cb) {evsignal_assign(&raw_event_, &dispatcher.base(), signal_num,[](evutil_socket_t, short, void* arg) -> void { static_cast<SignalEventImpl*>(arg)->cb_(); },this);evsignal_add(&raw_event_, nullptr);
}

Timer

Timer事件暴露了两个接口一个用于关闭Timer,另外一个则用于启动Timer,需要传递一个时间来设置Timer的到期时间间隔。

class Timer {
public:virtual ~Timer() {}virtual void disableTimer() PURE;virtual void enableTimer(const std::chrono::milliseconds& d) PURE;
};

创建Timer的时候会通过evtimer_assgin对event进行初始化,这个时候事件还处于未决状态而不会触发,需要通过event_add添加到Dispatcher中才能被触发。

class TimerImpl : public Timer, ImplBase {
public:TimerImpl(Libevent::BasePtr& libevent, TimerCb cb);// Timervoid disableTimer() override;void enableTimer(const std::chrono::milliseconds& d) override;private:TimerCb cb_;
};TimerImpl::TimerImpl(DispatcherImpl& dispatcher, TimerCb cb) : cb_(cb) {ASSERT(cb_);evtimer_assign(&raw_event_, &dispatcher.base(),[](evutil_socket_t, short, void* arg) -> void { static_cast<TimerImpl*>(arg)->cb_(); }, this);
}

disableTimer被调用时其内部会调用event_del来删除事件,使事件成为非未决状态,enableTimer被调用时则间接调用event_add使事件成为未决状态,这样一旦超时时间到了就会触发超时事件。

void TimerImpl::disableTimer() { event_del(&raw_event_); }
void TimerImpl::enableTimer(const std::chrono::milliseconds& d) {if (d.count() == 0) {event_active(&raw_event_, EV_TIMEOUT, 0);} else {std::chrono::microseconds us = std::chrono::duration_cast<std::chrono::microseconds>(d);timeval tv;tv.tv_sec = us.count() / 1000000;tv.tv_usec = us.count() % 1000000;event_add(&raw_event_, &tv);}
}

上面的代码在计算timer时间timeval的时候实现的并不优雅,应该避免使用像1000000这样的不具备可读性的数字常量,社区中有人建议可以改成如下的形式。

auto secs = std::chrono::duration_cast<std::chrono::seconds>(d);
auto usecs = std::chrono::duration_cast<std::chrono::microseconds>(d - secs);
tv.tv_secs = secs.count();
tv.tv_usecs = usecs.count();

FileEvent

socket套接字相关的事件被封装为FileEvent,其上暴露了二个接口:activate用于主动触发事件,典型的使用场景比如: 唤醒EventLoop、Write Buffer有数据,可以主动触发下可写事件(Envoy中的典型使用场景)等;setEnabled用于设置事件类型,将事件添加到EventLoop中使其成为未决状态。

void FileEventImpl::activate(uint32_t events) {int libevent_events = 0;if (events & FileReadyType::Read) {libevent_events |= EV_READ;}if (events & FileReadyType::Write) {libevent_events |= EV_WRITE;}if (events & FileReadyType::Closed) {libevent_events |= EV_CLOSED;}ASSERT(libevent_events);event_active(&raw_event_, libevent_events, 0);
}void FileEventImpl::setEnabled(uint32_t events) {event_del(&raw_event_);assignEvents(events);event_add(&raw_event_, nullptr);
}

任务队列

Dispatcher的内部有一个任务队列,也会创建一个线程专们处理任务队列中的任务。通过Dispatcherpost方法可以将任务投递到任务队列中,交给Dispatcher内的线程去处理。

void DispatcherImpl::post(std::function<void()> callback) {bool do_post;{Thread::LockGuard lock(post_lock_);do_post = post_callbacks_.empty();post_callbacks_.push_back(callback);}if (do_post) {post_timer_->enableTimer(std::chrono::milliseconds(0));}
}

post方法将传递进来的callback所代表的任务,添加到post_callbacks_所代表的类型为vector<callback>的成员表变量中。如果post_callbacks_为空的话,说明背后的处理线程是处于非活动状态,这时通过post_timer_设置一个超时时间时间为0的方式来唤醒它。post_timer_在构造的时候就已经设置好对应的callbackrunPostCallbacks,对应代码如下:

DispatcherImpl::DispatcherImpl(TimeSystem& time_system,Buffer::WatermarkFactoryPtr&& factory): ......post_timer_(createTimer([this]() -> void { runPostCallbacks(); })),current_to_delete_(&to_delete_1_) {RELEASE_ASSERT(Libevent::Global::initialized(), "");
}

runPostCallbacks是一个while循环,每次都从post_callbacks_中取出一个callback所代表的任务去运行,直到post_callbacks_为空。每次运行runPostCallbacks都会确保所有的任务都执行完。显然,在runPostCallbacks被线程执行的期间如果post进来了新的任务,那么新任务直接追加到post_callbacks_尾部即可,而无需做唤醒线程这一动作。

void DispatcherImpl::runPostCallbacks() {while (true) {std::function<void()> callback;{Thread::LockGuard lock(post_lock_);if (post_callbacks_.empty()) {return;}callback = post_callbacks_.front();post_callbacks_.pop_front();}callback();}
}

DeferredDeletable

最后讲一下Dispatcher中比较难理解也很重要的DeferredDeletable,它是一个空接口,所有要进行延迟析构的对象都要继承自这个空接口。在Envoy的代码中像下面这样继承自DeferredDeletable的类随处可见。

class DeferredDeletable {
public:virtual ~DeferredDeletable() {}
};

那何为延迟析构呢?用在哪个场景呢?延迟析构指的是将析构的动作交由Dispatcher来完成,所以DeferredDeletableDispatcher密切相关。Dispatcher对象有一个vector保存了所有要延迟析构的对象。

class DispatcherImpl : public Dispatcher {......private:........std::vector<DeferredDeletablePtr> to_delete_1_;std::vector<DeferredDeletablePtr> to_delete_2_;std::vector<DeferredDeletablePtr>* current_to_delete_;}

to_delete_1_to_delete_2_就是用来存放所有的要延迟析构的对象,这里使用两个vector存放,为什么要这样做呢?。current_to_delete_始终指向当前正要析构的对象列表,每次执行完析构后就交替指向另外一个对象列表,来回交替。

void DispatcherImpl::clearDeferredDeleteList() {ASSERT(isThreadSafe());std::vector<DeferredDeletablePtr>* to_delete = current_to_delete_;size_t num_to_delete = to_delete->size();if (deferred_deleting_ || !num_to_delete) {return;}ENVOY_LOG(trace, "clearing deferred deletion list (size={})", num_to_delete);if (current_to_delete_ == &to_delete_1_) {current_to_delete_ = &to_delete_2_;} else {current_to_delete_ = &to_delete_1_;}deferred_deleting_ = true;for (size_t i = 0; i < num_to_delete; i++) {(*to_delete)[i].reset();}to_delete->clear();deferred_deleting_ = false;
}

上面的代码在执行对象析构的时候先使用to_delete来指向当前正要析构的对象列表,然后将current_to_delete_指向另外一个列表,这样在添加延迟删除的对象时,就可以做到安全的把对象添加到列表中了。因为deferredDeleteclearDeferredDeleteList都是在同一个线程中运行,所以current_to_delete_是一个普通的指针,可以安全的更改指针指向另外一个,而不用担心有线程安全问题。

void DispatcherImpl::deferredDelete(DeferredDeletablePtr&& to_delete) {ASSERT(isThreadSafe());current_to_delete_->emplace_back(std::move(to_delete));ENVOY_LOG(trace, "item added to deferred deletion list (size={})", current_to_delete_->size());if (1 == current_to_delete_->size()) {deferred_delete_timer_->enableTimer(std::chrono::milliseconds(0));}
}

当有要进行延迟析构的对象时,调用deferredDelete即可,这个函数内部会通过current_to_delete_把对象放到要延迟析构的列表中,最后判断下当前要延迟析构的列表大小是否是1,如果是1表明这是第一次添加延迟析构的对象,那么就需要通过deferred_delete_timer_把背后的线程唤醒执行clearDeferredDeleteList函数。这样做的原因是避免多次唤醒,因为有一种情况是线程已经唤醒了正在执行clearDeferredDeleteList,在这个过程中又有其他的对象需要析构而加入到vector中。

到此为止deferredDelete的实现原理就基本分析完了,可以看出它的实现和任务队列的实现很类似,只不过一个是循环执行callback所代表的任务,另一个是对对象进行析构。最后我们来看一下deferredDelete的应用场景,却“为何要进行延迟析构?”在Envoy的源代码中经常会看到像下面这样的代码片段。

ConnectionImpl::ConnectionImpl(Event::Dispatcher& dispatcher, ConnectionSocketPtr&& socket,TransportSocketPtr&& transport_socket,bool connected) {
......}// 传递裸指针到回调中file_event_ = dispatcher_.createFileEvent(fd(), [this](uint32_t events) -> void { onFileEvent(events); }, Event::FileTriggerType::Edge,Event::FileReadyType::Read | Event::FileReadyType::Write);......
}

传递给Dispatchercallback都是通过裸指针的方式进行回调,如果进行回调的时候对象已经析构了,就会出现野指针的问题,我相信C++水平还可以的同学都会看出这个问题,除非能在逻辑上保证Dispatcher的生命周期比所有对象都短,这样就能保证在回调的时候对象肯定不会析构,但是这不可能成立的,因为DispatcherEventLoop的核心。

一个线程运行一个EventLoop直到线程结束,Dispatcher对象才会析构,这意味着Dispatcher对象的生命周期是最长的。所以从逻辑上没办法保证进行回调的时候对象没有析构。可能有人会有疑问,对象在析构的时候把注册的事件取消不就可以避免野指针的问题吗? 那如果事件已经触发了,callback正在等待运行呢? 又或者callback运行了一半呢?前者libevent是可以保证的,在调用event_del的时候可以把处于等待运行的事件取消掉,但是后者就无能为力了,这个时候如果对象析构了,那行为就是未定义了。沿着这个思路想一想,是不是只要保证对象析构的时候没有callback正在运行就可以解决问题了呢?是的,只要保证所有在执行中的callback执行完了,再做对象析构就可以了。可以利用Dispatcher是顺序执行所有callback的特点,向Dispatcher中插入一个任务就是用来对象析构的,那么当这个任务执行的时候是可以保证没有其他任何callback在运行。通过这个方法就完美解决了这里遇到的野指针问题了。

或许有人又会想,这里是不是可以用shared_ptr和shared_from_this来解这个呢? 是的,这是解决多线程环境下对象析构的秘密武器,通过延长对象的生命周期,把对象的生命周期延长到和callback一样,等callback执行完再进行析构,同样可以达到效果,但是这带来了两个问题,第一就是对象生命周期被无限拉长,虽然延迟析构也拉长了生命周期,但是时间是可预期的,一旦EventLoop执行了clearDeferredDeleteList任务就会立刻被回收,而通过shared_ptr的方式其生命周期取决于callback何时运行,而callback何时运行这个是没办法保证的,比如一个等待socket的可读事件进行回调,如果对端一直不发送数据,那么callback就一直不会被运行,对象就一直无法被析构,长时间累积会导致内存使用率上涨。第二就是在使用方式上侵入性较强,需要强制使用shared_ptr的方式创建对象。

总结

Dispatcher总的来说其实现还是比较简单明了的,比较容易验证其正确性,同样功能也相对较弱,和chromium的MessageLoop、boost的asio都是相似的用途,但是功能上差得比较多。好在这是专门给Envoy设计的,而且Envoy的场景也比较单一,不必做成那么通用的。另外一个我觉得比较奇怪的是,为什么在DeferredDeletable的实现中要用to_delete_1_to_delete_2_两个队列交替来存放,其实按照我的理解一个队列即可,因为clearDeferredDeleteListdeferredDelete是保证在同一个线程中执行的,就和Dispatcher的任务队列一样,用一个队列保存所有要执行的任务,循环的执行即可。但是Envoy中没有这样做,我理解这样设计的原因可能是因为相比于任务队列来说延迟析构的重要性更低一些,大量对象的析构如果保存在一个队列中循环的进行析构势必会影响其他关键任务的执行,所以这里拆分成两个队列,多个任务交替的执行,就好比把一个大任务拆分成了好几个小任务顺序来执行。

附录

  • Libevent状态转换图


阿里云双十一1折拼团活动:满6人,就是最低折扣了!
【满6人】1核2G云服务器99.5元一年298.5元三年 2核4G云服务器545元一年 1227元三年
【满6人】1核1G MySQL数据库 119.5元一年
【满6人】3000条国内短信包 60元每6月
参团地址:http://click.aliyun.com/m/1000020293/


原文链接
本文为云栖社区原创内容,未经允许不得转载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/520636.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

这项技术厉害了!让旅行者 2 号从星际空间发首批数据!

限时8.3折&#xff0c;立即购票&#xff1a;https://dwz.cn/z1jHouwE物联网作为信息系统向物理世界的延伸&#xff0c;极大地拓展了人类认知和控制物理世界的能力&#xff0c;被称为继计算机和互联网之后的世界信息产业的第三次浪潮&#xff0c;正在深刻地改变着人类的生存环境…

修改文件 华为交换机_华为交换机系统文件管理配置命令大全(二)

11、解压文件&#xff08;unzip&#xff09;<Huawei>dirDirectory of flash:/Idx Attr Size(Byte) Date Time FileName0 drw- - Aug 07 2015 13:51:14 src1 drw- - Apr 02 2016 11:29:41 pmdata2 drw- - Apr 02 2016 11:29:52 dhcp3 -rw- 28 Apr 02 2016 11:29:53 privat…

从阿里云数据库入选Gartner谈数据库的演化

根据全球权威的IT咨询公司Gartner的最新研究报告&#xff0c;在2018年度数据库系统的魔力象限中&#xff0c;阿里云数据库被列入“远见者”象限&#xff0c;这是国产数据库首次进入Gartner魔力象限。Gartner的魔力四象限&#xff0c;描述了数据库厂商的产品能力和市场规模。四个…

申请美国计算机科学,美国计算机科学的申请特点

计算机科学官方定义&#xff1a;计算机科学是系统性研究信息与计算的理论基础以及它们在计算机系统中如何实现与应用的实用技术的学科。它通常被形容为对那些创造、描述以及转换信息的算法处理的系统研究&#xff0c;计算机科学专业的申请特点如下&#xff1a;申请难度中等学校…

mysql 插入数据时 自动设置创建时间和更新时间

一般除了配置表&#xff0c;表中都会有create_time &#xff0c;update_time 2个字段&#xff0c;而这个2个字段测处理方式雨2种&#xff1a; 1在代码中设置当前日期 2>mysq自动设置&#xff08;推荐使用&#xff09; 加入&#xff0c;已经设置好了&#xff0c;修改一下表结…

基于智能家居场景的POALRDB性能体验

Polardb 是阿里云研发的一种关系型数据库&#xff0c;与mysql完全兼容&#xff0c;而性能又是其6倍&#xff0c;具有高吞吐&#xff0c;低延迟等特性&#xff1b; 本测试通过模拟控制智能家居开关的终端场景&#xff0c;来体验polardb的性能&#xff1b; 1、环境搭建 1.1 po…

云计算软件生态圈:摸到一把大牌

戳蓝字“CSDN云计算”关注我们哦&#xff01;作者 | 老姜责编 | 阿秃出品 | CSDN云计算&#xff08;ID&#xff1a;CSDNcloud&#xff09;“我觉得我摸着了一把大牌。”软件领域的新锐企业——有赞公司创始人兼CEO白鸦在转向SaaS领域的一个细分市场时&#xff0c;曾对天使投资人…

箱梁终张拉后弹性上拱度计算_高速铁路预应力简支箱梁反拱预设分析

高速铁路预应力简支箱梁反拱预设分析关叶沆1&#xff0c;吴成龙1&#xff0c;王玲1【摘要】结合成贵铁路宜宾制梁场后张法预应力简支箱梁制作的具体情况&#xff0c;对梁体预设反拱理论计算及设置方法进行分析&#xff0c;并通过对箱梁梁体徐变及上拱度测量数据进行统计分析&am…

一体台式计算机名称,【一体台式电脑】一体台式电脑品牌推荐,台式一体机电脑哪款好_什么值得买...

1. Apple 苹果 MXWT2CH/A iMac(2019)27英寸一体机 8GB 2666MHZ DDR4 内存商品简介&#xff1a;Apple 苹果 iMac(2019)27英寸一体机保留了前代产品的27英寸5K屏幕(分辨率5120 x 2880&#xff0c;DCI-P3色域)对比上代Retina视网膜屏幕电脑&#xff0c;外观没有变化&#xff0c;主…

linux shell脚本 删除指定目录下文件夹(可指定文件夹名、时间)

情景&#xff1a;需要删除以201812开头的、6天前修改的文件夹&#xff08;文件夹里包含文件&#xff09;。鼓捣了好一会&#xff0c;开始用find /home/users/niu/test/log/ -name 201812* -type d -mtime 5 -exec rm -f {} \; 会报错&#xff1a;no such file or dire…

SLS机器学习介绍(02):时序聚类建模

文章系列链接 SLS机器学习介绍&#xff08;01&#xff09;&#xff1a;时序统计建模SLS机器学习介绍&#xff08;02&#xff09;&#xff1a;时序聚类建模SLS机器学习介绍&#xff08;03&#xff09;&#xff1a;时序异常检测建模SLS机器学习介绍&#xff08;04&#xff09;&a…

蚂蚁金服金融级容器引擎实践之路

小蚂蚁说&#xff1a; 在金融级分布式架构中使用容器&#xff0c;许多企业的开发者都面临许多挑战。在2018年ATEC蚂蚁金服技术探索大会上&#xff0c;蚂蚁金服高级技术专家盛延敏在演讲中分析了容器与云原生技术的本质&#xff0c;为容器在分布式架构上的使用带来了实用高效的…

【耿老师公开课】反转!物联网火爆,开发者却很难入门?

戳蓝字“CSDN云计算”关注我们哦&#xff01;在 2019 北京网络安全大会上&#xff0c;工信部负责人表示&#xff0c;我国面向 5G 和车联网将建设网安防护体系&#xff0c;提升监测预警和应急响应能力。其中物联网设备已成为网安防护新重点。为什么工信部会这么重视物联网&#…

rust腐蚀怎么单人游戏_腐蚀游戏怎么提高帧数 Rust设置隐藏画质提高FPS教程

腐蚀游戏怎么提高帧数 Rust设置隐藏画质提高FPS教程2018-03-04 10:58:10来源&#xff1a;游戏下载编辑&#xff1a;云曼衍评论(0)《腐蚀(Rust)》已经正式上市发售了&#xff0c;因为游戏的配置比较高&#xff0c;有的玩家在玩游戏的时候画面总是达不到自己想要的效果。今天小编…

哈工大未来计算机院士,2017年中国高校新增工程院院士名单出炉,哈工大依然很强!...

原标题&#xff1a;2017年中国高校新增工程院院士名单出炉&#xff0c;哈工大依然很强&#xff01;院士是一个国家在科学技术方面的最高称号&#xff0c;不光在我们国家有&#xff0c;其他很多国家也有院士称号。而我国的院士一般都是指中国工程院院士和中国科学院院士&#xf…

使用NAS动态存储卷创建有状态应用

介绍&#xff1a; 目前动态生成NAS存储卷的实现&#xff1a;在一个已有文件系统上&#xff0c;自动生成子文件系统&#xff08;子目录&#xff09;&#xff0c;并生成目标存储卷&#xff08;PV&#xff09;&#xff1b; 生成的PV名字为&#xff1a;pvc-${pv-uid} 镜像介绍&a…

vue项目使用sass-loader

npm install -g cnpm --registryhttps://registry.npm.taobao.orgcnpm install node-sass --save-dev cnpm install sass-loader --save-dev

WAM计算机术语,最新的计算机一级考试MSOffice模拟试题及答案

A、数据处理B、文献检索C、资源共享和传输D、传输28&#xff0e;PowerPoint中&#xff0c;下列说法错误的是。DA、允许插入在其他图形程序中创建的图片B、为了将某种格式的图片插入到POWERPOINT中&#xff0c;必须安装相应的图形过滤器C、选择插入菜单中的"图片"命令…

老司机开车,教会女朋友什么是「马拉车算法」

戳蓝字“CSDN云计算”关注我们哦&#xff01;作者 | 李威责编 | 阿秃马拉车算法&#xff08; Manacher‘s Algorithm &#xff09;是小吴最喜欢的算法之一&#xff0c;因为&#xff0c;它真的很牛逼&#xff01;马拉车算法是用来 查找一个字符串的最长回文子串的线性方法 &…