前言

  1. redis有哪些使用场景?
  2. redis五种数据结构选择以及其底层实现原理?string、hashmap、list、set、zset
  3. redis 常用命令以及时间复杂度?
  4. 如何处理可能遇到的key大面积失效的缓存雪崩,无效key缓存穿透、热key缓存击穿问题?是否需要缓存预热
  5. 如何考虑缓存和数据库一致性的问题?更新DB之后删除缓存?还是更新缓存?
  6. redis 数据持久化是怎么做的?RDB和AOF机制?
  7. redis 分布式架构,codis,rdis cluster?
  8. redis 超过使用容量时的内存淘汰策略
  9. redis 过期键的删除策略
  10. redis 的单线程架构为什么快,有哪些优势和缺点?
  11. redis & Lua ?
  12. redis 性能调优?避免大key、热key导致集群倾斜,比秒复杂命令的使用,CPU、内存、宽带的监控
  13. 实践:秒杀系统的设计和实现
  14. 实践:分布式锁,setnx,expire,del
  15. 实践:bloomfilter 和 bitmap
  16. 实线:使用redis实现微信步数排行榜

redis 使用场景

  1. 缓存数据(db,service) 的数据,提高访问效率

    • 缓存容量评估
    • 缓存过期机制,时间
    • 缓存miss,溯源和监控
    • 缓存雪崩,大面积key失效DB保护。
    • 缓存击穿:热key击穿保护
    • 缓存穿透:无效key击穿DB保护
    • 缓存更新和一致性问题
    • 缓存热key和大key问题
  2. 限流和计数。lua脚本

  3. 延时队列

    • 使用 ZSET+ 定时轮询的方式实现延时队列机制,任务集合记为 taskGroupKey
    • 生成任务以 当前时间戳 与 延时时间 相加后得到任务真正的触发时间,记为 time1,任务的 uuid 即为 taskid,当前时间戳记为 curTime
    • 使用 ZADD taskGroupKey time1 taskid 将任务写入 ZSET
    • 主逻辑不断以轮询方式 ZRANGE taskGroupKey curTime MAXTIME withscores 获取 [curTime,MAXTIME) 之间的任务,记为已经到期的延时任务(集)
    • 处理延时任务,处理完成后删除即可
    • 保存当前时间戳 curTime,作为下一次轮询时的 ZRANGE 指令的范围起点
    • https://github.com/bitleak/lmstfy
  4. 消息队列

    • redis 支持 List 数据结构,有时也会充当消息队列。使用生产者:LPUSH;消费者:RBPOP 或 RPOP 模拟队列
  5. 分布式锁:https://juejin.cn/post/6936956908007850014

  6. bloomfilter: https://juejin.cn/post/6844903862072000526

    $m = -\frac{nln(p)}{(ln2)^2}$

    $k=\frac{m}{n}ln(2)$

    1
    2
    3
    4
    n 是预期插入的元素数量(数据规模),例如 20,000,000。
    p 是预期的误判率,例如 0.001。
    m 是位数组的大小。
    k 是哈希函数的数量。

redis 5种数据类型和底层数据结构

计算所需的缓存的容量,当容量超过限制时的淘汰策略

1
2
3
4
5
6
7
8
- noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
- allkeys-lru:从所有key中使用LRU算法进行淘汰
- volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
- allkeys-random:从所有key中随机淘汰数据
- volatile-random:从设置了过期时间的key中随机淘汰
- volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used

redis 内存淘汰策略解析

redis 过期键的删除策略

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
    (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

redis 两种数据持久化的原理以及优缺点

选择local、remote、multilevel cache

双buffer vs LRU/LFU

本地缓存的双缓冲机制和本地LRU(Least Recently Used)算法都是常见的缓存优化技术,它们具有不同的优点和缺点。

  1. 双缓冲机制:

    • 优点:
      • 提高并发性能:双缓冲机制使用两个缓冲区,一个用于读取数据,另一个用于写入数据。这样可以避免读写冲突,提高了并发性能。
      • 提高数据访问效率:由于读取操作不会直接访问主缓存,而是读取缓冲区的数据,因此可以更快地获取数据。
    • 缺点:
      • 内存开销增加:双缓冲机制需要维护两个缓冲区,这会增加内存开销。
      • 数据延迟:数据更新定时同步,有一定延时。
  2. 本地LRU算法:

    • 优点:
      • 数据访问效率高:LRU算法根据数据的访问顺序进行缓存替换,将最近最少使用的数据淘汰出缓存。这样可以保留最常用的数据,提高数据的访问效率。
      • 简单有效:LRU算法的实现相对简单,只需要维护一个访问顺序链表和一个哈希表即可。
    • 缺点:
      • 缓存命中率下降:如果数据的访问模式不符合LRU算法的假设,即最近访问的数据在未来也是最有可能被访问的,那么LRU算法的效果可能不理想,缓存命中率会下降。
      • 对于热点数据不敏感:LRU算法只考虑了最近的访问情况,对于热点数据(频繁访问的数据)可能无法有效地保留在缓存中。

综合来看,双缓冲机制适用于需要提高并发性能、批量更新等场景,但会增加内存开销。本地LRU算法适用于需要提高数据访问效率的场景,但对于访问模式不符合LRU假设的情况下,缓存命中率可能下降。在实际应用中,可以根据具体需求和场景选择适合的缓存优化技术。

怎么考虑缓存和db数据一致性的问题

  • 当使用redis缓存db数据时,db数据会发生update,如何考虑redis和db数据的一致性问题呢?
  • 通常来说,对于流量较小的业务来说,可以设置较小的expire time,可以将redis和db的不一致的时间控制在一定的范围内部
  • 对于缓存和db一致性要求较高的场合,通常采用的是先更新db,再删除或者更新redis,考虑到并发性和两个操作的原子性(删除或者更新可能会失败),可以增加重试机制(双删除),如果考虑主从延时,可以引入mq做延时双删
  • http://kaito-kidd.com/2021/09/08/how-to-keep-cache-and-consistency-of-db/

缓存更新方式 优缺点
缓存模式+TTL 业务代码只更新DB,不更新cache,设置较短的TTL(通常分钟级),依靠cache过期无法找到key时回源DB,热key过期可能回导致请求大量请求击穿到DB,需要使用分布式锁或者singleflight等方式避免这种问题
定时刷新模式 定时任务异步获取DB数据刷新到cache,读请求可不回源,需要考虑刷新时间和批量读写
写DB,写cache 在并发条件下,DB写操作顺序和cache操作不同保证顺序一致性,需要增加分布式锁等操作
写DB,删除cache 删除cache可能失败,需要增加重试,重试也可能失败,比较复杂的加个MQ补偿重试

思考:

  • 对一致性要求有多强?
  • TTL 设置的时长
  • 并发冲突可能性
  • 热key缓存击穿保护

redis 怎么扩容扩容和收缩

redis 为什么使用单线程模型

缓存异常与对应的解决办法

  • 缓存雪崩问题,大面积键失效或删除
  • 缓存穿透问题,不存在key的攻击行为
  • 热点数据缓存击穿,热门key失效
  • 是否需要缓存预热
  • 缓存穿透,缓存击穿,缓存雪崩解决方案分析,https://juejin.im/post/6844903651182542856

redis 为什么这么快

  • 1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);
  • 2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
  • 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 4、使用多路 I/O 复用模型,非阻塞 IO;
  • 5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

Redis实现分布式锁

  • Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。当且仅当 key 不存在,将 key 的值设为 value。 若给定的 key 已经存在,则 SETNX 不做任何动作SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。返回值:设置成功,返回 1 。设置失败,返回 0

redis分布式方案

  1. 单机版,并发访问有限,存储有限,单点故障。
  2. 数据持久化
  3. 主从复制。主库(写)同步到从库(读)的延时会造成数据的不一致;主从模式不具备自动容错,需要大量的人工操作
  4. 哨兵模式sentinel。在主从的基础上,实现哨兵模式就是为了监控主从的运行状况,对主从的健壮进行监控,就好像哨兵一样,只要有异常就发出警告,对异常状况进行处理。当master出现故障时,哨兵通过raft选举,leader哨兵选择优先级最高的slave作为新的master,其它slaver从新的master同步数据。哨兵解决和主从不能自动故障恢复的问题,但是同时也存在难以扩容以及单机存储、读写能力受限的问题,并且集群之前都是一台redis都是全量的数据,这样所有的redis都冗余一份,就会大大消耗内存空间
  5. codis: https://github.com/CodisLabs/codis
  6. redis cluster集群模式:集群模式时一个无中心的架构模式,将数据进行分片,分不到对应的槽中,每个节点存储不同的数据内容,通过路由能够找到对应的节点负责存储的槽,能够实现高效率的查询。并且集群模式增加了横向和纵向的扩展能力,实现节点加入和收缩,集群模式时哨兵的升级版,哨兵的优点集群都有
  7. redis 分布式架构演进
  8. Redis集群化方案对比:Codis、Twemproxy、Redis Cluster

redis & Lua

Redis 执行 Lua 脚本会以原子性方式进行,在执行脚本时不会再执行其他脚本或命令。并且,Redis 只要开始执行 Lua 脚本,就会一直执行完该脚本再进行其他操作,所以 Lua 脚本中 不能进行耗时操作 。此外,基于 Redis + Lua 的应用场景非常多,如分布式锁,限流,秒杀等等。
基于项目经验来看,使用 Redis + Lua 方案有如下注意事项:

  • 使用 Lua 脚本实现原子性操作的 CAS,避免不同客户端先读 Redis 数据,经过计算后再写数据造成的并发问题
  • 前后多次请求的结果有依赖关系时,最好使用 Lua 脚本将多个请求整合为一个;但请求前后无依赖时,使用 pipeline 方式,比 Lua 脚本方便
  • 为了保证安全性,在 Lua 脚本中不要定义自己的全局变量,以免污染 Redis 内嵌的 Lua 环境。因为 Lua 脚本中你会使用一些预制的全局变量,比如说 redis.call()
  • 注意 Lua 脚本的时间复杂度,Redis 的单线程同样会阻塞在 Lua 脚本的执行中,Lua 脚本不要进行高耗时操作
  • Redis 要求单个 Lua 脚本操作的 key 必须在同一个 Redis 节点上,因此 Redis Cluster 方式需要设置 HashTag(实际中不太建议这样操作)

redis 常用命令

  • redis-cli -h host -p port -a password
  • set key value [NX|XX] [EX seconds|PX milliseconds|EXAT unix]
  • get key
  • keys pattern,*表示通配符,表示任意字符,会遍历所有键显示所有的键列表,时间复杂度O(n),在生产环境不建议使用
  • exists key [key …]
  • 秒语法查询key的过期时间:ttl key

sdk

  • github.com/go-redis/redis

推荐阅读:

  1. https://blog.csdn.net/ThinkWon/article/details/103522351
  2. https://tech.meituan.com/2017/03/17/cache-about.html
  3. 一不小心肝出了4W字的Redis面试教程
  4. 你的 Redis 为什么变慢了?
  5. redis dbindex. https://blog.csdn.net/lsm135/article/details/52945197
  6. 颠覆认知——Redis会遇到的15个「坑」,你踩过几个?
  7. Redis最佳实践:7个维度+43条使用规范
  8. Redis为什么变慢了?
  9. redis 常用命令以及时间复杂度
  10. 单线程redis为什么快

多查看文档
MySQL 5.7 Reference Manual

数据建模

https://vertabelo.com/blog/types-data-models/
https://blog.csdn.net/zhulangfly/article/details/130432124
https://aws.amazon.com/cn/what-is/data-modeling
https://www.qlik.com/us/data-modeling

基本使用

如何建表?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `hotel_info_tab` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`hotel_id` bigint(20) NOT NULL DEFAULT '0',
`hotel_name` varchar(64) NOT NULL DEFAULT '',
`area_code` varchar(64) NOT NULL DEFAULT '',
`phone_no` varchar(24) NOT NULL DEFAULT '',
`address` text,
`star_rating` varchar(16) NOT NULL DEFAULT '',
`popularity_score` int(11) NOT NULL DEFAULT '0',
`longitude` varchar(64) NOT NULL DEFAULT '',
`latitude` varchar(64) NOT NULL DEFAULT '',
`policies` text,
`ext_info` text,
`update_time` bigint(20) NOT NULL DEFAULT '0',
`create_time` bigint(20) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_hotel_id` (`hotel_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED

类型选择?

  • 数值类型:int,tinyint,int(10),bigint
  • 定点数(exact-value),decimal,使用字符串存储,精度
  • 浮点数(approximate-value (floating-point)):float,double,精度缺失
  • 字符串: varchar(256),char(10)(定长,根据需要使用空格填充)
  • 文本: text,json
    1
    2
    JSON 数据类型提供了数据格式验证和以及一些内置函数帮助查询和检索。
    JSON数据类型更适合存储和处理结构化的JSON数据,而TEXT数据类型更适合存储纯文本字符串。如果你需要在数据库中存储和操作JSON数据,并且使用MySQL 5.7及更高版本,那么JSON数据类型是更好的选择。如果你只需要存储普通的文本字符串,而不需要对JSON数据进行特殊处理,那么TEXT数据类型就足够了
  • 时间time:建表时通常会带上create_time,update_time,datetime,timestamp类型,有时也会用int32和int64的时间戳类型
    1
    2
    `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    通常存储的都是时间戳,需要考虑使用mysql服务器的时间还是业务的时间戳,考虑使用mysql时间戳是否会有不利的影响

primary key

  • 主键PRIMARY KEY。数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。主键是数据库确保数据行在整张表唯一 性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键。设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。
  • 自增主键还是UUID?优缺点?怎么生成UUID?,比如item表使用自增ID,order表使用订单id,订单id可以认为是uuid。

unique key

  • 唯一性约束UNIQUE KEY:唯一性约束是很重要的特性,防止重复插入数据

关于FOREIGN KEY约束,不建议使用

int(10),bigint(20)

  • 整数类型的括号中的数字仅用于指定显示宽度,并不会影响存储范围或存储空
  • 显示宽度:括号中的数字用于指定在查询结果中显示整数类型字段时的字符个数。它可以控制字段在查询结果中的对齐和显示格式。例如,如果将一个整数字段定义为 int(3),并插入值 100,在查询时该字段将以 ‘100’ 的形式显示,左侧用空格填充以达到指定的宽度
  • 零填充:括号中的数字还可以与 ZEROFILL 属性一起使用,以实现零填充的效果。当整数类型字段定义为 int(3) ZEROFILL 时,如果插入的值不足指定的宽度,MySQL 将在左侧用零进行填充

编码方式

  • 编码方式utf8mb4:通过 show variables like ‘character_set_%’; 可以查看系统默认字符集。mysql中有utf8和utf8mb4两种编码,在mysql中请大家忘记utf8,永远使用utf8mb4。这是mysql的一个遗留问题,mysql中的utf8最多只能支持3bytes长度的字符编码,对于一些需要占据4bytes的文字,mysql的utf8就不支持了,要使用utf8mb4才行
  • COLLATE=utf8mb4_unicode_ci,所谓utf8_unicode_ci,其实是用来排序的规则。对于mysql中那些字符类型的列,如VARCHAR,CHAR,TEXT类型的列,都需要有一个COLLATE类型来告知mysql如何对该列进行排序和比较。简而言之,COLLATE会影响到ORDER BY语句的顺序,会影响到WHERE条件中大于小于号筛选出来的结果,会影响DISTINCTGROUP BYHAVING语句的查询结果。另外,mysql建索引的时候,如果索引列是字符类型,也会影响索引创建,只不过这种影响我们感知不到。总之,凡是涉及到字符类型比较或排序的地方,都会和COLLATE有关。
  • 行格式,row_format,(https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html)
  • 10.9.1 The utf8mb4 Character Set (4-Byte UTF-8 Unicode Encoding)

关于 null 的使用

  • 除text类型外其它类型一般不使用null,都应该指定默认值
    在MySQL和许多其他数据库系统中,NULL是一个特殊的值,表示缺少值或未知值。虽然NULL在某些情况下是有用的,但由于它的特殊性,使用NULL可能会带来一些问题,因此在某些情况下不建议过度使用NULL。一般只有text类型回用到,其它都应该制定默认值
  1. 逻辑判断和比较的复杂性:由于NULL表示未知或缺少值,它的比较结果不是true也不是false,而是NULL。这意味着使用NULL进行逻辑判断和比较时需要额外的注意,可能需要使用IS NULL或IS NOT NULL等特殊的操作符。
  2. 聚合函数的结果处理:在使用聚合函数(如SUM、AVG、COUNT等)进行计算时,NULL的处理可能会产生意外的结果。通常情况下,聚合函数会忽略NULL值,因此如果某列中有NULL值,可能会导致计算结果不准确。
  3. 索引的使用限制:某些类型的索引在处理NULL值时可能会受到限制。例如,对于普通索引(B-tree索引)来说,NULL值并不会被索引,因此在查询时可能无法充分利用索引的性能优势。
  4. 查询语句的复杂性增加:当使用NULL值进行查询时,可能需要编写更复杂的查询语句来处理NULL的情况,这会增加查询的复杂性和维护成本。

虽然NULL有其合理的用途,例如表示缺失的数据或未知的值,但过度使用NULL可能会导致代码的复杂性增加、查询的不准确性和性能问题。在设计数据库模式和数据模型时,需要根据实际需求和业务逻辑合理使用NULL,并考虑到其带来的潜在问题。

存储引擎(Storage Engine) 选择

Setting the Storage Engine
MySQL支持多种存储引擎,每种存储引擎都有其特点和适用场景。以下是几种常见的MySQL存储引擎对比:

  • InnoDB:

    • 事务支持:InnoDB是MySQL默认的事务性存储引擎,支持ACID事务特性,适用于需要强一致性和事务支持的应用。
    • 行级锁定:InnoDB支持行级锁定,提供更好的并发性能。
    • 外键约束:InnoDB支持外键约束,可以保持数据完整性。
    • Crash Recovery:InnoDB具有崩溃恢复机制,能够在故障恢复时保证数据的一致性。
    • 适用场景:适用于高并发、需要事务支持和数据完整性的应用,如电子商务、在线交易等。
  • MyISAM:

    • 速度和性能:MyISAM对于读取操作有很好的性能表现,适用于读取频繁的应用。
    • 表级锁定:MyISAM使用表级锁定,对并发性能有一定影响。
    • 不支持事务:MyISAM不支持事务和崩溃恢复机制,不保证数据的完整性和一致性。
    • 全文索引:MyISAM支持全文索引,适用于对文本内容进行高效搜索的应用。
    • 适用场景:适用于读取频繁、对事务和数据完整性要求不高的应用,如博客、新闻等。
  • mysql存储引擎是插件式的,支持多种存储引擎,比较常用的是innodb和myisam

  • 存储结构上的不同:innodb数据和索引时集中存储的,myism数据和索引是分开存储的

  • 数据插入顺序不同:innodb插入记录时是按照主键大小有序插入,myism插入数据时是按照插入顺序保存的

  • 事务的支持:Innodb提供了对数据库ACID事务的支持,并且还提供了行级锁和外键的约束。MyIASM引擎不提供事务的支持,支持表级锁,不支持行级锁和外键。

  • 索引的不同:innodb主键索引是聚簇索引,非主键索引是非聚簇索引,myisam是非聚簇索引。聚簇索引的叶子节点就是数据节点,而myism索引的叶子节点仍然是索引节点,只不过是指向对应数据块的指针,InnoDB的非聚簇索引叶子节点存储的是主键,需要再寻址一次才能得到数据
    总结:

  • 是否需要支持事务?innodb

  • 并发写是不是很多?innoda

  • 读多,写少,追求读速度?myisam

索引选择

  • 唯一性约束
  • 联合索引
  • 见索引

mysql隐式类型变换(有一次面试题:存储类型和查询类型不一致会发生什么?)

在MySQL中,隐式类型转换是指在表达式或操作中自动将一个数据类型转换为另一个数据类型。MySQL会根据一组规则来执行隐式类型转换,以便执行操作或比较不同类型的数据。以下是MySQL中的一些常见的隐式类型转换规则:

  • 数值类型之间的转换:MySQL会自动将不同数值类型之间进行隐式转换,例如将整数转换为浮点数,或将较小的数值类型转换为较大的数值类型。
  • 字符串和数值类型之间的转换:MySQL会尝试将字符串转换为数值类型,或将数值类型转换为字符串。如果字符串可以解析为有效的数值,那么它将被转换为相应的数值类型。
  • 日期和时间类型之间的转换:MySQL会自动将日期和时间类型转换为其他日期和时间类型。例如,可以将日期类型转换为字符串,或将字符串转换为日期类型。
  • NULL的处理:在与其他数据类型进行操作时,MySQL会将NULL隐式转换为适当的数据类型。例如,NULL与数值类型相加时会被转换为0

mysql 线上DDL表结构变更注意事项

在MySQL中进行字段类型修改、增加字段、增加索引和删除索引时,需要注意以下事项:

  • 数据备份:在进行任何结构变更之前,务必备份数据库的数据。这样可以在出现意外情况或错误时恢复数据。
  • 考虑数据类型转换:如果要修改字段的数据类型,需要考虑可能的数据类型转换问题。确保目标数据类型能够容纳原有数据,并且进行数据类型转换时不会导致数据丢失或截断。
  • 处理依赖关系:在修改字段类型、增加字段或删除字段时,需要考虑是否存在其他对象(如视图、存储过程或触发器)依赖于该字段。如果存在依赖关系,需要先处理这些依赖关系,以免操作失败或导致不一致性。
  • 使用ALTER TABLE语句:对于字段类型修改、增加字段和删除字段操作,可以使用ALTER TABLE语句来执行。确保在执行ALTER TABLE语句之前,先检查表的当前状态和结构,以避免不必要的错误。
  • 考虑数据量和性能:在进行结构变更操作时,特别是增加字段或增加索引时,需要考虑表中的数据量和性能影响。某些操作可能需要较长时间来完成,或者会对数据库的性能产生影响。在进行这些操作时,要谨慎评估和测试,以确保不会对正常运行产生负面影响。
  • 索引的选择和删除:在增加索引时,需要根据查询需求和数据访问模式选择合适的索引类型(如B-tree索引、哈希索引等)。而在删除索引时,需要确保不会影响到相关查询的性能。在进行索引的修改和删除操作时,最好事先进行性能测试和评估。
  • 注意并发操作和锁定:某些结构变更操作可能需要锁定表或行,以确保数据的一致性。在进行这些操作时,要注意可能的并发访问冲突,并在必要时进行合理的调度和通知,以避免对系统的影响。
  • 测试和验证:在进行结构变更之后,务必进行充分的测试和验证,以确保数据库的功能和性能没有受到不良影响。验证包括执行常见的查询、操作和业务逻辑,以确保一切正常。
  • 一般要求先变更DB,再发布代码
    总之,在进行MySQL的字段类型修改、增加字段、增加索引和删除索引时,需要谨慎行事,提前做好充分的准备、备份和测试,以确保操作的成功和数据的安全性
  • 表锁定和影响:某些DDL操作可能需要锁定整个表,这可能会对其他用户的操作产生影响。请在合适的时机执行DDL操作,避免对关键业务时间或频繁访问的表造成过多的阻塞。
  • 大型表操作:对于大型表的DDL操作(如ALTER TABLE),可能会涉及大量的数据移动和重建,可能会导致长时间的操作和额外的存储空间使用。在执行这些操作之前,请确保对表的大小和操作的影响进行评估
  • 错误处理和回滚:在执行DDL操作时,要注意捕获和处理可能的错误。如果DDL操作失败,确保有适当的错误处理机制和回滚策略,以保持数据的一致性
  • 数据库备份:在执行重要的DDL操作之前,请确保对数据库进行备份,以防操作出现问题导致数据丢失或不可恢复。这可以帮助你在需要时还原到先前的状态

mysql架构扩展

关系型数据库扩展包括许多技术:主从复制主主复制联合分片非规范化SQL调优

主从复制


资料来源:可扩展性、可用性、稳定性、模式

主库同时负责读取和写入操作,并复制写入到一个或多个从库中,从库只负责读操作。树状形式的从库再将写入复制到更多的从库中去。如果主库离线,系统可以以只读模式运行,直到某个从库被提升为主库或有新的主库出现。主要的优缺点: - 读写分离提供集群的性能 - 主、从多节点,宕机容灾 - 将从库提升为主库需要额外的逻辑 - 主从延时问题,需要监控

主主复制,多主复制


资料来源:可扩展性、可用性、稳定性、模式

两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。

  • 多主复制
    优缺点:
  • 你需要添加负载均衡器或者在应用逻辑中做改动,来确定写入哪一个数据库。
  • 多数主-主系统要么不能保证一致性(违反 ACID),要么因为同步产生了写入延迟。
  • 随着更多写入节点的加入和延迟的提高,如何解决冲突显得越发重要
  • 多活架构

联合(垂直分实例,比如商品实例、订单实例等分开)


资料来源:扩展你的用户数到第一个一千万

优缺点:
联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:论坛用户产品,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。

  • 如果你的数据库模式需要大量的功能和数据表,联合的效率并不好。
  • 你需要更新应用程序的逻辑来确定要读取和写入哪个数据库。
  • 从两个库联结数据更复杂。
  • 联合需要更多的硬件和额外的复杂度。

分片 (水平分实例,比如订单按照用户shard)


资料来源:可扩展性、可用性、稳定性、模式

https://www.digitalocean.com/community/tutorials/understanding-database-sharding

分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。
类似联合的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。
常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。

  • 你需要修改应用程序的逻辑来实现分片,这会带来复杂的 SQL 查询。
  • 分片不合理可能导致数据负载不均衡。例如,被频繁访问的用户数据会导致其所在分片的负载相对其他分片高。
  • 再平衡会引入额外的复杂度。基于一致性哈希的分片算法可以减少这种情况。
  • 联结多个分片的数据操作更复杂。
  • 分片需要更多的硬件和额外的复杂度。
  • 分片时代来临
  • 数据库分片架构
  • 一致性哈希

分表/分库/历史数据归档和路由

原文链接:https://juejin.cn/post/6844903872134135816

  • 今天,探讨一个有趣的话题:MySQL 单表数据达到多少时才需要考虑分库分表?有人说 2000 万行,也有人说 500 万行。那么,你觉得这个数值多少才合适呢?
    曾经在中国互联网技术圈广为流传着这么一个说法:MySQL 单表数据量大于 2000 万行,性能会明显下降。事实上,这个传闻据说最早起源于百度。具体情况大概是这样的,当年的 DBA 测试 MySQL性能时发现,当单表的量在 2000 万行量级的时候,SQL 操作的性能急剧下降,因此,结论由此而来。然后又据说百度的工程师流动到业界的其它公司,也带去了这个信息,所以,就在业界流传开这么一个说法。
    再后来,阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。对此,有阿里的黄金铁律支撑,所以,很多人设计大数据存储时,多会以此为标准,进行分表操作。那么,你觉得这个数值多少才合适呢?为什么不是 300 万行,或者是 800 万行,而是 500 万行?也许你会说这个可能就是阿里的最佳实战的数值吧?那么,问题又来了,这个数值是如何评估出来的呢?稍等片刻,请你小小思考一会儿。事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为,MySQL 为了提高性能,会将表的索引装载到内存中。InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。这里,增加硬件配置,可能会带来立竿见影的性能提升哈。
    那么,我对于分库分表的观点是,需要结合实际需求,不宜过度设计,在项目一开始不采用分库与分表设计,而是随着业务的增长,在无法继续优化的情况下,再考虑分库与分表提高系统的性能。对此,阿里巴巴《Java 开发手册》补充到:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。那么,回到一开始的问题,你觉得这个数值多少才合适呢?我的建议是,根据自身的机器的情况综合评估,如果心里没有标准,那么暂时以 500 万行作为一个统一的标准,相对而言算是一个比较折中的数值。

案例1. 酒店分表:

  • 酒店数量100w, 支持8中语言,2000kw种房型,1亿的图片。支持未来3年可能扩展成:酒店数量500w, 支持8钟语言,房型1亿,图片5亿
  • 分表方式:hotel 1张表,多语言表10张表,房型表20张,图片表:100张表
  • 酒店和多语言文本垂直分表
  • 根据酒店id水平分表。
  • 如果还要继续扩展,可以重新搞一个库,酒店id从500w开始,不断扩展。增加一个数据路由的模块。

案例2. 订单分表和历史订单归档(3个月或者更长时间)

案例3. 数据历史版本记录、快照表

  • 在有些场景中,数据变更不回特别频繁,特别是人工变更时,记录数据版本和快照是非常好的习惯,方便追溯历史行为记录
  • 数据变更时通常会先写入快照表或者历史记录表,通常在业务代码中实现
  • 有时也会采用mysql 存储过程实现:https://blog.csdn.net/wcdunf/article/details/129792810

案例4. 商品库存扣减方案

添加索引index,优化访问速度

  • 关于MySQL索引那些事
  • 什么是索引,对索引的理解,索引时一种数据结构,通过增加索引通常可以提高数据库查询的效率,但是为了维护索引结构也会降低数据更新的效率和增加一些存储代价。
  • 索引类型
    1
    2
    3
    4
    5
    普通索引(INDEX):最基本的索引,没有任何限制
    唯一索引(UNIQUE):与"普通索引"类似,不同的就是:索引列的值必须唯一,但允许有空值。
    主键索引(PRIMARY):它 是一种特殊的唯一索引,不允许有空值。
    全文索引(FULLTEXT ):仅可用于 MyISAM 表, 用于在一篇文章中,检索文本信息的, 针对较大的数据,生成全文索引很耗时好空间。
    组合索引:为了更多的提高mysql效率可建立组合索引,遵循”最左前缀“原则。
  • 理解主键索引和普通索引、聚簇索引和非聚簇索引、单列索引和联合索引、覆盖索引和回表
    1
    2
    3
    4
    5
    - 主键索引和普通索引。数据和主键索引用B+Tree来组织的,没有主键innodb会生成唯一列,类似于rowid。InnoDB非主键索引的叶子节点存储的是主键
    - 单列索引和联合索引,联合索引的存储结构,联合索引的左前缀原则
    - 聚簇索引和非聚簇索引,聚簇索引数据和索引一起存储,非聚簇索引在无法做到索引覆盖的情况下需要回表
    - 覆盖索引。覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。
    如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引
  • 索引的数据结构,红黑树、B树、B+树的比较
  • 面试题:InnoDB中一棵B+树能存多少行数据?计算innob的高度
  • 列出索引失效的几种场景?
    • 条件中包含or
    • 条件中包含%like
    • 联合索引,违背最左匹配原则
    • 在索引列上有一些额外的计算操作
  • 联合索引和最左匹配原则

ACID、事务、数据库锁(数据准确性和并发安全,一锁二判三更新)

  • 精读mysql事务
  • innodb事务的ACID特性,以及其对应的实现原理?
    • 原子性:在很多场景中,一个操作需要执行多条 update/insert SQL。原子性保证了SQL语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undolog/redolog
    • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
    • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
    • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障
  • innodb四种隔离属性以及分别会产生什么问题?分别举例说明
    • 读未提交(READ UNCOMMITTED),会产生脏读问题
    • 读提交,READ-COMMITTED,会产生不可重复读问题
    • 可重复读 (REPEATABLE READ),幻读问题(insert),mysql 默认的事务隔离级别
    • SERIALIZABLE(可串行化)
  • 事务的隔离属性底层实现原理,关于锁和mvcc
    • 可以先阐述四种隔离级别,再阐述它们的实现原理。隔离级别就是依赖锁和MVCC实现的。
  • 悲观锁与乐观锁
    • Select for update使用详解 及在库存和金钱系统上的应用
    • 悲观锁:悲观锁是一种保守的并发控制机制,它假设在并发访问中会发生冲突,因此在访问数据之前会锁定资源,阻止其他事务对资源进行修改。在MySQL中,悲观锁主要通过以下方式实现:
      • 使用SELECT … FOR UPDATE语句:在读取数据时对所选行进行锁定,确保其他事务不能对这些行进行修改。
      • 使用LOCK TABLES语句:锁定整个表,防止其他事务对该表进行读取和修改。
    • 乐观锁:乐观锁是一种乐观的并发控制机制,它假设在并发访问中不会发生冲突,允许多个事务同时访问资源。当提交事务时,系统会检查资源是否被其他事务修改,如果检测到冲突,则回滚事务。在MySQL中,乐观锁通常通过以下方式实现:
      • 使用版本号或时间戳:在数据表中增加一个版本号或时间戳字段,每次修改数据时更新该字段。在提交事务时,检查版本号或时间戳是否与开始事务时的值相同,如果不同则表示发生了冲突。
      • 使用CAS(Compare and Swap)操作:在编程语言层面,通过CAS操作来比较内存中的值与预期值是否相等,如果相等则修改,否则放弃修改。
        使用乐观锁和悲观锁的选择取决于应用场景和需求:悲观锁适合在并发冲突频繁的情况下,通过独占资源避免并发问题,但会对系统性能产生一定的影响。乐观锁适合在并发冲突较少的情况下,通过乐观的并发控制机制提高系统性能,但需要处理冲突的情况。在实际使用时,需要根据具体业务场景和需求选择适当的并发控制机制,并注意处理冲突和回滚事务的策略,以确保数据的一致性和完整性。
  • 死锁问题,如何避免死锁
    • 死锁的条件:
      • 事务并发执行:多个事务同时操作相同的数据,请求相同或不同的锁资源。
      • 锁竞争:事务之间竞争相同的资源而产生死锁。
      • 不同的锁顺序:不同的事务以不同的顺序请求锁资源,导致死锁。
    • 避免死锁的方法:
      • 统一锁资源访问顺序:对于需要操作多个锁资源的事务,保持统一的访问顺序,避免不同事务之间出现交叉的锁请求顺序
      • 减少事务持有时间:尽量将事务的持有时间缩短,减少锁资源的占用时间,降低死锁的概率。
      • 使用合理的索引:合理的索引设计可以减少查询中的锁竞争,提高并发性能,减少死锁的可能性。
      • 限制事务并发度:通过调整事务的并发度,限制同时执行的事务数量,减少锁竞争的机会。
  • 分布式事务

数据库调优

  • 优化的步骤

    • 考虑数据量大导致的性能问题,访问量大导致的性能问题?
    • sql语句优化。分析执行计划,减少load的数据量
    • 考虑能否通过增加索引优化查询效率,检查索引是否生效
    • 是否有缓存
    • 垂直分表、水平分表、分库
    • 根据场景来看,写操作多的情况下,考虑读写分离
    • 数据归档:数据是否有冷热的区别,例如订单数据有比较明显的时间冷热的区别,可以考虑冷数据归档。比如半年前的订单数据可以写入hbase
    • 池化
  • 架构优化

    • 分库,分表。垂直分,水平分。依据QPS和耗时,服务端最大并非连接数量
    • 读写分离
    • 批量读写,批量更新
    • 异步写,写平滑
    • 缓存优化
    • 历史数据归档
  • 连接池的配置和使用

    • 连接池能减少连接创建和释放带来的开销,大多数SDK也支持是支持连接池的,通常实际生产环境中也都会使用到连接池,需要关注一下几个参数
    • max_idle_connections: 最大空闲连接数
    • max_open_connections: 最大连接数
    • connection_max_lifetime: 连接最大可重用时间
    • 要使用好连接池,除了关注客户端的配置还需要关注mysql服务端的配置
    • 服务端最大连接数量:show variables like ‘%connection%’; max_connections
    • 服务端连接最大生命周期:show variables like ‘%wait_timeout%’
      1
      2
      3
      最大空闲连接数 =(QPS*请求平均耗时)/ 应用节点个数
      最大连接数 =(QPS*请求最大耗时)/ 应用节点个数
      客户端连接maxlifetime < 数据库服务端设置的connection_max_lifttime
  • 慢sql优化

    • 慢查询问题,查看慢查询设置的阈值。show variables like ‘%long_query%’;
    • 打开慢查询日志
    • 分析数据sql的结构是否加载了不必要的字段和数据
    • 深度分页查询优化
    • 子查询和连接查询
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
            	explain select * from test_xxxx_tab txt order by id limit 10000,10;
      explain SELECT * from test_xxxx_tab txt where id >= (select id from test_xxxx_tab txt order by id limit 10,1) limit 10;
      id列:在复杂的查询语句中包含多个查询使用id标示
      select_type:select/subquery/derived/union
      table: 显示对应行正在访问哪个表
      type:访问类型,关联类型。非常重要,All,index,range,ref,const,
      possible_keys: 显示可以使用哪些索引列
      key列:显示mysql决定使用哪个索引来优化对该表的访问
      key_len:显示在索引里使用的字节数
      rows:为了找到所需要的行而需要读取的行数
    • 慢查询日志样例子
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      # Time: 2022-05-10T10:15:32.123456Z
      # User@Host: myuser[192.168.0.1] @ localhost [] Id: 12345
      # Query_time: 3.456789 Lock_time: 0.123456 Rows_sent: 10 Rows_examined: 100000
      SET timestamp=1657475732;
      SELECT * FROM orders WHERE customer_id = 1001 ORDER BY order_date DESC LIMIT 10;
      这个慢查询日志示例包含以下重要的信息:

      时间戳(Time): 日志记录的时间,以 UTC 时间表示。
      用户和主机(User@Host): 执行查询的用户和主机地址。
      连接 ID(Id): 表示执行查询的连接 ID。
      查询时间(Query_time): 查询执行所花费的时间,以秒为单位。
      锁定时间(Lock_time): 在执行查询期间等待锁定资源所花费的时间,以秒为单位。
      返回行数(Rows_sent): 查询返回的结果集中的行数。
      扫描行数(Rows_examined): 在执行查询过程中扫描的行数。
      时间戳(SET timestamp): 查询开始执行的时间戳。
      查询语句(SELECT * FROM orders WHERE customer_id = 1001 ORDER BY order_date DESC LIMIT 10): 实际执行的查询语句
  • index优化

    • 会查看sql执行计划explain
    • 关注:type、const、ref
    • 关注:extra等字段
  • 使用缓存优化DB需要考虑的问题

    • 缓存更新、过期、淘汰的策略
    • 缓存可能遇到的三大问题,雪崩、穿透、击穿
    • 缓存和db的一致性问题,缓存更新策略及其分析?,业界比较通用的先更新DB,再删除cache
  • 库表优化/分表/分库

    • 垂直分表
    • 水平分表
    • 分库
    • 业界成熟的方案
  • 架构优化读写分离优化

    • 在写操作的较多的情况可以考虑数据库读写分离的方案
    • 业界的方案,代理实现和业务实现
  • 核心监控告警指标

    • read write qps 监控/select/update/insert
    • connections
    • thread
    • InnoDB buffer pool
    • 慢查询监控
    • 网络流量IO
    • 读写分离架构时需要监控主从延时
  • 关键配置查看

    1
    2
    3
    4
    5
    show global variables;
    show variables like '%max_connection%'; 查看最大连接数
    show status like 'Threads%';
    show processlist;
    show variables like '%connection%';
  • 存储空间information_schema

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    -- desc information_schema.tables;
    -- 查看 MySQL「所有库」的容量大小
    SELECT table_schema AS '数据库', SUM(table_rows) AS '记录数',
    SUM(truncate(data_length / 1024 / 1024, 2)) AS '数据容量(MB)',
    SUM(truncate(index_length / 1024 / 1024, 2)) AS '索引容量(MB)',
    SUM(truncate(DATA_FREE / 1024 / 1024, 2)) AS '碎片占用(MB)'
    FROM information_schema.tables
    GROUP BY table_schema
    ORDER BY SUM(data_length) DESC, SUM(index_length) DESC;
    -- 指定书库查看表的数据量
    SELECT
    table_schema as '数据库',
    table_name as '表名',
    table_rows as '记录数',
    truncate(data_length/1024/1024, 2) as '数据容量(MB)',
    truncate(index_length/1024/1024, 2) as '索引容量(MB)',
    truncate(DATA_FREE/1024/1024, 2) as '碎片占用(MB)'
    from
    information_schema.tables
    where
    table_schema='<数据库名>'
    order by
    data_length desc, index_length desc;

MySQL多表关联查询 vs 多次单表查询service组装

  • 多次单表查询+Service组装:
    • 灵活性:多次单表查询+Service组装方式更加灵活,可以根据具体需求灵活组装和调整查询逻辑,适应各种复杂的查询需求。
    • 可扩展性:通过多次单表查询和Service组装,可以将查询逻辑分解为多个简单的查询,有助于代码的模块化和可扩展性,方便后续的维护和修改。
    • 缓存利用:多次单表查询+Service组装方式可以更好地利用缓存,针对每个单表查询的结果进行缓存,提高查询性能
      https://www.zhihu.com/question/68258877

mysql binlog

show processlist;

常用命令

  • mysql登陆:
    mysql -h主机 -P端口 -u用户 -p密码
    SET PASSWORD FOR ‘root‘@’localhost’ = PASSWORD(‘root’);
    create database wxquare_test;
    show databases;
    use wxquare_test;
  • 查看见表sql:show create table table_name;
  • show variables like ‘%timeout%’;
  • update json 文本需要转义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      update table set extinfo='{
    \"urls\": [
    {
    \"url\": \"/path1\",
    \"type\": \"type1\"
    },
    {
    \"url\": \"/path2\",
    \"type\": \"type2\"
    },
    ]
    }' where id = 2;
  • truncate table 属于ddl语句,需要ddl的权限
  • mysqldump 库表结构
    1
    mysqldump --column-statistics=0 -hhost -PPort -uuser_name -ppassword --databases -d db_name --skip-lock-tables --skip-add-drop-table --set-gtid-purged=OFF | sed 's/ AUTO_INCREMENT=	[0-9]*//g' > db.sql

    - 批量更新
    1
    2
    3
    4
    5
    6
    7
    8
    UPDATE employees
    SET salary = CASE
    WHEN grade = 'A' THEN salary * 1.1
    WHEN grade = 'B' THEN salary * 1.05
    WHEN grade = 'C' THEN salary * 1.03
    ELSE salary
    END
    WHERE department = 'IT';

推荐阅读:

负载均衡器和反向代理



来源:可扩展的系统设计模式

负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:

  • 防止请求进入不好的服务器
  • 防止资源过载
  • 帮助消除单一的故障点
  • SSL 终结 ─ 解密传入的请求并加密服务器响应,这样的话后端服务器就不必再执行这些潜在高消耗运算了。
  • 不需要再每台服务器上安装 X.509 证书
  • Session 留存 ─ 如果 Web 应用程序不追踪会话,发出 cookie 并将特定客户端的请求路由到同一实例。
  • 通常会设置采用工作─备用双工作 模式的多个负载均衡器,以免发生故障。

负载均衡器能基于多种方式来路由流量:

四层负载均衡

四层负载均衡根据监看传输层的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行网络地址转换(NAT)来向上游服务器转发网络数据包。

七层负载均衡器

七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。

以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。

水平扩展

负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上垂直扩展更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。

缺陷:水平扩展

  • 水平扩展引入了复杂度并涉及服务器复制
  • 服务器应该是无状态的:它们也不该包含像 session 或资料图片等与用户关联的数据。
  • session 可以集中存储在数据库或持久化缓存(Redis、Memcached)的数据存储区中。
  • 缓存和数据库等下游服务器需要随着上游服务器进行扩展,以处理更多的并发连接。

缺陷:负载均衡器

  • 如果没有足够的资源配置或配置错误,负载均衡器会变成一个性能瓶颈。
  • 引入负载均衡器以帮助消除单点故障但导致了额外的复杂性。
  • 单个负载均衡器会导致单点故障,但配置多个负载均衡器会进一步增加复杂性。

反向代理(web 服务器)


资料来源:维基百科

反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。

带来的好处包括:

  • 增加安全性 - 隐藏后端服务器的信息,屏蔽黑名单中的 IP,限制每个客户端的连接数。
  • 提高可扩展性和灵活性 - 客户端只能看到反向代理服务器的 IP,这使你可以增减服务器或者修改它们的配置。
  • 本地终结 SSL 会话 - 解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作。免除了在每个服务器上安装 X.509 证书的需要
  • 压缩 - 压缩服务器响应
  • 缓存 - 直接返回命中的缓存结果
  • 静态内容 - 直接提供静态内容
    • HTML/CSS/JS
    • 图片
    • 视频
    • 等等

负载均衡器与反向代理

  • 当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上。
  • 即使只有一台 web 服务器或者应用服务器时,反向代理也有用,可以参考上一节介绍的好处。
  • NGINX 和 HAProxy 等解决方案可以同时支持第七层反向代理和负载均衡。

不利之处:反向代理

  • 引入反向代理会增加系统的复杂度。
  • 单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如故障转移)会进一步增加复杂度。

来源及延伸阅读

前言

为什么要做设计方案

  • 设计是系统实现的蓝图
  • 设计是沟通协作的基础
  • 设计是思考的过程决定了产品的质量
    理解对齐:所有软件系统的目的都是为了实现用户需求,但实现的途径有无限种可能性(相比传统工程行业,软件的灵活性更大、知识迭代更快)。架构设计就是去选择其中一条最合适的实现途径,因此其中会涉及非常多关键的选路决策(为什么要这么拆分?为什么选择 A 技术而不是 B?)。这些重要的技术决策需要通过架构描述这种形式被记录和同步,才能让项目组所有成员对整个系统的理解对齐,形成共识。
    工作量化:项目管理最重要的步骤之一就是工时评估,它是确定项目排期和里程碑的直接依据。显然,只通过 PRD / 交互图是无法科学量化出项目工作量的,因为很难直观判断出一句简短需求或一个简单页面背后,究竟要写多少代码、实现起来难度有多大。有了清晰明确的架构之后,理论上绝大部分开发工作都能做到可见、可预测和可拆解,自然而然也就能够被更准确地量化。当然,精准的工作量评估在 IT 行业内也一直是个未解之谜,实际的工期会受太多未知因素影响,包括程序员的技能熟练度、心情好不好、有没有吃饱等。
    标准术语:编程作为一种具有创造力的工作,从某种角度看跟写科幻小说是类似的。好的科幻小说都喜欢造概念,比如三体中的智子,如果没看过小说肯定不知道这是个啥玩意儿。软件系统在造概念这一点上,相比科幻小说只有过之而无不及,毕竟小说里的世界通常还是以现实为背景,而软件中的世界就全凭造物者(程序员)的想象(建模)了。稍微复杂一点的软件系统,都会引入一些领域特定甚至全新创作的概念。为了避免在项目过程中出现鸡同鸭讲的沟通障碍和理解歧义,就必须对描述这些概念的术语进行统一。而架构的一个重要目的,就是定义和解释清楚系统中涉及的所有关键概念,并在整个架构设计和描述过程中使用标准和一致的术语,真正做到让大家的沟通都在一个频道上。
    言之有物 :就跟讨论产品交互时需要对着原型图、讨论代码细节时需要直接看代码一样,架构是在讨论一些较高维技术问题时的必要实物(具体的实物化形式就是所谓架构描述)。否则,要么一堆人对着空气谈(纸上谈兵都说不上),要么每次沟通时都重新找块白板画一画(费时费力且容易遗落信息,显然不是长久之计)。
    知识沉淀 & 新人培训:架构应该被作为与代码同等重要的文档资产持续沉淀和维护,同时也是项目新人快速理解和上手系统的重要依据。不要让你的系统跟公司内某些祖传遗留系统一样 —— 只有代码遗留了下来,架构文档却没有;只能靠一些口口相传的残留设计记忆,苦苦维系着项目的生命延续

技术方案应该包含哪些内容


  1. 背景:

    • 解决的问题:明确要解决的技术问题和产品问题的具体描述。
    • 难点和挑战:列出可能遇到的难点、挑战和限制条件。
    • 目标和关键指标:明确解决方案的目标和关键指标,例如性能要求、用户体验等。
  2. 外部依赖调研

    • 外部服务和组件:列出系统所依赖的外部服务、组件或系统,并描述其功能和接口。
    • 管理和集成策略:说明如何管理和集成外部依赖,包括版本控制、接口规范等。
  3. 业界方案调研和对比:

    • 调研结果:调研现有的业界解决方案,并总结其优缺点。
    • 对比分析:比较不同方案之间的特点、适用性和可行性。
  4. 整体设计:

    • 业务流程架构图:展示系统的业务流程和组件之间的关系。
    • 系统调用拓扑图:显示系统内部和外部的调用关系。
    • 技术架构图:描述系统的技术架构,包括各个模块、组件和数据流之间的关系。
  5. 功能设计:

    • 存储设计:定义系统中数据的存储方式和结构。
    • 接口设计:定义系统的各个模块之间的接口和通信方式。
    • 流程设计:描述系统的各个功能模块的流程和交互方式。
    • 缓存设计:确定系统中需要使用的缓存策略和机制。
  6. 非功能设计:

    • 兼容性设计:考虑系统与不同平台、浏览器或设备的兼容性。
    • 稳定性设计:定义系统的容错和恢复机制,确保系统的稳定性和可用性。
    • 扩展性设计:考虑系统的可扩展性,以便在需要时能够方便地扩展功能和容量。
    • 安全设计:定义系统的安全策略和机制,保护用户数据和系统资源。
    • 性能设计:考虑系统的性能需求,并设计相应的优化措施。
    • 部署设计:定义系统的部署架构和流程,包括服务器配置、网络拓扑等。
    • 可维护性设计:考虑系统的可维护性,包括日志记录、错误处理和调试功能。
    • 测试策略和方案:定义系统的测试策略和测试计划,包括单元测试、集成测试和系统测试等。
    • 部署和运维设计:描述系统的部署和运维策略,包括自动化部署、监控和故障处理等。
    • 风险点:识别系统设计中的潜在风险和问题,并提供相应的应对措施。
    • 监控设计和异常处理机制:
    • 监控需求:定义系统的监控需求,包括日志记录、性能监控和错误监控等。
    • 异常处理机制:描述系统对异常情况的处理方式和机制,包括错误提示、异常捕获和处理流程等。
  7. 资源清单:

    • 硬件资源:列出系统所需的硬件资源,例如服务器、存储设备等。
    • 软件资源:列出系统所需的软件资源,例如操作系统、数据库等。
    • 人力资源:确定系统开发和维护所需的人力资源,包括开发人员、测试人员等。
  8. 任务拆分和排期:

    • 任务拆分:将系统开发和实施过程分解为具体的任务和子任务。
    • 排期计划:为每个任务和子任务确定时间表和优先级。
  9. 评审记录:

    • 评审会议记录:记录技术方案评审会议的讨论和决策结果。
    • 修改和改进建议:记录评审过程中提出的修改和改进建议,并记录其处理状态。

如何评估技术设计的质量


功能性

  • 功能完整度
  • 功能正确性
  • 功能恰当性

稳定性(Dependability Criteria):

  • 可靠性(Reliability):系统处理错误和故障,保证数据完整性和可用性的能力
  • 兼容性,向前兼容性值
  • 可用性(Availability):系统在投入使用时可操作和可访问的程度。
  • 安全性(Security):系统保护用户数据和系统资源,防止未经授权的访问和恶意行为的能力

性能(Performance):

  • 响应时间(Latency):系统对请求的反应速度。
  • 吞吐量(Throughput):系统处理的工作量

成本(Cost):

  • 开发成本(Development Cost):系统的构建和开发所需的费用。
  • 部署成本(Deployment Cost):系统部署和运行所需的资源成本。
  • 升级成本(Upgrade Cost):将数据从旧系统转换到新系统,以及满足向后兼容性要求的成本。
  • 维护成本(Maintenance Cost):包括错误修复和未来功能增强的成本。
  • 运营成本(Administration Cost):运行系统的成本。

维护性(Maintainability

  • 可扩展性(Extensibility):系统添加新功能的容易程度。
  • 可修改性(Modifiability):系统更改功能的容易程度。
  • 适应性(Adaptability):系统适应不同应用领域的能力。
  • 可移植性(Portability):系统在不同计算机平台上运行的容易程度。
  • 可读性(Readability):代码的理解难度。
  • 需求可追溯性(Tracability of Requirements):代码与需求之间的映射关系
  • 可测试性

用户体验(User Experience)

  • 系统提供友好的用户界面和良好的用户交互,以提高用户满意度和使用效率

如何量化系统指标(SLA指标)

reliable


available


efficiency

latency and throughput

manageability


系统设计的权衡(top15 trade-off)

性能与可扩展性的权衡:提高性能可能需要牺牲一部分可扩展性,因为某些优化可能会引入复杂性或限制系统的扩展性。
可维护性与性能的权衡:某些优化措施可能会降低代码的可读性和可维护性,因此需要在维护性和性能之间进行权衡。
时间与成本的权衡:系统设计需要考虑开发时间和成本,以确保在给定资源限制下实现最佳的设计方案
安全性与用户体验的权衡:强大的安全措施可能会增加用户的身份验证和授权过程,从而影响用户体验。
架构权衡评估方法(ATAM):如何评估一个系统的质量
架构-trade-off(架构权衡
https://haomo-tech.com/project-docs/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3/assets/%E7%B3%BB%E7%BB%9F%E4%B8%9A%E5%8A%A1%E6%9E%B6%E6%9E%84%E5%9B%BE.omnigraffle
架构-trade-off(架构权衡
架构权衡评估方法(ATAM):如何评估一个系统的质量
系统架构

技术方案模板

** 附录:设计文档模板 **
设计文档没有定式。即使如此,笔者参考谷歌设计文档的结构和格式,并结合实际工作经验加以完善。在此提供一个可供新手参考的设计文档模版,您可以使用此文档模板作为思考的基础。通常,无须事无巨细地填写每一部分,不相关的内容直接略过即可。

计决策的合理性,同时也有助于日后迭代设计时,检查最初的假设是否仍然成立。

背景

我们要解决的问题是什么

为设计文档的目标读者提供理解详细设计所需的背景信息。按读者范围来提供背景。见上文关于目标读者的圈定。设计文档应该是“自足的”(self-contained),即应该为读者提供足够的背景知识,使其无需进一步的查阅资料即可理解后文的设计。保持简洁,通常以几段为宜,每段简要介绍即可。如果需要向读者提供进一步的信息,最好只提供链接。警惕知识的诅咒(知识的诅咒(Curse of knowledge)是一种认知偏差,指人在与他人交流的时候,下意识地假设对方拥有理解交流主题所需要的背景知识)

背景通常可以包括:
需求动机以及可能的例子。 如,“(tRPC) 微服务模式正在公司内变得流行,但是缺少一个通用的、封装了常用内部工具及服务接口的微服务框架”。 - 这是放置需求文档的链接的好地方。
此前的版本以及它们的问题。 如,“(tRPC) Taf 是之前的应用框架, 有以下特点,…………, 但是有以下局限性及历史遗留问题”。
其它已有方案, 如公司内其它方案或开源方案, “tRPC v.s. gRPC v.s. Arvo”
相关的项目,如 “tRPC 框架中可能会对接的其它 PCG 系统”
不要在背景中写你的设计,或对问题的解决思路。

难点和挑战

“解决这个问题的难点和挑战”

用几句话说明该设计文档的关键目的,让读者能够一眼得知自己是否对该设计文档感兴趣。 如:“本文描述 Spanner 的顶层设计”

目标和关键指标

继而,使用 Bullet Points 描述该设计试图达到的重要目标,如:

  • 可扩展性
  • 多版本
  • 全球分布
  • 同步复制
    非目标也可能很重要。非目标并非单纯目标的否定形式,也不是与解决问题无关的其它目标,而是一些可能是读者非预期的、本可作为目标但并没有的目标,如:
  • 高可用性
  • 高可靠性 如果可能,解释是基于哪些方面的考虑将之作为非目标。如:
  • 可维护性: 本服务只是过渡方案,预计寿命三个月,待 XX 上线运行后即可下线
    设计不是试图达到完美,而是试图达到平衡。 显式地声明哪些是目标,哪些是非目标,有助于帮助读者理解下文中设

总体设计

“我们如何解决这个问题?”

用一页描述高层设计。说明系统的主要组成部分,以及一些关键设计决策。应该说明该系统的模块和决策如何满足前文所列出的目标。

本设计文档的评审人应该能够根据该总体设计理解你的设计思路并做出评价。描述应该对一个新加入的、不在该项目工作的腾讯工程师而言是可以理解的。

推荐使用系统关系图描述设计。它可以使读者清晰地了解文中的新系统和已经熟悉的系统间的关系。它也可以包含新系统内部概要的组成模块。

注意:不要只放一个图而不做任何说明,请根据上面小节的要求用文字描述设计思想。

  • 一个示例体统关系图

  • 自举的文档结构图

  • 可能不太好的顶层设计
    不要在这里描述细节,放在下一章节中; 不要在这里描述背景,放在上一章节中。

详细设计

在这一节中,除了介绍设计方案的细节,还应该包括在产生最终方案过程中,主要的设计思想及权衡(tradeoff)。这一节的结构和内容因设计对象(系统,API,流程等)的不同可以自由决定,可以划分一些小节来更好地组织内容,尽可能以简洁明了的结构阐明整个设计。

不要过多写实现细节。就像我们不推荐添加只是说明”代码做了什么”的注释,我们也不推荐在设计文档中只说明你具体要怎么实现该系统。否则,为什么不直接实现呢? 以下内容可能是实现细节例子,不适合在设计文档中讨论:

  • ** API 的所有细节 **
  • ** 存储系统的 Data Schema **
  • ** 具体代码或伪代码 **
  • ** 该系统各模块代码的存放位置、各模块代码的布局 **
  • ** 该系统使用的编译器版本 **
    开发规范
    通常可以包含以下内容(注意,小节的命名可以更改为更清晰体现内容的标题):

** 各子模块的设计 **
阐明一些复杂模块内部的细节,可以包含一些模块图、流程图来帮助读者理解。可以借助时序图进行展现,如一次调用在各子模块中的运行过程。每个子模块需要说明自己存在的意义。如无必要,勿添模块。如果没有特殊情况(例如该设计文档是为了描述并实现一个核心算法),不要在系统设计加入代码或者伪代码。

** API 接口 **
如果设计的系统会暴露 API 接口,那么简要地描述一下 API 会帮助读者理解系统的边界。避免将整个接口复制粘贴到文档中,因为在特定编程语言中的接口通常包含一些语言细节而显得冗长,并且有一些细节也会很快变化。着重表现 API 接口跟设计最相关的主要部分即可。

** 存储 **
介绍系统依赖的存储设计。该部分内容应该回答以下问题,如果答案并非显而易见:

该系统对数据/存储有哪些要求? - 该系统会如何使用数据? - 数据是什么类型的? - 数据规模有多大? - 读写比是多少?读写频率有多高? - 对可扩展性是否有要求? - 对原子性要求是什么? - 对一致性要求是什么?是否需要支持事务? - 对可用性要求是什么? - 对性能的要求是什么? - …………
基于上面的事实,数据库应该如何选型? - 选用关系型数据库还是非关系型数据库?是否有合适的中间件可以使用? - 如何分片?是否需要分库分表?是否需要副本? - 是否需要异地容灾? - 是否需要冷热分离? - …………
数据的抽象以及数据间关系的描述至关重要。可以借助 ER 图(Entity Relationshiop) 的方式展现数据关系。

回答上述问题时,尽可能提供数据,将数据作为答案或作为辅助。 不要回答“数据规模很大,读写频繁”,而是回答“预计数据规模为 300T, 3M 日读出, 0.3M 日写入, 巅峰 QPS 为 300”。这样才能为下一步的具体数据库造型提供详细的决策依据,并让读者信服。 注意:在选型时也应包括可能会造成显著影响的非技术因素,如费用。

避免将所有数据定义(data schema)复制粘贴到文档中,因为 data schema 更偏实现细节。

其他方案
“我们为什么不用另一种方式解决问题?”

在介绍了最终方案后,可以有一节介绍一下设计过程中考虑过的其他设计方案(Alternatives Considered)、它们各自的优缺点和权衡点、以及导致选择最终方案的原因等。通常,有经验的读者(尤其是方案的审阅者)会很自然地想到一些其他设计方案,如果这里的介绍描述了没有选择这些方案的原因,就避免读者带着疑问看完整个设计再来询问作者。这一节可以体现设计的严谨性和全面性。

交叉关注点
基础设施
如果基础设施的选用需要特殊考量,则应该列出。 如果该系统的实现需要对基础设施进行增强或变更,也应该在此讨论。

可扩展性
你的系统如何扩展?横向扩展还是纵向扩展?注意数据存储量和流量都可能会需要扩展。

安全 & 隐私
项目通常需要在设计期即确定对安全性的保证,而难以事后补足。不同于其它部分是可选的,安全部分往往是必需的。即使你的系统不需要考虑安全和隐私,也需要显式地在本章说明为何是不必要的。安全性如何保证?

系统如何授权、鉴权和审计(Authorization, Authentication and Auditing, AAA)?
是否需要破窗(break-glass)机制?
有哪些已知漏洞和潜在的不安全依赖关系?
是否应该与专业安全团队讨论安全性设计评审?
……
数据完整性
如何保证数据完整性(Data Integrity)?如何发现存储数据的损坏或丢失?如何恢复?由数据库保证即可,还是需要额外的安全措施?为了数据完整性,需要对稳定性、性能、可复用性、可维护性造成哪些影响?

延迟
声明延迟的预期目标。描述预期延迟可能造成的影响,以及相关的应对措施。

冗余 & 可靠性
是否需要容灾?是否需要过载保护、有损降级、接口熔断、轻重分离?是否需要备份?备份策略是什么?如何修复?在数据丢失和恢复之间会发生什么?

稳定性
SLA 目标是什么? 如果监控?如何保证?

外部依赖

你的外部依赖的可靠性(如 SLA)如何?会对你的系统的可靠性造成何种影响?如果你的外部依赖不可用,会对你的系统造成何种影响?除了服务级的依赖外,不要忘记一些隐含的依赖,如 DNS 服务、时间协议服务、运行集群等。

任务查分和研发排期

描述时间及人力安排(如里程碑)。 这利于相关人员了解预期,调整工作计划。

遗留的问题、未来计划

未来可能的计划会方便读者更好地理解该设计以及其定位。

技术方案设计的规范与模板

技术设计基础

面向对象系统设计的原则

单一职责原则(SRP):每个组件或模块应该具有单一的责任,降低耦合度,提高可维护性。
开闭原则(OCP):系统应对扩展开放,对修改关闭,通过接口和抽象来实现。
替换原则(LSP):子类应该能够替换其基类,而不会影响系统的正确性。
接口隔离原则(ISP):客户端不应该依赖于它不需要的接口,接口应该精简而专注。
依赖倒置原则(DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象
SOLID 原则是一套比较经典且流行的架构原则(主要还是名字起得好):
单一职责:与 Unix 哲学所倡导的“Do one thing and do it well”不谋而合;
开闭原则:用新增(扩展)来取代修改(破坏现有封装),这与函数式的 immutable 思想也有异曲同工之妙;
里式替换:父类能够出现的地方子类一定能够出现,这样它们之间才算是具备继承的“Is-A”关系;
接口隔离:不要让一个类依赖另一个类中用不到的接口,简单说就是最小化组件之间的接口依赖和耦合;
依赖反转:依赖抽象类与接口,而不是具体实现;让低层次模块依赖高层次模块的稳定抽象,实现解耦
此外,我们做架构设计时也会尽量遵循如下一些原则(与上述 SOLID 原则在本质上也是相通的):
正交性:架构同一层次拆分出的各组件之间,应该尽量保持正交,即彼此职责独立,边界清晰,没有重叠;
高内聚:同一组件内部应该是高度内聚的(cohesive),像是一个不可分割的整体(否则就应该拆开);
低耦合:不同组件之间应该尽量减少耦合(coupling),既降低相互的变化影响,也能增强组件可复用性;
隔离变化:许多架构原则与模式的本质都是在隔离变化 —— 将预期可能变化的部分都隔离到一块,减少发生变化时受影响(需要修改代码、重新测试或产生故障隐患)的其他稳定部分
https://github.com/leewaiho/Clean-Architecture-zh/tree/master?tab=readme-ov-file

互联网系统八大谬论


数学估算

延迟数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Latency Comparison Numbers
--------------------------
L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns 14x L1 cache
Mutex lock/unlock 25 ns
Main memory reference 100 ns 20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy 10,000 ns 10 us
Send 1 KB bytes over 1 Gbps network 10,000 ns 10 us
Read 4 KB randomly from SSD* 150,000 ns 150 us ~1GB/sec SSD
Read 1 MB sequentially from memory 250,000 ns 250 us
Round trip within same datacenter 500,000 ns 500 us
Read 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memory
Disk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtrip
Read 1 MB sequentially from 1 Gbps 10,000,000 ns 10,000 us 10 ms 40x memory, 10X SSD
Read 1 MB sequentially from disk 30,000,000 ns 30,000 us 30 ms 120x memory, 30X SSD
Send packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms

基于上述数字的指标:

  • 从磁盘以 30 MB/s 的速度顺序读取
  • 以 100 MB/s 从 1 Gbps 的以太网顺序读取
  • 从 SSD 以 1 GB/s 的速度读取
  • 以 4 GB/s 的速度从主存读取
  • 每秒能绕地球 6-7 圈
  • 数据中心内每秒有 2,000 次往返

traffic estimates


memory estimates


bandwidth estimates


storage estimates


系统设计核心概念

📌 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐊𝐞𝐲 𝐂𝐨𝐧𝐜𝐞𝐩𝐭𝐬

  • Scalability: lnkd.in/gpge_z76
  • CAP Theorem: lnkd.in/g3hmVamx
  • ACID Transactions: lnkd.in/gMe2JqaF
  • Consistent Hashing: lnkd.in/gd3eAQKA
  • Rate Limiting: lnkd.in/gWsTDR3m
  • API Design: lnkd.in/ghYzrr8q
  • Strong vs Eventual Consistency: lnkd.in/gJ-uXQXZ
  • Synchronous vs. asynchronous communications: lnkd.in/g4EqcckR
  • REST vs RPC: lnkd.in/gN__zcAB
  • Batch Processing vs Stream Processing: lnkd.in/gaAnP_fT
  • Fault Tolerance: lnkd.in/dVJ6n3wA
  • Consensus Algorithms: lnkd.in/ggc3tFbr
  • Gossip Protocol: lnkd.in/gfPMtrJZ
  • Service Discovery: lnkd.in/gjnrYkyF
  • Disaster Recovery: lnkd.in/g8rnr3V3
  • Distributed Tracing: lnkd.in/d6r5RdXG
  • Top 15 Tradeoffs: lnkd.in/gnM8QC-z

🛠️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐁𝐮𝐢𝐥𝐝𝐢𝐧𝐠 𝐁𝐥𝐨𝐜𝐤𝐬

  • Horizontal vs Vertical Scaling: lnkd.in/gAH2e9du
  • Databases: lnkd.in/gti8gjpz
  • Content Delivery Network (CDN): lnkd.in/gjJrEJeH
  • Domain Name System (DNS): lnkd.in/gkMcZW8V
  • Caching: lnkd.in/gC9piQbJ
  • Distributed Caching: lnkd.in/g7WKydNg
  • Load Balancing: lnkd.in/gQaa8sXK
  • SQL vs NoSQL: lnkd.in/g3WC_yxn
  • Database Indexes: lnkd.in/dGnZiNmM
  • HeartBeats: lnkd.in/gfb9-hpN
  • Circuit Breaker: lnkd.in/gCxyFzKm
  • Idempotency: lnkd.in/gPm6EtKJ
  • Database Scaling: lnkd.in/gAXpSyWQ
  • Data Replication: lnkd.in/gVAJxTpS
  • Data Redundancy: lnkd.in/gNN7TF7n
  • Database Sharding: lnkd.in/gRHb-67m
  • Failover: lnkd.in/dihZ-cEG
  • Proxy Server: lnkd.in/gi8KnKS6
  • Message Queues: lnkd.in/gTzY6uk8
  • WebSockets: lnkd.in/g76Gv2KQ
  • Bloom Filters: lnkd.in/dt4QbSUz
  • API Gateway: lnkd.in/gnsJGJaM
  • Distributed Locking: lnkd.in/gRxNJwWE
  • Checksum: lnkd.in/gCTa4DrS

🖇️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐀𝐫𝐜𝐡𝐢𝐭𝐞𝐜𝐭𝐮𝐫𝐚𝐥 𝐏𝐚𝐭𝐭𝐞𝐫𝐧𝐬

  • Client-Server Architecture: lnkd.in/dAARQYzq
  • Microservices Architecture: lnkd.in/gFXUrz_T
  • Serverless Architecture: lnkd.in/gQNAXKkb
  • Event-Driven Architecture: lnkd.in/dp8CPvey
  • Peer-to-Peer (P2P) Architecture: lnkd.in/di32HDu3

整体设计

软件架构模式(patterns)

Application Landscape Patterns

Application structure Patterns

User Interface Patterns

  • MVC
  • MVP

参考阅读:

架构 EA+4A

什么是架构 EA+4A

业务架构

应用架构

技术架构

数据架构

架构设计原则

扩展阅读

微服务架构

单体服务、微服务、Service Mesh


什么是服务治理

  • 单体服务(Monolithic Services):单体服务是指将整个应用程序作为一个单一的、紧密耦合的单元进行开发、部署和运行的架构模式。在单体服务中,应用程序的各个功能模块通常运行在同一个进程中,并共享相同的数据库和资源。单体服务的优点是开发简单、部署方便,但随着业务规模的增长,单体服务可能变得庞大且难以维护。

  • 微服务(Microservices):微服务是一种将应用程序拆分为一组小型、独立部署的服务的架构模式。每个微服务都专注于单个业务功能,并通过轻量级的通信机制(如RESTful API或消息队列)进行相互通信。微服务的优点是灵活性高、可扩展性好,每个微服务可以独立开发、测试、部署和扩展。然而,微服务架构也带来了分布式系统的复杂性和管理的挑战。

  • Service Mesh:Service Mesh是一种用于解决微服务架构中服务间通信和治理问题的基础设施层。它通过在服务之间插入一个专用的代理(称为Sidecar)来提供服务间的通信、安全性、可观察性和弹性的功能。Service Mesh可以提供流量管理、负载均衡、故障恢复、安全认证、监控和追踪等功能,而不需要在每个微服务中显式实现这些功能。常见的Service Mesh实现包括Istio、Linkerd和Consul Connect等。

微服务


gRPC 概述

与此讨论相关的话题是 微服务,可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。1例如,Pinterest 可能有这些微服务: 用户资料、关注者、Feed 流、搜索、照片上传等。

服务发现

ZooKeeper

  • ZooKeeper是一个开源的分布式协调服务,最初由雅虎开发并后来成为Apache软件基金会的顶级项目。
  • ZooKeeper提供了一个分布式的、高可用的、强一致性的数据存储服务。它的设计目标是为构建分布式系统提供可靠的协调机制。
  • ZooKeeper使用基于ZAB(ZooKeeper Atomic Broadcast)协议的一致性算法来保证数据的一致性和可靠性。
  • ZooKeeper提供了一个类似于文件系统的层次化命名空间(称为ZNode),可以存储和管理数据,并支持对数据的读写操作。
  • ZooKeeper还提供了一些特性,如临时节点、顺序节点和观察者机制,用于实现分布式锁、选举算法和事件通知等。

etcd

  • etcd是一个开源的分布式键值存储系统,由CoreOS开发并后来成为Cloud Native Computing Foundation(CNCF)的项目之一。

  • etcd被设计为一个高可用、可靠的分布式存储系统,用于存储和管理关键的配置数据和元数据。

  • etcd使用Raft一致性算法来保证数据的一致性和可靠性,Raft是一种强一致性的分布式共识算法。

  • etcd提供了一个简单的键值存储接口,可以存储和检索键值对数据,并支持对数据的原子更新操作。

  • etcd还提供了一些高级特性,如目录结构、事务操作和观察者机制,用于构建复杂的分布式系统和应用

  • Etcd

  • Zookeeper

  • Consul

  • grpc

Service Mesh


service Mesh 是怎么工作的

远程过程调用协议(RPC)


Source: Crack the system design interview

在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 ProtobufThriftAvro

RPC 是一个“请求-响应”协议:

  • 客户端程序 ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。
  • 客户端 stub 程序 ── 将请求过程的 id 和参数打包进请求信息中。
  • 客户端通信模块 ── 将信息从客户端发送至服务端。
  • 服务端通信模块 ── 将接受的包传给服务端存根程序。
  • 服务端 stub 程序 ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。

网络通讯协议

OSI 七层网络模型


资料来源:OSI 7层模型

常用的应用层协议

HTTP (Hypertext Transfer Protocol)

用途:主要用于Web浏览器和服务器之间的通信,是万维网的数据传输基础。
特点:无状态、请求-响应模式。
版本:HTTP/1.1, HTTP/2, HTTP/3

FTP (File Transfer Protocol)

用途:用于在客户端和服务器之间传输文件。
特点:支持文件上传和下载,支持匿名访问和身份验证。

邮件协议

  • SMTP (Simple Mail Transfer Protocol)
    用途:用于发送电子邮件。
    特点:主要用于邮件服务器之间的邮件传输。
  • POP3 (Post Office Protocol 3)
    用途:用于从邮件服务器下载邮件到本地客户端。
    特点:下载后邮件通常会从服务器删除。
  • IMAP (Internet Message Access Protocol)
    用途:用于从邮件服务器读取邮件。
    特点:支持在服务器上管理和存储邮件,客户端和服务器邮件同步

WebSocket

用途:提供全双工通信的协议,允许在客户端和服务器之间建立持久连接。
特点:低延迟、实时通信、减少HTTP请求开销。
为什么需要websocket

WebRTC (Web Real-Time Communication)

用途:用于实现浏览器和移动应用之间的实时音视频通信和数据共享。
特点:P2P通信、低延迟、高质量音视频传输。
webRTC

MQTT (Message Queuing Telemetry Transport)

用途:轻量级的发布/订阅消息传输协议,常用于物联网(IoT)设备之间的通信。
特点:低带宽、低能耗、可靠性高

超文本传输协议

  • aws http 选择介绍
  • HTTPS 是基于 HTTP 的安全版本,通过使用 SSL 或 TLS 加密和身份验证通信。
  • HTTP/1.1 是 HTTP 的第一个主要版本,引入了持久连接、管道化请求等特性。
  • HTTP/2 是 HTTP 的第二个主要版本,使用二进制协议,引入了多路复用、头部压缩、服务器推送等特性。
  • HTTP/3 是 HTTP 的第三个主要版本,基于 QUIC 协议,使用 UDP,提供更快的传输速度和更好的性能

域名/代理/负载均衡

域名系统

Amazon Route 53域名系统


Amazon Route 53 工作原理

### 域名解析的过程


来源:DNS 安全介绍

域名系统是把 www.example.com 等域名转换成 IP 地址。域名系统是分层次的,一些 DNS 服务器位于顶层。当查询(域名) IP 时,路由或 ISP 提供连接 DNS 服务器的信息。较底层的 DNS 服务器缓存映射,它可能会因为 DNS 传播延时而失效。DNS 结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于存活时间 TTL

  • A 记录(地址) ─ 指定域名对应的 IP 地址记录。
  • CNAME(规范) ─ 一个域名映射到另一个域名或 CNAME 记录( example.com 指向 www.example.com )或映射到一个 A 记录。
  • NS 记录(域名服务) ─ 指定解析域名或子域名的 DNS 服务器。
  • MX 记录(邮件交换) ─ 指定接收信息的邮件服务.

域名管理服务

常用命令

  • nslookup
  • dig

来源及延伸阅读

代理+负载均衡器

正向forward proxy

反向reverse proxy


#### 负载均衡器和反向代理



来源:可扩展的系统设计模式

负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:

  • 防止请求进入不好的服务器
  • 防止资源过载
  • 帮助消除单一的故障点
  • SSL 终结 ─ 解密传入的请求并加密服务器响应,这样的话后端服务器就不必再执行这些潜在高消耗运算了。
  • 不需要再每台服务器上安装 X.509 证书
  • Session 留存 ─ 如果 Web 应用程序不追踪会话,发出 cookie 并将特定客户端的请求路由到同一实例。
  • 通常会设置采用工作─备用双工作 模式的多个负载均衡器,以免发生故障。

负载均衡器能基于多种方式来路由流量:

四层负载均衡

四层负载均衡根据监看传输层的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行网络地址转换(NAT)来向上游服务器转发网络数据包。

七层负载均衡器

七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。

以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。

水平扩展

负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上垂直扩展更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。

缺陷:水平扩展

  • 水平扩展引入了复杂度并涉及服务器复制
  • 服务器应该是无状态的:它们也不该包含像 session 或资料图片等与用户关联的数据。
  • session 可以集中存储在数据库或持久化缓存(Redis、Memcached)的数据存储区中。
  • 缓存和数据库等下游服务器需要随着上游服务器进行扩展,以处理更多的并发连接。

缺陷:负载均衡器

  • 如果没有足够的资源配置或配置错误,负载均衡器会变成一个性能瓶颈。
  • 引入负载均衡器以帮助消除单点故障但导致了额外的复杂性。
  • 单个负载均衡器会导致单点故障,但配置多个负载均衡器会进一步增加复杂性。

反向代理(web 服务器)


资料来源:维基百科

反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。

带来的好处包括:

  • 增加安全性 - 隐藏后端服务器的信息,屏蔽黑名单中的 IP,限制每个客户端的连接数。
  • 提高可扩展性和灵活性 - 客户端只能看到反向代理服务器的 IP,这使你可以增减服务器或者修改它们的配置。
  • 本地终结 SSL 会话 - 解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作。免除了在每个服务器上安装 X.509 证书的需要
  • 压缩 - 压缩服务器响应
  • 缓存 - 直接返回命中的缓存结果
  • 静态内容 - 直接提供静态内容
    • HTML/CSS/JS
    • 图片
    • 视频
    • 等等

负载均衡器与反向代理

  • 当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上。
  • 即使只有一台 web 服务器或者应用服务器时,反向代理也有用,可以参考上一节介绍的好处。
  • NGINX 和 HAProxy 等解决方案可以同时支持第七层反向代理和负载均衡。

不利之处:反向代理

  • 引入反向代理会增加系统的复杂度。
  • 单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如故障转移)会进一步增加复杂度。

来源及延伸阅读

应用层web网关


百亿规模API网关服务Shepherd的设计与实现

将 Web 服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的 API 只需要添加应用服务器,而不必添加额外的 web 服务器。用于完成基础的:

API 设计规范和管理

API 架构风格

  • RESTful API
  • GraphQL
  • RPC
  • SOA

RESTful API

  • 路径名称避免动词

    1
    2
    3
    4
    5
    路径名称避免动词
    # Good
    curl -X GET /orders
    # Bad
    curl -X GET /getOrders
  • GET 获取指定 URI 的资源信息

    1
    2
    3
    4
    5
    6
    7
    # 代表获取当前系统的所有订单信息
    curl -X GET /orders

    curl -X GET /users/{user_id}/orders

    # 代表获取指定订单编号为订单详情信息
    curl -X GET /orders/{order_id}
  • POST 通过指定的 URI 创建资源

    1
    2
    curl -X POST /orders \
    -d '{"name": "awesome", region: "A"}' \
  • PUT 创建或全量替换指定 URI 上的资源

    1
    2
    curl -X PUT http://httpbin.org/orders/1 \
    -d '{"name": "new awesome", region: "B"}' \
  • PATCH 执行一个资源的部分更新

    1
    2
    3
    4
    5
    # 代表将 id 为 1 的 order 中的 region 字段进行更改,其他数据保持不变
    curl -X PATCH /orders/{order_id} \
    -d '{name: "nameB"}' \
    curl -X order/{order_id}/name (用来重命名)
    curl -X /order/{order_id}/status(用来更改用户状态)
  • DELETE 通过指定的 URI 移除资源

    1
    2
    # 代表将id的 order 删除
    curl -X DELETE /orders/{order_id}

其它规则:
规则1:应使用连字符( - )来提高URI的可读性
规则2:不得在URI中使用下划线(_)
规则3:URI路径中全都使用小写字母

API 错误码设计规范

  1. 不论请求成功或失败,始终返回 200 http status code,在 HTTP Body 中包含用户账号没有找到的错误信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
如: Facebook API 的错误 Code 设计,始终返回 200 http status code:
{
"error": {
"message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\" at character 23: id,name,picture,picture",
"type": "OAuthException",
"code": 2500,
"fbtrace_id": "xxxxxxxxxxx"
}
}

缺点:
对于每一次请求,我们都要去解析 HTTP Body,从中解析出错误码和错误信息

  1. 返回 http 404 Not Found 错误码,并在 Body 中返回简单的错误信息:
1
2
3
4
5
如: Twitter API 的错误设计
根据错误类型,返回合适的 HTTP Code,并在 Body 中返回错误信息和自定义业务 Code

HTTP/1.1 400 Bad Request
{"errors":[{"code":215,"message":"Bad Authentication data."}]}
  1. 返回 http 404 Not Found 错误码,并在 Body 中返回详细的错误信息:
1
2
3
4
5
6
7
如: 微软 Bing API 的错误设计,会根据错误类型,返回合适的 HTTP Code,并在 Body 中返回详尽的错误信息
HTTP/1.1 400
{
"code": 100101,
"message": "Database error",
"reference": "https://github.com/xx/tree/master/docs/guide/faq/xxxx"
}
  1. 业务 Code 码设计
  • 纯数字表示
  • 不同部位代表不同的服务
  • 不同的模块(品类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如: 错误代码说明:100101
10: 服务
01: 某个服务下的某个模块
01: 模块下的错误码序号,每个模块可以注册 100 个错误
建议 http status code 不要太多:

200 - 表示请求成功执行
400 - 表示客户端出问题
500 - 表示服务端出问题

如果觉得这 3 个错误码不够用,可以加如下 3 个错误码:
401 - 表示认证失败
403 - 表示授权失败
404 - 表示资源找不到,这里的资源可以是 URL 或者 RESTful 资源

接口幂等性设计

幂等性的重要性

  • 提高可靠性:在网络不稳定的情况下,客户端可能会重试请求。幂等性确保重复请求不会导致意外的副作用。
  • 简化客户端代码:客户端不需要担心重复请求的副作用,从而简化了错误处理逻辑。
  • 改善用户体验:确保用户操作的可预测性,避免因重复提交表单等操作导致的错误或重复数据。

怎么实现幂等性

  • 幂等键(Idempotency Key: 由客户端生成一个唯一标识请求的ID,并在请求头中包含此ID。服务器端会检查此ID是否已处理过,如果是,则返回之前的响应。
  • 幂等令牌(Idempotency Token):在需要创建资源的请求中,通过幂等令牌保证幂等性。服务器端生成并验证令牌,确保同一令牌只能创建一个资源

中间件和存储

如何选择存储组件

内容分发网络(CDN)


来源:为什么使用 CDN

内容分发网络(CDN)是一个全球性的代理服务器分布式网络,它从靠近用户的位置提供内容。通常,HTML/CSS/JS,图片和视频等静态内容由 CDN 提供,虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。

将内容存储在 CDN 上可以从两个方面来提供性能:

  • 从靠近用户的数据中心提供资源
  • 通过 CDN 你的服务器不必真的处理请求

CDN 推送(push)

当你服务器上内容发生变动时,推送 CDN 接受新内容。直接推送给 CDN 并重写 URL 地址以指向你的内容的 CDN 地址。你可以配置内容到期时间及何时更新。内容只有在更改或新增是才推送,流量最小化,但储存最大化。

CDN 拉取(pull)

CDN 拉取是当第一个用户请求该资源时,从服务器上拉取资源。你将内容留在自己的服务器上并重写 URL 指向 CDN 地址。直到内容被缓存在 CDN 上为止,这样请求只会更慢,

存活时间(TTL)决定缓存多久时间。CDN 拉取方式最小化 CDN 上的储存空间,但如果过期文件并在实际更改之前被拉取,则会导致冗余的流量。

高流量站点使用 CDN 拉取效果不错,因为只有最近请求的内容保存在 CDN 中,流量才能更平衡地分散。

缺陷:CDN

  • CDN 成本可能因流量而异,可能在权衡之后你将不会使用 CDN。
  • 如果在 TTL 过期之前更新内容,CDN 缓存内容可能会过时。
  • CDN 需要更改静态内容的 URL 地址以指向 CDN。

来源及延伸阅读

mysql 数据库


资料来源:扩展你的用户数到第一个一千万

延伸思考和学习

  • 如何正确建表。类型选择、主键约束、not null、编码方式等
  • 外建约束、还是业务约束
  • mysql join 还是业务关联等
  • 如何使用index优化查询
  • 如何使用事物acid
  • DDL注意事项
  • 是否需呀分库分表
  • 历史数据如何处理
  • 如何扩展mysql?垂直分、水平分、主备复制、主主复制
  • 性能调优?架构优化、索引优化、sql优化、连接池优化、缓存优化

redis 键值存储系统

延伸思考和学习

  • redis 五种数据结构
  • redis 使用场景。缓存数据、计数器和限流、分布式锁、bloomfilter等
  • redis key 过期时间
  • redis 存储数据一致性的容忍度
  • redis 扩展和分不少方案
  • redis 热key和大key问题

文档类型存储(es)

延伸思考和学习

  • ES index 的mapping结构
  • setting 分片和副本机制
  • 分词器
  • 检索query dsl
  • 读写流程
  • 集群架构和规划
  • 读写优化

列型存储(hbase)


资料来源: SQL 和 NoSQL,一个简短的历史

抽象模型:嵌套的 ColumnFamily<RowKey, Columns<ColKey, Value, Timestamp>> 映射

类型存储的基本数据单元是列(名/值对)。列可以在列族(类似于 SQL 的数据表)中被分组。超级列族再分组普通列族。你可以使用行键独立访问每一列,具有相同行键值的列组成一行。每个值都包含版本的时间戳用于解决版本冲突。

Google 发布了第一个列型存储数据库 Bigtable,它影响了 Hadoop 生态系统中活跃的开源数据库 HBase 和 Facebook 的 Cassandra。像 BigTable,HBase 和 Cassandra 这样的存储系统将键以字母顺序存储,可以高效地读取键列。

列型存储具备高可用性和高可扩展性。通常被用于大数据相关存储。

来源及延伸阅读:列型存储

图数据库


资料来源:图数据库

抽象模型: 图

在图数据库中,一个节点对应一条记录,一个弧对应两个节点之间的关系。图数据库被优化用于表示外键繁多的复杂关系或多对多关系。

图数据库为存储复杂关系的数据模型,如社交网络,提供了很高的性能。它们相对较新,尚未广泛应用,查找开发工具或者资源相对较难。许多图只能通过 REST API 访问。

相关资源和延伸阅读:图

来源及延伸阅读:NoSQL

SQL 还是 NoSQL


资料来源:从 RDBMS 转换到 NoSQL

选取 SQL 的原因:

  • 结构化数据
  • 严格的模式
  • 关系型数据
  • 需要复杂的联结操作
  • 事务
  • 清晰的扩展模式
  • 既有资源更丰富:开发者、社区、代码库、工具等
  • 通过索引进行查询非常快

选取 NoSQL 的原因:

  • 半结构化数据
  • 动态或灵活的模式
  • 非关系型数据
  • 不需要复杂的联结操作
  • 存储 TB (甚至 PB)级别的数据
  • 高数据密集的工作负载
  • IOPS 高吞吐量

适合 NoSQL 的示例数据:

  • 埋点数据和日志数据
  • 排行榜或者得分数据
  • 临时数据,如购物车
  • 频繁访问的(“热”)表
  • 元数据/查找表

来源及延伸阅读:SQL 或 NoSQL

缓存redis


资料来源:可扩展的系统设计模式

缓存可以提高页面加载速度,并可以减少服务器和数据库的负载。在这个模型中,分发器先查看请求之前是否被响应过,如果有则将之前的结果直接返回,来省掉真正的处理。

数据库分片均匀分布的读取是最好的。但是热门数据会让读取分布不均匀,这样就会造成瓶颈,如果在数据库前加个缓存,就会抹平不均匀的负载和突发流量对数据库的影响。

  • 客户端缓存
    缓存可以位于客户端(操作系统或者浏览器),服务端或者不同的缓存层。
  • CDN 缓存,CDN 也被视为一种缓存。
  • Web 服务器缓存
    反向代理和缓存(比如 Varnish)可以直接提供静态和动态内容。Web 服务器同样也可以缓存请求,返回相应结果而不必连接应用服务器。
  • 应用服务缓存(本地缓存)
  • 缓存服务器(remote cache)
  • 数据库本身的缓存

延伸思考和学习

  • 本地缓存、分布式缓存
  • 缓存的TTL
  • 缓存的安全性
  • 缓存的更新模式

异步与队列


资料来源:可缩放系统构架介绍

异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。

消息队列

消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列:

  • 应用程序将作业发布到队列,然后通知用户作业状态
  • 一个 worker 从队列中取出该作业,对其进行处理,然后显示该作业完成
    不去阻塞用户操作,作业在后台处理。在此期间,客户端可能会进行一些处理使得看上去像是任务已经完成了。例如,如果要发送一条推文,推文可能会马上出现在你的时间线上,但是可能需要一些时间才能将你的推文推送到你的所有关注者那里去。
  • kafka 是一个令人满意的简单的消息代理,但是消息有可能会丢失。
  • RabbitMQ 很受欢迎但是要求你适应「AMQP」协议并且管理你自己的节点。
  • Apache Pulsar Pulsar是一个开源的、可扩展的消息队列和流处理平台。它具有高吞吐量、低延迟和可持久化的特点,支持多租户、多数据中心和多协议等功能

任务队列 (xxl-job)


资料来源:xxl-job系统构架介绍

  • 单点调度:https://github.com/robfig/cron
  • 分布式调度:https://github.com/xuxueli/xxl-job
    将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性
  • 调度模块(调度中心):
    负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块;
    支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover。
  • 执行模块(执行器,executor):
    负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;
    接收“调度中心”的执行请求、终止请求和日志请求等

参考:

如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。背压可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者 HTTP 503 状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是指数退避

延时任务调度


资料来源:lmstfy github

延时任务场景

  • 延时处理:有时候需要在某个事件发生后的一段时间内执行任务。例如,当用户提交订单后,可以设置一个延时任务,在一段时间后检查是否是支付
  • 提醒和通知:延时任务调度可用于发送提醒和通知。例如,你可以设置一个延时任务,在用户注册后的24小时内发送一封欢迎邮件,或在用户下单后的一段时间内发送订单确认通知。
  • 缓存刷新:延时任务调度可用于刷新缓存数据。当缓存过期时,可以设置一个延时任务,在一定的延时时间后重新加载缓存数据,以保持数据的新鲜性
  • 任务队列跟消息队列在使用场景上最大的区别是: 任务之间是没有顺序约束而消息要求顺序(FIFO),且可能会对任务的状态更新而消息一般只会消费不会更新。 类似 Kafka 利用消息 FIFO 和不需要更新(不需要对消息做索引)的特性来设计消息存储,将消息读写变成磁盘的顺序读写来实现比较好的性能。而任务队列需要能够任务状态进行更新则需要对每个消息进行索引,如果把两者放到一起实现则很难实现在功能和性能上兼得。比如一下场景:
  • 定时任务,如每天早上 8 点开始推送消息,定期删除过期数据等
  • 任务流,如自动创建 Redis 流程由资源创建,资源配置,DNS 修改等部分组成,使用任务队列可以简化整体的设计和重试流程
  • 重试任务,典型场景如离线图片处理

可用组件

  • redis 包括有序集合(Sorted Set)你可以使用Redis的有序集合来实现延时任务队列。将任务的执行时间作为分数(score),任务的内容作为成员(member),将任务按照执行时间排序。通过定期轮询有序集合,检查是否有任务的执行时间到达,然后执行相应的任务
  • https://github.com/bitleak/lmstfy

框架和引擎

工作流引擎与任务编排

https://github.com/s8sg/goflow
https://github.com/go-workflow/go-workflow

规则引擎与风控、资损、校验

https://github.com/bilibili/gengine

脚本执行引擎与低代码平台

https://github.com/d5/tengo
https://github.com/mattn/anko

好用的规范和工具

规范:

  • Go编码规范
  • api 设计规范
  • git 使用规范

工具:

云原生和服务部署CI/CD

大数据存储和计算

系统稳定性建设

影响系统可用性的因素

在系统可以用性可以做哪些工作

架构上设计 (拆分/解偶/资源隔离)

  • 支持异地多活(DR集群)
  • 服务支持横向扩容,扩容时注意事项(mysql,redis,kafka,es,依赖方,监控)
  • 离线和在线分离
  • mysql分库,分表、kafka topic、不同的ES集群
  • 辑架构和物理架构分离,订单系统支持根据业务类型路由

系统保护

  • 限流
  • 熔断降级(核心功能报错,非核心功能返回空或者固定内容)

技术选型,组件本身的可用性保证和容量评估

  • 适用性
  • 优缺点
  • 产品口碑
  • 社区活跃度
  • 实战案例
  • 扩展性等多个方面进行全量评估
  • 容量评估,mysql 一写多读,codis,kafka,ES
  • 灾备,快速恢复

功能设计时考虑

  • 接口维度的限流、用户维度限流
  • 避免单点:比如在主页设计时,主页配置数据需要写在多个redis中
  • 核心功能降级策略:redis→cdn

变更和服务扩容发布流程

  • 新版本发布兼容,数据准备,变更流程,服务发布顺序
  • 扩容时注意事项(mysql,redis,kafka,es,依赖方,监控)
  • DB变更、配置变更、组件变更

可观测性&告警

  • metric & log & trace
  • 监控体系和告警指标
  • SLA和NOC指标

可观测性、监控和告警

  • 业务层的监控,例如NOC核心指标监控,登陆、首页流量、成功率、PDP流量、成功率、下单流量、成功率、支付数量、成功率等
  • 网关的监控。所有接口的流量、成功率、耗时95线,限流监控
  • 接口详情监控:可以筛选出每个接口流量、成功率、具体错误吗、耗时均线、95线等
  • 核心功能监控:缓存命中率、变价率、数据一致性监控
  • 中间件外部组件监控:mysql、redis、kafka、es,容器资源监控等
  • 外部依赖监控:支付团队,履约团队、供应商依赖服务监控

如何搭建监控和日志系统

应该知道的安全问题

这一部分需要更多内容。一起来吧
安全是一个宽泛的话题。除非你有相当的经验、安全方面背景或者正在申请的职位要求安全知识,你不需要了解安全基础知识以外的内容:

  • 在运输和等待过程中加密
  • 对所有的用户输入和从用户那里发来的参数进行处理以防止

参考:

系统设计实践

参考:

k8s 网络

linux 虚拟网络 veth pair 和 bridge

  • Network namespace 实现网络隔离
  • Veth pair提供了一种连接两个network namespace的方法
  • Bridge 实现同一网络中多个namespace的连接
  • 添加路由信息,查看路由信息
  • iptabels 和 NAT
  • 实战练习
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
sudo ip netns add ns1
sudo ip netns add ns2
sudo ip netns add ns3

sudo brctl addbr virtual-bridge

sudo ip link add veth-ns1 type veth peer name veth-ns1-br
sudo ip link set veth-ns1 netns ns1
sudo brctl addif virtual-bridge veth-ns1-br

sudo ip link add veth-ns2 type veth peer name veth-ns2-br
sudo ip link set veth-ns2 netns ns2
sudo brctl addif virtual-bridge veth-ns2-br

sudo ip link add veth-ns3 type veth peer name veth-ns3-br
sudo ip link set veth-ns3 netns ns3
sudo brctl addif virtual-bridge veth-ns3-br


sudo ip -n ns1 addr add local 192.168.1.1/24 dev veth-ns1
sudo ip -n ns2 addr add local 192.168.1.2/24 dev veth-ns2
sudo ip -n ns3 addr add local 192.168.1.3/24 dev veth-ns3

sudo ip link set virtual-bridge up
sudo ip link set veth-ns1-br up
sudo ip link set veth-ns2-br up
sudo ip link set veth-ns3-br up
sudo ip -n ns1 link set veth-ns1 up
sudo ip -n ns2 link set veth-ns2 up
sudo ip -n ns3 link set veth-ns3 up

sudo ip netns delete ns1
sudo ip netns delete ns2
sudo ip netns delete ns3
sudo ip link set virtual-bridge down
sudo brctl delbr virtual-bridge

$ sudo ip netns exec ns1 ping 192.168.1.2
PING 192.168.1.2 (192.168.1.2): 56 data bytes
64 bytes from 192.168.1.2: seq=0 ttl=64 time=0.068 ms
--- 192.168.1.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.060/0.064/0.068 ms
$ sudo ip netns exec ns1 ping 192.168.1.3
PING 192.168.1.3 (192.168.1.3): 56 data bytes
64 bytes from 192.168.1.3: seq=0 ttl=64 time=0.055 ms
--- 192.168.1.3 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.055/0.378/1.016 ms

docker 网络 和 docker0

  • docker0网桥和缺省路由
  • docker0
  • route
  • iptables 和 nat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 查看网桥
$ brctl show
bridge name bridge id STP enabled interfaces
docker0 8000.02421557ce52 no veth91e1730
vethc858a6a
# 查看docker 网络
docker network inspect bridge

# 查看container route信息
# 目的地址为172.17的网络不走route,其它走默认的172.17.0.1 route
$ docker exec busybox1 route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.17.0.1 0.0.0.0 UG 0 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0

# 查看iptables
# 出口不为0docker的流量都使用SNAT
$ sudo iptables -t nat -S | grep docker
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
-A DOCKER -i docker0 -j RETURN

pod 网络

pause

1
2
3
4
5
6
7
8
$ docker ps | grep etcd
8fd1337b0bf2 73deb9a3f702 "etcd --advertise-cl…" 3 hours ago Up 3 hours k8s_etcd_etcd-minikube_kube-system_94aa022caf543792dfcddf4a2ca05a30_0
1202ef34af2b registry.k8s.io/pause:3.9 "/pause" 3 hours ago Up 3 hours k8s_POD_etcd-minikube_kube-system_94aa022caf543792dfcddf4a2ca05a30_0

$ docker inspect 8fd1337b0bf2 | grep -i networkMode
$ docker inspect 8fd1337b0bf2 | grep -i networkMode
"NetworkMode": "container:1202ef34af2b155e938cbe770870ba6c8edd3a57c88545a697816c340a6ce320",

CNI 标准和插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ls -l /opt/cni/bin/
-rwxr-xr-x 1 root root 2660408 Nov 7 2023 bandwidth
-rwxr-xr-x 1 root root 3018552 Nov 7 2023 bridge
-rwxr-xr-x 1 root root 1984728 Nov 7 2023 cnitool
-rwxr-xr-x 1 root root 7432152 Nov 7 2023 dhcp
-rwxr-xr-x 1 root root 3096120 Nov 7 2023 firewall
-rwxr-xr-x 1 root root 2250104 Nov 7 2023 host-local
-rwxr-xr-x 1 root root 2775128 Nov 7 2023 ipvlan
-rwxr-xr-x 1 root root 2305848 Nov 7 2023 loopback
-rwxr-xr-x 1 root root 2799704 Nov 7 2023 macvlan
-rwxr-xr-x 1 root root 2615256 Nov 7 2023 portmap
-rwxr-xr-x 1 root root 2891096 Nov 7 2023 ptp
-rwxr-xr-x 1 root root 2367288 Nov 7 2023 tuning
-rwxr-xr-x 1 root root 2771032 Nov 7 2023 vlan

service 网络

背景

  • Zookeeper提供名字服务,pod自身实现负载均衡,RPC框架实现负载均衡
  • Service 为 Pods 提供的固定 IP,其他服务可以通过 Service IP 找到提供服务的Endpoints。
  • Service提供负载均衡。Service 由多个 Endpoints 组成,kubernetes 对组成 Service 的 Pods 提供的负载均衡方案,例如随机访问、robin 轮询等。
  • 暂时将Pod等同于Endpoint


实现原理

  • Service IP IP 由API server分配,写入etcd
  • Etcd 中存储service和endpoints
  • Controllermanager watch etcd的变换生成endpoints
  • node 中的kube-proxy watch service 和 endpoints的变化


kube-proxy 服务发现和负载均衡

  • Order -> item 的流程
  • 服务发现:环境变量和DNS
  • servicename.namespace.svc.cluster.local
  • kub-proxy 通过watch etcd中service和endpoint的变更,维护本地的iptables/ipvs
  • kub-proxy 通过转发规则实现service ip 到 pod ip的转发,通过规则实现负载均衡


service 类型

  • ClusterIP
  • NodePort
  • LoadBalancer

ingress 网络

背景

  • 集群外部访问集群内部资源?nodeport,loadbalancer。一个服务一个port或者一个外网IP,一个域名
  • Ingress 是 Kubernetes 中的一种 API 对象,用于管理入站网络流量,基于域名和URL路径把用户的请求转发到对应的service
  • ingress相当于七层负载均衡器,是k8s对反向代理的抽象
  • ingress负载均衡,将请求自动负载到后端的pod


实现原理

  • ingress 资源对象用于编写资源配置规则
  • Ingress-controller 监听apiserver感知集群中service和pod的变化动态更新配置规则,并重载proxy反向代理的配置
  • proxy反向代理负载均衡器,例如ngnix,接收并按照ingress定义的规则进行转发,常用的是ingress-nginx等,直接转发到pod中


支持的路由方式

  • 通过使用路径规则。例如: /app1 路径映射到一个服务,将 /app2 路径映射到另一个服务。路径匹配支持精确匹配和前缀匹配两种方式。
  • 基于主机的路由匹配。例如,可以将 app1.example.com 主机名映射到一个服务,将 app2.example.com 主机名映射到另一个服务。主机匹配也可以与路径匹配结合使用,实现更细粒度的路由控制。
  • 其他条件的路由匹配::请求方法(如 GET、POST)、请求头(如 Content-Type)、查询参数等。

docker k8s 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# minikube
minikube start
minikube status
minikube ssh

# docker
docker ps # 查看所有正在运行的容器
docker ps -a # 查看所有的容器,包括正在运行的和停止的

# 用交互式的方式启动容器
docker start -ai <容器名或容器ID>

# 打开容器进行交互式终端对话框
docker exec -it <容器名或容器ID> bash

# 容器中执行命令
docker exec <容器名或容器ID> ls

参考资料

Go 和 C++ 语言对比

Go and C++ are two different programming languages with different design goals, syntax, and feature sets. Here’s a brief comparison of the two:

Syntax: Go has a simpler syntax than C++. It uses indentation for block structure and has fewer keywords and symbols. C++ has a more complex syntax with a lot of features that can make it harder to learn and use effectively.

Memory Management: C++ gives the programmer more control over memory management through its support for pointers, manual memory allocation, and deallocation. Go, on the other hand, uses a garbage collector to automatically manage memory, making it less error-prone.

Concurrency: Go has built-in support for concurrency through goroutines and channels, which make it easier to write concurrent code. C++ has a thread library that can be used to write concurrent code, but it requires more manual management of threads and locks.

Performance: C++ is often considered a high-performance language, and it can be used for system-level programming and performance-critical applications. Go is also fast but may not be as fast as C++ in some cases.

Libraries and Frameworks: C++ has a vast ecosystem of libraries and frameworks that can be used for a variety of applications, from game development to machine learning. Go’s ecosystem is smaller, but it has good support for web development and distributed systems.

Overall, the choice of programming language depends on the project requirements, the available resources, and the developer’s expertise. Both Go and C++ have their strengths and weaknesses, and the best choice depends on the specific needs of the project.

Go语法介绍

string/[]byte

  • string是golang的基本数组类型,s := “hello,world”,一旦初始化后不允许修改其内容
  • 内部实现结构,指向数据的指针data和表示长度的len
  • 字符串拼接和格式化四种方式,+=,strings.join,buffer.writestring,fmt.sprintf
  • string 与 []byte的类型转换
  • 标准库strings提供了许多字符串操作的函数,例如Split、HasPrefix,Trim。

array

  • 数组array: [3]int{1,2,3}
  • 数组是值类型,数组传参发生拷贝
  • 定长
  • 数组的创建、初始化、访问和遍历range,len(arr)求数组的长度

slice

  • 切片slice初始化: make([]int,len,cap)
  • slice是引用类型
  • 变长,用容量和长度的区别,分别使用cap和len函数获取
  • 内存结构和实现:指针、cap、size共24字节
  • 常用函数,append,cap,len
  • 切片动态扩容
  • 深拷贝copy和浅拷贝“=”的区别
  • copy(slice1,slice2)

map

sync.map

struct

  • 空结构体struct{}的用途,节省内存。
  • 不支持继承,使用结构体嵌套组合
  • struct 可以比较吗?普通struct可以比较,带引用的struc不可比较,需要使用reflect.DeepEqual
  • struct没有slice和map类型时可直接判断
  • slice和map本身不可比较,需要使用reflect.DeepEqual()。
  • struct中包含slice和map等字段时,也要使用reflect.DeepEqual().
  • https://stackoverflow.com/questions/24534072/how-to-compare-struct-slice-map-are-equal

interface

  • https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/
  • 隐式接口,实现接口的所有方法就隐式地实现了接口;不需要显示申明实现某接口
  • 接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}:
  • interface{} 类型不是任意类型,而是将类型转换成了 interface{} 类型
  • 结构体实现接口 vs 结构体指针实现接口 区别?
  • runtime.eface 和 runtime.iface 结构?
  • 结构体类型转化为接口的类型相互变换,interface类型断言为struct类型 过程
  • 动态派发与多态。动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性6。Go 语言虽然不是严格意义上的面向对象语言,但是接口的引入为它带来了动态派发这一特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。
  • Golang没有泛型,通过interface可以实现简单泛型编程,例如的sort的实现
  • 接口实现的源码

channel

  • Go鼓励CSP模型(communicating sequential processes),Goroutin之间通过channel传递数据
  • 非缓冲的同步channel和带缓冲的异步channel
  • 内部实现结构,带锁的循环队列runtime.hchan
  • channel创建make
  • chan <- i
  • 向channel发送数据。在发送数据的逻辑执行之前会先为当前 Channel 加锁,防止多个线程并发修改数据。如果 Channel 已经关闭,那么向该 Channel 发送数据时会报 “send on closed channel” 错误并中止程序。分为的三个部分:
    当存在等待的接收者时,通过 runtime.send 直接将数据发送给阻塞的接收者;
    当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
    当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据;
  • i <- ch,i, ok <- ch
  • 从channel接收数据的五种情况:
    • 如果 Channel 为空,那么会直接调用 runtime.gopark 挂起当前 Goroutine;
    • 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 会直接返回;
    • 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区;
    • 如果 Channel 的缓冲区中包含数据,那么直接读取 recvx 索引对应的数据;
    • 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒;
  • 关闭channel
  • 如何优雅的关闭channel?https://www.jianshu.com/p/d24dfbb33781, channel关闭后读操作会发生什么?写操作会发生什么?

类型和拷贝方式

  • 值类型 :String,Array,Int,Struct,Float,Bool,pointer(深拷贝)
  • 引用类型:Slice,Map (浅拷贝)

函数和方法,匿名函数

  • init函数
  • 值接收和指针接收的区别
  • 匿名函数?闭包?闭包延时绑定问题?用闭包写fibonacci数列?

指针和unsafe.Pointer

  • 相比C/C++,为了安全性考虑,Go指针弱化。不同类型的指针不能相互转化,指针变量不支持运算,不支持c/c++中的++,需要借助unsafe包
  • 任何类型的指针都可以被转换成unsafe.Pointer类型,通过unsafe.Pointer实现不同类型指针的转化
  • uintptr值可以被转换成unsafe.Pointer类型,通过uintptr实现指针的运算
  • unsafe.Pointer是一个指针类型,指向的值不能被解析,类似于C/C++里面的(void *),只说明这是一个指针,但是指向什么的不知道。
  • uintptr 是一个整数类型,这个整数的宽度足以用来存储一个指针类型数据;那既然是整数类类型,当然就可以对其进行运算了
  • nil
  • 实践string和[]byte的高效转换
  • 在业务场景中,使用指针虽然方便,但是要注意深拷贝和浅拷贝,这种错误还是比较常见的
  • 当你对象是结构体对象的指针时,你想要获取字段属性时,可以直接使用’.’,而不需要解引用

集合set

  1. golang中本身没有提供set,但可以通过map自己实现
  2. 利用map键值不可重复的特性实现set,value为空结构体。 map[interface{}]struct{}
  3. 如何自己实现set?

defer

  • defer定义的延迟函数参数在defer语句出时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作,执行过程是: 保存返回值(若有)–>执行defer(若有)–>执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯
  • golang中的defer用途?调用时机?调用顺序?预计算值?
  • defer 实现原理?

Go 错误处理 error、panic

  • 在Go 语言中,错误被认为是一种可以预期的结果;而异常则是一种非预期的结果,发生异常可能表示程序中存在BUG 或发生了其它不可控的问题。
  • Go 语言推荐使用 recover 函数将内部异常转为错误处理,这使得用户可以真正的关心业务相关的错误处理。
  • 在Go服务中通常需要自定义粗错误类型,最好能有效区分业务逻辑错误和系统错误,同时需要捕获panic,将panic转化为error,避免某个错误影响server重启
  • panic 时需要保留runtime stack
    1
    2
    3
    4
    5
    6
    7
    8
    9
     defer func() {
    if x := recover(); x != nil {
    panicReason := fmt.Sprintf("I'm panic because of: %v\n", x)
    logger.LogError(panicReason)
    stk := make([]byte, 10240)
    stkLen := runtime.Stack(stk, false)
    logger.LogErrorf("%s\n", string(stk[:stkLen]))
    }
    }()

Go channel通道

channel

channel是golang中的csp并发模型非常重要组成部分,使用起来非常像阻塞队列。

  • 通道channel变量本身就是指针,可用“==”操作符判断是否为同一对象
  • 未初始化的channel为nil,需要使用make初始化
  • 理解初始化的channel和nil channel的区别?读写nil channel都会阻塞,关闭nil channel会出现panic;可以读关闭的channel,写关闭的channel会发出panic,close关闭了的channel会发出panic
  • 同步模式的channel必须有配对操作的goroutine出现,否则会一直阻塞,而异步模式在缓冲区未满或者数据未读完前,不会阻塞。
  • 内置的cap和len函数返回channel缓冲区大小和当前已缓冲的数量,而对于同步通道则返回0
  • 除了使用”<-“发送和接收操作符外,还可以用ok-idom或者range模式处理chanel中的数据。
  • 重复关闭和关闭nil channel都会导致pannic
  • make可以创建单项通道,但那没有意义,通产使用类型转换来获取单向通道,并分别赋予给操作方
  • 无法将单向通道转换成双向通道

基本用法

  1. 协程之间传递数据
  2. 用作事件通知,经常使用空结构体channel作为某个事件通知
  3. select帮助同时多个通道channel,它会随机选择一个可用的通道做收发操作
  4. 使用异步channel(带有缓冲)实现信号量semaphore
  5. 标准库提供了timeout和tick的channel实现。
  6. 通道并非用来取代锁的,通道和锁有各自不同的使用场景,通道倾向于解决逻辑层次的并发处理架构,而锁则用来保护数据的安全性。
  7. channel队列本质上还是使用锁同步机制,单次获取更多的数据(批处理),减少收发的次数,可改善因为频繁加锁造成的性能问题。
  8. channel可能会导致goroutine leak问题,是指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
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    func main() {
    done := make(chan struct{})
    s := make(chan int)
    go func() {
    s <- 1
    close(done)
    }()
    fmt.Println(<-s)
    <-done
    }

    func main() {
    sem := make(chan struct{}, 2) //two groutine
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
    defer wg.Done()
    defer func() { <-sem }()
    sem <- struct{}{}
    time.Sleep(1 * time.Second)
    fmt.Println("id=", id)
    }(i)
    }
    wg.Wait()
    }


    func main() {
    go func() {
    tick := time.Tick(1 * time.Second)
    for {
    select {
    case <-time.After(5 * time.Second):
    fmt.Println("time out")
    case <-tick:
    fmt.Println("time tick 1s")
    default:
    fmt.Println("default")
    }
    }
    }()
    <-(chan struct{})(nil)
    }

Go并发模型 (Goroutine/channel/GMP)

what’s CSP?

The Communicating Sequential Processes (CSP) model is a theoretical model of concurrent programming that was first introduced by Tony Hoare in 1978. The CSP model is based on the idea of concurrent processes that communicate with each other by sending and receiving messages through channels.The Go programming language provides support for the CSP model through its built-in concurrency features, such as goroutines and channels. In Go, concurrent processes are represented by goroutines, which are lightweight threads of execution. The communication between goroutines is achieved through channels, which provide a mechanism for passing values between goroutines in a safe and synchronized manner.

Which is Goroutine ?

  • Goroutines are lightweight, user-level threads of execution that run concurrently with other goroutines within the same process.
  • Unlike traditional threads, goroutines are managed by the Go runtime, which automatically schedules and balances their execution across multiple CPUs and makes efficient use of available system resources.

比较Goroutine、thread、process

  • 比较进程、线程和Goroutine。进程是资源分配的单位,有独立的地址空间,线程是操作系统调度的单位,协程是更细力度的执行单元,需要程序自身调度。Go语言原生支持Goroutine,并提供高效的协程调度模型。
  • Goroutines, threads, and processes are all mechanisms for writing concurrent and parallel code, but they have some important differences:
  • Goroutines: A goroutine is a lightweight, user-level thread of execution that runs concurrently with other goroutines within the same process. Goroutines are managed by the Go runtime, which automatically schedules and balances their execution across multiple CPUs. Goroutines require much less memory and have much lower overhead compared to threads, allowing for many goroutines to run simultaneously within a single process.
  • Threads: A thread is a basic unit of execution within a process. Threads are independent units of execution that share the same address space as the process that created them. This allows threads to share data and communicate with each other, but also introduces the need for explicit synchronization to prevent race conditions and other synchronization issues.
  • Processes: A process is a self-contained execution environment that runs in its own address space. Processes are independent of each other, meaning that they do not share memory or other resources. Communication between processes requires inter-process communication mechanisms, such as pipes, sockets, or message queues.
  • In general, goroutines provide a more flexible and scalable approach to writing concurrent code compared to threads, as they are much lighter and more efficient, and allow for many more concurrent units of execution within a single process. Processes provide a more secure and isolated execution environment, but have higher overhead and require more explicit communication mechanisms.

Why is Goroutine lighter and more efficient than thread or process?

  • Stack size: Goroutines have a much smaller stack size compared to threads. The stack size of a goroutine is dynamically adjusted by the Go runtime, based on the needs of the goroutine. This allows for many more goroutines to exist simultaneously within a single process, as they require much less memory.
  • Scheduling: Goroutines are scheduled by the Go runtime, which automatically balances and schedules their execution across multiple CPUs. This eliminates the need for explicit thread management and synchronization, reducing overhead.
  • Context switching: Context switching is the process of saving and restoring the state of a running thread in order to switch to a different thread. Goroutines have a much lower overhead for context switching compared to threads, as they are much lighter and require less state to be saved and restored.
  • Resource sharing: Goroutines share resources with each other and with the underlying process, eliminating the need for explicit resource allocation and deallocation. This reduces overhead and allows for more efficient use of system resources.
  • Overall, the combination of a small stack size, efficient scheduling, low overhead context switching, and efficient resource sharing makes goroutines much lighter and more efficient than threads or processes, and allows for many more concurrent units of execution within a single process.
  • Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP…等寄存器的刷新;内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;Golang 程序中可以轻松支持10w 级别的 Goroutine 运行,而线程数量达到 1k 时,内存占用就已经达到 2G。
  • 理解G、P、M的含义以及调度模型

How are goroutines scheduled by runtime?

  • Cooperative (协作式). The scheduler uses a cooperative scheduling model, which means that goroutines voluntarily yield control to the runtime when they are blocked or waiting for an event.
  • Timer-based preemption. The scheduler uses a technique called timer-based preemption to interrupt the execution of a running goroutine and switch to another goroutine if it exceeds its time slice
  • Work-stealing. The scheduler uses a work-stealing algorithm, where each CPU has its own local run queue, and goroutines are dynamically moved between run queues to balance the o balance the load and improve performance.
  • no explicit prioritization. The Go runtime scheduler does not provide explicit support for prioritizing goroutines. Instead, it relies on the cooperative nature of goroutines to ensure that all goroutines make progress. In a well-designed Go program, the program should be designed such that all goroutines make progress in a fair and balanced manner.
  • https://blog.csdn.net/sinat_34715587/article/details/124990458
  • G 的数量可以远远大于 M 的数量,换句话说,Go 程序可以利用少量的内核级线程来支撑大量 Goroutine 的并发。多个 Goroutine 通过用户级别的上下文切换来共享内核线程 M 的计算资源,但对于操作系统来说并没有线程上下文切换产生的性能损耗,支持任务窃取(work-stealing)策略:为了提高 Go 并行处理能力,调高整体处理效率,当每个 P 之间的 G 任务不均衡时,调度器允许从 GRQ,或者其他 P 的 LRQ 中获取 G 执行。
  • 减少因Goroutine创建大量M:
    • 由于原子、互斥量或通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 LRQ 上的其他 Goroutine;
    • 由于网络请求和 IO 操作导致 Goroutine 阻塞,通过使用 NetPoller 进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞 M。这可以让 M 执行 P 的 LRQ 中其他的 Goroutines,而不需要创建新的 M。有助于减少操作系统上的调度负载。
    • 当调用一些系统方法的时候,如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用的 Goroutine 将阻塞当前 M,则创建新的M。阻塞的系统调用完成后:M1 将被放在旁边以备将来重复使用
    • 如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。

What are the states of Goroutine and how do they flow?

  • 协程的状态流转?Grunnable、Grunning、Gwaiting
  • In Go, a Goroutine can be in one of several states during its lifetime. The states are:
  • New: The Goroutine is created but has not started executing yet.
  • Running: The Goroutine is executing on a machine-level thread.
  • Waiting: The Goroutine is waiting for some external event, such as I/O, channel communication, or a timer.
  • Sleeping: The Goroutine is sleeping, or waiting for a specified amount of time.
  • Dead: The Goroutine has completed its execution and is no longer running.

In summary, the lifetime of a Goroutine in Go starts when it is created and ends when it completes its execution or encounters a panic, and can be influenced by synchronization mechanisms such as channels and wait groups.

  • Golang context 用于在树形goroutine结构中,通过信号减少资源的消耗,包含Deadline、Done、Error、Value四个接口
  • 常用的同步原语:channel、sync.mutex、sync.RWmutex、sync.WaitGroup、sync.Once、atomic
  • 协程的状态流转?Grunnable、Grunning、Gwaiting
  • sync.Mutex 和 sync.RWMutex 互斥锁和读写锁的使用场景?
  • sync.Mutex: “锁”实现背后那些事
  • Golang 协程优雅的退出?
  • 深入理解协程gmp调度模型,以及其发展历史
  • 理解操作系统是怎么调度的,golang协程调度的优势,切换代价低,goroutine开销低,并发度高。
  • Golang IO 模型和网络轮训器

Go 内存管理和垃圾回收(memory and gc)

内存管理基本策略

为了兼顾内存分配的速度和内存利用率,大多数都采用以下策略进行内存管理:

  1. 申请:每次从操作系统申请一大块内存(比如1MB),以减少系统调用
  2. 切分:为了兼顾大小不同的对象,将申请到的内存按照一定的策略切分成小块,使用链接相连
  3. 分配:为对象分配内存时,只需从大小合适的链表中提取一块即可。
  4. 回收复用: 对象不再使用时,将该小块内存归还到原链表
  5. 释放: 如果闲置内存过多,则尝试归凡部分内存给操作系统,减少内存开销。

golang内存管理

 golang内存管理基本继承了tcmolloc成熟的架构,因此也符合内存管理的基本策略。

  1. 分三级管理,线程级的thread cache,中央center cache,和管理span的center heap。
  2. 每一级都采用链表管理不同size空闲内存,提高内存利用率
  3. 线程级的tread local cache能够减少竞争和加锁操作,提高效率。中央center cache为所有线程共享。
  4. 小对象直接从本地cache获取,大对象从center heap获取,提高内存利用率
  5. 每一级内存不足时,尝试从下一级内存获取
    内存三级管理
    线程cache
    大对象span管理
  • 多级缓存:内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc 和 Go 运行时分配器都会引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存
  • 对象大小:Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种,tiny,small,large
  • mspan、mcache、mcentral、mheap

What are the memory leak scenarios in Go language?

  • Goroutine leaks: If a goroutine is created and never terminated, it can result in a memory leak. This can occur when a program creates a goroutine to perform a task but fails to provide a mechanism for the goroutine to terminate, such as a channel to receive a signal to stop.

  • Leaked closures: Closures are anonymous functions that capture variables from their surrounding scope. If a closure is created and assigned to a global variable, it can result in a memory leak, as the closure will continue to hold onto the captured variables even after they are no longer needed.

  • Incorrect use of channels: Channels are a mechanism for communicating between goroutines. If a program creates a channel but never closes it, it can result in a memory leak. Additionally, if a program receives values from a channel but never discards them, they will accumulate in memory and result in a leak.

  • Unclosed resources: In Go, it’s important to close resources, such as files and network connections, when they are no longer needed. Failure to do so can result in a memory leak, as the resources and their associated memory will continue to be held by the program.

  • Unreferenced objects: In Go, unreferenced objects are objects that are no longer being used by the program but still exist in memory. This can occur when an object is created and never explicitly deleted or when an object is assigned a new value and the old object is not properly disposed of.
    By following best practices and being mindful of these common scenarios, you can help to avoid memory leaks in your Go programs. Additionally, you can use tools such as the Go runtime profiler to detect and diagnose memory leaks in your programs.

  • Memory Leaking Scenarios

    • hanging goroutine
    • cgo
    • substring/slice
    • ticker

golang支持垃圾回收,gc能减少编程的负担,但与此同时也可能造成程序的性能问题。那么如何测量golang程序使用的内存,以及如何减少golang gc的负担呢?经历了许多版本的迭代,golang gc 沿着低延迟和高吞吐的目标在进化,相比早起版本,目前有了很大的改善,但仍然有可能是程序的瓶颈。因此要学会分析golang 程序的内存和垃圾回收问题。

如何查看程序的gc信息?

  1. 通过设置环境变量?env GODEBUG=gctrace=1
    例如: env GODEBUG=gctrace=1 godoc -http=:8080
  2. import _ “net/http/pprof”,查看/debug/pprof

tips:

  1. 减少内存分配,优先使用第二种APIs
    func (r *Reader) Read() ([]byte, error)
    func (r *Reader) Read(buf []byte) (int, error)
  2. 尽量避免string 和 []byte之间的转换
  3. 尽量减少两个字符串的合并
  4. 对slice预先分配大小
  5. 尽量不要使用cgo,因为c和go毕竟是两种语言。cgo是个high overhead的操作,调用cgo相当于阻塞IO,消耗一个线程
  6. defer is expensive?在性能要求较高的时候,考虑少用
  7. 对IO操作设置超时机制是个好习惯SetDeadline, SetReadDeadline, SetWriteDeadline
  8. 当数据量很大的时候,考虑使用流式IO(streaming IO)。io.ReaderFrom / io.WriterTo

gc 的过程

  • Marking phase: In this phase, the Go runtime identifies all objects that are accessible by the program and marks them as reachable. Objects that are not marked as reachable are considered unreachable and eligible for collection.
  • Sweeping phase: In this phase, the Go runtime scans the memory heap and frees all objects that are marked as unreachable. The memory space occupied by these objects is now available for future allocation.
  • Compacting phase: In this phase, the Go runtime rearranges the remaining objects on the heap to reduce fragmentation and minimize the impact of future allocations and deallocations.

垃圾回收算法概述

  golang是近几年出现的带有垃圾回收的现代语言,其垃圾回收算法自然也相互借鉴。因此在学习golang gc之前有必要了解目前主流的垃圾回收方法。

  1. 引用计数:熟悉C++智能指针应该了解引用计数方法。它对每一个分配的对象增加一个计数的域,当对象被创建时其值为1。每次有指针指向该对象时,其引用计数增加1,引用该对象的对象被析构时,其引用计数减1。当该对象的引用计数为0时,该对象也会被析构回收。引用对象对于C++这类没有垃圾回收器,对于便于对象管理的是不错的工具,但是维护引用计数会造成程序运行效率下降。
  2. 标记-清扫: 标记清扫是古老的垃圾回收算法,出现在70年代。通过指定每个内存阈值或者时间长度,垃圾回收器会挂起用户程序,也称为STW(stop the world)。垃圾回收器gc会对程序所涉及的所有对象进行一次遍历以确定哪些内存单元可以回收,因此分为标记(mark)和清扫(sweep),标记阶段标明哪些内存在使用不能回收,清扫阶段将不需要的内存单元释放回收。标记清扫法最大的问题是需要STW,当程序使用的内存较多时,其性能会比较差,延时较高。
  3. 三色标记法: 三色标记法是对标记清扫的改进,也是golang gc的主要算法,其最大的的优点是能够让部分gc和用户程序并发进行。它将对象分为白色、灰色和黑色:
    • 开始时所有的对象都是白色
    • 从根出发,将所有可到达对象标记为灰色,放入待处理队列
    • 从待处理队列中取出灰色对象,并将其引用的对象标记为灰色放入队列中,其自身标记为黑色。
    • 重复步骤3,直到灰色对象队列为空。最终只剩下白色对象和黑色对象,对白色对象尽心gc。
  4. 另外,还有一些在此基础上进行优化改进的gc算法,例如分代收集,节点复制等,它会考虑到对象的生命周期的长度,减少扫描标记的操作,相对来说效率会高一些。

golang垃圾回收

  golang gc是使用三色标记清理法,为了对用户对象进行标记需要将用户程序所有线程全部冻结(STW),当程序中包含很多对象时,暂停时间会很长,用户逻辑对用户的反应就会中止。那么如何缩短这个过程呢?一种自然的想法,在三色标记法扫描之后,只会存在黑色和白色两种对象,黑色是程序正在使用的对象不可回收,白色对象是此时不会被程序的对象,也是gc的要清理的对象。那么回收白色对象肯定不会和用户程序造成竞争冲突,因此回收操作和用户程序是可以并发的,这样可以缩短STW的时间。

  写屏障使得扫描操作和回收操作都可以和用户程序并发。我们试想一下,刚把一个对象标记为白色,用户程序突然又引用了它,这种扫描操作就比较麻烦,于是引入了屏障技术。内存扫描和用户逻辑也可以并发执行,用户新建的对象认为是黑色的,已经扫描过的对象有可能因为用户逻辑造成对象状态发生改变。所以**对扫描过后的对象使用操作系统写屏障功能用来监控用户逻辑这段内存,一旦这段内存发生变化写屏障会发生一个信号,gc捕获到这个信号会重新扫描改对象,查看它的引用或者被引用是否发生改变,从而判断该对象是否应该被清理。因此通过写屏障技术,是的扫描操作也可以合用户程序并发执行。

  gc控制器:gc算法并不万能的,针对不同的场景可能需要适当的设置。例如大数据密集计算可能不在乎内存使用量,甚至可以将gc关闭。golang 通过百分比来控制gc触发的时机,设置的百分比指的是程序新分配的内存与上一次gc之后剩余的内存量,例如上次gc之后程序占有2MB,那么下一次gc触发的时机是程序又新分配了2MB的内存。我们可以通过SetGCPercent函数动态设置,默认值为100,当百分比设置为负数时例如-1,表明关闭gc。
SetGCPercent

golang gc调优实例

gc 是golang程序性能优化非常重要的一部分,建议依照下面两个实例实践golang程序优化。

 

What’s Go closure?

In Go, a closure is a function that has access to variables from its outer (enclosing) function’s scope. The closure “closes over” the variables, meaning that it retains access to them even after the outer function has returned. This makes closures a powerful tool for encapsulating data and functionality and for creating reusable code.

Encapsulating State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func counter() func() int {
i := 0
return func() int {
i++
return i
}
}

func main() {
c := counter()

fmt.Println(c()) // Output: 1
fmt.Println(c()) // Output: 2
fmt.Println(c()) // Output: 3
}

Implementing Callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func forEach(numbers []int, callback func(int)) {
for _, n := range numbers {
callback(n)
}
}

func main() {
numbers := []int{1, 2, 3, 4, 5}

// Define a callback function to apply to each element of the numbers slice.
callback := func(n int) {
fmt.Println(n * 2)
}

// Use the forEach function to apply the callback function to each element of the numbers slice.
forEach(numbers, callback)
}

Fibonacci

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
package main

import "fmt"

func memoize(f func(int) int) func(int) int {
cache := make(map[int]int)
return func(n int) int {
if val, ok := cache[n]; ok {
return val
}
result := f(n)
cache[n] = result
return result
}
}

func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}

func main() {
fib := memoize(fibonacci)
for i := 0; i < 10; i++ {
fmt.Println(fib(i))
}
}

Factorial

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
factorial := func(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}

fmt.Println(factorial(5)) // Output: 120
}

Event Handling

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
package main

import (
"fmt"
"time"
)

type Button struct {
onClick func()
}

func NewButton() *Button {
return &Button{}
}

func (b *Button) SetOnClick(f func()) {
b.onClick = f
}

func (b *Button) Click() {
if b.onClick != nil {
b.onClick()
}
}

func main() {
button := NewButton()
button.SetOnClick(func() {
fmt.Println("Button Clicked!")
})

go func() {
for {
button.Click()
time.Sleep(1 * time.Second)
}
}()

fmt.Scanln()
}

Go http client 实践

最近在项目开发中使用http服务与第三方服务交互,感觉golang的http封装得很好,很方便使用但是也有一些坑需要注意,一是自动复用连接,二是Response.Body的读取和关闭

http客户端自动复用连接

首先用代码直观的体验http客户端自动复用连接特点
server.go

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello!")
    })
    http.ListenAndServe(":8848", nil)
}

client.go

func doReq() {
    resp, err := http.Get("http://127.0.0.1:8848/test")
    if err != nil {
        fmt.Println(err)
        return
    }
    io.Copy(os.Stdout, resp.Body)
    defer resp.Body.Close()
}

func main() {
    //http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 10
    for {
        go doReq()
        go doReq()
        //	go doReq()
        time.Sleep(300 * time.Millisecond)
    }
}

测试1:执行netstat | grep "8848" | wc -l 结果:一直都是4
测试2:增加一个go doReq(),继续测试,结果:是一直增大
测试3:在测试2的基础上设置MaxIdleConnsPerHost = 10,结果:一直都是6

测试1已经能说明golang的http会自动复用连接
测试2为什么连接数量会一直增加呢?原因是golang中默认只保持两条持久连接,http.Transport没有设置MaxIdleConnPerHost,于是便采用了默认的DefaultMaxIdleConnsPerHost,这个值是2。
测试3通过加大MaxIdleConnPerHost的值,就能高效的利用http的自动复用机制。

读取和关闭Response.Body

将Resonse.Body的读取的代码屏蔽,继续测试。

func doReq() {
    resp, err := http.Get("http://127.0.0.1:8848/test")
    if err != nil {
        fmt.Println(err)
        return
    }
    //io.Copy(os.Stdout, resp.Body)
    defer resp.Body.Close()
}  

测试结果发现,连接数一直增加。
产生的原因:body实际上是一个嵌套了多层的net.TCPConn,当body没有被完全读取,也没有被关闭是,那么这次的http事物就没有完成,除非连接因为超时终止了,否则相关资源无法被回收。
从实现上看只要body被读完,连接就能被回收,只有需要抛弃body时才需要close,似乎不关闭也可以。但那些正常情况能读完的body,即第一种情况,在出现错误时就不会被读完,即转为第二种情况。而分情况处理则增加了维护者的心智负担,所以始终close body是最佳选择。

Go sync.Pool

基本使用

https://golang.org/pkg/sync/
sync.Pool的使用非常简单,它具有以下几个特点:

  • sync.Pool设计目的是存放已经分配但暂时不用的对象,供以后使用,以减轻gc的代价,提高效率
  • 存储在Pool中的对象会随时被gc自动回收,Pool中对象的缓存期限为两次gc之间
  • 用户无法定义sync.Pool的大小,其大小仅仅受限于内存的大小
  • sync.Pool支持多协程之间共享

sync.Pool的使用非常简单,定义一个Pool对象池时,需要提供一个New函数,表示当池中没有对象时,如何生成对象。对象池Pool提供Get和Put函数从Pool中取和存放对象。

下面有一个简单的实例,直接运行是会打印两次“new an object”,注释掉runtime.GC(),发现只会调用一次New函数,表示实现了对象重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"runtime"
"sync"
)

func main() {
p := &sync.Pool{
New: func() interface{} {
fmt.Println("new an object")
return 0
},
}

a := p.Get().(int)
a = 100
p.Put(a)
runtime.GC()
b := p.Get().(int)
fmt.Println(a, b)
}

sync.Pool 如何支持多协程共享?

sync.Pool支持多协程共享,为了尽量减少竞争和加锁的操作,golang在设计的时候为每个P(核)都分配了一个子池,每个子池包含一个私有对象和共享列表。 私有对象只有对应的和核P能够访问,而共享列表是与其它P共享的。

在golang的GMP调度模型中,我们知道协程G最终会被调度到某个固定的核P上。当一个协程在执行Pool的get或者put方法时,首先对改核P上的子池进行操作,然后对其它核的子池进行操作。因为一个P同一时间只能执行一个goroutine,所以对私有对象存取操作是不需要加锁的,而共享列表是和其他P分享的,因此需要加锁操作。

一个协程希望从某个Pool中获取对象,它包含以下几个步骤:

  1. 判断协程所在的核P中的私有对象是否为空,如果非常则返回,并将改核P的私有对象置为空
  2. 如果协程所在的核P中的私有对象为空,就去改核P的共享列表中获取对象(需要加锁)
  3. 如果协程所在的核P中的共享列表为空,就去其它核的共享列表中获取对象(需要加锁)
  4. 如果所有的核的共享列表都为空,就会通过New函数产生一个新的对象

在sync.Pool的源码中,每个核P的子池的结构如下所示:

// Local per-P Pool appendix.
type poolLocalInternal struct {
    private interface{}   // Can be used only by the respective P.
    shared  []interface{} // Can be used by any P.
    Mutex                 // Protects shared.
}

更加细致的sync.Pool源码分析,可参考http://jack-nie.github.io/go/golang-sync-pool.html

为什么不使用sync.pool实现连接池?

刚开始接触到sync.pool时,很容易让人联想到连接池的概念,但是经过仔细分析后发现sync.pool并不是适合作为连接池,主要有以下两个原因:

  • 连接池的大小通常是固定且受限制的,而sync.Pool是无法控制缓存对象的数量,只受限于内存大小,不符合连接池的目标
  • sync.Pool对象缓存的期限在两次gc之间,这点也和连接池非常不符合

golang中连接池通常利用channel的缓存特性实现。当需要连接时,从channel中获取,如果池中没有连接时,将阻塞或者新建连接,新建连接的数量不能超过某个限制。

https://github.com/goctx/generic-pool基于channel提供了一个通用连接池的实现

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package pool

import (
"errors"
"io"
"sync"
"time"
)

var (
ErrInvalidConfig = errors.New("invalid pool config")
ErrPoolClosed = errors.New("pool closed")
)

type Poolable interface {
io.Closer
GetActiveTime() time.Time
}

type factory func() (Poolable, error)

type Pool interface {
Acquire() (Poolable, error) // 获取资源
Release(Poolable) error // 释放资源
Close(Poolable) error // 关闭资源
Shutdown() error // 关闭池
}

type GenericPool struct {
sync.Mutex
pool chan Poolable
maxOpen int // 池中最大资源数
numOpen int // 当前池中资源数
minOpen int // 池中最少资源数
closed bool // 池是否已关闭
maxLifetime time.Duration
factory factory // 创建连接的方法
}

func NewGenericPool(minOpen, maxOpen int, maxLifetime time.Duration, factory factory) (*GenericPool, error) {
if maxOpen <= 0 || minOpen > maxOpen {
return nil, ErrInvalidConfig
}
p := &GenericPool{
maxOpen: maxOpen,
minOpen: minOpen,
maxLifetime: maxLifetime,
factory: factory,
pool: make(chan Poolable, maxOpen),
}

for i := 0; i < minOpen; i++ {
closer, err := factory()
if err != nil {
continue
}
p.numOpen++
p.pool <- closer
}
return p, nil
}

func (p *GenericPool) Acquire() (Poolable, error) {
if p.closed {
return nil, ErrPoolClosed
}
for {
closer, err := p.getOrCreate()
if err != nil {
return nil, err
}
// 如果设置了超时且当前连接的活跃时间+超时时间早于现在,则当前连接已过期
if p.maxLifetime > 0 && closer.GetActiveTime().Add(time.Duration(p.maxLifetime)).Before(time.Now()) {
p.Close(closer)
continue
}
return closer, nil
}
}

func (p *GenericPool) getOrCreate() (Poolable, error) {
select {
case closer := <-p.pool:
return closer, nil
default:
}
p.Lock()
if p.numOpen >= p.maxOpen {
closer := <-p.pool
p.Unlock()
return closer, nil
}
// 新建连接
closer, err := p.factory()
if err != nil {
p.Unlock()
return nil, err
}
p.numOpen++
p.Unlock()
return closer, nil
}

// 释放单个资源到连接池
func (p *GenericPool) Release(closer Poolable) error {
if p.closed {
return ErrPoolClosed
}
p.Lock()
p.pool <- closer
p.Unlock()
return nil
}

// 关闭单个资源
func (p *GenericPool) Close(closer Poolable) error {
p.Lock()
closer.Close()
p.numOpen--
p.Unlock()
return nil
}

// 关闭连接池,释放所有资源
func (p *GenericPool) Shutdown() error {
if p.closed {
return ErrPoolClosed
}
p.Lock()
close(p.pool)
for closer := range p.pool {
closer.Close()
p.numOpen--
}
p.closed = true
p.Unlock()
return nil
}

Go指针和unsafe.pointer

  1. 不同类型的指针不能相互转化
  2. 指针变量不能进行运算,不支持c/c++中的++,–运算
  3. 任何类型的指针都可以被转换成unsafe.Pointer类型,反之也是
  4. uintptr值可以被转换成unsafe.Pointer类型,反之也是
  5. 对unsafe.Pointer和uintptr两种类型单独解释两句:
    • unsafe.Pointer是一个指针类型,指向的值不能被解析,类似于C/C++里面的(void *),只说明这是一个指针,但是指向什么的不知道。
    • uintptr 是一个整数类型,这个整数的宽度足以用来存储一个指针类型数据;那既然是整数类类型,当然就可以对其进行运算了
      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
      package main
      import (
      "fmt"
      "unsafe"
      )
      func main() {
      var ii [4]int = [4]int{11, 22, 33, 44}
      px := &ii[0]
      fmt.Println(&ii[0], px, *px)
      //compile error
      //pf32 := (*float32)(px)

      //compile error
      // px = px + 8
      // px++

      var pointer1 unsafe.Pointer = unsafe.Pointer(px)
      var pf32 *float32 = (*float32)(pointer1)

      var p2 uintptr = uintptr(pointer1)
      print(p2)
      p2 = p2 + 8
      var pointer2 unsafe.Pointer = unsafe.Pointer(p2)
      var pi32 *int = (*int)(pointer2)

      fmt.Println(*px, *pf32, *pi32)

      }

nil

引用类型声明而没有初始化赋值时,其值为nil。golang需要经常判断nil,防止出现panic错误。

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
bool  -> false  
numbers -> 0
string-> ""

pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil

package main

import (
"fmt"
)

type Person struct {
AgeYears int
Name string
Friends []Person
}

func main() {
var p Person
fmt.Printf("%v\n", p)

var slice1 []int
fmt.Println(slice1)
if slice1 == nil {
fmt.Println("slice1 is nil")
}
// fmt.Println(slice1[0]) panic

// var c chan int
// close(c) panic
}

编译器优化和逃逸分析

逃逸分析(Escape analysis)

golang在内存分配的时候没有堆(heap)和栈(stack)的区别,由编译器决定是否需要将对象逃逸到堆中。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	func Sum() int {
const count = 100
numbers := make([]int, count)
for i := range numbers {
numbers[i] = i + 1
}

var sum int
for _, i := range numbers {
sum += i
}
return sum
}

func main() {
answer := Sum()
fmt.Println(answer)
}
1
2
3
4
5
$ go build -gcflags=-m test_esc.go 
command-line-arguments
./test_esc.go:9:17: Sum make([]int, count) does not escape
./test_esc.go:23:13: answer escapes to heap
./test_esc.go:23:13: main ... argument does not escape

内敛(Inlining)

了解C/C++的应该知道内敛,golang编译器同样支持函数内敛,对于较短且重复调用的函数可以考虑使用内敛

Dead code elimination/Branch elimination

编译器会将代码中一些无用的分支进行优化,分支判断,提高效率。例如下面一段代码由于a和b是常量,编译器也可以推导出Max(a,b),因此最终F函数为空
1
2
3
4
5
6
7
8
9
10
11
12
13
func Max(a, b int) int {
if a > b {
return a
}
return b
}

func F() {
const a, b = 100, 20
if Max(a, b) == b {
panic(b)
}
}

常用的编译器选项: go build -gcflags=”-lN” xxx.go

  • “-S”,编译时查看汇编代码
  • “-l”,关闭内敛优化
  • “-m”,打印编译优化的细节
  • “-l -N”,关闭所有的优化

Go runtime 介绍

  为了避开直接通过系统调用分配内存而导致的性能开销,通常会通过预分配、内存池等操作自主管理内存。golang由运行时runtime管理内存,完成初始化、分配、回收和释放操作。目前主流的内存管理器有glibc和tcmolloc,tcmolloc由Google开发,具有更好的性能,兼顾内存分配的速度和内存利用率。golang也是使用类似tcmolloc的方法进行内存管理。建议参考下面链接学习tcmalloc的原理,其内存管理的方法也是golang内存分配的方法。另外一个原因,golang自主管理也是为了更好的配合垃圾回收。
【1】.https://zhuanlan.zhihu.com/p/29216091
【2】.http://goog-perftools.sourceforge.net/doc/tcmalloc.html

What is the Go runtime?

The Go runtime is a collection of software components that provide essential services for Go programs, including memory management, garbage collection, scheduling, and low-level system interaction. The runtime is responsible for managing the execution of Go programs and for providing a consistent, predictable environment for Go code to run in.

At a high level, the Go runtime is responsible for several core tasks:

  • Memory management: The runtime manages the allocation and deallocation of memory used by Go programs, including the stack, heap, and other data structures.
  • Garbage collection: The runtime automatically identifies and frees memory that is no longer needed by a program, preventing memory leaks and other related issues.
  • Scheduling: The runtime manages the scheduling of Goroutines, the lightweight threads used by Go programs, to ensure that they are executed efficiently and fairly.
  • Low-level system interaction: The runtime provides an interface for Go programs to interact with low-level system resources, including system calls, I/O operations, and other low-level functionality.

The Go runtime is an essential component of the Go programming language, and it is responsible for many of the language’s unique features and capabilities. By providing a consistent, efficient environment for Go code to run in, the runtime enables developers to write high-performance, scalable software that can run on a wide range of platforms and architectures.

程序启动流程

  在golang中,可执行文件的入口函数并不是我们写的main函数,编译器在编译go代码时会插入一段起引导作用的汇编代码,它引导程序进行命令行参数、运行时的初始化,例如内存分配器初始化、垃圾回收器初始化、协程调度器的初始化。golang引导初始化之后就会进入用户逻辑,因为存在特殊的init函数,main函数也不是程序最开始执行的函数。

  golang可执行程序由于运行时runtime的存在,其启动过程还是非常复杂的,这里通过gdb调试工具简单查看其启动流程:

  1. 找一个golang编译的可执行程序test,info file查看其入口地址:gdb test,info files
    (gdb) info files
    Symbols from “/home/terse/code/go/src/learn_golang/test_init/main”.
    Local exec file:
    /home/terse/code/go/src/learn_golang/test_init/main’,
    file type elf64-x86-64.
    Entry point: 0x452110
    …..
  2. 利用断点信息找到目标文件信息:
    (gdb) b *0x452110
    Breakpoint 1 at 0x452110: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
  3. 依次找到对应的文件对应的行数,设置断点,调到指定的行,查看具体的内容:
    (gdb) b _rt0_amd64
    (gdb) b b runtime.rt0_go
    至此,由汇编代码针对特定平台实现的引导过程就全部完成了,后续的代码都是用Go实现的。分别实现命令行参数初始化,内存分配器初始化、垃圾回收器初始化、协程调度器的初始化等功能。
    1
    2
    3
    4
    5
    6
    7
    CALL	runtime·args(SB)
    CALL runtime·osinit(SB)
    CALL runtime·schedinit(SB)

    CALL runtime·newproc(SB)

    CALL runtime·mstart(SB)

特殊的init函数

  1. init函数先于main函数自动执行,不能被其他函数调用
  2. init函数没有输入参数、没有返回值
  3. 每个包可以含有多个同名的init函数,每个源文件也可以有多个同名的init函数
  4. 执行顺序 变量初始化 > init函数 > main函数。在复杂项目中,初始化的顺序如下:
    • 先初始化import包的变量,然后先初始化import的包中的init函数,,再初始化main包变量,最后执行main包的init函数
    • 从上到下初始化导入的包(执行init函数),遇到依赖关系,先初始化没有依赖的包
    • 从上到下初始化导入包中的变量,遇到依赖,先执行没有依赖的变量初始化
    • main包本身变量的初始化,main包本身的init函数
    • 同一个包中不同源文件的初始化是按照源文件名称的字典序

  

程序bootstrap过程

如上图所示,Go程序启动大致分为一下一个部分:

  • 参数处理,runtime·args(SB)
  • 操作系统初始化,runtime·osinit(SB)
  • 调度器初始化,runtime·schedinit(SB)
  • 运行runtime.main函数,装载用户main函数并运行,runtime.main()
    参数处理和osinit逻辑比较简单,代码也较少,这里主要记录下调度器初始化和runtime.main函数两个部分

runtime·schedinit

schedinit内容比较多,主要包含:

  • 栈初始化 stackinit()
  • 堆初始化 mallocinit()
  • gc初始化 gcinit()
  • 初始化resize allp []*p procresize()

stack

stackinit() 核心代码用于初始化全局的stackpool和stackLarge两个结构

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
var stackpool [_NumStackOrders]struct {
item stackpoolItem
_ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
}

//go:notinheap
type stackpoolItem struct {
mu mutex
span mSpanList
}

// Global pool of large stack spans.
var stackLarge struct {
lock mutex
free [heapAddrBits - pageShift]mSpanList // free lists by log_2(s.npages)
}

func stackinit() {
if _StackCacheSize&_PageMask != 0 {
throw("cache size must be a multiple of page size")
}
for i := range stackpool {
stackpool[i].item.span.init()
lockInit(&stackpool[i].item.mu, lockRankStackpool)
}
for i := range stackLarge.free {
stackLarge.free[i].init()
lockInit(&stackLarge.lock, lockRankStackLarge)
}
}

newproc 需要一个初始的stack

1
2
3
4
5
6
if gp.stack.lo == 0 {
// Stack was deallocated in gfput or just above. Allocate a new one.
systemstack(func() {
gp.stack = stackalloc(startingStackSize)
})
gp.stackguard0 = gp.stack.lo + _StackGuard

goroutine 运行时需要把stack 地址传给m

runtime.main

内存分配和管理策略mallocgc

垃圾回收garbage collector

程序并发Goroutine调度

Go 可测试编程、单元测试和性能优化

  Golang非常注重工程化,提供了非常好用单元测试、性能测试(benchmark)和调优工具(pprof),它们对提高代码的质量和服务的性能非常有帮助。参考链接中通过一段http代码非常详细的介绍了golang程序优化的步骤和方便之处。实际工作中,我们很难每次都对代码都有那么高的要求,但是能使用一些工具对程序进行优化程序性能也是golang程序员必备的技能。
dave它通过几个case非常清晰的介绍了golang性能分析与优化的技术,非常值得学习。https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html

  • testing 标准库
  • go test 测试工具
  • go tool pprof 分析 profile数据

单元测试,测试正确性

  1. 为了测试某个文件中的某个函数的性能,在相同目录下定义xxx_test.go文件,使用go build命令编译程序时会忽略测试文件

  2. 在测试文件中定义测试某函数的代码,以TestXxxx方式命名,例如TestAdd

  3. 在相同目录下运行 go test -v 即可观察代码的测试结果

     func TestAdd(t *testing.T) {
         if add(1, 3) != 4 {
             t.FailNow()
         }
     }
    

性能测试,benchmark

  1. 单元测试,测试程序的正确性。benchmark 用户测试代码的效率,执行的时间
  2. benchmark测试以BenchMark开头,例如BenchmarkAdd
  3. 运行 go test -v -bench=. 程序会运行到一定的测试,直到有比较准备的测试结果
    func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
    _ = add(1, 2)
    }
    }

    BenchmarkAdd-4 2000000000 0.26 ns/op

pprof性能分析

  1. 除了使用使用testing进行单元测试和benchanmark性能测试,golang能非常方便捕获或者监控程序运行状态数据,它包括cpu、内存、和阻塞等,并且非常的直观和易于分析。
  2. 有两种捕获方式: a、在测试时输出并保存相关数据;b、在运行阶段,在线采集,通过web接口获得实时数据。
  3. Benchamark时输出profile数据:go test -v -bench=. -memprofile=mem.out -cpuprofile=cpu.out
  4. 使用go tool pprof xxx.test mem.out 进行交互式查看,例如top5。同理,可以分析其它profile文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(pprof) top5
Showing nodes accounting for 1994.93MB, 63.62% of 3135.71MB total
Dropped 28 nodes (cum <= 15.68MB)
Showing top 5 nodes out of 46
flat flat% sum% cum cum%
475.10MB 15.15% 15.15% 475.10MB 15.15% regexp/syntax.(*compiler).inst
455.58MB 14.53% 29.68% 455.58MB 14.53% regexp.progMachine
421.55MB 13.44% 43.12% 421.55MB 13.44% regexp/syntax.(*parser).newRegexp
328.61MB 10.48% 53.60% 328.61MB 10.48% regexp.onePassCopy
314.09MB 10.02% 63.62% 314.09MB 10.02% net/http/httptest.cloneHeader

- flat:仅当前函数,不包括它调用的其它函数
- cum: 当前函数调用堆栈的累计
- sum: 列表前几行所占百分比的总和

实际操作

Go实践: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
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
66
67
68
package main

import (
"context"
"fmt"
"sync"
"time"
)

//sync package
func sync1() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1) //设置协程等待的个数
go func(x int) {
defer func() {
wg.Done()
}()
fmt.Println("I'm", x)
}(i)
}
wg.Wait()
}

//chan
func sync2() {
chanSync := make([]chan bool, 10)
for i := 0; i < 10; i++ {
chanSync[i] = make(chan bool)
go func(x int, ch chan bool) {
fmt.Println("I'm ", x)
ch <- true
}(i, chanSync[i])
}

for _, ch := range chanSync {
<-ch
}
}

//context
func sync3() {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()

for i := 0; i < 10; i++ {
go func(ctx context.Context, i int) {
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err(), i)
return
case <-time.After(2 * time.Second):
fmt.Println("time out", i)
return
}
}
}(ctx, i)
}
time.Sleep(5 * time.Second)
}

func main() {
sync1()
sync2()
sync3()
time.Sleep(10 * time.Second)
}

Go实践:生产者、消费者模型,并行计算累加求和

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import (
"math/rand"
"sync"
"sync/atomic"

"fmt"
)

var total int32 = 100000

var producerLimit int32 = 3

var consumerLimit int32 = 4

var Q chan int32
var SumQ chan int32

var AtomicSum int32 = 0

func init() {
Q = make(chan int32, 10)
SumQ = make(chan int32)
}

func produce() {
a := total / producerLimit
b := total % producerLimit
var wg sync.WaitGroup
for i := 0; i < int(producerLimit); i++ {
batch := a
if i < int(b) {
batch += 1
}
wg.Add(1)
go func(x int32) {
defer wg.Done()
for j := 0; j < int(x); j++ {
num := rand.Intn(10)
atomic.AddInt32(&AtomicSum, int32(num))
Q <- int32(num)
}
}(batch)
}
go func() {
wg.Wait()
close(Q)
}()
}

func consumer() int32 {
var wg sync.WaitGroup
for i := 0; i < int(consumerLimit); i++ {
wg.Add(1)
go func() {
defer wg.Done()
var batchSum int32 = 0
for num := range Q {
batchSum += num
}
SumQ <- batchSum
}()
}

go func() {
wg.Wait()
close(SumQ)
}()

var ans int32 = 0
for sum := range SumQ {
ans += sum
}
return ans
}

func main() {
produce()
fmt.Printf("%d,%d\n", consumer(), atomic.LoadInt32(&AtomicSum))
}

Go 实践:interface/base/derive

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
package main

import "fmt"

type service interface {
foo1()
foo2()
}

type baseService struct {
name string
}

func NewBaseService(name string) *baseService {
b := baseService{}
b.name = name
return &b
}

func (b *baseService) foo1() {
fmt.Println(b.name)
}

func (b *baseService) foo2() {
fmt.Println(b.name)
}

type AService struct {
*baseService
name string
}

func NewAService(name string, b *baseService) *AService {
s := AService{}
s.baseService = b
s.name = name
return &s
}

func (a *AService) foo1() {
fmt.Println(a.name)
}

func foo(s service) {
s.foo1()
s.foo2()
}

func main() {
b := NewBaseService("baseService")
s := NewAService("AService", b)
foo(s)
}

Go实践:设计模式的实现

https://refactoringguru.cn/design-patterns/chain-of-responsibility/go/example

Go 1.12 压测后rss内存一直无法释放问题

包和库(package)

参考

Python程序为什么慢?

  不同的场景下,代码是有不同的要求,大体有三个等级,“管用、更好、更快”。相比C/C++,Python具有较好的开发系效率,但是程序的性能运行速度会差一些。究其原因是Python为了灵活性,牺牲了效率。

  1. 动态类型。对于C/C++等静态类型语言,由于变量的类型固定,变量之间的运算很容易指定特定的函数。而动态类型在运行的时间需要大量if else判断处理,直到找到符合条件的函数。动态类型增加语言的易用性,但是牺牲了程序的运行效率

  2. GIL(Global Interpreter Lock)全局解释锁,CPython在解释执行任何Python代码的时候,首先都需要they acquire GIL when running,release GIL when blocking for I/O。如果没有涉及I/O操作,只有CPU密集型操作时,解释器每隔100一段时间(100ticks)就会释放GIL。GIL是实现Python解释器的(Cython)时所引入的一个概念,不是Python的特性。
    由于GIL的存在,使得Python对于计算密集型任务,多线程threading模块形同虚设,因为线程在实际运行的时候必须获得GIL,而GIL只有一个,因此无法发挥多核的优势。为了绕过GIL的限制,只能使用multiprocessing等多进程模块,每个进程各有一个CPython解释器,也就各有一个GIL。

  3. CPython不支持JIL(Just-In-Time Compiler),JIL 能充分利用程序运行时信息,进行类型推导等优化,对于重复执行的代码段来说加速效果明显。对于CPython如果想使用JIT对计算密集型任务进行优化,可以尝试使用JIT包numba,它能使得相应的函数变成JIT编译。

Python程序优化的思路?

  最近在做一些算法优化方面的工作,简单总结一下思路:

  1. 熟悉算法的整体流程,对于算法代码,最开始尽可能不要使用多线程和多进程方法,
  2. 在1的基础上跑出算法的CPU profile,整体了解算法耗时分布和瓶颈。Python提供的cProfile模块灵活的针对特定函数或者文件产生profile文件,根据profile数据进行代码性能优化。
  1. 程序(算法)本身的剪枝。比如视频追踪中,考虑是否让每个像素点都参与计算?优化后选择梯度变化最大的1w个像素点参与计算,能提高分辨率大的视频追踪效率。
  2. 使用矩阵操作代替循环操作。(get_values())
  3. 任务分解,在理解算法的基础上寻找并行机会,利用多线程或者多进程充分利用机器资源。生产者消费者模型,专门的线程负责图像获取和图形变换,专门的线程负责特征提取和追踪。
  4. 使用C/C++重写效率低的瓶颈部分
  5. 使用GPU计算

title: Python通过swig调用复杂C++库
categories:
- Python

  
  最近项目中需要用到visp库中的模板追踪算法,visp库用C++编写的,代码多,功能丰富。但是,对于项目来说直接调visp库并不方便,因此我们摘取visp库中的所需代码,提供python调用的接口,并根据项目需求进行优化和扩展。开源项目越来越多,以后工作也可能会遇到提取复杂库中部分功能,然后提供python调用的接口,因此这里总结一下,过程并不复杂,但是也遇到一些坑。主要注意以下几点:

  1. 依赖库采用静态方式编译。最开始的时候采用默认的动态编译,导致项目依赖复杂,部署起来非常不方便。要注意的是,visp库依赖opencv库,两个库都要采用静态方式编译。
  2. 提取所需代码,封装成类。提取visp库中的模板追踪算法,封装成类。为了便于后续的优化工作,接口扩展性尽可能好。
  3. **swig实现python调用C++**。有很多方法实现python调用C++,我这里采用swig,适合懒人。

静态编译opencv库

  opencv采用cmake项目管理,通过ccmake可以很方便的设置静态编译选项。BUILD_SHARED_LIBS设置为OFF即为静态编译。另外,为了保持系统整洁,避免安装到系统路径,设置了安装路径,CMAKE_INSTALL_PREFIX=/home/terse/code/terse-visp/opencv-3.4.6/build

  1. git clone https://github.com/opencv/opencv.git
  2. cd opencv-3.4.6
  3. mkdir build && cd build
  4. ccmake ..(关闭动态编译选项,设置安装路径),cmake ..
  5. make -j4
  6. make install

静态编译visp库

  visp库https://github.com/lagadic/visp.git 和opencv库一样都采用cmake管理,编译过程和opencv一样,这里只需要设置静态编译和设置安装路径:
关闭动态编译选项:BUILD_SHARED_LIBS=OFF
设置安装路径: CMAKE_INSTALL_PREFIX=/home/terse/code/terse-visp/visp/build

  1. git clone https://github.com/lagadic/visp.git
  2. cd visp
  3. mkdir build && cd build
  4. ccmake ..(关闭动态编译选项,设置安装路径),cmake ..
  5. make -j4
  6. make install

提取模板追踪算法,封装成C++类

  visp库中提供了模板追踪算法,但是它不能解决遮挡的情况,参考区域很大的时候,追踪速度也很慢,因此在项目中针对这些问题做了一些优化,这个不是本文的重点就不赘述了。下面从visp中摘取的代码,封装成C++类,say_hello成员函数,没有实际用途,只是为了后续的验证python代码的正确性。

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
#ifndef VISP_H_
#define VISP_H_

#include <visp3/io/vpImageIo.h>
#include <visp3/tt/vpTemplateTrackerSSDInverseCompositional.h>
#include <visp3/tt/vpTemplateTrackerWarpHomography.h>
#include <visp3/core/vpException.h>
#include <opencv2/opencv.hpp>
#include <fstream>

class TemplateTracker
{
public:
TemplateTracker();

void SetSampling(unsigned int sample_i, unsigned int sample_j);
void SetLambda(double lamda);
void SetIterationMax(unsigned int n);
void SetPyramidal(unsigned int nlevels, unsigned int level_to_stop);
void SetUseTemplateSelect(bool bselect);
void SetThresholdGradient(float threshold);

int Init(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow);
int InitWithMask(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow, unsigned char* mask_data,int h2,int w2);

int ComputeH(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num);
int ComputeHWithMask(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num,unsigned char* mask_data,int h2,int w2);

void Reset();
~TemplateTracker();
void say_hello();


private:
vpTemplateTrackerWarpHomography warp_;
vpTemplateTrackerSSDInverseCompositional tracker_;
int height_, width_;
vpImage<unsigned char> I_;
bool bshow_;
};

#endif /* VISP_H_ */
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include "visp.h"  


TemplateTracker::TemplateTracker() :
tracker_(&warp_),
bshow_(false)
{};

void TemplateTracker::SetSampling(unsigned int sample_i, unsigned int sample_j)
{
tracker_.setSampling(sample_i, sample_j);
}

void TemplateTracker::SetLambda(double lamda)
{
tracker_.setLambda(lamda);
}

void TemplateTracker::SetIterationMax(unsigned int n)
{
tracker_.setIterationMax(n);
}

void TemplateTracker::SetPyramidal(unsigned int nlevels, unsigned int level_to_stop)
{
tracker_.setPyramidal(nlevels, level_to_stop);
}

void TemplateTracker::SetUseTemplateSelect(bool bselect)
{
tracker_.setUseTemplateSelect(bselect);
}

void TemplateTracker::SetThresholdGradient(float threshold)
{
tracker_.setThresholdGradient(threshold);
}


int TemplateTracker::Init(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow)
{
cv::Mat img_gray(h, w, CV_8UC1, (unsigned char*)(imgData)); //浅拷贝
I_.init(imgData, h, w, true);
height_ = h;
width_ = w;
bshow_ = bshow;
std::vector<vpImagePoint> v_ip;
for (int i = 0; i < points_num/2; i++)
{
vpImagePoint ip(ref[i * 2], ref[i * 2 + 1]);
v_ip.push_back(ip);
}

try{
tracker_.initFromPoints(I_, v_ip);
}catch(vpException &e){
return e.getCode();
}

return 0;
}


int TemplateTracker::InitWithMask(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow, unsigned char* mask_data,int h2,int w2)
{
cv::Mat img_gray(h, w, CV_8UC1, (unsigned char*)(imgData)); //浅拷贝
I_.init(imgData, h, w, true);
height_ = h;
width_ = w;
bshow_ = bshow;
if (NULL != mask_data)
{
cv::Mat mask_gray(h, w, CV_8UC1, (unsigned char*)(mask_data)); //浅拷贝
I_.SetMask(mask_gray);
}

std::vector<vpImagePoint> v_ip;
for (int i = 0; i < points_num/2; i++)
{
vpImagePoint ip(ref[i * 2], ref[i * 2 + 1]);
v_ip.push_back(ip);
}

try{
tracker_.initFromPoints(I_, v_ip);
}catch(vpException &e){
return e.getCode();
}

return 0;
}



int TemplateTracker::ComputeH(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num)
{
I_.init(imgData, height_, width_, true);
try{
tracker_.track(I_);
}catch(vpTrackingException &e){
std::cout << e.getMessage() << std::endl;
return e.getCode();
}
vpColVector p = tracker_.getp();
vpHomography H = warp_.getHomography(p);
for (int m = 0; m < 3; m++)
{
for (int n = 0; n < 3; n++)
{
H_matrix[m * 3 + n] = H[m][n];
}
}
return 0;
}


int TemplateTracker::ComputeHWithMask(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num,unsigned char* mask_data,int h2,int w2)
{
I_.init(imgData, height_, width_, true);
if (NULL != mask_data)
{
cv::Mat mask_gray(height_, width_, CV_8UC1, (unsigned char*)(mask_data)); //浅拷贝
I_.SetMask(mask_gray);
}

try{
tracker_.track(I_);
}catch(vpTrackingException &e){
std::cout << e.getMessage() << std::endl;
return e.getCode();
}
vpColVector p = tracker_.getp();
vpHomography H = warp_.getHomography(p);
for (int m = 0; m < 3; m++)
{
for (int n = 0; n < 3; n++)
{
H_matrix[m * 3 + n] = H[m][n];
}
}
return 0;
}

void TemplateTracker::Reset()
{
tracker_.resetTracker();
}

TemplateTracker::~TemplateTracker(){};

void TemplateTracker::say_hello(){
std::cout << "hello" << std::endl;
}

采用swig实现python调用C++

  python调用C++的方法有很多,例如ctypes、PyObject、Boost.python,采用了swig方法,使用之后感觉确挺方便的。为了给追踪功能提供numpy参数的输入和输出,这里需要引入numpy.i文件。
参考:http://www.swig.org/Doc1.3/Python.html#Python

1. 定义接口文件:visp.i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* File: visp.i */
%module visp

%{
#define SWIG_FILE_WITH_INIT
#include "visp.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (unsigned char* IN_ARRAY2, int DIM1, int DIM2) {(unsigned char* imgData, unsigned int h, unsigned int w)}
%apply (unsigned char* IN_ARRAY2, int DIM1, int DIM2) {(unsigned char* mask_data, int h2, int w2)}
%apply (int* IN_ARRAY1, int DIM1) {(int* ref, unsigned int points_num)}
%apply (float* INPLACE_ARRAY1, int DIM1) {(float* H_matrix,int num)}

%include "visp.h"

2. swig 编译visp.i 文件生成C++和py代码,生成visp_wrap.cxx,visp.py

1
swig -c++ -python -py3 visp.i //python3

3. 分别编译visp.cc和visp_wrap.cxx代码

1
2
g++  -O2 -fPIC  -c visp.cc -I/home/terse/code/terse-visp/VispSource/build/include
g++ -O2 -fPIC -c visp_wrap.cxx -I/home/terse/anaconda3/include/python3.6m -I/home/terse/code/terse-visp/VispSource/build/include -I//home/terse/anaconda3/lib/python3.6/site-packages/numpy/core/include/

4. 链接生成_visp.so文件

1
2
3
g++ -shared visp_wrap.o visp.o -L/home/terse/code/terse-visp/VispSource/build/lib -lvisp_ar -lvisp_blob -lvisp_core -lvisp_detection -lvisp_core -lvisp_gui -lvisp_imgproc -lvisp_io -lvisp_klt -lvisp_mbt -lvisp_me -lvisp_robot -lvisp_sensor -lvisp_tt -lvisp_tt_mi -lvisp_vision -lvisp_visual_features -lvisp_vs -lvisp_tt  -lvisp_ar -lvisp_blob -lvisp_core -lvisp_detection -lvisp_core -lvisp_gui -lvisp_imgproc -lvisp_io -lvisp_klt -lvisp_mbt -lvisp_me -lvisp_robot -lvisp_sensor -lvisp_tt -lvisp_tt_mi -lvisp_vision -lvisp_visual_features -lvisp_vs -Wl,-Bstatic -L/home/terse/code/terse-visp/opencv-3.4.6/build/lib -lopencv_dnn -lopencv_ml -lopencv_objdetect -lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_calib3d -lopencv_features2d -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_video -lopencv_photo -lopencv_imgproc -lopencv_flann -lopencv_core -Wl,-Bstatic -L/home/terse/code/terse-visp/opencv-3.4.6/build/share/OpenCV/3rdparty/lib -littnotify -llibprotobuf -llibjasper -lquirc -lippiw -lippicv -Wl,-Bdynamic -lpython3.7m -Wl,-Bdynamic  -llapack  -fopenmp -ldl  -lz -lrt -ltiff -o _visp.so


  这里有个坑纠结了挺久了,最开始生成的_visp.so文件中通过ldd -r 查看一直有几个未定义的符号。排查后发现来自opencv_core库中,通过nm查看,发现libopencv_core.a中是未定义的,而libopencv_sore.so是正常的。最后发现那几个未定义的符号在/home/terse/code/terse-visp/opencv-3.4.6/build/3rdparty/lib/libittnotify.a库中,链接时加入这个库就将未定义的符号解决了。这个链接文件中有许多依赖的库是不需要的,这里就没有仔细排查了,只要没少就能链接成功。

简单测试

  通过ldd -r 检查_visp.so文件没有问题,理论上就没什么问题里,这里通过代码中故意遗留的函数测试一下。

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
import visp
import cv2
import numpy as np

def get_frame(cap, frame_index):
pos = cap.get(cv2.CAP_PROP_POS_FRAMES)
if pos != frame_index:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
ret, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
return gray

if __name__ == '__main__':
video_name = "./test_data/1166/input.mp4"

ref_area = [105, 73, 479, 62, 126, 309, 120, 297, 471, 57, 457, 291]
key_frame = 5520

cap = cv2.VideoCapture(video_name) #video name
tracker = visp.TemplateTracker()
tracker.SetLambda(0.001)
tracker.SetPyramidal(3,0)
tracker.SetIterationMax(200)
tracker.SetSampling(1,1) #x和y方向的降采样率
tracker.say_hello()
img = get_frame(cap, key_frame)
ret_code = tracker.Init(img,ref_area,True)

H_array = np.empty(9,dtype=np.float32)
img = get_frame(cap, key_frame+1)
ret_code = tracker.ComputeH(img,H_array)
print(ret_code,H_array)


title: Python 多线程和多进程
categories:
- Python

  
  最近在做一些算法优化的工作,由于对Python认识不够,开始的入坑使用了多线程。发现在一个四核机器,即使使用多线程,CPU使用率始终在100%左右(一个核)。后来发现Python中并行计算要使用多进程,改成多进程模式后,CPU使用率达到340%,也提升了算法的效率。另外multiprocessing对多线程和多进程做了很好的封装,需要掌握。这里总结下面两个问题:

  1. Python中的并行计算为什么要使用多进程?
  2. Python多线程和多进程简单测试
  3. multiprocessing库的使用

Python中的并行计算为什么要使用多进程?

  Python在并行计算中必须使用多进程的原因是GIL(Global Interpreter Lock,全局解释器锁)。GIL使得在解释执行Python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL。这使得Python一个进程内同一时间只能允许一个线程进行运算”,也就是说多线程无法利用多核CPU。因此:

  1. 对于CPU密集型任务(循环、计算等),由于多线程触发GIL的释放与在竞争,多个线程来回切换损耗资源,因此多线程不但不会提高效率,反而会降低效率。所以计算密集型程序,要使用多进程
  2. 对于I/O密集型代码(文件处理、网络爬虫、sleep等待),开启多线程实际上是**并发(不是并行)**,线程A在IO等待时,会切换到线程B,从而提升效率。
  3. 大多数程序包含CPU和IO操作,但不考虑进程的资源开销,多进程通常都是优于多线程的
  4. 由于Python多线程的问题,因此通常情况下都使用多进程,使用多进程需要注意进程间变量的共享。

Python多线程和多进程简单测试

  • job1是一个完成CPU没有任务IO的死循环,观察CPU使用率,无论使用多少线程数量num,CPU使用率始终在100%左右,也就是说只能利用核的资源。而多进程则可以使用多核资源,num为1时CPU使用率为100%,num为2时CPU使用率接近200%。
  • job2是一个IO密集型的程序,主要的耗时在print系统调用。num=4时,多线程跑了10.81s,cpu使用率93%;多进程只用了3.23s,CPU使用率130%。

 

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
import multiprocessing
import threading

def job1():
'''
full cpu
'''
while True:
continue

NUMS = 100000
def job2():
'''
cpu and io
'''
for i in range(NUMS):
print("hello,world")

def multi_threads(num,job):
threads = []
for i in range(num):
t = threading.Thread(target=job,args=())
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()

def multi_process(num,job):
process = []
for i in range(num):
p = multiprocessing.Process(target=job,args=())
process.append(p)
for p in process:
p.start()
for p in process:
p.join()

if __name__ == '__main__':
# multi_threads(4,job1)
# multi_process(4,job1)
# multi_threads(4,job2)
multi_process(4,job2)

multiprocessing的使用

参考:https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing

  1. 单个进程multiprocessing.Process对象,和threading.Thread的API完全一样,start(),join(),参考上文中的测试代码。
  2. 进程池
  3. 进程间对象共享队列multiprocessing.Queue()
  4. 进程同步multiprocessing.Lock()
  5. 进程间状态共享multiprocessing.Value,multiprocessing.Array
  6. multiprocessing.Manager()
  7. 进程池:multiprocessing.Pool()
  • pool.map
  • pool.imap_unordered
  • pool.apply_async

  Python多线程和多进程的使用非常方面,因为multiprocessing提供了非常好的封装。为了方便设置线程和进程的数量,通常都会使用池pool技术。

1
2
from multiprocessing.dummy import Pool as DummyPool   # thread pool
from multiprocessing import Pool # process pool

multilprocessing包的使用可参考:

1、linux基础命令

  • 帮助命令:man、info
  • 查找命令路径:which、whereis
  • 查看文件文件个数:find ./ | wc -l
  • 以时间顺序显示目录项:ls -lrt
  • 查看文件时同时显示行数:cat -n xxx
  • 查看两个文件的差别:diff file1 file2
  • 动态显示文本最新信息,常用于查看日志: tail -f xxx.log
  • 软连接/硬链接: ln cc ccAgain 和 ln -s cc ccAgain
  • command1 && command2
  • comamand1 || command2
  • 查找txt和pdf文件:find . ( -name “.txt” -o -name “.pdf” ) -print
  • find查找文件时指定深度:find . -maxdepth 1 -type f
  • find只查找目录:find . -type d -print
  • 文本处理
  • 打包:taf -cvf xxx.tar . 解包: tar -xvf xxx.tar
  • 压缩与解压:-z 解压gz文件;-j解压bz2;-J解压xz文件
  • grep 查找文件中指定字符出现的次数
1
cat Temp\ Query\ 1_20230914-171937.csv | grep  "\"sop_v3_user" | grep -v "xxxx" | awk -F ',' '{print $2,$5,$6}' | sort | uniq -c | sort -rk 2

2、系统信息查看工具

  • 查看操作系统发行版:lsb_release -a
  • 查看内核版本信息:uname -a
  • 查看cpu信息:cat /proc/cpuinfo
  • 查看cpu核数:cat /proc/cpuinfo | grep processor | wc -l
  • 查看内存信息:cat /proc/meminfo
  • 显示架构:arch
  • 查看进程间ipc资源情况:ipcs
  • 显示当前所有的系统资源limit信息: ulimit -a
  • 对生成的core文件的大小不进行限制:ulimit -c unlimited

3、系统资源管理和监控

  • 查询正在运行的进程信息:ps -ef 或者 ps -ajx
  • 查询某用户的进程: ps -ef | grep username 或者 ps -lu username
  • 实时显示进程信息: top linux下的任务管理器,内存VIRT和RES
  • 查看用户打开的文件: lsof -u username
  • 查看某进程打开的文件: lsof -p pid
  • 杀死某进程:kill -9 pid
  • pmap输出进程内存你的状况,用来分析线程堆栈
  • 查看内存使用量:free -m 或者 vmstat n m
  • 查看磁盘使用情况:df -h
  • du -ha –max-depth=1
  • iostat 监视I/O子系统,ubuntu安装systat。通过iostat方便查看CPU、网卡、tty设备、磁盘、CD-ROM 等等设备的活动情况, 负载信息
  • sar 找出系统瓶颈的利器
    *ubuntu系统下,默认可能没有安装这个包,使用apt-get install sysstat 来安装;
    安装完毕,将性能收集工具的开关打开: vi /etc/default/sysstat
    设置 ENABLED=”true”
    启动这个工具来收集系统性能数据: /etc/init.d/sysstat start.

4、网络工具

  • 查看网络流量信息iftop
  • netstat命令用于显示各种网络相关信息
  • 查询某端口port被某个进程占用:netstat -antp | grep port,然后使用ps pid查询进程名称
  • 也可以使用lsof -i:port 直接查询该端口的进程
  • ping 测试网络连通情况
  • traceroute IP 探测前往ip的路由信息
  • 直接下载文件或者网页:wget
  • 网络远程复制:scp -r localpath ID@host:path
  • 使用ssh协议下载: scp -r ID@host:path localpath
  • nc服务器编程常用,既可以作为客户端又可以指定端口作为服务端。
  • 查看网络端口使用情况:https://www.runoob.com/w3cnote/linux-check-port-usage.html

5、环境变量

  • 全局/etc/profile->/etc/profile.d;
  • 读取当前用户下面的:/.bash_profile->/.bash_login->~/.profile
  • 读取当前用户目录下面的:~/.bashrc
  • export环境变量,退出失效

6、查看GPU信息

  • 查看gpu信息 nvidia-smi
  • 查看gpu驱动版本信息 cat /proc/driver/nvidia/version
  • pkgconfig? PKG_CONFIG_PATH环境变量

7、测试系统磁盘的性能

dd是Linux/UNIX 下的一个非常有用的命令,作用是用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。另外在linux中,有两个特殊的设备:/dev/null:回收站、无底洞,经常作为写端,不会产生IO,/dev/zero产生字符,经常作为读端,也不会产生IO。
(1)测试磁盘写能力
dd if=/dev/zero of=/test1.img bs=4k count=10000
因为/dev//zero是一个伪设备,它只产生空字符流,对它不会产生IO,所以,IO都会集中在of文件中,of文件只用于写,所以这个命令相当于测试磁盘的写能力。命令结尾添加oflag=direct将跳过内存缓存,添加oflag=sync将跳过hdd缓存。
(2)测试磁盘读能力
dd if=/dev/sda of=/dev/null bs=4k count=10000
因为/dev/sdb是一个物理分区,对它的读取会产生IO,/dev/null是伪设备,相当于黑洞,of到该设备不会产生IO,所以,这个命令的IO只发生在/dev/sdb上,也相当于测试磁盘的读能力。
(3)测试同时读写能力
time dd if=/dev/sda of=/test1.img bs=4k count=10000
在这个命令下,一个是物理分区,一个是实际的文件,对它们的读写都会产生IO(对/dev/sda是读,对/test.img是写),假设它们都在一个磁盘中,这个命令就相当于测试磁盘的同时读写能力。

8、使用dd和nc命令测试网络性能

nc是netcat的简写,有着网络界的瑞士军刀美誉。因为它短小精悍、功能实用,被设计为一个简单、可靠的网络工具
(1)实现任意TCP/UDP端口的侦听,nc可以作为server以TCP或UDP方式侦听指定端口
(2)端口的扫描,nc可以作为client发起TCP或UDP连接
(3)机器之间传输文件
(4)机器之间网络测速
nc命令有个-l参数可以用来监听指定端口,因此我们要完成上面的功能,就只需要简单的从/dev/zero或者其他虚拟设备读入数据:

time nc -l -p 5001 < /test.img

然后另外一台电脑使用nc来连接到这个端口并读入数据:
time nc 192.168.0.11 5001 > /dev/null
上面的测试的结果中,是从磁盘读数据通过网络获取,通过time命令或缺时间参数,可以计算出网络的性能。更准备的测试应该从/dev/zero中多数据会更好一些

参考:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/index.html

了解google C++编码规范

C/C++基础

1. 关键字的作用const用途

- 定义常量、指针、引用、对象,const进行修饰的变量的值在程序的任意位置将不能再被修改,就如同常数一样使用
- 修饰参数const int x;
- 修饰成员变量;const成员变量必须通过初始化列表进行初始化。
- 修饰成员函数;
- C++ mutable变量突破const修饰成员函数的限制

2. 关键字static的用途

- 静态全局变量
- 修饰函数内部的局部变量,作用相当于全局变量
- 类的静态成员变量
- 类的静态成员函数
参考:https://www.cnblogs.com/wxquare/p/6692924.html

3. 宏定义和内敛函数的区别

- 内联函数和宏定义减少函数调用所带来的时间和空间的开销,以空间换时间的策略
- 宏是在预编译阶段简单文本替代,inline在编译阶段实现展开,宏定义是预编译期、内敛是编译器优化
- 宏肯定会被替代,而复杂的inline函数不会被展开
- 宏容易出错(运算顺序),且难以被调试,inline不会
- 宏不是类型安全,而inline是类型安全的,会提供参数与返回值的类型检查
- 当函数size太大,inline虚函数,函数中存在循环或递归,内敛可能失效
- 当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用
- 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量代替之.
参考:https://www.cnblogs.com/wxquare/p/6800488.html

4. extern 与 static

  extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块使用。

5. extern “C”

  extern “C”是为了实现C和C++的混合编程,而C和C++的编译和链接是不完全相同的,extern “C”表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译和链接。C++是一个面向对象语言,它为了支持函数的重载,在编译的时候会带上参数的类型来唯一标识每个函数,而C语言并不需要这么做。C语言中并没有重载和类这些特性,故并不像C++那样print(int i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的print(3)时,它会去找_print_int(3)。因此extern”C”的作用就体现出来了。假设一个C函数print(int i),为了在C++中能够调用它,必须要加上extern关键字。
  参考:https://www.cnblogs.com/skynet/archive/2010/07/10/1774964.html

6. struct和class的区别

  在C++中struct和class中struct和class的区别比较小,主要类成员的默认访问权限继承权限
默认继承权限:如果不明确指定,来自class的继承按照private继承处理,来自struct的继承按照public继承处理。成员的默认访问权限:class的成员默认是private权限,struct默认是public权限。
仅当只有数据成员时使用 struct, 其它一概使用 class.(google 编码规范)

7. 什么是类型安全、内存安全,C/C++不是类型安全

静态类型 vs 动态类型: 静态类型(C/C++,java,golang),动态类型(python)
弱类型 vs 强类型:弱类型(C、C++),强类型(python、golang)
类型安全: 一般来说弱类型存在隐含的类型转换都不是类型安全的,而强类型是类型安全的
https://www.zhihu.com/question/19918532/answer/21647195

8. C++ 中的四种类型转换

C风格的强制类型转换(Type Cast)很简单,不管什么类型的转换统统是:TYPE b = (TYPE)a。
C++风格的类型转换提供了4种类型转换操作符来应对不同场合的应用。
const_cast:字面上理解就是去const属性。
static_cast:命名上理解是静态类型转换。如int转换成char。类似于C风格的强制转换。无条件转换,静态类型转换。基本类型转换用static_cast。
dynamic_cast:命名上理解是动态类型转换。如子类和父类之间的多态类型转换。有条件转换,动态类型转换,运行时类型安全检查(转换失败返回NULL)。多态类之间的类型转换用daynamic_cast。
reinterpret_cast:仅仅重新解释类型,但没有进行二进制的转换。
4种类型转换的格式,如:TYPE B = static_cast(a)
参考:https://www.cnblogs.com/goodhacker/archive/2011/07/20/2111996.html

9. 关键字volatile的作用

  volatile int i = 10
  volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说volatile直接存取原始内存地址,禁止执行期寄存器的优化。

10. 指针和引用

  指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用初始化后不能改变指向。使用时,引用更加安全,指针更加灵活。

  • 初始化。引用必须初始化,且初始化之后不能呢改变;指针可以不必初始化,且指针可以改变所指的对象
  • 空值。指针可以指向空值,不存在指向空值的引用。当引用或者指针作为参数传递的时候,拿到一个引用的时候,是不需要判断引用是否为空的,而拿到一个指针的时候,我们则需要判断它是否为空。这点经常在判断函数参数是否有效的时候使用。
  • 引用和指针指向一个对象时,引用的创建和销毁不会调用类的拷贝构造函数和析构函数。delete一个指针会调用该对象的析构函数,注意防止二次析构。
  • 引用和指针与const。存在常量指针和常量引用指针,表示指向的对象是常量,不能通过指针或者常量修改常量;存在指针常量,不存在引用常量,因为引用本身不能修改指向的特性和与指针常量的特性相同,不需要引用常量。
  • 函数参数传递时使用指针或者引用的效果是相同的,都是简洁操作主调函数中的相关变量,当时引用会更加的安全,因为指针一些修改指向,将不能影响主调函数中的相关变量。所以参数传递时尽可能使用引用。
  • sizeof引用的时候是对象的大小,sizeof指针是指针本身的大小
  • 引用和指针的实现是相同的,“引用只是一个别名,不会占内存空间”的说法是错误的,实际上引用也会再用内存空间。

11.C++的空类八个默认函数

  C++空类会默认产生的8个类成员函数,需要牢记函数的具体形式,尽可能少用默认函数,自己重新定义。参考:

  • 缺省构造函数
  • 拷贝构造函数
  • 赋值构造函数
  • 析构函数
  • 取值操作符函数
  • const 取值操作符
  • 移动构造函数C++11
  • 移动赋值构造函数C++11
  • 如果你的类型需要, 就让它们支持拷贝 / 移动. 否则, 就把隐式产生的拷贝和移动函数禁用.(google编码规范)

12.C++11中delete和default的作用

=default显式缺省,告知编译器生成函数默认的缺省版本
=delete显式删除,告知编译器不生成函数默认的缺省版本
C++11中引进这两种新特性的目的是为了增强对“类默认函数的控制”,从而让程序员更加精准地去控制默认版本的函数

13.C++11禁用隐式类型转换explicit

  explicit关键字用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。

14.new/delete和malloc/free的使用

(1) new/delete是C++的运算符,malloc/free是C/C++的库函数
(2) new/delete和malloc/free必须配套使用
(3) mallocl/free仅仅是在堆中分配内存,需要自己指定分配内存大小以及指针类型的转换
(4) new/delete会根据对象的类型调用对应的构造函数和析构函数,因此在C++中使用更加多
(5) new是类型安全的,而malloc不是,比如:

1
2
int* p = new float[2]; // 编译时指出错误
int* p = malloc(2*sizeof(float)); // 编译时无法指出错误

15 new/operator new和placement new

参考:https://www.cnblogs.com/luxiaoxun/archive/2012/08/10/2631812.html
(1) new:新建对象时用,是C++操作符。本质上是调用operator new函数分配内存,然后调用构造函数生成类的对象,返回对应类型的指针。
(2) operator new就像operator + 一样,是可以重载的。如果类中没有重载operator new,那么调用的就是全局的::operator new来完成堆的分配。要实现不同的内存分配行为,应该重载operator new。
(3) placement new:只是operator new重载的一个版本。它并不分配内存,只是返回指向已经分配好的某段内存的一个指针。因此不能删除它,但需要调用对象的析构函数。如果你想在已经分配的内存中创建一个对象,使用new时行不通的。也就是说placement new允许你在一个已经分配好的内存中(栈或者堆中)构造一个新的对象。原型中void* p实际上就是指向一个已经分配好的内存缓冲区的的首地址。

1
placement new函数形式:void *operator new( size_t, void * p ) throw() { return p; }

16.sizeof

(1)空对象的大小为1个字节
(2)编译器内存对齐
(3)继承
(4)虚函数的影响
参考:https://www.cnblogs.com/wxquare/p/6675523.html 学习C++对象模型

17.编译器内存对齐

  现代计算机中内存空间都是按照 byte 划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但是实际的计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐。假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的联系四个字节地址中,该处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器.这需要做很多工作。因此编译器内存对齐是为了提高数据读写的效率。每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。gcc中默认#pragma pack(4),可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。

18.浅拷贝和深拷贝

  对于含有堆内存的对象,浅拷贝只是对指针的拷贝,拷贝后两个指针指向同一个内存空间,深拷贝对指针所指向的内容进行拷贝。默认拷贝构造函数为浅拷贝。

19.friend友元函数和友元类

  C++中使用类对数据进行了隐藏和封装,类的数据成员一般都定义为私有成员,成员函数一般都定义为公有的,以此提供类与外界的通讯接口。但是,有时需要定义一些函数,这些函数不是类的一部分,但又需要频繁地访问类的数据成员,这时可以将这些函数定义为该函数的友元函数。除了友元函数外,还有友元类,两者统称为友元。友元的作用是提高了程序的运行效率(即减少了类型检查和安全性检查等都需要时间开销),但它破坏了类的封装性和隐藏性,使得非成员函数可以访问类的私有成员。实际中这一特性很少使用。
参考:https://www.cnblogs.com/wxquare/p/5015440.html

20.类的初始化列表

(1)初始化列表是C++11中新增的类成员初始化方式。
(2)没有默认构造函数的类自定义类型成员必须使用初始化列表
(3)const成员、引用类型成员必须使用初始化列表。
(4)初始化列表中初始化初始化的顺序与成员定义的顺序相同,与初始化列表的顺序无关
初始化列表的优点:主要是对于自定义类型,初始化列表是作用在函数体之前,他调用构造函数对对象进行初始化。然而在函数体内,需要先调用构造函数,然后进行赋值,这样效率就不如初始化列表

21. 重载、覆盖和重写

(1) 重载(overload):同类中同名函数,参数的类型、个数或者返回类型不同。与virtual无关。(同类中)
(2) 覆盖(override):基类函数virtual函数,派生类中重写该函数,函数名称和参数完全相同。(基类和派生类中,基类函数virtual函数)
(3) 重写(overwrite):派生类的函数屏蔽了与其同名的基类函数,与virtual无关,是一种派生类和基类之间同名覆盖

22. 继承/多继承/虚继承

关于继承这个问题,不同语言有自己的设计思路,有的支持继承,单继承。有的支持组合,C++支持多继承。
(1) 单继承、多继承,继承时构造函数和析构函数的调用顺序。C++支持多继承。
(2) 继承方式,public/protected/private,默认为private继承,通常为public继承
(3) 友元函数不能被继承,那么基类的友元函数是不能被派生类继承
(4) 静态成员和静态成员函数是可以继承的
(5) 虚继承的概念
(6) C++ 对象内存模型:https://www.cnblogs.com/wxquare/p/6675523.html
  虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如下:虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。

1
2
3
4
class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;

谷歌编码规范:

  1. 使用组合常常比使用继承更合理. 如果使用继承的话, 定义为 public 继承.
  2. C++ 实践中, 继承主要用于两种场合: 实现继承, 子类继承父类的实现代码; 接口继承, 子类仅继承父类的方法名称.
  3. 必要的话, 析构函数声明为 virtual. 如果你的类有虚函数, 则析构函数也应该为虚函数
  4. 真正需要用到多重实现继承的情况少之又少. 只在以下情况我们才允许多重继承: 最多只有一个基类是非抽象类; 其它基类都是以 Interface 为后缀的 纯接口类.

23. virtual虚

C++中的virtual虚问题是一大难点,需要掌握以下几点:
(1) 虚基类成员函数,派生类override这个虚函数,虚成员函数可以实现多态性(多态)
(2) 虚析构函数,在继承关系中,基类的构造函数经常为虚函数,这是因为当用一个基类的指针删除一个派生类的对象时,如果基类的析构函数不是虚函数,派生类的析构函数不会被调用,造成内存泄露。因此对于含有虚函数的类的析构函数一般为虚函数。
(3) 虚函数的实现,虚函数表和虚函数指针,参考C++内存模型。
(4) 虚函数的动态绑定机制与运行期多态。参考:https://www.cnblogs.com/wxquare/p/5017326.html
(5) 虚继承,在多重继承关系中,为了避免菱形继承导致的资源浪费,会使用虚继承。
(6) 虚继承的实现,虚基表,参考C++内存模型。
(7) 内敛函数不能为虚函数,因为内敛函数时静态的
(8) 静态函数不能为虚函数,因为静态函数属于类,不属于对象
(9) 构造函数不能为虚函数,需要构造函数初始化虚函数表
(10) 纯虚函数,类似于的接口的作用。

24.C++对象模型

https://www.cnblogs.com/wxquare/p/5017326.html

25.C++11中的移动语义

C++中的拷贝语义和移动语义,右值引用和移动语义?C++11右值引用和移动语义 对含堆内存类的临时对象的拷贝和赋值函数的优化,使的深拷贝转化为浅拷贝。拷贝语义和移动语义
参考:https://www.cnblogs.com/wxquare/p/6836271.html

26.C++11智能指针

参考:https://www.cnblogs.com/wxquare/p/4759020.html

27.模板编程

C++模板编程问题?模板,函数模板,类模板,C++ 类模板碰到static,每个类型一个static值,C++ 类中不能包含虚函数模板,类模板可以包含虚函数?模板的声明和实现为何要放在头文件中?
C++中模板与泛型编程:https://www.cnblogs.com/wxquare/p/4743180.html
模板的声明和实现为什么要放在头文件中?
https://www.cnblogs.com/wanyao/archive/2011/06/29/2093588.html
什么是模板元编程?

28. 访函数/函数指针/lamda表达式

(1)函数对象(function object)又叫仿函数(functor),就是重载了调用运算符()的类,所生成的对象,就叫做函数对象/仿函数。因为重载了()之后,我们就能像函数一样去使用这个类,同时类里面又可以储存一些信息,所以要比普通的函数更加灵活
(2)函数指针也是一个函数对象,因为指针在C++中都是对象。
(3)lamda表达式
访函数和函数指针的区别,哪个效率更高?

29. C++异常处理

为什么C++很少使用异常处理?
https://www.zhihu.com/question/22889420

  1. 返回错误码
  2. 断言
  3. 异常处理Exception。代价是产生的二进制文件大小的增加,因为异常产生的位置决定了需要如何做栈展开(stack unwinding),这些数据需要存储在表里。典型情况,使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升。C++ 由于本身是强调实时性、高性能、低开销的语言,异常在某些使用场景下会被人诟病。然而,异常对表达性的改进是巨大的。因而,除非项目有特别严苛的实时性、空间之类的限制,使用异常应当是缺省选择。
    构造函数可以抛异常,析构函数不能抛异常?
    https://www.cnblogs.com/fly1988happy/archive/2012/04/11/2442765.html
    C++标准中假定了析构函数中不应该,也不永许抛出异常的。通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题

30. RTTI

RTTI是”Runtime Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。
实现机制是虚函数和虚函数表
谷歌禁用使用 RTTI.
在运行时判断类型通常意味着设计问题. 如果你需要在运行期间确定一个对象的类型, 这通常说明你需要考虑重新设计你的类.
随意地使用 RTTI 会使你的代码难以维护. 它使得基于类型的判断树或者 switch 语句散布在代码各处. 如果以后要进行修改, 你就必须检查它们

31. 前置声明(forward declaration)

所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明,没伴随着其定义
尽可能地避免使用前置声明。使用 #include 包含需要的头文件即可。

32. #include头文件的顺序

  1. C 系统文件
  2. C++ 标准库文件
  3. 其它库的.h文件
  4. 本项目内的的. 文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <sys/types.h>
    #include <unistd.h>

    #include <hash_map>
    #include <vector>

    #include "base/basictypes.h"
    #include "base/commandlineflags.h"
    #include "foo/public/bar.h"

33. 命名空间namespace

  1. 命名空间将全局作用域细分为独立的, 具名的作用域, 可有效防止全局作用域的命名冲突
  2. 不应该使用 using 指示 引入整个命名空间的标识符号
  3. 禁止用内联命名空间
1
2
// 禁止 —— 污染命名空间
using namespace foo;
1
2
3
4
5
6
7
8
9
10
11
12
// .h 文件
namespace mynamespace {

// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};

} // namespace mynamespace
1
2
3
4
5
6
7
8
9
10
11
12
// .h 文件
namespace mynamespace {

// 所有声明都置于命名空间中
// 注意不要使用缩进
class MyClass {
public:
...
void Foo();
};

} // namespace mynamespace

34. 接口类

接口是指满足特定条件的类, 这些类以 Interface 为后缀 (不强制).

35. 函数使用引用参数

  1. 在 C 语言中, 如果函数需要修改变量的值, 参数必须为指针, 如 int foo(int *pval). 在 C++ 中, 函数还可以声明为引用参数: int foo(int &val).
  2. 定义引用参数可以防止出现 (*pval)++ 这样丑陋的代码. 引用参数对于拷贝构造函数这样的应用也是必需的. 同时也更明确地不接受空指针.
  3. 函数参数列表中, 所有引用参数都必须是 const

36. 函数返回值后置语法

只有在常规写法 (返回类型前置) 不便于书写或不便于阅读时使用返回类型后置语法.
C++11引入了后置返回值的语法:
auto foo(int x) -> int;
后置返回类型是显式地指定 Lambda 表达式 的返回值的唯一方式. 某些情况下, 编译器可以自动推导出 Lambda 表达式的返回类型, 但并不是在所有的情况下都能实现. 即使编译器能够自动推导, 显式地指定返回类型也能让读者更明了.有时在已经出现了的函数参数列表之后指定返回类型, 能够让书写更简单, 也更易读, 尤其是在返回类型依赖于模板参数时. 例如:

1
template <class T, class U> auto add(T t, U u) -> decltype(t + u);

37. 流

  1. 流用来替代 printf() 和 scanf()
  2. 有了流, 在打印时不需要关心对象的类型. 不用担心格式化字符串与参数列表不匹配 (虽然在 gcc 中使用 printf 也不存在这个问题). 流的构造和析构函数会自动打开和关闭对应的文件
  3. 不要使用流, 除非是日志接口需要. 使用 printf 之类的代替.

38.前置自增和自减

不考虑返回值的话, 前置自增 (++i) 通常要比后置自增 (i++) 效率更高. 因为后置自增 (或自减) 需要对表达式的值 i 进行一次拷贝. 如果 i 是迭代器或其他非数值类型, 拷贝的代价是比较大的. 既然两种自增方式实现的功能一样, 为什么不总是使用前置自增呢?

39.constexpr

  1. 在 C++11 里,用 constexpr 来定义真正的常量,或实现常量初始化
  2. 变量可以被声明成 constexpr 以表示它是真正意义上的常量,即在编译时和运行时都不变。函数或构造函数也可以被声明成 constexpr, 以用来定义 constexpr 变量
  3. 靠 constexpr 特性,方才实现了 C++ 在接口上打造真正常量机制的可能。好好用 constexpr 来定义真・常量以及支持常量的函数。避免复杂的函数定义,以使其能够与constexpr一起使用。 千万别痴心妄想地想靠 constexpr 来强制代码

40. Lambda 表达式

  1. 适当使用 lambda 表达式。别用默认 lambda 捕获,所有捕获都要显式写出来。
  2. Lambda 表达式是创建匿名函数对象的一种简易途径,常用于把函数当参数传,例如:
    1
    2
    3
    std::sort(v.begin(), v.end(), [](int x, int y) {
    return Weight(x) < Weight(y);
    });

41 .命名

  1. 文件名:http_server_logs.h,http_server_logs.cc
  2. 类型性:MyExcitingClass
  3. 变量名:string table_name
  4. 函数名:AddTableEntry()
  5. 命名空间:websearch::index

C/C++ 常见问题

  1. 如何让类对象只在栈(堆)上分配空间?https://blog.csdn.net/hxz_qlh/article/details/13135433
    只能在栈上建立对象:

  2. C++不可继承类的实现?
    https://www.cnblogs.com/wxquare/p/7280025.html

  3. 如何定义和实现一个类的成员函数为回调函数?
    友元函数/静态成员函数消除this指针的影响

  4. C++复制构造函数的参数为什么是引用类型?

  • 编译时报错
  • 需要首先调用该类的拷贝构造函数来初始化形参(局部对象),造成无线循环递归
  1. C++全局对象如何在main函数之前构造和析构?
    https://blog.csdn.net/iyangyoulei/article/details/46925973

其它

 STL是C++程序重要的组成部分,这里主要记录工作中遇到的问题,目标是熟悉C++的两个网站和两本书籍:

  1. http://www.cplusplus.com/reference/
  2. https://en.cppreference.com/w/
  3. 《Effective STL》书籍
  4. 《STL源码分析》书籍

1. 熟悉STL中17种容器及其背后对应的数据结构

2. map和unordered_map的区别

  1. map背后是红黑树,unordered_map背后是哈希表
  2. map是key值有序的,unordered_map是key值无序的
  3. 两者内存消耗差不多,但是插入/查找/删除效率unordered_map是map的2到3倍
  4. unordered_map是通过链地址法解决冲突的
  5. std::map [] operator 和 insert 的区别。如果key已经存在,[] operator会将key对应的value用新值替换,而insert会返回一个pair说这组元素已经存在,如果key不存在,二者效果相同

4. priority_queue优先队列的实现

  priority_queue 优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。在优先队列中,没有 front() 函数与 back() 函数,而只能通过 top() 函数来访问队首元素(也可称为堆顶元素),也就是优先级最高的元素。基本操作有:

  • empty() 如果队列为空返回真
  • pop() 删除对顶元素
  • push() 加入一个元素
  • size() 返回优先队列中拥有的元素个数
  • top() 返回优先队列对顶元素
  • priority_queue 默认为大顶堆,即堆顶元素为堆中最大元素(比如:在默认的int型中先出队的为较大的数)。

5. 迭代器和迭代器失效iterator

  为了提高C++编程的效率,STL中提供了许多容器,包括vector、list、map、set等。有些容器例如vector可以通过脚标索引的方式访问容器里面的数据,但是大部分的容器不能使用这种方式,例如list、map、set。STL中每种容器在实现的时候设计了一个内嵌的iterator类,不同的容器有自己专属的迭代器,使用迭代器来访问容器中的数据。除此之外,通过迭代器,可以将容器和通用算法结合在一起,只要给予算法不同的迭代器,就可以对不同容器执行相同的操作,例如find查找函数。迭代器对指针的一些基本操作如*、->、++、==、!=、=进行了重载,使其具有了遍历复杂数据结构的能力,其遍历机制取决于所遍历的数据结构,所有迭代的使用和指针的使用非常相似。通过begin,end函数获取容器的头部和尾部迭代器,end 迭代器不包含在容器之内,当begin和end返回的迭代器相同时表示容器为空。
  容器的插入insert和erase操作可能导致迭代器失效,对于erase操作不要使用操作之前的迭代器,因为erase的那个迭代器一定失效了,正确的做法是返回删除操作时候的那个迭代器。
参考:https://www.cnblogs.com/wxquare/p/4699429.html

6. 容器的线程安全性Thread safety

  STL为了效率,没有给所有操作加锁。不同线程同时读同一容器对象没关系,不同线程同时写不同的容器对象没关系。但不能同时又读又写同一容器对象的。因此,多线程要同时读写时,还是要自己加锁。

7. STL 排序

  1. sort,快排加插入排序
  2. stable_sort,稳定排序
  3. sort_heap,堆排序
  4. list.sort,链表归并排序
    https://www.cnblogs.com/wxquare/p/4922733.html

8. STL容器的内存管理方式

9. vector和map的内存释放

  1. STL容器的内存管理方式?

其它常见的问题

  1. vector和map的内存释放问题?容器删除数据的时候注意迭代器失效?vector和map正确的内存释放?

  2. C++ 的iostream 的局限
    根据以上分析,我们可以归纳 iostream 的局限:输入方面,istream 不适合输入带格式的数据,因为“纠错”能力不强,进一步的分析请见孟岩写的《契约思想的一个反面案例》,孟岩说“复杂的设计必然带来复杂的使用规则,而面对复杂的使用规则,用户是可以投票的,那就是你做你的,我不用!”可谓鞭辟入里。如果要用 istream,我推荐的做法是用 getline() 读入一行数据,然后用正则表达式来判断内容正误,并做分组,然后用 strtod/strtol 之类的函数做类型转换。这样似乎更容易写出健壮的程序。输出方面,ostream 的格式化输出非常繁琐,而且写死在代码里,不如 stdio 的小语言那么灵活通用。建议只用作简单的无格式输出。log 方面,由于 ostream 没有办法在多线程程序中保证一行输出的完整性,建议不要直接用它来写 log。如果是简单的单线程程序,输出数据量较少的情况下可以酌情使用。当然,产品代码应该用成熟的 logging 库,而不要用其它东西来凑合。in-memory 格式化方面,由于 ostringstream 会动态分配内存,它不适合性能要求较高的场合。文件 IO 方面,如果用作文本文件的输入或输出,(i|o)fstream 有上述的缺点;如果用作二进制数据输入输出,那么自己简单封装一个 File class 似乎更好用,也不必为用不到的功能付出代价(后文还有具体例子)。ifstream 的一个用处是在程序启动时读入简单的文本配置文件。如果配置文件是其他文本格式(XML 或 JSON),那么用相应的库来读,也用不到 ifstream。性能方面,iostream 没有兑现“高效性”诺言。iostream 在某些场合比 stdio 快,在某些场合比 stdio 慢,对于性能要求较高的场合,我们应该自己实现字符串转换(见后文的代码与测试)。iostream 性能方面的一个注脚:在线 ACM/ICPC 判题网站上,如果一个简单的题目发生超时错误,那么把其中 iostream 的输入输出换成 stdio,有时就能过关。
    既然有这么多局限,iostream 在实际项目中的应用就大为受限了,在这上面投入太多的精力实在不值得。说实话,我没有见过哪个 C++ 产品代码使用 iostream 来作为输入输出设施。

  3. STL::list::sort链表归并排序


title: C/C++程序的项目构建、编译、调试工具和方法
categories:
- 计算机基础

  在Linux C/C++项目实践中,随和项目越来越复杂,第三方依赖项的增加,有时会遇到一些编译、链接和调试问题,这里总结一下遇到的问题、解决的办法和使用到的工具:

  1. 了解gcc/g++编译过程、常见和编译选项解决编译过程遇到的问题
  2. 了解链接过程、动态链接、静态链接,解决链接过程中遇到的问题
  3. 解决程序运行出现的依赖问题、符号未定义问题
  4. 学习会使用gdb调试一些基本问题
  5. 学会使用makefile和cmake工具构建项目

一、排查编译问题常用工具

1. gcc/g++的区别和使用

  1. 后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序;后缀为.cpp的,两者都会认为是c++程序,注意,虽然c++是c的超集,但是两者对语法的要求是有区别的
  2. 对于C代码,编译和链接都使用gcc
  3. 对于C++代码,编译时可以使用gcc/g++,gcc实际也是调用g++;链接时gcc 不能自动和C++使用库链接,因此要使用g++或者gcc -lstdc++

2. 常见gcc编译链接选项

  • -c 只编译并生成目标文件
  • -g 生成调试信息,gdb可以利用该调试信息
  • -o 指定生成的输出文件,可执行程序或者动态链接库文件名
  • -I 编译时添加头文件路径
  • -L 链接时添加库文件路径
  • -D 定义宏,常用于开关控制代码
  • -shared 用于生成共享库.so
  • -Wall 显示所有警告信息,-w不生成任何警告信息
  • -O0选项不进行任何优化,debug会产出和程序预期的结果;O1优化会消耗少多的编译时间,它主要对代码的分支,常量以及表达式等进行优化;O2会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。 通常情况下线上代码至少加上O2优化选项。
  • -fPIC 位置无关选项,生成动态库时使用,实现真正意义上的多进程共享的.so库。
  • -Wl选项告诉编译器将后面的参数传递给链接器
  • -Wl,-Bstatic,指明后面是链接今静态库
  • -Wl,-Bdynamic,指明后面是链接动态库

3. 编译时添加头文件依赖路径

  -include用来包含头文件,但一般情况下包含头文件都在源码里用#include xxxxxx实现,-include参数很少用。-I参数是用来指定头文件目录,/usr/include目录一般是不用指定的,gcc知道去那里找,但 是如果头文件不在/usr/include里我们就要用-I参数指定了,比如头文件放在/myinclude目录里,那编译命令行就要加上-I /myinclude参数了,如果不加你会得到一个”xxxx.h: No such file or directory”的错误。-I参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定。

二、排查链接问题常用工具

  1. 查看ld链接器的搜索顺序 ld –verbose | grep SEARCH
  2. 链接时指定链接目录 -L/dir
  3. -Wl,-Bstatic,指明后面是链接今静态库
  4. -Wl,-Bdynamic,指明后面是链接动态库
  5. 运行时找不到动态库so文件,设置LD_LIBRARY_PATH,添加依赖so文件所在路径
  6. 链接完成后使用ldd查看动态库依赖关系,如果依赖的某个库找不到,通过这个命令可以迅速定位问题所在
  7. ldd -r,帮助检查是否存在未定义的符号undefine symbol,so库链接状态和错误信息

三、gdb调试基本使用

1. 对C/C++程序的调试,需要在编译前就加上-g选项。

  1. $gdb
  2. 设置参数:set args 可指定运行时参数。(如:set args 10 20 30 40 50)

2. 查看源代码

  • list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。
  • list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12
  • list 函数名:将显示“函数名”所在函数的源代码,如:list main
  • list :不带参数,将接着上一次 list 命令的,输出下边的内容

3. 设置断点和关闭断点

  • break n (简写b n): 在第n行处设置断点(可以带上代码路径和代码名称: b test.cpp:578)
  • break func(简写b func): 在函数func()的入口处设置断点,如:break test_func
  • info b (info breakpoints):显示当前程序的断点设置情况
  • delete 断点号n:删除第n个断点
  • disable 断点号n:暂停第n个断点
  • clear 行号n:清除第n行的断点

4. 程序调试运行

  • run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令。
  • continue (简写c ):继续执行,到下一个断点处(或运行结束)
  • next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
  • step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
  • until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。
  • until+行号: 运行至某行,不仅仅用来跳出循环
  • finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
  • call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
  • quit:简记为 q ,退出gdb

5. 打印程序运行的调试信息

  • print 表达式:简记为 p ,其中“表达式”可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么“表达式”可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用。
  • print a:将显示整数 a 的值
  • print name:将显示字符串 name 的值
  • print gdb_test(22):将以整数22作为参数调用 gdb_test() 函数
  • print gdb_test(a):将以变量 a 作为参数调用 gdb_test() 函数
  • 扩展info locals: 显示当前堆栈页的所有变量

6. 查询运行信息

  • where/bt :当前运行的堆栈列表;
  • bt backtrace 显示当前调用堆栈
  • up/down 改变堆栈显示的深度
  • set args 参数:指定运行时的参数
  • show args:查看设置好的参数
  • info program: 来查看程序的是否在运行,进程号,被暂停的原因。

四、gdb调试coredump问题

  Coredump叫做核心转储,它是进程运行时在突然崩溃的那一刻的一个内存快照。操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下,会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里。该文件也是二进制文件,可以使用gdb调试。虽然我们知道进程在coredump的时候会产生core文件,但是有时候却发现进程虽然core了,但是我们却找不到core文件。在ubuntu系统中需要进行设置,ulimit -c 可以设置core文件的大小,如果这个值为0.则不会产生core文件,这个值太小,则core文件也不会产生,因为core文件一般都比较大。使用ulimit -c unlimited来设置无限大,则任意情况下都会产生core文件。
  gdb打开core文件时,有显示没有调试信息,因为之前编译的时候没有带上-g选项,没有调试信息是正常的,实际上它也不影响调试core文件。因为调试core文件时,符号信息都来自符号表,用不到调试信息。如下为加上调试信息的效果。
调试步骤:
$gdb program core_file 进入
$ bt或者where # 查看coredump位置
当程序带有调试信息的情况下,我们实际上是可以看到core的地方和代码行的匹配位置。但往往正常发布环境是不会带上调试信息的,因为调试信息通常会占用比较大的存储空间,一般都会在编译的时候把-g选项去掉。这种情况啊也是可以通过core_dump文件找到错误位置的,但这个过程比较复杂,参考:https://blog.csdn.net/u014403008/article/details/54174109

五、gdb调试线上死锁问题

  如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试。对于服务进程,我们除了使用gdb调试之外,还可以使用pstack跟踪进程栈。这个命令在排查进程问题时非常有用,比如我们发现一个服务一直处于work状态(如假死状态,好似死循环),使用这个命令就能轻松定位问题所在;可以在一段时间内,多执行几次pstack,若发现代码栈总是停在同一个位置,那个位置就需要重点关注,很可能就是出问题的地方。gdb比pstack更加强大,gdb可以随意进入进程、线程中改变程序的运行状态和查看程序的运行信息。思考:如何调试死锁?
$gdb
$pstack pid

六、undefined symbol问题解决步骤

  1. file 检查so或者可执行文件的架构

    1
    2
    $ file _visp.so 
    _visp.so: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=6503ba6b7545e38e669ab9ed31f86449d8a5f78b, stripped
  2. ldd -r _visp.so 命令查看so库链接状态和错误信息

    1
    2
    undefined symbol: __itt_api_version_ptr__3_0	(./_visp.so)
    undefined symbol: __itt_id_create_ptr__3_0 (./_visp.so)
  3. c++filt symbol 定位错误在那个C++文件中

    1
    2
    base) terse@ubuntu:~/code/terse-visp$ c++filt __itt_domain_create_ptr__3_0
    __itt_domain_create_ptr__3_0
  4. 还可以使用grep -R __itt_domain_create_ptr__3_0 ./
    最终发现这个符号来自XXX/opencv-3.4.6/build/share/OpenCV/3rdparty/libittnotify.a

  5. 通过nm命令也能看出该符号确实未定义

    1
    2
    $ nm _visp.so | grep __itt_domain_create_ptr__3_0
    U __itt_domain_create_ptr__3_0

七、pkg-config 找第三方库的头文件和库文件

pkg-config能方便使用第三方库和头文件和库文件,其运行原理

  • 它首先根据PKG_CONFIG_PATH环境变量下寻找库对应的pc文件
  • 然后从pc文件中获取该库对应的头文件和库文件的位置信息

例如在项目中需要使用opencv库,该库包含的头文件和库文件比较多

  • 首先查看是否有对应的opencv.pc find /usr -name opencv.pc
  • 查看该路径是否包含在PKG_CONFIG_PATH
  • 使用pkg-config –cflags –libs opencv 查看库对应的头文件和库文件信息
  • pkg-config –modversion opencv 查看版本信息
    参考链接:https://blog.csdn.net/luotuo44/article/details/24836901

八、cmake中的find_package

https://www.jianshu.com/p/46e9b8a6cb6a
find_package原理
首先明确一点,cmake本身不提供任何搜索库的便捷方法,所有搜索库并给变量赋值的操作必须由cmake代码完成,比如下面将要提到的FindXXX.cmake和XXXConfig.cmake。只不过,库的作者通常会提供这两个文件,以方便使用者调用。
find_package采用两种模式搜索库:

Module模式:搜索CMAKE_MODULE_PATH指定路径下的FindXXX.cmake文件,执行该文件从而找到XXX库。其中,具体查找库并给XXX_INCLUDE_DIRS和XXX_LIBRARIES两个变量赋值的操作由FindXXX.cmake模块完成。

Config模式:搜索XXX_DIR指定路径下的XXXConfig.cmake文件,执行该文件从而找到XXX库。其中具体查找库并给XXX_INCLUDE_DIRS和XXX_LIBRARIES两个变量赋值的操作由XXXConfig.cmake模块完成。

两种模式看起来似乎差不多,不过cmake默认采取Module模式,如果Module模式未找到库,才会采取Config模式。如果XXX_DIR路径下找不到XXXConfig.cmake文件,则会找/usr/local/lib/cmake/XXX/中的XXXConfig.cmake文件。总之,Config模式是一个备选策略。通常,库安装时会拷贝一份XXXConfig.cmake到系统目录中,因此在没有显式指定搜索路径时也可以顺利找到。

九、ldd解决运行时问题

现象

  • error while loading shared libraries: libopencv_cudabgsegm.so.3.4: cannot open shared object file: No such file or directory

  • ldd ./xxx,发现库文件not found

    libopencv_cudaobjdetect.so.3.4 => not found  
    libopencv_cudalegacy.so.3.4 => not found
    

ld.so 动态共享库搜索顺序

  1. ELF可执行文件中动态段DT_RPATH指定;gcc加入链接参数“-Wl,-rpath”指定动态库搜索路径;
  2. 环境变量LD_LIBRARY_PATH指定路径;
  3. /etc/ld.so.cache中缓存的动态库路径。可以通过修改配置文件/etc/ld.so.conf 增删路径(修改后需要运行ldconfig命令);
  4. 默认的 /lib/;
  5. 默认的 /usr/lib/

解决办法

  • 确认系统中是包含这个库文件的
  • pkg-config –libs opencv 查看opencv库的路径
  • export LD_LIBRARY_PATH=/usr/local/lib64,增加运行时加载路径

参考链接:https://www.cnblogs.com/amyzhu/p/8871475.html

十、makefile和cmake的使用

其它问题

  1. c++进程内存空间分布
  2. ELF是什么?其大小与程序中全局变量的是否初始化有什么关系(注意.bss段)、elf文件格式和运行时内存布局
  3. 标准库函数和系统调用的区别
  4. 编译器内存对齐和内存对齐的原理
  5. 编译器如何区分C和C++?
  6. C++动态链接库和静态链接库?如何创建和使用静态链接库和动态链接库?(fPIC, shared)
  7. 如何判断计算机的字节序是大端还是小端的?
  8. 预编译、编译、汇编、链接
  9. GDB的基本工作原理是什么?和断点调试的实现原理:在程序中设置断点,现将该位置原来的指令保存,然后向该位置写入int 3,当执行到int 3的时候,发生软中断。内核会给子进程发出sigtrap信号,当然这个信号首先被gdb捕获,gdb会进行断点命中判定,如果命中的话就会转入等待用户输入进行下一步的处理,否则继续运行,替换int 3,恢复执行
  10. gdb调试、coredump、调试运行中的程序?通过ptrace让父进程可以观察和控制其它进程的执行,检查和改变其核心映像以及寄存器,主要通过实现断电调试和系统调用跟踪。
  11. 编译器的编译过程?链接的时候做了什么事?在中间层优化时怎么做?编译。词法分析、句法分析、语义分析生成中间的汇编代码。汇编,链接:静态链接库、动态链接库
  12. gcc 和 g++的区别
  13. 项目构建工具makefile、cmake
  14. 预处理:#include文件、条件预编译指令、注释。保留#pargma编译器指令
  15. valgrind(内存、堆栈、函数调用、多线程竞争、缓存,可扩展),valgrind内存检查的原理、和具体使用!
  16. C++内存管理:内存布局、堆栈的区别、内存操作四个原则、内存泄露检查、智能指针、STL内存管理(内存池)
  17. gdb调试多进程和多线程命令

参考:
[1]. gdb 调试利器:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html
[2]. 陈皓专栏gdb调试系列:https://blog.csdn.net/haoel/article/details/2879
[3]. gdb core_dump调试:https://blog.csdn.net/u014403008/article/details/54174109
[4]. 进程调试,死循环和死锁卡死:https://blog.csdn.net/guowenyan001/article/details/46238355


title: C/C++程序性能分析的工具
categories:
- 计算机基础

  C++代码编译测试完成功能之后,有时会遇到一些性能问题,此时需要学会使用一些工具对其进行性能分析,找出程序的性能瓶颈,然后进行优化,基本需要掌握下面几个命令:

  1. time分析程序的执行时间
  2. top观察程序资源使用情况
  3. perf/gprof进一步分析程序的性能
  4. 内存问题与valgrind
  5. 自己写一个计时器,计算局部函数的时间

一、time

1.shell time。

  time非常方便获取程序运行的时间,包括用户态时间user、内核态时间sys和实际运行的时间real。我们可以通过(user+sys)/real计算程序CPU占用率,判断程序时CPU密集型还是IO密集型程序。
$time ./kcf2.0 ../data/bag.mp4 312 146 106 98 1 196 result.csv 1
real 0m2.065s
user 0m4.598s
sys 0m0.907s
cpu使用率:(4.598+0.907)/2.065=267%
视频帧数196,196/2.065=95

2./usr/bin/time

  Linux中除了shell time,还有/usr/bin/time,它能获取程序运行更多的信息,通常带有-v参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ /usr/bin/time -v  ./kcf2.0 ../data/bag.mp4 312 146 106 98 1 196 result.csv 1
User time (seconds): 4.28 # 用户态时间
System time (seconds): 1.11 # 内核态时间
Percent of CPU this job got: 279% # CPU占用率
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:01.93
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 63980 # 最大内存分配
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 0
Minor (reclaiming a frame) page faults: 19715 # 缺页异常
Voluntary context switches: 3613 # 上下文切换
Involuntary context switches: 295682
Swaps: 0
File system inputs: 0
File system outputs: 32
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0

二、top

top是linux系统的任务管理器,它既能看系统所有任务信息,也能帮助查看单个进程资源使用情况。
主要有以下几个功能:

  1. 查看系统任务信息:
    Tasks: 87 total, 1 running, 86 sleeping, 0 stopped, 0 zombie
  2. 查看CPU使用情况
    Cpu(s): 0.0%us, 0.2%sy, 0.0%ni, 99.7%id, 0.0%wa, 0.0%hi, 0.0%si, 0.2%st
  3. 查看内存使用情况
    Mem: 377672k total, 322332k used, 55340k free, 32592k buffers
  4. 查看单个进程资源使用情况
    • PID:进程的ID
    • USER:进程所有者
    • PR:进程的优先级别,越小越优先被执行
    • NInice:值
    • VIRT:进程占用的虚拟内存
    • RES:进程占用的物理内存
    • SHR:进程使用的共享内存
    • S:进程的状态。S表示休眠,R表示正在运行,Z表示僵死状态,N表示该进程优先值为负数
    • %CPU:进程占用CPU的使用率
    • %MEM:进程使用的物理内存和总内存的百分比
    • TIME+:该进程启动后占用的总的CPU时间,即占用CPU使用时间的累加值。
    • COMMAND:进程启动命令名称
  5. 除此之外top还提供了一些交互命令:
    • q:退出
    • 1:查看每个逻辑核
    • H:查看线程
    • P:按照CPU使用率排序
    • M:按照内存占用排序

参考:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/top.html

三、perf

参考:https://www.ibm.com/developerworks/cn/linux/l-cn-perf1/index.html
参考:https://zhuanlan.zhihu.com/p/22194920

1. perf stat

  做任何事都最好有条有理。老手往往能够做到不慌不忙,循序渐进,而新手则往往东一下,西一下,不知所措。面对一个问题程序,最好采用自顶向下的策略。先整体看看该程序运行时各种统计事件的大概,再针对某些方向深入细节。而不要一下子扎进琐碎细节,会一叶障目的。有些程序慢是因为计算量太大,其多数时间都应该在使用 CPU 进行计算,这叫做 CPU bound 型;有些程序慢是因为过多的 IO,这种时候其 CPU 利用率应该不高,这叫做 IO bound 型;对于 CPU bound 程序的调优和 IO bound 的调优是不同的。如果您认同这些说法的话,Perf stat 应该是您最先使用的一个工具。它通过概括精简的方式提供被调试程序运行的整体情况和汇总数据。虚拟机上面有些参数不全面,cycles、instructions、branches、branch-misses。下面的测试数据来自服务器。
$time ./kcf2.0 ../data/bag.mp4 312 146 106 98 1 196 result.csv 1

1
2
3
4
5
6
7
8
9
10
11
12
 25053.120420      task-clock (msec)         #   17.196 CPUs utilized          
1,509,877 context-switches # 0.060 M/sec
3,427 cpu-migrations # 0.137 K/sec
34,025 page-faults # 0.001 M/sec
65,242,918,152 cycles # 2.604 GHz
0 stalled-cycles-frontend # 0.00% frontend cycles idle
0 stalled-cycles-backend # 0.00% backend cycles idle
64,695,693,541 instructions # 0.99 insns per cycle
8,049,836,066 branches # 321.311 M/sec
42,734,371 branch-misses # 0.53% of all branches

1.456907056 seconds time elapsed

2. perf top

  Perf top 用于实时显示当前系统的性能统计信息。该命令主要用来观察整个系统当前的状态,比如可以通过查看该命令的输出来查看当前系统最耗时的内核函数或某个用户进程。

3. perf record/perf report

  使用 top 和 stat 之后,这时对程序基本性能有了一个大致的了解,为了优化程序,便需要一些粒度更细的信息。比如说您已经断定目标程序计算量较大,也许是因为有些代码写的不够精简。那么面对长长的代码文件,究竟哪几行代码需要进一步修改呢?这便需要使用 perf record 记录单个函数级别的统计信息,并使用 perf report 来显示统计结果。您的调优应该将注意力集中到百分比高的热点代码片段上,假如一段代码只占用整个程序运行时间的 0.1%,即使您将其优化到仅剩一条机器指令,恐怕也只能将整体的程序性能提高 0.1%。俗话说,好钢用在刀刃上,要优化热点函数。

1
2
perf record – e cpu-clock ./t1 
perf report

增加-g参数可以获取调用关系

1
2
perf record – e cpu-clock – g ./t1 
perf report

$perf record -e cpu-clock -g ./kcf2.0 ../data/bag.mp4 312 146 106 98 1 196 result.csv 1
$perf report

经过perf的分析,我们的目标应该很明确了,cv::DFT和get_feature这两个函数比较耗时,另外还有一个和线程相关的操作也比较耗时,接下来要去分析代码,做代码级别的优化。

四、gprof

参考: https://blog.csdn.net/stanjiang2010/article/details/5655143

五、内存问题与valgrind

5.1常见的内存问题

  1. 使用未初始化的变量
    对于位于程序中不同段的变量,其初始值是不同的,全局变量和静态变量初始值为0,而局部变量和动态申请的变量,其初始值为随机值。如果程序使用了为随机值的变量,那么程序的行为就变得不可预期。
  2. 内存访问越界
    比如访问数组时越界;对动态内存访问时超出了申请的内存大小范围。
  3. 内存覆盖
    C 语言的强大和可怕之处在于其可以直接操作内存,C 标准库中提供了大量这样的函数,比如 strcpy, strncpy, memcpy, strcat 等,这些函数有一个共同的特点就是需要设置源地址 (src),和目标地址(dst),src 和 dst 指向的地址不能发生重叠,否则结果将不可预期。
  4. 动态内存管理错误
    常见的内存分配方式分三种:静态存储,栈上分配,堆上分配。全局变量属于静态存储,它们是在编译时就被分配了存储空间,函数内的局部变量属于栈上分配,而最灵活的内存使用方式当属堆上分配,也叫做内存动态分配了。常用的内存动态分配函数包括:malloc, alloc, realloc, new等,动态释放函数包括free, delete。一旦成功申请了动态内存,我们就需要自己对其进行内存管理,而这又是最容易犯错误的。下面的一段程序,就包括了内存动态管理中常见的错误。
    a. 使用完后未释放
    b. 释放后仍然读写
    c. 释放了再释放
  5. 内存泄露
    内存泄露(Memory leak)指的是,在程序中动态申请的内存,在使用完后既没有释放,又无法被程序的其他部分访问。内存泄露是在开发大型程序中最令人头疼的问题,以至于有人说,内存泄露是无法避免的。其实不然,防止内存泄露要从良好的编程习惯做起,另外重要的一点就是要加强单元测试(Unit Test),而memcheck就是这样一款优秀的工具

5.1 valgrind内存检测

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
#include <iostream>
using namespace std;


int main(int argc, char const *argv[])
{
int a[5];
a[0] = a[1] = a[3] = a[4] = 0;

int s=0;
for(int i=0;i<5;i++){
s+=a[i];
}
if(s == 0){
std::cout << s << std::endl;
}
a[5] = 10;
std::cout << a[5] << std::endl;


int *invalid_write = new int[10];
delete [] invalid_write;
invalid_write[0] = 3;

int *undelete = new int[10];

return 0;
}
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
==102507== Memcheck, a memory error detector
==102507== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==102507== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==102507== Command: ./a.out
==102507==
==102507== Conditional jump or move depends on uninitialised value(s)
==102507== at 0x1091F6: main (learn_valgrind.cpp:14)
==102507==
10
==102507== Invalid write of size 4
==102507== at 0x109270: main (learn_valgrind.cpp:23)
==102507== Address 0x4dc30c0 is 0 bytes inside a block of size 40 free'd
==102507== at 0x483A55B: operator delete[](void*) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==102507== by 0x10926B: main (learn_valgrind.cpp:22)
==102507== Block was alloc'd at
==102507== at 0x48394DF: operator new[](unsigned long) (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==102507== by 0x109254: main (learn_valgrind.cpp:21)
==102507==
==102507==
==102507== HEAP SUMMARY:
==102507== in use at exit: 40 bytes in 1 blocks
==102507== total heap usage: 4 allocs, 3 frees, 73,808 bytes allocated
==102507==
==102507== LEAK SUMMARY:
==102507== definitely lost: 40 bytes in 1 blocks
==102507== indirectly lost: 0 bytes in 0 blocks
==102507== possibly lost: 0 bytes in 0 blocks
==102507== still reachable: 0 bytes in 0 blocks
==102507== suppressed: 0 bytes in 0 blocks
==102507== Rerun with --leak-check=full to see details of leaked memory
==102507==
==102507== For counts of detected and suppressed errors, rerun with: -v
==102507== Use --track-origins=yes to see where uninitialised values come from
==102507== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

  1. https://www.ibm.com/developerworks/cn/linux/l-cn-valgrind/index.html
  2. http://senlinzhan.github.io/2017/12/31/valgrind/
  3. https://www.ibm.com/developerworks/cn/aix/library/au-memorytechniques.html

六、自定义timer计时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class timer {
public:
clock_t start;
clock_t end;
string name;
timer(string n) {
start = clock();
name = n;
}
~timer() {
end = clock();
printf("%s time: %f \n", name.c_str(),
(end - start) * 1.0 / CLOCKS_PER_SEC * 1000);
}
};

TCP和UDP协议

  1. tcp头格式,其20个字节包含哪些内容? udp头部格式,其8个字节分别包含哪些内容?

  2. 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度

  3. tcp和udp的区别以及应用场景

    • TCP是面向连接的,而UDP是不需要建立连接的
    • TCP 是一对一的两点服务,UDP 支持一对一、一对多、多对多的交互通信
    • 可靠性,TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。UDP 是尽最大努力交付,不保证可靠交付数据。
    • TCP有拥塞控制、流量控制
    • 首部开销,TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
    • 传输方式,TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序

    TCP 和 UDP 应用场景:由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于,FTP 文件传输HTTP / HTTPS,由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:包总量较少的通信,如 DNS 、SNMP 等视频、音频等多媒体通信广播通信

  4. TCP协议如何保证可靠传输?

    • 三次握手四次挥手确保连接的建立和释放
    • 超时重发:数据切块发送,等待确认,超时未确认会重发
    • 数据完整性校验:TCP首部中数据有端到端的校验和,接收方会校验,一旦出错将丢弃且不确认收到此报文
    • 根据序列码进行数据的排序和去重
    • 根据接收端缓冲区大小做流量控制
    • 根据网络环境做拥塞控制。当网络拥塞时,会减少数据的发送
  5. TCP怎么通过三次握手和四次挥手建立可靠连接以及需要注意的问题

    • 分别准确画出三次握手和四次挥手状态转换图 从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题
    • 为什么需要三次握手? 通过三次握手实现了同步序列号和避免了旧的重复连接初始化造成混乱,浪费服务器资源,两个作用
    • 为什么需要四次挥手?全双工通信
    • time_wait状态什么作用? 防止之前的报文造成新连接数据混乱,通过2msl使前一连接数据失效;确保ack报文发送给服务端。
  6. 超时重传和快速重传

    • 客户端通过定时器在指定时间内未发现会收到ack信息就认为进行超时重传
    • 客户端收到连续三个重复ack信息就会发起快速重传而不用等待超时重传
  7. 如何解决可能出现的乱序和重复数据问题

  8. TCP流量控制和滑动窗口

    • 为了提高数据传输的小路,tcp避免了一问一答式的消息传输策略
    • 通过累积确认ACK的方式提高效率
    • 在累积确认时通过接收窗口进行流量控制

  9. tcp拥塞控制和拥塞窗口?
    TCP拥塞控制

    • tcp在数据发送时会结合整个网络环境调整数据发送的速率
    • 发送者如何判断拥塞已经发生的?发送超时,或者说TCP重传定时器溢出;接收到重复的确认报文段
    • 快重传算法(接收端到失序的报文段立即重传、发送端一旦接收三个重复的确认报文段,立即重传,不用等定时器)
  10. TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看

  11. 什么是SYN攻击,怎么避免SYN攻击?

  • SYN攻击属于DOS攻击的一种,它利用TCP协议缺陷,通过发送大量的半连接请求,耗费CPU和内存资源。SYN攻击除了能影响主机外,还可以危害路由器、防火墙等网络系统,事实上SYN攻击并不管目标是什么系统,只要这些系统打开TCP服务就可以实施。从上图可看到,服务器接收到连接请求(syn=j),将此信息加入未连接队列,并发送请求包给客户(syn=k,ack=j+1),此时进入SYN_RECV状态。当服务器未收到客户端的确认包时,重发请求包,一直到超时,才将此条目从未连接队列删除。配合IP欺骗,SYN攻击能达到很好的效果,通常,客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
  1. 如何解决close_wait和time_wait过多的问题?

    • CLOSE_WAIT,只会发生在客户端先关闭连接的时候,但已经收到客户端的fin包,但服务器还没有关闭的时候会产生这个状态,如果服务器产生大量的这种连接一般是程序问题导致的,如部分情况下不会执行socket的close方法,解决方法是查程序
    • TIME_WAIT,time_wait是一个需要特别注意的状态,他本身是一个正常的状态,只在主动断开那方出现,每次tcp主动断开都会有这个状态的,维持这个状态的时间是2个msl周期(2分钟),设计这个状态的目的是为了防止我发了ack包对方没有收到可以重发。那如何解决出现大量的time_wait连接呢?千万不要把tcp_tw_recycle改成1,这个我再后面介绍,正确的姿势应该是降低msl周期,也就是tcp_fin_timeout值,同时增加time_wait的队列(tcp_max_tw_buckets),防止满了。
  2. 什么是TCP粘包,应用层怎么解决,http是怎么解决的。tcp是字节流,需要根据特殊字符和长度信息将消息分开

  3. udp协议怎么做可靠传输?
    由于在传输层UDP已经是不可靠的连接,那就要在应用层自己实现一些保障可靠传输的机制,简单来讲,要使用UDP来构建可靠的面向连接的数据传输,就要实现类似于TCP协议的,超时重传(定时器),有序接受 (添加包序号),应答确认 (Seq/Ack应答机制),滑动窗口流量控制等机制 (滑动窗口协议),等于说要在传输层的上一层(或者直接在应用层)实现TCP协议的可靠数据传输机制,比如使用UDP数据包+序列号,UDP数据包+时间戳等方法。目前已经有一些实现UDP可靠传输的机制,比如UDT(UDP-based Data Transfer Protocol)基于UDP的数据传输协议(UDP-based Data Transfer Protocol,简称UDT)是一种互联网数据传输协议。UDT的主要目的是支持高速广域网上的海量数据传输,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。 顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。 由于UDT完全在UDP上实现,它也可以应用在除了高速数据传输之外的其它应用领域,例如点到点技术(P2P),防火墙穿透,多媒体数据传输等等

  4. TCP 保活机制KeepAlive?其局限性?Http的keep-alive?为什么应用层也经常做心跳检查?

    • TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。TCP-Keepalive-HOWTO 有对 TCP KeepAlive 特性的详细介绍,有兴趣的同学可以参考。
    • TCP KeepAlive 的局限。首先 TCP KeepAlive 监测的方式是发送一个 probe 包,会给网络带来额外的流量,另外 TCP KeepAlive 只能在内核层级监测连接的存活与否,而连接的存活不一定代表服务的可用。例如当一个服务器 CPU 进程服务器占用达到 100%,已经卡死不能响应请求了,此时 TCP KeepAlive 依然会认为连接是存活的。因此 TCP KeepAlive 对于应用层程序的价值是相对较小的。需要做连接保活的应用层程序,例如 QQ,往往会在应用层实现自己的心跳功能。
      除了TCP自带的Keeplive机制,实现业务中经常在业务层面定制“心跳”功能,主要有以下几点考虑:
    • TCP自带的keepalive使用简单,仅提供连接是否存活的功能
    • 应用层心跳包不依赖于传输协议,支持tcp和udp
    • 应用层心跳包可以定制,可以应对更加复杂的情况或者传输一些额外的消息
    • Keepalive仅仅代表连接保持着,而心跳往往还表示服务正常工作
      在 HTTP 1.0 时期,每个 TCP 连接只会被一个 HTTP Transaction(请求加响应)使用,请求时建立,请求完成释放连接。当网页内容越来越复杂,包含大量图片、CSS 等资源之后,这种模式效率就显得太低了。所以,在 HTTP 1.1 中,引入了 HTTP persistent connection 的概念,也称为 HTTP keep-alive,目的是复用TCP连接,在一个TCP连接上进行多次的HTTP请求从而提高性能。HTTP1.0中默认是关闭的,需要在HTTP头加入”Connection: Keep-Alive”,才能启用Keep-Alive;HTTP1.1中默认启用Keep-Alive,加入”Connection: close “,才关闭。两者在写法上不同,http keep-alive 中间有个”-“符号。 HTTP协议的keep-alive 意图在于连接复用,同一个连接上串行方式传递请求-响应数据。TCP的keepalive机制意图在于保活、心跳,检测连接错误。
  5. TCP 协议性能问题分析?

    • TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差;
    • TCP 的三次握手带来了额外开销,这些开销不只包括需要传输更多的数据,还增加了首次传输数据的网络延迟;
    • TCP 的重传机制在数据包丢失时可能会重新传输已经成功接收的数据段,造成带宽的浪费;
  6. QUIC 是如何解决TCP 性能瓶颈的?

  7. 科普:QUIC协议原理分析

http和https

  1. HTTP协议协议格式详解

    • 请求行(request line)。请求方法、域名、协议版本。
    • 请求头部(header)从第二行起为请求头部,Host指出请求的目的地(主机域名);User-Agent是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送
    • 空行
    • 请求数据
  2. http 常见的状态码有哪些?

    • 200 成功
    • 3xx重定向相关,301 永久重定向,302临时重定向
    • 4xx客户端错误,400请求报文有问题,403服务器禁止访问资源,404资源不存在
    • 5xx服务器内部错误,501 请求的功能暂不支持,502 服务器逻辑有问题,503 服务器繁忙
  3. get 和 post 区别

    • GET参数通过URL传递,POST放在Request body中
    • GET请求只能进行url编码,而POST支持多种编码方式
    • GET请求在URL中传送的参数是有长度限制的,而POST没有
    • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
    • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  4. https的工作原理和流程

  5. http和https的区别

    • http采用明文传输,http+ssl的加密传输
    • http是80端口,https是443端口
    • HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全
  6. 浏览器输入http://www.baidu.com
    事件顺序
    (1) 浏览器获取输入的域名www.baidu.com
    (2) 浏览器向DNS请求解析www.baidu.com的IP地址
    (3) 域名系统DNS解析出百度服务器的IP地址
    (4) 浏览器与该服务器建立TCP连接(默认端口号80)
    (5) 浏览器发出HTTP请求,请求百度首页
    (6) 服务器通过HTTP响应把首页文件发送给浏览器
    (7) TCP连接释放
    (8) 浏览器将首页文件进行解析,并将Web页显示给用户。

  7. http长连接和短连接?http长连接和短连接以及keep-Alive的含义,HTTP 长连接不可能一直保持,例如 Keep-Alive: timeout=5, max=100,表示这个TCP通道可以保持5秒,max=100,表示这个长连接最多接收100次请求就断开。

  8. http cookie和session

    • Cookie和Session都是客户端与服务器之间保持状态的解决方案,具体来说,cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案
    • Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie,而客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器,服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容
  9. http1.0,tttp1.1,http2.0,http 3.0各有什么变化

    • http 1.0
    • http 1.1, 长连接
    • http 2.0,二进制压缩+连接复用
    • http QUIC,udp+ssl
  10. HTTP/3 竟然基于 UDP,HTTP 协议这些年都经历了啥?

  11. 使用curl

  12. https中间人攻击原理以及防御措施

  13. 如何理解http的无连接和无状态的特点?

  14. 半链接和Sync 攻击原理及防范技术


资料来源:OSI 7层模型

超文本传输协议(HTTPS/HTTP1.1/HTTP2/HTTP3)

https://aws.amazon.com/cn/compare/the-difference-between-https-and-http/

HTTP 是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。

一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词:

动词 描述 *幂等 安全性 可缓存
GET 读取资源 Yes Yes Yes
POST 创建资源或触发处理数据的进程 No No Yes,如果回应包含刷新信息
PUT 创建或替换资源 Yes No No
DELETE 删除资源 Yes No No

  • HTTPS 是基于 HTTP 的安全版本,通过使用 SSL 或 TLS 加密和身份验证通信。
  • HTTP/1.1 是 HTTP 的第一个主要版本,引入了持久连接、管道化请求等特性。
  • HTTP/2 是 HTTP 的第二个主要版本,使用二进制协议,引入了多路复用、头部压缩、服务器推送等特性。
  • HTTP/3 是 HTTP 的第三个主要版本,基于 QUIC 协议,使用 UDP,提供更快的传输速度和更好的性能

多次执行不会产生不同的结果

HTTP 是依赖于较低级协议(如 TCPUDP)的应用层协议。

来源及延伸阅读:HTTP

传输控制协议(TCP)


资料来源:如何制作多人游戏

TCP 是通过 IP 网络的面向连接的协议。 使用握手建立和断开连接。 发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏:

如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP 实行流量控制拥塞控制。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。

为了确保高吞吐量,Web 服务器可以保持大量的 TCP 连接,从而导致高内存使用。在 Web 服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 memcached 服务器。连接池 可以帮助除了在适用的情况下切换到 UDP。

TCP 对于需要高可靠性但时间紧迫的应用程序很有用。比如包括 Web 服务器,数据库信息,SMTP,FTP 和 SSH。

以下情况使用 TCP 代替 UDP:

  • 你需要数据完好无损。
  • 你想对网络吞吐量自动进行最佳评估。

用户数据报协议(UDP)


资料来源:如何制作多人游戏

UDP 是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP 不支持拥塞控制。虽然不如 TCP 那样有保证,但 UDP 通常效率更高。

UDP 可以通过广播将数据报发送至子网内的所有设备。这对 DHCP 很有用,因为子网内的设备还没有分配 IP 地址,而 IP 对于 TCP 是必须的。

UDP 可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。

以下情况使用 UDP 代替 TCP:

  • 你需要低延迟
  • 相对于数据丢失更糟的是数据延迟
  • 你想实现自己的错误校正方法

来源及延伸阅读:TCP 与 UDP

远程过程调用协议(RPC)


Source: Crack the system design interview

在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 ProtobufThriftAvro

RPC 是一个“请求-响应”协议:

  • 客户端程序 ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。
  • 客户端 stub 程序 ── 将请求过程的 id 和参数打包进请求信息中。
  • 客户端通信模块 ── 将信息从客户端发送至服务端。
  • 服务端通信模块 ── 将接受的包传给服务端存根程序。
  • 服务端 stub 程序 ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。

RPC 调用示例:

1
2
3
4
5
6
7
GET /someoperation?data=anId

POST /anotheroperation
{
"data":"anId";
"anotherdata": "another value"
}

RPC 专注于暴露方法。RPC 通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。

当以下情况时选择本地库(也就是 SDK):

  • 你知道你的目标平台。
  • 你想控制如何访问你的“逻辑”。
  • 你想对发生在你的库中的错误进行控制。
  • 性能和终端用户体验是你最关心的事。

遵循 REST 的 HTTP API 往往更适用于公共 API。

缺点:RPC

  • RPC 客户端与服务实现捆绑地很紧密。
  • 一个新的 API 必须在每一个操作或者用例中定义。
  • RPC 很难调试。
  • 你可能没办法很方便的去修改现有的技术。举个例子,如果你希望在 Squid 这样的缓存服务器上确保 RPC 被正确缓存的话可能需要一些额外的努力了。

表述性状态转移(REST)

REST 是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。

RESTful 接口有四条规则:

  • 标志资源(HTTP 里的 URI) ── 无论什么操作都使用同一个 URI。
  • 表示的改变(HTTP 的动作) ── 使用动作, headers 和 body。
  • 可自我描述的错误信息(HTTP 中的 status code) ── 使用状态码,不要重新造轮子。
  • HATEOAS(HTTP 中的HTML 接口) ── 你的 web 服务器应该能够通过浏览器访问。

REST 请求的例子:

1
2
3
4
GET /someresources/anId

PUT /someresources/anId
{"anotherdata": "another value"}

REST 关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共 HTTP API 接口设计。REST 使用更通常与规范化的方法来通过 URI 暴露资源,通过 header 来表述并通过 GET、POST、PUT、DELETE 和 PATCH 这些动作来进行操作。因为无状态的特性,REST 易于横向扩展和隔离。

缺点:REST

  • 由于 REST 将重点放在暴露数据,所以当资源不是自然组织的或者结构复杂的时候它可能无法很好的适应。举个例子,返回过去一小时中与特定事件集匹配的更新记录这种操作就很难表示为路径。使用 REST,可能会使用 URI 路径,查询参数和可能的请求体来实现。
  • REST 一般依赖几个动作(GET、POST、PUT、DELETE 和 PATCH),但有时候仅仅这些没法满足你的需要。举个例子,将过期的文档移动到归档文件夹里去,这样的操作可能没法简单的用上面这几个 verbs 表达。
  • 为了渲染单个页面,获取被嵌套在层级结构中的复杂资源需要客户端,服务器之间多次往返通信。例如,获取博客内容及其关联评论。对于使用不确定网络环境的移动应用来说,这些多次往返通信是非常麻烦的。
  • 随着时间的推移,更多的字段可能会被添加到 API 响应中,较旧的客户端将会接收到所有新的数据字段,即使是那些它们不需要的字段,结果它会增加负载大小并引起更大的延迟。

RPC 与 REST 比较

操作 RPC REST
注册 POST /signup POST /persons
注销 POST /resign
{
“personid”: “1234”
}
DELETE /persons/1234
读取用户信息 GET /readPerson?personid=1234 GET /persons/1234
读取用户物品列表 GET /readUsersItemsList?personid=1234 GET /persons/1234/items
向用户物品列表添加一项 POST /addItemToUsersItemsList
{
“personid”: “1234”;
“itemid”: “456”
}
POST /persons/1234/items
{
“itemid”: “456”
}
更新一个物品 POST /modifyItem
{
“itemid”: “456”;
“key”: “value”
}
PUT /items/456
{
“key”: “value”
}
删除一个物品 POST /removeItem
{
“itemid”: “456”
}
DELETE /items/456

资料来源:你真的知道你为什么更喜欢 REST 而不是 RPC 吗

其它

  1. https://blog.csdn.net/justloveyou_/article/details/78303617
  2. 图解https的过程:https://segmentfault.com/a/1190000021494676
  3. 35 张图解:被问千百遍的 TCP 三次握手和四次挥手面试题
  4. 30张图解: TCP 重传、滑动窗口、流量控制、拥塞控制
  5. 硬核!30 张图解 HTTP 常见的面试题

CPU任务调度,进程/线程/协程

  1. 进程和线程的区别,了解协程吗?CPU调度,数据共享。
  2. 复杂系统中通常融合了多进程编程,多线程编程,协程编程
  3. 进程之间怎么通信,线程通信通信,协程怎么通信
  4. 进程之间怎么同步(信号量,自旋锁,屏障),线程之间怎么同步(锁),协程怎么同步。进程之间通过共享内存、管道、消息队列消息队列等方式通信,通过信号和信号量进行同步。线程在进程内部,全部变量时共享的,通过锁机制来同步。
  5. 死锁:产生的四个条件、四个解决方法,死锁检测
  6. 守护进程,linux系统编程实现守护进程
  7. 在Linux上,对于多进程,子进程继承了父进程的下列哪些?堆栈、文件描述符、进程组、会话、环境变量、共享内存
  8. 僵尸进程和孤儿进程。孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
  9. 进程的状态。
    • TASK_RUNNING(运行态):进bai程是可执行du的;或者正在执行,zhi或者在运行队列中等待执行。
    • TASK_INTERRUPTIBLE(可中断睡眠态):进程被阻塞,等待某些条件的完成。一旦完成这些条件,内核就会将该进程的状态设置为运行态。
    • TASK_UNINTERRUPTIBLE(不可中断睡眠态):进程被阻塞,等待某些条件的完成。与可中断睡眠态不同的是,该状态进程不可被信号唤醒。
    • TASK_ZOMBIE(僵死态):该进程已经结束,但是其父进程还没有将其回收。
    • TASK_STOP(终止态):进程停止执行。通常进程在收到SIGSTOP、SIGTTIN、SIGTTOU等信号的时候会进入该状态。
  10. linux的CFS调度机制是什么?时间片/policy(进程类别)/priority(优先级)/counter。linux的任务调度机制是什么?在每个进程的task_struct结构中有以下四项:policy、priority、counter、rt_priority。这四项是选择进程的依据。其中,policy是进程的调度策略,用来区分实时进程和普通进程,实时进程优先于普通进程运行;priority是进程(包括实时和普通)的静态优先级;counter是进程剩余的时间片,它的起始值就是priority的值;由于counter在后面计算一个处于可运行状态的进程值得运行的程度goodness时起重要作用,因此,counter 也可以看作是进程的动态优先级。rt_priority是实时进程特有的,用于实时进程间的选择。 Linux用函数goodness()来衡量一个处于可运行状态的进程值得运行的程度。该函数综合了以上提到的四项,还结合了一些其他的因素,给每个处于可运行状态的进程赋予一个权值(weight),调度程序以这个权值作为选择进程的唯一依据。
  11. goroutine的GPM,没有时间片和优先级的概念,但也支持“抢占式调度”。 goroutine的主要状态grunnable、grunning、gwaiting
  12. 线程的状态
    • runnable
    • running
    • blocked
    • dead
  13. 进程、线程与协程的区别
  14. 操作系统写时复制:https://juejin.cn/post/6844903702373859335
  15. 操作系统为什么设计用户态和内核态,用户态和内核态的权限不同?怎么解决IO频繁发生内核和用户态的态的切换(缓存)?
  16. select、epoll的监听回调机制,红黑树?
  17. 从一道面试题谈linux下fork的运行机制
  18. malloc分配多少内存:http://fallincode.com/blog/2020/01/malloc%e6%9c%80%e5%a4%9a%e8%83%bd%e5%88%86%e9%85%8d%e5%a4%9a%e5%b0%91%e5%86%85%e5%ad%98/

存储系统,内存和存储

  1. 寄存器、缓存cache、内存和磁盘
  2. 可执行文件的空间结构,进程的空间结构(虚拟地址空间,栈,堆,未初始化变量,初始化区,代码)
  3. 查看进程使用的资源,top,ps,cat /proc/pid/status
  4. 进程的虚拟内存机制(虚拟地址-页表-物理地址)。Linux虚拟内存的实现需要6种机制的支持:地址映射机制、内存分配回收机制、缓存和刷新机制、请求页机制、交换机制和内存共享机制,内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址。当用户程序运行时,如果发现程序中要用的虚地址没有对应的物理内存,就发出了请求页要求。如果有空闲的内存可供分配,就请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在缓存中(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制;腾出一部分内存。另外,在地址映射中要通过TLB(翻译后援存储器)来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中,也要修改页表来映射文件地址。
  5. 操作系统内存分配算法常用缓存置换算法(FIFO,LRU,LFU),LRU算法的实现和优化?
  6. Linux系统原理之文件系统(磁盘、分区、文件系统、inode表、data block)
  7. 在linux执行ls上实际发生了什么
  8. CPU寻址过程,tlb,cache miss.
  9. 栈和堆的区别

系统编程以及其它注意事项

  1. 使用过哪些进程间通讯机制,并详细说明,linux进程之间的通信7种方式
  2. 内核函数、系统调用、库函数/API,strace系统调用追踪调试
  3. coredump文件产生?内存访问越界、野指针、堆栈溢出等等
  4. fork 和 vfork,exec,system(进程的用户空间是在执行系统调用的fork时创建的,基于写时复制的原理,子进程创建的时候继承了父进程的用户空间,仅仅是mm_struc结构的建立、vm_area_struct结构的建立以及页目录和页表的建立,并没有真正地复制一个物理页面,这也是为什么Linux内核能迅速地创建进程的原因之一。)写时复制(Copy-on-write)是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如,fork()后立即调用exec(),就无需复制父进程的页了。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的PCB。这种优化可以避免拷贝大量根本就不会使用的数据
  5. 锁?互斥锁的属性设置、多进程共享内存的使用、多线程的使用互斥锁、pshaed和type设置。使用互斥量和条件变脸实现互斥锁
  6. 共享内存的同步机制,使用信号量,无锁数据结构
  7. 多线程里一个线程sleep,实质上是在干嘛,忙等还是闲等。?
  8. exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。
  9. select/epoll https://www.cnblogs.com/anker/p/3265058.html
  • select 内核态和用户态重复拷贝
  • select 需要遍历遍历查找就绪的socket
  • select 有数量限制1024
  • epoll 注册时写进内核
  • epoll_wait 返回就绪的事件

网络编程

  1. 简单了解C语言的socket编程api。socket,bind,listen,accept,connect,read/write.

  2. Linux下socket的五种I/O 模式,同步阻塞、同步非阻塞、同步I/O复用、异步I/O、信号驱动I/O

  3. Linux套接字和I/O模型

  4. select和epoll的区别

  5. 什么是I/O 复用?关于I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

  6. 网络分析工具。ping/tcpdump/netstat/lsof

其它问题

  1. 计算机中浮点数表示方法,以及浮点数转换中精度缺失的问题
0%