技术哲学

文化、风格、艺术


  • 首页

  • 分类

  • 归档

  • 标签

Debugging Go Program

发表于 2019-01-24   |   分类于 软件开发   |  

概述

定位Go程序的错误,通常有两种方式:

  • 打印日志
  • 调试

Go是编译型语言,且IDE对调试的支持不太好,绝大多数Go的初学者调试Go程序都是通过log.Printf等打印日志方式定位问题。通常过程如下:

  1. 程序panic或者报错
  2. 修改Go程序,添加打印调试日志代码
  3. 编译Go程序
  4. 重复错误出现时的操作,查看日志
    1. 如果定位问题原因,修复程序错误,删除打印debug日志代码,返回第3步的操作
    2. 如果未定位问题原因,返回到第2步的操作

如果程序比较复杂,需要反复增加日志输出才能找到问题原因。熟练的使用调试器能够提高我们面对这样问题的灵活性。本文重点总结介绍调试相关知识,具体调试操作网上相关资料已经很全面(见最后一章参考),不作为重点。

使用GDB调试Go程序

简介

GDB不能很好的理解Go程序。Go程序和GDB的stack management、threading、runtime模型差异很大,并可能导致调试器输出不正确的结果。因此,虽然GDB在某些场景下有用,比如调试Cgo代码、调试runtime,但是对于Go程序来说,尤其是高并发程序,GDB不是一个可靠的调试器。而且,对于Go语言项目本身来说,解决这些的问题很困难,也不是一个高优先级的事情。

当你在Linux、macOS、FreeBSD、NetBSD上使用gc工具链编译和连接Go程序的时候,产生的二进制包含DWARFv4调试信息,最近版本的GDB调试器可以利用这些信息观察一个运行的进程或者core dump。

可以通过-w标记告诉连接器去掉这些调试信息,比如:

1
go build -ldflags "-w" .

gc编译器生成的程序包含函数内联和变量注册。这些优化可能会让gdb调试更加困难。如果你需要禁用这些优化,使用下面的参数构建程序:

1
go build -gcflags "all=-N -l" .

如果你想要使用gdb检查一个core dump,你可以在程序崩溃的时候触发一个dump。在支持dump的OS上,使用GOTRACEBACK=crash环境变量(参考runtime package documentation)。

Go 1.11版本中,由于编译器会产生更多更准确的调试信息,为了减少二进制的大小,DWARF调试信息编译时候会默认被压缩。这对于大多数ELF工具来说这是透明的,也得到Delve支持。但是macOS和Windows上一些工具不支持。如果要禁用DWARF压缩,可以在编译的时候传入参数-ldflags "-compressdwarf=false"。

Go 1.11添加了一个实验性的功能,允许在调试器中调用函数。目前这个特性仅得到Delve(version 1.1.0及以上)的支持。

常用命令和教程

可以参考下面几篇文章,这里不做赘述:

  • Debugging Go Code with GDB
    • Go 1.11 Release Notes | Debugging
    • Using the gdb debugger with Go
    • GDB Tutorial | Debugging with GDB

使用LLDB调试Go程序

简介

Mac下如果你安装XCode,应该会自动安装了LLDB,LLDB是XCode的默认调试器。LLDB的安装方法可以参考这里。

GDB的命令格式非常自由,和GDB的命令不同,LLDB命令格式非常结构化(“严格”的婉转说法)。LLDB的命令格式如下:

1
<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]

