【Linux多线程服务端编程】| 【03】多线程服务器的适用场合和常用编程模型

索引

【Linux多线程服务端编程】| 【01】线程安全的对象生命期管理笔记
【Linux多线程服务端编程】| 【02】线程同步精要
【Linux多线程服务端编程】| 【03】多线程服务器的适用场合和常用编程模型
【Linux多线程服务端编程】| 【04】C++多线程系统编程精要

在这里插入图片描述

1 进程与线程

Linux系统编程 | 【03】进程、环境变量、IPC
Linux | Linux中的线程、互斥量、信号量的基本使用

面谈(同一个服务器)通信,可时刻知道对方是否已死,电话谈(不同一个服务器通信)通过心跳包周期来判断对方是否存活;
【设计分布式需要考虑】

  • 容错:突然有服务器宕机;
  • 扩容:增加新发服务器;
  • 负载均衡:将一个服务器的工作移到另外一台;
  • 退休:一个服务器要修复bug,等它处理完任务就重启;

2 单线程服务器的常用编程模型

【Reactor模型】 在高性能网络程序中,最广泛non-blocking IO + IO multiplexing,适用该模型的有;

  • lighttpd,单线程服务器;
  • libevent,libev;
  • ACE,Poco C++ libraries;
  • Java NIO,包括Apache Mina和Netty;
  • POE;
  • Twisted;

【Reactor模型】 另外一种模型,常见的有:

  • Boost.Asio;
  • Windows I/O Completion Ports;
2.1 Reactor模型

【基本结构】:事件循环、事件驱动、事件回调(函数必须为非阻塞)方式来实现;


在这里插入图片描述


【优点】效率高,易编写,可用于读写socket、连接建立、DNS解析等都可用非阻塞来提高并发,对于IO密集不错;

3 多线程服务器的常用编程模型

【常见】

  • 每个请求创建一个线程,适用阻塞IO,伸缩性不佳;
  • 适用线程池,适用阻塞IO操作,比第一种性能略提升;
  • 适用non-blocking IO+IO multiplexing,默认使用这种;
  • Leader/Follower等高级模型;

默认选择第三种,即non-blocking IO + one loop per thread来编写;

3.1 one loop per thread

该模型下,每个IO线程都有一个event loopReactor,用于处理读写定时事件;

【优点】

  • 线程数目自设置基本固定, 不会频繁创建与销毁

  • 可方便再线程间调配负载

  • IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发;

  • Eventloop为线程的主循环,若要指定线程干活,即将timerIOchannel注册到哪个线程的loop;

  • 实时性有要求、数据量大的connection可单用一个线程;

  • 数据处理任务分摊到另外几个线程中;

  • 其他次要的辅助性connections可共享一个线程;

  • 对于non-trivial服务端程序,一般会采用non-blockingIO + IOmultiplexing,每个connect/accept都会注册到某个event loop上,每个线程至多只有一个event loop;

  • 需要线程安全,允许一个线程给别的线程里塞东西;

3.2 线程池

适用阻塞队列针对于没有IO只有计算任务的线程;
【简单实现】:
在这里插入图片描述

可参考muduo中的无界BlockingQueue和有界的BoundedBlockingQueue及Intel Threading Building Blocks的concurrent_queue<T>;

3.3 推荐模式

【推荐使用】one(event) loop per thread + thread pool

  • event loop即IO loop用作IO multiplexing,配合non-blocking IO定时器
  • thread pool用来计算,处理任务队列或生产者消费者队列;
  • 分配资源是否需要考虑执行特殊任务的线程,如loggin;
3.4 进程间通信只用TCP

首选Socket,主要是TCP;
【好处】:可以跨主机,具有伸缩性;

【pipe应用场景】:写Reactor/event loop时用来异步唤醒epoll_wait调用;

TCPport由一个进程独占,且操作系统会自动回收监听端口socket文件描述符,进程结束时会关闭所有文件描述符;

  • port独占可防止程序重复启动,抢不到port;

【与IPC相比】:TCP可记录、可重现、可跨语言、可再生;

  • 可使用tcpdumpWireshark解决进程间协议和状态争端,可分析性能;
  • 可用tcpcopy来压测;
  • tcp通信会有数据编排和非编排的开销,需要使用合适的消息格式,如Google Portocol Buffers;

4 分布式系统使用TCP长连接通信

