关于cache-control:DatenLord前沿技术分享No23

达坦科技专一于打造新一代开源跨云存储平台DatenLord,通过软硬件深度交融的形式买通云云壁垒,致力于解决多云架构、多数据中心场景下异构存储、数据对立治理需要等问题,以满足不同行业客户对海量数据跨云、跨数据中心高性能拜访的需要。在本周的前沿技术分享中,咱们邀请到了浙江大学在读硕士研究生缪晨露为大家分享她在MICRO和HPCA上发表的两篇论文的精髓,演讲主题是Something About Cache Security。 1、演讲题目Something About Cache Security 2、演讲工夫2023年4月23日上午10:30 3、演讲人缪晨露浙江大学硕士研究生在读 4、引言Cache的引入尽管进步了性能,但同时也引入了攻打的渠道。在本次分享中,我将简略分享一下几个cache平安相干的工作。 5、内容简介只管Cache的应用能够进步性能,但它也引入了攻打的危险。攻击者能够利用缓存未命中和命中的工夫等方面进行攻打,也能够利用缓存一致性等其余方面进行攻打。此外,像Spectre和Meltdown这样的瞬态执行攻打更是给系统安全带来了严重威胁。针对这些攻打,零碎设计者须要采取一系列的进攻措施,包含优化缓存拜访策略,继续关注和钻研新型攻打技术,及时更新进攻措施等。 6、直播预约欢迎您通过微信视频号:达坦科技DatenLrod预约直播,或者登陆腾讯会议观看直播:会议号:955-6910-3992

April 21, 2023 · 1 min · jiezi

关于cache-control:揭秘-cache-访问延迟背后的计算机原理

简介:本文介绍如何测试多级 cache 的访存提早,以及背地蕴含的计算机原理。 CPU 的 cache 往往是分多级的金字塔模型,L1 最靠近 CPU,拜访提早最小,但 cache 的容量也最小。本文介绍如何测试多级 cache 的访存提早,以及背地蕴含的计算机原理。 图源:https://cs.brown.edu/courses/... Cache LatencyWikichip[1] 提供了不同 CPU 型号的 cache 提早,单位个别为 cycle,通过简略的运算,转换为 ns。以 skylake 为例,CPU 各级 cache 提早的基准值为: CPU Frequency:2654MHz (0.3768 nanosec/clock) 设计试验1. naive thinking 申请一个 buffer,buffer size 为 cache 对应的大小,第一次遍历进行预热,将数据全副加载到 cache 中。第二次遍历统计耗时,计算每次 read 的提早平均值。 代码实现 mem-lat.c 如下: #include <sys/types.h>#include <stdlib.h>#include <stdio.h>#include <sys/mman.h>#include <sys/time.h>#include <unistd.h>#define ONE p = (char **)*p;#define FIVE ONE ONE ONE ONE ONE#define TEN FIVE FIVE#define FIFTY TEN TEN TEN TEN TEN#define HUNDRED FIFTY FIFTYstatic void usage(){ printf("Usage: ./mem-lat -b xxx -n xxx -s xxx\n"); printf(" -b buffer size in KB\n"); printf(" -n number of read\n\n"); printf(" -s stride skipped before the next access\n\n"); printf("Please don't use non-decimal based number\n");}int main(int argc, char* argv[]){ unsigned long i, j, size, tmp; unsigned long memsize = 0x800000; /* 1/4 LLC size of skylake, 1/5 of broadwell */ unsigned long count = 1048576; /* memsize / 64 * 8 */ unsigned int stride = 64; /* skipped amount of memory before the next access */ unsigned long sec, usec; struct timeval tv1, tv2; struct timezone tz; unsigned int *indices; while (argc-- > 0) { if ((*argv)[0] == '-') { /* look at first char of next */ switch ((*argv)[1]) { /* look at second */ case 'b': argv++; argc--; memsize = atoi(*argv) * 1024; break; case 'n': argv++; argc--; count = atoi(*argv); break; case 's': argv++; argc--; stride = atoi(*argv); break; default: usage(); exit(1); break; } } argv++; } char* mem = mmap(NULL, memsize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0); // trick3: init pointer chasing, per stride=8 byte size = memsize / stride; indices = malloc(size * sizeof(int)); for (i = 0; i < size; i++) indices[i] = i; // trick 2: fill mem with pointer references for (i = 0; i < size - 1; i++) *(char **)&mem[indices[i]*stride]= (char*)&mem[indices[i+1]*stride]; *(char **)&mem[indices[size-1]*stride]= (char*)&mem[indices[0]*stride]; char **p = (char **) mem; tmp = count / 100; gettimeofday (&tv1, &tz); for (i = 0; i < tmp; ++i) { HUNDRED; //trick 1 } gettimeofday (&tv2, &tz); if (tv2.tv_usec < tv1.tv_usec) { usec = 1000000 + tv2.tv_usec - tv1.tv_usec; sec = tv2.tv_sec - tv1.tv_sec - 1; } else { usec = tv2.tv_usec - tv1.tv_usec; sec = tv2.tv_sec - tv1.tv_sec; } printf("Buffer size: %ld KB, stride %d, time %d.%06d s, latency %.2f ns\n", memsize/1024, stride, sec, usec, (sec * 1000000 + usec) * 1000.0 / (tmp *100)); munmap(mem, memsize); free(indices);}这里用到了 3 个小技巧: ...

