cgroup 资源预留

cgroup 资源预留

背景

在之前的博客中,提到了 cgroup 中如何为自己的服务配置资源限制,比如 CPU,内存等,当时以为在 cgroup.conf 中配置的服务,那么相应绑定的 CPU 就归属于该服务,也就是资源预留,今天发现并不是这样,记录下如何通过 cgroup 做资源预留。

资源预留

在之前提到的博客中关于资源限制是这么配置的:

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
[root@node 20:56:49 ~]$cat /etc/cgconfig.conf
# yiran cgroups configuration

group . {
cpuset {
cpuset.memory_pressure_enabled = "1";
}
}

group yiran {
cpuset {
cpuset.cpus = "0,1,2,3,4,5";
cpuset.mems = "0-1";
cpuset.cpu_exclusive = "1";
cpuset.mem_hardwall = "1";
}
}

group yiran/test {
cpuset {
cpuset.cpus = "0";
cpuset.mems = "0-1";
cpuset.cpu_exclusive = "1";
cpuset.mem_hardwall = "1";
}
}

我为 yiran 这个服务做了限制,其中分配给 yiran/test 的 CPU 是 0,并且配置了 cpuset.cpu_exclusive 选项。注意,这里的 exclusive 的意思并不是说分配到 yiran/test 中的服务独自占用该线程,而是说分配到 yiran/test 中的服务只会占用该线程,不会侵入到其他线程中,也就是通常我们说的 Limit。
可以通过 stress 命令来进行验证:

1
2
3
# 对一台配置好 cgroup 的物理服务器 CPU 加压
[root@node 21:05:37 ~]$stress -c 24
stress: info: [4033] dispatching hogs: 24 cpu, 0 io, 0 vm, 0 hdd

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
# 新开终端,执行 top 命令查看 CPU 情况
[root@node 21:05:37 ~]$top
top - 21:06:35 up 5 days, 9:03, 3 users, load average: 32.58, 19.04, 15.43
Tasks: 449 total, 36 running, 412 sleeping, 0 stopped, 1 zombie
%Cpu0 : 89.0 us, 8.6 sy, 0.0 ni, 2.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 99.7 us, 0.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 90.4 us, 8.9 sy, 0.0 ni, 0.3 id, 0.3 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 95.4 us, 4.6 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 84.4 us, 13.6 sy, 0.3 ni, 1.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 46.5 us, 36.0 sy, 1.7 ni, 15.8 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu6 : 93.4 us, 6.6 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu7 : 94.4 us, 5.6 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu8 : 88.0 us, 12.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu9 : 88.7 us, 11.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu10 : 88.4 us, 10.9 sy, 0.0 ni, 0.3 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
%Cpu11 : 85.4 us, 13.9 sy, 0.7 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu12 : 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu13 : 39.0 us, 12.0 sy, 0.0 ni, 49.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu14 : 92.7 us, 6.9 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
%Cpu15 : 98.7 us, 1.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu16 : 97.0 us, 3.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu17 : 97.0 us, 2.3 sy, 0.0 ni, 0.3 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
%Cpu18 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu19 : 93.7 us, 6.3 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu20 : 88.4 us, 11.6 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu21 : 89.4 us, 10.6 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu22 : 88.1 us, 11.9 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu23 : 90.4 us, 9.6 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st

那么我们如何确保自己的服务有足够的资源运行呢? 比如像 vSphere ESXi 上面可以针对某些虚拟机进行具体的资源预留(CPU,内存,磁盘等)。
其实只要将其他资源也限制在一个 cgroup 中就可以了。

  1. /etc/cgconfig.conf 中创建一个新的 cgroup,将系统上其余的资源分配给该 group,如创建 yiran/others 并分配 3个 线程;
  2. 修改 /etc/cgrules.conf ,将系统其它所有服务分配到步骤1创建的 group 中,如:
    1
    *       cpuset       yiran/others

修改配置完成后,分别重启 cgredcgconfig 使配置生效:

1
2
systemctl restart cgred
systemctl restart cgconfig

此时我们再通过 stress 进行验证

1
2
[root@node 21:09:29 ~]$stress -c 24
stress: info: [12475] dispatching hogs: 24 cpu, 0 io, 0 vm, 0 hdd

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
top - 21:11:58 up 5 days,  9:09,  3 users,  load average: 38.11, 30.37, 21.48
Tasks: 445 total, 38 running, 406 sleeping, 0 stopped, 1 zombie
%Cpu0 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 :100.0 us, 0.0 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu2 : 0.0 us, 0.0 sy, 0.3 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu3 : 0.0 us, 0.3 sy, 0.0 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu4 : 88.1 us, 11.9 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu5 : 81.9 us, 16.1 sy, 1.6 ni, 0.0 id, 0.0 wa, 0.0 hi, 0.3 si, 0.0 st
%Cpu6 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu7 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu8 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu9 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu10 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu11 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu12 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu13 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu14 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu15 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu16 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu17 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu18 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu19 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu20 : 0.0 us, 0.0 sy, 0.3 ni, 99.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu21 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu22 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu23 : 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 13174792+total, 11522948+free, 6439344 used, 10079088 buff/cache
KiB Swap: 0 total, 0 free, 0 used. 12406676+avail Mem

我在 yiran/others 中配置了 3 个线程,那么此时哪怕我执行 stress worker 为 24,也只会在这3个线程中,不会对其他 cgroup 服务产生干扰,从而达到了资源预留的效果。

剑指-Offer(七)

反转链表

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
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None

def reverse_list(list_head):

if not list_head or not list_head.next:
return list_head

reverse_node = None
node_pre = None
node = list_head
while node:
node_next = node.next
if node_next == None:
reverse_node = node
node.next = node_pre
node_pre = node
node = node_next
return reverse_node

node1 = ListNode(10)
node2 = ListNode(11)
node3 = ListNode(13)
node1.next = node2
node2.next = node3

reverse_list(node1)

合并两个排序的链表

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
class ListNode(object):
def __init__(self, x):
self.val = x
self.next = None

def merge_link(head1, head2):
if not head1:
return head2
if not head2:
return head1
if head1.val <= head2.val:
ret = head1
ret.next = merge_link(head1.next, head2)
else:
ret = head2
ret.next = merge_link(head1, head2.next)
return ret


node1 = ListNode(10)
node2 = ListNode(15)
node3 = ListNode(18)
node1.next = node2
node2.next = node3

node4 = ListNode(11)
node5 = ListNode(16)
node6 = ListNode(19)
node4.next = node5
node5.next = node6


a = merge_link(node1, node2)
print a.next.next.val

树的子结构

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

class TreeNode:

def __init__(self, x):
self.val = x
self.left = None
self.right = None


class Solution:

def HasSubtree(self, pRoot1, pRoot2):
result = False
if pRoot1 and pRoot2:
if pRoot1.val == pRoot2.val:
result = self.DoesTree1haveTree2(pRoot1, pRoot2)
if not result:
result = self.HasSubtree(pRoot1.left, pRoot2) or self.HasSubtree(pRoot1.right, pRoot2)
return result

def DoesTree1haveTree2(self, pRoot1, pRoot2):
if not pRoot2:
return True
if not pRoot1:
return False
if pRoot1.val != pRoot2.val:
return False

return self.DoesTree1haveTree2(pRoot1.left, pRoot2.left) and self.DoesTree1haveTree2(pRoot1.right, pRoot2.right)


pRoot1 = TreeNode(8)
pRoot2 = TreeNode(8)
pRoot3 = TreeNode(7)
pRoot4 = TreeNode(9)
pRoot5 = TreeNode(2)
pRoot6 = TreeNode(4)
pRoot7 = TreeNode(7)
pRoot1.left = pRoot2
pRoot1.right = pRoot3
pRoot2.left = pRoot4
pRoot2.right = pRoot5
pRoot5.left = pRoot6
pRoot5.right = pRoot7

pRoot8 = TreeNode(8)
pRoot9 = TreeNode(9)
pRoot10 = TreeNode(2)
pRoot8.left = pRoot9
pRoot8.right = pRoot10

S = Solution()
print(S.HasSubtree(pRoot1, pRoot8))

二叉树镜像

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
class TreeNode:

def __init__(self, x):
self.val = x
self.left = None
self.right = None


class Solution:

def Mirror(self, root):
if not root:
return
if not root.left and not root.right:
return root

pTemp = root.left
root.left = root.right
root.right = pTemp