通常一个分布式系统运行再多台机器的多进程组成,进程间采用TCP长连接通信
【长连接好处】

  • 容易定位分布式系统中的服务之间的依赖关系;
    • 可通过netstat -tpna | grep :port来查看服务客户端地址,再客户端上用netstatlsof可找出哪个进程发起的连接;这样可在迁移服务的时候能防止出现outage;(TCP短链接及UDP没有该特性)
    • 通过接收发送队列的长度易定位网络或程序故障;
      • 运行时,netstat -tn打印的Recv-QSend-Q应该在0左右
      • Revc-Q不变或持续增加,则服务进程处理速度可能变慢,死锁或阻塞
      • Send-Q不变或持续增加,则可能服务器忙,来不及处理,或网络中丢包、掉线

5 多线程服务器的使用场合

5.1 服务器基本任务

【处理并发连接】

  • 若机器可创建高于CPU数目的线程,此时一线程只处理一个TCP连接(半个),一般使用阻塞IO
  • 若机器可创建线程相当于CPU数目,此时一个线程要处理多个TCP连接上的IO,通常使用非阻塞IO和IO多路复用

【在处理并发连接时,要充分发挥硬件资源,当一台多核机器提供一种服务或执行一个任务,可用模式】

  • 【模式1】运行一个线程的进程;
  • 【模式2】运行一个线程的进程;
  • 【模式3】运行多个单线程的进程:
    • 【模式a】:把模式1中的进程运行多份;
    • 【模式b】:主进程+woker进程;
  • 【模式4】运行多个多线程的进程;

【使用速率为50MB/s的数 据压缩库、在进程创建销毁的开销是800μs、线程创建销毁的开销是 50μs的前提下,考虑如何执行压缩任务:】

  • 偶尔压缩1GB的文件,时间是20s,则一个进程去做是合理的,因为进程启动和销毁的开销远远小于实际任务的耗时
  • 经常压缩500kB的文本,预计运行时间是10ms,那么每次都起进程似乎有点浪费了,可以每次单独起一个线程去做;
  • 若要频繁压缩10kB的文本数据,预计运行时间是200μs,那么每次起线程似乎也很浪费,可直接在当前线程搞定;也可以用一个线程池,处理任务,避免阻塞当前线程
5.2 必须使用功能单线程的场合

【必须使用的场合】

  • 程序可能会fork
    【fork后的两种行为】:
    • 立刻执行exec,变为另一个程序或子程序运行fastcgi程序,或看门狗进程(该程序必须为单线程);
    • 执行当前程序
  • 限制程序的CPU占用率
    • 如一个8核服务器,开一个单线程出现了忙等待,占满一个核,在最坏情况下CPU使用率也就占到12.5%,还有剩余供其他服务使用,即使用单线程来避免抢夺系统资源;

【单线程的缺点】

  • Event loop中,是非抢占式的,若a优先级高于b,a需要1ms来处理,b需要10ms;若b早于a发送,则当a事件唤醒时,程序已执行poll,并处理b,那么a只有等10ms后才有机会被处理,该情况为优先级反转,可多线程来解决

【多线程下性能不一定能提升】
在很少的IO流量下,让CPU跑满,用多线程无法提高性能

  • 【1】对于静态web或FTP服务器,主要瓶颈在于磁盘IO网络IO;一个线程即可撑满IO,即在硬件容量上已达饱和,无法提高;
  • 【2】从n个整数总选出m个整数,和为0,计算该程序应选用3a模式
5.3 适用多线程程序的场景

主要提高响应速度、让IO计算重叠降低潜在因素,可提高平均响应性能;

【满足条件】:

  • 多个CPU可用,单核机器上多线程没有性能优势;
  • 线程间共享数据,即内存中的全局状态,若没有共享数据,用模式3b;
  • 共享的数据是可修改,不是静态的常量表,若数据不能修改,则可在进程间用共享内存,适用模式3;
  • 事件响应有优先级差异,可用专门的线程来处理优先级高的事件,防止优先级反转;
  • 程序有相对的计算量,不是逻辑简单IO bound或CPU bound;
  • 利用异步操作,如日志,往文件或服务器都不应该阻塞;
  • 能搭配好增加CPU数目带来的好处;
  • 具有可预测的性能;
  • 有效的划分责任与功能;每个线程逻辑任务单一,不能都塞到一个event loop;

【机群管理软件】:Linux服务器机群,8个计算节点,1个控制节点,需要3个程序:

  • 运行在控制节点上的master监视控制机群状态
  • 运行在每个计算机节点上的salve负责启动和终止job,并监控本机的资源;
  • 供用户适用的client工具,用于提供job;