February 10, 2022 · 10 min · jiezi

Kubernetes Client-go Informer 源码分析

几乎所有的Controller manager 和CRD Controller 都会使用Client-go 的Informer 函数,这样通过Watch 或者Get List 可以获取对应的Object,下面我们从源码分析角度来看一下Client go Informer 的机制。kubeClient, err := kubernetes.NewForConfig(cfg)if err != nil { klog.Fatalf(“Error building kubernetes clientset: %s”, err.Error())}kubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)controller := NewController(kubeClient, exampleClient, kubeInformerFactory.Apps().V1().Deployments(), exampleInformerFactory.Samplecontroller().V1alpha1().Foos())// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)// Start method is non-blocking and runs all registered informers in a dedicated goroutine.kubeInformerFactory.Start(stopCh)这里的例子是以https://github.com/kubernetes/sample-controller/blob/master/main.go节选,主要以 k8s 默认的Deployment Informer 为例子。可以看到直接使用Client-go Informer 还是非常简单的,先不管NewCOntroller函数里面执行了什么,顺着代码来看一下kubeInformerFactory.Start 都干了啥。// Start initializes all requested informers.func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Run(stopCh) f.startedInformers[informerType] = true } }}可以看到这里遍历了f.informers,而informers 的定义我们来看一眼数据结构type sharedInformerFactory struct { client kubernetes.Interface namespace string tweakListOptions internalinterfaces.TweakListOptionsFunc lock sync.Mutex defaultResync time.Duration customResync map[reflect.Type]time.Duration informers map[reflect.Type]cache.SharedIndexInformer // startedInformers is used for tracking which informers have been started. // This allows Start() to be called multiple times safely. startedInformers map[reflect.Type]bool}我们这里的例子,在运行的时候,f.informers里面含有的内容如下type *v1.Deployment informer &{0xc000379fa0 <nil> 0xc00038ccb0 {} 0xc000379f80 0xc00033bb00 30000000000 30000000000 0x28e5ec8 false false {0 0} {0 0}}也就是说,每一种k8s 类型都会有自己的Informer函数。下面我们来看一下这个函数是在哪里注册的,这里以Deployment Informer 为例。首先回到刚开始初始化kubeClient 的代码,controller := NewController(kubeClient, exampleClient, kubeInformerFactory.Apps().V1().Deployments(), exampleInformerFactory.Samplecontroller().V1alpha1().Foos()) deploymentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: controller.handleObject, UpdateFunc: func(old, new interface{}) { newDepl := new.(*appsv1.Deployment) oldDepl := old.(*appsv1.Deployment) if newDepl.ResourceVersion == oldDepl.ResourceVersion { // Periodic resync will send update events for all known Deployments. // Two different versions of the same Deployment will always have different RVs. return } controller.handleObject(new) }, DeleteFunc: controller.handleObject, })注意这里的传参, kubeInformerFactory.Apps().V1().Deployments(), 这句话的意思就是指创建一个只关注Deployment 的Informer.controller := &Controller{ kubeclientset: kubeclientset, sampleclientset: sampleclientset, deploymentsLister: deploymentInformer.Lister(), deploymentsSynced: deploymentInformer.Informer().HasSynced, foosLister: fooInformer.Lister(), foosSynced: fooInformer.Informer().HasSynced, workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), “Foos”), recorder: recorder, }deploymentInformer.Lister() 这里就是初始化了一个Deployment Lister,下面来看一下Lister函数里面做了什么。// NewFilteredDeploymentInformer constructs a new informer for Deployment type.// Always prefer using an informer factory to get a shared informer instead of getting an independent// one. This reduces memory footprint and number of connections to the server.func NewFilteredDeploymentInformer(client kubernetes.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).List(options) }, WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } return client.AppsV1().Deployments(namespace).Watch(options) }, }, &appsv1.Deployment{}, resyncPeriod, indexers, )}func (f *deploymentInformer) defaultInformer(client kubernetes.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { return NewFilteredDeploymentInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)}func (f *deploymentInformer) Informer() cache.SharedIndexInformer { return f.factory.InformerFor(&appsv1.Deployment{}, f.defaultInformer)}func (f *deploymentInformer) Lister() v1.DeploymentLister { return v1.NewDeploymentLister(f.Informer().GetIndexer())}注意这里的Lister 函数,它调用了Informer ,然后触发了f.factory.InformerFor ,这就最终调用了sharedInformerFactory InformerFor函数,// InternalInformerFor returns the SharedIndexInformer for obj using an internal// client.func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { f.lock.Lock() defer f.lock.Unlock() informerType := reflect.TypeOf(obj) informer, exists := f.informers[informerType] if exists { return informer } resyncPeriod, exists := f.customResync[informerType] if !exists { resyncPeriod = f.defaultResync } informer = newFunc(f.client, resyncPeriod) f.informers[informerType] = informer return informer}这里可以看到,informer = newFunc(f.client, resyncPeriod)这句话最终完成了对于informer的创建,并且注册到了Struct object中,完成了前面我们的问题。下面我们再回到informer start // Start initializes all requested informers.func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { f.lock.Lock() defer f.lock.Unlock() for informerType, informer := range f.informers { if !f.startedInformers[informerType] { go informer.Run(stopCh) f.startedInformers[informerType] = true } }}这里可以看到,它会遍历所有的informer,然后选择异步调用Informer 的RUN方法。我们来全局看一下Run方法func (s *sharedIndexInformer) Run(stopCh <-chan struct{}) { defer utilruntime.HandleCrash() fifo := NewDeltaFIFO(MetaNamespaceKeyFunc, s.indexer) cfg := &Config{ Queue: fifo, ListerWatcher: s.listerWatcher, ObjectType: s.objectType, FullResyncPeriod: s.resyncCheckPeriod, RetryOnError: false, ShouldResync: s.processor.shouldResync, Process: s.HandleDeltas, } func() { s.startedLock.Lock() defer s.startedLock.Unlock() s.controller = New(cfg) s.controller.(*controller).clock = s.clock s.started = true }() // Separate stop channel because Processor should be stopped strictly after controller processorStopCh := make(chan struct{}) var wg wait.Group defer wg.Wait() // Wait for Processor to stop defer close(processorStopCh) // Tell Processor to stop wg.StartWithChannel(processorStopCh, s.cacheMutationDetector.Run) wg.StartWithChannel(processorStopCh, s.processor.run) defer func() { s.startedLock.Lock() defer s.startedLock.Unlock() s.stopped = true // Don’t want any new listeners }() s.controller.Run(stopCh)}首先它根据得到的 key 拆分函数和Store index 创建一个FIFO队列,这个队列是一个先进先出的队列,主要用来保存对象的各种事件。func NewDeltaFIFO(keyFunc KeyFunc, knownObjects KeyListerGetter) *DeltaFIFO { f := &DeltaFIFO{ items: map[string]Deltas{}, queue: []string{}, keyFunc: keyFunc, knownObjects: knownObjects, } f.cond.L = &f.lock return f}可以看到这个队列创建的比较简单,就是使用 Map 来存放数据,String 数组来存放队列的 Key。后面根据client 创建的List 和Watch 函数,还有队列创建了一个 config,下面将根据这个config 来初始化controller. 这个controller是client-go 的Cache controller ,主要用来控制从 APIServer 获得的对象的 cache 以及更新对象。下面主要关注这个函数调用wg.StartWithChannel(processorStopCh, s.processor.run)这里进行了真正的Listering 调用。func (p *sharedProcessor) run(stopCh <-chan struct{}) { func() { p.listenersLock.RLock() defer p.listenersLock.RUnlock() for _, listener := range p.listeners { p.wg.Start(listener.run) p.wg.Start(listener.pop) } p.listenersStarted = true }() <-stopCh p.listenersLock.RLock() defer p.listenersLock.RUnlock() for _, listener := range p.listeners { close(listener.addCh) // Tell .pop() to stop. .pop() will tell .run() to stop } p.wg.Wait() // Wait for all .pop() and .run() to stop}主要看 run 方法,还记得前面已经把ADD UPDATE DELETE 注册了自定义的处理函数了吗。这里就实现了前面函数的触发func (p processorListener) run() { // this call blocks until the channel is closed. When a panic happens during the notification // we will catch it, the offending item will be skipped!, and after a short delay (one second) // the next notification will be attempted. This is usually better than the alternative of never // delivering again. stopCh := make(chan struct{}) wait.Until(func() { // this gives us a few quick retries before a long pause and then a few more quick retries err := wait.ExponentialBackoff(retry.DefaultRetry, func() (bool, error) { for next := range p.nextCh { switch notification := next.(type) { case updateNotification: p.handler.OnUpdate(notification.oldObj, notification.newObj) case addNotification: p.handler.OnAdd(notification.newObj) case deleteNotification: p.handler.OnDelete(notification.oldObj) default: utilruntime.HandleError(fmt.Errorf(“unrecognized notification: %#v”, next)) } } // the only way to get here is if the p.nextCh is empty and closed return true, nil }) // the only way to get here is if the p.nextCh is empty and closed if err == nil { close(stopCh) } }, 1time.Minute, stopCh)}可以看到当p.nexhCh channel 接收到一个对象进入的时候,就会根据通知类型的不同,选择对应的用户注册函数去调用。那么这个channel 谁来向其中传入参数呢func (p *processorListener) pop() { defer utilruntime.HandleCrash() defer close(p.nextCh) // Tell .run() to stop var nextCh chan<- interface{} var notification interface{} for { select { case nextCh <- notification: // Notification dispatched var ok bool notification, ok = p.pendingNotifications.ReadOne() if !ok { // Nothing to pop nextCh = nil // Disable this select case } case notificationToAdd, ok := <-p.addCh: if !ok { return } if notification == nil { // No notification to pop (and pendingNotifications is empty) // Optimize the case - skip adding to pendingNotifications notification = notificationToAdd nextCh = p.nextCh } else { // There is already a notification waiting to be dispatched p.pendingNotifications.WriteOne(notificationToAdd) } } }}答案就是这个pop 函数,这里会从p.addCh中读取增加的通知,然后转给p.nexhCh 并且保证每个通知只会读取一次。下面就是最终的Controller run 函数,我们来看看到底干了什么// Run begins processing items, and will continue until a value is sent down stopCh.// It’s an error to call Run more than once.// Run blocks; call via go.func (c *controller) Run(stopCh <-chan struct{}) { defer utilruntime.HandleCrash() go func() { <-stopCh c.config.Queue.Close() }() r := NewReflector( c.config.ListerWatcher, c.config.ObjectType, c.config.Queue, c.config.FullResyncPeriod, ) r.ShouldResync = c.config.ShouldResync r.clock = c.clock c.reflectorMutex.Lock() c.reflector = r c.reflectorMutex.Unlock() var wg wait.Group defer wg.Wait() wg.StartWithChannel(stopCh, r.Run) wait.Until(c.processLoop, time.Second, stopCh)}这里主要的就是wg.StartWithChannel(stopCh, r.Run),// Run starts a watch and handles watch events. Will restart the watch if it is closed.// Run will exit when stopCh is closed.func (r *Reflector) Run(stopCh <-chan struct{}) { klog.V(3).Infof(“Starting reflector %v (%s) from %s”, r.expectedType, r.resyncPeriod, r.name) wait.Until(func() { if err := r.ListAndWatch(stopCh); err != nil { utilruntime.HandleError(err) } }, r.period, stopCh)}这里就调用了r.ListAndWatch 方法,这个方法比较复杂,我们慢慢来看。// watchHandler watches w and keeps *resourceVersion up to date.func (r *Reflector) watchHandler(w watch.Interface, resourceVersion *string, errc chan error, stopCh <-chan struct{}) error { start := r.clock.Now() eventCount := 0 // Stopping the watcher should be idempotent and if we return from this function there’s no way // we’re coming back in with the same watch interface. defer w.Stop() // update metrics defer func() { r.metrics.numberOfItemsInWatch.Observe(float64(eventCount)) r.metrics.watchDuration.Observe(time.Since(start).Seconds()) }()loop: for { select { case <-stopCh: return errorStopRequested case err := <-errc: return err case event, ok := <-w.ResultChan(): if !ok { break loop } if event.Type == watch.Error { return apierrs.FromObject(event.Object) } if e, a := r.expectedType, reflect.TypeOf(event.Object); e != nil && e != a { utilruntime.HandleError(fmt.Errorf("%s: expected type %v, but watch event object had type %v", r.name, e, a)) continue } meta, err := meta.Accessor(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) continue } newResourceVersion := meta.GetResourceVersion() switch event.Type { case watch.Added: err := r.store.Add(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to add watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Modified: err := r.store.Update(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to update watch event object (%#v) to store: %v", r.name, event.Object, err)) } case watch.Deleted: // TODO: Will any consumers need access to the “last known // state”, which is passed in event.Object? If so, may need // to change this. err := r.store.Delete(event.Object) if err != nil { utilruntime.HandleError(fmt.Errorf("%s: unable to delete watch event object (%#v) from store: %v", r.name, event.Object, err)) } default: utilruntime.HandleError(fmt.Errorf("%s: unable to understand watch event %#v", r.name, event)) } resourceVersion = newResourceVersion r.setLastSyncResourceVersion(newResourceVersion) eventCount++ } } watchDuration := r.clock.Now().Sub(start) if watchDuration < 1time.Second && eventCount == 0 { r.metrics.numberOfShortWatches.Inc() return fmt.Errorf(“very short watch: %s: Unexpected watch close - watch lasted less than a second and no items received”, r.name) } klog.V(4).Infof("%s: Watch close - %v total %v items received", r.name, r.expectedType, eventCount) return nil}这里就是真正调用watch 方法,根据返回的watch 事件,将其放入到前面创建的 FIFO 队列中。最终调用了controller 的POP 方法// processLoop drains the work queue.// TODO: Consider doing the processing in parallel. This will require a little thought// to make sure that we don’t end up processing the same object multiple times// concurrently.//// TODO: Plumb through the stopCh here (and down to the queue) so that this can// actually exit when the controller is stopped. Or just give up on this stuff// ever being stoppable. Converting this whole package to use Context would// also be helpful.func (c *controller) processLoop() { for { obj, err := c.config.Queue.Pop(PopProcessFunc(c.config.Process)) if err != nil { if err == FIFOClosedError { return } if c.config.RetryOnError { // This is the safe way to re-enqueue. c.config.Queue.AddIfNotPresent(obj) } } }}前面是将 watch 到的对象加入到队列中,这里的goroutine 就是用来消费的。具体的消费函数就是前面创建的Process 函数func (s *sharedIndexInformer) HandleDeltas(obj interface{}) error { s.blockDeltas.Lock() defer s.blockDeltas.Unlock() // from oldest to newest for _, d := range obj.(Deltas) { switch d.Type { case Sync, Added, Updated: isSync := d.Type == Sync s.cacheMutationDetector.AddObject(d.Object) if old, exists, err := s.indexer.Get(d.Object); err == nil && exists { if err := s.indexer.Update(d.Object); err != nil { return err } s.processor.distribute(updateNotification{oldObj: old, newObj: d.Object}, isSync) } else { if err := s.indexer.Add(d.Object); err != nil { return err } s.processor.distribute(addNotification{newObj: d.Object}, isSync) } case Deleted: if err := s.indexer.Delete(d.Object); err != nil { return err } s.processor.distribute(deleteNotification{oldObj: d.Object}, false) } } return nil}这个函数就是根据传进来的obj,先从自己的cache 中取一下,看是否存在,如果存在就代表是Update ,那么更新自己的队列后,调用用户注册的Update 函数,如果不存在,就调用用户的 Add 函数。到此Client-go 的Informer 流程源码分析基本完毕。本文作者:xianlubird 阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 22, 2019 · 8 min · jiezi

