聊聊Golang中的锁(2)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 在 聊聊golang中的锁(1) 中,笔者提到Golang的锁,可能引发的非常高的CPU消耗,本文我们一起来探究一下,CPU时钟都消耗再了哪里。 2. 分析 修改代码, 使用pprof package main import ( "fmt" "sync" "time" _ "net/http/pprof" "net/http" "log" ) type LockBox struct{ sync.Mutex Value int } func deal(wpg *sync.WaitGroup, bp *LockBox, count int){ for i:=0;i< count;i++{ bp.Lock() bp.Value++ bp.Unlock() } wpg.Done() } func main() { go func() { log.Println(http.ListenAndServe(":18081", nil)) }() timeStart := time.Now() workerCount := 100 taskCount := 10000000000 var wg sync.WaitGroup var box LockBox for i:=0;i<workerCount;i++{ wg.Add(1) go deal(&wg, &box, taskCount/workerCount) } wg.Wait() fmt.Println("cost", time.Since(timeStart)) } 记录采样运行数据 ...

October 10, 2018 · 1 min

Golang 内存分配优化

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 前言 如果你正在使用imroc/req io.Copy 或 ioutil.ReadAll,或者尝试对大量对象的内存分配和释放的场景进行优化,这篇文章可能对你有帮助 2. 起因 在我们的一个程序中,使用库imroc/req请求后端的HTTP服务, 从HTTP响应读取结果 有如下调用关系 Resq.Bytes() -> Resq.ToBytes() -> ioutil.ReadAll(r io.Reader) -> Buff.ReadFrom(r io.Reader) 使用pprof收集内存累计分配情况可以发现大量的内存分配由Buffer.grow(n int) 触发 3. 原因分析 ReadFrom的源码如下 // MinRead is the minimum slice size passed to a Read call by // Buffer.ReadFrom. As long as the Buffer has at least MinRead bytes beyond // what is required to hold the contents of r, ReadFrom will not grow the // underlying buffer. const MinRead = 512 // ReadFrom reads data from r until EOF and appends it to the buffer, growing // the buffer as needed. The return value n is the number of bytes read. Any // error except io.EOF encountered during the read is also returned. If the // buffer becomes too large, ReadFrom will panic with ErrTooLarge. func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) { b.lastRead = opInvalid for { i := b.grow(MinRead) m, e := r.Read(b.buf[i:cap(b.buf)]) if m < 0 { panic(errNegativeRead) } b.buf = b.buf[:i+m] n += int64(m) if e == io.EOF { return n, nil // e is EOF, so return nil explicitly } if e != nil { return n, e } } } 由于ioutil.ReadAll不知道最终需要多大的空间来存储结果数据,它采取的做法是,初始分配一个较小的Buff(大小为MinRead), 一边从输入中读取数据放入Buff, 一边看是否能够存下所有数据,如果不能则尝试扩大这个Buff(调用Buffer.grow()) ...

October 8, 2018 · 3 min

