fengbeihong


  • Home

  • Archives

从虚拟机使用角度看docker容器实现

Posted on 2019-07-19

本文旨在从虚拟机使用的角度对照着理解docker容器的实现。
容器化技术也是属于虚拟化的一种,虚拟机的虚拟化不管技术如何实现,使用者角度体验都是类似。

namespace和cgroups

namespace的隔离机制是虚拟化的基础,Linux在chroot的基础上提供了对UTS、IPC、mount、PID、network、User等隔离机制。而cgroups在隔离的基础上,提供了资源的限制和控制功能。
这些基础技术的文章已经有很多,推荐看下左耳朵耗子的,本文只希望从稍微大点的层面把这些概念串起来。


当我们希望虚拟出一个貌似独立的操作系统环境时,都需要哪方面的虚拟化,以及在容器里如何实现的?

独立的用户系统

登录该操作系统需要一个用户,那么不管是root还是非root,都需要一套独立的用户系统。
User namespace可以提供该功能,容器内的用户和外部不会冲突。

独立的主机名

UTS namespace可以提供该隔离功能。

独立的进程树

PID namespace可以提供该隔离功能。

IPC通信

IPC namespace可以提供该隔离功能。

独立的文件系统,可以从镜像启动

典型的Linux文件系统由bootfs和rootfs组成,bootfs(boot file system)主要包含 bootloader和kernel,bootloader主要是引导加载kernel,当kernel被加载到内存中后 bootfs就被umount了。rootfs (root file system) 包含的就是典型 Linux 系统中的/dev,/proc,/bin,/etc等标准目录和文件。

Mount namespace可以提供该隔离功能(类似于chroot命令),而在docker里用这个功能挂载rootfs,并且和AUFS(也可以是其他的Union 文件系统)一起使用。各种容器里运行的进程不一定需要这么多系统目录,在docker里把原本linux文件系统里的rootfs分为多份,使用AUFS的union mount技术一层一层组合起来,组成read only层,最上层则有write权限,可以由容器中允许的进程修改。

linux镜像的内容和其文件系统一致,有bootfs和rootfs,而docker有自己的启动方式,所以上面的readonly层和readwrite层组合起来就是最终的docker镜像。

独立的网络

和宿主机隔离开,有自己的网络设备和网段。

Network namespace可以提供该隔离功能,网络设备在两个namespace之间是互相看不到的。但也只是隔离,虚拟机中的网络设备模拟正常的linux系统,有许多默认的比如lo设备,还有自己的网卡设备,docker都可以模拟出来。

docker有多种网络模式,这里只用下图说明下bridge网络模式。

docker的namespace内创建一个网卡设备,宿主机创建一个bridge设备,通过veth pair讲两者连接起来,则成功将两个network namespace的设备连通。

网络也有自己的安全机制,可以控制数据的进出

有了上面的模拟网络的基础,则可以通过宿主机的iptables来控制网络数据。

资源限制

上述隔离算是对环境的隔离,以及对linux环境的模拟,在实际使用中还要有对资源的隔离,这时cgroups派上用场,可以对CPU、memory等资源的使用加以限制或控制。


上面讲了可以用来做容器虚拟化的各种系统功能,但是这一切功能在具体实现中都需要一个起点,这个起点就是开启一个新的进程。

各种namespace的使用在具体代码实现中,都是fork一个新的进程过程中,使用不同的系统调用或选项参数组合,来决定这个新的进程进行哪些环境的隔离。比如说network namespace,可以控制容器启动时可以和宿主机处于同一个network namespace下,这种就是docker的host网络模式,可以宿主机直连。

启动的新进程在docker容器里就是dockerinit进程,该进程做一些初始化工作,使用cgroups对资源使用做限制,或者创建容器的network namespace下的网络设备等等,最后则会执行entrypoint或者用户定义的CMD应用程序(Dockerfile中的CMD)。

如下图,所有的这些工作都是在同一个进程下进行的,一般情况下,一个容器就是一个进程。

参考文章
《Docker源码分析》
https://coolshell.cn/articles/17010.html
https://coolshell.cn/articles/17029.html
https://coolshell.cn/articles/17049.html

