源码分析——Envoy Dispatcher
Envoy threading model
Envoy使用三种不同类型的线程
Main线程
该线程包括server的启动和关闭、所有xDS的API处理(包括DNS、健康检查和通用集群管理)、Runtime、Stat flush、Admin以及Process management(信号、热重启等)。这个线程上发生的所有事情都是异步的,并且是非阻塞的。通常,主线程协调所有不需要大量CPU来完成的关键进程功能。
这使大多数管理代码可以像单线程一样编写。
Worker线程
默认情况下,Envoy为系统中的每个硬件线程生成一个Worker线程。(通过--concurrency选项控制)。每个工作线程运行一个非阻塞的event loop,负责监听每个侦听器(目前没有侦听器分片),接受新连接,为连接实例化一个filter stack,并在连接的生存期内处理所有IO。 这使得大多数连接处理代码可以像单线程一样编写。
File flusher线程
Envoy写入的每个文件(主要是access logs)目前都有一个独立的阻塞flush线程。这是因为即使使用O_NONBLOCK,向文件系统缓存文件写入数据有时也会阻塞。当Worker线程需要写入文件时,数据实际上被移动到内存缓冲区中,最终通过File flusher线程将数据刷新。这是代码的一个区域,从技术上讲,所有worker都可以阻塞于同一个锁上,以尝试填充内存缓冲区。
Dispatcher
在Envoy的代码中Dispatcher是随处可见的,可以说在Envoy中有着举足轻重的地位,一个Dispatcher就是一个EventLoop,其承担了任务队列、网络事件处理、定时器、信号处理等核心功能。在Envoy threading model这篇文章所提到的EventLoop(Each worker thread runs a “non-blocking” event loop)指的就是这个Dispatcher对象。这个部分的代码相对较独立,和其他模块耦合也比较少,但重要性却不言而喻。下面是与Dispatcher相关的类图,在接下来会对其中的关键概念进行介绍。
libevent
Dispatcher 本质上就是一个 EventLoop ,Envoy 并没有重新实现,而是复用了Libevent 中的event_base,在Libevent的基础上进行了二次封装并抽象出一些事件类,比如FileEvent、SignalEvent、Timer等。Libevent是一个C库,而Envoy是C++,为了避免手动管理这些C结构的内存,Envoy通过继承unique_ptr的方式重新封装了这些libevent暴露出来的C结构。
1.实现CSmartPtr。
source\common\common\c_smart_ptr.h
2.通过CSmartPtr就可以将Libevent中的一些C数据结构的内存通过RAII机制自动管理起来。这样libevent的结构体就变成了C++的智能指针。
source\common\event\libevent.h
3.ImplBase是libevent事件实现的基类。event_struct嵌入在该类中,派生类要在构造函数中分配它。对象在析构时会自动调用 event_del。
source\common\event\event_impl_base.h
4.在Libevent中无论是定时器到期、收到信号、还是文件可读写等都是事件,统一使用event类型来表示,Envoy中则将event作为ImplBase的成员,然后让所有的事件类型的对象都继承ImplBase,从而实现了事件的抽象。
source\common\event\timer_impl.h
source\common\event\signal_impl.h
source\common\event\file_event_impl.h
他们的interface的声明在这里:
include\envoy\event\file_event.h
include\envoy\event\signal.h
include\envoy\event\timer.h
SignalEvent
暂略。
Timer
暂略。
FileEvent
文件相关的事件封装为 FileEvent。我们知道 linux 中 socket 也是一个文件,因此 socket 套接字相关的事件也属于 FileEvent。FileEvent 使用持久性事件假定用户一直读或写,直到收到 EAGAIN。
(未完待续)
任务队列
Dispatcher的内部有一个任务队列,也会创建一个线程专们处理任务队列中的任务。通过Dispatcher的post方法可以将任务投递到任务队列中,交给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_在构造的时候就已经设置好对应的callback为runPostCallbacks,对应代码如下:
runPostCallbacks是一个while循环,每次都从post_callbacks_中取出一个callback所代表的任务去运行,直到post_callbacks_为空。每次运行runPostCallbacks都会确保所有的任务都执行完。显然,在runPostCallbacks被线程执行的期间如果post进来了新的任务,那么新任务直接追加到post_callbacks_尾部即可,而无需做唤醒线程这一动作。
(这个声明位于循环体内部,这样在没有post_lock_时回调就会被销毁。如果在循环之外声明了回调,并且在每个迭代中重用它,那么在重新分配回调时,先前的迭代的回调将被销毁,这种情况在持有锁时发生。如果销毁回调会运行一个析构函数,这个析构函数会通过这个调度器上的一些调用栈调用post(),这会导致死锁(通过递归互斥体的获取)。)
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(); } }
(未完待续)
延迟析构
(未完待续)
总结与思考
Dispatcher总的来说其实现还是比较简单明了的,比较容易验证其正确性,同样功能也相对较弱,和chromium的MessageLoop、boost的asio都是相似的用途,但是功能上差得比较多。好在这是专门给Envoy设计的,而且Envoy的场景也比较单一,不必做成那么通用的。
传递给Dispatcher的callback都是通过裸指针的方式进行回调,如果进行回调的时候对象已经析构了,就会出现野指针的问题,我相信C++水平还可以的同学都会看出这个问题,除非能在逻辑上保证Dispatcher的生命周期比所有对象都短,这样就能保证在回调的时候对象肯定不会析构,但是这不可能成立的,因为Dispatcher是EventLoop的核心。
另外一个我觉得比较奇怪的是,为什么在DeferredDeletable的实现中要用to_delete_1_和to_delete_2_两个队列交替来存放,其实按照我的理解一个队列即可,因为clearDeferredDeleteList和deferredDelete是保证在同一个线程中执行的,就和Dispatcher的任务队列一样,用一个队列保存所有要执行的任务,循环的执行即可。但是Envoy中没有这样做,我理解这样设计的原因可能是因为相比于任务队列来说延迟析构的重要性更低一些,大量对象的析构如果保存在一个队列中循环的进行析构势必会影响其他关键任务的执行,所以这里拆分成两个队列,多个任务交替的执行,就好比把一个大任务拆分成了好几个小任务顺序来执行。
- 点赞
- 收藏
- 关注作者
评论(0)