剑指 Offer(二)

数值的整数次方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 给定一个double类型的浮点数base和int类型的整数exponent。求base的exponent次方。
# 考虑边界情况,base = 0, exponent < 0, exponent = 0 场景。
def equal_zero(num):
if abs(num - 0.0) < 0.0000001:
return True


def power(base, exponent):
if equal_zero(base):
result = False
if exponent == 0:
result = 1
result = power(base, abs(exponent) >> 1)
result *= result
if abs(exponent) & 1 == 1:
result *= base
if exponent < 0:
result = 1.0 / result
return result

power_value(2, 2)

Traceroute 简易实现

背景

在平时遇到网络问题时,我们通常会使用 ping,route,ip 等命令去 debug,当我们确定我们本机的网络配置及服务没有问题后,我通常会使用 traceroute 来判断网络走向。

最近公司搬家之后,整体网络架构进行了改进,随着配置的复杂化,稳定性相较于原来有了很大的下降,导致最近频繁使用 traceroute,一直使用它却不知道是怎么工作的,研究了一下,作为总结。

Traceroute

先上维基百科的解释:

traceroute,现代Linux系统称为tracepath,Windows系统称为tracert,是一种计算机网络工具。它可显示数据包在IP网络经过的路由器的IP地址。

我们通常使用无需特殊配置,直接用 traceroute 加上我们的目标地址即可,如:

1
2
3
4
5
6
root@yiran-workstation:~ 
$ traceroute 192.168.16.1
traceroute to 192.168.16.1 (192.168.16.1), 30 hops max, 60 byte packets
1 gateway (192.168.8.1) 19.469 ms 19.089 ms 18.911 ms
2 192.168.1.201 (192.168.1.201) 11.539 ms 11.423 ms 11.307 ms
3 192.168.16.1 (192.168.16.1) 18.289 ms 18.184 ms 18.064 ms

当我们想设置 TTL 数值时,我们可以使用 -m 参数:

1
2
3
4
5
root@yiran-workstation:~ 
$ traceroute 192.168.16.1 -m 2
traceroute to 192.168.16.1 (192.168.16.1), 2 hops max, 60 byte packets
1 gateway (192.168.8.1) 20.914 ms 20.700 ms 20.616 ms
2 192.168.1.201 (192.168.1.201) 20.497 ms 20.465 ms 20.383 ms

实现原理:

主叫方首先发出 TTL=1 的数据包,第一个路由器将 TTL 减1得0后就不再继续转发此数据包,而是返回一个 ICMP 逾时报文,主叫方从逾时报文中即可提取出数据包所经过的第一个网关地址。然后又发出一个 TTL=2 的 ICMP 数据包,可获得第二个网关地址,依次递增 TTL 便获取了沿途所有网关地址。

需要注意的是,并不是所有网关都会如实返回 ICMP 超时报文。出于安全性考虑,大多数防火墙以及启用了防火墙功能的路由器缺省配置为不返回各种 ICMP 报文,其余路由器或交换机也可被管理员主动修改配置变为不返回 ICMP 报文。因此 Traceroute 程序不一定能拿全所有的沿途网关地址。所以,当某个 TTL 值的数据包得不到响应时,并不能停止这一追踪过程,程序仍然会把 TTL 递增而发出下一个数据包。一直达到默认或用参数指定的追踪限制(maximum_hops)才结束追踪。

这里要说明一下,加入我们去 traceroute 最常用的 baidu.com,就会看到这个现象,traceroute 命令的结果中包含 * * * ,我也没有找到一个较为明确的解释,猜测这个节点禁止了 ping 或其他配置,无法返回 ICMP 超时报文,导致 traceroute 无法正常解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
master ✗ $ traceroute  baidu.com -m 20 
traceroute to baidu.com (220.181.57.216), 20 hops max, 60 byte packets
1 gateway (192.168.8.1) 18.535 ms 18.456 ms 29.185 ms
2 10.1.1.1 (10.1.1.1) 6.599 ms 6.533 ms 6.378 ms
3 106.38.14.1 (106.38.14.1) 28.707 ms 28.606 ms 28.514 ms
4 5.0.142.219.broad.bj.bj.dynamic.163data.com.cn (219.142.0.5) 28.384 ms 28.305 ms 28.205 ms
5 * * *
6 36.110.244.46 (36.110.244.46) 37.131 ms 31.660 ms 31.489 ms
7 * * *
8 220.181.17.94 (220.181.17.94) 10.700 ms 220.181.17.146 (220.181.17.146) 10.511 ms 220.181.17.150 (220.181.17.150) 10.336 ms
9 * * *
10 * * *
11 * * *
12 * * *
13 * * *
14 * * *
15 * * *
16 * * *
17 * * *
18 * * *
19 * * *
20 * * *

依据上述原理,利用了 UDP 数据包的 Traceroute 程序在数据包到达真正的目的主机时,就可能因为该主机没有提供 UDP 服务而简单将数据包抛弃,并不返回任何信息。为了解决这个问题,Traceroute 故意使用了一个大于 30000 的端口号,因 UDP 协议规定端口号必须小于 30000 ,所以目标主机收到数据包后唯一能做的事就是返回一个“端口不可达”的 ICMP 报文,于是主叫方就将端口不可达报文当作跟踪结束的标志。

简易实现

源地址: https://github.com/dnaeon/pytraceroute
对整个实现中最重要的部分做下注释:

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# Copyright (c) 2015 Marin Atanasov Nikolov <dnaeon@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer
# in this position and unchanged.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""
Core module

"""

import socket
import random
import struct
import time

__all__ = ['Tracer']


class Tracer(object):
def __init__(self, dst, hops=30):
"""
Initializes a new tracer object

Args:
dst (str): Destination host to probe
hops (int): Max number of hops to probe

"""
self.dst = dst # 目标地址:域名或 IPv4 地址
self.hops = hops
self.ttl = 1

# Pick up a random port in the range 33434-33534
# 对应上述解释,随机选择一个 > 30000 的端口用于连接
self.port = random.choice(range(33434, 33535))

def run(self):
"""
Run the tracer

Raises:
IOError

"""
try:
dst_ip = socket.gethostbyname(self.dst) # 解析域名
except socket.error as e:
raise IOError('Unable to resolve {}: {}', self.dst, e)

text = 'traceroute to {} ({}), {} hops max'.format(
self.dst,
dst_ip,
self.hops
)

print(text)

while True:
startTimer = time.time()
receiver = self.create_receiver() # 创建接收 socket 实例
sender = self.create_sender() # 创建发送 socket 实例
sender.sendto(b'', (self.dst, self.port)) # 向目标地址指定端口发送报文

addr = None
try:
data, addr = receiver.recvfrom(1024) # 获取发送 ICMP 超时报文,并解析地址
entTimer = time.time()
except socket.error:
pass
# raise IOError('Socket error: {}'.format(e))
finally:
receiver.close()
sender.close()

if addr: # 如果获取到地址,则打印相应信息及用时
timeCost = round((entTimer - startTimer) * 1000, 2)
print('{:<4} {} {} ms'.format(self.ttl, addr[0]), timeCost)
if addr[0] == dst_ip:
break
else:
print('{:<4} *'.format(self.ttl))

self.ttl += 1 # 增加 TTL,获取下一跳地址

if self.ttl > self.hops:
break

def create_receiver(self):
"""
Creates a receiver socket

Returns:
A socket instance

Raises:
IOError