数据库锁

Posted on 2019-01-04 | Edited on 2019-07-19

数据库锁

全局锁

1
flush table with read lock

全局锁:对整个数据库实例加锁,MySQL提供一个加全局锁的方法:Flush tables with read lock(FTWRL),这条语句可以让数据库处于只读状态,所有的写操作将被堵塞;全局锁的典型应用场景就是做全库的逻辑备份,即是把整个库 数据查询出来,保存为文本;

表级锁

  • 表锁 locktables table_name read/write
  • 元数据锁 meta data link, MDL

表级锁:一种是表锁,另一种是元数据锁(meta data lock,MDL锁);表锁,在修改表、或者显示加锁会出现,元数据锁,在事务开始时候加上,是为了保证在并发的情况下,结构变更的一致性,也就是说,事务开始时候,要去做改表操作,必须等待这个事务完成,会堵塞DDL操作。

行锁 record lock

对包含索引列做操作时会产生,在并发的系统下,行锁是不可缺少的,但是行锁在提高并发性的同时,又会带来一些性能问题
行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高


如何避免?减少锁争用,程序中尽可能使用自动提交,短事务

  • Innodb_row_lock_current_waits:当前等待锁的数量
  • Innodb_row_lock_time:系统启动到现在、锁定的总时间长度
  • Innodb_row_lock_time_avg:每次平均锁定的时间 I
  • nnodb_row_lock_time_max:最长一次锁定时间
  • Innodb_row_lock_waits:系统启动到现在、总共锁定次数
减少锁争用
  • 尽可能让所有的数据检索都通过索引来完成,避免Innodb因为无法通过索引键加锁而升级为表级锁定
  • 合理设计索引,让Innodb在索引键上面加锁尽可能准确,缩小锁定范围,避免造成不必要的锁定而影响其他Query的执行
  • 尽可能减少基于范围的数据检索过滤条件,避免间隙锁带来的负面影响而锁定了不该锁定的记录
  • 尽量控制事务的大小,减少锁定的资源量和锁定时间长度
  • 在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少MySQL因为实现事务隔离级别所带来的附加成本
死锁避免
  • 同类的业务模块中,尽可能按照相同的访问顺序来访问,防止产生死锁
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率

对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率

GAP锁 间隙锁

特点:锁定一个范围,但不包括记录本身
作用:为了防止同一事务的两次当前读,出现幻读的情况

在可重复读隔离级别下,为了防止幻读,会对某行数据的前后添加间隙锁,不包含某行数据本身

行锁和间隙锁合成为next-key lock

数据库日常使用优化

Posted on 2019-01-04 | Edited on 2019-07-19

大表操作

在数据量特别少的时候,如何操作都不会影响数据库的使用。但是当表的数据量特别大时,就要万分注意,任何操作可能对数据库服务造成的阻塞都要考虑到。

删除大表

如果想删除一张有十几亿条数据的大表,不能直接drop table,因为有可能内存缓冲池里也有该表数据,删除表会去遍历内存缓冲区。会导致内存缓冲区被锁住,以至于整个服务hang住,所有读写都会被堵塞。并且从磁盘中删除几百G的大文件,磁盘IO也会瞬间打满,会对性能造成很大影响。比较安全的操作就是批量的删除数据,最后再drop回收空间。

在线DDL

线上环境的DDL一般会用到一些专用的工具,避免直接执行造成服务使用异常。

Mysql5.5的DDL工具

copy数据的过长需要耗费额外的存储空间,并且执行过程耗时较长。copy数据过程中有锁,堵塞了写操作

mysql5.6、5.7原生Online-DDL

MYSQL 5.6之后,支持更多的ALTER TABLE类型避免copy data操作,并且支持在DDL的过程中不阻塞DML操作,即实现ONLINE特性。

1
ALTER TABLE  TABLE_NAME  ADD COLUMN _new_column  ALGORITHM = inplace, LOCK = default;

ALGORITHM选项

  • INPLACE
  • COPY
  • DEFAULT

LOCK选项

  • NONE
  • SHARED
  • DEFAULT
  • EXCLUISIVE

  • 仍然存在排他锁,有锁等待的风险。
  • 跟5.6一样,增量日志大小是有限制的,由innodb_online_alter_log_max_size参数决定大小
  • 有可能造成主从延迟
  • 无法暂停,只能中断
pt-online-schema-change

整个过程,只有第五步的时候会进行锁表,阻塞服务,时间极短

Online DDL与PT-OSC对比

  • 1.原表不能存在触发器。
  • 2.原表必须存在主键 PRIMARY KEY 或者 UNIQUE KEY
  • 3.外键的处理需要指定 alter-foreign-keys-method 参数,存在风险
  • 4.在 pt-osc 的执行过程中,如果有对主键的更新操作则会出现重复的数据

k8s里的pause镜像

Posted on 2018-05-07

在kubernetes的Pod启动时,会有一个叫做pause的容器先启动,然后才会轮到有真正服务的pod容器启动。这个pod的作用是什么呢?

pause容器的Dockerfile的内容很少

1
2
3
4
FROM scratch
ARG ARCH
ADD bin/pause-${ARCH} /pause
ENTRYPOINT ["/pause"]

可以看到,这里就启动了一个pause程序,再来看下pause程序

https://github.com/kubernetes/kubernetes/blob/master/build/pause/pause.c

只看main函数里的几行代码

1
2
3
4
5
6
7
8
9
10
if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
.sa_flags = SA_NOCLDSTOP}, NULL) < 0)
return 3;

for (;;)
pause();

sigaction函数指定本程序在接收到SIGINT, SIGTERM, SIGCHLD信号之后做什么处理。
设置完后就会通过pause()暂停本进程,一直等待信号,而信号到来后仍然继续循环等待,所以该进程只能通过接收信号后,从信号处理函数里退出了。

信号处理函数就sigdown和sigreap两个函数。

1
2
3
4
5
6
7
8
9
static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}

static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}

结合上面的代码可以看到,进程终止的信号SIGINT, SIGTERM,就是调用psignal输出了信号信息,然后就退出了。集合main函数里的逻辑,pause进程只能从这两个信号退出。

SIGCHLD信号的处理比较特殊。

1
2
while (waitpid(-1, NULL, WNOHANG) > 0)
;

这段代码,一般用父进程有多个子进程时使用,让父进程等待子进程结束。这样做可以防止产生僵尸进程。

1
2
进程可以使用fork和exec syscalls启动其他进程。当启动了其他进程,新进程的父进程就是调用fork syscall的进程。fork用于启动正在运行的进程的另一个副本,而exec则用于启动不同的进程。每个进程在OS进程表中都有一个条目。这将记录有关进程的状态和退出代码。当子进程运行完成,它的进程表条目仍然将保留直到父进程使用wait syscall检索其退出代码将其退出。这被称为“收割”僵尸进程。
僵尸进程是已停止运行但进程表条目仍然存在的进程,因为父进程尚未通过wait syscall进行检索。从技术层面来说,终止的每个进程都算是一个僵尸进程,尽管只是在很短的时间内发生的,但只要不终止他们就可以存活更久。

总结下,pause进程启动后,也就干了两件事

  • 重复循环等待信号,只有SIGTERM和SIGINT时才会退出进程
  • 负责回收僵尸子进程

在Kubernetes中,pause容器的功能会有所扩展

  • pause容器在创建pod时第一个启动,创建的命名空间作为基础和pod中的其他容器共享
  • pause容器在PID namespace中是PID为1的init进程,也是Pod中其他容器的父进程,负责回收pod内的僵尸子进程

如果不使用pause容器呢,一个是命名空间并不好统一管理,一个是容器将会只能自己收集僵尸进程本身,这对内存会是一个隐患。

参考文章:
https://kubernetes.io/docs/concepts/workloads/pods/pod/
http://dockone.io/article/2785
http://dockone.io/article/2682

goim解析-数据结构

Posted on 2018-05-07

goim中的数据结构主要存在于需要保存状态的节点,除了logic是无状态外,其他的节点comet、router、job都会有数据结构来保持信息。

comet

comet服务和客户端保持长连接,存在最多最复杂的数据结构

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// DefaultServer是全局变量,每启动一个comet服务就会有一个Server对象
// Server主要保存了多个bucket和
var DefaultServer *Server
type Server struct {
Buckets []*Bucket // 一个server会有多个bucket
bucketIdx uint32 // 其实是保存server对象中bucket的数量,方便进行hash计算索引
round *Round // 一个server会有一个round
operator Operator
Options ServerOptions // options保存一些数组初始化时的数量,暂时忽略
}

// --- Round ---

// round用来保存tcp相关的读写缓冲池,定时器对象等
// readers,writers,timers数组的个数是在服务器启动时指定的,之后无法更改。
// 每次获取时会计算索引,轮询获取Pool或Timer对象,而Pool或Timer里会维护空闲链表
//
type Round struct {
readers []bytes.Pool // 读写缓冲区都使用Pool结构体,初始化时创建固定数量的Pool
writers []bytes.Pool
timers []time.Timer // 定时器使用Timer作为结构体,初始化时创建固定数量的Timer
options RoundOptions
readerIdx int // 下面3个都是代表数组的长度,也是轮询方式计算索引获取读写缓冲池或定时器对象
writerIdx int
timerIdx int
}

// tcp内存缓冲池,tcp读写操作都会用到这个自定义缓冲区
// 具体tcp连接分配到哪个Pool,是每次递增索引,轮询方式获取
type Pool struct {
lock sync.Mutex
free *Buffer // 这里放的都是空闲的Buffer,tcp连接使用时会从free链表取出Buffer,绑定成该tcp连接的用户态缓冲。tcp连接关闭,则会把Buffer放回到free链表。如果free链表没有可使用的缓冲区,就会grow,创建num个新的Buffer。
max int // 每次Pool grow时,会先申请max大小字节数组,然后分配给这个num个Buffer
num int // 每次Pool grow时,创建num个Buffer
size int // 每个Buffer的大小(字节个数)
}

// 定时器对象,TimerData是具体定时器信息,signal是系统定时器
// 看实现是一个时间轮,心跳时间是 infiniteDuration = itime.Duration(1<<63 - 1)
type Timer struct {
lock sync.Mutex
free *TimerData // 空闲的TimerData对象链表,操作和Pool的free链表类似
timers []*TimerData // 在时间轮中等待的TimerData,每次心跳都会检查,然后调用回调
signal *itime.Timer // 系统Timer对象,会对signal设置固定的定时时间,每次超时就会操作一次timers数组
num int
}

// 定时数据
type TimerData struct {
Key string
expire itime.Time // 超时时间
fn func() // 超时回调函数
index int
next *TimerData
}

// --- Bucket ---

// 一个Bucket对象保存多个Channel,每个tcp连接对应一个Channel,然后根据hash计算分配到某个bucket上,然后这个tcp连接的Channel和bucket绑定。
// 这里涉及到sub key概念,一个tcp连接对应一个sub key,一个用户可以有多个sub key
type Bucket struct {
cLock sync.RWMutex // protect the channels for chs
chs map[string]*Channel // map sub key to a channel
boptions BucketOptions
rooms map[int32]*Room // map roomid to room
routines []chan *proto.BoardcastRoomArg // room广播,包含多个chan的数组,在服务启动时就会开启同等数量的goroutine,监听chan,每次有广播请求时,会轮询计算出一个goroutine对应的chan,触发信号。
routinesNum uint64 // 用来轮询计算索引
}

// 房间,一个房间可以有多个Channel
type Room struct {
id int32
rLock sync.RWMutex
next *Channel // 通过链表形式保存Channel地址
drop bool // 表示该room是否所有Channel都退出了
Online int // dirty read is ok
}

// Channel对应一个TCP连接,关联着读写缓冲区,以及一个tcp连接的数据队列Ring
// 用户态缓冲区主要用来处理封包粘包问题
// Channel used by message pusher send msg to write goroutine.
type Channel struct {
RoomId int32
CliProto Ring // 和Ring名称一样,是一个环形数据结构,保存着一组客户端tcp发送来的数据。
signal chan *proto.Proto // 通过signal这个chan来向tcp连接写数据
Writer bufio.Writer // 缓冲区,当TCP连接到来,Channel创建时,从Round中获取用户态缓冲区
Reader bufio.Reader // 缓冲区,同上
Next *Channel // 双向链表
Prev *Channel
}

// 环形数据结构,保存着一组proto.Proto,也就是按照二进制协议解析完成后的对象,这个数组的数量是固定的。
// Ring.data的数组元素个数固定,如果某次写入不进去,就会阻塞,不会增加Ring.data的大小
type Ring struct {
// read
rp uint64
num uint64
mask uint64
// TODO split cacheline, many cpu cache line size is 64
// pad [40]byte
// write
wp uint64
data []proto.Proto
}

router

router是goim里专门负责保存数据的服务,对其他节点提供了rpc调用,主要被logic和job调用。这个服务也可以被redis等替代,因为基本就是在读写一些内存数据,没有额外逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// router也有一个bucket结构体,不过和comet的bucket保存的信息不同,这里主要保存用户、tcp连接、房间等对象的关联信息。
type Bucket struct {
bLock sync.RWMutex
server int // session server map init num
session int // bucket session init num
sessions map[int64]*Session // userid->sessions,一个用户一个session
roomCounter map[int32]int32 // roomid->count,某个room里的channel总数量
serverCounter map[int32]int32 // server->count,某个server里的channel总数量,这个server指的是某个具体的comet节点的server id
userServerCounter map[int32]map[int64]int32 // serverid->userid->count,某个server里的某个用户的channel总数
cleaner *Cleaner // bucket map cleaner
}

// 一个用户对应一个session
type Session struct {
seq int32 // seq代表一个用户的新的连接的序列号,从1开始,不断递增,userid+seq就是subkey
servers map[int32]int32 // seq:server,保存某个连接在哪个comet节点上这样的关联关系,因为需要知道这个关联关系,才可以发送通知时,找到具体的comet节点,然后进行tcp写操作
rooms map[int32]map[int32]int32 // roomid:seq:server with specified room id,一个用户所在的房间信息
}

job

job服务主要工作就是连接上kafka,接收kafka的消息,然后job调用comet的rpc服务,comet则把请求消息写入到tcp连接,客户端则会接收到通知消息。

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
// job服务主要工作就是从router上同步channel和commet节点的关联关系,然后当从kafka收到发送通知的请求时,就要调用指定comet节点的rpc服务,进行对应tcp写操作。
// 下面的多个routines都是数组,数组个数相同,job服务启动时会启动这个个数的goroutine,用来接收到kafka消息时,通过chan触发job调用comet的rpc服务的操作
type Comet struct {
serverId int32
rpcClient *xrpc.Clients
pushRoutines []chan *proto.MPushMsgArg // 数组,每个元素又是一个chan链表
broadcastRoutines []chan *proto.BoardcastArg // 同上
roomRoutines []chan *proto.BoardcastRoomArg // 同上
pushRoutinesNum int64
roomRoutinesNum int64
broadcastRoutinesNum int64
options CometOptions
}

// 这个结构体用来定时从router获取信息,更新当前goim系统里的客户端连接服务端的状态
type RoomBucket struct {
roomNum int
rooms map[int32]*Room
rwLock sync.RWMutex
options RoomOptions
round *Round
}

// 定时器,不再赘述
// Ronnd userd for connection round-robin get a timer for split big lock.
type Round struct {
timers []time.Timer
options RoundOptions
timerIdx int
}

goim解析-架构图

Posted on 2018-05-07

goim是用来搭建im系统的一套开源服务,主要包含comet、logic、router和job四个服务,然后也需要和消息队列服务kafka一起使用。

