项目GeeCache面试题

Olivia的小跟班 Lv3

项目地址

https://github.com/zhouxing9454/Geecache

项目流程(不是Grpc版本)

202308222129814.png

什么是分布式缓存系统?

分布式缓存是指将缓存数据分布在多台机器上,以提高缓存容量和并发读写能力的缓存系统。分布式缓存通常由多台机器组成一个集群,每台机器上都运行着相同的缓存服务进程,缓存数据被均匀地分布在集群中的各个节点上。

为什么要使用分布式缓存?

高并发环境下,例如典型的淘宝双11秒杀,几分钟内上亿的用户涌入淘宝,这个时候如果访问不加拦截,让大量的读写请求涌向数据库,由于磁盘的处理速度与内存显然不在一个量级,服务器马上就要宕机。从减轻数据库的压力和提高系统响应速度两个角度来考虑,都会在数据库之前加一层缓存,访问压力越大的,在缓存之前就开始CDN拦截图片等访问请求。
并且由于最早的单台机器的内存资源以及承载能力有限,如果大量使用本地缓存,也会使相同的数据被不同的节点存储多份,对内存资源造成较大的浪费,因此,才催生出了分布式缓存。

分布式缓存系统的优缺点?

优点:

  1. 容量和性能可扩展

通过增加集群中的机器数量,可以扩展缓存的容量和并发读写能力。同时,缓存数据对于应用来讲都是共享的。

  1. 高可用性

由于数据被分布在多台机器上,即使其中一台机器故障,缓存服务也能继续提供服务。但是分布式缓存的缺点同样不容忽视。
缺点:

  1. 网络延迟

分布式缓存通常需要通过网络通信来进行数据读写,可能会出现网络延迟等问题,相对于本地缓存而言,响应时间更长。

  1. 复杂性

分布式缓存需要考虑序列化、数据分片、缓存大小等问题,相对于本地缓存而言更加复杂。

分布式缓存系统的应用场景?

  1. 页面缓存:用来缓存Web 页面的内容片段,包括HTML、CSS 和图片等。
  2. 应用对象缓存:缓存系统作为ORM 框架的二级缓存对外提供服务,目的是减轻数据库的负载压力,加速应用访问;解决分布式Web部署的 session 同步问题,状态缓存.缓存包括Session 会话状态及应用横向扩展时的状态数据等,这类数据一般是难以恢复的,对可用性要求较高,多应用于高可用集群。
  3. 并行处理:通常涉及大量中间计算结果需要共享。
  4. 云计算领域提供分布式缓存服务

设计一个分布式缓存系统需要考虑哪些方面?

image.png

缓存雪崩、击穿、穿透分别是什么,如何应对?

什么是缓存雪崩、击穿、穿透?

项目GeeCache中如何应对缓存雪崩和缓存击穿问题?

项目实现了singleFlight机制。

  • singleFlight的原理是:在多个并发请求触发的回调操作里,只有第⼀个回调方法被执行,其余请求(落在第⼀个回调方法执行的时间窗口里)阻塞等待第⼀个回调函数执行完成后直接取结果,以此保证同⼀时刻只有⼀个回调方法执行,达到防止缓存击穿的目的。
  • singleFlight常用场景:
    • 缓存失效时的保护性更新
    • 防止突增的接口请求对后端服务造成瞬时高负载
  • 具体实现: 利用mutex互斥锁sync.WaitGroup 机制来实现多goroutine的并发控制策略
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    type call struct { //call 代表正在进行中,或已经结束的请求。使用 sync.WaitGroup 锁避免重入。
    wg sync.WaitGroup
    val interface{}
    err error
    }

    type Group struct { //Group 是 singleflight 的主数据结构,管理不同 key 的请求(call).
    mu sync.Mutex
    m map[string]*call
    }

    func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error) {
    g.mu.Lock()
    if g.m == nil {
    g.m = make(map[string]*call)
    }
    if c, ok := g.m[key]; ok {
    g.mu.Unlock()
    c.wg.Wait() // 如果请求正在进行中,则等待
    return c.val, c.err // 请求结束,返回结果
    }
    c := new(call)
    c.wg.Add(1) // 发起请求前加锁
    g.m[key] = c // 添加到 g.m,表明 key 已经有对应的请求在处理
    g.mu.Unlock()

    c.val, c.err = fn() // 调用 fn,发起请求
    c.wg.Done() // 请求结束

    g.mu.Lock()
    delete(g.m, key) // 更新 g.m
    g.mu.Unlock()

    return c.val, c.err // 返回结果
    }

缓存淘汰策略有哪些?

image.png

什么是一致性哈希,为什么要在项目中使用它?

一致性哈希是一种用于分布式系统中数据分片的算法。
image.png

如何保证一致性哈希算法的有效性?

image.png

一致性哈希算法中的虚拟节点是什么?它们的作用是什么?

image.png

虚拟节点怎么实现的?查找目标 key 的过程是怎样的?

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
func (m *Map) Get(key string) string {
if len(m.keys) == 0 {
return ""
}
hash := int(m.hash([]byte(key)))
idx := sort.Search(len(m.keys), func(i int) bool {
return m.keys[i] >= hash
})
return m.hashMap[m.keys[idx%len(m.keys)]]
} //选择节点就非常简单了,第一步,计算 key 的哈希值。
//第二步,顺时针找到第一个匹配的虚拟节点的下标 idx,从 m.keys 中获取到对应的哈希值。
//如果 idx == len(m.keys),说明应选择 m.keys[0],因为 m.keys 是一个环状结构,所以用取余数的方式来处理这种情况。
//第三步,通过 hashMap 映射得到真实的节点。

