IM面试题

1.消息存储中,内容表和索引表如果需要分库处理,应该按什么字段来哈希? 索引表可以和内容表合并成一个表吗?

答: 内容表应该按主键消息ID来哈希做分库分表处理,这样便于定位某一条具体的消息;索引表应该按索引的用户UID来哈希做分库分表处理,这样可以使得当前用户的所有联系人都落在一张表上,减少遍历所有表的麻烦。 索引表可以与内容表合成一张表,好处是显而易见的,能减少拉取历史消息时的数据库IO,不好的地方就是消息内容冗余存储,浪费了空间。

2.能从索引表里获取到最近联系人所需要的信息,为什么还需要单独的联系人表呢?

答: 如果从索引表中获取一个用户的所有联系人信息(包括最后一条聊天内容和时间)的话,SQL语句中会有分组后取top 1的操作,性能不理想; 另外当前用户与单个联系人之间的未读数需要维护,用联系人表的一个字段来存储,比用索引表方便许多。

3.TCP 长连接的方式是怎么实现“当有消息需要发送给某个用户时,能够准确找到这个用户对应的网络连接”?

答: 首先用户有一个登陆的过程: (1)tcp客户端与服务端通过三次握手建立tcp连接;(2)基于该连接客户端发送登陆请求;(3)服务端对登陆请求进行解析和判断,如果合法,就将当前用户的uid和标识当前tcp连接的socket描述符(也就是fd)建立映射关系; (4)这个映射关系一般是保存在本地缓存或分布式缓存中。 然后,当服务端收到要发送给这个用户的消息时,先从缓存中根据uid查找fd,如果找到,就基于fd将消息推送出去。

4.有了 TCP 协议本身的 ACK 机制,为什么还需要业务层的 ACK 机制?

答:这个问题从操作系统(linux/windows/android/ios)实现TCP协议的原理角度来说明更合适:
     1 操作系统在TCP发送端创建了一个TCP发送缓冲区,在接收端创建了一个TCP接收缓冲区;
     2 在发送端应用层程序调用send()方法成功后,实际是将数据写入了TCP发送缓冲区;
     3 根据TCP协议的规定,在TCP连接良好的情况下,TCP发送缓冲区的数据是“有序的可靠的”到达TCP接收缓冲区,然后回调接收方应用层程序来通知数据到达;
     4 但是在TCP连接断开的时候,在TCP的发送缓冲区和TCP的接收缓冲区中可能还有数据,那么操作系统如何处理呢?
           首先,对于TCP发送缓冲区中还未发送的数据,操作系统不会通知应用层程序进行处理(试想一下:send()函数已经返回成功了,后面再告诉你失败,这样的系统如何设计?太复杂了...),通常的处理手段就是直接回收TCP发送缓存区及其socket资源;
           对于TCP接收方来说,在还未监测到TCP连接断开的时候,因为TCP接收缓冲区不再写入数据了,所以会有足够的时间进行处理,但若未来得及处理就发现了连接断开,仍然会为了及时释放资源,直接回收TCP接收缓存区和对应的socket资源。

总结一下就是: 发送方的应用层程序,调用send()方法返回成功的时候,数据实际是写入到了TCP的发送缓冲区,而非已经被接收方的应用层程序处理。怎么办呢?只能借助于应用层的ACK机制。即使数据成功发送到接收方设备了,tcp层再把数据交给应用层时也可能出现异常情况,比如存储客户端的本地db失败,导致消息在业务层实际是没成功收到的。这种情况下,可以通过业务层的ack来提供保障,客户端只有都执行成功才会回ack给服务端。

5.在即时消息收发场景中,用于保证消息接收时序的序号生成器为什么可以不是全局递增的?

答: 这是由业务场景决定的,这个群的消息和另一个群的消息在逻辑上是完全隔离的,只要保证消息的序号在群这样的一个局部范围内是递增的即可; 当然如果可以做到全局递增最好,但是会浪费很多的资源,却没有带来更多的收益。

6.TLS 能识别客户端模拟器仿冒用户真实访问的问题吗?如果不能有什么其他更好的办法?

答: TLS 是传输层的加密协议,是用来保证消息传输过程中不被截获、篡改和伪造的,但是无法识别仿冒的真实用户。
         客户端模拟器如果像真实用户一样来访问服务端,其实是没有必要去识别的,因为此时模拟器一般是为了帮助真实用户做一些事情,没有恶意行为;如果存在恶意行为,进行识别的办法是通过机器学习的方式进行识别,例如:客户端模拟器会频繁发送消息,针对这一特征,可以对线上访问流量进行甄别。

7.类似 Redis+Lua 的原子化嵌入脚本的方式,是否真的能够做到“万无一失”的变更一致性?比如,执行过程中机器掉电会出现问题吗?

redis在执行lua脚本过程中如果发生掉电,是可能会导致两个未读不一致的,因为lua脚本在redis中的执行只能保证多条命令会原子执行,整体执行完成才会同步给从库并写入aof,所以如果执行过程中掉电,会直接导致被中断的后面部分的脚本得不到执行。当然, 实际情况中这种概率非常小。作为兜底的方案,可以在未读变更时如果会话比较少,可以获取一次全量的会话未读来覆盖总未读,从而有机会能得到最终一致。

8.心跳机制中可以结合 TCP 的 keepalive 和应用层心跳来一起使用吗?

如果从实现功能角度看,传输层和应用层的心跳机制没有结合的必要,因为传输层的心跳探测连接可用性,应用层的心跳机制也可以完成探测; 但从debug角度看,应用层的心跳探测机制无法定位是网络的问题还是系统的问题,此时由传输层辅助就非常好,但实现会相对复杂!

9.二分法来进行心跳探测 的逻辑?

其实就是下一次动态调整的心跳间隔是:当前已经确认的安全的心跳间隔最大值 和 已经确认的心跳探测过大的最小值 的中间均值。比如上一次心跳间隔是4分钟,而且连续N次都成功ack了,那么当前已经确认的安全的心跳间隔是4分钟,假设已经确认10分钟时心跳间隔过大了,那么下一次调整的心跳就是 (4 + 10) / 2 = 7分钟。

10.tcp的keepalive怎么开启?

比如netty下可以通过如下代码开启:

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

另外tcp keepalive的心跳间隔的配置也需要修改一下系统的/etc/sysctl.conf,类似下面:

net.ipv4.tcp_keepalive_time=120
net.ipv4.tcp_keepalive_intvl=30
net.ipv4.tcp_keepalive_probes=3

11.如果用户的离线消息比较多,有没有办法来减少用户上线时离线消息的数据传输量?

答: 用户所有的离线消息对用户来说,并不都是关心和感兴趣的,用户可能只是看了与某个最近联系人的最近的几条消息后,之前的都不想看了,所以这个时候如果将之前的离线消息都拉到本地是非常浪费资源的。通常的做法是:
1 将用户的所有离线消息,按联系人进行分开;
2 用户登录后进入与联系人的聊天窗口时,首先加载与该联系人的最近的10条离线消息;
3 当用户用手滑动手机屏幕的时候,再分页拉取10条。

12.通过长连接的接入网关机,缩容时与普通的 Web 服务机器缩容相比有什么区别?

普通的Web服务器机器提供http的短连接服务,缩容时拿掉机器,会导致前端连接失败,但通过nginx的负载均衡算法,会使重连的客户端连接到另外一台服务器上,这对客户端来说,基本是无感知的; 但是长连接的接入网关机,在缩容拿掉机器时,会导致这台机器上的所有的长连接全部断掉,此时是会影响到所有连接到这台网关机的所有用户,当然通过入口调度服务,客户端可以通过重连连接到新的网关机上,但是用户的体验始终是不好的。

13.为了避免每条消息都查询用户的在线状态,所有的消息都发送给所有的网关节点,这样也会造成每台网关机器的流量成倍数增长吧。这样,是不是会影响消费者推送消息的速率呢?毕竟,如果有50台网关节点,原来每台网关节点只需要取1条消息,现在却需要取50条消息,其中有49条是无效的。

所以这个需要一个权衡,如果业务场景大部分都是点对点场景那么使用全局在线状态来精确投递是更好的选择,如果是群聊和直播类似扇出较大的场景推荐使用所有网关来订阅全量消息的方式。

14.上下线通知好友时,是要先查询好友们的在线状态以取得他们所连接的服务器,然后向这些服务器推送上下线消息吗? 从几亿人的在线状态数据中,查询出几百个在线好友,有什么优化手段吗?

一个用户的好友是有限的,在线状态如果是通过中央kv型存储的,并发查询几百个好友也并不是个问题,性能上不会太慢,只是存储压力会比较大。如果真要优化,好友数太多的情况下,我个人觉得可以把这个用户的好友查出后,组装成一条特殊消息下发给所有网关机,由各台网关机认领各自本机维护的这些好友中的那些在本机登录连接的,然后push上下线消息就可以。

15.自动熔断机制中,如何来确认 Fail-fast 时的熔断阈值(比如:当单位时间内访问耗时超过 1s 的比例达到 50% 时,对该依赖进行熔断)

一方面做压力测试 另外一方面可以做半熔断机制 结合流控 只放行50%的流量。

16.请问做限流有好的开源代码推介吗?

单机限流推荐guava的RateLimiter,全局限流直接基于Redis+Lua写一个很简单。
对,限流阈值需要模拟压测,避免由于阈值设置太宽松导致服务仍然可能被拖死或者阈值太敏感导致一点抖动也会整体熔断。

 

猜你喜欢

转载自blog.csdn.net/madongyu1259892936/article/details/106003906