使用gopkg进行Go包管理

在使用go的过程中,我们有时候会引入一些第三方库来使用,而通常的方式就是使用 go get ,但是这种方式有一个很严重的问题,如果第三方库更新了相关接口,很有可能你就无法使用了,所以我们一套很好地包管理机制。

读生产环境下go语言最佳实践有感一文中,我介绍过soundcloud公司的做法,直接将第三库的代码check下来,放到自己工程的vendor目录里面,或者使用godep。

不过现在,我发现了一种更好的包管理方式gopkg。它通过约定使用带有版本号的url来让go tool去check指定的版本库,虽然现在只支持github的go repositories,但是我觉得已经足够强大。

一个很简单的例子,我们通过如下方式获取go的yaml包

go get gopkg.in/yaml.v1

而实际上,该yaml包对应的地址为:

https://github.com/go-yaml/yaml

yaml.v1表明版本为v1,而在github上面,有一个对应的v1 branch。

gopkg支持的url格式很简单:

gopkg.in/pkg.v3      → github.com/go-pkg/pkg (branch/tag v3, v3.N, or v3.N.M)
gopkg.in/user/pkg.v3 → github.com/user/pkg   (branch/tag v3, v3.N, or v3.N.M)

我们使用v.N的方式来定义一个版本,然后再github上面对应的建立一个同名的分支。gopkg支持(vMAJOR[.MINOR[.PATCH]])这种类型的版本模式,如果存在多个major相同的版本,譬如v1,v1.0.1,v1.1.2,那么gopkg会选用最高级别的v1.1.2使用,譬如有如下版本:

  • v1
  • v2.0
  • v2.0.3
  • v2.1.2
  • v3
  • v3.0

那么gopkg对应选用的方式如下:

  • pkg.v1 -> v1
  • pkg.v2 -> v2.1.2
  • pkg.v3 -> v3.0

gopkg不建议使用v0,也就是0版本号。

gopkg同时列出了一些建议,在更新代码之后是否需要升级主版本或者不需要,一些必须升级主版本的情况:

  • 删除或者重命名了任何的导出接口,函数,变量等。
  • 给接口增加,删除或者重命名函数
  • 给函数或者接口增加参数
  • 更改函数或者接口的参数或者返回值类型
  • 更改函数或者接口的返回值个数
  • 更改结构体

而一下情况,则不需要升级主版本号:

  • 增加导出接口,函数或者变量
  • 给函数或者接口的参数名字重命名了
  • 更改结构体

上面都提到了更改结构体,譬如我给一个结构体增加字段,就可能不需要升级主版本,但是如果删除结构体的一个导出字段,那就必须要升级了。如果只是单纯的更改改结构体里面非导出字段的东西,也不需要升级。

更加详细的信息,请直接查看gopkg

可以看到,gopkg使用了一种很简单地方式让我们方便的对go pakcage进行版本管理。于是我也依葫芦画瓢,给我的log package做了一个v1版本的,你可以直接

go get gopkg.in/siddontang/go-log.v1/log

LeetCode程序员面试技巧

起因

写在开头,脑袋铁定秀逗了,历时20多天,刷完了leetcode上面151道题目(当然很多是google的),感觉自己对算法和数据结构算是入门了,但仍然还有很多不清楚的地方,于是有了对于每道题目写分析的冲动。不过在看到leetcode上面的文章之后,决定先从翻译入手,顺带再写写自己做题的心得体会。今天是第一篇:程序员面试技巧。

如果你主修计算机科学,那么在你工作的时候会碰到很多有难度的编程问题。当你去找工作的时候,你会有很多的面试,而面试官通常很喜欢问你很多技术性的问题,以下就是三类主要的题型:

  • 算法/数据结构
  • 特定编程语言/通用的计算机知识
  • 脑筋急转弯

后续我将详细的讨论上面这几点:

算法/数据结构

你必须深刻的理解以下几种数据结构:数组,链表,二叉树,哈希表等。你必须明确地知道什么时候该使用数组或者链表,譬如在实现一个列表翻转的时候。

面试官通常会问你一些算法问题用以检验你的编程解决问题能力,一些算法问题如下:

  • 在一个句子里面反转单词。
  • 在一个单词里面反转字母。(尝试使用迭代或者递归解决)
  • 如何在一个列表里面找到重复的数字。
  • 生成数字n的全排列。(小提示:递归)
  • 找到两个排好序数组的中间值。(小提示:分治法)
  • 如何进行拼写检查并验证单词。(小提示:编辑距离)

(译者吐槽,说句实话,上面几个问题如果没做题大部分我还真答不出来。)

特定编程语言/通用的计算机知识

如果你应聘的是C++职位,需要准备好应对很多C++的问题,其它编程语言也一样。如果你应聘的职位不需要特定的编程语言,那么你可能会被问到通用的计算机知识,譬如在堆和栈上面创建对象的区别。

我经常碰到的一些C++问题如下:

  • 什么是虚函数?怎么实现的?使用虚函数会有啥性能问题?
  • 什么是虚析构函数?为什么需要他们?如果你不实现成虚析构会出现什么问题?
  • 什么是拷贝构造函数?它什么时候被调用?
  • malloc和new的区别是什么?

(译者吐槽,上面这些感觉还算靠谱,至少没问虚拟继承是啥!我面试的时候通常还会问很多STL的东西,毕竟这在C++里面已经是很基础的了。Template这些的就算了,有时候都能把自己绕晕,还是别坑面试者了。)

脑筋急转弯

一些面试官很喜欢用一个脑筋急转弯来结束面试。这个其实很坑爹的,不过还是老老实实的准备一些吧。

一些经典的问题:

  • 两个鸡蛋问题:

    你有两个相同的鸡蛋,然后还有100层楼等你去爬。你需要知道最高到哪一层扔下鸡蛋,鸡蛋不会摔碎。(鸡蛋语录:为啥受伤的总是蛋蛋?)。很有可能第一层就碎了,也可能到了100层才碎。你需要知道最多尝试几次就能找到答案。

  • 尽可能快的在一个byte里面反转bit。

  • 你有5个相同的罐子,都装着球,其中有4个里面每个球都是重10g,另一个灌每个球重9g,你有一台电子称,只称一次,找到那个重9g球的罐子。

(译者吐槽,上面这些脑筋急转弯问题感觉就是算法问题,哪里是脑筋急转弯,明显的忽悠!)

总结

当然这是译者自己的,原文在上面就结束了,可以看到,作为一个程序员,我们其实是幸运的,因为我们能够接触如此多的挑战,并不断提升自己。可我们同时也是郁闷的,很多时候往往都有这样的错觉,数据结构和算法在很多工作中并不用到,但是为啥偏偏就面这个,linus貌似说过:“Bad programmers worry about the code. Good programmers worry about data structures and their relationships”,而在《大教堂与集市》这本书里面,作者直接说明“Smart data structures and dumb code works a lot better than the other way around”。有时候当写程序写多了,自然就发现算法和数据结构的重要性了。

另外,现在无论哪个公司,除非你是大牛级别的,面试几乎不会脱离上面那些东西,所以还是老老实实的学习吧,骚年。

一个Go可变参数问题

几天前纠结了一个蛋疼的问题,在go里面函数式支持可变参数的,譬如…T,go会创建一个slice,用来存放传入的可变参数,那么,如果创建一个slice,例如a,然后以a…这种方式传入,go会不会还会新建一个slice,将a的数据全部拷贝一份过去?

如果a很大,那么将会造成很严重的性能问题,不过后来想想,可能是自己多虑了,于是查看go的文档,发现如下东西:

Passing arguments to … parameters

If f is variadic with a final parameter p of type …T, then within f the type of p is equivalent to type []T. If f is invoked with no actual arguments for p, the value passed to p is nil. Otherwise, the value passed is a new slice of type []T with a new underlying array whose successive elements are the actual arguments, which all must be assignable to T. The length and capacity of the slice is therefore the number of arguments bound to p and may differ for each call site.

Given the function and calls

func Greeting(prefix string, who ...string)
Greeting("nobody")
Greeting("hello:", "Joe", "Anna", "Eileen")

within Greeting, who will have the value nil in the first call, and []string{“Joe”, “Anna”, “Eileen”} in the second.

If the final argument is assignable to a slice type []T, it may be passed unchanged as the value for a …T parameter if the argument is followed by …. In this case no new slice is created.

Given the slice s and call

s := []string{"James", "Jasmine"}
Greeting("goodbye:", s...)

within Greeting, who will have the same value as s with the same underlying array.

也就是说,如果我们传入的是slice…这种形式的参数,go不会创建新的slice。写了一个简单的例子验证:

package main

import "fmt"

func t(args ...int) {
    fmt.Printf("%p\n", args)
}

func main() {
    a := []int{1,2,3}
    b := a[1:]

    t(a...)
    t(b...)

    fmt.Printf("%p\n", a)
    fmt.Printf("%p\n", b)
}

//output
0x1052e120
0x1052e124
0x1052e120
0x1052e124

可以看到,可变参数args的地址跟实际外部slice的地址一样,用的同一个slice。

celery最佳实践

作为一个Celery使用重度用户,看到Celery Best Practices这篇文章,不由得菊花一紧。干脆翻译出来,同时也会加入我们项目中celery的实战经验。

通常在使用Django的时候,你可能需要执行一些长时间的后台任务,没准你可能需要使用一些能排序的任务队列,那么Celery将会是一个非常好的选择。

当把Celery作为一个任务队列用于很多项目中后,作者积累了一些最佳实践方式,譬如如何用合适的方式使用Celery,以及一些Celery提供的但是还未充分使用的特性。

1,不要使用数据库作为你的AMQP Broker

数据库并不是天生设计成能用于AMQP broker的,在生产环境下,它很有可能在某时候当机(PS,当掉这点我觉得任何系统都不能保证不当吧!!!)。

作者猜想为啥很多人使用数据库作为broker主要是因为他们已经有一个数据库用来给web app提供数据存储了,于是干脆直接拿来使用,设置成Celery的broker是很容易的,并且不需要再安装其他组件(譬如RabbitMQ)。

假设有如下场景:你有4个后端workers去获取并处理放入到数据库里面的任务,这意味着你有4个进程为了获取最新任务,需要频繁地去轮询数据库,没准每个worker同时还有多个自己的并发线程在干这事情。

某一天,你发现因为太多的任务产生,4个worker不够用了,处理任务的速度已经大大落后于生产任务的速度,于是你不停去增加worker的数量。突然,你的数据库因为大量进程轮询任务而变得响应缓慢,磁盘IO一直处于高峰值状态,你的web应用也开始受到影响。这一切,都因为workers在不停地对数据库进行DDOS。

而当你使用一个合适的AMQP(譬如RabbitMQ)的时候,这一切都不会发生,以RabbitMQ为例,首先,它将任务队列放到内存里面,你不需要去访问硬盘。其次,consumers(也就是上面的worker)并不需要频繁地去轮询因为RabbitMQ能将新的任务推送给consumers。当然,如果RabbitMQ真出现问题了,至少也不会影响到你的web应用。

这也就是作者说的不用数据库作为broker的原因,而且很多地方都提供了编译好的RabbitMQ镜像,你都能直接使用,譬如这些

对于这点,我是深表赞同的。我们系统大量使用Celery处理异步任务,大概平均一天几百万的异步任务,以前我们使用的mysql,然后总会出现任务处理延时太严重的问题,即使增加了worker也不管用。于是我们使用了redis,性能提升了很多。至于为啥使用mysql很慢,我们没去深究,没准也还真出现了DDOS的问题。

2,使用更多的queue(不要只用默认的)

Celery非常容易设置,通常它会使用默认的queue用来存放任务(除非你显示指定其他queue)。通常写法如下:

@app.task()
def my_taskA(a, b, c):
    print("doing something here...")

@app.task()
def my_taskB(x, y):
    print("doing something here...")

这两个任务都会在同一个queue里面执行,这样写其实很有吸引力的,因为你只需要使用一个decorator就能实现一个异步任务。作者关心的是taskA和taskB没准是完全两个不同的东西,或者一个可能比另一个更加重要,那么为什么要把它们放到一个篮子里面呢?(鸡蛋都不能放到一个篮子里面,是吧!)没准taskB其实不怎么重要,但是量太多,以至于重要的taskA反而不能快速地被worker进行处理。增加workers也解决不了这个问题,因为taskA和taskB仍然在一个queue里面执行。

3,使用具有优先级的workers

为了解决2里面出现的问题,我们需要让taskA在一个队列Q1,而taskB在另一个队列Q2执行。同时指定x workers去处理队列Q1的任务,然后使用其它的workers去处理队列Q2的任务。使用这种方式,taskB能够获得足够的workers去处理,同时一些优先级workers也能很好地处理taskA而不需要进行长时间的等待。

首先手动定义queue

CELERY_QUEUES = (
    Queue('default', Exchange('default'), routing_key='default'),
    Queue('for_task_A', Exchange('for_task_A'), routing_key='for_task_A'),
    Queue('for_task_B', Exchange('for_task_B'), routing_key='for_task_B'),
)

然后定义routes用来决定不同的任务去哪一个queue

CELERY_ROUTES = {
    'my_taskA': {'queue': 'for_task_A', 'routing_key': 'for_task_A'},
    'my_taskB': {'queue': 'for_task_B', 'routing_key': 'for_task_B'},
}

最后再为每个task启动不同的workers

celery worker -E -l INFO -n workerA -Q for_task_A
celery worker -E -l INFO -n workerB -Q for_task_B

在我们项目中,会涉及到大量文件转换问题,有大量小于1mb的文件转换,同时也有少量将近20mb的文件转换,小文件转换的优先级是最高的,同时不用占用很多时间,但大文件的转换很耗时。如果将转换任务放到一个队列里面,那么很有可能因为出现转换大文件,导致耗时太严重造成小文件转换延时的问题。

所以我们按照文件大小设置了3个优先队列,并且每个队列设置了不同的workers,很好地解决了我们文件转换的问题。

4,使用Celery的错误处理机制

大多数任务并没有使用错误处理,如果任务失败,那就失败了。在一些情况下这很不错,但是作者见到的多数失败任务都是去调用第三方API然后出现了网络错误,或者资源不可用这些错误,而对于这些错误,最简单的方式就是重试一下,也许就是第三方API临时服务或者网络出现问题,没准马上就好了,那么为什么不试着重试一下呢?

@app.task(bind=True, default_retry_delay=300, max_retries=5)
def my_task_A():
    try:
        print("doing stuff here...")
    except SomeNetworkException as e:
        print("maybe do some clenup here....")
        self.retry(e)

作者喜欢给每一个任务定义一个等待多久重试的时间,以及最大的重试次数。当然还有更详细的参数设置,自己看文档去。

对于错误处理,我们因为使用场景特殊,例如一个文件转换失败,那么无论多少次重试都会失败,所以没有加入重试机制。

5,使用Flower

Flower是一个非常强大的工具,用来监控celery的tasks和works。

这玩意我们也没怎么使用,因为多数时候我们都是直接连接redis去查看celery相关情况了。貌似挺傻逼的对不,尤其是celery在redis里面存放的数据并不能方便的取出来。

6,没事别太关注任务退出状态

一个任务状态就是该任务结束的时候成功还是失败信息,没准在一些统计场合,这很有用。但我们需要知道,任务退出的状态并不是该任务执行的结果,该任务执行的一些结果因为会对程序有影响,通常会被写入数据库(例如更新一个用户的朋友列表)。

作者见过的多数项目都将任务结束的状态存放到sqlite或者自己的数据库,但是存这些真有必要吗,没准可能影响到你的web服务的,所以作者通常设置CELERY_IGNORE_RESULT = True去丢弃。

对于我们来说,因为是异步任务,知道任务执行完成之后的状态真没啥用,所以果断丢弃。

7,不要给任务传递 Database/ORM 对象

这个其实就是不要传递Database对象(例如一个用户的实例)给任务,因为没准序列化之后的数据已经是过期的数据了。所以最好还是直接传递一个user id,然后在任务执行的时候实时的从数据库获取。

对于这个,我们也是如此,给任务只传递相关id数据,譬如文件转换的时候,我们只会传递文件的id,而其它文件信息的获取我们都是直接通过该id从数据库里面取得。

最后

后面就是我们自己的感触了,上面作者提到的Celery的使用,真的可以算是很好地实践方式,至少现在我们的Celery没出过太大的问题,当然小坑还是有的。至于RabbitMQ,这玩意我们是真没用过,效果怎么样不知道,至少比mysql好用吧。

最后,附上作者的一个Celery Talk https://denibertovic.com/talks/celery-best-practices/

我的编程语言经历

Alan Perlis 说过:“一种不改变你编程的思维方式的语言,不值得去学。”,虽然写了这么多年程序,用了这么多的语言,但我自认还没悟道编程语言如何改变我的思维方式。

几天前,我需要用python来为ledisdb写一个客户端,我突然发现,对于c++,go这种语言,我如果需要实现一个功能,首先想到的是问题是代码应该怎么写。但是当我使用python的时候,我首先考虑的问题是在哪里去找一个库用来解决我的问题。可能这就是使用不同语言带给我的不同思考方式吧。

我的编程语言经历没有那么复杂,没用过很多,但是其实也够我受的了,尤其是在不同语言语法糖之间切换的时候,有种让人崩溃的感觉。没准我应该升级一下我的大脑cpu,使其能够更快速的进行中断处理。

c

我是从大学才开始学习编程的,相比现在的小朋友来说,可以叫做输在了起跑线上面。谁叫以前生活在山区,没机会接触电脑这玩意。

