0%

DotNetty完全教程(十)

Excerpt

单元测试EmbeddedChannel介绍EmbeddedChannel是专门为了测试ChannelHandler的传输。我们先看一下他的API用一张图来描述这样的一个模拟过程编写基于xUnit的单元测试新建一个xUnit工程 UnitTest新建一个用于测试EmbededChannel的工程 EmbededChannelTestEmbededChannelTest工程需要引用…

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

单元测试

EmbeddedChannel介绍

EmbeddedChannel是专门为了测试ChannelHandler的传输。我们先看一下他的API

用一张图来描述这样的一个模拟过程

编写基于xUnit的单元测试

  1. 新建一个xUnit工程 UnitTest
  2. 新建一个用于测试EmbededChannel的工程 EmbededChannelTest
  3. EmbededChannelTest工程需要引用DotNetty的类库,这里因为我们需要测试一个解码器,所以除了原先的Buffer Common Transport之外我们还需要引用Codecs
  4. xUnit工程需要引用EmbededChannelTest工程
  5. 在EmbededChannelTest工程之下新建FixedLengthFrameDecoder待测试类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using DotNetty.Buffers;
using DotNetty.Codecs;
using DotNetty.Transport.Channels;
using System;
using System.Collections.Generic;
using System.Text;

namespace EmbededChannelTest
{
public class FixedLengthFrameDecoder : ByteToMessageDecoder
{
private int _frameLength;
public FixedLengthFrameDecoder(int frameLength)
{
if (frameLength <= 0)
{
throw new Exception("不合法的参数。");
}
_frameLength = frameLength;
}
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
// 解码器实现固定的帧长度
while (input.ReadableBytes >= _frameLength)
{
IByteBuffer buf = input.ReadBytes(_frameLength);
output.Add(buf);
}
}
}
}

我们可以看到这个解码器将buffer中的字节流转化为每3个一帧。接下来我们需要编写测试类,我们在UnitTest工程下新建一个类,名字叫做UnitTester,编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using DotNetty.Buffers;
using DotNetty.Transport.Channels.Embedded;
using EmbededChannelTest;
using System;
using System.Collections.Generic;
using System.Text;
using Xunit;

namespace UnitTest
{
public class UnitTester
{
[Fact]
public void testFrameDecoder()
{
IByteBuffer buf = Unpooled.Buffer();
for (int i = 0; i < 9; i++)
{
buf.WriteByte(i);
}
IByteBuffer input = buf.Duplicate();
EmbeddedChannel channel = new EmbeddedChannel(new FixedLengthFrameDecoder(3));
// 写数据
// retain能够将buffer的引用计数加1,并且返回这个buffer本身
Assert.True(channel.WriteInbound(input.Retain()));
Assert.True(channel.Finish());
// 读数据
IByteBuffer read = channel.ReadInbound<IByteBuffer>();
Assert.Equal(buf.ReadSlice(3), read);
read.Release();

read = channel.ReadInbound<IByteBuffer>();
Assert.Equal(buf.ReadSlice(3), read);
read.Release();

read = channel.ReadInbound<IByteBuffer>();
Assert.Equal(buf.ReadSlice(3), read);
read.Release();

Assert.Null(channel.ReadInbound<object>());
buf.Release();
}
}
}

编写完成之后直接右键点击运行测试即可。同理我们可以测试用于出站数据的Encoder,这里不贴代码了,感兴趣的可以去工程中自己看源码进行学习。

DotNetty完全教程(四)

Excerpt

ByteBufferNetty中ByteBuffer的介绍Netty 的数据处理 API 通过两个组件暴露——abstract class ByteBuf 和 interfaceByteBufHolderDotNetty中有AbstractByteBuffer IByteBuffer IByteBufferHolder优点:它可以被用户自定义的缓冲区类型扩展;通过内置的复合缓冲区…


ByteBuffer

Netty中ByteBuffer的介绍

Netty 的数据处理 API 通过两个组件暴露——abstract class ByteBuf 和 interface
ByteBufHolder

DotNetty中有AbstractByteBuffer IByteBuffer IByteBufferHolder

优点:

  • 它可以被用户自定义的缓冲区类型扩展;
  • 通过内置的复合缓冲区类型实现了透明的零拷贝;
  • 容量可以按需增长(类似于 JDK 的 StringBuilder);
  • 在读和写这两种模式之间切换不需要调用 ByteBuffer 的 flip()方法;
  • 读和写使用了不同的索引;
  • 支持方法的链式调用;
  • 支持引用计数;
  • 支持池化

原理:

每一个ByteBuf都有两个索引,读索引和写索引,read和write会移动索引,set和get不会引动索引。

使用ByteBuf

  1. 堆缓冲区(使用数组的方式展示和操作数据)

使用支撑数组给ByteBuf提供快速的分配和释放的能力。适用于有遗留数据需要处理的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
// 检查是否有支撑数组
if (message.HasArray)
{
// 获取数组
byte[] array = message.Array;
// 计算第一个字节的偏移
int offset = message.ArrayOffset + message.ReaderIndex;
// 获取可读字节数
int length = message.ReadableBytes;
// 调用方法,处理数据
HandleArray(array, offset, length);
}
Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}
  1. 直接缓冲区
1
2
3
4
5
6
7
8
9
10
11
12
13
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
if (message.HasArray)
{
int length = message.ReadableBytes;
byte[] array = new byte[length];
message.GetBytes(message.ReaderIndex, array);
HandleArray(array, 0, length);
}
Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}
  1. CompositeByteBuffer 复合缓冲区

如果要发送的命令是由两个ByteBuf拼接构成的,那么就需要复合缓冲区,比如Http协议中一个数据流由头跟内容构成这样的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
// 创建一个复合缓冲区
CompositeByteBuffer messageBuf = Unpooled.CompositeBuffer();
// 创建两个ByteBuffer
IByteBuffer headBuf = Unpooled.CopiedBuffer(message);
IByteBuffer bodyBuf = Unpooled.CopiedBuffer(message);
// 添加到符合缓冲区中
messageBuf.AddComponents(headBuf, bodyBuf);
// 删除
messageBuf.RemoveComponent(0);

Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}

字节级操作

  1. 读取(不移动索引)
1
2
3
4
5
6
7
8
9
10
11
12
13
public override void ChannelRead(IChannelHandlerContext ctx, object msg)
{
IByteBuffer message = msg as IByteBuffer;
for (int i = 0; i < message.Capacity; i++)
{
// 如此使用索引访问不会改变读索引也不会改变写索引
byte b = message.GetByte(i);
Console.WriteLine(b);
}

Console.WriteLine("收到信息:" + message.ToString(Encoding.UTF8));
ctx.WriteAsync(message);
}
  1. 丢弃可丢弃字节
    所谓可丢弃字节就是调用read方法之后,readindex已经移动过了的区域,这段区域的字节称为可丢弃字节。
1
message.DiscardReadBytes();

只有在内存十分宝贵需要清理的时候再调用这个方法,随便调用有可能会造成内存的复制,降低效率。
3. 读取所有可读字节(移动读索引)

1
2
3
4
while (message.IsReadable())
{
Console.WriteLine(message.ReadByte());
}
  1. 写入数据
1
2
3
4
5
// 使用随机数填充可写区域
while (message.WritableBytes > 4)
{
message.WriteInt(new Random().Next(0, 100));
}
  1. 管理索引
  • MarkReaderIndex ResetReaderIndex 标记和恢复读索引
  • MarkWriterIndex ResetWriterIndex 标记和恢复写索引
  • SetReaderIndex(int) SetWriterIndex(int) 直接移动索引
  • clear() 重置两个索引都为0,但是不会清除内容
  1. 查找
  • IndexOf()
  • 使用Processor
1
2
// 查找\r
message.ForEachByte(ByteProcessor.FindCR);
  1. 派生

派生的意思是创建一个新的ByteBuffer,这个ByteBuf派生于其他的ByteBuf,派生出来的子ByteBuf具有自己的读写索引,但是本质上指向同一个对象,这样就导致了改变一个,另一个也会改变。

  • duplicate();
  • slice();
  • slice(int, int);
  • Unpooled.unmodifiableBuffer(…);
  • order(ByteOrder);
  • readSlice(int)。
  1. 复制
    复制不同于派生,会复制出一个独立的ByteBuf,修改其中一个不会改变另一个。
  • copy
  1. 释放
1
2
// 显式丢弃消息
ReferenceCountUtil.release(msg);
  1. 增加引用计数防止释放
1
ReferenceCountUtil.retain(message)
  1. 其他api
    在这里插入图片描述

ByteBufHolder

  1. 目的
    再数据处理的过程中不仅仅有字节数据内容本身,还会有一些附加信息,比如HTTP响应的状态码,Cookie等。给ByteBuf附加信息就要用到ByteBufHolder.
  2. API

管理ByteBuffer

  1. 按需分配 ByteBufAllocator

    注意分配是池化的,最大程度上降低分配和释放内存的开销。

    1
    2
    3
    4
    5
    6
    7
    // 获取Allocator
    // 1
    IChannelHandlerContext ctx = null;
    IByteBufferAllocator allocator = ctx.Allocator;
    // 2
    IChannel channel = null;
    allocator = channel.Allocator;

有两种ByteBufAllocator的实现:PooledByteBufAllocator和UnpooledByteBufAllocator,前者池化了ByteBuf的实例,极大限度的提升了性能减少了内存碎片。
2. Unpooled缓冲区
获取不到 ByteBufAllocator的引用的时候我们可以使用Unpooled工具类来操作ByteBuf。
在这里插入图片描述

  1. ByteBufUtil
    这个类提供了一些通用的API,都是静态的辅助方法,例如hexdump方法可以以十六进制的方式打印ByteBuf的内容。还有equal方法判断bytebuf是否相等。

引用计数

  1. 目的

    ByteBuf和ByteBufHolder都有计数的机制。引用计数都从1开始,如果计数大于0则不被释放,如果等于0就会被释放。它的目的是为了支持池化的实现,降低了内存分配的开销。

  2. 异常

    如果访问一个计数为0的对象就会引发IllegalReferenceCountException。

DotNetty系列三:编码解码器,IdleStateHandler心跳机制

Excerpt

在上一节基础上,实现编码解码器。1.创建一个类库项目。用于实现编码解码器。编码器: public class CommonServerEncoder : MessageToByteEncoder<string> { protected override void Encode(IChannelHandlerContext context, s…


在上一节基础上,实现编码解码器。

1.创建一个类库项目。用于实现编码解码器。

编码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CommonServerEncoder : MessageToByteEncoder<string>    
{
protected override void Encode(IChannelHandlerContext context, string message, IByteBuffer output)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IByteBuffer initialMessage = Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
output.WriteBytes(initialMessage);
}
}

public class CommonClientEncoder : MessageToByteEncoder<string>
{
protected override void Encode(IChannelHandlerContext context, string message, IByteBuffer output)
{
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
IByteBuffer initialMessage = Unpooled.Buffer(messageBytes.Length);
initialMessage.WriteBytes(messageBytes);
output.WriteBytes(initialMessage);
}
}

解码器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class CommonServerDecoder : ByteToMessageDecoder    
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
byte[] array = new byte[input.ReadableBytes];
input.GetBytes(input.ReaderIndex, array, 0, input.ReadableBytes);
input.Clear();
output.Add(array);
}
}

public class CommonClientDecoder : ByteToMessageDecoder
{
protected override void Decode(IChannelHandlerContext context, IByteBuffer input, List<object> output)
{
byte[] array = new byte[input.ReadableBytes];
input.GetBytes(input.ReaderIndex, array, 0, input.ReadableBytes);
input.Clear();
output.Add(array);
}
}

2.服务端里添加:

                        //配置编码解码器
                        pipeline.AddLast(new CommonServerEncoder());
                        pipeline.AddLast(new CommonServerDecoder());

客户端里添加:

                        //配置编码解码器
                        pipeline.AddLast(new CommonClientEncoder());
                        pipeline.AddLast(new CommonClientDecoder());

3.服务端接收和发送:

1
2
3
4
5
6
7
8
9
public override void ChannelRead(IChannelHandlerContext context, object message)        
{
if (message is byte[] o)
{
Console.WriteLine($"解码器方式,从客户端接收:{Encoding.UTF8.GetString(o)}:{DateTime.Now}");
}
string msg = "服务端从客户端接收到内容后返回,我是服务端";
context.WriteAsync(msg);
}

客户端接收和发送:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public override void ChannelActive(IChannelHandlerContext context)        
{
Console.WriteLine("我是客户端.");
Console.WriteLine($"连接至服务端{context}.");
string message = "客户端1";
context.WriteAndFlushAsync(message);
}

public override void ChannelRead(IChannelHandlerContext context, object message)
{
if (message is byte[] o)
{
Console.WriteLine($"解码器方式,从服务端接收:{Encoding.UTF8.GetString(o)}:{DateTime.Now}");
}
}

实现了上一节一样的效果。

4.IdleStateHandler心跳机制:

4.1服务端添加IdleStateHandler心跳检测处理器,添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理.

IdleStateHandler心跳检测每十五秒进行一次读检测,如果十五秒内ChannelRead()方法未被调用则触发一次userEventTrigger()方法.

                        // IdleStateHandler 心跳
                        //服务端为读IDLE
                        pipeline.AddLast(new IdleStateHandler(15, 0, 0));//第一个参数为读,第二个为写,第三个为读写全部

4.2服务端Handler重载UserEventTriggered:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int lossConnectCount = 0;
public override void UserEventTriggered(IChannelHandlerContext context, object evt) {
Console.WriteLine("已经15秒未收到客户端的消息了!");
if (evt is IdleStateEvent eventState)
{
if (eventState.State == IdleState.ReaderIdle)
{
lossConnectCount++;if (lossConnectCount > 2)
{
Console.WriteLine("关闭这个不活跃通道!");
context.CloseAsync();
} }
}
else
{
base.UserEventTriggered(context, evt);
}
}

接收部分改为判断心跳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public override void ChannelRead(IChannelHandlerContext context, object message)        
{
if (message is byte[] o)
{
Console.WriteLine($"解码器方式,从客户端接收:{Encoding.UTF8.GetString(o)}:{DateTime.Now}");
if (Encoding.UTF8.GetString(o).Contains("biubiu:"))
{
string temp = "服务端接收到心跳连接";
context.WriteAsync(temp);return;
}
}
string msg = "服务端从客户端接收到内容后返回,我是服务端";
context.WriteAsync(msg);
}

