Linux(muduo网络库):08---多线程服务器之(进程间通信只用TCP)

  • 本文内容衔接于前一篇文章(单线程服务器、多线程服务器的常用编程模型):https://blog.csdn.net/qq_41453285/article/details/104954338

一、Linux下IPC、同步原语的种类

  • Linux下进程间通信(IPC)的方式数不胜数,光《UNIX网络编程》列出的就有:匿名管道(pipe)、具名管道(FIFO)、POSIX消息队列、共享内存、信号(signals)等等,更不必说Sockets了
  • 同步原语 (synchronization primitives)也很多,互斥器(mutex)、条件变量 (condition variable)、读写锁(reader-writer lock)、文件锁(record locking)、信号量(semaphore)等等

二、进程间通信首先TCP

  • 如何选择呢?根据个人经验,贵精不贵多,认真挑选三四样东西就能完全满足我的工作需要,而且每样我都能用得很熟,不容易犯错
  • 进程间通信我首选Sockets(主要指TCP,我没有用过UDP,也不考虑Unix domain协议)

跨主机

  • 其最大的好处在于:可以跨主机,具有伸缩性(scalability)
  • 反正都是多进程了,如果一台机器的处理能力不够,很自然地就能用多台机器来处理。把进程分散到同一局域网的多台机器上,程序改改host:port配置就能继续用。相反,前面列出的其他IPC都不能跨机器(比如共享内存效率最高,但受网络带宽及延迟限制,无论如何也不能高效地共享两台物理机器的内存), 这就限制了scalability

双通道

  • 在编程上,TCP sockets和pipe都是操作文件描述符,用来收发字节流,都可以read/write/fcntl/select/poll等
  • TCP比pipe的优势:
    • TCP是双向的, Linux的pipe是单向的,进程间双向通信还得开两个文件描述符,不方便(可以用socketpair替代)
    • 而且进程要有父子关系才能用pipe,这些都限制了pipe的使用
  • 在收发字节流这一通信模型下,没有比Sockets/TCP更自然的IPC了
  • 当然, pipe也有一个经典应用场景,那就是:
    • 写Reactor/event loop时用来异步唤醒select(或等价的poll/epoll_wait)调用(在Linux下,可以用eventfd代替,效率更高)
    • Sun HotSpot JVM在Linux就是这么做的,可参阅:https://blog.csdn.net/haoel/article/details/2224055

port

  • TCP port由一个进程独占,且操作系统会自动回收(listening port和已建立连接的TCP socket都是文件描述符,在进程结束时操作系统会关闭所有文件描述符)
  • 这说明,即使程序意外退出,也不会给系统留下垃圾,程序重启之后能比较容易地恢复,而不需要重启操作系统(用跨 进程的mutex就有这个风险)
  • 还有一个好处,既然port是独占的,那么可以防止程序重复启动,后面那个进程抢不到port,自然就没法初始化 了,避免造成意料之外的结果

安全关闭

  • 两个进程通过TCP通信,如果一个崩溃了,操作系统会关闭连接, 另一个进程几乎立刻就能感知,可以快速failover
  • 当然应用层的心跳也是必不可少的(可参阅后面的“分布式系统工程实践之分布式系统中心跳协议的设计”文章)

可记录、可重现、跨语言

  • 与其他IPC相比,TCP协议的一个天生的好处是“可记录、可重 现”
  • tcpdump和Wireshark是解决两个进程间协议和状态争端的好帮手, 也是性能(吞吐量、延迟)分析利器。我们可以借此编写分布式程序 的自动化回归测试。也可以用tcpcopy(http://code.google.com/p/tcpcopy)之类的工具进行压力测试
  • TCP还能跨语言,服务端和客户端不必使用同一种语言。试想如果用共享内存作为IPC,C++程序如何与Java通信,难道用JNI吗?

可再生

  • 另外,如果网络库带“连接重试”功能的话,我们可以不要求系统里的进程以特定的顺序启动,任何一个进程都能单独重启
  • 换句话说, TCP连接是可再生的,连接的任何一方都可以退出再启动,重建连接之后就能继续工作,这对开发牢靠的分布式系统意义重大

字节流通信

  • 使用TCP这种字节流(byte stream)方式通信,会有marshal/unmarshal的开销,这要求我们选用合适的消息格式,准确地说是wire format,目前我推荐Google Protocol Buffers
  • 可参阅后面的“分布式系统工程实践之为系统演化做准备”文章中关于分布式系统消息格式的讨论
  • 有人或许会说,具体问题具体分析,如果两个进程在同一台机器, 就用共享内存,否则就用TCP,比如MS SQL Server就同时支持这两种通信方式。那么:
    • 试问,是否值得为那么一点性能提升而让代码的复杂度大大增加呢?何况TCP的local吞吐量一点都不低,见后面“muduo简介之性能评测文章中对于muduo与Boost.Asio、libevent2的吞吐量对比”的测试结果
    • TCP 是字节流协议,只能顺序读取,有写缓冲;共享内存是消息协议,a进 程填好一块内存让b进程来读,基本是“停等(stop wait)”方式。要把这两种方式揉到一个程序里,需要建一个抽象层,封装两种IPC。这会带来不透明性,并且增加测试的复杂度。而且万一通信的某一方崩溃,状态reconcile也会比sockets麻烦。(数据刚写到一半,怎么办?)为我所 不取
    • 再说了,你舍得让几万块买来的SQL Server和其他应用程序分享机器资源吗?生产环境下的数据库服务器往往是独立的高配置服务器, 一般不会同时运行其他占资源的程序
  • TCP本身是个数据流协议,除了直接使用它来通信外,还可以在此之上构建RPC/HTTP/SOAP之类的上层通信协议,这超过了本专栏的范围。另外,除了点对点的通信之外,应用级的广播协议也是非常有用的,可以方便地构建可观可控的分布式系统,可参阅后面的“muduo编程实例之简单的消息广播服务”文章

三、分布式系统中使用TCP长连接通信

  • 前面提到,分布式系统的软件设计和功能划分一般应该以“进程”为单位。从宏观上看,一个分布式系统是由运行在多台机器上的多个进程组成的,进程之间采用TCP长连接通信
  • 我提倡用多线程,并不是说把整个系统放到一个进程里实现,而是指功能划分之后, 在实现每一类服务进程时,在必要时可以借助多线程来提高性能。对于整个分布式系统,要做到能scale out,即享受增加机器带来的好处
  • 最近几篇文章讨论分布式系统中单个服务进程的设计方法,在后面“分布式系统工程实践”专栏将谈一谈整个系统的设计

TCP长连接的两个优点

  • 一是容易定位分布式系统中的服务之间的依赖关系:
    • 只要在机器上运行netstat -tpna | grep :port就能立刻列出用到某服务的客户端地址(Foreign列),然后在客户端的机器上用 netstat或lsof命令找出是哪个进程发起的连接
    • 这样在迁移服务的时候能有效地防止出现outage
    • TCP短连接和UDP则不具备这一特性
  • 二是通过接收和发送队列的长度也较容易定位网络或程序故障:
    • 在正常运行的时候,netstat打印的Recv-Q和Send-Q都应该接近0,或者在0附近摆动
      • 如果Recv-Q保持不变或持续增加,则通常意味着服务进程的处理速度变慢,可能发生了死锁或阻塞
      • 如果Send-Q保持不变或持续增加,有可能是对方服务器太忙、来不及处理,也有可能是网络中间某个路由器或交换机故障造成丢包,甚至对方服务器掉线,这些因素都可能表现为数据发送不出去
    • 通过持续监控Recv-Q和Send-Q就能及早预警性能或可用性故障
    • 以下是服务端线程阻塞造成Recv-Q和客户端Send-Q激增的例子

Linux(muduo网络库):08---多线程服务器之(进程间通信只用TCP)_第1张图片

四、附加

  • 本专题未完结,参阅下一篇文章(单线程、多线程服务器的适用场合)https://blog.csdn.net/qq_41453285/article/details/105005052

你可能感兴趣的:(Linux(muduo网络库))