构建高并发低延时系统
并发指系统的吞吐率,在单位时间内可以处理多少请求;延时是从单个请求的角度来看,需要多长时间才能处理完成,这两个指标经常用来衡量一个系统的性能
降低延时的手段
一个请求从客户端发出,服务器接受请求,然后交给相应的线程处理,最后返回。降低延时也就是从这个过程来考虑(不考虑具体业务逻辑的优化):
采用多线程或多进程
降低延时主要思想就是减少处理请求的等待时间,一个自然的想法就是采用多进程或多线程,这样一个请求过来就不必等待前面请求处理完成才能开始处理;
多线程方面
一般采用线程池来减少线程创建/销毁带来的额外开销。使用多线程的优点是,线程间通信的代价较低;缺点是一个线程出问题可能会影响整个进程,可以创建线程的个数也是有限的
多进程方面
进程是系统资源调度单位,他独享进程内所有的资源,单个进程出问题不会影响整个系统;但是进程间同步,共享数据则比线程要复杂。
尽量避免竞争
竞争也增加了等待时间,所以要尽量减少竞争。但有的时候,竞争不可避免,譬如多个请求要同时访问数据库中同一张表,这个时候我们就要仔细设计竞争的资源,尽可能的减少竞争的粒度。譬如,我们若要更改同一张表,我们可以按照请求的属性来分表。
提高IO效率
IO相对来讲是比较慢的,特别是网络IO,针对网络IO,可以采用IO复用或异步IO来提高效率
IO复用
Linux下IO模型有阻塞/非阻塞IO,IO复用和异步IO,前三者都属于同步IO。阻塞IO会等待IO操作完成才返回,否则就阻塞在那里;非阻塞IO若数据为准备好则会返回失败,需要调用着不断重试直到成功。系统要尽快的相应客户端的请求就要使用IO复用。
IO复用有select,poll,epoll。select要遍历所有监听的描述符来确定哪个可操作,当描述符过多时,性能有比较大的影响;而且每次调用select的时候他需要重构监听描述符集合,这个集合要从用户空间拷贝到内核空间也影响性能;此外他对于监听的描述符个数也有限制。epoll则引进epoll_ctl来修改监听的描述符,只有描述符有变化时才修改,同时在epoll_wait返回时使用了mmap等措施,这些都减少了内存操作;返回时只返回ready的描述符列表,省却了逐个遍历的开销;epoll和poll都没有描述符个数限制。
异步IO
不同于同步IO,发起IO操作请求之后,请求进程就不用管了,当IO操作完成后,系统会通知我们处理。他的缺点就是编程复杂
提高并发的手段
延时降低了,系统并发自然也就提高了,除此之外,还可以考虑如下方面:
IO与任务分离
如前所述,IO速度比较慢的,那么我们可以将IO交给单独线程/进程处理,然后将得到的数据提交给任务线程/进程处理;一般可以选用队列用于提交数据。这样将通信同业务处理分离,使模块更加清晰;为我们优化IO提供方便;
队列
除了通信与业务间可以用队列来传递数据,我们也可以在不同的模块间使用队列,不同模块处理速度不一样,队列在不同模块间提供了缓存,这个系统的吞吐量就是系统中正在处理的请求加上队列中等待的请求
集群
单机的处理能力是有上限的,当然可以通过系统升级(scale up)来提高这个门限,不过更多的时候我们采用集群(scale out)来处理。对于集群我们要考虑以下几个问题:
成员管理
在构成集群时,最简单的可以采用单个节点来管理成员,实现上比较简单,问题就是单点故障;我们还可以考虑p2p,所有节点功能相同,没有占主要地位的节点,每个节点在处理业务上可以无差别,也可以只负责特定范围内的请求;在网络构成上,若节点需要处理特定范围的请求可以采用一致性哈希,他的优点就是节点的更改只用修改局部的节点,但实现复杂;我们还可以采用一个居中的方案,由多个节点构成Master的集合,提供网络管理等功能;对于这个Master集合构成,可以P2P采用网络或是主从结构,在主从结构中,主节点的产生可以通过选举,轮询等方案
成员加入/删除
不同的成员管理方案,对于节点加入/删除处理不一样,最简单的就是有Master的方案,只需要向Master注册/删除即可;在P2P网路中,新节点需要向网络中的一个节点注册,这个节点在将新节点的信息广播到全网
故障检查
通过心跳来检查网络中节点的健康状态。显然,点对点的心跳方案在节点数目较多的情况下对网络带宽提出了挑战;现在心跳方案可以采用Gossip,SWIM。心跳可以有两个方向Pull,Push或者两者结合。
同时在检测到节点故障时候,需要把故障信息传播到全网,他可以携带在心跳信息中。
故障检测时候有两个指标需要考虑:准确率和召回率,总体来讲他们是相反的,一个指标高,另一个势必低,在系统设计时要确定取舍。
请求处理
请求处理也依赖于集群构成方式,需要不同的考虑。一般可以在前端提供一个反向代理,将请求转给集群中的一个节点;对于有状态的节点,他可能需要请求转给真正能处理这个请求的节点。