基于 PostgreSQL 的数据统计系统
看到标题就知道我要写什么了, 这是之前一个项目的小结吧, 自己对统计的一些认识和看法.
当时从前到后, 包括技术选型, 花了接近一个月的时间, 也在生产上用了两三个月, 一致在持续维护, 做完图表配置化已然接近完工, 无奈后来离开了, 不过目前应该还在运转
至于源代码, 暂时不考虑开源, 太渣(其中在看了几天js情况下, 自己撸了1000行js的前端框架, 质量堪忧), 全套用python实现.
提供一种快速实现运营统计需求的思路.
(图为百度 echarts 示例)
一. 场景
- 统计
所谓统计, 抽象出来就是计数而已(还有各个计数之间的算术运算). 再具体一些, 根据不同维度进行计数.
而统计后台, 无外乎数据的输入, 处理, 及输出.
对于实时性, 一般会以天为单位进行统计.
而在具体业务场景下, 需要计数的数据来源于各个项目和同一个项目的不同机器(分布式部署), 就需要考虑, 如何将日志进行汇聚, 如何更为便捷地进行处理, 存储, 以及展现.
其中要考虑, 需求是不断在变化的, 如何将成本降到最低?
- 以往的统计方式:
分析统计需求 -> 修改项目记录日志内容和格式(到磁盘) -> 自行将日志汇总到一台机器(rsync) -> crontab脚本分析日志(要删或备份历史数据) -> 新建db表, 存储统计结果 -> 写管理后台, 查询统计结果(最繁琐) -> 处理分页/图表等
虽然每次耗时或许并不会太长(0.5-2d, 视需求大小), 但对于不同项目和需求变更, 这些工作都是纯体力毫无技术含量的枯燥工作, 可以说是无意义的资源浪费.
- 新的方式
分析统计需求 -> 确认日志内容和格式 -> 统计后台配置输入/处理/输出逻辑 -> 查看结果
说白了就是, 处理统计需求变成了 写sql
+ 配置
二. 处理思路
- 大体思路如下(从后往前):
1. 将日志进行汇总
2. 日志格式一致化
3. 将日志导入到一个容器中
4. 便捷地通过容器进行计算(计数)
5. 统计结果进行统一存储
6. 提供统一的查询接口
7. 提供前端框架组件, 可以通过配置调用统一查询接口, 并对数据进行分页及图表化
8. 提供配置入口, 可以配置日志入口, 处理逻辑, 展现逻辑. 即完全地配置化
- 需要统一的地方:
日志格式
容器存储
报告存储
查询接口
前端组件
- 系统成型后
增加/修改统计需求: 只需要在后台配置数据来源(日志), 处理逻辑(一段 sql), 展示逻辑(一段前端 json配置), 就可以实现图标
三. 具体
0. 基本架构
----------------------------------------------
| 日志(UniteStats or ApplicationLogs) |
----------------------------------------------
||
---------------------------
| load处理程序 |
---------------------------
||
___________________________
|存储容器--计算容器 |
| |
| Container(Postgresql) | //json - sql - 聚集函数
| |
|___________________________
||
--------------------------------------
| [自定义统计脚本-查询逻辑及报告表] |
--------------------------------------
||
---------------------------------------
| 统计报告 |
---------------------------------------
||
---------------------------------------
| 统一查询接口 |
---------------------------------------
||
------------------------------------
| [自定义前端-使用统一库-配置生成] |
------------------------------------
1. 日志格式
日志, 即文本.
但是文本存在各种格式, 例如常见的’\t’分隔的列, csv, json, xml等等.
这里的要求是: 一定要满足自描述, 易读(人), 易处理(生成和解析).
最终选择了json
. 将原先无结构数据转成半结构化数据.
原因之一, 容器
对半结构化的数据支持已经非常完善了, 例如postgresql, mongodb等, 对于后续计算很重要.
原因之二, 作为一个统一的平台, 我只在乎数据是一份日志, 但是不在乎, 日志里存了些什么, 每个字段的意义, 这些只有平台的使用者需要知道. 否则带来很大一个问题是, 对于使用者在新增或变更一份日志格式时, 需要明确告诉系统这份日志各个字段是什么(名称和类型), 复杂化了
到这里, 我们统一了日志的格式, 记录为json, 每条记录一行.
2. 日志收集汇总
目的: 将日志汇总到同一台机器上, 便于统一处理
命名规则: $THE_LOG_PATH/{projectName}/{projectName}_{moduleName}_{ip}_{yyMMdd}.log
(示例)
日志汇总的方案有很多:
scp
rsync
nfs
logstash
最终的处理方案: 数据量小, 同一个机房, 使用NFS将日志汇总到目录, 不同机房, 使用rsync进行汇总. 如果数据量大, 可以考虑使用logstash, 直接将日志经过节点处理实时写到一台机器上(就不要分别记录到各自磁盘了).
扩展: 使用多台机器, 只要保证最终导入同一个库即可.(同一个项目, 同一天存在一张表, 不同机器的日志导入之)
到这里, 我们将所有json格式的日志汇集到了一起
3. 导入容器处理
目前每个项目的日志格式是,
{projectName}/{projectName}_{moduleName}_{ip}_{yyMMdd}.log
我们会将同一个项目, 可能来自不同机器的日志导入同一张表
{projectName}/{projectName}_{moduleName}_*_{yyMMdd}.log
=>
table: projectName_moduleName_yyMMdd
处理方式: 批量入库, 并且清理保留日期外的表
建议使用批量导入的方式, 速度杠杠的. Postgresql请使用copy命令
4. 容器
一个计算容器, 仅此而已
技术选型时, 考虑过Mysql/Mongdb/Redis/MariaDB/OrientDB/CouchDB/RethinkDB等等, 最终敲定使用postgresql, 无它, 对json的完美支持, 满足业务: 一定的数据量, 足够简单的统计方式, 足够稳定, 简单易运维等
提下redis
, 当时做了整套的redis
方案(接口文档都明确完了就差写代码了), 但是后来毙掉了. (典型的拿着锤子满世界都是钉子的案例). 思想是: 流式日志处理, 根据业务需求使用redis counter, 主从, 后台从redis直接取counter进行展示. 脑洞很大, 可以搞定实时/非实时情况, 还可以顺带把各类业务中的counter需求给做了, 以及更为灵活的展现方式, 但是学习成本较高, 对每个写统计的人要求较高(素质, 具备正确的统计思维, 否则会悲剧掉). 再加上业务本身要求实时性并不高, 所以废弃.
MySql
对 json
的支持, 相对于 postgresql 而言逊色太多了, 对json
格式存在限制(多层复杂嵌套的情况)
Mongodb
虽然对json
支持不错, 但是对于数据量较大的情况支持并不好, 并且查询以及运维都会带来一定困难, 对于使用者有一定学习成本
PostGresql作为容器的好处:
1. 支持的数据量
2. 查询简单,支持json, 所有sql查询,group by/order by/嵌套子查询,聚集等
3. 各种聚集、统计函数均可用,搞定基本统计查询无障碍(再复杂的都可以)
4. 运维简单
5. 对于开发而言几乎没有学习成本, 会sql再学习下postgresql的json查询
示例: 假设搜索日志:
{‘ip’: ‘127.0.0.1’,
‘keyword’: ‘test’,
‘result_count’: ‘1’,
}
统计 pv
select count(data->'ip') from search_20141101;
统计 uv
select count(DISTINCT data->>'ip') from search_20141101;
无结果数
select count(*) from search_20141101 where data->>'result_count' = '0';
搜索热词排行榜
select data->>’keyword’, count(*)
from search_20141101
where data->>'result_count' != '0'
group by data->>’keyword’
order by count(*) desc
limit 100;
5. 批处理
这里要做的事情, 需要有一个管理后台, 让开发可以配置上传自己的处理脚本, 设定脚本执行时间, 执行参数(处理日期/报告表名), 甚至是执行依赖.
这里需要形成一个约定
报告表名: projectName_statsModuleName
报告表一些字段名(因为统一查询接口需要用到): 日期 date,
其他约定字段
每天, 系统会扫描并调度任务, 执行, 处理得到统计结果, 存入报告表.
到这里, 我们每天的统计结果都存入到了报告表中
6. 输出
报告表, 是以时间为维度的, 每条记录带有日期, 每条记录细化到要统计到的精确维度.(具体表现是一个维度会多一列字段), 原则是, 需求分析时充分考虑当前及后续可能的统计需求(要预见还是蛮容易的), 直接将统计维度最细化.
当然, 如果无法最细化, 后面存在变更, 可以修改统计脚本, 根据情况对历史数据进行重新统计.
7. 统一查询层
一层通用的接口, 支持传入表名, 条件, 需要结果字段, 格式等, 可以对系统中各类报告表进行各种形式的查询, 获取统计结果.
8. 前端框架及展现
是一整套的js款干啊
分成几块
- 生成查询表单: 模块化组件, 通过json配置, 自动生成统计查询的表单, 支持各类维度
配置示例:
// 产生条件html
var condition_configs = {
title: "频道访问统计摘要",
conditions: [
{
type: "date_begin_to_end", //开始结束日期选择框
},
{
type: "select", //下拉框
label: "频道",
id: "channel",
options: [
{
text: "所有",
value: "",
},
{
text: "快速访问",
value: "quickaccess",
},
]
},
{
type: "version", //文档框
},
]
};
就会自动生成表单
begin_date:
end_date:
channel:
version:
-
组合查询条件: 表单提交时, 根据json配置, 将表单内容/字段/值/表等, 拼接成统一查询层接口需要的请求串
-
查询后数据处理: 将查询后的结果, 根据json配置, 进行转化和展现, 并图表化.
一个配置示例:
一般文本
{
'column': 'date',
'name': '日期',
'type': 'text',
},
百分比 $后面跟的是sql查询结果列名
{
'column': 'uninstall_ratio',
'name': '卸载率',
'type': 'ratio',
'value': '$uninstall_pv/$install_pv'
},
公式计算
{
'column': 'the_qvod_link_pv',
'name': '导入链接数',
'type': 'calculate',
'value': '$qvod_link_pv + $qvod_start_pv'
},
列值翻译
{
'column': 'channel',
'name': '渠道',
'type': 'text',
'translation': {
"all": "all",
"player": "播放器",
"zx": "资讯",
"other": "其他导入",
}
},
9. 图表
使用百度 echats
可以根据配置, 将统一查询层的接口返回数据直接灌入echats, 生成表单
10. 过程日志及监控
需要一组管理表, 进行任务配置/调度/执行/执行结果, 整个过程中的操作可以配置和查看, 用于监控.
四. 小结
It’s Simple, but it works.
数据情况, 当时大概每天 10G 日志 load 到库(处理前>10G), 每天日志数据大概是五千万条, 具体业务上了大概40个的样子, 每天30分钟左右处理完. 对于开发的改进是, 将原先0.5-2d的工作, 缩减到了1-2小时, 对生产力的提升较为显著.(对于日志数多且单一日志量较小的情况处理尤为便捷)
适用范围: 对于一般团队应该足够了(流量百万级别), 每个项目每天3-5百万访问量, 日志数据10-20G, 当然, 一直没机会测试上限, 不过只要PostGresql能抗住, 量再大些应该也ok.(可以考虑上elasticsearch)
以上思路, 仅供借鉴:) 就这样吧