笨熊之家

欢迎来到笨熊之家~

0%

软件项目全流程实录 —— 数据平台的开发与维护

这次来聊一聊项目开发中的一些阶段,以自身经历来讲述,仅供参考。

前期准备与初步开发

这一阶段持续三个月,主要用于各个模块的需求分析和初步实现。

需求分析

首先是需求分析,公司代理了挺多游戏,之前数据统计都是采用第三方统计平台,最近接到通知,说是其中一个平台不日将关闭这一业务。于是打算自建一个数据平台,来应对逐渐增加的游戏数据和客制化需求。

数据来源是手机应用中集成的统计 SDK 实时上报的用户行为、广告及内购数据,服务端收集到原始数据后先存储,之后定期执行分析程序,计算出定义好的几十种指标,并由前端界面以图表形式展示,支持导出报表。

公司和这个平台有长期合作关系,所以后续得到了他们一部分的项目源码,我这边只是略读了一下 pom.xml 看看他们用了哪些依赖。

大致了解了一下,应该是业界大数据处理分析方面的成熟方案了,不过考虑到团队这边后端只有两个人,这套方案估计是没时间也没人手去实施了。

限定了使用 Python 和 MongoDB 之后,相关的方案也确定下来了,核心就是使用最简单的方案,容易上手,后期根据实际需求优化重构,毕竟小公司,说实在数据量不是很多,再加上需要快速上线,很多时候就是在各种条件中进行权衡。

  • 数据收集方面,服务端使用 Tornado,主要看重异步,并发性能。数据采集端包含 Android iOS JavaScript 三个平台,主要是根据平台特性收集相关的设备信息,用于区分和关联用户。
  • 数据库方面,MongoDB 直接存储 JSON 数据,数据处理模块根据各种指标的算法,构造出对应 MongoDB 语法的 pipeline 语句。
  • 前端方面初期是外包出去了,使用的技术是 jQuery BootstrapEcharts,主要是绘制图表展示计算出的若干数据指标。

数据指标熟悉与数据迁移

大概确定了技术方案,接下来就是熟悉相关的业务,准备迁移之前的游戏数据,做好兼容处理。

原先的数据平台实际挺成熟的,因此前期我们是直接沿用了那边的数据库表结构,这样最大化地兼容之前的数据,同时简化数据格式定义的过程。

花时间最多的是数据指标熟悉,几十上百个指标的定义,好在有相关说明文档,麻烦的主要是将这些指标的的算法翻译成 MongoDB 的 pipeline 语法,这样才能够将新的数据源计算出指标数值,然后写入 MySQL。

数据迁移方面,因为给到的服务器用户权限是受限制的,无法在服务器上直接执行脚本,而且登录需要通过跳板机,所以下载数据时还是折腾了一段时间。

各自开发

方案确定之后,就开始各自开发了,前后端分离,接口定义通过 Postman | The Collaboration Platform for API Development 共享,跟外包的前端在微信上联系,边开发边对接,小步快跑。

问题与改进,需求与重构

前面大致的陈述了技术方案与开发流程,此后的详细进程就不再赘述,下面着重写一下开发过程中遇到的一些问题以及相关的改进措施。

流量高峰与消息队列

项目运行一段时间后,发现许多数据上传请求出现 500 的响应,检查发现是过多请求同时占用,触发了 Nginx 的并发数限制,后续请求直接被拒绝了。

从日志中看到每次数据上传请求都需要几秒钟给到十几秒钟的响应时间,原来之前的实现里,接收到数据后直接同步写入数据库,大部分请求被卡在 IO 并且排队。

于是引入了消息队列,把数据收集和数据库写入操作分离开来, API 把收到的数据放入队列后,立即响应客户端,避免过多请求在队列中等待,占用资源。拆分一个专门处理数据的消费端,从队列中取出数据,处理后写入数据库。

选用了 Celery 框架搭配 RabbitMQ 服务,这个成熟框架支持多种实现,做了异步任务的包装,就不需要直接接触消息队列的细节。

热点数据与缓存

数据平台首页是一个数据概览页面,会展示所有游戏的总和指标,同时也会列举每个游戏的总和指标,如总用户量、总收入等。

对于这种高频访问,较少修改的数据,我们引入了 Redis 来缓存这些数据。Redis 将数据都保存在内存中,可以用作数据库、缓存和消息分发。使用方式上,可以看做是读写键值对,所有数据由 key 跟 value 组成。

Redis 自身是支持许多数据类型的,字符串、列表、集合、哈希和字节数组等,也支持设置过期时间。

因为 Redis 的 key 是全局的,所以不同应用如果要共用,需要注意在 key 的命名规则上做好区分,避免冲突。不过 Redis 默认有 16 个数据库,可以作为命名空间使用。

协同开发与工作流

虽说只有几个后端,前期各自开发自己的模块,互不冲突,但随着人事调动,每个人都或多或少的改起非自己开发的模块,这个时候就需要定好工作流,避免对同一个地方同时修改,避免冲突和覆盖等。