我的第一门编程语言是c,不同于很多童鞋使用的谭浩强神书,我用的是周纯杰的<>,不知道每年有多少同学受到过它的摧残,当然还有那哥们蹩脚的普通话。

在大学里面,很多同学的c的毕业设计都是我帮其完成,但我始终觉得自己仍然是个半吊子,除了c的基础稍微强一点之外,很多方面譬如操作系统,算法等完全不会。(现在随着工作年限的增加让我越发后悔,当初怎么就不稍微学点这些知识,尤其是编译原理。)

我几乎没怎么用c开发过项目,只在tencent可怜的维护过别人的c项目,但至少能看懂c代码,这就够了。

因为大多数时候,我用的是c++,而不是c来解决我的问题。

c++

c++是我工作使用的第一门语言,也是我使用时间最长的一门语言,都七年之痒了,不过还是有点不离不弃的。

以前上学的时候有一句口头禅,叫学好c++,走遍天下都不怕。但是有几个人能把它学好的?所以千万别说自己精通c++,那会被人鄙视的。

我使用c++可以分为三个阶段:

类c阶段

这个阶段主要是我第一份工作的时候,那时候才毕业,c的烙印很深,面向对象除了有个概念,真正是啥完全不知道。所以最喜欢的方式还是写着一堆函数来解决问题,当初VIA身边那帮c++的牛人竟然能忍受我这样的代码,真佩服他们。

面向对象阶段

后来去了第二家公司linekong,开始做游戏,才开始真正意义上的用c++写代码了。

记得最开始去第一家公司面试的时候,被问了啥是面向对象,当时不假思索的答了继承,多态和封装。

啥叫封装?整一个class,把该包的都包了,一个同事曾告诉我,他见过有几万行代码的class,看来我这个几千行的太小儿科了。

啥叫继承?先来一个父类,干脆叫bird,有一个fly方法,再来一个子类,叫duck吧,继承了bird,不过duck会fly吗?一个父类不够,再来一个,搞个多重继承,什么?出现了菱形继承,那干脆在来一个virtual继承得了。

啥叫多态?不就是virtual function,然后父类指针能在运行时根据实际情况调用相应的子类实现。那c++的多态是怎么实现的?看看<<深度探索c++对象模型>>不就行了。

这段时间,可以算是我写c++代码最多的时候,都快写到吐了,尤其还要忍受那龟速的编译。我们竟然都实现了直接通过汇编改c++的虚表,使其调用自己的函数这种变态的东西。在那时候我就得出结论,如果不想早死,尽量别用这个东西写代码。可是到如今我都在不停的慢性自杀。

现代C++阶段

不知道从什么时候开始,我突然觉得我应该来点modern c++的编写方式了,c++0x都出了,还不玩一下就晚了。当然新特性那么多,不可能全部都拿来用的,Bjarne Stroustrup貌似都说过,c++0x应该算是另一门语言了。

于是哥就走上了伪modern c++之路,class还是需要的,不然数据怎么封装。继承吗,比重减轻吧,最好采用面向接口的编程方式。而多态,能不用就不用吧,反而觉得bing + function的方式实现的delegate模型反而更方便,没准也更酷哟。

shared_ptr,weak_ptr是需要用的了,c++没有gc,所以一套好的内存管理方式是必不可少的,每次new之后还要记得delete也比较烦,最严重的是可能忘记那就内存泄露了。

于是,我就自认为我进化了,最典型的例子就是我写的高性能网络库libtnet,感觉很modern了。

lua

最开始知道lua,是云风那本编程感悟的书,当时可是菊花一紧,觉得这东西是啥,为什么能跟c结合到一起使用?

后来自己开发游戏了,才发现lua真的是一门很强大的语言,短小精悍,嵌入简单,性能超强,完全是作为游戏脚本语言开发的不二人选。不过我最开始使用lua的经历不怎么happy,最开始就是接手了一个c++与lua的粘合层库,关于这个库的传说,见这里Lua 不是 C++。后来,在踩了无数个坑,填了无数个坑之后,我终于弄得相对稳定了。貌似现在我以前的同事还在使用,不过正如我在lua c函数注册器中说明的那样,对于语言的交互,简单一点才好。现在以前做的游戏已经开源,见这里,那个传说中的蛋疼粘合层也见了世面。当然,我可不会告诉你们好多搓代码是我写的。

后来,在现在的公司,因为项目的需要,我们急需解决python的很多性能大坑问题,于是我开始推广使用openresty,一个用lua包裹的nginx,用了之后,腰不痛了,腿不痛了,性能妥妥的。

因为lua,我第一次尝到了在代码里面直接写配置的便捷,用一个table搞定,相比起来,c++处理ini,json这些的弱爆了。另外,动态语言的热更新机制使其代码升级异常方便,不过你得非常小心lua的闭包,没准你重新加载了代码运行还是老样子。

lua是一个动态语言,所以不用我们管内存释放问题,但是仍然可能会有引用泄露,在开发游戏的时候,为了解决我们程序lua内存泄露的问题,我曾经干过直接从_G递归遍历,扫描整个lua数据的事情。相比在c++使用valgrind这些程序的工具,lua配套的东西还是太小儿科了。

lua的调试也是一个大头问题,我曾今写过几个lua的调试器,例如这个,甚至都支持了类似gdb那样ctrl+c之后动态的设置断点,可是仍然没觉得比gdb方便,所以多数时候,我都是写log为主。

python

虽然小时候吃过很多蛇,但是蟒蛇可是从来没吃过的,现在看来python味道还不错。

我是来了kingsoft之后才开始正式使用python的。对于为啥使用python,我曾跟拉我进来的技术老大讨论过,他直接就说,开发快速,上手容易。