一致性hash 存在的问题及解决方案?

https://blog.csdn.net/CoderTnT/article/details/112383202

协商填充、预热机制

image.png

Raft算法解析

系统设计07-Raft

Raft中的一个Term是什么意思?

image.png

Raft状态机是怎样进行切换的?

image.png

如何保证最短时间内竞选出Leader,防止竞选冲突?

image.png

如何防止别的Candidate在遗漏部分数据的情况下发起投票成为Leader?

image.png

Raft有节点宕机后怎样?

image.png

为什么Raft算法在确定可用节点数量时不需要考虑拜占庭将军问题?

image.png

什么是etcd以及etcd的特点、缺点?

etcd是一种开源的分布式键值存储库,用于保存和管理分布式系统保持运行所需的关键信息。
特点:

  • 简单:支持REST风格的HTTP+JSON API
  • 安全:支持HTTPS方式的访问
  • 快速:支持并发1k/s的写操作
  • 可靠:支持分布式结构,基于Raft的一致性算法,Raft是一套通过选举主节点来实现分布式系统一致性的算法。

缺点:

  • 对于小规模应用来说,ETCD可能会带来额外的复杂性和开销
  • ETCD需要部署在集群中的多个节点上,如果节点之间的网络连接不稳定,可能会影响数据的一致性和可用性。
  • ETCD的存储能力和性能受到硬件和网络环境的限制。在处理大量数据或高并发请求时,可能需要增加节点数量或升级硬件设备。

etcd的使用场景?

  • 服务发现:服务发现主要解决在同一个分布式集群中的进程或服务,要如何才能找到对方并建立连接。本质上来说,服务发现就是想要了解集群中是否有进程在监听UDP或TCP端口,并且通过名字就可以查找和连接。
  • 消息发布与订阅:在分布式系统中,最适用的一种组件间通信方式就是消息发布与订阅。即构建一个配置共享中心,数据提供者在这个配置中心发布消息,而消息使用者则订阅他们关心的主题,一旦主题有消息发布,就会实时通知订阅者。通过这种方式可以做到分布式系统配置的集中式管理与动态更新。应用中用到的一些配置信息放到etcd上进行集中管理。
  • 负载均衡:在分布式系统中,为了保证服务的高可用以及数据的一致性,通常都会把数据和服务部署多份,以此达到对等服务,即使其中的某一个服务失效了,也不影响使用。etcd本身分布式架构存储的信息访问支持负载均衡。etcd集群化以后,每个etcd的核心节点都可以处理用户的请求。所以,把数据量小但是访问频繁的消息数据直接存储到etcd中也可以实现负载均衡的效果。
  • 分布式通知与协调:与消息发布和订阅类似,都用到了etcd中的Watcher机制,通过注册与异步通知机制,实现分布式环境下不同系统之间的通知与协调,从而对数据变更做到实时处理。
  • 分布式锁:因为etcd使用Raft算法保持了数据的强一致性,某次操作存储到集群中的值必然是全局一致的,所以很容易实现分布式锁。锁服务有两种使用方式,一是保持独占,二是控制时序。
  • 集群监控与Leader竞选:通过etcd来进行监控实现起来非常简单并且实时性强。

为什么在项目中使用etcd,怎么用的?


image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
var (
defaultEtcdConfig = clientv3.Config{
Endpoints: []string{"localhost:2379"},
DialTimeout: 5 * time.Second,
}
)

// etcdAdd 在租赁模式添加一对kv至etcd
func etcdAdd(c *clientv3.Client, lid clientv3.LeaseID, service string, addr string) error {
em, err := endpoints.NewManager(c, service)
if err != nil {
return err
}
//return em.AddEndpoint(c.Ctx(), service+"/"+addr, endpoints.Endpoint{Addr: addr})
return em.AddEndpoint(c.Ctx(), service+"/"+addr, endpoints.Endpoint{Addr: addr}, clientv3.WithLease(lid))
}

