读阿里-操作系统内核开发实战总结

资料来源是之前北邮人论坛下载的ppt.
是由伯松(阿里集团)和王智通(阿里云)做的整理的内部分享。
如作者所言,学习者需要关注的是概念,具体的实现有很多种。因此整理这篇博客,在ppt的基础上再做一些个人层面的总结。

1.内核加载

在一台已经关机的电脑前,我们拥有什么?

a.硬盘,存储着操作系统的内核程序。
b.cpu及寄存器。
c.内存。

概念1:去哪里找内核程序。 bootloader如linux grub去硬盘约定好的地方去读(印象里是第一个分区的第一个扇区?)。
#问题1.去哪里找第一个内核函数?
linux boot protocol. 提前约定好格式或者说是协议。

概念2:内核的第一个c函数调用前的必要条件。
cpu与寄存器的状态,基本内存布局。满足c语言条件即可。
一旦进入c语言,则整个进入了代码可视可控阶段。

2.保护模式基础

内核设计中需要考虑的几个概念:

1.权限控制。2.寻址方式。3.中断处理。4.进程调度。  

概念1:权限控制主要是在寻址的时候做。
页表的bit2 U/S标志位。1是user. 0是sup.
一个程序的地址空间分为两个部分:用户态与内核态。

概念2:页表的生成:
内核的页表在加载内核时已经完成。
用户态的程序在启动时,依赖一些系统调用构建自己的页表。

概念3:线性地址,物理地址。
依靠cr3寄存器,保留着页表自身的位置。

假设一个线性地址:  
31--------22 21------12 11-------0
   pde           pte        offset

概念4:中断/异常。
中断:外部硬件或者软件INT n(软中断?)。
异常:执行过程中探测到错误的指令。

概念5: 中断响应的流程。
“好好的吃着火锅,唱着歌,夯的来了一个中断!”
主要是上下文切换。一部分是cpu在中断时存储/恢复,一部分是中断程序来存储恢复。

两种情况:
1.中断时处于用户态:代码段寄存器,栈指针得留着(SS,ESP)。
另外要进入内核态,用户栈的地址与状态也得留着(EFLAGS/CS/EIP/error Code)。
2.中断时处于内核态:只需要栈切换(EFLAGS/CS/EIP/error Code)

因为中断时,已经保存好了前一个程序的状态,所以中断回来之后,可以发生进程调度。

概念6:进程调度。

1.地址空间:基于页表的寻址保护方式,所以要切回自己的页表。
2.栈:当时执行到哪里停了,接下来继续执行,当时的状态是什么。寄存器+栈就ok了。

问题:内核栈能否单个核只有一个?  
挂在单个程序上的话,就意味着在内核态,这个程序的事情也可以被抢占。
可以参考golang单个p的g0栈。

3.系统调用

概念1:消息传递机制

linux:EAX寄存器为系统调用号
MLXOS: 整个封装成消息体,包含:系统调用号,参数格式,接收者。

概念2:系统调用的过程梳理:

1.按照接口定义将参数准备好。寄存器/栈的分布等
2.调用中断命令。
3.代码段由中断门转换。数据段:先是把当前程序的寄存器等数据入栈,然后切换数据段为内核数据段。
4.内核执行操作。
5.反馈执行的结果。
6.返给用户层。

用户的上下文到底存在哪里: 该程序的内核栈中。
是不是可以这样理解:除了正在用户态运行的程序,
其他的进程都是处在内核的空间内,且内核栈上有自己的上下文。

golang的goroutie我理解状态是存储在本身的栈上,切换时确实时用的g0栈。

用户态调用某一个func:
function(para1) {
   准备好系统调用的参数
   保留现场(各种入栈)
   执行内核的systemcall
   解析内核的返回值,如果需要的话。
   恢复现场
   调用调度程序。(有可能继续执行该程序,也有可能不再执行)
}

4.进程调度

概念1:程序链接与地址分布
形同内核的形式,本质上就是一个定义/协议。ELF。

概念2:进程加载与创建。
硬盘上的代码与内核中的进程数据结构的区别。

解析elf文件:
进程管理数据结构:task_struct.
地址空间管理数据结构 mm_struct.
初始化用户态栈空间。-> 创建一段合法的虚拟地址空间(4KB)+页表注册。
根据程序中每一个程序段,初始化mm_struct。
准备启动的数据结构。
注册到内核进程表。

概念3:进程退出
从内核中创建,从内核中消亡。

概念4:进程切换
重要是两个部分:地址空间切换与栈的切换。

不同程序内核的地址空间是相同的,所以放心的切用户态cr3页目录表。
TLB中非全局项被刷新。   

概念5:休眠与唤醒

从执行队列中将这个进程拿出来,就是休眠。放回去就是唤醒。

依赖数据本身做传递。
1.比如等待某信号为空,让它休眠。之后把信号置位,再让它恢复,它得类似for循环这种。
2.类似函数调用一样。调用了某个函数,需要一个返回值位置,执行的下一条语句地址。把返回值放回去,继续执行就好了。

5.物理内存管理

buddy算法与slab算法。

6.虚拟内存管理

概念1:虚拟内存是在做什么?

内存映射 (mmap)
堆管理 (heap)
栈管理 (stack)
按需分配内存 (pagefault)
地址空间保护(#PF/#GP)
页交换 (swapping)
页缓存 (paging)

概念2: 文件映射与匿名映射

文件映射:磁盘文件–pagecache –进程地址空间 匿名映射:page frame–进程地址空间。

概念3: TLB
转换旁视缓冲。(Translate Lookaside Buffer)

tcpdump的高级用法

tcpdump是linux平台的,基于bpf的抓包工具。
比较常见的用法是: tcpdump -i eth1 tcp port 80

前几天在工作中遇到一个问题:线上的redis库,因为HGET命令中包含特殊字符报错。
不能确定是哪个模块引入的问题。

问题描述: tcp 7700 端口接收外部的查询命令, 同时包含多个redis命令字。
当前:只有 HGET 命令字有问题,且出错的字符串多是 a 域名。

抓全部的包去找有问题的场景简直大海捞针,因此让我们见识下tcpdump的新魔法:

tcpdump -i any 'tcp port 7700 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0) 
and (tcp[25] == 0x34) 
and (tcp[28] == 0x48) and (tcp[29] == 0x47) and (tcp[30] == 0x45) and (tcp[31] == 0x54)'
-nnn -vvv -w 49.7700.pcap

====基础知识:
ip[2:2] 以2起,2个字节表示包长度
(ip[0]&0xf)<<2  第一个字节 前4bit是协议 后4bit 乘以4 是ip包头大小。

tcp[12]&0xf0>>2 高位4bit是长度
tcp[0-19]表示包头,自tcp 20个字节起是 payload.

redis的tcp命令是:
*3
$4
HGET
$99
PLAN_...
$10
abcdef.com....

(ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0 
保证tcp的包大小大于0, 把握手包这种就过滤了。

tcp[25] == 0x34 检查$4的4 , 只要4个字符的命令字。
28,29,30,31 对应的就是 H G E T

为什么是25:
因为redis命令字后跟着2个字节额 0d 0a。 

本身思路出来就可以,再具体的可以:

  • 包长度的校验具体成大于多少。
  • redis命令中有变长的字符,可以针对$99,$100做单独的处理,这样就可以明确定位到abcdef.com的域名。

看SICP有感

17年最早是开始读sicp的pdf.看文本的效果比较一般,不太好抓重点,再一个编程的经验比较缺乏.
19年11月份的时候,从b站看到了sicp的视频,看了一遍.
20年4月,再次出发.有感于大型的系统设计,基于此,再次观看sicp,以此博客记录观看过程的思考.

lec1a: Lisp概览

对一门通用语言的学习,需要关注的几个点:
1.基本的元素: +,-,*,/  数字.
2.组合: conds,if
3.抽象的方式: define

lec2a: 高阶过程

抽象模式。举个简单的例子:
过程1:for(a= i->j) sum(a)
过程2:for(a= i->j) sum(a^2)
for与sum就是共同的模式,即可以被抽象的地方。


编程语言一等公民的权力:
1.可以被变量命名
2.可以被当作过程参数传递。
3.可以被当作过程的返回值。
4.可以被数据结构包含。

lec2b: 复合数据

数据和函数之间本身没有严格的界限。
c = (cons a b)
(car c) = a
(cdr c) = b

可以认为是一个内存的结构体布局。
也可以认为一个闭包函数。

一致性hash算法

看资料,了解到一致性hash算法。
简单的写了一个demo, 测试之后,再去看其他人的算法,理解得更快一些。这是一个好的模式。

func main() {
    hash := make([]string, 8)
    mask := (1 << 3) - 1

    hash[2] = "server1"
    hash[5] = "server2"
    hash[7] = "server3"

    i := 50
    index := 0
    for i < 58 {
        i++
        index = i & mask
        for {
            if hash[index] != "" {
                fmt.Println("find server: ", hash[index])
                break
            }
            index = (index + 1) & mask
        }
    }
}

github上搜到一个golang版本的代码

本质上是一样的。实现上有一些不同。使用数组 找机器+存机器 会浪费空间。
而这个版本将数组下标单独拿出来,一个是排好序的数组下标用来找机器,一个是map[uint32]string用来存机器。

type Consistent struct {
    circle           map[uint32]string   // 存机器。
    members          map[string]bool
    sortedHashes     uints               // 找机器
    NumberOfReplicas int                 // 节点复制个数,使得节点分布更均匀。
    count            int64
    scratch          [64]byte            // buf
    sync.RWMutex
}

圆形,真是神奇。
想想有没有其他什么地方能用到圆形?
环形缓冲区。
想到了再加。

从一道dp算法题说起

被这道leetcode题目516. Longest Palindromic Subsequence折腾良久,直到最后弄清楚它,才大呼奇妙。 算法与思维的神奇,大概就在此处。

这篇文章不错,可以作为入门。

1.题目分析

动态规划的关键就是得出状态与状态转移方程。

这道题我的第一个问题就是:没有想清楚,为什么它是一个二维的状态?

一维与二维的区别就在于状态之间的转换。
对于这道题而言,以一个字符为例,它可以通过左边增加字符或者右边增加字符,进入下一个状态.
由现有的状态有超过1种途径进入下一个状态。因为它是二(多)维的。

这道题的状态转移方程如下:

dp[i][j] = dp[i+1][j-1] + 2 if s.charAt(i) == s.charAt(j)
otherwise, dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])
Initialization: dp[i][i] = 1

2.代码分析。

看到上面的状态转移方程,很容易就可以得到递归的办法。

func longestPalindromeSubseq(s string) int {
    return check([]byte(s)) 
}

func check(s []byte) int {
    if s[0] == s[len(s)-1] {
        return 2 + check(s[1:len(s)-1])
    }else {
        a := check(s[0:len(s)-1])
        b := check(s[1:len(s)])
        if b > a {
            a = b
        }
        return a
    }
}

这样是过不了的。加上缓存,去掉重复的计算,accepted.

接下来,其实也就是一个递归转化成循环的问题。按照上述的状态转移方程,以abcd为例,要算abcd,必须先算出abc和bcd,则必须先算出ab,bc,cd,则必须先算出a,b,c,d。单个字符已知就是1.所以,可以得到以下的代码。

func longestPalindromeSubseq(s string) int {
    n := len(s)
    dp := make([][]int, n)
    for i := range dp {
        dp[i] = make([]int, n)
        dp[i][i] = 1
    }

    for Len := 2; Len <= n; Len++ {
        for i := 0; i+Len-1 < n; i++ {
            j := i + Len - 1
            if s[i] == s[j] {
                dp[i][j] = dp[i+1][j-1] + 2
            } else {
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
            }
        }
    }

    return dp[0][n-1]
}

进一步,研究状态转移方程,可以发现dp[i][j]只与dp[i+1][j-1],dp[i+1][j], dp[i][j-1]这几个量有关系,即要算出i,须先算出i+1.要算出j,须先算出j-1. 同时,想象一个二维的长方形,我们知道对角线上的每一个值。

每一个位置表示: 从本行左侧为1的字符起,到该字符位置的最大的回文字符个数。
| 1| c| c| c| // i层第四次循环 j层循环三次
|  | 1| b| b| // i层第三次循环 j层循环二次
|  |  | 1| a| // i层第二次循环 j层循环一次
|  |  |  | 1| // i层第一次循环 j层循环跳出

因此,双层循环嵌套,i由大变小,j由小变大。

dp := make([]int, n)
cur := make([]int, n)

for i := length-1;i >= 0; i-- {
    cur[i] = 1
    for j := i+1; j < length; j++ {
        if s[i] == s[j] {
            // 最是神奇的一步
            cur[j] = dp[j-1] + 2
        }else {
            cur[j] = max(cur[j-1],dp[j])
        }
    }
    dp, cur = cur, dp
} 

最后两份代码,只是循环的方式不同而已。代码二是可以采用代码三的双数组缓存去优化的。

直到最终的代码出来,才发现,最终的代码就是最开始状态转移方程最原始的体现。简单即是大美。

Count Bits

在刷算法的时候遇到了好几个有趣的计算一个数中,bit位为1的个数。

1.直接计算

直接计算移位计算1.需要注意的是,输入参数。int 与 uint 的移位策略是不一样的。
int count(uint64 x)
{
    int count = 0;
    for(; x; x>>1)
        ++count;
    return count;
}

2.一点点小技巧

利用(8)1000 - 1 = (7)0111.

int count(uint64 x)
{
    int count = 0;
    for (; x; ++count)
        x&=x-1;
    return count;
}

3.分治

const uint64 m1  = 0x5555555555555555;
const uint64 m2  = 0x3333333333333333;
const uint64 m4  = 0x0f0f0f0f0f0f0f0f;
const uint64 m8  = 0x00ff00ff00ff00ff;
const uint64 m16 = 0x0000ffff0000ffff;
const uint64 m32 = 0x00000000ffffffff;

int count(uint64 x) {
    x = (x & m1 ) + ((x >>  1) & m1 );  
    x = (x & m2 ) + ((x >>  2) & m2 ); 
    x = (x & m4 ) + ((x >>  4) & m4 ); 
    x = (x & m8 ) + ((x >>  8) & m8 ); 
    x = (x & m16) + ((x >> 16) & m16); 
    x = (x & m32) + ((x >> 32) & m32); 
    return x;
}

4.优化后的分治

int count(uint64 x) {
    x -= (x >> 1) & m1;              // 2 * a + b - a = a + b
    x = (x & m2) + ((x >> 2) & m2);  // 10 + 10 = 100 因此,会产生进位,所以为进位做好准备。
    x = (x + (x >> 4)) & m4;         // 最大是 0100 0100 = 1000 不会进位,因此可以先做加法再做与。
    x += x >>  8;                    // 不做与 运算。8位 已经表示 256, 
                                    // 在8位相加时,正确的和已经加在后8位了,前8位与后8位互不影响。
    x += x >> 16; 
    x += x >> 32;  
    return x & 0x7f;
}

5.再次优化

int count(uint64 x) {
    x -= (x >> 1) & m1;  
    x = (x & m2) + ((x >> 2) & m2);
    x = (x + (x >> 4)) & m4;    
    return (x * h01)>>56;   // 神来之笔
}

火焰图的学习与使用

以nodejs 为例,学习火焰图相关。

node –perf-basic-prof-only-functions demo.js&

perf record -F 99 -p pgrep -n node -g – sleep 30

perf script > nodestacks

./stackcollapse-perf.pl < ../nodestacks | ./flamegraph.pl –colors js > ../node-flamegraph.sv

1.如何生成火焰图
即如何实现数据可视化。 -- 数据,可视化。

函数本身提供符号的解析。(可视化)
由perf工具定时去抓取调用栈的数据。(数据源)
统计调用次数,生成火焰图。(可视化)


2.如何分析火焰图

示例如下:
在外层一共调用了 d, e, f 几个方法,其中d-b-a,e-b-a,f-c-a为调用链,
f 就是需要我们优化的“平顶山”,即占用了较多的cpu时间。

[d][ e ][  f  ]
[  b   ][  c  ]
[      a      ]

等到有具体的使用心得,再做进一步的更新。

关于mongo的一些体会

项目中主要的业务数据库使用了mongo。前几天在微博上看到有大佬在讨论正确使用mongo的事情,自己也梳理了一下,努力不让自己变成把mongo只当json存储的程序员。

1. mongo的数据形式

主要是两种设计,一种是refrence。refrence是传统的范式设计所推崇的,有代表性的操作就是join以及外键带来的一些操作。

关于refrence的实现,mongo有两种主要的形式,一种是手动id关联,另外一种是Dbref。手动id需要将关联”in mind”.使用Dbref的话,当数据要变动时,会很复杂。

知乎上看到的:mysql的设计初始,是传统的C-S架构,C端是不可靠以及不可信的,
因此在S端的功能很重,包括事务,外键等等。

而随着技术的发展,目前在mysql的前面一般会有内网的业务逻辑服务器,
也就是说是可信端在操作mysql,事务可以放在业务层去处理,外键这种关联也可以挪到业务层去实现。

第二种就是embedded。

mongo作为document类型的数据库,它的灵活与便利就是embedded。将所有的相关的数据放在一起,减少查询,单次操作。它的缺点是可能会带来数据的冗余,另外所有的数据放在一起,表越大,写性能会越差。

2. mongo是否具有可扩展性。

最近在项目中使用了mongo的复制集,自己就认为mongo是具有扩展性的。然而,答案并不是这么简单。

那么,什么是可扩展性?

先补充一点点概念:

Scale-up(纵向扩展) 主要是升级现有的机器,简而言之:加配置。
Scale-out(横向扩展) 主要是增加处理节点,简而言之:加机器。(主流的方向)

参考:SQL Databases Don’t Scale

可扩展性主要表现在三个方面:
1:水平扩展,机器越多,处理能力越强。
2:应用无感知。
3:系统不因单点故障不可用。

以RAID (Random Array of Inexpensive Disk)为例:
1.更多的磁盘更好的性能。
2.应用系统不需要关注文件存储的具体分布,RAID对外是一个整体。
3.从RAID中拿出一个磁盘,不影响RAID工作。

所以,我们可以看出,mongo的复制集并不是可扩展的。

  • 机器越多,读性能越好,写性能仍然是瓶颈。

  • 应用使用mongo驱动,可以实现无感知。

  • 单点故障时,如果写节点宕机,会丢一部分数据。

整体来说,复制集不是一个纯粹的可扩展。mongo最新支持的auto-sharding就可以称之为可扩展。

最后贴上引发这篇文章的链接 NoSQL: If Only It Was That Easy

The real thing to point out is that if you are being held back from making something super 
awesome because you can’t choose a database, you are doing it wrong.

Go:使用Error的几种姿势

Talk is cheap, I will show you the code.

总的来说,error 一共有3种用法:

1. 第一种夹带私货。使用struct结果包裹,提供更多的信息。

2. 还是基于struct , 加上了更多的逻辑的判断。

3. error变量的直接比较。在标准库中很常见。
package main

import "errors"
import "fmt"


//第一种姿势
type MyError struct {
    Op  string
    Err error
}

func (e *MyError) Error() string { return e.Op + " " + e.Err.Error() }

func test() error { return &MyError{"Op string", errors.New("this is my err")} }


//第二种姿势
type DNSError struct {
    Err error
}

func (e *DNSError) Error() string {
    return e.Err.Error()
}

func (e *DNSError) Timeout() bool {
    return true
}

func (e *DNSError) Temporary() bool {
    return false
}

//第三种姿势
var testErr = errors.New("this is my err")

func testErrFunc() error { return testErr }

func main() {

    a := test()
    if b, ok := a.(*MyError); ok {
        fmt.Println(b)
    }

    c := DNSError{errors.New("DNS ERRror")}
    if c.Temporary() {
        return
    } else if c.Timeout() {
        fmt.Println(c)
    }

    if testErr == testErrFunc() {
        fmt.Println("Err is equal")
    }
}
output:

Op string this is my err
{DNS ERRror}
Err is equal

Go:http服务探究

在单核CPU的情况下,实现一个返回”Hello World”的服务器为例,通过比较C与Go的代码,探究go的一些设计与理念。

如果,以C语言为例,要实现一个返回”Hello World”的http服务器。

1. Listen端口
2. 为了支持高并发,将fd加入 加入epoll监听。
3. 该fd可读,accept 新的fd. 并开始监听。
4. 读取新连接的数据(二进制流)
5. http协议的分析处理
6. 向新连接写入http协议二进制流"hello World"。

主要还是 listen,accept,send几个api.

看一下go的实现。下面的go代码也实现了上述的效果。

package main
import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, world \n")
}

func main() {
    http.HandleFunc("/", hello)
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println("return")
    }
}

通过追代码,有以下几个感悟:

1. Go 中listen的部分。
定义一个Listener的接口。
type Listener interface {
        Accept() (Conn, error)
        Close() error
        Addr() Addr
}

在上述的代码中,ListenAndServe中,使用了下面的结构。
type tcpKeepAliveListener struct {
    *net.TCPListener
}

通过对TCPListener的包装,继承了该结构体的其他函数,只是重写accept() 函数。
从逻辑上说只是一层包装,实现了从该fd accept的连接 增加keepalive的功能。
2. Server结构。

// handler接口,定义一个可处理http请求 r, 并写回 w 中的函数。
// 其实,看代码就可以知道 r 就是从 w 中读出来的。

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

type HandlerFunc func(ResponseWriter, *Request) 
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}



func (srv *Server) Serve(l net.Listener) {
    // 函数的参数是listener接口类型。
    
    rw = l.accept()
    // A conn represents the server side of an HTTP connection.
    c := srv.newConn(rw)
    c.setState(c.rwc, StateNew) // before Serve can return
    // go 高并发的秘诀就在这里,每一个accept 使用一个go程处理。
    // 和epoll 有异曲同工之妙。
    // 其实底子里,go 还是用的 epoll.
    go c.serve(ctx)
}
3. conn是一个结构体,表示http Server端的一个连接。

它会调用Server中的 handler, 也就是我们的 hello 函数。

func (c *conn) serve(ctx context.Context) {
    serverHandler{c.server}.ServeHTTP(w, w.req)
}

总结:

本来是想探索下go的实现中,接口设计相关的东西,但是代码看下来,只达到了窥一斑。

7 Common Mistakes

interface只是行为,behavior。function 不改变状态,same input, same output.所以说 它只是在做事情。

struct包括 state 和 methods. methods 会改变结构体本身的状态。

还需要更多的学习,加深体会。

Linux性能分析与监测

介绍

系统的性能分析对于性能提升来说,是一个缓慢,复杂的过程。生产环境一般都是类Linux系统,对于一个系统的性能监控,主要从CPU性能内存管理文件IO网络IO这几个主要的方面进行。

1.CPU与内存相关

CPU是整个计算机的处理核心,大脑。如果内核调度程序是管家的话,CPU就是在车间里一直干活的工人。它需要处理各种突发的事件(中断);它需要雨露均沾,让每个进程都不受冷落(时间片,上下文切换);鉴于车间的空间有限,有些重要的生产资料一直在手边,其他的资料有空间就继续使,没有就得放隔壁专用车间(内存与交换区)。我们需要做的就是监控上述行为,以便了解车间的现状。

测试场景:4核Centos 7, 使用go监听8080端口,对请求不做处理,立即返回”Hello World”,使用wrk -t4 -c800 来访问。

vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 7  0      0 1644432   9460 1452656    0    0     0    82 9345 9853 37 54  9  0  0
 8  0      0 1644592   9460 1452656    0    0     0     0 8789 9478 39 53  8  0  0
 9  0      0 1644432   9460 1452656    0    0     0     0 10070 11192 35 56  9  0  0
 8  0      0 1644352   9460 1452656    0    0     0     0 7861 7467 40 54  7  0  0

Procs:
    r: The number of runnable processes (running or waiting for run time)
    b: The number of processes in uninterruptible sleep.
Memory:
    swpd: the amount of virtual memory used.
    free: the amount of idle memory.
    buff: the amount of memory used as buffers.
    cache: the amount of memory used as cache
Swap:
    si: Amount of memory swapped in from disk (/s)
    so: Amount of memory swapped to disk (/s)
IO:
    bi: Blocks received from a block device (blocks/s)
    bo: Blocks sent to a block device (blocks/s)
System:
    in: The number of interrupts per second, including the clock.
    cs: The number of context switches per second
CPU:
    us: Time spent running non-kernel code.  (user time, including nice time)
    sy: Time spent running kernel code.  (system time)
    id: Time spent idle.  Prior to Linux 2.5.41, this includes IO-wait time
    wa: Time spent waiting for IO.  Prior to Linux 2.5.41, included in idle.
    st: Time stolen from a virtual machine.

    nice time: time running niced user processes (explain from man top)
mpstat -P ALL 1
Linux 3.10.0-123.el7.x86_64 (localhost.localdomain)   08/19/2017  _x86_64_  (4 CPU)

AM  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
AM  all   39.73    0.00   27.57    0.00    0.00   24.86    0.00    0.00    0.00    7.84
AM    0   35.87    0.00   32.61    0.00    0.00   23.91    0.00    0.00    0.00    7.61
AM    1   43.75    0.00   21.88    0.00    0.00   30.21    0.00    0.00    0.00    4.17
AM    2   39.13    0.00   29.35    0.00    0.00   22.83    0.00    0.00    0.00    8.70
AM    3   40.45    0.00   28.09    0.00    0.00   21.35    0.00    0.00    0.00   10.11

top H

top - 09:58:59 up 4 days, 11:03,  8 users,  load average: 7.64, 6.92, 4.92
Threads: 476 total,   9 running, 467 sleeping,   0 stopped,   0 zombie
%Cpu(s): 37.4 us, 23.3 sy,  0.0 ni, 23.5 id,  0.0 wa,  0.0 hi, 15.8 si,  0.0 st
KiB Mem:   3869044 total,  2231604 used,  1637440 free,     9460 buffers
KiB Swap:  2113532 total,        0 used,  2113532 free.  1452668 cached Mem

PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
 42091 du        20   0  381780  28936   2424 R 54.4  0.7   7:01.43 server
 42089 du        20   0  381780  28936   2424 R 45.5  0.7   6:25.43 server 
 42215 du        20   0  381780  28936   2424 R 45.2  0.7   5:11.28 server 
 43474 du        20   0  381780  28936   2424 S 37.6  0.7   0:05.30 server 
 42218 du        20   0  381780  28936   2424 S 30.7  0.7   1:38.23 server 
 42090 du        20   0  381780  28936   2424 R 24.7  0.7   7:16.26 server 
 42425 du        20   0  415204  15584   1248 S 24.4  0.4   1:39.59 wrk
 42214 du        20   0  381780  28936   2424 R 22.4  0.7   7:03.22 server 
 42424 du        20   0  415204  15584   1248 S 20.8  0.4   1:39.64 wrk
 42426 du        20   0  415204  15584   1248 R 19.5  0.4   1:39.70 wrk
 42427 du        20   0  415204  15584   1248 S 18.1  0.4   1:39.08 wrk
 42217 du        20   0  381780  28936   2424 R  3.0  0.7   7:24.14 server
 42088 du        20   0  381780  28936   2424 S  1.3  0.7   0:11.81 server 

ps: 我是采用go run server.go ,竟然生出了好多个server thread。go有运行时(runtime),一般情况下,
还有和核数相同的M即thread,如果有系统调用还会生成新的thread。
wrk倒是很彻底,fork了4个。

free -m
             total       used       free     shared    buffers     cached
Mem:          3778       2196       1581        104          9       1414
-/+ buffers/cache:        773       3005
Swap:         2063          0       2063

2.文件IO

iotop命令

Total DISK READ :   0.00 B/s | Total DISK WRITE :       0.00 B/s
Actual DISK READ:   0.00 B/s | Actual DISK WRITE:       0.00 B/s
TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
 1 be/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % systemd --system --deserialize 26
 2 be/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % [kthreadd]
 3 be/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % [ksoftirqd/0]
 5 be/0 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % [kworker/0:0H]
 7 rt/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % [migration/0]
 8 be/4 root        0.00 B/s    0.00 B/s  0.00 %  0.00 % [rcu_bh]

3.网络IO

/sbin/ethtool eno16777736 查看网卡的信息
Settings for eno16777736:
    Supported ports: [ TP ]
    Supported link modes:   10baseT/Half 10baseT/Full 
                            100baseT/Half 100baseT/Full 
                            1000baseT/Full 
    Supported pause frame use: No
    Supports auto-negotiation: Yes
    Advertised link modes:  10baseT/Half 10baseT/Full 
                            100baseT/Half 100baseT/Full 
                            1000baseT/Full 
    Advertised pause frame use: No
    Advertised auto-negotiation: Yes
    Speed: 1000Mb/s
    Duplex: Full
    Port: Twisted Pair
    PHYAD: 0
    Transceiver: internal
    Auto-negotiation: on
    MDI-X: off (auto)
    Supports Wake-on: d
    Wake-on: d
    Current message level: 0x00000007 (7)
                   drv probe link
    Link detected: yes


iftop

TX:     cum:   21.5MB   peak:   6.29Mb      rates:   6.29Mb  6.16Mb  5.74Mb
RX:            353KB            102Kb                 101Kb   100Kb  94.0Kb
TOTAL:         21.9MB           6.39Mb               6.39Mb  6.25Mb  5.83Mb

总结

其实也只是列举了一些命令,在使用的过程中对命令的一些参数的含义自己检查记忆。Linux系统发展至今,有许多优秀的工具来供我们使用。它们从各个角度对系统做出了阐述。

但是如果我们跳出系统的圈子来看一个系统,主要的方向其实就是cpu和IO。cpu的正常运行,需要一定的调度策略(这点通过中断以及上下文切换可以看出数量),拥有内核空间和用户空间的概念(sys时间以及user时间,以此推断),需要空间来保存信息(内存的使用),需要信息的交换(内存与SWAP的相关),IO根据业务的不同,包括文件IO以及网络IO,其实就是对内对外输出。以网络IO为例,查看实时的速率。

希望能有机会实际演练,纸上谈兵终觉浅。

三张图看遍Linux 性能监控、测试、优化工具

Go与Nodejs性能简单对比

背景

大半夜忽然想起来,之前同事提到的Go与Nodejs的性能比较,具体的数据差距很大。当时没有太在意,后来慢慢对Go了解了一些。从原理上来说

Nodejs采用的是Reactor这种结构,与Nginx类似,一个连接就是一个数据结构,通过Epoll等来实现IO异步。
Go底层也是Reactor,每一个Go程就可以认为是单个数据结构,调度是由Go本身来提供的。

同事的测试数据记不清了,我的观点是Go和Nodejs应该是一个数量级的,但是Go默认是多核,而Nodejs天生就是单核,所以造成了巨大的差异。因此初步的想法就是验证多核和单核的影响。

过程

centos7 ,虚拟机,cpu 4核。

package main

import (
    "fmt"
    "runtime"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello World")
}

func main() {
    runtime.GOMAXPROCS(1)   //关键点
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}
var http = require("http"); 

http.createServer(function(request, response) { 
    response.end("hello World"); 
}).listen(8888,"0.0.0.0");

测试使用Wrk,Nodejs使用pm2。

wrk -t12 -c400 -d30s http://localhost:8080
pm2 start app.js -i 0 --name "api"
Running 30s test @ http://localhost:8888
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    23.88ms    6.97ms 265.19ms   91.13%
    Req/Sec     1.37k   301.01     4.35k    84.09%
  487168 requests in 30.10s, 51.57MB read
Requests/sec:  16187.38
Transfer/sec:      1.71MB

Running 30s test @ http://localhost:8080
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    14.05ms    3.83ms 212.71ms   73.18%
    Req/Sec     2.35k   241.08     5.11k    75.92%
  843218 requests in 30.05s, 102.93MB read
Requests/sec:  28059.36
Transfer/sec:      3.43MB

Running 30s test @ http://localhost:8888
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     8.92ms    8.48ms 310.50ms   90.90%
    Req/Sec     4.11k     1.40k   19.47k    69.56%
  1471190 requests in 30.09s, 155.74MB read
Requests/sec:  48888.35
Transfer/sec:      5.18MB

Running 30s test @ http://localhost:8080
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     5.15ms    4.34ms 222.96ms   80.88%
    Req/Sec     7.04k     2.19k   61.53k    71.45%
  2512607 requests in 30.10s, 306.71MB read
Requests/sec:  83475.03
Transfer/sec:     10.19MB

使用postman看了下两个的response,略有差别,Go有keep-alive,Node没有。是不是因为close再重连影响的?使用 netstat -tnp | grep “8888” | wc -l 发现在过程中并没有变化,所以排除重连。Go和Node都是793。wrk并发调为1000的时候为1993。还是奇数。单独的奇数我看了,只要wrk一启动就会有一个TIME_WAIT状态,猜测是wrk测试连接。所以,还是差8个,8/2=4,也就是说wrk少连了4个,再找,应该是1000/12=83,83*12=996,所以有4个。400是33*12=396,无意中挑的1000和400竟然结果一样。

结论

有差距,是一个数量级,但是差距比较大。再找找原因。

微服务入门之Nodejs-Seneca

项目上可能会用到微服务,因此以Seneca构建微服务为例,记录一下自己的一些理解。主要内容以get-started为基础。

Seneca is a microservices toolkit for Node.js

what is Senaca

业务逻辑之外的一切都可以用微服务包装。通过解耦来让业务系统可以很容易的创建和改变。

Senaca有以下几个属性:

1. 模式匹配。
2. 隐藏服务间通信细节。
3. 组件化。通过插件组合成为微服务。

问题:多种通信协议的支持是怎么做的,或者是怎么配置的?Seneca内置有http,tcp等,消息队列等应该需要加新的微服务。搜索了一下,npm有seneca-queue。

在一个进程内

通过seneca.add()增加模式,以及对应的处理函数。通过seneca.act()来触发该模式,以供add中添加的处理函数在逻辑之后调用act的回掉函数。

问题:如果是远程调用,回调函数会被传递过去吗?
答:不会,把模式msg发过去,远程的微服务将处理的结果传递回来,再供回调函数处理。可以看出,一个模式,加一个
处理逻辑就是一个服务,区别只是这个服务的位置。远端需要类似rpc。

模式匹配

在seneca中,“The more specific pattern always wins. In other words, the pattern with the highest number of matching attributes has precedence”。“If the patterns have the same number of properties, they are matched in alphabetical order.”

举例说明:需要匹配的pattern信息是'a:1, b:2':
如果微服务提供 'a:1','a:1, b:2',这两个同时满足,则匹配more specific,即第二个。
字母顺序没太懂。感觉是在说精确匹配,不知道为什么要专门提这个。

因此,新的更具体的服务,只需要更加具体的模式,就可以在不影响原有功能的情况下扩展。

模式匹配的一些小tips

代码复用,新的模式可以直接通过this.act()来使用之前的其他服务。

如果需要在原有服务的基础上,加强新功能,可以选择overwrite,在原来add的基础上,继续add,在新的处理函数中,可以通过this.prior调用previous action definition。这样,可以在调用前功能之前改变输入,又可以在调用前功能之后改变输出。相当于给原有的服务包装了一层。

插件化

Seneca的插件是一个拥有单个参数的函数,通过use来引用,和express相似。

函数名就是插件名,插件内通过this.add()添加新的模式,this指代的就是调用use的Seneca实例(instance)。

调用this.add(‘init:插件名’, func(msg, respond){ })可以初始化插件。respond不用自己定义,出错了调用就行。

如果想要将单独的服务放在单独的文件中,必须是Node.js的modules了。module.exports = function name(options) {},调用时use中使用require或者直接用name都可以。

还有一个this.wrap函数,就类似自动overwrite所有满足条件的pattern。在测试中,通过express,再远程调用服务,我这边看到的结果是wrap影响了pattern,具体的需要再研究。

把微服务摘出来

之前的讨论都是在同一个进程中,现在要开始加上通信。

Server的一方listen({'type':"tcp", pin:'role:math'})
Client的一方client({'type':"tcp", pin:'role:math'})。

这样,Client端只管act,满足pin的pattern就会自动调client。

问题:seneca发送的包和接收的包是什么样的。

根据不同的协议走不同格式的数据。http没必要,太重,tcp可以直接传递流。udp也不错。

集成web框架

seneca不是一个web框架。它可以处理http请求,但是它不是一个web框架。这块倒没有什么新的点,主要就是将api向pattern的转换,以及参数的一些设置。注意配置。

数据存储

seneca-entity提供了一个数据抽象层.基于以下几个操作:

load:加载entity, role:entity,cmd:load,name:<entity-name> 
save:创建或者更新entity, role:entity,cmd:save,name:<entity-name>
list:列举entity.role:entity,cmd:list,name:<entity-name>
remove:删除entity.role:entity,cmd:remove,name:<entity-name>

总结

seneca本身也只是提供了一个管理微服务的框架,服务本身+index:即pattern+通信机制。微服务是一个解耦的产物,有点类似于unix哲学,一个物体实现一个功能。相对于之前一个程序的所有功能统一管理,微服务以通信为代价,换取了扩展,部署等等好处。在云上应该很方便。

技能的反面

介绍

这篇博客是技能的反面 - 魔方和模仿以及The Psychology of Cross Country的读后感。

这些文章通过对学习本身的一些思考,让我们能够更加高效的去获取技能。我想做的就是去获取这些文章后面的“渔”,而不是简简单单地得到“鱼”

整篇文章我将以游泳为例来说明,就像Bill Buxton以骑马为例一样。

技能与解决问题有什么区别

技能的定义就是:即Automatic,可以无意识的自动去做某件事。我现在可以浮在水面而不用去考虑我是怎么实现的。

解决问题:即Attentive Behaviour,是说做某件事的时候需要考虑很多东西。例如我最近在学习自由泳的打腿与划手的配合,在游的过程中,既要考虑腿的节奏,又需要考虑手的动作。

但是浮在水面却不是我需要考虑的,因为这已经是我的一个技能,skilled

练习的魔力

在获取某件技能的时候,一条最重要的法则就是:不断的练习,”do it over and over and over again, then do it some more”,直到它变成一项技能。

干扰

就像我自己提到的大腿的手脚配合,单独的打腿与单独的划手我是可以完成的,但是同时做这两件事情,这两件事就会互相干扰。

总结

为了获取某项技能,把某件事情由解决问题变成一项技能,一共有以下几点:

  • 1.练习,不断的练习。

  • 2.预案。预案的含义是对于突发情况做出准备计划。这样,当突发情况出现时,不至于手足无措。同时,能够减少精力的分散,这样对整个大局的影响最小。

  • 3.速度。只需要降低约10%的速度,就可以解决在过程中遇到的问题,以便更加平稳的进入下一个阶段。换言之,如果,操之过急,速度增加10%,那么就会面临只能解决一半问题的窘境。

应用

这几天结合我的学习,发现还是很有用的。文中提到的论文,我前后一共阅读了3遍。

分析一下,读懂文章,需要

1.理解单词的意思, 2.理解语法, 3.思考作者的思想

这几个技能。然而这些都不是我的技能。第一遍的时候,这几个点互相影响,同时追求速度,因此无法读懂内容。第二遍的时候,无意运用了预案和速度的观点,延长了思考作者思想的时间即读文章的时间,在过程中查词,理解语法,虽然速度慢,但是效果很好,花了30分钟左右,但是真的读懂了。第三遍即不断的练习,以便有更深刻的理解。

如果是听英语呢? 首先就是要多听,多读,即观点1。第二在听和读的过程中遇到不会的单词,句子怎么办,第三就是心别急,查完单词,理解完语法。也就是说把单词的含义以及语法Automatic之后,继续进入下一个阶段的问题解决。

如果是敲代码呢? 首先就是要多写,多做。第二就是如果在过程中遇到突发状况如何处理?应该停下来,解决问题,而不是改一下一遍又一遍的重复测试。第三就是要心别急,拉长解决问题的时间,把当前的问题解决掉,再进入下一个阶段。另外的一个体会就是阶段,如果不懂一个语言的语法,也就无法进行架构的设计。因为在实现的过程中,一直在解决各种神奇的小问题。因此对于底层的问题,先解决掉,变成技能之后,再进入高层次的问题解决。

如果是游泳呢? 首先就是要多练习,分别练习打腿和转身和手的动作。预案就是当同时进行,互相影响的时候,停下来继续练单个动作。速度的观点就是游慢点。哈哈。

我的Git之旅

echo "你好, Git!";

git remote 命令,定义远端的主机。这个远端,可以是另外一台机器,最常用的就是github

使用git remote -v 就可以显示本地当前仓库的远程主机。

         git remote add [name] [url]
example: git remote add origin https://github.com/wuhang-du/leetcode 远端仓库在远端
example: git remote add origin1 ../git-test                          远端仓库在本地

git merge 命令,即合并,主要提一下 –no-ff的使用。

示例当前的分支是:master。develop分支以master分支作为基准分支,增加了新的内容。

调用操作之后使用tig 查看当前的状态:

git merge develop

2017-07-26 11:32 du   [develop] [master] Merge branch 'develop'

结果:master指向了develop,变成了新的master.此时master与 develop 状态相同。

git merge develop --no-ff

2017-07-26 11:32 du   M   [master] Merge branch 'develop'
                        |
2017-07-26 11:31 du   | o [develop] test
                        |
2017-07-26 11:28 du   M   update

结果:master与develop合并,生成了新的master,此状态超前 develop 一次。

git pull 是从远程主机拉取变化,并更新本地的命令。

平常的使用中因为已经设置了默认的主机,会省略一些字段。具体情况具体对待。

还有一些参数,比如设置拉取远程仓库所有的分支等等。

git pull <远程主机名> <远程分支名>:<本地分支名>

git push origin master:master

未完待续:裸库的概念

增加一些参考文章:

深入浅出 Git

Mongo锁机制与索引的理解

对Mongo锁机制以及索引的一些分析与理解

1.锁机制


锁机制保证多个客户端读和写时看到的是同样的数据。

2.2 之前,mongo 实例只有一把全局的读写锁。

2.2 之后,实现了粒度更小,数据库级别的锁。同时,对于一些长时间运行的操作,当满足一些条件时,则放弃锁。 全局锁仍然存在,但是只使用在mongo实例的级别,而且用的很少。我的理解是对全局的admin等操作时,会用到。

我现在使用的版本是mongo 2.6, mongostat会返回一个 locked_db ,这个参数在以后的版本中没有了,之后的版本 是locked ,含义是 the percent of time in a global write lock.

mongo 2.2  locked_db

The percent of time in the per-database context-specific lock. mongostat will report
the database that has spent the most time since the last mongostat call with a write
 lock.

This value represents the amount of time that the listed database spent in a locked 
state combined with the time that the mongod spent in the global lock. Because of 
this, and the sampling method, you may see some values greater than 100%

choosing “global” displays a derived metric that adds the percent of time spent in 
the global lock (typically a very small number) plus the percent of time locked by the
 hottest database at the time of measurement, where hottest means “most locked.” 
Because the data is sampled and combined, it is possible to see values over 100%

ps:今天使用createindex({},{'background':true})时,这个值在10s内维持在160%-170%之间,
同时page faults从0增加,看起来是锁住了内存,query等操作从硬盘读数据,内存一直保持
在locked状态,所以升到了100%以上。

MongoDB uses a readers-writer lock that allows concurrent reads access to a database but gives exclusive access to a single write operation.

The “greediness” of our writes was not only keeping our clients from being able to access data (in any of our collections), but causing additional writes to be delayed

正常的Locked_db并没有什么定论,根据实际的场景决定,如果是 写频繁的业务,有可能普遍超过60%, 但是 读频繁的业务,就不会超过10%。

