github.com/cilium/cilium@v1.16.2/pkg/datapath/loader/cache.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package loader 5 6 import ( 7 "bytes" 8 "context" 9 "errors" 10 "fmt" 11 "os" 12 "path/filepath" 13 14 "github.com/cilium/ebpf" 15 "github.com/sirupsen/logrus" 16 17 "github.com/cilium/cilium/pkg/bpf" 18 "github.com/cilium/cilium/pkg/common" 19 "github.com/cilium/cilium/pkg/datapath/loader/metrics" 20 datapath "github.com/cilium/cilium/pkg/datapath/types" 21 "github.com/cilium/cilium/pkg/defaults" 22 "github.com/cilium/cilium/pkg/lock" 23 "github.com/cilium/cilium/pkg/logging/logfields" 24 ) 25 26 // objectCache amortises the cost of BPF compilation for endpoints. 27 type objectCache struct { 28 lock.Mutex 29 datapath.ConfigWriter 30 31 // The directory used for caching. Must not be accessed by another process. 32 workingDirectory string 33 34 baseHash datapathHash 35 36 // The cached objects. 37 objects map[string]*cachedSpec 38 } 39 40 type cachedSpec struct { 41 // Protects state, also used to serialize compilation attempts. 42 lock.Mutex 43 44 // The compiled and parsed spec. May be nil if no compilation has happened yet. 45 spec *ebpf.CollectionSpec 46 } 47 48 func newObjectCache(c datapath.ConfigWriter, workingDir string) *objectCache { 49 return &objectCache{ 50 ConfigWriter: c, 51 workingDirectory: workingDir, 52 objects: make(map[string]*cachedSpec), 53 } 54 } 55 56 // UpdateDatapathHash invalidates the object cache if the configuration of the 57 // datapath has changed. 58 func (o *objectCache) UpdateDatapathHash(nodeCfg *datapath.LocalNodeConfiguration) error { 59 newHash, err := hashDatapath(o.ConfigWriter, nodeCfg) 60 if err != nil { 61 return fmt.Errorf("hash datapath config: %w", err) 62 } 63 64 // Prevent new compilation from starting. 65 o.Lock() 66 defer o.Unlock() 67 68 // Don't invalidate if the hash is the same. 69 if bytes.Equal(newHash, o.baseHash) { 70 return nil 71 } 72 73 // Wait until all concurrent compilation has finished. 74 for _, obj := range o.objects { 75 obj.Lock() 76 } 77 78 if err := os.RemoveAll(o.workingDirectory); err != nil { 79 for _, obj := range o.objects { 80 obj.Unlock() 81 } 82 83 return err 84 } 85 86 o.baseHash = newHash 87 o.objects = make(map[string]*cachedSpec) 88 return nil 89 } 90 91 // serialize access to an abitrary key. 92 // 93 // The caller must call Unlock on the returned object. 94 func (o *objectCache) serialize(key string) *cachedSpec { 95 o.Lock() 96 defer o.Unlock() 97 98 obj, ok := o.objects[key] 99 if !ok { 100 obj = new(cachedSpec) 101 o.objects[key] = obj 102 } 103 104 obj.Lock() 105 return obj 106 } 107 108 // build attempts to compile and cache a datapath template object file 109 // corresponding to the specified endpoint configuration. 110 func (o *objectCache) build(ctx context.Context, nodeCfg *datapath.LocalNodeConfiguration, cfg datapath.EndpointConfiguration, stats *metrics.SpanStat, dir *directoryInfo, hash string) (string, error) { 111 isHost := cfg.IsHost() 112 templatePath := filepath.Join(o.workingDirectory, hash) 113 dir = &directoryInfo{ 114 Library: dir.Library, 115 Runtime: dir.Runtime, 116 Output: templatePath, 117 State: templatePath, 118 } 119 prog := epProg 120 if isHost { 121 prog = hostEpProg 122 } 123 124 objectPath := prog.AbsoluteOutput(dir) 125 126 if err := os.MkdirAll(dir.Output, defaults.StateDirRights); err != nil { 127 return "", fmt.Errorf("failed to create template directory: %w", err) 128 } 129 130 headerPath := filepath.Join(dir.State, common.CHeaderFileName) 131 f, err := os.Create(headerPath) 132 if err != nil { 133 return "", fmt.Errorf("failed to open template header for writing: %w", err) 134 } 135 defer f.Close() 136 if err = o.ConfigWriter.WriteEndpointConfig(f, nodeCfg, cfg); err != nil { 137 return "", fmt.Errorf("failed to write template header: %w", err) 138 } 139 140 stats.BpfCompilation.Start() 141 err = compileDatapath(ctx, dir, isHost, log) 142 stats.BpfCompilation.End(err == nil) 143 if err != nil { 144 return "", fmt.Errorf("failed to compile template program: %w", err) 145 } 146 147 log.WithFields(logrus.Fields{ 148 logfields.Path: objectPath, 149 logfields.BPFCompilationTime: stats.BpfCompilation.Total(), 150 }).Info("Compiled new BPF template") 151 152 return objectPath, nil 153 } 154 155 // fetchOrCompile attempts to fetch the path to the datapath object 156 // corresponding to the provided endpoint configuration, or if this 157 // configuration is not yet compiled, compiles it. It will block if multiple 158 // threads attempt to concurrently fetchOrCompile a template binary for the 159 // same set of EndpointConfiguration. 160 // 161 // Returns a copy of the compiled and parsed ELF and a hash identifying a cached entry. 162 func (o *objectCache) fetchOrCompile(ctx context.Context, nodeCfg *datapath.LocalNodeConfiguration, cfg datapath.EndpointConfiguration, dir *directoryInfo, stats *metrics.SpanStat) (spec *ebpf.CollectionSpec, hash string, err error) { 163 cfg = wrap(cfg) 164 165 // copySpec works around a bug in cilium/ebpf. 166 // 167 // See https://github.com/cilium/ebpf/issues/1517. 168 copySpec := func(s *ebpf.CollectionSpec) *ebpf.CollectionSpec { 169 s = s.Copy() 170 for _, m := range s.Maps { 171 if m.Extra != nil { 172 cpy := *m.Extra 173 m.Extra = &cpy 174 } 175 } 176 return s 177 } 178 179 hash, err = o.baseHash.hashTemplate(o, nodeCfg, cfg) 180 if err != nil { 181 return nil, "", err 182 } 183 184 // Capture the time spent waiting for the template to compile. 185 if stats != nil { 186 stats.BpfWaitForELF.Start() 187 defer func() { 188 // Wrap to ensure that "err" is compared upon return. 189 stats.BpfWaitForELF.End(err == nil) 190 }() 191 } 192 193 scopedLog := log.WithField(logfields.BPFHeaderfileHash, hash) 194 195 // Only allow a single concurrent compilation per hash. 196 obj := o.serialize(hash) 197 defer obj.Unlock() 198 199 if obj.spec != nil { 200 return copySpec(obj.spec), hash, nil 201 } 202 203 if stats == nil { 204 stats = &metrics.SpanStat{} 205 } 206 207 path, err := o.build(ctx, nodeCfg, cfg, stats, dir, hash) 208 if err != nil { 209 if !errors.Is(err, context.Canceled) { 210 scopedLog.WithError(err).Error("BPF template object creation failed") 211 } 212 return nil, "", err 213 } 214 215 obj.spec, err = bpf.LoadCollectionSpec(path) 216 if err != nil { 217 return nil, "", fmt.Errorf("load eBPF ELF %s: %w", path, err) 218 } 219 220 return copySpec(obj.spec), hash, nil 221 }