// Register 注册一个服务至etcd
// 注意 Register将不会return 如果没有error的话
func Register(service string, addr string, stop chan error) error {
// 创建一个etcd client
cli, err := clientv3.New(defaultEtcdConfig)
if err != nil {
return fmt.Errorf("create etcd client failed: %v", err)
}
defer cli.Close()
// 创建一个租约 配置5秒过期
resp, err := cli.Grant(context.Background(), 5)
if err != nil {
return fmt.Errorf("create lease failed: %v", err)
}
leaseId := resp.ID
// 注册服务
err = etcdAdd(cli, leaseId, service, addr)
if err != nil {
return fmt.Errorf("add etcd record failed: %v", err)
}
// 设置服务心跳检测
ch, err := cli.KeepAlive(context.Background(), leaseId)
if err != nil {
return fmt.Errorf("set keepalive failed: %v", err)
}

log.Printf("[%s] register service ok\n", addr)
for {
select {
case err := <-stop:
if err != nil {
log.Println(err)
}
return err
case <-cli.Ctx().Done():
log.Println("service closed")
return nil
case _, ok := <-ch:
// 监听租约
if !ok {
log.Println("keep alive channel closed")
_, err := cli.Revoke(context.Background(), leaseId)
return err
}
//log.Printf("Recv reply from service: %s/%s, ttl:%d", service, addr, resp.TTL)
}
}
}
  1. 发现方法的实现:服务端通过etcd的客户端实现的Watch方法监听事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // EtcdDial 向grpc请求一个服务
    // 通过提供一个etcd client和service name即可获得Connection
    func EtcdDial(c *clientv3.Client, service string) (*grpc.ClientConn, error) {
    etcdResolver, err := resolver.NewBuilder(c)
    if err != nil {
    return nil, err
    }
    return grpc.Dial(
    "etcd:///"+service,
    grpc.WithResolvers(etcdResolver),
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithBlock(),
    )
    }
    //用于通过gRPC与Etcd建立连接。它通过提供Etcd服务的名称,借助Etcd解析器将其解析为实际的网络地址。
    //然后,使用不安全的传输凭据建立连接,并在连接建立之前进行阻塞。
    //最终,函数返回一个指向已建立连接的grpc.ClientConn类型的指针,或者在发生错误时返回一个错误

etcd的组成部分?

image.png
从etcd的架构图中我们可以看到,etcd主要分为四个部分。

  • HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求。
  • Store:用于处理etcd支持的各类功能的事务,包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等,是etcd对用户提供的大多数API功能的具体实现。
  • Raft:Raft强一致性算法的具体实现,是etcd的核心。
  • WAL:Write Ahead Log(预写式日志),是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外,etcd就通过WAL进行持久化存储。WAL中,所有的数据提交前都会事先记录日志。Snapshot是为了防止数据过多而进行的状态快照;Entry表示存储的具体日志内容。

通常,一个用户的请求发送过来,会经由HTTP Server转发给Store进行具体的事务处理,如果涉及到节点的修改,则交给Raft模块进行状态的变更、日志的记录,然后再同步给别的etcd节点以确认数据提交,最后进行数据的提交,再次同步。

为什么需要Snapshot快照?

etcd使用快照(Snapshot)来保存当前数据的状态。当etcd的数据存储变得很大时,为了避免长时间的恢复操作,etcd会周期性地创建快照。快照包含了某个时刻的etcd数据状态,可以用来加速系统的启动和恢复。

什么是WAL,WAL的具体结构?

WAL(Write-Ahead Logging)是一种常见的数据持久化技术,用于在写入数据到存储引擎之前,首先将数据写入到一个预先分配的日志文件中。etcd使用WAL来持久化写入操作,确保数据的持久性。这意味着,即使在etcd节点崩溃后,通过WAL,系统可以回放日志,从而保证数据的一致性。
WAL通常由多个日志条目组成,每个日志条目记录一个写入操作。它包括写入的键、值,以及操作的类型等信息。这些日志条目被顺序地追加到WAL文件中。当etcd节点需要恢复数据时,它会从WAL文件中读取日志条目,将数据还原到崩溃前的状态。

什么是Raft协议的Entry记录?

在etcd中,Entry是指存储在日志中的基本操作单元。每个Entry包含一个唯一的索引号(index)、任期号(term)和一个命令(command)。这些Entries按照顺序附加到etcd的日志中,确保了在分布式系统中的一致性和可靠性。Entries的添加和读取是etcd中非常重要的操作,它们记录了系统的状态变化和用户的操作。

etcd的逻辑视图和物理视图?

在etcd中,逻辑视图是用户从应用程序或客户端看到的数据模型。它把数据组织成键值对的形式,用户可以使用这些键值对来进行读取、写入和删除操作。逻辑视图隐藏了etcd的内部实现细节,使得用户可以专注于数据的操作而不用担心底层存储的复杂性。物理视图则是etcd集群内部的数据存储和复制方式。etcd使用Raft一致性算法来确保数据的一致性和可靠性。在物理视图中,数据被分布在多个节点上,每个节点都有自己的存储引擎和文件系统。etcd集群会定期进行数据同步,以确保所有节点上的数据保持一致。
总的来说,逻辑视图提供了用户友好的数据操作接口,而物理视图则描述了etcd集群内部的数据存储和同步机制。这两者共同构成了etcd的数据管理体系。

Proxy模式取代Standby模式的原因?

etcd的Proxy模式取代了Standby模式,是因为Proxy模式允许etcd集群的前端引入代理节点,从而提供了更好的可伸缩性和灵活性。Proxy节点可以接收客户端请求,并将这些请求转发给后端的etcd集群节点。这种方式可以分摊集群节点的压力,提高系统的整体性能。相比之下,Standby模式通常只有一个备用节点,无法提供Proxy模式的灵活性和负载均衡能力。

etcd如何保证强一致性?

image.png

etcd分布式锁实现的基础机制是怎样的?

image.png
具体步骤
步骤 1: 准备

  • 客户端连接 Etcd,以 /lock/mylock 为前缀创建全局唯一的 key,假设第一个客户端对应的 key=”/lock/mylock/UUID1”,第二个为 key=”/lock/mylock/UUID2”;客户端分别为自己的 key 创建租约 - Lease,租约的长度根据业务耗时确定,假设为 15s;

步骤 2: 创建定时任务作为租约的“心跳”

  • 当一个客户端持有锁期间,其它客户端只能等待,为了避免等待期间租约失效,客户端需创建一个定时任务作为“心跳”进行续约。此外,如果持有锁期间客户端崩溃,心跳停止,key 将因租约到期而被删除,从而锁释放,避免死锁。

步骤 3: 客户端将自己全局唯一的 key 写入 Etcd

  • 进行 put 操作,将步骤 1 中创建的 key 绑定租约写入 Etcd,根据 Etcd 的 Revision 机制,假设两个客户端 put 操作返回的 Revision 分别为 1、2,客户端需记录 Revision 用以接下来判断自己是否获得锁。

步骤 4: 客户端判断是否获得锁

  • 客户端以前缀 /lock/mylock 读取 keyValue 列表(keyValue 中带有 key 对应的 Revision),判断自己 key 的 Revision 是否为当前列表中最小的,如果是则认为获得锁;否则监听列表中前一个 Revision 比自己小的 key 的删除事件,一旦监听到删除事件或者因租约失效而删除的事件,则自己获得锁。

步骤 5: 执行业务

  • 获得锁后,操作共享资源,执行业务代码。

步骤 6: 释放锁

  • 完成业务流程后,删除对应的key释放锁。

什么是RPC(远程过程调用)?能否提供一个简单的定义和例子?

RPC,全称为远程过程调用,是一种计算机通信协议。该协议允许运行在一台计算机上的程序调用另一台计算机上的子程序,就像调用本地程序一样,无需程序员显式地编制远程交互的细节。RPC是分布式系统中常用的通信方式。
举例来说,假设你有一个客户端应用需要从服务器获取天气信息。你可以在服务器端定义一个名为“getWeather”的过程,然后在客户端通过RPC调用这个过程,就像调用客户端本地的一个函数一样。客户端只需要知道这个过程的名称和参数,无需关心具体的网络通信细节。

RPC和RESTful API有什么区别?在什么情况下我应选择使用RPC而非RESTful API?

RPC和RESTful API是两种不同的网络通信模式。

  • RPC: 远程过程调用 (RPC) 通常被视为一种行为中心的模式,因为它关注的是执行特定的操作或函数。在RPC中,客户端知道需要远程访问的过程或方法的名称,以及所需的参数。一个RPC调用通常对应于服务器上的一个特定操作。
  • RESTful API: 代表状态转移 (REST) 是一种资源中心的模式,它将所有事物视为资源。这些资源通过URL进行标识,并使用HTTP动词(如GET, POST, PUT, DELETE等)进行操作。RESTful API通常更加简单、直观,也更符合Web的设计原则。

在选择RPC或RESTful API时,你可以根据以下因素进行考虑:

  • 复杂性: 如果你的服务非常复杂,有大量的操作和交互,那么RPC可能是更好的选择,因为它可以提供更加详细、精细的接口。
  • 可理解性和标准化: 如果你希望你的API能够被广泛理解和使用,那么RESTful API可能更适合,因为它对资源的操作更直观,也更符合Web的标准。
  • 性能: RPC通常比RESTful API有更高的性能,因为它可以直接调用服务端的函数,而无需经过HTTP的封装和解封装。

总的来说,选择RPC还是RESTful API主要取决于你的具体需求和偏好。

在RPC中,如何处理网络延迟和超时?有哪些常见的策略?

处理网络延迟和超时是RPC中的一个重要问题。以下是一些常见的策略:

  • 设置超时限制: 为RPC调用设置合理的超时时间,如果超过这个时间还没有得到响应,那么认为这个调用失败。这可以防止客户端无限期地等待响应,从而提高系统的健壮性。
  • 重试机制: 如果一个RPC调用失败(例如,因为超时或其他网络问题),客户端可以选择重试。重试机制需要慎重设计,以防止过度的重试导致系统负载过大。
  • 负载均衡: 对于高延迟的网络调用,可以使用负载均衡技术,将请求分散到多个服务器,以减少每个服务器的负载,从而降低延迟。
  • 异步RPC: 异步RPC允许客户端在等待响应时继续执行其他任务,从而不受网络延迟的影响。这通常需要更复杂的编程模型,但可以提高系统的整体性能。
  • 使用更快的网络协议: 例如,gRPC默认使用HTTP/2协议,它比传统的HTTP/1.1协议更高效,可以降低网络延迟。

在分布式系统中,为什么RPC通常被认为是阻塞的?这有什么影响?

RPC (远程过程调用) 在默认情况下通常被认为是阻塞的。这意味着当一个客户端向服务端发出RPC请求时,客户端会停止执行,等待服务端的响应。在服务端响应之前,客户端不会进行其他操作。
阻塞RPC有以下影响:

  • 性能影响:因为客户端在等待服务端响应期间不能做其他事情,所以如果网络延迟高或者服务端处理时间长,客户端将会被长时间阻塞,这可能会对性能产生负面影响。
  • 并发性影响:在单线程环境中,由于阻塞RPC会导致线程挂起,因此并发性能会受到限制。为了提高并发性能,可能需要引入多线程或异步编程模型。
  • 复杂性影响:虽然阻塞RPC的模型相对简单,易于理解,但在处理复杂的、需要并发或并行处理的任务时,可能需要使用更复杂的编程模型,如回调、异步RPC等。

