一、工具使用
1. pprof 工具。当 CPU 或者内存占用率过高时,使用 pprof 工具能够精准定位到消耗资源的热点代码。
2. benchmark 工具。要对不同方案进行性能对比时,使用 benchmark 工具,可以获取不同方案在耗时 和 内存消耗 方面的对比情况。
二、单机吞吐优化
1. 当用 map 构造集合时,可以将 value 类型设置为空结构体类型(struct{}),不占用内存空间,降低内存消耗。
2. 当创建 map 和切片对象时,如果可以提前确定容器容量,那就把容量传入 make 函数中,从而避免往集合中添加数据时触发扩容迁移,降低内存和 CPU 资源消耗。
3. 有大量字符串拼接操作时,可以使用 strings.Builder 类型,并利用它的内存预分配功能做字符串拼接。
4. 有大量整型转字符串操作时,可以用 strconv 库做转换,避免使用 fmt.Sprint 函数的反射和格式化。
5. 有大量字符串转字节切片操作时,可以用 unsafe 包,通过字符串和字节切片底层数组空间共用,实现高性能转换
6. 需要频繁创建相同类型的临时对象时,可以使用 sync.Pool 对象池,实现临时对象复用,从而减少 Go 中的内存分配和 GC 开销。
7. 需要频繁地创建协程,可以使用协程池。可以降低协程创建的开销。同时,协程池能限制同时运行的协程的最大数目,从而避免同时有太多协程,导致频繁进行协程调度。
三、并发等待:如何降低实时系统的响应延时?
1. WaitGroup 类型。一个规模较大的任务,为了提高执行效率,可以将其拆分成多个子任务,然后让这些子任务并发运行。而此时,如果还需要等待所有子任务都顺利执行完毕,那么 WaitGroup 类型能够精准地满足需求。
2. errgroup 包。errgroup 包可以看作是对 WaitGroup 类型的升级与封装。在实际开发中,需要周全地对可能出现的错误进行处理、灵活地取消任务以及精准地控制最大协程数等需求, errgroup 包就是最佳选择。
四、并发安全:如何为不同并发场景选择合适的锁?
1. 对数据的写操作较多或者读操作不频繁,可以使用互斥锁Mutex。
2. 读操作远多于写操作,可以使用读写锁,允许多个协程同时进行读操作,提高并发读取的性能。
3. 当大量数据存储在 map 中,并且协程对 map 的访问相对均匀地分布在不同的键上时,可以考虑使用分段锁。具体是将 map 分成多段,每段有自己的锁,降低锁粒度,从而提升并发性能。
4. 需要对共享对象进行原子操作,可以利用 atomic 包无锁编程,避免加锁操作,从而提升性能。
五、并发map:百万数据本地缓存,如何降延时减毛刺?
1. sync.Map 借助两个 map 来达成读写分离的设计,提升读取操作的性能。
2. 然而,这种设计虽然效率高,但存在着不容忽视的问题:
(1) 需要两个map,内存占用相对较高。
(2) 数据修改频繁时,读表命中率低。不命中的时候,既要读一次读表,又要读一次写表,降低性能。
(3) 读命中率低时,还会产生两个 map 之间的数据拷贝开销。数据量大时,会导致较大的性能开销。
3. 当 map 中缓存的数据比较多时,为了避免 GC 开销,可以将 map 中的 key-value 类型设计成非指针类型且大小不超过 128 字节,从而避免 GC 扫描。
六、网络编程:如何进行网络IO编程降消耗,提吞吐?
1. 网络 IO 模型有阻塞 IO、非阻塞 IO、IO 多路复用和异步 IO 多种类型,实践中比较常用的是 IO 多路复用模型。
2. 阻塞 IO: socket 缓冲区没有准备好,让线程阻塞在 IO 调用不返回。
每个连接我们都需要创建一个专门的线程来处理。
3. 非阻塞 IO:socket 缓冲区还没准备好,网络 IO 系统调用立即返回,不阻塞线程,线程可以处理另一个连接的请求。
需要利用轮询不断做系统调用,浪费大量 CPU 资源。
4. IO 多路复用:一个线程阻塞监听多个连接的网络 IO 事件,当有连接的 socket 缓冲区准备好,IO 调用就会返回。
既不用创建大量线程,也避免不断轮询。
5. 异步 IO:线程进行IO 调用会立即返回,由内核负责将 socket 缓冲区数据复制到用户空间,然后通知线程完成,整个过程完全没阻塞。
各个平台对异步 I/O 的支持程度不一,这种方式使用起来复杂度较高,因此并不是很广泛。
6. epoll 技术解析
(1) epoll_create 函数,在 Linux 内核创建一个内核需要监听的网络连接池子。
(2) epoll_ctl 函数,增删改池子里需要监听的连接和事件。
(3) epoll_wait 函数,阻塞等待池子里连接的网络 IO 事件。
7. 线程调用 epoll_wait 方法时,接收来自操作系统的关于 网络 IO 事件就绪的通知 的触发模式
(1) 水平触发:
a. 只要socket 缓冲区有未处理数据,持续触发事件
b. 应用层未处理完数据时,内核保持fd在就绪队列
c. 下次epoll_wait()会立即返回该fd
d. 不需要保证缓冲区足够大。
(2) 边缘触发:
a. 仅在fd状态变化时触发一次通知
b. 内核不会保留fd在就绪队列(除非新事件到达)
c. 应用层必须一次性处理完所有数据。
d. 需要保证缓冲区足够大。
七、网络通信:不改业务代码,如何降低延时?
1. 尽量不跨地域、不跨机房调用。
2. 响应速度要求高的上下游服务,部署到一台物理机里。
3. 将RPC 调用,合并编译成本地 SDK 的函数调用。
八、数据库:分库分表,没有用户id怎么分?
1. 读写分离。当数据库读 QPS 过高时,可以通过读写分离架构,增加从库来提升读 QPS。
2. 分表。当单表数据行数太多,导致读性能下降时,可以用分表架构,将一张表拆成多张小表,从而提升读性能。
3. 分库。当数据库写入 TPS 过高时,可以用分库架构,通过增加多个主库,分散单库的写压力,从而提高写 TPS 上限。
九、分布式缓存:大促抢购,不知热点咋防热Key?
1. 本地缓存全量数据。要是数据量不大,可以直接在服务本地内存把所有数据都缓存起来,能大幅度降低热 Key 问题导致的 Redis 访问压力。
2. 本地只缓存热 Key 数据。当服务不能缓存全部数据时,可以接入热 Key 探测框架,只把那些被频繁访问的热 Key 数据存到本地。
3. Redis 读写分离架构。要是不想让业务层变得复杂,可以采取读写分离架构,给每个 Redis Server 都加上从库,让从库去应对热 Key 的高频率读取,分担压力。
4. Redis 提供的Proxy 热 Key 承载方案。利用 Proxy 来缓存热 Key 数据,承担热 Key 访问所带来的压力。比如 阿里云 Redis 的 Proxy Query Cache