【其中】:

  • salve为看门狗进程,会启动别的job进程,需要用单线程,不需要占太多资源;
  • master应该为模式2的多线程程序:
    • 独占一个8核机器,若用模式1,则浪费87.5%的资源;
    • 整个机群的状态应该完全在内存中,状态时共享且可变的若用模式3,则进程间同步会是大问题,若用共享内存,则成了多进程的多线程,一个进程阻塞或crash,则其他进程死锁
    • master主要指标不是吞吐量,而是延迟尽快响应事件,不会出现IO/CPU跑满;
    • master监控的事件有优先级区别,一个程序正常运行结束核异常处理的优先级不同,计算节点的磁盘满机箱温度优先级也不同,若单线程,则会出现优先级反转
    • 若master与每个slave用一个TCP连接,则master采用2或4个IO线程来处理8个TCP连接可能有效降低延迟;
    • master要异步往本地硬盘写log,需要要求log lib有自己的IO线程;
    • master可能要读写数据库,则数据库连接第三方lib可能要有自己的线程,并回调master代码;
    • master要服务器与多个clent,用多线程降低客户响应事件,即可再用2个IO线程专门处理和clents的通信;
    • master可提供一个monitor接口,用来广播推送机群的状态,即用户不需主动轮询;可用单独线程实现
      【master一共开10个线程】:虽线程数>core数,但大多线程都是空闲,可依赖OS的进程调度来保证可控的延迟;
  • 4个用于和slaves通信的IO线程;
  • 1个logging线程;
  • 1个数据库IO线程;
  • 2个和clent通信的IO线程;
  • 1个主线程,用于做背景工作;
  • 1个pushing线程,用于主动广播机群状态;

【TCP聊天服务器】
聊天(人与人、机器与机器),服务器特点是并发连接之间数据交换,一个连接收到的data转发给其他多个连接,故只能用模式1、2

  • 若只有数据交换,则模式1,CPU快,单线程应付几百个连接也可以;
  • 若加上关键字过滤,黑名单,防灌水等每项功能都会占CPU,则需要考虑模式2;若顺序处理可能导致消息发送延迟,则将这些任务分散到多个线程中;

【服务器分类】

  • IO线程,主循环在IO multiplexing,阻塞等待epoll_wait上,也处理定时事件,也可处理消息编码解码等;
  • 计算线程,主循环在阻塞队列,阻塞等在条件变量上,一般为线程池中的线程,不涉及IO,避免阻塞
  • 第三方库所用的线程,如loggin,database;

6 多线程服务器的适用场合

Linux能同时启动多少个线程

  • 在32位Linux下,一个进程地址空间为4G,其中用户态为3G,而一个线程默认栈位10M,即一个进程大概可以同时启动300个线程;

多线程能提高并发度吗

  • 若指的是并发连接,则不能;thread per connection不适合高并发场合,其可扩展性不佳,one loop per thread的并发度足够大,且与CPU数目成正比

多线程提高吞吐量吗

  • 对于计算密集型服务不能
  • 【例1】:若有耗时的计算服务,用单线程需要0.8s,在8核机器上可启动8个线程或进程,来一起计算可将吞吐量从1.25提升到10qps(实际可能打8折);
    • 若用并行计算,用8核一起计算,若完全并行,则计算时间为0.1s,吞吐量仍为10qps,但首次请求响应慢,可能加速比也就只有6个,计算时间为0.133s;
  • 【例2】:8核机器上压缩100个1G的文件,每个core处理能力为200M/s,可每次起8个进程,每个进程压缩一个文件或依次压缩每个文件,每个文件用8个线程并行压缩;两种耗时差不多,但第二种能较快地拿到第一个压缩完的文件,即首次响应的延时更小;(进程创建的速度慢)
    • 若适用thread per request,每个客户请求用一个线程去处理,则并发请求数到达临界值,吞吐量将会下降,由于上下文切换需要开销;
  • 为了并发请求数高也要保证稳定的吞吐量,则我们可用线程池,大小需要满足阻抗匹配原则;
    • 若需要计算,且计算时间占请求时间的1/5,则用线程池是合理的;
    • 若一次请求响应,主要时间在等待IO,则为了进一步提高吞吐量,则需要使用其他模型,如Proactor;

多线程能降低响应时间?

  • 需要充分利用多核资源;

【例1 计算密集型】:多线程处理输入以memcached服务端为例,memcached一次请求响应为以下步骤:

  • 读取并解析客户端输入;
  • 操作hashtable;
  • 返回客户端;
    在单线程下,3步为串行执行
    在多线程下,启用多个输入线程(4个),并在建立连接时按循环法把新连接分派给其中一个输入线程,即one loop per thread模型;即第一步可多线程并,提高速度,第二步需要,还是单线程,可改进;

