MySQL Proxy - 底层实现篇
【 Configfile and Commandline Options】
glib2 提供了 config-file 解析和 command-line option 解析功能。 其提供了将 option 以相同方式暴露给调用者的方法,以及从 Configfile 和 Commandline 获取 option 的功能。
所有 option 的解析过程都可以分为三步:
1. 提取 command-line 上的 basic option
- --help
- --version
- --defaults-file
2. 处理 defaults-file 文件
3. 处理其余 command-line option 并覆盖 defaults-file 文件中的相同内容
【 Plugin Interface 】
chassis 为 plugin 接口调用提供了基础结构。值得注意的是,其不是专门用于 MySQL 的,而是可以用于任何符合其接口要求的 plugin 。提供的功能包括:
- 解析 plugin 所在路径
- 对 plugin 的加载
- 对 plugin 进行版本检查
- 提供 init 和 shutdown 函数
- 向 plugin 暴露配置选项
- 基于线程的 i/o
由于 chassis 不是仅针对于 MySQL 设计的,所以其可以用于加载任何种类的 plugin ,只要该 plugin 提供了符合 chassis 要求的 init 和 shutdown 函数。
就 MySQL Proxy 本身而言,一般情况下加载的 plugin 为:
- plugin-proxy
- plugin-admin
【 Threaded IO 】
从 MySQL Proxy 0.8 版本开始,已经添加了基于线程的 network-io 以使 proxy 能够按照可用 CPU 和网卡的数量进行线性扩展。
使能 network-threading 功能只需要在启动 proxy 时加入下面的参数:
1 |
--event-threads={2 * no-of-cores} (default: 0) |
每一个 event-thread 都通过 "event_base_dispatch()" 进行 loop ,并针对 network-event 或者 time-event 执行相关函数。这些线程只具有两种状态:执行函数状态和 idle 状态。如果其处于 idle 状态,则其能够从 event-queue 中获取要进行等待的新 event ,然后将其添加到自身的等待列表中。
connection 是可以在多个 event-thread 之间“跳跃”的:因为只要是 idle 状态的 event-thread 就能够获取到 wait-for-event request - 即具体的事件 - 并进行等待,触发后执行相关代码。无论何时,只要当前 connection 需要重新等待事件(也就是之前事件所对应的操作已经完成),其就会将自身从所在线程中 unregister ,之后重新向全局 event-queue 发送 wait-for-event request 以获取新事件。
一直到 MySQL Proxy 0.8 版本,脚本代码的执行都是单线程方式:通过一个全局 mutex 来保护 plugin 的接口操作。因为 connection 或者是处于发送包的状态,或者是处于调用 plugin 函数的状态,所以网络事件将会按照并行方式被处理,仅在多个 connection 需要调用同一个 plugin 函数的时候才会无法并行。
chassis_event_thread_loop() 函数就是 event-thread 的主循环实体(其中调用 event_base_dispatch() 函数),而函数 chassis_event_threads_init_thread() 用于设置要监听的事件和对应的回调。
下面的描述的是一种典型控制流(不包含连接池的情况)
涉及到的实体:EventRequestQueue, MainThread, WorkerThread1, WorkerThread2;
--- [ label = "Accepting new connection "]; MainThread -> MainThread [ label = "network_mysqld_con_accept()" ]; MainThread -> MainThread [ label = "network_mysqld_con_handle()" ]; MainThread -> EventRequestQueue [ label = "Add wait-for-event request" ]; WorkerThread1 <- EventRequestQueue [ label = "Retrieve Event request" ]; WorkerThread1 -> WorkerThread1 [ label = "event_base_dispatch()" ]; ...; WorkerThread1 -> WorkerThread1 [ label = "network_mysqld_con_handle()" ]; WorkerThread1 -> EventRequestQueue [ label = "Add wait-for-event request" ]; WorkerThread2 <- EventRequestQueue [ label = "Retrieve Event request" ]; WorkerThread2 -> WorkerThread2 [ label = "event_base_dispatch()" ]; ...; WorkerThread2 -> WorkerThread2 [ label = "network_mysqld_con_handle()" ]; WorkerThread2 -> EventRequestQueue [ label = "Add wait-for-event request" ]; ...;
在上面的例子中,存在两个用于处理 event 的工作线程(设置 --event-threads=2 ),每个线程都有自己的 event_base 。以 Proxy plugin 为例,首先将 network_mysqld_con_accept() 函数设置为被监听 socket 的回调,当有新连接发生时被触发。该回调函数是注册在主线程的 event_base 上的(同时也是全局 chassis 的 event_base)。在设置了连接相关结构 network_mysqld_con 后,程序将进入到状态机处理函数 network_mysqld_con_handle() 中,此时仍然处于主线程中。
状态机将进行入起始状态:CON_STATE_INIT ,在当前代码实现中该状态是主线程所必进入的第一个状态。接下来 MySQL Proxy 要做的事,要么是和 client 交互,要么是和 server 进行交互(即或者等待 socket 可读,或者主动向 backend server 建立连接),而状态机函数 network_mysqld_con_handle() 将设置等待处理事件(对应结构体为 chassis_event_op_t)。简单来说就是将 event 结构添加到异步队列中,具体讲,就是通过向之前创建的 wakeup-pipe 的写文件描述符写入一个字节,以产生一个文件描述符事件。这样就可以向所有线程通知有新事件请求需要处理。
该 pipe 的实现是 libevent 对应实现的一个翻版,其将各种事件与基于文件描述符的 event-handler 建立了对应关系,采用的轮询方式进行处理:
- 工作线程中的 event_base_dispatch() 函数在其监听的 fd 被触发前处于阻塞监听状态(在具体实现中是有定时唤醒机制的)。
- 定时器事件,信号事件等都不能直接中断 event_base_dispatch() 的运行。
- 上述事件均是通过 write(pipe_fd, ".", 1); 来触发 fd-event 的可读,从而通过回调来进行处理。
在文件 chassis-event-thread.c 中可以看到,通过 pipe 实现了向工作线程通知:在全局 event-queue 中有东东需要处理。从函数 chassis_event_handle() 可以看出,所有处于 idle 状态的线程都有平等机会进行事件处理,所以这些线程就能够“并行的”从全局事件队列中拉取 event ,并将其添加到自身的监听事件列表中。
通过调用 chassis_event_add() 或者 chassis_event_add_local() 函数可以将 event 添加到 event-queue 中。一般情况下,所有事件都由全局 event_base 负责处理。只有在使用 connection pool 的情况下,才会强制将与特定 server connection 对应的 events 投递到特定线程,即将当前 connection 加入到 connection pool 中的那个线程。
如果 event 被投递到全局 event_base 中,那么不同的线程都可以获取这个事件,并可以对无保护的 connection pool 数据结构进行修改,可能会导致竞争冒险和崩溃。令这个内部数据结构成为具有线程安全性质是 0.9 release 版本的工作,当前只提供了最小限度的线程安全性。
典型情况是,某个线程会从 event queue 中获取 request 信息(理论上,发送 wait request 的线程很可能也是处理这个 request 的线程),并将其添加到自身以 thread-local-store 方式保存的 event_base 中,并在对应 fd 有事件触发时获得通知。
该处理过程将一直持续到当前 connection 被 client 或者 server 关闭,或者发生了导致的 socket 关闭的网络错误。此后将无法处理任何新的 request 。
单独一个线程就足以处理任何添加到其 thread-local 的 event_base 上面的 event 。只有在一个新的 blocking I/O 操作发生时(一般来说也就是重新进入 event_base_dispatch() 阻塞时),event 才会在不同线程间被“跳跃着”处理,除此外没有其他例外。所以理论上讲,可能会出现一个线程处理了所有活跃的 socket 事件,而另一个线程一直处于 idle 状态。
然而,由于等待网络事件的发生的状态是常态(意思就是实际处理的速度都很快),所以(从概率上讲)活跃 connection 在所有线程中的分布必定是很均匀的,也就会减轻单个线程处理活跃 connection 的压力。
值得注意的是,尽管在下面的说明中没有具体指出,主线程当前会在 accept 状态后参与到对后续 event 的处理中。这不是一个非常理想的实现方式,因为所有 accept 动作本身就需要在主线程中完成。但从另一方面讲,这个问题暂时也没成为实际工作中的瓶颈显现出来:
涉及到的实体:Plugin, MainThread, MainThreadEventBase, EventRequestQueue, WorkerThread1, WorkerThread1EventBase, WorkerThread2, WorkerThread2EventBase;
--- [ label = "Accepting new connection "]; Plugin -> MainThread [ label = "network_mysqld_con_accept()" ]; MainThread -> MainThread [ label = "network_mysqld_con_handle()" ]; MainThread -> EventRequestQueue [ label = "Add wait-for-event request" ]; WorkerThread1 <- EventRequestQueue [ label = "Retrieve Event request" ]; WorkerThread1 -> WorkerThread1EventBase [ label = "Wait for event on local event base" ]; ...; WorkerThread1EventBase >> WorkerThread1 [ label = "Process event" ]; WorkerThread1 -> EventRequestQueue [ label = "Add wait-for-event request" ]; WorkerThread2 <- EventRequestQueue [ label = "Retrieve Event request" ]; WorkerThread2 -> WorkerThread2EventBase [ label = "Wait for event on local event base" ]; ...; WorkerThread2EventBase >> WorkerThread2 [ label = "Process event" ]; WorkerThread2 -> EventRequestQueue [ label = "Add wait-for-event request" ]; ...;