self.Mirror(root.left)
self.Mirror(root.right)

def Mirror2(self, root):
if not root:
return
stackNode = []
stackNode.append(root)
while len(stackNode) > 0:
nodeNum = len(stackNode) - 1
tree = stackNode[nodeNum]
stackNode.pop()
nodeNum -= 1
if tree.left or tree.right:
tree.left, tree.right = tree.right, tree.left
if tree.left:
stackNode.append(tree.left)
nodeNum += 1
if tree.right:
stackNode.append(tree.right)
nodeNum += 1


pNode1 = TreeNode(8)
pNode2 = TreeNode(6)
pNode3 = TreeNode(10)
pNode4 = TreeNode(5)
pNode5 = TreeNode(7)
pNode6 = TreeNode(9)
pNode7 = TreeNode(11)

pNode1.left = pNode2
pNode1.right = pNode3
pNode2.left = pNode4
pNode2.right = pNode5
pNode3.left = pNode6
pNode3.right = pNode7

S = Solution()
S.Mirror(pNode1)
print(pNode1.right.left.val)

顺时针打印矩阵

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
class Solution:
def printMatrix(self, matrix):
if matrix == None:
return
rows = len(matrix)
columns = len(matrix[0])
start = 0
while rows > start * 2 and columns > start * 2:
self.PrintMatrixInCircle(matrix, columns, rows, start)
start += 1
print('')

def PrintMatrixInCircle(self, matrix, columns, rows, start):
endX = columns - 1 - start
endY = rows - 1 - start

for i in range(start, endX+1):
number = matrix[start][i]
print(number, ' ', end='')

if start < endY:
for i in range(start+1, endY+1):
number = matrix[i][endX]
print(number, ' ', end='')

if start < endX and start < endY:
for i in range(endX-1, start-1, -1):
number = matrix[endY][i]
print(number, ' ', end='')

if start < endX and start < endY-1:
for i in range(endY-1, start, -1):
number = matrix[i][start]
print(number, ' ', end='')


matrix = [[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12],
[13, 14, 15, 16]]
matrix2 = [[1],[2],[3],[4],[5]]
matrix3 = [[1,2],[3,4],[5,6],[7,8],[9,10]]
S = Solution()
S.printMatrix(matrix)
S.printMatrix(matrix2)
S.printMatrix(matrix3)
# print(S.PrintMatrix(matrix))
# print(S.PrintMatrix(matrix2))
# print(S.PrintMatrix(matrix3))

戴尔服务器 iDrac 配置

背景

之前介绍过超微的 IPMI 配置时提过,超微的 IPMI 是最简陋的,最近在用戴尔服务器的时候碰到了一个比较坑的事情,查了资料解决了,更加坚定之前的结论,IPMI 相应的配置尽量独立。

问题

在戴尔服务器安装 CentOS 后,可见网卡比预期要多出一块,物理服务器安装了两块 PCIe 网卡,一块千兆,一块万兆,每块网卡有两个网口,那么理应在服务器看到的应该是 4个网口,即 ip ad 看到的应该是

1
2
3
4
5
root@node11 22:24:49 ~]$ip ad |grep enp
2: enp94s0f0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master ovs-system state UP qlen 1000
3: enp24s0f0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq portid 3cfdfe6c9e10 state DOWN qlen 1000
4: enp24s0f1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq master ovs-system portid 3cfdfe6c9e12 state UP qlen 1000
5: enp94s0f1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN qlen 1000

但是安装完 OS,发现多出一块网口 eno16 ,通过 ethtool 工具查看,发现该网口为千兆,且处于连接状态,最开始没多想,直接将该网口作为管理网络配置了,一切正常。

当我尝试通过该网口连接 IPMI 获取风扇信息时,发现无法获取,验证命令为:
ipmitool -I lanplus -H <ipmi ip> -U <user name> -P <password> lan print
最开始以为是密码不对,后来发现是在 OS 内部无法 ping 通 IPMI IP,这就很奇怪了。

当时网络情况如下:

连通情况 yiran’s PC IPMI IP OS IP
yiran’s PC 1 1 1
IPMI IP 1 1 0
OS IP 1 0 1