4.3客户端添加IdleStateHandler心跳检测处理器,并添加自定义处理Handler类实现userEventTriggered()方法作为超时事件的逻辑处理;

设定IdleStateHandler心跳检测每十秒进行一次写检测,如果十秒内write()方法未被调用则触发一次userEventTrigger()方法,实现客户端每十秒向服务端发送一次消息;

                        // IdleStateHandler 心跳
                        //客户端为写IDLE
                        pipeline.AddLast(new IdleStateHandler(0, 10, 0));//第一个参数为读,第二个为写,第三个为读写全部

4.4客户端Handler重载UserEventTriggered:

1
2
3
4
5
6
7
8
9
10
public override void UserEventTriggered(IChannelHandlerContext context, object evt)        {           
Console.WriteLine("客户端循环心跳监测发送: " + DateTime.Now);
if (evt is IdleStateEvent eventState)
{
if (eventState.State == IdleState.WriterIdle)
{
context.WriteAndFlushAsync($"biubiu:{DateTime.Now}");
}
}
}

4.5实现效果:

5.群发:将客户端上下线通知,群发至所有客户端。只在服务端修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static volatile IChannelGroup groups;
public override void HandlerAdded(IChannelHandlerContext context)
{
Console.WriteLine($"客户端{context}上线.");

base.HandlerAdded(context);
IChannelGroup g = groups;if (g == null)
{
lock (this)
{
if (groups == null)
{
g = groups = new DefaultChannelGroup(context.Executor);
}
}
}
g.Add(context.Channel);
groups.WriteAndFlushAsync($"欢迎{context.Channel.RemoteAddress}加入.");
}

public override void HandlerRemoved(IChannelHandlerContext context)
{
Console.WriteLine($"客户端{context}下线.");
base.HandlerRemoved(context);
groups.Remove(context.Channel);
groups.WriteAndFlushAsync($"恭送{context.Channel.RemoteAddress}离开.");
}

实现效果:

项目下载地址:项目下载

一、下载链接https://portal.influxdata.com/downloads,选windows版

二、解压到安装盘,目录如下

三、修改conf文件,代码如下,直接复制粘贴(1.4.2版本),注意修改路径,带D盘的改为你的安装路径就好,一共三个,注意网上有配置admin进行web管理,但新版本配置文件里没有admin因为官方给删除了,需下载Chronograf,后文会介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
### Welcome to the InfluxDB configuration file.

# The values in this file override the default values used by the system if
# a config option is not specified. The commented out lines are the configuration
# field and the default value used. Uncommenting a line and changing the value
# will change the value used at runtime when the process is restarted.

# Once every 24 hours InfluxDB will report usage data to usage.influxdata.com
# The data includes a random ID, os, arch, version, the number of series and other
# usage data. No data from user databases is ever transmitted.
# Change this option to true to disable reporting.
# reporting-disabled = false

# Bind address to use for the RPC service for backup and restore.
# bind-address = "127.0.0.1:8088"

###
### [meta]
###
### Controls the parameters for the Raft consensus group that stores metadata
### about the InfluxDB cluster.
###

[meta]
# Where the metadata/raft database is stored
dir = "D:/influxdb-1.4.2-1/meta"

# Automatically create a default retention policy when creating a database.
retention-autocreate = true

# If log messages are printed for the meta service
logging-enabled = true

###
### [data]
###
### Controls where the actual shard data for InfluxDB lives and how it is
### flushed from the WAL. "dir" may need to be changed to a suitable place
### for your system, but the WAL settings are an advanced configuration. The
### defaults should work for most systems.
###

[data]
# The directory where the TSM storage engine stores TSM files.
dir = "D:/influxdb-1.4.2-1/data"

# The directory where the TSM storage engine stores WAL files.
wal-dir = "D:/influxdb-1.4.2-1/wal"

# The amount of time that a write will wait before fsyncing. A duration
# greater than 0 can be used to batch up multiple fsync calls. This is useful for slower
# disks or when WAL write contention is seen. A value of 0s fsyncs every write to the WAL.
# Values in the range of 0-100ms are recommended for non-SSD disks.
# wal-fsync-delay = "0s"


# The type of shard index to use for new shards. The default is an in-memory index that is
# recreated at startup. A value of "tsi1" will use a disk based index that supports higher
# cardinality datasets.
# index-version = "inmem"

# Trace logging provides more verbose output around the tsm engine. Turning
# this on can provide more useful output for debugging tsm engine issues.
# trace-logging-enabled = false

# Whether queries should be logged before execution. Very useful for troubleshooting, but will
# log any sensitive data contained within a query.
query-log-enabled = true

# Settings for the TSM engine

# CacheMaxMemorySize is the maximum size a shard's cache can
# reach before it starts rejecting writes.
# Valid size suffixes are k, m, or g (case insensitive, 1024 = 1k).
# Vaues without a size suffix are in bytes.
# cache-max-memory-size = "1g"

# CacheSnapshotMemorySize is the size at which the engine will
# snapshot the cache and write it to a TSM file, freeing up memory
# Valid size suffixes are k, m, or g (case insensitive, 1024 = 1k).
# Values without a size suffix are in bytes.
# cache-snapshot-memory-size = "25m"

# CacheSnapshotWriteColdDuration is the length of time at
# which the engine will snapshot the cache and write it to
# a new TSM file if the shard hasn't received writes or deletes
# cache-snapshot-write-cold-duration = "10m"

# CompactFullWriteColdDuration is the duration at which the engine
# will compact all TSM files in a shard if it hasn't received a
# write or delete
# compact-full-write-cold-duration = "4h"

# The maximum number of concurrent full and level compactions that can run at one time. A
# value of 0 results in 50% of runtime.GOMAXPROCS(0) used at runtime. Any number greater
# than 0 limits compactions to that value. This setting does not apply
# to cache snapshotting.
# max-concurrent-compactions = 0

# The maximum series allowed per database before writes are dropped. This limit can prevent
# high cardinality issues at the database level. This limit can be disabled by setting it to
# 0.
# max-series-per-database = 1000000

# The maximum number of tag values per tag that are allowed before writes are dropped. This limit
# can prevent high cardinality tag values from being written to a measurement. This limit can be
# disabled by setting it to 0.
# max-values-per-tag = 100000

###
### [coordinator]
###
### Controls the clustering service configuration.
###

[coordinator]
# The default time a write request will wait until a "timeout" error is returned to the caller.
# write-timeout = "10s"

# The maximum number of concurrent queries allowed to be executing at one time. If a query is
# executed and exceeds this limit, an error is returned to the caller. This limit can be disabled
# by setting it to 0.
# max-concurrent-queries = 0

# The maximum time a query will is allowed to execute before being killed by the system. This limit
# can help prevent run away queries. Setting the value to 0 disables the limit.
# query-timeout = "0s"

# The time threshold when a query will be logged as a slow query. This limit can be set to help
# discover slow or resource intensive queries. Setting the value to 0 disables the slow query logging.
# log-queries-after = "0s"

# The maximum number of points a SELECT can process. A value of 0 will make
# the maximum point count unlimited. This will only be checked every second so queries will not
# be aborted immediately when hitting the limit.
# max-select-point = 0

# The maximum number of series a SELECT can run. A value of 0 will make the maximum series
# count unlimited.
# max-select-series = 0

# The maxium number of group by time bucket a SELECT can create. A value of zero will max the maximum
# number of buckets unlimited.
# max-select-buckets = 0

###
### [retention]
###
### Controls the enforcement of retention policies for evicting old data.
###

[retention]
# Determines whether retention policy enforcement enabled.
enabled = true

# The interval of time when retention policy enforcement checks run.
check-interval = "30m"

###
### [shard-precreation]
###
### Controls the precreation of shards, so they are available before data arrives.
### Only shards that, after creation, will have both a start- and end-time in the
### future, will ever be created. Shards are never precreated that would be wholly
### or partially in the past.

[shard-precreation]
# Determines whether shard pre-creation service is enabled.
enabled = true

# The interval of time when the check to pre-create new shards runs.
check-interval = "10m"

# The default period ahead of the endtime of a shard group that its successor
# group is created.
advance-period = "30m"

###
### Controls the system self-monitoring, statistics and diagnostics.
###
### The internal database for monitoring data is created automatically if
### if it does not already exist. The target retention within this database
### is called 'monitor' and is also created with a retention period of 7 days
### and a replication factor of 1, if it does not exist. In all cases the
### this retention policy is configured as the default for the database.

[monitor]
# Whether to record statistics internally.
store-enabled = true

# The destination database for recorded statistics
store-database = "_internal"

# The interval at which to record statistics
store-interval = "10s"

###
### [http]
###
### Controls how the HTTP endpoints are configured. These are the primary
### mechanism for getting data into and out of InfluxDB.
###

[http]
# Determines whether HTTP endpoint is enabled.
enabled = true

# The bind address used by the HTTP service.
bind-address = ":8086"

# Determines whether user authentication is enabled over HTTP/HTTPS.
# auth-enabled = false

# The default realm sent back when issuing a basic auth challenge.
# realm = "InfluxDB"

# Determines whether HTTP request logging is enabled.
# log-enabled = true

# Determines whether detailed write logging is enabled.
# write-tracing = false

# Determines whether the pprof endpoint is enabled. This endpoint is used for
# troubleshooting and monitoring.
# pprof-enabled = true

# Determines whether HTTPS is enabled.
# https-enabled = false

# The SSL certificate to use when HTTPS is enabled.
# https-certificate = "/etc/ssl/influxdb.pem"

# Use a separate private key location.
# https-private-key = ""

# The JWT auth shared secret to validate requests using JSON web tokens.
# shared-secret = ""

# The default chunk size for result sets that should be chunked.
# max-row-limit = 0

# The maximum number of HTTP connections that may be open at once. New connections that
# would exceed this limit are dropped. Setting this value to 0 disables the limit.
# max-connection-limit = 0

# Enable http service over unix domain socket
# unix-socket-enabled = false

# The path of the unix domain socket.
# bind-socket = "/var/run/influxdb.sock"

# The maximum size of a client request body, in bytes. Setting this value to 0 disables the limit.
# max-body-size = 25000000


###
### [ifql]
###
### Configures the ifql RPC API.
###

[ifql]
# Determines whether the RPC service is enabled.
# enabled = true

# Determines whether additional logging is enabled.
# log-enabled = true

# The bind address used by the ifql RPC service.
# bind-address = ":8082"


###
### [subscriber]
###
### Controls the subscriptions, which can be used to fork a copy of all data
### received by the InfluxDB host.
###

[subscriber]
# Determines whether the subscriber service is enabled.
# enabled = true

# The default timeout for HTTP writes to subscribers.
# http-timeout = "30s"

# Allows insecure HTTPS connections to subscribers. This is useful when testing with self-
# signed certificates.
# insecure-skip-verify = false

# The path to the PEM encoded CA certs file. If the empty string, the default system certs will be used
# ca-certs = ""

# The number of writer goroutines processing the write channel.
# write-concurrency = 40

# The number of in-flight writes buffered in the write channel.
# write-buffer-size = 1000


###
### [[graphite]]
###
### Controls one or many listeners for Graphite data.
###

[[graphite]]
# Determines whether the graphite endpoint is enabled.
# enabled = false
# database = "graphite"
# retention-policy = ""
# bind-address = ":2003"
# protocol = "tcp"
# consistency-level = "one"

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Batching
# will buffer points in memory if you have many coming in.

# Flush if this many points get buffered
# batch-size = 5000

# number of batches that may be pending in memory
# batch-pending = 10

# Flush at least this often even if we haven't hit buffer limit
# batch-timeout = "1s"

# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
# udp-read-buffer = 0

### This string joins multiple matching 'measurement' values providing more control over the final measurement name.
# separator = "."

### Default tags that will be added to all metrics. These can be overridden at the template level
### or by tags extracted from metric
# tags = ["region=us-east", "zone=1c"]

### Each template line requires a template pattern. It can have an optional
### filter before the template and separated by spaces. It can also have optional extra
### tags following the template. Multiple tags should be separated by commas and no spaces
### similar to the line protocol format. There can be only one default template.
# templates = [
# "*.app env.service.resource.measurement",
# # Default template
# "server.*",
# ]

###
### [collectd]
###
### Controls one or many listeners for collectd data.
###

[[collectd]]
# enabled = false
# bind-address = ":25826"
# database = "collectd"
# retention-policy = ""
#
# The collectd service supports either scanning a directory for multiple types
# db files, or specifying a single db file.
# typesdb = "/usr/local/share/collectd"
#
# security-level = "none"
# auth-file = "/etc/collectd/auth_file"

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Batching
# will buffer points in memory if you have many coming in.

# Flush if this many points get buffered
# batch-size = 5000

# Number of batches that may be pending in memory
# batch-pending = 10

# Flush at least this often even if we haven't hit buffer limit
# batch-timeout = "10s"

# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
# read-buffer = 0

# Multi-value plugins can be handled two ways.
# "split" will parse and store the multi-value plugin data into separate measurements
# "join" will parse and store the multi-value plugin as a single multi-value measurement.
# "split" is the default behavior for backward compatability with previous versions of influxdb.
# parse-multivalue-plugin = "split"
###
### [opentsdb]
###
### Controls one or many listeners for OpenTSDB data.
###

[[opentsdb]]
# enabled = false
# bind-address = ":4242"
# database = "opentsdb"
# retention-policy = ""
# consistency-level = "one"
# tls-enabled = false
# certificate= "/etc/ssl/influxdb.pem"

# Log an error for every malformed point.
# log-point-errors = true

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Only points
# metrics received over the telnet protocol undergo batching.

# Flush if this many points get buffered
# batch-size = 1000

# Number of batches that may be pending in memory
# batch-pending = 5

# Flush at least this often even if we haven't hit buffer limit
# batch-timeout = "1s"

###
### [[udp]]
###
### Controls the listeners for InfluxDB line protocol data via UDP.
###