简化版的基础工作流大概是这样,数据收集、数据分析、数据展示这几个独立为项目,使用 Git 管理,默认分支 master,开发分支 dev。开发时使用 dev 分支进行测试,以完整的修改为最小单位进行 commit,准备提交到远程的代码仓库时,先 pull 下来检查是否有冲突,没有冲突后 push 上去。如果有冲突,由最后的提交者负责解决冲突与代码合并。

当然,严格这样执行的话,一般是不会出现冲突问题的,每个人都是基于相关文件最新版本来进行开发。

以完整的功能为单位,发起 Pull Request 将 dev 分支最近的修改合并到 master 分支,主开发进行代码评审,修改完成并审核通过后,合并代码,之后在生产环境部署 master 分支的代码。

实时请求与远程调用

从前文可以得知,数据平台这个项目拆分成若干模块,API 和负责数据分析的模块是不在一起的。在一个实时展示线上指标数据变化的功能实现时,我们引入了 gRPC 来实现模块之间的远程过程调用。

使用 gRPC 的原因有几个,包括其使用 protobuf 协议序列化的性能更好,支持多种语言(与公司另一个用 PHP 写的系统对接时可以用到),以及 RPC 带来的对调用方式的简化,调用方不需要处理网络方面的细节,只需要像调用本地方法一样执行即可。

于是一个实时页面的请求路径就成了这样: 前端异步请求 API 服务端,然后通过 gRPC 请求到数据分析模块,获取到响应后,数据沿着路径回到前端参与渲染。

日志收集与分析

项目运行在服务器上,日志是很重要的,用于排查错误、检查异常和数据分析等,毕竟线上发生的事情没办法事后回溯,就只能通过当时的日志去分析。记录日志的点也很重要,写的日志太多,会有很多无效的日志,也占用存储空间;写的日志太少,没办法获取到足够的有效日志来做分析。

在开发过程中,发现了 jonathanj/eliottree: Render Eliot logs as an ASCII tree ,如项目描述所说,经过自带工具处理后,它的日志是树形的,可以获得一定量的调用栈信息,实例如下:

1
2
3
4
5
6
7
8
9
10
c8ae29b3-1321-48b7-89cd-7732663fcd2a
└── ProcessorTask:process_game/1 ⇒ started 2020-11-18 08:35:08Z ⧖ 208.443s
├── app_id: 39AC3A
├── game_name: Unknown
├── progress: 1/233
├── time_param: 2020-11-18 00:00:00
├── task_basic_day/2/1 ⇒ started 2020-11-18 08:35:08Z ⧖ 5.385s
│ └── task_basic_day/2/2 ⇒ succeeded 2020-11-18 08:35:13Z
├── task_churn_month/3/1 ⇒ started 2020-11-18 08:35:13Z ⧖ 0.181s
│ └── task_churn_month/3/2 ⇒ succeeded 2020-11-18 08:35:13Z

目前来说,日志还是以文件形式存储,通过命令行工具查看和筛选。有尝试过使用 ELK Stack: Elasticsearch, Logstash, Kibana | Elastic 这一成熟方案来收集与分析日志,大致了解后发现这一方案挺吃性能的,而现实是我们的生产服务器剩余的性能是不够的,因此放弃了这一方案。

开发环境与自动化部署

此前一段时间,我们项目组都只有一台服务器,开发阶段基本都是在本地跑起几个项目和数据库,只能说勉强够用。

开发服务器申请下来以后,能做的事就多多了,例如复刻生产环境的配置,拷贝最近的线上数据库来做测试,持续集成持续部署,远程开发这些都可以开始着手准备了。

作为一个不太规范的小项目组,单元测试和集成测试这些暂时还是没有的,不过利用 GitHub 的 Webhook ,在开发环境实现了简易的“自动部署”,原理就是监听代码仓库的 push 事件,然后执行 git pull 命令更新项目代码并重启服务。

程序报错与告警推送

最近用上了企业微信,将程序中部分地方的报错通过 HTTP 实时上报给告警应用,然后关联到企业微信,发送消息给相关开发者。

容器化

Docker 容器我也有尝试过,不过开发需求一直有,也没多少时间去研究,暂时搁置。

大致了解过 Docker,通过 Linux 自带的 clone(2) 这一系统调用,通过 flags 参数,可以实现进程、命名空间等资源的隔离。

目前我觉得主要用途是,通过配置文件指定各种依赖以及部署过程,可以做到简化部署和维护,不同容器之间相互隔离,适合微服务,可以很快的进行部署,扩容等操作。

考虑中的方案

  • 通过多进程/多线程,提高 CPU 利用率,优化程序性能
  • 数据库主从,定时备份
  • 重构部分逻辑
  • 从安全角度重新检查代码
  • 运维方面,细分权限,增加操作日志等