连接管理:连接池设计、连接复用、连接保活、优雅关闭、连接数限制与防攻击

连接管理,说白了就是让服务器跟客户端之间的那条「管道」跑得稳、跑得快、还不被人搞坏。我做了这么多年后端,见过太多因为连接没管好而翻车的案例。今天咱们就把这块掰开揉碎了讲清楚。

连接池设计:别让每次请求都重新握手

你想想看,每次建立TCP连接都要三次握手,每次断开都要四次挥手。如果每个请求都来一遍,光握手开销就能吃掉你一半的性能。连接池就是解决这个问题的。

连接池的核心思想很简单:提前创建一批连接,用的时候拿,用完归还。但实现起来有几个坑要注意。

连接池的核心参数

  • 最小连接数:池子里最少保持多少条连接。我习惯设成跟CPU核心数差不多,比如8或16。
  • 最大连接数:池子最多能膨胀到多少。这个要看后端服务的承载能力,设太大反而会压垮数据库。
  • 最大空闲时间:连接空闲多久后要回收。我一般设30秒,太短容易频繁创建,太长浪费资源。
  • 获取超时时间:等不到连接时最多等多久。设个100毫秒就差不多了,等太久请求都超时了。

我在项目中遇到过一个问题:连接池满了,新请求拿不到连接,直接报错。后来加了等待队列和超时机制,才把这个问题解决掉。

// 一个简单的连接池核心逻辑
class ConnectionPool {
public:
    Connection* acquire() {
        std::lock_guard<std::mutex> lock(mutex_);
        
        // 1. 先看看空闲队列有没有
        if (!idle_.empty()) {
            auto* conn = idle_.front();
            idle_.pop();
            return conn;
        }
        
        // 2. 没达到上限就新建
        if (total_ < max_size_) {
            total_++;
            return new Connection();
        }
        
        // 3. 满了就等一会儿
        // 这里用条件变量等待,超时返回nullptr
        return nullptr;
    }
    
    void release(Connection* conn) {
        std::lock_guard<std::mutex> lock(mutex_);
        idle_.push(conn);
        // 通知等待的线程
        cv_.notify_one();
    }
    
private:
    std::queue<Connection*> idle_;
    std::mutex mutex_;
    std::condition_variable cv_;
    int total_ = 0;
    int max_size_ = 100;
};

连接复用:一条连接跑多个请求

连接复用跟连接池是两码事。连接池管的是「连接的生命周期」,连接复用管的是「一条连接上怎么跑多个请求」。

HTTP/1.1的Keep-Alive就是典型的连接复用。一条TCP连接上可以连续发多个HTTP请求,不用每次重新握手。但有个问题:队头阻塞。前一个请求没响应,后面的就得等着。

HTTP/2用多路复用解决了这个问题。一条连接上可以同时跑多个流,每个流独立收发。我建议你在设计RPC框架时也采用类似思路。

我的经验:连接复用不是越多越好。一条连接上并发太多请求,一旦出问题,影响面会很大。我一般控制在一条连接上同时跑不超过128个请求。

连接保活:别让连接静悄悄地断掉

你有没有遇到过这种情况:客户端连上服务器,啥也没干,过了几分钟再发请求,发现连接已经断了?这就是因为没有做保活。

TCP本身有Keep-Alive机制,但默认是关闭的,而且间隔时间太长(默认2小时)。我建议你在应用层自己做保活。

// 应用层心跳检测
class HeartbeatManager {
public:
    void start(Connection* conn) {
        // 每30秒发一次心跳
        timer_.start(30000, [conn]() {
            if (conn->isIdle()) {
                conn->sendHeartbeat();
            }
        });
    }
    
    void onHeartbeatResponse(Connection* conn) {
        conn->resetIdleTimer();
    }
    
    void checkTimeout() {
        // 超过90秒没收到任何数据,判定连接死亡
        auto now = getCurrentTime();
        for (auto& conn : connections_) {
            if (now - conn.lastRecvTime > 90000) {
                conn.close();
            }
        }
    }
};

我曾经遇到过一个线上问题:客户端跟服务器之间的防火墙把空闲连接给掐了,但服务端不知道。结果就是服务端还维护着一堆「僵尸连接」,白白浪费资源。后来加了心跳检测,这个问题就解决了。

