1.自我介绍+项目
2.RPC框架和普通http有什么区别和优势? 基于Tcp封装还是http封装的
3.rpc是长连接吗?如果要传输一个特别大的文件 底层还是基于流吗? Nio是一个什么IO模型?
4.github了的watch star fork
5.异常和error的区别,oom是error还是异常?什么东西分配在堆上和栈上?
6.只对堆进行gc 这句话对不对 调用system.gc()马上就执行gc吗
7.缺页中断 分页地址转换 内存抖动
8.linux的fork指令对数据的拷贝是马上就拷贝的吗?
9.linux看网络状况用什么 看日志用什么?
10.拥塞控制 以及里面的算法?流量控制的协议
11.Ping命令做了什么?基于那一个层?ping是哪一个层的?
12.Mysql和Redis最大的区别? MyISAM和InnoDB的区别?
13.Redis 的实现。为什么这么高性能 ?set kv键值对进去的时候,kv键值的长度是不一样的 你觉得底层的数据结构是一样的吗? 持久化的策略 长久下来aof文件会很大 怎么办?InnoDB行锁的分类 (其实就是排他锁和共享锁)
14.Select from update 是什么效果?事务你平常是怎么处理的?
15.两个队列实现一个栈、圆里均匀地生成点(极坐标系)
16.ps命令的底层实现?
17.类加载器
2. RPC框架和普通http有什么区别和优势? 基于Tcp封装还是http封装的
RPC是一种远程过程调用的协议,使用这种协议向另一台计算机上的程序请求服务,不需要了解底层网络技术的协议。
在 RPC 中,发出请求的程序是客户程序,而提供服务的程序是服务器。
HTTP是一种超文本传输协议。是WWW浏览器和WWW服务器之间的应用层通讯协议。
RPC协议与HTTP协议的区别
1、RPC是一种API,HTTP是一种无状态的网络协议。RPC可以基于HTTP协议实现,也可以直接在TCP协议上实现。
2、RPC主要是用在大型网站里面,因为大型网站里面系统繁多,业务线复杂,而且效率优势非常重要的一块,这个时候RPC的优势就比较明显了。
HTTP主要是用在中小型企业里面,业务线没那么繁多的情况下。
3、HTTP开发方便简单、直接。开发一个完善的RPC框架难度比较大。
4、HTTP发明的初衷是为了传送超文本的资源,协议设计的比较复杂,参数传递的方式效率也不高。开源的RPC框架针对远程调用协议上的效率会比HTTP快很多。
5、HTTP需要事先通知,修改Nginx/HAProxy配置。RPC能做到自动通知,不影响上游。
6、HTTP大部分是通过Json来实现的,字节大小和序列化耗时都比Thrift要更消耗性能。RPC,可以基于Thrift实现高效的二进制传输。
3. github了的watch star fork
Watch
Watch翻译过来可以称之为观察。
默认每一个用户都是处于Not watching的状态,当你选择Watching,表示你以后会关注这个项目的所有动态,以后只要这个项目发生变动,如被别人提交了pull request、被别人发起了issue等等情况,你都会在自己的个人通知中心,收到一条通知消息,如果你设置了个人邮箱,那么你的邮箱也可能收到相应的邮件。
如果你不想接受这些通知,那么点击 Not Watching 即可。
Star
Star 翻译过来应该是星星,这里解释为`关注`或者`点赞`更合适,当你点击 Star,表示你喜欢这个项目或者通俗点,可以把他理解成朋友圈的点赞吧,表示对这个项目的支持。
不过相比朋友圈的点赞,github 里面会有一个列表,专门收集了你所有 start 过的项目,点击 github 个人头像,可以看到 your star的条目,点击就可以查看你 star 过的所有项目了。
不过,在你的 star 列表很容易出现这样的问题。就是你可能 star 成百上千个项目怎么办。这时,如果 github 可以提供一个分类功能该多好,就像微博网页版的收藏,你在收藏的时候可以设置 tag这样设置的好处是,以后再次查找项目时,可以根据归类查找。
Fork
当选择 fork,相当于你自己有了一份原项目的拷贝,当然这个拷贝只是针对当时的项目文件,如果后续原项目文件发生改变,你必须通过其他的方式去同步。
一般来说,我们不需要使用 fork 这个功能,至少我一般不会用,除非有一些项目,可能存在 bug 或者可以继续优化的地方,你想帮助原项目作者去完善这个项目,那么你可以 fork 一份项目下来,然后自己对这个项目进行修改完善,当你觉得项目没问题了,你就可以尝试发起 pull request给原项目作者了,然后就静静等待他的 merge。
当前很多人错误的在使用 fork。很多人把 fork 当成了收藏一样的功能,包括一开始使用 github 的我,每次看到一个好的项目就先 fork,因为这样,就可以我的 repository(仓库)列表下查看 fork 的项目了。其实你完全可以使用 star 来达到这个目的。
7. 缺页中断 分页地址转换 内存抖动
一、什么是缺页中断?
进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中(即存在位为0),那么停止该指令的执行,并产生一个页不存在的异常,对应的故障处理程序可通过从外存加载该页的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。
二、页面调度算法
将新页面调入内存时,如果内存中所有的物理页都已经分配出去,就按照某种策略来废弃整个页面,将其所占据的物理页释放出来;
三、查看进程发生缺页中断的次数
ps -o majflt,minflt -C program查看
majflt和minflt表示一个进程自启动以来所发生的缺页中断的次数;
四、产生缺页中断的几种情况
1、当内存管理单元(MMU)中确实没有创建虚拟物理页映射关系,并且在该虚拟地址之后再没有当前进程的线性区(vma)的时候,可以肯定这是一个编码错误,这将杀掉该进程;
2、当MMU中确实没有创建虚拟页物理页映射关系,并且在该虚拟地址之后存在当前进程的线性区vma的时候,这很可能是缺页中断,并且可能是栈溢出导致的缺页中断;
3、当使用malloc/mmap等希望访问物理空间的库函数/系统调用后,由于linux并未真正给新创建的vma映射物理页,此时若先进行写操作,将和2产生缺页中断的情况一样;若先进行读操作虽然也会产生缺页异常,将被映射给默认的零页,等再进行写操作时,仍会产生缺页中断,这次必须分配1物理页了,进入写时复制的流程;
4、当使用fork等系统调用创建子进程时,子进程不论有无自己的vma,它的vma都有对于物理页的映射,但它们共同映射的这些物理页属性为只读,即linux并未给子进程真正分配物理页,当父子进程任何一方要写相应物理页时,导致缺页中断的写时复制;
什么是内存抖动(颠簸)
本质是频繁的页调度行为频繁的页调度,进程不断产生缺页中断置换一个页,又不断再次需要这个页
运行程序太多;页面替换策略不好。通过杀掉一些无关的进程(终止进程)或者增加物理内存解决
10.拥塞控制 以及里面的算法?流量控制的协议
TCP如何实现一个靠谱的协议
为了保证顺序性,每个包都有一个ID。在建立连接的时候,商定起始的ID。然后按照ID一个一个发送。
收到包的一端需要对包做应答,一旦应答一个ID,就表明之前的ID都收到了,这个模式称为累计确认或者累计应答。
为了记录发送和接收的包,TCP在发送端和接收端都缓存记录。
发送端缓存结构
发送端的缓存按照包的ID一个个排列,分成4个部分:
(一)发送并且已经确认的
(二)发送了并且尚未确认的
(三)没有发送,但是已经确认要发送在等待的
(四)没有发送,并且暂时不会发送的
第三和第四部分区分开是为了流量控制,流量控制的依据是什么?TCP里接收端会给发送端报告一个窗口大小,叫Advertised Window。发送端需要保证上面第二和第三部分的长度加起来等于Advertised Window。
tcp发送端缓存结构
接收端缓存结构
接收端的缓存分成三个部分:
(一)接受并且确认过的
(二)还没接收,但是马上就能接收的,要等空格填满
(三)还没接收,也没法接收的,也就是超过工作量(max buffer)的部分
tcp接收端缓存结构
MaxRcvBuffer:最大缓存的量
LastByteRead之后是已经接收了,但是还没被应用层消耗
NextByteExpected之后是等待接收的
Advertised Window其实就是等待接收未确认部分的大小。其中这部分中有可能是有空挡的,比如7到14有,但6是空的。那NextByteExpected就只能待在这个位置了。
TCP的确认和重发机制
发送端在发出一个包后,会设置一个定时器,超过一定时间没收到ACK,就会把这个包重发。应该如何评估这个时间的大小呢?这个时间不能太短,必须大于正常的一次往返的时间(RTT)。也不能太长,太长了发送的速度就太慢了。
所以最关键的是估算一个平均RTT,所以TCP需要不断地采样RTT,还要根据RTT的波动范围,做加权平均算出一个值来。由于重传时间是不断变化的,称为自适应重传算法。
快速重传算法
以上算法的问题是超时时间会越加越长,所以有一个快速重传的机制。当接收方收到一个序号大于下一个期望的报文段时,就是说接收缓存有了空格,那还是发送原来的ACK,比如我在等6,这时候收到7,我ACK 5。然后又收到8和9,我还都是ack 5。这样发送端接连收到3个ACK,发送端收到后,会马上重发6,不再等超时。这里还是有一个问题,发送端这时候可能已经发到20了,收到的3个ACK只能知道6没收到,7,8,9有没有收到不知道,只能把6之后的全部重发一遍。
Selctive Acknowledgment(SACK)
还有一种重传的方式称为SACK,这种方式是在TCP头里加一个SACK的东西,将缓存地图放进去,比如还是7丢了,可以地图是ACK6,SACK8,SACK9。发送方收到后,立马能看出是7丢了。Linux下面可以通过tcp_ack参数打开这个功能。
sack示意图
这里还是有一个问题,就是SACK不是最终保证,就是说接收端在发送SACK后是可以把数据再丢了的。虽然这么做不鼓励,但是不排除极端情况,比如内存不够了。所以发送端不能想当然的再也不发SACK的那些包了,还是要看这些包有没有正式的ACK才能最终确认。
Duplicate SACK(D-SACK)
D-SACK的作用是接收端告诉发送端包发重了。比如ACK 6丢了,那么发送端会重发,接收端发现收到两个6,现在我都要ACK 8了,则回复ACK 8,SACK 6。
引入D-SACK,有这么几个好处:
1.让发送方知道,是发的包丢了还是对端回的ACK丢了
2.是不是自己的timeout设置小了,导致重传了
3.网络上出现先发后到的情况
4.网络上有可能把我的包复制了
5.基于以上的认知,可以更好的做流控。Linux下使用参数tcp_dsack开启这个功能。
RTT算法策略
以上提到重传的Timeout很重要,这个超时时间在不同的网络的情况下,根本没有办法设置一个死的值。只能动态地设置。 为了动态地设置,TCP引入了RTT——Round Trip Time,也就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置Timeout——RTO(Retransmission TimeOut),以让我们的重传机制更高效。
经典算法
首先采样最近几次的RTT,然后做平滑计算,算法叫加权移动平均。公式如下:(α取值在0.8到0.9之间)
SRTT = ( α * SRTT ) + ((1- α) * RTT)
然后计算RTO,公式如下
RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ]
其中:
UBOUND是最大的timeout时间,上限值
LBOUND是最小的timeout时间,下限值
β 值一般在1.3到2.0之间。
Karn / Partridge 算法
经典算法的问题就是RTT在有重传的时候怎么算?是用第一次发的时间算做开始时间还是重传的时间作为开始时间。比如下面这样:
RTT计算
所以为了避免这个坑,搞了一个Karn / Partridge 算法,这个算法的最大特点是——忽略重传,不把重传的RTT做采样。这样有一个问题就是突然有一段时间重传很多,如果都不算的话RTT就一直是原来的值,显然这个值已经不合适了。所以,算法想了一个办法,只要一发生重传,RTO翻倍。
Jacobson / Karels 算法
以上两种算法的问题就是RTT容易被平均掉,不能很好的应对突发情况。新的算法的公式如下:
SRTT = SRTT + α (RTT – SRTT) —— 计算平滑RTT
DevRTT = (1-β)DevRTT + β(|RTT-SRTT|) ——计算平滑RTT和真实的差距(加权移动平均)
RTO= µ * SRTT + ∂ *DevRTT —— 神一样的公式
(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…) 这个算法就是今天的TCP协议中用的算法。
RTO终于被算出来了,接下来就是两个最重要的窗口了。
流量控制
上面讲到TCP在包的ACK中会携带一个窗口大小,发送方就可以做一个滑动窗口了(简称rwnd)。每收到一个ACK就把窗口往右移动一个,也就是从未发送待发送中取一个发出去,然后从不可发送中取一个出来放到待发送里面。
如果接收方下次来的ack中带的窗口大小变小,则说明接收方处理不过来了,那发送方就不能将窗口右移了,而是要将窗口变小。最后如果窗口变成0,发送端窗口就变成0,不会再发了。这个时候发送方会一直发送窗口探测数据包ZWP(Zero Window Probe),看是否有机会调整窗口的大小。一般会探测3次,每次间隔30-60s,当然不同的实现可能配置不一样。
SockStress攻击
有等待就有攻击,利用ZWP的攻击方式就是,客户端连上服务端后就把窗口设置成0,然后服务端就等,然后ZWP。等的多了自然资源就耗尽了,这种攻击叫做SockStress。
Silly Window Syndrome (糊涂窗口综合征)
在接收端将窗口调整成0后,如果这个时候应用消耗了一个包,那窗口会变成1,如果这时候发送端立马发送一个包会发生什么?TCP+IP的头加起来有40字节,如果为了几个字节直接发送的话主要的工作都消耗在发头上了,这是一个问题。
还有就是网络里有个MTU的概念,就是一次发送的包大小,以太网是1500字节,出去TCP+IP的40字节,本来可以用1460字节而只发几个字节,那就是浪费带宽。这个1460就是俗称的MSS(Max Segment Size)。以上的表现就被称为糊涂窗口综合征。
发送端和接收端是怎么来处理这种病的呢?
如果是接收端造成窗口是0,一旦缓冲区开始有地方了,接收端不会立马发送一个窗口大小1给发送端,而是会等窗口达到一定大小,比如缓冲区一半为空或者空间大于MSS了,才会更新窗口大小,在此之前还是一直回答0。
如果是发送端造成包比较小,那就是发送端负责攒数据,他有两个主要的条件:1)要等到 Window Size>=MSS 或是 Data Size >=MSS,2)收到之前发送数据的ack回包,他才会发数据。这个就是著名的 Nagle’s algorithm 。
Nagle算法默认是打开的,所以,对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性比较强的程序,你需要关闭这个算法。你可以在Socket设置TCP_NODELAY选项来关闭这个算法。还有一个类似的参数TCP_CORK,其实是更激进的Nagle算法,完全禁止小包发送,而Nagle算法没有禁止小包发送,只是禁止了大量的小包发送。所以要分清楚这两个参数。
以上就是TCP的流量控制策略。
拥塞控制
流量控制通过滑动窗口来实现,但是rwnd窗口只考虑了发送端和接收端的问题,没考虑网络的问题。有可能接收端很快,但是网络拥塞了,所以加了一个拥塞窗口(cwnd)。拥塞窗口的意思就是一次性可以连续提交多少个包到网络中。最终的形态是LastByteSent-LastByteAcked<=min(cwnd,rwnd),由两个窗口共同控制发送速度。
TCP的拥塞控制主要避免两种现象,包丢失和包重传。网络的带宽是固定的,当发送端发送速度超过带宽后,中间设备处理不完多出来的包就会被丢弃,这就是包丢失。如果我们在中间设备上加上缓存,处理不过来的包就会被加到缓存队列中,不会丢失,但是会增加时延。如果时延到达一定的程度,就会超时重传,这就是包重传。
拥塞控制主要是四个算法:1)慢启动,2)拥塞避免,3)拥塞发生,4)快速恢复
慢启动
拥塞窗口(cwnd)的大小应该怎么设置呢?一个TCP连接,开始的时候cwnd设置成一个报文段(MSS),一次只能发送一个;当收到ACK后则cwnd++;如果ACK正常收到,每当过了一个RTT则翻倍,以指数增长。如果网速很快的话增长速度还是很可观的。
慢启动
[注] TCP的实现中cwnd并不都是从1个MSS开始的,Linux 3.0后依据google的论文《An Argument for Increasing TCP’s Initial Congestion Window》,初始化cwnd从10个MSS开始。Linux 3.0之前,cwnd是跟MSS的值来变的,如果MSS< 1095,则cwnd = 4;如果MSS>2190,则cwnd=2;其它情况下,则是3。
拥塞避免
cwnd一直涨下去不是办法,要设置一个限制。当涨到一次发送超过ssthresh(65535个字节),就会减速,改成每次加1/ cwnd,比如之前一次发送cwnd是10个MSS,现在每次收到一个确认cwnd加1/10个MSS,每过一个RTT加1个。这样一直加下去知道拥塞出现,比如包丢失。
拥塞发生时
前面虽然超过ssthresh时会减速,但是还是在涨,早晚会产生拥塞的。这时候有两种处理方式,1)一旦超时重传出现,则把ssthresh改成cwnd/2,cwnd窗口改成1,重新从头开始慢启动。2)还有一种情况就是收到接收端SACK要求重传,这种TCP认为不严重,ssthresh改成cwnd/2,cwnd降为cwnd/2+3进入快速恢复算法。
快速恢复
接着上一段拥塞发生的第二种情况,快速恢复算法的逻辑如下:
cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了)
重传Duplicated ACKs指定的数据包
如果再收到 duplicated Acks,那么cwnd = cwnd +1
如果收到了新的Ack,那么,cwnd = sshthresh ,然后就进入了拥塞避免的算法了。
其他的恢复算法有FACK,TCP Vegas等。
存在问题
拥塞控制用以上的方法控制窗口的大小有两个问题:
1)丢包不代表通道满了,也有可能是网络本来就有问题,所以这个时候收缩时不对的
2)等到发生丢包再收缩,其实已经晚了,应该在刚好用满时就不再加了
基于以上两个问题,又出现了TCP BBR拥塞算法。