Docker hackathon, teamspark 及团队协作软件设计上的思考

这个周末我参加了docker hackathon,一个旨在为docker 2015大会暖场的编程马拉松。大赛在旧金山Mission St.上的万豪举行,硕大的餐厅被临时改装成开发者的乐园。作为组织者而言,docker做的相当不错,除了WIFI偶尔会很慢甚至无响应外,其它都无可挑剔。参赛的项目要求和docker相关,现场各个团队的项目也基本和docker生态圈的工具有关。和自己感兴趣的两个项目的发起者聊了聊,感觉都是小公司的内部项目的某个部分拿出来做,团队已经亲密无间,我一个外人加进去有些尴尬;其他不少项目偏devops,暂时还不是我的菜。于是我干脆自己一个人重新考虑三年前用meteor做的一个pet project:teamspark。这个项目是个SPA(single page application),通过meteor提供的realtime/reactive cooperation的能力(在2012,这算是很新颖的技术,现在FRP漫天飞),用于团队间的实时协作。其中的主要一个功能是团队的任何人都可以提交新的任务(可以是bug,new feature request,点子等),并分配给其他相关的成员。被分配的成员只要打开了teamspark,可以收到实时的通知,双方可以围绕着这个任务实时聊天,协作完成,也可以随时把其他相关的人拉进来讨论。发起任务/完成任务/参与讨论等都会获得一定的积分,大家随时可以打开leaderboard,看实时的排名,游戏感挺强。总之,在teamspark里,一切交流都是实时的,有点slack的影子。

但是熟悉meteor的人应该知道,meteor的reactive代价不小:客户端的javascript subscribe到mongodb的的某个query,一旦该query的结果发生变化,则通过websocket通知所有subscriber。我不知道现在是否还是这样的逻辑,至少在0.5,应该是这么回事。这东西做出来后部署在一个大概是2G内存的阿里云服务器上,我的团队一直使用,十多个人的团队,积累了上千个topic后,用到后面客户端已经有一定程度的卡顿。

这次重新把teamspark拎出来审视,跟路书的小伙伴最近透露出来的对该项目的兴趣,以及今年以来slack如日中天的状态,有很大关系。hackathon之前想了一点,开始hackathon后,我又花了几个小时考虑设计。

XMPP or 私有协议

做一个实时团队协作软件,信息交互的高效,安全,功能完整是首先需要考虑的。就实时交流(说聊天是不是low了点 :p )而言,whatsapp用的是修改过的ejabberd,hipchat使用twisted自己实现了xmpp,slack虽然对xmpp有支持,但主要使用自己私有的json api。

(hipchat的tech stack,来源略旧,请google "hipchat high scalability" [1])

来源:http://www.quora.com/Whats-the-communication-protocol-used-by-Slack。slack的tech stack,见http://stackshare.io/slack/slack。

这三家都支持了 xmpp。xmpp是一个基于XML的开放即时通讯协议,如果你用过jabber,google chat,adium等聊天工具,它们都使用了xmpp。具体协议见 rfc3920(最新的是rfc6120)。应用比较广的支持xmpp的服务器是ejabber以及青出于蓝的mongooseIM(和mongodb的mongoose库没半毛钱关系),都是erlang上的实现,单机(16G内存)支持100k客户端没有问题;java上的实现是openfire,貌似口碑不如ejabber,而lua上也有一个轻量级的实现prosody。xmpp的客户端遍地都是,其中javascript的最为重要,比较火的是converse.js。

ejabber/mongooseIM 很好安装,在我的osx上照着文档(其实就是brew install一下),安装运行都很顺利,两个我都成功注册了两个用户,使用现有的adium和osx自带的iMessage进行聊天。

对于teamspark这样的团队协作软件而言,xmpp最大的优势是标准化:请相信IETF已经把即时通讯里面所有需要考虑的问题都考虑了(包括安全性);而ejabber/mongooseIM有商业上数百万用户级别应用的成功。

