本系列代码基于golang1.19
runtime篇一:接口 runtime篇二:通道 runtime篇三:defer runtime篇四:panic runtime篇五:slice 1. panic的底层结构 panic在runtime中的底层表示是runtime._panic结构体。
type _panic struct { argp unsafe.Pointer // 指向defer调用时参数的指针 arg any // panic参数 link *_panic // 连接到更早的_panic pc uintptr // 程序计数器 sp unsafe.Pointer // 栈指针 recovered bool // 当前panic是否被recover恢复 aborted bool // 当前panic是否被中止 goexit bool // 是否调用了runtime.Goexit } 类似于_defer,panic也被组织成链表结构,多个panic通过link字段连接成一个链表。
在_panic结构体中,pc、sp、goexit三个字段是为了修复runtime.Goexit带来的问题引入的[1].
2. 调用panic 在函数中调用panic时,底层会调用runtime.gopanic,其源码如下:
func gopanic(e any) { gp := getg() // 获取当前g // ....
在使用top命令查看系统状态时,会出现一堆参数,这些参数分别代表什么意思呢?
第一行 首先打印了当前时间21:11:55,然后是系统已经启动了多久up 4 days, 8:45,我已经4天8小时45分钟没关机了。。
然后当前有3个用户登录3 users,load average是1.47, 1.33, 1.14
load average load average后面有三个值,分别代表过去 1 分钟,5 分钟,15 分钟在这个节点上的 load average.
Load Average 是一种 CPU 资源需求的度量。
举例来说,对于一个单个 CPU 的系统,如果在 1 分钟的时间里,处理器上始终有一个进程在运行,同时操作系统的进程可运行队列中始终都有 9 个进程在等待获取 CPU 资源。那么对于这 1 分钟的时间来说,系统的"load average"就是 1+9=10,这个定义对绝大部分的 Unix 系统都适用。
对于 Linux 来说,如果只考虑 CPU 的资源,Load Averag 等于单位时间内正在运行的进程加上可运行队列的进程,这个定义也是成立的。
对于load average的理解有以下三点:
不论计算机 CPU 是空闲还是满负载,Load Average 都是 Linux 进程调度器中可运行队列(Running Queue)里的一段时间的平均进程数目。
计算机上的 CPU 还有空闲的情况下,CPU Usage 可以直接反映到"load average"上,什么是 CPU 还有空闲呢?具体来说就是可运行队列中的进程数目小于 CPU 个数,这种情况下,单位时间进程 CPU Usage 相加的平均值应该就是"load average"的值。...
容器具有自己的Network Namespace.
eht0是这个Network Namespace里的网络接口。而宿主机上也有自己的 eth0,宿主机上的 eth0 对应着真正的物理网卡,可以和外面通讯
要让容器 Network Namespace 中的数据包最终发送到物理网卡上,需要以下两步:
将数据包从容器的 Network Namespace 发送到 Host Network Namespace 上 数据包从宿主机的eth0发送出去 要想让容器从自己的Network Namespace连接到Host Namespace,一般来说就只有两类设备接口,一是veth,另外是macvlan/ipvlan.
veth是一个虚拟的网络设备,一般是成对建立,而且这对设备是相互连接的。当每个设备在不同的 Network Namespaces 的时候,Namespace 之间就可以用这对 veth 设备来进行网络通讯了。
到这里,解决了第一步,下一步需要将数据包从宿主机的eth0发送出去。
Docker 程序在节点上安装完之后,就会自动建立了一个 docker0 的 bridge interface。所以我们只需要把第一步中建立的 veth_host 这个设备,接入到 docker0 这个 bridge 上。
容器和docker0组成了一个子网,docker0上的IP就是这个子网的网关IP。
然后docekr0通过nat或route的方式,经过eth0将数据包向外发送。
Reference https://time.geekbang.org/column/article/323325
http://icyfenix.cn/immutable-infrastructure/network/linux-vnet.html
https://morven.life/posts/networking-4-docker-sigle-host/
https://network.51cto.com/article/708901.html
本系列代码基于golang1.19
runtime篇一:接口 runtime篇二:通道 runtime篇三:defer runtime篇四:panic runtime篇五:slice 1. chan的结构 一个channel长这样:
type hchan struct { qcount uint // total data in the queue dataqsiz uint // size of the circular queue buf unsafe.Pointer // points to an array of dataqsiz elements elemsize uint16 // chan中元素大小 closed uint32 // 是否关闭 elemtype *_type // element type sendx uint // send index recvx uint // receive index recvq waitq // list of recv waiters sendq waitq // list of send waiters lock mutex } channel的字段中,主要可以分为三部分:...
本系列代码基于golang1.19
runtime篇一:接口 runtime篇二:通道 runtime篇三:defer runtime篇四:panic runtime篇五:slice 1. 接口的内部结构 type iface struct { tab *itab data unsafe.Pointer } 一个接口是一个iface结构体,其中包含一个itab指针和一个unsafe.Pointer。
概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。
一个itab可以表示一个接口的类型和赋给这个接口的实体类型,即为接口的动态类型,而data所指向的unsafe.Pointer则指向接口的动态值。
近距离来看itab:
type itab struct { inter *interfacetype _type *_type hash uint32 // copy of _type.hash. Used for type switches. _ [4]byte fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter. } 其中inter字段描述了接口的类型,_type字段描述了实体类型,hash字段是类型哈希,用于类型匹配,fun字段放置和接口方法对应的具体数据类型的方法地址。
再来看interfacetype
type interfacetype struct { typ _type pkgpath name mhdr []imethod } 其中typ和itab中的_type为同一个值,pkgpath则存储了接口的包名,mhdr则表示接口所定义的函数列表。...
1. TLS在哪些地方被使用 TLS(Transport Layer Security),是为计算机网络中提供安全通信的密码学协议。
HTTPS = HTTP + TLS SMTPS = SMTPS + TLS FTPS = FTP + TLS … 2. TLS给我们带来了什么 认证 TLS检查通信双方的身份 借助于非对称加密,TLS保证我们访问的是“真网站”,不是假冒的。 加密 TLS 通过使用对称加密算法对其进行加密来保护交换的数据免受未经授权的访问。 校验 TLS 通过检查消息验证码来识别传输过程中的任何数据更改 3. TLS通信的工作的基本流程 通常,TLS包含2个过程,或者说2个协议。
Handshake Protocol 在这个阶段,客户端和服务端: 协商协议版本 选择加密算法 通过非对称加密算法验证对方身份呢 建立一个共享的对称加密密钥以应用于接下来的通信 Record Protocol 在这个阶段: 所有发出的信息都被上个阶段商定的对称密钥加密 信息被发送到对面 接收方验证信息是否受到篡改 如果未被篡改,信息将被解密 4....
极客时间《消息队列高手课》笔记
概览消息队列篇〇:为什么需要消息队列 概览消息队列篇一:不同的mq的消息模型 概览消息队列篇二:如何确保消息不丢失 概览消息队列篇三:重复消息的处理 1. 异步处理 对于一个秒杀系统,需要解决的核心问题是,如何利用有限的服务器资源,尽可能多地处理短时间内的海量请求。
处理一个秒杀请求包含了很多步骤:如风险控制、库存锁定、生成订单、短信通知、更新通知数据等。如果没有任何优化,正常的处理流程是:App 将请求发送给网关,依次调用上述 5 个流程,然后将结果返回给 APP。
但上述5个步骤中,只需要风险控制和库存锁定这两步就可以决定秒杀是否成功,对于后续的生成订单、短信通知和更新统计数据等步骤,并不一定要在秒杀请求中处理完成。
所以当服务端完成前面 2 个步骤,确定本次请求的秒杀结果后,就可以马上给用户返回响应,然后把请求的数据放入消息队列中,由消息队列异步地进行后续的操作。
处理一个秒杀请求,从 5 个步骤减少为 2 个步骤,这样不仅响应速度更快,并且在秒杀期间,我们可以把大量的服务器资源用来处理秒杀请求。秒杀结束后再把资源用于处理后面的步骤,充分利用有限的服务器资源处理更多的秒杀请求。
可以看到,在这个场景中,消息队列被用于实现服务的异步处理。
这样做的好处是:
可以更快地返回结果; 减少等待,自然实现了步骤之间的并发,提升系统总体的性能。 2. 流量控制 在用消息队列实现了部分工作的异步处理后,我们还需要考虑如何避免过多的请求压垮我们的秒杀系统。
一个设计健壮的程序有自我保护的能力,也就是说,它应该可以在海量的请求下,还能在自身能力范围内尽可能多地处理请求,拒绝处理不了的请求并且保证自身运行正常。
我们可以使用消息队列隔离网关和后端服务,以达到流量控制和保护后端服务的目的。
加入消息队列后,整个秒杀流程变为:
网关在收到请求后,将请求放入请求消息队列; 后端服务从请求消息队列中获取 APP 请求,完成后续秒杀处理过程,然后返回结果。 这种设计的优点是:能根据下游的处理能力自动调节流量,达到“削峰填谷”的作用。但这样做同样是有代价的:
增加了系统调用链环节,导致总体的响应时延变长。 上下游系统都要将同步调用改为异步消息,增加了系统的复杂度。 3. 服务解耦 对于一个电商系统来说,当一个新订单创建时:
支付系统需要发起支付流程; 风控系统需要审核订单的合法性; 客服系统需要给用户发短信告知用户; 经营分析系统需要更新统计数据; …… 这些订单下游的系统都需要实时获得订单数据。随着业务不断发展,这些订单下游系统不断的增加,不断变化,并且每个系统可能只需要订单数据的一个子集,负责订单服务的开发团队不得不花费很大的精力,应对不断增加变化的下游系统,不停地修改调试订单系统与这些下游系统的接口。任何一个下游系统接口变更,都需要订单模块重新进行一次上线,对于一个电商的核心服务来说,这几乎是不可接受的。
所有的电商都选择用消息队列来解决类似的系统耦合过于紧密的问题。引入消息队列后,订单服务在订单变化时发送一条消息到消息队列的一个主题 Order 中,所有下游系统都订阅主题 Order,这样每个下游系统都可以获得一份实时完整的订单数据。无论增加、减少下游系统或是下游系统需求如何变化,订单服务都无需做任何更改,实现了订单服务与下游服务的解耦。
其他 作为发布 / 订阅系统实现一个微服务级系统间的观察者模式; 连接流计算任务和数据; 用于将消息广播给大量接收者。 消息队列带来的一些问题 引入消息队列带来的延迟问题; 增加了系统的复杂度; 可能产生数据不一致的问题。
极客时间《消息队列高手课》笔记
概览消息队列篇〇:为什么需要消息队列 概览消息队列篇一:不同的mq的消息模型 概览消息队列篇二:如何确保消息不丢失 概览消息队列篇三:重复消息的处理 消息队列本身可以保证消息不重复吗 在 MQTT 协议中,给出了三种传递消息时能够提供的服务质量标准,这三种服务质量从低到高依次是:
At most once: 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。 At least once: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。 Exactly once:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级。 这个服务质量标准不仅适用于 MQTT,对所有的消息队列都是适用的。我们现在常用的绝大部分消息队列提供的服务质量都是 At least once,包括 RocketMQ、RabbitMQ 和 Kafka 都是这样。
也就是说,消息队列很难保证消息不重复
用幂等性解决重复消息问题 一般解决重复消息的办法是,在消费端,让我们消费消息的操作具备幂等性。
一个幂等操作的特点是,其任意多次执行所产生的影响均与一次执行的影响相同。
一个幂等的方法,使用同样的参数,对它进行多次调用和一次调用,对系统产生的影响是一样的。所以,对于幂等的方法,不用担心重复执行会对系统造成任何改变。
如果我们系统消费消息的业务逻辑具备幂等性,那就不用担心消息重复的问题了,因为同一条消息,消费一次和消费多次对系统的影响是完全一样的。也就可以认为,消费多次等于消费一次。从对系统的影响结果来说:At least once + 幂等消费 = Exactly once。
常用设计幂等操作的方法
利用数据库的唯一约束实现幂等 对于一个转账操作,我们可以设置对于每一个转账单只能进行一次转账操作,这样除第一次操作外其他重复操作都会失败。
基于这个思路,不光是可以使用关系型数据库,只要是支持类似“INSERT IF NOT EXIST”语义的存储类系统都可以用于实现幂等,比如,还可以用 Redis 的 SETNX 命令来替代数据库中的唯一约束,来实现幂等消费。
为更新的数据设置前置条件 如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。这样,重复执行这个操作时,由于第一次更新数据的时候已经变更了前置条件中需要判断的数据,不满足前置条件,则不会重复执行更新数据操作。
记录并检查操作 记录并检查操作,也称为“Token 机制或者 GUID(全局唯一 ID)机制”,实现的思路特别简单:在执行数据更新操作之前,先检查一下是否执行过这个更新操作。
具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。
极客时间《消息队列高手课》笔记
概览消息队列篇〇:为什么需要消息队列 概览消息队列篇一:不同的mq的消息模型 概览消息队列篇二:如何确保消息不丢失 概览消息队列篇三:重复消息的处理 检测消息丢失的方法 可以使用类似分布式链路追踪系统来追踪每一条信息。
还可以利用消息队列的有序性来验证是否有消息丢失。
在 Producer 端,我们给每个发出的消息附加一个连续递增的序号,然后在 Consumer 端来检查这个序号的连续性
如果没有消息丢失,Consumer 收到消息的序号必然是连续递增的,或者说收到的消息,其中的序号必然是上一条消息的序号 +1。如果检测到序号不连续,那就是丢消息了。
大多数消息队列的客户端都支持拦截器机制,你可以利用这个拦截器机制,在 Producer 发送消息之前的拦截器中将序号注入到消息中,在 Consumer 收到消息的拦截器中检测序号的连续性,这样实现的好处是消息检测的代码不会侵入到你的业务代码中,待你的系统稳定后,也方便将这部分检测的逻辑关闭或者删除。
确保消息可靠传递 一条消息从生产到消费完成这个过程,可以划分三个阶段,
生产阶段: 在这个阶段,从消息在 Producer 创建出来,经过网络传输发送到 Broker(mq服务端)端。 存储阶段: 在这个阶段,消息在 Broker 端存储,如果是集群,消息会在这个阶段被复制到其他的副本上。 消费阶段: 在这个阶段,Consumer 从 Broker 上拉取消息,经过网络传输发送到 Consumer 上。 下面就如何确保消息可靠传递分别对三个阶段进行分析
生产阶段 在生产阶段,消息队列通过最常用的请求确认机制,来保证消息的可靠传递:当你的代码调用发消息方法时,消息队列的客户端会把消息发送到 Broker,Broker 收到消息后,会给客户端返回一个确认响应,表明消息已经收到了。客户端收到响应后,完成了一次正常消息的发送。
只要 Producer 收到了 Broker 的确认响应,就可以保证消息在生产阶段不会丢失。有些消息队列在长时间没收到发送确认响应后,会自动重试,如果重试再失败,就会以返回值或者异常的方式告知用户。
存储阶段 在存储阶段正常情况下,只要 Broker 在正常运行,就不会出现丢失消息的问题,但是如果 Broker 出现了故障,比如进程死掉了或者服务器宕机了,还是可能会丢失消息的。
如果对消息的可靠性要求非常高,可以通过配置 Broker 参数来避免因为宕机丢消息。
对于单个节点的 Broker,需要配置 Broker 参数,在收到消息后,将消息写入磁盘后再给 Producer 返回确认响应,这样即使发生宕机,由于消息已经被写入磁盘,就不会丢失消息,恢复后还可以继续消费。例如,在 RocketMQ 中,需要将刷盘方式 flushDiskType 配置为 SYNC_FLUSH 同步刷盘。...
极客时间《Redis 核心技术与实战》学习笔记
概览Redis篇一:单线程模型 概览Redis篇二:AOF日志 概览Redis篇三:RDB快照 概览Redis篇四:主从 概览Redis篇五:哨兵 概览Redis篇六:切片集群 什么是RDB 所谓RDB,即Redis DataBase。是对Redis做的一个内存快照,即将内存中的数据在某一个时刻的状态记录。
对 Redis 来说,它实现类似照片记录效果的方式,就是把某一时刻的状态以文件的形式写到磁盘上,也就是快照。这样一来,即使宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件。
和 AOF 相比,RDB 记录的是某一时刻的数据,并不是操作,所以,在做数据恢复时,我们可以直接把 RDB 文件读入内存,很快地完成恢复。
給哪些内存数据做快照 給哪些内存数据做快照,这关系到快照的执行效率。
Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。这样做的好处是,一次性记录了所有数据,一个都不少。
但给内存的全量数据做快照,把它们全部写入磁盘也会花费很多时间。而且,全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。
这里我们要做一个“灵魂之问”:RDB文件的生成会阻塞主线程吗?
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。
save:在主线程中执行,会导致阻塞; bgsave:创建一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。 因此我们可以使用bgsave命令来执行全量快照,这既提供了数据的可靠性保证,也避免了对 Redis 的性能影响。
快照时数据能修改吗 这个问题非常重要,这是因为,如果数据能被修改,那就意味着 Redis 还能正常处理写操作。否则,所有写操作都得等到快照完了才能执行,性能一下子就降低了。
如果我们使用save命令,显然是不能修改的,以为主线程已经被阻塞掉了。
如果使用bgsave命令,看起来数据还是可以修改的,因为主线程没有被阻塞。但避免阻塞和正常处理写操作并不是一回事。此时,主线程的确没有阻塞,可以正常接收请求,但是,为了保证快照完整性,它只能处理读操作,因为不能修改正在执行快照的数据。
为了快照而暂停写操作,肯定是不能接受的。所以这个时候,Redis 就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。
简单来说,bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件。
此时,如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本(键值对 C’)。然后,主线程在这个数据副本上进行修改。同时,bgsave 子进程可以继续把原来的数据(键值对 C)写入 RDB 文件。...