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  }