labulaka521

labulaka521 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

labulaka521 关注了用户 · 6月3日

无闻 @unknwon

追逐自由的学术境界,为了技术的精湛而坚持不懈。

关注 14

labulaka521 赞了回答 · 4月28日

解决TCP 四元组和五元组的问题

如果确定是tcp的话 就是 四元组可以唯一确定一个连接;

如果不能确定是tcp,而是传输层的话,就是五元组,外加一个协议。

关注 3 回答 2

labulaka521 关注了用户 · 4月17日

iyacontrol @daimaxianyumeng

专注kubernetes,devops,aiops,service mesh。

关注 391

labulaka521 赞了文章 · 4月3日

2. Go 性能调优之 —— 编译优化

原文链接:https://github.com/sxs2473/go...
本文使用 Creative Commons Attribution-ShareAlike 4.0 International 协议进行授权许可。

编译优化

本节介绍Go编译器执行的三个重要优化。

  • 逃逸分析
  • 内联
  • 死码消除

Go 编译器的历史

Go 编译器在2007年左右开始作为 Plan9 编译器工具链的一个分支。当时的编译器与 Aho 和 Ullman 的 Dragon Book 非常相似。

2015年,当时的 Go 1.5 编译器 从 C 机械地翻译成 Go

一年后,Go 1.7 引入了一个基于 SSA 技术的 新编译器后端 ,取代了之前的 Plan 9风格的代码。这个新的后端为泛型和体系结构特定的优化提供了许多可能。

逃逸分析

我们要讨论的第一个优化是逃逸分析。

为了说明逃逸分析,首先让我们来回忆一下在 Go spec 中没有提到堆和栈,它只提到 Go 语言是有垃圾回收的,但也没有说明如何是如何实现的。

一个遵循 Go spec 的 Go 实现可以将每个分配操作都在堆上执行。这会给垃圾回收器带来很大压力,但这样做是绝对错误的 -- 多年来,gccgo对逃逸分析的支持非常有限,所以才导致这样做被认为是有效的。

然而,goroutine 的栈是作为存储局部变量的廉价场所而存在;没有必要在栈上执行垃圾回收。因此,在栈上分配内存也是更加安全和有效的。

在一些语言中,如CC++,在栈还是堆上分配内存由程序员手动决定——堆分配使用mallocfree,而栈分配通过alloca。错误地使用这种机制会是导致内存错误的常见原因。

在 Go 中,如果一个值超过了函数调用的生命周期,编译器会自动将之移动到堆中。我们管这种现象叫:该值逃逸到了堆。

type Foo struct {
    a, b, c, d int
}

func NewFoo() *Foo {
    return &Foo{a: 3, b: 1, c: 4, d: 7}
}

在这个例子中,NewFoo 函数中分配的 Foo 将被移动到堆中,因此在 NewFoo 返回后 Foo 仍然有效。

这是从早期的 Go 就开始有的。与其说它是一种优化,不如说它是一种自动正确性特性。无法在 Go 中返回栈上分配的变量的地址。

同时编译器也可以做相反的事情;它可以找到堆上要分配的东西,并将它们移动到栈上。

逃逸分析 - 例1

让我们来看下面的例子:

// Sum 函数返回 0-100 的整数之和
func Sum() int {
        const count = 100
        numbers := make([]int, count)
        for i := range numbers {
                numbers[i] = i + 1
        }

        var sum int
        for _, i := range numbers {
                sum += i
        }
        return sum
}

Sum 将 0-100 的 ints型数字相加并返回结果。

因为 numbers 切片仅在 Sum函数内部使用,编译器将在栈上存储这100个整数而不是堆。也没有必要对 numbers进行垃圾回收,因为它会在 Sum 返回时自动释放。

调查逃逸分析

证明它!

要打印编译器关于逃逸分析的决策,请使用-m标志。

% go build -gcflags=-m examples/esc/sum.go
# command-line-arguments
examples/esc/sum.go:8:17: Sum make([]int, count) does not escape
examples/esc/sum.go:22:13: answer escapes to heap
examples/esc/sum.go:22:13: main ... argument does not escape

第8行显示编译器已正确推断 make([]int, 100)的结果不会逃逸到堆。