[[udp]]
# enabled = false
# bind-address = ":8089"
# database = "udp"
# retention-policy = ""

# These next lines control how batching works. You should have this enabled
# otherwise you could get dropped metrics or poor performance. Batching
# will buffer points in memory if you have many coming in.

# Flush if this many points get buffered
# batch-size = 5000

# Number of batches that may be pending in memory
# batch-pending = 10

# Will flush at least this often even if we haven't hit buffer limit
# batch-timeout = "1s"

# UDP Read buffer size, 0 means OS default. UDP listener will fail if set above OS max.
# read-buffer = 0

###
### [continuous_queries]
###
### Controls how continuous queries are run within InfluxDB.
###

[continuous_queries]
# Determines whether the continuous query service is enabled.
# enabled = true

# Controls whether queries are logged when executed by the CQ service.
# log-enabled = true

# Controls whether queries are logged to the self-monitoring data store.
# query-stats-enabled = false

# interval for how often continuous queries will be checked if they need to run
# run-interval = "1s"

四、使配置生效并打开数据库连接,双击influxd.exe就好,然后双击influx.exe进行操作,网上有操作教程,注意操作数据库时不能关闭influxd.exe,我不知道为什么总有这么个提示:There was an error writing history file: open : The system cannot find the file specified.不过好像没啥影响

五、要使用web管理需要下载Chronograf,https://portal.influxdata.com/downloads第三个就是,下载完直接解压,双击exe程序,在浏览器输入http://localhost:8888/,一开始登录要账户密码,我都用admin就进去了

这个是查看建立的数据库

这个是查看数据库的数据

没了

关于子仓库或者说是仓库共用,git官方推荐的工具是git subtree。 我自己也用了一段时间的git subtree,感觉比git submodule好用,但是也有一些缺点,在可接受的范围内。
所以对于仓库共用,在git subtree 与 git submodule之中选择的话,我推荐git subtree。

git subtree 可以实现一个仓库作为其他仓库的子仓库。

使用git subtree 有以下几个原因:

  • 旧版本的git也支持(最老版本可以到 v1.5.2).
  • git subtree与git submodule不同,它不增加任何像.gitmodule这样的新的元数据文件.
  • git subtree对于项目中的其他成员透明,意味着可以不知道git subtree的存在.

当然,git subtree也有它的缺点,但是这些缺点还在可以接受的范围内:

  • 必须学习新的指令(如:git subtree).
  • 子仓库的更新与推送指令相对复杂。

git subtree的主要命令有:

1
2
3
4
5
6
git subtree add   --prefix=<prefix> <commit>
git subtree add --prefix=<prefix> <repository> <ref>
git subtree pull --prefix=<prefix> <repository> <ref>
git subtree push --prefix=<prefix> <repository> <ref>
git subtree merge --prefix=<prefix> <commit>
git subtree split --prefix=<prefix> [OPTIONS] [<commit>]

准备

我们先准备一个仓库叫photoshop,一个仓库叫libpng,然后我们希望把libpng作为photoshop的子仓库。
photoshop的路径为https://github.com/test/photoshop.git,仓库里的文件有:

1
2
3
4
5
6
photoshop
|
|-- photoshop.c
|-- photoshop.h
|-- main.c
\-- README.md

libPNG的路径为https://github.com/test/libpng.git,仓库里的文件有:

1
2
3
4
5
libpng
|
|-- libpng.c
|-- libpng.h
\-- README.md

以下操作均位于父仓库的根目录中。

在父仓库中新增子仓库

我们执行以下命令把libpng添加到photoshop中:

1
git subtree add --prefix=sub/libpng https://github.com/test/libpng.git master --squash

(--squash参数表示不拉取历史信息,而只生成一条commit信息。)

执行git status可以看到提示新增两条commit:

image

git log查看详细修改:

image

执行git push把修改推送到远端photoshop仓库,现在本地仓库与远端仓库的目录结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
photoshop
|
|-- sub/
| |
| \--libpng/
| |
| |-- libpng.c
| |-- libpng.h
| \-- README.md
|
|-- photoshop.c
|-- photoshop.h
|-- main.c
\-- README.md

注意,现在的photoshop仓库对于其他项目人员来说,可以不需要知道libpng是一个子仓库。什么意思呢?
当你git clone或者git pull的时候,你拉取到的是整个photoshop(包括libpng在内,libpng就相当于photoshop里的一个普通目录);当你修改了libpng里的内容后执行git push,你将会把修改push到photoshop上。
也就是说photoshop仓库下的libpng与其他文件无异。

从源仓库拉取更新

如果源libpng仓库更新了,photoshop里的libpng如何拉取更新?使用git subtree pull,例如:

1
git subtree pull --prefix=sub/libpng https://github.com/test/libpng.git master --squash

推送修改到源仓库

如果在photoshop仓库里修改了libpng,然后想把这个修改推送到源libpng仓库呢?使用git subtree push,例如:

1
git subtree push --prefix=sub/libpng https://github.com/test/libpng.git master

简化git subtree命令

我们已经知道了git subtree 的命令的基本用法,但是上述几个命令还是显得有点复杂,特别是子仓库的源仓库地址,特别不方便记忆。
这里我们把子仓库的地址作为一个remote,方便记忆:

1
git remote add -f libpng https://github.com/test/libpng.git

然后可以这样来使用git subtree命令:

1
2
3
git subtree add --prefix=sub/libpng libpng master --squash
git subtree pull --prefix=sub/libpng libpng master --squash
git subtree push --prefix=sub/libpng libpng master

InfluxDB是一个开源的时序数据库,使用GO语言开发,特别适合用于处理和分析资源监控数据这种时序相关数据。而InfluxDB自带的各种特殊函数如求标准差,随机取样数据,统计数据变化比等,使数据统计和实时分析变得十分方便。在我们的容器资源监控系统中,就采用了InfluxDB存储cadvisor的监控数据。本文对InfluxDB的基本概念和一些特色功能做一个详细介绍,内容主要是翻译整理自官网文档,如有错漏,请指正。

这里说一下使用docker容器运行influxdb的步骤,物理机安装请参照官方文档。拉取镜像文件后运行即可,当前最新版本是1.3.5。启动容器时设置挂载的数据目录和开放端口。InfluxDB的操作语法InfluxQL与SQL基本一致,也提供了一个类似mysql-client的名为influx的CLI。InfluxDB本身是支持分布式部署多副本存储的,本文介绍都是针对的单节点单副本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17


f216e9be15bff545befecb30d1d275552026216a939cc20c042b17419e3bde31

root@f216e9be15bf:/
Connected to http:
InfluxDB shell version: 1.3.5
> create database cadvisor
> show databases
name: databases
name
----
_internal
cadvisor
> CREATE USER testuser WITH PASSWORD 'testpwd'
> GRANT ALL PRIVILEGES ON cadvisor TO testuser
> CREATE RETENTION POLICY "cadvisor_retention" ON "cadvisor" DURATION 30d REPLICATION 1 DEFAULT

influxdb里面有一些重要概念:database,timestamp,field key, field value, field set,tag key,tag value,tag set,measurement, retention policy ,series,point。结合下面的例子数据来说明这几个概念:

1
2
3
4
5
6
7
8
9
10
11
name: census
-————————————
time butterflies honeybees location scientist
2015-08-18T00:00:00Z 12 23 1 langstroth
2015-08-18T00:00:00Z 1 30 1 perpetua
2015-08-18T00:06:00Z 11 28 1 langstroth
2015-08-18T00:06:00Z 3 28 1 perpetua
2015-08-18T05:54:00Z 2 11 2 langstroth
2015-08-18T06:00:00Z 1 10 2 langstroth
2015-08-18T06:06:00Z 8 23 2 perpetua
2015-08-18T06:12:00Z 7 22 2 perpetua

timestamp

既然是时间序列数据库,influxdb的数据都有一列名为time的列,里面存储UTC时间戳。

field key,field value,field set

butterflies和honeybees两列数据称为字段(fields),influxdb的字段由field key和field value组成。其中butterflies和honeybees为field key,它们为string类型,用于存储元数据。

而butterflies这一列的数据12-7为butterflies的field value,同理,honeybees这一列的23-22为honeybees的field value。field value可以为string,float,integer或boolean类型。field value通常都是与时间关联的。

field key和field value对组成的集合称之为field set。如下:

1
2
3
4
5
6
7
8
butterflies = 12 honeybees = 23
butterflies = 1 honeybees = 30
butterflies = 11 honeybees = 28
butterflies = 3 honeybees = 28
butterflies = 2 honeybees = 11
butterflies = 1 honeybees = 10
butterflies = 8 honeybees = 23
butterflies = 7 honeybees = 22

在influxdb中,字段必须存在。注意,字段是没有索引的。如果使用字段作为查询条件,会扫描符合查询条件的所有字段值,性能不及tag。类比一下,fields相当于SQL的没有索引的列。

tag key,tag value,tag set

location和scientist这两列称为标签(tags),标签由tag key和tag value组成。location这个tag key有两个tag value:1和2,scientist有两个tag value:langstroth和perpetua。tag key和tag value对组成了tag set,示例中的tag set如下:

1
2
3
4
location = 1, scientist = langstroth
location = 2, scientist = langstroth
location = 1, scientist = perpetua
location = 2, scientist = perpetua

tags是可选的,但是强烈建议你用上它,因为tag是有索引的,tags相当于SQL中的有索引的列。tag value只能是string类型 如果你的常用场景是根据butterflies和honeybees来查询,那么你可以将这两个列设置为tag,而其他两列设置为field,tag和field依据具体查询需求来定。

measurement

measurement是fields,tags以及time列的容器,measurement的名字用于描述存储在其中的字段数据,类似mysql的表名。如上面例子中的measurement为census。measurement相当于SQL中的表,本文中我在部分地方会用表来指代measurement。

retention policy

retention policy指数据保留策略,示例数据中的retention policy为默认的autogen。它表示数据一直保留永不过期,副本数量为1。你也可以指定数据的保留时间,如30天。

series

series是共享同一个retention policy,measurement以及tag set的数据集合。示例中数据有4个series,如下:

Arbitrary series number

Retention policy

Measurement

Tag set

series 1

autogen

census

location = 1,scientist = langstroth

series 2

autogen

census

location = 2,scientist = langstroth

series 3

autogen

census

location = 1,scientist = perpetua

series 4

autogen

census

location = 2,scientist = perpetua

point

point则是同一个series中具有相同时间的field set,points相当于SQL中的数据行。如下面就是一个point:

1
2
3
4
name: census
-----------------
time butterflies honeybees location scientist
2015-08-18T00:00:00Z 1 30 1 perpetua

database

上面提到的结构都存储在数据库中,示例的数据库为my_database。一个数据库可以有多个measurement,retention policy, continuous queries以及user。influxdb是一个无模式的数据库,可以很容易的添加新的measurement,tags,fields等。而它的操作却和传统的数据库一样,可以使用类SQL语言查询和修改数据。

influxdb不是一个完整的CRUD数据库,它更像是一个CR-ud数据库。它优先考虑的是增加和读取数据而不是更新和删除数据的性能,而且它阻止了某些更新和删除行为使得创建和读取数据更加高效。

influxdb函数分为聚合函数,选择函数,转换函数,预测函数等。除了与普通数据库一样提供了基本操作函数外,还提供了一些特色函数以方便数据统计计算,下面会一一介绍其中一些常用的特色函数。

  • 聚合函数:FILL(), INTEGRAL()SPREAD()STDDEV()MEAN(), MEDIAN()等。
  • 选择函数: SAMPLE(), PERCENTILE(), FIRST(), LAST(), TOP(), BOTTOM()等。
  • 转换函数: DERIVATIVE(), DIFFERENCE()等。
  • 预测函数:HOLT_WINTERS()

先从官网导入测试数据(注:这里测试用的版本是1.3.1,最新版本是1.3.5):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ curl https://s3.amazonaws.com/noaa.water-database/NOAA_data.txt -o NOAA_data.txt
$ influx -import -path=NOAA_data.txt -precision=s -database=NOAA_water_database
$ influx -precision rfc3339 -database NOAA_water_database
Connected to http://localhost:8086 version 1.3.1
InfluxDB shell 1.3.1
> show measurements
name: measurements
name
----
average_temperature
distincts
h2o_feet
h2o_pH
h2o_quality
h2o_temperature

> show series from h2o_feet;
key
---
h2o_feet,location=coyote_creek
h2o_feet,location=santa_monica

下面的例子都以官方示例数据库来测试,这里只用部分数据以方便观察。measurement为h2o_feet,tag key为location,field key有level descriptionwater_level两个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> SELECT * FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time level description location water_level
---- ----------------- -------- -----------
2015-08-18T00:00:00Z between 6 and 9 feet coyote_creek 8.12
2015-08-18T00:00:00Z below 3 feet santa_monica 2.064
2015-08-18T00:06:00Z between 6 and 9 feet coyote_creek 8.005
2015-08-18T00:06:00Z below 3 feet santa_monica 2.116
2015-08-18T00:12:00Z between 6 and 9 feet coyote_creek 7.887
2015-08-18T00:12:00Z below 3 feet santa_monica 2.028
2015-08-18T00:18:00Z between 6 and 9 feet coyote_creek 7.762
2015-08-18T00:18:00Z below 3 feet santa_monica 2.126
2015-08-18T00:24:00Z between 6 and 9 feet coyote_creek 7.635
2015-08-18T00:24:00Z below 3 feet santa_monica 2.041
2015-08-18T00:30:00Z between 6 and 9 feet coyote_creek 7.5
2015-08-18T00:30:00Z below 3 feet santa_monica 2.051

GROUP BY,FILL()

如下语句中GROUP BY time(12m),* 表示以每12分钟和tag(location)分组(如果是GROUP BY time(12m)则表示仅每12分钟分组,GROUP BY 参数只能是time和tag)。然后fill(200)表示如果这个时间段没有数据,以200填充,mean(field_key)求该范围内数据的平均值(注意:这是依据series来计算。其他还有SUM求和,MEDIAN求中位数)。LIMIT 7表示限制返回的point(记录数)最多为7条,而SLIMIT 1则是限制返回的series为1个。