wiki上读写锁的伪代码:
Begin Read
Lock r.
Increment b.
If b = 1, lock g.
Unlock r.
End Read
Lock r.
Decrement b.
If b = 0, unlock g.
Unlock r.
Begin Write
Lock g.
End Write
Unlock g.

2.索引


关于索引,mongo除了单索引之外,还支持复合索引,还有一个神奇的东西叫索引交集

单索引: 就是简单的单个索引,这种情况下顺序不重要,mongo会reverse.

复合索引:多个field生成一个索引,复合索引有几点需要注意:

  • a,b,c代表字段
  • 1.索引字段的顺序,例如,使用abc生成索引,查询顺序是abc,ab,a的时候都可以用的上,bc,c就不会使用。
  • 2.索引字段的升序(1)降序(-1),如果(a:1,b:-1)生成索引,则(a:1,b:-1),(a:-1,b:1)的查询都可以使用该索引,但是(a:-1,b:-1),(a:1,b:1)就不会使用。

索引交集:是mongo提供的一种机制,可以将单个索引联合起来使用。例如2提到的索引升序降序,如果使用 两个单个的(a:1),(b:1)就可以支持4种组合了。

索引交集与复合索引的比较:复合索引必须注重:fileds的顺序,以及fileds 的顺序逆序的组合。然而,索引交集的使用有一个限制,即如果sort部分,需要使用的索引与query不相关的索引,则无法使用索引交集。

{ qty: 1 }
{ status: 1, ord_date: -1 }
{ status: 1 }
{ ord_date: -1 }

{ qty: 1 }
{ status: 1, ord_date: -1 }
{ status: 1 }
{ ord_date: -1 }
no: 不使用索引交集
no: 没用上索引交集, compound index也不支持
db.orders.find({qty:{$gt:10}}).sort( { status: 1 } )
yes: 用上了compound index, 没用索引交集
db.orders.find({qty:{$gt:10}, status: "A" } ).sort( { ord_date: -1 } )

结论就是需要具体问题具体对待,没有技术银弹

举个今天的例子:

db.push_log.find({
        'msg.sendTime':{$gt:1496654230780.0,$nin:['']},
        'msg.msgType':{$in:['chat','g_card']},
       $or:[{'msg.recvId':{$in:['wh90013524']}},{'msg.userId':'wh90013524'}]
       }).limit(50).sort({'msg.sendTime':-1}).explain(2)

对于这个例子,两种思路:
第一种:重写查询语句,将or提到顶层,这样的话,就相当于两个不同的查询。

db.push_log.find({
    $or:[{
        'msg.msgType':{$in:['chat','g_card']},
        'msg.recvId':{$in:['wh90013524']}
        'msg.sendTime':{$gt:1496654230780.0,$nin:['']},
    },{
        'msg.msgType':{$in:['chat','g_card']},
        'msg.userId':'wh90013524'
        'msg.sendTime':{$gt:1496654230780.0,$nin:['']},
    }]
}).limit(50).sort({'msg.sendTime':-1}).explain(2)

两个变动:1.or裂开。 2.sendTime 后置。

此时建立 msgType,userId,sendTime  和 msgType,recvId,sendTime,或者建
立单索引,调用索引交集的机制。

第二种办法:不改变现有的语句,修改索引。

经测试:
        删掉所有的自建索引,当第一种情况处理,建立time,type,recvid 和 
time,type,userid, 此时使用的是compound index,并没有触发or机制。
        删掉所有的自建索引,建立type,recvid,time 和 type,userid,time ,
此时使用了caluse 机制,索引可以使用。但是对于or的两部分,两种方案,每种
方案下对两个分支使用了同样的索引1或2。
        删掉所有的自建索引,建立recvid 和 userId ,此时是一种方案下,
两个索引同时使用, or机制正常启用。

贴几篇有用的文章:

前两篇是关于锁机制。

Learn About Lock Percentage: Concurrency in MongoDB

mongodb-performance-optimization-with-mms

这几篇是关于索引的讨论。

复合索引

索引交集

MongoDB $or + sort + index. How to avoid sorting in memory?

Cardinal $ins: MongoDB Query Performance over Ranges

Mongodb: Performance impact of $HINT

Go: Context 理解

在看proxy的时候,看到了context这个package.

source 1: Package context

Incoming requests to a server should create a Context, 

and outgoing calls to servers should accept a Context

> * 对于,访问本服务器的Incoming 请求应该建立一个Context.

> * 对于,从本服务器访问其他服务器的Outgoing 请求应该接收Context.

下面的链接中包含了创建一个context的代码。

source 2: Go Concurrency Patterns: Context

WithCancel
WithDeadline
WithTimeout
WithValue

以上函数根据不同的含义,分别返回parent Context的child Context。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    d := time.Now().Add(50 * time.Millisecond)
    ctx, cancel := context.WithDeadline(context.Background(), d)

    // Even though ctx will be expired, it is good practice to call its
    // cancelation function in any case. Failure to do so may keep the
    // context and its parent alive longer than necessary.
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }

}

总结:Context是Go提供的一种机制,可以完成:

场景1:有一个请求,其中包含了若干子请求。当子请求还是与对应的服务器通信时,父请求因为各种各样的原因停止了,

此时,单纯的关闭父请求是没有意义的,Context机制就可以实现关闭父请求的同时,关闭其他的子请求。

场景2:传递 request-scoped 数据。传递动态上下文所需的数据。

示例如下:

package main

import (
    "context"
    "fmt"
    "time"
)

type key int

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    t := key(1)
    ctx_child := context.WithValue(ctx, t, "hello")
    go test(ctx_child)
    cancel()
    time.Sleep(1 * time.Second)
}

func test(ctx context.Context) {
    t := key(1)
    fmt.Printf("%s \n", ctx.Value(t))
    select {
    case <-ctx.Done():
        fmt.Printf("bye bye in child \n")
    }
}

output:
hello
bye bye in child

上面的程序刻意创造了两层结构,验证了parent ctx关闭时,child ctx也关闭,同时也实现了 scope传值。

ctx在使用中,官方推荐的是一层层深入,每一层都需要创建新的ctx,传递给新的go程,同时在本层中关闭。

调用必须是同步的调用。原因是上层的context调用,会关闭下层所有的 context的调用,则下层的各个功能模块还没有完成任务,而中途关闭。

反过来说,当上层出现问题的时候,这个作用刚好可以保证所有的下层都正常关闭。

参考:go-context使用

正确的调用:

  1层
  生成child_ctx
  defer child_cancel()
    2层调用(ctx)
        起go程跑任务
        select {
            case: ctx.Done()  //捕捉上层的异常,或者ctx本身关闭
            case: 任务完成
        }
    2层结束
  1层结束
  
错误的调用:

  1层
    2层调用
        起Go程去跑任务
    2层结束
  1层结束

这时候Go程还在跑,结果还没有出来,就被上层强制关闭了,逗。

从零开始用Go写代理

学习一门语言还是需要理论与实践相结合的,于是决定开始利用Go语言实现Proxy的功能。

1.需求分析


  • 实现http协议的代理。
  • 高性能,高并发。

大概写一个排期吧

6-16 – 6-23 把基本的架子搭起来,实现访问代理,代理返回后台的数据。

6-23 – 6-30 查看理论,进一步分析现有的功能,对第一期的代码重构。

2.理论分析


想到了自己比较常用的几款proxy。查了查,简单分析下:

  1. Nginx, Haproxy 反向代理:

Client <– –> Nginx, Haproxy <– –> Server

  1. ShadowSocks

Client <– –> (local ss client) <– –> (remote ss server) <– –> server


我的目标是实现诸如Nginx与Haproxy的反向代理。

从配置文件中可以看出,将请求导入到代理之后,根据host名以及url来做导向。

刚才想到一点,可以不解析body,根据head就可以确定导向。

3. 实践部分

6.16 完成了初步的框架。一个请求可以转接。

有一个问题:
问题背景: 
server: 监听8081端口,有请求时返回字符串 "hello"
proxy: 监听8080端口,外界的请求到达时,启动go程,go程内去构建新的请求,去8081访问,并写结果。
client: 采用wrk模式,并发1000个。

报错:

go程内client.do函数:
error: Get http://192.168.177.128:8081: dial tcp 192.168.177.128:8081: 
socket: too many open files
proxy本身:
2017/06/16 08:32:42 http: Accept error: accept tcp [::]:8080: accept4: 
too many open files; retrying in 10ms


刚才发现不同的shell中ulimit 不一样。

首先,可以确定:
1. wrk中使用了 1000个 套接字,利用这1000个 Fd 不停的发送请求。
2. proxy 使用了1个监听 8080 端口。 
    http.Client并没有建立请求,所以,实际是 来一个request, Go程就得 调用dial即connect一次
    Client结构体文档说有缓存http请求。这点不明确,需要再查询。
3. server 使用了1个监听8081 端口。当 accept 时,accept一个,内核也得建立新的 open file.
    这些应该是不断增加的,目前不确定是不是及时关掉了。


