Skip to content

Commit 4703764

Browse files
authored
feat: Exporter 12x faster hopefully (#5691)
Made a few changes to vastly increase the speed of the exporter. - Increased each channel buffer up to 100 from 0. - 5x ed the worker count from 600 to 3000 - Added a fast object list GCS library (mostly copied over from vulnfeeds), and split up the initial object listing loop at various places. - This is mostly guesswork at the moment, ideally we should do a review of where to roughly evenly split the work every now and then. But good enough for now. - This does mean the exporter will slow down near the end. - Refactored message receiving to use the `for` syntax. This should be a slight behavior change where we won't cancel based on the context, but only when the channel is closed. (Or when the context is cancelled on the sending side, just never on the receiving side) - Fixed README typos - Use logger.ErrorContext instead of logger.Error -
1 parent 4bf647c commit 4703764

11 files changed

Lines changed: 352 additions & 113 deletions

File tree

‎deployment/clouddeploy/gke-workers/environments/oss-vdb-test/exporter.yaml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ spec:
1515
args:
1616
# TODO(michaelkedar): single source of truth w/ terraform config
1717
- "--upload-to-gcs=true"
18-
- "--workers=600"
18+
- "--workers=3000"
1919
- "--bucket=osv-test-vulnerabilities"
2020
- "--osv-vulns-bucket=osv-test-vulnerabilities"
21+
- "--breakdown-prefixes=ALSA-,BIT-,CGA-0,CGA-a,CGA-m,CVE-,DLA-,DSA-,GHSA-,GO-,MAL-,MGASA-,OSV-,PYSEC-,RLSA-,RUSTSEC-,SUSE-,UBUNTU-"
22+
2123

‎deployment/clouddeploy/gke-workers/environments/oss-vdb/exporter.yaml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ spec:
1414
value: oss-vdb
1515
args:
1616
- "--upload-to-gcs=true"
17-
- "--workers=600"
17+
- "--workers=3000"
1818
- "--bucket=osv-vulnerabilities"
1919
- "--osv-vulns-bucket=osv-vulnerabilities"
20+
- "--breakdown-prefixes=ALSA-,BIT-,CGA-0,CGA-a,CGA-m,CVE-,DLA-,DSA-,GHSA-,GO-,MAL-,MGASA-,OSV-,PYSEC-,RLSA-,RUSTSEC-,SUSE-,UBUNTU-"
21+
2022

‎deployment/clouddeploy/gke-workers/environments/private/exporter.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ spec:
1414
value: oss-vdb-test
1515
args:
1616
- "--upload-to-gcs=true"
17-
- "--workers=600"
17+
- "--workers=3000"
1818
- "--bucket=osv-test-vulnerabilities-private"
1919
- "--osv-vulns-bucket=osv-test-vulnerabilities-private"
2020

‎go/cmd/exporter/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ To run the exporter locally, run the exporter from within the `go/cmd/exporter`
1717

1818
```sh
1919
# Example
20-
go run . -osv_vulns_bucket osv-test-vulnerabilities -uploadToGCS=false -bucket /tmp/osv-export
20+
go run . -osv-vulns-bucket osv-test-vulnerabilities -upload-to-gcs=false -bucket /tmp/osv-export
2121
```
2222

2323
This will write the exported files to the `/tmp/osv-export` directory.

‎go/cmd/exporter/downloader.go‎

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"log/slog"
66
"sync"
77

8+
"github.com/google/osv.dev/go/internal/osvutil"
89
"github.com/google/osv.dev/go/logger"
910
"github.com/google/osv.dev/go/osv/clients"
1011
"github.com/ossf/osv-schema/bindings/go/osvschema"
@@ -16,29 +17,24 @@ import (
1617
// result to outCh.
1718
func downloader(ctx context.Context, client clients.CloudStorage, inCh <-chan string, outCh chan<- *osvschema.Vulnerability, wg *sync.WaitGroup) {
1819
defer wg.Done()
19-
for {
20-
var path string
21-
var ok bool
22-
23-
// Wait to receive an object path, or be cancelled.
24-
select {
25-
case path, ok = <-inCh:
26-
if !ok {
27-
return // Channel closed.
28-
}
29-
case <-ctx.Done():
30-
return
31-
}
32-
20+
for path := range inCh {
3321
// Process object.
3422
data, err := client.ReadObject(ctx, path)
3523
if err != nil {
36-
logger.Error("failed to read vulnerability", slog.String("obj", path), slog.Any("err", err))
24+
if osvutil.IsContextError(err) {
25+
return
26+
}
27+
logger.ErrorContext(ctx, "failed to read vulnerability", slog.String("obj", path), slog.Any("err", err))
28+
3729
continue
3830
}
3931
vuln := &osvschema.Vulnerability{}
4032
if err := proto.Unmarshal(data, vuln); err != nil {
41-
logger.Error("failed to unmarshal vulnerability", slog.String("obj", path), slog.Any("err", err))
33+
if osvutil.IsContextError(err) {
34+
return
35+
}
36+
logger.ErrorContext(ctx, "failed to unmarshal vulnerability", slog.String("obj", path), slog.Any("err", err))
37+
4238
continue
4339
}
4440

‎go/cmd/exporter/exporter.go‎

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import (
77
"flag"
88
"log/slog"
99
"os"
10-
"path/filepath"
10+
"os/signal"
1111
"strings"
1212
"sync"
13+
"syscall"
1314

1415
"cloud.google.com/go/storage"
1516
"github.com/google/osv.dev/go/logger"
@@ -27,21 +28,26 @@ func main() {
2728
logger.InitGlobalLogger()
2829
defer logger.Close()
2930

30-
ctx, span := otel.Tracer("exporter").Start(context.Background(), "exporter")
31+
ctx, stopSignal := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
32+
defer stopSignal()
33+
34+
ctx, span := otel.Tracer("exporter").Start(ctx, "exporter")
3135
defer span.End()
3236

3337
outBucketName := flag.String("bucket", "osv-test-vulnerabilities", "Output bucket or directory name. If -local is true, this is a local path; otherwise, it's a GCS bucket name.")
3438
vulnBucketName := flag.String("osv-vulns-bucket", os.Getenv("OSV_VULNERABILITIES_BUCKET"), "GCS bucket to read vulnerability protobufs from. Can also be set with the OSV_VULNERABILITIES_BUCKET environment variable.")
3539
uploadToGCS := flag.Bool("upload-to-gcs", false, "If false, writes the output to a local directory specified by -bucket instead of a GCS bucket.")
36-
numWorkers := flag.Int("workers", 200, "The total number of concurrent workers to use for downloading from GCS and writing the output.")
40+
numWorkers := flag.Int("workers", 1000, "The total number of concurrent workers to use for downloading from GCS and writing the output.")
41+
breakdownPrefixesStr := flag.String("breakdown-prefixes", "", "Comma-separated list of prefix breakdowns for parallel GCS object listing. Defaults to A-Z if empty.")
3742

3843
flag.Parse()
3944

4045
logger.InfoContext(ctx, "exporter starting",
4146
slog.String("bucket", *outBucketName),
4247
slog.String("osv-vulns-bucket", *vulnBucketName),
4348
slog.Bool("upload-to-gcs", *uploadToGCS),
44-
slog.Int("workers", *numWorkers))
49+
slog.Int("workers", *numWorkers),
50+
slog.String("breakdown-prefixes", *breakdownPrefixesStr))
4551

4652
if *vulnBucketName == "" {
4753
logger.FatalContext(ctx, "OSV_VULNERABILITIES_BUCKET must be set")
@@ -76,9 +82,25 @@ func main() {
7682
// 4. The `ecosystemWorker`s and the `allEcosystemWorker` process the vulnerabilities and
7783
// generate the final files, sending the data to be written to `routerToWriteCh`.
7884
// 5. A pool of `writer` workers receive the file data and write it to the output.
79-
gcsPathToDownloaderCh := make(chan string)
80-
downloaderToRouterCh := make(chan *osvschema.Vulnerability)
81-
routerToWriteCh := make(chan writeMsg)
85+
gcsPathToDownloaderCh := make(chan string, 100)
86+
downloaderToRouterCh := make(chan *osvschema.Vulnerability, 100)
87+
routerToWriteCh := make(chan writeMsg, 100)
88+
89+
var breakdownPrefixes []string
90+
if *breakdownPrefixesStr != "" {
91+
for _, p := range strings.Split(*breakdownPrefixesStr, ",") {
92+
p = strings.TrimSpace(p)
93+
if p != "" {
94+
breakdownPrefixes = append(breakdownPrefixes, p)
95+
}
96+
}
97+
}
98+
// Fall back to simple A-Z prefixes
99+
if len(breakdownPrefixes) == 0 {
100+
for ch := 'A'; ch <= 'Z'; ch++ {
101+
breakdownPrefixes = append(breakdownPrefixes, string(ch))
102+
}
103+
}
82104

83105
var downloaderWg sync.WaitGroup
84106
for range *numWorkers / 2 {
@@ -95,21 +117,13 @@ func main() {
95117
routerWg.Add(1)
96118
go ecosystemRouter(ctx, downloaderToRouterCh, routerToWriteCh, &routerWg)
97119

98-
prevPrefix := ""
99120
MainLoop:
100-
for obj, err := range vulnClient.Objects(ctx, gcsProtoPrefix) {
121+
for objName, err := range vulnClient.ObjectsFast(ctx, gcsProtoPrefix, breakdownPrefixes) {
101122
if err != nil {
102123
logger.FatalContext(ctx, "failed to list objects", slog.Any("err", err))
103124
}
104-
// Only log when we see a new ID prefix (i.e. roughly once per data source)
105-
prefix := filepath.Base(obj.Name)
106-
prefix, _, _ = strings.Cut(prefix, "-")
107-
if prefix != prevPrefix {
108-
logger.InfoContext(ctx, "iterating vulnerabilities", slog.String("now_at", obj.Name))
109-
prevPrefix = prefix
110-
}
111125
select {
112-
case gcsPathToDownloaderCh <- obj.Name:
126+
case gcsPathToDownloaderCh <- objName:
113127
case <-ctx.Done():
114128
break MainLoop
115129
}

‎go/cmd/exporter/worker.go‎

Lines changed: 31 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type ecosystemWorker struct {
3838

3939
// newEcosystemWorker creates and starts a new ecosystemWorker.
4040
func newEcosystemWorker(ctx context.Context, ecosystem string, outCh chan<- writeMsg, wg *sync.WaitGroup) *ecosystemWorker {
41-
ch := make(chan *osvschema.Vulnerability)
41+
ch := make(chan *osvschema.Vulnerability, 100)
4242
worker := &ecosystemWorker{
4343
ecosystem: ecosystem,
4444
inCh: ch,
@@ -65,24 +65,11 @@ func (w *ecosystemWorker) run(ctx context.Context, outCh chan<- writeMsg, wg *sy
6565
defer span.End()
6666

6767
logger.InfoContext(ctx, "new ecosystem worker started", slog.String("ecosystem", w.ecosystem))
68-
var allVulns []vulnData
69-
var csvData [][]string
68+
// 500 size is around the minimum each ecosystem would need, most ecosystems are much bigger.
69+
allVulns := make([]vulnData, 0, 500)
70+
csvData := make([][]string, 0, 500)
7071
var vanirVulns []vulnData
71-
WorkLoop:
72-
for {
73-
var v *osvschema.Vulnerability
74-
var ok bool
75-
76-
// Wait to receive a vulnerability, or be cancelled.
77-
select {
78-
case <-ctx.Done():
79-
logger.WarnContext(ctx, "ecosystem worker cancelled", slog.String("ecosystem", w.ecosystem), slog.Any("err", ctx.Err()))
80-
return
81-
case v, ok = <-w.inCh:
82-
if !ok {
83-
break WorkLoop
84-
}
85-
}
72+
for v := range w.inCh {
8673
// Process vulnerability.
8774
b, err := marshalToJSON(v)
8875
if err != nil {
@@ -114,6 +101,10 @@ WorkLoop:
114101
}
115102
}
116103

104+
if ctx.Err() != nil {
105+
return
106+
}
107+
117108
logger.InfoContext(ctx, "All vulnerabilities processed", slog.String("ecosystem", w.ecosystem))
118109
writeModifiedIDCSV(ctx, filepath.Join(w.ecosystem, modifiedCSVFilename), csvData, outCh)
119110
writeZIP(ctx, filepath.Join(w.ecosystem, allZipFilename), allVulns, outCh)
@@ -143,7 +134,7 @@ type allEcosystemWorker struct {
143134

144135
// newAllEcosystemWorker creates and starts a new allEcosystemWorker.
145136
func newAllEcosystemWorker(ctx context.Context, outCh chan<- writeMsg, wg *sync.WaitGroup) *allEcosystemWorker {
146-
ch := make(chan vulnAndEcos)
137+
ch := make(chan vulnAndEcos, 100)
147138
worker := &allEcosystemWorker{
148139
inCh: ch,
149140
}
@@ -161,32 +152,31 @@ func (w *allEcosystemWorker) run(ctx context.Context, outCh chan<- writeMsg, wg
161152
defer span.End()
162153

163154
logger.InfoContext(ctx, "all-ecosystem worker started")
164-
var allVulns []vulnData
165-
var csvData [][]string
155+
// We have currently about 1.8 million entries, so start at 100k
156+
allVulns := make([]vulnData, 0, 100000)
157+
csvData := make([][]string, 0, 100000)
166158
ecosystems := make(map[string]struct{})
167-
WorkLoop:
168-
for {
169-
select {
170-
case <-ctx.Done():
171-
logger.WarnContext(ctx, "all-ecosystem worker cancelled", slog.Any("err", ctx.Err()))
172-
return
173-
case v, ok := <-w.inCh:
174-
if !ok {
175-
break WorkLoop
176-
}
177-
b, err := marshalToJSON(v.Vulnerability)
178-
if err != nil {
179-
logger.ErrorContext(ctx, "failed to marshal vulnerability to json", slog.String("id", v.GetId()), slog.Any("err", err))
180-
continue
181-
}
182-
modified := v.GetModified().AsTime()
183-
allVulns = append(allVulns, vulnData{id: v.GetId(), modified: modified, data: b})
184-
for _, e := range v.ecosystems {
185-
ecosystems[e] = struct{}{}
186-
csvData = append(csvData, []string{modified.Format(time.RFC3339Nano), e + "/" + v.GetId()})
159+
for v := range w.inCh {
160+
b, err := marshalToJSON(v.Vulnerability)
161+
if err != nil {
162+
logger.ErrorContext(ctx, "failed to marshal vulnerability to json", slog.String("id", v.GetId()), slog.Any("err", err))
163+
continue
164+
}
165+
modified := v.GetModified().AsTime()
166+
allVulns = append(allVulns, vulnData{id: v.GetId(), modified: modified, data: b})
167+
for _, e := range v.ecosystems {
168+
ecosystems[e] = struct{}{}
169+
csvData = append(csvData, []string{modified.Format(time.RFC3339Nano), e + "/" + v.GetId()})
170+
if len(csvData)%10000 == 0 {
171+
logger.InfoContext(ctx, "processed N vulnerabilities", slog.Int("n", len(csvData)))
187172
}
188173
}
189174
}
175+
176+
if ctx.Err() != nil {
177+
return
178+
}
179+
190180
writeModifiedIDCSV(ctx, modifiedCSVFilename, csvData, outCh)
191181
writeZIP(ctx, allZipFilename, allVulns, outCh)
192182
ecos := slices.Collect(maps.Keys(ecosystems))

‎go/cmd/exporter/writer.go‎

Lines changed: 26 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,46 +27,37 @@ type writeMsg struct {
2727
// bucket or a local directory.
2828
func writer(ctx context.Context, cancel context.CancelFunc, inCh <-chan writeMsg, client clients.CloudStorage, pathPrefix string, wg *sync.WaitGroup) {
2929
defer wg.Done()
30-
for {
31-
select {
32-
case msg, ok := <-inCh:
33-
if !ok {
34-
// Channel closed.
35-
return
30+
for msg := range inCh {
31+
path := filepath.Join(pathPrefix, msg.path)
32+
if client != nil {
33+
// Skip the upload if the object already has the same content.
34+
if gcsContentUnchanged(ctx, client, path, msg.data) {
35+
continue
3636
}
37-
path := filepath.Join(pathPrefix, msg.path)
38-
if client != nil {
39-
// Skip the upload if the object already has the same content.
40-
if gcsContentUnchanged(ctx, client, path, msg.data) {
41-
break
42-
}
43-
err := client.WriteObject(ctx, path, msg.data, &clients.WriteOptions{
44-
ContentType: msg.mimeType,
45-
})
46-
if err != nil {
47-
logger.Error("failed to write file", slog.String("path", path), slog.Any("err", err))
48-
cancel()
37+
err := client.WriteObject(ctx, path, msg.data, &clients.WriteOptions{
38+
ContentType: msg.mimeType,
39+
})
40+
if err != nil {
41+
logger.Error("failed to write file", slog.String("path", path), slog.Any("err", err))
42+
cancel()
4943

50-
break
51-
}
52-
} else {
53-
// Write locally.
54-
dir := filepath.Dir(path)
55-
if err := os.MkdirAll(dir, 0755); err != nil {
56-
logger.Error("failed to create directories", slog.String("dir", dir), slog.Any("err", err))
57-
cancel()
44+
break
45+
}
46+
} else {
47+
// Write locally.
48+
dir := filepath.Dir(path)
49+
if err := os.MkdirAll(dir, 0755); err != nil {
50+
logger.Error("failed to create directories", slog.String("dir", dir), slog.Any("err", err))
51+
cancel()
5852

59-
break
60-
}
61-
if err := os.WriteFile(path, msg.data, 0600); err != nil {
62-
logger.Error("failed to write file", slog.String("path", path), slog.Any("err", err))
63-
cancel()
53+
break
54+
}
55+
if err := os.WriteFile(path, msg.data, 0600); err != nil {
56+
logger.Error("failed to write file", slog.String("path", path), slog.Any("err", err))
57+
cancel()
6458

65-
break
66-
}
59+
break
6760
}
68-
case <-ctx.Done():
69-
return
7061
}
7162
}
7263
}

0 commit comments

Comments
 (0)