Linux多线程服务端编程 读书笔记3

工程实践经验谈

分布式系统

  • 分布式系统不是放大了的单机系统, 根本原因在于单机没有部分故障.
  • 单机与分布式的根本区别在于进程地址空间是一个还是多个.

负载均衡

一般的解决方案是 :

通过某个集中的负载均衡器来收集并分发负载情况, 可以把消息数目降到M + N(M台web服务器, N台服务提供服务器).

但是这种方式会引发单点故障(Single Point of Failure), 最简单的解决方法是进行冗余备份, Nginx就有通过在不同服务器上搭建主从实例的功能来实现保活.

  • 如何定义服务端的负载?

    负载可以认为是一种性能集合, 我们可以把很多因素考虑进去, 如 : CPU使用率, 内存占用率, 硬盘剩余空间, 网络带宽等等. 没有明确唯一的定义, 不同中间件对于负载的计算方法也有很大差异, 而且会给使用者留出很大的可操作空间.

  • 如何规避分布式系统中保持全局数据一致性的问题?

    其实很多时候如果对全局数据没有强制要求, 可以先忽视, 只是从当前视角分析哪个服务器负载小, 就选哪个, 这样体现在全局上, 大体依旧是均匀分配的.

时序混乱

虽然两个服务器间建立的TCP连接是可以保序的, 但是多个服务器之间的发送时序是无法包装, 就算是理应提前发出去的消息, 也有可能延迟于其他服务器后发的消息. 而且不同服务器的时钟一定会有误差, 通过发送方和接收方的时间戳插值计算延迟是完全不可靠的.

  • 可以使分布式系统内每台机器都运行NTP(网络时间协议)来使各台机器间的时间大体同步.
  • 如果还需更精确, 有逻辑时钟等方案.

可靠性和可用性

  • “并联”可以极大地提高可靠性, 在硬件上可以体现为两套电源, 在软件上可以体现为多个相同服务的服务器.
  • 提高冗余, 增加数据备份 / 降低重建硬盘的时间 可以进一步提高可靠性.
  • 可靠性指的是数据不丢失的概率, 可用性指的是数据或服务可以随时被访问到的概率.
  • 在没有 RAID 的服务器上启用 swap,等于将“全系统的内存可靠性”交给一个“可能随时坏掉的磁盘”,一旦 swap 区损坏,任意进程都可能崩溃或行为异常,因此建议关闭 swap.
  • 高可用的关键不在于做到不停机, 而是能做到随时重启任何一个进程或服务.
  • 软件的可靠性指标不应过高, 因为体量提高后的硬件故障率其实非常高, 软件非常可靠也没有用.

“随时重启进程”作为程序设计目标

  • 进程只要被kill或exit就可以直接重启.
  • 这要求我们的ICP(进程间通信方式)必须是操作系统可自动回收的, 其不可以对其他进程造成任何影响.
  • 只应当用TCP作为唯一的进程间通信手段, 进程一旦退出, 操作系统会自动将连接与断开关闭, 不要使用跨进程的信号量或共享内存.
  • 客户端应在连接断开后可以自动重连, 要求客户端可以检测服务端心跳, 在断开后可以自动failover到备用地址.
  • 客户端应兼容新旧版的服务端协议, 这样在升级服务端时就可以每次杀掉一部分的服务进行, 使服务升级交替进行(灰度升级).
  • 另一种升级方式是迁移, 可以先启动新服务进程, 再让旧服务进程停止接受新请求, 再将新请求导向新进程.

进程标识

  • 服务分为有状态服务和无状态服务, 其实就是指代服务需不需要记录上下文信息, 比如登录状态, 会话状态, 临时缓存的信息等等. 无状态服务是很有优势的, 在故障发生时重启也没有问题, 但是有状态服务需要考虑前世进程的影响, 因为进程一旦重启, 之前记录或缓存的所有信息都会清空.

  • 举个例子就是客户端已经在服务端登录了, 结果服务端崩溃, 重启之后服务端并不知道客户端登录, 但是客户端如果没有有效手段区分出服务端已经重启, 那么就会依旧认为自己是已经登录的状态, 就会造成双方的逻辑不一致, 很有可能崩溃.

  • 因此在使用有状态服务时, 我们需要对每个进程设置一个唯一进程标识, 让客户端分清进程是否发生变化, 一旦发生变化, 客户就可以进行对应的救灾措施, 比如马上重新执行登录操作, 恢复之前的状态.

  • 如果设定唯一进程标识?

    • "ip:prot"的方式只可被无状态服务使用, 有状态服务这样子使用是无法区分先后的.

    • "ip:port:start_time:pid" 这种定义标准可以被使用, 其几乎没有重复概率, 将其发给客户端就可以表示当前的唯一进程, 并且其本身是有意义的, 从中可以随时提取出服务定位和时间, 便于服务重启.

    • UUID是当前最主流标识唯一进程的方式, 其绝对不会重复且获取简单, 在终端界面可以用uuidgen获取 , 在c/c++代码可以通过以下方式获取:

      1
      2
      3
      4
      5
      6
      #include <uuid/uuid.h>
      //....
      uuid_t uuid;
      char uuid_str[37]; // UUID 字符串长度为 36 + 1 个 '\0'
      uuid_generate(uuid);
      uuid_unparse(uuid, uuid_str);

应用层心跳

  • 为什么不能用Tcp keepalive替代应用层心跳?

    Tcp keepalive只能证明目标端口上的进程依旧在线, 不能确定此进程是否正常运行, 不可知对方是否死锁或阻塞.

  • 因此我们需要设置应用层心跳, 一般是服务端向客户端发送心跳.

  • 延迟不可避免, 分布式系统没有全局的瞬时状态, 不存在立刻判断对方故障的方法.

  • 高置信度和低反应时间不可兼得.

  • 心跳包中可以包含当前服务端的复杂, 可以便于客户端做负载均衡.

  • 心跳协议实现还有两个关键点 :

    • 要在工作线程发送, 不要单独起一个心跳线程.

      这是防止工作线程死锁或阻塞时还在继续发心跳, 工作线程必须实际可用才行.

    • 与业务消息用同一个连接, 不要单独用一个心跳连接.

      应用层心跳还应该可以验证网络通畅, 只有用同一个连接发送才可以证明连接通畅. 另外还有一个用处是进行连接的保活, 因为很多服务器会设置防火墙, 在一个连接长时间没有数据交换后, 防火墙就会在双方不知情的清空下断开连接, 也就是说这之后的第一条消息必定是延迟的, TCP会根据延迟判断连接已经断开, 再重新建立连接发送.


Linux多线程服务端编程 读书笔记3
http://example.com/2025/07/16/[Linux多线程服务端编程] 读书笔记(3)/
作者
天目中云
发布于
2025年7月16日
许可协议