USTACK系统是怎么做到高性能的
前言
这东西写起来实际上并不简单,但是毕竟都做这个了,难免得研究研究。就好像评价一台电脑,不是单纯说显示器好,那这台电脑就无与伦比了。需要多个方面来评价,还要综合各种类别。
总述 USTACK是个什么系统
USTACK系统本质来说就是个综合Reactor和Proactor模式的服务器,以下几个方面的东西只针对TLS层面和系统层面,不能保证面面俱到,点列好以后,下面会一条条的说。
- 缓存 缓存包含两个层面,一个是数据层面:常用数据驻留内存,计算或者拷贝时直接利用,SSL证书和90x错误页面为典型例子。避免频繁的读取或者写入内存。另一方面是内存层面:常用数据结构缓存,或者以SLAB结构存取在系统内部,方便使用。每个CPU还绑核,减少缓存颠簸。
- 系统拆分 系统根据网络协议层或者任务功能划分成不同部分,比方说四层TLS层握手功能,和负载均衡服务器的选择分属不同模块。使用的时候,每个层面遵从最快服务原则,即尽快返回结果或者”在计算”状态。
- 计算/Verify任务拆分 系统尽量不去进行耗时过长的计算/verify任务,比方说对OCSP服务器的访问,对CLIENT cert的验证,都是放在了单独的进程CERTM里进行。CERTM进程的执行目前实际上还有问题。
- 计算层面简洁操作 这个计算层面简介操作实际指,采用最快的计算方式/trick减少自动机的状态转换,简化自动机流程。
- 尽量减少上下文切换 常见的上下文切换比方说用户态/内核态切换,中断上下文切换。USTACK系统两个层面都遇到了,用户态/内核态切换通过用户态DPDK使用。中断上下文切换的解决以SSL硬件卡支持为例,系统采用轮训方式去检查数据计算请求是否完成,从而将任务挪出队列。
- 尽量减少锁竞争 尽量减少锁竞争存在两个方面,每个线程尽量使用自己内部数据结构,如果必须要多线程同时操作,那么采用细粒度锁。前者例子是负载均衡模块的各种算法比方说RR,就是单线程内部检查。后者以TLS SESSION链表为例。
缓存层面
数据缓存层面比较直接,诸如证书OCSP_STAPLING结果之类的内容直接以M_BUF形式存储于vhost的数据结构中。
struct mbuf *ocsp_stapling[MAX_DOMAIN_NAMES+1];
struct mbuf *ocsp_stapling_ecc[MAX_DOMAIN_NAMES+1];
内存缓存就得看ATCP_ZONE的实现了,要考虑的问题是不同线程怎么申请释放,碰撞如何解决,还有SLAB着色问题如何解决。这部分不多说了,看ATCP ZONE设计就可得。
系统拆分
系统拆分是一个非常常见的方法,SSL层计算椭圆曲线点乘最慢,因此送卡之后就直接返回让系统去做别的事情。前面VS握手结束,选择后台RS,调用负载均衡模块进行算法选择。每次都以最快恢复为第一目标。
计算/Verify任务拆分
这部分以CERTM进程例子最为显著,无论访问OCSP服务器还是查询OCSP STAPLING消息亦或验证证书有效性,都会拆分出来,实现简单是一方面,另一方面一个进程出错不会干扰另一个进程,CERTM的CORE我也修过好几个了。目前CERTM还是功能太复杂,仍需要简化。
尽量减少上下文切换
这个以SSL card交互为例子最合适,SSL卡频繁中断会打断当前进程的上下文,干扰进程执行。最终我们决定通过采用轮训方式检测卡是否运转完成。enqueue请求时会将item的回调函数赋值为ssl_hw_out
,然后把请求扔到per atcp的队列里。然后大循环会每次检测每个命令是不是需要poll_out,调用顺序为clicktcpintr_real
==>ssl_card_poll_smp();
==>ssl_card_poll_generic
==>SslHw_PollResult
尽量减少锁竞争
锁竞争以SESSION CACHE为立即比较合适,锁共#define SSL_SC_LOCK_GRAIN_SIZE ATCP_MAXTHREADS
个,每个SESSION CACHE和session_id一样#define SESSION_CACHE_KEY(sp) (*((uint32_t *)((sp)->session_id)))
。哈希表同样由LOCK保护,只不过此时找的锁稍微大一些#define SSL_SC_KEY(sp) ((SESSION_CACHE_KEY(sp) & SSL_SC_HASH_SIZE ) % SSL_SC_LOCK_GRAIN_SIZE)
,然后把新的HASH值插入hash表。如果L4_atcp数量少于锁的数量,实际上碰撞会继续减少。
下面看看负载均衡的链表,
结尾
唉,尴尬