python开发的快速很大程度是建立在丰富的第三方库上面的,我们也使用了很多库,譬如tornado,gevent,django等,但是正如我最开始说的,因为我们有太多的选择,面对一个问题的时候,往往考虑的是如何选择一个库,而不是自己如何实现,这其实在某种程度上面使得很多童鞋知其然而不知其所以然。这点,lua可能是另一个极端,lua的定位在于嵌入式和高性能,所以自然地,你得自己动手造轮子(当然,现在也有很多好的第三方库了),虽然有时候写起来很不方便,但是至少自己很清楚程序怎么跑的。

当然,我绝对没有贬低python的意思,我很喜欢这门语言,用它来开发了很多东西,同时也知道很多公司使用python构建了很多大型稳定的系统(我们的产品应该也算吧)。

只是现在我越发觉得,看起来简单的语言,如果没有扎实的基本功底,写出来的东西也很烂,而python,恰恰给人放了一个很大的烟雾弹,你以为它有多容易,其实它是在玩你。

go

好了,终于开始说go了,let’s go!!!

我使用go的历史不长,可能也就一年多,但是它现在完全成了我最爱的语言,go具有了python开发的迅速,同时也有着c运行的性能。(当然,还是有差距的!)

网上有太多的语言之争,包括go,有人恨,有人爱。但萝卜白菜,各有所爱,对于我来说,能帮我解决问题,让我用着舒服的语言就是好语言。

go非常适用于服务端程序开发,比起用c++开发,我陡然觉得有一种很幸福的感觉,譬如对于网络编程,在c++里面,我需要自己写epoll的事件处理,而且这种异步的机制完全切分了整个逻辑,使得代码不怎么好写,我在开发libtnet的时候感触尤其深刻。但是在go里面,因为天生coroutine的支持,使得异步代码同步化了,非常利于代码的编写。

现在我的主要在项目中推动go的使用,我们已经用go搭建了一个高性能的推送服务器,后续还有几个系统会上线,而且开发的进度并不比使用python差,另外也很稳定,这让我对go的未来充满了期待。

我也用go写了很多的开源程序,也算是拿的出手了,譬如:

  • ledisdb:一个基于leveldb的提供类似redis高级数据结果的高性能NoSQL,真挺绕口的,简单点就是一个高性能NoSQL。
  • Mixer:一个mysql-proxy,现在支持通用的mysql命令代理,读写分离,以及自动主备切换。后续将要参考vitess支持分区,为此一直在恶补编译原理的知识。
  • go-log:一个类似python log模块的东西,支持多种handler,以及不同的log级别。

还有一些,可以参考我的github,譬如moonmq(一个高性能push模型的消息服务器),polaris(一个类似tornado的restful web框架),因为go,我开始热衷于开源了,并且认识了很多的好基友,这算得上一个很大的收获吧。

其它

好了,说完了上面我的长时间使用语言(至少一年以上),我也用了很多其他的语言,现在虽然使用时间比较短,但不排除后续会长久使用。

Objective-C

因为我家终于有了苹果三件套,所以决定开发app了,首要的任务就是得学会使用Objective-C。我承认这真是一门奇葩的语言,如果没有xcode的自动补齐,我真不知道写起来是神马样的感觉。

而且第一次见到了一门推荐写函数名,变量名就像写文章的语言,至少我写c++,go这些的一个函数名字不会写成一个句子。

我现在在自学iOS的开发,慢慢在整出点东西,毕竟答应给老婆的iphone做点啥的。后续干脆就写一个《小白学iOS》系列blog吧(如果我有精力!),反正对于iOS,我真是一个小白。

java

好吧,我也在android上面写过程序,build到我的S3上面去过,对于java,我的兴趣真不大,貌似自己还买过两本《java编程思想》,那时候脑袋铁定秀逗了。

但是不得不承认,java在服务器领域具有非常强的优势,很多很多的大企业采用java作为其服务器的编程语言。典型的就是淘宝,据传杭州的很多软件公司都不用java的,你用java就等于给淘宝培养人才了。(不过我发现他们很多基础组件譬如TFS这些的可是c++的哟!)

java是门好语言,只是我个人不怎么喜欢,可能我就是太小众了,只对c语言体系的感兴趣。所以很多公司我去不了,哈哈!

erlang

受《计算机程序的构造与解释》影像,我一直想学一门函数式编程语言,最开始玩的是elisp,谁叫以前我是个深度的emacser(后来竟然变成了一个vimer,再后来就是sublimer,这世界真神奇)。

后来还是决定好好学习一下erlang,也第一次领略到了函数式编程的魅力。自己唯一用erlang开发过的东西就是bt下载的客户端,不过后来发现用处不大就没继续进行下去了。(好吧,我承认当时想下岛国的东西)

学习erlang之后最大的优势在于现在能看懂很多优秀的erlang项目,譬如我在做moonmq以及公司的推送服务的时候,研究了rabbitmq,这玩意可是用erlang写的,而我竟然还能看懂,太佩服我了。

还有么?

想想自己还学了哪些语言?貌似没了,不知道awk算不算一门。看来我会得语言真不多。

后续可能会学的

逆水行舟,不进则退,计算机发展这么迅速,我也需要不断提升自己,其中学习一门新的语言可能是一个很好的提升途径,至少能为我打开一扇门。譬如,如果掌握了日文,就能更好的理解岛国片的精髓。我不会日文,所以还是个门外汉。

ruby

ruby是一门很优雅的语言,很多大神级别的人物推荐,github貌似也是ruby的幕后推手。

因为ROR的兴起,使得ruby更加流行。看来,一个好的框架库对于语言的推广帮助真挺大的。相比而言,python有django,tornado等,光选择适合自己的就得费点时间。

ruby可以算是一门完全面向对象的语言,连number这种的都是对象,而且看了几本Matz的书,觉得这哥们挺不错的,对技术的感悟很深,所以更让我有兴趣去了解ruby了。

javascript

作为一个技术人员,没一个自己的个人网站怎么行,我的阿里云都是包年买的(明年还是买国外的vps吧),自己的个人站点还无影无踪。

好吧,我完全不会javscript,看着css就头疼,没准我从一开始想自己写代码搭建个人站点这个步子就迈的太大,扯着蛋了。如果先用一个开源的框架搭建起来,再自己调整完善,可能会更好。但无论怎样,javascript这门语言还是要学习了解的,尤其是以后随着html5的流行,加之node.js疯狂流行,这门语言也会愈发的发光发热。

C Sharp

其实本来不准备跟ms的东西扯上关系的,虽然vs是一个很强大的开发工具,但是我自从换成mac之后就不准备再迁回windows。

只是c#我可能是必须要学会的,因为那个坑爹的unity3d,虽然unity3d也提供了其它语言的支持(譬如伪javascript),但是大量的开发者还是选用了c#,至少在中国我问过很多朋友,都妥妥的用c#,既然这样,我也只能考虑学习使用了。

至于我为啥蛋疼的想玩unity3d,毕竟干了很多年游戏开发,一直有自己弄一个简单小游戏的梦想,还是妥妥的unity3d吧。

自己造一个?

语言千千万,我不可能全部学会的,而且以后没准因为业务的需要,没准都会自己造一门语言,也就是DSL。不过这个貌似还离我比较遥远,编译原理的东西太差了(说多了都是泪呀)。自己写词法分析还成,后面就菜了。这也是Mixer一直没啥进展的原因。不过已经买了龙书,在学习屠龙秘籍,希望成为顶尖高手吧。

后记

写了这么多,看来随着年岁的增加,越来越啰嗦了。不是有一句古话:吾生也有涯,而知也无涯 。以有涯随无涯,殆已。不过不停地追逐不也是乐趣一件?

只是,现在我首先要做的就是向我老婆申请资金升级电脑了吧!

高性能NoSQL LedisDB设计2

ledisdb现在已经支持replication机制,为ledisdb的高可用做出了保障。

使用

假设master的ip为10.20.187.100,端口6380,slave的ip为10.20.187.101,端口为6380.

首先我们需要master打开binlog支持,在配置文件中指定:

use_bin_log : true

在slave的机器上面我们可以通过配置文件指定slaveof开启replication,或者通过命令slaveof显示的开启或者关闭。

slaveof 10.20.187.100 6380

ledisdb的replication机制参考了redis以及mysql的相关实现,下面简单说明。

redis replication

redis的replication机制主要介绍在这里,已经说明的很详细了。

  • slave向master发送sync命令
  • master将其当前的数据dump到一个文件,同时在内存中缓存新增的修改命令
  • 当数据dump完成,master就将其发送给slave
  • slave接受完成dump数据之后,将其本机先前的数据清空,然后在导入dump的数据
  • master再将先前缓存的命令发送给slave

在redis2.8之后,为了防止断线导致重新生成dump,redis增加了psync命令,在断线的时候master会记住当前的同步状态,这样下次就能进行断点续传了。

mysql replication

mysql的replication主要是通过binlog的同步来完成的。在master的任何数据更新,都会写入binlog,至于binlog的格式这里不再累述。

假设binlog的basename为mysql,index文件名字为mysql-bin.index,该文件记录着当前所有的binlog文件。

binlog有max file size的配置,当binlog写入的的文件大小超过了该值,mysql就会生成一个新的binlog文件。当mysql服务重启的时候,也会生成一个新的binlog文件。

在Percona的mysql版本中,binlog还有一个max file num的设置,当binlog的文件数量超过了该值,mysql就会删除最早的binlog。

slave有一个master.info的文件,用以记录当前同步master的binlog的信息,主要就是当前同步的binlog文件名以及数据偏移位置,这样下次重新同步的时候就能从该位置继续进行。

slave同步的数据会写入relay log中,同时在后台有另一个线程将relay log的数据存入mysql。

因为master的binlog可能删除,slave同步的时候可能会出现binlog丢失的情况,mysql通过dump+binlog的方式解决,其实也就是slave完全的dump master数据,在生成的dump中也同时会记录当前的binlog信息,便于下次继续同步。

ledisdb replication

ledisdb的replication机制参考了redis以及mysql,支持fullsync以及增量sync。

master没有采用aof机制,而是使用了binlog,通过指定max file size以及max file num用来控制binlog的总体大小,这样我就无需关心aof文件持续增大需要重新rewrite的过程了。

binlog文件名格式如下:

ledis-bin.0000001
ledis-bin.0000002

binlog文件名的后缀采用数字递增,后续我们使用index来表示。

slave端也有一个master.info文件,因为ledisdb会严格的保证binlog文件后缀的递增,所以我们只需要记录当前同步的binlog文件后缀的index即可。

整个replication流程如下:

  • 当首次同步或者记录的binlog信息因为master端binlog删除导致不一致的时候,slave会发送fullsync进行全同步。
  • master收到fullsync信息之后,会将当前的数据以及binlog信息dump到文件,并将其发送给slave。
  • slave接受完成整个dump文件之后,清空所有数据,同时将dump的数据导入leveldb,并保存当前dump的binlog信息。
  • slave通过sync命令进行增量同步,sync命令格式如下:

      sync binlog-index binlog-pos
    

    master通过index定位到指定的binlog文件,并seek至pos位置,将其后面的binlog数据发送给slave。

  • slave接收到binlog数据,导入leveldb,如果sync没有收到任何新增数据,1s之后再次sync。

对于最后一点,最主要就是一个问题,即master新增的binlog如何让slave进行同步。对于这点无非就是两种模型,push和pull。

对于push来说,任何新增的数据都能非常及时的通知slave去获取,而pull模型为了性能考虑,不可能太过于频繁的去轮询,略有延时。

mysql采用的是push + pull的模式,当binlog有更新的时候,仅仅通知slave有了更新,slave则是通过pull拉取实际的数据。但是为了支持push,master必须得维持slave的一些状态信息,这稍微又增加了一点复杂度。

ledisdb采用了非常简单的一种方式,定时pull,使用1s的间隔,这样既不会因为轮询太过频繁导致性能开销增大,同时也能最大限度的减少当机数据丢失的风险。

总结

ledisdb的replication机制才刚刚完成,后续还有很多需要完善,但足以使其成为一个高可用的nosql选择了。

