Druid用了一个module体系以便于在运行时加载定制的module扩展。
实现你自己的扩展
Druid的扩展使用Guice以便于在运行时加载。Guice时一个DI框架,但我们用它来保存Druid处理时用的对象图。通过增加Guice binding,扩展可以实现他们想要的对象拓扑图。虽然Druid扩展提供了任何可以扩展的能力,但一般来说,我们希望人们能够扩展列在下面的内容:
1. 通过实现 io.druid.segment.loading.DataSegment* 和 io.druid.tasklogs.TaskLog* 类来实现Deep Storage。
2. 通过扩展 io.druid.data.input.FirehoseFactory 来实现一个新的Firehose(消防水带,即入口)。
3. 通过扩展 io.druid.data.input.impl.InputRowParser 来实现一个新的解析器。
4. 通过扩展 io.druid.data.input.impl.ParseSpec 来实现一个新的基于字符串的格式化处理。
5. 通过扩展 io.druid.query.aggregation.AggregationFactory, io.druid.query.aggregation.Aggregator 和 io.druid.query.aggregation.BufferAggregator 来实现聚合器。
6. 通过扩展 io.druid.query.aggregation.PostAggregator 来实现 PostAggregators。
7. 通过扩展 io.druid.query.extraction.ExtractionFn 来增加 ExtractionFns。
8. 通过扩展 io.druid.segment.serde.ComplexMetricsSerde 来增加复杂指标。
9. 通过扩展 io.druid.query.QueryRunnerFactory, io.druid.query.QueryToolChest 和 io.druid.query.Query 增加新的查询类型。
10. 通过调用 Jerseys.addResources(binder, clazz) 来增加新的Jersey资源。
11. 通过扩展 io.druid.server.initialization.jetty.ServletFilterHolder 增加新的jetty过滤器。
12. 通过扩展 io.druid.metadata.PasswordProvider 来增加新的密钥提供器。
13. 跟其他Druid扩展一样绑定你自己的扩展。
扩展通过实现 io.druid.initialization.DruidModule 来增加到系统中。
创建一个Druid Module
DruidModule类有两个方法:
1. configure(Binder)
2. getJacksonModules()
configure(Binder)方法与普通Guice module有的方法一致。
getJacksonModules()方法提供了一个Jackson模型的list用来初始化Druid使用的Jackson ObjectMapper。这就是通过Jackson初始化(像AggregatorFactory和Firehose对象)来把Druid增加到扩展的原因。
注册你的Druid Module
一旦你的Druid Module创建了,你需要在你的jar里的META-INF/services目录下增加一个新的文件。如果你使用maven构建的项目,那这个目录应该在src/main/resources目录下。有一些cassandra-storage, hdfs-storage和s3-storage模块中的文件的例子。比如:
应该在你的jar中存在的文件为:
META-INF/services/io.druid.initialization.DruidModule
这个文件里的内容是一系列实现了你的DruidModule接口的完整类名。这些类名一行一个。如:
io.druid.storage.cassandra.CassandraDruidModule
如果你的jar中有这个文件,那么他就会被加载到classpath中作为一个扩展。Druid会注意到这个文件并且会初始化这个文件内的类。你的Module应该有一个默认的构造器。但如果你需要访问运行时的配置参数,你可以在类中实现一个方法,并在方法上标注@Inject注解,Guice会讲Properties对象注入到这个方法中。
增加一个新的Deep Storage实现
以azure-storage, google-storage, cassandra-storage, hdfs-storage 和 s3-extensions模块为例来看怎么实现。
这些扩展背后的基本思想是增加你的DataSegmentPusher和DataSegmentPuller对象的绑定。增加他们的方法类似于如下(例子取自HdfsStorageDruidModule中):
Binder.dataSegmentPullerBinder (binder)
.addBinding("hdfs")
.to(HdfsDataSegmentPuller.class).in(LazySingleton.class);
Binder.dataSegmentPusherBinder (binder)
.addBinding("hdfs")
.to(HdfsDataSegmentPusher.class).in(LazySingleton.class);
Binders.dataSegment*Binder() 是druid-api的jar中提供的一个调用,它建立了一个MapBind的绑定。
addBinding("hdfs") 为Puller binder创建了一个类型为"hdfs"的loadSpec的handler。对于Pusher binder,它创建了一个新的类型,你可以指定 druid.storage.type 参数。
to(...).in(...) 是一个通用的Guice操作。
除了DataSegmentPusher和DataSegmentPuller,你还可以绑定:
DataSegmentKiller:删除segments,用于在Kill Tasks中删除没有用的segments。例如:执行对新版本覆盖的segments或者已经被cluster抛掉的segments进行删除。
DataSegmentMover:把segments从一个地方移动到另一个地方。现在他用在MoveTask的一部分用来移动没用的segments到一个不同的S3 buckets或者其他地方。
DataSegmentArchiver:Mover的一个修饰器,来自一个预先配置的bucket或path,因此他不必作为一个ArchiveTask运行时的一部分。
校验你的Deep Storage实现
警告:这不是一个正式的步骤,但是会有很多提示来校验你的Deep Storage实现是否能够pull,push和删除segments。
推荐使用批注入的方式来校验你的实现。大概20秒以后,segment就会被存到historical node上。通过这种方法,你可以同时校验push(在实时节点)和pull(在历史节点) segments。
DataSegmentPusher
无论你的数据存储在哪里(云存储服务,分布式文件存储等),在注入结束后,你应该看到两个新文件:descriptor.json (在hdfs上是partitionNum_descriptor.json) 和 index.zip (在hdfs上是partitionNum_index.zip)。
DataSegmentPuller
在你的注入任务结束20秒以后,你应该能够看到你的historical node正在试图load新的segment。
下面的例子是通过Azure Deep Storage拿到的his节点的log:
2015-04-14T02:42:33,450 INFO [ZkCoordinator-0] io.druid.server.coordination.ZkCoordinator - New request[LOAD: dde_2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z_2015-04-14T02:41:09.484Z] with zNode[/druid/dev/loadQueue/192.168.33.104:8081/dde_2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z_2015-04-14T02:41:09.484Z].
2015-04-14T02:42:33,451 INFO [ZkCoordinator-0] io.druid.server.coordination.ZkCoordinator - Loading segment dde_2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z_2015-04-14T02:41:09.484Z
2015-04-14T02:42:33,463 INFO [ZkCoordinator-0] io.druid.guice.JsonConfigurator - Loaded class[class io.druid.storage.azure.AzureAccountConfig] from props[druid.azure.] as [io.druid.storage.azure.AzureAccountConfig@759c9ad9]
2015-04-14T02:49:08,275 INFO [ZkCoordinator-0] io.druid.java.util.common.CompressionUtils - Unzipping file[/opt/druid/tmp/compressionUtilZipCache1263964429587449785.zip] to [/opt/druid/zk_druid/dde/2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z/2015-04-14T02:41:09.484Z/0]
2015-04-14T02:49:08,276 INFO [ZkCoordinator-0] io.druid.storage.azure.AzureDataSegmentPuller - Loaded 1196 bytes from [dde/2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z/2015-04-14T02:41:09.484Z/0/index.zip] to [/opt/druid/zk_druid/dde/2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z/2015-04-14T02:41:09.484Z/0]
2015-04-14T02:49:08,277 WARN [ZkCoordinator-0] io.druid.segment.loading.SegmentLoaderLocalCacheManager - Segment [dde_2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z_2015-04-14T02:41:09.484Z] is different than expected size. Expected [0] found [1196]
2015-04-14T02:49:08,282 INFO [ZkCoordinator-0] io.druid.server.coordination.BatchDataSegmentAnnouncer - Announcing segment[dde_2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z_2015-04-14T02:41:09.484Z] at path[/druid/dev/segments/192.168.33.104:8081/192.168.33.104:8081_historical__default_tier_2015-04-14T02:49:08.282Z_7bb87230ebf940188511dd4a53ffd7351]
2015-04-14T02:49:08,292 INFO [ZkCoordinator-0] io.druid.server.coordination.ZkCoordinator - Completed request [LOAD: dde_2015-01-02T00:00:00.000Z_2015-01-03T00:00:00.000Z_2015-04-14T02:41:09.484Z]
DataSegmentKiller
测试segment kill的最简单的方式就是将一个segment标记为不可使用,然后通过老的Coordinator console启动一个Killing Task。
为了标记一个segment为不可用,你需要连到你的元数据存储,并将这个segment的used列的值置为false。
为了启动一个segment killing task,你需要访问老的Coordinator console (http://<COORDINATOR_IP>:<COORDINATOR_PORT>/old-console/kill.html), 然后选择一个合适的datasource,然后输入一个时间范围。
Killing task运行完成后,descriptor.json (在hdfs上是partitionNum_descriptor.json) 和 index.zip (在hdfs上是partitionNum_index.zip)就都被删掉了。
建一个新的Firehose
下面是一个使用StaticS3FirehoseFactory的s3-extensions模块。
增加一个Firehose基本完全通过Jackson而不是Guice:
@Override
public List<? extends Module> getJacksonModules() {
return ImmutableList.of(
new SimpleModule().registerSubtypes(new NamedType(StaticS3FirehoseFactory.class, "static-s3"))
);
}
这是用Jackson的polymophic serde layer注册的FirehoseFactory。更具体的,这就意味着如果你在实时配置中定义一个 "firehose": {"type": "static-s3", ...} ,系统就会load这个FirehoseFactory。
在Druid中,我们使用@JacksonInject注解反序列化成对象,然后使用基本的Guice注入到系统中。因此,如果你的FirehoseFactory需要访问一些对象,你可以在setter上增加一个@JacksonInject注解,他就会注入到实例中。
增加Aggregators
增加一个AggregatorFactory对象跟增加Firehose对象非常相似。他们完全是通过Jackson反序列化,然后加到DruidModule返回的Jackson modules中。
增加复杂的Metrics
在现有版本中增加一个ComplexMetrics有点丑陋。Get一个complex metric是通过ComplexMetrics.registerSerde()方法。 Guice没有其他的方法做这个工作,只是在configure(Binder)方法中注册serde。
增加新的查询类型
增加新的查询类型需要实现以下三个接口:
1. io.druid.query.Query
2. io.druid.query.QueryToolChest
3. io.druid.query.QueryRunnerFactory
跟Deep Storage的机制一样,注册她们使用相同的策略。你可以做如下:
DruidBinders.queryToolChestBinder(binder)
.addBinding(SegmentMetadataQuery.class)
.to(SegmentMetadataQueryQueryToolChest.class);
DruidBinders.queryRunnerFactoryBinder(binder)
.addBinding(SegmentMetadataQuery.class)
.to(SegmentMetadataQueryRunnerFactory.class);
增加新的Jersey resources
增加新的Jersey资源到一个module中使用如下方式:
Jerseys.addResource(binder, NewResource.class);
增加一个新的Password Provider实现
你需要实现 io.druid.metadata.PasswordProvider 接口。对每一个Druid使用PasswordProvider的地方,一个新的instance都会被创建。因此,需要保证每次新对象初始化的时候所有的关键信息都必须相应初始化。在你的io.druid.initialization.DruidModule中,getJacksonModules应该如下方式实现:
return ImmutableList.of(
new SimpleModule("SomePasswordProviderModule")
.registerSubtypes(
new NamedType(SomePasswordProvider.class, "some")
)
);
以上SomePasswordProvider是接口PasswordProvider的实现类。可以以io.druid.metadata.EnvironmentVariablePasswordProvider为例。
跟其他Druid扩展一起绑定你的扩展
当你使用mvn install时,所有的Druid扩展会被一起打包在extensions目录下。具体位置在distribution/target/下面。
如果你想你的扩展能够被包含进来,你可以把你扩展的maven coordinator参数加到distribution/pom.xml中。
在mvn install的过程中,maven会讲的你扩展安装到本地仓库中,然后调用pull-deps从那里取你的扩展。。最后,你可以在你的Druid安装包的distribution/target/extensions中找到你的扩展。