"""
s = socket.socket(
family=socket.AF_INET, # 指定 proto family 为 IPv4
type=socket.SOCK_RAW, # 指定接收 socket 类型为 raw,这里是因为普通的 socket 类型无法处理 ICMP 报文
proto=socket.IPPROTO_ICMP # 指定 socket 协议为 ICMP 协议,type 与 proto 需要特定的组合,不允许任意配置
)

timeout = struct.pack("ll", 5, 0)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, timeout)

try:
s.bind(('', self.port))
except socket.error as e:
raise IOError('Unable to bind receiver socket: {}'.format(e))

return s

def create_sender(self):
"""
Creates a sender socket

Returns:
A socket instance

"""
s = socket.socket(
family=socket.AF_INET, # 指定 proto family 为 IPv4
type=socket.SOCK_DGRAM, # 指定发送的类型为 UDP,即发送广播消息
proto=socket.IPPROTO_UDP # 指定协议为 IP UDP
)

s.setsockopt(socket.SOL_IP, socket.IP_TTL, self.ttl)

return s

总结

了解了实现原理之后,希望之后排查网络问题应该也会得心应手一些吧。
也希望自己能更多的关注于 Why,而不是 How。

参考链接

剑指 Offer(一)

Python 单实例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton(object):
_instances = {}

def __new__(cls, *args, **kwargs):
if cls not in cls._instances:
cls._instances[cls] = super(Singleton, cls).__new__(cls, *args, **kwargs)
return cls._instances[cls]

class MySingleton(Singleton):

def __init__(self, val):
self.val = val

a = MySingleton(1)
b = MySingleton(1)
print a.val
print b.val

二维数组查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

def find_num(matrix, num):
if not matrix:
return False
rows = len(matrix)
cols = len(matrix[0])
row, col = rows - 1, 0
while row >= 0 or col <= cols -1:
if matrix[row][col] == num:
return num
elif matrix[row][col] > num:
row -= 1
else:
col += 1
return False

matrix = [[1,2,3,4],
[5,6,7,8]
]
print find_num(matrix, 7)

打印链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

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

def print_links(links):
if links:
print_links(links.next)
print links.val

links = Links(1)
links.next = Links(2)
links.next.next = Links(3)
print_links(links)

重建二叉树

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
# 根据前序和中序遍历结果构建二叉树,遍历结果中不包含重复数值。
class TreeNode(object):
def __init__(self, x):
self.val = x
self.left = None
self.right = None

class Tree(object):
def __init__(self):
self.root = None

def pre_traversal(self):
ret = []
def traversal(head):
if not head:
return
ret.append(head.val)
traversal(head.left)
traversal(head.right)

traversal(self.root)
return ret

def in_traversal(self):
ret = []

def traversal(head):
if not head:
return
traversal(head.left)
ret.append(head.val)
traversal(head.right)

traversal(self.root)
return ret

def post_traversal(self):
ret = []

def traversal(head):
if not head:
return
traversal(head.left)
traversal(head.right)
ret.append(head.val)

traversal(self.root)
return ret

def construct_tree(preorder=None, inorder=None):
if not preorder or not inorder:
return None
index = inorder.index(preorder[0])
left = inorder[0:index]
right = inorder[index + 1:]
root = TreeNode(preorder[0])
root.left = construct_tree(preorder[1:1+len(left)], left)
root.right = construct_tree(preorder[-len(right):], right)
return root

t = Tree()
root = construct_tree(preorder=[1,2,4,7,3,5,6,8],
inorder=[4,7,2,1,5,3,8,6])
t.root = root
print t.pre_traversal()
print t.in_traversal()
print t.post_traversal()

旋转数组的最小数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def find_min(nums):
if not nums:
return False
length = len(nums)
left, right = 0, length - 1
while nums[right] >= nums[left]:
if right - left == 1:
return nums[right]
mid = (left + right) / 2
if nums[left] <= nums[mid]:
left = mid
if nums[right] >= nums[mid]:
right = mid
return nums[0]

nums = [3,4,5,6,0,1,2]
print find_min(nums)
nums = [1,0,0,1]
print find_min(nums)

二进制中 1 的个数

1
2
3
4
5
6
7
8
9
10
11
12

def find_num_of_1(n):
ret = 0
if n < 0:
n = n & 0xffffffff
while n:
ret += 1
n = n & (n - 1)
return ret

num = 3
print find_num_of_1(num)

超微服务器 IPMI 连接配置介绍

背景

目前在国内,大部分公司使用的服务器均为国内厂商的,如:华为、浪潮、联想、华三(H3C)、曙光等等。无论是从售后服务角度,还是国企央企招标采购角度,都是比较理想的。
但是还是有些国外的服务器如:惠普、超微、戴尔等等,占有着很大的市场。现在所在的公司提供一体机给客户,OEM 厂商就是超微和戴尔,今天来说说超微服务器 IPMI 连接配置。

IPMI

常规操作,先贴上维基百科的解释:

智能平台管理接口(Intelligent Platform Management Interface)原本是一种Intel架构的企业系统的周边设备所采用的一种工业标准。IPMI亦是一个开放的免费标准,用户无需支付额外的费用即可使用此标准。

就日常使用来说,IPMI 就是规模较大公司中常说的 带外网络 可连接控制的接口。一般用于物理服务器的管理,如:开关机、Web Console、硬件信息获取、硬件故障报警等功能。

大多数服务器厂商对该接口叫法不同,比如超微叫 IPMI,戴尔叫 RACADM ,惠普叫 iLo 等等。大部分厂商都会对该接口进行各种定制化功能,使用户上手更容易。而超微不同,超微的 IPMI 管理界面相当简(丑)陋,除了必备的功能外,没有特色功能。

连接方式

IPMI 接口就是一个普通的 1GbE 网口,常规连接到交换机上配置 IP 就可以正常管理与使用了,下面主要来说下三种不同的配置方式优势及区别。

Dedicated

专用模式,没有其他的乱七八糟配置,最简单的配置 IP 连接网线就可以直接使用。使用的网口就是 IPMI 的网口。

  • 优势
    最简配置,无须负责的网络策略。

  • 劣势
    需要单独的一根网线连接。在现阶段高密度服务器越来越流行的今天,一台普通的 2U4节点服务器,后面连接的网线可能达到:(1 IPMI + 2 管理 + 2 存储)*4 = 20根。这个数量是极为恐怖的,相信亲手布线过的同学一定深有体会。

Shared

共享模式。允许 IPMI 通过板载网口进行连接。这里要明确下,在有网络要求的公司,应该是不允许配置该模式的,因为客户要求网络要做到带内、带外的完全隔离,不能允许通过带内网络访问带外网络功能。但是小公司或者说一些测试环境,是可以选择该模式的。

  • 优势
    可以直接通过板载网口所在网络进行连接,无须为 IPMI 网口连接网线。
  • 劣势
    如果客户对网络有严格要求,那么是不允许配置的。
    看论坛上有部分用户在进行该配置,且同时配置了 VLAN,出现了部分问题,未解决。

Failover(Default)

故障转移模式,也是服务器默认模式。该模式优先检查 IPMI 网口是否可以连通,如果可以连通,则直接连接,如果不可以连接,则尝试通过板载网口所在网络进行连接。

  • 优势
    自动根据当前网络环境进行选择,无需认为干预。
  • 劣势
    在操作机器过程中,如果 IPMI 网口未连接网络,且 OS 关机状态,那么 IPMI 有可能失去连接。该问题在进行 IPMI Cold Reset 时尤为严重,随着 OS 的关闭,IPMI 也失去响应,这种场景下 IPMI 就丢失了它本身的作用。深坑。

10月杂记

10 月杂记

上一次写博客还是在 9 月末,感觉 10月份是每年最忙的时候,今年尤其忙。

公司搬家

从 15年底加入公司以来,一直在北四环边上的融科资讯中心写字楼办公。最近融科坐不下了,加上合同到期,为了追求更大(价格更低)的办公环境,只能离开。

之前陆陆续续听说融科在写字楼中属于档次稍高的,一直没什么感觉,最近公司搬离了融科,才知道租金 17元/天/平米比 9元/天/平米真是不知道好到哪里去了。

公司约定好周五晚上收拾东西,周六搬家公司全部搬走,本来没个人什么事情。诡异的责任心作祟,导致周末两天都在公司帮忙,瞎忙。

新办公室的装修如果单独拍照片看上去还不错,但是太糙了,赶工期的各种痕迹暴露无遗。办公室各种味道,感觉都在呼吸着甲醛。

但是老板照常工作了,员工也只能戴着口罩继续工作,也是切身体会了空气净化器对于装修污染来说,作用真是小的可怜,只有通风才是王道。

个人搬家

算起来我来北京3年的时间,已经换了 5 个住处了,这种感觉很不好。想起了某同事 买了房子的感慨:“我每年都要被房东赶“,心酸。

去年为了私人空间,脱离了一起生活 2年的室友,独自跑到回龙观住自如。当时图着房子新,空间大,也就没有考虑最重要的一点:距离。

一直觉得一个人生活,住的远一点没什么所谓。但是真遇到在公司加班到9,10点钟,晚上又叫不到车时,只能去坐75分钟的地铁回家,太痛苦了,已经远远超过了心酸。

这次趁着公司搬家,我也花了 2 周的时间找房子,也约了不少中介看房子。最终选择了距离公司 2 公里的小区,走路 30min,骑车 10min。

第一次体验了步行上下班,哪怕在公司加班再晚,也可以 10min 就回到家里休息,不会对我产生负面的心情,真好。

工作进展

因为公司和个人的搬家,导致整个 10月都很疲惫,无论是身体上的,还是心理上的。

工作上分配给我的功能,在 11月9号,就是今天,老板说要可 demo 的状态,但还没完成,只能周末加班抢救一下,不知道是否能把进度抢救回来。

现在做的功能其实跟 Cobbler 很像,很多功能都是一样的。配合上虚拟化感觉就像 OpenStack Ironic(一直想看却没抽出时间)。都是对物理机进行功能管理,没什么新意。

个人进展

无。(忧伤)

Python socket 编程

背景

平时工作很少涉及到 Socket 相关,基本上都是 HTTP 之上的业务,最近看到 Real Python 的一篇博客,非常详细的讲解了 Python 下的 socket 编程,其中有两个示例觉得很好,帮助我理解了一些要点,记录一下。

多连接情况

Server

multiconn-server.py

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
#!/usr/bin/env python3

import sys
import socket
import selectors
import types

sel = selectors.DefaultSelector()

def accept_wrapper(sock):
conn, addr = sock.accept() # 前提条件:可读状态
print('accepted connection from', addr)
conn.setblocking(False) # 置为非阻塞
data = types.SimpleNamespace(addr=addr, inb=b'', outb=b'')
events = selectors.EVENT_READ | selectors.EVENT_WRITE
sel.register(conn, events, data=data) # 注册事件到 select

def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # 前提条件:可读状态
if recv_data:
data.outb += recv_data # 保存数据
else:
print('closing connection to', data.addr) # 如果没有接受到数据,则需关闭该 socket 连接
sel.unregister(sock) # 将该 socket 事件从 select 删除
sock.close()
if mask & selectors.EVENT_WRITE:
if data.outb:
print('echoing', repr(data.outb), 'to', data.addr)
sent = sock.send(data.outb) #前提条件:可写状态
data.outb = data.outb[sent:] # 在数据发送完后, 将其删除

if len(sys.argv) != 3:
print('usage:', sys.argv[0], '<host> <port>')
sys.exit(1)

host, port = sys.argv[1], int(sys.argv[2])
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None)

try:
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None: # 如果没有传输数据,则该请求为连接请求
accept_wrapper(key.fileobj)
else:
service_connection(key, mask) # 处理数据
except KeyboardInterrupt:
print('caught keyboard interrupt, exiting')
finally:
sel.close()

Client

multiconn-client.py

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
#!/usr/bin/env python3

import sys
import socket
import selectors
import types

sel = selectors.DefaultSelector()
messages = [b'Message 1 from client.', b'Message 2 from client.']

def start_connections(host, port, num_conns):
server_addr = (host, port)
for i in range(0, num_conns):
connid = i + 1
print('starting connection', connid, 'to', server_addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(server_addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
data = types.SimpleNamespace(connid=connid,
msg_total=sum(len(m) for m in messages),
recv_total=0,
messages=list(messages),
outb=b'')
sel.register(sock, events, data=data)

def service_connection(key, mask):
sock = key.fileobj
data = key.data
if mask & selectors.EVENT_READ:
recv_data = sock.recv(1024) # 前提条件:可读状态
if recv_data:
print('received', repr(recv_data), 'from connection', data.connid)
data.recv_total += len(recv_data)
if not recv_data or data.recv_total == data.msg_total:
print('closing connection', data.connid)
sel.unregister(sock)
sock.close()
if mask & selectors.EVENT_WRITE:
if not data.outb and data.messages:
data.outb = data.messages.pop(0) # 如果没有可发送数据,则取 messages 中的第一条用户发送请求
if data.outb:
print('sending', repr(data.outb), 'to connection', data.connid)
sent = sock.send(data.outb) # Should be ready to write
data.outb = data.outb[sent:]

if len(sys.argv) != 4:
print('usage:', sys.argv[0], '<host> <port> <num_connections>')
sys.exit(1)

host, port, num_conns = sys.argv[1:4]
start_connections(host, int(port), int(num_conns))

try:
while True:
events = sel.select(timeout=1)
if events:
for key, mask in events:
service_connection(key, mask)
# Check for a socket being monitored to continue.
if not sel.get_map():
break
except KeyboardInterrupt:
print('caught keyboard interrupt, exiting')
finally:
sel.close()

协议封装处理

在多连接基础上,增加特殊协议处理,比如 json header 、 protocol header 等。

Server

libserver.py

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import sys
import selectors
import json
import io
import struct

request_search = {
'morpheus': 'Follow the white rabbit. \U0001f430',
'ring': 'In the caves beneath the Misty Mountains. \U0001f48d',
'\U0001f436': '\U0001f43e Playing ball! \U0001f3d0',
}

class Message:

def __init__(self, selector, sock, addr):
self.selector = selector
self.sock = sock
self.addr = addr
self._recv_buffer = b''
self._send_buffer = b''
self._jsonheader_len = None
self.jsonheader = None
self.request = None
self.response_created = False

def _set_selector_events_mask(self, mode):
"""Set selector to listen for events: mode is 'r', 'w', or 'rw'."""
if mode == 'r':
events = selectors.EVENT_READ
elif mode == 'w':
events = selectors.EVENT_WRITE
elif mode == 'rw':
events = selectors.EVENT_READ | selectors.EVENT_WRITE
else:
raise ValueError(f'Invalid events mask mode {repr(mode)}.')
self.selector.modify(self.sock, events, data=self)

def _read(self):
try:
# Should be ready to read
data = self.sock.recv(4096)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
if data:
self._recv_buffer += data
else:
raise RuntimeError('Peer closed.')

def _write(self):
if self._send_buffer:
print('sending', repr(self._send_buffer), 'to', self.addr)
try:
# Should be ready to write
sent = self.sock.send(self._send_buffer)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
self._send_buffer = self._send_buffer[sent:]
# Close when the buffer is drained. The response has been sent.
if sent and not self._send_buffer:
self.close()

# JSON 编码、解码
def _json_encode(self, obj, encoding):
return json.dumps(obj, ensure_ascii=False).encode(encoding)

def _json_decode(self, json_bytes, encoding):
tiow = io.TextIOWrapper(io.BytesIO(json_bytes), encoding=encoding,
newline='')
obj = json.load(tiow)
tiow.close()
return obj

def _create_message(self, *, content_bytes, content_type,
content_encoding):
jsonheader = {
'byteorder': sys.byteorder,
'content-type': content_type,
'content-encoding': content_encoding,
'content-length': len(content_bytes)
}
jsonheader_bytes = self._json_encode(jsonheader, 'utf-8')
message_hdr = struct.pack('>H', len(jsonheader_bytes))
message = message_hdr + jsonheader_bytes + content_bytes # 生成编码后信息
return message

def _create_response_json_content(self):
action = self.request.get('action')
if action == 'search': # 根据客户端发送请求返回相应响应信息
query = self.request.get('value')
answer = request_search.get(query) or f'No match for "{query}".'
content = {'result': answer}
else:
content = {'result': f'Error: invalid action "{action}".'}
content_encoding = 'utf-8'
response = {
'content_bytes': self._json_encode(content, content_encoding),
'content_type': 'text/json',
'content_encoding': content_encoding
}
return response

def _create_response_binary_content(self): # 如果为 binary,则直接返回
response = {
'content_bytes': b'First 10 bytes of request: ' +
self.request[:10],
'content_type': 'binary/custom-server-binary-type',
'content_encoding': 'binary'
}
return response

def process_events(self, mask):
if mask & selectors.EVENT_READ:
self.read()
if mask & selectors.EVENT_WRITE:
self.write()

def read(self):
self._read()

if self._jsonheader_len is None:
self.process_protoheader()

if self._jsonheader_len is not None:
if self.jsonheader is None:
self.process_jsonheader()

if self.jsonheader:
if self.request is None:
self.process_request()

def write(self):
if self.request:
if not self.response_created:
self.create_response()

self._write()

def close(self):
print('closing connection to', self.addr)
try:
self.selector.unregister(self.sock)
except Exception as e:
print(f'error: selector.unregister() exception for',
f'{self.addr}: {repr(e)}')

try:
self.sock.close()
except OSError as e:
print(f'error: socket.close() exception for',
f'{self.addr}: {repr(e)}')
finally:
# Delete reference to socket object for garbage collection
self.sock = None

def process_protoheader(self): # 解析协议头部信息,得到 json header 大小
hdrlen = 2
if len(self._recv_buffer) >= hdrlen:
self._jsonheader_len = struct.unpack('>H',
self._recv_buffer[:hdrlen])[0]
self._recv_buffer = self._recv_buffer[hdrlen:]

def process_jsonheader(self): # 解析 json header 信息,得到真实请求信息
hdrlen = self._jsonheader_len
if len(self._recv_buffer) >= hdrlen:
self.jsonheader = self._json_decode(self._recv_buffer[:hdrlen],
'utf-8')
self._recv_buffer = self._recv_buffer[hdrlen:]
for reqhdr in ('byteorder', 'content-length', 'content-type',
'content-encoding'):
if reqhdr not in self.jsonheader:
raise ValueError(f'Missing required header "{reqhdr}".')

def process_request(self):
content_len = self.jsonheader['content-length']
if not len(self._recv_buffer) >= content_len:
return
data = self._recv_buffer[:content_len]
self._recv_buffer = self._recv_buffer[content_len:]
if self.jsonheader['content-type'] == 'text/json':
encoding = self.jsonheader['content-encoding']
self.request = self._json_decode(data, encoding)
print('received request', repr(self.request), 'from', self.addr)
else:
# Binary 或者未知请求类型
self.request = data
print(f'received {self.jsonheader["content-type"]} request from',
self.addr)
# 设置 selector 监听写入事件,读事件已经完成
self._set_selector_events_mask('w')

def create_response(self):
if self.jsonheader['content-type'] == 'text/json':
response = self._create_response_json_content()
else:
# Binary or unknown content-type
response = self._create_response_binary_content()
message = self._create_message(**response)
self.response_created = True
self._send_buffer += message

app-server.py

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
#!/usr/bin/env python3

import sys
import socket
import selectors
import traceback

import libserver

sel = selectors.DefaultSelector()

def accept_wrapper(sock):
conn, addr = sock.accept() # Should be ready to read
print('accepted connection from', addr)
conn.setblocking(False)
message = libserver.Message(sel, conn, addr)
sel.register(conn, selectors.EVENT_READ, data=message)

if len(sys.argv) != 3:
print('usage:', sys.argv[0], '<host> <port>')
sys.exit(1)

host, port = sys.argv[1], int(sys.argv[2])
lsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Avoid bind() exception: OSError: [Errno 48] Address already in use
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
lsock.bind((host, port))
lsock.listen()
print('listening on', (host, port))
lsock.setblocking(False)
sel.register(lsock, selectors.EVENT_READ, data=None) # 将 socket 监听事件注册到 selector

try:
while True:
events = sel.select(timeout=None)
for key, mask in events:
if key.data is None: # 如果没有请求数据,则为客户端连接请求
accept_wrapper(key.fileobj)
else:
message = key.data
try:
message.process_events(mask) # 处理请求信息
except Exception:
print('main: error: exception for',
f'{message.addr}:\n{traceback.format_exc()}')
message.close()
except KeyboardInterrupt:
print('caught keyboard interrupt, exiting')
finally:
sel.close()

Client

libclient.py

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
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import sys
import selectors
import json
import io
import struct

class Message:

def __init__(self, selector, sock, addr, request):
self.selector = selector
self.sock = sock
self.addr = addr
self.request = request
self._recv_buffer = b''
self._send_buffer = b''
self._request_queued = False
self._jsonheader_len = None
self.jsonheader = None
self.response = None

def _set_selector_events_mask(self, mode):
"""Set selector to listen for events: mode is 'r', 'w', or 'rw'."""
if mode == 'r':
events = selectors.EVENT_READ
elif mode == 'w':
events = selectors.EVENT_WRITE
elif mode == 'rw':
events = selectors.EVENT_READ | selectors.EVENT_WRITE
else:
raise ValueError(f'Invalid events mask mode {repr(mode)}.')
self.selector.modify(self.sock, events, data=self)

def _read(self):
try:
# Should be ready to read
data = self.sock.recv(4096)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
if data:
self._recv_buffer += data
else:
raise RuntimeError('Peer closed.')

def _write(self):
if self._send_buffer:
print('sending', repr(self._send_buffer), 'to', self.addr)
try:
# Should be ready to write
sent = self.sock.send(self._send_buffer)
except BlockingIOError:
# Resource temporarily unavailable (errno EWOULDBLOCK)
pass
else:
self._send_buffer = self._send_buffer[sent:]

def _json_encode(self, obj, encoding): # JSON 编码
return json.dumps(obj, ensure_ascii=False).encode(encoding)

def _json_decode(self, json_bytes, encoding): # JSON 解码
tiow = io.TextIOWrapper(io.BytesIO(json_bytes), encoding=encoding,
newline='')
obj = json.load(tiow)
tiow.close()
return obj

def _create_message(self, *, content_bytes, content_type,
content_encoding): # 打包处理请求信息
jsonheader = {
'byteorder': sys.byteorder,
'content-type': content_type,
'content-encoding': content_encoding,
'content-length': len(content_bytes)
}
jsonheader_bytes = self._json_encode(jsonheader, 'utf-8')
message_hdr = struct.pack('>H', len(jsonheader_bytes))
message = message_hdr + jsonheader_bytes + content_bytes
return message

def _process_response_json_content(self):
content = self.response
result = content.get('result')
print(f'got result: {result}')

def _process_response_binary_content(self):
content = self.response
print(f'got response: {repr(content)}')

def process_events(self, mask):
if mask & selectors.EVENT_READ:
self.read()
if mask & selectors.EVENT_WRITE:
self.write()

def read(self):
self._read()

if self._jsonheader_len is None:
self.process_protoheader()

if self._jsonheader_len is not None:
if self.jsonheader is None:
self.process_jsonheader()

if self.jsonheader:
if self.response is None:
self.process_response()

def write(self):
if not self._request_queued:
self.queue_request()

self._write()

if self._request_queued:
if not self._send_buffer:
# Set selector to listen for read events, we're done writing.
self._set_selector_events_mask('r')

def close(self):
print('closing connection to', self.addr)
try:
self.selector.unregister(self.sock)
except Exception as e:
print(f'error: selector.unregister() exception for',
f'{self.addr}: {repr(e)}')

try:
self.sock.close()
except OSError as e:
print(f'error: socket.close() exception for',
f'{self.addr}: {repr(e)}')
finally:
# Delete reference to socket object for garbage collection
self.sock = None

def queue_request(self):
content = self.request['content']
content_type = self.request['type']
content_encoding = self.request['encoding']
if content_type == 'text/json':
req = {
'content_bytes': self._json_encode(content, content_encoding),
'content_type': content_type,
'content_encoding': content_encoding
}
else:
req = {
'content_bytes': content,
'content_type': content_type,
'content_encoding': content_encoding
}
message = self._create_message(**req)
self._send_buffer += message
self._request_queued = True

def process_protoheader(self):
hdrlen = 2
if len(self._recv_buffer) >= hdrlen:
self._jsonheader_len = struct.unpack('>H',
self._recv_buffer[:hdrlen])[0]
self._recv_buffer = self._recv_buffer[hdrlen:]

def process_jsonheader(self):
hdrlen = self._jsonheader_len
if len(self._recv_buffer) >= hdrlen:
self.jsonheader = self._json_decode(self._recv_buffer[:hdrlen],
'utf-8')
self._recv_buffer = self._recv_buffer[hdrlen:]
for reqhdr in ('byteorder', 'content-length', 'content-type',
'content-encoding'):
if reqhdr not in self.jsonheader:
raise ValueError(f'Missing required header "{reqhdr}".')

def process_response(self):
content_len = self.jsonheader['content-length']
if not len(self._recv_buffer) >= content_len:
return
data = self._recv_buffer[:content_len]
self._recv_buffer = self._recv_buffer[content_len:]
if self.jsonheader['content-type'] == 'text/json':
encoding = self.jsonheader['content-encoding']
self.response = self._json_decode(data, encoding)
print('received response', repr(self.response), 'from', self.addr)
self._process_response_json_content()
else:
# Binary or unknown content-type
self.response = data
print(f'received {self.jsonheader["content-type"]} response from',
self.addr)
self._process_response_binary_content()
# Close when response has been processed
self.close()

app-client.py

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
#!/usr/bin/env python3

import sys
import socket
import selectors
import traceback

import libclient

sel = selectors.DefaultSelector()

def create_request(action, value):
if action == 'search':
return dict(
type='text/json',
encoding='utf-8',
content=dict(action=action, value=value)
)
else:
return dict(
type='binary/custom-client-binary-type',
encoding='binary',
content=bytes(action + value, encoding='utf-8')
)

def start_connection(host, port, request):
addr = (host, port)
print('starting connection to', addr)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)
sock.connect_ex(addr)
events = selectors.EVENT_READ | selectors.EVENT_WRITE
message = libclient.Message(sel, sock, addr, request)
sel.register(sock, events, data=message)

if len(sys.argv) != 5:
print('usage:', sys.argv[0], '<host> <port> <action> <value>')
sys.exit(1)

host, port = sys.argv[1], int(sys.argv[2])
action, value = sys.argv[3], sys.argv[4]
request = create_request(action, value)
start_connection(host, port, request)

try:
while True:
events = sel.select(timeout=1)
for key, mask in events:
message = key.data
try:
message.process_events(mask)
except Exception as e:
print('main: error: exception for',
f'{message.addr}:\n{traceback.format_exc()}')
message.close()
# Check for a socket being monitored to continue.
if not sel.get_map():
break
except KeyboardInterrupt:
print('caught keyboard interrupt, exiting')
finally:
sel.close()

总结

学习了通过 selector 多路 I/O 复用方式来处理 socket 多连接情况,如果请求量少的情况下,也可以使用多线程方式处理。

selector 是 Python3.6 标准库,等之后更了解 select、poll、epoll 之后写一篇总结。
(不知道何年何月啊)

硬件故障坑死人(一)

背景

因为公司提供的产品不单单是软件形式提供,还对应的提供一体机形式(服务器 & 相应软件)。正式工作2年多也接触到了一些硬件的坑,特此总结。

磁盘

因为公司主要提供的产品是分布式存储和虚拟化相关产品,最直接的影响也是产生范围最大的影响就是磁盘了,会直接导致存储出现单副本等问题,从而产生数据恢复,影响集群稳定性。

固件版本

数据中心级别磁盘,相比于性能的要求,稳定性才是重中之重。大部分厂商的磁盘均支持 S.M.A.R.T. 规范,也就是“Self-Monitoring Analysis and Reporting Technology”,即“自我监测、分析及报告技术”,是一种自动的硬盘状态检测与预警系统和规范。我们可以通过相应命令比如 smartctl 直接获取磁盘相应信息,或者对磁盘进行检测。

S.M.A.R.T. 存在两个问题:

  1. 大部分厂商支持,意味着一部分厂商不支持
  2. 不同厂商对于自家磁盘的关键字定义不同

针对上述两个问题,我们只能说做到尽量多的测试踩坑,防止出现意外情况。

介绍了 S.M.A.R.T. ,我们来看看这节标题,固件。磁盘固件版本可以通过 smartctl 或者 sg_utils 工具获取:

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
[root@node 07:47:14 ~]$smartctl -i /dev/sdc
smartctl 6.5 2016-05-07 r4318 [x86_64-linux-3.10.0-693.11.6.el7.smartx.1.x86_64] (local build)
Copyright (C) 2002-16, Bruce Allen, Christian Franke, www.smartmontools.org

=== START OF INFORMATION SECTION ===
Model Family: Intel 730 and DC S35x0/3610/3700 Series SSDs
Device Model: INTEL SSDSC2BA400G4
Serial Number: BTHV518009D3400NGN
LU WWN Device Id: 5 5cd2e4 04c00728c
Firmware Version: G2010160
User Capacity: 400,088,457,216 bytes [400 GB]
Sector Sizes: 512 bytes logical, 4096 bytes physical
Rotation Rate: Solid State Device
Form Factor: 2.5 inches
Device is: In smartctl database [for details use: -P show]
ATA Version is: ACS-3 T13/2161-D revision 5
SATA Version is: SATA 3.1, 6.0 Gb/s (current: 6.0 Gb/s)
Local Time is: Wed Sep 26 07:47:20 2018 CST
SMART support is: Available - device has SMART capability.
SMART support is: Enabled

[root@node 07:47:07 ~]$sg_inq /dev/sdc
standard INQUIRY:
PQual=0 Device_type=0 RMB=0 version=0x05 [SPC-3]
[AERC=0] [TrmTsk=0] NormACA=0 HiSUP=0 Resp_data_format=2
SCCS=0 ACC=0 TPGS=0 3PC=0 Protect=0 [BQue=0]
EncServ=0 MultiP=0 [MChngr=0] [ACKREQQ=0] Addr16=0
[RelAdr=0] WBus16=0 Sync=0 Linked=0 [TranDis=0] CmdQue=0
[SPI: Clocking=0x0 QAS=0 IUS=0]
length=96 (0x60) Peripheral device type: disk
Vendor identification: ATA
Product identification: INTEL SSDSC2BA40
Product revision level: 0160
Unit serial number: BTHV518009D3400NGN

可以看到上面这块磁盘 /dev/sdc 的固件版本就是 0160
我们日常看到的磁盘根据出厂时间的不同,对应的固件版本也不同,平时也都没有在意固件版本。但是某次在进行 POC 时,发现性能不稳定,在排除了存储系统问题后,直接对该磁盘进行 Fio 测试,发现确实是磁盘自身性能不稳定。这块磁盘是 Intel 当时的中高端产品 S3710 系列,理论上不应该存在问题,经过排查,最终确定是固件版本导致的,通过 Intel 提供的升级工具升级后,性能恢复正常。

存储控制器

blkdiscard ,用来清理磁盘扇区的操作。某天 POC 过程中发现当 SSD 进行 blkdiscard 时,直接导致该 SSD IO Error。

当时想法:

  • 第一想法是该磁盘有问题,不支持,结果发现是惠普 OEM Intel 的 S3520 SSD,应该是支持相关指令的;
  • 想到上面一节提到的固件版本问题,由于磁盘是 OEM 的原因,固件只能更新惠普提供的固件,当时去惠普官方网站查询发现已经是最新版本;

上面两个原因都不是,当时没有什么其他的想法,我司售前文工提到,有没有可能是存储控制器的原因?
检查当时的存储控制器,是惠普的一块型号为 Smart Array P440ar Controller 的控制器。检查该控制器固件,查询官网,发现不是最新版本,于是下载更新,控制器固件版本更新方式有两种:

  1. DOS 更新
  2. UEFI 更新

两种方式都是将固件文件放置到 U盘 或者其他存储介质中,然后启动 OS,进行更新。由于现在新款服务器均带 UEFI ,那么方式 2 会简单一些。

更新固件后,发现磁盘执行 blkdiscard 无报错,检查命令返回值($?)也是 0 ,问题解决。

电源

通常服务器配置双路电源,避免单一故障,此为前提。

导致 CPU 频率过低

某次测试,由于上架偷懒,只连接了单路电源就开始进行测试。开始功能测试一切正常,到了性能测试,发现相同物理设备下,这台机器性能比之前验证结果低 20%,发现 CPU 频率过低,查看 /proc/cpuinfo 发现部分 core 低于标准主频数。

查看 IPMI & BIOS 配置,并无异常配置,咨询服务器厂商 400,提示是否是由于电源供电不足导致的,插上第二个电源后,CPU 频率稳定,sysbench 运行结果符合标准,问题解决。

磁盘闪断

在服务器正常运行过程中,磁盘的任何故障都会导致业务受到严重影响,哪怕是分布式存储,采用副本机制,如果同时有多个服务器出现磁盘闪断,后果也是极为严重的。

现在各大服务器厂商出货量较多的均为高密度服务器,也就是我们见到的一个机箱内部同时存在多个节点,比如:

  • 四子星就是一个机箱内部有4个节点,如果前置面板磁盘插槽(2.5 寸)为 24 的话,那么每个节点可以连接磁盘数为 6,;
  • 双子星就是一个机箱内部有 2 个节点,如果前置面板为 3.5 寸磁盘的话,那么每个节点连接磁盘数为 6;
  • 双子星如果前置面板为 2.5 的话,那么每个节点连接磁盘数为 12。

这样的服务器好处就是在同一个机箱中,可以放置更多的 CPU内存,成本低,功耗低。但是它的缺点同样明显:多个节点采用同一电源,若电源出现故障,会导致整个机箱内的所有节点出现故障。

最近发现某个机器频繁出现整个节点的磁盘同时断开再连接的场景,由于是双子星,也就是两个节点共用同一电源,影响较大。

最开始这种涉及到整个节点所有磁盘的故障,想法是这样的:

  • 节点操作系统日志只有磁盘连接断开的日志,无特殊问题
  • 整个节点所有磁盘故障,应该不是单一磁盘问题,估计是控制器问题
  • 检查控制器日志,没有发现报错
  • 检查控制器固件版本,已为最新版本
  • 检查控制器连线,连线正常

想法到这里就断了,只能求助服务器厂商了,厂商检查后发现是电源背板问题,由于电源背板故障,导致供电不足,磁盘连接一直处于连接断开重复状态。

总结

暂时总结了印象比较深的几次硬件故障,由于大家现在都是只做软件,对硬件了解仅限于概念,随着云计算的兴起,很多同学可能没见过真正的服务器,更别提遇到这些诡异的故障。

硬件故障特别难定位,如果没有一定的相关经验,估计会像我一样捉瞎。希望这篇文章对大家有所帮助。

OpenvSwitch Active-Backup failback 验证

背景

在虚拟化场景下,我们经常使用 OpenvSwitch 进行虚拟网络配置,最近看到有人问,Open vSwitch 的 bonding 模式 Avtice-Backup ,是否支持 failback 功能? 虽然一直经常使用该模式,但是不知道当故障恢复后,是否会出现故障恢复?
看官方文档中描述感觉有些模糊,来验证下。

验证方式

配置 VDS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@node 17:14:54 ~]$ovs-vsctl show
b9956069-4101-4aab-a8a2-86db4f5ae390
Bridge ovsbr-mgt
Port bond-mgt
Interface "eno2"
Interface "eno1"
Port port-mgt
tag: 0
Interface port-mgt
type: internal
Port ovsbr-mgt
Interface ovsbr-mgt
type: internal
ovs_version: "2.3.1"
1
2
3
[root@node 17:14:59 ~]$ovs-appctl bond/list
bond type recircID slaves
bond-mgt active-backup 0 eno1, eno2

故障前流量检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@node 17:15:58 ~]$ifconfig eno1
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::ec4:7aff:fe0f:8f68 prefixlen 64 scopeid 0x20<link>
ether 0c:c4:7a:0f:8f:68 txqueuelen 1000 (Ethernet)
RX packets 10046643 bytes 1131705628 (1.0 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4182418 bytes 443990895 (423.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdfd20000-dfd3ffff

[root@node 17:16:45 ~]$ifconfig eno2
eno2: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
ether 0c:c4:7a:0f:8f:69 txqueuelen 1000 (Ethernet)
RX packets 100 bytes 10240 (100 KiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 100 bytes 10240 (100 KiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdfd00000-dfd1ffff

故障后流量检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@node 17:15:58 ~]$ifconfig eno1
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::ec4:7aff:fe0f:8f68 prefixlen 64 scopeid 0x20<link>
ether 0c:c4:7a:0f:8f:68 txqueuelen 1000 (Ethernet)
RX packets 10046643 bytes 1131705628 (1.0 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4182418 bytes 443990895 (423.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdfd20000-dfd3ffff

[root@node 17:16:45 ~]$ifconfig eno2
eno2: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
ether 0c:c4:7a:0f:8f:69 txqueuelen 1000 (Ethernet)
RX packets 22222222 bytes 2231705628 (1.0 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 2231705628 bytes 2231705628 (1.0 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdfd00000-dfd1ffff

故障恢复后流量检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@node 17:15:58 ~]$ifconfig eno1
eno1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet6 fe80::ec4:7aff:fe0f:8f68 prefixlen 64 scopeid 0x20<link>
ether 0c:c4:7a:0f:8f:68 txqueuelen 1000 (Ethernet)
RX packets 10046643 bytes 1131705628 (1.0 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 4182418 bytes 443990895 (423.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdfd20000-dfd3ffff

[root@node 17:16:45 ~]$ifconfig eno2
eno2: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
ether 0c:c4:7a:0f:8f:69 txqueuelen 1000 (Ethernet)
RX packets 33333333 bytes 3331705628 (1.0 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 3331705628 bytes 3331705628 (1.0 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdfd00000-dfd1ffff

结论

可以看到整体流程中流量占用网口情况,主要看 RX 和 TX packets 数量:

  1. 故障前:eno1
  2. 故障后:eno2
  3. 故障恢复后:eno2

在 OpenvSwitch Active-Backup 场景下,未支持 failback 功能,我理解是因为这两块网卡完全等价,failback 在 Master-Slave 场景下更有用一些。

cgroups 常用配置

背景

在软件运行过程中,我们经常需要限制 CPU 、内存、磁盘的使用,方式程序超出了限定边界范围。在 Linux 中,我们可以通过 cgroups 来进行限制。

cgroups

中文名称为控制组群,具体功能分类为:

  • 资源限制:组可以被设置不超过设定的内存限制;这也包括虚拟内存。
  • 优先级:一些组可能会得到大量的CPU或磁盘IO吞吐量。
  • 结算:用来衡量系统确实把多少资源用到适合的目的上。
  • 控制:冻结组或检查点和重启动。

下面来说下常见的使用方式

CPU

RedHat 官方文档中描述 cgroups 在 RHEL7/CentOS7 之后的版本需要通过 systemd 配置,不再使用 libcgconfig 方式。
但是在 systemd 的配置中,CPU 相关的配置项比较简单,或者说 OS 自动配置了很多,没有暴露出来。所以我们这里还是采用 libcgconfig 的配置方式。

在进行 CPU 限制之前,我们需要了解一下 NUMA 结构。什么是 NUMA? NUMA 是一种为多处理器的计算机设计的内存,内存访问时间取决于内存相对于处理器的位置。在NUMA下,处理器访问它自己的本地内存的速度比非本地内存快一些。
不同的 Thread 在同一个 Core 上也会发生抢占情况,具体可以通过 sysbench 进行测试。

相关概念定义:

  1. Socket:物理服务器上的 CPU 插槽
  2. Core:物理 CPU 核心数
  3. Thread:超线程

简单的说就是如果你的程序是计算密集型,那么尽可能的要让 CPU 限制在同一个 NUMA node 上。
查看 NUMA node 方式:

1
2
3
4
5
6
7
8
9
10
11
12
[root@test 10:53:43 ~]$numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30
node 0 size: 65221 MB
node 0 free: 52239 MB
node 1 cpus: 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31
node 1 size: 65536 MB
node 1 free: 57566 MB
node distances:
node 0 1
0: 10 21
1: 21 10

这台服务器的配置为 2(socket) * Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz ,内存为 128 GB。观察 NUMA node 分配,可以看到分为两个 node,分别为 node 0 和 node 1。
那么 node $index cpus 字段代表的是什么意思呢? 在这里对应的是两个 socket ,也就是两个物理插槽上的 CPU 对应的 Thread。 Thread 排序方式是优先按照不同 Core 排序的,比如 Thread 0 在 Core 0 上,Thread 2 在 Core 1 上,以此类推。
具体的 CPU 配置大家可以通过 lscpu , 或者查看 /proc/cpuinfo 了解。

根据 NUMA 配置,我们可以选择将程序限制在 Thread 0,2,4,6,8 上。
那么我们编辑配置文件 /etc/cgconfig.conf

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@test 11:03:45 ~]$cat /etc/cgconfig.conf |head -n 25
# yiran cgroups configuration

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

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

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

首先,我们定义了一个组,组名叫 yiran,我们又在 yiran 下创建了一个叫 flask 的组,限制 CPU在 0,2,4 上。
接下来我们重启 cgconfig 相关服务,注意遵守重启顺序:

1
2
[root@test 11:04:45 ~]systemctl restart cgconfig
[root@test 11:04:46 ~]systemctl restart cgred

我们可以通过 lscgroup 检测配置是否生效,或者通过查看 /proc/$pid/cgroup 检查。

内存

相对于 CPU,内存配置我们可以直接通过 systemd 配置,就简化很多了。
最简单的,如果我们想立即配置一个服务的内存限制,我们可以直接执行命令:

1
[root@test 11:05:50 ~]systemctl set-property <service name> MemoryLimit=500M

执行完这条命令后,系统会自动在该服务的 systemd 配置路径(/etc/systemd/system/<service name>.d/ 下生成文件 50-MemoryLimit.conf ,文件内容为:

1
2
[Slice]
MemoryLimit=5368709120

当然我们如果直接在该路径下编写配置文件也是可以的,重启服务后生效。

Slice 配置

上面这种方式只是针对单一的服务配置,如果我们想创建一个组,控制多个服务计算资源总和的配置,我们就需要通过配置 Slice 完成。

slice —— 一组按层级排列的单位。slice 并不包含进程,但会组建一个层级,并将 scope 和 service 都放置其中。真正的进程包含在 scope 或 service 中。在这一被划分层级的树中,每一个 slice 单位的名字对应通向层级中一个位置的路径。小横线(”-“)起分离路径组件的作用。例如,如果一个 slice 的名字是:

parent-name.slice

这说明 parent-name.sliceparent.slice 的一个子 slice。这一子 slice 可以再拥有自己的子 slice,被命名为:parent-name-name2.slice,以此类推。

根 slice 的表示方式:-.slice

我们在 /etc/systemd/system/ 下创建一个组,名称还是 yiran ,创建 flaskyiran 下:

1
2
3
4
5
6
/etc/systemd/system
├── flask.service.d
│ └── cgroup.conf
├── system-yiran.slice
├── system-yiran.slice.d
│ └── 50-MemoryLimit.conf

配置完成后,我们重启 cgconfig 相关服务即可使能配置。

磁盘

如要降低 flask 服务读取某个目录 block IO 的权重,只需要修改该服务配置文件 增加 BlockIODeviceWeight=/home/jdoe 750 字段即可。

后续

日常用到最多的应该就是 CPU 和内存,如果很多对 IO 要求高的服务运行在一块磁盘上,那么可以先通过针对不同服务进行不同磁盘分区的方式(此方式不包括 lvm)验证,如果还不能解决的话,再考虑针对服务控制磁盘 IO。

为了追求性能,我们在 CPU 配置的时候给应用程序分配的 Thread 都位于同一个 Socket 上 CPU 的不同的 Core 上。 老板说还要考虑网卡、磁盘等 PCI 设备接入的 Socket ,如果应用程序分配在了 NUMA node0 上,但是网卡、磁盘等 PCI 设备连接在了 NUMA node1 上,还是会对性能有影响,还没找到相应配置,后续有时间再了解吧。

参考链接

基于zeroconf实现节点自发现

背景

通常我们使用联网的电子设备,都会配置一个 IP 地址用于通信,一般采用 DHCP 配置,DHCP 有
lease 时间,如果超过 lease 时间又没有续约的话,产生的 IP 地址有可能发生改变,那么如何
自动识别我们的设备呢? ZeroConf 是一个好的选择。

ZeroConf

以下介绍摘录自维基百科:

Zero-configuration networking (zeroconf) is a set of technologies that
automatically creates a usable computer network based on the
Internet Protocol Suite (TCP/IP) when computers or network peripherals are
interconnected. It does not require manual operator intervention or special
configuration servers. Without zeroconf, a network administrator must set up
network services, such as Dynamic Host Configuration Protocol (DHCP) and
Domain Name System (DNS), or configure each computer’s network settings manually.
Zeroconf is built on three core technologies: automatic assignment of numeric
network addresses for networked devices, automatic distribution and resolution
of computer hostnames, and automatic location of network services, such as printing devices.

简单来说我们可以通过 ZeroConf 进行服务自发现,日常使用最多的就是 Apple 家的产品及一些
打印设备。

Avahi

如果我们想要在 Linux 中使用的话,我们可以选择 Avahi,配合 nss-mdns 一起使用可以在复杂网络场景下自动发现服务器,并获取 IPv4 或 IPv6 地址进行通信。

扫描节点安装

我们使用 yum 进行相应工具的安装:

1
yum install avahi-libs avahi-tools avahi-autoipd avahi nss-mdns

扫描节点配置

安装完成后,我们修改配置文件,开启 IPv6 配置:

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
[root@yiran-pxe data]# cat /etc/nsswitch.conf |grep -v ^# |grep -v ^$
passwd: files sss
shadow: files sss
group: files sss
hosts: files mdns4_minimal [NOTFOUND=return] dns
bootparams: nisplus [NOTFOUND=return] files
ethers: files
netmasks: files
networks: files
protocols: files
rpc: files
services: files sss
netgroup: files sss
publickey: nisplus
automount: files
aliases: files nisplus
[root@yiran-pxe data]# cat /etc/avahi/avahi-daemon.conf |grep -v ^# |grep -v ^$
[server]
use-ipv4=no
use-ipv6=yes ## 开启 IPv6
ratelimit-interval-usec=1000000
ratelimit-burst=1000
[wide-area]
enable-wide-area=yes
[publish]
[reflector]
[rlimits]
rlimit-core=0
rlimit-data=4194304
rlimit-fsize=0
rlimit-nofile=768
rlimit-stack=4194304
rlimit-nproc=3

配置完成后我们启动服务:

1
systemctl start avahi-daemon

被扫描节点配置

扫描节点可以通过制定的域名进行扫描,我们可以通过给被扫描节点配置特殊的主机名称,来实现自动识别被扫描节点具体是什么类型的服务器,比如我们可以通过 ipmitool 获取 IPMI IP,并配置主机名为 node-192-168-67-173 来实现识别,具体操作:

1
2
3
[root@node-192-168-67-173 ~]# cat /etc/hostname 
node-192-168-67-173
[root@node-192-168-67-173 ~]# hostname -F /etc/hostname

同样编辑 avahi 配置文件,开启 IPv6 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@node-192-168-67-173 avahi]# cat avahi-daemon.conf |grep -v ^# |grep -v ^$
[server]
use-ipv4=yes
use-ipv6=yes
ratelimit-interval-usec=1000000
ratelimit-burst=1000
[wide-area]
enable-wide-area=yes
[publish]
publish-a-on-ipv6=yes
[reflector]
[rlimits]
rlimit-core=0
rlimit-data=4194304
rlimit-fsize=0
rlimit-nofile=768
rlimit-stack=4194304
rlimit-nproc=3

启动 avahi 服务:

1
systemctl start avahi-daemon

扫描节点发现节点

我们可以通过 avahi-tools 提供的 avahi-brower 工具进行持续扫描:

1
2
3
[root@yiran-pxe data]# avahi-browse -a
+ eno33559296 IPv6 node-192-168-67-173 [24:6e:96:7c:50:70] Workstation local
+ eno16780032 IPv6 node-192-168-67-173 [24:6e:96:7c:50:50] Workstation local

可以看到我们在扫描节点已经发现了被扫描节点的信息,那么我们接下来获取具体的 IP 地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@yiran-pxe data]# avahi-resolve --help 
avahi-resolve [options] -n <host name ...>
avahi-resolve [options] -a <address ... >

-h --help Show this help
-V --version Show version
-n --name Resolve host name
-a --address Resolve address
-v --verbose Enable verbose mode
-6 Lookup IPv6 address
-4 Lookup IPv4 address
[root@yiran-pxe data]# avahi-resolve -n node-192-168-67-173.local -4
node-192-168-67-173.local 127.0.0.1
[root@yiran-pxe data]# avahi-resolve -n node-192-168-67-173.local -6
node-192-168-67-173.local fe80::266e:96ff:fe7c:5070

如果被扫描节点没有 IPv4 地址,则会显示 127.0.0.1,若有 IPv4 地址,则会正常显示。

远程通信

在得到具体的 IP 地址之后我们就可以按照上一篇博客提到的 IPv6 地址远程连接的方式进行通信了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@yiran-pxe data]# ping6 fe80::266e:96ff:fe7c:5070%eno33559296
PING fe80::266e:96ff:fe7c:5070%eno33559296(fe80::266e:96ff:fe7c:5070) 56 data bytes
64 bytes from fe80::266e:96ff:fe7c:5070: icmp_seq=1 ttl=64 time=0.448 ms
64 bytes from fe80::266e:96ff:fe7c:5070: icmp_seq=2 ttl=64 time=0.338 ms
^C
--- fe80::266e:96ff:fe7c:5070%eno33559296 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.338/0.393/0.448/0.055 ms
[root@yiran-pxe data]# ssh fe80::266e:96ff:fe7c:5070%eno33559296
root@fe80::266e:96ff:fe7c:5070%eno33559296's password:
Last login: Wed Aug 22 15:44:17 2018 from fe80::250:56ff:fe9f:ef9c%eno3
[root@node-192-168-67-173 ~]# hostname
node-192-168-67-173
[root@node-192-168-67-173 ~]# logout
Connection to fe80::266e:96ff:fe7c:5070%eno33559296 closed.

总结

目前 Linux 发行版本都自动安装了 Avahi 服务,大家可以启用,方便远程连接控制。