然而,虽然RPC通常默认为阻塞,但许多RPC框架(如gRPC)也支持非阻塞或异步的RPC,允许客户端在等待RPC响应的同时进行其他操作。

如何理解同步RPC与异步RPC的区别?在实际应用中,应该如何选择?

同步RPC和异步RPC是两种不同类型的RPC调用方式:

  • 同步RPC: 客户端发出RPC请求后,会等待服务端的响应。在收到响应之前,客户端不会执行其他操作。这是最简单的RPC形式,也是最常见的形式。
  • 异步RPC: 客户端发出RPC请求后,不会立即等待服务端的响应。相反,它会继续执行其他操作。当服务端的响应到达时,客户端将通过某种方式(如回调函数或者Future对象)得到通知。异步RPC可以提高客户端的效率,特别是在需要进行多个并发RPC请求的情况下。

在实际应用中,选择同步RPC还是异步RPC主要取决于你的需求:

  • 如果你的应用只需要进行少量的RPC请求,且这些请求之间没有并发的需求,那么同步RPC可能是更简单、更方便的选择。
  • 如果你的应用需要进行大量的并发RPC请求,或者需要在等待RPC响应的同时进行其他操作,那么异步RPC可能是更好的选择。异步RPC虽然编程模型较复杂,但可以提高应用的性能和响应性。

在使用RPC时,如何处理服务端的状态管理?

服务端的状态管理是分布式系统中的一项重要任务,尤其是在使用RPC时。以下是一些处理服务端状态管理的策略:

  • 无状态服务: 无状态服务意味着服务不保留任何关于客户端的信息或状态。每个请求都被视为独立的,无关联的操作。这种设计可以极大地简化服务的扩展性,因为你可以简单地增加服务实例来处理更多的请求,而无需担心同步或迁移状态。
  • 有状态服务: 有状态服务会保持关于客户端的某种信息或状态。这种状态可能会影响服务对请求的处理。在有状态服务中,你需要在服务实例之间同步状态,或者确保客户端总是连接到具有其状态的特定服务器。这会增加设计的复杂性,并可能影响服务的可扩展性。
  • 会话数据: 如果服务需要跟踪会话数据(例如,用户登录信息),你通常需要使用某种形式的会话存储。这可能是内存中的存储(例如,使用分布式缓存),或者是持久的存储(例如,使用数据库)。
  • 持久化数据: 对于需要长期存储的数据,你通常会使用数据库或其他形式的持久化存储。这种存储可以在服务重启或失败后保留数据。

在实际应用中,理想的情况是使服务尽可能无状态,以最大限度地提高可扩展性。如果需要处理状态,应该使用合适的工具和策略,以确保数据的一致性和可靠性,并尽量减少状态管理对服务可扩展性的影响。

为什么说在RPC中序列化和反序列化是一个重要的环节?有哪些常见的序列化和反序列化方法?

序列化是将数据结构或对象状态转换为可以存储或传输的形式的过程,这通常涉及将数据结构或对象状态转换为字节流。反序列化则是将这种字节流恢复为原始数据结构或对象状态的过程。在RPC中,序列化和反序列化至关重要,原因如下:

  • 数据交换: RPC允许在不同的系统或网络节点之间进行函数调用。为了在这些节点之间传输数据(例如,函数参数和返回值),我们需要将数据序列化为通用格式,然后在接收端进行反序列化。
  • 网络效率: 有效的序列化和反序列化可以减少网络传输的数据量,从而提高网络效率。
  • 兼容性和扩展性: 选择一个能够支持向前和向后兼容性的序列化格式,可以帮助你更容易地升级和扩展系统。

以下是一些常见的序列化和反序列化方法:

  • JSON: JSON是一种常见的数据交换格式,广泛应用于Web服务。它易于阅读和编写,但可能不如其他格式那么高效,尤其是对于大型和复杂的数据。
  • XML: XML是另一种常见的数据交换格式,虽然它的可读性和易用性不如JSON,但它支持更复杂和灵活的数据结构。
  • Protocol Buffers: Protocol Buffers(或称为ProtoBuf)是Google开发的一种数据序列化协议。它小巧、快速、简单,且可以跨多种语言使用。
  • MessagePack: MessagePack是一种二进制的序列化格式,它像JSON一样可以序列化/反序列化复杂的数据类型,但比JSON更小、更快。
  • Avro: Avro是Apache的一个项目,它提供了一种数据序列化系统,设计用于大数据和高并发的场景。
  • Thrift: Thrift是Apache的一个轻量级、跨语言的服务开发框架,它提供了自己的数据序列化方式。

在大规模分布式系统中,如何保证RPC的可扩展性和稳定性?

