LOADING

加载过慢请开启缓存 浏览器默认开启

面经2

2025/12/25 面经

2025.12.25

微服务场景下相较于HTTP为什么选用gRPC?

在微服务架构中,选择 gRPC 而不是传统 HTTP 的主要原因通常集中在 性能效率 上,特别是当涉及到大规模、高并发的服务间通信时。以下是几个主要因素:

性能与效率:

  • 更高的吞吐量和更低的延迟:gRPC 使用 Protocol Buffers (Protobuf) 作为序列化协议,Protobuf 是二进制格式,相比于 JSON 格式(常用于 HTTP),它更紧凑、更高效,序列化和反序列化速度更快。这对于性能要求高的微服务系统尤其重要,能够减少通信延迟和带宽消耗。
  • 基于 HTTP/2 协议:gRPC 基于 HTTP/2 协议,支持 多路复用流控制头压缩 等特性,这些都能显著提高性能,减少网络延迟。而 HTTP/1.x 协议的性能在这些方面有限,特别是在高并发情况下。

双向流支持:

  • 实时双向通信:gRPC 内建支持 双向流,这使得客户端和服务器能够实现实时、双向的数据交换。例如,客户端可以在不等待响应的情况下发送多个请求,同时服务器可以返回多个响应。这对于实时通信、流式数据传输(如 WebSocket、视频流等)非常适用,而传统的 HTTP 通常只能进行单向的请求响应。

严格的接口定义:

  • 强类型接口:gRPC 使用 Protobuf 文件来定义服务的接口,这意味着接口在编译阶段就会进行类型检查,确保客户端和服务端的一致性。这减少了运行时的错误,提升了接口的可靠性。而传统的 HTTP API(尤其是 RESTful API)则依赖于 JSON 或 XML,通常没有强类型检查,容易出现类型不匹配或接口不一致的情况。

代码自动生成:

  • 自动生成代码:通过 gRPC 的 Protobuf 文件,可以自动生成客户端和服务端的代码,减少了大量手动编写的工作。这保证了接口的一致性,并减少了错误。HTTP API 通常需要手动编写请求和响应处理逻辑。

内建负载均衡和故障转移:

  • 服务治理的支持:gRPC 内建支持负载均衡、重试、故障转移等功能,这在微服务架构中尤为重要,因为微服务通常运行在多个实例上,如何高效地调度请求是一个关键问题。HTTP 需要通过额外的中间件(如 Nginx、API 网关)来处理这些功能。

邮箱验证怎么防止验证码被暴力破解?

验证码有效期限制

  • 短时间有效期:将验证码设置为短时间有效(例如,5-10分钟)。这样,即使攻击者在一段时间内获取到验证码,也无法长时间进行暴力破解。
  • 一次性使用:确保验证码只能使用一次,防止攻击者重复利用相同的验证码。

限制验证码请求次数

  • 请求次数限制:对同一个邮箱或者同一个 IP 地址的验证码请求次数进行限制。比如,可以限制每个邮箱每分钟最多请求一次验证码,避免攻击者通过频繁请求验证码进行暴力破解。
  • 动态限制:例如,如果检测到某个邮箱或 IP 地址请求验证码的频率异常(比如短时间内发送大量请求),则可以临时封禁或增加验证码请求的间隔。

IP 限制与设备指纹

  • IP 黑名单:在多次请求验证码失败后,可以将该 IP 地址加入黑名单,防止暴力破解者通过同一 IP 发起大量请求。
  • 设备指纹:结合设备信息(如浏览器、操作系统等),建立设备指纹,当多个设备同时请求验证码时可以触发警报,增强防护。

go的GMP有了解过吗?

GMP 模型

GMP 是 Go 语言的调度模型,代表 Goroutine、Machine、Processor。它帮助 Go 高效地管理并发任务。

**Goroutine (G)**:

  • Goroutine 是 Go 中的 轻量级线程。你可以通过 go 关键字启动一个 Goroutine,来执行并发任务。每个 Goroutine 占用很少的内存,通常是 2KB。

**Machine (M)**:

  • Machine 是 Go 语言中的 操作系统线程。每个 M 负责执行一个或多个 Goroutine。M 直接与操作系统的线程绑定。

**Processor (P)**:

  • Processor 是 虚拟处理器,负责调度和管理 Goroutine。P 有一个本地队列,存放着需要执行的 Goroutine。每个 P 会将任务交给 M 执行。

调度原理

  • 每个 P 会选择一个 M 来执行它的任务队列中的 Goroutine。
  • 当 P 的队列空了,P 会从其他 P 的队列中 偷取 一些任务,这叫做 工作窃取
  • 这样,Go 语言的调度器能够有效地分配任务,让多个 Goroutine 并发执行。

go的反射有了解过吗?

Go 反射的基本概念

  • 反射类型: Go 的反射通过 reflect 包实现,反射可以查看对象的类型、值等信息。反射对象有两种类型:
    • reflect.Type:表示类型信息。
    • reflect.Value:表示值信息。

常用的反射操作

  • 获取类型和值:
    • 使用 reflect.TypeOf() 获取对象的类型。
    • 使用 reflect.ValueOf() 获取对象的值。
  • 修改值:
    • 使用 reflect.ValueSet() 方法可以修改对象的值,但要求传入的是一个可以被修改的地址(如指针)。
  • 获取字段和方法:
    • 通过 reflect.Value 可以获取结构体的字段或方法。
    • Elem() 方法用于获取指针指向的值。

反射的应用场景

  • 动态类型检查: 反射使得可以在运行时检查接口的动态类型,特别是在与动态内容交互时(如解析 JSON 或处理动态字段)非常有用。
  • 通用代码: 比如编写通用的 DeepEqual 比较函数,或者根据对象的类型动态实现序列化功能。
  • 插件机制: 反射可以帮助实现插件系统,在运行时动态加载和执行插件。

反射的局限性和性能

  • 性能开销: 反射相较于直接操作类型,会有一定的性能损耗。特别是当反射操作频繁时,可能影响性能。
  • 类型安全: 反射通常会绕过类型安全检查,容易导致错误。
  • 复杂性: 使用反射可能导致代码难以理解和维护,建议只在必要时使用。

go 什么时候会抛panic,能举例吗

主动触发:手动调用 panic() 函数

开发者可主动调用 panic() 函数抛出异常,panic() 接收一个任意类型的参数(通常为字符串或 error,用于描述异常信息)。

被动触发:Go 运行时自动抛出

当程序出现违反 Go 语言语法规则或运行时约束的操作时,运行时会自动触发 panic,常见场景如下:

