分类目录归档:Uncategorized

3分钟AC68U打造成Time Machine

最近为了100M光纤,一咬牙买了台好点的华硕AC68U路由器。
结果发现这货竟然可以当作Time Machine用,原来想过用树莓派做的,结果发现传输速度太慢,只有仅仅1Mb/s,希望这次的AC68U能给力点。

因为常见的fat32不支持4G以上的文件,NTFS系统苹果又没有自带的,Linux支持也不好,所以我就准备用AC68U格式化磁盘。

首先是登入路由器,“系统管理 -> 系统设置”

把ssh选项打开,把自己的公钥,注意是公钥!文件名以.pub结尾的!把里面的内容粘贴到上图的黑框中,然后应用设置。

接着登入路由器

ssh admin@192.168.1.1

注意是用户名是admin,不是root

把移动硬盘接上。格式化成ext3,为啥是sda,因为我只接上了一个磁盘。

mkfs.ext3 /dev/sda

等格式化完成后,就可以愉快地使用咯,点击“USB 相关应用 -> Time Machine“,开启并选定我们刚才格式化好的磁盘。

打开Mac里的Time Machine,注意这里链接时需要的账号密码是路由器的管理员账号密码 ( = =)

这样就开始自动备份了~

Go零消耗debug log技巧

tL;DR, 本文末尾提供零消耗的日志代码,最高性能提升60000%。

看到题目,有人肯定会问,官方的log模块不好么?
Debug Log一般很长,在生产环境还输出的话,也很难找。
再者,log的消耗是比较大的,特别是需要打印行号时。

https://golang.org/src/log/log.go#L158

if l.flag&(Lshortfile|Llongfile) != 0 {
// Release lock while getting caller info - it's expensive.
l.mu.Unlock()
var ok bool
_, file, line, ok = runtime.Caller(calldepth)
if !ok {
file = "???"
line = 0
}
l.mu.Lock()
}

因为需要调用runtime.Caller,这样性能就有较多的损耗。
简单的benchmark,可以发现慢50%。

func BenchmarkWithLine(b *testing.B) {
logger := log.New(ioutil.Discard, "", log.Llongfile|log.LstdFlags)
tf := strings.Repeat("abcde", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
logger.Print(tf)
}
}
// BenchmarkWithLine-4 500000 2806 ns/op
// BenchmarkWithoutLine-4 1000000 1754 ns/op

虽然,log的性能不差,仅需要1us就能进行一次,但如果在代码中有大量的debug日志,这个损耗累积起来,那也是相当惊人的了。

那么,在生产环境,能不能不执行log语句呢?
可以的,例如

const Dev = false
func BenchmarkConst(b *testing.B) {
logger := log.New(ioutil.Discard, "", log.LstdFlags)
tf := strings.Repeat("abcde", 1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if Dev {
logger.Print(tf)
}
}
}
// BenchmarkConst-4 2000000000 0.29 ns/op

go tool objdump查看生成的二进制文件

 log_test.go:36 0x4efc32 48890424 MOVQ AX, 0(SP)
log_test.go:36 0x4efc36 e815d4fbff CALL testing.(*B).ResetTimer(SB)
log_test.go:36 0x4efc3b 488b842480000000 MOVQ 0x80(SP), AX
log_test.go:36 0x4efc43 31c9 XORL CX, CX
log_test.go:38 0x4efc45 eb03 JMP 0x4efc4a
log_test.go:38 0x4efc47 48ffc1 INCQ CX
log_test.go:38 0x4efc4a 488b90f0000000 MOVQ 0xf0(AX), DX
log_test.go:38 0x4efc51 4839d1 CMPQ DX, CX
log_test.go:38 0x4efc54 7cf1 JL 0x4efc47
log_test.go:43 0x4efc56 488b6c2470 MOVQ 0x70(SP), BP
log_test.go:43 0x4efc5b 4883c478 ADDQ $0x78, SP
log_test.go:43 0x4efc5f c3 RET

可以看出,ResetTimer之后,仅仅是跑了个空的for循环,这是因为编译器发现if语句永远不成立,所以不编译这一段了(如果Dev是var值,那么还是会对比一下,而不是没有语句生成),不过这个方法需要每次debug时都要改代码。不想改代码可以用go build -ldflags -X方法, 但这个仅仅支持字符串,特别麻烦。

所以有没有更好的解决方案呢?有的,使用build tags
下面是例子,一共三个文件:

log.go

package main
func main() {
Debug("it's expensive")
if Dev {
fmt.Println("we are in develop mode")
}
}

log_debug.go

//+build debug
package main
import (
"fmt"
)
const Dev = true
func Debug(a ...interface{}) {
fmt.Println(a...)
}

log_release.go

//+build !debug
package main
const Dev = false
func Debug(a ...interface{}) {}

debug和release最大的差别就在文件头的//+build !debug, 意思是告诉编译器,如果有debug这个tags,那么编译的时候就略过这个文件。
比如你运行go build -tags "debug" && ./main就会输出,不设定的话,就什么都不输出。

再用go tool objdump 查看生成的可执行文件,跟之前的if Dev效果相同,压根不生成语句。这样就不用每次都改代码了来debug了,是不是很赞啊?

对于Debug函数,由于Go的函数是first class,所以Call function不可避免,不过性能损失基本上为零了。

package main
import "testing"
var a = strings.Repeat("abcde", 1024)
func BenchmarkDebug(b *testing.B) {
for i := 0; i < b.N; i++ {
Debug(a)
}
}
go test -bench=.
BenchmarkDebug-4 500000000 3.27 ns/op
go test -bench=. -tags debug
BenchmarkDebug-4 10000000 146 ns/op

总结一下,如果极度要求性能,尽量使用if Dev这种判断模式,如果要求不高,可以使用Debug函数的方法。

Yaml To Go

项目地址:Yaml-To-Go

最近工作需要把yaml配置改成Go的对象,我知道有个json-to-go,但是没有发现yaml-to-go,所以就自己搞了一套。

其实原理很简单,主要是把原来的jsonToGo函数里的解析函数全部替换成yaml的解析器。
虽然简单,但是实际上移植的时候,发现js的库太复杂了……npm 不想安装,怎么办?

Heavy NPM

可以看项目里的dist文件夹,里面会有有编译好的文件。
这样就可以用老办法,直接用script标签载入了。

两行开启Go http quic

QUIC,简单来说,就是使用UDP的传输协议,根据Google自己的报告,速度可以加快30%。
主要优点有:

1. 快速建立链接(不用3次握手和TLS4次握手)
2. 多路复用
3. 改进的流控
4. 快速SSL/TLS握手
5. 适合移动用户访问

quic-layer

这么好的性能,当然要赶紧用Go试试看。

https://github.com/lucas-clemente/quic-go

示例中的代码也很简单。

http.Handle("/", http.FileServer(http.Dir(wwwDir)))
h2quic.ListenAndServeQUIC("localhost:4242", "/path/to/cert/chain.pem", "/path/to/privkey.pem", nil)

不过在实践里,还是碰到了2个坑。

TLS配置

因为我的服务是一个http.Handler, 所以quic需要重新配置TLSconfig,否则就会报错。
下面是示例代码

quic := &h2quic.Server{Server: server}
quic.TLSConfig = &tls.Config{}
quic.TLSConfig.GetCertificate = getCertificate
pln, err := net.ListenPacket("udp", cfg.Listen)
if err != nil {
log.Fatal(err)
}
log.Print("listen quic on udp:%s", cfg.Listen)
go quic.Serve(pln)

HEADER设置

成功启用后,Chrome中的SPDY插件并没有出现绿色的标志,还是继续使用HTTP2,经过查找后,发现Google在自家的header中添加了

 writer.ResponseWriter.Header().Add("alt-svc", `quic=":443"; ma=2592000; v="38,37,36"`)

其中

  • ma是过期时间,单位是秒
  • v是指支持的quic版本
  • alt-svc是alternative-service的缩写
  • quic中是quic的端口,我指定了443

最后通过在chrome地址栏中输入

chrome://net-internals/#quic

quic-demo

Go汇编实战的坑

为啥写

Go的汇编一直是我感兴趣的地方,为了验证之前所学的汇编知识和好玩 ,我决定往Go官方提交一个性能patch。
所以到官方的标准库里搜了一圈,发现adler32并没有硬件加速的实现,而Intel已经公布了相关的SSE加速实现
https://github.com/01org/isa-l/

所以我决定把Intel的抄过来,结果不停地掉坑和爬出来,终于提交了patch(撒花)

https://go-review.googlesource.com/c/51850

希望在Go 1.10发布的时候能进入官方源:)

  1. 写法的区别
  2. Go汇编不支持
  3. 难以调试
  4. 内存越界与LEA
  5. 向官方提交代码需要注意的地方,保证Change ID一致

坑一:Intel和AT&T的写法的区别

Intel写法是:
opcode destination source

AT&T 也就是Go官方汇编语言的写法正好是反过来的:
opcode destination source

抄代码的时候有点绕

坑二 Go汇编不支持:

Go的维护者们对于新添加汇编opcode一直是很保守的,比如2001年前后就有的SSE2 里的PSHLLW()竟然不支持(= =||)。
所以得自己填BYTE,比如官方文档中的MOVQ用BYTE方式编写 https://golang.org/doc/asm

BYTE $0x0f; BYTE $0x6f; BYTE $0x00 // MOVQ (AX), M0

但这里就有一个坑,比如PSHLLD和PSHRQ的Opcode是一样的……只是按/r 寄存器类型进行区别。需要注意

坑三:调试困难

一般代码测试时,都可以直接输出日志,帮助定位问题,但是汇编不行,所以我是通过把需要的值放入某个不用的寄存器, 例如

#define debug R15
// min(a, b int) int
TEXT min(SB), NOSPLIT, $0

在需要的时候,提前把函数返回

MOVQ debug, ret+16(FP)
RET

当然应该有更好的方法,可以把所有寄存器打印出来,不过这个方法够我自己调试用了。

坑四:内存越界与LEA

一般程序中都是指针,或者直接结构体。不过,汇编这里回归本真,只有内存地址和寄存器。所以一定要小心访问数据的边界和跳转的内存地址。
这里学到了一个LEA的用法,地址计算器,把内存地址到目标寄存器里。

LEAQ 0(data)(size*1), end

具体用法

  • 0: 写死的偏移量
  • data: 偏移值
  • size: 动态偏移量,可以用乘法

坑五:

提交代码到gerrit要保持Change ID相同,要不然算一个新的Change。

附录:opcode坑

  • MOVOU: O=oct, U=unaligned, 指的是八个word,即128位,用于XXM寄存器的移动。
  • TESTQ: 对比值,并影响FLAG
  • DIVL:低位除,如果高位有数据就会出错

资料:
Go官方介绍: https://golang.org/doc/asm