因为没遇到过这种情况,根据经验,先检查了下 iDRAC 网络配置,发现当时 iDRAC 网卡选择的是 LOM1,也就是板载网卡,且该网卡处于 pass-through 状态,官方解释如下:

板载网卡 (LAN On Motherboards, LOM),如果您选择 Network Settings(网络设置)下的 Auto Dedicated NIC(自动专用 NIC),则当 iDRAC 将其 NIC 选择作为共享 LOM(1、2、3 或 4)并且在 iDRAC 专用 NIC 上检测到链接时,iDRAC 会更改其 NIC 选择来使用专用 NIC。如果在专用 NIC 上检 测不到链接,则 iDRAC 使用共享 LOM。从共享 NIC 切换到专用 NIC 的超时为五秒,而从专用 NIC 切换到共享 NIC 的超时为 30 秒。可以使用 RACADM 或 WS-MAN 配置此超时值。
如果选择 LOM 作为直通配置,并且使用专用模式连接服务器,则输入操作系统的 IPv4 地址。 注: 如果在共享的 LOM 模式下连接了服务器,则操作系统 IP 地址字段将禁用。

解决办法

将 iDRAC 网络配置从 LOM 改为 Dedicated 后,重启 OS 即可,注意,改为 Dedicated 后,OS 将无法识别到 LOM,需要重新配置网络。

剑指 Offer(六)

链表中倒数第k个结点

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
# 使用两个指针,a 先遍历 k-1,之后一起遍历,直到a 指针到最后一个节点,则 b 为倒数 k 节点
class ListNode(object):

def __init__(self, x):
self.val = x
self.next = None


class Solution:

def find_k_to_tail(self, head, k):
if not head and k <= 0:
return None

a_head = head
b_head = None

for i in range(k - 1):
if a_head.next != None:
a_head = a_head.next
else:
return None

b_head = head
while a_head.next != None:
a_head = a_head.next
b_head = b_head.next
return b_head.val

def find_k_to_tail2(self, head, k):
if not head and k <= 0:
return None

a_head = head

while a_head and (k - 1) >= 0:
a_head = a_head.next
k -= 1

while a_head:
a_head = a_head.next
head = head.next
return head.val


a = ListNode(1)
a.next = ListNode(2)
a.next.next = ListNode(3)
a.next.next.next = ListNode(4)
a.next.next.next.next = ListNode(5)

s = Solution()
print s.find_k_to_tail2(a, 3)

剑指 Offer(五)

调整数组顺序使奇数位于偶数前面

1
2
3
4
5
6
7
8
9
10
11
12
13
def reorder(nums, func):
left, right = 0, len(nums) - 1
while left < right:
while not func(nums[left]):
left += 1
while func(nums[right]):
right -= 1
if left < right:
nums[left], nums[right] = nums[right], nums[left]


def is_even(num):
return (num & 1) == 0

调整数组顺序使奇数位于偶数前面,保持相对位置不变

1
2
3
4
5
6
7
def reorder2(nums):
left = [num for num in nums if num & 1 != 0]
right = [num for num in nums if num & 1 == 0]
return left + right

nums = [1,2,3,4,5,6,7]
print reorder2(nums)

剑指 Offer(四)

在O(1)时间内删除链表结点

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

class ListNode(object):

def __init__(self, x=None):
self.val = x
self.next = None

def __del__(self):
self.val = None
self.next = None

class Solution(object):
def delete_list(self, list_head, to_delete):
if not list_head or not to_delete:
return False

if to_delete.next != None:
next_node = to_delete.next
to_delete.val = next_node.val
to_delete.next = next_node.next
next_node.__del__()

elif list_head == to_delete:
list_head.__del__()
to_delete.__del__()
else:
list_node = list_head
while list_node.next != to_delete:
list_node = list_node.next
list_node.next = None
to_delete.__del__()


node1 = ListNode(10)
node2 = ListNode(11)
node3 = ListNode(13)
node4 = ListNode(15)
node1.next = node2
node2.next = node3
node3.next = node4


# Solution().delete_list(node1, node4)
Solution().delete_list(node1, node3)
print(node1.next.next.val)

个人常用工具

背景

公司标配 Thinkpad,也曾经折腾过 Linux,最终因为舍不得 Windows 下的各种软件,就老老实实用 Windows 。
周五的时候电脑故障,趁着重新配置开发环境的机会,整理下自己常用的软件|工具。