下图是四个程序启动时开启的不同协议的服务

  • comet是直接和客户端保持长连接的有状态服务,启动tcp和websocket服务。rpc是提供给job服务调用的。
  • logic是无状态服务,这里放置主要的业务逻辑。rpc服务供给comet调用。http是提供给其他系统模块调用,有其他业务需要给im连接用户发消息通知时,就可以调用logic的http来发送。
  • router是用来保存状态的,主要是在部署多个comet节点时,客户端最终和哪个节点建立了长连接,这样的关联关系。rpc服务提供给logic和job调用。作为一个纯内存存取操作的服务,也可以替换成redis之类的应用。
  • job是用来负责具体推送消息的服务。当logic的rpc被调用后,会执行一些业务逻辑,如果需要发送通知,就会把消息发送给kafka。job监听kafka,接收到kafka的消息后,job就会调用comet的rpc服务,把消息写到对应客户端的tcp连接里。

如何使用swagger整合api

Posted on 2018-01-17

swagger-ui的使用

  • 首先有个swagger-ui这个项目,可以根据swagger.json文件的内容,以一种非常漂亮的页面风格显示api信息。
  • 可以直接下载swagger-ui的项目代码,在dist目录下有已经用npm编译好的html页面,直接打开index.html就可以看到。
  • 在地址栏里会是一个默认的值http://petstore.swagger.io/v2/swagger.json,这个值是获取的一个server下的某个路径下的swagger.json文件,所以如果你想自己编写一个swagger.json文件,也必须放在一个服务下跑,才可以正常显示api。

如何编写swagger.json

  • swagger.json有自己的格式,我们写代码时不可能写一个api就修改下swagger.json文件,所以需要一种自动生成swagger.json的工具。
  • 一般是借鉴其他语言里的形式,在函数上以特殊规则编写注释,这样就可以使用工具统一生成swagger.json文件
  • 最早注释的形式是在beego里自动集成的,后来https://github.com/yvasiyarov/swagger借鉴beego编写了一个工具,根据注释可以生成各种格式的文档,再后来vmware赞助了一个项目https://github.com/go-swagger/go-swagger,就是借鉴了yvasiyarov的思路,功能更加完善。

如何使用go-swagger

假设已经有一些编写好的go代码文件,直接在目录下执行

1
swagger generate spec -o ./swagger.json

有了swagger.json文件之后,可以放到项目里保存。如果想查看api信息,可以执行以下命令启动服务。

参照openshift项目,只保存json文件就可以。如果想在web服务的指定端口直接可以访问API文档,那另说

1
swagger serve swagger.json

默认自动启动一个OpenAPI风格的API文档

1
http://localhost:61593/docs

而此时swagger.json文件的服务器地址是

1
http://localhost:61593/swagger.json

需要把这个地址放到swagger-ui里,就可以显示swagger-ui风格的API文档

如何编写api注释

下面是一个例子,详细的可以参考goswagger的文档https://goswagger.io

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
// swagger:operation GET /v1/auth/domain/get domainController getDomainInfo
//
// If the request is successful, will return domain information that the user be able to access.
//
// The system will check user permissions.
// So,you must first login system,and then you can send the request.
//
// You must pass in two arguments, first is offset ,second is limit.
//
// ---
// produces:
// - application/json
// - application/xml
// - text/xml
// - text/html
// parameters:
// - name: offset
// in: query
// description: start row number
// required: true
// type: integer
// format: int32
// - name: limit
// in: query
// description: maximum number of results to return
// required: true
// type: integer
// format: int32
// responses:
// '200':
// description: success
// '403':
// description: Insufficient permissions
// '421':
// description: get domain information failed.

k8s创建pod的流程

Posted on 2018-01-07 | Edited on 2019-07-19

1、用户执行kubectl命令创建pod,根据kubeconfig的配置像kube-apiserver发送创建请求。
2、kube-apiserver存储pod数据到etcd,然后返回。剩下的操作异步执行。
3、kube-scheduler通过kube-apiserver查看未绑定的pod,尝试为pod分配主机。
4、kube-scheduler的调度分为两步:过滤掉不符合要求的主机,为符合要求的主机计算权重。权重的计算设计到一些整体优化策略,比如replicas分配到不同主机,使用负载较低的主机等。
5、kube-scheduler选择权重最高的主机,进行binding操作,结果存储到etcd中。kube-scheduler会调用kube-apiserver在etcd创建boundpod对象,描述在一个工作节点上绑定运行的所有pod信息。
6、kubelet服务定时和etcd同步boundpod信息,发现一个新的boundpod对象没有在本节点工作,则调用Docker来创建pod。

