要控制和降低延迟,首先要能准确测量延迟,因此需要比较准的钟,每个机房配几个带GPS和/或原子钟primary standard的NTP服务器是少不了的。而且就算用了NTP,同一机房两台机器的时间也会有毫秒级的差异,计算延迟的时候,两台机器的时间戳不能直接相减,因为不在同一时钟域。解决办法是设法补偿这个时差。另外,不仅要测量平均延迟,更重要的是要测量并控制长尾延迟,即99百分位数或99.9百分位数的延迟,就算是sell side,系统偶尔慢一下被speculator利用了也是要亏钱的。
普通的C++服务程序,内部延迟(从进程收到消息到进程发出消息)做到几百微秒(即亚毫秒级)是不需要特殊的努力的。没什么忌讳,该怎么写就怎么写,不犯低级错误就行。我很纳闷国内流传的写 C++ 服务程序时的那些“讲究”是怎么来的(而且还不是 latency critical 的服务程序)。如果瓶颈在CPU,那么最有效的优化方式是“强度消减”,即不在于怎么做得快,而在于怎么做得少。哪些可以不用做,哪些可以不提前做,哪些做一次就可以缓存起来用一阵子,这些都是值得考虑的。
网络延迟分传输延迟和惯性延迟,通常局域网内以后者为主,广域网以前者为主。前者是传送1字节消息的基本延迟,大致跟距离成正比,千兆局域网单程是近百微秒,伦敦到纽约是几十毫秒。这个延迟受物理定律限制,优化办法是买更好的网络设备和租更短的线路(或者想办法把光速调大,据说 Jeff Dean 干过)。惯性延迟跟消息大小成正比,跟网络带宽成反比,千兆网TCP有效带宽按115MB/s估算,那么发送1150字节的消息从第1个字节离开本机网卡到第1150个字节离开本机网卡至少需要 10us,这是无法降低的,因此必要的话可以减小消息长度。举例来说,要发10k的消息,先花20us CPU时间,压缩到3k,接收端再花10us解压缩,一共“60us+传输延迟”,这比直接发送10k消息花“100us+传输延迟”要快一点点。(广域网是否也适用这个办法取决于带宽和延迟的大小,不难估算的。)
延迟和吞吐量是矛盾的,通常吞吐量上去了延迟也会跟着飚上去,因此控制负载是控制延迟的重要手段。延迟跟吞吐量的关系通常是个U型曲线,吞吐量接近0的时候延迟反而比较高,因为系统比较“冷”;吞吐量上去一些,平均延迟会降到正常水平,这时系统是“温”的;吞吐量再上去一些,延迟缓慢上升,系统是“热”的;吞吐量过了某个临界点,延迟开始飙升,系统是“烫”的,还可能“冒烟”。因此要做的是把吞吐量控制在“温”和“热”的范围,不要“烫”,也不要太冷。系统启动之后要“预热”。
延迟和资源使用率是矛盾的,做高吞吐的服务程序,恨不得把CPU和IO都跑满,资源都用完。而低延迟的服务程序的资源占用率通常低得可怜,让人认为闲着没干什么事,可以再“加码”,要抵住这种压力。就算系统到了前面说的“发烫”的程度,其资源使用率也远没有到 100%。实际上平时资源使用率低是为了准备应付突发请求,请求或消息一来就可以立刻得到处理,尽量少排队,“排队”就意味着等待,等待就意味着长延迟。消除等待是最直接有效的降低延迟的办法,靠的就是富裕的容量。有时候队列的长度也可以作为系统的性能指标,而不仅仅是CPU使用率和网络带宽使用率。另外,队列也可能是隐式的,比如操作系统和网络设备的网络输入输出 buffer 也算是队列。
延迟和可靠传输也是矛盾的,TCP做到可靠传输的办法是超时重传,一旦发生重传,几百毫秒的延迟就搭进去了,因此保持网络随时畅通,避免拥塞也是控制延迟的必要手段。要注意不要让batch job抢serving job的带宽,比方说把服务器上的日志文件拷到备份存储,这件事不要在繁忙交易时段做。QoS也是办法;或者布两套网,每台机器两个网口,两个IP。
最后,设法保证关键服务进程的资源充裕,避免侵占(主要是CPU和网络带宽)。比如把服务器的日志文件拷到别的机器会占用网络带宽,一个办法是慢速拷贝,写个程序,故意降低拷贝速度,每50毫秒拷贝50kB,这样用时间换带宽。还可以先压缩再拷贝,比如gzip压缩100MB的服务器日志文件需要1秒,在生产服务器上会短期占满1个core的CPU资源,可能造成延迟波动。可以考虑写个慢速压缩的程序,每100毫秒压缩100kB,花一分半钟压缩完100MB数据,分散了CPU资源使用,减少对延迟的影响。千万不要为了加快压缩速度,采用多线程并发的办法,这就喧宾夺主了。(文章来源:宽客界微信公众号quantview)