Nginx 是如何让你的缓存延期的

当 Nginx 使用 proxy cache 的文件作为响应时,它会更新其中的一些内容,比如 Date 响应头;但大部分响应头都不会得到更新,比如 Expires 和 Cache-Control。众所周知,Cache-Control 可以通过 max-age=xxx 或者 s-maxage=xxx 指令设置缓存的有效时间。跟 Expires 响应头不同,这一时间是相对的。假设上游服务器返回 Cache-Control: public; max-age=3600,那么 Nginx 会缓存该响应一小时。如果在这一小时到期之前,Client 访问了 Nginx,它会获取到同样的 Cache-Control 响应头,因此会再缓存多一小时。所以总体上该响应会被缓存两小时。这听起来很让人惊讶。但仔细想想,其实也不算什么严重的问题。首先,当我们设置 max-age=3600 时,大多数情况下并不要求其严格地在一小时后过期。其次,这个算是一般的多层缓存固有的弊端:缓存数据的最大过期时间,取决于各级缓存 TTL 的总和。如果想要避免,你可以选择根据外层数据剩下的 TTL 设置当前 TTL;或者提供主动 purge 的操作,从最外层开始逐层清理数据。当然,某些时候下,这一行为会带来一些问题。举个例子,假设我们开启了 proxy_cache_use_stale,在上游服务器出问题时使用过期的内容代替正常的响应。这种情况下,缓存只是作为一个临时救急的方案使用,我们并不希望 Client 多缓存更多的时间。否则会有上游应用的开发者抱怨,为何上游服务器已经正常了,用户刷新页面看到的还是旧数据。作为解决办法,我们可以在 Nginx 的 header filter 阶段,通过 Lua 代码或者 Nginx C module,把 Cache-Control: max-age=… 修改成 Cache-Control: no-cache。这么一来,Client 会在使用缓存之前先验证下,如果 Nginx 返回 304 状态码,那么该缓存会被继续使用;如果上游已经 OK 了且更新了响应,那么 Client 就会重新请求,避免使用过期的内容。这里需要强调下,no-cache 并非如字面上的意义表示不缓存,而是要求 Client 在使用该缓存之前,需要先验证下被缓存的内容是否还是最新的。MDN 的说法是:Forces caches to submit the request to the origin server for validation before releasing a cached copy.对应的,RFC 7234 的说法:The “no-cache” request directive indicates that a cache MUST NOT use a stored response to satisfy the request without successful validation on the origin server.如果要想让 Client 不缓存响应的内容,按 MDN 上的说法,需要用 Cache-Control: no-cache, no-store, must-revalidate(https://developer.mozilla.org…)。仔细看了下 no-cache / no-store / must-revalidate 这三项指令的介绍,似乎 no-store 就能让 Client 不用这个缓存,因为 no-store 要求:The cache should not store anything about the client request or server response.另外 must-revalidate 要求在使用过期缓存前验证下该内容是否是最新的,而 no-cache 也是要求重新验证的,那为什么需要两个都一起用呢?Google 搜索把我带到了这个 SO 问答:https://stackoverflow.com/que…。这个回答里面解释了为何不单单用 no-store:因为臭名昭著的 IE6 浏览器在处理 no-store 时有 bug。但可惜的是,这个回答没有给出这一论断的证据,比如 IE 的 bug report 之类。MDN 在给出 Cache-Control: no-cache, no-store, must-revalidate 这个例子的时候,也没有提及更多的上下文。这很像没有任何注释的老代码:我们不知道当初为何这么写,而把它删掉似乎不会带来什么问题。 ...

December 18, 2018 · 1 min · jiezi

HTTP缓存(Cache-Control、Expires 、ETag)

HTTP缓存HTTP缓存( ETag、Cache-Control)——谷歌开发者HTTP缓存有利于web性能优化Cache-Control例子假设我们首页有一个请求,请求js文件<script src="./main.js"></script>如何让CSS和JS请求速度加快?此时打开首页发现这个文件大小为279KB,使用时间为382ms如果再次刷新首页,那么这个文件还会被再次请求一次。那么如何重复利用之前获取的资源而不用反复请求呢?答案是HTTP缓存,这是性能优化的一个重要方面。接下来在响应里设置响应头Cache-Control: max-age=30刷新两次首页第二次的时间为0响应头中的Cache-Control: max-age=30表示客户端将这个缓存最多 保存30 秒,30秒后再次请求文件将会再次下载。即:设置这个响应头之后,浏览器请求时发现是相同的URL,浏览器直接从内存里返回已经缓存的main.js,没有向服务器发出请求问题为什么首页不设置Cache-Control呢?如果首页缓存,刷新首页的时候根本不会请求服务器,那么如果服务器更新了代码,浏览器将没有办法接收到新的版本。一般首页,HTML不要设置Cache-Control。js和css要设置久一点例如10年,即一直保留有缓存。那么js和css更新了怎么办?浏览器请求时发现是相同的URL才使用缓存,那么可以设置查询参数,例如第二个版本的js可以写<script src="./main.js?v=2"></script>,来保证URL的不同,重新获取新的js文件。这样即可以缓存很久,又可以随时更新例如知乎的网页里的请求:总结通过网络获取内容既速度缓慢又开销巨大。较大的响应需要在客户端与服务器之间进行多次往返通信,这会延迟浏览器获得和处理内容的时间,还会增加访问者的流量费用。因此,缓存并重复利用之前获取的资源的能力成为性能优化的一个关键方面。好在每个浏览器都自带了 HTTP 缓存实现功能。您只需要确保每个服务器响应都提供正确的 HTTP标头指令,以指示浏览器何时可以缓存响应以及可以缓存多久。ExpiresExpires 是以前用来控制缓存的http头,Cache-Control是新版的API。现在首选 Cache-Control。如果在Cache-Control响应头设置了 “max-age” 或者 “s-max-age” 指令,那么 Expires 头会被忽略。响应头设置方式:Expires: Wed, 21 Oct 2015 07:28:00 GMTExpires 响应头包含日期/时间, 即在此时候之后,响应过期。注意: 因为过期标准的时间用的是本地时间,所以不靠谱,所以要游侠使用Cache-Control代替Expires区别Cache-Control设置时间长度Expires 设置时间点详细:Expires - HTTP | MDNMD5MD5是消息摘要算法。用于确保信息传输完整一致。具体作用这样接受一个String 或 Buffer,返回一个固定的String如果接受的String改变,那么返回的String也会改变例如将1.txt中的其中一个1改为0,那么返回值如下可见返回至完全改变了这个特性可以用来判断两次信息传输是否完整一致ETag例子:例如我们请求一个js文件。设置一个ETag响应头设置的ETage响应头为这个JS文件的MD5值查看响应:那么:下一次请求这个JS的时候,浏览器会把上一次响应的那个ETage的值放到If-None-Match里面,如图:这样做的作用是:如果请求和响应的MD5一样,说明不需要重新下载这个js文件。这时我们修改代码:如果MD5一样,说明文件没改过,那么返回304304 Not Modified:HTTP 304 未改变说明无需再次传输请求的内容,也就是说可以使用缓存的内容。HTTP 304 没有响应体,因为不需要下载响应内容,直接用缓存就行了ETag与 Cache-Control的区别假设我们请求两个文件,CSS文件使用Cache-Control缓存,js文件使用ETag。代码如下:两个请求区别:所以:由于CSS的请求是用缓存(Cache-Control)的,所以直接不发请求而js用的ETag,有请求也有响应,只不过如果MD5一样,那么就不下载响应体我的博客即将同步至腾讯云+社区,邀请大家一同入驻:https://cloud.tencent.com/dev…

October 17, 2018 · 1 min · jiezi

Session

SessionCookie 和 Session区别与联系由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session。典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几件物品。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件、集群等。服务端如何识别特定的客户?第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,就可以依据此来识别不同客户端了。如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。总结:Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。来源链接:https://www.zhihu.com/questio…什么是session?服务器通过Cookie发送给客户端一个sessionIDsessionID对应服务器里的一小块内存,这里保存着用户的信息,例如登录信息,购物车信息等。每次用户访问服务器的时候,服务器通过浏览器发送来的cookie里的sessionID去读取对应的内存里的信息,以此来知道用户的隐私信息。注意session的好处是防止用户随意篡改cookie,获取别人的信息。如果用户随意篡改了sessionID,那么只能重新登录。因为sessionID是随机数,或者随机数夹杂着一些字母,所以没有可能暴力破解sessionID,获取别的用户的信息。类比:session相当于发会员卡,会员卡上只有卡号(sessionID)。下次去健身房的时候,只要看卡号上,就能确定你本人的去他信息。而cookie相当于把信息都写在会员卡上了。关于session的实现代码演示(nodejs)总结Session 与 Cookie 的关系一般来说,Session 基于 Cookie 来实现。Cookie服务器通过 Set-Cookie 头给客户端一串字符串客户端每次访问相同域名的网页时,必须带上这段字符串客户端要在一段时间内保存这个CookieCookie 默认在用户关闭页面后就失效,后台代码可以任意设置 Cookie 的过期时间大小大概在 4kb 以内SessionSession(不翻译,或翻译为会话)将 SessionID(随机数)通过 Cookie 发给客户端客户端访问服务器时,服务器读取 SessionID服务器有一块内存(哈希表)保存了所有 session通过 SessionID 我们可以得到对应用户的隐私信息,如 id、email这块内存(哈希表)就是服务器上的所有 session

October 16, 2018 · 1 min · jiezi