在大规模的分布式系统中,保证RPC(远程过程调用)的可扩展性和稳定性是非常重要的。以下是一些常见的策略:

  • 负载均衡: 使用负载均衡可以将RPC请求分发到多个服务实例,防止某个服务实例过载。负载均衡可以在客户端实现,也可以通过专门的负载均衡器实现。
  • 服务发现: 在动态的环境中,服务可能会增加或减少。服务发现机制允许客户端找到可用的服务实例,并在实例变更时进行更新。
  • 容错和重试策略: 当RPC请求失败时(例如,由于网络问题或服务实例故障),客户端可以选择重试。为了防止重试导致服务过载,可以使用指数退避等策略。
  • 监控和日志: 通过监控服务的性能和记录详细的日志,可以发现问题并进行调试。这对于保证RPC的稳定性非常重要。
  • 熔断器模式: 熔断器模式可以防止系统在故障状态下过度负载,当检测到服务连续失败或响应过慢时,熔断器会“打开”并开始拒绝请求,直到服务恢复正常。
  • 限流: 限流可以防止服务在高负载下过载,通过限制系统在单位时间内接收的请求数量,保证服务对请求的处理能力。
  • 使用高效的序列化/反序列化机制: 选择高效的序列化/反序列化机制,如Protocol Buffers或Avro,可以减少网络传输的数据量,提高RPC的效率。
  • 无状态服务设计: 尽可能设计无状态的服务,因为无状态的服务更容易扩展,无需同步和迁移状态。
  • 异步RPC: 异步RPC允许客户端在等待RPC响应的同时进行其他操作,可以提高系统的整体性能。

如何在RPC系统中实现错误处理和异常管理?有哪些常见的策略?

在RPC系统中,错误处理和异常管理是非常重要的。以下是一些常见的策略:

  • 错误码和错误信息: 当服务端遇到错误时,可以返回一个错误码和描述错误的信息。客户端可以根据这个错误码和错误信息来决定如何处理这个错误。
  • 重试策略: 如果一个RPC请求失败(例如,因为网络问题或服务端故障),客户端可以选择重试。重试策略需要慎重设计,以防止过度的重试导致服务过载。常见的重试策略包括固定间隔重试、指数退避等。
  • 熔断机制: 熔断机制类似于电路中的熔断器,当连续的RPC请求失败时,熔断器会“打开”,阻止进一步的RPC请求,防止系统过载。在一段时间后,熔断器会自动“关闭”,允许RPC请求再次通过。
  • 超时处理: 对于每个RPC请求,都应该设置一个合理的超时时间。如果超过这个时间还没有得到响应,那么认为这个请求失败。这可以防止客户端无限期地等待响应。
  • 故障转移: 当一个服务实例发生故障时,可以将RPC请求转移到其他健康的服务实例,这称为故障转移。
  • 服务降级: 当系统压力过大或部分服务不可用时,可以选择关闭或降低一些非核心服务的功能,以保证核心服务的正常运行。

有哪些常见的RPC框架?如gRPC, Thrift等。它们的优缺点是什么?

以下是一些常见的RPC框架,以及它们的优缺点:

  • gRPC:优点:gRPC是Google开发的一个高性能、开源的RPC框架,支持多种语言。它使用Protocol Buffers作为接口定义语言,可以生成客户端和服务端的代码。gRPC支持四种不同的服务类型:单项RPC、服务器流式RPC、客户端流式RPC和双向流式RPC。gRPC使用HTTP/2协议,支持流式传输,比HTTP/1.1更高效。缺点:尽管gRPC支持多种语言,但在某些语言上的支持可能不如其他语言那么完善。此外,由于gRPC使用HTTP/2协议,某些老旧的系统可能无法支持。
  • Apache Thrift:优点:Apache Thrift是Facebook开发的一个跨语言的服务开发框架。它提供了一个接口定义语言,可以生成客户端和服务端的代码。Thrift支持多种语言,包括C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, OCaml和Delphi等。Thrift的传输层可以使用多种协议,包括TCP、HTTP、非阻塞等。缺点:Thrift的文档和社区支持可能不如gRPC那么活跃。此外,Thrift的接口定义语言比Protocol Buffers更复杂,学习曲线可能更陡峭。
  • JSON-RPC 和 XML-RPC:优点:JSON-RPC和XML-RPC是两种简单的RPC协议,它们使用JSON和XML格式进行数据传输,因此可以在任何支持这两种格式的语言中使用。这两种协议都非常简单,易于理解和实现。缺点:由于JSON-RPC和XML-RPC使用文本格式进行数据传输,所以它们可能不如使用二进制格式的RPC协议那么高效。

什么是grpc,它比传统的HTTP通信有什么优势,适用什么场景?

gRPC是Google开发的一个高性能、开源的RPC框架,支持多种语言。它使用Protocol Buffers作为接口定义语言,可以生成客户端和服务端的代码(支持C++、Java、Go、Python等作为客户端和服务端语言)。
优势:

  1. 协议: gRPC使用HTTP/2作为通信协议,而传统的HTTP通信通常使用HTTP/1.1。HTTP/2相比HTTP/1.1具有更好的性能,支持多路复用、头部压缩、服务器推送等特性,提供了更高效的网络传输。
  2. 数据格式: gRPC使用Protocol Buffers(protobuf)作为接口描述语言和数据序列化格式,而传统的HTTP通信通常使用JSON或XML。Protocol Buffers是一种高效的二进制数据序列化格式,相比JSON和XML更轻量,序列化和反序列化的速度更快。
  3. IDL(Interface Definition Language): gRPC使用IDL来定义服务接口和消息格式,这样可以生成客户端和服务器端的代码,使得开发人员更容易理解和使用接口。而传统的HTTP通信通常使用RESTful API,开发人员需要通过文档来了解API的使用方式。
  4. 多语言支持: gRPC支持多种编程语言,包括但不限于C++, Java, Python, Go, Ruby, Node.js, Android, iOS等。传统的HTTP通信也支持多种语言,但是由于语言间的差异,可能需要手动处理一些细节。
  5. 流式处理: gRPC支持双向流(Bidirectional streaming),即客户端和服务器端可以在同一个连接上同时发送多个消息,这在处理实时性要求高的场景中非常有用。传统的HTTP通信也可以实现类似的功能,但需要通过WebSocket等技术来实现。

使用场景:

  1. 微服务架构: gRPC适用于构建微服务架构,其中各个微服务之间需要进行高效、快速的通信。gRPC的性能和多语言支持使其成为微服务间通信的理想选择。
  2. 多语言团队合作: 如果开发团队使用不同的编程语言,而需要进行跨语言通信,gRPC是一个非常好的选择。它支持多种编程语言,可以让不同语言编写的服务进行无缝通信。
  3. 实时应用和游戏: gRPC支持双向流(Bidirectional streaming),适用于实时应用和在线游戏等场景,其中需要在客户端和服务器之间进行双向通信,实时传输数据。
  4. 大规模数据处理: gRPC的高性能和流式处理特性使其适用于大规模数据处理,例如数据分析、数据挖掘和机器学习等领域,可以快速、高效地传输大量数据。

gRPC提供了哪些类型的通信模式及其使用场景,你在项目中选择了哪种模式?

  1. 单向通信:客户端发送单个请求并等待单个响应。适用于请求-响应模式的操作,例如发送邮件。
  2. 服务器流式通信:客户端发送单个请求,服务器可以返回多个响应。适用于客户端只需发送请求,然后等待服务器返回数据的场景,比如从服务器端实时推送日志数据给客户端。
  3. 客户端流式通信:客户端可以发送多个请求,服务器只需返回单个响应。适用于客户端需要向服务器发送大量数据,但只关心单个响应的情况,比如文件上传。
  4. 双向流式通信:客户端和服务器可以分别通过流发送多个消息。适用于需要交互式地发送多个消息的场景,例如聊天应用程序。

我在项目中使用了单向通信这种模式。

gRPC使用了哪种序列化和反序列化?为什么?

gRPC使用Protocol Buffers(protobuf)作为默认的数据序列化和描述接口语言。
原因:

  • 高效性: Protocol Buffers提供了高效的数据序列化和反序列化,生成的数据包相比文本格式更小,节省带宽,同时在解析上更为迅速。
  • 可扩展性: Protocol Buffers的数据格式是可扩展的,新的字段和消息类型可以被添加到已有的消息格式中,而不会破坏现有的客户端和服务端的兼容性。这种特性在分布式系统中尤为重要。
  • 多语言支持: Protocol Buffers支持多种编程语言,这意味着你可以在不同的语言中使用相同的接口定义,而不需要为每种语言单独定义接口和数据传输格式。

grpc的缺点?

  • 浏览器支持受限,无法通过浏览器直接调用grpc服务。可以通过grpc-web来做,但并不支持所有 gRPC 功能。 不支持客户端和双向流式传输,并且对服务器流式传输的支持有限。
  • 默认情况下,gRPC 消息使用 Protobuf 进行编码。 尽管 Protobuf 可以高效地发送和接收,但其二进制格式人工不可读。
  • gRPC尚未提供连接池,需要自行实现
  • 尚未提供“服务发现”、“负载均衡”机制

protobuf的缺点?

  1. 不易读: Protocol Buffers是一种二进制格式,不像JSON或XML那样易于人类阅读和调试。这使得在调试和开发阶段,理解数据结构变得更加困难。
  2. 不支持动态模式: Protocol Buffers使用预定义的消息格式,这意味着消息结构必须在编译时定义,并且不容易支持动态模式。对于需要在运行时动态定义消息结构的场景,这可能是一个限制。
  3. 不是自描述的: Protocol Buffers消息本身不包含关于消息类型的信息。这意味着接收者必须在其他地方知道消息的类型,这可能会导致一些通信上的困扰,尤其是当消息结构发生变化时。
  4. 没有内建的标准化架构: Protocol Buffers没有像XML Schema或JSON Schema那样的内建标准化架构,用于定义消息的结构和约束。这使得在不同团队或组织中共享数据结构时可能会引发一些问题。
  5. 不适用于所有的数据类型: Protocol Buffers适用于结构化数据,但不太适用于包含嵌套、非结构化或自由格式文本的数据。对于这类数据,其他格式可能更加合适。
  6. 版本兼容性: 当消息结构发生变化时,可能会导致与旧版本的兼容性问题。虽然Protocol Buffers提供了一些版本兼容性的支持,但在某些情况下,仍然需要小心处理版本升级的问题。