1. 验证不同的shell中限制不一样。

 之前默认是1024. 

 ulimit -n 分别修改了 proxy 之后,wrk 之后,二者不再显示错误。然而 server 出现了 accept 错误。
    
 也就是通过增大open file数量,可以解决问题,然而上面提到的问题还没有确定。

/******增加ulimit 方法***
/etc/security/limits.conf  增加: du hard nofile 10000  //系统限制
~/.bash_profile  增加:ulimit -n 10000                  //该用户每次登陆shell执行ulimit.
***********************/

/********增加的代码**
不再使用默认的配置文件,Client的配置文件
var tr *http.Transport = &http.Transport{
            Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
            }).Dial,
            DisableKeepAlives : false,
            
            //这个值就是关键,含义是:保持长链接的TCP连接个数。不设置默认为2.
            MaxIdleConnsPerHost : 1000,  
}
*****************/
var client *http.Client = &http.Client{Transport: tr}

结果: 5000并发无压力。
[du@localhost ~]$ wrk -t4 -c5000 -d3s http://127.0.0.1:8080
Running 3s test @ http://127.0.0.1:8080
  4 threads and 5000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   177.49ms   50.95ms 447.81ms   73.99%
    Req/Sec     3.44k     1.84k    7.03k    64.55%
  40019 requests in 3.05s, 5.00MB read
Requests/sec:  13132.85
Transfer/sec:      1.64MB

4.实现

利用了 reverse.go,站在了巨人的肩膀上,只是简单的改写了director.

github: proxy

Go内存泄漏分析及解决办法

文章将展示常见的Goroutine leak以及对应的解决办法。

Talk is cheap, I will show you the code.

各种泄露的展示

第一段代码来源

type Writer struct {
  queue chan []byte
}

func NewWriter() *Writer {
  w := &Writer{
    queue: make(chan []byte, 10),
  }
  go w.process()
  return w
}

func (w *Writer) Write(message []byte) {
  w.queue <- message
}

func (w *Writer) process() {
  for {
    message := <- w.queue
    // do something with message
  }
}

func main() {
  fmt.Println(runtime.NumGoroutine()) 
  test()
  fmt.Println(runtime.NumGoroutine())
}

func test() {
  NewWriter()
}

test中执行的NewWriter消失在了上下文里,Go程则存在于后台,造成了泄露。

第二段代码,做了一些增加

package main
import (
    "fmt"
    "math/rand"
    "time"
)

func queryFromSrc(src string) (ret string) {
    time.Sleep(1000)
    ret = fmt.Sprintf("query done")
    return ret
}

func multiQuery() (ret string) {
    res := make(chan string)
    go func() {
        temp := make(chan int)
        num := <- temp
        fmt.Printf("get num %d \n",num) 
    }()
    go func() {
        res <- queryFromSrc("ns2.dnsserver.com")
    }()
    return "hello"
}

func main() {
    fmt.Println("start multi query:")
    res := multiQuery()
    fmt.Println("res=", res)
}

上述的代码一共有两处导致内存泄露的部分:

第18行:从一个没有输入的管道中阻塞读取数据

第22行:向一个没有接收的管道中写入数据阻塞。

以上都会造成内存泄漏,总结可以得出: > * Go程想从一个通道读数据,但通道没有写入数据。 > * Go程想往一个通道写数据,但是这个通道没有接收方,该Go程将阻塞无法向下执行 > * 综合上述两种情况,如本文的第一段代码,Go程内形成闭环,与外部隔绝。

解决办法

1.增加管道缓存

上述第二段代码中,第22行的问题,如果,15行的代码变成res := make(chan string, 1),则不会写阻塞。最终数据会写入缓存管道,依靠Gc回收。

2.增加读超时处理

针对第18行的问题,展示一种超时处理:

go func() {
    defer done(0)
    temp := make(chan int)
    select {
        case num := <- temp:         //正常的接收
            fmt.Printf("%d \n",num)
        case <- time.After(1) :      //触发了超时
            fmt.Printf("time done stop 0 \n")
    }
}()

3.生产者close生产管道,在消费的Go程内部使用for range形式,遇到close会跳出循环。

4.全局的关闭管道, 可以做错误处理,下文的select, 当在外部因为任何原因关闭 in 时, select中in 会被触发。

package main

import (
    "fmt"
    "time"
    "runtime"
)


func test(in <- chan int, i int) {
    // 模拟的是close 关闭的情况
    for my := range in {        
        fmt.Printf(" %d is shut down \n", my)
    }
    fmt.Printf("out %d is shut down \n", i)
    
    /*
    // 模拟外部关闭的情况,in此时可以看作统一的退出管道。
    select {
        case  <- in:
            fmt.Printf(" %d is shut down \n", i)
            return
    }
    */
}

func main() {
    in := make(chan int)
    for i := 0; i < 10; i++ {
        go test(in, i)
    }
    fmt.Println(runtime.NumGoroutine())
    close(in)
    time.Sleep(10000)
    fmt.Println(runtime.NumGoroutine())
}

Go:使用channel共享内存

Go的口号:不要通过共享内存来通信,而应通过通信来共享内存

Talk is cheap, I will show you the code.

func main() {
    c1 := make(chan int)
    quit := make(chan int)
    test := 0
    a := [...]int{1, 2, 3}
    for _, v := range a {
        go func(v int) {
            for {
                select {
                case my := <-c1:
                    my++
                    fmt.Printf("id: %d now count is %d \n", v, my)
                    c1 <- my
                case <-quit:
                    fmt.Printf("id: %d return \n", v)
                    return
                }
            }
        }(v)
    }
    c1 <- test
    time.Sleep(5000)
    fmt.Printf("we get the count: %d \n", test)
    for _, v := range a {
        quit <- v
    }
    fmt.Printf("all is over \n")
    return
    //panic("hello")
}

如代码所示,3个Go程对test进行计数。使用c1传递消息,使用quit传递退出消息。

这段代码有一个问题。 在调试过程中,出现了以下的错误信息。

id: 1 now count is 495 
id: 2 now count is 496 
id: 3 now count is 497 
id: 1 return 
id: 2 return 
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /home/du/go-learning/test.go:33 +0x1e4

goroutine 7 [chan send]:
main.main.func1(0xc42006a060, 0xc42006a0c0, 0x3)
    /home/du/go-learning/test.go:22 +0x1c7
created by main.main
    /home/du/go-learning/test.go:27 +0x11a
exit status 2

可以看出,goroutine 1,7 分别卡在了发送上。即

13行: c1 <- my
25行: quit <- v

非缓冲的chan必须接收方和发送方都准备好,才能实现通信。 当ID为1,2的Go程退出后,c1没有接收方了,因此死锁。 最后一个quit也没有接收方,因此死锁。

刚开始,主Go程给c1中发送数据test,使整个流程流动起来,最终也要拿出test,终止流程。

    c1 <- test
    time.Sleep(5000)
    test  = <-c1   //增加的代码

最终输出:
    id: 3 now count is 1 
    id: 1 now count is 2 
    id: 3 now count is 3 
    id: 1 now count is 4 
    id: 2 now count is 5 
    we get the count: 5 
    all is over

实效Go编程简析

Effective Go是关于如何写出高效Go程序的文档。

分号


Go中的分号不在源码中出现,由词法分析器自动添加。当然If, For中自己写的。 规则是这样的:若在新行前的最后一个标记为标识符(包括 int 和 float64 这类的单词)、数值或字符串常量之类的基本字面或以下标记之一

break continue fallthrough return ++ -- ) }

so:

if i < f()  // 错!
{           // 该行行首会被增加一个分号
    g()
}

重新声明与重复赋值


Go中的短声明 := 的使用。

f, err := os.Open(name)

(some other code...)

d, err := f.Stat()

在上述的代码段中,err 在第一条语句中被声明,在第二次被重新赋值

以声明的变量v可以再次出现在 := 有几个条件:

  • 本次声明和已声明的err处于同一作用域。
  • 类型与之前的类型相应。
  • 在此次声明中至少有一个新声明的变量,如d.

For与Switch


注意,此处和C语言中的Goto不一样,标识符只能放在For或者Switch外面。 中间不能有其他语句,否则会报错。 执行效果是跳出当前的For或者Switch语句块,本次不会再重复进入。

如下所示,Here1,Here2分别为for和Switch工作,互不影响。

//第一段代码
Here1:
    for v := a[0]; v < 5; v++ {
        fmt.Println("for comming", v)
        if v == 3 {
            fmt.Printf("v==2 %+v \n", a)
            break Here1
        }
    Here2:
        switch {
        case m == 1:
            fmt.Printf("%+v \n", a)
            m = 2
            break Here2
        case m == 2:
            fmt.Printf("we get 2")
        }
        fmt.Printf("end : %+v \n", a)
    }
    return

switch还存在一个利用断言,动态判断接口类型的语法,如下

//第二段代码

    var t interface{}
    t = 8888
    switch v := t.(type) {
    default:
        fmt.Printf("unexpected type %T \n", t) // %T 输出 t 是什么类型
    case bool:
        fmt.Printf("boolean %t\n", t) // t 是 bool 类型
    case int:
        fmt.Printf("integer %d\n", t) // t 是 int 类型
        fmt.Printf("integer %d\n", v) // t 是 int 类型
    case *bool:
        fmt.Printf("pointer to boolean %t\n", *v) // t 是 *bool 类型
    case *int:
        fmt.Printf("pointer to integer %d\n", *v) // t 是 *int 类型
    }

可命名结果形参与Defer


  • Go函数的返回值或结果“形参”可被命名,并作为常规变量使用,就像传入的形参一样。
  • 命名后,一旦该函数开始执行,它们就会被初始化为与其类型相应的零值;
  • 若该函数执行了一条不带实参的 return 语句,则结果形参的当前值将被返回。

当可命名形参与Defer碰到一起,又会有不一样的问题。具体请看Go语言的defer,你真的懂了吗

由官网的例子,Defer函数的实参(如果该函数为方法则还包括接收者)在推迟执行时就会求值, 而不是在调用执行时才求值。所以,可以由这个特性来监控另一个函数的进入和退出。

New与Make


type test struct {             
    one int                    
    two bool                   
    three []int                
} 
  
func main() {                  
    first := new(test)         
    second := make([]test,1)   
    third := new([]bool)       
    fourth := make([]bool,1)   
    fifth := new(bool)
    sixth := make([]bool,1)
    fmt.Printf("%+v  \n%+v \n%+v \n%+v \n%+v \n%+v \n", *first, second, *third, fourth, *fifth, sixth)
}   

结果是:
{one:0 two:false three:[]}  //新声明的类型,除了切片都置零(bool类型的0就是false)。
[{one:0 two:false three:[]}]  //初始化了,0 和 false, 但是内层的切片未初始化。
[]         // 返回的是一个指向nil的空指针
[false]    // 返回的是初始化之后的 bool 切片
false      // 内置类型置零,就是false
[false]    // 返回初始化资源后的 test 切片

首先,明确的第一点:new返回的是指针,make返回的是object本身。

new: 可以对确定类型的一个对象置零,对slice, map, channel等无用。

make: 只能对slice, map, channel操作,初始化。

因此,针对官网的置零与初始化的差别:对于普通的类型,new和make都可以完成初始化,但是对于slice, channel, map这三个“本质上为引用数据类型”,只能使用make.

二维切片


和C语言的多维数组相同的原理,一共有两种初始化的方法:一次申请再重新分配和多次申请。

const (
    x = 2
    y = 4
)

func main() {
    picture := make([][]int, x)     // picture[x][y]。    
    /*
    test := make([]int,x * y)
    for i:= range picture {
        picture[i], test= test[:y], test[y:] //这招,太神奇。切片的magic.
    }
    */
    for i := range picture {
        picture[i] = make([]int, y)
    }
    fmt.Printf("%+v \n",picture)
}

输出都是:
[[0 0 0 0] [0 0 0 0]]

映射


没有定义相等的数据类型不能用作映射的键。

打印

有一个坑需要注意:

type MyString string

func (m MyString) String() string {
    return fmt.Sprintf("MyString=%s", m) // 错误:会无限递归
}

如果以以 %f 调用了Sprintf,它并不是一种字符串格式:Sprintf 只会在它需要字符串时才调用 String 方法,而 %f 需要一个浮点数值。

枚举器 iota


iota可以做表达式的一部分,该表达式隐式重复,可以表达更复杂的枚举,比C更强大。

const (
    _ = iota  // 忽略第一个0
    x         // 即x = iota
    y         // 即y = iota
)

结果:x = 1, y = 2

指针与值


  • 值方法可通过指针和值调用,执行效果都是值方法。

  • 指针方法只能通过指针来调用。如果值是可寻址的,则编译器自动将值调用指针方法转为地址调用。执行效果是指针方法。

总的来说,编译器会帮助coder保持值调用或者指针调用的效果。

断言与接口类型


type Stringer interface {
    String() string
}

var value interface{} // 调用者提供的值。
switch str := value.(type) {
case string:
    return str
case Stringer:
    return str.String()
}

//等价于以下的代码
if str, ok := value.(string); ok {   //不采用这种ok形式的话,如果断言失败,程序会崩。
    return str
} else if str, ok := value.(Stringer); ok {
    return str.String()
}

接口内嵌与结构体继承


Go中接口与继承的选择这部分的主要内容来自这里。

结论是:“为了不重写代码,必须使用继承。而为了存储抽象类型,调用具体类型的方法,必须用接口。”

const (
    PARENT      = iota
    CHILD
    GRANDCHILD
)

type object  int 

type ObjectInterface interface {       // object类的接口
    ObjectClass() int 
}

func (test *object) add() int {
        a := int(*test)
        a++ 
        *test = object(a)
        return a
}

func (test object) ObjectClass() int { // object类的方法
    return PARENT
}

type child struct {                    // child 继承 object
    object
}

func (my child) ObjectClass() int {    // child 实现Object 接口
    return CHILD
}

func (my child) add2() int {           // child 类的方法
    a := int(my.object)
    a++
    return a
}

type ChildInterface interface {        // child 接口
    ObjectInterface
    ChildClass() int
}

type grandchild struct {               // grandchild 继承 child
    child
}

func (my grandchild) ObjectClass()int{ // grand 继承 child 之后,是不用实现ObjectClass()
    return GRANDCHILD                  // 但是会返回child。因此单独实现。              
}

func (my grandchild) ChildClass()int { // 实现了child 接口。
    return GRANDCHILD
}

func main() {
    first   := object(0)
    second  := child{first}
    third   := grandchild{second}

    fmt.Printf("add: %d \n",third.add())
    fmt.Printf("add: %d \n",third.add())
    fmt.Printf("add: %d \n",third.add2())
    fmt.Printf("ObjectClass: %d \n",third.ObjectClass())

    if _, ok := interface{}(third).(ObjectInterface); ok {
        fmt.Printf("first have \n")
    }
    if _, ok := interface{}(third).(ChildInterface); ok {
        fmt.Printf("second have \n")
    }
}

输出是:
add: 1 
add: 2 
add: 3 
ObjectClass: 2 
first have 
second have
  • 未完待续

About

喜欢C语言,喜欢Unix以及Unix的文化

正在学习Go.

希望自己能越来越厉害。

2017-5-12 0:38 于北京

wrk压测工具简析

Wrk是一款高性能的基于Linux平台的压测工具。

1.基础功能


命令是:

wrk -t12 -c400 -d30s http://127.0.0.1:8080/index.html

-t : 线程数
-c : http连接总数。
-d : 测试时长。
-s : lua脚本参数

2.简单分析


wrk主要是

  • 基于redis的ae模块, 提供异步框架。
  • nginx的httpparser, 提供http的请求解析。
  • lua脚本,利用Lua的创建更复杂的测试用例。

3.简单使用


Http压测工具wrk使用指南

wrk – 小巧轻盈的 http 性能测试工具.

4.Update:2017-10-12


又重新捡了起来,附上一个比较完整的脚本。


-- 全局环境内执行的代码
local counter = 1 --全局的counter
local threads = {} --全局的threads
dofile("./token.lua")
wrk.method = "GET" --修改了全局的wrk参数。
 

function setup(thread)  --setup函数,各个thread分开执行前,执行。
    thread:set("id", counter) 
    table.insert(threads, thread) 
    counter = counter + 1 
end 


-- 各个thread内执行的函数 
function init(args) --每一个thread 各自一个独立的 lua环境。
    requests  = 0 
    responses = 0 
  
    local msg = "thread %d created"  
    print(msg:format(id))
    path = "/redeem/mobile/Controller/Handler.ashx?token="  
end 
  
function request() 
    requests = requests + 1
    tokentmp = getToken()
    tmp = path .. tokentmp
    -- function wrk.format(method, path, headers, body) 会merge 默认的值。
    return wrk.format(nil, tmp) 
end 
  
function response(status, headers, body) 
    responses = responses + 1 
    print(body) 
end 
 
 -- 全局环境中执行的函数
function done(summary, latency, requests) 
   for _, thread in ipairs(threads) do 
      local id        = thread:get("id") 
      local requests  = thread:get("requests") 
      local responses = thread:get("responses") 
      local msg = "thread %d made %d requests and got %d responses" 
      print(msg:format(id, requests, responses)) 
   end 
end

开始技术博客

第一版:2017-5-1

我的第一篇 github 博客, 本博客使用 jekyll + bootstrap 搭建!

感谢 github 提供的 Github Pages 功能!

第二版:2017-7-27

使用Hugo搭建,基于hyde

借鉴了rakyll的css.

独立添加了tags的功能。

echo "你好, Github Pages!";