聊聊Golang中的锁(1)--可能造成较高的CPU消耗

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 关于Golang中的锁的实现和源码早就文章阐述过了,这里我从实验的角度让大家看看对协程数量对任务执行速度,以及CPU开销的影响 运行环境 CPU: Intel(R) Xeon(R) CPU E5-2650 v4 @ 2.20GHz 8核 内存:16G GO version 1.10 协程数量分别是1, 2, 5, 10, 100, 200下完成1000000000次计数所花 总时间 CPU消耗 CPU峰值 分析 观察图可以发现, 协程数量是1的时候运行总时间反而最短,只耗时2.8秒,协程数量10时,运行总时间最长,耗时21.6秒 协程数量是100时,CPU峰值最大,为530%,协程数量是1是,CPU峰值最小,不到100% 代码 package main import ( "fmt" "sync" "time" ) type LockBox struct{ sync.Mutex Value int } func deal(wpg *sync.WaitGroup, bp *LockBox, count int){ for i:=0;i< count;i++{ bp.Lock() bp.Value++ bp.Unlock() } wpg.Done() } func main() { timeStart := time.Now() //workerCount := 1 //workerCount := 2 workerCount := 200 taskCount := 1000000000 var wg sync.WaitGroup var box LockBox for i:=0;i<workerCount;i++{ wg.Add(1) go deal(&wg, &box, taskCount/workerCount) } wg.Wait() fmt.Println("cost", time.Since(timeStart)) } 请我喝瓶饮料

October 8, 2018 · 1 min

机械键盘换轴记

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 1. 引言 我买的雷蛇的机械键盘,采用了1年半,坏了,先坏了F键,又坏了空格键和回车键,果然便宜没好货啊。买了几个cherry的轴体,本着人道主义精神再最后挽救一下。 2. 准备工作 2.1 cherry轴体 某宝上就有卖,红轴、茶轴、青轴3元/个,绿轴6元/个。 从阻尼的感受来看,从轻到重分别是红、棕、青(图上蓝色的那个)、绿 亲身的体验还是很重要的,可以各买1个试试,或者直接购买试轴器 轴体拆开了,就是下图这么个样子,外壳、铜片、弹簧、塑料的按键 不包含任何芯片,看起来原理也简单,但却真正考验配件的质量 2.2 电洛铁套装 2.3 螺丝刀套装 只用到一字和十字螺丝刀 2.4 拔轴器 因为笔者没有购买,这里直接用网上的图 3. 动手 3.1 拆除前置面板 注意 前置面板上有大量的卡扣,拆的时候要有耐心,用一字螺丝刀,一点点的撬开。 3.2 拆除后面板 注意 有几颗螺丝其实是隐藏在键盘的胶皮垫子下方,拆除时,不要忘了。 3.3 拆除原来的轴体 拆除轴体需要注意的以下事项 电洛铁加热到足够温度,首先沾少量的松香,再融开焊点的锡 此处松香作为助焊剂,沾松香可以使电洛铁更好的吸附焊点的锡,俗称挂锡。 2)待焊点的锡清除的差不多时,一边融开焊点的锡,一边可以使用拔轴器摁住轴体的卡扣(一字螺丝刀按住卡扣慢慢撬也可),向外慢慢的拔 注意 轴体有两侧有卡扣不能硬拔,否则可能损坏PCB板 小技巧 如果旧轴体已经拔出,但是PCB板的孔洞没有露出,可以一边用电洛铁融开焊点,一边用细铁丝(曲别针即可)插入小孔反复抽插。这样可以使孔洞露出。 3)插入新的cherry轴体,重新焊接好即可 4. 意外的惊喜 即使不小心电洛铁烫坏了PCB板的触点,键盘也未必完全报废 笔者在拆除旧的轴体时烫坏了一个触点(下图C位置)。通过万用表测量发现A、B、D均有4v左右的电压,唯独C点没有电压。初步揣测,键盘内部可能是并联电路,所以尝试在B和C之间用导线连接。经过测试,C位置的按键功能恢复。 最后来一张轴体安装完毕的图 请我喝瓶饮料

September 24, 2018 · 1 min