注意这里的时间区间,起始时间为整点前包含这个区间第一个12m的时间,比如这里为 2015-08-17T:23:48:00Z,第一条为 2015-08-17T23:48:00Z <= t < 2015-08-18T00:00:00Z这个区间的location=coyote_creekwater_level的平均值,这里没有数据,于是填充的200。第二条为 2015-08-18T00:00:00Z <= t < 2015-08-18T00:12:00Z区间的location=coyote_creekwater_level平均值,这里为 (8.12+8.005)/ 2 = 8.0625,其他以此类推。

GROUP BY time(10m)则表示以10分钟分组,起始时间为包含这个区间的第一个10m的时间,即 2015-08-17T23:40:00Z。默认返回的是第一个series,如果要计算另外那个series,可以在SQL语句后面加上 SOFFSET 1

那如果时间小于数据本身采集的时间间隔呢,比如GROUP BY time(10s)呢?这样的话,就会按10s取一个点,没有数值的为空或者FILL填充,对应时间点有数据则保持不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## GROUP BY time(12m)
> SELECT mean("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m),* fill(200) LIMIT 7 SLIMIT 1
name: h2o_feet
tags: location=coyote_creek
time mean
---- ----
2015-08-17T23:48:00Z 200
2015-08-18T00:00:00Z 8.0625
2015-08-18T00:12:00Z 7.8245
2015-08-18T00:24:00Z 7.5675

## GROUP BY time(10m),SOFFSET设置为1
> SELECT mean("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(10m),* fill(200) LIMIT 7 SLIMIT 1 SOFFSET 1
name: h2o_feet
tags: location=santa_monica
time mean
---- ----
2015-08-17T23:40:00Z 200
2015-08-17T23:50:00Z 200
2015-08-18T00:00:00Z 2.09
2015-08-18T00:10:00Z 2.077
2015-08-18T00:20:00Z 2.041
2015-08-18T00:30:00Z 2.051

INTEGRAL(field_key, unit)

计算数值字段值覆盖的曲面的面积值并得到面积之和。测试数据如下:

1
2
3
4
5
6
7
8
9
10
11
> SELECT "water_level" FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'

name: h2o_feet
time water_level
---- -----------
2015-08-18T00:00:00Z 2.064
2015-08-18T00:06:00Z 2.116
2015-08-18T00:12:00Z 2.028
2015-08-18T00:18:00Z 2.126
2015-08-18T00:24:00Z 2.041
2015-08-18T00:30:00Z 2.051

使用INTERGRAL计算面积。注意,这个面积就是这些点连接起来后与时间围成的不规则图形的面积,注意unit默认是以1秒计算,所以下面语句计算结果为3732.66=2.028*1800+分割出来的梯形和三角形面积。如果unit改为1分,则结果为3732.66/60 = 62.211。unit为2分,则结果为3732.66/120 = 31.1055。以此类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
# unit为默认的1秒
> SELECT INTEGRAL("water_level") FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time integral
---- --------
1970-01-01T00:00:00Z 3732.66

# unit为1分
> SELECT INTEGRAL("water_level", 1m) FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time integral
---- --------
1970-01-01T00:00:00Z 62.211

SPREAD(field_key)

计算数值字段的最大值和最小值的差值。

1
2
3
4
5
6
7
8
> SELECT SPREAD("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m),* fill(18) LIMIT 3 SLIMIT 1 SOFFSET 1
name: h2o_feet
tags: location=santa_monica
time spread
---- ------
2015-08-17T23:48:00Z 18
2015-08-18T00:00:00Z 0.052000000000000046
2015-08-18T00:12:00Z 0.09799999999999986

STDDEV(field_key)

计算字段的标准差。influxdb用的是贝塞尔修正的标准差计算公式 ,如下:

  • mean=(v1+v2+…+vn)/n;
  • stddev = math.sqrt(
    ((v1-mean)2 + (v2-mean)2 + …+(vn-mean)2)/(n-1)
    )
1
2
3
4
5
6
7
8
9
> SELECT STDDEV("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m),* fill(18) SLIMIT 1;
name: h2o_feet
tags: location=coyote_creek
time stddev
---- ------
2015-08-17T23:48:00Z 18
2015-08-18T00:00:00Z 0.08131727983645186
2015-08-18T00:12:00Z 0.08838834764831845
2015-08-18T00:24:00Z 0.09545941546018377

PERCENTILE(field_key, N)

选取某个字段中大于N%的这个字段值。

如果一共有4条记录,N为10,则10%*4=0.4,四舍五入为0,则查询结果为空。N为20,则 20% * 4 = 0.8,四舍五入为1,选取的是4个数中最小的数。如果N为40,40% * 4 = 1.6,四舍五入为2,则选取的是4个数中第二小的数。由此可以看出N=100时,就跟MAX(field_key)是一样的,而当N=50时,与MEDIAN(field_key)在字段值为奇数个时是一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> SELECT PERCENTILE("water_level",20) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m)
name: h2o_feet
time percentile
---- ----------
2015-08-17T23:48:00Z
2015-08-18T00:00:00Z 2.064
2015-08-18T00:12:00Z 2.028
2015-08-18T00:24:00Z 2.041

> SELECT PERCENTILE("water_level",40) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m)
name: h2o_feet
time percentile
---- ----------
2015-08-17T23:48:00Z
2015-08-18T00:00:00Z 2.116
2015-08-18T00:12:00Z 2.126
2015-08-18T00:24:00Z 2.051

SAMPLE(field_key, N)

随机返回field key的N个值。如果语句中有GROUP BY time(),则每组数据随机返回N个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> SELECT SAMPLE("water_level",2) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z';
name: h2o_feet
time sample
---- ------
2015-08-18T00:00:00Z 2.064
2015-08-18T00:12:00Z 2.028

> SELECT SAMPLE("water_level",2) FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z' GROUP BY time(12m);
name: h2o_feet
time sample
---- ------
2015-08-18T00:06:00Z 2.116
2015-08-18T00:06:00Z 8.005
2015-08-18T00:12:00Z 7.887
2015-08-18T00:18:00Z 7.762
2015-08-18T00:24:00Z 7.635
2015-08-18T00:30:00Z 2.051

CUMULATIVE_SUM(field_key)

计算字段值的递增和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> SELECT CUMULATIVE_SUM("water_level") FROM "h2o_feet" WHERE time >= '2015-08-17T23:48:00Z' AND time <= '2015-08-18T00:30:00Z';
name: h2o_feet
time cumulative_sum
---- --------------
2015-08-18T00:00:00Z 8.12
2015-08-18T00:00:00Z 10.184
2015-08-18T00:06:00Z 18.189
2015-08-18T00:06:00Z 20.305
2015-08-18T00:12:00Z 28.192
2015-08-18T00:12:00Z 30.22
2015-08-18T00:18:00Z 37.982
2015-08-18T00:18:00Z 40.108
2015-08-18T00:24:00Z 47.742999999999995
2015-08-18T00:24:00Z 49.78399999999999
2015-08-18T00:30:00Z 57.28399999999999
2015-08-18T00:30:00Z 59.334999999999994

DERIVATIVE(field_key, unit) 和 NON_NEGATIVE_DERIVATIVE(field_key, unit)

计算字段值的变化比。unit默认为1s,即计算的是1秒内的变化比。

如下面的第一个数据计算方法是 (2.116-2.064)/(6*60) = 0.00014..,其他计算方式同理。虽然原始数据是6m收集一次,但是这里的变化比默认是按秒来计算的。如果要按6m计算,则设置unit为6m即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> SELECT DERIVATIVE("water_level") FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time derivative
---- ----------
2015-08-18T00:06:00Z 0.00014444444444444457
2015-08-18T00:12:00Z -0.00024444444444444465
2015-08-18T00:18:00Z 0.0002722222222222218
2015-08-18T00:24:00Z -0.000236111111111111
2015-08-18T00:30:00Z 0.00002777777777777842

> SELECT DERIVATIVE("water_level", 6m) FROM "h2o_feet" WHERE "location" = 'santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z'
name: h2o_feet
time derivative
---- ----------
2015-08-18T00:06:00Z 0.052000000000000046
2015-08-18T00:12:00Z -0.08800000000000008
2015-08-18T00:18:00Z 0.09799999999999986
2015-08-18T00:24:00Z -0.08499999999999996
2015-08-18T00:30:00Z 0.010000000000000231

而DERIVATIVE结合GROUP BY time,以及mean可以构造更加复杂的查询,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> SELECT DERIVATIVE(mean("water_level"), 6m) FROM "h2o_feet" WHERE time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(12m), *
name: h2o_feet
tags: location=coyote_creek
time derivative
---- ----------
2015-08-18T00:12:00Z -0.11900000000000022
2015-08-18T00:24:00Z -0.12849999999999984

name: h2o_feet
tags: location=santa_monica
time derivative
---- ----------
2015-08-18T00:12:00Z -0.00649999999999995
2015-08-18T00:24:00Z -0.015499999999999847

这个计算其实是先根据GROUP BY time求平均值,然后对这个平均值再做变化比的计算。因为数据是按12分钟分组的,而变化比的unit是6分钟,所以差值除以2(12/6)才得到变化比。如第一个值是 (7.8245-8.0625)/2 = -0.1190

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> SELECT mean("water_level") FROM "h2o_feet" WHERE time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(12m), *
name: h2o_feet
tags: location=coyote_creek
time mean
---- ----
2015-08-18T00:00:00Z 8.0625
2015-08-18T00:12:00Z 7.8245
2015-08-18T00:24:00Z 7.5675

name: h2o_feet
tags: location=santa_monica
time mean
---- ----
2015-08-18T00:00:00Z 2.09
2015-08-18T00:12:00Z 2.077
2015-08-18T00:24:00Z 2.0460000000000003

NON_NEGATIVE_DERIVATIVEDERIVATIVE不同的是它只返回的是非负的变化比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> SELECT DERIVATIVE(mean("water_level"), 6m) FROM "h2o_feet" WHERE location='santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(6m), *
name: h2o_feet
tags: location=santa_monica
time derivative
---- ----------
2015-08-18T00:06:00Z 0.052000000000000046
2015-08-18T00:12:00Z -0.08800000000000008
2015-08-18T00:18:00Z 0.09799999999999986
2015-08-18T00:24:00Z -0.08499999999999996
2015-08-18T00:30:00Z 0.010000000000000231

> SELECT NON_NEGATIVE_DERIVATIVE(mean("water_level"), 6m) FROM "h2o_feet" WHERE location='santa_monica' AND time >= '2015-08-18T00:00:00Z' AND time <= '2015-08-18T00:30:00Z' group by time(6m), *
name: h2o_feet
tags: location=santa_monica
time non_negative_derivative
---- -----------------------
2015-08-18T00:06:00Z 0.052000000000000046
2015-08-18T00:18:00Z 0.09799999999999986
2015-08-18T00:30:00Z 0.010000000000000231

4.1 基本语法

连续查询(CONTINUOUS QUERY,简写为CQ)是指定时自动在实时数据上进行的InfluxQL查询,查询结果可以存储到指定的measurement中。基本语法格式如下:

1
2
3
4
5
6
7
8
CREATE CONTINUOUS QUERY <cq_name> ON <database_name>
BEGIN
<cq_query>
END

cq_query格式:
SELECT <function[s]> INTO <destination_measurement> FROM <measurement> [WHERE <stuff>] GROUP BY time(<interval>)[,<tag_key[s]>]

CQ操作的是实时数据,它使用本地服务器的时间戳、GROUP BY time()时间间隔以及InfluxDB预先设置好的时间范围来确定什么时候开始查询以及查询覆盖的时间范围。注意CQ语句里面的WHERE条件是没有时间范围的,因为CQ会根据GROUP BY time()自动确定时间范围。

CQ执行的时间间隔和GROUP BY time()的时间间隔一样,它在InfluxDB预先设置的时间范围的起始时刻执行。如果GROUP BY time(1h),则单次查询的时间范围为 now()-GROUP BY time(1h)now(),也就是说,如果当前时间为17点,这次查询的时间范围为 16:00到16:59.99999。

下面看几个示例,示例数据如下,这是数据库transportation中名为bus_data的measurement,每15分钟统计一次乘客数和投诉数。数据文件bus_data.txt如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# DDL
CREATE DATABASE transportation

# DML
# CONTEXT-DATABASE: transportation

bus_data,complaints=9 passengers=5 1472367600
bus_data,complaints=9 passengers=8 1472368500
bus_data,complaints=9 passengers=8 1472369400
bus_data,complaints=9 passengers=7 1472370300
bus_data,complaints=9 passengers=8 1472371200
bus_data,complaints=7 passengers=15 1472372100
bus_data,complaints=7 passengers=15 1472373000
bus_data,complaints=7 passengers=17 1472373900
bus_data,complaints=7 passengers=20 1472374800

导入数据,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
root@f216e9be15bf:/# influx -import -path=bus_data.txt -precision=s
root@f216e9be15bf:/# influx -precision=rfc3339 -database=transportation
Connected to http://localhost:8086 version 1.3.5
InfluxDB shell version: 1.3.5
> select * from bus_data
name: bus_data
time complaints passengers
---- ---------- ----------
2016-08-28T07:00:00Z 9 5
2016-08-28T07:15:00Z 9 8
2016-08-28T07:30:00Z 9 8
2016-08-28T07:45:00Z 9 7
2016-08-28T08:00:00Z 9 8
2016-08-28T08:15:00Z 7 15
2016-08-28T08:30:00Z 7 15
2016-08-28T08:45:00Z 7 17
2016-08-28T09:00:00Z 7 20

示例1 自动缩小取样存储到新的measurement中

对单个字段自动缩小取样并存储到新的measurement中。

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic" ON "transportation"
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h)
END

这个CQ的意思就是对bus_data每小时自动计算取样数据的平均乘客数并存储到 average_passengers中。那么在2016-08-28这天早上会执行如下流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
At 8:00 cq_basic 执行查询,查询时间范围 time >= '7:00' AND time < '08:00'.
cq_basic写入一条记录到 average_passengers:
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
At 9:00 cq_basic 执行查询,查询时间范围 time >= '8:00' AND time < '9:00'.
cq_basic写入一条记录到 average_passengers:
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 13.75

# Results
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75

示例2 自动缩小取样并存储到新的保留策略(Retention Policy)中

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic_rp" ON "transportation"
BEGIN
SELECT mean("passengers") INTO "transportation"."three_weeks"."average_passengers" FROM "bus_data" GROUP BY time(1h)
END

与示例1类似,不同的是保留的策略不是autogen,而是改成了three_weeks(创建保留策略语法 CREATE RETENTION POLICY "three_weeks" ON "transportation" DURATION 3w REPLICATION 1)。

1
2
3
4
5
6
> SELECT * FROM "transportation"."three_weeks"."average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75

示例3 使用后向引用(backreferencing)自动缩小取样并存储到新的数据库中

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic_br" ON "transportation"
BEGIN
SELECT mean(*) INTO "downsampled_transportation"."autogen".:MEASUREMENT FROM /.*/ GROUP BY time(30m),*
END

使用后向引用语法自动缩小取样并存储到新的数据库中。语法 :MEASUREMENT 用来指代后面的表,而 /.*/则是分别查询所有的表。这句CQ的含义就是每30分钟自动查询transportation的所有表(这里只有bus_data一个表),并将30分钟内数字字段(passengers和complaints)求平均值存储到新的数据库 downsampled_transportation中。

最终结果如下:

1
2
3
4
5
6
7
8
> SELECT * FROM "downsampled_transportation."autogen"."bus_data"
name: bus_data
--------------
time mean_complaints mean_passengers
2016-08-28T07:00:00Z 9 6.5
2016-08-28T07:30:00Z 9 7.5
2016-08-28T08:00:00Z 8 11.5
2016-08-28T08:30:00Z 7 16

示例4 自动缩小取样以及配置CQ的时间范围

1
2
3
4
CREATE CONTINUOUS QUERY "cq_basic_offset" ON "transportation"
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h,15m)
END