开发工具

编辑器

VIM

主力编辑器,因为平时写 Python 比较多,所以安装的插件并不多,主要用到的就是函数之间的跳转,具体配置在 github

VScode

在写代码的时候通常使用 vim ,但是看代码还是习惯于 VScode,主要是看代码用鼠标控制比较方便。
偶尔会碰到编写配置文件,比如 JSON/YAML 的时候,vim 写起来还是有些吃力,可能是我用的不到家吧。

终端

XShell

在 Windows 用的比较多的应该是 XShell,同时管理多台服务器简单高效,且有配套的 XSFTP,上传/下载文件很方便。

Tmux

在 Linux 上主要用系统自带的 Terminal 配合 Tmux 使用,主要原因是 Tmux 可以在多平台使用,不用针对不同平台记不同的快捷键(没错,说的就是 Jetbrains)。

并没有对 tmux 做太多定制化配置,因为有时候服务器上面不会允许你修改默认配置文件的,所以大部分用的默认配置,平时用的比较多的应该就是 Window & Panel 配合使用。

Hyper

好看。

周边工具

Wox

作为 Windows 下的 alfred, Wox 无疑是一个合格的软件,可以极大的提高效率。

Sumatra PDF

一款极小的 PDF 阅读器,该有的功能都有,比福昕好用,无广告。

Ditto

Windows 下的一款剪切板软件,可以记录复制粘贴的历史情况,提供搜索等功能。

个人管理

有道云笔记

笔记这类工具用上一款之后,随着使用时间的增加,切换成本是成正比的,所以就要选一块稳定可靠的。像近来流行的 Bear,为知,Notion,说实在的,就是担心哪天公司突然倒闭了。

有道现在支持 Markdown,部分功能需要开通会员才可以,比如 Markdown 中上传图片。不过对于我来说还是够用了。

Trello

由于现在工作内容每天变动很大,随时有可能调整任务优先级,所以平时记录 Todo 类型内容基本上靠着 Markdown 上的 - [ ] 过活。但是对于个人管理而言,还是使用 Trello ,便于管理和记录,方便观察自我成长。

Pocket

稍后阅读工具,配合 Chrome 上的插件使用很完美,多平台同步很快。
缺点就是分类较为痛苦,要管理自己的 tag。

Inoreader

RSS 阅读器。 随着微信/微博等社交公众号的推广,生活中充斥着一些片段信息,有些公众号发的内容毫无营养,极大的浪费时间,这时候关注自己想关注的就比较重要了。

个人比较喜欢 RSS 订阅的方式订阅自己感兴趣的内容,无论是博客,还是公众号,都可以通过 RSS 的方式订阅,如果没有提供 RSS,也可以通过 RSSHub 来订阅。之后找时间把我订阅的一些博客/网站整理分享出来。

记账

一直想找一款账本类工具,多平台,简单易用的,但是没找到,随着第三方支付的便捷性,很多账本都没办法去自动同步,如果手动同步的话又特别麻烦,很容易遗漏。

我个人的解决方案简单粗暴:支付宝。 所有个人支出全部通过支付宝支付,利用支付宝的账单统计了解个人消费情况。

时间管理

RescueTime

支持多平台,且会自动统计工作内容及相应软件使用时间,最终形成以天/周/月为单位的报告。

ManicTime

同样支持多平台,相比于 RescueTime ,ManicTime 记录的内容更细,具体到你每天工作的起始/终止时间,中间电脑待机时间等等,每款软件的使用时间及使用频率,最终报告也会精确到每款软件的总使用时间。

总结

把日常使用最多的软件列举了一下,基本上我每天清醒的时间都是在使用上述软件,想想还是很恐怖的。

最后的时间管理软件其实我中断使用过一段时间,那段时间我认为自己能够控制自己的工作内容分配,时间分配都在自己的计划中,应该不会出现 时间去哪了 之类的问题。结果最近随着工作内容的增加,每天感觉都有做不完的事情,又不清楚自己的时间都用来做什么了,就又安装回来观察下,结果很震惊:工作分配真的变多了 ( º﹃º )

Exponential backoff

背景

昨天看到 Ansible 关于 Linux reboot plugin 相关文章 时,看到了它关于重试等待的设计,了解了下 Exponential backoff,特此记录。

简介

假设存在需求:

  1. 获取服务器 A 启动时间
  2. 重启服务器 A
  3. 获取服务器 A 当前时间

其中第 2 点,我一般会重启服务器 A 后,不断的重连服务器 A 来判断服务器 A 是否正常启动,每次重连后等待,再次重试,设置一个最大超时时间,超过最大超时时间认为服务器 A 启动失败,任务失败。

那么什么是 Exponential backoff 呢? 中文应该叫“指数退避”,意思就是每次重连失败后,等待的时候随着重试次数的增加而成指数增长,如果我们第1次重试等待时间为2s,则第2次重试等待时间为4s,第三次重试等待时间为8s,以此类推。

我理解最大的好处就是防止短时间内大量的重复错误,有时候当你知道你的操作是短时间无法完成的(比如重启服务器 A),那么该操作执行过程中,短时间内重试多次是没有意义的。当然我们也不能让重试等待时间无限的增长,我们可以设置一个最大的重试时间(不是最大超时时间),如果大于等于最大重试时间,则等待最大重试时间后再次重试。

具体实现

伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Do some asynchronous operation.

retries = 0

DO
wait for (2^retries * 100) milliseconds

status = Get the result of the asynchronous operation.

IF status = SUCCESS
retry = false
ELSE IF status = NOT_READY
retry = true
ELSE IF status = THROTTLED
retry = true
ELSE
Some other error occurred, so stop calling the API.
retry = false
END IF

retries = retries + 1

WHILE (retry AND (retries < MAX_RETRIES))

Ansible linux reboot plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fail_count = 0
max_fail_sleep = 12

while datetime.utcnow() < max_end_time:
try:
action()
if action_desc:
display.debug('%s: %s success' % (self._task.action, action_desc))
return
except Exception as e:
# Use exponential backoff with a max timout, plus a little bit of randomness
random_int = random.randint(0, 1000) / 1000
fail_sleep = 2 ** fail_count + random_int
if fail_sleep > max_fail_sleep:
fail_sleep = max_fail_sleep + random_int
if action_desc:
display.debug("{0}: {1} fail '{2}', retrying in {3:.4} seconds...".format(self._task.action, action_desc, to_text(e), fail_sleep))
fail_count += 1
time.sleep(fail_sleep)

剑指 Offer(三)

打印1到最大的n位数

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
# 将数组转换为字符
def print_number(number):
is_beginning_0 = True
num_len = len(number)

for i in range(num_len):
if is_beginning_0 and number[i] != "0":
is_beginning_0 = False
if not is_beginning_0:
print("%c" % number[i], end="")
print("")

# 边界条件:n > 0
def print_1_to_max_of_n1(n):
if n <= 0:
return

number = ["0"] * n
while not increment(number):
print_number(number)

# 从最后一位开始计算,如果 最后一位增长为10,则重置为 0,且进位;如果首位增长为 10,则溢出
def increment(number):
is_carry = 0
is_overflow = False
sum = 0
num_len = len(number)

for i in range(num_len - 1, -1, -1):
sum = int(number[i]) + is_carry
if i == num_len - 1:
sum += 1

if sum >= 10:
if i == 0:
is_overflow = True
else:
sum -= 10
number[i] = str(sum)
is_carry = 1
else:
number[i] = str(sum)
break

return is_overflow


def print_1_to_max_of_n2(n):
if n <= 0:
return

number = ["0"] * n
for i in range(10):
number[0] = str(i)
print_1_to_max_of_n_recursively(number, n, 0)

# 始终找到最后一位,并将其计算
def print_1_to_max_of_n_recursively(number, num_len, index):
if index == num_len - 1:
print_number(number)
return
for i in range(10):
number[index + 1] = str(i)
print_1_to_max_of_n_recursively(number, num_len, index +1)


print_number(["0", "1", "1"])
print_1_to_max_of_n1(2)
print_1_to_max_of_n2(2)

Ansible最佳实践

背景

说起来我真正负责过大批量服务器线上管理的时间,还是在16年负责运维的时候,那时候还都是通过 Shell 脚本来完成一些自动化的工作,当时觉得还不错,至少我觉得可定制化上还是很好的。

目前负责公司产品中一部分功能目前是通过 Shell 来完成的,但是 Shell 脚本在使用中存在一些弊端,最近在用 Ansible 来重写这部分功能,在重写过程中感受负责,又爱又恨,也有一些疑惑,特此记录。

Ansible

相信大家都或多或少听过 Ansible,Puppet,SaltStack 等等自动管理工具,它们的功能都很强大,但使用起来又不简单,Ansible 可以说是这里面上手最快的一个。

这里我不讲述 Ansible 具体的使用规则,大家看文档就好,我讲下我常用的几个场景:

批量查看、操作、拷贝

无论是作为一名开发,还是测试、运维,应该都碰到过需要管理多台服务器的情况,比如我们想要查看一个集群中所有节点的负责情况,那么我们可以执行:

1
2
3
4
5
6
7
8
9
10
 $ ansible yiran-cluster -m raw -a 'uptime'
192.168.67.39 | SUCCESS | rc=0 >>
20:23:24 up 3 days, 7:32, 4 users, load average: 14.97, 13.81, 12.55
Shared connection to 192.168.67.39 closed.
192.168.67.40 | SUCCESS | rc=0 >>
20:23:24 up 3 days, 7:32, 1 user, load average: 11.79, 14.15, 14.81
Shared connection to 192.168.67.40 closed.
192.168.67.41 | SUCCESS | rc=0 >>
20:23:24 up 6 days, 22:36, 1 user, load average: 19.89, 20.10, 19.79
Shared connection to 192.168.67.41 closed.

如果我们想要拷贝自己的测试代码到所有的服务器上,我们可以执行:

1
ansible yiran-cluster -m synchronize -a 'src=zbs_rest dest=/usr/lib/python2.7/site-packages/'

重复性操作

如果我们经常要查看某些集群(无监控)情况下的性能,我们可以编写一个 playbook,这个 playbook 专门用来收集集群的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 $ cat cluster_status.yml
#!/usr/bin/env ansible-playbook
---
- name: cluster status
hosts: yiran-cluster
max_fail_percentage: 0
gather_facts: false
tasks:
- name: uptime
raw: uptime
register: out
- debug: var=out.stdout_lines

- name: check services status
shell: /usr/share/tuna/script/control_all_services.sh --action=status --group=role
register: out
async: 300
poll: 2
- debug: var=out.stdout_lines

- name: memory status
raw: free -h
register: out
- debug: var=out.stdout_lines

Shell Script vs Ansible

通过上述的简单示例,可以体会到 Ansible 的强大,但是 Ansible 真的有那么好么?

明明几行 Shell 就可以搞定的事情,为什么一定要使用 Ansible 来做呢?
明明一个 Shell 脚本就可以完成的环境监察,为什么一定要使用 Ansible Playbook 来做呢?要知道 Playbook 编写语法虽然是 YAML,但是使用起来并不简单,有很多特殊的语法需要去注意,完全没有必要花费精力去学习一个新的工具去完成。

前两天看到 卡瓦邦噶 介绍 Ansible的一篇博客中,提到了一篇 Shell Script vs Ansible: Fight 的文章(远古版真香),其中有一段总结,用来描述 Ansible 的优势,我加上了 Shell Script 的对比如下:

Ansible Shell Script 优胜者
可以进行源码管理 Shell 也可以 -
幂等性 Shell 中需要额外做条件判断 Ansible
同时在多台服务器运行 Shell 可以通过 sshpass 编写脚本同时在多台运行 Ansible
验证服务器正确性 Shell 需要编写脚本收集更多信息 Ansible
定位部分服务器组 Shell 需要编写脚本对配置文件进行过滤筛选 Ansible
支持模板 - Ansible
技术栈支持 - Ansible

综合看上去,感觉 Ansible 太好了,上述情况下如果可以选择的话,我们都应该选择 Ansible 来做管理,事实上真的是这样么?

我也以为是这样,直到我通过 Ansible 重写 Shell 脚本。

噩梦开始

由于产品功能需要处理多平台的、多场景的情况,该功能的 Shell 脚本大概有 4800 行左右。

1
2
3
4
5
6
 --------------------------------------------------------------------------------
Language files blank comment code
--------------------------------------------------------------------------------
Bourne Shell 57 990 154 4847
Python 2 48 0 201
Bourne Again Shell 1 4 5 28