玩转高性能日志库ZAP(3)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 引言 前几天有朋友问我,zap库是否支持同时打印到多个目标地址,比如1份打印到文件,1份到控制台,1份打印到kafka中。这是所有日志库都会支持的功能,zap当然也不例外。 示例 package main import ( "io/ioutil" "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack" "os" ) func main(){ // First, define our level-handling logic. // 仅打印Error级别以上的日志 highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.ErrorLevel }) // 打印所有级别的日志 lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool { return lvl >= zapcore.DebugLevel }) hook := lumberjack.Logger{ Filename: "/tmp/abc.log", MaxSize: 1024, // megabytes MaxBackups: 3, MaxAge: 7, //days Compress: true, // disabled by default } topicErrors := zapcore.AddSync(ioutil.Discard) fileWriter := zapcore.AddSync(&hook) // High-priority output should also go to standard error, and low-priority // output should also go to standard out. consoleDebugging := zapcore.Lock(os.Stdout) // Optimize the Kafka output for machine consumption and the console output // for human operators. kafkaEncoder := zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()) consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()) // Join the outputs, encoders, and level-handling functions into // zapcore.Cores, then tee the four cores together. core := zapcore.NewTee( // 打印在kafka topic中(伪造的case) zapcore.NewCore(kafkaEncoder, topicErrors, highPriority), // 打印在控制台 zapcore.NewCore(consoleEncoder, consoleDebugging, lowPriority), // 打印在文件中 zapcore.NewCore(consoleEncoder, fileWriter, highPriority), ) // From a zapcore.Core, it's easy to construct a Logger. logger := zap.New(core) defer logger.Sync() logger.Info("constructed a info logger", zap.Int("test", 1)) logger.Error("constructed a error logger", zap.Int("test", 2)) } 输出结果 文件中 ...

September 17, 2018 · 2 min

gin 优雅退出

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 gin底层使用的是net/http, 所以gin的优雅退出就等于 http.Server的优雅退出, Golang 1.8以后提供了Shutdown函数,可以优雅关闭http.Server func (srv *Server) Shutdown(ctx context.Context) error 优雅退出的过程 1)关闭所有的监听 2)后关闭所有的空闲连接 3)无限期等待活动的连接处理完毕转为空闲,并关闭 如果提供了带有超时的Context,将在服务关闭前返回 Context的超时错误 完整的例子 package main import ( "net/http" "time" "os" "os/signal" "syscall" "fmt" "github.com/gin-gonic/gin" "github.com/pkg/errors" ) func SlowHandler(c *gin.Context) { fmt.Println("[start] SlowHandler") //time.Sleep(30 * time.Second) time.Sleep(30 * time.Second) fmt.Println("[end] SlowHandler") c.JSON(http.StatusOK, gin.H{ "message": "success", }) } // 实现context.Context接口 type ExitContext struct{ Chan chan struct{} DeadLineTime time.Time } func NewExitContext(duration time.Duration) *ExitContext{ cxt := ExitContext{} cxt.DeadLineTime = time.Now().Add(duration) cxt.Chan = make(chan struct{}, 1) return &cxt } func (cxt *ExitContext) Done() <-chan struct{}{ if time.Now().After(cxt.DeadLineTime){ cxt.Chan <- struct{}{} } return cxt.Chan } func (cxt *ExitContext) Err() error{ return errors.New("can't exit before Specified time") } // 无意义的空函数 func(cxt *ExitContext) Value(key interface{}) interface{}{ return nil } func(ctx *ExitContext) Deadline() (deadline time.Time, ok bool){ deadline = ctx.DeadLineTime ok = true return } func main() { r := gin.Default() // 1. r.GET("/slow", SlowHandler) server := &http.Server{ Addr: ":8080", Handler: r, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 << 20, } go server.ListenAndServe() // 设置优雅退出 gracefulExitWeb(server) } func gracefulExitWeb(server *http.Server) { ch := make(chan os.Signal) signal.Notify(ch, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) sig := <-ch fmt.Println("got a signal", sig) now := time.Now() cxt := NewExitContext(3*time.Second) err := server.Shutdown(cxt) if err != nil{ fmt.Println("err", err) } // 看看实际退出所耗费的时间 fmt.Println("------exited--------", time.Since(now)) } 更简单的写法 ...

September 14, 2018 · 2 min

玩转高性能日志库ZAP (2)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 uber开源的高性能日志库zap, 除了性能远超logrus之外,还有很多诱人的功能,比如支持日志采样、支持通过HTTP服务动态调整日志级别。不过他原生不支持文件归档,如果要支持文件按大小或者时间归档,必须要使用第三方库, 根据官方资料参考资料1,官方推荐的是 natefinch/lumberjack 示例 package main import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) // logpath 日志文件路径 // loglevel 日志级别 func initLogger(logpath string, loglevel string) *zap.Logger { hook := lumberjack.Logger{ Filename: logpath, // 日志文件路径 MaxSize: 1024, // megabytes MaxBackups: 3, // 最多保留3个备份 MaxAge: 7, //days Compress: true, // 是否压缩 disabled by default } w := zapcore.AddSync(&hook) var level zapcore.Level switch loglevel { case "debug": level = zap.DebugLevel case "info": level = zap.InfoLevel case "error": level = zap.ErrorLevel default: level = zap.InfoLevel } encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder core := zapcore.NewCore( zapcore.NewConsoleEncoder(encoderConfig), w, level, ) logger := zap.New(core) logger.Info("DefaultLogger init success") return logger } func main() { logger := initLogger("/tmp/all.log", "info") logger.Info("test log", zap.Int("line", 47)) } 文件的清理策略如下 Cleaning Up Old Log Files Whenever a new logfile gets created, old log files may be deleted. The most recent files according to the encoded timestamp will be retained, up to a number equal to MaxBackups (or all of them if MaxBackups is 0). Any files with an encoded timestamp older than MaxAge days are deleted, regardless of MaxBackups. Note that the time encoded in the timestamp is the rotation time, which may differ from the last time that file was written to. ...

September 12, 2018 · 2 min

玩转高性能日志库zap (1)

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 前言 uber开源的高性能日志库zap, 除了性能远超logrus之外,还有很多诱人的功能,比如支持数据采样、支持通过HTTP服务动态调整日志级别 参考资料1虽然提到了可以通过HTTP服务动态调整日志级别,但是没有给出可用的代码实现,这里给出一个样例。 动态调整日志级别 非常简单直接上代码 package main import ( "fmt" "go.uber.org/zap" "net/http" "time" ) func main() { alevel := zap.NewAtomicLevel() http.HandleFunc("/handle/level", alevel.ServeHTTP) go func() { if err := http.ListenAndServe(":9090", nil); err != nil { panic(err) } }() // 默认是Info级别 logcfg := zap.NewProductionConfig() logcfg.Level = alevel logger, err := logcfg.Build() if err != nil { fmt.Println("err", err) } defer logger.Sync() for i := 0; i < 1000; i++ { time.Sleep(1 * time.Second) logger.Debug("debug log", zap.String("level", alevel.String())) logger.Info("Info log", zap.String("level", alevel.String())) } } 查看日志级别 ...

September 12, 2018 · 1 min

简单的Golang 协程池

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 引言 很多时候,还是需要用到协程池的,简单撸了一个,方便工作中的需要,有bug请反馈 1. 安装 go get github.com/vearne/golib/utils 2. 使用 2.1 创建协程池 // 协程池的大小是30 var p *utils.GPool = utils.NewGPool(30) 2.2 定义任务处理函数 任务处理函数形如 type JobFunc func(param interface{}) *GPResult GPResult type GPResult struct { Value interface{} Err error } 执行任务 ApplyAsync(f JobFunc, slice []interface{}) 示例 pool.go package main import ( "github.com/vearne/golib/utils" "log" "strconv" ) func Judge(key interface{}) *utils.GPResult { result := &utils.GPResult{} num, _ := strconv.Atoi(key.(string)) if num < 450 { result.Value = true } else { result.Value = false } return result } func main() { p := utils.NewGPool(30) slice := make([]interface{}, 0) for i := 0; i < 1000; i++ { slice = append(slice, strconv.Itoa(i)) } result := make([]bool, 0, 10) trueCount := 0 falseCount := 0 for item := range p.ApplyAsync(Judge, slice) { value := item.Value.(bool) result = append(result, value) if value { trueCount++ } else { falseCount++ } } log.Printf("cancel, %v, true:%v, false:%v\n", len(result), trueCount, falseCount) } 致谢 程序的API形式以及思路参考了python的multiprocessing模块,再此表示感谢 ...

September 5, 2018 · 1 min

一次电视盒子(Android)后端API的测试经历

版权声明 本站原创文章 由 萌叔 发表 转载请注明 萌叔 | http://vearne.cc 引言 最近笔者在为电视盒子APP开发API后端,API服务的开发不复杂,可是如何进行测试确实让我伤透了脑筋。在此把最终的方案记录下来,以享众人。 1. 解决域名指向问题 1.1 最初,我们希望的做法是用Charles或是Fiddler,在盒子上设置代理,然后把盒子的流量引到我的测试机上。默认盒子无法配置网络代理,可能需要使用如下命令调出Android的原生设置界面 自行下载对应版本的Settings.apk(小米盒子无需安装) install adb install Settings.apk 3)启动Settings adb shell am start -n com.android.settings/com.android.settings.Settings 但是发现很多流量并没有通过Charles。原来在Android设备上设置网络代理,并无法确保所有的服务都使用这个代理,APP可以选择使用这个代理。 1.2 最后我们直接使用极路由(路由器) 的自定义hosts插件 修改完成以后,可以连上设备,验证一下域名的解析修改是否生效 adb shell ping abc.com PS:另外也可以通过让设备root然后直接修改盒子的/etc/hosts文件,其实我们还曾经想过使用dnsmasq污染DNS的方式来修改域名指向,但是在盒子上貌似并没有生效。 2. 解决HTTPS证书问题 APP对后端的调用大部分接口走的都是HTTPS。对于测试场景,我们不应该使用公司真实的SSL证书,因此我选择自签名SSL证书。 自签名的证书,因为加密算法或者摘要算法选择不当仍然被浏览器或者设备认为是不安全的。 这里推荐一个库 Fishdrowned/ssl 生成完毕,需要在nginx配置域名的证书和私钥,同时需要将根证书导入设备 adb push ca.perm /sdcard/download/ 导入后,需要在打开盒子界面,安装此根证书 adb shell am start -n com.android.settings/com.android.settings.Settings 总结 本文介绍了如何针对移动设备(Android)后端API服务进行测试,希望能对大家有所启发。 请我喝瓶饮料

August 31, 2018 · 1 min