但一个协作软件的核心究竟是什么?是chat,还是围绕chat建立的沟通能力?在这一点上,hipchat和slack显然有不同的想法。slack既然认为自己的email killer,那么chat应该仅仅是其非常重要的一种能力,所以其定义自己的协议,然后以后对接xmpp是个不错的选择。

对于创业团队而言,一开始避免一些技术弯路很有必要。xmpp协议本身很重,需要花大量时间了解;ejabber是erlang撰写的,无论配置还是扩展,还是devops,懂erlang是必须的。(题外话:whatsapp一开始把宝押在ejabber上,是因为其创始人在此的累计:嫌BEAM性能不好,自己patch,更别提ejabber了)所以,使用自己熟悉的技术,先把chat的能力搭建起来,日后再补(或者不补)xmpp也是可以的。

嗯,所以对ejabber浅尝辄止后,我转向了定义teamspark自己的私有「协议」。毕竟,24小时的hackathon,等我搞明白ejabber的基本配置,可能就结束了,我不能这么颓废。

首先我要把teamspark的数据流缕一缕,分出control plane和data plane。

按照teamspark的功能,chat和notification是data plane,需要最高的效率,使用websocket承载。

其他信息流都是control plane,可以走慢速通道,用HTTP承载。比如成员A在任务B下上传一个文件,上传文件的动作使用HTTP API;"A上传了一个文件:xxx" 这个消息,用websocket传输。这样可以保证websocket上的通道上总是小数据,保持畅通。

HTTP/WS上面跑的数据,使用msgpack而非json封装,为什么使用msgpack,而非protobuf,thrift等,见这个repo,就不详述 [2]。考虑到这个应用以后有可能跑在手机上,在2G/3G下的带宽和流量都是需要考虑的事情,msgpack是一个比较折中的方案(必要时还可以启用HTTP协议自身支持的压缩功能)。定义二进制格式,使用TLV封装自然是最省流量的方式,但开发和调试的效率太低,扩展和升级也比较麻烦。

(如果从流量的角度考虑,xmpp也不是一个好的选择,XML的封装有效载荷估计不到三成 - 当然,你可以从产品的角度反驳:毕竟这样的产品的应用场景主要还是在WIFI下)

接下来就是稀里哗啦定义了一些消息的格式,比如说chat是什么格式,notification是什么格式,获取团队成员列表的API接受什么参数等等,都是枯燥和没太多技术含量的东西,不提也罢。

技术栈

接下来要考虑的是使用什么样的技术栈来实现。

Load balancer

如果不考虑AWS的stack(如果基于AWS构建,可以是ELB + Lambda + SQS + SNS + Elasticache + S3,整个是另外一个故事了),load balancer和reverse proxy都有nginx兼任。nginx自身的load balancer基本够用,不够的话还可以方便地通过lua扩展。

作为buffered proxy,nginx能够从容地应对慢速客户端对系统的「攻击」,这也是各种application server,尤其是blocking IO + multi threading(processing)的server,如gunicorn,极力强调production环境下,一定要放在像nginx这样的proxy之后的原因。

对于teamspark这样一个企业级的应用,TLS是必不可少的,nginx可以proxy HTTPS/WSS;应用层可以专注剩下的事情。

Application server

这个不多说,和选用的语言关系很大,但最好支持epoll(event driven)。

Application server里的一个难点是如何做websocket的pub/sub。比如说我在一个10个人关注(watch)的任务下发了条消息,如何快速转发给所有关注者的websocket,是个关键的技术问题。

想简单直观地做到这一点,要建立一个约束条件:一个团队下的所有成员的所有websocket连接不能跨进程。这个约束在teamspark产品范畴下是合理的,因为团队的规模不会特别大,slack/hipchat的使用者中,小于100人的团队是主流。因此,在产品上可以建立这样一个约束:一个团队的成员不能超过2000人。假设团队中每个人都browser/desktop app/mobile app各启动一个websocket连接,也就是6000 ws connections / team,分配到同一个进程处理没有问题。

