2018年5月13日星期日

Linux IO调度器


1.Linux IO体系

IO调度器系统(I/O Scheduler)是Linux I/O体系中的一个组件,其所处的体系位置如下图所示:

Linux整体I/O体系可以分为七层,它们分别是

  1. VFS虚拟文件系统:抽象出文件系统接口,其下可以对接ext4,xfs等多种具体的文件系统而上层使用者只需要使用VFS提供的统一接口即可。
  2. 文件系统缓存Page Cache/磁盘缓冲Buffer Cache:通过空闲内存将对文件读写的数据放入内存,以获取更快的IO响应。
  3. 映射层:内核通过映射层来确定数据在物理块设备上的位置。
  4. 通用块层:由于绝大多数情况的I/O操作是跟块设备打交道,所以Linux在此提供了一个类似vfs层的块设备操作抽象层。下层对接各种不同属性的块设备,对上提供统一的Block IO请求标准。
  5. I/O调度层:大多数的块设备都是磁盘设备,所以有必要根据这类设备的特点以及应用特点来设置一些不同的调度器。
  6. 块设备驱动:块设备驱动对外提供高级的设备操作接口。
  7. 物理硬盘:这层就是具体的物理设备。

以一次读操作为例:

  1. 系统调用read()触发VFS函数,参数为文件描述符与偏移量
  2. VFS确定请求的数据是否已经在内存缓冲区中;若数据不在内存中,确定如何执行读 操作。 
  3. 假设内核必须从块设备上读取数据,这样内核就必须确定数据在物理设备上的位置。 这由映射层(Mapping Layer)来完成。 
  4. 内核通过通用块设备层(Generic Block Layer)在块设备上执行读操作,启动I/O操作, 传输请求的数据。 
  5. 在通用块设备层之下是I/O调度层(I/O Scheduler Layer),根据内核的调度策略, 对等待的I/O等待队列排序。 
  6. 最后,块设备驱动(Block Device Driver)通过向磁盘控制器发送相应的命令,执行 真正的数据传输。

本文介绍图中蓝色部分Linux I/O调度层支持的策略以及比较。

2018年5月5日星期六

SMP/MPP/NUMA系统架构了解

本文涉及到如下三种常见服务器架构:

  1. SMP(Symmetric Multi-Processor,对称多处理结构)。单主机架构。
  2. NUMA(Non-Uniform Memory Access,非一致性内存访问)。单主机架构。
  3. MPP(Massive Parallel Processing,海量并行处理结构)。多主机架构。

2018年5月1日星期二

golang disk free采集不准确问题

     工作中接手的golang程序运行一段时间后,出现了采集的主机信息free不准确的问题,golang采集的程序与df看到的结果不一致。追踪源码发现核心的采集逻辑是如下这段:
func getVfsStats(path string) (total uint64, free uint64, avail uint64, inodes uint64, inodesFree uint64, err error) {
var s syscall.Statfs_t
if err = syscall.Statfs(path, &s); err != nil {
return 0, 0, 0, 0, 0, err
}
total = uint64(s.Frsize) * s.Blocks
free = uint64(s.Frsize) * s.Bfree
avail = uint64(s.Frsize) * s.Bavail
inodes = uint64(s.Files)
inodesFree = uint64(s.Ffree)
return total, free, avail, inodes, inodesFree, nil
}
这段逻辑其实沿用的是cadvisor里面的代码,上层使用的时候,用这里的free(uint64(s.Frsize) * s.Bfree)作为返回的磁盘空间可用值free。要解决这个问题,得看看syscall.Statfs这个函数调用到底返回了些什么信息,也即syscall.Statfs_t各项的含义。

2018年4月19日星期四

golang slice函数值传递的“隐患”

    直接上现场,看看你能不能一眼识破“隐患”根因!

    下文函数基本功能是针对一个分片silce的数据,设置将前toCount个数据更新为100以内随机数,如果toCount大于当前的slice总长度,则扩大到toCount大小并填满100以内的随机数。
    代码如下:
package main
import ( "fmt" "math/rand")
func getRand() int {
return rand.Int() % 100
}
func fillInRandom1(ori []int, toCount int) {
fillInRandom2(&ori, toCount)}
func fillInRandom2(ori *[]int, toCount int) {
if toCount <= len(*ori) {
for i:=0; i< toCount; i++ {
(*ori)[i] = getRand() }
} else {
for i:=0; i< len(*ori); i++ {
(*ori)[i] = getRand() }
for i:=0; i< toCount - len(*ori); i++ {
*ori = append(*ori, getRand()) }
return }
}
func printSlice(s []int) {
for _, i := range s {
fmt.Printf("%v ", i) }
fmt.Println()}
func main() {
fmt.Println("slice参数值传递效果:") nums := make([]int, 4, 5) fillInRandom1(nums, 2) printSlice(nums) fillInRandom1(nums, 4) printSlice(nums) fillInRandom1(nums, 8) printSlice(nums)
fmt.Println("slice指针传递效果:") nums = make([]int, 4, 5) fillInRandom2(&nums, 2) printSlice(nums) fillInRandom2(&nums, 4) printSlice(nums) fillInRandom2(&nums, 8) printSlice(nums)
}
输出如下:

golang struct中内部类型embedding Type用法

    Golang通过内部类型或者嵌入类型embedding的方式来实现一种代码的复用以及重定义,类似与Java中的继承和重载。
     这种内部类有时候也被称为匿名属性(anonymous field,形式上,与其他属性相比,就是没有名字~),这里统一用“内部类型”来指代。
    内部类的方式,可以让外部类直接访问和使用内部类中的属性和方法(只要这些语法上是允许访问的!)
    我们结合代码来看看内部类型的用法。

1.同package下内部类

package main
import (
    "fmt"
)
type user struct {
    name string
    email string
}
func (u *user) notify() {
    fmt.Printf("Send email to %s@<%s>\n", u.name, u.email)
}
type admin struct {
    user //设置内部类型
    level string //什么外部类的属性
}
func main() {
    u1 := admin{
         user: user{
            "haha",
            "haha@evertrain.cn",
         },
         //name: "lala",
         level: "high",
    }
    u1.user.notify()  //既可以通过内部类型调用方法。内部类的属性和方法始终可以通过内部类名进行访问
    u1.notify()       //也可以直接通过外部类的对象访问
    fmt.Println(u1.user.name)
    fmt.Println(u1.name)
}
输出是
Send email to haha@<haha@evertrain.cn>
Send email to haha@<haha@evertrain.cn>
haha
haha

2.覆盖内部类的属性和方法

如果在外部类中重新定义name和notify方法,如下:
...同上部分省略...
type admin struct {
    user //设置内部类型
    name string  //外部类新定义
    level string //什么外部类的属性
}
func (a *admin) notify() {
    fmt.Printf("Send admin email to %s@<%s>\n", a.name, a.email)
}
func main() {
    u1 := admin{
         user: user{
            "haha",
            "haha@evertrain.cn",
         },
         name: "lala",
         level: "high",
    }
    u1.user.notify()  //既可以通过内部类型调用方法。内部类的属性和方法始终可以通过内部类名进行访问
    u1.notify()       //也可以直接通过外部类的对象访问
    fmt.Println(u1.user.name)
    fmt.Println(u1.name)
}
则输出
Send email to haha@<haha@evertrain.cn>
Send admin email to lala@<haha@evertrain.cn>
haha
lala

3.使用跨package的内部类

    在考虑跨package的情况,将user定义放到package user中去,由于小写开头的user变成了user包的私有类,外部包无法访问,将其变成公开类User,其余不变(属性和方法仍然为私有)。
admin定义如下:
import (
    "fmt"
    u "./user"
)
type admin struct {
    u.User //设置内部类型
    name string  //外部类新定义
    level string //什么外部类的属性
}
func (a *admin) notify() {
    fmt.Printf("Send admin email to %s\n", a.name)
}
func main() {
    u1 := admin{
         User: u.User{
            "haha",
            "haha@evertrain.cn",
         },
         name: "lala",
         level: "high",
    }
    //u1.User.notify()  //无法访问其私有方法
    u1.notify()       //也可以直接通过外部类的对象访问
    //fmt.Println(u1.User.name) //无法访问其私有属性
    fmt.Println(u1.name)
}
其中u1创建的时候就报错了,类似如下:
implicit assignment of unexported field 'name' in user.User literal
无法通过这种形式来指定赋值私有变量。为User类新加一个公开的NewUser方法
func NewUser(name string, email string) *User {
    return &User{
        name,
        email,
    }
}
然后admin的创建改成:
func main() {
    u1 := admin{
         User: *u.NewUser("haha", "haha@evertrain.cn"),
         name: "lala",
         level: "high",
    }
    //u1.User.notify()  //无法访问其私有方法
    u1.notify()       //也可以直接通过外部类的对象访问
    //fmt.Println(u1.User.name) //无法访问其私有属性
    fmt.Println(u1.name)
}
则输出变成了
Send admin email to lala
lala
可以看到,跨package后私有属性、方法都不再可见,需要将应该公开的属性和方法改成公开才能达到复用的效果。

4.指针Point形式的内部类型与值Value形式的内部类型的差别

admin还可以用这样的方式定义:
type admin struct {
    *u.User //设置内部类型,指针类型
    name string  //外部类新定义
    level string //什么外部类的属性
}
则稍作修改后,返回同样的结果
u1 := admin{
         User: u.NewUser("haha", "haha@evertrain.cn"), //保持指针
         name: "lala",
         level: "high",
    }
指针类型的内部类型,保留了指针Pointer的特点,初始化不指定的话,就是nil,所以必须明确的指定。
Pointer形式下,外部类访问内部类的属性和方法的方式与Value一致。不同的是,Pointer形式下,是根据指针操作,操作的是对象本身。而如果是Value形式的,例如赋值就是变成一份拷贝,再更新属性和原对象就已经无关了。示例如下:
package main
import (
    "fmt"
)
type user struct {
    name string
    email string
}
func (u *user) setName(n string) {
    u.name = n
}
type admin struct {
    user //Value形式内部类
    level string
}
type operator struct {
    *user  //Pointer形式内部类
    level string
}
func main() {
    u1 := user{"haha1","haha1@evertrain.cn"}
    u2 := user{"haha2","haha2@evertrain.cn"}
    a1 := admin{
         user: u1,
         level: "high",
    }
    o1 := operator{
        user: &u2,
        level: "middle",
    }
    a1.setName("new_haha1")
    o1.setName("new_haha2")
    fmt.Println(a1.user.name)
    fmt.Println(a1.name)
    fmt.Println(u1.name) //此处无变化,因为a1中的user是u1的一份拷贝,其上的更新不会作用到u1
    fmt.Println(o1.user.name)
    fmt.Println(o1.name)
    fmt.Println(u2.name)
}
输出:
new_haha1
new_haha1
haha1
new_haha2
new_haha2
new_haha2

参考


  • https://groups.google.com/forum/#!topic/golang-nuts/ctRUq62cgME
  • 《Manning Go in Action》

2018年4月10日星期二

分区与文件系统问题两例


MySQL服务器划盘经常会遇到各种各样的问题,究其原因还是重装主机的时候,盘并没有进行完整彻底的格式化,记录下今天遇到的两例问题.

1)分区删除

为了解决装机时偶尔有分区信息残留导致后续初始化失败,脚本中加入了如下代码进行前置操作,无脑清理:
fdisk ${device} <<EOF    d
    w
EOF
今天某台机器处理时开始报这样的问题:
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): Selected partition 1
Partition 1 is deleted
Command (m for help): The partition table has been altered!
Calling ioctl() to re-read partition table.
WARNING: Re-reading the partition table failed with error 22: Invalid argument.
The kernel still uses the old table. The new table will be used at
the next reboot or after you run partprobe(8) or kpartx(8)
可以看出意思是分区表更新后需要重读,内核信息没有更新。同时也给出了解决方案(很棒!):重启或者运行partprobe或者kpartx
了解了一下partprobe命令,目标很明确,就是用来通知OS分区表了变化。
partprobe ${device} 即能解决设备分区表更新后的信息同步问题。

2)创建文件系统报错

mkfs.ext4 -m 1 /dev/vgdata/volume2
mke2fs 1.43.5 (04-Aug-2017)Discarding device blocks: done                            Creating filesystem with 937500672 4k blocks and 234381312 inodes
Filesystem UUID: 0b7bf05c-92cf-42ea-9028-a053753ea1ed
Superblock backups stored on blocks:         32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,         4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,         102400000, 214990848, 512000000, 550731776, 644972544
Allocating group tables: done                            Writing inode tables: done                            ext2fs_update_bb_inode: Cannot iterate data blocks of an inode containing inline data while setting bad block inode
最后的报错也导致无法进行挂载。报错信息看,应该是块损坏之类的报错,因为是服务器初始化,理论上因为完全是空白盘才对。(/dev/vgdata/volume2是划出一个LV作为MySQL的日志盘。

解决方案:使用ddlv上的头部残留信息归0
dd if=/dev/zero of=/dev/vgdata/volume1 bs=1M count=10
然后继续 mkfs.ext4 -m 1 /dev/vgdata/volume2,成功。

参考:




2018年4月8日星期日

OOM的触发条件与配置

    OOM(Out of Memory)指Linux在无可用内存时的一个最后策略。当系统中可用内存过少时,OOM killer会选择kill某些进程,来释放这些进程所占用的内存。

1.何时触发oom killer

现代内核通常在分配内存时,允许申请的内存量超过实际可分配的free内存,这种技术称为Overcommit,可以认为是一种内存超卖的策略。开启了Overcommit,其实是基于一个普遍的规律——大部分应用并不会将其申请的内存全部用满——来尽量提升内存的利用率,可以允许系统分配出的内存总和超过系统能提供之和(物理内存+Swap空间),但是这种做法也导致OOM的风险。

2018年3月22日星期四

/dev/shm的用途

    工作中遇到在公司内部容器环境下yum报错的情况,报错如下:
Total size: 229 M
Installed size: 1.2 G
Is this ok [y/d/N]: y
Downloading packages:
Running transaction check
Running transaction test

Transaction check error:
  installing package xxxxx.x86_64 needs 274MB on the /dev/shm filesystem
Error Summary
-------------
Disk Requirements:
  At least 274MB more space needed on the /dev/shm filesystem.
报错信息还比较清晰,安装这个软件包需要1.2G,但是/dev/shm空间不够。

看下/dev/shm占用了多少










问题来了
1) /dev/shm作用是什么,能否调整
2) tmpfs是什么文件系统
3) 这里什么决定了使用/dev/shm,问题如何解决

下面逐个解决这些问题。


2018年3月20日星期二

dd命令用法详解

一.dd命令用法

dd的作用是拷贝文件,从指定的来源,指定的方式进行文件创建、拷贝,过程中可以进行各种转换。
dd可以很方便的验证各种IO表现,还可以用来进行整个盘/文件的复制。
dd的拷贝方式是数据块block级别的, 忽略文件系统类型和操作系统。

2018年3月17日星期六

iostat用法详解


iostatCPU以及IO设备/分区信息统计(注意,不是文件系统)的工具,非常常用,可以查看cpu使用情况以及IO设备当前繁忙情况。本文介绍常用方法,以及指标解读。

.用法

iostat [options] [interval [ count ]
interval表示汇报内容的间隔,count表示汇报次数。如果指定了interval不指定count,则无限次输出
  • -c 显示CPU报告
  • -d 显示Disk报告
  • -k 使用KB代替(512B)块数目
  • -m 使用MB代替(512B)块数目
  • -p 包括分区的统计信息。不带则只显示device
  • -t 时间戳输出
  • -x 显示扩展信息
  • -z 忽略无活动的设备
  • -h 格式化,使得更易读
  • -N 显示已注册的设备mapper名,例如使用了lvm2的时候的输出

以下示例基于RedHat 7.2
#iostat -V
sysstat version 10.1.5
(C) Sebastien Godard (sysstat <at> orange.fr)

2018年3月15日星期四

sysbench 1.0用法探索

一.介绍

sysbench很久用过之前0.4的版本,目前已经正式到1.0版本,用法也有了变化,探索一下sysbench的使用场景,方便后续使用。

官方介绍 https://github.com/akopytov/sysbench, sysbench是一个模块化,跨平台,多线程的基准测试工具。