第22行显示answer逃逸到堆的原因是fmt.Println是一个可变函数。 可变参数函数的参数被装入一个切片,在本例中为[]interface{},所以会将answer赋值为接口值,因为它是通过调用fmt.Println引用的。 从 Go 1.6(可能是)开始,垃圾收集器需要通过接口传递的所有值都是指针,编译器看到的是这样的:

var answer = Sum()
fmt.Println([]interface{&answer}...)

我们可以使用标识 -gcflags="-m -m" 来确定这一点。会返回:

examples/esc/sum.go:22:13: answer escapes to heap
examples/esc/sum.go:22:13:      from ... argument (arg to ...) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13:      from *(... argument) (indirection) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13:      from ... argument (passed to call[argument content escapes]) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13: main ... argument does not escape

总之,不要担心第22行,这对我们的讨论并不重要。

逃逸分析 - 例2

这个例子是我们模拟的。 它不是真正的代码,只是一个例子。

package main

import "fmt"

type Point struct{ X, Y int }

const Width = 640
const Height = 480

func Center(p *Point) {
        p.X = Width / 2
        p.Y = Height / 2
}

func NewPoint() {
        p := new(Point)
        Center(p)
        fmt.Println(p.X, p.Y)
}

NewPoint 创建了一个 *Point 指针值 p。 我们将p传递给Center函数,该函数将点移动到屏幕中心的位置。最后我们打印出 p.Xp.Y 的值。

% go build -gcflags=-m examples/esc/center.go
# command-line-arguments
examples/esc/center.go:10:6: can inline Center
examples/esc/center.go:17:8: inlining call to Center
examples/esc/center.go:10:13: Center p does not escape
examples/esc/center.go:18:15: p.X escapes to heap
examples/esc/center.go:18:20: p.Y escapes to heap
examples/esc/center.go:16:10: NewPoint new(Point) does not escape
examples/esc/center.go:18:13: NewPoint ... argument does not escape
# command-line-arguments

尽管p是使用new分配的,但它不会存储在堆上,因为Center被内联了,所以没有p的引用会逃逸到Center函数。

内联

在 Go 中,函数调用有固定的开销;栈和抢占检查。

硬件分支预测器改善了其中的一些功能,但就功能大小和时钟周期而言,这仍然是一个成本。

内联是避免这些成本的经典优化方法。

内联只对叶子函数有效,叶子函数是不调用其他函数的。这样做的理由是:

  • 如果你的函数做了很多工作,那么前序开销可以忽略不计。
  • 另一方面,小函数为相对较少的有用工作付出固定的开销。这些是内联目标的功能,因为它们最受益。

还有一个原因就是严重的内联会使得堆栈信息更加难以跟踪。

内联 - 例1

func Max(a, b int) int {
        if a > b {
                return a
        }
        return b
}

func F() {
        const a, b = 100, 20
        if Max(a, b) == b {
                panic(b)
        }
}

我们再次使用 -gcflags = -m 标识来查看编译器优化决策。

% go build -gcflags=-m examples/max/max.go
# command-line-arguments
examples/max/max.go:3:6: can inline Max
examples/max/max.go:12:8: inlining call to Max

编译器打印了两行信息:

  • 首先第3行,Max的声明告诉我们它可以内联
  • 其次告诉我们,Max的主体已经内联到第12行调用者中。

内联是什么样的?

编译 max.go 然后我们看看优化版本的 F() 变成什么样了。

% go build -gcflags=-S examples/max/max.go 2>&1 | grep -A5 '"".F STEXT'
"".F STEXT nosplit size=1 args=0x0 locals=0x0
        0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10)       TEXT    "".F(SB), NOSPLIT, $0-0
        0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10)       FUNCDATA        $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (/Users/dfc/devel/gophercon2018-performance-tuning-workshop/4-compiler-optimisations/examples/max/max.go:10)       FUNCDATA        $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
        0x0000 00000 (<unknown line number>)    RET
        0x0000 c3

一旦Max被内联到这里,这就是F的主体 - 这个函数什么都没干。我知道屏幕上有很多没用的文字,但是相信我的话,唯一发生的就是RET。实际上F变成了:

func F() {
        return
}

