现状:原先公司服务器的日志是用来事后调试。日志是根据时间按照键值对存储的本地文件。业务繁忙或日子一长,便会产生大量无效的文件。除此之外,别无他用。
目标:从日志中提取有用的信息。从软件上可以获取一定时间内的IO性能;客户端请求的统计。从业务上则可以获取更多。
这是大致的工作流程。
技能预备:
python 3.4 | 文本分析和数据处理 | 快速开发,快速实现业务 |
mongodb | 键值对的存储 | 数据由于业务不同,相关的键值对也不一致,所以采用nosql |
mysql | 最终记录的管理 | 具体业务的最终结果是相对固定的,同时也为了后续客户端的快速访问 |
workbench | 数据库table的维护 | 数据库维护工具 |
c# && wcf | 服务端,给浏览器提供数据支持 | 构建一个简单的服务端,支持浏览器的post方式获取数据 |
Javascript && HTML | 客户端界面和操作 | html,javascript分离界面和业务,手工绑定数据 |
Fiddler | 测试wcf请求 | 测试客户端的请求和返回值,调试封装后的API |
如何安装这些软件,就不在这里详述了。
测试环境:
windows 2008 r2 | c#,workbench |
ubuntu14.04 | mongodb,mysql |
实现过程:
1. 原始日志文件的输入
日志基本说明:
文件按照目录存放,日志内容是有时序。
每条日志占用一行。
它的格式如下:2015-11-09 07:37:09 Request sn=0 topic=RealTimeSEHKFeed6881 fields=<F>P1<D>6881<F>P2<D>CGS<F>P3<D>中國銀河<F>P4<D>中国银河<F>P6<D>7.730<F>P15<D>500
时间 | 2015-11-09 07:37:09 |
关键字 | RealTimeSEHKFeed6881 |
数据 | <F>P1<D>6881<F>P2<D>CGS<F>P3<D>中國銀河<F>P4<D>中国银河<F>P6<D>7.730<F>P15<D>500 |
实现文件列表:
pyCollectMore.py | 启动脚本,设置参数,捕获错误 |
pyScanner.py | 枚举目录的文件,并发pyResolve(由于开发时间有限,暂时做成单线程的方法调用了) |
pyStoreDefine.py | 常量定义 |
mongoAccesslib.py | mongodb api封装 |
pyResolve.py | 利用正则表达式分析日志行,写入mongo数据库 |
开发细节:
1. 由于日志之间有时间顺序,所以同一个目录的文件需要依次遍历,但不同目录可以并行执行。
2. 主要是日志topic对象的信息分解。
原始数据如下:
2015-11-09 07:37:09 Request sn=0 topic=RealTimeSEHKFeed6881 fields=<F>P1<D>6881<F>P2<D>CGS<F>P3<D>中國銀河<F>P4<D>中国银河<F>P6<D>7.730<F>P15<D>500
分解1:获取时间,topic名称,原始键值对。
正则表达式:(dddd-dd-ddsdd:dd:dd)s(?:Request|Update)ssn=0stopic=\([S]+)\([S]+)sfields=(.+)
分解2:键值对。
正则表达式:<F>([^<]+)<D>([^<]*)
整理和合并日志记录:
对于一个topic而言,包含2部分:命名空间和topic对象名称。
在之前的示例中“RealTimeSEHKFeed6881”---命名空间:“RealTimeSEHKFeed”;对象是:“6881”。
所以处理完成全部的日志行后,需要合并相同命名空间下的对象名称。
2. 中间结果的存储
在之前任务中,我们可以获取2部分的信息:topic的明细以及topic的清单。
这时候需要在mongodb中创建2个表。
tb_topic_list | 记录已收集的topic对象名称 |
tb_命名空间 | 根据日志收集后的命名空间,创建对应的表,存放每个topic的全部明细 |
这时候使用nosql的好处了,事先不用定义表以及相关字段。
tb_topic_list字段定义:
_id | 记录id |
tps | 新建表名称,后续调度程序使用 |
tpn | 对象名称 |
tpu | 是否已经处理过 |
tb_命名空间(根据上下文应该是tb_RealTime_SEHKFeed)字段定义:
_id | 记录id |
nss | 记录序列号(python维护) |
nstp | topic名称 |
nsd | 时间 |
nsn | 命名空间 |
业务的字段P1,P2 | .. |
3. 任务调度和数据分析
实现文件列表:
pyTaskMng.py | 启动脚本,定时查询待处理的topic,并发执行pyReduce(topic数量很多,所以这里是并发进程) |
pyReduce.py | 从mongodb获取已经分类的topic,根据时间顺序,计算总数,最大值,最小值以及记录明细 |
pyStoreDefine.py | 常量定义 |
mongoAccesslib.py | mongodb api封装 |
mysql.connector | mysql官方引用,在reduce之后写入计算的结果 |
开发细节:
1. pyTaskMng运行在不同的机器上,定时检查tb_topic_list。
如果有记录上, 并发交给进程池,由pyReduce负责获取topic对应的全部记录,数量大的时候,批量获取。
2. pyReduce分批获取topic的明细。
根据业务规则:
P1 | code |
P2 | 名称 |
P3 | big5名称 |
P4 | gb名称 |
P5 | 价格 |
P6 | 数量 |
由于这个项目属于demo,所以我们假定以下的规则:
开盘价 | 每个股票第一笔价格 |
收盘价 | 每个股票最后一笔价格 |
最高价 | 初值等于开盘价,比较当前价后判断是否更改 |
最低价 | 初值等于开盘价,比较当前价后判断是否更改 |
变化次数 | 累计记录数 |
成交量 | 每个记录报价数 |
pyReduce并发执行,执行完成后更新tb_topic_list。
计算的结果存入mysql表。
4. 最终结果的存储
鉴于nosql在统计查询上有些限制,所以使用mysql作为统计信息的存储设备。
secDaily表记录统计信息
字段 | 类型 | 说明 |
tID | int | 主键 |
code | varchar(45) | 股票code |
secName | varchar(32) | 股票名称 |
ns | varchar(64) | 命名空间 |
secTime | datetime | 日期 |
dayNum | int | 逻辑天 |
openPrice | double | 开盘价 |
closePrice | double | 收盘价 |
highPrice | double | 最高价 |
lowPrice | double | 最低价 |
changeTotal | int | 变化次数 |
exQty | double | 成交量 |
secDetail表记录时间戳
字段 | 类型 | 说明 |
sdID | int | 主键 |
secDailyID | int | 主表id |
seq | int | 序列号 |
detailTime | datetime | 时间戳 |
price | double | 价格 |
qty | double | 数量 |
5. 提供数据的服务端
为了快速显示,这次使用wcf作为服务端。
提供5个接口:
获取指定sec列表总数 | |
select count(tID) from secDaily where secCode like '%{0}%' or secName like '%{0}%'; | |
http://localhost:8000/DEMOService/secSum | |
R: {"filter" :"1"} | |
获取批量sec列表 | |
select secCode,secName,ns,DATE_FORMAT(secTime,'%Y-%m-%%d %H:%i:%S'),openPrice,closePrice ,highPrice,lowPrice,changeTotal,exQty from secDaily where secCode like '%{0}%' or secName like '%{0}%' order by secCode limit {1},{2}; | |
http://localhost:8000/DEMOService/secbatch | |
R: {"filter" :"1", "offset": 0,"count":10} | |
获取指定sec明细总数 | |
select count(sdID) from secDetail inner join secDaily on secDetail.secDailyID = secDaily.tID where secDaily.secCode = '{0}'; | |
http://localhost:8000/DEMOService/detailSum | |
R:{"code" :"1"} | |
获取指定sec批量列表(关键是item的时间) | |
select seq,DATE_FORMAT(detailTime,'%Y-%m-%%d %H:%i:%S') from secDetail inner join secDaily on secDetail.secDailyID = secDaily.tID where secDaily.secCode = '{0}' order by seq limit {1},{2}; | |
获取指定sec批量列表 | |
R: {"code" :"1", "offset": 0,"count":2} | |
获取指定时间内统计 | |
create procedure getRange(codeRange varchar(45), beginTime varchar(20), endTime varchar(20),rangval int) 需要构建临时表,填充空记录 | |
http://localhost:8000/DEMOService/rangeTotal |
客户端通过浏览器使用POSt方式查询数据.所以服务器请求需要特别处理。
[ServiceContract(Name = "RESTDemoServices")]
public interface IRESTDemoServices
{
[OperationContract]
[WebInvoke(UriTemplate = "secSum", Method = "*", BodyStyle = WebMessageBodyStyle.Bare, RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
SecSumResponse GetSecSum(SecSumRequest req);
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single, IncludeExceptionDetailInFaults = true)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
[JavascriptCallbackBehavior(UrlParameterName = "callback")]
public class RestDemoServices : BaseEntry.IRESTDemoServices
{
public SecSumResponse GetSecSum(SecSumRequest req)
{
if (WebOperationContext.Current.IncomingRequest.Method == "OPTIONS")
{
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "POST");
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Accept");
return null;
}
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
SecSumResponse resp = new SecSumResponse();
}
}
6. 客户端实现 Javascript, chart.JS
实现文件列表:
main.htm | 界面布局 |
bigDataStyle.css | css |
jquery-1.11.2.js | jquery |
mainImpl.js | 界面实现脚本 |
SecDailyApi.js | api封装脚本 |
table.js | 表格脚本 |
Chart.JS | Chart.JS引用 |
效果图
测试文件:ch1.fxl
7. 接下来可以做的任务
这个项目属于demo性质。下一步预备使用java改写python的实现,scan和reduce部分的存储可能考虑hadoop实现存储和分发。