??xml version="1.0" encoding="utf-8" standalone="yes"?>
]]>
LT一般用在单U程?/span>
ET和EPOLLONESHOT配合用在多线E共享一个epoll环境下,EPOLLONESHOT标记触发q的事g从epoll中移除,下次必须重新注册Q用来防止多U程同时取到同一个socket的事件生冲H?/span>
epoll_wait W三个参?取事件数量:
单线E模型当然尽可能一ơ多取一些效率高Q多U程Z防止一个线E把所有事件取完其他线E饥饿,ACE实现是只?个?/span>
错误处理Q?/span>
EAGIN | EINTR | EWOULDBLOCK 重试?/span>
EPOLLERR | EPOLLHUP | EPOLLRDHUP 断开q接?/span>
惊群Q?/span>
默认pȝ都会有这问题Q据说新pȝ有修复不q还是处理一下比较好Q一般解x案是同时只有一个线E等待acceptQ可以单独线EacceptQ将q接在分l其他工作线E。nginx是多q程模型Q用了Z׃n内存的互斥锁Q得同时只有一个工作进E的epoll含有accept的socketQ通过q种方式实现q接C的负载均衡(q接数少的工作进E得到accept锁的概率高)?br />
Z避免大数据量ioӞet模式下只处理一个fd,其他fd被饿ȝ情况发生。linux可以在fd联系到的l构Q一般都会自己封装一个包含fd和读写缓冲的l构体)中增加ready位,然后epoll_wait触发事g之后仅将其置位ؓready模式Q然后在下边轮询ready fd列表?/span>
epoll实现Q?
epoll内部用了一个红黑树记录d的socketQ用了一个双向链表接收内核触发的事g?/span>
注册的事件挂载在U黑树中(U黑树的插入旉效率是logNQ其中n为树的高??/span>
挂蝲的事件会与设?|卡)驱动建立回调关系Q也是_当相应的事g发生时会调用q个回调Ҏ。这个回调方法在内核中叫ep_poll_callback,它会发生的事gd到rdlist双链表中?/span>
使用mmap映射内存Q减内核态和用户态的不同内存地址I间拯开销?/span>每次注册新的事g到epoll中时Q会把fd拯q内核,通过内核于用L间mmap同一块内?/span>保证了只会拷贝一ơ。(q回的时候不需要拷贝,select要)
执行epoll_ctlӞ除了把socket攑ֈU黑树上Q还会给内核中断处理E序注册一个回调函敎ͼ告诉内核Q如果这个句柄的中断CQ就把它攑ֈ准备qAlist链表里。所以,当一个socket上有数据CQ内核在把网卡上的数据copy到内怸后就把socket插入到准备就l链表里了。链表又是通过mmap映射的空_所以在传递给用户E序的时候不需要复Ӟq也是ؓ什么比select效率高的原因Qepoll_waitq回的只是就l队列,不需要轮?不需要复制完成的事g列表QselectQpoll实现需要自׃断轮询所有fd集合Q直到设备就l)?/span>
epoll_wait最后会查socketQ如果是 LTQƈ且这些socket上确实有未处理的事gӞ又把该句柄放回到刚刚清空的准备就l链表了QLT比ET低效的原因)?/span>
可见Q如果没有大量的I闲Q无效连接,epoll效率不比select高?/span>
试数据Q仅是刚接触go的时候好奇做的参考意义的试Q:
同样的环境,echo服务器测q发ioQ单U程epoll qps:45000左右Q每q接/协程 goQ?50000多,多线EepollQ开6个epollQ每个epoll开8U程Q一?8U程Q:qps 70000多?/span>
慢启动:一开始指数的增加拥塞窗口,C个门阀值后变成U性的Q?之后每次时都把门阀值降低到原来一半(q且rtod,TCP时计算是RTOx2Q这栯l丢三次包就变成RTOx8了,十分恐?/span>Q,拥塞H口讄?重新开始慢启动Q指数增加Q。一切都是ؓ了让路由器有旉处理U压的缓册Ӏ(所以不适用于频J断开q接的移动网l,q也是ؓ什么以前的下蝲工具开多条tcp传输速度更快的原因)?/span>
Nagle法Q第一ơ(此时没有{待ack认Q空闲连接)发送小包成功,W二ơl发?Q哪怕发送窗口,拥塞H口都很大,之前的包没有ack认依旧不让发直到收C前的 ack认?/span>
shutdown和close的区?
close只是递减引用计数Q?shutdown的半关闭会媄响所有的q程?/span>
dpdk是通过许多不同的纬度来加速包处理的,其中主要包括Q?/span>
hugepage大页内存(q程使用的是虚拟地址Q一般页?4k)能映的虚拟地址I间有限Q用大能减少换页ơ数提高cache命中Q通过mmap把大|到用户态的虚拟地址I间有用qmmap的都知道q是实现׃n内存的手D,所以dpdkq支持多q程׃n内存)
cache预取 (每次预读当前数据盔R前后的数?Q批量操作数据,cache line寚w(通过费一点内存将要操作的数据寚w)
接管了网卡用h驱动用轮询而不是网卡中?/span>
网卡rx tx队列映射到用h空间实现真正的零拷贝(传统堆栈臛_也得一ơ拷贝,因ؓ队列I间在内核而内核和用户态用不同的地址I间Q?传统堆栈Z支持通用性,例如ipx{其他网l,包处理q程分了很多层次Q层之间的接口标准统一数据l构需要{换,无Ş中带来了巨大的成本,如osi七层模型而实用的是tcp/ip四层模型)
U程l定cpu
支持NUMAQ不同的core属于不同的nodeQ每个node有自qmempool减少冲突
无锁环Ş队列(冲突发生时也是一ơcas的开销)
dpdk通过tools/dpdk-setup.sh的脚本,通过~译、挂载内核模块, l定|卡Q先把网卡ifconfig downQ,讄hugepage后就可以使用了?/span>
在内核模块igb加蝲Ӟ会注册pci讑֤驱动
static struct pci_driver igbuio_pci_driver = {
.name = "igb_uio",
.id_table = NULL,
.probe = igbuio_pci_probe,
.remove = igbuio_pci_remove,
};
在绑定网卡时Q会调用igbuio_pci_probeQ用用h驱动uio接管|卡(中断处理、mmap映射讑֤内存到用L?
pȝ启动Ӟbios会将讑֤ȝ地址信息记录?sys/bus/pci/devicesQdpdkE序启动时会去这里扫描pci讑֤Q根据不同类型的NIC有对应的初始化流E。在后面配置队列的时候会把用h的队列内存地址通过g指o交给NICQ从而实现零拯?/span>
如果NIC收到包,会做标记Q轮询的时候通过标记取数据包
while (nb_rx < nb_pkts) {
/*
* The order of operations here is important as the DD status
* bit must not be read after any other descriptor fields.
* rx_ring and rxdp are pointing to volatile data so the order
* of accesses cannot be reordered by the compiler. If they were
* not volatile, they could be reordered which could lead to
* using invalid descriptor fields when read from rxd.
*/
rxdp = &rx_ring[rx_id];
staterr = rxdp->wb.upper.status_error;
if (! (staterr & rte_cpu_to_le_32(E1000_RXD_STAT_DD)))
break;
rxd = *rxdp;
发包的轮询就是轮询发包结束的g标志位,g发包完成会写回标志位Q驱动发现后再释攑֯应的描述W和~冲块?/span>
KNI
通过创徏一个虚拟网卡,收到的包丢l协议栈
/* 发送skb到协议栈 */
/* Call netif interface */
netif_receive_skb(skb);
POWER
在负载小的时候没有必要用轮询模式,q时可以打开|卡中断 使用eventfd epoll通知用户?/span>
Ring
无锁环Ş队列的核心就是操作头引,先将头尾索引赋给临时变量Q再把尾索引往后蟩n个位|,利用cas判断头如果还是在原来的位|就指向则就重复q个q程Q然后在操作中间跌的n个元素就是安全的了,此时头尾索引应该指向同一个位|,如果不同应该是有别的U程也在操作Q重复等待即可?q里有个l节Q烦引是只加不减的,因ؓ是环形队列烦引又是unsigned 32bitsQ所以每ơ取数据前把索引模队列长?1Q?uint32_t mask; /**< Mask (size-1) of ring. */卛_)
Windows下用vmware虚拟机的时候出现EAL: Error reading from file descriptorQ根据网上的说法打了patchq是不行Q后来尝试挂载内核模块的时候不加蝲vfio模块可以了
Redis是工作中很常用的Q这里将比较普遍使用的结构研I了下做个备忘?/span>
hash
实现和dnspod的dataset半斤八两Q本质上是个二维数组Q通过key哈希作ؓ一l的下表Q第二维的数l存相同哈希的元素,查找使用遍历的方式,所以这里redis做了优化Q当满条g的时?数组数量太大)会进行rehashQ动态扩大桶的数量来减少最后一l遍历的ơ数.
函数名称 | 作用 | 复杂?/span> |
dictCreate | 创徏一个新字典 | O(1) |
dictResize | 重新规划字典的大?/span> | O(1) |
dictExpand | 扩展字典 | O(1) |
dictRehash | 对字典进行N步渐q式Rehash | O(N) |
_dictRehashStep | 对字典进?步尝试Rehash | O(N) |
dictAdd | d一个元?/span> | O(1) |
dictReplace | 替换l定key的value?/span> | O(1) |
dictDelete | 删除一个元?/span> | O(N) |
dictRelease | 释放字典 | O(1) |
dictFind | 查找一个元?/span> | O(N) |
dictFetchValue | 通过key查找value | O(N) |
dictGetRandomKey | 随机q回字典中一个元?/span> | O(1) |
字典l构
typedef struct dict {
// cd特定函数
dictType *type;
// U有数据
void *privdata;
// 哈希?/span>
dictht ht[2];
// rehash 索引
// ?rehash 不在q行Ӟgؓ -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在q行的安全P代器的数?/span>
int iterators; /* number of iterators currently running */
} dict;
q里哈希表有两个Q一般都用ht[0]Q当需要rehash的时候会创徏一个比ht[0]大的 2 ?N ơ方的ht[1]Q然后渐q式的将数据dictEntryU过?除了定时的rehashQ在每次操作哈希表时都会_dictRehashStep)Q完成后ht[1]替换ht[0]
zset
zset本质是list,只不q每个元素都有若q个指向后span长的指针Q这L单的设计大大提高了效率,使得可以比拟q二叉树,查找、删除、插入等操作都可以在Ҏ期望旉内完成,Ҏq树,跌表的实现要简单直观很多?/span>
/* ZSETs use a specialized version of Skiplists */
/*
* 跌表节?/span>
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分?/span>
double score;
// 后退指针
struct zskiplistNode *backward;
// ?/span>
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
/*
* 跌?/span>
*/
typedef struct zskiplist {
// 表头节点和表节?/span>
struct zskiplistNode *header, *tail;
// 表中节点的数?/span>
unsigned long length;
// 表中层数最大的节点的层?/span>
int level;
} zskiplist;
/*
* 有序集合
*/
typedef struct zset {
// 字典Q键为成员,gؓ分?/span>
// 用于支持 O(1) 复杂度的按成员取分值操?/span>
dict *dict;
// 跌表,按分值排序成?/span>
// 用于支持q_复杂度ؓ O(log N) 的按分值定位成员操?/span>
// 以及范围操作
zskiplist *zsl;
} zset;
虽然q种方式排序查找很快Q但是修改的话就得多做些工作?/span>
/* Delete an element with matching score/object from the skiplist.
*
* 从蟩跃表 zsl 中删除包含给定节?score q且带有指定对象 obj 的节炏V?/span>
*
* T_wrost = O(N^2), T_avg = O(N log N)
*/
int zslDelete(zskiplist *zsl, double score, robj *obj)
intset
typedef struct intset {
uint32_t encoding; //所使用cd的长度,4\8\16
uint32_t length; //元素个数
int8_t contents[]; //保存元素的数l?/span>
} intset;
intset其实是数组Q有序、无重复C存多个整数|查找用的是二分查?* T = O(log N)Q添加的话在扑ֈ对应的数l中应该存在的位子后使用memmove向后UdIZ填补(当然需要先realloc预分配空?Q同理删除也是用memmove向前Ud
set
当用整数时Q用intsetQ否则用哈希表
其他的关于网l事件处理,epollQ回调,拆包都和正常使用差不多,关于错误处理EINTR(pȝ调用期间发生中断)和EAGAIN l箋重试而如果是EPOLLHUP或EPOLLERR则让io该读读该写写Q有错处理就是了?/span>
dns的递归解析q程q是挺繁琐的Q要知道一个域名可能有cname、ns 而请求的cname、ns可能q有cname、nsQ如果按照线性的处理每个h那逻辑变成毛U团?/span>
dnspod的处理还是挺巧妙的,通过一个公q数据集dataset所有域名对应的a、cname、ns{类型的数据作ؓ单独的条目存入,当有需要某个域名的信息时先去dataset找,找不到在加入qlisth根,有专门的U程不间断的qlist轮询dataset找(q里只要ơ数允许Q没得到惌的结果就轮询所有qlist到dataset找虽然可以简化逻辑分离的彻底但是会是个性能瓉Q后面有ҎQ当根返回以后只是简单的记?通常是一个域名的cname、ns或者a)存入dataset(而不是l流E,因ؓҎq个q回是cnameq是ns或者a处理不同逻辑复杂Q而这样处理对于用到相同域名的hq有优化作用)Q剩下的工作交给那边不间断轮询的U程
Dnspod主要?个runQ若q个U程Q组?/span>
run_sentinel 监听53端口接收客户端请求,请求放到队列中
run_fetcher 从队列中取出hQ根据qname取得最后一UcnameQ查看本地dataset 是否有记录,如果有则q回Q没有则该h攑օqlist?/span>
run_quizzer
1.不间断的遍历qlistQ只要状态ؓPROCESS_QUERY?/span>dataset中没有的向对应的根发送请求?/span>
2.通过epoll{待根返回,解析q回的数据加?dataset
3.查记录的ttlQ在记录加入dataset时还会将q些记录以红黑树的Ş式组lv来,取得ttl最早到期的Q将其放入qlist中等待刷斎ͼ注意q里不是删除Q如果收不到不返回则该记录一直存?/span>
关于dataset的实?/span>
dataset是用哈希表实现的,本质上是个二l数l,域名哈希成一个|模上数组的数量作Z标,扑ֈ对应的数l接着遍历查找Q根据需要可以扩大数l的数量提升性能?/span>
我们的优化手D?/span>
之前提到dnspod的qlist会不间断轮询Q属于主动查询,Ҏ能有不的影响Q这里我们采取的做法是被?cM回调的方?Q我们将h的域名和cd分类Q相同的攑֜一l,当dataset找不到向根发求后我们q不每次d轮询Q而是在等到应{后Q触发该域名和类型的hl,让他们根据自q逻辑C一步(一般是先找该域名的最后一UcnameQ根据这个cname查是否存在他的对应请求类型的记录Q一般是a或者nsQ如果没有,则找q个cname的nsQ?/span>
以上可以看出dataset很重要,负蝲也不,q经帔R要ƈ发访问,q里我们每次接收到根的回复后Q除了将记录的答案加qdatasetQ还创徏一个时的datasetQ只存该ơ回复的信息Q在后面的流E会优先到这里去找,没有的再找dataset?/span>
最q听朋友?.7 debug下数提?00多,挺惊讶的Q重新到久违了的官网上下?.7.1
看了下渲染的实现(GL)
首先Q添加了GeometryBuffer玩意Q得每个window保存了属于自q点和纹理信?/p>
然后在RenderingSurface中有GeometryBuffer队列Q得每个拥有AutoRenderingSurface属性的window有属于自q队列(默认只有FrameWindow才有)
而在drawself中执行的则是先通过looknfeelQ把需要渲染的信息丢到每个部g自己的GeometryBuffer里,然后把GeometryBuffer丢到RenderingSurface的队列中(一般ؓ
FrameWindow的GeometryBuffer队列Q每个面板就有自q渲染队列?
要知道以往都是只有一个队列的Q要渲染啥直接往里塞???br>q样一改就不必每个部件有更改都要全部重新清空渲染?/p>
再往后就是把每个H口队列里的GeometryBuffer渲染到各自的RenderingSurface表面上,q里要注意的是ƈ不是渲染到屏q上而是表面上,cegui在这里用了渲染到纹理,GL
用的是fbo实现的?/p>
注意RenderingSurface只有两个来源Q一是通过讄AutoRenderingSurface属性,另一个就是RenderingRoot了,RenderingRoot只有一个,在render中,通过W一个来源的?/p>
用的是fbo的渲染,而第二个来源则直接渲染到屏幕了?/p>
所有的q些执行完后可以渲染到屏幕了,通过RenderingRoot执行Q注意这里的RenderingRoot中的RenderTarget和之前的不一Pq里用的是OpenGLViewportTarget而不?/p>
OpenGLFBOTextureTarget?br>