brpc 摘要
Cacheline
1.背景:cpu的L1和L2级cache为每个核独有,cpu的L3cache为所有核心共享
2.原因:核写入自己的L1级cache是极快的,但当另一核读写同一处内存时,由于每个核有local cache,需要进行一致性同步,确保内存和所有local cache的数据是一致的,这个复杂的硬件算法使得原子操作变得很慢
3.例子:所有线程频繁修改一个计数器,性能就会很差,让每个线程修改独立的thread_local变量,在需要时再同步
4.解决方法:避免共享,变量按访问规律排列,频繁修改的线程要放在独立的cacheline中
Memory fence
1.背景:cpu和编译器重排指令导致读写顺序的变化
Non-blocking wait-free& lock-free
1.Non-blocking:一个线程的失败或挂起不会导致其他线程的失败或挂起。 通过Non-blocking实现高并发
2.Non-blocking算法如果保证每个线程始终在做有用的事,那么就是wait-free,如果保证至少有一个线程在做有用的事,那就是lock-free
3.wait-free&lock-free并不保证高性能,因为需要处理更多更复杂的race condition和ABA problem
4.mutex:锁导致低性能的原因是临界区过大,或者上下文切换频繁
5.使用链表记录 处理多线程写的问题,通过swap(head)的方式,竞争写权限,如果返回null,则成功,否则swap(head)->next 。如果数据量较大,会启动keep Write写线程批量写出
Socket
1.Create/Address/SetFailed
2.Socket类似shared_ptr,SocketId类似于weak_ptr,SetFailed可以再需要时确保Socket不能被继续Address而最终引用计数归0.
3.存储SocketUniquePtr还是SocketId取决于是否需要强引用。如果需要大量数据交互,存放的是SocketUniquePtr。epoll主要提醒对应fd上发生了事件,如果socket回收了,那这个事件可有可无,所以存放SocketId
自适应限流
1.背景:服务能力有上限,请求速度超过处理速度,服务会过载。通过设置最大并发能直接拒绝一部分请求。
2.原因:当服务数量多,拓扑复杂,且处理能力会逐渐变化的局面下,固定最大并发数带了了巨大测试工作量
3.Little' Law: 服务稳定时:concurrency = qps*latency, noload_latency 单纯任务处理的延时,peak_qps是处理或回复的qps
3.解决方案:besk_max_concurrrency = noload_latency * peak_qps。 自适应限流就是要找到服务的noload_latency 和 peak_qps,并将最大并发设置为靠近两者乘积的一个值
max_concurrency = max_qps * ((2+alpha) * min_latency - lantency)。 不断对请求进行采样,通过窗口采样获得样本的平均延迟和当前qps, 计算出下一个窗口的max_concurrency。 通过定期降低max_concurrency
为避免流量损失太多的情况 1.请求数量足够多时,直接提交当前采样窗口 2.计算公式方面,当current_qps > max_qps时,直接进行更新,不进行平滑处理。 2秒左右将qps打满
雪崩
1.解决方法:1)设置合理的max_concurrency,最大qps*非拥塞时的延时来评估最大并发。2)注意重试设置。只有当连接错误时重试,而不是连接超时时重试。如果需要连接时重试,可以设置backup request,这种重试只有一次,放大程度降低到最低。3)brpc默认在bthread中处理请求,个数是软件限制
backup request
1.设置backup_request_ms,先发送请求,经过backup_request_ms后再发送一次请求,之后谁先返回取哪一个
bthread
单核reator,libevent等为典型,实质是把多段逻辑按事件触发顺序交织在一个系统线程中
N:1 线程库,所有协程运行于一个系统线程中,计算能力与各种eventloop库等价。 由于不跨线程,可以高效的利用 local cache, 无上下文切换代价,但不能高效的利用多核,代码必须非阻塞。 更适合写运行时间确定的IO服务器,典型如http server
多核reator,以boost::asio为典型,一般由一个或者多个线程分别运行event dispatcher,待事情发生后把event_handler交给一个worker线程。由于cache一致性,未必能获取线性与核心数的性能。
M:N 线程库,用户态的实现以go为主。一个bthread被卡住不会影响其他bthread,关键技术两点: work stealing调度和butex
lambda是一种语法糖,可以让异步编程没那么麻烦,但无法降低多线程编码难度
bvar
多线程环境下的计数器类库,方便记录和查看用户程序中各类数值,利用local cache减少了cache bouncing。本质是将写时的竞争转移到了读,读得合并所有写过的线程数据,不可避免的慢了
适合: 写多读少
不适合:基于最新值做逻辑判断,读多写多
方式:定期把监控项目打入固定目录下的文件
内存管理
内存管理都存在如下两点权衡
1.线程间竞争小,resource pool通过批量发送和归还避免全局竞争,并降低单次的开销
2.浪费的空间少,需要共享内存
多线程框架广泛地通过传递对象的ownership来让问题异步化,如何让分配这些小对象的开销变得更小是值得研究的问题:大多数结构是等长的
对象可以被归还,但归还后并没有被删除,也没有被析构,而是仅仅进入freelist
一个pool中所有的bthread的栈必须一样大
ABA问题
A->B->A 通过加入版本号解决
A1->B2->A3
id通过 (32位偏移量 + 32位版本)决定 ,偏移量相当于指针
DoublyBufferData
背景:读远多于写的数据结构。一个方法是双缓冲,实现无锁查找1)数据分前台和后台2)检索线程只读前台,不用加锁3)另一个写线程:修改后台数据,切换前后台,睡眠一段时间,以确保老前台不再被检索线程访问
解决方案:读拿一把thread-local锁,写需要拿所有的thread-local锁。
1.数据分前台和后台
2.读拿到自己所在线程的thread-local锁,执行查询逻辑后释放
3.同时只有一个写,修改后台数据,切换前后台,挨个获得所有thread-local锁并立刻释放,结束后再改一遍新后台(保证之前的读线程所在的thread-local已结束,即所有读线程都看到了新前台)
健康检查
只要一个节点不从NamingService删除,它要么是正常的,要么在做健康检查
cpu profile
在定期被调用的SIGPROF handler中采用所在线程的栈,cpu profiler在运行一段时间后能以很大概率采集到所有活跃线程中的活跃函数,最后根据栈代表的函数调用关系汇总为调用图,并把地址转为符号
heap profiler
每分配满一些内存就采样被调用处的栈