被动触发场景 代码示例
数组 / 切片索引越界 arr := []int{1,2,3}; fmt.Println(arr[10])
空指针解引用(nil 指针调用方法 / 访问字段) var str *string; fmt.Println(*str)
ok 模式类型断言失败 var i interface{} = "hello"; num := i.(int)(用 num, ok := i.(int) 不会 panic)
整数除以 0 fmt.Println(10 / 0)
关闭已关闭的通道(chan ch := make(chan int); close(ch); close(ch)
向已关闭的只读通道发送数据 ch := make(chan int); close(ch); ch <- 10

go的map了解吗,map是并发安全的吗,map的底层是什么

  • 默认情况下,map 是并发不安全的:如果多个 goroutine 同时读写同一个 map,程序会引发竞态条件(race condition),可能会导致程序崩溃或数据不一致。
  • 读写同时进行:如果一个 goroutine 正在写一个 map,另一个 goroutine 试图读取或写入该 map,就会触发 fatal error: concurrent map writesfatal error: concurrent map read and map write 错误。
  • 读取是安全的,写入不是:在并发环境下,如果仅是多个 goroutine 读取同一个 map,那么这是安全的。但是如果同时有写操作,就会出现问题。
  • 如何保证并发安全: 为了使 map 在并发环境中安全,可以通过 sync.Mutexsync.RWMutex 来保护 map,或者使用 Go 1.9 之后的 sync.Map,它是并发安全的,专为并发设计。

map 的底层实现

  • 桶(Buckets):Go 的 map 底层使用的是哈希表(hash table)。哈希表将键通过哈希函数转换为一个索引,值存储在哈希表的桶中。Go 的 map 采用开放定址法(open addressing)来处理哈希冲突。
  • 哈希桶的管理:
    • 每个桶是一个包含多个键值对的结构,Go 的 map 会根据桶的负载因子来动态扩展哈希表。当 map 中元素的数量超过负载因子的限制时,哈希表会进行重新哈希,扩展桶的数量。
  • 扩容:当哈希表的负载因子达到一定阈值时,map 会进行扩容,并将所有元素重新哈希到新的桶中。这个过程是通过重新分配内存来实现的。
  • 优化:Go 的 map 实现还做了一些性能优化,比如懒加载(lazy deletion),即删除元素时不会立即回收内存,而是标记为删除,直到某个时刻才会进行垃圾回收。

map 的特点

  • 无序:Go 的 map 是无序的,插入的顺序和遍历的顺序不一定相同。每次遍历 map 时,元素的顺序是随机的。
  • 键的唯一性map 中的每个键是唯一的。如果插入的键已经存在,新的值会覆盖原有的值。
  • 动态大小:Go 的 map 会根据实际的存储量动态调整大小,使用哈希表来提高查询效率。

哈希冲突的解决?

在 Go 中,map 的底层实现使用哈希表(hash table)来存储键值对。当发生哈希冲突时,Go 采用 开放定址法(open addressing)来解决冲突。具体来说,Go 的 map 使用桶(buckets)来存储数据,并通过以下方法处理冲突:

哈希表的桶(Buckets)

Go 的 map 底层实现基于哈希表,每个桶可以存储多个键值对。当我们将一个键值对插入 map 时,首先会使用哈希函数计算该键的哈希值,然后根据该哈希值决定将键值对插入哪个桶。如果多个键映射到同一个桶,就发生了哈希冲突。

开放定址法(Open Addressing)

当哈希冲突发生时,Go 使用开放定址法来寻找空的槽位存储冲突的键值对。具体来说,Go 的 map 通过 线性探查 来解决冲突。

线性探查(Linear Probing)

  • 如果一个桶已经有元素,那么 map 会顺序检查下一个桶的位置,直到找到一个空槽位或者找到了匹配的键。
  • 比如,如果插入的位置为桶 i,如果 i 位置已经有元素,map 会检查 i+1 位置,如果那里也有元素,再检查 i+2,依此类推。

了解go的垃圾回收吗?

Go 垃圾回收的基本原理

Go 的垃圾回收机制采用了 三色标记法,分为 标记清除 两个阶段:

  • 标记阶段(Mark Phase):
    在这个阶段,GC 会从根对象(通常是栈上的变量、全局变量等)开始,遍历整个程序中可达的对象,并将这些对象标记为存活对象(reachable)。
  • 清除阶段(Sweep Phase):
    在标记完成后,GC 会遍历堆上的所有对象,回收那些没有被标记为存活的对象的内存。
  • 压缩阶段(Compact Phase):
    Go 在 1.5 版本后引入了压缩阶段,以减少内存碎片。在清除后,它会将存活的对象按顺序排列,使得堆内存空间更紧凑,减少了空闲内存的浪费。

Go 垃圾回收的特点

  • 并发垃圾回收:
    Go 的 GC 是 并发的,这意味着 GC 可以在程序运行的同时进行。通过并发执行标记阶段,减少了 GC 阻塞程序执行的时间。
  • 增量垃圾回收:
    Go 的垃圾回收采用 增量式回收,即将垃圾回收过程分为多个小的步骤执行,而不是一次性完成,这样可以减少对程序的暂停时间(Stop-the-World 时间)。
  • Stop-the-World:
    虽然 Go 的 GC 是并发的,但在标记阶段的某些操作需要停止应用程序的执行,这时称为 Stop-the-World。在 Go 语言的 GC 实现中,Stop-the-World 时间尽可能短。
  • GC 调优:
    Go 的 GC 可以通过 GOGC 环境变量进行调优。GOGC 控制着触发垃圾回收的条件,它的值表示回收触发的 百分比(即堆大小增长的百分比)。默认情况下,GOGC 为 100,意味着当堆大小增长到当前堆的两倍时,垃圾回收会被触发。可以通过调整 GOGC 来控制垃圾回收的频率。

Go GC 的实现细节

  • 三色标记法:
    Go 使用 三色标记法来标记对象的状态。对象可以处于以下三种状态:

    • 白色(White):对象未被访问,垃圾回收将会回收它。
    • 灰色(Gray):对象已经被访问,但它的子对象尚未被访问。
    • 黑色(Black):对象已经被访问,且它的所有子对象也都已经被访问。

    在标记阶段,Go 会使用这三种颜色来表示对象的遍历状态,灰色对象会被继续遍历,直到所有可达对象都被标记为黑色。

  • 分代回收(Generational GC):
    Go 的 GC 并没有明确采用分代回收的策略(与 JVM 等语言的 GC 不同),但是它通过其他优化来提高回收效率。比如,它会优先回收年轻对象(对象创建时间较短的对象),因为这些对象更可能是短生命周期的。

你的普罗米修斯设置了哪些指标?如何监控的?

暴露指标:首先,在你的 IM 服务中使用 Prometheus 的 Go 客户端库(github.com/prometheus/client_golang)来暴露一些自定义的监控指标。例如,监控当前在线用户数、消息发送量、API 响应时间等。可以在你的应用中定义计数器、直方图等类型的指标。

暴露 /metrics 端点:在应用中添加一个 HTTP 服务器,用于暴露 /metrics 端点,让 Prometheus 可以访问到这些指标数据。例如,使用 Go 语言的话,可以通过 promhttp.Handler() 来暴露该端点。

设置告警规则:根据你的需求设置告警规则。例如,消息量过大、API 响应时间过长时触发告警。这些告警可以通过 Alertmanager 转发到邮件、Slack 或其他通知渠道。

可视化数据:你还可以将 Prometheus 与 Grafana 配合使用,实现实时的监控数据可视化。通过 Grafana 的 Prometheus 数据源插件,你可以创建丰富的仪表板,展示你的 IM 服务的健康状况、消息流量等关键信息。”

Token的过期机制的是怎么实现的?

用户登录时,生成两个 Token

  • 访问 Token(JWT):有效期较短,通常为 15 分钟到 1 小时。
  • 刷新 Token:有效期较长,通常为 7 天或更长。

用户请求时

  • 客户端将访问 Token 放在请求头中发送给服务器。
  • 如果访问 Token 过期,服务器会根据客户端提供的刷新 Token 来生成新的访问 Token。

实现流程

  • 刷新 Token:在访问 Token 过期时,客户端可以通过发送刷新 Token 请求到服务器,服务器验证刷新 Token 的有效性后,重新颁发一个新的访问 Token 和刷新 Token。
  • 刷新 Token 的过期与失效:当刷新 Token 也过期时,用户必须重新登录。

kafka如何确保消费者消费正确

  • Kafka 的消费者是 分布式的,并且支持 异步消费。消费者可以 并行 消费多个分区的数据,每个消费者只负责自己的分区,因此能够在多个消费者之间均衡负载,从而有效应对高并发的消费需求。

  • 消费者组(Consumer Group)可以将多个消费者组合起来并行处理数据。每个消费者组中的消费者会分配到不同的分区,确保每个消息只能被一个消费者处理。这样多个消费者可以同时处理多个分区,提高了处理的并发能力。

  • Kafka 使用 分区 来管理数据,每个 Topic 可以有多个 Partition,每个 Partition 负责一部分数据的存储和处理。每个分区的数据是有序的,可以并行处理不同分区的数据,极大地提升了 Kafka 的吞吐量和并发处理能力。

  • 在生产者和消费者之间,Kafka 会根据消息的键(key)或者 轮询(Round-robin) 等方式将数据分布到不同的分区,从而支持并行写入和读取。

你的im项目kafka有哪些topic?

“在我们的 IM 项目中,Kafka 作为消息队列用于高效处理消息传递和状态更新。我们设置了多个 Topic 来实现不同功能的消息传输,包括:

  1. **user-message**:用于用户之间的消息传输,每当用户发送消息时,消息会被推送到这个 Topic。
  2. **message-status**:用于记录消息的状态变化,如消息的送达状态、已读状态等。当消息状态改变时,会将状态更新推送到此 Topic。
  3. **user-typing**:这个 Topic 用于实时监控用户的输入状态,例如当某个用户正在输入时,会将该状态更新到该 Topic,其他用户可以实时看到。
  4. **user-presence**:用于跟踪用户的在线和离线状态,确保每个用户的状态是最新的,所有相关服务都能及时获取到这些信息。
  5. **chat-room**:用于处理群聊中的消息。当群聊中的任何用户发送消息时,消息会被广播到这个 Topic,所有成员都会收到。
  6. **notification**:用于系统通知,处理如系统消息、公告、警告等类型的通知。
  7. **media-message**:用于处理多媒体消息,如图片、视频、语音等。多媒体消息通过这个 Topic 传输。

如果说一个Replicas宕机了怎么办?

  • Kafka 的分区可以有多个副本(Replicas),即使某个节点宕机,其他副本可以继续提供服务,确保系统的高可用性。