-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathcollector.go
207 lines (175 loc) · 5.54 KB
/
collector.go
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
// +build linux
// ebpf_exporter - A Prometheus exporter for Linux block IO statistics.
//
// Copyright 2018 Daniel Swarbrick
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"bytes"
"encoding/binary"
"math"
"github.com/iovisor/gobpf/bcc"
"github.com/prometheus/client_golang/prometheus"
)
const (
latTableLen = 28 // Must match max_io_lat_slot in BPF program.
reqSzTableLen = 16 // Must match max_io_req_sz_slot in BPF program.
// cf. <linux/genhd.h>
DISK_NAME_LEN = 32
// Linux req_opf enums, cf. <linux/blk_types.h>
REQ_OP_READ = 0
REQ_OP_WRITE = 1
REQ_OP_FLUSH = 2
REQ_OP_DISCARD = 3
REQ_OP_ZONE_REPORT = 4
REQ_OP_SECURE_ERASE = 5
REQ_OP_ZONE_RESET = 6
REQ_OP_WRITE_SAME = 7
REQ_OP_WRITE_ZEROES = 9
REQ_OP_SCSI_IN = 32
REQ_OP_SCSI_OUT = 33
REQ_OP_DRV_IN = 34
REQ_OP_DRV_OUT = 35
)
var (
// Map of request operation enums to human-readable strings. This map does not include all
// possible request operations, but covers the most commonly observed ones.
reqOpStrings = map[uint8]string{
REQ_OP_READ: "read",
REQ_OP_WRITE: "write",
REQ_OP_FLUSH: "flush",
REQ_OP_DISCARD: "discard",
REQ_OP_WRITE_SAME: "write_same",
REQ_OP_WRITE_ZEROES: "write_zeroes",
}
)
type exporter struct {
bpfMod *bcc.Module
ioLat *bcc.Table
ioReqSz *bcc.Table
latency *prometheus.Desc
reqSize *prometheus.Desc
tableEntries *prometheus.GaugeVec
}
func newExporter(m *bcc.Module) *exporter {
e := exporter{
bpfMod: m,
ioLat: bcc.NewTable(m.TableId("io_lat"), m),
ioReqSz: bcc.NewTable(m.TableId("io_req_sz"), m),
latency: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "bio", "req_latency"),
"A histogram of bio request latencies in microseconds.",
[]string{"device", "operation"},
nil,
),
reqSize: prometheus.NewDesc(
prometheus.BuildFQName(namespace, "bio", "req_size"),
"A histogram of bio request sizes in KiB.",
[]string{"device", "operation"},
nil,
),
tableEntries: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: namespace,
Subsystem: "bio",
Name: "bpf_table_entries",
Help: "The number of BPF table entries used.",
},
[]string{"table"},
),
}
prometheus.MustRegister(e.tableEntries)
return &e
}
func (e *exporter) Collect(ch chan<- prometheus.Metric) {
var (
n uint
tbl map[string]map[uint8][]uint64
)
n, tbl = decodeTable(e.ioLat, latTableLen)
e.tableEntries.WithLabelValues("req_latency").Set(float64(n))
e.emit(ch, e.latency, tbl)
n, tbl = decodeTable(e.ioReqSz, reqSzTableLen)
e.tableEntries.WithLabelValues("req_size").Set(float64(n))
e.emit(ch, e.reqSize, tbl)
}
func (e *exporter) Describe(ch chan<- *prometheus.Desc) {
ch <- e.latency
ch <- e.reqSize
}
func (e *exporter) emit(ch chan<- prometheus.Metric, hist *prometheus.Desc, devBuckets map[string]map[uint8][]uint64) {
for devName, reqs := range devBuckets {
for reqOp, bpfBuckets := range reqs {
var (
count uint64
sum float64
)
// Skip unrecognized request operations.
if _, ok := reqOpStrings[reqOp]; !ok {
continue
}
promBuckets := make(map[float64]uint64)
for k, v := range bpfBuckets {
// Prometheus histograms are cumulative, so count must be a running total of
// previous buckets also.
count += v
// Sum will not be completely accurate, since the BPF program already discarded
// some resolution when storing occurrences of values in log2 buckets. Count and
// sum are required however to calculate an average from a histogram.
exp2 := math.Exp2(float64(k))
sum += exp2 * float64(v)
promBuckets[exp2] = count
}
ch <- prometheus.MustNewConstHistogram(hist,
count,
sum,
promBuckets,
devName, reqOpStrings[reqOp],
)
}
}
}
// decodeTable decodes a BPF table and returns a per-device map of values as ordered buckets.
func decodeTable(table *bcc.Table, tableSize uint) (uint, map[string]map[uint8][]uint64) {
var (
numEntries uint
// Struct must match disk_key_t in BPF program
key struct {
Disk [DISK_NAME_LEN]byte
Op uint8
_ [7]byte // padding
Slot uint64
}
)
devBuckets := make(map[string]map[uint8][]uint64)
// bcc.Table.Iter() returns unsorted entries, so write the decoded values into an order-
// preserving slice.
for it := table.Iter(); it.Next(); {
if err := binary.Read(bytes.NewReader(it.Key()), binary.LittleEndian, &key); err != nil {
continue
}
// key.Disk is a null-terminated char array.
devName := string(key.Disk[:bytes.IndexByte(key.Disk[:], 0)])
// First time seeing this device, create map for request operations.
if _, ok := devBuckets[devName]; !ok {
devBuckets[devName] = make(map[uint8][]uint64)
}
// First time seeing this req op for this device, create slice for buckets.
if _, ok := devBuckets[devName][key.Op]; !ok {
devBuckets[devName][key.Op] = make([]uint64, tableSize)
}
devBuckets[devName][key.Op][key.Slot] = binary.LittleEndian.Uint64(it.Leaf())
numEntries++
}
return numEntries, devBuckets
}