English Version: https://taowen.gitbooks.io/tsdb/content/indexing/indexing.html
现状
所谓 structured logging 就是往日志文件里打json格式的日志,然后按照固定字段去检索的需求。这种需求目前一般是用这样的一个基础设施来满足的:
业务进程 => 日志文件 => 同机的日志搜集agent => 日志解析 => kafka => 日志索引程序 => elasticsearch
对于日志的存储和检索需求来说,用上面设置的 elasticsearch 可以说已经解决得很好了(唯一查询DSL比较古怪的问题,用SQL也可以解决 https://github.com/taowen/es-monitor)。
存在的主要问题就是现在的设置是用“非结构化日志”来支持“结构化日志”的需求。因为整个基础设施假定输入的数据都是无格式,并且面向行处理的,所以整个导入的过程无法为结构化日志优化,没有办法获得本来可以达到的性能。问题表现为三个主要的点上:
应用程序必须在可靠性和高性能之间选择日志方案
数据在多个集群之间倒腾,导致占用大量机房内网带宽以及提高了端到端的延迟
低效的数据封包格式,以及按行处理
可靠性 v.s. 高性能
当业务进程需要提供一份流式的事件做为分析用途的时候,就往往需要在高可靠性和高性能之间选择。一般来说有这样两种选择:
业务进程 => kafka (直接写kafka)
业务进程 => 日志文件 => 采集agent => kafka
选择第一种方式会非常可靠。首先少了中间环境,其次写kafka是同步过程,甚至可以让kafka做到全部replica都ack才返回success。但是第一种方式的主要问题是一旦kafka出问题了就可能会影响到业务进程本身。如果业务进程决定不同步写kafka,那又有丢数据的风险。
第二种方式因此变得有吸引力,一方面日志文件写入不影响业务进程自身的速度,同时持久化的文件一定程度上可以保证数据不丢失。但是目前还没有特别靠谱的方案来保证这条路径和第一种方式一样可靠。
使用方需要在这两种模式之间进行选择说明现在的基础设施还不够完善。理想的kafka写入方式应该兼具可靠性和高性能:
首先业务进程通过本地socket报告事件给本机的agent
本机的agent通过memory mapped file 落磁盘保证事件不丢
agent批量把汇集的事件可靠写入到 kafka 集群里
也就是在业务进程和远程的kafka集群之间添加一个本地的可靠队列。基本上rsyslog的设计是满足这样的条件的,但是实际使用中发现rsyslog仍然不够完美,可以为structured logging定制一个更好的agent:
更好的性能
对结构的原生支持,并可以在入口处校验格式
批量汇集的事件可以把小的event打成大的封包格式,甚至可以把行转成列压缩,并利于后端按列处理
add a local https://github.com/OpenHFT/Chronicle-Queue before kafka would be awsome
多集群之间倒腾数据
分布式系统最常见的口号是你不用关心数据在哪里,我们帮你把数据自动分布到多台机器上去。问题是现实情况是一个完整的流程需要多个分布式系统来彼此配合,但是它们都各自为政,自己都是一个独立的王国。最差的情况是这样
clusterA 1 clusterB 1
clusterA 接口机 => clusterA 2 => clusterB 接口机 => clusterB 2
clusterA 3 clusterB 3
每个集群都提供一个数据的入口,然后从这个入口转发一层到自己后端。从一条数据的角度来说,他需要不断地在机器之间倒手,序列化反序列,路由再路由,才能到达最终的目的地。对于一个常见的日志集群来说,数据就是这样地不断地被倒腾。对于非结构化日志来说,这样的倒腾是必要的,因为日志要经过不断的再处理,但是对于结构化日志的场景来说,就是浪费:
业务进程集群 => 日志解析集群 (或者kafka入库程序集群) => kafka集群 => ES入库程序集群 => ES集群
理想的设置是
业务进程集群 => 本地agent => kafka/es 集群
数据通过本地agent直接录入到 kafka/es混合的集群里。这种混合无法通过简单地同机部署kafka和es来达到,因为这两个分布式系统都是假定自己掌控数据的分布的。只有修改kafka和es让两个系统深度融合才能实现,让kafka做为es的write ahead logging(WAL,在es中叫translog)才能达到最佳的效果。这个kafka/es混合集群就是一个混合的事件存储
kafka提供事件的顺序消费支持
es提供事件的检索支持
es利用kafka做为自己的WAL,实现索引的可靠建立
低效地封包格式和按行处理
因为整个基础设施是为非结构化日志准备的,所以一些习惯地做法也是为了方便非结构化日志而存在的,比如:
每个事件封装成一个json
事件之间用\n换行符隔开
这种封包格式相比正则解析日志是巨大的进步了,但是仍然是非常低效的。对于结构化日志我们有更好的选择
事件可以用带schema信息的格式,避免反复出现key/value里的key。比如pb,avro这样的格式
事件可以被打包成一个大的包,包内可以按列表示数据,进行高效压缩
事件之间可以用头信息指定自己的包的尺寸来隔开,比扫描\n更快
除了包格式的选择上,处理方式上也是从头到尾都是按行处理的模式。最佳的处理方式应该是这样的
按行产生事件 => n行批量组包成一个块 => 块内按列进行处理和存储
在记录处理的offset上时,每个offset对应一个块而不是具体的一行事件。多个行打包成一个高效的按列组织的块方便批量处理(特别是CPU有针对性的向量化优化)。
在Elasticsearch/Lucene内部也缺乏高效地结构化数据入库(最终是写入lucene的doc-values文件)的渠道。从源头就假定输入是按行组织的JSON文档。而且内部是以List<Number>
而不是long[]
来表示数据。原始的数据如果产生就是有格式的,完全可以避免一堆中间对象产生。直接产生就是long[]
的形式,然后简单传输到Elasticsearch服务器就以long[]
进行文件写入。
总结
Elasticsearch是一个很好的存储事件的数据库,非常好地解决了存储和检索两大难题。但是对于结构化的数据入库支持并不是很好,理想地情况应该是这样的:
提供一个本地agent,实现数据可靠地高性能入kafka
kafka/elasticsearch深度融合,避免集群拷贝产生的额外负担,kafka直接作为elasticsearch的WAL存在
数据以高效的格式封包,并且能够从源头开始就按列处理,从头到尾都是
long[]
而不是List<Map<String, Object>>