Go分布式爬虫学习笔记(十四)

文章目录

  • 14_context
  • 为什么需要Context?
  • 级联退出
  • Context 的使用方法。
  • context.Value
  • context
  • Context 最佳实践
  • Context 底层原理

14_context

Never start a goroutine without knowing how it will stop。

如果你不知道协程如何退出,就不要使用它。

为什么需要Context?

  • 安全并及时地停止协程和与协程关联的子协程,避免白白消耗资源。

  • 在没有 Context 之前我们一般会怎么做呢?我们需要借助通道的 close 机制,这个机制会唤醒所有监听该通道的协程,并触发相应的退出逻辑。写法大致如下:

    select {
      case <-c:
        // 业务逻辑
      case <-done:
        fmt.Println("退出协程")
    }
    
  • 为了对超时进行规范处理,在 Go 1.7 之后,Go 官方引入了 Context 来实现协程的退出。

  • Context 还提供了跨协程、甚至是跨服务的退出管理。

  • Context 本身的含义是上下文,我们可以理解为它内部携带了超时信息、退出信号,以及其他一些上下文相关的值(例如携带本次请求中上下游的唯一标识 trace_id)。 由于 Context 携带了上下文信息,父子协程就可以“联动”了。

级联退出

如下图所示,服务器处理 HTTP 请求一般会单独开辟一个协程,假设该处理协程调用了函数 A,函数 A 中也可能创建一个新的协程。

假设新的协程调用了函数 G,函数 G 中又有可能通过 RPC 远程调用了其他服务的 API,并最终调用了函数 F。

image

假设这个时候上游将连接断开,或者服务处理时间超时,我们希望能够立即退出函数 A、函数 G 和函数 F 所在的协程。

在实际场景中可能是这样的,上游给服务的处理时间是 500ms,超过这一时间这一请求就无效了。

A 服务当前已经花费了 200ms 的时间,G 又用了 100ms 调用 RPC,那么留给 F 的处理时间就只有 200ms 了。

如果远程服务 F 在 200ms 后没有返回,所有协程都需要感知到并快速关闭。

而使用 Context 标准库就是当前处理这种协程级联退出的标准做法。

Context 的使用方法。

Context 标准库中重要的结构 context.Context 其实是一个接口,它提供了 Deadline、Done、Err、Value 这 4 种方法:

type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key interface{}) interface{}
}
  • Deadline 方法用于返回 Context 的过期时间。Deadline 第一个返回值表示 Context 的过期时间,第二个返回值表示是否设置了过期时间,如果多次调用 Deadline 方法会返回相同的值。
  • Done 是使用最频繁的方法,它会返回一个通道。一般的做法是调用者在 select 中监听该通道的信号,如果该通道关闭则表示服务超时或异常,需要执行后续退出逻辑。多次调用 Done 方法会返回相同的通道。
  • 通道关闭后,Err 方法会返回退出的原因。
  • Value 方法返回指定 key 对应的 value,这是 Context 携带的值。key 必须是可比较的,一般的用法 key 是一个全局变量,通过context.WithValue 将 key 存储到 Context 中,并通过Context.Value 方法取出。

Context 接口中的这四个方法可以被多次调用,其返回的结果相同。同时,Context 的接口是并发安全的,可以被多个协程同时使用。

context.Value

  • 一般在远程过程调用中使用
    例如存储分布式链路跟踪的 traceId 或者鉴权相关的信息,并且该值的作用域在请求结束时终结。
    同时 key 必须是访问安全的,因为可能有多个协程同时访问它。

  • 如下所示,withAuth 函数是一个中间件,它可以让我们在完成实际的 HTTP 请求处理前进行 hook。
    在这个例子中,我们获取了 HTTP 请求 Header 头中的鉴权字段 Authorization,并将其存入了请求的上下文 Context 中。
    而实际的处理函数 Handle 会从 Context 中获取并验证用户的授权信息,以此判断用户是否已经登录。

    const TokenContextKey = "MyAppToken"
    
    // 中间件
    func WithAuth(a Authorizer, next http.Handler) http.Handler {
      return http.HandleFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        if auth == "" {
          next.ServeHTTP(w, r) // 没有授权
          return
        }
        token, err := a.Authorize(auth)
        if err != nil {
          http.Error(w, err.Error(), http.StatusUnauthorized)
          return
        }
        ctx := context.WithValue(r.Context(), TokenContextKey, token)
        next.ServeHTTP(w, r.WithContext(ctx))
      })
    }
    
    // HTTP请求实际处理函数
    func Handle(w http.ResponseWriter, r *http.Request) {
      // 获取授权
      if token := r.Context().Value(TokenContextKey); token != nil {
        // 用户登录
      } else {
        // 用户未登录
      }
    }
    
  • Context 是一个接口,这意味着需要有对应的具体实现。用户可以自己实现 Context 接口,并严格遵守 Context 接口规定的语义。当然,我们使用得最多的还是 Go 标准库中的实现。

context

  • 当我们调用 context.Background 函数或 context.TODO 函数时,会返回最简单的 Context 实现。
  • context.Background 返回的 Context 一般是作为根对象存在,不具有任何功能,不可以退出,也不能携带值。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
  • WithCancel 函数会返回一个子 Context 和 cancel 方法。子 Context 会在两种情况下触发退出:一种情况是调用者主动调用了返回的 cancel 方法;另一种情况是当参数中的父 Context 退出时,子 Context 将级联退出。
  • WithTimeout 函数指定超时时间。当超时发生后,子 Context 将退出。因此,子 Context 的退出有三种时机,一种是父 Context 退出;一种是超时退出;最后一种是主动调用 cancel 函数退出。
  • WithDeadline 和 WithTimeout 函数的处理方法相似,不过它们的参数指定的是最后到期的时间。
  • WithValue 函数会返回带 key-value 的子 Context。

Context 最佳实践

在网络连接到请求处理的多个阶段,都可能有相对应的超时时间。

以 HTTP 请求为例,http.Client 有一个参数 Timeout 用于指定当前请求的总超时时间,它包括从连接、发送请求、到处理服务器响应的时间的总和。

c := &http.Client{
    Timeout: 15 * time.Second,
}
resp, err := c.Get("<https://baidu.com/>")

标准库 client.Do 方法内部会将超时时间换算为截止时间并传递到下一层。setRequestCancel 函数内部则会调用 context.WithDeadline ,派生出一个子 Context 并赋值给 req 中的 Context。

func (c *Client) do(req *Request) (retres *Response, reterr error) {
  ...
  deadline      = c.deadline()
  c.send(req, deadline);
}

func setRequestCancel(req *Request, rt RoundTripper, deadline time.Time) {
    req.ctx, cancelCtx = context.WithDeadline(oldCtx, deadline)
    ...
}

在获取连接时,如果从闲置连接中找不到连接,则需要陷入 select 中去等待。如果连接时间超时,req.Context().Done() 通道会收到信号立即退出。在实际发送数据的 transport.roundTrip 函数中.

func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error){
...
select {
  case <-w.ready:
    return w.pc, w.err
  case <-req.Cancel:
    return nil, errRequestCanceledConn
  case <-req.Context().Done():
    return nil, req.Context().Err()
    return nil, err
  }
}

获取 TCP 连接需要调用 sysDialer.dialSerial 方法,dialSerial 的功能是从 addrList 地址列表中取出一个地址进行连接,如果与任一地址连接成功则立即返回。代码如下所示,不出所料,该方法的第一个参数为上游传递的 Context。

// net/dial.go
func (sd *sysDialer) dialSerial(ctx context.Context, ras addrList) (Conn, error) {
  for i, ra := range ras {
    // 协程是否需要退出
    select {
    case <-ctx.Done():
      return nil, &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: mapErr(ctx.Err())}
    default:
    }

    dialCtx := ctx

     // 是否设置了超时时间
    if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
      // 计算连接的超时时间
      partialDeadline, err := partialDeadline(time.Now(), deadline, len(ras)-i)
      if err != nil {
        // 已经超时了.
        if firstErr == nil {
          firstErr = &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: err}
        }
        break
      }
      // 派生出新的context,传递给下游
      if partialDeadline.Before(deadline) {
        var cancel context.CancelFunc
        dialCtx, cancel = context.WithDeadline(ctx, partialDeadline)
        defer cancel()
      }
    }

    c, err := sd.dialSingle(dialCtx, ra)
    ...
}

我们来看看 dialSerial 函数几个比较有代表性的 Context 用法。

  • 首先,第 3 行代码遍历地址列表时,判断 Context 通道是否已经退出,如果没有退出,会进入到 select 的 default 分支。如果通道已经退出了,则直接返回,因为继续执行已经没有必要了。
  • 接下来,第 14 行代码通过 ctx.Deadline() 判断是否传递进来的 Context 有超时时间。
    如果有超时时间,我们需要协调好后面每一个连接的超时时间。
    例如,我们总的超时时间是 600ms,一共有 3 个连接,那么每个连接分到的超时时间就是 200ms,这是为了防止前面的连接过度占用了时间。partialDeadline 会帮助我们计算好每一个连接的新的到期时间,如果该到期时间小于总到期时间,我们会派生出一个子 Context 传递给 dialSingle 函数,用于控制该连接的超时。
  • dialSingle 函数中调用了 ctx.Value,用来获取一个特殊的接口 nettrace.Trace。nettrace.Trace 用于对网络包中一些特殊的地方进行 hook。dialSingle 函数作为网络连接的起点,如果上下文中注入了 trace.ConnectStart 函数,则会在 dialSingle 函数之前调用 trace.ConnectStart 函数,如果上下文中注入了 trace.ConnectDone 函数,则会在执行 dialSingle 函数之后调用 trace.ConnectDone 函数。
func (sd *sysDialer) dialSingle(ctx context.Context, ra Addr) (c Conn, err error) {
  trace, _ := ctx.Value(nettrace.TraceKey{}).(*nettrace.Trace)
  if trace != nil {
    raStr := ra.String()
    if trace.ConnectStart != nil {
      trace.ConnectStart(sd.network, raStr)
    }
    if trace.ConnectDone != nil {
      defer func() { trace.ConnectDone(sd.network, raStr, err) }()
    }
  }
  la := sd.LocalAddr
  switch ra := ra.(type) {
  case *TCPAddr:
    la, _ := la.(*TCPAddr)
    // tcp连接
    c, err = sd.dialTCP(ctx, la, ra)
    ...  
}

由于标准库为我们提供了 Timeout 参数,我们在项目中实践超时控制就容易多了。只要在 BrowserFetch 结构体中增加 Timeout 超时参数,然后设置超时参数到 http.Client 中就大功告成了。

Context 底层原理

Context 在很大程度上利用了通道的一个特性:通道在 close 时,会通知所有监听它的协程。

每个派生出的子 Context 都会创建一个新的退出通道,这样,只要组织好 Context 之间的关系,就可以实现继承链上退出信号的传递。如图所示的三个协程中,关闭通道 A 会连带关闭调用链上的通道 B,通道 B 会关闭通道 C。

image

Context.Background 函数和 Context.TODO 函数会生成一个根 Context。

要使用 context 的退出功能,需要调用 WithCancel 或 WithTimeout,派生出一个新的结构 Context。

WithCancel 底层对应的结构为 cancelCtx,WithTimeout 底层对应的结构为 timerCtx,timerCtx 包装了 cancelCtx,并存储了超时时间。

type cancelCtx struct {
  Context   //保留了父 Context 的信息

  mu       sync.Mutex   
  done     atomic.Value  
  children map[canceler]struct{} //保存了当前 Context 派生的子 Context 的信息
  err      error
}

type timerCtx struct {
  cancelCtx
  timer *time.Timer 

  deadline time.Time
}

而 WithDeadline 函数会先判断父 Context 设置的超时时间是否比当前 Context 的超时时间短。如果是,那么子协程会随着父 Context 的退出而退出,没有必要再设置定时器。

当我们使用了标准库中默认的 Context 实现时,propagateCancel 函数会将子 Context 加入父协程的 children 哈希表中,并开启一个定时器。当定时器到期时,会调用 cancel 方法关闭通道,级联关闭当前 Context 派生的子 Context,并取消与父 Context 的绑定关系。这种特性就产生了调用链上连锁的退出反应。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  
  ...
   // 关闭当前通道
  close(d)
  // 级联关闭当前context派生的子context
  for child := range c.children {
    child.cancel(false, err)
  }
  c.children = nil
  c.mu.Unlock()
  // 从父context中能够删除当前context关联
  if removeFromParent {
    removeChild(c.Context, c)
  }
}

「此文章为3月Day14学习笔记,内容来源于极客时间《Go分布式爬虫实战》,强烈推荐该课程!/推荐该课程」

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/5101.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

家政服务系统APP小程序需具备哪些功能?

由于工作忙碌或者是懒人经济作祟&#xff0c;现代人对于家政服务的需求直线上升。而且互联网技术深入生活的方方面面&#xff0c;让上门家政服务系统开发成为很多线下家政公司转型互联网的方式&#xff0c;那么开发一款家政服务系统APP小程序需具备哪些功能呢&#xff1f; …

逻辑回归 算法推导与基于Python的实现详解

文章目录1 逻辑回归概述2 逻辑回归公式推导与求解2.1 公式推导2.2公式求解3 基于Python的实现3.1可接收参数3.2 完整代码示例1 逻辑回归概述 逻辑回归&#xff08;Logistic Regression&#xff09;是一种用于分类问题的统计学习方法。它基于线性回归的原理&#xff0c;通过将线…

【软考——系统架构师】架构、系分、软设的区别和联系

&#x1f50e;这里是【软考——系统架构师】&#xff0c;关注我考试轻松过线 &#x1f44d;如果对你有帮助&#xff0c;给博主一个免费的点赞以示鼓励 欢迎各位&#x1f50e;点赞&#x1f44d;评论收藏⭐️ 文章目录&#x1f440;三科相同点&#x1f440;三科不同点--上午题&am…

CISAW-CISDR灾难备份与恢复专业级认证

证书样板&#xff1a; 认证机构 中国网络安全审查技术与认证中心&#xff08;英文缩写为&#xff1a;CCRC,原为中国信息安全认证中心&#xff09;于 2006 年由中央机构编制委员会办公室批准成立&#xff0c;为国家市场监督管理总局直属事业单位。依据《网络安全法》 《网络安…

Java每日一练(20230401)

目录 1. 合并K个升序链表 &#x1f31f;&#x1f31f;&#x1f31f; 2. 最长有效括号 &#x1f31f;&#x1f31f;&#x1f31f; 3. 分割回文串 &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一练 专栏 …

Linux系统一键安装最新内核并开启 BBR 脚本

本脚本适用环境 系统支持&#xff1a;CentOS 6&#xff0c;Debian 8&#xff0c;Ubuntu 16 虚拟技术&#xff1a;OpenVZ 以外的&#xff0c;比如 KVM、Xen、VMware 内存要求&#xff1a;≥128M 更新日期&#xff1a;2022 年 5 月 11 日 关于本脚本 1、本脚本已在 蓝易云 上…

后端Springboot框架搭建APi接口开发(第二章)

上一章我讲述了如何使用Mybatis操作数据库。这一章我讲述如何利用Sptring框架搭建API接口 第一节&#xff1a;封装SqlSessionFactory工具类 在API操作数据库大量调用SqlSessionFactory&#xff0c;因此应将SqlSessionFactory封装成工具类供方法随时调用 在文件结构中的util文…

前端直接生成GIF动态图实践

前言去年在博客中发了两篇关于GIF动态生成的博客&#xff0c;GIF图像动态生成-JAVA后台生成和基于FFmpeg的Java视频Mp4转GIF初探&#xff0c;在这两篇博客中都是采用JAVA语言在后台进行转换。使用JAVA的同学经过自己的改造和开发也可以应用在项目上。前段时间有朋友私下问&…

MySQL主从复制、读写分离(MayCat2)实现数据同步

文章目录1.MySQL主从复制原理。2.实现MySQL主从复制&#xff08;一主两从&#xff09;。3.基于MySQL一主两从配置&#xff0c;完成MySQL读写分离配置。&#xff08;MyCat2&#xff09;1.MySQL主从复制原理。 MySQL主从复制是一个异步的复制过程&#xff0c;底层是基于Mysql数据…

设计模式之门面模式(C++)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 一、门面模式是什么&#xff1f; 门面模式是一种结构型的软件设计模式&#xff0c;也叫外观模式&#xff0c;它提供了统一的接口去…

程序员的天花板到底有多高?

程序员是很多人眼里的职业天花板&#xff0c;毕竟薪资水平、发展前景摆在那里&#xff0c;而且&#xff0c;一个又会debug又会修电脑&#xff08;划掉&#xff09;&#xff0c;还会费心为你制造浪漫的程序员&#xff0c;有谁不喜欢呢&#xff1f; 但现实中很多程序员都焦虑担忧…

从0使用TCP手撸http服务器六

html模板设计&#xff1a; 上篇我们说到通过路由去返回不同的html页面&#xff0c;如果每一个页面都是一个数组的话&#xff0c;那么我们可能需要很多数组&#xff0c;里面很多内容都是一样的&#xff0c;这样子会浪费我们单片机很多flash&#xff0c;我们需要把共同的部分固定…

神经网络之反向传播算法(加入Nesterov动量的误差反向传播算法)

文章目录1、Nesterov动量2、结合Nesterov动量的误差反向传播算法3、算法实现3.1 训练过程3.2 测试过程4、参考源码及数据集1、Nesterov动量 在动量法提出不久之后&#xff0c;Nesterov动量也随之被提了出来&#xff0c;此方法属于动量法的进一步发展&#xff0c;与动量法不同的…

基于windows11配置深度学习环境包含WSL2配置ubuntu20.04

基于windows11配置深度学习环境包含WSL2配置ubuntu20.04目录平台WSL2 系统准备(Windows Subsystem of Linux)安装WSL2基于WSL2中的Ubuntu安装CUDA和cudnnCUDA 安装目录 平台 系统 : windows11 专业版 CPU Intel I7 8750hq 显卡&#xff1a;Nvidia GTX1060移动端 显卡驱动版本…

安装Windows11提示这台电脑不符合安装此版本的Windows所需的最低系统要求

现在很多用户都会选择用U盘来安装系统&#xff0c;最新有用户在使用U盘安装Win11系统的时候&#xff0c;结果安装到第一步就提示这台电脑无法运行Windows11&#xff0c;这台电脑不符合安装此版本的Windows所需的最低系统要求。下面小编就来教大家解决此问题的方法。 问题解析&a…

BIO/NIO/AIO/IO多路复用简介

bio、nio、aio、io多路复用、reactor模式io&#xff0c;在将IO的时候&#xff0c;是不是都遇到过这些概念&#xff0c;也有种傻傻分不清&#xff1f;甚至别人在大谈特谈的时候&#xff0c;一会nio&#xff0c;一会io多路复用&#xff0c;一会又搞到reactor模式上去了&#xff1…

一文搞懂原型和原型链

在了解原型和原型链之前首先得明确它俩是什么东西&#xff1a; 原型&#xff1a;prototype 又称显示原型 1、原型是一个普通对象 2、只有构造函数才具备该属性 3、公有属性可操作 隐式原型&#xff1a;__proto__ 1、只有对象(普通对象、函数对象&#xff09;具备 2、私有的对…

《Spring系列》第2章 解析XML获取Bean

一、基础代码 Spring加载bean实例的代码 public static void main(String[] args) throws IOException {// 1.获取资源Resource resource new ClassPathResource("bean.xml");// 2.获取BeanFactoryDefaultListableBeanFactory factory new DefaultListableBeanFa…

Airtest自动化测试工具实战演练

一开始知道Airtest大概是在年初的时候&#xff0c;当时&#xff0c;看了一下官方的文档&#xff0c;大概是类似Sikuli的一个工具&#xff0c;主要用来做游戏自动化的&#xff0c;通过截图的方式用来解决游戏自动化测试的难题。最近&#xff0c;移动端测试的同事尝试用它的poco库…

iwebsec靶场-命令执行漏洞

漏洞简介 命令执行漏洞&#xff08;Command Injection&#xff09;是一种常见的安全漏洞&#xff0c;也被称为代码注入漏洞。它允许攻击者将恶意代码注入到受攻击的应用程序中&#xff0c;从而可以在应用程序的上下文中执行任意命令。 命令执行漏洞通常出现在Web应用程序中&…