protobuf为什么拥有更少的数据量?

  1. 二进制格式: Protocol Buffers使用二进制编码,而不是文本编码(像XML和JSON)。二进制数据通常比文本数据更紧凑,因为它不包含像标签名这样的额外字符,也没有可读性的空格、缩进和换行符。这意味着相同的数据在二进制格式下会更小。
  2. 紧凑的数据结构: Protocol Buffers的数据结构非常紧凑,不仅避免了标签重复,还使用了一种紧凑的编码方案(Varint编码)来表示数字,使得小数字使用更少的字节数。这种紧凑性减少了数据的传输和存储成本。
  3. 预定义的消息格式: Protocol Buffers使用预定义的消息格式,这意味着消息的结构在编译时就确定了。相比之下,JSON和XML通常需要在数据中包含字段名,而这些字段名在每个数据实例中都会重复,增加了数据量。
  4. 高效的序列化和反序列化算法: Protocol Buffers的序列化和反序列化算法被高度优化,能够以非常高的速度进行数据的编码和解码。这使得数据在传输和处理时更加高效。
  5. 类型信息的压缩: Protocol Buffers使用了标签(tag)而不是字段名称进行标识。这些标签在编码时被映射到字段的名称,而不需要在每个数据实例中重复传输字段名称。这种方式减少了数据量。

请描述一下gRPC的工作原理,包括请求和响应的过程。

  1. 定义服务和消息格式: 首先,您需要定义一个服务接口和消息格式。服务接口定义了可以远程调用的方法,而消息格式定义了这些方法所使用的参数和返回值的数据结构,通常使用Protocol Buffers进行定义。
  2. 生成代码: 使用定义好的服务接口和消息格式,通过gRPC的编译器生成客户端和服务器端的代码。这些生成的代码包含了用于序列化和反序列化消息、处理RPC调用的代码等。
  3. 启动服务器: 在服务器端,您需要编写代码来实现定义好的服务接口。然后,服务器启动并监听指定的网络端口,等待客户端的请求。
  4. 客户端发起请求: 客户端根据生成的代码创建相应的RPC客户端,并且调用远程服务的方法,传递方法所需的参数。
  5. 请求的传输和序列化: 当客户端调用远程方法时,gRPC会负责将方法调用和参数序列化为字节流,然后通过网络传输到服务器端。
  6. 服务器端处理请求: 服务器接收到请求后,gRPC会负责将接收到的字节流反序列化为方法调用和参数。然后,服务器执行相应的方法,并将结果序列化为字节流。
  7. 响应的传输和反序列化: 服务器将序列化后的结果作为响应,通过网络传输回客户端。客户端接收到响应后,gRPC会负责将字节流反序列化为方法的返回值。
  8. 客户端处理响应: 客户端接收到响应后,可以将结果转换为适当的数据类型,并且继续执行后续的逻辑处理。

微服务架构的好处?

1. 易于开发和维护
一个微服务只关注一个特定的业务功能,所以它的业务清晰、代码量较少。开发和维护单个微服务相对是比较简单的。而整个应用是由若干个微服务构建而成的,所以整个应用也会维持在可控状态。
2. 单个微服务启动较快
单个微服务代码量较少,所以启动会比较快。
3. 局部修改容易部署
单体应用只要有修改,就得重新部署整个应用,微服务解决了这样的问题。一般来说,对某个微服务进行修改,只需要重新部署这个服务即可。
4. 技术栈不受限
在微服务中,我们可以结合项目业务及团队的特点,合理地选择技术栈。例如某些服务可使用关系型数据库MySQL;某些微服务有图形计算的需求,我们可以使用Neo4J;甚至可以根据需要,部分微服务使用Java开发,部分微服务使用NodeJS进行开发。
5. 按需伸缩
我们可以根据需求,实现细粒度的扩展。例如:系统中的某个微服务遇到了瓶颈,我们可以结合这个微服务的业务特点,增加内存、升级CPU或者是增加节点,节约了硬件成本

maxBytes=0有啥意思?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Add 方法用于向缓存中添加新的键值对。如果键已存在,则更新对应的值,并将节点移动到链表的最前面;
// 如果键不存在,则在链表头部插入新的节点,并更新已占用的容量。
// 如果添加新的键值对后超出了最大存储容量,则会连续移除最久未使用的记录,直到满足容量要求。
func (c *LRUCache) Add(key string, value Value, ttl time.Duration) {
if ele, ok := c.cache[key]; ok {
c.ll.MoveToFront(ele)
kv := ele.Value.(*entry)
c.nBytes += int64(value.Len()) - int64(kv.value.Len())
kv.value = value
// 更新过期时间时,判断是否应该保留原本的过期时间
if kv.expire.Before(time.Now().Add(ttl)) {
kv.expire = time.Now().Add(ttl)
}
} else {
ele = c.ll.PushFront(&entry{key: key, value: value, expire: time.Now().Add(ttl)})
c.cache[key] = ele
c.nBytes += int64(len(key)) + int64(value.Len())
}
for c.maxBytes != 0 && c.maxBytes < c.nBytes {
c.RemoveOldest()
}
}

如果 maxBytes 的值为 0,表示没有限制缓存的总大小,即不限制缓存的内存使用量。在这种情况下,不需要执行缓存大小控制的相关逻辑,可以直接添加或更新缓存项。因此,不需要在 Add 方法中执行删除最旧的缓存项 (RemoveOldest) 的操作。

最后请你记住项目中的所有代码必须看懂、看透。

  • 标题: 项目GeeCache面试题
  • 作者: Olivia的小跟班
  • 创建于 : 2023-10-23 22:51:00
  • 更新于 : 2024-03-06 18:09:08
  • 链接: https://www.youandgentleness.cn/2023/10/23/项目GeeCache面试题/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
此页目录
项目GeeCache面试题