与前面几个示例不同的是,这里的GROUP BY time(1h, 15m)指定了一个时间偏移,也就是说 cq_basic_offset执行的时间不再是整点,而是往后偏移15分钟。执行流程如下:

1
2
3
4
5
6
7
8
9
10
At 8:15 cq_basic_offset 执行查询的时间范围 time >= '7:15' AND time < '8:15'.
name: average_passengers
------------------------
time mean
2016-08-28T07:15:00Z 7.75
At 9:15 cq_basic_offset 执行查询的时间范围 time >= '8:15' AND time < '9:15'.
name: average_passengers
------------------------
time mean
2016-08-28T08:15:00Z 16.75

最终结果:

1
2
3
4
5
6
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:15:00Z 7.75
2016-08-28T08:15:00Z 16.75

4.2 高级语法

InfluxDB连续查询的高级语法如下:

1
2
3
4
5
CREATE CONTINUOUS QUERY <cq_name> ON <database_name>
RESAMPLE EVERY <interval> FOR <interval>
BEGIN
<cq_query>
END

与基本语法不同的是,多了RESAMPLE关键字。高级语法里CQ的执行时间和查询时间范围则与RESAMPLE里面的两个interval有关系。

高级语法中CQ以EVERY interval的时间间隔执行,执行时查询的时间范围则是FOR interval来确定。如果FOR interval为2h,当前时间为17:00,则查询的时间范围为15:00-16:59.999999RESAMPLE的EVERY和FOR两个关键字可以只有一个

示例的数据表如下,比之前的多了几条记录为了示例3和示例4的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
name: bus_data
--------------
time passengers
2016-08-28T06:30:00Z 2
2016-08-28T06:45:00Z 4
2016-08-28T07:00:00Z 5
2016-08-28T07:15:00Z 8
2016-08-28T07:30:00Z 8
2016-08-28T07:45:00Z 7
2016-08-28T08:00:00Z 8
2016-08-28T08:15:00Z 15
2016-08-28T08:30:00Z 15
2016-08-28T08:45:00Z 17
2016-08-28T09:00:00Z 20

示例1 只配置执行时间间隔

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_every" ON "transportation"
RESAMPLE EVERY 30m
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h)
END

这里配置了30分钟执行一次CQ,没有指定FOR interval,于是查询的时间范围还是GROUP BY time(1h)指定的一个小时,执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
At 8:00, cq_advanced_every 执行时间范围 time >= '7:00' AND time < '8:00'.
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
At 8:30, cq_advanced_every 执行时间范围 time >= '8:00' AND time < '9:00'.
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 12.6667
At 9:00, cq_advanced_every 执行时间范围 time >= '8:00' AND time < '9:00'.
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 13.75

需要注意的是,这里的 8点到9点这个区间执行了两次,第一次执行时时8:30,平均值是 (8+15+15)/ 3 = 12.6667,而第二次执行时间是9:00,平均值是 (8+15+15+17) / 4=13.75,而且最后第二个结果覆盖了第一个结果。InfluxDB如何处理重复的记录可以参见这个文档

最终结果:

1
2
3
4
5
6
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75

示例2 只配置查询时间范围

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_for" ON "transportation"
RESAMPLE FOR 1h
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(30m)
END

只配置了时间范围,而没有配置EVERY interval。这样,执行的时间间隔与GROUP BY time(30m)一样为30分钟,而查询的时间范围为1小时,由于是按30分钟分组,所以每次会写入两条记录。执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
At 8:00 cq_advanced_for 查询时间范围:time >= '7:00' AND time < '8:00'.
写入两条记录。
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
At 8:30 cq_advanced_for 查询时间范围:time >= '7:30' AND time < '8:30'.
写入两条记录。
name: average_passengers
------------------------
time mean
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
At 9:00 cq_advanced_for 查询时间范围:time >= '8:00' AND time < '9:00'.
写入两条记录。
name: average_passengers
------------------------
time mean
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

需要注意的是,cq_advanced_for每次写入了两条记录,重复的记录会被覆盖。

最终结果:

1
2
3
4
5
6
7
8
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

示例3 同时配置执行时间间隔和查询时间范围

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_every_for" ON "transportation"
RESAMPLE EVERY 1h FOR 90m
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(30m)
END

这里配置了执行间隔为1小时,而查询范围90分钟,最后分组是30分钟,每次插入了三条记录。执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
At 8:00 cq_advanced_every_for 查询时间范围 time >= '6:30' AND time < '8:00'.
插入三条记录
name: average_passengers
------------------------
time mean
2016-08-28T06:30:00Z 3
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
At 9:00 cq_advanced_every_for 查询时间范围 time >= '7:30' AND time < '9:00'.
插入三条记录
name: average_passengers
------------------------
time mean
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

最终结果:

1
2
3
4
5
6
7
8
9
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T06:30:00Z 3
2016-08-28T07:00:00Z 6.5
2016-08-28T07:30:00Z 7.5
2016-08-28T08:00:00Z 11.5
2016-08-28T08:30:00Z 16

示例4 配置查询时间范围和FILL填充

1
2
3
4
5
CREATE CONTINUOUS QUERY "cq_advanced_for_fill" ON "transportation"
RESAMPLE FOR 2h
BEGIN
SELECT mean("passengers") INTO "average_passengers" FROM "bus_data" GROUP BY time(1h) fill(1000)
END

在前面值配置查询时间范围的基础上,加上FILL填充空的记录。执行流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
At 6:00, cq_advanced_for_fill 查询时间范围:time >= '4:00' AND time < '6:00',没有数据,不填充。

At 7:00, cq_advanced_for_fill 查询时间范围:time >= '5:00' AND time < '7:00'. 写入两条记录,没有数据的时间点填充1000。
------------------------
time mean
2016-08-28T05:00:00Z 1000 <------ fill(1000)
2016-08-28T06:00:00Z 3 <------ average of 2 and 4

[…] At 11:00, cq_advanced_for_fill 查询时间范围:time >= '9:00' AND time < '11:00'.写入两条记录,没有数据的点填充1000。
name: average_passengers
------------------------
2016-08-28T09:00:00Z 20 <------ average of 20
2016-08-28T10:00:00Z 1000 <------ fill(1000)

At 12:00, cq_advanced_for_fill 查询时间范围:time >= '10:00' AND time < '12:00'。没有数据,不填充。

最终结果:

1
2
3
4
5
6
7
8
9
10
> SELECT * FROM "average_passengers"
name: average_passengers
------------------------
time mean
2016-08-28T05:00:00Z 1000
2016-08-28T06:00:00Z 3
2016-08-28T07:00:00Z 7
2016-08-28T08:00:00Z 13.75
2016-08-28T09:00:00Z 20
2016-08-28T10:00:00Z 1000

gitea 安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: "2"
services:
web:
image: gitea/gitea
volumes:
- ./data:/data
ports:
- "3000:3000"
restart: always
db:
image: mariadb:10
restart: always
environment:
- MYSQL_ROOT_PASSWORD=xxxx
- MYSQL_DATABASE=gitea
- MYSQL_USER=gitea
- MYSQL_PASSWORD=xxxxx
volumes:
- ./db/:/var/lib/mysql

drone 安装(server + runner)(请参考官方教程)

官方教程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
version: "3"

services:
server:
image: drone/drone:1
container_name: drone_server
restart: always
environment:
DRONE_GITEA_SERVER: https://git.xxxx.cn
DRONE_GITEA_CLIENT_ID: "xxxxx"
DRONE_GITEA_CLIENT_SECRET: "xxxxx"
DRONE_RPC_SECRET: "xxxxx"
DRONE_SERVER_HOST: ci.xxxxx.cn
DRONE_SERVER_PROTO: https
DRONE_USER_CREATE: username:xxxx,admin:true
ports:
- "3001:80"
volumes:
- "./data:/var/lib/drone"

runner:
image: drone/drone-runner-docker
container_name: drone_runner_docker
restart: always
environment:
DRONE_RPC_PROTO: https
DRONE_RPC_HOST: ci.xxxx.cn
DRONE_RPC_SECRET: xxxxx
DRONE_RUNNER_CAPACITY: 5
DRONE_UI_USERNAME: happyxhw
DRONE_UI_PASSWORD: xxxxx
DRONE_RUNNER_NAME: xxxxx
DRONE_RUNNER_LABELS: role:xxxxx
ports:
- "8484:3000"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"

流水线配置(参考示例项目 .drone.yml)

push, pull_request,merge:

  1. linter: 执行 golangci-lint
  2. test
  3. build
  4. notification
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
kind: pipeline
name: drone-golang-example-dev-build

# 指定编译平台
platform:
os: linux
arch: amd64

# 指定代码空间,git代码会被clone到指定的path
workspace:
path: /drone/src

# type: docker, k8s, ssh, exec
type: docker
# 流水线
steps:
- name: linter
image: golang:latest # 编译用的镜像,最好在runner主机上提前下载好,否则会很慢,墙,一生之敌
pull: if-not-exists # 默认always,指定if-not-exists或never可以大幅度提高速度,pull在中国很费时
environment:
GOPROXY: "https://goproxy.cn,direct" # 懂的都懂
volumes: # 缓存 go mod & pkg,可以大幅度提高速度,避免每次都下载
- name: pkgdeps
path: /go/pkg
commands:
- go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0 # golang下好评最高的linter工具,强迫症最爱
- golangci-lint run

- name: test
image: golang:latest
pull: if-not-exists
environment:
GOPROXY: "https://goproxy.cn,direct"
volumes:
- name: pkgdeps
path: /go/pkg
commands:
- go test -v ./...

- name: build
image: golang:latest
pull: if-not-exists
environment:
GOPROXY: "https://goproxy.cn,direct"
volumes:
- name: pkgdeps
path: /go/pkg
commands:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build # 使用alpine务必要指定 GO_ENABLED=0

- name: notification # 钉钉通知,自行探索
image: lddsb/drone-dingtalk-message
pull: if-not-exists
settings:
token:
from_secret: dingtalk_token # 敏感数据请使用 from_secret
type: markdown
secret:
from_secret: dingtalk_secret
sha_link: true
message_color: true
when: # 即使流水线失败也能通知
status:
- success
- failure

trigger: # 触发条件,并关系
branch: # git 分支
- develop
- master
event: # 事件
- push
- pull_request

volumes: # 挂载,持久化数据
- name: pkgdeps
host:
path: /tmp/pkg

node:
role: xxxx # 指定标签为role:xxxx的runner执行流水线

Drone | Continuous Integration

![](基于 gitea + drone + docker 的 CI 流程实践/v2-f9e22bde7e07201d1568f2d952648a7f_b.jpg)

promote:

  1. linter: 执行 golangci-lint
  2. test
  3. build
  4. publish: 发布镜像
  5. scp: 复制部署文件到目标部署主机
  6. deploy: 拉取镜像,重启容器
  7. notification

注意:

我在docker-compose.yml中将镜像的tag设置为了环境变量${DOCKER_TAG},在开发环境中我喜欢将DRONE_COMMIT作为镜像的tag;在生产环境中我喜欢将DRONE_TAG(tag 事件触发生产环境部署)作为镜像tag,这样的好处是可以快速回滚到指定版本。
此外也可以将部署流程自动化到git事件中(master merge后自动部署生产环境,不需要手动promote)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140

# 编译、部署开发镜像, 主动触发
kind: pipeline
name: drone-golang-example-dev-deploy

platform:
os: linux
arch: amd64

workspace:
path: /drone/src

type: docker
steps:
- name: linter
image: golang:latest
pull: if-not-exists
environment:
GOPROXY: "https://goproxy.cn,direct"
volumes:
- name: pkgdeps
path: /go/pkg
commands:
- go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0
- golangci-lint run

- name: test
image: golang:latest
pull: if-not-exists
environment:
GOPROXY: "https://goproxy.cn,direct"
volumes:
- name: pkgdeps
path: /go/pkg
commands:
- go test -v ./...

- name: build
image: golang:latest
pull: if-not-exists
environment:
GOPROXY: "https://goproxy.cn,direct"
volumes:
- name: pkgdeps
path: /go/pkg
commands:
- CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build