解释一下:

  • <command>(命令)和<subcommand>(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
  • <action>:执行命令的操作
  • <options>:命令选项。需要注意的是,如果aguments的第一个字母是”-“,<options>和<arguments>中间必须以”–”分隔开。所以如果你想启动一个程序,并给这个程序传入-program_arg value参数,可以输入(lldb) process launch --stop-at-entry -- -program_arg value
  • <arguement>:命令的参数
  • []:表示命令是可选的,可以有也可以没有

LLDB也减少了gdb中一些命令的特殊写法,让用户更加容易理解命令的意图。可以阅读LLDB文档中下面一段文字了解细节:

We also tried to reduce the number of special purpose argument parsers, which sometimes forces the user to be a little more explicit about stating their intentions.

……

LLDB的命令同样给很多命令提供了缩写形式,可以通过(lldb) help查看所有的缩写命令。

gdb和LLDB的命令之间的差别可以访问这里查看。

常用命令

使用LLDB需要熟悉的常用命令如下:

帮助

(lldb) help help
Show a list of all debugger commands, or give details about a specific command.

Syntax: help []

使用LLDB加载一个程序

1
2
3
4
5
6
$lldb /binary-path
Current executable set to '/binary-path'(x86_64).

$lldb
(lldb) file /binary-path
Current executable set to '/binary-path'(x86_64).

设置断点(breakpoints)

常见的设置断点的命令如下:

1
2
(lldb) breakpoint set --file source-file.go --line 11
Breakpoint 1: where = sample1`github.com/ethancai/go-debug-practice/sample1/model.(*MyStruct).Print + 19 at my_struct.go:11, address = 0x00000000010b2713

breakpoint命令会创建一个逻辑的断点,一个逻辑的断点可以对应一个或者多个位置location。比如,通过selector设置的断点对应所有实现了selector的方法。

breakpoint命令:

1
2
3
4
5
6
(lldb) help breakpoint
Commands for operating on breakpoints (see 'help b' for shorthand.)

Syntax: breakpoint <subcommand> [<command-options>]

...

设置观察点(Watchpoints)

watchpoint命令:

1
2
3
4
5
6
(lldb) help watchpoint
Commands for operating on watchpoints.

Syntax: watchpoint <subcommand> [<command-options>]

...

运行程序或者附着程序

process命令:

1
2
3
4
5
6
(lldb) help process
Commands for interacting with processes on the current platform.

Syntax: process <subcommand> [<subcommand-options>]

...

控制程序执行或者检查Thread状态

thread命令

1
2
3
4
5
6
(lldb) help thread
Commands for operating on one or more threads in the current process.

Syntax: thread <subcommand> [<subcommand-options>]

...

检查堆栈结构(Stack Frame)状态

frame命令

1
2
3
4
5
6
(lldb) help frame
Commands for selecting and examing the current thread's stack frames.

Syntax: frame <subcommand> [<subcommand-options>]

...

expression命令

1
2
3
4
5
6
7
(lldb) help expression
Evaluate an expression on the current thread. Displays any returned value with LLDB's
default formatting. Expects 'raw' input (see 'help raw-input'.)

Syntax: expression <cmd-options> -- <expr>

...

操作教程

可以参考下面几篇文章:

  • Debugging Go Code with LLDB](http://ribrdb.github.io/lldb/): (中文翻译)
  • 熟练使用 LLDB,让你调试事半功倍
  • LLDB Tutorial

使用Delve调试Go程序

可以参考下面几篇文章:

  • Debugging Go programs with Delve
    • Golang调试工具Delve

不要使用调试器

对于调试器,一众计算机大牛都给出了明确而且强烈的建议:不要使用调试器。

  • Linus Torvalds, the creator of Linux, does not use a debugger.
  • Robert C. Martin, one of the inventors of agile programming, thinks that debuggers are a wasteful timesink.
  • John Graham-Cumming hates debuggers.
  • Brian W. Kernighan and Rob Pike wrote that stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places. Kernighan once wrote that the most effective debugging tool is still careful thought, coupled with judiciously placed print statements.
  • The author of Python, Guido van Rossum has been quoted as saying that uses print statements for 90% of his debugging.

调试技术是一众纯手工的技术,诞生于计算机程序的规模还不是很大的时期。在当今软件规模不断扩展的情况下,调试无法解决软件质量问题。深入的思考、合理的架构、优美的代码、充分的单元测试才是提高软件质量的正确方向。调试应该仅作为调查问题最后一种办法。

参考

  • Debugging Go Program
    • Debugging Go Code with GDB
      • Go 1.11 Release Notes | Debugging
      • Using the gdb debugger with Go
      • GDB Tutorial | Debugging with GDB
    • Debugging Go Code with LLDB: (中文翻译)
      • 熟练使用 LLDB,让你调试事半功倍
      • LLDB Tutorial
    • Debugging Go programs with Delve
      • Golang调试工具Delve
    • Post-mortem debugging
      • Go Post-mortem
      • Debugging Go core dumps
    • Debugging Concurrent Programs
      • Debugging Concurrent Programs
      • Data Race Detector
  • Other
    • Diagnostics: Profiling, Tracing, Debugging, Rutime statistics and events
    • Build Go Program
      • Compile packages and dependencies
    • ELF format and Tools
      • The 101 of ELF files on Linux: Understanding and Analysis
      • Executable and Linkable Format
    • Methodology
      • I don’t use a debugger
      • Debugging golang programs
    • Debugging in IDE
      • VSCode
        • Debugging Go code using VS Code
        • DEBUGGING GO WITH VS CODE AND DELVE
      • Goland
        • Goland Help | Debugging code
  • Tools
    • GDB
    • LLDB
    • Delve: Delve is a debugger for the Go programming language
    • Spew: Implements a deep pretty printer for Go data structures to aid in debugging
    • panicparse: Crash your app in style (Golang)

Go语言中的错误处理(Error Handling in Go)

发表于 2017-12-29   |   分类于 软件开发   |  

概述

在实际工程项目中,我们希望通过程序的错误信息快速定位问题,但是又不喜欢错误处理代码写的冗余而又啰嗦。Go语言没有提供像Java、C#语言中的try...catch异常处理方式,而是通过函数返回值逐层往上抛。这种设计,鼓励工程师在代码中显式的检查错误,而非忽略错误,好处就是避免漏掉本应处理的错误。但是带来一个弊端,让代码啰嗦。

Go标准包提供的错误处理功能

error是个interface:

1
2
3
type error interface {
Error() string
}

如何创建error:

1
2
3
4
5
6
7
8
9
10
11
12
// example 1
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// implementation
}

// example 2
if f < 0 {
return 0, fmt.Errorf("math: square root of negative number %g", f)
}

如何自定义error:

1
2
3
4
5
6
7
8
// errorString is a trivial implementation of error.
type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

自定义error类型可以拥有一些附加方法。比如net.Error定义如下:

1
2
3
4
5
6
7
package net

type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}

网络客户端程序代码可以使用类型断言判断网络错误是瞬时错误还是永久错误。比如,一个网络爬虫可以在碰到瞬时错误的时候,等待一段时间然后重试。

1
2
3
4
5
6
7
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}

如何处理错误

go标准包提供的错误处理方式虽然简单,但是在实际项目开发、运维过程中,会经常碰到如下问题:

  • 函数该如何返回错误,是用值,还是用特殊的错误类型
  • 如何检查被调用函数返回的错误,是判断错误值,还是用类型断言
  • 程序中每层代码在碰到错误的时候,是每层都处理,还是只用在最上层处理,如何做到优雅
  • 日志中的异常信息不够完整、缺少stack strace,不方便定位错误原因

我一直思考Go语言中该如何处理错误。下面的内容介绍了生产级Go语言代码中如何处理错误。

以下内容,主要来自于Dave Cheney 写的一个演讲文档。

Go语言中三种错误处理策略

go语言中一般有三种错误处理策略:

  • 返回和检查错误值:通过特定值表示成功和不同的错误,上层代码检查错误的值,来判断被调用func的执行状态
  • 自定义错误类型:通过自定义的错误类型来表示特定的错误,上层代码通过类型断言判断错误的类型
  • 隐藏内部细节的错误处理:假设上层代码不知道被调用函数返回的错误任何细节,直接再向上返回错误

返回和检查错误值

这种方式在其它语言中,也很常见。比如,C Error Codes in Linux。

go标准库中提供一些例子:

  • io.EOF: 参考这里
  • syscall.ENOENT: 参考这里
  • go/build.NoGoError: 参考这里
  • path/filepath.SkipDir: 参考这里

这种策略是最不灵活的错误处理策略,上层代码需要判断返回错误值是否等于特定值。如果想修改返回的错误值,则会破坏上层调用代码的逻辑。

1
2
3
4
5
6
buf := make([]byte, 100)
n, err := r.Read(buf) // 如果修改 r.Read,在读到文件结尾时,返回另外一个 error,比如 io.END,而不是 io.EOF,则所有调用 r.Read 的代码都必须修改
buf = buf[:n]
if err == io.EOF {
log.Fatal("read failed:", err)
}

另外一种场景也属于这类情况,上层代码通过检查错误的Error方法的返回值是否包含特定字符串,来判定如何进行错误处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func readfile(path string) error {
err := openfile(path)
if err != nil {
return fmt.Errorf("cannot open file: %v", err)
}
//...
}

func main() {
err := readfile(".bashrc")
if strings.Contains(error.Error(), "not found") {
// handle error
}
}

error interface 的 Error 方法的输出,是给人看的,不是给机器看的。我们通常会把Error方法返回的字符串打印到日志中,或者显示在控制台上。永远不要通过判断Error方法返回的字符串是否包含特定字符串,来决定错误处理的方式。

如果你是开发一个公共库,库的API返回了特定值的错误值。那么必须把这个特定值的错误定义为public,写在文档中。

“高内聚、低耦合”是衡量公共库质量的一个重要方面,而返回特定错误值的方式,增加了公共库和调用代码的耦合性。让模块之间产生了依赖。

自定义错误类型

这种方式的典型用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义错误类型
type MyError struct {
Msg string
File string
Line int
}

func (e *MyError) Error() string {
return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}


// 被调用函数中
func doSomething() {
// do something
return &MyError{"Something happened", "server.go", 42}
}

// 调用代码中
err := doSomething()
if err, ok := err.(SomeType); ok { // 使用 类型断言 获得错误详细信息
//...
}

这种方式相比于“返回和检查错误值”,很大一个优点在于可以将 底层错误 包起来一起返回给上层,这样可以提供更多的上下文信息。比如os.PathError:

1
2
3
4
5
6
7
8
9
// PathError records an error and the operation
// and file path that caused it.
type PathError struct {
Op string
Path string
Err error
}

func (e *PathError) Error() string

然而,这种方式依然会增加模块之间的依赖。

隐藏内部细节的错误处理

这种策略之所以叫“隐藏内部细节的错误处理”,是因为当上层代码碰到错误发生的时候,不知道错误的内部细节。

作为上层代码,你需要知道的就是被调用函数是否正常工作。 如果你接受这个原则,将极大降低模块之间的耦合性。

1
2
3
4
5
6
7
8
9
import “github.com/quux/bar”

func fn() error {
x, err := bar.Foo()
if err != nil {
return err
}
// use x
}

上面的例子中,Foo这个方法不承诺返回的错误的具体内容。这样,Foo函数的开发者可以不断调整返回错误的内容来提供更多的错误信息,而不会破坏Foo提供的协议。这就是“隐藏内部细节”的内涵。

最合适的错误处理策略

上面我们提到了三种错误处理策略,其中第三种策略耦合性最低。然而,第三种方式也存在一些问题:

  • 如何获得更详细错误信息,比如stack trace,帮助定位错误原因
  • 如何优雅的处理错误
    • 有些场景需要了解错误细节,比如网络调用,需要知道是否是瞬时的中断
    • 是否每层捕捉到错误的时候都需要处理

如何输出更详细的错误信息

1
2
3
4
5
6
7
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return err // No such file or directory
}
return nil
}

上面这段代码,在我看来,在顶层打印错误的时候,只看到一个类似于”No such file or directory”的文字,从这段文字中,无法了解到错误是哪行代码产生的,也无法知道当时出错的调用堆栈。

我们调整一下代码,如下:

1
2
3
4
5
6
7
func AuthenticateRequest(r *Request) error {
err := authenticate(r.User)
if err != nil {
return fmt.Errorf("authenticate failed: %v", err) // authenticate failed: No such file or directory
}
return nil
}

通过fmt.Errorf创建一个新的错误,添加更多的上下文信息到新的错误中,但这样仍不能解决上面提出的问题。

这里,我们通过一个很小的包github.com/pkg/errors来试图解决上面的问题。这个包提供这样几个主要的API:

1
2
3
4
5
// Wrap annotates cause with a message.
func Wrap(cause error, message string) error

// Cause unwraps an annotated error.
func Cause(err error) error

goErrorHandlingSample这个repo中的例子演示了,不同错误处理方式,输出的错误信息的区别。

1
2
3
4
> go run sample1/main.go

open /Users/caichengyang/.settings.xml: no such file or directory
exit status 1
1
2
3
4
> go run sample2/main.go

could not read config: open failed: open /Users/caichengyang/.settings.xml: no such file or directory
exit status 1
1
2
3
4
> go run sample3/main.go

could not read config: open failed: open /Users/caichengyang/.settings.xml: no such file or directory
exit status 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> go run sample4/main.go

open /Users/caichengyang/.settings.xml: no such file or directory
open failed
main.ReadFile
/Users/caichengyang/go/src/github.com/ethancai/goErrorHandlingSample/sample4/main.go:15
main.ReadConfig
/Users/caichengyang/go/src/github.com/ethancai/goErrorHandlingSample/sample4/main.go:27
main.main
/Users/caichengyang/go/src/github.com/ethancai/goErrorHandlingSample/sample4/main.go:32
runtime.main
/usr/local/Cellar/go/1.9.2/libexec/src/runtime/proc.go:195
runtime.goexit
/usr/local/Cellar/go/1.9.2/libexec/src/runtime/asm_amd64.s:2337
could not read config
main.ReadConfig
/Users/caichengyang/go/src/github.com/ethancai/goErrorHandlingSample/sample4/main.go:28
main.main
/Users/caichengyang/go/src/github.com/ethancai/goErrorHandlingSample/sample4/main.go:32
runtime.main
/usr/local/Cellar/go/1.9.2/libexec/src/runtime/proc.go:195
runtime.goexit
/usr/local/Cellar/go/1.9.2/libexec/src/runtime/asm_amd64.s:2337
exit status 1

sample4/main.go中将出错的代码行数也打印了出来,这样的日志,可以帮助我们更方便的定位问题原因。

为了行为断言错误,而非为了类型

在有些场景下,仅仅知道是否出错是不够的。比如,和进程外其它服务通信,需要了解错误的属性,以决定是否需要重试操作。

这种情况下,不要判断错误值或者错误的类型,我们可以判断错误是否实现某个行为。

1
2
3
4
5
6
7
8
type temporary interface {
Temporary() bool // IsTemporary returns true if err is temporary.
}

func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}

这种实现方式的好处在于,不需要知道具体的错误类型,也就不需要引用定义了错误类型的三方package。如果你是底层代码的开发者,哪天你想更换一个实现更好的error,也不用担心影响上层代码逻辑。如果你是上层代码的开发者,你只需要关注error是否实现了特定行为,不用担心引用的三方package升级后,程序逻辑失败。

习大大说:“听其言、观其行”。小时候父母总是教导:“不要以貌取人”。原来生活中的道理,在程序开发中也是适用的。

不要忽略错误,也不要重复处理错误

遇到错误,而不去处理,导致信息缺失,会增加后期的运维成本

1
2
3
func Write(w io.Writer, buf []byte) {
w.Write(buf) // Write(p []byte) (n int, err error),Write方法的定义见 https://golang.org/pkg/io/#Writer
}

重复处理,添加了不必要的处理逻辑,导致信息冗余,也会增加后期的运维成本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func Write(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err) // 第1次错误处理

return err
}
return nil
}

func main() {
// create writer and read data into buf

err := Write(w, buf)
if err != nil {
log.Println("Write error:", err) // 第2次错误处理
os.Exit(1)
}

os.Exit(0)
}

总结

  • 使用“隐藏内部细节的错误处理”
    • 使用errors.Wrap封装原始error
    • 使用errors.Cause找出原始error
  • 为了行为而断言,而不是类型
  • 尽量减少错误值的使用

参考

  • The Go Blog
    • Error handling in Go
    • Defer, Panic, and Recover
  • Dave Cheney
    • presentation on my philosophy for error handling
    • Don’t just check errors, handle them gracefully
    • Stack traces and the errors package
  • Go Packages
    • errors
    • runtime
  • pkg: Artisanal, hand crafted, barrel aged, Go packages
    • github.com/pkg/errors

在macOS上创建自启动服务解决软件启动时的注册验证问题

发表于 2017-11-18   |   分类于 善用佳软   |  

经常使用的一个开发软件需要注册。网上找了一个注册证书服务程序,这个程序启动后会提供一个注册服务。软件启动的时候,提供这个服务监听的本地http端口,就能自动通过注册验证。但是这个注册证书服务程序是个命令行程序,每次手动启动这个程序实在太麻烦,我就想把这个程序做成一个后台服务,这样就节省了每次手动操作的时间。在macOS可以通过launchd启动后台服务,关于launchd,wiki2上介绍如下:

In computing, launchd, a unified service-management framework, starts, stops and manages daemons, applications, processes, and scripts.

There are two main programs in the launchd system: launchd and launchctl.

launchd manages the daemons at both a system and user level. Similar to xinetd, launchd can start daemons on demand. Similar to watchdogd, launchd can monitor daemons to make sure that they keep running. launchd also has replaced init as PID 1 on macOS and as a result it is responsible for starting the system at boot time.

Configuration files define the parameters of services run by launchd. Stored in the LaunchAgents and LaunchDaemons subdirectories of the Library folders, the property list-based files have approximately thirty different keys that can be set. launchd itself has no knowledge of these configuration files or any ability to read them - that is the responsibility of “launchctl”.

launchctl is a command line application which talks to launchd using IPC and knows how to parse the property list files used to describe launchd jobs, serializing them using a specialized dictionary protocol that launchd understands. launchctl can be used to load and unload daemons, start and stop launchd controlled jobs, get system utilization statistics for launchd and its child processes, and set environment settings.

具体操作步骤如下:

在/Users/{your_username}/Library/LaunchAgents下创建com.myutils.licsrv.plist文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.myutils.licsrv.activator</string>
<key>ProgramArguments</key>
<array>
<string>/Users/{your_username}/Applications/myutils/licsrv.darwin.amd64</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/dev/null</string>
<key>StandardOutPath</key>
<string>/dev/null</string>
</dict>
</plist>

将注册证书服务程序拷贝到/Users/{your_username}/Applications/myutils目录下。

1
2
3
> launchctl load com.myutils.licsrv.plist
> launchctl start com.myutils.licsrv.plist
> ps aux | grep licsrv

然后重启再测试一下,这样一个后台服务就创建好了。

参考

  • Daemons and Services Programming Guide
    • Creating Launch Daemons and Agents
  • launchd info
  • wiki2 - launchd

如何搞定技术面试编码环节

发表于 2017-07-12   |   分类于 软件开发   |  

编码,是软件工程师必须掌握的技能。以下介绍的方法,帮助软件工程师更顺利的通过技术面试的编码环节。

解决问题的步骤

  1. 听:仔细聆听问题描述,完整获取信息
  2. 举例:注意特殊用例、边界场景
  3. 快速解决:尽快找到解决方案,不一定要最优。想想最优方案是什么样子的,你的最终解决方案可能处于当前方案、最优方案之间
  4. 优化:优化第一版解决方案
    • BUD优化方法
      • 瓶颈,Bottlenecks
      • 不必要的工作,Unnecessary work
      • 重复的工作,Duplicated work
    • Four Algorithm Approaches
      • Pattern Matching: What problems is this similar to?
      • Simplify & Generalize: Tweak and solve simpler problem.
      • Base Case & Build: Does it sound recursive-ish?
      • Data Structure Brainstorm: Try various data structures.
    • 或者尝试下面的方法
      • Look for any unused info
      • Use a fresh example
      • Solve it “incorrectly”
      • Make time vs. space tradeoff
      • Precompute or do upfront work
      • Try a hash table or another data structure
  5. 重新审视解决方案,确保在编码前理解每个细节
  6. 实现
    • Write beautiful code
      • Modularize your code from the beginning
      • Refactor to clean up anything that isn’t beautiful
  7. 测试
    • FIRST Analyze
      • What’s it doing? Why?
      • Anything that looks wired?
      • Error hot spots
    • THEN use test cases
      • Small test cases
      • Edge cases
      • Bigger test cases
    • When you find bugs, fix them carefully.

需要掌握的基础知识

  1. 数据结构:hash tables, linked lists, stacks, queues, trees, tries, graphs, vectors, heaps
  2. 算法:quick sort, merge sort, binary search, breadth-first search, depth-first search
  3. 基础概念:Big-O Time, Big-O Space, Recursion & Memoization, Probability, Bit Manipulation

参考

  • Cracking the Coding interview
    • Cracking the Coding Interview 6th Ed. Python Solutions
  • Architecture of Tech Interviews
  • 在AWS面试是怎样一种体验
  • Amazon Leadership Principles

2 - Make PHP development VM using Vagrant | Study PHP

发表于 2017-06-13   |   分类于 软件开发   |  

这篇文章介绍的项目地址:https://github.com/EthanCai/php-dev-vm

简介

php-dev-vm可以帮助快速构建PHP虚拟机开发环境。你也可以修改Vagrantfile,定制自己的PHP虚拟机开发环境。

解决什么问题

多人团队开发环境不同步一般带来如下问题:

  • 不同电脑上单独安装配置占用时间,后续解决环境配置、依赖等问题也会占用较多工作时间,团队越大问题越突出
  • 没有标准安装、配置步骤,不同工程师水平不一致,每次环境配置的最终结果可能不一致,最终导致开发时存在潜在调试、运行等问题,解决办法也无法快速在团队中应用
  • 环境安装配置对新人是障碍,耽误熟悉业务代码时间,资深工程师帮助新人配置环境也占用时间
  • 开发环境后续调整、同步也是障碍

做什么 - 为什么使用VM,而不是Docker

如果能够实现自动化的构建开发环境,通过指定的分发机制同步开发环境配置,就可以很好解决以上问题。

通过Vagrant这个工具自动化构建开发环境虚拟机镜像,将开发环境需要的软件、配置打包到镜像中,然后通过Rsync或者FTP分发镜像,可以实现这一点,满足我们的需求。

至于为什么不使用 PHP Docker镜像,是因为目前生产环境中PHP程序运行在虚拟机中,迁移不是短时间就能完成的事情。

怎么做 - 使用Vagrant制作PHP虚拟机开发环境

可以通过本项目快速构建PHP虚拟机开发环境。

依赖软件

  • macOS Sierra Version 10.12.5
  • homebrew
  • vagrantup
  • virualbox

安装依赖Vagrant Box

Install CentOS 7.2 box

1
> vagrant box add "CentOS-7.2-x64" https://github.com/CommanderK5/packer-centos-template/releases/download/0.7.2/vagrant-centos-7.2.box

如何构建PHP开发环境Vagrant Box

构建php-dev-vm-5.6.30.box

编写Vagrantfile:

见这里

构建Vagrant Box:

1
2
3
4
5
6
7
8
9
> # start vm
> vagrant up

> # config mysql root password
> mysql_secure_installation
> # todo: add more config statements

> # create vagrant box
> vagrant package --output "php-dev-vm-5.6.30.box" {vm_id} && mv php-dev-vm-5.6.30.box ../../box/

如何使用Vagrant Box启动PHP开发环境虚拟机

使用 php-dev-vm-5.6.30.box

1
2
3
4
5
6
7
> # build php-dev-vm-5.6.30.box or download box from ftp://10.75.87.202/php-dev-vm-5.6.30.box (get the url from Ethan if not available)
> wget ftp://10.75.87.202/php-dev-vm-5.6.30.box && mv php-dev-vm-5.6.30.box ./box/php-dev-vm-5.6.30.box

> cd work/php56
> vagrant box remove "../../box/php-dev-vm-5.6.30.box" || true && vagrant up # ignore error when box not exists

> # the password of mysql `root` user is `eVkU,iO);5R>`

References

  • Vagrant: https://www.vagrantup.com/docs/index.html
  • Discover Vagrant Boxes
    • https://atlas.hashicorp.com/boxes/search
    • http://www.vagrantbox.es/
  • Nginx配置
    • Nginx Beginner’s Guide
    • Setting up PHP-FastCGI and nginx? Don’t trust the tutorials: check your configuration!
    • nginx和php-fpm基础环境的安装和配置
  • PHP-FPM配置
    • FastCGI 进程管理器(FPM)配置:http://php.net/manual/zh/install.fpm.configuration.php
    • https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/
  • PHP Function Reference/扩展列表
    • http://php.net/manual/en/funcref.php
  • Debugging with PhpStorm

1 - Install PHP | Study PHP

发表于 2017-05-17   |   分类于 软件开发   |  

Environment

  • macOS Sierra Version 10.12.4
  • Xcode: xcode-select --install
  • Homebrew

Install PHP

安装PHP:

1
2
3
4
5
6
7
8
9
# 快速安装php,参考 https://github.com/Homebrew/homebrew-php
> brew install brew-php-switcher
> brew install php56

> which php
/usr/local/bin/php

> which php-fpm
lrwxr-xr-x 1 leeco admin 32B May 17 15:08 /usr/local/bin/php -> ../Cellar/php56/5.6.30_6/bin/php

配置PHP:

1
2
3
4
5
6
7
# 配置 /private/etc/php.ini
> sudo vim /usr/local/etc/php/5.6/php.ini
# date.timezone = Asia/Shanghai

> php -i | grep timezone
Default timezone => Asia/Shanghai
date.timezone => Asia/Shanghai => Asia/Shanghai

安装 PEAR 和 PECL:

1
2
3
4
5
6
7
8
9
# 参考 https://jason.pureconcepts.net/2012/10/install-pear-pecl-mac-os-x/
> curl -O http://pear.php.net/go-pear.phar
> sudo php -d detect_unicode=0 go-pear.phar
# Press return

# 安装 intl 扩展,参考 http://note.rpsh.net/posts/2015/10/07/installing-php-intl-extension-os-x-el-capitan/
> sudo pear channel-update pear.php.net
> sudo pecl channel-update pecl.php.net
> sudo pear upgrade-all

如果要安装多个版本的PHP

1
2
3
4
5
# 用于在不同版本的php之间切换,参考 https://github.com/philcook/brew-php-switcher
> brew unlink php56
> brew install php71

> brew-php-switcher 56 -s # 切回 php 5.6

Install Composer

1
2
3
> brew install composer

> composer

Install PHP Extension

Install intl

安装

1
2
3
> brew install autoconf
> brew install icu4c
> brew install php56-intl # /usr/local/etc/php/5.6/conf.d/ext-intl.ini was created

检查是否安装成功:

1
> php -m | grep intl # 正常会包含 intl

Install OPcache

1
2
3
4
5
6
7
8
9
10
11
> brew install php56-opcache

> brew info php56-opcache
...
To finish installing opcache for PHP 5.6:
* /usr/local/etc/php/5.6/conf.d/ext-opcache.ini was created,
do not forget to remove it upon extension removal.
* Validate installation via one of the following methods:
...

> php -m | grep OPcache # 检查 OPcache 是否已生效

Install Xdebug

安装:

1
2
3
4
5
6
7
> brew install php56-xdebug
...
To finish installing xdebug for PHP 5.6:
* /usr/local/etc/php/5.6/conf.d/ext-xdebug.ini was created,
do not forget to remove it upon extension removal.
* Validate installation via one of the following methods:
...

The Xdebug extension will be enabled per default after the installation, additional configuration of the extension should be done by adding a custom ini-file to /usr/local/etc/php/<php-version>/conf.d/.

配置:

1
2
3
4
5
> sudo echo 'xdebug.remote_enable=1
xdebug.remote_host=127.0.0.1
xdebug.remote_port=9000
xdebug.profiler_enable=1
xdebug.profiler_output_dir="/tmp/xdebug-profiler-output"' >> /usr/local/etc/php/5.6/conf.d/ext-xdebug.ini

安装xdebug-osx(xdebug开关工具):

1
2
3
4
5
6
7
8
9
10
11
12
13
> brew install xdebug-osx
...
Signature:
xdebug-toggle <on | off> [--no-server-restart]

Usage:
xdebug-toggle # outputs the current status
xdebug-toggle on # enables xdebug
xdebug-toggle off # disables xdebug

Options:
--no-server-restart # toggles xdebug without restarting apache or php-fpm
...

Use PHPStorm as IDE

Config Language & Frameworks

打开Preferences > Languages & Frameworks > PHP

添加CLI Intepreter

Config Xdebug

打开Preferences > Languages & Frameworks > PHP > Debug

如何配置参考Configuring Xdebug in PhpStorm

验证

创建一个PHP项目,新建一个php文件,创建执行配置:

打上断点,以Debug方式运行:

参考

  • macOS 10.12 Sierra Apache Setup: MySQL, APC & More…
  • Cannot find libz when install php56
  • How to install Xdebug - Xdebug Documents
  • Xdebug Installation Guide
  • PHPStorm Help - Configuring Xdebug

支持游标和偏移量的通用翻页机制

发表于 2016-11-01   |   分类于 软件开发   |  

前言

对于大多数mobile App,当 App 发出请求时,通常不会在单个响应中收到该请求的全部结果,而是以分片的方式获取部分结果。

随着业务需求的变化,某些情况下 App 的翻页机制可能会调整。一般我们通过 App 重新发版,服务端和客户端同步调整分页机制来完成调整。而本文提供了一种通用协议,支持仅通过服务端发版来调整 App 的分页机制。

常见分页机制

基于游标的分页

游标是指标记数据列表中特定项目的一个随机字符串。该项目未被删除时,游标将始终指向列表的相同部分,项目被删除时游标即失效。因此,客户端应用不应存储任何旧的游标,也不能假定它们仍然有效。

Request的结构一般如下:

1
2
https://api.sample.com/v3/users/?limit=30&before=NDMyNzQyODI3OTQw
https://api.sample.com/v3/users/?limit=30&after=MTAxNTExOTQ1MjAwNzI5NDE=

参数说明:

  • limit:每个页面返回的单独对象的数量。请注意这是上限,如果数据列表中没有足够的剩余对象,那么返回的数量将小于这个数。为防止客户端传入过大的值,某些列表的 limit 值将设置上限。
  • before:向后查询的起始位置。
  • after:向前查询的起始位置。

Response结构一般如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"rows": [
... Endpoint data is here
],
"paging": {
"cursors": {
"top": "MTAxNTExOTQ1MjAwNzI5NDE=",
"last": "NDMyNzQyODI3OTQw"
},
"previous": "NDMyNzQyODI3OTQw",
"next": "MTAxNTExOTQ1MjAwNzI5NDE="
}
}

参数说明:

  • rows:如果当前页没有数据,或者根据过滤规则(比如隐私)当前页所有数据都被过滤掉,返回空的数组。客户端程序不能根据rows是否为空数组来判断,是否已经滚动到列表的末尾,而应根据下面的next字段是否有值来决定是否滚动到了列表尾部。
  • top:已返回的数据页面开头的游标。
  • last:已返回的数据页面末尾的游标。
  • previous:上一页数据的 API 端点。如果是null或者没有此字段,则表示返回的是第一页数据。
  • next:下一页数据的 API 端点。如果是null或者没有此字段,则表示返回的是最后一页数据。

基于偏移量的分页

Request的结构一般如下:

1
https://api.sample.com/v3/users/?limit=30&offset=30

参数说明:

  • limit:每个页面返回的单独对象的数量。
  • offset:偏移量,查询的起始位置。

一般情况下,还会包含其他查询条件,比如根据关键字查找姓名和关键字匹配的用户

Response结构一般如下:

1
2
3
4
5
6
{
"rows": [
... Endpoint data is here
],
"count": 10765
}

参数说明:

  • count:符合查询条件的总记录数。客户端根据offset和count判断是否已经滚动到列表尾部。

注意,如果正分页的项目列表添加了新的对象,后续基于偏移量的查询的内容都将发生更改。

支持游标和偏移量的通用分页机制

每个API视场景需要实现部分规范(比如仅实现向后翻页,不实现向前翻页),没有实现的行为统一返回一个特定错误码 “not supported”

HTTP Request

  • limit: 必填项;期望返回的记录数量;整数类型;必须大于等于0
  • after: 可选项;字符串类型;表示查询从after指向的记录之后(不包括after指向的当前记录)的limit条记录
  • before: 可选项;字符串类型;表示查询从before指向的记录之前(不包括before指向的当前记录)的limit条记录
  • 备注:
    • Request中before、after不能并存
    • 如果Request中没有before和after,视为从结果集起始位置向后查询
    • MySQL和MongoDB均不支持基于游标位置的向前查询,如需支持需要在程序逻辑中实现

HTTP Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"code": 0,
"result": {
"rows": [
//"... Endpoint data is here"
],
"paging": {
"cursors": {
"top": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19",
"last": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19"
},
"previous": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19",
"next": "MTAxNTExOTQ1MjAwNzI5NDE=" or "19",
"count": 1087
}
}
}
  • paging.cursors.top: 必填项;字符串或者null;指向已返回的数据页面开头的游标
  • paging.cursors.last: 必填项;字符串或者null;指向已返回的数据页面末尾的游标
  • paging.previous: 必填项;字符串或者null;查询前一页数据的末尾位置,Request中将此值赋给before,为null时,表示没有前一页
  • paging.next: 必填项;字符串或者null;查询后一页数据的起始位置,Request中将此值赋给after,为null时,表示没有后一页
  • paging.count: 可选项;整型;结果集总数
  • 备注:
    • rows为空结果集时:paging.cursors.top、paging.cursors.last为null,paging.previous、paging.next不一定为null
    • 仅一条结果集是,paging.cursors.top、paging.cursors.last相同

参考

  • twitter API - GET statuses/user_timeline

应对业务增长,对MongoDB集群进行扩容的一种路径

发表于 2016-11-01   |   分类于 软件开发   |  

本文以用户feed作为案例

MongoDB集群结构

数据量较小

采用MongoDB三节点副本集的方式构造集群

replica-set-primary-with-secondary-and-arbiter

数据量较大

使用sharding方式扩展单个集群的容量

shardreplica

数据量非常大

不同时期的Feed数据写入到不同的MongoDB Cluster中,避免单个MongoDB集群规模过大带来各种运维上的问题

multiple_cluster

  • 每个MongoDB Cluster保存的数据包括:
    • 元数据
      • 时间范围:指定当前cluster保存那一段时间的feed信息
    • Feed数据
      • 使用一个collection保存所有用户的feed
      • 这个collection的根据用户的user_id进行分片,适应写、读扩容场景
  • 客户端程序根据MongoDB Cluster的元数据将收到的Feed消息写入到对应的MongoDB Cluster
  • 客户端程序启动时从所有的MongoDB Cluster中加载元数据

Feed DB的结构

metadata集合

1
2
3
4
5
6
7
8
{
"_id": "cluster_1",
"name": "cluster_1",
"start_date": new Date("2016-05-01"),
"end_date": new Date("2016-08-01"),
"creator_name": "ethan",
"created_at": new Date("2016-05-01 00:00:00")
}

feed集合

1
2
3
4
5
6
7
8
9
10
11
{
"_id": ObjectId(""),
"data_key": "e6755cfae343b6719cc2121e888b0a41",
"receiver_id": 1000386,
"sender_id": 1000765,
"event_time": new Date("2016-05-01 10:00:00"),
"type": 1,
"data": {
"fabula_id": 1000983
}
}

feed.data_key用于根据业务对象查找对应feed记录的标识,主要用于删除场景,生成算法如下:

  • feed.data_key = MMH3Hash("fabula_" + $fabulaId)

feed._id的生成算法:

  • 同ObjectID的生成算法,包含time, machine identifier, process id, counter四部分,使用feed.event_time作为第一部分
  • ObjectID生成算法参考: https://github.com/go-mgo/mgo/blob/v2/bson/bson.go

参考

  • 陌陌:日请求量过亿,谈陌陌的Feed服务优化之路
  • 几个大型网站的Feeds(Timeline)设计简单对比
  • 新浪微博:大数据时代的feed流架构
  • 新浪微博:Feed架构-我们做错了什么
  • 新浪微博:Feed消息队列架构分析
  • Pinterest:Pinterest的Feed架构与算法
  • Pinterest:Building a smarter home feed
  • Pinterest:Building a scalable and available home feed
  • Pinterest:Pinterest 的 Smart Feed 架构与算法
  • Pinterest:Pinnability: Machine learning in the home feed

在Ubuntu Server 14.04上配置一个最小的MongoDB副本集

发表于 2016-11-01   |   分类于 软件开发   |  

在Ubuntu Server 14.04上安装MongoDB 3.2.6

  • Import the public key used by the package management system
    • sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927
  • Create a list file for MongoDB
    • echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list
  • Reload local package database
    • sudo apt-get update
  • Install the MongoDB packages
    • sudo apt-get install -y mongodb-org=3.2.6 mongodb-org-server=3.2.6 mongodb-org-shell=3.2.6 mongodb-org-mongos=3.2.6 mongodb-org-tools=3.2.6
  • Pin a specific version of MongoDB

    1
    2
    3
    4
    5
    echo "mongodb-org hold" | sudo dpkg --set-selections
    echo "mongodb-org-server hold" | sudo dpkg --set-selections
    echo "mongodb-org-shell hold" | sudo dpkg --set-selections
    echo "mongodb-org-mongos hold" | sudo dpkg --set-selections
    echo "mongodb-org-tools hold" | sudo dpkg --set-selections
  • 修改MongoDB的配置文件/etc/mongod.conf

    • 修改net.bindIp为0.0.0.0
    • 增加配置
      1
      2
      storage:
      directoryPerDB: true
  • 验证MongoDB是否成功安装

    • sudo service mongod restart
    • mongo
  • Disable Transparent Huge Pages,参考这里

    • Create the init.d script

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      #!/bin/sh
      ### BEGIN INIT INFO
      # Provides: disable-transparent-hugepages
      # Required-Start: $local_fs
      # Required-Stop:
      # X-Start-Before: mongod mongodb-mms-automation-agent
      # Default-Start: 2 3 4 5
      # Default-Stop: 0 1 6
      # Short-Description: Disable Linux transparent huge pages
      # Description: Disable Linux transparent huge pages, to improve
      # database performance.
      ### END INIT INFO

      case $1 in
      start)
      if [ -d /sys/kernel/mm/transparent_hugepage ]; then
      thp_path=/sys/kernel/mm/transparent_hugepage
      elif [ -d /sys/kernel/mm/redhat_transparent_hugepage ]; then
      thp_path=/sys/kernel/mm/redhat_transparent_hugepage
      else
      return 0
      fi

      echo 'never' > ${thp_path}/enabled
      echo 'never' > ${thp_path}/defrag

      unset thp_path
      ;;
      esac
    • Make it executable

      1
      sudo chmod 755 /etc/init.d/disable-transparent-hugepages
    • Configure your operating system to run it on boot

      1
      sudo update-rc.d disable-transparent-hugepages defaults
    • 重启OS

配置MongoDB ReplicaSet副本集

副本集结构:

  • 2数据节点,1个仲裁节点

配置步骤

  • 准备2台高配ec2(假设为A、B)和1台低配ec2(假设为C)
  • 在A、B、C上参考上一节的步骤安装MongoDB
  • 在A的Shell中执行mongo命令,然后创建超级管理员

    1
    2
    3
    $ mongo
    > admin = db.getSiblingDB("admin");
    > admin.createUser({ user: "ethan", pwd: "{ethan的密码}", roles: [{ role: "root", db: "admin" }] });
  • 准备keyfile

    • 生成keyfile
      • openssl rand -base64 755 > rs0.key
    • 上传rs0.key到A、B、C的/etc目录
    • 修改rs0.key的权限和所有者
      • chmod 400 rs0.key
      • chown mongodb:mongodb /etc/rs0.key
  • 修改A、B的配置文件/etc/mongod.conf

    • 增加配置
      1
      2
      3
      4
      5
      6
      security:
      keyFile: "/etc/rs0.key"
      authorization: enabled

      replication:
      replSetName: rs0
  • 修改C的配置文件/etc/mongod.conf

    • 增加配置

      1
      2
      3
      4
      5
      6
      security:
      keyFile: "/etc/rs0.key"
      authorization: enabled

      replication:
      replSetName: rs0
    • 修改配置

      1
      2
      3
      storage:
      journal:
      enabled: false
  • 重启A、B、C上的mongod实例

  • 配置集群

    • 连接A上的mongod实例

      1
      $ mongo -u ethan -p {ethan的密码} --authenticationDatabase admin
    • 通过下面的命令配置ReplicaSet:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      > rs.initiate()
      > cfg = rs.conf()
      > cfg.members[0].host = "{A的IP}:27017"
      > rs.reconfig(cfg)
      > rs.add("{B的IP}")
      > rs.addArb("{C的IP}")

      // 等待几秒...
      > rs.status() // 检查副本集状态
  • 为副本集客户端创建访问账户

    1
    2
    > use {dbname};
    > db.createUser({ user: "{账户名}", pwd: "{账户密码}", roles: [{ role: "readWrite", db: "{dbname}" }] });

参考

  • Install MongoDB on Ubuntu, 注意:
    • 安装过程需要指定MongoDB的安装版本
    • 锁定MongoDB的安装版本,避免执行apt-get升级命令的时候连带升级MongoDB
  • Enforce Keyfile Access Control in a Replica Set
    • Security between members of the replica set using Internal Authentication
    • Security between connecting clients and the replica set using User Access Controls
  • MongoDB configuration file options

收集的论文

发表于 2016-08-16   |   分类于 阅读   |  

持续补充中…

安全(Security)

  • 已读 Green Lights Forever: Analyzing the Security of Traffic Infrastructure:密歇根大学团队在这篇文章中介绍了,如何利用无限网络安全漏洞为入口攻破交通信号灯系统。并总结出嵌入式系统在新环境下防御攻击的方法。对于发现和解决其它类型的嵌入式系统的安全问题,也十分有参考意义。
12
Ethan Cai

Ethan Cai

兼容并包、学以致用

17 日志
4 分类
15 标签
GitHub
© 2019 Ethan Cai
由 Hexo 强力驱动
主题 - NexT.Muse