这个约束进一步变成一个需求:load balancer需要把隶属同一个团队的连接分配到同一个服务器下的同一个进程。有两个方法让ningx知道team-id:

  1. HTTP GET URL
GET /ws/<team-id>/ HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com

Nginx可以通过URL做dispatch。在HTTP upgrade时,根据GET URL的team_id进行hash。

  1. 根据Auth token,我们之后讲。

建立好这样一个约束后,pub/sub实现起来就比较容易:

  1. 每个任务对应一个channel,建立channel和subscribers的对应关系:当某人关注某个任务,就将其添加到key为channel-id的hashmap中: channel-id → user-id。每个用户再建一个user-id → (presence-id, presence-websocket) 的hashmap。presence是这样一个概念:一个用户在手机登录,是一个presence,在browser登录,是另一个presence。这里可以建立另外一个约束:每个用户最多有10个presence(再多的话就是耍流氓了)。
  2. 当有人在channel发表内容,找出channel对应的所有presence-websockets,进行群发了。查找的过程是个O(1)的操作,群发是个O(N)的操作。
  3. 群发的过程一定要异步(或者使用coroutine)完成,防止某个websocket不能及时处理,而block住整个群发的过程。

这就是使用websocket需要付出的代价。这也是为什么control plane都使用无连接的HTTP,脱离了状态,也就解决了很多本会很头疼的问题。

(如果想移除这个约束,那么额外需要redis这样的第三方缓存记录 channel-id → (server-id server-scheme)

Cache server

目前没有想到什么非用cache server不可的地方,也许HTTP API读写数据库的过程可以cache起来。

Data persistence

这是个让人挠头的地方。team/user/channel等数据的创建,尤其是其关系的创建,需要满足ACID —— 毕竟做一个企业级的应用,要考虑到赚钱,既然要赚钱,有些事务性的操作,就要满足transaction的要求。所以production需要postgres(主要是license比MySQL更友好,其他见 [3])。这种SAAS软件,有些企业(一般都是金主)不敢用,他们想在自己的data center里部署所谓的 "on-premise server" 使用,到时一些严格的license,如GPL(尤其是v3)会挺折磨人。

基础的数据有了着落,聊天信息怎么办?可以存在本地的mmap的文件日志里,定期扔到S3,同时放一份到elasticsearch中,便于查询。

Search

hipchat/slack相对于microsoft lync的一大优势是无下限的全文搜索,啥都是秒搜。teamspark自然少不了搜索,前文提到过elasticsearch,基本上,我们需要把所有数据都扔到elasticsearch中;这样,数据库的查询功能大部分都可以迁移到ES中,数据库可以少建索引,对单个记录的读写多做优化。ES的原理就不在这里讲,请自行google。

使用ES的时候考虑每个team一个index,索引这个team里的所有数据。ES对索引的数量是没有限制的,无非就是更多的机器,更多的内存和更多的磁盘。

Message Queue

以上诸多事情,如果都在application server里完成,对performance/latency会有很大的影响。所以MQ不可或缺。我之前有文章讲过做一个应用,要重点考虑其event bus:内部的数据流是如何流入流出event bus的,需要几条bus,都承载什么样的数据?

MQ里跑的一个个数据具体是什么样子的,就不展开了,否则这文章无法结束。举一个小例子:

  1. A上传图片:arch.jpg 到 task X。这是一个HTTP file post请求。
  2. 上传完毕,服务器返回一个可以获取到图片的url。
  3. 服务器给MQ发送一条消息。
  4. MQ将消息分发给对此关心的index-task和photo-task。
  5. index-task对该图片取exif,然后索引到elasticsearch。
  6. photo-task对该图片截取正常尺寸,上传到S3;然后再截取各种其他尺寸,连同原图一起放在S3。

其他服务

手机端的push notification,还没有考虑,可能需要用AWS的SNS实现。

其他的没想到。

技术问题

Authentication

REST API / websocket(可能日后手机端还需要使用tcp/udp?)混搭的服务,authentication是个问题。websocket可以在upgrade之前,如果访问URI(/ws/<team-id>/)的用户没有登录,就redirect到登录页面,完成登录后,再回到 /ws/<team-id>/,完成websocket的升级。至此,这个连接只要不断(当然,服务器可以在满足一定条件下强行关闭这个连接),里面传输的数据都无需检查登录授权。由于外层采用了WSS,数据的安全性是没有问题的。

HTTP API比较麻烦,session based auth问题很多,CSRF,Session hajacking等安全问题都需要考虑;oauth或是类似的解决方案,每个API都需要进行token的验证,如果每次都hit db,是个很大的开销;引入cache server来做auth也比较麻烦。

还好,我们还有个新武器 - 上个月才成为 standard 的rfc7519:JSON Web Token (JWT)。和其他token技术不同,JWT使用的是stateless的token,服务器签发的token包含了所有验证用户身份的必要信息,而且服务器还可以还原出来。这样,服务器只要签发一次(包含超时时间),送给客户端,在超时时间内,客户端都可以使用这个token表明自己的身份。服务器使用的是hmac或者签名技术,生成的token满足:

  1. 客户端无法篡改
  2. 客户端无法生成(没有服务端的私钥或用于hmac的密钥)

加上TLS(https),token的机密性也能够保证。所以,当服务器收到一个自己能解得开的token,又没有超时,那么,可以认为token的拥有者是一个合法的用户。

这个技术乍听起来有些吓人,万一token被别人恶意获取了怎么办?TLS可以保证token无法在网络上被截获,但并不能保证攻击者入侵我的电脑(或者钓鱼,或者我自己二逼,主动散播),获取这个信息。不过这种场合基本上所有安全手段都会失效:被各大公司使用的oauth一样有这个问题。

JWT最大的优点是无状态,最适合HTTP API的上下文使用,很容易scale。

Scaling

到目前为止,所用技术scaling还都可以:

  1. authentication: stateless,很容易scale out
  2. nginx:很容易scale out
  3. application server:除了同一team的presence在同一个进程这个约束影响team内部的scaling,其他横向扩展很容易。
  4. elasticserach:基本就是在一个cluster里不断加机器。
  5. database:读写分离比较容易,日后可以按<team-id>对数据集进行sharding。

客户端

browser / desktop app 主要使用 javascript(clojurescript)。像这样的客户端,reactive非常重要,因此选一个好的reactive framework(或者能够很好支撑reactive的framework)很重要。对teamspark,我选择了 reactjs。我在不同场合表达过对reactjs的欣赏 —— 主要是对其思路上的革新的欣赏:控制状态,增加indirection(使用VDOM)。在控制状态这点,函数式语言具有天生优势,因此clojurescript上的reactjs的集成,其performance都比reactjs本身要好不少。看了一些解决方案,我打算使用 re-frame(reagent + re-com + re-frame)。

跨平台的桌面app基本上就是在nw.js(前身node-webkit)和eletron(前身atom-shell)中选择。两者都是基于webkit打造,技术上差别不大。我试验过node-webkit,这次打算试试eletron,毕竟github口碑在哪里。

结语

hackathon毕竟不是畅想大会,还是涉及实现。所以很多细节在这里并未考虑。我的app和docker唯一契合的地方是:我打算用docker registry里现成的elasticsearch container。挺囧的。把以上问题想清楚,还是花了不少时间的。hackathon 周六12点开始,我干到第二天早上4点支持不住,回家了。人老了,熬夜熬不住是一方面,主要是太冷,SF早晚简直就是个冰窖,穿着抓绒还抖。其实我周六晚上11点多就已经冻得不行了,可是我不想半夜走到停车场(为了省停车费,我的车停在了步行20分钟以外的地方,我不想自己大半夜的被黑又壮打劫了),所以只好一直熬到天亮。半夜我冻的瑟瑟发抖时有个哥们变戏法似的从箱子里取出一个床单裹在自己身上,我就羡慕得眼睛都红了。下次类似的活动,如果还想参加,我一定备好秋衣秋裤,以及毛毯。

(你见过4点的旧金山是什么样子么?:)

除去思考的时间,玩street fighter的时间,吃饭的时间,半夜的低效时间,前前后后写代码的时间也就是五个小时。写了六百多行clojure代码,主要是实现application server(http api和ws)的一部分。效率还挺低的。我原以为自己能起码写个能对话的app。先写UI好了,起码还是个东西。

周日(今天)在家基本就是各种打盹,脑袋疼得要死。突然觉得这么熬夜挺zuo的。


1. 链接:http://highscalability.com/blog/2014/1/6/how-hipchat-stores-and-indexes-billions-of-messages-using-el.html

2. https://github.com/thekvs/cpp-serializers

3. https://www.wikivs.com/wiki/MySQL_vs_PostgreSQL

原文发布于微信公众号 - 程序人生(programmer_life)

原文发表时间:2015-06-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java架构师历程

微服务在微信的架构实践

微服务的理念与腾讯一直倡导的“大系统小做”有很多相通之处,本文将分享微信后台架构的服务发现、通信机制、集群管理等基础能力与其上层服务划分原则、代码管理规则等。

6273
来自专栏python开发者

规范化的软件项目演进管理--从 Github 使用说起

规范化的软件项目演进管理 从 Github 使用说起 1   前言 首先,本文的层次定位是:很基本很基础的 Github 工具的入门级应用,写给入门级的用户看的...

2438
来自专栏Debian社区

Debian 成为主流 Linux 操作系统的七个原因

Debian也许是历史最悠久的发行版之一,但很显然,它仍可以教其他发行版好几招。要是没有Debian,Linux领域的境况会大不一样,会黯然失色好多。Debia...

712
来自专栏PPV课数据科学社区

【学习】百万级别数据,数据库Mysql,Mongodb,Hbase如何选择?

情况说明: 现在需要做一个数据存储,500w左右的数据,日后每天大约产生5w条左右的数据。想把这些数据存储起来,供日后的数据分析用?使用上面说的三种数据库中的哪...

4808
来自专栏FreeBuf

涨姿势:如何让你的Google账户更安全

如果你使用Gmail作为你主要的电子邮件,或者长期依赖于谷歌提供的服务,再或者你是“Google脑残粉”……那么这篇文章就值得你来读读。本文将指导你重新审视并重...

1958
来自专栏后端技术探索

电商平台搞秒杀背后的技术实现

每当电子商务平台搞活动,“秒杀”经常是提升网站活跃度的利器之一。比如活动日早上10点1元爱疯7秒杀7台,谁看到了估计都想去秒一把,万一秒中了呢。秒杀的典型特征就...

1043
来自专栏CreateAMind

从GITLAB误删除数据库想到的

酷 壳 – CoolShell http://coolshell.cn/articles/17680.html

902
来自专栏静晴轩

快应用之开发体验纪要

何谓「快应用」呢?它是基于手机硬件平台的新型应用形态,标准是由主流手机厂商组成的快应用联盟联合制定。其标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准...

1232
来自专栏CreateAMind

从GITLAB误删除数据库想到的

酷 壳 – CoolShell http://coolshell.cn/articles/17680.html

1754
来自专栏即时通讯技术

扫盲贴:认识MQTT通信协议

MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是IBM开发的一个即时通讯协议,有可能成为物联网的重要组成部...

3093

扫码关注云+社区