epoll中的LT和ET

本来我在看的是golang的gmp调度器,然后看到注释和代码里面有提到netpoll这个东西,不知不觉又去翻看了下linux网络编程相关的知识,上网找了下博客,找到了ants开源库作者关于go netpoll的博客,而后又因为我不了解其中epoll的LT/ET是什么东西,又赶紧补了一下这些知识,唉,本该早点就了解的知识,一直没能沉下心来看,虽然平时开发业务大概率用不到,但是看稍微底层一点的知识就会涉及到这些东西呢… LT和ET的区别 epoll(linux)/kqueue(unix)是现代操作系统用来做I/O多路复用的著名技术,通常有两种不同的工作模式LT(level- triggered,水平触发)和ET(edge-triggered,边缘触发),默认情况下epoll是工作在LT模式下的。 之前摸过一段时间的verilog,很熟悉edge-triggered这个玩意。电平见过吧,1是高0是低,那么一段连续变化的电平,大概可以表示成这个样子: 电平是一段时间内的值,比如横线在上代表这段时间内电平值是1,下面横线代表0,竖线代表由0变1(下降沿,如图中绿色竖线)或者1变0(上升沿,如图中红色竖线)。 这个跟我们的要说的LT和ET有什么关系呢?我们将1代表有fd可读写(对应电平1),0代表没有fd可读写(对应电平0),如果epoll工作在LT模式下,只要fd可读写,那么就epoll_wait一定返回有值,否则阻塞,相当于你去询问现在电平是多少,他就返回当前电平的值给你。但如果epoll工作在ET模式下,只有当fd是从不可读写变为可读写的时候(有新事件到来),epoll_wait才会告诉你可读写,相当于存储了一个上升沿状态,你epoll_wait调用相当于去消费这个状态,消费完后,在下一次边缘到来之前,epoll_wait就没有数据返回了,因为这个边缘已经在上一次epoll_wait被消费掉了。 举个例子,当前注册fd1和fd2的可读事件到epoll上,初始状态为[0,0]: 工作模式为LT,调用epoll_wait fd1可读事件到达 epoll_wait返回fd1可读,状态变成[1,0] 再次调用epoll_wait(注意此时状态依然是[1,0]),立刻返回fd1可读 ET模式: 工作模式为ET,调用epoll_wait fd1可读事件到达,状态变成[1,0] epoll_wait返回fd1可读 再次调用epoll_wait(注意此时状态依然是[1,0]),阻塞 结论就是,在LT工作模式下,可以通过不断询问epoll_wait来决定是否要读写fd,因为他总能告诉你最真实的是否可读写的状态。在ET工作模式下,你只会被通知一次fd可以读写,然后你最好就一次性读写完成,相当于将fd的当前状态清除掉,然后才去进行下一次epoll_wait,这里说的“一次性读写”并不是说只调用一次read/write,而是反复调用直到没有数据可以读出/写入,注意到因为需要反复调用,如果fd是阻塞模型的话,很可能最后一次就阻塞住了,导致你的I/O从多路复用模式直接变成了阻塞模式,甚至这个fd以后可能不再可读写,那你这个程序就再也跑不下去了,所以ET一般和fd设置成非阻塞一起使用。而LT模式下,fd设成阻塞或非阻塞都可以,因为epoll_wait会告诉你是否可读写,如果可以读写,read/write一定是成功的,因此不会阻塞。 当然,ET还有一些坑需要了解,详情看参考链接 参考链接 https://strikefreedom.top/archives/linux-epoll-with-level-triggering-and-edge-triggering https://strikefreedom.top/archives/go-netpoll-io-multiplexing-reacto https://zhuanlan.zhihu.com/p/40572954

九月 16, 2024 · by NOSAE

golang Context源码阅读

