Golang 在十二赞的深度应用

作者为黄滚,十二赞小程序开店平台创始人。2007年至2016年在阿里巴巴任职,曾担任中国雅虎平台技术主管、淘宝搜索研发专家。

我们是“十二赞”,一个致力于帮助电商卖家进入小程序的小团队,我们的主页是http://www.12zan.cn/。在实际运行中,我们使用了大量由golang写就的小工具,几乎每一个工具代码量都超短,一般在200行左右就完成了一个独立的功能,同时担当了相当重要的角色;像代理服务器,代码量一共500行多一点点,却是我们的核心支柱,压测时QPS也直追nginx,表现优异。

基于Docker的基础结构

做为基础架构,我介绍一下我们的机器架构。
我们的整个业务构建于阿里云之上,有5台server,每一对都有独立的外网IP,同时也在同一个内网之中。在每一台机器上都跑了一个我们自己用golang写的守护进程,这个进程负责监听一些业务重启、新增域名等类似的指令并执行(这些指令最后都传递给了docker)。同时,每台机器上都有一个consul进程,这些consul都join到了一起。另外,我们每一台机器上,都用docker跑了一个nginx来做80端口的服务,同时跑了一个用golang自己写的HTTP代理。nginx做做日志啊基础的功能之后就把请求丢给这个http代理 ,HTTP代理会到consul里去查应该将请求转发到哪个IP的哪个端口上。

400行Golang代码写的HTTP Proxy

在架构选型的第一天,我们就决定,我们会服务化,会大量使用http 接口来提供服务,并使用自己的http proxy来分发请求、添加自有的一些业务逻辑比如API的权限验证等逻辑。我们限定,所有业务,域名都是*.app.12zan.net,比如我们要上一个聊天服务,请求的接口就会是chat.app.12zan.net,哪天再上个评论服务,请求的接口就是comment.app.12zan.net。
确定域名后我第一件事情,就是拿golang自己写了一个非常简单的基于consul的http proxy server;感谢Golang这完善的HTTP库,我们只用了几百行代码就完成了所有功能。每当有http请求过来时,这个proxy server就会根据HTTP请求中HTTP_HOST 字段去consul去查,有哪些后端是用这个域名名称来注册服务的,并根据指定的算法,取出一台后端来,把这个HTTP请求Proxy过去。每个具体的业务,可能运行在我们5台机器中的任何一台之中的docker上,也可能是多个docker实例上。所以这里有一个机制,选择哪个实际的docker实例来服务这个请求的问题。我们现在支持随机选取、按客户端IP地址做hash之后选取、按URL做Hash选取、按负载选取几种方式。

WEB服务的服务注册

基于php+laralel和nodejs+koajs两种场景,我们制作了自己的docker镜像。这个镜像除开可以将php+nginx和nodejs构建的web服务运行起来之外,还包含一个golang写的consul客户端。在docker容器里,这个客户端随着php+nginx或是nodejs的web服务一起启动,启动之后会向宿主机的consul 进程注册自己这个服务,注册的时候会通知说,某某应用,在某某IP某某端口提供服务啦,如果前面有到**.app.12zan.net的请求你可以转发给我;同时会每隔一秒上报自己的进程数、当前机器CPU占用、内存占用情况。也是一样的简单,几百行golang代码,就鼓捣出了这个consul客户端。为什么使用golang呢?第一个原因当然是因为consul天生是golang阵营,第二个,是因为我们的docker容器种类较多,所以这个客户端直接就是在Mac上跨平台编译出来的在linux64平台上运行的,不管docker容器是python为基准的还是ruby为基准的,还是nodejs的,只要把这个二进制文件拷贝进去就能正确运行,不像别的语言需要解决依赖问题。

我们还开发了一个web console界面,在这里,我们可以注册app,也可以为app新增实例。注册app时,我们要指定代码仓库的地址(对了,我们的代码管理是用的golang写的gogs),指定对外服务的域名,指定是nodejs应用还是php+laravel应用。添加应用之后,可以在这个应用下新建实例,让系统在指定的IP上去跑这个实例。实例运行的过程实际就是下发一个通知到某个机器上,去执行一个docker实例启动的过程。docker启动的时候带了一些环境变量,比如当前内网IP、docker监听的端口、对外提供服务时是用何域名提供服务。

日志和存储

前面这种架构有一个问题,就是后端可能是在任何一台机器上运行的,今天可能是A,明天可能是B,那我要是把文件存在A上了是不是让B来提供服务的时候就挂掉了?所以我们想了这么一个办法(也是因为穷。。。。),我们把所有的文件都挪到阿里云的OSS服务上。同时为了不管是Nodejs应用还是php应用 还是python写的应用都能做到把用户上传的文件或是系统生成的文件存到oss上面,我们很省事地写了一个ossUploader,编译好的可执行文件发布,只需要执行它,传进来本地路径和oss上的目标路径,就保证给你上传到oss上去就完整,不需要再在nodejs、php、python、ruby、java各种平台下都琢磨一遍oss的SDK。

对日志的处理是一样的, 所有的日志文件的内容,都会被一个golang写的工具gtail监听着(就像linux 的tail -f命令一样),所有新产生的内容都会被gtail挪到oss上去存储。当然,也是可执行文件发布的。

这个实现之后, 我们的实际业务就真正可以在5台机器上之间任意腾挪了。

消息广播

得益于golang的一些开源仓库,我们还做了一些好玩的东西。
比如,看到https://github.com/gorilla/websocket这个东东,我们忍不住撸了一个websocket server,或是说叫群聊服务器更好一点。
接下来,我们看到有一个golang的库叫go-mysql-elasticsearch,伪装了一个mysql的slave,去MySQL的master机器上去读binlog,读到binlog以后就将MySQL里的数据发送给ElasticSearch去索引数据。
我们就结合了一个,把这两个结合起来,修改了一下go-mysql-elastichsearch,让它监听到MySQL的数据变更之后,在WebSocket server的某个群聊里推送出来,形成一个数据变更的广播。
再接下来我们就可以用nodejs写一个应用,连上这个websocket server,加入特定的某个群聊,就源源不断地收听到数据变更的消息。这个nodejs端的代码就非常简洁了,只需要不到100行代码可以做各种好玩的事情,比如监听到用户留言表有新增,可以发邮件让运营马上去审核。还有比如说,每当订单表有成交的时候,我们某个小小的nodejs应用因为监听了数据库消息,第一时间就知道了,马上就去追溯用户来源,来计算返利;同时这个nodejs的代码更新是和订单主逻辑完全不相关的,写这个业务的开发人员只需要知道订单表的结构,不需要了解订单应用后台代码。

发表评论