------------------------
总体设计
------------------------
---------------
结构:
---------------
多线程。三类线程:
主线程(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分析-主从复制实现"里详细描述了其中一种协议的格式。