服务注册与服务发现

最近把公司所有的 RPC 服务都加上了服务注册及服务发现,多亏了各位同事的支持才能这么短时间就全部替换完毕并更新上线。

为什么做这件事情呢?公司里的业务服务都是微服务架构,每个 RPC 服务做的事情都很简单,随着业务的不断发展,RPC 服务也越来越多,目前已经有上百个 RPC 服务了。之前调用这些 RPC 服务的地方都是通过配置文件写死具体每个 RPC 服务所在的 ip 和端口,配置文件里充斥着各种服务的 ip 端口信息,想要动态扩容服务机器或者做故障转移也很不方便。而部署 RPC 服务的时候还得到 WIKI 页面上登记该服务所在的 ip 和占用的端口号,调用方也需要到这个页面来查找服务部署信息,对我这种懒人来说也是无法忍受的。为了解决这些问题,决定加上服务注册发现。

服务注册发现有非常多的开源解决方案,作为一个懒人,有现成能用的当然就捡现成的用了。问题是这么多解决方案,怎么挑呢?当时候选列表有 servicex(公司同事作品)、 dubbo(阿里巴巴)、curator(Netflix)、consul(HashiCorp)、etcd(CoreOS),最终我选择了 consul。至于选择 consul 的原因,很简单:设计合理、使用方便、维护轻松、可用性高、符合我的审美 :D

ServiceX: 公司同事个人作品,是他用了几天时间开发的,时间仓促设计不合理,而且公司人手紧缺后续也肯定没有时间精力继续维护,这个首先就被我排除在考虑范围了。

dubbo: 阿里巴巴开源作品,Java API,服务需使用 API 自主注册。github 上该项目还挺多人关注,但注意到该项目最后发布时间是 2014年10月,距今已有一年半时间了,新版本似乎也无疾而终,这个也被我排除掉了。

curator: Netflix 开源作品,目前归到 Apache 基金会下,使用 zookeeper 存储服务信息,Java API,服务需使用 API 自主注册,设计理念及使用方式不符合个人审美,淘汰。

consul: HashiCorp 开源作品,提供 RESTful API 和 DNS API,设计思想和 DNS 非常类似(其实服务注册发现本质上就是 DNS SRV 记录)。服务通过 consul 配置文件注册,无需在现有程序中加任何代码,当然也可利用 RESTful API 进行自主注册。服务只需和本机的 consul 通讯即可完成服务注册及服务发现,本机的 consul agent 会自行与 consul server 通讯以发现其他机器上注册的服务,consul 与 consul 之间通过 gossip 协议自动组成集群。

etcd: CoreOS 提供的 key/value 存储服务,可用于服务注册发现,未详细了解。

总的来说,consul 的设计比较合理,专注做一件事情并把这件事情做好了。

Nginx配置使用过期缓存

今天下午一朋友在群里问起

nginx有什么module可以达到这个效果:
1. 对指定uri做缓存,可配置有效时间
2. 失效时会继续返回当前缓存的页面,但会起个线程去读新的内容;并更新已缓存内容
3. 当2发生时,如果请求到的内容非200返回码,不更新缓存,继续等一个超时再去取

另一朋友提议说还是自己写一个吧,第三条需求应该没有能满足的。但一看这问题,全都是很通用的使用场景啊,nginx肯定有现成的模块了吧。翻查了下 ngx_http_proxy_module 的文档,找到

语法: proxy_cache_use_stale error | timeout | invalid_header | updating | http_500 | http_502 | http_503 | http_504 | http_404 | off ...;  
默认值: proxy_cache_use_stale off;

看这描述完全符合这朋友的需求嘛,为何他却没发现呢?打探了一下才发现,原来他们误以为proxy_cache_use_stale只能设置一个值,文档上多个值之间是用‘|’隔开的,而且该指令的默认值是说明上只显示了一个off,让他们误解成这指令只能设置一个值了。

其实该指令说明上还有一句,这条指令的参数与proxy_next_upstream指令的参数相同,如果认真去查看指令proxy_next_upstream的说明也应该能发现是支持多个值的。不过这文档确实也还可以写得更清晰详细一些,避免粗心的人误解。

