当前位置: 首页 > 工具软件 > Tyrant > 使用案例 >

tokyo tyrant源码分析-总体设计

易俊远
2023-12-01

 

------------------------

总体设计

------------------------

---------------

结构:

---------------

多线程。三类线程:

 

主线程(1个):

监听socket,将接收到的请求sockfd分发给工作线程,以及信号处理。

worker线程(thnum个,参数指定,默认为8):

从主线程得到请求sockfd,处理请求(二进制、HTTP、memcache协议)

timer线程(最多8个):

do_slave (1个):

更新从库。定期(1s)向master请求更新数据日志,用来更新自己的数据库

do_extpc (参数指定,默认0个):

script extension. 提供对脚本和一些命令的支持

 

线程间联系:

主线程通过全局queue把accept到的请求sockfd传递给worker线程;

主线程捕捉信号,设置对应的全局变量。worker线程和timer线程周期性的检查全局变量来获取通知。

----------------------

实现的简单代码描述

(所有代码中错误处理均已被过滤)

----------------------

主线程:

--------

 

解释:

前面省略的部分检查参数有效性, <script src="http://hi.images.csdn.net/js/blog/tiny_mce/themes/advanced/langs/zh.js" type="text/javascript"></script> <script src="http://hi.images.csdn.net/js/blog/tiny_mce/plugins/syntaxhl/langs/zh.js" type="text/javascript"></script> 函数注册(do_slave ...),deamonize等;

信号处理:

SIGINT、SIGTERM做相同处理,设置serv->term = true,这样其它线程检查到该变量的变化后就退出。(信号捕捉函数不直接处理退出而只是设置全局变量,是一种多用的技巧。好处除了保持捕捉函数简洁,还能提供更好的灵活性:线程实现可以选择只在自己"愿意"的地方才检查该全局变量进而做相应处理,保证一些不能被打断的流程的完整性);

SIGHUP一是设置serv->term = true,让所有线程退出;二是设置g_restart = true,让while循环再次执行,启动所有其它线程。除了重启线程,它还重新打开日志文件。(tyrant没配置文件);

SIGPIPE忽略;

SIGCHLD捕捉,但捕捉函数什么也不做。

 

ttservstart创建worker线程和timer线程,统一accept用户请求并用全局queue将它们分发给worker线程:

解释:

创建监听socket:本地使用UNIX域协议(只需本机通信时使用。通过指定参数 -port 为一个小于1的数,比如0)以获得更好的性能,远端使用TCP协议;

创建thnum个worker线程和若干个timer线程 (pthread_create);

在循环中等待请求的到来。通过线程共享的数据结构queue来分配请求任务,queue中放置的是请求sockfd,访问前加互斥锁,push任务后用信号通知worker线程;

一次完整的处理后才检查是否收到信号(while(!serv->term);

主线程退出前要广播信号通知worker线程和timer线程是因为:

SIGTERM、SIGINT捕捉函数只是设置serv->term为true;

这时worker线程可能正在pthread_cond_timedwait等待serv->qcnd,(200ms),即正等待主线程给它分配任务;

timer线程是定期执行的(1s),它现在可能正在周期里。

虽然它们都可以在超时后(200ms,1s)发现到serv->term被设置进而退出,但这样的等待一段时间显然并不是最好。这里直接广播信号让它们马上退出阻塞函数。

 

 

这就是主线程的结构,很简单

 

 

 

---------------

worker线程:

---------------

worker从全局queue取请求sockfd并处理它们:

解释:

首先禁止线程结束请求,一次流程后才打开测试。也就是,不允许在流程中终止线程,不然会带来不完整性;

从全局queue取任务fd流程:

给全局queue加上互斥锁

如果上次成功取到fd(empty == false,即上次取时queue不为空),这次直接取

否则等待主线程发信号。不管是收到信号还是超时,都试着去从queue取fd

如果成功取到fd,do task

如果失败,设置标志(empty = true,这次取时queue为空),表示这次没取到fd

解锁

对fd读写的处理用TTSOCK做了封装,它底层一次从fd读取大块数据放入内部buffer(减少网络请求次数),但对上层提供一次读一个字节、一次读指定数目字节的API,方便上层开发。在"tyrant分析-编程小技巧"里有对它的详细描述。

 

补充说明:

原则上无碍对"总体设计"理解的代码全部去除,这里保留(1)是想说明,tyrant里大量使用了这种线程编程技术:开始时disable掉其它线程可能的取消该线程的请求,等一次流程结束后再打开测试是否有请求。目的是保护流程的完整性。

tyrant大量使用的另一个线程编程技术是在对某个资源的处理前pthread_cleanup_push资源释放函数,处理完后再pop。它相当于面向对象里"析构"的意思,确保资源的释放。这里没列出对应的代码,以后的"总体设计"里也都不会列出这些技术细节。

 

do_task分析请求的格式,根据请求协议和请求命令做相应处理:

解释:

do_task就是做三种协议的用户请求处理:

tt自己的二进制协议:第一个字节为TTMAGICNUM(0xc8)

memcache协议:   "get|set|add|... ..."

http协议:             "GET|PUT|DELETE|... HTTP/1.x ..."

三种协议只是格式不同,提供的服务一致:

get:  得到请求key对应的value

put:  先更新日志(ulog),再更新db

...

当然,http,memcache只是"外部"协议,tyrant只对它们提供一些"对外"服务,如get,put等。tyrant自己的"内部"二进制协议则支持更多的命令,比如主从复制(do_repl)。

get、put等底层实现是属于cabinet部分,会在"cabinet分析"部分详细描述

传输数据格式是tt自定义的,学习的意义不大,格式也很简单,比如它一般的格式是"ksiz + vsiz + key + value",其中没有'+',是紧挨的,因为siz长度是知道的,所以可以解析。当然还有一些有细微差别的格式。"tyrant分析-主从复制实现"里详细描述了其中一种协议的格式。

 

-------------
timer线程:
-------------
ttservtimer提供"定时执行"功能,它按timers提供的频率定期调用它们,这样timers只需要专注实现自己的功能:
解释:
(1)将timer函数的频率时间分割出整数和小数部分,并加上当前时间,作为等待超时时间。do_slave的频率是1秒。
(2)只有在一种情况下才会收到信号tcnd,那是在主线程退出时。这时不执行do_timed <script src="http://hi.images.csdn.net/js/blog/tiny_mce/themes/advanced/langs/zh.js" type="text/javascript"></script> <script src="http://hi.images.csdn.net/js/blog/tiny_mce/plugins/syntaxhl/langs/zh.js" type="text/javascript"></script> ,直接进入下个循环,检查到serv->term为true(被信号捕捉函数设置),退出。
---------------------
timer之do_slave:
---------------------
do_slave用于slave端,它和master建立socket连接,请求更新内容并用之更新自己,实现主从复制:
解释:
slave把上次更新到的时间rts放在文件里。每次更新就是从文件取rts,和master建立连接,循环请求到数据并用它们更新自己的ulog和db,然后更新rts文件。 master中负责处理请求的是do_task函数中的do_repl
slave-master的具体实现请参见"tyrant分析-主从复制实现"。

 

 类似资料: