如何思考问题, 如何提升自我能力
如果没有自己思考, 现有的东西都是合理的, 这显然是不行的.
每看一个东西, 都要思考, 这个东西合不合理? 是否可以优化? 有哪些类似的? 要想如果怎么样
例如: 刚开始接触闪聊项目的时候, 觉得这个架构不错, 觉得不用优化了, 但是后面需要大规模推广后, 剑冰就提出了一些优化点, 通过量级的提高, 暴露了一些问题
并发大后, mysql 慢请求问题
并发大后, 请求资源并发太多, 连接数太多问题, 因此需要合并资源请求
Access 长连接的问题, Access 服务升级不方便, 因此需要拆分 Access 长连接, 提升稳定性. 方便服务升级.
除了熟悉代码框架外, 一定还要深入到细节, 比如 golang 的底层优化, 系统级别的优化.
围绕 im 领域思考问题和量级, 当前的量级是什么级别, 然后需要考虑更高级别要做的事情.
当前级别为 w 级别的时候, 就要考虑十万级别该做的事, 十万级别后, 就要考虑百万, 不一定要马上做, 但是一定要先想, 先考虑, 目前性能如何, 怎么扩展? 怎么重构?
了解业界相关技术方案, 了解别人踩过的坑. 用来后续量大了后, 可以提供更好的技术方法和架构, 往资深 im/im 高级方向发展, 不仅仅限于闪聊. 要能够围绕整个 IM 领域方向思考
业界的架构, 技术方案, 选型, 都需要先了解.
接入层的服务器程序如何升级
对于闪聊 Access 服务而言
那 Access 服务, tcp 长连接的, 如果这样的话, 那不是客户端需要重新登录
目前是, 但是现在开始改造, access 再剥一层出来专门维护长连接
access 分为连接层和 access,前者不涉及业务,所以预期不用重启,后者承载业务,更新重启对连接没有影响.后面还考虑把 push 合进 access
连接层和 access 通过共享内存来维护连接信息.
对于通用接入层而言
调整接入层有状态 => 无状态, 接入层与逻辑层严格分离.
无状态的接入层, 可以随时重启
逻辑服务层, 可以通过 etcd 来做服务发现和注册, 方便升级和扩容
TCP 长连接数能到多少? 限制因素是什么
操作系统包含最大打开文件数 (Max Open Files) 限制, 分为系统全局的, 和进程级的限制
fs.file-max
soft nofile/ hard nofile
每个 tcp 的 socket 连接都要占用一定内存
通过测试验证和相关数据, 只是保持 connect, 什么也不做, 每个 tcp 连接, 大致占用 4k 左右的内存, 百万连接, OS 就需要 4G 以上内存.
这里注意还要修改 net.ipv4.tcp_rmem/net.ipv4.tcp_wmem
网络限制
假设百万连接中有 20% 是活跃的, 每个连接每秒传输 1KB 的数据, 那么需要的网络带宽是 0.2M x 1KB/s x 8 = 1.6Gbps, 要求服务器至少是万兆网卡 (10Gbps).
一些基本常用的 sysctl 的修改:
net.ipv4.tcp_mem = 78643200 104857600 157286400
net.ipv4.tcp_rmem=4096 87380 16777216
net.ipv4.tcp_wmem=4096 87380 16777216
net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_recycle=1
net.ipv4.tcp_tw_reuse=1
fs.file-max = 1048576
net.ipv4.ip_conntrack_max = 1048576
n = (mempages * (PAGE_SIZE / 1024)) / 10; PAGE_SIZE:typically 4096 in an x86_64 files_stat.max_files = n;
epoll 机制, 长连接数太多, 会影响性能吗? <底层采用红黑树和链表来管理数据>
这个不会影响 tcp 连接和性能, 哪怕 epoll 监控的事件再多, 都 OK
内核除了帮我们在 epoll 文件系统里建了个 file 结点,在内核 cache 里建了个红黑树用于存储以后 epoll_ctl 传来的 socket 外,还会再建立一个 list 链表,用于存储准备就绪的事件,当 epoll_wait 调用时,仅仅观察这个 list 链表里有没有数据即可.
实际应用中应该考虑哪些点呢
网卡多队列的支持, 查看网卡是否支持, 要不然 cpu 不能很好处理网络数据, 这个需要好的网卡, 也消耗 cpu
维护 tcp 长连接的节点管理, 这个需要消耗 cpu, 需要有对应的数据结构来进行管理
实际中, 还应该考虑, 每秒中能够建立连接的速度, 因为百万连接并不是一下就建立的, 如果重启了重连, 那么连接速度如何呢
如果这个节点挂掉了, 请求的分摊处理怎么弄
应用层对于每个连接的处理能力怎样? 服务端对协议包的解析处理能力如何
tcp mem 问题, 没有用到就不会分配内存, 但是不一定会马上回收.
关于长连接的另外考虑点:
在稳定连接情况下,长连接数这个指标,在没有网络吞吐情况下对比,其实意义往往不大,维持连接消耗 cpu 资源很小,每条连接 tcp 协议栈会占约 4k 的内存开销,系统参数调整后,我们单机测试数据,最高也是可以达到单实例 300w 长连接.但做更高的测试,我个人感觉意义不大.
实际网络环境下,单实例 300w 长连接,从理论上算压力就很大:实际弱网络环境下,移动客户端的断线率很高,假设每秒有 1000 分之一的用户断线重连.300w 长连接,每秒新建连接达到 3w,这同时连入的 3w 用户,要进行注册,加载离线存储等对内 rpc 调用,另外 300w 长连接的用户心跳需要维持, 假设心跳 300s 一次,心跳包每秒需要 1w tps.单播和多播数据的转发,广播数据的转发,本身也要响应内部的 rpc 调用,300w 长连接情况下,gc 带来的压力,内部接口的响应延迟能否稳定保障.这些集中在一个实例中,可用性是一个挑战.所以线上单实例不会 hold 很高的长连接, 实际情况也要根据接入客户端网络状况来决定.
注意的一点就是 close_wait 过多问题,由于网络不稳定经常会导致客户端断连,如果服务端没有能够及时关闭 socket,就会导致处于 close_wait 状态的链路过多.
close_wait 状态的链路并不释放句柄和内存等资源,如果积压过多可能会导致系统句柄耗尽,发生 "Too many open files" 异常,新的客户端无法接入,涉及创建或者打开句柄的操作都将失败.
考虑到不同地区不同网络运营商的情况下,用户可能因为网络限制,连接不上我们的服务或者比较慢.
我们在实践中就发现,某些网络运营商将某些端口封禁了,导致部分用户连接不上服务.为了解决这个问题,可以提供多个 ip 和多个端口,客户端在连接某个 ip 比较慢的情况下,可以进行轮询,切换到一个更快的 ip.
TCP_NODELAY
针对这个话题,Thompson 认为很多在考虑微服务架构的人对 TCP 并没有充分的理解.在特定的场景中,有可能会遇到延迟的 ACK,它会限制链路上所发送的数据包,每秒钟只会有 2-5 个数据包.这是因为 TCP 两个算法所引起的死锁:Nagle 以及 TCP Delayed Acknowledgement.在 200-500ms 的超时之后,会打破这个死锁,但是微服务之间的通信却会分别受到影响.推荐的方案是使用 TCP_NODELAY,它会禁用 Nagle 的算法,多个更小的包可以依次发送.按照 Thompson 的说法,其中的差别在 5 到 500 req/sec.
tcp_nodelay 告诉 nginx 不要缓存数据,而是一段一段的发送 -- 当需要及时发送数据时,就应该给应用设置这个属性,这样发送一小块数据信息时就不能立即得到返回值.
我们发现 gRPC 的同步调用与 Nagle's algorithm 会产生冲突,虽然 gRPC 在代码中加入了 TCP_NODELAY 这个 socketopt 但在 OS X 中是没有效果的.后来通过设定 net.inet.tcp.delayed_ack = 0 来解决,同样我们在 linux 下也设置了 net.ipv4.tcp_low_latency = 1,这样在 100M 带宽下一次同步调用的时间在 500us 以下.而且在实际应用中,我们通过 streaming 调用来解决大量重复数据传输的问题,而不是通过反复的同步调用来传相同的数据,这样一次写入可以在 5us 左右.其实批量写入一直都是一个很好的解决性能问题的方法 S
心跳相关处理
心跳其实有两个作用
心跳保证客户端和服务端的连接保活功能, 服务端以此来判断客户端是否还在线
心跳还需要维持移动网络的 GGSN.
最常见的就是每隔固定时间 (如 4 分半) 发送心跳, 但是这样不够智能.
心跳时间太短, 消耗流量 / 电量, 增加服务器压力.
心跳时间太长, 可能会被因为运营商的策略淘汰 NAT 表中的对应项而被动断开连接
心跳算法 (参考 Android 微信智能心跳策略)
维护移动网 GGSN(网关 GPRS 支持节点)
大部分移动无线网络运营商都在链路一段时间没有数据通讯时,会淘汰 NAT 表中的对应项,造成链路中断.NAT 超时是影响 TCP 连接寿命的一个重要因素 (尤其是国内),所以客户端自动测算 NAT 超时时间,来动态调整心跳间隔,是一个很重要的优化点.
参考微信的一套自适应心跳算法:
为了保证收消息及时性的体验,当 app 处于前台活跃状态时,使用固定心跳.
app 进入后台(或者前台关屏)时,先用几次最小心跳维持长链接.然后进入后台自适应心跳计算.这样做的目的是尽量选择用户不活跃的时间段,来减少心跳计算可能产生的消息不及时收取影响.
精简心跳包,保证一个心跳包大小在 10 字节之内, 根据 APP 前后台状态调整心跳包间隔 (主要是安卓)
弱网环境下的相关处理
网络加速 cdn
包括信令加速点和图片 CDN 网络
协议精简和压缩
使用压缩算法, 对数据包进行压缩
TCP 第一次通过域名连接上后,缓存 IP,下次进行 IP 直连;若下次 IP 连接失败,则重新走域名连接
对于大文件和图片等, 使用断点上传和分段上传
平衡网络延迟和带宽的影响
在包大小小于 1500 字节时, 尽量合并请求包. 减少请求
ip 就近接入
ip 直连 (域名转 ip)
域名解析 (ip 库), 域名解析的耗时在移动网络中尤其慢
计算距离用户地理位置最近的同一运营商的接入点
断线重连策略
掉线后,根据不同的状态需要选择不同的重连间隔.如果是本地网络出错,并不需要定时去重连,这时只需要监听网络状态,等到网络恢复后重连即可.如果网络变化非常频繁,特别是 App 处在后台运行时,对于重连也可以加上一定的频率控制,在保证一定消息实时性的同时,避免造成过多的电量消耗.
断线重连的最短间隔时间按单位秒 (s) 以 4,8,16...(最大不超过 30)数列执行,以避免频繁的断线重连,从而减轻服务器负担.当服务端收到正确的包时,此策略重置
有网络但连接失败的情况下,按单位秒 (s) 以间隔时间为 2,2,4,4,8,8,16,16...(最大不超过 120)的数列不断重试
重连成功后的策略机制
合并部分请求, 以减少一次不必要的网络请求来回的时间
简化登录后的同步请求,部分同步请求可以推迟到 UI 操作时进行,如群成员信息刷新.
在重连 Timer 中,为了防止雪崩效应的出现,我们在检测到 socket 失效 (服务器异常),并不是立马进行重连,而是让客户端随机 Sleep 一段时间(或者上述其他策略) 再去连接服务端,这样就可以使不同的客户端在服务端重启的时候不会同时去连接,从而造成雪崩效应.
网络切换怎么处理? 是否需要重连, 是否重新登录?
一般的话, 有网络切换 (3g->4g->wifi->4g) 就重连, 重新走一遍整体流程
最好 APP 能以尽量少的通讯量来重新注册服务器, 比如不再从服务器获取配置信息, 从上一次拉取的服务器配置的缓存数据直接读取 (如果服务器改变, 最好能够发一条通知给 app 更新)
如从 wifi 切换到 4G,处于地铁,WIFI 边缘地带等,为避免造成重连风暴 (因为网络不稳定, 会频繁发起重连请求), 可以采用稍加延迟重连策略
服务端程序怎么扩容 / 缩容? 水平扩展方案?
采用业界常用的分布式服务发现, 配置方案. 如通过 etcd 来进行服务发现和注册.
设计的各个模块要能独立化部署, 设计为无状态,例如所谓的微服务, 这样才能够很好的做服务的升级,扩容, 保证无单点故障, 也方便灰度发布更新
动态配置
群消息相关
消息是写扩散, 还是读扩散: 群里面每个人都写一次相同的消息, 还是群里面都从同一个地方读取这条相同消息
写扩散: 简单, 但是群里面每个人都要写一遍缓存. 数据量有点大, 而且增加网络消耗 (比如写 redis 的时候).
读扩算: 只写一份到缓存, 拉取的时候, 从这个群缓存消息里面拉, 需要增加一点逻辑处理, 方便在所有群成员都拉取完后删掉缓存数据 (或者过期)
发送方式
遍历群成员, 如果在线就依次发送, 但是群成员多, 群活跃的时候, 可能会增大压力.
遍历群成员, 在线人员, 服务内部流转 (rpc) 的时候是否可以批量发送
闪聊的群方式
在线的, msg 只有一份到 db 中, index 还是写扩散到 cache 和 db 中.
离线的, 缓存中, 写扩散 (msg 和 index), 如果缓存失效, 则穿透到 db 中拉取.
对于群消息, 每条消息都需要拉取群成员的在线状态. 如果存放在 redis, 拉取会太过频繁. 连接数会暴增, 并发过高. 这样可以增加一级本地缓存, 把连接信息放到本地缓存 (通过消耗内存来减少网络连接和请求)
客户端减小电量消耗策略
不能影响手机休眠, 采用 alarm manager 触发心跳包
尽量减少网络请求, 最好能够合并 (或者一次发送多个请求). 批量,合并数据请求 / 发送
移动网络下载速度大于上传速度, 2G 一次发送数据包不要太大, 3G/4G 一次发送多更省电.
消息是如何保证可达 (不丢)/ 唯一 / 保序?
消息头包含字段 dup, 如果是重复递送的消息, 置位此字段, 用来判定重复递送
服务端缓存对应的 msgid 列表, 客户端下发已收到的最大 msgid, 服务端根据客户端收到的最大 msgid 来判断小于此 id 的消息已经全部被接收. 这样保证消息不丢.
服务端确保 msgid 生成器的极度高的可用性, 并且递增, 通过 msgid 的大小, 来保证消息的顺序
详细说明消息防丢失机制
为了达到任意一条消息都不丢的状态,最简单的方案是手机端对收到的每条消息都给服务器进行一次 ack 确认,但该方案在手机端和服务器之间的交互过多,并且也会遇到在弱网络情况下 ack 丢失等问题.因此, 引入 sequence 机制
每个用户都有 42 亿的 sequnence 空间(从 1 到 UINT_MAX), 从小到大连续分配
每个用户的每条消息都需要分配一个 sequence
服务器存储有每个用户已经分配到的最大 sequence
手机端存储有已收取消息的最大 sequence
** 方案优点 **
根据服务器和手机端之间 sequence 的差异,可以很轻松的实现增量下发手机端未收取下去的消息
对于在弱网络环境差的情况,丢包情况发生概率是比较高的,此时经常会出现服务器的回包不能到达手机端的现象.由于手机端只会在确切的收取到消息后才会更新本地的 sequence,所以即使服务器的回包丢了,手机端等待超时后重新拿旧的 sequence 上服务器收取消息,同样是可以正确的收取未下发的消息.
由于手机端存储的 sequence 是确认收到消息的最大 sequence,所以对于手机端每次到服务器来收取消息也可以认为是对上一次收取消息的确认.一个帐号在多个手机端轮流登录的情况下,只要服务器存储手机端已确认的 sequence,那就可以简单的实现已确认下发的消息不会重复下发,不同手机端之间轮流登录不会收到其他手机端已经收取到的消息.
通信方式 (TCP/UDP/HTTP) 同时使用 tcp 和 http.
IM 系统的主要需求:包括账号,关系链,在线状态显示,消息交互(文本,图片,语音),实时音视频
http 模式(short 链接)和 tcp 模式(long 链接),分别应对状态协议和数据传输协议
保持长连接的时候, 用 TCP. 因为需要随时接受信息. 要维持长连接就只能选 TCP, 而非 UDP
获取其他非及时性的资源的时候, 采用 http 短连接. 为啥不全部用 TCP 协议呢? 用 http 协议有什么好处
目前大部分功能可以通过 TCP 来实现.
文件上传下载的话, 就非 http 莫属了
支持断点续传和分片上传.
离线消息用拉模式,避免 tcp 通道压力过大,影响即时消息下发效率
大涂鸦,文件采用存储服务上传,避免 tcp 通道压力过大
IM 到底该用 UDP 还是 TCP 协议
UDP 和 TCP 各有各的应用场景,作为 IM 来说,早期的 IM 因为服务端资源(服务器硬件,网络带宽等)比较昂贵且没有更好的办法来分担性能负载,所以很多时候会考虑使用 UDP,这其中主要是早期的 QQ 为代表.
TCP 的服务端负载已经有了很好的解决方案,加之服务器资源成本的下降,目前很多 IM,消息推送解决方案也都在使用 TCP 作为传输层协议.不过,UDP 也并未排除在 IM,消息推送的解决方案之外,比如:弱网络通信(包括跨国的高延迟网络环境),物联网通信,IM 中的实时音视频通信等等场景下,UDP 依然是首选项.
关于 IM 到底该选择 UDP 还是 TCP,这是个仁者见仁智者见智的问题,没有必要过于纠结,请从您的 IM 整体应用场景,开发代价,部署和运营成本等方面综合考虑,相信能找到你要的答案.
服务器和客户端的通信协议选择
常用 IM 协议: IM 协议选择原则一般是: 易于拓展,方便覆盖各种业务逻辑,同时又比较节约流量.后一点的需求在移动端 IM 上尤其重要
xmpp: 协议开源,可拓展性强,在各个端 (包括服务器) 有各种语言的实现,开发者接入方便.但是缺点也是不少: XML 表现力弱,有太多冗余信息,流量大,实际使用时有大量天坑.
MQTT: 协议简单,流量少,但是它并不是一个专门为 IM 设计的协议,多使用于推送. 需要自己在业务上实现群, 好友相关等等. 适合推送业务,适合直播 IM 场景.
SIP: 多用于 VOIP 相关的模块,是一种文本协议. sip 信令控制比较复杂
私有协议: 自己实现协议. 大部分主流 IM APP 都是是使用私有协议,一个被良好设计的私有协议一般有如下优点: 高效,节约流量 (一般使用二进制协议),安全性高,难以破解.
协议设计的考量:
网络数据大小--占用带宽,传输效率:虽然对单个用户来说,数据量传输很小,但是对于服务器端要承受众多的高并发数据传输,必须要考虑到数据占用带宽,尽量不要有冗余数据,这样才能够少占用带宽,少占用资源,少网络 IO,提高传输效率;
网络数据安全性--敏感数据的网络安全:对于相关业务的部分数据传输都是敏感数据,所以必须考虑对部分传输数据进行加密
编码复杂度--序列化和反序列化复杂度,效率,数据结构的可扩展性
协议通用性--大众规范:数据类型必须是跨平台,数据格式是通用的
常用序列化协议比较
提供序列化和反序列化库的开源协议: pb,Thrift. 扩展相当方便,序列化和反序列化方便
文本化协议: xml,json. 序列化,反序列化容易, 但是占用体积大.
定义协议考量
包数据可以考虑压缩, 减小数据包大小
包数据考虑加密, 保证数据安全
协议里面有些字段 uint64, 可以适当调整为 uint32. 减小包头大小
协议头里面最好包含 seq_num
这个是为了异步化的支持.这种消息通道最重要的是解决通道问题,所有消息处理不能是同步的,必须是异步的,你发一个消息出去,ABC 三个包,你收到 XYZ 三个包之后,你怎么知道它是对应的,就是对应关系的话我们怎么处理,就是加一个 ID
一个典型的 IM 系统架构设计,还有以下性能方面的热点问题需要设计者重点关注
编码角度:采用高效的网络模型,线程模型,I/O 处理模型,合理的数据库设计和操作语句的优化;
垂直扩展:通过提高单服务器的硬件资源或者网络资源来提高性能;
水平扩展:通过合理的架构设计和运维方面的负载均衡策略将负载分担,有效提高性能;后期甚至可以考虑加入数据缓存层,突破 IO 瓶颈;
系统的高可用性:防止单点故障;
在架构设计时做到业务处理和数据的分离,从而依赖分布式的部署使得在单点故障时能保证系统可用.
对于关键独立节点可以采用双机热备技术进行切
数据库数据的安全性可以通过磁盘阵列的冗余配置和主备数据库来解决.
TCP 拥堵解决方案
TCP 的拥塞控制由 4 个核心算法组成:"慢启动"(Slow Start),"拥塞避免"(Congestion voidance),"快速重传"(Fast Retransmit),"快速恢复"(Fast Recovery).
怎么判断 kafka 队列是否滞后了? 有命令查看吗?
kafka 队列,没有满的概念, 只有消费滞后 / 堆积的概念
通过 offset monitor 监控对 kafka 进行实时监控
对于 kafka
本身就是一个分布式,本身就能给支持这种线性的扩展,所以不会面临这种问题.
你会写数据不消费么.
## 在操作缓存和数据库的时候, 一般用法:
写: 先写数据库, 成功后, 更新缓存
读: 先读缓存, 没有数据则穿透到 db.
但是, 假如我写数据库成功, 更新缓存失败了. 那下次读的时候, 就会读到脏数据 (数据不一致), 这种情况怎么处理
方案:
先淘汰缓存, 再写数据库. 但是如果在并发的时候, 也可能出现不一致的问题, 就是假如淘汰掉缓存后, 还没有及时写入 db, 这个时候来了读请求, 就会直接从 db 里面读取旧数据.
因此, 需要严格保证针对同一个数据的操作都是串行的.
由于数据库层面的读写并发,引发的数据库与缓存数据不一致的问题 (本质是后发生的读请求先返回了),可能通过两个小的改动解决:
修改服务 Service 连接池,id 取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上
修改数据库 DB 连接池,id 取模选取 DB 连接,能够保证同一个数据的读写在数据库层面是串行的
## 数据库为什么要分库分表? 什么情况下分库分表
解决磁盘系统最大文件限制
减少增量数据写入时的锁 对查询的影响,减少长时间查询造成的表锁,影响写入操作等锁竞争的情况. (表锁和行锁) . 避免单张表间产生的锁竞争,节省排队的时间开支,增加呑吐量
由于单表数量下降,常见的查询操作由于减少了需要扫描的记录,使得单表单次查询所需的检索行数变少,减少了磁盘 IO,时延变短
一台服务器的资源(CPU,磁盘,内存,IO 等)是有限的,最终数据库所能承载的数据量,数据处理能力都将遭遇瓶颈.分库的目的是降低单台服务器负载,切分原则是根据业务紧密程度拆分,缺点是跨数据库无法联表查询
当数据量超大的时候,B-Tree 索引的作用就没那么明显了.如果数据量巨大,将产生大量随机 I/O,同时数据库的响应时间将大到不可接受的程度.
数据量超大的时候,B-TREE 的树深度会变深,从根节点到叶子节点要经过的 IO 次数也会增大.当 IO 层数超过 4 层之后,就会变得很慢,其实 4 层 IO,存储的数据都是 TB 级别的了,除非你的数据类型都是 INT 等小类型的.也不能说 BTREE 不起作用,只是说作用没那么明显了.
数据量巨大,就一定是随机 IO 吗?这不一定的,如果都是主键查询,10E 条记录都可以很快返回结果.当用二级索引来查询的时候,就变成随机 IO 了,响应时间是会变慢,这也要看数据的分布.另外他也没说存储介质,如果用 SSD 盘,随机 IO 比 SAS 的强 100 倍,性能也是不错的
关于 goroutine 太多后, 会不会影响性能?(待确定)
goroutine 都是用户态的调度, 协程切换只是简单地改变执行函数栈,不涉及内核态与用户态转化, 上下文切换的开销比较小.
创建一个 goroutine 需要大概 2k(V1.4) 左右的栈空间.
go 有抢占式调度: 如果一个 Goroutine 一直占用 CPU,长时间没有被调度过,就会被 runtime 抢占掉
是不是表示, 在内存够用的条件下, 创建一定量 (比如, 30w,50w) 的 goroutine, 不会因为 cpu 调度原因导致性能下降太多
如果系统里面 goroutine 太多, 可能原因之一就是因为每个 goroutine 处理时间过长, 那么就需要查看为啥处理耗时较长.
给出大概数据,24 核,64G 的服务器上,在 QoS 为 message at least,纯粹推,消息体 256B~1kB 情况下,单个实例 100w 实际用户(200w+)协程,峰值可以达到 2~5w 的 QPS... 内存可以稳定在 25G 左右,gc 时间在 200~800ms 左右(还有优化空间). (来自 360 消息系统分享)
接入层几百万的连接, 怎么管理这些连接 ? 后端数据来了, 怎么快速找到这个请求对应的连接呢 ?
管理 tcp 长连接
一个连接结构. 包含 tcp 连接信息, 上次通信时间, 加解密 sharekey, clientaddr. 还包含一个用户结构
用户结构里面包含 uid, deviceid. name ,version ...., 还包含上面的这个连接, 两者一一对应.
不用 map 来管理, 而是把 tcp 连接信息和 user 信息来进行一一对应, 如果 map 的话, 几百万可能查找起来比较慢.
登录请求的时候, 可以根据这个 tcp 连接信息, 获取 user 信息, 但是此时 user 信息基本没有填充什么数据, 所以就需要根据登录来填充 user 信息结构. 关键是: 在当前 Access 接入服务里面, 会有一个 useMap, 会把 uid 和 user 信息对应起来, 可以用来判断此 uid, 是否在本实例上登录过
返回数据的时候, 可以根据这个 uid, 来获取对应的 user 结构, 然后通过这个结构可以获取对应的 tcp 连接信息, 可以进行发送信息.
另外, 登录登出的时候, 会有另外的连接信息 (uid/topic/protoType/addr...) 添加删除到用户中心
登录成功: UseAddConn
登出下线: UserDelConn
这里的连接信息, 供其他远程服务调用, 如 Oracle.
如果有多个 Access 接入层, 每个接入层都会有一个 useMap 结构.
如果多个终端登录同一个账号, 而且在不同的 Access, 那么就不能通过 useMap 来踢出, 就需要上步说的用户中心来管理踢出
多个 Access, 意味着多个 useMap, 那么就需要保证, 从某个 Access 下发的请求, 一定会回到当前 Access. 怎么保证呢? 把当前 Access 的 ip:addr 一直下发下去, 然后返回的时候, 根据下发的 Access 的 ip:addr 来回到对应的 Access.
然后根据 uid, 来获取当前 uid 对应的 user 结构和 tcp 连接结构.
数据结构: map/hash(红黑树)
管理收发异常, 请求回应 ack, 超时
利用 map 数据结构, 发送 (publish) 完消息后, 立即通过 msgid 和 uid, 把对应的消息体添加到 map 结构.
收到回应后, 删除对应的 map 结构.
超时后, 重新提交 OfflineDeliver. 然后删除对应的 map 结构.
乐逗的 IM, 其连接信息是怎么管理的? 等待补充
异步, 并发的时候, rpc 框架, 怎么知道哪个请求是哪个的呢 ?
client 线程每次通过 socket 调用一次远程接口前,生成一个唯一的 ID,即 requestID(requestID 必需保证在一个 Socket 连接里面是唯一的),一般常常使用 AtomicLong 从 0 开始累计数字生成唯一 ID, 或者利用时间戳来生成唯一 ID.
grpc 也需要服务发现. grpc 服务可能有一个实例. 2 个, 甚至多个? 可能某个服务会挂掉 / 宕机. 可以利用 zookeeper 来管理.
同步 RPC 调用一直会阻塞直到从服务端获得一个应答,这与 RPC 希望的抽象最为接近.另一方面网络内部是异步的,并且在许多场景下能够在不阻塞当前线程的情况下启动 RPC 是非常有用的. 在多数语言里,gRPC 编程接口同时支持同步和异步的特点.
gRPC 允许客户端在调用一个远程方法前指定一个最后期限值.这个值指定了在客户端可以等待服务端多长时间来应答,超过这个时间值 RPC 将结束并返回 DEADLINE_EXCEEDED 错误.在服务端可以查询这个期限值来看是否一个特定的方法已经过期,或者还剩多长时间来完成这个方法. 各语言来指定一个截止时间的方式是不同的
服务性能方面的考虑点
编码角度:
采用高效的网络模型,线程模型,I/O 处理模型,合理的数据库设计和操作语句的优化;
垂直扩展:
通过提高单服务器的硬件资源或者网络资源来提高性能;
水平扩展:
通过合理的架构设计和运维方面的负载均衡策略将负载分担,有效提高性能;后期甚至可以考虑加入数据缓存层,突破 IO 瓶颈;
系统的高可用性:
防止单点故障;
在架构设计时做到业务处理和数据的分离,从而依赖分布式的部署使得在单点故障时能保证系统可用.
对于关键独立节点可以采用双机热备技术进行切换.
数据库数据的安全性可以通过磁盘阵列的冗余配置和主备数据库来解决.
服务器的瓶颈在哪里? grpc? 为啥是 grpc? 为啥消耗 cpu? 怎么解决? 网络一定不会影响吞吐.
采用 uarmy 方式. 可以考虑采用 streaming 方式. 批量发送, 提高效率
uarmy 方式一对一, 并发增大的时候, 连接数会增大
streaming 方式的话, 就是合并多个请求 (批量打包请求 / 响应), 减少网络交互, 减少连接
做过 streaming 的压测,性能说比 unary 高一倍还多
一般服务器都会有个抛物线规律, 随着并发数的增大, 会逐渐消耗并跑满 (cpu / 内存 / 网络带宽 / 磁盘 io), 随之带来的就是响应时间变慢 (时延 Latency 变成长), 而 qps / 吞吐量也上不去.
对于 grpc 而言, 并发数增多后, 能看到实际效果就是延迟增大, 有部分请求的一次请求响应时间达到了 5s 左右 (ACCESS/PUSH), 这样说明时延太长, qps / 吞吐量 = 并发数 / 响应时间. 响应时间太长, 吞吐当然上不去.
为啥响应时间这么长了? 是因为 cpu 跑满了么
还有一个原因倒是响应慢, 那就是最终请求会到 Oracle 服务, 而 oracle 会请求数据资源 (cache/db), oracle 的设计中请求资源的并发增多 (连接数也增多), 导致请求资源的时延增长, 因此返回到上级 grpc 的调用也会增大时延.
因此关键最终又回到了 cpu / 内存 / 网络带宽 / 磁盘 io 这里了
rpc 而言, 连接数增多了, 会导致:
类似 tcp 长连接一样, 每个连接肯定要分配一定的内存
要同时处理这么多连接, 每个连接都有相应的事务, cpu 的处理能力要强
后来经过调查我们发现 gRPC 的同步调用与 Nagle's algorithm 会产生冲突,虽然 gRPC 在代码中加入了 TCP_NODELAY 这个 socketopt 但在 OS X 中是没有效果的.后来通过设定 net.inet.tcp.delayed_ack = 0 来解决,同样我们在 linux 下也设置了 net.ipv4.tcp_low_latency = 1,这样在 100M 带宽下一次同步调用的时间在 500us 以下.而且在实际应用中,我们通过 streaming 调用来解决大量重复数据传输的问题,而不是通过反复的同步调用来传相同的数据,这样一次写入可以在 5us 左右.其实批量写入一直都是一个很好的解决性能问题的方法
服务器在北京, 客户端在广州, 如何能够快速接入? 除了走 cdn 还有其他方式没 ?
如果只有一个数据中心, 暂时除了 cdn 加速, 没有其他方法.
如果有两个数据中心, 可以采取就近原则, 但是需要两个数据中心的数据进行同步
就近接入: 就是利用 DNS 服务找到离用户最近的机器,从而达到最短路径提供服务
怎么提高在 IM 领域的能力 ?
要能在不压测的情况下, 就能够预估出系统能够支持的 qps. 要能够粗略估算出一次 db 的请求耗时多久, 一次 redis 的请求耗时多少, 一次 rpc 调用的请求耗时多少
系统中有哪些是比较耗时, 比较消耗 cpu 的.
所有系统, 一定都是分为几层, 从上层到底层, 每一步的请求是如何的? 在每个层耗时咋样
系统有没有引入其他资源
性能瓶颈无法是 cpu/io.
db 查询慢, 是为啥慢? 慢一定有原因的
查询一条 sql 语句的时间大致在 0.2-0.5ms(在表数据量不大的情况下, 是否根据索引 id 来查询, 区别不大.)
单台机, qps 为 8k, 是比较少的. qps: 8k, 那么平均请求响应时间: 1/8ms=0.125ms, qps 为 8k, 那么 5 台机器, qps 就是 4w, 同时 10w 人在线, 收发算一个 qps 的话, 那么 qps 减半, 那就是 2w qps, 10w 同时在线, 每个人 3-4s 发一次消息, 需要 qps 到 3w.
之前测试 redis 的时候, 有测试过, 如果并发太高, 会导致拉取 redis 耗时较长, 超过 3s 左右.
正常情况下, 一个人发送一条消息需要耗时至少 5s 左右 (6-8 个字).
要深入提高 IM 技术, 就必须要能够学会分析性能, 找到性能瓶颈, 并解决掉.
还要看别人如微信的一些做法
架构都是逐步改造的, 每个阶段有每个阶段的架构, 一般架构, 初始都是三层 / 四层架构. 然后开始改造, 改造第一阶段都是拆分服务, 按逻辑拆分, 按业务拆分, 合并资源请求, 减少并发数, 减少连接数.
要经常关注一些大数据, 比如注册用户数, 日活, 月活, 留存. 要对数据敏感, 为什么一直不变, 为什么突然增高, 峰值是多少? 目前能抗住多少
关注系统性能指标, cpu, 内存, 网络, 磁盘等数据, 经常观测, 看看有没有异常, 做到提前发现问题, 而不是等到问题出现了再进行解决, 就是是出现问题了再进行解决, 也要保证解决时间是分钟级别的.
完全理解系统底层工具的含义, 如 sar,iosta,dstat,vmstat,top 等, 这些数据要经常观察, 经常看
保证整套系统中所涉及的各个部分都是白盒的
依赖的其他服务是谁负责, 部署情况, 在哪个机房
使用的资源情况, redis 内存多大 ? mysql 数据库多少? 表多少? 主从怎么分布 ? 对于消息: 一主两从, 32 库, 32 表. 对于好友数据: 一主一从, 128 表. 对于朋友圈, 按月分表.
来源: https://juejin.im/post/5a694f9a6fb9a01cb3165dad