前言 本文基于go1.21,不同版本的Context内部实现可能会有细微差别 使用场景 为什么需要Context,首先思考一个场景:客户端去请求某个服务,这个服务依赖于多个可并行执行的下游服务,为了提高这个服务的响应速度,自然会为每个下游服务开启一个协程去执行。 倘若在执行的过程中,客户端取消了对这个服务的请求,下游服务也应该被停掉。但是go不提供直接关闭协程的方法,我们可以使用chan来协作式地关闭这些子协程:在服务代码中创建一个no buffer的chan并传递给这些子协程,通过子协程read chan+父亲协程close chan来达到通知子协程关闭的效果。 我们将这个场景扩展一下: 在这个场景中,child svc1又依赖于两个下游服务,并且某次请求中child svc1因为某些原因会取消对下游服务的请求(不影响其他服务节点的正常运行),老样子,在child svc1中创建一个chan并通过这个chan去关闭下游协程就好了。child svc1的代码可能会这么写: func childSvc1(upstream <-chan) { downstream := make(chan struct{}, 0) defer close(downstream) go childSvc1_1(downstream) go childSvr1_2(downstream) for { select { case <-upstream: close(downstream) return default: // 处理业务... // 因为某些原因需要取消下游服务 close(downstream) } } } 可以看到这段小snippet还能勉强阅读,但是从使用语义上来说,chan本身是用于传递数据的,这种read+close来关闭协程的方式就是某种hack手段,对于有一定规模的项目来说,这样的使用方式可读性将会非常差并且容易出错。因此我们需要一个带有“取消下游”语义的库来完成这种任务。 Context则很好地充当了这样的角色,它不但能控制下游的生命的生命周期,还自带超时控制、取消传递、并发安全、数据传递等功能: 取消传递:父Context的取消会自动传递到所有子孙Context,使得子孙Context也自动取消 超时控制:通过 context.WithTimeout 和 context.WithDeadline,提供自动超时取消机制 数据传递:可以携带请求范围内的数据(比如请求ID、path、begin timestamp等),这个特性在分布式链路追踪框架中被广泛使用 上面第二个场景中的代码换成Context来实现: import "context" func childSvc1(ctx context.Context) { cancel, ctx := context.WithCancel(ctx) // 创建子Context defer cancel() go childSvc1_1(ctx) go childSvr1_2(ctx) for { select { case ctx.Done(): return default: // 处理业务... // 因为某些原因需要取消下游服务 cancel() } } } 带超时控制的Context: ...

九月 14, 2024 · by NOSAE

golang底层知识汇总

Note 本文基于go1.21.2,不同版本的go可能会有差异。文中部分代码会由于不是知识点强相关而省略,但被忽略的每行代码或多或少都有它们实际的用处,甚至可能不宜删除,欢迎指出 Tip 分析底层的相关的代码时,往往会由于平台架构而带来代码上的差异,比如我机器是darwin/arm64,使用vscode查看代码,而且我希望基于linux/amd64来看代码,可以在.vscode/settings.json中设置GOOS和GOARCH: ...

九月 3, 2024 · by NOSAE

MQ

消息队列引入的好处 通过异步处理提高系统性能(减少响应所需时间) 削峰/限流 降低系统耦合性。 消息队列引入的问题 系统可用性降低:需要处理mq宕机问题 系统复杂度提高:需要处理消息重复、丢失、保序等问题 一致性问题:消息没有被正确消费的话,引入一致性问题 kafka特点 高吞吐量、低延迟。topic可以分为多个partition,消费者组的消费者并行对topic进行消费 可扩展性:支持热扩展 持久性:消息被持久化到磁盘,并支持数据备份 容错性:partition replica 高并发:支持数千个客户端同时进行读写 kafka架构 Producer(生产者) : 产生消息的一方。 Consumer(消费者) : 消费消息的一方。 Consumer Group(消费者组):同一个组内不同消费者负责消费不同的partation,消费者组之间互不影响 Broker(代理) : 可以看作是一个独立的 Kafka 实例。多个 Kafka Broker 组成一个 Kafka Cluster。 Controller:通过 zk 从 Brokers 中选举出来管理整个 Broker 集群的 Broker,名为 Controller。Controller 通过定时任务,或者监听器模式获取 zk 信息,将 zk 的变动通过事件的方式发送给事件队列,队列就是一个LinkedBlockingQueue,事件消费者线程组通过消费消费事件,将相应的事件同步到各 Broker 节点。 每个Broker又包含了topic和partition Topic(主题) : Producer 将消息发送到特定的主题,Consumer 通过订阅特定的 Topic 来消费消息。 Partition(分区) : 一个 Topic 划分为 Partition ,分布在不同的 Broker 上,便于负载均衡以及并发消费。每个 Partition 以文件夹的形式存储在文件系统中。每个对应的 Partition 数据目录下存储 .index,.log ,.timeindex三种文件 partition实际上就可以看成是一个队列。 ...

九月 2, 2024 · by NOSAE

MySQL

select流程 连接 获取TCP连接,查询用户的权限,该权限保存在连接中,就算管理员改了用户权限,该连接的权限不会变。 空闲连接的最大空闲时长由wait_time控制,超过最大时长就自动断开。 最大连接数由max_connections控制,超过最大连接数就拒绝新的连接。 MySQL连接也分长连接和短连接: 连接 mysql 服务(TCP 三次握手) 执行sql 断开 mysql 服务(TCP 四次挥手) // 长连接 连接 mysql 服务(TCP 三次握手) 执行sql 执行sql 执行sql .... 断开 mysql 服务(TCP 四次挥手) 由于很多资源保存在内存中,并且在连接断开后才释放,长连接很多很可能MySQL程序占用内存过大导致被系统杀死并重启,对于长连接的解决方式是客户端主动调用mysql_reset_connection重置连接释放内存,这个过程不需要重连和重新做权限验证。 在实际开发中(以Java为例),会使用DBCP、C3P0或者Druid的连接池来管理连接,由连接池处理这种问题。 解析SQL 词法分析+语法分析。 这一步不会检查表/字段是否存在,只是单纯检查SQL语法 执行SQL 预处理:检查表/字段是否存在、将select *展开为所有字段 优化:制定执行计划,选择索引(使用explain SQL语句可以查看执行计划) 执行:执行器根据执行计划,将索引传给引擎去查询一条符合的记录,得到一条记录后检查剩余的非索引条件是否满足,满足的话发送这条记录给客户端。不断重复上面这个过程。 redo log(引擎层) redo log具有crash safe的能力 redo log是物理日志,记录的是“在某个数据页上做了什么修改” redo log是循环写的,空间固定会用完。 redo log buffer是全局共用的,参数innodb_flush_log_at_trx_commit=N控制write和fsync,N=0的时候不write也不fsync,N=1的时候每个事务提交都write+fsync,N=2的时候只write bin log(服务层) binlog是归档日志,没有crash safe的能力 binlog是逻辑日志,比如“给 ID=2 这一行的 c 字段加 1" binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志 binlog+某个时间点的数据库全量备份,可以将数据库恢复到之前某个时间点,比如误删了数据库想要恢复: 首先,找到最近的一次全量备份,如果你运气好,可能就是昨天晚上的一个备份,从这个备份恢复到临时库; 然后,从备份的时间点开始,将备份的 binlog 依次取出来,重放到中午误删表之前的那个时刻。 每个线程维护自己的binlog cache,参数sync_binlog=N控制fsync,0为不fsync,N为每N个事务fsync。为N的话,binlog最多会丢失N个binlog日志。 ...

九月 2, 2024 · by NOSAE