面试常用套路
墨菲定理:任何事情都没有表面看起来那么简单;所有的事情都会比你预估的时间长;会出错的事情总会出错;你总是担心的事情,它总会发生的。
如何规范写日志
日志要有分隔符
大多数时候使用|
作为分隔符。分析数据的时候直接用分隔符拆分对应的字段和属性。
1 | # 正确例子 |
通过UUID编号来保证日志的连贯性
每次请求都应该有一个唯一编号,每记录一次日志还应该有一个唯一编号。例如:
1 | api.ERROR: 79a8ea37dceff105|0|responseObj is error:{"return_code":"SUCCESS","return_msg":"OK"} |
79a8ea37dceff105
是本次请求的全局uuid,0,1表示记录的顺序编号。这样能保证一次请求的所有日志都可追踪,可查看链路信息。下面是一个简单的代码:
1 | class Logger { |
打印日志的时候会自动添加请求编号实现日志链路的追踪。
对于数组、对象类型统一用JSON格式
比较通用,方便解析。特别是添加字段的时候节省大量的维护成本。
重要日志要脱敏
用户绑定手机号或者邮箱时,会把手机号和邮箱作为参数传到服务端,我们在记录日志时应该把用户手机号和邮箱做脱敏处理,比如中间几位用*号代替。还有密码,身份证等敏感信息更要脱敏。日志是最容易泄露的数据,很难去保护,如果哪天大量用户的手机号等信息泄露可能就是日志未脱敏惹的祸,这个严重的大锅只能自己背。
怎么限制文件上传大小?原理是什么
express中使用的是multer处理文件上传(multipart/form-data),用户上传大文件不仅会占用大量的内存资源,也会浪费带宽和磁盘。multer内部依赖的是busboy,这是一个解析stream为HTML的模块,代码比较简单:Busboy继承了可写流,并重写了_write
方法:
1 | function Busboy(opts) { |
在parseHeaders方法中根绝HTTP的header选择合适的parser:
1 | Busboy.prototype.parseHeaders = function(headers) { |
一般上传文件的content-type是multipart,所以调用的是Multipart,先看下这个类的初始化,中间有一个计算文件限制大小的:
1 | var fileSizeLimit = (limits && typeof limits.fileSize === 'number' |
上面计算的这个文件大小阈值会在onData回调函数中用到:
1 | onData = function(data) { |
可以看出当文件大小超过限制的时候移除了data的事件监听,从而后续不会触发data事件了,并且触发了limit,事件,这个事件在multer层接收,并返回错误:
1 | fileStream.on('limit', function () { |
将错误回调到express中返回给前端。原理就是流式读取,当发现读取的字节数超过限制的时候立即结束,关闭流,并且报错。
如何理解 NIO 中的 Selector
selector是多路复用器,是NIO中的核心。
API 网关有什么用
SpringCloud中的API网关可以实现认证登陆、授权、限流、日志、监控。常见的组件有:
- 基于nginx的二次开发:KONG,API Umbrella
- Zuul:netflix提供的,包括Zuul1,Zuul2
- Spring Cloud Gateway
- Linkerd
如何进行接口的鉴权
JWT
由于HTTP的无状态性,客户端需要携带凭证来调用接口。传统web依赖cookie和session。
而原生app不是基于浏览器的,常用的解决方案是JWT。它的原理是服务端认证完成后生成一个JSON对象,返回给用用户,后续客户端的所有请求都会带上这个JSON对象。
如上图所示,一个JWT分为3个部分:Header,Payload,Signature。左边的字符串以.
分割后解码(Base64)可以得到右边的部分。其中最关键的是Signature:编码后的header和编码后的payload以及一个秘钥使用header中指定的签名算法进行签名:
1 | signature = HMACSHA256(base64Encode(header) + '.' + base64Encode(payload),secert) |
因为秘钥secert保存在服务器,即使别人拿到之后篡改了token中的数据,整个字符串就发生了变化,服务端就会鉴权失败,这样就可以保证安全性。一个基于Koa的实现如下:
1 | const Koa = require('koa'); |
进行统一错误处理的时候其实遇到了一个坑:原先采用的不是错误处理中间件而是使用监听koa的全局onerror事件,代码如下:
1 | app.on('error',(err,ctx) => { |
但是在路由中抛出错误的时候捕获到了错误,但是前端还是得到的字符串是Internal Server Error
,状态码也是500,仔细查看源码发现koa对于每个请求中的错误调用的是其Context上的onerror
1 | // application.js |
ctx中的onerror事件通过this.app.emit('error', err, this);
将错误委托给application,接下来移除了httpHeaders,设置http状态码,最后最关键的一句话是res.end(msg)
,这句话关闭http的输入流,所以即使我们后面自己设置ctx.body也不会响应任何信息给前端了,而msg默认就是Internal Server Error。所以这里使用application的的onerror事件处理错误其实是有坑的,而对于ctx的onerror方法这是个私有api官方并没有提供重写的方法。所以错误处理还是尽量选用中间件吧。
系统调优
- 高并发服务器建议调小 TCP 协议的 time_wait 超时时间。说明:操作系统默认 240s 后才会关闭 time_wait 状态的连接。在高并发访问下,服务器端会因为处于 time_wait 的连接数过多,而无法建立新的连接,所以需要在服务器上调小此等待值。
- 给 JVM 设置
-XX:+HeapDumpOnOutOfMemoryError
参数,让 JVM 碰到 OOM 场景时输出 dump 信息。说明:OOM 的发生是有概率的,甚至有规律地相隔数月才出现一例,出现时的现场信息对查错非常有价值。 - 在线上生产环境,JVM 的 Xms 和 Xms 设置一样大小的内存容量,避免在 GC 后调整堆大小带来的压力。
为什么 DNS 服务器要采用分层结构
互联网的规模太大,域名数量更是不胜枚举。一台DNS Server存储是不现实的。因此DNS在设计的时候就是分层架构,每一部分存储下一级的相关信息。
举个例子,我们想知道 www.example.com 的 IP 地址是什么,将这个请求发送给了我们的 DNS Server。
DNS Server 需要先问根域名服务器,谁负责管理 .com?然后再问 .com 域名服务器,谁负责管理 example.com?最后,再问 example.com 域名服务器,www.example.com 的 IP 地址是什么,从而获得答案返回给我们。
以上就是域名的分级架构,域名查询需要从根开始,一级一级向下,直到找到答案。当然,因为域名查询是一个高频词的动作,无时无刻都在发生,如果每次都是这样一层一层获取,效率将十分低下,因此,DNS 系统中大量使用缓存,每一个中间环节都会缓存相关结果来节省时间提高效率。在不考虑缓存的情况下,我们每次都需要询问根域名服务器,那么DNS服务器是如何知道根域名服务器的地址呢?它其实采用了一个非常简单的做法——使用配置文件写死。全世界一共13组根域名服务器:[a-m].root-servers.net
。IINA提供的配置文件。下载下来之后配置相关的DNS软件即可。
DNS 域名解析的流程
- 检查浏览器缓存
- 检查 OS 缓存
- 检查 hosts 文件
- 请求本地 DNS 服务器
- 请求根域名服务器
DNS 查询的方式可以分为迭代查询和递归查询。
- 迭代查询:向 DNS 服务器查询的时候如果对方无法给出答案,会返回下一级服务器的地址,由解析器自行继续查询
- 递归查询:客户端向 DNS 服务器(例如 ISP 的 DNS)发起请求后,必须返回最终答案。期间解析器会代替客户端完成所有层级的查询。
为什么需要 2 种查询?
- 递归查询:简化客户端逻辑,适合终端用户
- 迭代查询:分散 DNS 层级压力,避免根/TLD 服务器过载。
CDN 的原理以及如何更新缓存
更新缓存本质上回源操作,一般的CDN控制台都有这个功能。
谈谈 TCP 中的状态机
状态 | 描述 |
---|---|
LISTEN | 等待来自远程TCP应用程序的请求 |
SYN_SENT | 发送连接请求后等待来自远程端点的确认。TCP第一次握手后客户端所处的状态 |
SYN-RECEIVED | 该端点已经接收到连接请求并发送确认。该端点正在等待最终确认。TCP第二次握手后服务端所处的状态 |
ESTABLISHED | 代表连接已经建立起来了。这是连接数据传输阶段的正常状态 |
FIN_WAIT_1 | 等待来自远程TCP的终止连接请求或终止请求的确认 |
FIN_WAIT_2 | 在此端点发送终止连接请求后,等待来自远程TCP的连接终止请求 |
CLOSE_WAIT | 该端点已经收到来自远程端点的关闭请求,此TCP正在等待本地应用程序的连接终止请求 |
CLOSING | 等待来自远程TCP的连接终止请求确认 |
LAST_ACK | 等待先前发送到远程TCP的连接终止请求的确认 |
TIME_WAIT | 等待足够的时间来确保远程TCP接收到其连接终止请求的确认 |
TCP三次握手
- 第一次握手:客户端发送SYN包(SYN = J),并进入SYN_SENT状态,等待服务器确认。
- 第二次握手:服务端收到SYN包,确认客户端的SYN(ACK = J + 1),同时发送自己的SYN包(SYN = k),即服务器发送SYN + ACK包,此时服务器进入SYN_RCVD状态。
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包(ACK = K + 1).此包发送完毕后客户端和服务器进入ESTABLISHED状态,握手完成。
下图显示了TCP三次握手的过程,以及客户端和服务端状态的变化。
半连接队列是什么
服务器第一次收到客户端的SYN之后就会进入到SYN_RCVD状态,此时双方还没有完全建立连接,服务器会把这种状态下的请求放入到一个队列中,这种队列就是半连接队列。与此对应,全连接队列指的就是已经完成3次握手,建立起连接的请求。如果队列满了(backlog)就可能出现丢包。
这里在补充一点关于SYN-ACK 重传次数的问题: 服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传,如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。注意,每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s, 2s, 4s, 8s, ….
正因为TCP设计的有这些缺陷,有一种针对半连接的攻击叫做SYN攻击,大量构造三次握手中的第一次握手包SYN包浪费服务器CPU和内存资源,造成半连接队列溢出从而正常客户端发送的请求直接被服务器丢了,这样对于正常客户端服务就不可用了,检测SYN攻击也非常简单:
1 | netstat -n | grep SYN_RECV |
当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。
为什么需要3次握手
为什么要三次握手,我握两次不行吗?我觉得我说发,你说好,不就完了吗,非要矫情一下,握第三次手的意义是什么?
首先我们先来理解一下为什么需要握手?
客户端和服务器端通信前需要连接,而”握手“作用就是为了证明,客户端的发送能力和服务器端的接受能力都是正常的,这是”握手“来达到的目的。
第一次握手:客户端发送网络包,服务器端收到了,这样服务器端就能证明:客户端的发送能力、以及服务器端的接收能力都是正常的。
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。 从客户端的视角来看,我接到了服务端发送过来的响应数据包,说明服务端接收到了我在第一次握手时发送的网络包,并且成功发送了响应数据包,这就说明,服务端的接收、发送能力正常。而另一方面,我收到了服务端的响应数据包,说明我第一次发送的网络包成功到达服务端,这样,我自己的发送和接收能力也是正常的。
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力,服务端的发送、接收能力是正常的。 第一、二次握手后,服务端并不知道客户端的接收能力以及自己的发送能力是否正常。而在第三次握手时,服务端收到了客户端对第二次握手作的回应。从服务端的角度,我在第二次握手时的响应数据发送出去了,客户端接收到了。所以,我的发送能力是正常的。而客户端的接收能力也是正常的。而从上面的过程可以看到,最少是需要三次握手过程的。两次达不到让双方都得出自己、对方的接收、发送能力都正常的结论。
TCP四次挥手
- 第一次挥手:客户端主动关闭方发送一个FIN(此报文中指定一个序列号),此时客户端处于 FIN_WAIT1 状态。用来关闭客户端到服务器端的数据传送,也就是客户端告诉服务器端:我已经不会再给你发数据了, (当然,在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 确认报文,客户端依然会重发这些数据),但是,此时客户端还可以接受数据。
- 第二次挥手:服务端收到 FIN 包后,发送一个 ACK 给客户端,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号).此时服务器处于 CLOSE_WAIT 状态。
- 第三次挥手:服务器端发送一个 FIN,用来关闭服务器端到客户端的数据传送,也就是告诉客户端,我的数据也发送完了,不会再给你发送数据了!!!这一次其实就和客户端第一次挥手一样发送 FIN 报文携带序列号,发送完毕后服务器处于 LAST_ACK 状态。
- 第四次挥手:客户端收到 FIN 后,发送一个 ACK 给服务端,确认序号为收到序号 +1,至此,完成四次挥手。此时客户端处于 TIME_WAIT 阶段。需要过一阵子确保服务器收到自己的 ACK 报文之后才会进入到 CLOSED 状态.
- 服务器收到 ACK 之后就关闭连接了,处于 CLOSED 状态。
这里特别需要主要的就是 TIME_WAIT 这个状态了,这个是面试的高频考点,就是要理解,为什么客户端发送 ACK 之后不直接关闭,而是要等一阵子才关闭。这其中的原因就是,*要确保服务器是否已经收到了我们的 ACK 报文,如果没有收到的话,服务器会重新发 FIN 报文给客户端,*客户端再次收到 ACK 报文之后,就知道之前的 ACK 报文丢失了,然后再次发送 ACK 报文。
至于 TIME_WAIT 持续的时间至少是一个报文的来回时间。一般会设置一个计时,如果过了这个计时没有再次收到 FIN 报文,则代表对方成功就是 ACK 报文,此时处于 CLOSED 状态。
注意:上述描述中的客户端和服务器的角色是对等的,以主动断开连接的一方作为客户端。
关于 3 次握手和 4 次挥手的网络抓包如下:
以上的客户端和服务器在同一台机器上。但是我们仅在客户端抓包可能会出现如下的情况:
为什么建立连接时 3 次握手,而关闭连接时 4 次挥手
这是因为服务器在 LISTEN 状态下,收到建立连接请求的 SYN 报文后,把 ACK 和 SYN 放在一个报文里发送给客户端。而关闭连接时,当收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定。因此,一般 ACK 和 FIN 一般都会分开发送。
为什么 TIME_WAIT 状态还需要等 2MSL 后才能返回到 CLOSED 状态
两个存在的理由:
- 无法保证最后发送的 ACK 报文会一定被对方收到,所以需要重发可能丢失的 ACK 报文。
- 当关闭当前的 TCP 连接时,最后发送出去的数据报可能被路由器的转发队列缓存,如果立马切换到 CLOSED 状态,可能使用相同窗口的新的 TCP 连接收到的数据报还是前一个 TCP 连接缓存在路由器中的数据。2MSL 足以让分组最多存活MSL 秒被丢弃。
TCP 和 UDP
- TCP: 面向连接,可靠,面向字节流
- UDP: 面向非连接,不可靠,面向报文, 限制报文大小为 64k
如何设计一个高并发系统
系统拆分
将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。
缓存
大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家redis轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。
MQ
可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用 redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql 啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费后慢慢写,控制在 mysql 承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok 的。
分库分表
分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高 sql 跑的性能。
读写分离
读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。
ElasticSearch
es 是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es 来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。
其实大部分公司,真正看重的,不是说你掌握高并发相关的一些基本的架构知识,架构中的一些技术,RocketMQ、Kafka、Redis、Elasticsearch,高并发这一块,你了解了,也只能是次一等的人才。对一个有几十万行代码的复杂的分布式系统,一步一步架构、设计以及实践过高并发架构的人,这个经验才是难能可贵的。
为什么 Protocol Buffer 的效率是最高的
- 使用proto编译器,自动进行序列化和反序列化,速度非常快,比XML和JSON快20-100倍
- 数据压缩效果非常好,序列化后的数据量非常小,传输起来占用更少的带宽
ZK的使用场景有哪些
统一命名服务(Name Service)、配置管理(Configuration Management)、集群管理(Group Membership)、共享锁(Locks)、队列管理。
- Zookeeper是一个类似linux、hdfs的树形文件结构,zookeeper可以用来保证数据在(Zookeeper)集群之间的数据的事务性一致性,zookeeper也是我们常说的CAP理论中的CP(强一致性)。
- Zookeeper有一个概念叫watch(也称之为事件),是一次性触发的,当watch监视的数据发生变化时,通知设置了该watch的client端,即watcher实例对象(用于改变节点的变化而做出相应的行为)
- Zookeeper有三个角色:Leader,Follower,Observer
- Leader:数据总控节点,用于接收客户端连接请求,分发给所有的Follower节点后,各个Follower节点进行更新数据操作并返回给Leader节点,如果满足半数以上(所以Zookeeper集群一般是奇数个节点)更新成功则此次操作成功;
- Follower:相当于跟随者的角色,Zookeeper的Leader宕机(挂掉)时,所有的Follower角色内部会产生选举机制,选举出新的Leader用于总控;
- Observer:顾名思义,就是我们的客户端,用于观察Zookeeper集群的数据发送变化,如果产生变化则zookeeper会主动推送watch事件给Observer(客户端),用于对数据变化的后续处理;当然Observer(客户端)也可以发送数据变更请求;
分布式协调
简单来说,就好比,你 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上对某个节点的值注册个监听器,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 系统立马就可以收到通知,完美解决。
分布式锁
举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode,接着执行操作;然后另外一个机器也尝试去创建那个 znode,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。
某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。
与redis分布式锁的对比:
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
- zk分布式锁,获取不到锁,注册个监听器即可,不需要主动尝试获取锁,性能开销小。
另外一点就是,如果是 redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。redis分布式锁好麻烦:遍历上锁,计算时间等等……zk 的分布式锁语义清晰实现简单。综上所述zk的分布式锁比redis的分布式锁牢靠,而且模型简单易用。
元数据/配置信息管理
zookeeper可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持zookeeper么?
HA高可用
比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过 zookeeper 感知到切换到备用进程。
微服务
好的架构不是设计出来的,而是进化出来的。过早将一个系统划分为微服务的代价是非常高的(尤其是面对一个全新的领域时)。很多时候将一个已有的单体架构划分成微服务比从头开始构建微服务要简单得多!
微服务的一大特点就是异构,不同的服务可以采用不同的语言,不同的 DB。
不适用的场景
- 强事务
- 业务相对稳定、迭代周期长,几个月都不会更新 1 次
- 访问压力不大,可用性要求不高,例如中小型公司的内部系统
高可用架构
电商网站详情页系统架构
小型电商网站的页面展示采用页面全量静态化的思想。数据库中存放了所有的商品信息,页面静态化系统,将数据填充进静态模板中,形成静态化页面,推入 Nginx 服务器。用户浏览网站页面时,取用一个已经静态化好的 html 页面,直接返回回去,不涉及任何的业务逻辑处理。
这样做,好处在于,用户每次浏览一个页面,不需要进行任何的跟数据库的交互逻辑,也不需要执行任何的代码,直接返回一个 html 页面就可以了,速度和性能非常高。对于小网站,页面很少,很实用,非常简单,Java 中可以使用 velocity、freemarker、thymeleaf 等等,然后做个 cms 页面内容管理系统,模板变更的时候,点击按钮或者系统自动化重新进行全量渲染。坏处在于,仅仅适用于一些小型的网站,比如页面的规模在几十到几万不等。对于一些大型的电商网站,亿级数量的页面,你说你每次页面模板修改了,都需要将这么多页面全量静态化,靠谱吗?每次渲染花个好几天时间,那你整个网站就废掉了。
大型电商网站商品详情页的系统设计中,当商品数据发生变更时,会将变更消息压入 MQ 消息队列中。缓存服务从消息队列中消费这条消息时,感知到有数据发生变更,便通过调用数据服务接口,获取变更后的数据,然后将整合好的数据推送至 redis 中。Nginx 本地缓存的数据是有一定的时间期限的,比如说 10 分钟,当数据过期之后,它就会从 redis 获取到最新的缓存数据,并且缓存到自己本地。
用户浏览网页时,动态将 Nginx 本地数据渲染到本地 html 模板并返回给用户。
虽然没有直接返回 html 页面那么快,但是因为数据在本地缓存,所以也很快,其实耗费的也就是动态渲染一个 html 页面的性能。如果 html 模板发生了变更,不需要将所有的页面重新静态化,也不需要发送请求,没有网络请求的开销,直接将数据渲染进最新的 html 页面模板后响应即可。
在这种架构下,我们需要保证系统的高可用性。如果系统访问量很高,Nginx 本地缓存过期失效了,redis 中的缓存也被 LRU 算法给清理掉了,那么会有较高的访问量,从缓存服务调用商品服务。但如果此时商品服务的接口发生故障,调用出现了延时,缓存服务全部的线程都被这个调用商品服务接口给耗尽了,每个线程去调用商品服务接口的时候,都会卡住很长时间,后面大量的请求过来都会卡在那儿,此时缓存服务没有足够的线程去调用其它一些服务的接口,从而导致整个大量的商品详情页无法正常显示。这其实就是一个商品接口服务故障导致缓存服务资源耗尽的现象。
数据结构和算法
设计实现一个 LRU
该结构支持2个操作:访问和删除。访问操作会将不存在于数据结构中的元素插入(或者数据结构中存在该元素,则该元素放在头部),删除操作会返回最近最少访问的元素。
思考:取得最近访问元素就像是FIFO,可以想到栈,但是删除最老的数据就需要遍历整个栈了。我们需要一种快速访问容器头部并快速移除尾部元素的结构,自然而然可以想到队列,但是LRU还有一个特性:如果容器中已经有元素,需要将元素移动到头部,队列对于这样的操作需要从头遍历到指定元素,删除元素,再在 对头插入。显然是低效的。联想到双端链表删除元素只需要O(1),(单链表删除元素为O(N),因为要遍历链表)。因此LRU的底层可以采用双向链表存储。快速根据key找到node节点就需要使用hash表了。
1 | class Node { |
如何回退代码到指定版本
方式一:可以新建一个分支,然后选择你想回退到到那个版本,切换到新分支之后,代码就是那个版本了,可以对那个版本进行操作,修改等,如果想回到之前最新版本,直接切换分支到原来到分支即可,这杨相互不影响,Reset master to this commit 只有是hard的时候,项目里代码内容才是你想切到的版本内容,不过这样会把你新改的代码丢失了.
方式二:git reset
注意如果需要保留后面commit的更改就选mixed,不然选hard。
注意:从这个reset 之后,再push服务器,会提示落后xx个版本,这个时候需要强制push。git push --force
。
一个 TCP 连接能发几个 HTTP 请求
如果是 HTTP 1.0 版本协议,一般情况下,不支持长连接,因此在每次请求发送完毕之后,TCP 连接即会断开,因此一个 TCP 发送一个 HTTP 请求,但是有一种情况可以将一条 TCP 连接保持在活跃状态,那就是通过 Connection 和 Keep-Alive 首部,在请求头带上 Connection: Keep-Alive,并且可以通过 Keep-Alive 通用首部中指定的,用逗号分隔的选项调节 keep-alive 的行为,如果客户端和服务端都支持,那么其实也可以发送多条,不过此方式也有限制,可以关注《HTTP 权威指南》4.5.5 节对于 Keep-Alive 连接的限制和规则。
而如果是 HTTP 1.1 版本协议,支持了长连接,因此只要 TCP 连接不断开,便可以一直发送 HTTP 请求,持续不断,没有上限;
同样,如果是 HTTP 2.0 版本协议,支持多用复用,一个 TCP 连接是可以并发多个 HTTP 请求的,同样也是支持长连接,因此只要不断开 TCP 的连接,HTTP 请求数也是可以没有上限地持续发送
HTTP2/HTTP3
什么是 TCP 的队头阻塞
TCP 协议中的一个性能问题。当一个 TCP 连接中的某个数据包丢失或延迟时,后续的数据包即使已经到达接收端,也无法被处理,必须等待丢失或者延迟的数据包重传并到达。这种现象会导致整个连接的吞吐量下降和延迟增加。
原因:
- TCP 的可靠性机制:TCP 是可靠协议,要求数据按照顺序到达接收端。如果某个数据包丢失或者乱序,接收端会将其缓存,直到丢失的数据包到达。
- 单一路径传输:TCP 连接中的所有数据包通过同一条路径传输,某个包丢失会导致整个连接的性能受影响。
- 滑动窗口机制:使用这种机制来进行流控。某个数据包丢失,发送端的窗口会被阻塞,直到丢失的数据包被确认。
解决方案:
- 使用多路复用协议:在应用层使用多路复用协议(如 HTTP2)可以在同一个 TCP 连接上同时传输多个请求和响应,但是 HTTP2 仍然受限于 TCP 的队头阻塞
- 使用 QUIC:基于 UDP 不存在 TCP 层面的队头阻塞,支持多路复用,每个数据流是独立的,丢失的数据包只会影响当前数据流不会阻塞其他数据流。
- 使用多个 TCP 连接:可以减少单个连接中队头阻塞的影响,但是这种方法会增加连接管理的复杂性和资源开销。
- 前向纠错:发送端可以附加冗余数据,接收端可以通过冗余数据恢复丢失的数据包,减少重传请求。
WebSocket 和 HTTP 之间有什么关系
WebSocket 是一个独立的基于 TCP 的协议,它与 HTTP 之间的唯一关系就是它的握手请求可以作为一个升级请求(Upgrade request)经由 HTTP 服务器解释(也就是可以使用 Nginx 反向代理一个WebSocket)。
负载均衡
HTTP重定向负载均衡
来自用户的HTTP请求到达负载均衡服务器以后,负载均衡服务器根据某种负载均衡算法计算得到一个应用服务器的地址,通过HTTP状态码302重定向响应,将新的IP地址发送给用户浏览器,用户浏览器收到重定向响应以后,重新发送请求到真正的应用服务器,以此来实现负载均衡。
实现起来非常简单,但是缺点也非常明显:
- 客户端完成一次访问需要2次请求(请求负载均衡服务器+应用服务器),处理性能会受到影响
- 因为客户端需要连接应用服务器,所以需要将真实IP暴露给客户端,可能会带来安全问题
这种方案在实际开发中几乎不会使用。
DNS负载均衡
当用户从浏览器发起HTTP请求的时候,首先要到DNS域名服务器进行域名解析,解析得到IP地址以后,用户才能够根据IP地址建立 HTTP连接,访问真正的数据中心的应用服务器,这时候就可以在DNS解析的时候进行负载均衡,也就是说,不同的用户进行域名解析的时候,返回不同的IP地址,从而实现负载均衡。
DNS负载均衡和HTTP重定向负载均衡非常像,但是不会有性能问题:因为DNS解析之后IP地址就被客户端缓存了。但是会不会有安全问题呢?大型互联网应用通常并不直接通过DNS解析得到应用服务器IP地址,而是解析得到负载均衡服务器的IP地址。也就是说,大型网互联网应用需要两次负载均衡,一次通过DNS负载均衡,用户请求访问数据中心负载均衡服务器集群的某台机器,然后这台负载均衡服务器再进行一次负载均衡,将用户请求分发到应用服务器集群的某台服务器上。通过这种方式,应用服务器不需要用公网IP将自己暴露给外部访问者,避免了安全性问题。
DNS域名解析是域名服务商提供的一项基本服务,几乎所有的域名服务商都支持域名解析负载均衡,只需要在域名服务商的服务控制台进行一下配置,不需要开发代码进行部署,就可以拥有DNS负载均衡服务了。目前大型的互联网应用,淘宝、百度、Google 等全部使用DNS负载均衡。比如用不同的电脑ping www.baidu.com就可以看到,不同电脑得到的IP地址是不同的。
反向代理负载均衡
常用的web服务器NGINX就有这个功能。反向代理服务器是工作在HTTP协议层之上的,因此这一层的代理负载均衡也叫做应用层负载均衡。所以它代理的也是HTTP的请求和响应。作为互联网应用层的一个协议,HTTP协议相对说来比较重,效率比较低,所以反向代理负载均衡通常用在小规模的互联网系统上,只有几台或者十几台服务器的规模。
IP负载均衡
工作在TCP/IP的IP层,所以有时候也叫做网络层负载均衡。工作原理:当用户的请求到达负载均衡服务器以后,负载均衡服务器会对网络层的数据包的IP地址进行转换,将其修改为应用服务器的IP地址,然后把数据包重新发送出去,请求数据就会到达应用服
务器。
在操作系统内核直接修改IP数据包的地址,效率比应用层的反向代理负载均衡高得多。但是它依然有一个缺陷,不管是请求还是响应的数据包,都要通过负载均衡服务器进行IP地址转换,才能够正确地把请求数据分发到应用服务器,或者正确地将响应数据包发送到用户端程序。请求的数据通常比较小,一个URL或者是一个简单的表单,但是响应的数据不管是HTML还是图片,或者是 JS、CSS
这样的资源文件通常都会比较大,因此负载均衡服务器会成为响应数据的流量瓶颈。
数据链路层负载均衡
可以解决响应数据量大而导致的负载均衡服务器输出带宽不足的问题,像淘宝这种规模的应用,通常使用Linux内核支持的链路层负载均衡。
这种负载均衡模式也叫直接路由模式,在负载均衡服务器的Linux操作系统内核拿到数据包后,直接修改数据帧中的mac地址,将其修改为应用服务器集群中某个服务器的mac地址,然后将数据重新发送回服务器集群所在的局域网,这个数据帧就会被某个真实的应用服务器接收到。
负载均衡服务器和集群内的应用服务器配置相同的虚拟IP地址,也就是说,在网络通信的IP层面,负载均衡服务器变更mac地址的操作是透明的,不影响TCP/IP的通信连接。所以真实的应用服务器处理完应用请求,发送应答响应的时候,就会直接发送回请求的 App手机,不会再经过负载均衡服务器。
Linux上实现IP负载均衡和链路层负载均衡的技术是LVS,目前LVS功能已经集成到Linux中了,可以直接配置实现这两种负载均衡。
如何改善数据的存储能力
在整个互联网系统架构中,承受着最大处理压力,最难以被伸缩的,就是数据存储部分。原因主要有两方面:
- 数据存储需要使用硬盘,而硬盘的处理速度要比其他几种计算资源,比如 CPU、内存、网卡都要慢一些;
- 另一方面,数据是公司最重要的资产,公司需要保证数据的高可用以及一致性,非功能性约束更多一些。
因此数据存储通常都是互联网应用的瓶颈。在高并发的情况下,最容易出现性能问题的就是数据存储。目前用来改善数据存储能力的主要手段包括:数据库主从复制、数据库分片和NoSQL数据库。
实现高可用常用的手段有哪些
冗余备份
提供同一服务的服务器要存在冗余,即任何服务都不能只有一台服务器,服务器之间要互相进行备份,任何一台服务器出现故障的时候,请求可以发送到备份的服务器去处理。这样,即使某台服务器失效,在用户看来,系统依然是可用的。
失败隔离
将失败限制在一个较小的范围之内,使故障影响范围不扩大。具体实现失败隔离的主要架构技术是消息队列。
一方面,消息的生产者和消费者通过消息队列进行隔离。如果消费者出现故障的时候,生产者可以继续向消息队列发送消息,而不会感知到消费者的故障,等消费者恢复正常以后再去从消息队列中消费消息,所以从用户处理的视角看,系统一直是可用的。
发送邮件消费者出现故障,不会影响生产者应用的运行,也不会影响发送短信等其他消费者正常的运行。
另一方面,由于分布式消息队列具有削峰填谷的作用,所以在高并发的时候,消息的生产者可以将消息缓冲在分布式消息队列中,消费者可以慢慢地从消息队列中去处理,而不会将瞬时的高并发负载压力直接施加到整个系统上,导致系统崩溃。也就是将压力隔离开来,使消息生产者的访问压力不会直接传递到消息的消费者,这样可以提高数据库等对压力比较敏感的服务的可用性。
同时,消息队列还使得程序解耦,将程序的调用和依赖隔离开来,我们知道,低耦合的程序更加易于维护,也可以减少程序出现 Bug的几率。
限流降级
保护系统高可用的一种手段。比如说在电商系统中有确认收货这个功能,即便我们不去确认收货,系统也会超时自动确认收货。但际上确认收货这个操作是一个非常重的操作,因为它会对数据库产生很大的压力:它要进行更改订单状态,完成支付确认,并进行评价等一系列操作。如果在系统高并发的时候去完成这些操作,那么会对系统雪上加霜,使系统的处理能力更加恶化。
解决办法就是在系统高并发的时候,比如说像淘宝双 11 的时候,当天可能整天系统都处于一种极限的高并发访问压力之下,这时候就可以将确认收货、评价这些非核心的功能关闭,将宝贵的系统资源留下来,给正在购物的人,让他们去完成交易。
异地多活
主要是解决整个数据中心不可用问题的。将数据中心分布在多个不同地点的机房里,这些机房都可以对外提供服务,用户可以连接任何一个机房进行访问,这样每个机房都可以提供完整的系统服务,即使某一个机房不可使用,系统也不会宕机,依然保持可用。
异地多活的架构考虑的重点就是,用户请求如何分发到不同的机房去。这个主要可以在域名解析的时候完成,也就是用户进行域名解析的时候,会根据就近原则或者其他一些策略,完成用户请求的分发。另一个至关重要的技术点是,因为是多个机房都可以独立对外提供服务,所以也就意味着每个机房都要有完整的数据记录。用户在任何一个机房完成的数据操作,都必须同步传输给其他的机房,进行数据实时同步。
数据库实时同步最需要关注的就是数据冲突问题。同一条数据,同时在两个数据中心被修改了,该如何解决?为了解决这种数据冲突的问题,某些容易引起数据冲突的服务采用类似MySQL 的主主模式,也就是说多个机房在某个时刻是有一个主机房的,某些请求只能到达主机房才能被处理,其他的机房不处理这一类请求,以此来避免关键数据的冲突。
除了上述的高可用架构方案,还有一些高可用的运维方案:通过自动化测试减少系统的bug,通过自动化监控尽早发现系统的故障,通过预发布验证发现测试环境无法发现的bug,灰度发布降低软件错误带来的影响以及评估软件版本升级带来的业务影响等等。
如何实现电子邮件的已读回执
参考http://www.getnotify.com/,在邮件列表插入一个随机生成的一张1px的空白图片,检测该图片链接的访问次数。
基于nginx_http_empty_gif借助access.log打点性能开销最小。nginx 1 px 像素gif的妙用:http://www.ttlsa.com/nginx/nginx-modules-empty_gif/ ,常用于统计打点,这种请求一般只需要单向上报数据,尽可能少返回数据。
说一说你的缺点
只需要说一说自己现在正在学的东西即可,不要傻乎乎说自己这个不行,那个不行。
你有什么想要问的
这个岗位具体会做哪些事情,会与哪些人合作?
进一步对工作岗位进行深入了解,以及对将来的合作伙伴有更多提前认知,同时给面试官一些尽情表达的机会,显得你是非常关心这个工作机会。
咱们团队目前面对的最大挑战/困难是什么?
抓住机会深入了解团队目前的困难、痛点,利用入职前的空档期,提前做好预习与准备,以便顺利入职之后快速产出。
总之,**问太肤浅的问题,会让你显得格局小。**太细节、太琐碎的,比如作息制度、报销制度,是否管午饭,是否经常加班,团队有几个女生,有没有健身房……等等类似太具体的问题,还是留在该公司将offer发给你之后、入职已经胜券在握的时候再去问HR吧。
太上无败,其次败而有以成,此之谓用民。