Swift的相关问题

Posted on 2018-01-07

swift配置时,一般会在系统里单独挂载一个目录,然后当修改配置后,有时候会在系统启动时挂载磁盘时出现错误。

1
Continue to wait; or Press S to skip mounting or M for manual recovery

在配置swift时都会配置/etc/fstab这个文件,会让系统启动时自动挂载设备。
想要跳过这个错误,也可以在该配置文件中加参数nobootwait

1
UUID=1234-5678 /osshare vfat utf8,auto,rw,user,nobootwait 0 0

具体的可以man fstab来查看相关信息

1
The  mountall(8) program that mounts filesystem during boot also recognises additional options that the ordinary  mount(8) tool does not. These are:  bootwait which can be applied to remote filesystems mounted outside of /usr or /var, without which mountall(8) would not hold up the boot for these; nobootwait which can be applied to non-remote filesystems to explicitly instruct mountall(8) not to hold up the boot for them;

在用devstack安装开发环境后,如果重启,swift服务经常就不能用了。

根据信息查看了目录mount情况

1
2
3
4
5
6
7
8
9
10
stack@magnum:/opt/stack/solum$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 987M 4.0K 987M 1% /dev
tmpfs 200M 544K 200M 1% /run
/dev/dm-0 91G 6.5G 80G 8% /
none 4.0K 0 4.0K 0% /sys/fs/cgroup
none 5.0M 0 5.0M 0% /run/lock
none 1000M 944K 999M 1% /run/shm
none 100M 0 100M 0% /run/user
/dev/sda1 236M 41M 183M 19% /boot

少了一个和swift相关的挂载

1
/dev/loop0      6.0G  873M  5.2G  15% /opt/stack/data/swift/drives/sdb1

找到默认情况下,swift创建的swift.img,然后手动挂载上去

1
mount -t xfs -o loop,noatime,nodiratime,nobarrier,logbufs=8 /opt/stack/data/swift/drives/images/swift.img /opt/stack/data/swift/drives/sdb1/

最后还要把这个加到/etc/fstab里,重启时自动挂载,注意这里也加了上面提到的nobootwait

1
/opt/stack/data/swift/drives/sdb1/ /opt/stack/data/swift/drives/images/swift.img xfs noatime,nodiratime,nobootwait,nobarrier,logbufs=8 0 0

Ironic和magnum中遇到的错误

Posted on 2018-01-07

涉及到的component有nova, ironic, glance, neutron, magnum, swift
基本错误都是在创建虚拟机的过程中发生的

很多openstack的应用场景都是企业内网,一般会有代理服务器和外部网络隔离


用nova boot在bare metal上部署

这里和创建虚拟机类似,在nova里都归属为了hypervisor,ironic-conductor会一直和bare metal交互。
交互分为两步:先把deploy image安装到机器上,然后用deploy image的操作系统把user image写入到机器的硬盘内。
在deploy image启动过程中会出现许多问题。

ironic在mitaka版本以前都只支持flat网络模式,如果创建一个新的节点,会在flat网络中添加一个port。
就如neutron port-list中,多出来一个port,而这个port对应于bare metal的四个网口中的一个。

baremetal的inspection操作之后,会自动创建多个port,服务器一般都有四个网口,那么就创建四个port。
当nova boot时,会随机选中一个port的mac地址和neutron的网络关联起来,所以就有了ironic node和neutron port的一一对应。
但是,实验时有时为了方便,四个网口只插了一跟网线,这在启动过程中就会出问题。

deploy image的挂载没问题,启动过程也不影响,当启动完成后,当bare metal希望和ironic-conductor交互时,网络就连不通了。
为何连不通,因为neutron注册的是某个port,只能和这个port交互,但是bare metal这个port没有插网线,数据出不来,然后就悲剧了。

ironic自动创建的port有四个

1
2
3
4
5
6
7
8
9
stack@Magnum:~/devstack$ ironic port-list
+--------------------------------------+-------------------+
| UUID | Address |
+--------------------------------------+-------------------+
| bc05cbb9-b6fd-4ebc-b860-81f0033c8828 | ec:b1:d7:83:74:43 |
| 34a4e46c-ff1f-4a24-b351-6a9bb1a5f21b | ec:b1:d7:83:74:42 |
| 3e40c8d2-4f7b-430d-85e3-a884ca48acd0 | ec:b1:d7:83:74:41 |
| 0035a7c8-534c-4d41-a8e0-c0271384408c | ec:b1:d7:83:74:40 |
+--------------------------------------+-------------------+

而在ironic conductor服务这边,通过br-ex一直ping不通nova boot的instance分配的ip

1
2
3
stack@Magnum:~/devstack$ ping -I br-ex 172.30.100.104
PING 172.30.100.104 (172.30.100.104) from 172.30.100.10 br-ex: 56(84) bytes of data.
From 172.30.100.10 icmp_seq=1 Destination Host Unreachable

查看neutron的port信息

1
2
3
4
5
6
7
8
9
10
11
stack@Magnum:~/devstack$ neutron port-list
+--------------------------------------+------+-------------------+----------------------------------------------------+
| id | name | mac_address | fixed_ips |
+--------------------------------------+------+-------------------+----------------------------------------------------+
| 3eebcadf-3b22-4208-8960-fed19b2d124b | | ec:b1:d7:83:74:41 | {"subnet_id": |
| | | | "59b992ff-1567-4132-a777-70545c075035", |
| | | | "ip_address": "172.30.100.104"} |
| a0e1fc5f-15bb-4840-98fb-0577422a62f6 | | fa:16:3e:0f:14:91 | {"subnet_id": |
| | | | "59b992ff-1567-4132-a777-70545c075035", |
| | | | "ip_address": "172.30.100.100"} |
+--------------------------------------+------+-------------------+----------------------------------------------------+

可以看到,neutron port-list中新创建的这个port的mac address和ironic port-list中的某个port对应

neutron网络创建port时,是根据ironic node已有的port进行随机创建的,所以删掉没插网线的port,或者把四个网口全插上网线,这个问题就解决了。


ironic node inspection时 clean node failed

在inspection时会自动执行clean node,但是会clean失败(为何失败?)
先避开这个功能,在/etc/ironic/ironic.conf中设置

1
autoclean=False


ironic在deoloy过程中,因为某些原因导致deploy失败,会产生脏数据,ironic node已经绑定instance uuid,但是instance已经被删除

1
Error contacting Ironic server: Node b362d8ce-1fd3-4cff-afac-c12f01fc02d9 is associated with instance 0d1c7ced-3137-46ce-a3ba-beb7b0c490e1. (HTTP 409). Attempt 2 of 2

这个错误有可能是个隐藏BUG,在ironic的deploy image启动完成后,网络不通连接不上ironic conductor的话,timeout之后,instance就失败被删除????,然后就留下来脏数据。
一般通过两种方式清理掉:

正规的就是ironic node-set-maintance true,然后ironic node-delete
另一种不推荐,就是直接进入数据库改数据,把instance uuid置为NULL


magnum bay-create时会出现请求不到外网的错误

magnum本身是和容器结合的模块,在创建时时会有一个请求discovery url的过程,是外网地址,企业内网中要死不死的没有设置代理的话,就会出错。
magnum的github上有说可以在命令行指定代理参数,但是估计是比较新的版本上有的,我在公司机器上试会找不到这个参数。
Openstack官方文档:http://docs.openstack.org/developer/magnum/troubleshooting-guide.html#etcd-service
这个对外网的请求是和etcd service有关,etcd是一个高可用的键值存储系统,主要用于共享配置和服务发现,灵感来源于zookeeper.
在集群中,用于同步同类节点之间的状态,replication等等的功能都得依靠这个。

12

fengbeihong

18 posts
2 tags
© 2019 fengbeihong
Powered by Hexo v3.7.1
|
Theme — NexT.Muse v6.2.0