ledisdb的网址在这里https://github.com/siddontang/ledisdb,希望感兴趣的童鞋共同参与。

最近几个bug记录

记录最近出的几个bug

connection reset by peer

最近服务器经常性的出现connection reset by peer的错误,开始我们只是以为小概率的网络断开导致的,可是随着压力的增大,每隔2分钟开始出现一次,这就不得不引起我们的重视了。

我们的业务很简单,lvs负责负载均衡(采用的是DR模式),keepalive timeout设置的为2分钟,后面支撑两台推送服务(后面叫做push),客户端首先通过lvs路由到某台push之后,频繁的向其发送推送消息。

客户端使用的是python request(底层基于urllib3),首先我很差异出了这样的错误竟然没有重试,因为写代码的童鞋告诉我会有重试机制的。于是翻了一下request的代码,竟然发现默认的重试是0,一下子碉堡了。

不过,即使改了重试,仍然没有解决reset by peer的问题。通常出现这种情况,很大的原因在于客户端使用的是keep alive长连接保活tcp,但是服务器端关闭了该连接。可是我们的服务器实现了定时ping的保活机制,应该也不会出现的。

然后我将目光投向了lvs,因为它的timeout设置的为2分钟,而reset by peer这个错误也是两分钟一个,所以很有可能就是我们的定时ping机制不起作用,导致lvs直接close掉了连接。

于是查看push自己的代码,陡然发现我们自己设置的定时ping的时间是3分钟,顿时无语了,于是立刻改成1分钟,重启push,世界清静了。

ifconfig overruns

push换上新的机器之后,(性能妥妥的强悍),我们竟然发现推送的丢包率竟然上升了,一下子碉堡了,觉得这事情真不应该发生的。通常这种情况发生在cpu处理网络中断响应不过来。但是我们可是妥妥的24核cpu,并且开启了irqbalance。

好不,用cat /proc/interrupts之后,发现所有的网卡中断都被cpu0处理了,irqbalance完全没有起作用。google之后发现,有些网卡在PCI-MSI模式下面irqbalance无效,而我们的网卡恰好是PCI-MSI模式的。

没办法,关停irqbalance,手动设置网卡中断的SMP_AFFINITY,一下子世界清静了。

总结

可以发现,最近出的几次蛋疼的事情都是在运维层面上面出现的,实际测试也测不出来,碰到这样的问题,只能通过log这些的慢慢摸索排查了。当然也给了我一个教训,任何error级别的log都应该重视,不应该想当然的忽略。

高性能NoSQL LedisDB设计1

ledisdb是一个用go实现的基于leveldb的高性能nosql数据库,它提供多种数据结构的支持,网络交互协议参考redis,你可以很方便的将其作为redis的替代品,用来存储大于内存容量的数据(当然你的硬盘得足够大!)。

同时ledisdb也提供了丰富的api,你可以在你的go项目中方便嵌入,作为你app的主要数据存储方案。

与redis的区别

ledisdb提供了类似redis的几种数据结构,包括kv,hash,list以及zset,(set因为我们用的太少现在不予支持,后续可以考虑加入),但是因为其基于leveldb,考虑到操作硬盘的时间消耗铁定大于内存,所以在一些接口上面会跟redis不同。

最大的不同在于ledisdb对于在redis里面可以操作不同数据类型的命令,譬如(del,expire),是只支持kv操作的。也就是说,对于del命令,ledisdb只支持删除kv,如果你需要删除一个hash,你得使用ledisdb额外提供的hclear命令。

为什么要这么设计,主要是性能考量。leveldb是一个高效的kv数据库,只支持kv操作,所以为了模拟redis中高级的数据结构,我们需要在存储kv数据的时候在key前面加入相关数据结构flag。

譬如对于kv结构的key来说,我们按照如下方式生成leveldb的key:

func (db *DB) encodeKVKey(key []byte) []byte {
    ek := make([]byte, len(key)+2)
    ek[0] = db.index
    ek[1] = kvType
    copy(ek[2:], key)
    return ek
}

kvType就是kv的flag,至于第一个字节的index,后面我们在讨论。

如果我们需要支持del删除任意类型,可能的一个做法就是在另一个地方存储该key对应的实际类型,然后del的时候根据查出来的类型再去做相应处理。这不光损失了效率,也提高了复杂度。

另外,在使用ledisdb的时候还需要明确知道,它只是提供了一些类似redis接口,并不是redis,如果想用redis的全部功能,这个就有点无能为力了。

db select

redis支持select的操作,你可以根据你的业务选择不同的db进行数据的存放。本来ledisdb只打算支持一个db,但是经过再三考虑,我们决定也实现select的功能。

因为在实际场景中,我们不可能使用太多的db,所以select db的index默认范围就是[0-15],也就是我们最多只支持16个db。redis默认也是16个,但是你可以配置更多。不过我们觉得16个完全够用了,到现在为止,我们的业务也仅仅使用了3个db。

要实现多个db,我们开始定了两种方案:

  • 一个db使用一个leveldb,也就是最多ledisdb将打开16个leveldb实例。
  • 只使用一个leveldb,每个key的第一个字节用来标示该db的索引。

这两种方案我们也不知道如何取舍,最后决定采用使用同一个leveldb的方式。可能我们觉得一个leveldb可以更好的进行优化处理吧。

所以我们任何leveldb key的生成第一个字节都是存放的该db的index信息。

KV

kv是最常用的数据结构,因为leveldb本来就是一个kv数据库,所以对于kv类型我们可以很简单的处理。额外的工作就是生成leveldb对应的key,也就是前面提到的encodeKVKey的实现。

Hash

hash可以算是一种两级kv,首先通过key找到一个hash对象,然后再通过field找到或者设置相应的值。

在ledisdb里面,我们需要将key跟field关联成一个key,用来存放或者获取对应的值,也就是key:field这种格式。

这样我们就将两级的kv获取转换成了一次kv操作。

另外,对于hash来说,(后面的list以及zset也一样),我们需要快速的知道它的size,所以我们需要在leveldb里面用另一个key来实时的记录该hash的size。

hash还必须提供keys,values等遍历操作,因为leveldb里面的key默认是按照内存字节升序进行排列的,所以我们只需要找到该hash在leveldb里面的最小key以及最大key,就可以轻松的遍历出来。

在前面我们看到,我们采用的是key:field的方式来存入leveldb的,那么对于该hash来说,它的最小key就是“key:”,而最大key则是“key;”,所以该hash的field一定在“(key:, key;)”这个区间范围。至于为什么是“;”,因为它比“:”大1。所以“key:field”一定小于“key;”。后续zset的遍历也采用的是该种方式,就不在说明了。

List

list只支持从两端push,pop数据,而不支持中间的insert,这样主要是为了简单。我们使用key:sequence的方式来存放list实际的值。

sequence是一个int整形,相关常量定义如下:

listMinSeq     int32 = 1000
listMaxSeq     int32 = 1<<31 - 1000
listInitialSeq int32 = listMinSeq + (listMaxSeq-listMinSeq)/2

也就是说,一个list最多存放1<<31 - 2000条数据,至于为啥是1000,我说随便定得你信不?

对于一个list来说,我们会记录head seq以及tail seq,用来获取当前list开头和结尾的数据。

当第一次push一个list的时候,我们将head seq以及tail seq都设置为listInitialSeq。

当lpush一个value的时候,我们会获取当前的head seq,然后将其减1,新得到的head seq存放对应的value。而对于rpush,则是tail seq + 1。

当lpop的时候,我们会获取当前的head seq,然后将其加1,同时删除以前head seq对应的值。而对于rpop,则是tail seq - 1。

我们在list里面一个meta key来存放该list对应的head seq,tail seq以及size信息。

ZSet

zset可以算是最为复杂的,我们需要使用三套key来实现。

  • 需要用一个key来存储zset的size
  • 需要用一个key:member来存储对应的score
  • 需要用一个key:score:member来实现按照score的排序

这里重点说一下score,在redis里面,score是一个double类型的,但是我们决定在ledisdb里面只使用int64类型,原因一是double还是有浮点精度问题,在不同机器上面可能会有误差(没准是我想多了),另一个则是我不确定double的8字节memcmp是不是也跟实际比较结果一样(没准也是我想多了),其实更可能的原因在于我们觉得int64就够用了,实际上我们项目也只使用了int的score。

因为score是int64的,我们需要将其转成大端序存储(好吧,我假设大家都是小端序的机器),这样通过memcmp比较才会有正确的结果。同时int64有正负的区别,负数最高位为1,所以如果只是单纯的进行binary比较,那么负数一定比正数大,这个我们通过在构建key的时候负数前面加”<”,而正数(包括0)加”=”来解决。所以我们score这套key的格式就是这样:

key<score:member //<0
key=score:member //>=0

对于zset的range处理,其实就是确定某一个区间之后通过leveldb iterator进行遍历获取,这里我们需要明确知道的事情是leveldb的iterator正向遍历的速度和逆向遍历的速度完全不在一个数量级上面,正向遍历快太多了,所以最好别去使用zset里面带有rev前缀的函数。

总结

总的来说,用leveldb来实现redis那些高级的数据结构还算是比较简单的,同时根据我们的压力测试,发现性能还能接受,除了zset的rev相关函数,其余的都能够跟redis保持在同一个数量级上面,具体可以参考ledisdb里面的性能测试报告以及运行ledis-benchmark自己测试。

后续ledisdb还会持续进行性能优化,同时提供expire以及replication功能的支持,预计6月份我们就会实现。

ledisdb的代码在这里https://github.com/siddontang/ledisdb,希望感兴趣的童鞋共同参与。

LedisDB嵌入使用介绍

ledisdb现在可以支持嵌入式使用。你可以将其作为一个独立的lib(类似leveldb)直接嵌入到你自己的应用中去,而无需在启动单独的服务。

ledisdb提供的API仍然类似redis接口。首先,你需要创建ledis对象:

import "github.com/siddontang/ledisdb/ledis"

configJson = []byte('{
    "data_db" : 
    {
        "path" : "/tmp/testdb",
        "compression":true,
        "block_size" : 32768,
        "write_buffer_size" : 2097152,
        "cache_size" : 20971520
    }    
}
')

l, _ := ledis.Open(configJson)

data_db就是数据存储的leveldb位置,简单起见,所有的size配置全部使用byte作为单位。

然后我们选择一个db使用,

db, _ := l.Select(0)

类似redis,我们也只支持数字类型的db,最多16个db,索引范围为[0-15]。支持太多的db真没啥意义。

下面是一些简单的例子:

kv

db.Set(key, value)
db.Get(key)
db.SetNX(key, value)
db.Incr(key)
db.IncrBy(key, 10)
db.Decr(key)
db.DecrBy(key, 10)

db.MSet(KVPair{key1, value1}, KVPair{key2, value2})
db.MGet(key1, key2)

list

db.LPush(key, value1, value2, value3)
db.RPush(key, value4, value5, value6)

db.LRange(key, 1, 10)
db.LIndex(key, 10)

db.LLen(key)

hash

db.HSet(key, field1, value1)
db.HMSet(key, FVPair{field1, value1}, FVPair{field2, value2})

db.HGet(key, field1)

db.HGetAll()
db.HKeys()

zset

db.ZAdd(key, ScorePair{score1, member1}, ScorePair{score2, member2})

db.ZCard(key)

//range by score [0, 100], withscores = true and no limit
db.ZRangeByScore(key, 0, 100, true, 0, -1)

//range by score [0, 100], withscores = true and limit offset = 10, count = 10
db.ZRangeByScore(key, 0, 100, true, 10, 10)

db.ZRank(key, member1)

db.ZCount(key, member1)

ledisdb的源码在这里https://github.com/siddontang/ledisdb,欢迎反馈。