0%

在网上看见很多问node.js如何获取客户端IP,所以记录下来,以供大家参考。

1
2
3
4
5
6
function getClientIp(req) {
return req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
req.connection.socket.remoteAddress;
};

代码,第一段判断是否有反向代理IP(头信息:x-forwarded-for),再判断connection的远程IP,以及后端的socket的IP。

nodejs获取客户端IP Address

背景

线上一个go语言写的服务,在连续跑了10多天后,占用内存从最开始的20M左右,涨到了1个多G,有内存泄漏的问题,将整个排查过程和相应方法进行整理,做个记录。

pprof

Go语言自带的pprof工具,是检测Golang开发应用性能的利器。

pprof 采样数据主要有三种获取方式:

  1. runtime/pprof: 手动调用runtime.StartCPUProfile或者runtime.StopCPUProfile等 API来生成和写入采样文件,灵活性高。参考文档profiling-go-programs
  1. net/http/pprof: 通过 http 服务获取Profile采样文件,简单易用,适用于对应用程序的整体监控。通过runtime/pprof实现。这里推荐这种方法

  2. go test: 通过 go test -bench . -cpuprofile prof.cpu生成采样文件 适用对函数进行针对性测试

net/http/pprof

线上的服务是一直运行的,用过“net/http/pprof”可以看到实时的状态,看到各个信息指标,这里主要介绍这种方法。

1
2
3
4
5
6
7
8
import _ "net/http/pprof"
func main() {
......
go func() {
log.Println(http.ListenAndServe("localhost:8211", nil))
}()
......
}

之后可以通过 http://localhost:8211/debug/pprof/CMD获取对应的采样数据

pprof

Profile翻译为“剖面,侧面等”,可以理解为当前应用的的一个剖面。可以知道哪些地方耗费了cpu、memory。

// Each Profile has a unique name. A few profiles are predefined:

  1. goroutine    - stack traces of all current goroutines
    
  2. heap - a sampling of memory allocations of live objects
  3. allocs - a sampling of all past memory allocations
  4. threadcreate - stack traces that led to the creation of new OS threads
  5. block - stack traces that led to blocking on synchronization primitives
  6. mutex - stack traces of holders of contended mutexes
  • goroutine: 获取程序当前所有 goroutine 的堆栈信息。
  • heap: 包含每个 goroutine 分配大小,分配堆栈等。每分配 runtime.MemProfileRate(默认为512K) 个字节进行一次数据采样。
  • threadcreate: 获取导致创建 OS 线程的 goroutine 堆栈
  • block: 获取导致阻塞的 goroutine 堆栈(如 channel, mutex 等),使用前需要先调用 runtime.SetBlockProfileRate
  • mutex: 获取导致 mutex 争用的 goroutine 堆栈,使用前需要先调用 runtime.SetMutexProfileFraction

pprof_debug

The debug parameter enables additional output.

Passing debug=0 prints only the hexadecimal addresses that pprof needs.

Passing debug=1 adds comments translating addresses to function names and line numbers, so that a programmer can read the profile without tools.

The predefined profiles may assign meaning to other debug values;

for example, when printing the “goroutine” profile, debug=2 means to print the goroutine stacks in the same form that a Go program uses when dying due to an unrecovered panic.

以上几种 Profile 可在 http://localhost:8211/debug/pprof/ 中看到,除此之外,go pprof 的 CMD 还包括,

  • cmdline: 获取程序的命令行启动参数
  • profile: 获取指定时间内(从请求时开始)的cpuprof,倒计时结束后自动返回。参数: seconds, 默认值为30。cpuprofile 每秒钟采样100次,收集当前运行的 goroutine 堆栈信息。
  • symbol: 用于将地址列表转换为函数名列表,地址通过’+’分隔,如 URL/debug/pprof?0x18d067f+0x17933e7
  • trace: 对应用程序进行执行追踪,参数: seconds, 默认值1s

参数具体信息可以参考http_pprof

再回顾来看服务的问题,从图2中看到,goroutine的数量在一直增长;从图3看出,共起了414个goroutine, 其中395个是在调用FetchZkName。
然后再查看我们的代码,有哪里在调用go, 哪个goroutine会最终调用FetchZkName,就可以查到出问题的地方。

最终查到原因是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  // 10s去更新下zk节点
ticker2 := time.NewTicker(time.Second * 10)
go func() {
for {
select {
case <-ticker2.C:
UpdateInstanceFromZk()
}
}
}()

// 里面的函数调用关系如下:
UpdateInstanceFromZk --> nameservice.InitNameService
--> updateNames --> FetchZkName

// FetchZkName函数里起了一个goroutine,封装了watcher,自己已实现了更新zk节点的功能
go func() {
select {
case ev := <-ch:
uflog.INFO("cat watcher", ev)
if ev.Type == zk.EventNotWatching && ev.State == zk.StateDisconnected {
reConnect(connStr)
}
nc.FetchZkName(connStr, shortName, fullName)
}
}()

// 该goroutine不会退出,会一直watch。定时调用UpdateInstanceFromZk,会导致goroutine不断增加,内存泄漏。

解决方案是:UpdateInstanceFromZk()调用一次即可。

改进后,占用内存一直保持在20M左右,完美~

Go Tool PProf 分析工具

上面通过浏览器访问http服务的方式,需要有图形界面。而线上服务器通常是没有图形界面的,可以通过命令行方式来分析,或者将profile拷贝到本地,再生成调用关系图

Go Tool PProf 命令行

支持的cmd跟net/http/pprof里提到的一样

1
2
3
4
go tool pprof http://localhost:8211/debug/pprof/goroutine
go tool pprof http://localhost:8211/debug/pprof/profile
go tool pprof http://localhost:8211/debug/pprof/heap
...

以采集CPU耗时举例,

1
2
3
4
5
6
7
8
9
# /root/go/bin/go tool pprof http://localhost:8211/debug/pprof/profile
Fetching profile over HTTP from http://localhost:8211/debug/pprof/profile
Saved profile in /root/pprof/pprof.uhost_server.samples.cpu.003.pb.gz
File: uhost_server
Type: cpu
Time: Dec 11, 2018 at 6:12pm (CST)
Duration: 30s, Total samples = 10ms (0.033%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

会经历30s的采样时间,生成pprof.uhost_server.samples.cpu.003.pb.gz,这个可以拷贝到本地分析。

最重要的分析命令是top N

topN 命令可以查出程序最耗 CPU 的调用

1
2
3
4
5
6
7
8
9
(pprof) top 5
Showing nodes accounting for 70ms, 38.89% of 180ms total
Showing top 5 nodes out of 83
flat flat% sum% cum cum%
30ms 16.67% 16.67% 30ms 16.67% syscall.Syscall
10ms 5.56% 22.22% 20ms 11.11% fmt.Sprintf
10ms 5.56% 27.78% 30ms 16.67% os.(*File).write
10ms 5.56% 33.33% 10ms 5.56% runtime._ExternalCode
10ms 5.56% 38.89% 10ms 5.56% runtime.epollwait
  • flat / flat% : 函数占用CPU的运行时间和百分比
  • sum% : 按列累加的累计占用CPU百分比
  • cum / cum% : 采样过程中, the function appeared (either running or waiting for a called function to return).

top -cum

以累加的方式,看出消耗cpu的函数运行。可以和后面图形的方式做个对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(pprof) top10 -cum
Showing nodes accounting for 40ms, 22.22% of 180ms total
Showing top 10 nodes out of 83
flat flat% sum% cum cum%
0 0% 0% 140ms 77.78% runtime.goexit
0 0% 0% 40ms 22.22% uframework/log.RealWrite
0 0% 0% 40ms 22.22% uframework/task.(*TCPTask).Run.func1
0 0% 0% 40ms 22.22% uframework/task.TCPTaskFunc.ServeTCP
0 0% 0% 40ms 22.22% uhost-go/uhost-scheduler/logic.getSuitableResource
0 0% 0% 30ms 16.67% os.(*File).Write
10ms 5.56% 5.56% 30ms 16.67% os.(*File).write
30ms 16.67% 22.22% 30ms 16.67% syscall.Syscall
0 0% 22.22% 30ms 16.67% uframework/message/protobuf/proto.(*Buffer).dec_slice_struct
0 0% 22.22% 30ms 16.67% uframework/message/protobuf/proto.(*Buffer).dec_slice_struct_message

list Func

显示函数名以及每行代码的采样分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(pprof) list dec_slice_struct_message
Total: 180ms
ROUTINE ======================== uframework/message/protobuf/proto.(*Buffer).dec_slice_struct_message in /Users/patrick.xu/go/src/uframework/message/protobuf/proto/decode.go
0 30ms (flat, cum) 16.67% of Total
. . 822: return err
. . 823:}
. . 824:
. . 825:// Decode a slice of embedded messages.
. . 826:func (o *Buffer) dec_slice_struct_message(p *Properties, base structPointer) error {
. 30ms 827: return o.dec_slice_struct(p, false, base)
. . 828:}
. . 829:
. . 830:// Decode a slice of embedded groups.
. . 831:func (o *Buffer) dec_slice_struct_group(p *Properties, base structPointer) error {
. . 832: return o.dec_slice_struct(p, true, base)

web / web Func

显示调用图 / 显示某个具体函数的调用图

mac上需要先安装graphviz http://www.graphviz.org/

homebrew安装完毕后运行 brew install graphviz即可

1
2
3
4
➜  uhost-go git:(from_pxu) ✗ go tool pprof uhost_server pprof.uhost_server.samples.cpu.004.pb
Entering interactive mode (type "help" for commands)
(pprof) web
(pprof) web getSuitableResource

但是web命令生成的调用图往往是残缺的,不知道是什么原因。但找到下面更好的方式。

本地显示远程的调用图(推荐)

后来返现一种更易用的方式,服务在线上运行,本地浏览器直接打开调用图。
线上服务器ip为172.21.212.101,pprof监听端口为8991。

下面还是以cpu耗时为例

1
2
3
4
5
6
7
8
9
[root@gw ~]# go tool pprof http://172.21.212.101:8991/debug/pprof/profile
Fetching profile over HTTP from http://172.21.212.101:8991/debug/pprof/profile
Saved profile in /root/pprof/pprof.node_server.samples.cpu.005.pb.gz
File: node_server
Type: cpu
Time: Sep 16, 2021 at 4:36pm (CST)
Duration: 30.18s, Total samples = 5.15mins (1023.28%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

然后在线上服务器再起一个8992的端口

1
2
3
4
5
6
[root@gw ~]# go tool pprof -http="172.21.212.101:8992" http://172.21.212.101:8991/debug/pprof/profile
Fetching profile over HTTP from http://172.21.212.101:8991/debug/pprof/profile
Saved profile in /root/pprof/pprof.node_server.samples.cpu.006.pb.gz
Serving web UI on http://172.21.212.101:8992
http://172.21.212.101:8992

在本地浏览器可以直接打开web UI

还有火焰图,汇编代码等,功能更加强大。

pprof

As of Go 1.11, flamegraph visualizations are available in go tool pprof directly!

This will listen on :8081 and open a browser.
Change :8081 to a port of your choice.

$ go tool pprof -http=”:8081” [binary] [profile]

If you cannot use Go 1.11, you can get the latest pprof tool and use it instead:

Get the pprof tool directly

$ go get -u github.com/google/pprof

$ pprof -http=”:8081” [binary] [profile]

由于我的Mac上用的是go1.8.1版本,所用pprof命令。

1
2
3
4
5
6
7
8
9
➜  uhost-go git:(from_pxu) ✗ pprof uhost_server pprof.uhost_server.samples.cpu.004.pb
uhost_server: parsing profile: unrecognized profile format
Fetched 1 source profiles out of 2
File: uhost_server
Type: cpu
Time: Dec 11, 2018 at 6:18pm (CST)
Duration: 30s, Total samples = 180ms ( 0.6%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) web

其他工具

以后用到了再整理,这里mark下

参考链接

Profiling Go Programs

Source file src/runtime/pprof/pprof.go

go pprof性能分析

使用pprof和火焰图调试golang应用

中断是什么

中断使得硬件可以发松通知给处理器。例如敲击键盘时,键盘就会产生一个中断,通知操作系统有键被按下。

中断本质上是一种电信号,由硬件设备生成,送入中断控制器的输入引脚中。中断控制器(如8259A)是个简单的电子芯片,将多路中断管线,采用复用技术只通过一个和处理器连接的管线和处理器通信。当产生一个中断后,处理器会检测到一个电信号,中断自己的当前正在运行的程序,通知内核。内核调用一个称为中断处理程序(interrupt handler)或中断服务例程(interrupt service routine)的特定程序。中断处理程序或中断服务例程可以在中断向量表中找到,而这个中断向量表位于内存中的固定地址中。CPU处理中断后,就会恢复执行之前被中断的程序,整个流程如下图所示。

不同设备同时中断如何知道哪个中断是来自硬盘、哪个来自网卡呢?这个很容易,系统上的每个硬件设备都会被分配一个 IRQ 号,通过这个唯一的IRQ号就能区别不同硬件设备了。

中断控制器

常见的中断控制器有两种:可编程中断控制器 8259A 和高级可编程中断控制器(APIC, Advanced Programmable Interrupt Controller),中断控制器应该在大学的硬件接口和计算机体系结构的相关课程中都学过。传统的 8259A 只适合单 CPU 的情况,现在都是多CPU多核的SMP体系,所以为了充分利用SMP体系结构、把中断传递给系统上的每个CPU 以便更好实现并行和提高性能,Intel 引入了高级可编程中断控制器(APIC)。

网卡收发包过程

由于中断会频繁发生,因此要求中断处理程序执行要快速。为了实现快速执行,必须要将一些繁重且不非常紧急的任务从中断处理程序中剥离出来,这一部分Linux中称为下半部,有三种方法处理下半部——软中断、tasklet和工作队列。

内核如何从网卡接受数据,传统的经典过程:

  1. 数据到达网卡;
  2. 网卡产生一个中断给内核;
  3. 内核使用I/O指令,从网卡I/O区域中去读取数据;

但是,这一种方法,有一种重要的问题,就是大流量的数据来到,网卡会产生大量的中断,内核在中断上下文中,会浪费大量的资源来处理中断本身。所以,一个问题是,“可不可以不使用中断”,这就是轮询技术,所谓NAPI技术,说来也不神秘,就是说,内核屏蔽中断,然后隔一会儿就去问网卡,“你有没有数据啊?”

从这个描述本身可以看到,哪果数据量少,轮询同样占用大量的不必要的CPU资源,大家各有所长吧,呵呵……

OK,另一个问题,就是从网卡的I/O区域,包括I/O寄存器或I/O内存中去读取数据,这都要CPU去读,也要占用CPU资源,“CPU从I/O区域读,然后把它放到内存(这个内存指的是系统本身的物理内存,跟外设的内存不相干,也叫主内存)中”。于是自然地,就想到了DMA技术——让网卡直接从主内存之间读写它们的I/O数据,CPU,这儿不干你事,自己找乐子去:

  1. 首先,内核在主内存中为收发数据建立一个环形的缓冲队列(通常叫DMA环形缓冲区)。
  2. 内核将这个缓冲区通过DMA映射,把这个队列交给网卡;
  3. 网卡收到数据,就直接放进这个环形缓冲区了——也就是直接放进主内存了;然后,向系统产生一个中断;
  4. 内核收到这个中断,就取消DMA映射,这样,内核就直接从主内存中读取数据;

剩下的处理和操作数据包的工作就会交给软中断。高负载的网卡是软中断产生的大户,很容易形成瓶颈。

在相当长的时间内,网卡的中断都是通过CPU 0来处理的,造成CPU 0的压力很高、其他CPU相对空闲的情况。直到网卡多队列技术的出现,网卡多队列实际就是网卡的数据请求可以通过多个CPU处理。

多队列网卡的实现

多队列网卡硬件实现

常见的有Intel的82575、82576,Boardcom的57711等,下面以公司的服务器使用较多的Intel 82575网卡为例,分析一下多队列网卡的硬件的实现以及Linux内核软件的支持。

Intel 82575硬件逻辑图,有四个硬件队列。当收到报文时,通过hash包头的SIP、Sport、DIP、Dport四元组,将一条流总是收到相同的队列。同时触发与该队列绑定的中断。

RSS(Receive-Side Scaling, also known as multi-queue receive),是网卡的硬件特性,实现多队列,将不同的流分发到不同的CPU上,同一数据流始终在同一CPU上,避免TCP的顺序性和CPU的并行性发生冲突。

Linux kernel 2.6.21前网卡驱动的实现

Linux kernel从2.6.21之前不支持多队列特性,一个网卡只能申请一个中断号,因此同一个时刻只有一个核在处理网卡收到的包。如图2.1,协议栈通过NAPI轮询收取各个硬件queue中的报文到图2.2的net_device数据结构中,通过QDisc队列将报文发送到网卡。

Linux kernel 2.6.21后网卡驱动的实现

Linux kernel 2.6.21开始支持多队列特性,当网卡驱动加载时,通过获取的网卡型号,得到网卡的硬件queue的数量,并结合CPU核的数量,最终通过Sum=Min(网卡queue,CPU core)得出所要激活的网卡queue数量(Sum),并申请Sum个中断号,分配给激活的各个queue。

如图3.1,当某个queue收到报文时,触发相应的中断,收到中断的核,将该任务加入到协议栈负责收包的该核的NET_RX_SOFTIRQ队列中(NET_RX_SOFTIRQ在每个核上都有一个实例),在NET_RX_SOFTIRQ中,调用NAPI的收包接口,将报文收到CPU中如图3.2的有多个netdev_queue的net_device数据结构中。这样,CPU的各个核可以并发的收包,就不会因为一个核不能满足需求,导致网络IO性能下降。

但当CPU可以平行收包时,就会出现不同的核收取了同一个queue的报文,这就会产生报文乱序的问题,解决方法是将一个queue的中断绑定到唯一的一个核上去,从而避免了乱序的问题。

查看网卡是否支持多队列

查看网卡是否支持多队列,使用lspci -vvv命令,找到Ethernet controller项:

如果有MSI-X, Enable+ 并且Count > 1,则该网卡是多队列网卡。

Message Signaled Interrupts(MSI)是PCI规范的一个实现,可以突破CPU 256条interrupt的限制,使每个设备具有多个中断线变成可能,多队列网卡驱动给每个queue申请了MSI。MSI-X是MSI数组,实际应用场景中,MSI方式的中断对多核cpu的利用情况不佳,网卡中断全部落在某一个cpu上,即使设置cpu affinity也没有作用,而MSI-X中断方式可以自动在多个cpu上分担中断

然后可以查看是否打开了网卡多队列,使用命令cat /proc/interrupts,如果看到如下图信息表明多队列支持已经打开:

是不是某个CPU在一直忙着处理IRQ?

这个问题我们可以从 mpstat -P ALL 1 的输出中查明:里面的 %irq一列即说明了CPU忙于处理中断的时间占比

上面的例子中,第四个CPU有25.63%时间在忙于处理中断(这个数值还不算高,如果高达80%(而同时其它CPU这个数值很低)以上就说明有问题了),后面那个 intr/s 也说明了CPU每秒处理的中断数(从上面的数据也可以看出,其它几个CPU都不怎么处理中断)。

然后我们就要接着查另外一个问题:这个忙于处理中断的CPU都在处理哪个(些)中断?这要看/proc/interrupts文件

/proc/interrupts文件

这里记录的是自启动以来,每个CPU处理各类中断的数量(第一列是中断号,最后一列是对应的设备名)

对上面文件的输出,解释如下:

● 第一列表示IRQ号。

● 第二、三、四列表示相应的CPU核心被中断的次数。在上面的例子中,timer表示中断名称(为系统时钟)。1825291229表示CPU0被中断了1825291229次。i8042表示控制键盘和鼠标的键盘控制器。

● 对于像rtc(real time clock)这样的中断,CPU是不会被中断的。因为RTC存在于电子设备中,是用于追踪时间的。

● NMI和LOC是系统所使用的驱动,用户无法访问和配置。

IRQ号决定了需要被CPU处理的优先级,IRQ号越小意味着优先级越高。

例如,如果CPU同时接收了来自键盘和系统时钟的中断,那么CPU首先会服务于系统时钟,因为他的IRQ号是0。

● IRQ0 :系统时钟(不能改变)。

● IRQ1 :键盘控制器(不能改变)。

● IRQ3 :串口2的串口控制器(如有串口4,则其也使用这个中断)。

● IRQ4 :串口1的串口控制器(如有串口3,则其也使用这个中断)。

● IRQ5 :并口2和3或声卡。

● IRQ6 :软盘控制器。

● IRQ7 : 并口1,它被用于打印机或若是没有打印机,可以用于任何的并口。

而对于像操作杆(或称为游戏手柄)上的CPU,它并不会等待设备发送中断。因为操作杆主要用于游戏,操作杆的移动必须非常快,因此使用轮询的方式检测设备是否需要CPU的关注还是比较理想的。使用轮询方式的缺点是CPU就处于了忙等状态,因为CPU会不停的多次检查设备。但是需要注意的是在Linux中,这种处理信号的方式也是必不可少的。

最后确认每个队列是否绑定到不同的CPU核心上,cat /proc/interrupts查询到每个队列的中断号,对应的文件/proc/irq/${IRQ_NUM}/smp_affinity为中断号IRQ_NUM绑定的CPU核的情况。以十六进制表示,每一位代表一个CPU核:

(00000001)代表CPU0

(00000010)代表CPU1

(00000011)代表CPU0和CPU1

SMP是指”对称多处理器”,smp_affinity文件主要用于某个特定IRQ要绑定到哪个CPU核心上。在 /proc/irq/IRQ_NUMBER/目录下都有一个smp_affinity文件,例如,网卡的中断号是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
[root@192-168-152-52 ~]# cat /proc/irq/108/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
[root@192-168-152-52 ~]# cat /proc/irq/109/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000002
[root@192-168-152-52 ~]# cat /proc/irq/110/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000004
[root@192-168-152-52 ~]# cat /proc/irq/111/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000008
[root@192-168-152-52 ~]# cat /proc/irq/112/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000010
[root@192-168-152-52 ~]# cat /proc/irq/113/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000020
[root@192-168-152-52 ~]# cat /proc/irq/114/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000040
[root@192-168-152-52 ~]# cat /proc/irq/115/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000080
[root@192-168-152-52 ~]# cat /proc/irq/116/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000100
[root@192-168-152-52 ~]# cat /proc/irq/117/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000200
[root@192-168-152-52 ~]# cat /proc/irq/118/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
[root@192-168-152-52 ~]# cat /proc/irq/119/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000002
[root@192-168-152-52 ~]# cat /proc/irq/120/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000004
[root@192-168-152-52 ~]# cat /proc/irq/121/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000008
[root@192-168-152-52 ~]# cat /proc/irq/122/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000010
[root@192-168-152-52 ~]# cat /proc/irq/123/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000020
[root@192-168-152-52 ~]# cat /proc/irq/124/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000040
[root@192-168-152-52 ~]# cat /proc/irq/125/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000080
[root@192-168-152-52 ~]# cat /proc/irq/126/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000100
[root@192-168-152-52 ~]# cat /proc/irq/127/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000200
[root@192-168-152-52 ~]# cat /proc/irq/128/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
[root@192-168-152-52 ~]# cat /proc/irq/129/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000002
[root@192-168-152-52 ~]# cat /proc/irq/130/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000004
[root@192-168-152-52 ~]# cat /proc/irq/131/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,00000008
[root@192-168-152-52 ~]# cat /proc/irq/132/smp_affinity
0000,00000000,00000000,00000000,00000000,00000000,00000000,0003f03f

上面说明网卡队列绑定在CPU0~CPU0上。我们可以通过手动改变smp_affinity文件中的值来将IRQ绑定到指定的CPU核心上,或者启用irqbalance服务来自动绑定IRQ到CPU核心上。

IRQ Balance

Irqbalance是一个Linux的实用程序,它主要是用于分发中断请求到CPU核心上,有助于性能的提升。它的目的是寻求省电和性能优化之间的平衡。你可以使用yum进行安装(CentOS系统一般默认安装):

1
2
yum -y install irqbalance
/etc/init.d/irqbalance start

Irqbalance对于包含多个核心的系统来说是非常有用的,因为通常中断只被第一个CPU核心服务。

手动绑定亲和性

● 动态监控CPU中断情况,观察中断变化

1
watch -d -n 1 cat /proc/interrupts

● 查看网卡中断相关信息

1
cat /proc/interrupts | grep -E “eth|CPU”

● 网卡亲和性设置
修改proc/irq/irq_number/smp_affinity之前,先停掉irq自动调节服务,不然修改的值就会被覆盖。

1
/etc/init.d/irqbalance stop

通过查看网卡中断相关信息,得到网卡中断为19

1
2
3
4
5
[root@master ~]# cd /proc/irq/19
[root@master 19]# cat smp_affinity
00000000,00000000,00000000,00000001
[root@master 19]# cat smp_affinity_list
0

修改值,将19号中断绑定在cpu2上:

1
2
3
4
5
[root@master 19]# echo 4 > smp_affinity
[root@master 19]# cat smp_affinity
00000000,00000000,00000000,00000004
[root@master 19]# cat smp_affinity_list
2

如果是要将网卡中断绑定在cpu0和cpu2上怎么做了?请先参照上文中的CPU列表。cpu0和2的十六进制值分别为1,4。那么如果要同时绑定在cpu0和cpu2上,则十六进制值为5,如下:

1
2
3
4
5
[root@master 19]# echo 5 > smp_affinity
[root@master 19]# cat smp_affinity
00000000,00000000,00000000,00000005
[root@master 19]# cat smp_affinity_list
0,2

taskset为系统进程PID设置CPU亲和性

查看某个进程的CPU亲和性

1
2
# taskset -p 30011
pid 30011's current affinity mask: ff

设置某个进程的CPU亲和性

1
2
3
# taskset -p 1 30011
pid 30011's current affinity mask: ff
pid 30011's new affinity mask: 1

使用-c选项可以将一个进程对应到多个CPU上去

1
2
3
4
5
6
7
# taskset -p -c 1,3 30011
pid 30011's current affinity list: 0
pid 30011's new affinity list: 1,3

# taskset -p -c 1-7 30011
pid 30011's current affinity list: 1,3
pid 30011's new affinity list: 1-7

RPS/RFS

前面大量介绍了多队列网卡及中断绑定,但是在单网卡单队列的情况下要想负载做网卡软中断绑定怎么办呢?RPS/RFS就是为此而生的,RPS/RFS功能出现在Linux kernel 2.6.35中,由google的工程师提交的两个补丁,这两个补丁的出现主要功能是在单队列网卡的情况下,在系统层用模拟了多队列的情况,以便达到CPU的均衡。

RPS(Receive Packet Steering)主要是把软中断的负载均衡到各个cpu,简单来说,是网卡驱动对每个流生成一个hash标识,这个HASH值得计算可以通过四元组来计算(SIP,SPORT,DIP,DPORT),然后由中断处理的地方根据这个hash标识分配到相应的CPU上去,这样就可以比较充分的发挥多核的能力了。通俗点来说就是在软件层面模拟实现硬件的多队列网卡功能,如果网卡本身支持多队列功能的话RPS就不会有任何的作用。该功能主要针对单队列网卡多CPU环境,如网卡支持多队列则可使用SMP irq affinity直接绑定硬中断。

由于RPS只是单纯把数据包均衡到不同的cpu,这个时候如果应用程序所在的cpu和软中断处理的cpu不是同一个,此时对于cpu cache的影响会很大,那么RFS(Receive flow steering)确保应用程序处理的cpu跟软中断处理的cpu是同一个,这样就充分利用cpu的cache,这两个补丁往往都是一起设置,来达到最好的优化效果, 主要是针对单队列网卡多CPU环境。

网卡软中断分发的软件解决方法RPS/RFS
RSS需要网卡硬件的支持,在使用不支持RSS的网卡时,为了充分利用多核cpu,centos6.1开始提供了RPS(Receive Packet Steering)和RFS(Receive Flow Steering)。
RPS使网卡可以把一个rx队列的软中断分发到多个cpu核上,从而达到负载均衡的目的。RFS是RPS的扩展,RPS只依靠hash来控制数据包,提供了好的负载平衡,但是它没有考虑应用程序的位置(注:这个位置是指程序在哪个cpu上执行)。RFS则考虑到了应用程序的位置。RFS的目标是通过指派应用线程正在运行的CPU来进行数据包处理,以此来增加数据缓存的命中率。

参考文献

多队列网卡及网卡中断绑定阐述

RECEIVE-SIDE SCALING (RSS)

多队列网卡CPU中断均衡

[精] Linux内核数据包处理流程-数据包接收(2)

Redis 高负载下的中断优化

待整理。。。。

背景

在删除文件后,有时会遇到磁盘空间并未被释放的场景。这里自己尝试复现,并去做相关的说明。

删除后空间未释放

根目录下当前占用空间是626G。

创建的文件/tmp/safedog_windows_2012_new.img占了15G。(ls和du的区别留到后面讲)
![image](文件已删除空间未释放的例子/2_ 文件占用空间.png)

这里用vim打开文件,让有个进程一直在使用这个文件。

删除文件后,空间并未释放。

原因分析:

一个文件在文件系统中的存放分为两个部分:数据部分和指针部分,指针位于文件系统的meta-data中,数据被删除后,这个指针就从meta-data中清除了,而数据部分存储在磁盘中,数据对应的指针从meta-data中清除后,文件数据部分占用的空间就可以被覆盖并写入新的内容,之所以出现删除文件后,空间还没释放,就是因为有进程还在一直向这个文件写入内容,导致虽然删除了文件,但文件对应的指针部分由于进程锁定,并未从meta-data中清除,而由于指针并未被删除,那么系统内核就认为文件并未被删除,因此通过df命令查询空间并未释放也就不足为奇了。

释放的解决措施

停掉或重启占用文件的进程

重启或停止进程后,系统会自动回收

Truncate File Size

Alternatively, it is possible to force the system to de-allocate the space consumed by an in-use file by forcing the system to truncate the file via the proc file system. This is an advanced technique and should only be carried out when the administrator is certain that this will cause no adverse effects to running processes. Applications may not be designed to deal elegantly with this situation and may produce inconsistent or undefined behavior when files that are in use are abruptly truncated in this manner.

1
2
3
> /proc/$pid/fd/$fd_number   
echo > /proc/$pid/fd/$fd_number
echo "" > /proc/$pid/fd/$fd_number

// 以上截断命令的区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
> /proc/$pid/fd/$fd_number   //截断后占据空间为0 

[root@xxg-uhost1683 2018-01-09]# > /proc/21780/fd/4
[root@xxg-uhost1683 2018-01-09]# lsof -p 21780 | grep delete
less 21780 root 4r REG 253,0 0 171442340 /opt/data/delay_delete/2018-01-09/1356f3f2-1c93-4c9e-9f9f-0a4cee57f5b7.img_1515479993 (deleted)

echo > /proc/$pid/fd/$fd_number //截断后占据空间为1个字节

[root@xxg-uhost1683 2018-01-09]# echo > /proc/27052/fd/4
[root@xxg-uhost1683 2018-01-09]# lsof -p 27052 | grep delete
less 27052 root cwd DIR 253,0 4096 7995395 /opt/data/delay_delete/2018-01-09
less 27052 root 4r REG 253,0 1 171442356 /opt/data/delay_delete/2018-01-09/1ede93ec-4424-413f-ad24-9781038abdf7.img_1515479994 (deleted)

echo "" > /proc/$pid/fd/$fd_number // 占据空间为1个字节

[root@xxg-uhost1683 2018-01-09]# echo "" > /proc/7706/fd/4
[root@xxg-uhost1683 2018-01-09]# lsof -p 7706 | grep delete
less 7706 root cwd DIR 253,0 4096 7995395 /opt/data/delay_delete/2018-01-09
less 7706 root 4r REG 253,0 1 284164259 /opt/data/delay_delete/2018-01-09/439e3eb3-0272-44ad-b1ee-5ad248ca2db8.disk_1515479994 (deleted)

echo "" > /proc/$pid/fd/$fd_number // 占据空间为2个字节

[root@xxg-uhost1683 2018-01-09]# echo " " > /proc/7706/fd/4
[root@xxg-uhost1683 2018-01-09]# lsof -p 7706 | grep delete
less 7706 root cwd DIR 253,0 4096 7995395 /opt/data/delay_delete/2018-01-09
less 7706 root 4r REG 253,0 2 284164259 /opt/data/delay_delete/2018-01-09/439e3eb3-0272-44ad-b1ee-5ad248ca2db8.disk_1515479994 (deleted)

以下直接操作文件名均不起作用

1
2
3
> /tmp/safedog_windows_2012.img //不起作用
echo > /tmp/safedog_windows_2012.img //不起作用
echo "" > /tmp/safedog_windows_2012.img不起作用

删除后文件未释放的恢复

1
2
cp /proc/44848/fd/4 /tmp/safedog_windows_2012_new.img1    //方法1
cat /proc/44848/fd/4 > /tmp/safedog_windows_2012_new.img2 //方法2

恢复的文件占用空间还会更大,是否恢复出来的已经不是稀疏的了?后面待解释

比较文件的一致性

1
diff /tmp/safedog_windows_2012_new.img1 /tmp/safedog_windows_2012_new.img2

参考链接

由一次磁盘告警引发的血案 – du 和 ls 的区别

Why is space not being freed from disk after deleting a file in Red Hat Enterprise Linux?

变量的大小写

go中根据首字母的大小写来确定可以访问的权限。如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用。包括接口、类型、函数和变量等。

可以简单的理解成,首字母大写是公有的,首字母小写是私有的

下面是一个排查该问题的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"encoding/json"
"fmt"
)

type Position struct {
X int
Y int
Z int
}

type Student struct {
Name string
Sex string
Age int
position Position
}

func main() {
position1 := Position{10, 20, 30}
student1 := Student{"zhangsan", "male", 20, position1}
position2 := Position{15, 10, 20}
student2 := Student{"lisi", "female", 18, position2}

var srcSlice = make([]Student, 2)
srcSlice[0] = student1
srcSlice[1] = student2
fmt.Printf("Init:srcSlice is : %v\n", srcSlice)
data, err := json.Marshal(srcSlice)
if err != nil {
fmt.Printf("Serialize:json.Marshal error! %v\n", err)
return
}
fmt.Println("After serialize, data : \n", string(data))

var dstSliece = make([]Student, 2)
err = json.Unmarshal(data, &dstSliece)
if err != nil {
fmt.Printf("Deserialize: json.Unmarshal error! %v\n", err)
return
}
fmt.Printf("Deserialize:dstSlice is : %v\n", dstSliece)
}

很意外的是,我们反序列化后获取的对象数据dstSliece是错误的,Position里的数据都变成了0。而json.Unmarshal没有返回任何异常。

观察将序列化后的json串,Position的数据丢了,这使得我们想到了可见性,即大写的符号在包外可见。通过走查代码,我们发现Student的定义中,Position的变量名是小写开始的。

1
2
3
4
5
6
7
type Student struct {
Name string
Sex string
Age int
//position Position
Position Position
}

改成大写后再观察结果,可以正常序列化。

序列化到json后改成小写

对于json串,很多人喜欢全小写,对于大写开头的key感觉很刺眼,我们继续改进:

1
2
3
4
5
6
7
8
9
10
11
12
type Position struct {
X int `json:"x"`
Y int `json:"y"`
Z int `json:"z"`
}

type Student struct {
Name string `json:"name"`
Sex string `json:"sex"`
Age int `json:"age"`
Posi Position `json:"position"`
}

再次运行程序,结果是我们期望的,打印如下:

1
2
3
4
Init:srcSlice is : [{zhangsan male 20 {10 20 30}} {lisi female 18 {15 10 20}}]
After serialize, data :
[{"name":"zhangsan","sex":"male","age":20,"position":{"x":10,"y":20,"z":30}},{"name":"lisi","sex":"female","age":18,"position":{"x":15,"y":10,"z":20}}]
Deserialize:dstSlice is : [{zhangsan male 20 {10 20 30}} {lisi female 18 {15 10 20}}]

omitempty

1
2
3
4
type Dog struct {
Breed string
WeightKg int
}
1
2
3
4
5
6
7
8
func main() {
d := Dog{
Breed: "dalmation",
WeightKg: 45,
}
b, _ := json.Marshal(d)
fmt.Println(string(b))
}

output:

1
{"Breed":"dalmation","WeightKg":45}

如果对dog没有设置:

1
2
3
4
5
6
7
func main() {
d := Dog{
Breed: "pug",
}
b, _ := json.Marshal(d)
fmt.Println(string(b))
}

这会输出

1
{"Breed":"pug","WeightKg":0}

即使WeightKg的值是未知的。

更好的方式是使“WeightKg”为null, 或者根本没有这个值。

omitempty tag 正好可以起到这个作用.

1
2
3
4
5
type Dog struct {
Breed string
// The first comma below is to separate the name tag from the omitempty tag
WeightKg int `json:",omitempty"`
}

这会输出

1
{"Breed":"pug"}

参考链接

Golang初学者易犯的三种错误

Go’s “omitempty” explained

TCP Keepalive的作用

链接建立之后,如果应用程序或者上层协议一直不发送数据,或者隔很长时间才发送一次数据,当链接很久没有数据报文传输时如何去确定对方还在线,到底是掉线了还是确实没有数据传输,链接还需不需要保持,这种情况在TCP协议设计中是需要考虑到的。

TCP协议通过一种巧妙的方式去解决这个问题,当超过一段时间之后,TCP自动发送一个数据为空的报文给对方,如果对方回应了这个报文,说明对方还在线,链接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为链接丢失,没有必要保持链接。

阅读全文 »

并发场景

秒杀场景,多人并发申请购买同一种商品。
下面以2人抢购同一件商品举例,商品数量为1。
基本流程如下:

这个流程中存在明显的并发问题,当进程A查看有1个商品,但未开始创建购买记录,此时另一个进程B也进行了资源检查,发现有1个商品,顺利通过,并完成购买操作,此时A继续执行,则1个商品会产生两条购买的记录。(这个并发场景很简单,很普遍)

解决方案

解决方案通常是2类:
1 通过将请求分发到不同set,减少并发的可能性,分而治之。这里暂时不讨论。
2 当收到并发请求时,如何处理。

加锁操作先占有锁资源,再占有资源

a. 锁库存
b. 插入“秒杀”记录
c. 更新库存

“秒杀”系统的设计难点就在这个事务操作上。商品库存在DB中记为一行,大量用户同时“秒杀”同一商品时,第一个到达DB的请求锁住了这行库存记录。在第一个事务完成提交之前这个锁一直被第一个请求占用,其他到达的请求全部失败。

加锁的方案,效率会比较低。实际应用时,通常要配合排队系统,让没有获取到锁的请求排队。

单独开发请求排队调度模块

排队模块接收用户的抢红包请求,以FIFO模式保存下来,调度模块负责FIFO队列的动态调度,一旦有空闲资源,便从队列头部把用户的访问请求取出后交给真正提供服务的模块处理。优点是,具有中心节点的统一资源管理,对系统的可控性强,可深度定制。缺点是,所有请求流量都会有中心节点参与,效率必然会比分布式无中心系统低,并且,中心节点也很容易成为整个系统的性能瓶颈。

巧用Redis 特性,使其成为分布式序号生成器(我们最终采用的做法) – 具体还要再看,如何利用redis的特性

参考
https://www.ibm.com/developerworks/cn/web/wa-design-small-and-good-kill-system/index.html
https://blog.csdn.net/zhanjianshinian/article/details/53342730
每件商品以一个数字ID来标识,这个ID是全局唯一的,所有围绕商品的操作都使用这个ID作为数据的关联项。

Redis节点内存储的是这个分组可以分发的商品ID号段,利用Redis特性实现红包分发,各服务节点通过Redis原语获取当前拆到的红包。这种做法的思路是,Redis 本身是单进程工作模型,来自分布式系统各个节点的操作请求天然的被 Redis Server 做了一个同步队列,只要每个请求执行的足够快,这个队列就不会引起阻塞及请求超时。而本例中我们使用了 DECR 原语,性能上是可以满足需求的。Redis 在这里相当于是充当一个分布式序号发生器的功能,分发红包 ID。

通过memcached来控制并发数 – 具体还要再看

https://blog.csdn.net/echo3/article/details/17580347 使用memcached进行并发控制(写的非常好)
http://www.infoq.com/cn/articles/2017hongbao-weixin# 百亿级微信红包的高并发资金交易系统设计方案
https://blog.csdn.net/yanker1990/article/details/78737626 Memcache的并发问题和利用CAS的解决方案

参考

如何设计一个小而美的秒杀系统?
https://www.ibm.com/developerworks/cn/web/wa-design-small-and-good-kill-system/index.html
使用memcached进行并发控制(写的非常好)
https://blog.csdn.net/echo3/article/details/17580347
电商秒杀系统设计分析
https://blog.csdn.net/zhanjianshinian/article/details/53342730

https://memcached.org/about
Redis DECR命令
https://www.kancloud.cn/thinkphp/redis-quickstart/36180

云主机的问题:
申请时要排队,好像没有意义。
防止申请到同一台宿主机,前面做的临时添加资源是否有用。跟定时更新是否有冲突?

Go语言的安装

参照官网文档 https://golang.org/doc/install
可以在命令行中输入命令来验证是否安装成功:

  • go version:查看安装的版本
  • go env:当前go的配置环境

2个环境变量,即 GOROOT、GOPATH和GOBIN: