Project Resource Board: a scalable webapp with Flask socketio and Vue.js
关于
我在做一个叫做 resource board的 side project, 它是一个方便团队协作中使用共享资源的web app。
本文是我在写第一个发布版本之前持续更新的记录。
updated in
2019-April-6
以单体架构先实现了可用的版本,部署在公司内网给项目使用上了,也公布一下代码:
https://github.com/wey-gu/resource-board
之后我会花时间迭代下去,不过现在课余时间在做一个机器学习调优的项目,这个优先级只能降低。
缘起
为什么开始
在云计算研发部门搬砖半年,一直做一些很小的feature开发,连着几个月早出晚归加上牺牲周末时间的赶进度让我内心很不平静。我需要做点什么,因为本质上我热爱的是 build things
,做一个proper side project
的主意在脑子里转了好久了,我决定先做一个简单的东西出来。
这个东西就是一个 resource board
。
学到什么
resourece borard
是我起的名字,其实就是一份在线的表格、面板,本质上用 O365
/ onedrive for business
完全可以实现类似的功效。不过我还是想造这样一个轮子,涉及一下产品开发的全生命周期的从无到有的创造、学习、实践:
- planning, prototyping
- architecture
- front end
- back end
- CI/ CD
- maintenance
- refactoring
- for test
- for scale
- tuning
User story of resource board
resource board
主要解决异地团队协作中对共享资源使用的问题,它是一个电子化的资源使用白板。
试想一下没有资源占用白板的时候,团队里的人共享10个虚拟化数据中心,我们简称 DC
,对于它的使用分几个场景:
- CI
- 被CI占用,没有再触发CI调用它的人不可以干预
- testing
- 被非独占式的人使用中,特定的人可以并行使用它,(理想情况下)不会干扰到彼此
- occupied
- 被人独占使用,不可与别人共享
- free
- 任何人可以使用它,或者挪作其他用途
这种情况下,使用它的A同学需要站起来告诉所有人,或者群发邮件,说我要用 DC123
独占调试一个代码,有人在用么? 10分钟后,在注意到没人回复的情况下,ta就在 DC123
上开始做坏事 调试ta的代码了。然而刚从厕所回来的B同学,却发现ta的环境被破坏了,半天白忙活了😢。
如果这个资源共享的工作方式之中引入了记录资源占用情况的白板,A同学只需要看一下白板上空闲的DC,发现DC125
可以使用,他可以做到:
- 无需等待确认广播信息的回复立刻使用
DC125
- 不会破坏任何被占用的资源环境比如
DC123
看起来比较自然。
Planning
关键点
- 数字化白板应该是一个Wep App
- 它需要是实时的
- 支持面向连接的(server —> client)
- 可扩展,可扩容
- 后端
- 只做api
- 分布式,可scale up
- 前端
- single page client
- websocket
- 后端
简单原型
我用Balsamiq Mockups 简单做了一个原型:
- 用卡片表示每一个资源的信息
- 不同的section表示资源被使用的状态,或者分类
- 提供面向资源的历史查看页面
我没有花更多时间去画更详细的原型图,只是在画这个初始的版本的过程中引导我思考我初始的数据schema,前后端最开始要做的事情,一旦雏形确定下来,我就直接进入前后端的开发,在开发中直接实现业务逻辑的进化。
这里是我截取的原型图的demo动画 😁
前端、设计
基础架子
因为我不是一个JS guy、UI design guy,我选择了适合轻量级快速实现的方案: vue.js + material design。
一年两年之前,我用:
做了一个遗留系统改造的Prove of Concept (inno-search),那个过程我尝到了设计甜头,对于我这个没有任何设计经验的人,只是使用几个基本的设计 principle 就做出来明显没有设计缺陷的东西真的很有成就感,我甚至做了一个推广视频来在公司内部电视上播放 ( promo video here: https://vimeo.com/219328622 )。
扯远了,这一次我还想使用 material design 的设计元素,来垒砌这个简单的board。
因为想尝试一下响应式的前端的开发,我选择 Vue.js 和 Vuetify.js 。
- 如果我继续丰富工具的feature,我会考虑用 vue matierial admin 堆砌更丰富的功能出来。
另外,element-ui 也是一个选择,也许我之后会用它做点什么 😝。
实时性需求
为了做到实时性,就是服务端推送实时的数据变化给连接的web客户端,我选择 Socket.IO,而不是client 去pulling,这样做的好处是:
- WebSocket的connection的延时(ms)远比pull 的interval(s) 小,能做到
真·实时
- 实时性要求高的pulling带来的后端的开销大于WebSocket连接
后端
还是因为我不是 一个JS guy,没有选择更native的node.js,再来,我最熟悉的是 python,于是也就没有尝鲜 golang(以后可以试试)。
好久之前我在翻pycon的视频时候,注意到Miguel老师写了 python socket-io 实现,还有和他亲自基于此的 Flask-socketio ,一直都想要研究来试试,所以我还是选择熟悉的Flask 😁。
消息队列
因为可扩展架构的需求(分布式数据库、可扩展后端):
- 后端的 server/service instance 在分布式的情况下需要支持对所有client连接的广播
- 考虑对数据库的写操作通过基于队列的异步call规避数据并行写入做到一致性
任务队列
还有两个个我没有提及的需求是:
- 外部 API 周期性调用
- 我们的board之中,resource应该可以通过周期性的call CI系统的API 获取它被CI调用的状态信息。这需要异步执行的周期性任务
- resource booking、scheduling
- 后续提供终端用户去预约资源未来使用的功能,需要background task去监测、维护事件的触发和相关数据的读写
终于找到机会研究使用一下 Celery 了。
生产环境部署
因为兴趣原因,选择容器化,实践一下k8s的部署。
测试
Test Driven Develop,做到尽可能高coverage的单元测试。
架构
我简单地使用keynote.app 创建了一下架构图草稿如下。
Note:
- 通过 Nginx作为load balancer,以及外部用户的http webSocket 的tls加密解密节点,也就是说tls terminated in loadbalancer。
- 数据库的读写操作透过message queue(MQ),这里它既是cache,又是写操作的queue,考虑使用Radis(rq)。
- 后端为计划任务单独做一个instance,board-scheduler:
- 用来处理resource 预约到期事件
- 周期地通过call Celery抓取 外部CI 系统的状态
- Celary instance处理 async call(非 cpu tense的)
- 抓取 外部系统CI状态
- DB call
- socketIO broadcast
Notes during coding
这里放一些我在实现过程中的一点点注意的点。
It’s not reactive?!
因为没有看完所有Vue.js的文档,就开始搞,没有意识到Vue.js对对象和数组内部变化的track是有限制的,所以做mutation的时候不可以直接假设嘴边更改都被响应式的反应到view之中,需要用Vue.set或者splice😁。
我写到中间发现View怎么都无法按照我的预期得到更新,debug 去print值也明明更改了(vuex mutation
按照预期执行了),有那么半个小时我的价值观崩坍了,各种Google都无果(大家不响应变化的原因都是实际上mutation没有执行),直到我重头看了Vue.js的文档才发现我犯了最蠢的错误….
ref:
Time zone
1 | npm install moment-timezone --save |
数据库中存储的时间戳是UTC时区的,在前端转换为浏览器中的local时区只需要用tz这方法
1 | moment.tz.(timeString, timezoneString) |
我的例子:
1 | moment.tz(timeString, 'UTC').local().format('MMMM Do YYYY, h:mm:ss a') |
Redis
在读这个小书,发现写的不错,有不会的概念去Google学清楚就好了。
ref: https://www.openmymind.net/2012/1/23/The-Little-Redis-Book
vue-socket.io
authentication with flask-socket.io
flask-login flask-socket.io vue-socket.io 登录鉴权的实现
虽然server端实现了简单地RESTful API
,用来为之后增加cli-client
方便,我依然决定不通过 http request
,使用flask-login
管理服务端状态,它把login信息记录在flask-socketio
的session
之中了。
注意: 需要给出 load_user
的定义
否则,会有如下异常:
1 | "No user_loader has been installed for this " |
实现的例子如下,另外还参考flask-scoket.io
官方文档糖一下 authenticated_only
这个装饰器,真香。
1 |
|
登录流程
vuex
之中记录一个登录状态以供前端反应到view之中,click
登录触发socket.io
调用,flask-socket.io
server接收登录信息,验证,通过socket.io
调用返回login response,vue-socket.io
在vuex之中的 action hook 触发变化,mutation
去更新login
状态,或者发送失败 alert。
Vue-socket.io
& Vuex
例子
1 | export default new Vuex.Store({ |
Post build hook for npm build
因为要替换一下Google字体cdn到国内friendly的地方,我修改了 package.json
中的 postbuild
这个hook,来做cdn 的FQDN替换。
1 | "scripts": { |
updated on 2021 Jan: 这个项目已经运行了快一整年了,因为一直稳定运行,也没有新的需求,就没抽出时间来迭代 :-P,这里放一个实际跑起来的 demo 视频:
And some screenshots here :-)
Board Portal Page:
Resource History Page:
Resource Edit Card: