事件驅動架構EDA
EDA組件
- 事件源/發起器(event emitters): 負責輪詢檢測事件狀態的變化
- 解複用器(Demultiplexer): 等待從事件源上獲取就緒事件的集合,並將就緒事件通過轉發器分發給響應就緒事件的處理器進行回調處理
- 事件處理引擎(event handlers): 響應就緒事件發生的處理程序,由開發人員在應用程序上進行定義並針對就緒事件發生的狀態進行註冊綁定
- 事件隊列(event queue): 或者稱爲事件通道,可以理解爲註冊綁定對應的事件存儲的位置,一旦就緒事件發生,解複用器就會從事件隊列中檢測並返回對應的就緒事件
EDA組件運作與設計
簡要流程
AWT完整事件流程
對於AWT事件的設計需要有客戶端,事件源(發生器),事件通道,事件處理器以及事件對象組件一起配合完成完整的點擊事件流程,基於監聽者模式的設計思路如下:
- 客戶端
// 需要註冊和綁定處理器
class Client{
public static void main(String[] args){
// 獲取事件源
Button button = new Button();
// 綁定事件源
button.bind("click", new ClickHandler());
// 執行點擊事件
button.click();
}
}
- 事件處理器定義
interface ActionHandler{
void handler(ActionEvent e);
}
class ClickListener implements ActionHandler {
// 類似於上述的handler,用於處理點擊事件的響應
@Override
public void handler(ActionEvent e) {
}
}
- 事件源定義
// 這個時候Button只是一個普通class,我們知道事件源需要具備檢測監聽的行爲,對此繼承監聽者的功能
class Button extends ActionListener {
// 這個時候click只是一個普通方法
void click(){
// 在上述事件流程可知,要讓click事件被傳播,需要藉助事件通道進行傳播執行回調,此時觸發監聽傳播
this.trigger("click");
}
// 事件源還需要具備綁定具體的動作行爲
void bind(String type, ActionHandler handler){
// 在上述的事件流程中,將其綁定到事件通道中
// 存儲哪個事件源 哪個事件行爲類型, 對應的處理動作
this.store(this, type, handler);
}
}
- 事件通道組件
// 作爲通道組件
class ActionListener {
// 定義map存儲事件類型以及對應的事件,作爲存儲事件的通道
private Map<String, ActionEvent> events = new HashMap<>();
public void store(Object source, String type, ActionHandler hander){
events.put(type, new ActionEvent(source, handler, type));
}
public void trigger(String type){
// 從事件通道中搜索事件,並回調執行事件
ActionEvent event = map.get(type);
Object target = event.getTarget();
Method callback = target.getClass(),getDeclaredMethod("handler", ActionEvent.class);
callback.invoke(target, event);
}
}
- 事件組件
// 通過上述的事件通道組件可知
class ActionEvent{
// 定義事件源
private Object source;
// 定義處理事件的目標處理器
private Object target;
// 定義事件的類型
private String eventType;
// ..others such as status, timestamp, id etc....
}
最後,關於編程設計的一個思考,就是在推導設計的時候,可以嘗試借用TDD的方式進行編程設計,先預先定義自己想要實現的效果,一步步從最簡單的目標效果思考逼近最終的設計,最後言歸正傳,通過上述的一個設計思路,我們接下來要思考如何實現一個IO事件驅動設計呢?對此,先從簡單的網絡NIO事件處理流程開始.
網絡NIO事件處理流程
對於web服務設計,主要處理服務端監聽連接並接收客戶端連接事件以及客戶端發起服務端讀取事件,這裏主要以服務端的設計爲準.
- 服務端監聽連接事件流程 – Accept事件流程
上述的Accept監聽是對服務端的ServerSocket進行連接事件的監聽.
- 服務端讀取事件流程 – 響應IO事件流程
在先前的Unix的IO模型中,真正進行IO操作的是調用recvfrom
方法產生阻塞,對於非阻塞IO是當內核真正接收到可操作的IO事件時候才發起recvfrom
方法,對此這裏的事件是指對客戶端socket的讀取事件進行監聽,在上述建立連接監聽的基礎上,事件讀取流程如下
上述是一個完整的IO事件連接與讀取流程,可以看出,最左邊的一個是事件處理器負責處理事件狀態發生變化的一個響應,而右邊的一側則是屬於處理網絡IO事件的監聽,此時所有的資源都阻塞該非阻塞IO的API調用,通過接收到就緒事件的通知由內核發起喚醒回調並返回就緒事件集合,然後傳輸給響應事件的處理器,於是也就有了Reactor反應器設計.
Reactor設計原理
Reactor運作流程
通過上述的NIO事件流程可知,對於web服務端而言,一個是我們需要關注IO輪詢就緒事件以及獲取就緒事件集合的操作,另一個是關注響應IO就緒事件的處理,主要包含連接的響應處理以及讀取請求處理的響應處理,可以從宏觀上,引入中間組件分別處理上述事件的輪詢監聽以及事件響應操作,在Reactor設計中,Reactor組件負責實現事件的輪詢監聽操作,Handler負責就緒事件的響應操作,對此,一個Reactor模式的簡要事件流程如下:
對此,一個Reactor模式的web服務設計實現需要兩個核心組件,即Handler以及Reactor,而一個Handler則需要拆分僞RequestHandler
以及Acceptor
兩個處理器,而一個Reactor組件中,參與的工作有註冊綁定操作,IO事件的監測以及就緒事件的轉發操作,同時也可以看到Reactor與系統內核之間都通過socket事件源來感知到事件狀態的變化,是系統內核與Reactor之間通信的一個重要渠道,即網絡設備接收到連接或者請求操作喚醒socket然後異步回調讓Reactor獲取CPU執行權,這個時候Reactor獲取到socket事件爲就緒事件.
Reactor組件具體實現
現理清Reactor整個事件流程之後,接下來要思考如何實現,先從一個服務端入口程序開始一步步往後推導.
- 服務端入口程序(也可以稱爲客戶端)
// 這裏使用java實現一個簡易版本的Reactor模式
class NIOReactorServer {
public static void main(String[] args){
// ServerSocketChannel 類似Button
ServerSocketChannel server = ServerSocketChannel.open();
server.bind(new InetSocketAddress(port), MaxBackLogs);
server.configurable(false);
// 類似於button.bind操作
Reactor reactor = new Reactor();
Handler request = new RequestHandler();
reactor.register(ACCEPT, server, new Acceptor(request));
// 類似於button.click();只不過這裏是處於阻塞等待事件
reactor.handle_events();
}
}
- 定義事件處理器
abstract class Handler {
public void acceptorHandler(SelectableChannel server){
// do nothing
}
public void requestHandler(SelectableChannel client){
// do nothing
}
}
// 服務端處理Acceptor服務
class Acceptor extends Handler{
private Handler handler;
public Acceptor(Hanlder handler){
this.handler = handler;
}
public void acceptorHandler(SelectionKey key){
// 需要從server中獲取客戶端的socket
Socket client = key.channel().accept();
// 重新註冊到reactor上
reactor.register(READ, client, handler);
}
}
// 服務端處理Request請求操作
class RequestHandler extends Handler {
public void requestHandler(Request req,Response resp){
// decode
// process
// encode
}
}
- Reactor組件
// 爲了保證實現的Reactor是通用的,這裏不使用java的NIO實現,僅用java僞代碼實現
class Reactor {
// 事件通道,在Java中是使用SelectionKey保存每個socket事件
private Map<SelectionKey, Invoker> acceptMap = new ConcurrentHashMap<>();
private Map<SelectionKey, Invoker> readMap = new ConcurrentHashMap<>();
private Selector demultiplexer;
public Reactor(){
this.demultiplexer = Selector.open();
}
public void register(String type,SelectableChannel socket, Handler handler){
// 向系統內核註冊socket事件並投遞到事件等待隊列中
if (ACCEPT.equals(type)){
socket.register(demultiplexer, SelectionKey.ACCEPT);
map.put(socket, new Invoker(ACCEPT,handler));
}else if(READ.equals(type)){
socket.register(demultiplexer, SelectionKey.READ, new Request());
map.put(socket, new Invoker(READ,handler));
}
}
public void handle_events(){
while(true){
Set<SelectionKey> readySet = demultiplexer.select();
Iterator<SelectionKey> it = readySet.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
it.remove();
if (acceptMap.keys().contains(key)){
dispatch(ACCEPT, key);
}else if(readMap.keys().contains(key)){
dispatch(READ, key);
}
}
readySet.clear();
}
}
public void dispatch(String type,Selection key){
Invoker invoker = null;
Method method = null;
if (ACCEPT == type){
invoker = acceptMap.get(key);
method = invoker.getCallbackMethod();
// method callback
}else if(READ == type){
invoker = readMap.get(key);
method = invoker.getCallbackMethod();
// read data ...
Request req = (Request)key.attachement();
req.setData(readData);
// using method callback (pass req and resp)
// write data ...
}
}
}
// 可以理解爲事件通道
class Invoker {
private Handler handler;
private Method method;
public Invoker(String type,Handler handler){
this.handler = handler;
if(ACCEPT == type){
this.method = this.handler.getClass().getDeclaredMethod("acceptHandler", SelectionKey.class);
}else if(READ == type){
this.method = this.handler.getClass().getDeclaredMethod("requestHandler", Request.class, Response.class);
}
}
public Handler getHandler(){
return this.handler;
}
public Method getCallbackMethod(){
return this.method;
}
}
通過上述部分僞代碼的設計實現,一個通用的NIO設計組件結構如下所示:
關於Reactor使用Java的NIO實現,後面講述netty的時候會更爲詳細,這裏主要是說明Reactor設計的實現思路,最後通過實現Reactor時序展示運作流程,以epoll/kqueue爲準,如果爲select,那麼圖中的第2步和下面的事件輪詢都是合併在同一步操作中
接下來我們可以來了解下IO事件驅動設計的異步實現原理,即Proactor模式實現
Proactor設計原理
在IO事件驅動設計實現,還有另一種實現模式,即Proactor模式,以網絡AIO模型爲基礎,面向IO事件編程的一種模式
AIO模型以及API
AIO使用的API
// 將處理請求入隊進行異步讀取
int aio_read(struct aiocb *aiocbp)
// 返回aio的處理結果
ssize_t aio_return(struct aiocb *aiocbp);
// 將處理請求入隊進行異步寫出
aio_write()
// 對一個IO同步操作入隊並通過異步的方式執行
aio_fsync()
// 返回入隊異步執行請求的錯誤
aio_error()
// 掛起調用者直到有一個或者多個就緒事件發生
aio_suspend()
// 嘗試取消IO操作
aio_cancel()
// 使用單個函數調用已入隊的多個IO請求
lio_listio()
// 攜帶的結構體
struct aiocb {
int aio_fildes; /* 文件描述符 */
off_t aio_offset; /* IO操作執行的文件位置 */
volatile void *aio_buf; /* 實現數據交換的buffer */
size_t aio_nbytes; /* buffer大小 */
int aio_reqprio; /* 執行請求的優先級 */
struct sigevent aio_sigevent; /* IO操作完成的時候進行異步回調通知的方法 */
int aio_lio_opcode; /* 操作類型,僅用於lio_listio*/
};
AIO模型
在Unix網絡IO模型中,AIO的工作原理是由應用進程定義好一個異步操作並通過aio_read
方法的調用告知內核啓動某個操作(異步操作)並在整個操作(等待數據+數據copy)完成之後通知應用進程,同時需要向內核傳遞文件描述符,緩衝區引用和其大小以及文件的偏移offset,並告知內核完成操作之後如何通知應用進程.
Proactor運作流程
通過上述的AIO模型分析,我們可以類比Proactor與Reactor實現模式,對於Proactor模式而言,只是使用的IO策略不同,因而在設計的實現細節也會有所不同,可以通過Reactor事件流程,我們可以推測Proactor模式的事件流程如下:
通過上述可以粗略看到Proactor模式與Reactor模式在設計思路上是基本一致,都是基於事件驅動設計實現,同時將Handler與關注的IO事件操作分離,開發者可以更加集中於Handler的業務實現邏輯,重要的區分在於Reactor依賴的是同步IO的複用器,Proactor依賴的是異步IO的複用器實現.同時Proactor的核心操作主要有註冊異步操作以及業務處理的Handler,異步接收完成操作的通知以及獲取就緒事件和對應的完成結果的集合,而Handler與Reactor模式基本一致.
Proactor組件具體實現
Proactor組件運作流程
Proactor組件參與者
- Handle: 可以理解爲事件源,在這裏表示網絡socket對象
- Completion Handler: 定義一系列接口模板方法用於處理異步操作完成的結果處理邏輯
- Proactor: 提供應用程序的事件循環,將完成事件分解爲相關的完成處理程序,並分派抽象模板方法來處理結果
- Asynchronous Event Demultiplexer: 異步多路複用器,阻塞等待添加到完成隊列中的完成事件,並將它們返回給調用者
- Completion Event Queue: 對等待多路複用器的完成事件進行緩衝,以便於完成事件的處理Handler能夠從隊列緩衝中獲取相應的completion event進行處理.
- 異步操作: 主要用於處理程序中長時間持續操作
- 異步處理器: 綁定在Handle上,負責對監聽到Handle執行進行回調喚醒對應的異步操作,生成對應的CompletionEvent並添加到事件的緩衝隊列中
- Initiator: 本地應用程序服務入口,初始化一個異步操作並註冊一個完成處理程序和一個帶有異步操作處理器的Proactor,當操作完成時通知它
具體實現
通過上述組件的協作流程以及Proactor的組件說明,對此,我們可以從主程序入口開始推導Proactor的實現流程.
- 主程序入口Initiator
// 用java僞代碼模擬
class NIOServer {
public static void main(String[] args){
// 保存異步操作產生的CompletionEvent
final Queue<CompletionEvent<Object>> completedEventQueue = new ConcurrentLinkedQueue<>();
// 初始化一個異步操作,提交給內核接收到事件變化執行異步操作
AsyncOperactionProcessor<Object> processor = new AsyncOperactionProcessor<>(){
public void asyncOprAccept(AsyncChannel channel, Handler<V,A> handler, Object attach){
// 操作完成之後創建CompletionEvent
CompletionEvent event = new CompletionEvent(channel, handler, attach);
// 新的連接
event.setResult(channel);
//添加到隊列中以便於處理CompletionHandler能夠進行處理
completedEventQueue.add(event);
}
public void asyncRead(AsyncChannel channel, Handler<V,A> handler, ByteBuffer result, Object attach){
// 操作完成之後創建CompletionEvent
CompletionEvent event = new CompletionEvent(channel, handler, attach);
// 請求操作
// 發起讀取操作
channel.read(result);
event.setResult(read.size());
}
// write opr ...
};
// 創建server
AsyncServerSocketChannel server = AsyncServerSocketChannel.open().bind(9999);
// 創建Proactor並註冊一個處理Accept的完成事件的Handler
Proactor proactor = new Proactor(server, processor, new Acceptor(server,processor));
// 監聽socket事件並處理socket事件
proactor.handle_events();
}
}
- 異步操作處理器
// 定義異步處理器接口
abstract class AsyncOperactionProcessor<A> {
public abstract void asyncAccept(AsyncChannel channel, Handler<V,A> handler, A attach);
public abstract void asyncRead(AsyncChannel channel, Handler<V,A> handler, ByteBuffer result, A attach);
// write opr ...
}
- Proactor組件
// 僞代碼實現Proactor組件
class Proactor {
private AsyncOperactionProcessor processor;
private Handler handler;
private AsyncServerSocketChannel server;
public Proactor(AsyncServerSocketChannel server, AsyncOperactionProcessor processor, Handler handler){
this.server = server;
this.processor = processor;
this.handler = handler;
}
public void handle_events(){
while(true){
// 接收客戶端新的請求
Future<Queue<CompletionEvent>> result = this.server.accept(this.processor, this.handler);
Queue<CompletionEvent> queue = result.get();
CompletionEvent event = null;
while((event = queue.pop()) != null){
dispatch(event);
}
}
}
public void dispatch(CompletionEvent event){
Handler handler = event.getHandler();
ParameterizedType type = event.getHandler().getClass().getGenericInterfaces();
Class<?> resultClass = (Class<?>) type.getActualTypeArguments()[0];
Class<?> attachClass = (Class<?>) type.getActualTypeArguments()[1];
Method method = event.getClass().getDeclaredMethod("completed", event.getResult(), attachClass);
method.invoke(handler, event.getResult(), event.getAttachment());
}
}
- CompletionEvent組件
class CompletionEvent<A> {
private AsyncChannel channel;
private Handle<V, A> handler;
private A attach;
public CompletionEvent(AsyncChannel channel, Handle<V, A> handler, A attach){
//....
}
}
- Handler組件
interface Handler<V, A> {
void completed(V result, A attach);
}
// Acceptor類實現Handler,處理對應的業務,即實現客戶端socket的註冊流程
class Acceptor<AsyncSocketChannel, Object> implements AcceptorHandler {
private AsyncServerChannel server;
private AsyncOperactionProcessor processor;
public Acceptor(AsyncServerChannel server, AsyncOperactionProcessor processor){
this.server = server;
this.processor = processor;
}
void completed(AsyncSocketChannel socketChannel, Object attch){
ByteBuffer buffer = ByteBuffer.allocate(MAX_SIZEs);
Handler reqHandler = new ReqHandler(this.processor, client, read);
// val根據業務自定義,可以定義爲存儲Session信息
// 註冊讀取事件並執行異步的讀取操作
socketChannel.read(this.processor, reqHandler, read, attch);
}
}
// RequestHandler類實現Handler,處理的對應業務,即處理decode-process-encode操作,同時註冊寫操作
class ReadHandler<Integer, Object> extends Handler{
private AsyncSocketChannel client;
private ByteBuffer read;
private AsyncOperactionProcessor<CompletionEvent> processor;
void completed(Integer buffSize, Object attch){
byte[] buffer = new byte[buffSize];
read.flip();
// Rewind the input buffer to read from the beginning
read.get(buffer);
// deocde
// process
// encode
// 註冊寫出事件,並以異步的方式執行寫出操作
Handler writeHandler = new WriteHandler(client);
ByteBuffer output = ByteBuffer.wrap(buffer);
client.write(this.processor, writeHandler, output, attch);
}
}
至此,Proactor核心組件都用java模擬出來,主要目的是爲了能夠更好地去理解Proactor模式,同時對於一個異步處理器以及其中的異步操作,可以將其綁定在對應的AsyncChannel上,由AsyncChannel去實現相應的異步操作就可以將上述的設計變得更爲簡單,不需要再傳遞對應的異步操作處理器processor,綁定在channel能夠直接傳遞到系統內核中,當有事件就緒的時候內核直接觸發異步操作然後喚醒到應用程序執行操作後的結果處理Handler.在Java的AIO使用的API是CompletionHandler
以及AsynchronousChannel
之間的協作.最後,基於上述的代碼實現,對一個通用的Proactor模式組件設計類圖如下:
Reactor&Proactor小結
Reactor模式與Proactor模式對比
相同點
- 均是基於事件驅動設計模式的解決方案來設計支持併發連接的web服務,指示如何在網絡IO環境中發起,接收就緒事件,解複用事件,分發以及執行不同類型的事件.
- 提供可重用以及可配置的解決方案和應用程序組件,通過組件分離不同事件的關注點,有助於針對相應的關注點進行調試和優化
不同點
- Reactor模式是基於同步多路複用器,使用的非阻塞同步IO的API協作完成,Proactor模式是基於異步多路複用器,使用的是異步IO的API協作完成,整個執行過程都是異步化.
- 對於異步讀取數據(從內核數據複製到用戶緩存區)是持續不間斷執行,因此會對內存空間的緩存區域造成很大的壓力,存儲的數據會越來越多,不知道數據什麼時候能夠被消費完成釋放空間,而Reactor模式屬於同步讀取,不存在對緩存空間的內存壓力.
- Reactor模式本質上是屬於同步操作,而Proactor是屬於異步操作,在先前的高性能IO中表述到,同步存在以下幾個問題,一個是同步在資源競爭環境下性能會比異步更差些,二是存在可伸縮性問題,Reactor模式是在原有的連接線程架構分離關注點優化,但是在處理有業務邏輯的相關處理時候仍然存在同步的移植以及伸縮問題,也就是對於併發連接的優化上去了,但是對於複雜的QPS仍然會是一個瓶頸.對於Proactor模式的異步操作,其運作效率依賴於內核執行效率,和操作系統有關,無法控制被調度的異步操作以及難以對程序進行調試排錯.
- Reactor模式是等待就緒事件發生然後依次順序處理就緒事件,Proactor模式是等待就緒事件完成處理完成之後的
Reactor&Proactor使用庫
- ACE框架: 提供Reactor以及Proactor模式實現,可以瞭解下UniPi項目,一個並行環境使用ACE的Reactor模式實現併發通信的分佈式程序.
- Boost.Asio庫: 基於Proactor模式提供同步與異步操作提供並行支持
- TProactor: 模擬Proactor
一個關於TProactor的性能分析對比如下:
最後關於Java相關NIO的API
https://docs.oracle.com/javase/7/docs/api/java/nio/package-summary.html
https://www.ibm.com/developerworks/java/library/j-nio2-1/index.html
https://www.javacodegeeks.com/2012/08/io-demystified.html