ma6174 / blog Goto Github PK
View Code? Open in Web Editor NEW博客
Home Page: https://ma6174.github.io/blog/
博客
Home Page: https://ma6174.github.io/blog/
使用mongodb有一段时间了,总体来说,mongodb还是非常给力的,但是有几个问题也是需要注意的。
理论上说在数据库中,一个主键(单一主键或者联合主键)在数据库中的值是唯一确定的。在一次倒数据时发现对于同一个主键(存在非UTF8编码字符)在数据库中存在多个值,并且值还是不完全一样的。使用数据库API查询的时候当然只能查到一个值,但查到的值不一定是最新的,可能会导致数据不一致现象。因此在数据库存储过程中最好不要使用非UTF8编码的KEY
mongodb使用复制集的时候,可以指定一个主数据库,接收所有的写请求(包括修改),然后记录相应的操作日志即oplog,从数据库从主数据库拉取oplog,再按先后顺序重放一下oplog,最终主数据库和从数据库数据应该是一致的。
最近在倒数据偶然发现主数据库和从数据库数据条目数不一致,表现为所有从数据库数据条目数比主数据库少并且所有从数据库数据条目数是一致的,数据正确性还需要进一步验证。主数据库在几天前已经停止写入和修改数据库了,从数据库已经同步完成了,从数据库条目数一致可以从反面印证。后来经过精确主从数据库数据对比发现有些数据只在主数据库有,有些数据只在从数据库有,有些数据在两个数据库都有但是内容不一致,最奇怪的一条是从数据库中的记录少了几个字段。后来查询数据库详细日志发现从数据库有恢复迹象,当数据文件损坏的时候数据库会进行数据修复。目前的问题是一个从数据库修复会影响所有的从数据库导致主从不一致。当磁盘出现问题的时候要格外关注,操作不当会导致丢数据,需要慎重。
当数据量达到亿级别以后,从数据源倒数据到mongodb性能会快速下降。有一张tokumx和mongodb的对比图可以看出来随着数据量的增加mongodb写入性能下降非常快。
我曾经测试过几种导入方式,比如单条写入、批量写入、使用mongoimport或mongorestore等官方工具导入,速度都不理想,导入速度越来越慢,可能和需要创建索引有关。sharding数据库导入速度更慢,估计和mongodb在分片的时候平衡数据有关。
因此需要对数据的量最好预估,不要在磁盘满的时候再想换大磁盘,不要等数据放不下的时候再做分片。
另外tokumx数据库看起来性能比mongodb好很多,还支持数据压缩,能节省很大的磁盘空间,并且是基于mongodb改造的,大部分api接口都是兼容的,可以做进一步的测试。
原文链接:https://willnorris.com/2014/05/go-rest-apis-and-pointers
在 go-github项目中,有一个有趣的设计就是几乎所有和Github交互的API结构体成员,几乎全部使用指针。经过大量试错,我决定将我的遇到的问题和解决方案分享出来,并且我认为其他用Go语言写API客户端的人也需要思考。最原始的问题来源在google/go-github#19,整个讨论过程可能也是比较有趣的;这篇文章会以一种更加容易理解的方式展示这个问题。接下来会讲以下内容的相互影响:Go语言的零值、JSON和XML标签中的omitempty
参数,和HTTP请求中PATCH
的语义。
Go语言对大部分数据编码优雅而简单。你只需要定义一个结构体,对结构体里面的每一个成员添加一个标签来说明这个成员如何被编码成特定的格式就可以了。例如,对于Github仓库,可以使用一个简单的结构体来表示:
type Repository struct {
Name string `json:"name"`
Description string `json:"description"`
Private bool `json:"private"`
}
结构体中每一个成员都明确指定当被编码成JSON之后key的名字,接下来我们构建一个新的仓库并且编码成JSON格式:
r := new(Repository)
b, _ := json.Marshal(r)
println(string(b))
输出 >>> {"name":"","description":"","private":false}
当我们创建了一个新的仓库之后,每一个成员都被赋值为其对应的零值:字符串类型是一个空字符串""
,布尔类型是false
。Go语言不存在一个变量定义了但是没初始化这一说。当前按照定义,如果最初一个变量没有被赋值,那么就会被初始化为其对应的零值。牢记,这个当前非常重要。
正如PATCH
名字一样,基于REST
的API会传递当前资源的状态信息。这在HTTP协议中很常见,也很直接:要获取当前资源的信息,发一个GET
请求到资源的URI就可以了。要更新一个资源,发送一个PUT
请求到其资源的URI,需要附带上资源的新描述信息。PUT
被定义为将URI对应的资源完整替换,也就是说你必须提供一个资源的完整信息。但是如果你只想更新资源的部分信息呢?这个需要通过PATCH
来完成,定义在:RFC 5789。
PATCH
请求中的body内容如何应用到对应的资源要根据请求的media type
来确定。Github(和其他很多JSON API)处理PATCH
请求的方式是:对于要更新的资源,你以json形式提供一个新的值,JSON中不存在的字段将不会被更新。举个例子,要更新一个仓库的描述信息,HTTP请求应该看起来是这个样子:
PATCH /repos/google/go-github HTTP/1.1
Host: api.github.com
{"description": "new description"}
要删除描述信息,直接将其设置为空字符串就好了:
PATCH /repos/google/go-github HTTP/1.1
Host: api.github.com
{"description": ""}
如果你发了一个PATCH
请求,包含了资源的每一个字段会怎么样?这和你使用相同Body的发了一个PUT
请求是等价的。事实上,正因如此,所有的资源更新操作在Github上都由PATCH
来完成。他们甚至不支持(至少文档上没说)使用PUT
来完成这种请求。
go-github 库有一个名为Edit的方法用于更新仓库信息。因为Repository
结构体包含了需要更新的字段,更新描述信息的Go代码应该是这样的:
r := &github.Repository{Description:"new description"}
client.Repositories.Edit("google", "go-github", r)
这段代码发出的HTTP请求是什么样的?如果你记得之前关于JSON编码的讨论,那么请求应该是这个样子:
PATCH /repos/google/go-github HTTP/1.1
Host: api.github.com
{"name": "", "description": "new description", "private": false}
但这并不和我们所指定的字段完全一样。Repository
结构体即使没有设置name
和private
这两个字段,Body里面包含了。但是需要明确的是,这些字段被设置为它们的零值,因此和我们指定的是一致的。name
字段倒是问题不大,因为它是不变的,github会忽略它。但是是private
问题就比较大了,如果原先是一个私有仓库,类似看起来无害的操作可能导致仓库被公开!
为了避免这个问题,我们可以更新我们的Repository
类型,让JSON编码的时候自动忽略空值:
type Repository struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Private bool `json:"private,omitempty"`
}
现在name
的空字符串和private
的false
值被忽略了,实现了我们所预期的HTTP请求:
PATCH /repos/google/go-github HTTP/1.1
Host: api.github.com
{"description": "new description"}
到目前为止看起来是没问题的。
现在我们回到上一个例子看一下代码是什么样子,现在我们删除仓库的描述信息,通过设置一个空字符串实现:
r := &github.Repository{Description:""}
client.Repositories.Edit("google", "go-github", r)
之前我们的结构体加上了omitempty
参数,对这个请求会有影响吗?不幸的是,请求不是我们所预期的:
PATCH /repos/google/go-github HTTP/1.1
Host: api.github.com
{}
因为我们设置Repository
结构体里面所有值为他们的零值,编码之后就成了一个空的JSON对象,这个请求不会产生任何影响,无法实现预期的删除仓库描述信息目的。
我们需要有一种方式来区分某个字段是因为初始化而被赋值为零值(JOSN序列化需要忽略)还是开发者有意将其设置为零值(JOSN序列化需要包含这些字段)。这时候指针就派上用场了、
指针的零值是nil
,不管指针指向什么类型。因此通过在我们的结构体成员中使用指针,我们可以明确区分因为没有设置而导致的空值:nil
,和有意设置的空值,比如""
,false
或0
,golang/protobuf就是这样做的,就是因为这个原因,我们的Repository
结构体最终定义是这个样子的:
type Repository struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Private *bool `json:"private,omitempty"`
}
这确实会带来一些损耗,不管是内存分配还是开发者体验,因为如果一个字段是字符串或布尔值,是必须创建一个指向它的指针才能赋值,这个确实比较烦人。最终可能会写出类似这样的过于冗余代码:
d := "new description"
r := &github.Repository{Description:&d}
client.Repositories.Edit("google", "go-github", r)
为了让这个操作更加简单,go-github提供了一个获取某种类型对应的指针的函数(拷贝自protobuf包):
r := &github.Repository{Description: github.String("new description")}
client.Repositories.Edit("google", "go-github", r)
使用指针也意味着使用这个库的客户端必须进行适当nil检查去避免程序painc。protobuf库提供了访问这些成员的代码,这让开发更简单一些。但是go-github并没有提供。
你Go实现的客户端API会因此受到影响吗?答案是看情况。如果API不提供像PATCH
这样的部分更新功能,那么你不需要设置omitempty
这个参数,也不用担心指针问题,你原来是方式没有问题。如果你在你的JSON或者XML请求里面永远不会设置零值,比如空字符串、false
或0
(或许不太可能),那么你只需要设置omitempty
参数就好了,其他照常。但是对于大部分现代API接口,这些都不应该出现的。你可以测试一下你当前的库是否会阻止你执行特定的操作。
(我想再说明一点是在google/go-github#19讨论了一些可替代的方法并没有实际尝试,比如使用字段掩码或者直接使用protobuf包。研究学习一下可能是值得的。指针仅仅在这个库里面是有效的;其他方法只要能解决问题都是没问题的)。
当一个container起来之后,我们有时候希望能进入container内部去看看,比如查查日志,执行些操作等。目前有几种方式可以实现:
这个是官方提供的一种方法。
测试,首先启动一个container:
$ docker run -i -t ubuntu bash
root@4556f5ad6067:/#
不要退出,打开另一个终端:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4556f5ad6067 ubuntu:14.04 "bash" 45 seconds ago Up 43 seconds jolly_ardinghelli
$ docker attach 4556f5ad6067
root@4556f5ad6067:/#
这样就连接进去了。这时候如果我们输入一些命令,就能看到在两个终端都有显示和输出。这种方式有比较大的局限性,如果知道了entrypoint或者有程序正在执行,通过docker attach
进入之后是不能执行操作的,一个终端退出之后整个container就终止了。不推荐使用这种方式。
如果使用这种方式,首先要保证docker是以lxc方式启动的,具体可以这样做:
/etc/default/docker
增加DOCKER_OPTS="-e lxc"
sudo service docker restart
启动container的方式和之前一样:
$ docker run -i -t ubuntu bash
root@e7f01f0ff598:/#
进入container可以这样:
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e7f01f0ff598 ubuntu:14.04 "bash" 17 seconds ago Up 15 seconds grave_jones
$ ps aux | grep e7f01f0ff598
root 23691 0.0 0.0 43140 1876 pts/9 Ss 21:47 0:00 lxc-start -n e7f01f0ff598c80d70a996135c98fbaeddc6daa61436bbbfa735233e8b6f8ebe -f /var/lib/docker/containers/e7f01f0ff598c80d70a996135c98fbaeddc6daa61436bbbfa735233e8b6f8ebe/config.lxc -- /.dockerinit -g 172.17.42.1 -i 172.17.0.3/16 -mtu 1500 -- bash
ma6174 23756 0.0 0.0 13428 928 pts/12 S+ 21:47 0:00 grep --color=auto e7f01f0ff598
$ sudo lxc-attach -n e7f01f0ff598c80d70a996135c98fbaeddc6daa61436bbbfa735233e8b6f8ebe
root@e7f01f0ff598:/#
这种方式还是很方便的。前提是需要重启docker服务以lxc的方式执行,进入container之后会有一个终端可以执行命令,不影响正在执行的程序。
如果docker不是以lxc方式启动的,这时候还想进入一个正在执行的container的话,可以考虑使用nsenter
这个程序的安装方式很独特,使用docker进行安装:
$ docker run --rm -v /usr/local/bin:/target jpetazzo/nsenter
使用方法也很简单,首先你要进入的container的PID
:
$ PID=$(docker inspect --format {{.State.Pid}} <container_name_or_ID>)
然后就可以用这个命令进入container了:
$ nsenter --target $PID --mount --uts --ipc --net --pid
为了使用方便可以写一个脚本自动完成:
$ cat /bin/docker_enter
#!/bin/bash
sudo nsenter --target `docker inspect --format {{.State.Pid}} $1` --mount --uts --ipc --net --pid bash
这样每次要进入某个container只需要执行docker_enter <container_name_or_ID>
就可以了。
这个原理也很简单,在container里面启动ssh服务,然后通过ssh
的方式去登陆到container里面,不推荐这种方式,主要是配置ssh登陆比较繁琐,开启ssh服务也会耗费资源,完全没有必要。
~~~~~~~~~~~~~~~华丽的分割线~~~~~~~~~~~~~~~
docker 1.2.0发布,带来一个比较实用的特性的是支持restart
参数,可以在docker run
的时候指定:
--restart="" Restart policy to apply when a container exits (no, on-failure, always)
有这个参数就比较方便了,比如container里面的服务因为某些原因退出了,之前只能通过外部程序去重新启动container,有了这参数之后可以放container自动重启,当然也可以设置失败重试次数,通过on-failure:5
这种方式来指定失败后最多尝试重启5次。
官方的两个例子:
docker run --restart=always redis
docker run --restart=on-failure:5 redis
其他新特性参考:https://blog.docker.com/2014/08/announcing-docker-1-2-0/
近期网站访问量大增,随之而来出现一些莫名其妙的慢请求,表现为:有两个服务A和B,服务A要去服务B请求数据,从日志看,服务A有比较多的秒级的慢请求,根据日志判断慢请求是因为服务B变慢了,接下来查B的日志,发现B只有极少的慢请求的日志,最起码从某个慢请求跟踪来看,服务B的处理返回数据很快,并没有出现所谓的慢请求。这个就比较有趣了,A说B变慢了,B说我没有变慢。到底是A自己慢了,还是B慢了,或者是A到B之间的链路慢了?
首先要确认是否是链路变慢了,A和B两个服务在同一个局域网内,两个服务互ping时间在1ms以内。之前有发现因为丢包导致网络变慢的现象,但是查两台机器发现丢包率都很低,在正常范围内。这个基本可以排除网络问题。
因为之前日志记录不是很完善,接下来首先要做的是补日志,关键操作记录时间。随后分析日志发现,服务A从收到请求到将请求发送到B这个时间是毫秒级的,B从收到请求到处理完返回也是毫秒级的,也就是说两边处理请求都很快。后来详细对比服务A和B的时间戳发现一个惊人的现象:服务A发请求给B的时间和服务B收到请求的时间差了几秒!出现这样的情况首先要确认服务A和服务B所在是机器时间是一致的,经检查确实是一致的。会不会是网络丢包导致的?如果有比较严重的丢包问题的话,收发请求出现大量重传可能会导致比较大的网络延迟。但是之前已经分析过,两台机器之间的网络状况非常好,没有丢包现象。即使有丢包的话,出现丢包问题的时候,如果如果等待200ms没收到回复会进行重传,如果要达到秒级别的延时的话至少一个请求要丢包5次以上,这已经是很严重的丢包事故了,很容易被检测到。
这样看起来似乎问题没法继续分析解答了。就在这时,服务B出现了一条500的日志,看错误信息是too many open files
,一般出现这种情况是因为服务B自己文件句柄数超了,当超过6000的时候就比较容易出现上面的错误。这里的files
不一定是真实的文件,有可能是tcp连接,因为在linux下一切皆是文件。先写个程序监测一些服务B的文件句柄数吧,检查一个进程的文件句柄数可以用lsof -n -p pid | wc -l
来观测,pid就是进程的pid。写个shell脚本循环观测:
#!/bin/sh
while true
do
echo -n `date`"\t"
lsof -n -p `pidof service_name` | wc -l
sleep 0.5
done
部分观测结果:
Tue Jul 22 19:42:51 CST 2014 2715
Tue Jul 22 19:42:52 CST 2014 2713
Tue Jul 22 19:42:53 CST 2014 2711
Tue Jul 22 19:42:53 CST 2014 2711
Tue Jul 22 19:42:54 CST 2014 2708
Tue Jul 22 19:42:55 CST 2014 10177
Tue Jul 22 19:42:59 CST 2014 3103
Tue Jul 22 19:43:00 CST 2014 2722
Tue Jul 22 19:43:00 CST 2014 2719
从上面的结果我们很容易看出来,在19:42:55
文件句柄数从2708
突然升到了10177
,随后很快恢复。之前说过,当文件句柄数超过6000就有可能会出现too many open files
错误。这里文件句柄数都升到1w了。继续观察fd(文件句柄)突然升高的频率,发现在网络高峰期大约每2~3分钟就会出现一次。是不是因为fd突然升高导致慢请求呢?继续分析服务A的日志,发现确实在fd升高的时候会出现慢请求。如果是这样的话,是不是就能认为服务B在那个时间点有太多请求了导致从服务A新来的连接无法建立而出现慢请求?这只是一种猜测,当然还有一个更大的疑惑需要答案:服务B的fd为什么会突然升高?
因为服务B基本不会读写本地文件,因此可以认定fd升高肯定是突然有大量请求到来。是不是服务A的请求数有大幅度波动导致B有大幅度波动?这个需要统计服务A的每秒的请求数,发现并没有突然升高迹象。因为我们对服务B做了负载均衡,如果B1服务器突然有大量请求到来的话,那么B2服务器也应当会同时出现相同的fd突然升高现象。观察发现,B2服务器确实有升高现象,升高频率相似,但是时间点不同,能差几十秒。从这里我们基本能确定是B服务器在某个时间点发生了什么事情导致fd升高。
fd升高会有什么影响?继续查B的日志,惊奇的发现,fd升高之前,会有几秒钟没有任何日志的情况。难道是服务卡住了?如果是卡住的话,还能接受请求吗?为了验证这个问题,我们用一个简单的curl来测试
#!/bin/sh
while true
do
echo `date`
curl http://127.0.0.1:8080 -o /dev/null > /dev/null 2>&1
sleep 0.2
done
部分结果:
Tue Jul 22 18:26:09 CST 2014
Tue Jul 22 18:26:09 CST 2014
Tue Jul 22 18:26:09 CST 2014
Tue Jul 22 18:26:09 CST 2014
Tue Jul 22 18:26:14 CST 2014
Tue Jul 22 18:26:14 CST 2014
Tue Jul 22 18:26:14 CST 2014
从这里我们看到,在18:26:09
curl确实卡住了,直到18:26:14
才恢复,卡了将近5秒!我们有理由相信,curl会卡住,那么来自服务A的请求也有可能会卡住,因为我们在这段时间并没有看到服务B的任何日志,也就是说服务B卡住了,拒绝提供服务了。
如果我们curl和检查fd同时进行,对比结果会有新发现(左边是检查fd结果,右边是curl日志)。
Tue Jul 22 19:44:52 CST 2014 2708 │Tue Jul 22 19:44:57 CST 2014
Tue Jul 22 19:44:52 CST 2014 2710 │Tue Jul 22 19:44:57 CST 2014
Tue Jul 22 19:44:53 CST 2014 2707 │Tue Jul 22 19:44:58 CST 2014
Tue Jul 22 19:44:54 CST 2014 2709 │Tue Jul 22 19:44:58 CST 2014
Tue Jul 22 19:44:54 CST 2014 2729 │Tue Jul 22 19:44:58 CST 2014
Tue Jul 22 19:44:55 CST 2014 2707 │Tue Jul 22 19:44:58 CST 2014
Tue Jul 22 19:44:56 CST 2014 2709 │Tue Jul 22 19:45:03 CST 2014
Tue Jul 22 19:44:56 CST 2014 2714 │Tue Jul 22 19:45:03 CST 2014
Tue Jul 22 19:44:57 CST 2014 2718 │Tue Jul 22 19:45:03 CST 2014
Tue Jul 22 19:44:58 CST 2014 2710 │Tue Jul 22 19:45:03 CST 2014
Tue Jul 22 19:44:58 CST 2014 2713 │Tue Jul 22 19:45:04 CST 2014
Tue Jul 22 19:44:59 CST 2014 2713 │Tue Jul 22 19:45:04 CST 2014
Tue Jul 22 19:45:02 CST 2014 10219 │Tue Jul 22 19:45:04 CST 2014
Tue Jul 22 19:45:03 CST 2014 2877 │Tue Jul 22 19:45:04 CST 2014
Tue Jul 22 19:45:04 CST 2014 2721 │Tue Jul 22 19:45:04 CST 2014
Tue Jul 22 19:45:05 CST 2014 2710 │Tue Jul 22 19:45:05 CST 2014
Tue Jul 22 19:45:05 CST 2014 2710 │Tue Jul 22 19:45:05 CST 2014
Tue Jul 22 19:45:06 CST 2014 2714 │Tue Jul 22 19:45:05 CST 2014
Tue Jul 22 19:45:07 CST 2014 2716 │Tue Jul 22 19:45:05 CST 2014
我们可以看到,在19:44:58
,curl卡住了,直到19:45:03
恢复。而句柄数升高是在19:45:02
。我们基本能够确认,在19:44:58
的时候,服务B卡住了,但是服务A的请求还是会到来,请求会堆积,大约4~5秒之后,服务B恢复正常,大量请求到来导致服务B句柄数升高。
看来句柄数升高只是一个结果,服务B卡住是直接原因,那么服务B为什么会卡住呢?从上面的分析看,当服务B卡住之后,会出现下面几个现象:
如果是某个goroutine卡住了,其他goroutine不一定卡住,还会有日志的,也能正常接收新请求。看起来整个进程卡住了,卡住这段时间发生了什么?Go有自动垃圾回收(gc)机制,我们有理由怀疑是因为gc卡住了。
如何验证是否是GC的问题?最简单的方法是看GC日志。如果你使用Go1.2之前的版本,可以这样启动程序:
GOGCTRACE=1 /path/to/your/program
如果你是Go1.2之后的版本,需要这样开启GC日志:
GODEBUG=gctrace=1 /path/to/your/program
这样程序启动的时候我们就能看到类似这样的日志:
gc1(1): 0+0+0 ms, 0 -> 0 MB 16 -> 18 (19-1) objects, 0(0) handoff, 0(0) steal, 0/0/0 yields
gc2(1): 0+0+0 ms, 0 -> 0 MB 29 -> 29 (30-1) objects, 0(0) handoff, 0(0) steal, 0/0/0 yields
gc3(1): 0+0+0 ms, 0 -> 0 MB 972 -> 747 (973-226) objects, 0(0) handoff, 0(0) steal, 0/0/0 yields
gc4(1): 0+0+0 ms, 0 -> 0 MB 1248 -> 904 (1474-570) objects, 0(0) handoff, 0(0) steal, 0/0/0 yields
想了解每个字段的含义的话可以看 源码,这里我们关注的是GC在什么时候发生的,频率是多少,GC一次花多长时间,GC的效果可以看内存减少了多少,也可以看对象减少多少。
开启GC log之后,我们再去看日志,就会发现有这样的信息:
2014/07/25 11:03:28 app log...
gc183(8): 18+4043+495 ms, 32426 -> 16950 MB 205909094 -> 3045275 (23371853344-23368808069) objects, 60(1982) handoff, 71(105762) steal, 564/267/314 yields
2014/07/25 11:03:33 app log...
很明显,在11:03:28
到11:03:33
这几秒时间都在GC,再看之前的fd测试和curl测试结果:
Fri Jul 25 11:03:26 CST 2014 2463 │Fri Jul 25 11:03:27 CST 2014
Fri Jul 25 11:03:26 CST 2014 2461 │Fri Jul 25 11:03:27 CST 2014
Fri Jul 25 11:03:27 CST 2014 2460 │Fri Jul 25 11:03:27 CST 2014
Fri Jul 25 11:03:28 CST 2014 2459 │Fri Jul 25 11:03:28 CST 2014
Fri Jul 25 11:03:29 CST 2014 2462 │Fri Jul 25 11:03:28 CST 2014
Fri Jul 25 11:03:31 CST 2014 2462 │Fri Jul 25 11:03:28 CST 2014
Fri Jul 25 11:03:31 CST 2014 9738 │Fri Jul 25 11:03:28 CST 2014
Fri Jul 25 11:03:34 CST 2014 2501 │Fri Jul 25 11:03:33 CST 2014
Fri Jul 25 11:03:35 CST 2014 2500 │Fri Jul 25 11:03:33 CST 2014
Fri Jul 25 11:03:36 CST 2014 2492 │Fri Jul 25 11:03:34 CST 2014
Fri Jul 25 11:03:36 CST 2014 2493 │Fri Jul 25 11:03:34 CST 2014
Fri Jul 25 11:03:37 CST 2014 2494 │Fri Jul 25 11:03:34 CST 2014
Fri Jul 25 11:03:38 CST 2014 2490 │Fri Jul 25 11:03:35 CST 2014
Fri Jul 25 11:03:38 CST 2014 2490 │Fri Jul 25 11:03:35 CST 2014
Fri Jul 25 11:03:39 CST 2014 2490 │Fri Jul 25 11:03:35 CST 2014
Fri Jul 25 11:03:40 CST 2014 2489 │Fri Jul 25 11:03:35 CST 2014
Fri Jul 25 11:03:40 CST 2014 2493 │Fri Jul 25 11:03:36 CST 2014
现在原因很清晰了,程序发生了GC,curl会卡住,GC结束之后大量请求到来会导致fd升高。GC竟然卡了5秒!对于一个线上服务来说,这个是不能忍受的。
GC时间长,说明程序创建了大量的对象,占用了较多的内存,进行垃圾回收的时间肯定会延长,对比了一下,在半夜访问量比较少的时候,GC的时间会更短。
如何解决GC时间长导致程序卡住问题?
先去网上找了一下,果然找到一个人遇到了类似的情况:https://groups.google.com/forum/#!topic/golang-nuts/S9goEGuoMRM,原文是这样的:
Hello everyone,
Our business suffered from an annoying problem. We are developing an
iMessage-like service in Go, the server can serves hundreds of
thousands of concurrent TCP connection per process, and it's robust
(be running for about a month), which is awesome. However, the process
consumes 16GB memory quickly, since there are so many connections,
there are also a lot of goroutines and buffered memories used. I
extend the memory limit to 64GB by changing runtime/malloc.h and
runtime/malloc.goc. It works, but brings a big problem too - The
garbage collecting process is then extremely slow, it stops the world
for about 10 seconds every 2 minutes, and brings me some problems
which are very hard to trace, for example, when stoping the world,
messages delivered may be lost. This is a disaster, since our service
is a real-time service which requires delivering messages as fast as
possible and there should be no stops and message lost at all.
I'm planning to split the "big server process" to many "small
processes" to avoid this problem (smaller memory footprint results to
smaller time stop), and waiting for Go's new GC implementation.
Or any suggestions for me to improve our service currently? I don't
know when Go's new latency-free garbage collection will occur.
Thanks.
大体意思是他用Go开发了一个实时消息系统,访问量很大,占用内存比较多,服务每两分钟就会因为GC问题卡住10秒(比我们那个还严重),对于一个实时的系统来说,这个是不能接受的。
后面有比较多的讨论,有建议他升级Go语言版本,他确实升级了,效果也比较明显,升级之后GC减小到1~2秒,不过还是比较长的:
Hello everyone,
Thanks for all your help, I updated our Go version to:
go version devel +852ee39cc8c4 Mon Nov 19 06:53:58 2012 +1100
and rebuilt our servers, now GC duration reduced to 1~2 seconds, it's
a big improvement!
Thank contributors on the new GC!
看来升级Go版本是一个办法,新版本会对GC进行优化。尝试升级Go版本。
先从Go1.1升级到Go1.2.1,发现GC问题基本没解决,甚至比之前更差。升到1.3之后,惊喜得发现GC时间明显缩短,一次GC在几十毫秒内就能完成,fd没有突然升高现象了,curl也没有卡住现象了:
gc43(8): 44+21+49805+104 us, 3925 -> 7850 MB, 50135750 (1781280527-1731144777) objects, 549097/394298/0 sweeps, 117(9232) handoff, 69(2683) steal, 969/439/2061 yields
再看一下go 1.3的release note,确实对GC有优化:
Changes to the garbage collector
For a while now, the garbage collector has been precise when examining values in the heap; the Go 1.3 release adds equivalent precision to values on the stack. This means that a non-pointer Go value such as an integer will never be mistaken for a pointer and prevent unused memory from being reclaimed.
Starting with Go 1.3, the runtime assumes that values with pointer type contain pointers and other values do not. This assumption is fundamental to the precise behavior of both stack expansion and garbage collection. Programs that use package unsafe to store integers in pointer-typed values are illegal and will crash if the runtime detects the behavior. Programs that use package unsafe to store pointers in integer-typed values are also illegal but more difficult to diagnose during execution. Because the pointers are hidden from the runtime, a stack expansion or garbage collection may reclaim the memory they point at, creating dangling pointers.
Updating: Code that uses unsafe.Pointer to convert an integer-typed value held in memory into a pointer is illegal and must be rewritten. Such code can be identified by go vet
几十毫秒是能接受的。如果升级Go版本没有用那么还有没有其他措施?
服务个数多了,每个服务的请求数就少了,程序内部对象数目就少了,GC的时间会减少。
对于频繁使用的,数量较多的对象,程序初始化的时候生成一定数量的对象,以后程序用到这类对象的时候不是去重新生成而是去对象池中取一个对象。这样对象的总数是一定的,减少垃圾回收的对象个数。
Go默认并发数是8,参考http://golang.org/src/pkg/runtime/malloc.h#L140,如果机器性能比较高的话,可以尝试提高GC并发数,理论上说应该会快一些。
假设正常的话服务B1在100毫秒内会返回数据,某次请求发现150毫秒还没有返回数据,这时候服务A主动断开,尝试去服务B2去请求数据。这个依据是服务B1和服务B2并不一定会同时卡住。这样的好处是即使服务B的GC时间比较长,也不会影响服务A,因为A可以选一个没有GC的的B去请求数据。
为了提高系统可靠性,当一个HTTP请求因为某些原因失败了之后,需要重新发送请求进行尝试。如果是GET
请求,重试很简单,再重新发一下GET请求就可以了,如果是带有Body
的POST
请求,在重发请求的时候需要把Body
也重发一遍。我们先看看Go HTTP中对Request
的定义:
type Request struct {
Method string
URL *url.URL
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
}
这里的Body类型是io.ReadCloser
,再简化一些就是一个Reader
,服务端收到请求之后就会从这个Reader
中调用Read()
函数去读取数据,通常情况当服务端去读取数据的时候,offset
会随之改变,下一次再读的时候会从offset
位置继续向后读取。
当一个请求失败了我们要进行重试的时候,就必须重新发送Body,因为Body有可能已经被读过了,因此需要将Body这个Reader进行重置,我们可能首先想到的就是将Body执行Seek
操作,Seek到Reader的起始位置,这样做的前提是这个Reader支持Seek操作,也就是说这个Reader是一个ReadSeeker
。这样做有没有问题呢?我们先写一个脚本测试一下:
package main
import (
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/ma6174/slowrw"
)
func handle(rw http.ResponseWriter, req *http.Request) {
go func() {
buf := make([]byte, 1)
n, err := io.ReadFull(req.Body, buf)
if err != nil || n != 1 {
log.Fatal("read failed:", n, err)
}
if buf[0] != '1' {
log.Fatalf("buf not start at 1: %#v", string(buf))
}
out, err := os.Open(os.DevNull)
if err != nil {
log.Fatal(err)
}
io.Copy(out, req.Body)
}()
time.Sleep(time.Second)
}
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
go func() {
http.HandleFunc("/", handle)
log.Fatal(http.ListenAndServe(":9999", nil))
}()
reader := strings.NewReader("1234567890abcdefghigklmnopqrst")
sr := slowrw.NewReadSeeker(reader, time.Millisecond)
req, err := http.NewRequest("POST", "http://127.0.0.1:9999/", sr)
if err != nil {
log.Fatal(err)
}
client := http.Client{
Timeout: time.Millisecond * 10,
}
for {
_, err := client.Do(req)
if err != nil {
log.Println(err)
}
_, err = sr.Seek(0, 0)
if err != nil {
log.Fatal("seek failed", err)
}
}
}
对上面的测试简单说明一下:这里定义了客户端和服务端,先说服务端,收到请求后会开一个goutine去异步地读取数据,读数据的时候先读一个字节,正常情况下我们认为第一个字节数据是1
,如果读到的数据非1
则认为body已经被读过了,数据是错的。
通常情况下读数据和收数据非常快,为了放慢速度方便测试,这里引用了我写的一个 github.com/ma6174/slowrw 库,这里我们用的是SlowReader
,也就是说客户端在发数据的时候,1毫秒才能发一个字符。服务端读数据不限速。
另外客户端有设置一个超时时间:Timeout: time.Millisecond * 10
,因为上面说了客户端没1毫秒才能发送一个字符,我们设置的字符有30个,所以全部发送完至少要30毫秒,但是我们客户端限制整个请求时间是10毫秒,也就是说客户端请求最终肯定会超时失败,这样做的目的是模拟客户端请求失败情况,并触发重试。
先看一下执行结果:
$ go run seek_http.go
2015/01/11 16:35:39 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:39 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:39 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:39 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:39 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:39 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:39 seek_http.go:22: buf not start at 1: "4"
exit status 1
$
$ go run seek_http.go
2015/01/11 16:35:42 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:42 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:42 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:42 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:42 seek_http.go:22: buf not start at 1: "5"
exit status 1
$
$ go run seek_http.go
2015/01/11 16:35:46 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:46 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:46 seek_http.go:51: Post http://127.0.0.1:9999/: read tcp 127.0.0.1:9999: use of closed network connection
2015/01/11 16:35:46 seek_http.go:22: buf not start at 1: "4"
exit status 1
看程序运行结果能看出,在经过几次正常
(数据以1开始,最终超时)的请求之后,服务端就会出现收到不是以1
开始的Body,这就意味着请求失败了,这样证实了通过每次Seek
到起始位置的方法是不靠谱的。
HTTP Server在读取数据的时候可能是在一个goroutine
里面进行,当一个请求失败之后,虽然客户端调用了CancelRequest
将请求取消,但是服务端可能还会继续读取部分数据。如果我们调用Seek
之后服务端还在读取数据的话,就会造成数据偏移,导致服务端下次再读数据的时候读到的数据不完整。当Body中数据量比较大或者网络不稳定或者客户端速度限制等原因造成读取数据传输时间很长的情况,用Seek
出现问题的可能性就非常大。
解决方案也很多,既然同一个reader可能被并发读取,那我们只要保证一个reader只能被读取一次就可以了,最简单的方法是每次都重新构造一个reader。上面的代码简单修改是这样的:
package main
import (
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/ma6174/slowrw"
)
func handle(rw http.ResponseWriter, req *http.Request) {
go func() {
buf := make([]byte, 1)
n, err := io.ReadFull(req.Body, buf)
if err != nil || n != 1 {
log.Fatal("read failed:", n, err)
}
if buf[0] != '1' {
log.Fatalf("buf not start at 1: %#v", string(buf))
}
out, err := os.Open(os.DevNull)
if err != nil {
log.Fatal(err)
}
io.Copy(out, req.Body)
}()
time.Sleep(time.Second)
}
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
go func() {
http.HandleFunc("/", handle)
log.Fatal(http.ListenAndServe(":9999", nil))
}()
reader := strings.NewReader("1234567890abcdefghigklmnopqrst")
sr := slowrw.NewReader(reader, time.Millisecond)
req, err := http.NewRequest("POST", "http://127.0.0.1:9999/", sr)
if err != nil {
log.Fatal(err)
}
client := http.Client{
Timeout: time.Millisecond * 10,
}
for {
_, err := client.Do(req)
if err != nil {
log.Println(err)
}
reader = strings.NewReader("1234567890abcdefghigklmnopqrst")
sr = slowrw.NewReader(reader, time.Millisecond)
req, err = http.NewRequest("POST", "http://127.0.0.1:9999/", sr)
if err != nil {
log.Fatal(err)
}
}
}
这段代码执行没有任何问题,但是存在隐患,主要体现在reader每次都是通过通过数据新生成的,这里用的是strings.NewReader
,这就意味着客户端必须持有原始数据,直到请求成功或者放弃重试。如果请求的数据比较大或者请求量大的话可能会占用大量的内存。
另外一种解决方案是用io.ReaderAt
,我们只需要对现有的reader进行封装,对于每一个请求,保证数据是从起始位置开始读取即可,先上代码:
package main
import (
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/ma6174/slowrw"
)
func handle(rw http.ResponseWriter, req *http.Request) {
go func() {
buf := make([]byte, 1)
n, err := io.ReadFull(req.Body, buf)
if err != nil || n != 1 {
log.Fatal("read failed:", n, err)
}
if buf[0] != '1' {
log.Fatalf("buf not start at 1: %#v", string(buf))
}
out, err := os.Open(os.DevNull)
if err != nil {
log.Fatal(err)
}
io.Copy(out, req.Body)
}()
time.Sleep(time.Second)
}
type Reader struct {
Reader io.ReaderAt
Offset int64
}
func (p *Reader) Read(val []byte) (n int, err error) {
n, err = p.Reader.ReadAt(val, p.Offset)
p.Offset += int64(n)
return
}
func (p *Reader) Close() error {
if rc, ok := p.Reader.(io.ReadCloser); ok {
return rc.Close()
}
return nil
}
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
go func() {
http.HandleFunc("/", handle)
log.Fatal(http.ListenAndServe(":9999", nil))
}()
reader := strings.NewReader("1234567890abcdefghigklmnopqrst")
sr := slowrw.NewReaderAt(reader, time.Millisecond)
ra := &Reader{reader, 0}
req, err := http.NewRequest("POST", "http://127.0.0.1:9999/", ra)
if err != nil {
log.Fatal(err)
}
client := http.Client{
Timeout: time.Millisecond * 10,
}
for {
_, err := client.Do(req)
if err != nil {
log.Println(err)
}
sr = slowrw.NewReaderAt(reader, time.Millisecond)
reader2 := &Reader{sr, 0}
req.Body = reader2
}
}
上面代码中,reader只有一个,但是每次都用&Reader{sr, 0}
去生成一个从起点开始读数据的reader2,因为reader2每次都是从起点开始读取的,所以不会出现一个reader被多次读取的情况。当然用这种方式的前提是这个reader必须是io.ReaderAt
即支持ReadAt()
方法。
当正则表达式搜索条件中包含\x00
(或者\u0000
,\0
,null
)这个特殊字符时,可能得到非预期的结果,我们可以一步一步来验证:
\x00
是一个合法的Value
mongodb对文档的定义可以参考http://docs.mongodb.org/manual/core/document/#field-value-limit
,这里面提到,key
不能包含null
,但是value
是可以的,实际测试下来也是这样的:
$ mongo
MongoDB shell version: 3.0.3
connecting to: test
> db.test.save({"a\x00":"a0"}) // key contains \x00 is invalid
2015-06-13T10:46:41.223+0800 E QUERY Error: JavaScript property (name) contains a null char which is not allowed in BSON. {}
at addToOperationsList (src/mongo/shell/bulk_api.js:602:29)
at insert (src/mongo/shell/bulk_api.js:649:14)
at DBCollection.insert (src/mongo/shell/collection.js:243:18)
at DBCollection.save (src/mongo/shell/collection.js:493:21)
at (shell):1:9 at src/mongo/shell/bulk_api.js:602
> db.test.save({"_id":"a\x00"}) // value contains \x00 is ok
WriteResult({ "nMatched" : 0, "nUpserted" : 1, "nModified" : 0, "_id" : "a\u0000" })
> db.test.find()
{ "_id" : "a\u0000" }
>
> db.test.save({"_id":"a\x00b"})
WriteResult({ "nMatched" : 0, "nUpserted" : 1, "nModified" : 0, "_id" : "a\u0000b" })
> db.test.save({"_id":"ab"})
WriteResult({ "nMatched" : 0, "nUpserted" : 1, "nModified" : 0, "_id" : "ab" })
> db.test.save({"_id":"a"})
WriteResult({ "nMatched" : 0, "nUpserted" : 1, "nModified" : 0, "_id" : "a" })
> db.test.find()
{ "_id" : "a\u0000" }
{ "_id" : "a\u0000b" }
{ "_id" : "ab" }
{ "_id" : "a" }
>
> db.test.find({_id:"a\x00"})
{ "_id" : "a\u0000" }
> db.test.find({_id:"a\x00b"})
{ "_id" : "a\u0000b" }
> db.test.find({_id:"ab"})
{ "_id" : "ab" }
> db.test.find({_id:"a"})
{ "_id" : "a" }
a\x00
为前缀的文档,{ "_id" : "a" }
不应该出现> db.test.find({_id: {"$regex" : "^a\x00"}})
{ "_id" : "a" }
{ "_id" : "a\u0000" }
{ "_id" : "a\u0000b" }
{ "_id" : "ab" }
>
\x00
开头,期望不返回任何文档,结果是返回所有文档> db.test.find({_id: {"$regex" : "^\x00"}})
{ "_id" : "a" }
{ "_id" : "a\u0000" }
{ "_id" : "a\u0000b" }
{ "_id" : "ab" }
>
\x00RandomString
开头,期望不返回任何文档,结果是返回所有文档> db.test.find({_id: {"$regex" : "^\x00RandomString"}})
{ "_id" : "a" }
{ "_id" : "a\u0000" }
{ "_id" : "a\u0000b" }
{ "_id" : "ab" }
>
\x00RandomString
,期望不返回任何文档,结果是返回所有文档> db.test.find({_id: {"$regex" : "\x00RandomString"}})
{ "_id" : "a" }
{ "_id" : "a\u0000" }
{ "_id" : "a\u0000b" }
{ "_id" : "ab" }
>
\x00
的mongodb使用的正则表达式是Perl compatible regular expressions (i.e. “PCRE” ) version 8.36 with UTF-8 support
(http://docs.mongodb.org/manual/reference/operator/query/regex/)
下载地址:http://sourceforge.net/projects/pcre/?source=typ_redirect
编译参数:./configure --enable-utf8 --enable-unicode-properties
pcre-8.36$ ./pcretest -C
PCRE version 8.36 2014-09-26
Compiled with
8-bit support
UTF-8 support
Unicode properties support
No just-in-time compiler support
Newline sequence is LF
\R matches all Unicode newlines
Internal link size = 2
POSIX malloc threshold = 10
Parentheses nest limit = 250
Default match limit = 10000000
Default recursion depth limit = 10000000
Match recursion uses stack
pcre-8.36$ ./pcretest -d
PCRE version 8.36 2014-09-26
re> "^\x61\x00"
------------------------------------------------------------------
0 8 Bra
3 ^
4 a\x00
8 8 Ket
11 End
------------------------------------------------------------------
Capturing subpattern count = 0
Options: anchored
No first char
No need char
data> a
No match
data> a\x00
0: a\x00
data> a\x00b
0: a\x00
data> ab
No match
data>
$ mongo
MongoDB shell version: 3.0.3
connecting to: test
> /^a\x00/.test("a")
false
> /^a\x00/.test("a\x00")
true
> /^a\x00/.test("a\x00b")
true
> /^a\x00/.test("ab")
false
>
$ node
> var reg = new RegExp("^a\x00")
undefined
> reg.test("a")
false
> reg.test("a\x00")
true
> reg.test("a\x00b")
true
> reg.test("ab")
false
>
抓取这个请求的包
> db.test.find({_id: {"$regex" : "^a\x00b"}})
{ "_id" : "a" }
{ "_id" : "a\u0000" }
{ "_id" : "a\u0000b" }
{ "_id" : "ab" }
>
结果:
$ sudo tcpdump -i lo0 -X dst port 27017
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo0, link-type NULL (BSD loopback), capture size 65535 bytes
11:27:01.302450 IP localhost.64032 > localhost.27017: Flags [P.], seq 2309521406:2309521476, ack 2695537044, win 11950, options [nop,nop,TS val 1556359160 ecr 1556325546], length 70
0x0000: 4500 007a 6867 4000 4006 0000 7f00 0001 E..zhg@.@.......
0x0010: 7f00 0001 fa20 6989 89a8 7ffe a0aa a194 ......i.........
0x0020: 8018 2eae fe6e 0000 0101 080a 5cc4 27f8 .....n......\.'.
0x0030: 5cc3 a4aa 4600 0000 5600 0000 0000 0000 \...F...V.......
0x0040: d407 0000 0000 0000 7465 7374 2e74 6573 ........test.tes
0x0050: 7400 0000 0000 0000 0000 2000 0000 035f t.............._
0x0060: 6964 0016 0000 0002 2472 6567 6578 0005 id......$regex..
0x0070: 0000 005e 6100 6200 0000 ...^a.b...
11:27:01.303661 IP localhost.64032 > localhost.27017: Flags [.], ack 105, win 11946, options [nop,nop,TS val 1556359161 ecr 1556359161], length 0
0x0000: 4500 0034 9232 4000 4006 0000 7f00 0001 E..4.2@.@.......
0x0010: 7f00 0001 fa20 6989 89a8 8044 a0aa a1fc ......i....D....
0x0020: 8010 2eaa fe28 0000 0101 080a 5cc4 27f9 .....(......\.'.
0x0030: 5cc4 27f9 \.'.
11:27:01.304142 IP localhost.64032 > localhost.27017: Flags [P.], seq 70:149, ack 105, win 11946, options [nop,nop,TS val 1556359161 ecr 1556359161], length 79
0x0000: 4500 0083 f7af 4000 4006 0000 7f00 0001 E.....@.@.......
0x0010: 7f00 0001 fa20 6989 89a8 8044 a0aa a1fc ......i....D....
0x0020: 8018 2eaa fe77 0000 0101 080a 5cc4 27f9 .....w......\.'.
0x0030: 5cc4 27f9 4f00 0000 5700 0000 0000 0000 \.'.O...W.......
0x0040: d407 0000 0000 0000 7465 7374 2e24 636d ........test.$cm
0x0050: 6400 0000 0000 ffff ffff 2900 0000 0169 d.........)....i
0x0060: 734d 6173 7465 7200 0000 0000 0000 f03f sMaster........?
0x0070: 0166 6f72 5368 656c 6c00 0000 0000 0000 .forShell.......
0x0080: f03f 00 .?.
11:27:01.304442 IP localhost.64032 > localhost.27017: Flags [.], ack 299, win 11940, options [nop,nop,TS val 1556359161 ecr 1556359161], length 0
0x0000: 4500 0034 63bb 4000 4006 0000 7f00 0001 E..4c.@.@.......
0x0010: 7f00 0001 fa20 6989 89a8 8093 a0aa a2be ......i.........
0x0020: 8010 2ea4 fe28 0000 0101 080a 5cc4 27f9 .....(......\.'.
0x0030: 5cc4 27f9 \.'.
在0x0070
行能看到6100 62
,0x61
是a
,0x62
是b
,中间的字符是\x00
,确认mongodb shell正确将查询请求发送给了mongodb,mongodb处理有问题,返回了非预期的结果。
mongodb在正则表达式处理上面有问题,\x00
这个字符有可能直接将整个正则表达式截断了,使用不完整的正则表达式进行匹配得到错误的结果。
根据bsonspec对正则表达式的定义:
regexp ::= "\x0B" e_name cstring cstring
cstring ::= (byte*) "\x00"
正则表达式是由4部分组成:
\x0B
,一个标志位。e_name
是正则表达式的名字。cstring
是正则表达式字符串。cstring
是正则表达式的标志位。再看cstring
的定义,就是一个字节数组,以\x00
结束。官方也明确说cstring
是不能包含\x00
字符的:
Zero or more modified UTF-8 encoded characters followed by '\x00'. The (byte*) MUST NOT contain '\x00', hence it is not full UTF-8.
如果mongodb使用bson来存储正则表达式,看起来是天然有问题的,首先正则表达式是可以使用\x00
字符,但是bson的正则表达式不允许使用\x00
字符,如果使用了整个正则表达式会被截断。
如果正则表达式部分使用string
而不是cstring
存储就不会有问题,看一下string
的定义:
string ::= int32 (byte*) "\x00"
string
的组成里面,第一部分是一个int32的数,表示整个字符串长度,因为长度明确,那么即使中间出现\x00
也不会被截断。
这个问题终归到底看起来是bson的设计问题,正则表达式应该使用string
存储而不是cstring
存储。
mongodb自身也没处理好,如果不允许使用 \x00
,应该明确指出,在用户使用的时候需要报一个错,而不是返回错误的结果。
如果用户输入中包含\x00
字符,有可能会取得非预期结果,有可能会获取整个collection
里面的所有内容,这个还是需要重视的。测试会影响所有版本的mongodb。
在mongodb修复这个bug之前,可以采取几种措施:
\x00
字符。\x00
字符,需要对mongodb返回的结果用标准正则表达式再匹配一次。已经向mongodb官方报bug:https://jira.mongodb.org/browse/SERVER-18824,欢迎围观。
docker container默认是UTC时间的,如果要使用北京时间需要修改时区,有以下几种方式:
$ docker run -i -t ubuntu bash
root@1f8ccb4c3dc1:/# date
Wed Sep 10 16:02:38 UTC 2014
root@1f8ccb4c3dc1:/# dpkg-reconfigure tzdata
Current default time zone: 'Asia/Shanghai'
Local time is now: Thu Sep 11 00:02:50 CST 2014.
Universal Time is now: Wed Sep 10 16:02:50 UTC 2014.
root@1f8ccb4c3dc1:/# date
Thu Sep 11 00:02:52 CST 2014
root@1f8ccb4c3dc1:/#
$ docker run -i -t -v /etc/localtime:/etc/localtime ubuntu bash
root@6213cd50d722:/# date
Thu Sep 11 00:08:56 CST 2014
root@6213cd50d722:/#
旧版本的dockere可能文件是/etc/localtime:ro
,需要修改挂载方式为:-v /etc/localtime:/etc/localtime:ro
$ docker run -i -t ubuntu bash
root@d4b926fb2c75:/# date
Wed Sep 10 16:11:15 UTC 2014
root@d4b926fb2c75:/# apt-get install -y wget > /dev/null 2>&1
root@d4b926fb2c75:/# wget http://ma6174.u.qiniudn.com/localtime -O /etc/localtime -o /dev/null
root@d4b926fb2c75:/# date
Thu Sep 11 00:13:05 CST 2014
root@d4b926fb2c75:/#
近期测试了一下snappy压缩算法,总体感觉是压缩、解压速度非常快,应用场景也很多。
官网: https://code.google.com/p/snappy/
Snappy is a compression/decompression library. It does not aim for maximum compression, or compatibility with any other compression library; instead, it aims for very high speeds and reasonable compression. For instance, compared to the fastest mode of zlib, Snappy is an order of magnitude faster for most inputs, but the resulting compressed files are anywhere from 20% to 100% bigger. On a single core of a Core i7 processor in 64-bit mode, Snappy compresses at about 250 MB/sec or more and decompresses at about 500 MB/sec or more.
基本意思就是说snappy压缩算法不追求压缩比,而是追求压缩和解压速度。在i7
64位CPU上压缩速度在250MB/s,解压速度达到500MB/s
snappy
VS gzip
压缩速度 | 解压速度 | 压缩比 | |
---|---|---|---|
snappy | 146.6M/s | 257.7M/s | 54.8% |
gzip | 23.8M/s | 233.1M/s | 69.2% |
$ time cat ubuntu_ele.vdi | wc -c
4062183424
cat ubuntu_ele.vdi 0.08s user 3.61s system 15% cpu 23.522 total
wc -c 21.20s user 1.00s system 94% cpu 23.521 total
$ time cat ubuntu_ele.vdi | snappy | wc -c
1837598172
cat ubuntu_ele.vdi 0.09s user 3.18s system 12% cpu 27.215 total
snappy 25.45s user 0.98s system 97% cpu 27.217 total
wc -c 13.59s user 0.91s system 53% cpu 27.216 total
$ time cat ubuntu_ele.vdi | gzip | wc -c
1252402891
cat ubuntu_ele.vdi 0.08s user 2.63s system 1% cpu 2:44.34 total
gzip 161.52s user 1.11s system 98% cpu 2:44.34 total
wc -c 12.66s user 0.30s system 7% cpu 2:44.34 total
$ time cat ubuntu_ele.vdi | snappy | snappy -d | wc -c
4062183424
cat ubuntu_ele.vdi 0.09s user 3.12s system 9% cpu 33.553 total
snappy 28.39s user 1.31s system 88% cpu 33.552 total
snappy -d 13.36s user 1.67s system 44% cpu 33.552 total
wc -c 24.09s user 1.03s system 74% cpu 33.553 total
$ time cat ubuntu_ele.vdi | gzip | gzip -d | wc -c
4062183424
cat ubuntu_ele.vdi 0.08s user 2.70s system 1% cpu 2:44.92 total
gzip 161.62s user 1.15s system 98% cpu 2:44.92 total
gzip -d 15.63s user 0.99s system 10% cpu 2:44.92 total
wc -c 24.36s user 0.71s system 15% cpu 2:44.92 total
这些都在用snappy
进行压缩了:
MongoDB
Cassandra
Couchbase
Hadoop
LessFS
LevelDB (which is in turn used by Google Chrome)
Rocksdb
Lucene
VoltDB
在Google内部也大量被使用
Snappy is widely used inside Google, in everything from BigTable and MapReduce to our internal RPC systems. (Snappy has previously been referred to as “Zippy” in some presentations and the likes.)
如果对压缩解压速度要求比较高,并且压缩比一般可以接受的话,snappy是一种比较好的选择。永久存储(日志等)或者实时传输(rpc等)都是比较好的使用场景。
为了方便使用,我写了一个类似gzip
的一个工具snappy
,可以随时对文件进行压缩,也可以通过管道对流实时压缩传输,项目主页: https://github.com/ma6174/snappy
首先从一个日志分析的Go程序说起,基本功能就是一行一行读取数据并处理。代码大体是这样的:
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
if err := deal(line); err != nil {
log.Println(line)
}
}
}
因为数据是用hadoop直接cat出来的,通过管道传输到日志处理程序,所以这里输入源是stdin
。
实际处理的时候发现一个比较奇怪的现象,每个日志文件的总行数差别不大,但是有的文件处理时间明显比其他短,并且还会能看到这样的日志信息:cat: Unable to write to output stream.
。这个日志不是Go里面的,确认是hadoop报了这个错误,起初以为是hadoop在某些情况下可能会出现这个错误,因为出现这个错误就意味着某些文件没有处理完程序就退出了。这个是不能容忍的。于是便向运维那边报bug。
运维那边承认这个错误是hadoop报的,但是出现这个错误一般是因为stream
断了,比如如果有head操作就会出现这个错误:
$ hdfs dfs -cat 0.log | head -1
*****log contents*****
cat: Unable to write to output stream.
为了证明是后面处理程序的问题而不是hadoop的问题,运维那边做了这样一个操作:
$ cat 0.log | go_program
$ echo ${PIPESTATUS[@]}
141 0
cat非0,后面的程序返回是0,UNIX基本程序一般是不会有问题的,看来是后面的Go程序有问题。开始检查验证。
首先要确认的问题有两个:
查询这个问题也是比较容易的,将处理过的每一行日志输出并打印行号,这样就知道处理到哪一行了,也就能知道在哪一行出错。
修改程序后重新执行,确实是Go程序提前退出了,并且没处理的下一行唯一的特殊之处是这一行超级长。难道Go不能处理很长的一行?这看起来不可能发生,但是还要验证一下。
首先是重新review代码,按理说程序出错之后会有错误日志的,为啥Go程序不但没报错还正常退出了?首先是review数据处理代码,没发现问题,然后就开始怀疑是bufio.Scanner的问题,重新看了一下Go文档,官网有一个example是这么写的:
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fmt.Println(scanner.Text()) // Println will add back the final '\n'
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading standard input:", err)
}
原来scanner还有一个Err()
方法,官方的说法是Err returns the first non-EOF error that was encountered by the Scanner.
,也就是说如果scanner.Scan()
如果出错的话错误信息是要通过Err()方法才能得到的,我的go程序将这个Err忽略了,出错后退出for循环就直接退出程序了,到底是有什么错误?代码补充完整之后看到这样的错误:bufio.Scanner: token too long
。
到目前能确认的确实是Go程序问题,在读数据的时候出现错误,上面的错误是什么意思?翻Go代码。
~/go$ grep -n ` find -name "*.go"` -e "bufio.Scanner: token too long"
./src/pkg/bufio/scan.go:61: ErrTooLong = errors.New("bufio.Scanner: token too long")
确实有这个错误,继续看scan.go,看看在哪报ErrTooLong
这个错误:
~/go$ grep -n ` find -name "*.go"` -e "ErrTooLong"
./src/pkg/bufio/scan.go:61: ErrTooLong = errors.New("bufio.Scanner: token too long")
./src/pkg/bufio/scan.go:147: s.setErr(ErrTooLong)
./src/pkg/bufio/scan_test.go:244: if err != ErrTooLong {
./src/pkg/bufio/scan_test.go:245: t.Fatalf("expected ErrTooLong; got %s", err)
scan.go 147行出现的错误,看具体代码:
144 // Is the buffer full? If so, resize.
145 if s.end == len(s.buf) {
146 if len(s.buf) >= s.maxTokenSize {
147 s.setErr(ErrTooLong)
148 return false
149 }
150 newSize := len(s.buf) * 2
151 if newSize > s.maxTokenSize {
152 newSize = s.maxTokenSize
153 }
154 newBuf := make([]byte, newSize)
155 copy(newBuf, s.buf[s.start:s.end])
156 s.buf = newBuf
157 s.end -= s.start
158 s.start = 0
159 continue
160 }
看了一下Scanner模块的代码,原来是这样的:Scanner在初始化的时候有设置一个maxTokenSize
,这个值默认是MaxScanTokenSize = 64 * 1024
,当一行的长度大于64*1024即65536之后,就会出现ErrTooLong
错误。这样就能解释之前为什么长行处理报错的问题了。
毕竟日志一行可能不止65536这么长,问题还是要解决的,有两种方案:
对于方案1,仔细看了一下Scnner,并没有发现有接口可以修改这个值,除非修改Go源码并重新编译Go,这个太折腾。在官方文档里面发现这样一段话:
Scanning stops unrecoverably at EOF, the first I/O error, or a token too large to fit in the buffer. When a scan stops, the reader may have advanced arbitrarily far past the last token. Programs that need more control over error handling or large tokens, or must run sequential scans on a reader, should use bufio.Reader instead.
它告诉我们如果token太大(行太长)的时候,要使用bufio.Reader
。这里是第一个坑,怪当初用的时候没仔细看文档,没仔细研读Go源码。从这里我们可以得到一个教训: 除非能确定行长度不超过65536,否则不要使用bufio.Scanner!
那接下来老老实实用bufio.Reader
吧。那bufio.Reader
有没有类似bufio.Scanner
的问题呢?
看了一下bufio.Reader
代码,这个也是有缓冲区大小限制的,并且默认缓冲区大小是4096
,还好有一个函数NewReaderSize
可以调整这个缓冲区大小。
还是之前的需求,如何按行去读取数据呢?bufio.Reader
提供了一个ReadLine()
函数,文档上是这么说的:
ReadLine is a low-level line-reading primitive. Most callers should use ReadBytes('\n') or ReadString('\n') instead or use a Scanner.
意思是这个函数比较底层,建议使用ReadBytes或ReadString或者Scanner。继续看文档说明:
ReadLine tries to return a single line, not including the end-of-line bytes. If the line was too long for the buffer then isPrefix is set and the beginning of the line is returned. The rest of the line will be returned from future calls. isPrefix will be false when returning the last fragment of the line. The returned buffer is only valid until the next call to ReadLine. ReadLine either returns a non-nil line or it returns an error, never both.
从这里我们能看出来设置缓冲区的作用了,ReadLine会尽量去读取并返回完整的一行,但是如果行太长缓冲区满了的话,就不会返回完整的一行而是返回缓冲区里面的内容,并且会设置isPrefix为true。这时候需要继续调用ReadLine直到将完整一行读完。然后外层调用程序需要将这些块拼起来才能组成完整的行。不仅要处理isPrefix还要处理前缀,麻烦!除非我们主动设置一个非常大的缓冲,但是前提是你必须知道最长行的长度,在大多数情况下这个是无法预先知道的。怪不得建议我们使用ReadBytes或ReadString或者Scanner。
之前讨论了,Scanner在行太长的时候是有问题的,ReadBytes和ReadString原理上是一样的,这里我们以ReadString为例看看这个函数。ReadString原理很简单,使用也很方便,基本用法是指定一个分隔符,比如我们如果读取一行的话就指定分隔符为\n
,这样ReadString就去不断去读数据,直到发现分隔符\n
或者出错为止。官网是这么说的:
ReadString reads until the first occurrence of delim in the input, returning a string containing the data up to and including the delimiter. If ReadString encounters an error before finding a delimiter, it returns the data read before the error and the error itself (often io.EOF). ReadString returns err != nil if and only if the returned data does not end in delim. For simple uses, a Scanner may be more convenient.
(ps: 这里又在推荐Scanner...)
ReadString有没有缓冲区大小限制呢?这个其实也还是有的,不过,它在代码里面就将这个缓冲区的问题处理好了,也就是说能保证返回的一行肯定是完整的。最起码用起来比ReadLine函数方便。
那么用ReadString去按行读取有没有坑呢?答案是肯定的。
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
func main() {
s := "a\nb\nc"
reader := bufio.NewReader(strings.NewReader(s))
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
fmt.Printf("%#v\n", line)
}
}
上面这段代码的输出是:
"a\n"
"b\n"
为什么c
没有输出?这里算是一个坑。之前讨论过,按行读取的话ReadString函数需要以\n
作为分割,上面那种特殊情况当数据末尾没有\n
的时候,直到EOF还没有分隔符\n
,这时候返回EOF错误,但是line里面还是有数据的,如果不处理的话就会漏掉最后一行。简单修改一下:
package main
import (
"bufio"
"fmt"
"io"
"strings"
)
func main() {
s := "a\nb\nc"
reader := bufio.NewReader(strings.NewReader(s))
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Printf("%#v\n", line)
break
}
panic(err)
}
fmt.Printf("%#v\n", line)
}
}
这样执行会输出:
"a\n"
"b\n"
"c"
这样处理的时候也要当心,因为最后一行后面没有\n
。
相比之下,python处理要简单和清晰很多,有生成器,直接for循环遍历即可:
import StringIO
s = "a\nb\nc"
sio = StringIO.StringIO(s)
for line in sio:
print line,
数据库采用一个master两个slave方式部署,分别设置priority为3,2,1,当master数据库受到毁灭性的破坏(比如文件误删,系统挂掉)之后,master已经完全不可用,此时两个slave会进行选举,priority为2的slave会被选为master。此时数据库处在一个危险状态,如果再有任何一个数据库挂掉那么整个集群将变只读。
如何修复?要分几种情况:
数据库当发生重新选举的时候,会导致数据库短时无master而不能正常提供服务。根据经验,在集群中增加一个节点是安全的,不会触发重新选举。但是删除节点会导致重新选举。不管是添加还是删除节点,都会或多或少的导致数据库出现慢请求。还有一个问题是当需要移除一个节点的时候,最好先将该节点停掉再移除,因为测试发现不停机直接移除导致数据库出现不可用和慢请求的个数远比停机操作多。
从上面的分析看,网上有人为了节省,只设置了一个master和一个slave,再加一个选举节点,当某个非选举节点挂掉之后有很大可能性是无法恢复的。所以集群中至少应该设置3个数据节点。
在A Tour of Go看到这样一段代码:
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
上述代码执行结果:
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90
官方是这么解释的:
Go 函数可以是闭包的。闭包是一个函数值,它来自函数体的外部的变量引用。函数可以对这个引用值进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。例如,函数 adder 返回一个闭包。每个闭包都被绑定到其各自的 sum 变量上。
记得之前在面试某团的时候面试官一直问我闭包的定义,当时只知道这个东西怎么定义,如何使用,概念没答上来,后来特意搜了一下,wikipedia是这么说的:
在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
闭包可以用来在一个函数与一组“私有”变量之间建立关联关系。在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所建立所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。
有这样一个需求:我要测试一个服务的平均响应时间、最大响应时间,每100次输出一次结果。
一般我们会这样写:
package main
import (
"fmt"
"math/rand"
"time"
)
func testService() {
time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}
func main() {
rand.Seed(time.Now().UnixNano())
timeTotal := 0.0
timeMax := 0.0
timeCount := 0
printCount := 100
for i := 0; i < 1000; i++ {
timeStart := time.Now()
testService()
timeCost := time.Since(timeStart).Seconds()
timeCount += 1
timeTotal += timeCost
if timeCost > timeMax {
timeMax = timeCost
}
if timeCount%printCount == 0 {
fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
timeMax = 0
timeCount = 0
timeTotal = 0
}
}
}
执行结果示例
$ go run count_time.go
100 runs ave time: 5.26ms, max time 10.18ms
100 runs ave time: 6.03ms, max time 10.19ms
100 runs ave time: 5.75ms, max time 10.19ms
100 runs ave time: 6.06ms, max time 10.31ms
100 runs ave time: 5.62ms, max time 10.19ms
100 runs ave time: 5.52ms, max time 10.20ms
100 runs ave time: 5.82ms, max time 10.19ms
100 runs ave time: 5.61ms, max time 10.19ms
100 runs ave time: 5.41ms, max time 10.19ms
100 runs ave time: 6.24ms, max time 10.19ms
上面的代码在功能方面是没有问题的,但是服务和统计代码混合到一块,看起来比较乱。程序也不方便复用,如果有其他类似的统计又要再写一次,比较麻烦。
看看用闭包如何实现类似的功能:
package main
import (
"fmt"
"math/rand"
"time"
)
func testService() {
time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}
func NewTimeCount(printCount int) func(timeCost float64) {
rand.Seed(time.Now().UnixNano())
timeTotal := 0.0
timeMax := 0.0
timeCount := 0
return func(timeCost float64) {
timeCount += 1
timeTotal += timeCost
if timeMax < timeCost {
timeMax = timeCost
}
if timeCount%printCount == 0 {
fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
timeMax = 0
timeCount = 0
timeTotal = 0
}
}
}
func main() {
timeCount := NewTimeCount(100)
for i := 0; i < 1000; i++ {
timeStart := time.Now()
testService()
timeCost := time.Since(timeStart).Seconds()
timeCount(timeCost)
}
}
这里我们定义了一个NewTimeCount
函数,这个函数的参数是每隔printCount
输出一次统计结果。函数内部包含了timeTotal
、timeMax
、timeCount
三个变量,在函数内部又定义了一个匿名函数来做我们的统计工作,我们需要每次将timeCost
参数传递给这个匿名函数,执行匿名函数的时候,计数器自动加一,时间总数,最大时间自动自动更新,到我们设定的timeCount
之后自动输出并清零。timeTotal
、timeMax
、timeCount
这三个变量在NewTimeCount
之外是不可见的,可以认为是匿名函数的全局变量。
再看一下main
函数,比之前清晰了好多,这里我们是每100
次输出一次结果,我们可以通过NewTimeCount
来生成多个计数器来做统计,代码可重用行和可读性都比之前的代码要好很多。
这里还有一个问题是要计算TimeCost
,对首次使用的人来说可能不不清楚如何计算。如果程序再简化一些,将计算TimeCost
也自动化的话就方便多了。可以这样做:
package main
import (
"fmt"
"math/rand"
"time"
)
func testService() {
time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}
func NewTimeCount(printCount int) (clockStart, clockStop func()) {
rand.Seed(time.Now().UnixNano())
var (
timeTotal float64
timeMax float64
timeCount int
timeStart time.Time
)
clockStart = func() {
timeStart = time.Now()
}
clockStop = func() {
timeCost := time.Since(timeStart).Seconds()
timeCount += 1
timeTotal += timeCost
if timeMax < timeCost {
timeMax = timeCost
}
if timeCount%printCount == 0 {
fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
timeMax = 0
timeCount = 0
timeTotal = 0
}
}
return
}
func main() {
clockStart, clockStop := NewTimeCount(100)
for i := 0; i < 1000; i++ {
clockStart()
testService()
clockStop()
}
}
这次我们修改了NewTimeCount
函数,让其返回两个函数,一个函数用来指定程序的起始时间,另一个来指定终止时间并统计和输入。main
函数得到更进一步简化。
如果将testService
作为参数传入的话代码可以更进一步简化:
package main
import (
"fmt"
"math/rand"
"time"
)
func testService() {
time.Sleep(time.Duration(rand.Intn(10)+1) * time.Millisecond)
}
func NewTimeCount(printCount int) func(f func()) {
rand.Seed(time.Now().UnixNano())
timeTotal := 0.0
timeMax := 0.0
timeCount := 0
return func(f func()) {
timeStart := time.Now()
f()
timeCost := time.Since(timeStart).Seconds()
timeCount += 1
timeTotal += timeCost
if timeMax < timeCost {
timeMax = timeCost
}
if timeCount%printCount == 0 {
fmt.Printf("%d runs ave time: %.2fms, max time %.2fms\n", printCount, timeTotal/float64(timeCount)*1000, timeMax*1000)
timeMax = 0
timeCount = 0
timeTotal = 0
}
}
}
func main() {
timeCount := NewTimeCount(100)
for i := 0; i < 1000; i++ {
timeCount(testService)
}
}
总的来说,闭包这个功能还是比较实用的。
使用lxc方式启动可以方便进行管理控制
docker -d -e lxc -dns 8.8.8.8 -dns 8.8.4.4
docker build -t tag_name --rm=true .
docker container_id tag_name
下面的命令将container的8080端口映射为主机的80端口,指定container_name,将主机目录挂载到container里面,启动时执行bash命令。
docker run -t -i -p 80:8080 --name container_name -v host_addr:container_addr image_name /bin/bash
docker ps
ps -ef | grep 5531741b0ff0
sudo lxc-attach -n 5531741b0ff05a46c793b24bfaa2b33dba66cad29fe9926641fee98b6b3e24df
docker save test_2 | gzip > image.tar.gz
cat image.tar.gz | gzip -d | sudo docker load
cat image.tar.gz | gzip -d | sudo docker import - test_import
docker tag ubuntu:12.04 10.0.0.6:5000/ubuntu
docker commit 797d1da068ce 10.0.0.6:5000/busybox:version2 -a ma6174 -m "rm aaaa"
docker push 10.0.0.6:5000/ubuntu
docker pull 10.0.0.6:5000/ubuntu
docker history 10.0.0.6:5000/busybox
内核版本一定要在3.8以上,不然容易出一些莫名其妙的问题,还有被提权的风险。
开发过程中经常会遇到这样的问题:我改了一个包,哪些服务受影响?我们期望是把所有相关的服务都升级,避免出现遗漏。
对于业务不复杂或者比较偏上层的包,大部分直接在代码库里面grep
一下就知道了。如果的偏底层的包,业务不一定会直接引用,这种直接分析的话就比较复杂了。
之前有种比较麻烦但是也相对有效的方法。我们知道Go的编译是有缓存的,如果代码有变更的话就会触发包重新编译,通过 go build -v
可以把重新编译的包的名字输出出来,当然也包含最终的服务程序。实际操作的时候需要避免ide编译影响。
后来研究了一下,实际有更好的办法来实现。go list
命令有个-json
的参数,会自动分析各个包的依赖关系,以json的格式输出出来,我们以官方库里面的encoding/base64
包为例:
# go list -json encoding/base64
{
"Dir": "/usr/local/go/src/encoding/base64",
"ImportPath": "encoding/base64",
"Name": "base64",
"Doc": "Package base64 implements base64 encoding as specified by RFC 4648.",
"Target": "/usr/local/go/pkg/linux_amd64/encoding/base64.a",
"Root": "/usr/local/go",
"Match": [
"encoding/base64"
],
"Goroot": true,
"Standard": true,
"GoFiles": [
"base64.go"
],
"Imports": [
"encoding/binary",
"io",
"strconv"
],
"Deps": [
"encoding/binary",
"errors",
"internal/bytealg",
"internal/cpu",
"internal/race",
"io",
"math",
"math/bits",
"reflect",
"runtime",
"runtime/internal/atomic",
"runtime/internal/math",
"runtime/internal/sys",
"strconv",
"sync",
"sync/atomic",
"unicode",
"unicode/utf8",
"unsafe"
],
"TestGoFiles": [
"base64_test.go"
],
"TestImports": [
"bytes",
"errors",
"fmt",
"io",
"io/ioutil",
"reflect",
"strings",
"testing",
"time"
],
"XTestGoFiles": [
"example_test.go"
],
"XTestImports": [
"encoding/base64",
"fmt",
"os"
]
}
其中有几个字段需要关注一下:
ImportPath
:在执行包引用时的名字 encoding/base64
Name
:包的名字 base64
Imports
:这个包直接引用的包:"encoding/binary", "io", "strconv"
Deps
:这个包所有递归依赖的包有了上面的Deps
信息之后,开头提到的依赖分析问题就比较简单了。只要在Deps
里面有我们修改的包就可以了,如果要找是哪个服务的话,需要看Name
是main
的包。另外go list -json ./...
就可以分析当前项目里面的所有包。
go list -json ./...
输出的信息还是挺多的,为了方便分析,写了个小工具 go_dep_search (https://github.com/ma6174/go_dep_search)
比如找出官方库里面所有依赖net/http
或者encoding/json
的可执行程序(ImportPath
那一列):
/usr/local/go/src# go list -json ./... | go_dep_search -main net/http encoding/json
Name -> ImportPath -> dep_package
main -> cmd/compile -> encoding/json
main -> cmd/cover -> encoding/json
main -> cmd/dist -> encoding/json
main -> cmd/go -> net/http
main -> cmd/go -> encoding/json
main -> cmd/link -> encoding/json
main -> cmd/pprof -> net/http
main -> cmd/pprof -> encoding/json
main -> cmd/test2json -> encoding/json
main -> cmd/trace -> net/http
main -> cmd/trace -> encoding/json
main -> cmd/vet -> encoding/json
希望对大家有帮助。
一直觉得IFTTT是一个很伟大的产品,它将互联网的世界串了起来,用 if (this) then {that}
这样简单又强大的语句对世界编程,我们的生活将更加丰富多彩。
最初的IFTTT Channel比较少,我们能做的比较有限,今天登陆IFTTT发现Channel数竟然达到了121个之多,有几个是非常好玩的。
最明显的是增加了移动客户端,使用安卓或IOS安装IFTTT客户端之后,我们不仅能在客户端上创建和修改recipes
,客户端也成了一个trigger
或者Action
。比如对安卓客户端来说,我们可以使用以下触发器:
移动设备可以触发的Action
:
智能硬件最近很火,IFTTT上也有一些关于智能硬件的,比如说谷歌眼镜,安卓智能手表(手环),智能开关等有这些硬件的支持,可以做更多有趣的事情,比如下班回家离开公司自动打开家里的电源开始煮饭,比如晚上十点自动打开热水器烧水洗澡,比如晚上回家手机连接家里wifi之后自动打开客厅的灯,有智能开关,一切变得简单。
关于提醒的Channel更丰富了,比如pushover
,pushbullet
,instapush
等,有了这些专业消息推送工具,可以让信息提醒更加简单和实时。比如gmail收到某人的一封重要邮件自动推送到手机或其他客户端,比如社交网站有动态自动提醒,比如当天气突变的时候发送提醒等。
关于文章、RSS、阅读、笔记等Channel也是非常高效和实用。比如我们可以用feedly
来订阅感兴趣的文章,if (feedly有更新) then (自动推送到pocket)
,然后我们就可以在pocket上阅读文章,if (i started an article) then (save to evernote)
还可以设置一个触发器当我感觉某篇文章比较好加了星标之后自动保存到印象笔记进行备份,当然也可以再自动分享这篇文章到社交网络,这一切都是自动的。
有gmail
这个channel之后,我们可以使用邮件来扩展更丰富的内容。比如在网上淘到一本电子书,我们可以保存到dropbox
里面,if (dropbox新增电子书) then (自动使用邮箱将附件发送到kindle邮箱)
,随后我们就可以用kindle来阅读。
一个比较有趣的channel是github
,目前提供了这样几个trigger
:
pull request
有了上面的trigger
我们就可以做写提醒啥的。此外还提供了一个Action: 创建一个新的issue
,测试了一下貌似不太好用。
IFTTT上还有一个“神器”是Yo
,这个大家可能不太熟悉,国内的仿造品是呵呵
,功能超级简单,官方是这么说的:
世界上最简单、最高效的通信工具。
Yo是一款单触式、零字符的通信工具。
Yo就是一切,它完全取决于你,取决于收信人,取决于你们有没有时间说Yo。
想说“早上好?”Yo一下。
想说“宝贝我想你?”-Yo一下
“我开完会了,来我办公室”-Yo一下
“你起床了吗?”-Yo一下。
无穷无尽的可能性。
我们不要你的电子邮件、Facebook,没有搜索,什么都没有。只有Yo。
打开应用程序,轻按Yo就好了。
就是那么简单。Yo。"
IFTTT增加了一个触发器,当你“Yo” IFTTT的时候去执行某个操作,比如Yo一下打开或关闭电灯,Yo一下自动给自己打一个电话(可能在想逃离某个场合的时候比较有用),比如Yo一下将实时的天气发过来。当然还有更多可能性的,Yo一下,就这么简单。
IFTTT还有好多好玩的Recipes可以去探索:https://ifttt.com/recipes
IFTTT还可以变得更加强大,目前仅支持if this then that
这种模式,关于“this”,可以支持 and
和or
,也就是说多个条件同时成立的时候去触发某个Action,可以支持not
,当某个条件不成立的时候触发某个Action,可以支持else
,这样trigger可以更加灵活。同样的,某个trigger可以支持多个Action
,当某个事情发生的时候,自动触发去做一系列的事情。此外,目前的channel是比较固定的,如果能开放api
,让开发者自己去实现trigger,自己去定义Action,IFTTT将会变得更加强大和有趣。
如果不清楚一致性哈希的原理,先移步这里: http://blog.codinglabs.org/articles/consistent-hashing.html
项目初期,为了节省成本,可能只用3台机器做缓存,通过一致性哈希方式将请求分摊到3台机器上面。后面随着业务量扩大,3台机器遇到性能瓶颈就需要扩容。假设我们扩容一倍到6台机器,如果直接将三个新节点加入一致性哈希环里面,就意味着一半的缓存会失效,这无疑会给后端服务造成巨大冲击。为了减小冲击,可以每次新增一个节点,第一次有25%的缓存失效,为了减小对后端的冲击可能需要在凌晨操作,过一段时间,等缓存新节点里面有足够多数据之后,再增加第二个节点,理想情况下第二次添加节点只有20%的缓存失效。同样的方式再增加第三个节点,16.7%的缓存失效。后面再新增节点失效的缓存会逐渐减少。
回顾一下上面的的扩容方式,有几个地方不太优雅:
有没有什么方式可能一次性扩容多个节点,并且缓存不失效呢?这个想法很美好,其实也是可以做到的。
对应原先三台机器A,B,C组成一个哈希环H1,三台新机器D,E,F和A,B,C组成一个新的哈希环H2,我们需要实现一个特殊的读逻辑:先去H2读,当H2不存在的时候去H1读,两个都不存在意味着缓存不存在。写操作全部在H2,一段时间后当D,E,F三台机器缓存被填充之后移除哈希环H1,只保留H2,也就是我们希望的扩容的最终结果。
再来分析一下上面扩容方式的优缺点
大部分情况下,读缓存的时间远小于读后端的时间,可以推测,两次读缓存的时间也是远小于读后端的时间的,这种情况下读两次带来的额外开销可以忽略。
新的扩容方式还有优化的空间,当在H2读不到的时候,会再去H1读,因为我们H2集群里面已经包含了H1集群,如果在H2读的是A,B,C三台机器,那么就不需要再去H1集群读,因为即使去H1读,也是同一台机器,结果是一样的。优化之后每台机器的读写请求数不会比扩容前高。
对于简单哈希,也可以使用上面的方法进行优雅扩容。
为了不让树莓派继续在家吃灰,周末又拿出来折腾了一下。apt-get升了个级,300M+的更新之后竟然没挂!可以继续折腾。。
家里电信宽带,路由器admin密码有待破解,在外网如何方便控制家里的树莓派成了一个问题。
目前有个小vps,可以作为中继连接树莓派,有几种方案选择:
这个很简单,vps上搭建一个vpn,然后树莓派和控制端都连上vpn,那么树莓派和控制端就在同一个局域网内部了,可以直接ssh连接的。
网上找到这样的方法连vpn:sudo pptpsetup --create vpnname --server ip --username test --password test --encrypt --start
,但是树莓派一执行这个命令就断网,原因未知,只能重启。
原理也很简单,假设vps地址是1.1.1.1
,树莓派通过ssh连接到vps,同时将vps上某个端口比如1234
映射到树莓派的ssh端口比如22
,这样在vps上访问1234
端口就相当于访问树莓派的22
端口,命令很简单:
$ ssh -f -N -R 2222:localhost:22 [email protected]
上面那个命令会将vps上的2222端口映射到树莓派的22端口(ps:你也可以映射你感兴趣的其他端口^_^):
# lsof -i :2222
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sshd 16893 username 10u IPv6 238723171 0t0 TCP localhost:2222 (LISTEN)
sshd 16893 username 11u IPv4 238723172 0t0 TCP localhost.localdomain:2222 (LISTEN)
#
# telnet localhost 2222
Trying ::1...
Connected to localhost.localdomain.
Escape character is '^]'.
SSH-2.0-OpenSSH_6.0p1 Debian-4+deb7u2
^]
telnet>
看起来这样就OK了,但是,如果树莓派的ssh因为某种原因断开了,我们就无法再控制了,因此我们需要让ssh断开之后自动重连,autossh
是一个好选择,在树莓派开启ssh隧道需要这样做:
我们需要autossh自动连接,就需要让树莓派可以免密码ssh登陆vps,这样重连的时候可以自动连上:
$ ssh-copy-id [email protected]
$ autossh -M 5678 -fNR 2222:localhost:22 [email protected]
假设上面的操作我们是在树莓派上用rpi
这个账户
$ cat /etc/rc.local | grep autossh
su rpi -c "autossh -M 5678 -fNR 2222:localhost:22 [email protected]" &
这样树莓派只要开机就自动连接vps,我们就可以方便地控制和重启了。
上面提到,在vps上,2222
这个端口实际上是在127.0.0.1
上监听的,也就是说在外网是没法直接ssh的,我们可以通过以下方式来解决
ssh登上vps,然后再ssh -p 2222 rpi@localhost
登陆树莓派
可以用iptables
做端口映射,例如将vps的23端口映射到vps的2222端口
# iptables -t nat -A PREROUTING -p tcp --dport 23 -j REDIRECT --to-ports 2222
需要注意的是需要打开ip_forward
功能:
# echo '1' > /proc/sys/net/ipv4/ip_forward
修改客户端的~/.ssh/config
如下
$ cat ~/.ssh/config
Host vps
Hostname 1.1.1.1
User username
Host 127.0.0.1 rpi
Hostname localhost
Port 2222
User rpi
ProxyCommand ssh vps netcat -q 600 %h %p 2> /dev/null
保存之后首先测试 ssh vps
可以正常登陆,然后确认vps上有安装netcat
,接下来就可以ssh rpi
来登陆树莓派了,如果ssh-copy-id
之后是可以一键直接登陆的,还可以用scp
来拷贝文件,非常方便。
接下来就是看看在树莓派上折腾点东西玩~
ps: 贴张树莓派的照片
先上链接:
https://ifttt.com/products/do/button
https://ifttt.com/products/do/note
https://ifttt.com/products/do/camera
Do由三个独立的APP组成:Do Button
,Do Note
,Do Camera
。
传统的 IFTTT
,If This Then That
中的 This
条件是由已有的Channel
自动触发的,Do
最大的创新之处是This
条件由用户主动去触发,触发也超级简单,Do Button
只需要按一个按钮就好了,Do Note
只需要输入一些文字,Do Camera
只需要拍一张照片。当用户点击Do
之后,Do
会记录你点击的时间、地理坐标,输入的文字,拍摄的照片等信息,这些都可以被加进That
中去。
看看大家怎么用这三个工具:
每个Do工具最多只能设置三个Recipes
,这样也好,切换起来非常方便,最常用的功能也不过两三个。
我现在最常用的是Do Note
:
一般HTTP请求或响应包含Header
和Body
,如果有些信息是在Body发完才知道,比如Body的校验、数字签名、后期处理结果等希望在同一个请求里面延后发送,就需要用到Trailer
。
一个带Trailer
的响应例子:
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
Trailer: Expires
7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n
\r\n
使用Trailer
有几个注意事项:
Header
里面的Transfer-Encoding
必须是chunked
,也就是说不能指定Content-Length
。Trailer
的字段名字必须在 Header
里面提前声明,比如上面的Trailer: Expires
。Trailer
在Body
发完之后再发,格式和Header
类似。用Go实现一个HTTP客户端,对所发的Body
计算MD5
并通过Trailer
传给服务端。
服务端收到请求并对Body进行校验。
package main
import (
"crypto/md5"
"fmt"
"io/ioutil"
"log"
"net/http"
)
func index(w http.ResponseWriter, r *http.Request) {
fmt.Printf("header: %+v\n", r.Header)
fmt.Printf("trailer before read body: %+v\n", r.Trailer)
data, err := ioutil.ReadAll(r.Body)
bodyMd5 := fmt.Sprintf("%x", md5.Sum(data))
fmt.Printf("body: %v,body md5: %v, err: %v\n", string(data), bodyMd5, err)
fmt.Printf("trailer after read body: %+v\n", r.Trailer)
if r.Trailer.Get("md5") != bodyMd5 {
panic("body md5 not equal")
}
}
func main() {
http.HandleFunc("/", index)
log.Fatal(http.ListenAndServe(":1235", nil))
}
package main
import (
"crypto/md5"
"fmt"
"hash"
"io"
"net/http"
"os"
"strconv"
"strings"
)
type headerReader struct {
reader io.Reader
md5 hash.Hash
header http.Header
}
func (r *headerReader) Read(p []byte) (n int, err error) {
n, err = r.reader.Read(p)
if n > 0 {
r.md5.Write(p[:n])
}
if err == io.EOF {
r.header.Set("md5", fmt.Sprintf("%x", r.md5.Sum(nil)))
}
return
}
func main() {
h := &headerReader{
reader: strings.NewReader("body"),
md5: md5.New(),
header: http.Header{"md5": nil, "size": []string{strconv.Itoa(len("body"))}},
}
req, err := http.NewRequest("POST", "http://localhost:1235", h)
if err != nil {
panic(err)
}
req.ContentLength = -1
req.Trailer = h.header
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
fmt.Println(resp.Status)
_, err = io.Copy(os.Stdout, resp.Body)
if err != nil {
panic(err)
}
}
$ go run server.go
header: map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
trailer before read body: map[Md5:[] Size:[]]
body: body,body md5: 841a2d689ad86bd1611447453c22c6fc, err: <nil>
trailer after read body: map[Md5:[841a2d689ad86bd1611447453c22c6fc] Size:[4]]
$ go run client.go
200 OK
nc
来看服务端收到的请求$ nc -l 1235
POST / HTTP/1.1
Host: localhost:1235
User-Agent: Go-http-client/1.1
Transfer-Encoding: chunked
Trailer: Md5,Size
Accept-Encoding: gzip
4
body
0
Md5: 841a2d689ad86bd1611447453c22c6fc
size: 4
可以看到服务端在读完body之前只能知道有Md5
这个Trailer
,值为空;读完body之后,能正常拿到Trailer
的Md5
值。
Go语言使用Trailer
也有几个注意事项:
req.ContentLength
必须设置为0
或者-1
,这样body
才会以chunked
的形式传输。req.Trailer
需要在发请求之前声明所有的key字段,在body发完之后设置相应的value,如果客户端提前知道Trailer
的值的话也可以提前设置,比如上面例子里面的size
字段。body
之后Trailer
不允许再更改,否则可能会因为map并发读写,导致程序panic,同样的道理服务端在读body
的时候也不应该对Trailer
有引用。body
之后才能知道Trailer
的值。A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.