Golang协程调度

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 本文基于go1.17.6 1.什么是协程? 协程,英文Coroutines,是一种基于线程之上,但又比线程更加轻量级的存在,这种由程序员自己写程序来管理的轻量级线程叫做『用户空间线程』, 具有对内核来说不可见的特性。 1.1 协程的特点 线程的切换由操作系统负责调度,协程由用户自己进行调度 线程的默认Stack大小是MB级别,而协程更轻量,接近KB级别。 在同一个线程上的多个协程,访问某些资源可以不需要锁 适用于被阻塞的,且需要大量并发的场景。 1.2 Golang的GMP模型 CPU驱动线程上的任务执行 线程由操作系统内核进行调度,Goroutine由Golang运行时(runtime)进行调度 P的 local runnable queue是无锁的,global runnable queue是有锁的 P的 local runnable queue长度限制为256 注意: M和P是绑定关系 M和G是绑定关系 P只是暂存G,他们之间不是绑定关系 E-R图 简化后的E-R图 注意: 后面为了书写简单直接将 local runnable queue表示为本地队列 global runnable queue表示为全局队列 延伸 timer的四叉堆和内存分配器使用的mcache也是每个P一个 Q: 为什么默认情况下P的数量与CPU数量一致? A: 这样可以避免把CPU时间浪费在上线文切换上 1.3 协程和线程的资源消耗对比 类别 栈内存 上下文切换 备注 Thread 1MB 1us 内存占用使用的是Java线程的默认栈大小 Goroutine 4KB 0.2us 内存占用使用的是Linux+x86下的栈大小 2. 常见的Goroutine 让出/调度/抢占场景 ...

March 25, 2022 · 5 min

BoltDB-MVCC的一种极简实践

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.前言 BoltDB是一个纯Golang实现的极简的key/value数据库。 传送门:boltdb/bolt 主库从2018年起就不再更新了。但它有2个衍生版本 hashicorp/raft-boltdb和etcd-io/bbolt分别被使用在Consul和Etcd。另外其它使用BoltDB的项目 BoltDB之所以能够得到广泛的应用,最大原因是它足够轻量,核心代码只有4000多行,另外大多数关系数据库(SQLite 除外)都需要在应用程序之外独立运行服务。比如MySQL你需要启动MySQL Server,这样就增加了额外的维护成本。而BoltDB可以嵌入在应用程序之中,同生同死。 本文萌叔会把重点放在BoltDB的事务机制的实现上 2. 事务并发控制关系 在BoltDB中官方的描述是有2中类型的事务,读写事务和读事务 2.1 读写事务包含 Bucket.Put()和Bucket.Delete() 等操作 func (b *Bucket) Put(key []byte, value []byte) error func (b *Bucket) Delete(key []byte) error 2.2 读事务包含 Bucket.Get() 等操作 func (b *Bucket) Get(key []byte) []byte 注意: 读写事务在后面的文章中简写为写事务 2.3 写事务并发控制 2.3.1 同一个进程内部的多个协程 通过读写锁来保证某一时刻只能有一个写事务运行。 模式 可以并发 备注 读-读 是 可以并发 读-写 是 1个写事务和多个读事务可以并发 写-写 否 互斥 type DB struct { ... rwlock sync.Mutex // Allows only one writer at a time. ... } 2.3.2 同一台设备上的多个进程 使用操作系统的文件读写锁控制并发 ...

March 2, 2022 · 3 min

聊聊Golang中形形色色的同步原语

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 注意: 本文基于Golang 1.17.1 1.引言 阅读Golang的源码的话,会发现里面有形形色色的同步原语,发挥着重要的作用。这篇文章,萌叔打算把它们之间的依赖关系,以及与Linux的同步原语的对应关系整理一下。 2. 与Linux系统的同步原语之间的对应关系 锁类型 Golang Linux 互斥锁 runtime.mutex (低阶) sync.Mutex (高阶) #include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t *mutex);int pthread_mutex_unlock(pthread_mutex_t *mutex); 信号量 runtime.semaphore #include <sys/sem.h>int semget(key_t key, int nsems, int semflg);int semctl(int semid, int semnum, int cmd, …);int semop(int semid, struct sembuf *sops, size_t nsops); 条件变量 sync.Cond #include <pthread.h>int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);int pthread_cond_signal(pthread_cond_t *cond);int pthread_cond_broadcast(pthread_cond_t *cond); 3.依赖关系 3.1 runtime.mutex runtime/lock_futex.go ...

January 5, 2022 · 1 min

为什么Golang的Timer实现使用四叉堆而不是二叉堆

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 Golang在Timer(计时器)的实现中使用了四叉堆,而不是常见的二叉堆,这个问题引起了我的兴趣。 对于这个问题,网上的解释是这样的 但是我仔细查阅了资料以后,有了点有趣的发现。 2.说明 假设: 堆为d叉堆,堆中共有n个节点 备注: 时间复杂度的证明见d-ary heap Timer最常见的2个操作 1)Insert() 添加一个计时器。涉及 sift-up(上推)操作,时间复杂度为O(log n / log d) Python代码 import math result = math.log(n, d) 时间消耗(需要交换或者比较的次数) n(数量) 二叉堆 四叉堆 八叉堆 1w 13.3 6.64 4.43 10w 16.61 8.30 5.54 100w 19.93 9.97 6.64 1000w 23.25 11.63 7.75 2) Delete() 指定时间到达后,清除计时器。涉及 sift-down(下推)操作,时间复杂度为O(d * log n / log d) Python代码 import math result = d * math.log(n, d) 时间消耗(需要交换或者比较的次数) n(数量) 二叉堆 四叉堆 八叉堆 1w 26.58 26.58 35.43 10w 33.33 33.33 44.29 100w 39.86 39.86 53.15 1000w 46.51 46.51 62.01 3. 结论 正常情况下,插入操作:多叉堆执行性能优于二叉堆; 删除操作:多叉堆执行性能弱于二叉堆。 但是细心的朋友应该已经发现,四叉堆插入操作的耗时只有二叉堆的一半,而删除操作的耗时与二叉堆完全相同。 这么一对比,优势确实显著。 ...

