TCP 长衔接层的规划和在 IM 项目的实战运用

TCP 长衔接接入层的衔接办理

TCP 长衔接的办理思路

完成思路

IM 架构中的 TCP 长衔接接入层的 NET 衔接一般会许多,比方单台服务器至少会有几十万,有的乃至会到百万衔接;这个长衔接的坚持,也就代表中会有这么多客户端(用户)的接入。那么咱们怎样去办理这些衔接?当有数据需求下发的时分,怎样能够快速依据衔接信息找到用户、或者依据用户快速定位到网络衔接?这就需求咱们能够有一个适宜的数据结构去维护,而且咱们需求考虑一些其他的点比方快速定位、机器内存大小等。

最容易想到的一个思路是经过 map 数据结构来办理,比方 map<conn,user>,由于每个用户的 uid(user)是唯一,因而,这样做,初期来看,并不会有太大的影响;可是试想一下,这个只能单向定位,只能依据 Conn 网络衔接查找用户,那么我想依据用户信息快速查找到对应的 Conn 然后下发数据怎样办呢?

为此,一个更为适宜的做法便是将用户和网络衔接进行一一对应,这样,不仅是能够彼此查找,而且查找定位的时刻复杂度总是 O(1)。详细完成的 Golang 代码如下,只列出关键信息:

// Conn 与 User 一一映射,用来优化 map 查询方法type Conn struct {    conn        net.Conn  // TCP 网络 衔接信息     user        *User     // 客户端用户信息(一般包括 uid、name等)}type User struct {    uid              int64    Name             string    conn             *Conn}

这样的结构规划,便是 Conn 里边包括了 User、User 里边包括了 Conn,这样便是一一对应,不论多少数据量,查询定位的时刻复杂度都是 O(1)。这儿运用了一个思想便是空间换时刻,由于咱们当时的机器的内存是很大的,所以就能够运用这个空间换时刻的思想,快速查询。

运用场景

IM 体系中,必定会有这么几个操作:

  • • 用来衔接(accept)

  • • 用户登录(login)

TCP Socket 编程模型是:

socket -> bind -> listen -> accept -> recv -> send -> close

因而对 IM 接入层来说,首要会收到用户的 accept 恳求,accept 成功之后,咱们就有了 Conn 信息,然后咱们开始填充 Conn 结构 和 User 结构,这儿算是开始树立起了对应联系,可是 User 中的信息还不够,还需求用户登录之后才有更多的数据。

衔接成功之后,用户就会建议登录恳求,登录成功之后,就会有了足够的 User 信息,这样就能够依据相关信息彼此定位了。登录成功之后,长衔接接入层还需求给用户回应 ACK ,因而在登录包之后,长衔接接入层就能够从 User 结构中取出 Conn 进行回包给用户(客户端)。

随后的操作中,咱们能够依据事务场景的需求,从 User(uid)中快速定位到 Conn,然后发送音讯给客户端;也能够依据 Conn 快速定位到 User,更新 User 信息,或者获取 User 信息。

TCP 长衔接心跳超时的处理

再来看看别的一个场景,首要,咱们要清楚,长衔接接入层必定是有多个的,一台机器肯定扛不住,也无法做到高可用。因而在每个接入层节点中的处理上,还有一点十分重要的便是,坚持着很多长衔接后,假如客户端一直没有恳求,或者客户端以为反常导致封闭了衔接可是服务端并不知晓,那么这些无用的长衔接,服务端肯定是需求清理的,防止占用很多资源。

怎样完成?当然需求经过心跳来坚持衔接,假如心跳超时则踢出衔接。心跳这儿多说一句,一般固定心跳设置为 4.5 分钟,还有更为适宜的智能心跳战略。咱们现在重点在于办理 TCP 长衔接,不讨论心跳战略的完成。

上面的 TCP 长衔接的办理思路是需求一一对应,便利彼此查找,那么针对心跳是否超时,这个和用户没有联系,因而只需 Conn 的处理。经过一个红黑树能够搞定,经过递归地从根节点向左遍历节点,直到左节点为空,能够查找树中的一切 Conn 的超时状况。

Golang 的代码片段如下:

var timeoutTree *rbtree.Rbtree  //红黑树type TimeoutInfo struct {    conn    *Conn         // 衔接信息    latestTime time.Time  //心跳的最新时刻}每次收到心跳包都从头更新时刻func AddTimeoutCheckInfo(conn *Conn) {    timeoutTree.Insert(&TimeoutInfo{conn: conn, latestTime: time.Now()})}

独立协程来遍历扫描并铲除超时的衔接:

    for {        // 遍历        item := timeoutTree.Min()        // 取衔接、取最新时刻        latestTime := item.(*TimeoutInfo).latestTime        conn := item.(*TimeoutInfo).conn        // 核算衔接的最新时刻是否超时,超时则封闭衔接和清理        if timeout {             timeoutTree.Delete(item)            conn.Close()        }     }

TCP 长衔接层的负载均衡战略

既然长衔接接入层节点有多个,而且能够随时依据需求扩缩容,然而客户端并不清楚你服务端到底布置了多少台节点,那么客户端该怎样建议衔接呢?怎样做才干确保合理的负载均衡呢?

一般的负载均衡战略如 RR 轮询,是否能够满意 IM 的诉求呢?试想这么一个真实的场景,当时线上有 5 台机器,每台机器负载都很高了,此刻衔接会很不安稳,客户端出现频繁重连。此刻肯定需求扩容,OK,那么扩容了 2 台,然后 client 建连假如仍是轮询,那么新扩容的机器,仍是不能马上涣散其他机器上的压力,压力仍是会往老的机器上面去打,明显不合理。

因而,针对 IM 场景,最合理的负载均衡战略,便是依据衔接数来负载均衡,客户端新建议衔接需求接入的接入层节点必定是衔接数最少的,由于每台节点会需求操控最大衔接数的约束才干确保最优功能,而且能够及时给压力大的节点减压。

怎样完成呢?这儿就需求有一个服务注册发现的组件(如 Etcd)来协助咱们到达诉求。首要,接入层发动后,往 Etcd 里边注册信息,而且再在后续的生命周期中,定时更新当时节点已有的衔接数到 Etcd 中;然后咱们需求有一个 Router Server,这个服务去 watch Etcd 中的接入层节点信息,Etcd 的运用能够参阅etcd/clientv3;然后实时核算,得到一个列表排序,这个排序是依照节点数最少的节点排序的。

然后 Router Server 供给一个 HTTP 服务的 API 接口,用来回来一切节点中衔接数最少的节点的一批 IP 列表(一般能够 3 个)给到客户端。为何不是回来一个呢?由于咱们回来的节点,或许由于其他原因导致衔接不上,或者衔接不安稳,那么此刻 客户端就能够有备选计划,选择回来的下一个节点建连。

触及点包括:

  • • 接入层注册信息(节点 IP 和 port、节点衔接数)

  • • 路由层 watch 接入层的信息

  • • 路由层核算路由算法

  • • 路由层供给 HTTP 接口回来适宜的节点 IP 列表

TCP 长衔接接入层服务的高雅重启和缩容

关于通用的长衔接接入层而言

长衔接接入层是和用户客户端直接相连的,客户端经过 TCP 长衔接衔接到接入层,因而接入层假如需求重启,那么必定会导致客户端衔接断开,产生重连。假如此刻用户正在发送音讯,那么必定会导致发送反常,然后影响用户体会。

那么咱们需求怎样完成接入层,才干确保重启或者缩容的时分,不影响用户、对用户无感知呢?有这么几个思路:

  1. 1. 接入层做的足够轻量,尽量仅仅坚持 TCP 长衔接和数据包的转发,一切其他事务逻辑,尽量转发到事务层去处理,接入层与事务逻辑层严厉别离;由于事务层逻辑是需求频繁变动,而接入层的长衔接坚持能够做到尽量不变,这样就会尽或许的削减重启。

  2. 2. 接入层尽或许的做到无状况化,便利随时的扩缩容;这样就需求有一个叫用户中心的服务来保存用户的各种状况和信息,如在线状况、离线状况、用户是经过哪个接入层节点衔接的;经过这个方法,用户就能够随意接入到任何接入层节点,而且接入层节点也可随时扩缩容;这样的话,事务逻辑层就能够和用户中心经过 RPC 通讯获取用户的各种衔接信息和是否在线的状况,然后精准下发音讯到指定接入层,然后接入层将音讯下发给客户端用户。

  3. 3. 自动搬迁信令。添加一条信令和客户端进行交互,服务端假如要重启/缩容,那么自动奉告衔接在此接入层节点上的一切客户端,服务端自动发送搬迁信令,比方 publish(搬迁信令,100%),表明发送给一切此接入层节点上的客户端,客户端收到此搬迁信令后,就自动进行从头衔接其他节点。由于是客户端自动断开重连其他节点的,虽然仍是会有重连,可是客户端是自动建议的,因而能够经过代码逻辑来确保从事务逻辑上不会影响用户的体会,这样的话,用户在操作上就会无感知,然后提高用户体会。一起,接入层节点要发送自动搬迁信令之前,需求先从服务发现与注册中心(Etcd)中下线自己,防止重连的时分还持续衔接到此节点。然后当重启之前,也需求判别一下是否当时节点上一切的用户衔接都现已搬迁到其他节点上了。

长衔接接入层的高雅扩容计划

扩容计划是指在线用户越来越多,当时已有的接入层节点现已扛不住了,需求扩容接入层节点来分摊在线用户的衔接和恳求。这儿分两种状况考虑:

  • • 其他节点的压力还相对较小,可是事前预知到需求扩容,也便是提早扩容。此刻依照路由层的最小衔接数优先接入恳求的战略并无不妥,新扩容的能够均摊流量,原有的节点也不会由于压力过大而导致功能问题。

  • • 其他节点压力现已扛不住了,需求紧迫扩容而且快速给老的节点减压。这个时分,假如还仅仅仅仅新增节点,然后依据原有的负载均衡路由战略来减压是达不到减压效果的,由于只要新的衔接才会接入到新扩容的节点;原有老的节点上的衔接假如没有断连那么仍是持续坚持在原有节点上,因而底子不能给老的节点减压。

  • • 所以,就需求服务端有更好的机制,经过服务端的机制来促进客户端从头衔接到新的节点上,然后进行减压。这儿,仍是需求一个搬迁信令,可是这个信令服务端仅仅需求随机发送给部分份额的用户,比方 publish(搬迁信令,30%),表明发送搬迁信令给 30% 份额的用户,让这 30%的用户重连到新的节点上。

TCP 长衔接层节点怎样防止进犯

基本的防火墙战略

公司内常规的防火墙战略,经过 iptable 设置 iptables 的防火墙战略。比方约束只能接收指定 IP 和 Port 的包,防止进犯者经过节点上其他端口的缝隙登录机器;比方只接收某些协议(TCP)的包。

SYN 进犯

SYN 进犯是一个典型的 DDOS 进犯,详细便是进犯客户端在短时刻内假造很多不存在的 IP 地址,然后向服务端发送 TCP 握手衔接的 SYN 恳求包,服务端收到 SYN 包后会回复 ACK 承认包,并等待客户端的 ACK 承认。可是,由于源 IP 地址不是真实有效的,因而服务端需求不断的重发直至 63s 超时后才会断开衔接。这些假造的 SYN 包将长时刻占用未衔接行列,引起网络堵塞乃至体系瘫痪,让正常的 TCP 握手衔接恳求不能处理。经过 netstat -n -p TCP | grep SYN_RECV 能够检查是否有很多 SYN_RECV 状况,假如有则或许存在 SYN 进犯。

Linux 在体系层面上,供给了三个选项来应对相关进犯:

  • • tcp_max_syn_backlog,增大 SYN 衔接数

  • • tcp_synack_retries,削减重试次数

  • • tcp_abort_on_overflow,过载直接丢掉,回绝衔接

别的,还有一个 tcp_syncookies 参数能够缓解,当 SYN 行列满了后,TCP 会经过相关信息(源 IP、源 port)制造出一个 SYN Cookie 回来,假如是进犯者则不会有响应,假如是正常衔接,则会把这个 SYN Cookie 发回来,然后服务端能够经过 SYN Cookie 建衔接。

TCP 长衔接层面上

黑名单机制

能够静态或者动态装备黑名单列表,处于黑名单中的 IP 列表则直接回绝 accept 建连;服务端执行 accept 之后,首要先判别 remote IP 是否存在于黑名单中,假如是则直接 close 衔接;假如不是则持续下一步。

约束建连速度

IM 体系为了防止歹意进犯,需求防止单个 IP 很多频繁建连,防止反常 socket 衔接数爆满;因而需求约束每个 IP 每秒树立速度,假如单个 IP 在单位时刻内建连的衔接数超越必定阈值(如 100)该值,则将 IP 列入黑名单而且一起封闭此衔接

怎样完成呢?分如下几步。

1. 定义一个合理的防进犯的数据结构,里边包括 connRates 字段、startTime 字段。

  • • startTime 表明此衔接接入的初始时刻

  • • connRates 用来对核算时刻内的接入 IP 做累加

2. 服务端每次 accept 之后,针对这个 Conn 衔接,先判别当时时刻和此衔接的 startTime 的差值是否现已超越一个核算周期,假如超越则清零重置;假如没有超越,则对此衔接的 IP 做累加。

3. 然后判别 IP 累加的成果是否超越阈值,假如超越则参加黑名单而且 close 衔接;假如没有超越则进行下一步的恳求。

约束发包速度

IM 体系要能够发送音讯包,必定需求先进行登录操作,登录主要是为了鉴权,然后获取得到正确的 token,才干正常登录。为了防止 token 等被盗取,为了更为安全,登录之后发送音讯的频率也需求进行操控;操控的机制便是针对单个衔接约束每秒处理包的上限,在单位时刻内收到的包的恳求数量超越必定阈值(如 100p/s)则直接丢掉。

怎样完成呢?需求几个步骤:

  • • 针对每个 Conn 的数据结构,添加一个 packetsNum 字段;

  • • 当时 Conn 每收到一个包,先核算核算时刻内 packetsNum 的次数是否超越阈值,然后 packetsNum++;假如超越阈值则丢包并回来错误;

  • • 开一个定时器,每隔一个核算时刻周期,清零 packetsNum。

TLS 加密传输

TLS 安全传输层协议用于在两个通讯运用程序之间供给保密性和数据完整性,是咱们 IM 体系中确保音讯传输过程中不被截获、篡改、假造的常用手法。

TLS 过程运用到了对称加密、非对称加密、CA 认证等,安全性十分高;可是相比于 TCP 传输会多了几个秘钥相关的环节,然后导致整个握手阶段会多出 1~2 个 RTT 的耗时;不过仅仅握手阶段的耗时对咱们 IM 的运用场景并不影响。为此,为了安全性,尽或许的运用 TLS 来树立 TCP 衔接

这边文章首发在我微信大众号【后端体系和架构】中,点击这儿能够去往大众号检查原文链接,假如对你有协助,欢迎前往重视,愈加便利快捷的接收最新优质文章

本文正在参加「金石计划 . 分割6万现金大奖」