注意 : 利用 -S 的输出并不是进入二进制文件的最终机器码。链接器在最后的链接阶段进行一些处理。像FUNCDATAPCDATA这样的行是垃圾收集器的元数据,它们在链接时移动到其他位置。 如果你正在读取-S的输出,请忽略FUNCDATAPCDATA行;它们不是最终二进制的一部分。

调整内联级别

使用-gcflags=-l标识调整内联级别。有些令人困惑的是,传递一个-l将禁用内联,两个或两个以上将在更激进的设置中启用内联。

  • -gcflags=-l,禁用内联。
  • 什么都不做,常规的内联
  • -gcflags='-l -l' 内联级别2,更积极,可能更快,可能会制作更大的二进制文件。
  • -gcflags='-l -l -l' 内联级别3,再次更加激进,二进制文件肯定更大,也许更快,但也许会有 bug。
  • -gcflags=-l=4 (4个 -l) 在 Go 1.11 中将支持实验性的 中间栈内联优化

死码消除

为什么ab是常数很重要?

为了理解发生了什么,让我们看一下编译器在把Max内联到F中的时候看到了什么。我们不能轻易地从编译器中获得这个,但是直接手动完成它。

Before:

func Max(a, b int) int {
        if a > b {
                return a
        }
        return b
}

func F() {
        const a, b = 100, 20
        if Max(a, b) == b {
                panic(b)
        }
}

After:

func F() {
        const a, b = 100, 20
        var result int
        if a > b {
                result = a
        } else {
                result = b
        }
        if result == b {
                panic(b) 
        }
}

因为ab是常量,所以编译器可以在编译时证明分支永远不会是假的;100总是大于20。因此它可以进一步优化 F

func F() {
        const a, b = 100, 20
        var result int
        if true {
                result = a
        } else {
                result = b
        }
        if result == b {
                panic(b) 
        }
}

既然分支的结果已经知道了,那么结果的内容也就知道了。这叫做分支消除。

func F() {
        const a, b = 100, 20
        const result = a
        if result == b {
                panic(b) 
        }
}

现在分支被消除了,我们知道结果总是等于a,并且因为a是常数,我们知道结果是常数。 编译器将此证明应用于第二个分支

func F() {
        const a, b = 100, 20
        const result = a
        if false {
                panic(b) 
        }
}

并且再次使用分支消除,F的最终形式减少成这样。

func F() {
        const a, b = 100, 20
        const result = a
}

最后就变成

func F() {
}

死码消除(续)

分支消除是一种被称为死码消除的优化。实际上,使用静态证明来表明一段代码永远不可达,通常称为死代码,因此它不需要在最终的二进制文件中编译、优化或发出。

我们发现死码消除与内联一起工作,以减少循环和分支产生的代码数量,这些循环和分支被证明是不可到达的。

你可以利用这一点来实现昂贵的调试,并将其隐藏起来

const debug = false 

结合构建标记,这可能非常有用。

进一步阅读

编译器标识练习

编译器标识提供如下:

go build -gcflags=$FLAGS

研究以下编译器功能的操作:

  • -S 打印正在编译的包的汇编代码
  • -l 控制内联行为; -l 禁止内联, -l -l 增加-l(更多-l会增加编译器对代码内联的强度)。试验编译时间,程序大小和运行时间的差异。
  • -m 控制优化决策的打印,如内联,逃逸分析。-m打印关于编译器的想法的更多细节。
  • -l -N 禁用所有优化。

注意 : If you find that subsequent runs of go build ... produce no output, delete the ./max binary in your working directory.

查看原文

赞 28 收藏 18 评论 2

labulaka521 赞了文章 · 3月30日

疫情期间的Go分享

前序

Hello,大家好~ 由于新冠状病毒的原因、现场面试全部改为视频面试。下面来分享下疫情期间的一些面试经验和反思哦。