【例2 计算密集型】:多线程分担负载,若我们要左一个求解Sudoku的服务,这个服务程序在9981端口接受请求,输入一行81个数字,输出为填好后的81个数字(1~9)若无解,输出NO\r\n;

  • 输入格式简单,只用单线程做IO即可;假设求解用时为10ms,用前面的方法计算,单线程能达到的吞吐量上限为100qps;在8核机器上,若用线程池来计算,能达到的吞吐量上限为800qps;
  • 【分析多线程如何快】:
    • 单线程处理,reqs会排队处理缓冲区,第一个10ms……第十个100ms平均响应时间为55ms;
    • 多线程中一个IO线程,8个计算线程,有任务进来即分配到空闲线程,平均响应为12ms;

多线程程序如何让IO和计算互相重叠,降低延迟
【基本思路】:把IO操作通过阻塞队列交给别的线程去做;

【例1】:日志,在多线程服务器程序中,日志很重要,仅考虑log file;
在一次请求响应中,可能要写多条日志信息,而若用同步方式写文件,多半会降低性能

  • 【原由】:
    • 文件操作,线程阻塞在IO上,让CPU闲置,增加相应时间;
    • 若使用buffer来解决则需要加锁,让线程等待,降低并发度;
  • 【解决】:用一个单独的loggin线程,负责写磁盘文件,通过一个或多个阻塞队列对外提供接口;当别的线程要写日志时,先把消息准备好,往队列中加,即不用等待;

【例2】:memcached客户端,若我们用memcached来保存用户最后的发帖时间,则每次响应用户发帖的请求时,程序要去设置一下memcached里的值,若这里用同步IO,则会增加延迟;

  • 对于设置一个值这样的write-only idempotent操作,不需要等待memcached返回操作结果,不需要在乎set操作是否失败,可借助多线程来降低响应延迟;
    • 如:我们写一个多线程版memcached客户端,对set操作,调用方只要把key和value准备好,调用给你set,将数据压入阻塞队列,剩下的事情交给memcached客户端的线程操作,服务端线程不受阻碍;
  • 所有的网络写操作都可异步操作,但也有缺点:每次write都要再线程间传递数据,若TCP缓冲区为空,则我们可以再本线程写完,不要交给专门的IO线程(Netty使用该做法);

什么是线程池大小的阻抗匹配原则

  • 若线程池的任务,密集计算占时比重为P(0<P<=1),而系统一共有C个CPU,为了让C个CPU跑满而不过载,线程池大小的经验公式:T = C/P;T值上下浮动50%
  • P<0.2则T可以取固定值,则5xC;C为分配给这个任务的CPU数目;

还有其他的non-trivial多线程编程模型?
【Proactor】:若一次请求响应中要和别的进程多打几次交道,则用该模型可提高并发度,但代码繁杂,不能降低延迟

【例1】:一次HTTP proxy的请求若没有命中本地cache,则:

  • 解析域名,陌生可能会花几秒钟;
  • 建立连接;
  • 发送HTTP请求;
  • 等待对方回应;
  • 返回响应;
  • 上述中2个server发生3次往返,每次可能都要花几百毫秒:
    • 向DNS问域名,等待回复;
    • 向对方的HTTP服务器发起连接,等待TCP三路握手完成;
    • 向对方发送HTTP request,等待对方response;
  • 实际HTTP proxy本身运算量小,若使用线程池,则线程数将会很多,不利于管理;
  • 【方法1】:把域名已解析、连接已建立、对方已响应做成event,继续按Reactor编程,即每次客户请求就不能用一个函数从头到尾执行完成,而要分成多个阶段,并管理好请求的状态;
  • 【方法2】:使用回调函数,让系统把任务串起来,若用户收到请求,如没有命中本地缓存,则执行:
    • 立刻发起异步的DNS解析startDNSResolve(),告诉系统在解析完之后调用DNSResolved()函数;
    • 在DNSResolved()中,发起TCP连接请求,告诉系统在连接建立之后调用connectionEstablished();
    • 在connectionEstablished()中发送HTTP request,告诉系统在收到响应之后调用httpResponsed();
    • 最后,在httpResponsed()里把结果返回给客户。
  • Proactor依赖系统或库来高效地调度子任务,子任务不会阻塞,因此能用比较少的线程达到很高的IO并发度;

模式2和模式3a该如何取舍

  • 根据服务程序响应一次请求所访问的内存大小来取舍;
  • 若工作集较大,则用多线程避免CPU cache换入换出,影响性能;
  • 否则,就用单线程,便利;
  • 【例】:若程序有较大的本地cache,用于缓存一些基础参考数据,几乎每次请求都会访问cache;
    • 则使用多线程合适,因为可避免每个进程都自己留一份cache,增加内存使用;
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jxiepc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值