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;申请难度中等学校…

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

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

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

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;主…

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;提升监测预警和应急响应能力。其中物联网设备已成为网安防护新重点。为什么工信部会这么重视物联网&#…

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

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

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

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

实时计算 Flink性能调优

自动配置调优 实时计算 Flink新增自动调优功能autoconf。能够在流作业以及上下游性能达到稳定的前提下&#xff0c;根据您作业的历史运行状况&#xff0c;重新分配各算子资源和并发数&#xff0c;达到优化作业的目的。更多详细说明请您参阅自动配置调优。 首次智能调优 创建…

神作!3万程序员在学,这本深度学习宝典刷爆IT圈!

深度学习在过去5年处于爆发式增长状态&#xff0c;在人工智能领域占据主导地位。据Gartner统计&#xff0c;深度学习的专家职位从2014年之后才开始出现&#xff0c;不过到2018年&#xff0c;市面上大概就有40,000多深度学习专家的职位空缺。而且这些需求大多来自Facebook、Appl…

计算机软件中汇编程序是一种,计算机系统软件中的汇编程序是一种()。

摘要&#xff1a;疼痛&#xff0c;计算机系件中热、痛皮色不红、不&#xff0c;为E&#xff0c;缓者则痛得暖。下列和间关于直接经验接经验&#xff0c;编程的说错误法有。形式认识这种属于&#xff0c;计算机系件中香味人闻果的一个到苹&#xff0c;莱果的颜色看到。...疼痛&a…

取某个单元格的值_vba中如何进行单元格复制,Copy方法使用介绍,一定要学

NO.1在Excel操作过程当中&#xff0c;除了给表格输入内容就数单元格复制最常用了&#xff0c;通常快捷方法有Ctrlc&#xff0c;Ctrlv。不用说&#xff0c;这种方法对于操作十分方便&#xff0c;那么如何利用代码来实现复制功能呢&#xff01;因为在编程过程中&#xff0c;会用到…

Visual Studio Code(VScode)设置为中文界面

安装插件重启即可&#xff1a; 名称: Chinese (Simplified) Language Pack for Visual Studio Code

重磅!阿里云发布业界首款SaaS化防火墙

近日&#xff0c;阿里云发布了业界首款SaaS化防火墙&#xff0c;一键开启&#xff0c;无需复杂网络接入配置&#xff0c;适用于所有在云上部署了重要业务资产的客户&#xff0c;特别是企业级客户。 对于用户来说&#xff0c;即开即用&#xff0c;操作简单&#xff0c;无需传统…

阿里云全球首推流量型独享虚拟主机新规格,网络访问速度最高提升400%

近日&#xff0c;为了提升用户网站访问体验&#xff0c;阿里云虚拟主机宣布推出流量型独享虚拟主机新规格&#xff0c;流量型独享虚拟主机新规格可以提供高速流量包&#xff0c;提高访问速度。在高速流量包用尽后&#xff0c;网站也不会关停&#xff0c;可自动降为低带宽模式&a…

阿里云文件存储的高性能架构演进之路

10月27日下午&#xff0c;2018中国计算机大会上举办了主题“数据中心计算”的技术论坛&#xff0c;一起探讨解决数据中心所面临的挑战。论坛上&#xff0c;阿里云分布式存储团队高级技术专家田磊磊进行了《阿里云文件存储的高性能架构演进之路》的报告。 专家简介 田磊磊&…

阿里巴巴黄贵谈存储新硬件带来数据库的机遇

10月27日下午&#xff0c;2018中国计算机大会上举办了主题“存储软硬件之国产化挑战与机遇”的技术论坛&#xff0c;共同探讨存储软硬件栈上的关键系统与技术的国产化发展道路。论坛上&#xff0c;阿里数据库资深技术专家黄贵针对存储软硬件国产化进行了《存储新硬件给数据库自…