中序

    switch "company" {
    case "某未来":
        process:=` 1-2面 -120分钟 
1. mysql .索引相关.select * from table  where id=1 或  where a=1  数据是怎么查出来的。(id 主键 a是普通索引或者a 无索引)
2. update set a=1 from table where a=2 会加锁吗。怎么加的。会锁表吗 。
3. redolog undolog binlog 的作用。
4. mysql主从怎么实现的
5. go 调度模型。发生网络io,会怎么调度。发生阻塞的IO会怎么调度。epoll详解
6. go gc 相关问题
7. go channel 的实现原理。
8. 实现一个并发模型。生产者消费者 (在线写)
9. 既然你提到了gearman.有其他的队列可以替代吗?比如哪些?可以举例吗?
10. redis 的删除策略。定时 定期 惰性 lru(要写)
11. redis 各类型的底层数据结构讲讲~
12. etcd 怎么保持一致性的? 脑裂怎么解决的?
13. 你对分布式怎么看?可以举个例子吗?
14. 线上突然大量502.怎么排查?
15. 架构设计-微信朋友圈
16. 线上熔断降级怎么做的
17. 描述 APP push推送架构设计
18. 既然你说XX奖励系统模型 不合理。如果让你重新设计。你会怎么设计?
19. 讲一下open-falcon的架构设计吧。metrics 采集标准?
20. 为什么你们把php换成go ?优点是什么?为什么单机 go 的吞吐比php 高?原因是?
21. 举几个例子 说明下 你对代码做优化的地方?提升的原因是什么?
22. 你怎么评估重构业务。重构的标准是什么?怎么灰度切换?
24. 你对自己未来的规划是怎么样子的?
             3面 - 60分钟
1. C++了解吗?
2. 虚拟地址和位地址怎么映射?
3. C++段错误是什么情况引起的?
4. Go runtime 了解吗?讲一下调度。发生文件IO的时候 G 怎么调度的?
5. 有10亿用户,让你设计一个社区架构。包括点赞 发帖 删帖 的积分架构、期间一直在追问设计合不合理。不断的优化~
6. 给你1亿个URL 。爬取信息。会遇到什么问题?从 CPU 磁盘 网络 等方面。这个聊了很多case
7. 平常喜欢看什么书?想往管理层走 还是技术路线。
        `
        suggest:=`
        该公司在线教育龙头,团队氛围很棒。核心团队主要由360、滴滴、swoole系组成。
`
    case "某度":
        process:=` 1-3 面 总计3小时
1. 设计一个抢红包的系统架构、如何保证每个人抢到、讨论了半天、如何抗住流量?
2. mysql 隔离级别、mvvc  索引种类和区别、gap、innodb、
3. 给你一字符串、abcabc 找出最长出现偶数次的字符串
4. 业务场景上、mysql redis 怎么达到分布式一致性
5. 设计一个直播系统、包含送礼、长连接、推送、实效性、等 (在线画架构图)
6. 公司负责业务的职责

1. 手写前缀树
2. 设计一个缓存系统
3. leetcode AB数组升序 中间数
4. Go runtime讲解- 网上文章很多。不想听网上的文章。有没有独到的见解
5. 讲解open-falcon架构设计
6. 讲解jaeger 全链路原理、 traceid 的生成规则
7. etcd源码分析

      大概就记住这些、其他忘记了、
`
    case "某滴":
    process:=`1面 100分钟
1. 在线画推送系统架构-期间会不断地问问题。
2. redis 为什么是单线程? 一直聊得很深。讲到epoll 底层 和两种模式。 红黑树
3. HTTP FTP区别、迅雷传输文件的协议格式,为什么用udp?
4. ping ip 这一过程发生了什么?
5. 如果让你负责一个项目。你怎么保证项目高效稳定零BUG?
6. 打开一个socket 发生了什么?怎么写一个socket服务器?
7. http 状态码 你知道哪些?
8. 长连接写过吗? 你们全都用的rpc 请求吗?讲一下grpc 和ws
9. 在线写算法、获取一个二叉树的最小深度。
10. 分布式事务?
2面 没记
3面 60分钟
1.给你一个10PB文件  3000台机器。如何做字典树排序?
2.一直在聊各个公司所做的项目架构

`
case "某巴巴":
    process:=`
1. int int64的区别、 占多少位、慢慢分析。 
2. go 逃逸分析   
3. go gc  三色标记-黑白灰
4. 反射了解吗 —主要做序列化、反序列化、GORM、yaml/ini 等文件解析 —设备信息存储的是json格式的数据。接收按照interface来
reflect.TypeOf(app).String()  来处理的。传输不一致  
reflect.TypeOf(regId).String() == "map[string]interface {}” 
1. im 了解吗? 核心是什么?我是谁 我在那?? 
2. go map 底层源码了解吗?
3. redis list set 区别。什么场景使用
4. 写代码~ 用 map 设计一个 set集合。支持增删改查
5. []byte{}  string 的区别
6. rocketmq 底层原理
`

}

后序

建议大家在业余时间,要及时的积累知识点,巩固底层源码和自己做的项目架构哦~。这样无论是在任何时刻,都能保持良好的求职状态。居安思危ing~

祝大家都能找到心满意足的工作

附件模板

我平时知识点巩固的模板分享给大家~
左侧是线索cues 记录大的模块概念,右侧是我们自己的一些小心地和笔记
最下面是对 整体知识板块的总结~
image.png

查看原文

赞 14 收藏 6 评论 3

labulaka521 关注了标签 · 3月25日

gorm

go语言写的orm库,支持MySQL,MSSQL,PostgreSQL

关注 17

labulaka521 关注了用户 · 3月18日

public0821 @public0821

关注 572

labulaka521 关注了专栏 · 1月10日

Linux程序员

专注Linux相关技术

关注 604

labulaka521 赞了文章 · 2019-12-26

Linux网络 - 数据包的接收过程

本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的。

如果英文没有问题,强烈建议阅读后面参考里的两篇文章,里面介绍的更详细。

本文只讨论以太网的物理网卡,不涉及虚拟设备,并且以一个UDP包的接收过程作为示例.

本示例里列出的函数调用关系来自于kernel 3.13.0,如果你的内核不是这个版本,函数名称和相关路径可能不一样,但背后的原理应该是一样的(或者有细微差别)

网卡到内存

网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。

下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理:

                   +-----+
                   |     |                            Memroy
+--------+   1     |     |  2  DMA     +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+         |     |             +--------+--------+--------+--------+
                   |     |<--------+
                   +-----+         |
                      |            +---------------+
                      |                            |
                    3 | Raise IRQ                  | Disable IRQ
                      |                          5 |
                      |                            |
                      ↓                            |
                   +-----+                   +------------+
                   |     |  Run IRQ handler  |            |
                   | CPU |------------------>| NIC Driver |
                   |     |       4           |            |
                   +-----+                   +------------+
                                                   |
                                                6  | Raise soft IRQ
                                                   |
                                                   ↓
  • 1: 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。
  • 2: 网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注: 老的网卡可能不支持DMA,不过新的网卡一般都支持。
  • 3: 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了
  • 4: CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数
  • 5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
  • 6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。

内核的网络模块

软中断会触发内核网络模块中的软中断处理函数,后续流程如下

                                                     +-----+
                                             17      |     |
                                        +----------->| NIC |
                                        |            |     |
                                        |Enable IRQ  +-----+
                                        |
                                        |
                                  +------------+                                      Memroy
                                  |            |        Read           +--------+--------+--------+--------+
                 +--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... |
                 |                |            |          9            +--------+--------+--------+--------+
                 |                +------------+
                 |                      |    |        skb
            Poll | 8      Raise softIRQ | 6  +-----------------+
                 |                      |             10       |
                 |                      ↓                      ↓
         +---------------+  Call  +-----------+        +------------------+        +--------------------+  12  +---------------------+
         | net_rx_action |<-------| ksoftirqd |        | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |
         +---------------+   7    +-----------+        +------------------+   11   +--------------------+      +---------------------+
                                                               |                                                      | 13
                                                            14 |        + - - - - - - - - - - - - - - - - - - - - - - +
                                                               ↓        ↓
                                                    +--------------------------+    15      +------------------------+
                                                    | __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |
                                                    +--------------------------+            +------------------------+
                                                               |
                                                               | 16
                                                               ↓
                                                      +-----------------+
                                                      | protocol layers |
                                                      +-----------------+
  • 7: 内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数
  • 8: net_rx_action调用网卡驱动里的poll函数来一个一个的处理数据包
  • 9: 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
  • 10: 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数
  • 11: napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用enqueue_to_backlog
  • 12: 在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置
  • 13: CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core)
  • 14: 如果没开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core
  • 15: 看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。
  • 16: 调用协议栈相应的函数,将数据包交给协议栈处理。
  • 17: 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU
enqueue_to_backlog函数也会被netif_rx函数调用,而netif_rx正是lo设备发送数据包时调用的函数

协议栈

IP层

由于是UDP包,所以第一步会进入IP层,然后一级一级的函数往下调:

          |
          |
          ↓         promiscuous mode &&
      +--------+    PACKET_OTHERHOST (set by driver)   +-----------------+
      | ip_rcv |-------------------------------------->| drop this packet|
      +--------+                                       +-----------------+
          |
          |
          ↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
          |
          |
          ↓
      +---------+
      |         | enabled ip forword  +------------+        +----------------+
      | routing |-------------------->| ip_forward |------->| NF_INET_FORWARD |
      |         |                     +------------+        +----------------+
      +---------+                                                   |
          |                                                         |
          | destination IP is local                                 ↓
          ↓                                                 +---------------+
 +------------------+                                       | dst_output_sk |
 | ip_local_deliver |                                       +---------------+
 +------------------+
          |
          |
          ↓
 +------------------+
 | NF_INET_LOCAL_IN |
 +------------------+
          |
          |
          ↓
    +-----------+
    | UDP layer |
    +-----------+
  • ip_rcv: ip_rcv函数是IP模块的入口函数,在该函数里面,第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
  • NF_INET_PRE_ROUTING: netfilter放在协议栈中的钩子,可以通过iptables来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走
  • routing: 进行路由,如果是目的IP不是本地IP,且没有开启ip forward功能,那么数据包将被丢弃,如果开启了ip forward功能,那将进入ip_forward函数
  • ip_forward: ip_forward会先调用netfilter注册的NF_INET_FORWARD相关函数,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk函数
  • dst_output_sk: 该函数会调用IP层的相应函数将该数据包发送出去,同下一篇要介绍的数据包发送流程的后半部分一样。
  • ip_local_deliver:如果上面routing的时候发现目的IP是本地IP,那么将会调用该函数,在该函数中,会先调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到UDP层

UDP层

          |
          |
          ↓
      +---------+            +-----------------------+
      | udp_rcv |----------->| __udp4_lib_lookup_skb |
      +---------+            +-----------------------+
          |
          |
          ↓
 +--------------------+      +-----------+
 | sock_queue_rcv_skb |----->| sk_filter |
 +--------------------+      +-----------+
          |
          |
          ↓
 +------------------+
 | __skb_queue_tail |
 +------------------+
          |
          |
          ↓
  +---------------+
  | sk_data_ready |
  +---------------+
  • udp_rcv: udp_rcv函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续
  • sock_queue_rcv_skb: 主要干了两件事,一是检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)
  • __skb_queue_tail: 将数据包放入socket接收队列的末尾
  • sk_data_ready: 通知socket数据包已经准备好
调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中。

socket

应用层一般有两种方式接收数据,一种是recvfrom函数阻塞在那里等着数据来,这种情况下当socket收到通知后,recvfrom就会被唤醒,然后读取接收队列的数据;另一种是通过epoll或者select监听相应的socket,当收到通知后,再调用recvfrom函数去读取接收队列的数据。两种情况都能正常的接收到相应的数据包。

结束语

了解数据包的接收流程有助于帮助我们搞清楚我们可以在哪些地方监控和修改数据包,哪些情况下数据包可能被丢弃,为我们处理网络问题提供了一些参考,同时了解netfilter中相应钩子的位置,对于了解iptables的用法有一定的帮助,同时也会帮助我们后续更好的理解Linux下的网络虚拟设备。

在接下来的几篇文章中,将会介绍Linux下的网络虚拟设备和iptables。

参考

Monitoring and Tuning the Linux Networking Stack: Receiving Data
Illustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
NAPI

查看原文

赞 124 收藏 150 评论 18

labulaka521 赞了文章 · 2019-10-16

分布式服务框架gRPC

什么是gRPC

gRPC是Google开发的高性能、通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计,基于Protobuf(Protocol Buffers)序列化协议开发,且支持众多开发语言。在gRPC中一个客户端可以像使用本地对象那样直接调用位于不同机器上的服务端应用的方法(methods)。这让你能够更容易的构建分布式的应用和服务。和其他RPC系统类似,gRPC也是基于定义一个服务,指定服务可以被远程调用的方法以及他们的参数和返回类型。在服务端,实现服务的接口然后运行一个gRPC服务来处理可出端的请求。在客户端,客户端拥有一个存根(stub在某些语言中仅称为客户端),提供与服务器相同的方法。

![grpc](/Users/qsc/Desktop/Writing/grpc.png)

·gRPC客户端和服务器可以在各种环境中运行并相互通信,并且可以使用gRPC支持的任何语言编写。因此,例如,您可以使用Go,Python或Ruby的客户端轻松地用Java创建gRPC服务器。此外,最新的Google API的接口将拥有gRPC版本,可让您轻松地在应用程序中内置Google功能。

使用protocol buffer

默认情况下,gRPC使用protocol buffer,用于序列化结构化数据(尽管它可以与其他数据格式(例如JSON)一起使用)。使用协议缓冲区的第一步是在proto文件中为要序列化的数据定义结构:proto文件扩展名为.proto的普通文本文件。protocol buffer数据被构造为消息,其中每个消息都是信息的逻辑记录,其中包含一系列称为字段的名称/值对。这是一个简单的示例:

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
}

定义了数据结构后,就可以使用protocol buffer编译器protoc生成你所选语言的数据访问类。访问类为每个字段提供了简单的访问器(例如name())和set_name()),以及将整个结构序列化为原始字节或从原始字节中解析出整个结构的方法-例如,如果您选择的语言是C ++,则在上面的示例将生成一个名为Person的类。然后,您可以在应用程序中使用此类来填充,序列化和检索Person的protocol buffer消息。

除此之外你还要在.proto件中定义gRPC服务,并将RPC方法参数和返回类型指定为protocol buffer消息:

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

gRPC使用也是使用编译器protoc从proto文件生成代码,不过编译器要首先安装一个gRPC插件。使用gRPC插件,你可以获得生成的gRPC客户端和服务器代码,以及用于填充,序列化和检索消息类型的常规protocol buffer访问类代码。

下面会更详细地介绍gRPC里的一些关键的概念。

服务定义

与许多RPC系统一样,gRPC围绕定义服务的思想,指定可通过其参数和返回类型远程调用的方法。默认情况下,gRPC使用protocol buffer作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构。如果需要,可以使用其他替代方法。

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

gRPC允许定义四种服务方法:

  • 一元RPC,客户端向服务器发送单个请求并获得单个响应,就像普通函数调用一样。
rpc SayHello(HelloRequest) returns (HelloResponse){
}
  • 服务器流式RPC,客户端向服务器发送请求,并获取流以读取回一系列消息。客户端从返回的流中读取,直到没有更多消息为止。 gRPC保证单个RPC调用中的消息顺序。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){
}
  • 客户端流式RPC,客户端使用提供的流写入消息序列然后将它们发送到服务器。客户端写完消息后,它将等待服务器读取消息并返回响应。 gRPC保证了在单个RPC调用中的消息顺序。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {
}
  • 双向流式RPC,双方都使用读写流发送一系列消息。这两个流是独立运行的,因此客户端和服务器可以按照自己喜欢的顺序进行读写:例如,服务器可以在写响应之前等待接收完所有客户端消息,或者可以先读取一条消息再写入一条消息,或其他一些读写组合。每个流中的消息顺序都会保留。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){
}

在下面的RPC生命周期章节我们会更详细的比较这几种不同的RPC。

使用API界面

.proto文件中的服务定义开始,gRPC提供了protocol buffer编译器插件,插件可生成客户端和服务器端代码。 gRPC用户通常在客户端调用这些API,并在服务器端实现相应的API。

  • 在服务侧,服务器实现服务中声明的方法并运行一个gRPC服务器来处理客户端的调用。gRPC的基础设施解码传入的请求,执行服务的方法,编码服务的响应。
  • 在客户端,客户端拥有一个名为stub(存根)的本地对象(在有些语言中更倾向于把stub叫做客户端)该对象同样实现了服务中方法。客户端可以只在本地对象上调用这些方法,将调用参数包装在适当的protocol buffer消息类型中,gRPC会负责将请求发送给服务器并且返回服务端的protocol buffer响应。

同步vs异步

同步RPC调用会阻塞当前线程直到服务器收到响应为止,这是最接近RPC所追求的过程调用抽象的近似方法。另一方面,网络本质上是异步的,并且在许多情况下能够启动RPC而不阻塞当前线程很有用。

大多数语言中的gRPC编程界面都有同步和异步两种形式。可以在每种语言的教程和参考文档中找到更多信息。

RPC生命周期

现在让我们具体看一下当一个gRPC客户端调用了一个gRPC服务器的方法后都发生了什么。我们不会查看具体实现细节,留到后面的编程语言教程中再看实现细节。

一元RPC

首先来看一个最简单的RPC类型,客户端发送一个请求然后接受一个响应。

  • 一旦客户端调用了存根/客户端对象上的方法,服务器会被通知RPC已经被调用了,同样会接收到调用时客户端的元数据、调用的方法名称以及制定的截止时间(如果适用的话)。
  • 然后,服务器可以立即发送自己的初始元数据(必须在发送任何响应之前发送),也可以等待客户端的请求消息-哪个先发生应用程序指定的。
  • 服务器收到客户的请求消息后,它将完成创建和填充其响应所需的必要工作。然后将响应(如果成功)连同状态详细信息(状态代码和可选状态消息)以及可选尾随元数据一起返回。
  • 如果状态是OK,客户端将获得响应,从而在客户端完成并终结整个调用过程。

服务器流式RPC

一个服务器流式RPC与简单的一元RPC类似,不同的是服务器在接收到客户端的请求消息后会发回一个响应流。在发送回所有的响应后,服务器的状态详情(状态码和可选的状态信息)和可选的尾随元数据会被发回以完成服务端的工作。客户端在接收到所有的服务器响应后即完成操作。

客户端流式RPC

客户端流式RPC也类似于一元PRC,不同之处在于客户端向服务器发送请求流而不是单个请求。服务器通常在收到客户端的所有请求后(但不一定)发送单个响应,以及其状态详细信息和可选的尾随元数据。

双向流式RPC

在双向流式RPC中,调用再次由客户端调用方法发起,服务器接收客户端元数据,方法名称和期限。同样,服务器可以选择发回其初始元数据,或等待客户端开始发送请求。

接下来发生的情况取决于应用程序,因为客户端和服务器可以按任何顺序进行读取和写入-流操作完全是独立地运行。因此,例如,服务器可以等到收到所有客户端的消息后再写响应,或者服务器和客户端可以玩“乒乓”:服务器收到请求,然后发回响应,然后客户端发送基于响应的另一个请求,依此类推。

截止时间/超时时间

gRPC允许客户端指定在RPC被DEADLINE_EXCEEDED错误终结前愿意等待多长时间来让RPC完成工作。在服务器端,服务器可以查看一个特定的RPC是否超时或者还有多长时间剩余来完成RPC。

如何指定期限或超时的方式因语言而异-例如,并非所有语言都有默认期限,某些语言API按照期限(固定的时间点)工作,而某些语言API根据超时来工作(持续时间)。

RPC终止

在gRPC中,客户端和服务端对调用是否成功做出独立的基于本地的决定,而且两端的结论有可能不匹配。这意味着,比如说,你可能会有一个在服务端成功完成(“我已经发送完所有响应了”)但是在客户端失败(“响应是在我指定的deadline之后到达的”)的RPC。服务器也有可能在客户端发送所有请求之前决定RPC完成了。

取消RPC

客户端或服务器都可以随时取消RPC。取消操作将立即终止RPC,因此不再进行任何工作。这不是“撤消”:取消之前所做的更改不会回滚。

元数据

元数据是以键值对列表形式提供的关于特定RPC调用的信息(比如说身份验证详情),其中键是字符串,值通常来说是字符串(但是也可以是二进制数据)。元数据对gRPC本身是不透明的-它允许客户端向服务器提供与调用相关的信息,反之亦然。

对元数据的访问取决于语言。

通道

一个gRPC通道提供了一个到指定主机和端口号的gRPC服务器的连接,它在创建客户端存根(或者对某些语言来说就是“客户端”)时被使用。客户端可以指定通道参数来更改gRPC的默认行为,比如说打开/关闭消息压缩。每个通道都有状态,状态包括connectedidle(闲置)

gRPC怎么处理关掉的通道是语言相关的,有些语言还允许查询通道的状态。


WX20191117-152623@2x.png

查看原文

赞 13 收藏 9 评论 1

认证与成就

  • 获得 0 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-07-05
个人主页被 52 人浏览