2024 年了,为什么我还是舍弃不掉 RSS?
Slice 扩容
slice 切片扩容机制?为什么不一直用 2 倍扩容?
go1.18 版本之后,slice 扩容使用了更加平滑的方式,不再使用 1024 作为临界点,而是使用 threshold 作为临界点(threshold 设定为 256)。当 slice 容量 < threshold 时,每次扩容为原来的两倍。
当 slice 容量 > threshold 时,每次增加 (oldcap + 3threshold)3/4 的容量。
如果每次扩容都用两倍扩容,那这是一个 2 的指数级增加,后面扩容会扩非常大,浪费很大的空间。
内存分配
Go 内存分配机制?多级缓存?组件?(mspan/mcache/mcentral...)
Go 内存会自己管理,释放的内存不会立马归还给操作系统,而是尽量对内存进行复用,减少和操作系统的沟通(涉及到内核态和用户态的转换,消耗较大)。
Go 内存管理非常高效,每一个线程 M 独享一个 mcache,在申请内存时优先从 mcache 上获取,如果 mcache 没有足够内存,则向 mcentral 获取内存到 mcache 中,如果 mcentral 中也没有足够的内存则向 mheap 申请,如果 mheap 也没有足够的内存的话就向操作系统申请了(mcache->mcentral->mheap->OS)。
高效体现在从 mcache 获取内存和释放都是无锁的,速度很快,向 mcentral 和 mheap 申请内存虽然需要竞争锁,但是 mcentral 和 mheap 通过 span class 进行分类(设置桶锁而不是一把大锁),锁的粒度更小,申请内存时都是通过 span class 找到对应的桶获取内存,锁竞争会减少很多,性能也得到了提升。
Go 垃圾回收 GC 原理
go 垃圾回收?三色标记法?读写屏障?STW?
go1.18 之后垃圾回收采用三色标记法和混合写屏障。
三色标记法:
将对象标记为白色,灰色或黑色。
白色:不确定对象(默认色);黑色:存活对象。灰色:存活对象,子对象待处理。
标记开始时,先将所有对象加入白色集合(需要 STW)。首先将根对象标记为灰色,然后将一个对象从灰色集合取出,遍历其子对象,放入灰色集合。同时将取出的对象放入黑色集合,直到灰色集合为空。最后的白色集合对象就是需要清理的对象。
这种方法有一个缺陷,如果对象的引用被用户修改了,那么之前的标记就无效了。因此 Go 采用了写屏障技术,当对象新增或者更新会将其着色为灰色。一次完整的 GC 分为四个阶段:
准备标记(需要 STW),开启写屏障。
开始标记
标记结束(STW),关闭写屏障
清理(并发)
基于插入写屏障和删除写屏障在结束时需要 STW 来重新扫描栈,带来性能瓶颈。混合写屏障分为以下四步:
GC 开始时,将栈上的全部对象标记为黑色(不需要二次扫描,无需 STW);
GC 期间,任何栈上创建的新对象均为黑色
被删除引用的对象标记为灰色
被添加引用的对象标记为灰色
总而言之就是确保黑色对象不能引用白色对象,这个改进直接使得 GC 时间从 2s 降低到 2us。
CSP 并发模型
channel 使用场景?CSP 思想?优势?
在并发编程中,两个 goroutine 之间使用 channel 进行通信。
CSP 思想是指通过通信来共享内存,而不是通过共享内存来实现通信。
CSP 思想优势是使用非常灵活,缺点是容易造成死锁。
互斥锁和读写锁
go 的锁有哪些?互斥锁的读写锁的区别?
互斥锁和读写锁。
互斥锁是有一个 goroutine 获取到互斥锁后,其他 goroutine 必须要等待锁被释放才能获取到锁。
而读写锁是可以允许多个 goroutine 获取到读锁执行读操作,但是写锁被获取了就和互斥锁一样了,其他 goroutine 必须等待写锁释放才能获取到读锁或者写锁。
sync
sync 包还有什么?sync.map 介绍一下?如何保证并发安全?
sync.waitgroup, sync.map ,sync.Lock, sync.RWLock,sync.Pool....
sync.map 底层有两个 map,一个是 read,一个是 dirty, 在读操作时优先在 read 中查找,read 没有就去 dirty 中查找,写操作时如果 read 中有则利用 CAS 机制尝试更新 value 值,read 中没有则写入 dirty 中。在 misses >= len(dirty)时,同步 read 和 dirty 的数据。
在 read 中不需要加锁,在 dirty 中需要加锁
sync.map 通过 read 和 dirty 实现读写分离来减少锁时间来提高并发效率
协程池
sync.Pool?了解线程池吗?优势亮点?未工作的线程如何等待?协程池?
sync.Pool 其实就是一个线程安全的对象池,用于保存和复用临时对象,在大批量申请和释放相同类型的临时对象时使用 sync.Pool 可以减少很多内存分配和回收操作,减小 GC 压力。
线程池是一种并发编程的技术,可以管理和复用线程,提供一种高效的方式来处理并发。
线程池优势亮点:
提高性能,线程池通过复用线程,减少线程频繁地创建和销毁,避免线程的频繁创建和销毁的开销。
控制并发度,线程池可以控制并发任务的数量,通过设计线程池的大小来控制并发度,避免激烈的锁竞争导致系统性能的下降。
提高响应速度, 线程池可以提前创建好线程,在任务到来时可以立即处理任务,提高系统的响应速度。
资源管理,线程池可以更好的对线程进行调度和管理,避免线程资源的浪费。
未工作的线程可以通过以下方式等待:
使用条件变量:线程可以使用条件变量来等待某个条件的发生。当线程需要等待时,它可以调用条件变量的等待函数,将自己置于等待状态,直到条件满足时被唤醒。
使用信号量:线程可以使用信号量来进行等待操作。线程在需要等待时,可以调用信号量的等待操作,将自己阻塞,直到其他线程释放信号量时被唤醒。
使用锁和条件变量组合:线程可以使用锁和条件变量的组合来实现等待操作。线程在需要等待时,可以先获取锁,然后检查条件是否满足,如果条件不满足,则调用条件变量的等待函数将自己置于等待状态,直到条件满足时被唤醒。
协程池和线程池基本差不多,可以更好地管理和复用协程,提高系统的性能和资源利用率。
协程泄露
防止 Go 协程泄露/未关闭?(waitgroup,context)
通过管道 channel 通知关闭,使用 waitgroup 监控协程全部退出,使用 contex 上下文来设置超时或者手动 cancle 关闭协程
select 的用法?执行顺序?
执行顺序是随机的
map
什么可以作为 map 的键?结构体可以吗?
实现了 == 操作的可以比较的就可以作为 map 的键(基本数据类型、数组等),结构体中如果所有字段都可以比较那么久可以作为键,否则不行。
微服务
微服务之间通信方式 rpc、grpc、http 等
介绍一下 grpc?protobuf/json 区别与优势?
GRPC 是一种高性能、开源的远程过程调用(RPC)框架,由 Google 开发并基于 HTTP/2 协议实现。它允许在不同的计算机之间进行跨语言和跨平台的通信,使得构建分布式系统变得更加简单和高效。
GRPC 使用 Protocol Buffers(简称 Protobuf)作为默认的序列化机制,而不是使用 JSON。
Protobuf 是一种轻量级的数据交换格式,具有以下优势:
效率高:Protobuf 使用二进制编码,相比于文本格式的 JSON,它的编码和解码速度更快,传输的数据量更小,节省了带宽和存储空间。
可读性好:虽然 Protobuf 是二进制格式,但它的定义文件是可读的,易于理解和维护。相比之下,JSON 是一种文本格式,可读性较好,但在大型数据结构的情况下,Protobuf 的定义文件更加清晰和简洁。
跨语言支持:Protobuf 支持多种编程语言,包括 Java、C++、Python 等,这使得在不同语言之间进行通信变得更加方便。
版本兼容性:Protobuf 支持向后和向前兼容的数据格式演化,这意味着可以在不破坏现有客户端和服务端的情况下,对数据结构进行扩展和修改。
相比之下,JSON 是一种常用的文本格式,具有以下特点:
可读性好:JSON 使用文本格式,易于阅读和理解,对于调试和开发过程中的数据交换非常方便。
平台无关性:JSON 是一种独立于编程语言的数据格式,几乎所有的编程语言都支持 JSON 的解析和生成。
灵活性:JSON 支持动态的数据结构,可以轻松地添加、删除和修改字段,适用于一些需要频繁变动的数据。
总的来说,GRPC 使用 Protobuf 作为默认的序列化机制,相比于 JSON,Protobuf 在性能、可读性和跨语言支持方面具有优势。然而,选择使用 GRPC 还是 JSON 取决于具体的应用场景和需求。
介绍一下 Gorm 优势?
简单易用、支持多种数据库、自动迁移、支持事务、
Redis
1.常见数据结构
答:(字符串、set、zset、hash、list...)hash 和 zset 底层使用 skiplist 和 ziplist,同时满足两个条件时使用 ziplist,否则 skiplist
集合元素个数小于 redis.conf 中 zset-max-ziplist-entries 属性的值,其默认值为 128
每个集合元素大小都小于 redis.conf 中 zset-max-ziplist-value 属性的值,其默认值为 64 字节
list 底层使用了快表(quicklist),quickList 本质上是 zipList 和 linkedList 的混合体。其将 linkedList 按段切分,每一段使用 zipList 来紧凑存储若干真正的数据元素,多个 zipList 之间使用双向指针串接起来。对于每个 zipList 中最多可存放多大容量的数据元素,在配置文件中通过 list-max-ziplist-size 属性可以指定。
2.zset 介绍一下?zset 底层借助那些数据结构实现?跳表介绍一下?查找过程?复杂度?
zset 是 redis 中的有序集合,借助了跳表和哈希表(哈希表存储成员和对应的分数)实现有序集合,跳表是一种有序的链表结构,它通过在每个节点中维护多个指针,使得在查找和插入操作时可以跳过部分节点,从而提高了查找的效率。跳表的高度是通过随机函数决定的,因此在平均情况下,查找的时间复杂度为 O(log n),其中 n 是跳表中的节点数量。
3.set 介绍?
set 底层使用哈希表。
4.压缩列表介绍?
压缩列表的设计目标是在尽可能节省内存的同时,提供高效的插入、删除和访问操作。它通过使用连续的内存块来存储数据,减少了指针和额外的元数据开销,从而节省了内存空间。
5.渐进式 rehash?
渐进式 rehash 是 Redis 在进行哈希表扩容时采用的一种策略。当哈希表需要扩容时,Redis 会创建一个新的哈希表,并将原有哈希表中的数据逐步迁移到新的哈希表中。这个过程是逐步进行的,每次只迁移一小部分数据,以避免在一次性迁移过程中对系统性能造成较大的影响。
渐进式 rehash 的过程如下:
Redis 会创建一个新的空哈希表,大小是原有哈希表的两倍。
Redis 会将原有哈希表中的一个桶(bucket)中的键值对逐个迁移到新的哈希表中。这个迁移过程是逐个键值对进行的,而不是一次性迁移整个桶。
在每次迁移过程中,Redis 会将新哈希表中对应的桶指向原有哈希表中的桶,以保持对原有哈希表的访问能力。
迁移完成后,Redis 会将新哈希表设置为当前使用的哈希表,并释放原有哈希表的内存空间。
通过渐进式 rehash,Redis 可以在不中断服务的情况下进行哈希表的扩容。这种方式可以避免在一次性迁移过程中对系统性能造成较大的影响,因为每次只迁移一小部分数据。同时,渐进式 rehash 还可以保持对原有哈希表的访问能力,确保在迁移过程中数据的一致性。
技术前沿拓展
前端开发,你的认知不能仅局限于技术内,需要发散思维了解技术圈的前沿知识。细心的人会发现,开发内部工具的过程中,大量的页面、场景、组件等在不断重复,这种重复造轮子的工作,浪费工程师的大量时间。
介绍一款程序员都应该知道的软件JNPF快速开发平台,很多人都尝试用过它,它是功能的集大成者,任何信息化系统都可以基于它开发出来。
这是一个基于 Java Boot/.Net Core 构建的简单、跨平台快速开发框架。前后端封装了上千个常用类,方便扩展;集成了代码生成器,支持前后端业务代码生成,实现快速开发,提升工作效率;框架集成了表单、报表、图表、大屏等各种常用的 Demo 方便直接使用;后端框架支持 Vue2、Vue3。如果你有闲暇时间,可以做个知识拓展。
看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~
评论