https://github.com/oknet/atsinternals/blob/master/CH01-EventSystem/CH01S00-Index.zh.md
事件驱动的引擎(Event Driven Engine)是 Apache Traffic Server 的核心组件之一。该引擎通过事件(Event)与外部进行交互。
为了实现对事件的并行处理,在该引擎内有多个事件处理线程(EThread),这些线程由全局唯一实例(eventProcessor)统一创建和管理。
https://docs.trafficserver.apache.org/en/6.2.x/developer-guide/plugins/introduction.en.html
Traffic Server provides special event-driven mechanisms for efficiently scheduling work: the event system and continuations.
The event system is used to schedule work to be done on threads. 事件系统是用来调度任务的
A continuation is a passive, event-driven state machine that can do some work until it reaches a waiting point; it then sleeps until it receives notification that conditions are right for doing more work.
continuation是一个被动的,事件驱动的状态机。 执行---等待----执行---等待。。。。
For example, HTTP state machines (which handle HTTP transactions) are implemented as continuations.
https://github.com/oknet/atsinternals/blob/master/CH01-EventSystem/CH01S01-Engine-and-SM.zh.md
事件(Event)可以通过三类API(EventProcessor,EThread,Event)放入这个引擎:
- EventProcessor
- 创建一个 Event,将状态机(SM)包含进去
- 然后,按照轮询算法从引擎里找到一个 EThread
- 把 Event 交给这个 EThread 处理
- EThread
- 直接把一个 Event 交给指定的 EThread 来处理
- Event
- 把 Event 交回原来的 EThread 重新处理
引擎根据事件的类型执行不同的策略。
通过调用Event->Cont->handleEvent来驱动状态机(SM),状态机就可以获得CPU资源来完成当前状态的工作,然后进入下一个状态。
状态机在工作时,一旦遇到阻塞的情况,就要暂停运行,并迅速返回到引擎,等待下一次被引擎驱动的时候再继续之前未完成的任务。
状态机在运行时必须是无阻塞的,凡是遇到阻塞时必须返回到引擎。
- EventProcessor 把 Event 放入 EThread 中处理,然后根据 Event 的指示回调“状态机”
ProxyMutex 是在 Continuation 和 Thread 中使用的互斥锁。
在整个事件系统中(Event System),ProxyMutex 是最基本的同步对象。
ProxyMutex 通过ink_mutex来实现互斥锁的功能,这样就没有特定平台的函数调用的负担。
ProxyMutex 也有一个指针,指回到当前持有锁的 EThread,用于验证它是否正确的被释放。
- 一般来说 Continuation 的派生类就是状态机,Continuation是所有状态机接收事件通知的基类。
Action,Event,VC 等都封装 Continuation 数据结构。
以下部分来自源代码中注释的简单翻译:
- Continuation 用来完成 IOCore EventSystem 向它的使用者传递事件的功能。
- Continuation 是一个通用类,可以用于实现事件驱动的状态机。
- 通过派生类,扩展出更多状态和方法,Continuation 可以将状态与控制流结合。通常这样的设计用于支持分阶段的,由事件驱动的控制流程。
#define CONTINUATION_EVENT_NONE 0
#define CONTINUATION_DONE 0
#define CONTINUATION_CONT 1
typedef int (Continuation::*ContinuationHandler)(int event, void *data);
class Continuation: private force_VFPT_to_top
{
public:
/**
当前的 Continuation 处理函数
在对 mutex 成员上锁之后,使用 SET_HANDLER 宏进行设置。
不要直接对该成员进行操作。
*/
ContinuationHandler handler;
/**
当前 Continuation 对象的锁
通过构造函数完成初始化,不要直接对该成员进行操作。
*/
Ptr<ProxyMutex> mutex;
/**
链接到其他Continuation的双向链表
在需要创建一个队列用来保存 Continuation 对象时使用。
*/
LINK(Continuation, link);
/**
接收来自 Event 回调的事件代码和事件数据
接收事件代码和事件数据并且透传给 handler。
回调 Continuation 的 Processor 负责对 Continuation 的 mutex 上锁。
@param event 由 Processor 指定,在回调时传入的事件代码
@param data 由 Processor 指定,与事件代码相关的数据
@return 状态机和 Processor 指定的返回值(代码)
*/
int handleEvent(int event = CONTINUATION_EVENT_NONE, void *data = 0) {
return (this->*handler) (event, data);
}
Continuation(ProxyMutex * amutex = NULL);
};
inline Continuation::Continuation(ProxyMutex *amutex)
: handler(NULL), mutex(amutex)
{
}
#define SET_HANDLER(_h) (handler = ((ContinuationHandler)_h))
#define SET_CONTINUATION_HANDLER(_c, _h) (_c->handler = ((ContinuationHandler)_h))
延续(Continuation)是一个轻型数据结构,
- 它的实现只有一个用于回调的方法
int handleEvent(int , void *)- 该方法是面向 Processor 和 EThread 的一个接口。
- 它可以被继承,以添加额外的状态和方法
- 可以通过提供
ContinuationHandler(成员handler的类型)来决定延续的行为- 该函数在事件到达时,由 handleEvent 调用。
- 可以通过以下方法来设置/改变(头文件内定义了两个宏):
- SET_HANDLER(_h)
- SET_CONTINUATION_HANDLER(_c, _h)
在 ATS 中,我们不会直接创建 Continuation 类型的对象,而是创建其继承类的实例,因此也就不存在直接与之对应的 ClassAllocator 对象。
ProxyMutex 对象
鉴于事件系统的多线程特性,每个 Continuation 都带有一个对 ProxyMutex 对象的引用,以保护其状态,并确保原子操作。
因此,在创建任何一个 Continuation 对象或由其派生的对象时:
- 通常由使用 EventSystem 的客户来创建 ProxyMutex 对象
- 然后在创建 Continuation 对象时,将 ProxyMutex 对象作为参数传递给 Continuation 类的构造函数。
TSAPI
Continuation 在 Plugin API 中叫做 TSCont,是插件开发中最常用到的类型之一,是多数 API 要求的参数类型。
ClassAllocator是一个C++的类模版,继承自Allocator类。
ATS中使用到的各种数据结构,特别是需要在EventSystem与各种Processor之间进行传递的数据结构,都需要分配内存。
对于ATS这种服务器系统,每一个会话进来的时候动态的分配内存,会话结束后释放内存,这种对内存的操作频率是非常高的。
而且在会话存续期间,对于各种类会有各种不同尺寸的内存空间被分配和释放,内存的碎片化也非常严重。
所有的数据结构都使用Allocator类来分配,不符合面向对象的设计习惯,因此ATS设计了ClassAllocator模版。
ATS的Allocator是ATS内部使用的一种内存分配管理技术,主要用来减少内存分配次数,从而提高性能。
ProxyAllocator是线程访问ClassAllocator的缓存,看名字就知道,这个类是访问ClassAllocator的一个Proxy实现。
这个类本身并不负责分配和释放内存,而只是维护了上层ClassAllocator的一个可用内存块的列表。
ProxyAllocator
- 该结构维护一个可用“小块内存”的链表 freelist
- 和一个表示该链表内有多少元素的计数器 allocated
ATS是一个多线程的系统,Allocator和ClassAllocator是全局数据,那么多线程访问全局数据,必然要加锁和解锁,这样效率就会比较低。
于是ATS设计了一个ProxyAllocator,它是每一个Thread内部的数据结构,它将Thread对ClassAllocator全局数据的访问进行了一个Proxy操作
- Thread请求内存时调用THREAD_ALLOC方法(其实是一个宏定义),判断ProxyAllocator里是否有可用的内存块
- 如果没有
- 调用对应的ClassAllocator的alloc方法申请一块内存
- 此操作将从大块的内存中取出一个未分配的小块内存
- 如果有
- 直接从ProxyAllocator里拿一个,freelist指向下一个可用内存块,allocated--
- 如果没有
- Thread释放内存时调用THREAD_FREE方法(其实是一个宏定义)
- 直接把该内存地址放入ProxyAllocator里freelist里,allocated++
- 注意:此时,这块内存并未被在ClassAllocator里标记为可分配状态
从以上过程我们看出来,ProxyAllocator就是不断的将全局空间据为己有。
那么会出现一种比较恶劣的情况
- 某个Thread由于在执行特殊操作时,将大量的全局空间据为己有
- 导致了Thread间内存分配不均衡
对于这种情况,ATS也做了处理
- 参数:thread_freelist_low_watermark 和 thread_freelist_high_watermark
- 在THREAD_FREE内部,会做一个判断:如果ProxyAllocator的freelist持有的内存片超过了High Watermark值
- 就调用thread_freeup方法
- 从freelist的头部删除连续的数个节点,直到freelist只剩下Low Watermark个节点
- 调用ClassAllocator的free或者free_bulk,将从freelist里删除的节点标记为可分配内存块
- 就调用thread_freeup方法
总结一下
- 凡是直接操作ProxyAllocator的内部元素freelist
- 都是不需要加锁和解锁的,因为那是Thread内部数据
- 但是需要ClassAllocator介入的
- 都是需要加锁和解锁的
- 当ProxyAllocator的freelist指向NULL时
- 当allocated大于thread_freelist_high_watermark时
- 通过ProxyAllocator
- Thread在访问全局内存池的资源时,可以有较少的资源锁冲突
核心部件:Action & Event
- 当一个 状态机 通过某个 Processor方法 发起一个异步操作时, Processor 将返回一个 Action 类的指针。
- 通过一个指向 Action 对象的指针, 状态机 可以取消正在进行中的异步操作。
- 在取消之后, 发起该操作的 状态机 将不会接收到来自该异步操作的回调。
- 对于Event System中的Processor,还有整个IO核心库公开的方法/函数来说, Action或其派生类是一种常见的返回类型。
Event继承自Action,首先看一下Action类
class Action
{
public:
Continuation * continuation;
Ptr<ProxyMutex> mutex;
// 防止编译器缓存该变量的值, 在 64bits 平台, 对该值的 读取 或 设置 是原子的
volatile int cancelled;
// 可由继承类重写, 实现继承类中对应的处理
// 作为 Action 对外部提供的唯一接口
virtual void cancel(Continuation * c = NULL) {
if (!cancelled)
cancelled = true;
}
// 此方法总是直接对 Action 基类设置取消操作, 跳过继承类的取消流程
// 在 ATS 代码内, 此方法为 Event 对象专用
void cancel_action(Continuation * c = NULL) {
if (!cancelled)
cancelled = true;
}
// 重载赋值(=)操作
// 用于初始化 Action
// acont 为操作完成时回调的状态机
// mutex 为上述状态机的锁, 采用 Ptr<> 自动指针管理
Continuation *operator =(Continuation * acont)
{
continuation = acont;
if (acont)
mutex = acont->mutex;
else
mutex = 0;
return acont;
}
// 构造函数
// 初始化continuation为NULL,cancelled为false
Action():continuation(NULL), cancelled(false) {
}
virtual ~ Action() {
}
};
Processor 方法实现者:
在实现一个 Processor 的方法时, 必须确保:
- 在操作被取消之后,不会有事件发送给状态机。
返回一个Action:
Processor 方法通常是异步执行的,因此必须返回Action,这样状态机才能在任务完成前随时取消该任务。
- 此时, 状态机总是先获得 Action,
- 然后才会收到该任务的回调,
- 在收到回调之前, 随时可以通过 Action 取消该任务。
由于某些Processor的方法是可以同步执行的(可重入的),因此可能会出现先回调状态机, 再向状态机返回Action的情况。 此时返回Action是毫无意义的, 为了处理这种情况,返回特殊的几个值来代替Action对象,以指示状态机该动作已经完成。
- ACTION_RESULT_DONE 该Processor已经完成了任务,并内嵌(同步)回调了状态机
- ACTION_RESULT_INLINE 当前未使用
- ACTION_RESULT_IO_ERROR 当前未使用
也许会出现这样一种更复杂的问题:
- 当结果为ACTION_RESULT_DONE
- 同时,状态机在同步回调中释放了自身
因此,状态机的实现者必须:
- 同步回调时, 不要释放自身(不容易判断出回调的类型是同步还是异步) 或者,
- 立即检查 Processor 方法返回的 Action
- 如果该值为ACTION_RESULT_DONE,那么就不能对状态机的任何状态变量进行读或写。
无论使用哪种方式,都要对返回值进行检查(是否为ACTION_RESULT_DONE),同时进行相应的处理。
分配/释放策略:
Action的分配和释放遵循以下策略:
- Action由执行它的Processor进行分配。
- 通常 Processor 方法会创建一个Task状态机来异步执行某个特定任务
- 而 Action 对象则是该Task状态机的一个成员对象
- 在Action完成或者被取消后,Processor有责任和义务来释放它。
- 当 Task状态机 需要回调 状态机 时,
- 通过 Action 获得 mutex 并对其上锁
- 然后检查 Action 的成员 cancelled
- 如已经 cancelled, 则销毁 Task状态机
- 否则回调 Action.continuation
- 当 Task状态机 需要回调 状态机 时,
- 当返回的Action已经完成,或者状态机对一个Action执行了取消操作,
- 状态机就不可以再访问该Action。
核心部件:Event
Event类继承自Action类, 它是EventProcessor返回的专用Action类型,它作为调度操作的结果由EventProcessor返回。
不同于Action的异步操作,Event是不可重入的。
- EventProcessor 总是返回 Event 对象给状态机,
- 然后, 状态机才会收到回调。
- 不会像 Action类 的返回者, 可能存在同步回调 状态机 的情形。
除了能够取消事件(因为它是一个动作),你也可以在收到它的回调之后重新对它进行调度。
class Event : public Action
{
public:
// 设置事件(Event)类型的方法
void schedule_imm(int callback_event = EVENT_IMMEDIATE);
void schedule_at(ink_hrtime atimeout_at, int callback_event = EVENT_INTERVAL);
void schedule_in(ink_hrtime atimeout_in, int callback_event = EVENT_INTERVAL);
void schedule_every(ink_hrtime aperiod, int callback_event = EVENT_INTERVAL);
// inherited from Action
// Continuation * continuation;
// Ptr<ProxyMutex> mutex;
// volatile int cancelled;
// virtual void cancel(Continuation * c = NULL);
// 处理此Event的ethread指针,在ethread处理此Event之前填充(就是在schedule时)。
// 当一个 Event 由一个 EThread 管理后, 就无法在转交给其它 EThread 管理。
EThread *ethread;
// 状态及标志位
unsigned int in_the_prot_queue:1;
unsigned int in_the_priority_queue:1;
unsigned int immediate:1;
unsigned int globally_allocated:1;
unsigned int in_heap:4;
// 向Cont->handler传递的事件(Event)类型
int callback_event;
// 组合构成四种事件(Event)类型
ink_hrtime timeout_at;
ink_hrtime period;
// 在回调Cont->handler时作为数据(Data)传递
void *cookie;
// 构造函数
Event();
// 初始化一个Event
Event *init(Continuation * c, ink_hrtime atimeout_at = 0, ink_hrtime aperiod = 0);
// 释放Event
void free();
private:
// use the fast allocators
void *operator new(size_t size);
// prevent unauthorized copies (Not implemented)
Event(const Event &);
Event & operator =(const Event &);
public:
LINK(Event, link);
virtual ~Event() {}
};
TS_INLINE Event *
Event::init(Continuation *c, ink_hrtime atimeout_at, ink_hrtime aperiod)
{
continuation = c;
timeout_at = atimeout_at;
period = aperiod;
immediate = !period && !atimeout_at;
cancelled = false;
return this;
}
TS_INLINE void
Event::free()
{
mutex = NULL;
eventAllocator.free(this);
}
TS_INLINE
Event::Event()
: ethread(0), in_the_prot_queue(false), in_the_priority_queue(false),
immediate(false), globally_allocated(true), in_heap(false),
timeout_at(0), period(0)
{
}
// Event 的内存分配不对空间进行bzero()操作, 因此在 Event::init() 方法中会初始化所有必要的值
#define EVENT_ALLOC(_a, _t) THREAD_ALLOC(_a, _t)
#define EVENT_FREE(_p, _a, _t) \
_p->mutex = NULL; \
if (_p->globally_allocated) \
::_a.free(_p); \
else \
THREAD_FREE(_p, _a, _t)
取消Event的规则
与下面这些对于Action的规则是一样的
- Event的取消者必须是该任务将要回调的状态机,同时在取消的过程中需要持有状态机的锁
- 任何在该状态机持有的对于该Event对象(例如:指针)的引用,在取消操作之后都不得继续使用
使用
创建一个Event实例,有两种方式
- 全局分配
- Event *e = ::eventAllocator.alloc();
- 默认情况都是通过全局方式分配的,因为分配内存时还不确认要交给哪一个线程来处理。
- 构造函数初始化globally_allocated(true)
- 这样就需要全局锁
- 本地分配
- Event *e = EVENT_ALLOC(eventAllocator, this);
- 如果预先知道这个Event一定会交给当前线程来处理,那么就可以采用本地分配的方法
- 调用EThread::schedule_*_local()方法时,会修改globally_allocated=false
- 不会影响全局锁,效率更高
放入EventSystem
- 根据轮询规则选择下一个线程,然后将Event放入选择的线程
- eventProcessor.schedule(e->init(cont, timeout, period));
- EThread::schedule(e->init(cont, timeout, period));
- 放入当前线程
- e->schedule_*();
- this_ethread()->schedule_*_local(e);
- 只能在e->ethread==this_ethread的时候使用
释放Event
- 全局分配
- eventAllocator.free(e);
- ::eventAllocator.free(e);
- 在ATS的代码里能看到上面两种写法,我理解两种是一个意思,因为只有一个全局的eventAllocator
- 自动判断
- EVENT_FREE(e, eventAllocator, t);
- 根据e->globally_allocated来判断
重新调度Event
状态机在收到来自 EThread 的回调后, void *data 指向触发此次回调的 Event 对象。 简单的进行类型转换后, 可以调用 e->schedule_*() 将此 Event 重新放入当前线程。 在重新调度后, Event 的类型将会被 schedule_*() 方法重新设置。
核心部件:Thread & EThread
ATS可以创建两种类型的EThread线程:
- DEDICATED类型
- 该类型的线程,在执行完某个延续后,就消亡了
- 换句话说,这个类型只处理/执行一个Event,在创建这种类型的EThread时就要传入Event。
- 或者是处理一个独立的任务,例如NetAccept的延续就是通过此种类型来执行的。
- REGULAR类型
- 该类型的线程,在ATS启动后,就一直存在着,它的线程执行函数:execute()是一个死循环
- 该类型的线程维护了一个事件池,当事件池不为空时,线程就从事件池中取出事件(Event),同时执行事件(Event)封装的延续(Continuation)
- 当需要调度某个线程执行某个延续(Continuation)时,通过EventProcessor将一个延续(Continuation)封装成一个事件(Event)并将其加入到线程的事件池中
- 这个类型是EventSystem的核心,它会处理很多的Event,而且可以从外部传入Event,然后让这个EThread处理/执行Event。
- MONITOR类型
- 实际上还有第三种类型,但是MONITOR类型当前未使用到
- 也许在Yahoo开源时被删除了
在创建一个EThread实例之前就要决定它的类型,创建之后就不能更改了。
事件池/队列的类型
为了处理事件,每个EThread实例实际有四个事件队列:
- 外部队列(EventQueueExternal被声明为EThread的成员,是一个保护队列)
- 让EThread的调用者,能够追加事件到该特定的线程
- 由于它同时可以被其它线程访问,所以对它的操作必须保持原子性
- 本地队列(外部队列的子队列)
- 外部队列是满足原子操作的,可以批量导入本地队列
- 本地队列只能被当前线程访问,不支持原子操作
- 内部队列(EventQueue被声明为EThread的成员,是一个优先级队列)
- 在另一方面,专门用于由EThread处理在一定时间框架内的定时事件
- 这些事件在内部被排队
- 也可能包含来自外部队列的事件(本地队列内符合条件的事件会进入到内部队列)
- 隐性队列(NegativeQueue被声明为execute()函数的局部变量)
- 由于是局部变量,不可被外部直接操作,所以称之为隐性队列
- 通常只有epoll_wait的事件出现在这个队列里
接口界面:EventProcessor
EventProcessor负责启动EThread,进行每一个EThread的初始化。
事件系统的主线程组(eventProcessor),是事件系统的核心组成部分。
- 启动后,它负责创建和管理线程组,线程组中的线程则周期性的或者在指定的时间执行用户自定义的异步任务。
- 通过它提供的一组调度函数,你可以让一个线程来回调指定的延续。这些函数调用不会阻塞。
- 他们返回一个Event对象,并安排在 稍后 或 特定时间之后 或 尽快 或 以一定的时间间隔 回调延续。
唯一实例模式
- 每一个可执行的操作都是通过全局实例eventProcessor提供给EThread组
- 没有必要创建重复的EventProcessor实例,因此EventProcessor被设计为唯一实例模式
- 注意:EventProcessor的任何函数/方法都是不可重入的
线程组(事件类型):
- 当EventProcessor开始时,第一组诞生的线程被分配了特定的id:ET_CALL
- 根据不同的状态机或协议的复杂性,你一定需要创建额外的线程和EventProcessor,你可以选择:
- 通过spawn_thread创建一个独立的线程
- 该线程是完全独立于线程组之外的
- 在延续执行结束之前,它都不会退出
- 该线程有自己的事件系统
- 通过spawn_event_theads创建一个线程组
- 你会得到一个id或Event Type
- 你可以在将来通过该id或者Event Type在该组线程上调度延续
- 通过spawn_thread创建一个独立的线程
调度方式:
- schedule_imm
- 在特定的线程组上调度该延续,等待事件或超时。
- 请求EventProcessor立即调度对于此延续的回调,该回调由指定的线程组(event type)里的线程处理。
- schedule_imm_signal
- 与schedule_imm相似的功能,只是在最后还要向线程发送信号。
- 这个函数其实是专为网络模块设计的
- 一个线程可能一直阻塞在epoll_wait上,通过引入一个pipe或者eventfd,当调度一个线程执行某个event时,异步通知该线程从epoll_wait解放出来
- 另外一个具有相同功能的函数 EThread::schedule_imm_signal
- schedule_at
- 请求EventProcessor在指定的时间(绝对时间)调度对于此延续的回调,该回调由指定的线程组(event type)里的线程处理。
- schedule_in
- 请求EventProcessor在指定时间内(相对时间)调度对于此延续的回调,该回调由指定的线程组(event type)里的线程处理。
- schedule_every
- 请求EventProcessor在每隔指定的时间调度对于此延续的回调,该回调由指定的线程组(event type)里的线程处理。
- 如果间隔时间为负数,则表示此回调为随时执行,只要有机会,EventProcessor就会进行调度。
- 目前此种类型的调度只有epoll_wait一种
与 Event,EThread 事件调度方法的对比:
event
EventProcessor::schedule_*(cont, etype) ----------------> EThread[etype][RoundRobin]->QUEUE
event
EThread::schedule_*(cont) ----------------> this->QUEUE
(self)
Event::schedule_*() ----------------> this->ethread->QUEUE
浙公网安备 33010602011771号