注意:心跳间隔不要太短,否则会产生大量无效流量。我一般设30秒发一次,90秒没响应就判定超时。这个「30/90」的配比是我在多个项目中验证过的。

优雅关闭:别让数据丢在半路上

关闭连接看起来简单,调个close()就完事了。但你真的确定数据都发完了吗?

TCP关闭有四种方式,每种都有不同的语义:

关闭方式 行为 适用场景
close() 立即关闭,丢弃未发送数据 出错时快速释放资源
shutdown(SHUT_WR) 关闭写端,读端仍可接收 通知对端「我发完了」
shutdown(SHUT_RD) 关闭读端,写端仍可发送 不再接收数据
shutdown(SHUT_RDWR) 同时关闭读写 类似close()但更温和

我建议的优雅关闭流程是这样的:

  1. 先调用shutdown(SHUT_WR),告诉对端「我不再发了」
  2. 继续读取对端发来的数据,直到read返回0
  3. 确认所有数据都处理完了,再调用close()

为什么要这么麻烦?因为TCP是全双工的。你关闭写端后,对端可能还有数据要发给你。如果你直接close(),那些数据就丢了。

避坑指南:我曾经在做一个网关项目时,直接调了close()就返回了。结果客户端那边还在等响应,等了好几分钟才超时。后来改成优雅关闭,这个问题就再没出现过。

连接数限制与防攻击:别让坏人把资源耗光

没有连接数限制的服务器,就像没锁门的房子。攻击者可以建立大量连接,把你的内存和文件描述符全部耗尽。

连接数限制要分几个层面来做:

  • 系统层面:ulimit -n 设置最大文件描述符数
  • 进程层面:限制每个worker进程的最大连接数
  • 客户端层面:限制每个IP的最大连接数
  • 全局层面:限制整个服务的最大连接数
// 连接数限制器
class ConnectionLimiter {
public:
    bool accept(const std::string& client_ip) {
        std::lock_guard<std::mutex> lock(mutex_);
        
        // 1. 全局限制
        if (total_connections_ >= max_global_) {
            return false;
        }
        
        // 2. 单IP限制
        auto& count = ip_count_[client_ip];
        if (count >= max_per_ip_) {
            return false;
        }
        
        // 3. 通过
        total_connections_++;
        count++;
        return true;
    }
    
    void release(const std::string& client_ip) {
        std::lock_guard<std::mutex> lock(mutex_);
        total_connections_--;
        ip_count_[client_ip]--;
    }
    
private:
    int total_connections_ = 0;
    int max_global_ = 10000;
    int max_per_ip_ = 100;
    std::unordered_map<std::string, int> ip_count_;
    std::mutex mutex_;
};

防攻击这块,除了限制连接数,还要注意:

  • 慢速连接攻击:建立连接后不发数据,慢慢耗你资源。解决方案是设置连接超时,比如30秒内没收到任何数据就断开。
  • 连接风暴:短时间内大量连接涌入。可以用令牌桶算法做限流,或者用连接队列缓冲一下。
  • SSL重协商攻击:频繁发起SSL重协商,消耗CPU。我一般限制每个连接每10秒最多重协商一次。

我的习惯:在接入层做连接数限制,在业务层做请求频率限制。两层防护,坏人想搞破坏就没那么容易了。

知识体系总览

下面这张图把连接管理的几个核心模块串起来了,你可以对照着看:

连接管理 连接池设计 最小/最大连接数 连接复用 多路复用/队头阻塞 连接保活 心跳检测/超时判定 优雅关闭 shutdown/close流程 连接数限制 全局/IP级别限制 防攻击 慢速攻击/连接风暴 核心目标:高性能 + 高可用 + 高安全 连接复用提性能 · 保活+优雅关闭保可用 · 限制+防攻击保安全

连接管理这块,说白了就是三个目标:性能、可用性、安全性。连接池和复用解决性能问题,保活和优雅关闭解决可用性问题,连接数限制和防攻击解决安全性问题。把这六个点都做到位了,你的服务器在连接层面就稳了。

嗯,今天就先聊到这儿。这些内容都是我这些年踩坑踩出来的经验,希望能帮你少走些弯路。


公众号:蓝海资料掘金营,微信deep3321