- name: publish
image: docker/compose
pull: if-not-exists
environment:
USERNAME:
from_secret: aliyun_registry_username
PASSWORD:
from_secret: aliyun_registry_password
volumes:
- name: dockersock
path: /var/run/docker.sock
commands:
- tag=${DRONE_COMMIT} # 使用 DRONE_COMMIT 作为tag
- echo "publish"
- echo dev_$tag
- export DOCKER_TAG=dev_$tag
- docker login --username=$USERNAME registry.cn-hangzhou.aliyuncs.com -p $PASSWORD
- docker-compose build
- docker-compose push
-
- name: scp
image: appleboy/drone-scp
pull: if-not-exists
settings:
host:
from_secret: dev_host
username:
from_secret: dev_user
password:
from_secret: dev_password
port: 22
target: /home/xuhewen/drone-golang-example
source:
- docker-compose.yml

- name: deploy
image: appleboy/drone-ssh
pull: if-not-exists
settings:
host:
from_secret: dev_host
username:
from_secret: dev_user
password:
from_secret: dev_password
port: 22
script:
- tag=${DRONE_COMMIT} # 请预先在目标主机上执行 docker login
- echo "deploy"
- echo dev_$tag
- cd /home/xuhewen/drone-golang-example
- export DOCKER_TAG=dev_$tag
- docker-compose pull
- docker-compose stop
- docker-compose up -d

- name: notification
image: lddsb/drone-dingtalk-message
pull: if-not-exists
settings:
token:
from_secret: dingtalk_token
type: markdown
secret:
from_secret: dingtalk_secret
sha_link: true
message_color: true
when: # 即使流水线失败也能通知
status:
- success
- failure

volumes:
- name: pkgdeps
host:
path: /tmp/pkg
- name: dockersock
host:
path: /var/run/docker.sock

trigger:
event:
- promote
target:
- develop # 指定部署环境: dev,stage,production

when:
branch:
- develop

node:
role: xxxx

触发 promote:

![](基于 gitea + drone + docker 的 CI 流程实践/v2-c4a5ef1e6b0955dde4d9908d71ccd776_b.jpg)

Drone | Continuous Integration

![](基于 gitea + drone + docker 的 CI 流程实践/v2-296c70343b96b87b68340739ce602616_b.jpg)

成功部署:

![](基于 gitea + drone + docker 的 CI 流程实践/v2-88b6e0e8a778d8781224d27c5f66143a_b.jpg)

rollback:根据 DRONE_COMMIT 或 DRONE_TAG 快速回滚

  1. scp: 复制部署文件(clone时git已经切换到了需要回滚的版本)
  2. rollback: 根据 DRONE_COMMIT 或 DRONE_TAG 拉取指定tag的镜像进行回滚
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
 # 根据commit id回滚
kind: pipeline
name: drone-golang-example-dev-rollback

platform:
os: linux
arch: amd64

workspace:
path: /drone/src

type: docker
steps:
- name: scp
image: appleboy/drone-scp
pull: if-not-exists
settings:
host:
from_secret: dev_host
username:
from_secret: dev_user
password:
from_secret: dev_password
port: 22
target: /home/xuhewen/drone-golang-example
source:
- docker-compose.yml

- name: rollback
image: appleboy/drone-ssh
pull: if-not-exists
settings:
host:
from_secret: dev_host
username:
from_secret: dev_user
password:
from_secret: dev_password
port: 22
script:
- tag=${DRONE_COMMIT}
- echo "deploy"
- echo dev_$tag
- cd /home/xuhewen/drone-golang-example
- export DOCKER_TAG=dev_$tag
- docker-compose pull
- docker-compose stop
- docker-compose up -d

- name: notification
image: lddsb/drone-dingtalk-message
pull: if-not-exists
settings:
token:
from_secret: dingtalk_token
type: markdown
secret:
from_secret: dingtalk_secret
sha_link: true
message_color: true
when:
status:
- success
- failure

volumes:
- name: dockersock
host:
path: /var/run/docker.sock

trigger:
event:
- rollback
target:
- develop # 指定部署环境: dev,stage,production

when:
branch:
- develop

node:
role: xxxx

Drone | Continuous Integration

![](基于 gitea + drone + docker 的 CI 流程实践/v2-e7601b0c866cc762b64390f54a1751fb_b.jpg)

成功回滚到指定版本(’hello, to happyxhw’ 回滚到 ‘hi, from happyxhw’):

![](基于 gitea + drone + docker 的 CI 流程实践/v2-b8f3556bccb50089bb41de74bef436a0_b.jpg)

secrets:

![](基于 gitea + drone + docker 的 CI 流程实践/v2-6e17660da65691d41816a75557bd7e64_b.jpg)

镜像:

![](基于 gitea + drone + docker 的 CI 流程实践/v2-50cdbdf6a180f54a2d2b79dca451e781_b.jpg)

补充:

上面我只写出了开发环境中的CI流程,大家可以自行补充,比如加入预发布、生产环境的编译、部署和回滚流程,加入 master 分支、tag 事件的触发流程,具体请参考 drone 官方文档。


TestLink是最广泛使用的基于Web的开源测试管理工具。它一起同步需求和测试用例。用户可以使用这个工具创建测试项目和文档测试用例。通过TestLink,您可以为多个用户创建一个帐户,并分配不同的用户角色。管理员用户可以管理测试用例分配任务。

它支持测试用例的自动和手动执行。测试人员可以用这个工具在很短的时间内生成测试计划和测试报告。它支持各种格式的测试报告,如Excel、MS Word和HTML格式。除此之外,它还支持与许多流行的缺陷跟踪系统集成,如JIRA、MANTIS、BugZILLA、TRAC等。因为它是一种基于Web的工具,多个用户以他们的凭据和分配的角色可以同时访问其功能。在本教程中,我们将学习:

  • TestLink的优点
  • 登录到TestLink
  • 创建测试项目
  • 创建测试计划
  • 创建构建
  • 创建测试集
  • 创建测试用例
  • 将测试用例分配给测试计划
  • 在TestLink中创建用户和分配角色
  • 执行测试用例
  • 生成测试报告
  • 导出测试用例/测试集
  • 导入测试用例/测试集

TestLink的优点

  • 它支持多个项目
  • 易于导出导入测试用例
  • 易于与多种缺陷管理工具集成
  • 基于XML- RPC的自动化测试用例执行
  • 易于使用版本、关键字和测试用例ID过滤测试用例
  • 易于向多个用户分配测试用例
  • 易于生成各种格式的测试计划和测试报告
  • 向多个用户提供凭据并向其分配角色

第1步:打开TestLink主页。

  1. 输入用户名 admin;
  2. 输入密码;
  3. 点击“登录”按钮。
    这里写图片描述

创建测试项目

第1步:在主窗口中点击“测试项目管理”,它将打开另一个窗口。
这里写图片描述
第2步:点击“创建”按钮创建一个新项目。
这里写图片描述
第3步:在窗口中输入所有必需的字段,如测试项目的类别、项目名称、前缀、描述等。在填写所有必要的详细信息后,点击窗口末尾的“创建”按钮。
这里写图片描述
已成功创建项目“GURU99”:
这里写图片描述

创建测试计划

测试计划包含完整的信息,如软件测试的范围、里程碑、测试集和测试用例。一旦创建了测试项目,下一步就是创建测试计划。

第1步:从主页上点击Test Plan Management。
这里写图片描述

第2步:它将打开另一个页面,在页面底部点击“创建”按钮。
这里写图片描述

第3步:在打开的窗口中填写所有必要的信息,如名称、说明、从现有的测试计划创建等,并点击“创建”。
这里写图片描述

第4步:成功创建Gru 99测试计划。
这里写图片描述

创建构建

Build是一个特定的软件版本。

第1步:从主页的测试计划下点击“构建/发布”。
这里写图片描述

第2步:在下一个窗口中,填写软件发布的所有必要细节,然后单击“创建”以保存发布。

  1. 输入标题名;
  2. 输入关于软件发布的描述;
  3. 为状态激活标记复选框;
  4. 标记复选框状态打开;
  5. 选择发布日期;
  6. 点击“创建”按钮。
    这里写图片描述

一旦你有了一个软件发布,它会这样显示:
这里写图片描述

创建测试集

测试集是测试或验证同一组件的测试用例的集合。下面的步骤将说明如何为项目创建测试集。

第1步:点击主页上的“编辑测试用例”(即Test Specification)选项。
这里写图片描述

第2步:在面板右侧,点击设置图标这里写图片描述,它将显示一系列的测试操作。

第3步:单击测试集的“创建”按钮。
这里写图片描述

第4步:填写测试套件的所有细节,点击“保存”选项卡。

  1. 输入测试集名称;
  2. 输入测试集的详细信息;
  3. 点击“保存”按钮保存测试集的详细信息。
    这里写图片描述
    您可以看到Guru 99的测试集已被创建:
    这里写图片描述
    您的测试集出现在面板左侧,在文件夹结构树下面。

创建测试用例

测试用例保存一系列测试步骤以及预期的结果去测试特定场景。下面的步骤将解释如何创建测试用例以及测试步骤。

第1步:单击面板左侧文件夹树结构下的测试集文件夹。
这里写图片描述

第2步:点击右侧面板中的设置图标,测试用例操作列表将显示在右侧面板上。
这里写图片描述

第3步:点击测试用例操作项中的创建按钮,将会打开新窗口创建测试用例。
这里写图片描述

第4步:在测试用例页中输入详细信息。
这里写图片描述

第5步:输入详细信息后,点击“创建”按钮来保存,GURU99的测试用例就创建成功了。
这里写图片描述

第6步:点击如上所示的文件夹中的测试用例,它将打开一个窗口,再点击“创建步骤”按钮,它将打开一个测试用例步骤编辑器。
这里写图片描述

第7步:它将在同一页上打开另一个窗口,在那个窗口中,你必须输入以下细节:
1. 输入测试用例的步骤操作;
2. 输入步骤动作的预期结果;
3. 单击“保存”并添加另一步操作,或单击“保存”和“退出”,如果没有更多的步骤要添加。
这里写图片描述

第8步:一旦保存并退出测试步骤,就会像这样:
这里写图片描述

将测试用例分配给测试计划

对于测试用例要执行,它应该被分配给测试计划。这里我们将看到如何将测试用例分配给测试计划。

第1步:点击测试面板上的设置图标这里写图片描述,将会展示操作列表。

第2步:点击“添加到测试计划”。
这里写图片描述

第3步:新窗口将打开,找到你的项目“GURU99”。
1. 在测试计划上标记复选框;
2. 点击“添加”按钮。
这里写图片描述
这将将您的测试用例添加到测试计划中了。

在TestLink中创建用户和分配角色

TestLink提供用户管理和授权功能,以下是TestLink中默认角色及其权限的列表:

角色

测试用例

测试计划度量

游客

查看

查看

测试执行人员

执行

查看

测试分析人员

编辑&执行

查看

负责人&管理员

编辑&执行

编辑&执行

第1步:从TestLink主页,点击导航栏中的用户/角色图标。
这里写图片描述

第2步:点击创建。
这里写图片描述

第3步:填写所有用户详细信息并点击“保存”按钮。
这里写图片描述

在列表中,我们可以看到用户已经被创建了:
这里写图片描述

第4步:将测试项目角色分配给用户。
1. 单击“分配测试项目角色”选项卡;
2. 选择项目名称;
3. 从下拉菜单中选择用户角色。
这里写图片描述

执行测试用例

在TestLink中,我们可以运行一个测试用例,并改变一个测试用例的执行状态。测试用例的状态可以设置为“阻塞”“通过”或“失败”。最初,它将处于“不运行”状态,但是一旦您更新它,它就不能再次更改为“不运行”状态。

第1步:从导航栏点击“测试执行”链接,它将引导您进入测试执行面板。
这里写图片描述

第2步:从左侧面板选择要运行的测试用例。
这里写图片描述

第3步:一旦选择了测试用例,它将打开一个窗口。
这里写图片描述

第4步:遵循以下步骤:

  1. 输入与被执行的测试用例相关的注释;
  2. 选择其状态。
    这里写图片描述

第5步:在同一页上,您可以填写关于测试用例执行的类似细节,填上详细信息,然后在右下角选择状态保存。
这里写图片描述

生成测试报告

TestLink支持各种测试报告格式:

  • HTML
  • MS word
  • MS Excel
  • OpenOffice Writer
  • OpenOffice Calc

第1步:从导航栏中点击测试报告选项。
这里写图片描述

第2步:从左侧面板选择“测试报告”链接。
这里写图片描述

第3步:按照以下步骤生成报告:

  1. 标记您想在测试报告中显示的选项;
  2. 点击项目文件夹。

测试报告看起来像这样:
这里写图片描述

导出测试用例/测试集

TestLink提供了导出测试项目/测试集的功能,然后您可以将它们导入到不同服务器或系统的另一个TestLink项目中。为了做到这一点,你应遵循以下步骤。

第1步:在编辑测试用例页面中选择你想要导出的测试用例。
这里写图片描述

第2步:点击面板右侧的设置图标这里写图片描述,它将显示可以在测试用例上执行的所有操作。

第3步:点击“导出”按钮。
这里写图片描述

第4步:它将打开另一个窗口,按需要标记选项并单击“导出”按钮。
这里写图片描述
生成如下XML:
这里写图片描述

导入测试用例/测试集

第1步:选择要导入测试用例的测试集文件夹。
这里写图片描述

第2步:点击面板右侧的设置图标这里写图片描述,它将显示可以在测试集/测试用例上执行的所有操作。

第3步:点击测试用例操作列表中的导入按钮。
这里写图片描述

第4步:浏览选择从TestLink导出的XML测试用例文件并点击上传按钮。

  1. 使用浏览选项来选择您从TestLink导出的XML测试用例文件;
  2. 点击“上传文件”。
    这里写图片描述
    当您上传文件时,它将打开声明导入测试用例的窗口。
    这里写图片描述

第5步:测试用例将会被上传并显示在你选择的测试集里。
这里写图片描述

总结:
在本教程中,我们已经介绍了TestLink的各个方面,比如如何使用TestLink进行测试管理。它将一步一步地说明如何管理项目的测试计划、如何创建用户并相应地分配它们的角色,甚至如何将测试用例导入或导出到项目中。其他有用的功能,如生成报告等也在本教程中得到了很好的演示。


可以在一个Jenkins的全局系统配置中设置多个SonarQube服务器。
在每个具体的任务中,可以指定特定的SonarQube服务器来完成代码扫描。

默认已安装并启动Jenkins与SonarQube。