December 28, 2021 · 1 min

打印struct中Field内存对齐的小工具

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 因为使用golangci-lint时长有关于内存对齐的告警提示,所以做了一个小工具,可以查看struct中Field在内存中的对齐情况 传送门: vearne/mem-align 2. 使用 package main import ( &quot;github.com/vearne/mem-align&quot; ) type Car struct { flag bool age int32 F1 int8 age2 int32 age3 int16 F2 int64 F3 *int32 InnerStruct struct{ InnerByte byte //InnerStr string } F4 []byte Name string F5 error } func main(){ memalign.PrintStructAlignment(Car{}) } 输出: 相同字符,相同颜色的的字符,表示同一个Field。 后记 其实golang官方在go vet中提供了一个子工具fieldalignment golangci-lint也是使用这个分析器进行内存对齐的检查 安装 fieldalignment go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest 使用-fix参数可以直接修改代码文件 fieldalignment -fix {packagePath} 参考资料 opennota/check mdempsky/maligned Structure size optimization in Golang (alignment/padding) size and alignment guarantees fieldalignment

December 13, 2021 · 1 min

RocketMQ架构设计中的"暴力美学"(1)-NameServer高可用

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1.引言 人们在潜意识里,总会觉得复杂且精巧的东西是好东西。但是这个复杂这个词在软件架构设计中,却不一定是好事情。因为过于精巧和复杂的系统往往意味着系统更难以维护,出现问题后,故障更难排查。萌叔在阅读和分享RocketMQ的过程中,发现它有很多设计非常的简单粗暴,堪称"暴力美学"的典范,同时又给人眼前一亮的感觉(还能这么玩)。 2. NameServer高可用 在RocketMQ的架构体系中,NameServer的作用类似于注册中心,Broker会周期性的向NameServer发送心跳,注册Topic信息。Producer和Consumer会向NameServer查询某个Topic的路由信息(Topic位于哪个Broker) Topic路由信息 { &quot;OrderTopicConf&quot;: &quot;&quot;, &quot;queueDatas&quot;: [{ &quot;brokerName&quot;: &quot;broker-3&quot;, &quot;readQueueNums&quot;: 4, &quot;writeQueueNums&quot;: 4, &quot;perm&quot;: 6, &quot;topicSynFlag&quot;: 0 }, { &quot;brokerName&quot;: &quot;broker-4&quot;, &quot;readQueueNums&quot;: 4, &quot;writeQueueNums&quot;: 4, &quot;perm&quot;: 6, &quot;topicSynFlag&quot;: 0 }], ... } 如上面所示,某个Topic位于broker-3和broker-4,每个Broker上有4个MessageQueue 那么如何保证部分NameServer实例宕机后,注册中心的功能仍然能够正常运转呢? 按照正常点的想法,肯定是NameServer分成Master和Slave,然后Master和Slave之间在加上数据同步,如果Master宕机了,只要进行主从切换即可。这种做法肯定没问题,毕竟Hadoop中的NameNode也就是这么做的。 RocketMQ中的实现要简单的多,每个NameServer相互独立,并且它们之间没有通信。Broker会向每一个NameServer、NameServer1、NameServer2等等发送心跳,心跳中包含有它所维护的每个Topic的信息),这样每个NameServer就都含有路由信息。 等到Producer和Consumer向NameServer查询路由信息时,它会尝试向每一个NameServer进行请求。 下面的代码萌叔使用的是rocketmq-client-go的代码,因为rocketmq-client-go比Java的代码更加明显。 internal/route.go#queryTopicRouteInfoFromServer func (s *namesrvs) queryTopicRouteInfoFromServer(topic string) (*TopicRouteData, error) { request := &amp;GetRouteInfoRequestHeader{ Topic: topic, } var ( response *remote.RemotingCommand err error ) ... // 遍历每一个NameServer for i := 0; i &lt; s.Size(); i++ { rc := remote.NewRemotingCommand(ReqGetRouteInfoByTopic, request, nil) ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) response, err = s.nameSrvClient.InvokeSync(ctx, s.getNameServerAddress(), rc) if err == nil { cancel() break } cancel() } if err != nil { rlog.Error(&quot;connect to namesrv failed.&quot;, map[string]interface{}{ &quot;namesrv&quot;: s, &quot;topic&quot;: topic, }) return nil, primitive.NewRemotingErr(err.Error()) } switch response.Code { case ResSuccess: if response.Body == nil { return nil, primitive.NewMQClientErr(response.Code, response.Remark) } routeData := &amp;TopicRouteData{} err = routeData.decode(string(response.Body)) if err != nil { rlog.Warning(&quot;decode TopicRouteData error: %s&quot;, map[string]interface{}{ rlog.LogKeyUnderlayError: err, &quot;topic&quot;: topic, }) return nil, err } return routeData, nil case ResTopicNotExist: return nil, ErrTopicNotExist default: return nil, primitive.NewMQClientErr(response.Code, response.Remark) } }

December 7, 2021 · 1 min

服务调优经验总结

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 萌叔工作十多年,有不少服务调优的经验,在这里整理一下思路。希望对自己和同行有所帮助。 1. 发现问题 1.1 发现问题比解决问题难度更大 找出系统瓶颈和问题难度很大,一般只要能够找出且能够稳定复现,问题都能够被解决。 1.2 压测能够帮我们发现系统瓶颈,找出问题 压测不仅可以帮助我们发现系统瓶颈,还能用于发现死锁问题 1.3 尽可能细粒度的监控指标对发现问题帮助很大 讲几个容易忽略的细节 使用Cache的场景,要留意Cache的命中率 使用Channel的细节,要监控channel的长度,注意Channel是否写满,阻塞其它协程 对于比较复杂的接口,如果延迟高,需要有tracing记录,关注到底是那个环节耗时过多 对于MQ和数据库的场景,甚至还需要监控PageCache的命中率和使用情况、磁盘IO等等 GC 的耗时(特别是Java中Full GC) 2. 常见的思路 2.1 绝大多数服务的系统瓶颈是IO 我们接触到的绝大多数服务,都是IO型服务,影响延迟的主要因素是IO,磁盘IO、网络IO,数据库、微服务的响应延迟等等 2.2 如果CPU是瓶颈,需要有火焰图,关注是哪个环节消耗的CPU过多?每个部分占比是否合理?(注意压测) 比如是否大量的CPU时间被消耗在了锁竞争、上下文切换、垃圾回收、调度等。 在Golang中,由于锁中含有自旋锁,如果对锁的竞争激烈,会有大量的CPU时间被消耗在锁上。 在Golang中,P设置不合理也会导致大量的上下文切换 见文章 GOMAXPROCS你设置对了吗? 2.3 某一类型的服务,往往会有一组需要额外注意的核心指标 某些特定的服务容易受到特定因素的影响,比如 基于模型的分类器,压测时要特别关注它的CPU使用率是否能够跑满 见文章 AI预测模型工程化性能调优 数据库服务要留意磁盘IO 2.4 GC 有GC的场景,还要特别留意GC可能带来的负面影响 2.5 日志对服务性能影响不小 写文件和写控制台的性能差不多, 每秒钟差不多30w条。假定一个请求产生30条日志,也就是说即使处理这个请求没有任何逻辑,光是打日志,QPS也就差不多1w左右。(这也是很多服务压测的QPS无法超过1w的原因)。 见文章 玩转高性能日志库ZAP(5)-异步写日志 当你不知道该怎么调优的时候,可以试试关闭所有日志看看。? 3. 常见的优化手段 3.1 串行变并发 在IO型服务非常常见 3.2 同步变异步 常见的场景是回写数据库 3.3 单条变批量 比如数据需要回写数据库,可以在内存中缓存一拨,批量回写数据库,可以减少网络开销,降低数据库的开销 另外像写日志文件,批量写可以减少系统调用,提高写入效率。 笔者之前曾做过测试,将日志从单条同步写改为异步批量写,吞吐能力提升了1倍多。 见文章 玩转高性能日志库ZAP(5)-异步写日志 3.4 善用Cache 在多读少写的服务中,Cache对提升服务的能力,降低延迟,效果非常显著。(别忘了监控Cache) ...

December 4, 2021 · 1 min

rocketmq分享

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 注意:文中使用的部分图,误将ConsumeQueue写成了ConsumerQueue 1.简介 RocketMQ是一个分布式消息和流数据平台,具有低延迟、高性能、高可靠性、万亿级容量和灵活的可扩展性。RocketMQ是2012年阿里巴巴开源的第三代分布式消息中间件,2016年11月21日,阿里巴巴向Apache软件基金会捐赠了RocketMQ;第二年2月20日,Apache软件基金会宣布Apache RocketMQ成为顶级项目。 2.架构 正常情况,写和读都走Master,Master如果宕机,读可以走Slave 在 RocketMQ 4.5 版本之前,RocketMQ 只有 Master/Slave 一种部署方式,虽然这种模式可以提供一定的高可用性但也存在比较大的缺陷。为了实现新的高可用多副本架构,RockeMQ 最终选用了基于 Raft 协议的 commitlog 存储库 DLedger。 2.1 四种角色 2.1.1 NameServer 存储元数据 topic -> broker 无状态 接收来自broker的心跳 检查与borker的通讯是否过期 Topic路由信息 { &quot;OrderTopicConf&quot;: &quot;&quot;, &quot;queueDatas&quot;: [{ &quot;brokerName&quot;: &quot;broker-3&quot;, &quot;readQueueNums&quot;: 4, &quot;writeQueueNums&quot;: 4, &quot;perm&quot;: 6, &quot;topicSynFlag&quot;: 0 }, { &quot;brokerName&quot;: &quot;broker-4&quot;, &quot;readQueueNums&quot;: 4, &quot;writeQueueNums&quot;: 4, &quot;perm&quot;: 6, &quot;topicSynFlag&quot;: 0 }], &quot;brokerDatas&quot;: [{ &quot;cluster&quot;: &quot;Default_Cluster&quot;, &quot;brokerName&quot;: &quot;broker-4&quot;, &quot;brokerAddrs&quot;: { &quot;0&quot;: &quot;192.168.12.123:10911&quot;, &quot;1&quot;: &quot;192.168.12.127:10911&quot; } }, { &quot;cluster&quot;: &quot;Default_Cluster&quot;, &quot;brokerName&quot;: &quot;broker-3&quot;, &quot;brokerAddrs&quot;: { &quot;1&quot;: &quot;192.168.12.220:10911&quot;, &quot;0&quot;: &quot;192.168.12.12:10911&quot; } }] } 2.1.2 Producter 有发往broker的心跳(Master) ...

November 25, 2021 · 3 min

Grafana-Variable配置小技巧

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 Dashboard是Grafana中非常重要的概念,每个Dashboard都是一个巨大的看板,在Dashboard上可以配置Panel(图表)。在Dashboard中有个特别的配置–Variables,Variables提供了用户和面板交互,可以动态刷新面板。 有些特殊的场景,需要有些小技巧,否则Variables仍然无法满足我们的要求。 1. Case 1 我要查看不同股票的日线图 1.1 Query SELECT trade_date AS &quot;time&quot;, close FROM daily_data where ts_code = &quot;$ts_code&quot; order by trade_date ts_code是股票的代码,显然如果只看股票代码,很难想起这个代码究竟对应的是哪一只股票。 最好的方式是如下图这样,在变量的下拉列表中,显示股票名称,但是在实际的Query中使用股票代码查询。 grafana支持这种玩法,称为text和value 1.2 变量配置 注意需要Grafana的版本在7.4.5 或以上 1.2.1 配置Query select concat(name, &quot;#&quot;, ts_code) from target; 这一步会查出形如下面的结果 中国平安#601318.SH 三一重工#600031.SH ... 1.2.2 分离出text和value 配置Regex /(?&lt;text&gt;.*)#(?&lt;value&gt;.*)/ 2. Case 2 我们有一组MySQL实例需要监控,MySQL的数量多达几十个。 但是实际上大家都知道,MySQL是主从的,1个主和多个从构成了1个集群。每个集群单独为1个业务提供服务。 MySQL实例如果这样列出来,萌叔根本不可能知道,这个实例属于哪个集群,支撑那个业务。 我们希望的效果,是先选择partner(业务方),然后再选择再从这个集群中选出关心的实例 2.1 Query 事实上,我们可以通过构建2个Variables来达到这个目标 2.1.1 Query1 label_values(mysql_up, partner) 得到变量partner 2.1.2 Query2 label_values(mysql_up{partner=&quot;$partner&quot;}, instance) Query2引用Query1的结果 3. Case 3 萌叔的服务部署在2个机房,机房名称是由k8s的维护定义好的,形如 ...

November 18, 2021 · 1 min

利用划分子集限制连接池的大小(2)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 2.3 连接池的变化情况 如果采用确定性算法来划分子集还需要考虑在有节点宕机或者Backend进行过滚动升级的情况下,引起的连接池中连接的变化情况。 这里先上结论,要保证连接池中的连接不大量的销毁和创建,Backend也需要绑定backendID,以确保对于每个Client选出的子集不发生重大的变化 绑定backendID确保调用Subset时,Backend在backends中的位置不发生变化。(绑定backendID可以利用分布式锁来实现) func Subset(backends []string, clientID, subsetSize int) 我们来看一个反例,假定某个Backend backend-10实例宕机,连接池的变化情况 subset/certainty4/verify.go 结果 所有连接数:2500, 重建的连接总数:1257, 百分比: 50.280000 % 因为一个实例宕机,整个集群的连接池中一半的连接都要重新销毁,重连一次,开销还是非常大的。大家都知道集群做滚动发布,持续时间可能长达数分钟。也就是在这数分钟内Client的连接池可能会频繁的发生销毁和创建,这不得不引起我们的重视。不过好在发布动作在服务运行的整个生命周期占比不大。 3. 效果 最后我们来看下,使用划分子集方案后,带来的实际效果 连接数大概下降了30% 常驻内存大概下降40% 协程数下降30% 令人惊喜的是,GC耗时也有了一定程度的下降 总结 使用划分子集的方案来限制连接池的大小确实有不俗的效果,但也要注意细节和风险点。河豚虽然美味,但也有可能有毒。 参考资料 用subsetting 限制连接池中的连接数量 load balancing datacenter/

August 24, 2021 · 1 min