我们找一个简单的脚本来看下:

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
master ✔ $ cat stage_mount_extent_disks.sh
#!/usr/bin/env bash

cur=`dirname $0`
. $cur/zbs_util.sh


if [[ "$#" -lt 1 ]]; then
echo "Usage: $0 <disk1> <disk2>"
exit 1
fi

disks=$*

echo "waiting service start ....."
for ((i = 0; i < 30; i++)); do
if pidof service; then
break
else
sleep 2
fi
done
echo "service started"


# mount partition
for disk in ${disks[@]}; do
echo "mount $disk"
$cur/mount_extent_disk.sh "/dev/$disk"
done

这是一个很简单的脚本,首先我们判断了下输入参数,需要输入两块磁盘盘符,接下来等待服务启动后,我们调用另一个脚本进行磁盘的挂载,如果我们想要执行这个脚本,那么我们可以执行执行:

1
./stage_mount_extent_disks.sh sda sdb

然后等待脚本执行结束就可以了,这里的 sda 和 sdb 是存在一个 json 文件中,我们使用 jq 命令可以很容易的获取到 json 文件中的执行磁盘。

那么我们在 Ansible Playbook 我们要怎么做?

  1. 读取配置文件,并将读取结果设置为参数 myvar
  2. 解析 myvar 获取 extent disks list,注意,这里的解析语法是 JMESPath 的语法
  3. 将 extent disk list 转换为 extent disk string ,这里是因为如果调用 raw 模块,需要传递字符串
  4. 如果平台是 xen7 的话,执行 xen7 的脚本
  5. 如果平台不是 xen7,且是 halo 的话,执行 halo 脚本

具体 Playbook 如下:

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
- name: mount extent disk
gather_facts: false
hosts: master:storage
tasks:
- shell: cat /path/config.json
register: result
- set_fact:
myvar: "{{ result.stdout | from_json }}"

- name: get all extent disk list
set_fact:
extent_disk_list: "{{ myvar | json_query('disks[?function==`extent`][].drive') | list }}"

- name: get all extent disk string
set_fact:
extent_disk_string: "{{ extent_disk_list | join(' ') }}"

- name: mount xen7 extent disk
raw: /usr/share/tuna/script/xen70/stage_mount_extent_disks.sh "{{ extent_disk_string }}"
when: myvar.xen7 == True
register: out
- debug: var=out.stdout_lines

- name: mount halo extent disk
raw: /usr/share/tuna/script/halo/stage_mount_extent_disks.sh "{{ extent_disk_string }}"
when: myvar.xen7 == False and myvar.halo == True
register: out
- debug: var=out.stdout_lines

相信大家通过这个简单的示例发现一些问题,我总结了下:

  1. 如果脚本中存在较多判断,不宜使用 Playbook 实现逻辑
  2. 如果脚本中存在部分参数解析功能,不宜使用 Playbook 实现逻辑
  3. 不要过度拆分 task,保证每个 task 完整性

其实上面的 Playbook 我们完全可以写成这样:

1
2
3
4
5
6
7
8
9
- name: mount extent disk
gather_facts: false
hosts: master:storage
tasks:
- name: mount extent disk
# 所有逻辑判断均在 stage_mount_extent_disks.sh 中完成
raw: /usr/share/tuna/script/stage_mount_extent_disks.sh "{{ extent_disk_string }}"
register: out
- debug: var=out.stdout_lines

Ansible 只作操作分发,减轻 Playbook 复杂性,虽然这会损失一部分幂等性,但是可以最简化的满足要求,同时执行,获取执行结果。

总结

从个人使用上来说,Ansible 还是很好用的,至少它无需 Agent,SSH 连接等特性,使用起来很友好。
但是我们也不应该过分使用 Playbook,编写 Playbook 解析 json 花费了不少的时间,远不如直接在被执行脚本中完成的成本低。

Ansible 还有一些未能合理解决的问题,比如如何知道一个 Playbook 执行的总体进度?
如何获取执行的实时结果输出等。

如果我们只是普通的操作一些节点执行命令,获取信息,那么完全可以通过 sshpass,mmh 等命令完成,相对来说更方便。

希望随着自己的使用,能够更好的掌握使用 Ansible 的度。