2.1 安装SonarQube Scanner插件

2.2 配置 SonarQube Server 信息

Jenkins—》系统管理—-》系统设置,配置 SonarQube Server 信息

在SonarQube上生成令牌

将令牌添加到Jenkins

选择应用令牌

2.3 配置 SonarQube Scanner

Jenkins—》系统管理—》全局工具配置, 配置 SonarQube Scanner

2.4 Jenkins任务配置

设置源码仓库

指定构建前的操作

1
2
sonar.projectKey=testsonar
sonar.sources=.
  • Path to project properties:指定sonar-project.properties 文件,默认使用项目根目录的sonar-project.properties文件
  • Analysis properties:传递给 SonarQube的配置参数,优先级高于 sonar-project.properties 文件的参数
  • Additional arguments:附加的参数,例如-X表示启用Debug 模式输出更多的日志信息

指定构建参数

1
clean package sonar:sonar -Dsonar.host.url=http:

3.1 查看扫描报告

控制台输出

在任务界面会出现多个Sonar的链接

点击Sonar链接 即可看到扫描报告

3.2 直接在SonarQube Server上查看

需要借助JaCoCo插件,才能获取到代码的真实单元测试覆盖率,否则在有单元测试的情况下也只会显示为0%。
单击“覆盖率”可看到详细的代码统计展示。

安装JaCoCo插件

通过指定构建的Goals and options参数
clean package org.jacoco:jacoco-maven-plugin:prepare-agent sonar:sonar -Dsonar.host.url=http://192.168.16.200:9000

注意:添加如下参数-Dmaven.test.failure.ignore=false可以忽略失败的单元测试,以便完成对覆盖率的统计。

在SonarQube社区版本中,可以通过配置构建的Goals and options参数,来简单地进行不同分支的扫描。
只需要增加参数-Dsonar.branch=<branch name>
针对同一项目的不同分支进行构建后,在Sonar界面,会看到根据“ ”的扫描任务和状态。

这篇文章延续上一篇 《win10下docker-compose搭建gitlab+gerrit+sonar+jenkins持续集成环境》,上一篇我们搭建好了持续集成的环境,这篇文章介绍如何将这些工具结合起来,把每个环节打通。下一篇我们会使用集成环境做安卓开发实战。

idea和Android SDK的下载安装使用不是本文的重点,我假设各位都是成熟的安卓开发人员并且已经搭建好了基于idea的安卓开发环境(菜鸟一般不需要了解敏捷开发)。

我们回过头来看一下这张集成图 ,这里面没有把sonarqube画出来,因为sonarqube是可以和Jenkins进行集成的,这在后面我们会慢慢提到。
在这里插入图片描述

使用浏览器打开gitlab网址,我这里是localhost:10080,首次打开需要修改密码。修改成功后我们就可以登录了,这个时候用的管理员账号。
在这里插入图片描述

注册开发机账号

Step1:gitlab配置
我们还需要gitlab上注册一个普通账号,叫develop01,具体如下,点击注册他会自动登录,这个developer01就像我们普通的开发者,再window下做开发,因此我需要为这位开发者配置对应的ssh信息。
由于windows下已经安装了git,因此直接打开C:\Users\Administrator\.ssh,复制出里面的内容,粘贴到gitlab该用户设置的ssh配置上(由于我本地机器配置的qq邮箱只好用自己的qq邮箱)
在这里插入图片描述然后点击增加密钥,就配置好了;
在这里插入图片描述Step2:开发机配置
如果你的开发机没有配置多个git账户可以略过这一步;如果你的开发机git配置了多个用户,需要先把生成的ssh添加进来,否则会出现如下错误:

1
2
3
4
5
6
7
8
9
10
11
12
$ git clone ssh://git@localhost:10022/dev-group/ci-test-project.git
Cloning into 'ci-test-project'...
The authenticity of host '[localhost]:10022 ([::1]:10022)' can't be established.
ECDSA key fingerprint is SHA256:XMNZCD1uHPib/WU/4OziotP557y/jefwmhoVCmjkKK8.
Are you sure you want to continue connecting (yes/no/[fingerprint])? n
Please type 'yes', 'no' or the fingerprint:
Host key verification failed.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

解决方法是:

1
2
$ ssh-agent bash
ssh-add ~/.ssh/id_rsa

创建gitlab项目

我们假定管理员账号是技术总监,那么技术总监需要创建一个软件项目。
登陆进去我们进入到主页面,点击创建项目->私有项目->填写项目信息->完成。这个时候跳转到创建成功页面,并提示我们需要配置ssh key。
在这里插入图片描述
由于我们的代码首先需要通过gerrit审核才能合并到gitlab,因此,我们需要添加一个gerrit的用户。原理很简单,就是使用gerrit服务器生成一个gerrit的sshkey,配置到gitlab上,这样gerrit所在的服务器等同于gitlab的客户机,就可以通过gerrit服务器向gitlab推送代码了。

要拿到docker中gerrit容器的ssh信息,需要进入该容器,使用docker exec -it xxx /bin/bash或者直接在docker的gerrit容器上点击cli按钮。进入控制台;
在这里插入图片描述
Step1:确认gerrit容器是否已经配置了git

1
2

fatal: unable to read config file '/root/.gitconfig': No such file or directory

发现没有配置,我们来配置一下。
我们知道配置git需要两个信息:username和email,因此我们需要登录gitlab的管理员账号查看对应的邮箱。为了方便区别我把邮箱修改成了gerrit@example.com;
在这里插入图片描述Step2: 我们回到gerrit容器中,使用ssh-keygen命令生成ssh信息(三次回车),然后使用cat命令打印ssh信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
sh-4.2$ ssh-keygen -t rsa -C gerrit@example.com
Generating public/private rsa key pair.
Enter file in which to save the key (/var/gerrit/.ssh/id_rsa):
Created directory '/var/gerrit/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /var/gerrit/.ssh/id_rsa.
Your public key has been saved in /var/gerrit/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:yhrSuWAlFyK2IC0NOwIjczbLSceobmUSZpW8IJlsH24 gerrit@example.com
The key's randomart image is:
+---[RSA 2048]----+
|BoB+o |
|*^.O. |
|&o% + |
|*=.E . |
|..* o S |
| o = o . |
|. + + o |
| . o + |
| o |
+----[SHA256]-----+
sh-4.2$ cat /var/gerrit/.ssh/id_rsa.pub
ssh-rsa ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDs/u6ZEU0ES1niaGpD7PbDnQMkDpjf0YW0XR9sYgHbawL+F4OBjc3wJ2xHjOLQNZHPpt/yIuSYWphGpSJmrWZ6YwiKQEV0BJIf2ttNn5qSz9ds9riE6eCCn2hJu/mIh2f1+DK3xF7lryzUAYhO8c0Mb1WAxD3xp69A+jKGvMvx6/AaZvjIm4fiQv/0JZ05nX9p6oqyYVO9XE25i5eJ9erJrLVeUx8wgxpfWROcH27Yt1YOMcj50m5pGbjp997tUO+r+jUEdhcpilW8GPyvV9eW+69xEbRKeCsDmBPKr3G5Gjn/iVeZSeH7tj/YJOU5Y281yJd6XL21uvHSh+o8zvv9 gerrit@example.com

然后和前面一样,在gitlab端配置管理员账户的ssh信息(为了方便辨识,我把原本为root的用户名改成了gerrit),用户信息也更新成了gerrit@example.com
在这里插入图片描述Step3:创建开发组
接下来就是创建一个开发组。
在这里插入图片描述
Step4:创建组内项目
创建好开发组之后会跳出一个页面,我们继续点击新建项目,项目名称就叫ci-test-project吧。
在这里插入图片描述Step5 :把developer01拉入小组
点击群组->dev-group->成员->搜索成员并添加为报告者(repoter)->添加到群组;
在这里插入图片描述Step6:测试创建的项目
由于我们刚刚在添加开发机的时候角色赋予的是repoter,它是没有提交权限的,我们来验证一下;

1
2
3
4
5
6
7
8
9
10
11
$ git clone ssh://git@localhost:10022/dev-group/ci-test-project.git
$ cd ci-test-project/
$ touch testfile.txt
$ git add testfile.txt
$ git commit -m 'add testfile'
$ git push

GitLab: You are not allowed to push code to this project.
fatal: Could not read from remote repository.
Please make sure you have the correct access rights
and the repository exists.

嗯,被拒绝了,成功了。因为普通用户没有直接push的权限。需要先review到gerrit上进行审核并commit后,才能更新到代码中心仓库里。

  1. 页面上写的是git clone git@localhost:dev-group/ci-test-project.git
    但是我这里使用docker做了端口映射,因此需要写出上面这种形式
  2. 如果你在clone的时候发现执行不了,试试在C:/docker/gitlab/etc/gitlab.rb文件开头加上:
    gitlab_rails['gitlab_shell_ssh_port'] = 10022

我们通过在本地创建一个文件,使用push指令测试该用户是否正常,如果push被拒绝说明配置正常。

问题1:一直卡在git clone。control+c之后提示没有权限访问这个项目。这是一个笼统的回答,要看真正问题出现在哪里,需要看日志,那么日志就放在安装目录的log文件夹下面:“C:\Docker\gitlab\log\sshd”。
打开current文件,在最下面我们会看到错误日志,我这里是权限开得太大了;

1
2
3
4
5
6
7
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
2020-03-12_02:47:17.13033 Permissions 0755 for '/etc/gitlab/ssh_host_ed25519_key' are too open.
2020-03-12_02:47:17.13100 It is required that your private key files are NOT accessible by others.
2020-03-12_02:47:17.13210 This private key will be ignored.
2020-03-12_02:47:17.13268 key_load_private: bad permissions
2020-03-12_02:47:17.13334 Could not load host key: /etc/gitlab/ssh_host_ed25519_key
2020-03-12_02:47:17.13429 Bad protocol version identification 'HEAD / HTTP/1.1' from 172.18.0.1 port 45886

知道原因就很简单了,我们找到/etc/gitlab/ssh_host_ed25519_key然后把他的权限修改成0771就可以了。如果修改之后权限没有变化就可能是你的key保存在了windows机器上,我的办法就是重新安装了gitlab然后把key保存在了容器里面。(其实windows下我们只需要留一个宿主机和容器通讯的文件夹就可以了,因为权限问题很肉疼的)。

我们知道Gerrit有好几种认证方式,常见的OAuth(Gitlab的OAuth和Github的OAuth),Http,Ldap,openID。我们这里使用的是Ldap方式(安装gerrit镜像的时候同时自动安装了),Ldap在管理用户信息上还是蛮好用的。

Step1:使用ldap创建一个Gerrit账号(上一篇文章有介绍,这里不再赘述);注意邮箱要和gitlab的管理员邮箱保持一致。

参数为:
Given Name: Gerrit
Last Name: Admin
Common Name: Gerrit Admin
User ID: gerrit
Email: gerrit@example.com
Password: 12345678

Step2:使用该账号登录Gerrit,网址是http://localhost:20080/
Step3:登陆后点击右上角用户->Setting->SSH Keys配置sshkey,值为Gitlab上配置的管理员(Gerrit)的SSH;
在这里插入图片描述Step4:采用同样方法为开发人员develop01在gerrit上注册账号,并配置对应的ssh key(开发机上的ssh key,与gitlab用户对应);

在这里插入图片描述
Step5:以同样的方式为jenkins创建账号,参数为:

Given Name: Jenkins
Last Name: Admin
Common Name: Jenkins Admin
User ID: jenkins
Email: jenkins@example.com
Password: 12345678

到这个时候,ldap上有三个账号可以登录gerrit了:
在这里插入图片描述Step6:进入Jenkins所在的服务器,生成jenkins的ssh并设置到gerrit对应账户

1
2
3
4
5
$ ssh-keygen -t rsa -C jenkins@example.com
Generating public/private rsa key pair.
...
$ cat /var/jenkins_home/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDNK8+nl9XptdAedHl3r4zuvmdzUhMjySuHhTMi7kI/wkq8uiX+xNfP9vsCuo/xio4icr9BC6nJFMUNlI8amhH+ub9RJmsQPdHjPT5rAiYxmLjhKng8WY2pznnvhsqflN+DBnPvuekDvnynkt7gngHkf4b/SowWlVzVn4S3msrJ5zxcy0vU1oPG8xoDA4kQc4HfgxApzeZsePe2JPcwcXlDmvgtPHul1xiXCwkpmsM7Vd/xQSY7O4cZdt25qfuL7HprovCdhVEJuScd4AENOLK6NUiHwkkCGA0vkB+mpi/2Zq41jXvfH2v+6gipAPDZjt+a3aUJ7mUisCiYtl7dZO+9 jenkins@example.com

注意:这里一定要写密码,我的密码是11111111(8个1)

Step7:将Jenkins添加到Non-Interactive Users 用户组
使用管理员账号登陆gerrit->BROWSE->Groups > Non-Interactive Users > members->Add your jenkins user。

Step8:手动添加Verified标签功能(已经有了就跳过这一步)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

sh-4.2$ git init cfg; cd cfg
sh-4.2$ git config --global user.name 'gerrit'
sh-4.2$ git config --global user.email 'gerrit@example.com'
sh-4.2$ git remote add origin ssh://gerrit@localhost:29418/All-Projects
sh-4.2$ git pull origin refs/meta/config
sh-4.2$ vi project.config
[label "Verified"]
function = MaxWithBlock
value = -1 Fails
value = 0 No score
value = +1 Verified
sh-4.2$ git commit -a -m 'Updated permissions'
sh-4.2$ git push origin HEAD:refs/meta/config
sh-4.2$ rm -rf cfg
sh-4.2$ rm -rf cfg

以上方法也可以在本地使用push的方式完成修改。然后就可以看到这个标签了;
在这里插入图片描述