另外就是,千万不要总想着自己遇到的问题是特殊的,自己能遇到的问题绝大多数情况下都已经有前人解决过了,多查文档多用搜索,绝对比自己又重新发明一次轮子省时省事而且安全可靠多了。

搭建Redis存储集群

以前都是将redis当做纯粹的cache server使用,关掉snapshot和aof,也不需要slave。其中任何一台挂掉也无所谓,反正可以从db中再加载回来。

有个项目DBA抱怨了很久,每天频繁往db插入太多数据,而这些数据90%以上都是只需要存储几天就过期,哪怕丢失部分数据也是可以接受的。综合这些条件,这些会过期的数据用redis来做临时存储应该是比较适合的。打开redis的aof,能够尽量保证数据持久存储下来。安全起见,给master配2个slave,再起3个 sentinel 做服务监控和故障转移,这样发生故障也尽量不需要人工介入。

需要注意的是,redis是单进程的,而且slave初次和master同步时,会强制master做一次snapshot。为了充分利用机器cpu资源并减少snapshot的开销,应该在一台机器上起多个redis实例,每个redis实例占用内存不应太大。为了充分利用内存,修改 /etc/sysctl.conf,增加配置项 vm.overcommit_memory=1

redis 2.8之前,replication不支持 Partial resynchronization,网络瞬断会导致强制重新同步,造成不必要的开销,影响服务稳定性。故请务必使用redis 2.8以上版本。

需要注意的是,sentinel严重依赖系统时间,一旦系统时间被非预期的方式修改,或者系统非常繁忙,又或者进程因为某些原因而被阻塞时,sentinel可能会工作不正常。sentinel检测到系统不正常后,将切换到 TILT 模式,只监控而不做其他操作(故障转移等)。

Master参考配置:

# ${port} 表示实际的端口号
# 复制N份redis.conf到/etc/redis/redis_${port}.conf
# 注意:不能共用同一份配置文件,会被redis sentinel重写
# 需要修改的配置如下(除提到的修改项外其余保持默认配置不变):

port ${port}

# /var/run/redis/${port}/ 目录必须已经存在
dir /var/run/redis/${port}/

pidfile redis.pid
logfile redis.log

# 开启aof文件记录,启动后再手工关闭aof
appendonly yes
appendfilename redis.aof

# 虽然关掉了snapshot,但是slave初次同步时会强制master snapshot
# 请留够内存用于fork snapshot,建议留40%机器内存
# 避免多个slave同时SYNC
dbfilename redis.rdb

# 关闭rdb snapshot
# save 900 1
# save 300 10
# save 60 10000

# 后台运行
daemonize yes

# 每个redis 4gb
maxmemory 4294967296
maxmemory-policy volatile-ttl

Master启动命令:

./redis-server /etc/redis/redis_${port}.conf
# 若是故障后重启,不需要关闭aof,而是在同步完成后关闭新master的aof
./redis-cli -p ${port} config set appendonly no

Slave参考配置:

# 略,基本同Master配置,只在最后加多一行
slaveof $master_ip $master_port  

Slave启动命令:

./redis-server /etc/redis/redis_${port}.conf

Sentinel参考配置:

# ${port} 表示实际的端口号
port ${port}
# /var/run/redis/${port}/ 目录必须已经存在
dir /var/run/redis/${port}/

pidfile redis.pid
logfile redis.log

# 后台运行
daemonize yes

#start config for ${master1_name}
sentinel monitor ${master1_name} ${master1_ip} ${master1_port} 2
sentinel down-after-milliseconds ${master1_name} 5000
sentinel failover-timeout ${master1_name} 900000
sentinel parallel-syncs ${master1_name} 1

# 触发报警脚本,${monitor1_path}是报警脚本的路径
# 调用脚本时传递两个参数:${event_type}, ${event_description}
#sentinel notification-script ${master1_name} ${monitor1_path}
#end config for ${master1_name}

# 剩余master的配置参考master1的配置项进行补充

Sentinel启动命令:

./redis-server /etc/redis/sentinel_${port}.conf --sentinel

在Ivy中引用Maven管理依赖的lib

若只需要该lib本身而不引入该lib的任何依赖, 在dependency的属性中设置

transitive="false"  

若需要该lib和该lib所必须的依赖关系而不引入optional的依赖, 在dependency的属性中设置

conf="${yourconf}->default"  

Maven中的scope都被Ivy转换成对应的conf,可以对应着修改设置。

参考资料:
https://ant.apache.org/ivy/history/latest-milestone/ivyfile/dependency.html
https://www.symphonious.net/2010/01/25/using-ivy-for-dependency-management/

Mou总在最前窗口

最近发现打开Mou后一直占据着最前窗口,点击其他程序都无法切换窗口,而将Mou最小化后就再也无法切换回Mou了。

为这个问题烦恼几天后终于受不了,Google “Mou 总在最前”后找到V2EX上的一篇文章,看症状与我的几乎一模一样。遂试之,症状消失,感谢楼主!

解决方法如下:

View->Toggle Floating

看来是不小心在用Mou编辑文档时,按下了其他程序的快捷键 - -#

Java RPC工具eurpc 0.2.0更新日志

  1. 将I/O逻辑从RpcClient中抽取出来到RpcConnection中,得益于此,使得同一个client多次方法调用间可以使用不同的connection(连接池实现起来就更自然了)
  2. 一些类名称做了修改,含义更清晰。SimpleSerializer重命名为JDKObjectSerializer,SimpleRpcServer重命名为BIORpcServer
  3. JDKObjectSerializer的网络传输方式也增加了length field header,终于和其他Serializer的格式一致(NettyRpcServer/NettyRpcConnection也可以使用JDKObjectSerializer了)
  4. 增加了Logger接口定义以及一些常用日志组件的LoggerAdapter,并且提供了日志组件自动检测机制(LoggerHolder类,检测顺序:slf4j–>commons-logging–>log4j–>java.util.Logger)

JS条件编译

最近给公司开发新邮件提醒的浏览器扩展时,需要分别开发搜狗和Chrome版本,而这两个版本绝大部分代码都是相同的,只有少数代码的区别。如果因为这么几行代码的不同而需要分拆到多个js文件中就会存在重复代码,后续有修改时就需要修改多处,维护起来不太方便。

在C语言里,可以根据不同的平台编译出不同的二进制代码,那么是否也可以对JS做这样的处理呢?答案是肯定的,google后发现有两个项目可以处理这个事情,一个是js_build_tools,另一个是js-prepross。试用js_build_tools时发现这个东西只能对单个文件进行处理,不支持fileset,且输入文件不能和输出文件同名,使用起来不太方便。而js-preprocess支持fileset,输入文件可以和输出文件同名。相比js_build_tools,js-preprocess使用较为方便,但也存在一些不足。js-preprocess只支持相对于项目的相对路径,ant的taskname只能命名为preprocess。js_build_tools里的编译指令相比C的编译指令多了注释符号//,对js IDE相对友好。

花了一点时间对js-preprocess做了一些修改,fork的项目地址->传送门。fork后支持绝对路径,ant的taskname没有限制,编译指令也改成js_build_tools的方式。

setSoTimeout does not work with nio SocketChannel

通过SocketChannel.socket().setSoTimeout(timeout)设置读超时,对于SocketChannel.read(buffer)操作来说是不会有任何效果的,如果SocketChannel设置了blocking mode的话会一致阻塞直到有可读取的内容或EOF。

有人就此给Sun提了个bug(http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4614802), 但Sun不认为这是个bug:

Not a bug.  The read methods in SocketChannel (and DatagramChannel) do not
support timeouts.  If you need the timeout functionality then use the read
methods of the associated Socket (or DatagramSocket) object.

既然Sun不认为这是个bug只能自己使用的时候注意了(有人说起码是个Javadoc的bug, absolutely!)

网易新邮件提醒Chrome扩展开发

最近突然对chrome扩展开发来了兴趣,刚好最近了解了下网易邮箱助手获取新邮件到达的方法就想着自己动手写一个新邮件到达提醒的chrome扩展(其实挺蛋疼的,网易邮箱网页版本身就提供了提醒功能)。协议可以通过抓包来了解,也比较简单易懂。

周六睡醒就花了几个小时一边看chrome扩展的开发文档一边码代码,花了几个小时就把第一版弄出来了,chrome的扩展开发还是挺方便的。

一般来说,extensions都会有一个背景页面(background_page)用于主流程处理,逻辑代码通常都在这里。除了流程处理外一般还会有extension的设置需要处理,那么一般也会提供一个选项页面(options_page)。extension各个页面之间可以通过chrome.extension的api来进行通讯,比如可以通过chrome.extension.getBackgroudPage()获得背景页面的DOM树。API都相对比较简单,用的时候翻翻手册就很容易明白了(PS:360翻译的质量真不敢恭维)。

项目代码托管在google code上,有兴趣的可以自行查看

TODO:
1. 目前版本账号密码都是明文保存的,可以改成md5处理。
2. 点击进入邮箱查看邮件。
3. 5秒后自动关闭弹窗。
4. 自动更新。
5. 多账号支持。
6. 异常情况处理。

[RT] Using stunnel to telnet into GMail IMAP

PS: 原文被墙, 转来方便墙内翻阅。 最近在搞IMAP相关的东西, 一直头疼不知道怎么命令行下测gmail的一些行为, 这篇文章真是帮大忙了。

By edwin - Posted on 12 February 2009

Here is a case study of how stunnel can be used to test an SSL based protocol. We will create an stunnel configuration that reroutes the IMAP port (TCP 143) to the Secure IMAP port (TCP 993) on GMail's IMAP server (imap.gmail.com). We will than test the setup by using telnet.

I will be using Ubuntu 8.10 (Intrepid Ibex).

First, let's install stunnel.

sudo apt-get install stunnel

Edit /etc/default/stunnel4, change ENABLED=0 to ENABLED=1

Edit /etc/stunnel/stunnel.conf as shown in the example below:

; Sample stunnel configuration file by Michal Trojnara 2002-2006
; Some options used here may not be adequate for your particular configuration
; Please make sure you understand them (especially the effect of chroot jail)

; Certificate/key is needed in server mode and optional in client mode
;cert = /etc/stunnel/mail.pem
;key = /etc/stunnel/mail.pem

; Protocol version (all, SSLv2, SSLv3, TLSv1)
sslVersion = SSLv3

; Some security enhancements for UNIX systems - comment them out on Win32
chroot = /var/lib/stunnel4/
setuid = stunnel4
setgid = stunnel4
; PID is created inside chroot jail
pid = /stunnel4.pid

; Some performance tunings
socket = l:TCP_NODELAY=1
socket = r:TCP_NODELAY=1
;compression = rle

; Workaround for Eudora bug
;options = DONT_INSERT_EMPTY_FRAGMENTS

; Authentication stuff
;verify = 2
; Don't forget to c_rehash CApath
; CApath is located inside chroot jail
;CApath = /certs
; It's often easier to use CAfile
;CAfile = /etc/stunnel/certs.pem
; Don't forget to c_rehash CRLpath
; CRLpath is located inside chroot jail
;CRLpath = /crls
; Alternatively you can use CRLfile
;CRLfile = /etc/stunnel/crls.pem

; Some debugging stuff useful for troubleshooting
debug = 7
output = /var/log/stunnel4/stunnel.log

; Use it for client mode
client = yes

; Service-level configuration

;[pop3s]
;accept = 995
;connect = 110

[imaps]
accept = 143
connect = imap.gmail.com:993

;[ssmtp]
;accept = 465
;connect = 25

;[https]
;accept = 443
;connect = 80
;TIMEOUTclose = 0

; vim:ft=dosini

Start up Stunnel

sudo /etc/init.d/stunnel4 start

Verify that the IMAP is listening on the local server.

netstat -an | grep -iw LISTEN
tcp 0 0 0.0.0.0:143 0.0.0.0:* LISTEN

The following requires that your GMail account have IMAP enabled. This is not enabled by default. Replace username@gmail.com with your real email address. Replace password with your real password.

telnet localhost 143
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
* OK Gimap ready for requests from 71.65.199.7 c5if2789008nfi.67
)
01 LOGIN username@gmail.com password
01 OK username@gmail.com authenticated (Success)
02 LOGOUT
* BYE LOGOUT Requested
02 OK 73 good day (Success)
Connection closed by foreign host.

That's it. If you're feeling adventourous you can use Hydra to brute force an account you own.

./hydra -l yourfriend@gmail.com -P password.txt -V localhost imap