二面总结(AutoMQ)

二面总结(AutoMQ)

这次面试问了大量关于做项目和实习过程中的思考,回答的不太好,因此打算好好总结一下相关的问题,也是对自己学习内容的整理回顾。

1.关于RPC存在的必要性

纯裸TCP本事就可以收发数据,但它是基于字节流的无边界数据流,上层需要定义消息格式用于定义消息边界,因此基于TCP很多应用层的协议崭露头角,例如HTTP和各类RPC协议(RPC本质上不算是协议,而是一种调用方式,而像gRPC和Thrift这样的具体实现才是协议,它们是实现了RPC调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法,同时RPC有很多种实现方式,不一定非得基于TCP协议)。

主流的HTTP/1.1协议传输的内容非常冗余,例如请求头中那些信息,如果我们约定好头部的第几位是Content-Type,就不需要每次都真的把”Content-Type”这个字段传过来。而RPC,因为它的可定制程度更高,可以采用紧凑的二进制格式定义RPC协议,采用体积更小的序列化协议去保存结构体数据,同时也不需要像HTTP那样考虑各种浏览器行为,比如302重定向跳转啥的。因此性能也会更好一些,这也是在公司内部微服务中抛弃HTTP,选择使用RPC的最主要原因。

其次,很多RPC调用往往是因为服务的拆分或者公司内部的多个服务之间的通信,这其中必然需要网络通信,利用HTTP当然可以,但是我们想要即使服务被拆分了但是使用起来还是跟之前的本地调用一样方便,这时候RPC框架就很合适,我们编码上还是和之前本地调用相差不大。

而且,很多RPC框架包含了重试机制路由策略负载均衡策略高可用策略流量控制策略等等。如果应用进程之间只使用HTTP协议通信,显然是无法完成上述功能的。

总结:RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。HTTP主要用于对外的环境,浏览器接口调用,APP接口调用,第三方接口调用等。

2.关于实习内容对你未来的编码工作的启示

LFI:Linux Fault Injection作为Linux内核内置的原生故障注入机制,提供了多种类型的故障注入如fail_make_request、fail_page_alloc等,默认情况下出于安全的考虑,内核并没有开启故障注入功能,需要我们手动配置内核选项,重新编译内核。Linux内核利用#ifdef等宏对相同的函数的不同实现进行了隔离,并且内核代码中很多流程处理过程中都留了口子如should_fail_request函数,根据内核编译选项选择性对这些口子的实现进行编译,例如默认情况下should_fail_request函数都是直接返回false即默认不走故障注入逻辑判断,当我们开启了CONFIG_FAIL_MAKE_REQUEST内核编译选项后,should_fail_request函数会真正的根据故障注入配置判断故障注入是否触发。

KRF是对内核空间的系统调用表进行拦截替换的工具

3.线程安全问题的判断依据

线程安全和不安全是在多线程环境下对于同一份数据的访问是否能够保证其正确性和一致性的描述。

  • 线程安全指的是在多线程环境下,对于同一份数据,不管有多少个线程同时访问,都能保证这份数据的正确性和一致性。
  • 线程不安全则表示在多线程环境下,对于同一份数据,多个线程同时访问时可能会导致数据混乱、错误或者丢失。

对于共享变量如类变量和实例变量,多线程环境下需要考虑线程安全问题,但是需要注意如果不存在写操作或者对象没有被复用,那么也不会存在线程安全问题。

4.序列化时关注大小端问题

所谓的大端模式,就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

所谓的小端模式,就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

image-20240229223144538

1
2
3
4
// 检索底层平台的本机字节序,定义此方法是为了使性能敏感的Java代码可以分配与硬件相同字节序的直接缓冲区。
public static ByteOrder nativeOrder() {
return NATIVE_ORDER;
}

image-20240229223557993

通过查看ByteBuffer.wrap,其声明了一个HeapByteBuffer:

1
2
3
4
5
6
7
8
public static ByteBuffer wrap(byte[] array, int offset, int length)
{
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}

所以byteBuffer.asIntBuffer()实际调用了HeapByteBuffer中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// package-private
boolean bigEndian = true;
// package-private
boolean nativeByteOrder = (ByteOrder.nativeOrder() == ByteOrder.BIG_ENDIAN);

public IntBuffer asIntBuffer() {
int pos = position();
int size = (limit() - pos) >> 2;
long addr = address + pos;
return (bigEndian
? (IntBuffer)(new ByteBufferAsIntBufferB(this,-1,0,size,size,addr))
: (IntBuffer)(new ByteBufferAsIntBufferL(this,-1,0,size,size,addr)));
}

bigEndian表示是否是大端,默认为true,所以后面put的时候实际是往ByteBufferAsIntBufferB大端中存储数据,但也可以调整为使用小端来存储数据,byteBuffer.order(ByteOrder.LITTLE_ENDIAN),可见Java默认使用了大端字节序。