Step9:配置All-Projects权限
Browe->Repositories->All-Projects->Access-> Edit,往下拉,找到Reference: refs/*在下面有一个输入框,在输入框输入non会出现Non-Interactive Users点击add,就给这个用户组添加了该特性权限。
然后以同样的方法在Reference: refs/heads/*下面的Label Code-Review把Non-Interactive Users加上去,值为-1, +1 ;
同样的Label Verified也添加Non-Interactive Users设置为-1, +1 ;

因为我的gerrit版本是3.0,在2.7+的版本上还需要额外配置Stream Events的权限,位于Global Capabilities的节点下,点击保存,完毕;
在这里插入图片描述

Step1:安装插件
使用管理员账户登录jenkins后,点击我们需要安装Gerrit Trigger插件和Git plugin插件;
“系统管理”->拖到下面“插件管理”->”可选插件”->Filter搜索->输入Gerrit Trigger安装,然后输入Git plugin,也安装。勾选安装完成后重启。jenkins会自动重启,我们等待他重启完成。

重启完成我们登录之后,我们可以在插件管理中看到Gerrit Jenkins:
在这里插入图片描述Step2:配置Gerrit Trigger
点击插件->点击左侧的”New Server”->在”Add New Server”一栏输入名字,我这里是”check4Gerrit”->勾选”Gerrit Server with Default Configurations”->点击”OK”;

然后进行”Gerrit Connection Setting”,
在这里插入图片描述我们看到顶上有一排黄色的警告,意思就是没有安装event-log,我参考这篇文章搞了一个,把这个jar放在gerrit的plugins文件夹里面。如上所示,信息填写完毕之后,点击右侧的test Connection,提示success表示配置成功;

我这里的信息是:

Name:check4Gerrit
Hostname:192.168.31.189
Frontend URL:http://192.168.31.189:20080
SSH Port:29418
Username:jenkins
E-mail:jenkins@example.com
SSH Keyfile:/var/jenkins_home/.ssh/id_rsa
SSH Keyfile Password:11111111

在这一步我卡了很久,一直没成功,最后是把给ssh设置了密码,并且把ip地址改成了宿主机的局域网ip,然后才看到期盼已久的success。最终这里会变成蓝色;
在这里插入图片描述

添加review命令

虽然我们配置dev01客户机没有push权限,但是我们会让他有git review的权限,不过这里我们需要配置一下才可以。我们需要在项目里面添加一个 .gitreview来支持git review。目前只有管理员组才具备提交权限,也就是gerrit账号。因此我们需要用gerrit账号把代码下过来并添加.gitreview文件。

以root方式进入gerrit所在的容器;不加–user root进入的非root账号。由于我之前创建的sshkey在非root用户的.ssh目录下,因此我只是用root用户创建了一个开发权限的文件夹就退出切换成了非root用户;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker exec -it --user root gerrit /bin/bash 
[root@fba3f1d5f6f6 /]
[root@fba3f1d5f6f6 opt]
[root@fba3f1d5f6f6 opt]
[root@fba3f1d5f6f6 /]
PS C:\Docker\gerrit> docker exec -it gerrit /bin/bash
bash-4.2$ cd opt/testCode/
bash-4.2$ git clone ssh://git@192.168.31.189:10022/dev-group/ci-test-project.git
Cloning into 'ci-test-project'...
The authenticity of host '[192.168.31.189]:10022 ([192.168.31.189]:10022)' can't be established.
ECDSA key fingerprint is SHA256:B/IihaaLfGY0ZW7CtBhfCFvp9Nh2lZtMNAB51gjQWb0.
ECDSA key fingerprint is MD5:ec:8d:ae:bc:f3:5a:6a:36:c3:18:3d:75:46:97:e0:90.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[192.168.31.189]:10022' (ECDSA) to the list of known hosts.
remote: Counting objects: 3, done

然后我们添加.gitreview,.gitreview的内容如下(192.168.31.189是宿主机的ip):

1
2
3
4
[gerrit]
host=192.168.31.189
port=29418
project=ci-test-project.git
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash-4.2$ cd ci-test-project/
bash-4.2$ vi .gitreview
bash-4.2$ git add .gitreview
bash-4.2$ git config --global user.name 'gerrit'
bash-4.2$ git config --global user.email 'gerrit@example.com'
bash-4.2$ git commit .gitreview -m 'add .gitreview file by gerrit.'
[master 9b7dc37] add .gitreview file by gerrit.
1 file changed, 4 insertions(+)
create mode 100644 .gitreview
bash-4.2$ git push origin master
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 343 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ssh://git@192.168.31.189:10022/dev-group/ci-test-project.git
4ef534d..9b7dc37 master -> master

以同样的方法添加.testr.conf 文件(由于下面的Python 代码我使用了 testr,需要先在Jenkins容器内安装 testr 命令yum -y install epel-release->yum -y install python-pip->pip install testrepository,否则会编译不过),.testr.conf 文件内容如下:

1
2
3
4
5
6
7
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=1
OS_STDERR_CAPTURE=1
OS_TEST_TIMEOUT=60
${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=-list

然后我们把它提交到版本仓库;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash-4.2$ vi .testr.conf
bash-4.2$ git add .testr.conf
bash-4.2$ git commit .testr.conf -m 'add .testr.conf file by gerrit'
[master 54a8810] add .testr.conf file by gerrit
1 file changed, 7 insertions(+)
create mode 100644 .testr.conf
bash-4.2$ git push origin master
Counting objects: 4, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 480 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
To ssh://git@192.168.31.189:10022/dev-group/ci-test-project.git
9b7dc37..54a8810 master -> master

这个时候我们回到gitlab就能看到我们提交的这两个文件了。
在这里插入图片描述

同步gerrit和Gitlab项目文件

到目前为止,gerrit上面并没有和gitlab同步的项目,我们需要把gerrit和gitlab关联起来,也就是说必须在gerrit上创建相同的项目,并有相同的仓库文件;

有两种方式创建项目,一种是登录gerrit的管理页面在Repo点击create new,还有一种是在gerrit容器中使用命令ssh-gerrit gerrit create-project ci-test-project (后一种方式可以创建空的项目);

接下来我们要使用gerrit上的管理员账户clone –bare Gitlab上的仓库到 Gerrit ,由于我gerrit的管理员和gitlab的管理员都是gerrit且有相同的ssh key,因此我不需要担心两个弄混导致的同步问题;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sh-4.2$ cd /var/gerrit/git/
sh-4.2$ ls
All-Projects.git All-Users.git ci-test-project.git
sh-4.2$ rm -rf ci-test-project.git/
sh-4.2$ git clone --bare ssh://git@192.168.31.189:10022/dev-group/ci-test-project.git
Cloning into bare repository 'ci-test-project.git'...
remote: Counting objects: 12, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 12 (delta 1), reused 0 (delta 0)
Receiving objects: 100% (12/12), done.
Resolving deltas: 100% (1/1), done.
sh-4.2$ cd ci-test-project.git/
sh-4.2$ ls
HEAD branches config description hooks info objects packed-refs refs

这个时候gerrit的ci项目下的文件就和gitlab一毛一样的了。

gerrit和Gitlab项目联动

现在只是在gerrit上复制了gitlab的文件,还没有实现真正的联动。我们还不能把gerrit上审核的代码提交到gitlab。我们要的效果是gerrit上的ci上的仓库已发生变化就立马同步到gitlab上去。这个时候就需要使用插件做触发器啦,这个插件叫做Replication ,gerrit默认自带了这个插件,因此我们需要配置这个插件。

修改 /var/gerrit/etc/replication.config,如果修改的是C:\Docker\gerrit\etc\replication.config。确认一下replication.config文件访问权限为600,否则后面会报错。

1
2
3
4
5
6
7
[remote "ci-test-project"]
projects = ci-test-project
url = ssh://git@192.168.31.189:10022/dev-group/ci-test-project.git
push = +refs/heads/*:refs/heads/*
push = +refs/tags/*:refs/tags/*
push = +refs/changes/*:refs/changes/*
threads = 3

然后设置gerrit用户的 ~/.ssh/config

1
2
3
Host 192.168.31.189:
IdentityFile ~/.ssh/id_rsa
PreferredAuthentications publickey

在known_hosts中给192.168.31.189添加rsa密钥(如果你的端口号不是22就需要把-p 端口号加上,我这里是10022,所以需要加上);

1
2
3
sh-4.2$ vi ~/.ssh/known_hosts
sh-4.2$ sh -c "ssh-keyscan -p 10022 -t rsa 192.168.31.189 >> /var/gerrit/.ssh/known_hosts"
sh-4.2$ sh -c "ssh-keygen -H -f ~/.ssh/known_hosts"

到这里,项目复制就完成了,如果需要复制多个项目,则在 replication.config 中添加多个 [remote ….] 字段即可。

安装必要插件

登陆jenkins,“系统管理”->“管理插件”->“可选插件”->选择Git Pluin插件进行安装。(我的可选中心在安装中文插件后一直菊花疼,最终我从插件中心手动下载了一个安装)。
安装Git插件是为了创建项目的时候能够支持git触发,否则是没办法做联动的。Git插件长这样,别下错了。
在这里插入图片描述

创建项目

Git插件安装成功后,我们在jenkins上添加一个和gitlab同名的项目。jenkins->新建任务->填入名字ci-test-project,勾选构建自由风格->确定到下一页->源码管理选择Git->填入如下字段:

源码管理:Git
Repository URL:http://192.168.31.189:20080/ci-test-project(和gerrit仓库http地址对应)
Branches to build:origin/$GERRIT_BRANCH
构建触发器:Gerrit event

在这里插入图片描述在这里插入图片描述在这里插入图片描述
这个时候我们配置好了,在主界面上会看到这个任务。
在这里插入图片描述接下来就是做联调测试了。

测试流程如下:
我们在开发机上使用developer01账号增加一个文件,并将文件提交到gerrit的HEAD:refs/for/master分支上。

1
2
3
4
5
6
$ git clone "ssh://developer01@localhost:29418/ci-test-project" && scp -p -P 29418 developer01@localhost:hooks/commit-msg "ci-test-project/.git/hooks/"
$ cd ci-test-project/
$ touch test.txt
$ git add test.txt
$ git commit -m 'test'
$ git push origin HEAD:refs/for/master

然后我们会在gerrit上看到这条记录,
在这里插入图片描述我们会看到jenkins已经verfiled而且是+1,其实是因为代码一提交就会触发jenkins使用构建脚本构建这个代码,构建成功就会+1。于是我点击了codereview+2;在这里插入图片描述
然后说明可以提交了,于是乎我点击submit。状态变成了merged。而且我们能在merged一栏可以看到:
在这里插入图片描述进到Gitlab项目查看,发现代码已经同步过来了。(最开始由于在做gerrit和Gitlab项目联动的时候没有添加端口号导致折腾了半天没有通,看gerrit的repulic日志才发现这个问题)。
在这里插入图片描述然后我们在Jenkins中也会看到编译成功了。蓝色表示成功,红色表示失败。天气图标表示多云,意思是又存在不成功的,但是总体ok(Jenkins还真是人性化);
在这里插入图片描述

安卓开发环境

windows的android sdk和Linux下的还是有一定区别的,因此建议在windows和linux下下载相同的sdk环境。为了确保版本一致,我都下载了r23的,linux的sdk下载戳这里。linux安装sdk使用如下命令:

1
2
android list sdk --all
android update sdk -u -a -t 序号

window的sdk配置比较简单,麻烦的是linux,因为要解决被墙的问题,这里我提供一种代理的方式,比如我们使用list sdk ,在中间–proxy一下。

1
./android list sdk --proxy-host mirrors.neusoft.edu.cn --proxy-port 80 -s --all

然后再Android studio中创建了一个工程,把这个工程的内容移到了之前使用developer01账号从gerrit克隆下来的项目ci-test-project中。再次使用android Studio打开,运行,安装到手机,确定项目代码是可以编译成功的。
在这里插入图片描述

Jenkins配Android SDK

点击Jenkins的系统管理->系统配置->找到全局属性->勾选环境变量->按如下方式添加ANDROID_HOME后保存。
在这里插入图片描述

配置gradle

首先要安装gradle插件,如果你已经安装好了,可以跳过这一步。Gradle的插件名字就叫做Gradle,大家注意辨别,这里就不然贴图介绍怎么安装了(gradle安装经常失败我最终不得不使用离线安装一个个把依赖安装上了)。

点击系统管理->全局工具配置->配置Git;JDK;Gradle,最后保存;由于Jenkins自带jdk1.8所以我这里不需要配置。在Jenkins容器中使用java -version知道是否已经安装了java;Git也不需要配置。

1
2
3
4
5
6
7
$ java -version
openjdk version "1.8.0_242"
OpenJDK Runtime Environment (build 1.8.0_242-b08)
OpenJDK 64-Bit Server VM (build 25.242-b08, mixed mode)

$ git --version
git version 2.11.0

那么我们配置一下gradle,我们在gradle一栏新增安装,需要注意的是gradle的版本需要和android 项目代码的gradle保持一致。我项目代码中gradle版本为4.10.1,因此安装的时候也需要选择对应版本。

1
2
3
4
5
6

distributionBase=GRADLE_USER_HOME
distributionUrl=https\:
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

在这里插入图片描述

Tasks:--stacktrace build
Root Build script:${WORKSPACE}
Build File:${WORKSPACE}\build.gradle

创建Apk构建任务

由于我们在前面已经构建了一个ci-test-project,因此这里我们只需要在那个构建上修改一下,添加gradle构建。
找到添加构建步骤 选择 Invoke Gradle script,选择gradle版本->保存。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200313144720523.png?x-oss-process=image/watermark,type\_ZmFuZ3poZW5naGVpdGk,shadow\_10,text\_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pob25nbHVuc2h1bg==,size\_16,color\_FFFFFF,t\_70

提交代码,通过审核触发构建

前面我们虽然把代码写好了,带式还没有提交上去,这个时候我们来提交一下。
在这里插入图片描述我们点进来发现Jenkins已经verfied+1了。转到Jenkins发现已经构建成功。
在这里插入图片描述我们在gerrit上把这个提交merged(注意要等待verfied+1也就是jenkins构建成功才能submit),我公共构建了一个错误,我们发现是不能够submit的。
在这里插入图片描述

获取编译结果

然后我们需要设置用于存档的结果文件,我们这里是一个apk文件,需要修改构建配置如下 (对应你android studio编译后的apk输出目录).我这里是myapplication/build/outputs/*.apk
${WORKSPACE}\app\build\outputs\apk\app-debug.apk


赠人玫瑰手留余香,帮忙点个赞吧。