Skip to main content

· 36 分钟阅读

目录:

一、face_recognition是什么

二、如何安装

三、原理四、演示

五、手写简单的神经网络

六、结尾

一、face_recognition是什么

1. face_recognition是一个强大、简单、易上手的人脸识别开源项目,并且配备了完整的开发文档和应用案例。
2. 基于业内领先的C++开源库 dlib中的深度学习模型,用Labeled Faces in the Wild人脸数据集进行测试,有高达99.38%的准确率。

二、 如何安装

Linux下配置face_recognition

具体详情参考我的博客: https://www.cnblogs.com/UniqueColor/p/10992407.html

1、如linux下已有python2.7,但需要更新一下python 2.7至python2.x

sudo add-apt-repository ppa:fkrull/deadsnakes-python2.7
sudo apt-get update
sudo apt-get upgrade

2、部署步骤

安装Boost, Boost.Python
sudo apt-get install build-essential cmake
sudo apt-get install libgtk-3-dev
sudo apt-get install libboost-all-dev

 Installation of Cmake:(it tooks a while to install ~1.5 min)

sudo wget  https://cmake.org/files/v3.9/cmake-3.9.0-rc5.tar.gz -O cmake.tar.gz
sudo tar -xvf cmake.tar.gz
cd cmake-3.9.0-rc5/
sudo chmod +x bootstrap
sudo ./bootstrap
sudo make
sudo make install
注:安装好cmake后,输入cmake -version查看cmake版本是否安装成功。

 pip installation

$ wget https://bootstrap.pypa.io/get-pip.py
$ sudo python get-pip.py
注:安装完成后,终端输入pip -V查看pip版本是否安装成功。
注:如果使用python3.x版本,最后一步命令python改为python3

通过手动编译dlib的方式进行安装dlib

git clone https://github.com/davisking/dlib.git  //Clone the code from github
cd dlib
mkdir build
cd build
cmake ..                        //以默认方式(SSE41指令)编译dlib
cmake --build .
cd ..
sudo python setup.py install
注:最后一步需要等待一些时间。如果使用python3.x版本,最后一步命令python改为python3

安装完成后,运行python,输入 import dlib 此时执行成功。

安装face_recognition

sudo pip install face_recognition

安装完成后,运行python,输入  import face_recognition 此时执行成功。

安装opencv-python

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python

测试:运行python,输入 import cv2 此时执行成功。

Mac下配置face_recognition

具体详情参考我的博客 https://www.cnblogs.com/UniqueColor/p/10992415.html

安装依赖库:

1、安装cmake (是一个跨平台的安装工具)

 brew install cmake

2、安装boost、boost-python(C++的程序库)

brew install boost
brew install boost-python --with-python2.7

3、编译dlib

git clone https://github.com/davisking/dlib.git  
cd dlib
mkdir build
cd build
cmake ..                        //以默认方式(SSE41指令)编译dlib
cmake --build .
cd ..
sudo python setup.py install
  1. 安装人脸识别的python库
pip install face_recognition

5、安装opencv-python

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python

此时报错:

Cannot uninstall 'numpy'. It is a distutils installed project and thus we cannot accurate

解决方案:强行升级

pip install numpy --ignore-installed numpy

三、原理:

1. HOG

HOG是什么?

HOG即方向梯度直方图。特征是一种在计算机视觉和图像处理中用来进行物体检测的特征描述子。HOG特征通过计算和统计图像局部区域的梯度方向直方图来构成特征。

使用 HOG 算法给图片编码,以创建图片的简化版本。 使用这个简化的图像,找到其中看起来最像通用 HOG 面部编码的部分。

步骤:

   1.1  将图像转换为黑白,为了降维,不用考虑色彩

   1.2  查看图片中的每一个像素。 对于单个像素,同时要查看它周围的其他像素。目的是找出并比较当前像素与直接围绕它的像素的深度,形成箭头方向。通过对图片中的每一个像素重复这个过程,最终每个像素会被一个箭头取代。这些箭头被称为梯度(gradients),它们能显示出图像上从明亮到黑暗的流动过程。(使用梯度替代像素的原因是,同一个人在明暗不同的两张照片中具有不同的像素,但是如果只是分析梯度,明暗不同的两张照片也会得到相同的结果)。

  1.3 计算每一个像素未免太繁琐,所以将图像分割成一些 16×16 像素的小方块。在每个小方块中,我们将计算出每个主方向上有多少个梯度(有多少指向上,指向右上,指向右等)。然后我们将用指向性最强那个方向的箭头来代替原来的那个小方块。

具体文字算法步骤:

1、读入检测图片即输入的image

2、将图像灰度化(即将输入的彩色的图像的r,g,b值通过特定公式转换为灰度值)

3、采用Gamma校正法对输入图像进行颜色空间的标准化(归一化)

4、计算图像每个像素的梯度(包括大小和方向),捕获轮廓信息。如果我们直接分析像素,同一个人明暗不同的两张照片将具有完全不同的像素值。但是如果只考虑亮度变化方向(direction)的话,明暗图像将会有同样的结果

5、统计每个cell的梯度直方图(不同梯度的个数),形成每个cell的descriptor

6、将每几个cell(如上文提到的16*16)组成一个block,一个block内所有cell的特征串联起来得到该block的HOG特征descriptor

7、将image里所有block的HOG特征descriptor串联起来得到该image(检测目标)的HOG特征descriptor,得到最终分类的特征向量

附上HOG实现代码: 

  # coding=utf-8
import cv2
import numpy as np
import math
import matplotlib.pyplot as plt

# 1、读入检测图片即输入的image
# 2、将图像灰度化(即将输入的彩色的图像的r,g,b值通过特定公式转换为灰度值)
# 3、采用Gamma校正法对输入图像进行颜色空间的标准化(归一化)
# 4、计算图像每个像素的梯度(包括大小和方向),捕获轮廓信息。如果我们直接分析像素,同一个人明暗不同的两张照片将具有完全不同的像素值。但是如果只考虑亮度变化方向(direction)的话,明暗图像将会有同样的结果
# 5、统计每个cell的梯度直方图(不同梯度的个数),形成每个cell的descriptor
# 6、将每几个cell(如上文提到的16*16)组成一个block,一个block内所有cell的特征串联起来得到该block的HOG特征descriptor
# 7、将image里所有block的HOG特征descriptor串联起来得到该image(检测目标)的HOG特征descriptor,得到最终分类的特征向量

class Hog_descriptor():
def __init__(self, img, cell_size=16, bin_size=8):
# 数据处理
self.img = img
# 采用Gamma校正法对输入图像进行颜色空间的标准化(归一化),调节图像的对比度
self.img = np.sqrt(img / np.max(img))

self.img = img * 255
self.cell_size = cell_size
self.bin_size = bin_size
self.angle_unit = 360 / self.bin_size

def extract(self):
height, width = self.img.shape
# 1、计算每个像素的梯度
gradient_magnitude, gradient_angle = self.global_gradient()
gradient_magnitude = abs(gradient_magnitude)

# 2、开始为细胞单元构建梯度方向直方图
cell_gradient_vector = np.zeros(
(height / self.cell_size, width / self.cell_size, self.bin_size)) # 初始化8个直方图构成的向量组,即构成细胞元,前面两个参数表示每个直方图长和宽
# 3、以直方图为单位 循环填充所有细胞元
for i in range(cell_gradient_vector.shape[0]):
for j in range(cell_gradient_vector.shape[1]):
cell_magnitude = gradient_magnitude[i * self.cell_size:(i + 1) * self.cell_size,
j * self.cell_size:(j + 1) * self.cell_size]
cell_angle = gradient_angle[i * self.cell_size:(i + 1) * self.cell_size,
j * self.cell_size:(j + 1) * self.cell_size]
cell_gradient_vector[i][j] = self.cell_gradient(cell_magnitude, cell_angle)

# 4、之后,通过所有的细胞元获取hog图像
hog_image = self.render_gradient(np.zeros([height, width]), cell_gradient_vector)
hog_vector = []
for i in range(cell_gradient_vector.shape[0] - 1):
for j in range(cell_gradient_vector.shape[1] - 1):
block_vector = []
block_vector.extend(cell_gradient_vector[i][j])
block_vector.extend(cell_gradient_vector[i][j + 1])
block_vector.extend(cell_gradient_vector[i + 1][j])
block_vector.extend(cell_gradient_vector[i + 1][j + 1])
mag = lambda vector: math.sqrt(sum(i ** 2 for i in vector))
magnitude = mag(block_vector)
if magnitude != 0:
normalize = lambda block_vector, magnitude: [element / magnitude for element in block_vector]
block_vector = normalize(block_vector, magnitude)
hog_vector.append(block_vector)
return hog_vector, hog_image

# 计算每个像素的梯度
def global_gradient(self):
gradient_values_x = cv2.Sobel(self.img, cv2.CV_64F, 1, 0, ksize=5) # 对X求导检测X方向上是否有边缘,即求出X方向的梯度分量
gradient_values_y = cv2.Sobel(self.img, cv2.CV_64F, 0, 1, ksize=5) # 对Y求导检测Y方向上是否有边缘,即求出Y方向的梯度分量
gradient_magnitude = cv2.addWeighted(gradient_values_x, 0.5, gradient_values_y, 0.5,
0) # 对XY方向混合加权,都为0.5的weight,即求出梯度值
gradient_angle = cv2.phase(gradient_values_x, gradient_values_y, angleInDegrees=True) # 计算方向
# print(gradient_magnitude, gradient_angle)
return gradient_magnitude, gradient_angle

# 通过细胞元内,每个像素的梯度值与细胞元的角度 求出整个细胞元的梯度
def cell_gradient(self, cell_magnitude, cell_angle):
orientation_centers = [0] * self.bin_size
for i in range(cell_magnitude.shape[0]):
for j in range(cell_magnitude.shape[1]):
gradient_strength = cell_magnitude[i][j]
gradient_angle = cell_angle[i][j]
min_angle, max_angle, mod = self.get_closest_bins(gradient_angle)
orientation_centers[min_angle] += (gradient_strength * (1 - (mod / self.angle_unit)))
orientation_centers[max_angle] += (gradient_strength * (mod / self.angle_unit))
return orientation_centers

def get_closest_bins(self, gradient_angle):
idx = int(gradient_angle / self.angle_unit)
mod = gradient_angle % self.angle_unit
return idx, (idx + 1) % self.bin_size, mod

# 以细胞元为单位构建整张图
def render_gradient(self, image, cell_gradient):
cell_width = self.cell_size / 2
max_mag = np.array(cell_gradient).max()
for x in range(cell_gradient.shape[0]):
for y in range(cell_gradient.shape[1]):
cell_grad = cell_gradient[x][y]
cell_grad /= max_mag
angle = 0
angle_gap = self.angle_unit
for magnitude in cell_grad: angle_radian = math.radians(angle) # 角度转化为弧度
x1 = int(x * self.cell_size + magnitude * cell_width * math.cos(angle_radian))
y1 = int(y * self.cell_size + magnitude * cell_width * math.sin(angle_radian))
x2 = int(x * self.cell_size - magnitude * cell_width * math.cos(angle_radian)) y2 = int(y * self.cell_size - magnitude * cell_width * math.sin(angle_radian))
cv2.line(image, (y1, x1), (y2, x2), int(255 * math.sqrt(magnitude)))
angle += angle_gap
return image

img = cv2.imread('Stephen_Curry.jpg', cv2.IMREAD_GRAYSCALE)
hog = Hog_descriptor(img, cell_size=8, bin_size=8)
vector, image = hog.extract()
print np.array(vector).shape
plt.imshow(image, cmap=plt.cm.gray)
plt.show()

  运行结果: 

2. 图像扭转

场景:对于电脑来说,面朝不同方向的同一张脸,是不同的东西。所以第二步需要对脸部扭转。

通过找到脸上的68个主要特征点,找出脸部的姿势。一旦我们找到这些特征点,就利用它们把图像扭曲,使眼睛和嘴巴居中。

通过使用称为面部特征点估计(face landmark estimation)的算法可以解决这一问题,基本思路:这一算法的基本思路是找到 68 个人脸上普遍存在的特定点(称为特征点, landmarks)——包括下巴的顶部、每只眼睛的外部轮廓、每条眉毛的内部轮廓等。接下来我们训练一个机器学习算法,让它能够在任何脸部找到这 68 个特定的点。

既然已经知道了眼睛和嘴巴在哪儿,接下来将图像进行旋转、缩放和错切,使得眼睛和嘴巴尽可能靠近中心。这里不会做任何花哨的三维扭曲,因为这会让图像失真。只会使用那些能够保持图片相对平行的基本图像变换,比如旋转和缩放(称为仿射变换):

3.  神经网络

现在进入到最关键的一部分内容,就是如何区分不同的人脸?

解决方案是训练一个深度卷积神经网络,但是,并不是让它去识别图片中的物体,这一次我们的训练是要让它为脸部生成 128 个测量值。

每次训练要观察三个不同的脸部图像:

. 加载一张已知的人的面部训练图像
. 加载同一个人的另一张照片
. 加载另外一个人的照片

然后,算法查看它自己为这三个图片生成的测量值。再然后,稍微调整神经网络,以确保第一张和第二张生成的测量值接近,而第二张和第三张生成的测量值略有不同。 把上一步得到的面部图像放入神经网络中,神经网络知道如何找到 128 个特征测量值。保存这 128 个测量值。

在为几千个人的数百万图像重复该步骤数百万次之后,神经网络学习了如何可靠地为每个人生成 128 个测量值。对于同一个人的任何十张不同的照片,它都应该给出大致相同的测量值。 128个测试值对于我们来说,并不需要知道它们是什么东西,也不需要它们是如何计算出来的,这是计算机通过深度学习而得到的对它有意义的值。

4. 分类

看看我们过去已经测量过的所有脸部,找出哪个人的测量值和我们要测量的面部最接近。这一步相对简单,使用各类分类算法即可,下面有两种分类算法推荐。

k-means:k均值聚类算法(k-means clustering algorithm) 参考我的博客 https://www.cnblogs.com/UniqueColor/p/10996269.html

KNN:K最近邻(k-NearestNeighbor)参考我的博客 https://www.cnblogs.com/UniqueColor/p/10996331.html

四、演示(使用Face Recognition)

 1、识别到照片上的所有人脸

# -*- coding: utf-8 -*-
# 检测人脸
import face_recognition
import cv2
# 读取图片并识别人脸
img = face_recognition.load_image_file("scl.jpg")
face_locations = face_recognition.face_locations(img)
print(face_locations)

# 调用opencv函数显示图片
img = cv2.imread("scl.jpg")
# cv2.namedWindow("origin")
# cv2.imshow("origin", img)
# 遍历每个人脸,并标注
faceNum = len(face_locations)
for i in range(0, faceNum):
top = face_locations[i][0]
right = face_locations[i][1]
bottom = face_locations[i][2]
left = face_locations[i][3]
start = (left, top)
end = (right, bottom)
color = (55, 255, 155)
thickness = 3
cv2.rectangle(img, start, end, color, thickness)
# 显示识别结果
cv2.namedWindow("recognition")
cv2.imshow("recognition", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

  效果:

 2、照片上识别人脸并且确定是哪个人

 # -*- coding: utf-8 -*-
import face_recognition
from PIL import Image, ImageDraw
import numpy as np


# 加载数据源.
klay_image = face_recognition.load_image_file("Klay_Thompson.jpg")
klay_face_encoding = face_recognition.face_encodings(klay_image)[0]

# 加载数据源.
curry_image = face_recognition.load_image_file("Stephen_Curry.jpg")
curry_face_encoding = face_recognition.face_encodings(curry_image)[0]
# 创建已知人脸编码及其名称的数组
known_face_encodings = [
klay_face_encoding,
curry_face_encoding
]
known_face_names = [
"Klay Thompson",
"Stepthen Curry"
]
# 加载要识别的图像
unknown_image = face_recognition.load_image_file("scl.jpg")
# 查找未知图像中所有的脸和脸编码
face_locations = face_recognition.face_locations(unknown_image)
face_encodings = face_recognition.face_encodings(unknown_image, face_locations)
# 将图像转换为PIL格式的图像,创建绘制实例
pil_image = Image.fromarray(unknown_image)
draw = ImageDraw.Draw(pil_image)
# 循环要识别的图像中的每一张脸
for (top, right, bottom, left), face_encoding in zip(face_locations, face_encodings):
# 看看这张脸是否与已知的脸匹配
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
name = "Unknown"

# 使用将要识别的脸与已知的脸作比较,计算出距离最小的脸
face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = known_face_names[best_match_index]
# 在面部周围绘制一个方框
draw.rectangle(((left, top), (right, bottom)), outline=(0, 0, 255))
# 在脸下绘制名称
text_width, text_height = draw.textsize(name)
draw.rectangle(((left, bottom - text_height - 10), (right, bottom)), fill=(0, 0, 255), outline=(0, 0, 255))
draw.text((left + 6, bottom - text_height - 5), name, fill=(255, 255, 255, 255))

del draw
# 展示结果
pil_image.show()

 图片效果:

3、视频上实时识别人脸并且确定是哪个人

# -*- coding: utf-8 -*-
import face_recognition
import cv2
# 打开视频
input_movie = cv2.VideoCapture("hamilton_clip.mp4")
length = int(input_movie.get(cv2.CAP_PROP_FRAME_COUNT))
# 创建输出的视频
fourcc = cv2.VideoWriter_fourcc(*'XVID')
output_movie = cv2.VideoWriter('output.mp4', fourcc, 29.97, (640, 360))
# # 加载数据源.
lmm_image = face_recognition.load_image_file("lin-manuel-miranda.png")
lmm_face_encoding = face_recognition.face_encodings(lmm_image)[0]
# 加载数据源.
curry_image = face_recognition.load_image_file("Stephen_Curry.jpg")
curry_face_encoding = face_recognition.face_encodings(curry_image)[0]
# 创建已知人脸编码及其名称的数组
known_face_encodings = [
lmm_face_encoding,
curry_face_encoding
]

face_locations = []
face_encodings = []
face_names = []
frame_number = 0
while True:
# 抓取视频帧
ret, frame = input_movie.read()
frame_number += 1
# 抓取结束
if not ret:
break
# 将图像从bgr颜色(opencv使用)转换为rgb颜色(人脸识别使用)
rgb_frame = frame[:, :, ::-1]
# 找到视频中所有的人脸
face_locations = face_recognition.face_locations(rgb_frame)
face_encodings = face_recognition.face_encodings(rgb_frame, face_locations)
face_names = []
for face_encoding in face_encodings:
# 看看这张脸是否与已知的脸匹配
match = face_recognition.compare_faces(known_face_encodings, face_encoding, tolerance=0.50)
# 判断人脸逻辑
name = None
if match[0]:
name = "Lin-Manuel Miranda"
elif match[1]:
name = "Stepthen Curry"
face_names.append(name)
# 展示识别结果
for (top, right, bottom, left), name in zip(face_locations, face_names):
if not name:
continue
# 画框
cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)
# 写上人名
cv2.rectangle(frame, (left, bottom - 25), (right, bottom), (0, 0, 255), cv2.FILLED)
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (left + 6, bottom - 6), font, 0.5, (255, 255, 255), 1)
# 写入到结果视频中
print("Writing frame {} / {}".format(frame_number, length))
output_movie.write(frame)

input_movie.release()
cv2.destroyAllWindows()

视频由于比较大,就不做演示了,有兴趣可以自己试试。

五、手写神经网络

上文提到了神经网络,那么神经网络到底是什么?接下来就以一个简单的例子探探神经网络。

1. 神经元

如图所示生物神经元包括细胞体,树突,轴突等部分。

树突是用于接受输入信息,输入信息经过突触处理,当达到一定条件时通过轴突传出,此时神经元处于激活状态;反之没有达到相应条件,则神经元处于抑制状态。

受到生物神经元的启发,于是神经网络模型就出来了。神经元是神经网络的基本组成部分,它获取到输入后,然后执行某些数学运算后,产生了输出。

如下所示,将神经元转化成数学图形

在这个神经元中,设x1、x2为输入,w1、w2为权重,b为偏置,

按照上图的数学运算有:

 x1w1+x2w2+b 

最后再经过激活函数处理得到:

y = f(x1 × w1 + x2 × w2 + b)

思考:为什么使用激活函数? 

如果不使用激励函数(相当于激活函数是f(x) = x),在这种情况下你每一层节点的输入都是上层输出的线性函数,很容易验证,无论你神经网络有多少层,输出都是输入的线性组合,与没有隐藏层效果相当,这种情况就是最原始的感知机(Perceptron)了,那么神经网络的逼近能力就相当有限。

正因如此,引入非线性函数作为激励函数,这样深层神经网络表达能力就更加强大(不再是输入的线性组合,而是几乎可以逼近任意函数)。

激活函数就是将无限制的输入转换为可预测形式的输出,输出介于0和1, 即把 (−∞,+∞) 范围内的数压缩到 (0, 1)以内。正值越大输出越接近1,负向数值越大输出越接近0。

2.搭建神经网络

神经网络其实就是若干神经元组成而来,如下:

这个网络有2个输入、一个包含2个神经元的隐藏层(h1和h2)、包含1个神经元的输出层o1。

隐藏层是夹在输入输入层和输出层之间的部分,一个神经网络可以有多个隐藏层。

把神经元的输入向前传递获得输出的过程称为前馈(feedforward)。

即:

h1=f(x1 × w1 + x2 × w2 + b1)
h2=f(x1 × w3 + x2 × w4 + b2)
o1=f(h1 × w5 + h2 × w6 + b3)

3.训练神经网络

 训练神经网络,其实是不断迭代优化的过程,可以理解为寻找最优解。

如:接下来的目标是通过某人的身高、体重预测这个人的性别。有如下的数据:

这里为了简便将每个人的身高、体重减去一个固定数值(体重-100,身高-170),性别男定义为0、性别女定义为1。

 

在训练神经网络之前,我们需要有一个标准定义它到底好不好,以便我们进行改进,这就是损失

如用 均方误差 来定义损失:

公式①

n是样本的数量,在上面的数据集中是4;

y代表人的性别,男性是0,女性是1;

y_real是变量的真实值,y_expect是变量的预测值。

均方误差就是所有数据方差的平均值(损失函数MSE)。预测结果越好,损失就越低,训练神经网络就是将损失最小化。

例如上面网络的输出一直是y_real=0,也就是预测所有人都是女性,那么损失是:

MSE= 1/4 [(1-0)^2+(0-0)^2+(0-0)^2+(1-0)^2]= 0.5

4. 减杀神经网络损失

上面的结果一看就知道不够好,所以还需要不断的优化神经网络,减少损失。所以这里通过改变权重(w)和偏置(b)值可以影响神经网络的产出。为了方便计算,这里把数据集缩减到只剩下一个人的情况。

于是上述MSE的计算可简化成:

公式②

预测值是由一系列网络权重和偏置计算出来的:

所以损失函数实际上是包含多个权重、偏置的多元函数:

M(w1, w2, w3, w4, w5, w6, b1, b2, b3) 

如果调整w1,MSE是如何变化?这里就要求出偏导数∂L/∂w1是正是负才能验证这个问题。

接下来是一系列计算过程,由链式法则得:

公式③

再由方差计算公式①②得:

公式④

 将④代入,以求导得:

公式⑤

又根据神经元运算规则得:

公式⑥

 再由单个神经元可知h1与w1关系密切,所以再次使用链式法则得:

 公式⑦

 将⑥代入

 公式⑧

 

同样再次运行神经元运算法则得:

公式⑨

  将上式代入到

公式⑩

 最后是激活函数的公式

公式⑪

 

对激活函数求导得:

公式⑫

 综上,由③⑦得:

公式⑬

 

 由⑬⑤⑧⑩得

 

假设x1=-2,x2=-1,w1=w2=w3=w4=w5=w6=1,b1=b2=b3=0,代入神经元计算公式得:

 o1=y(expect)=0.524这个值并不能强烈的预测是男是女,说明这个值还不够准确,接下来代入⑪⑫看看:

∂M/∂w1=0.0214,可得w1与M成正比,M随着w1增大而增加。

5. 迭代优化

由上经过一次优化显示还不够,所以这里还需要再经过随机梯度下降法(SGD) 的迭代优化。

随机梯度下降(SGD)是每次迭代使用一个样本来对参数进行更新。使得训练速度加快。普通的BGD算法是每次迭代把所有样本都过一遍,每训练一组样本就把梯度更新一次。而SGD算法是从样本中随机抽出一组,训练后按梯度更新一次,然后再抽取一组,再更新一次,在样本量及其大的情况下,可能不用训练完所有的样本就可以获得一个损失值在可接受范围之内的模型了。

 t表示变量,表示变化率。通过不断修改变量值来达到降低损失(MES)的目的,从而达到神经网络的不断优化。

代码Demo(注意项目上的神经网络可没这么简单):

import numpy as np

def sigmoid(x):
# sigmoid激活函数: f(x) = 1 / (1 + e^(-x)),将无限制的输入转换为可预测形式的输出,输出介于01
# 即把 (−∞,+) 范围内的数压缩到 (0, 1)以内。正值越大输出越接近1,负向数值越大输出越接近0
return 1 / (1 + np.exp(-x))
def deriv_sigmoid(x):
# 求激活函数的偏导数: f'(x) = f(x) * (1 - f(x))
fx = sigmoid(x)
return fx * (1 - fx)
def mse_loss(y_real, y_expect):
# 计算损失函数,其实就是所有数据方差的平均值(均方误差),预测结果越好,损失就越低,训练神经网络就是将损失最小化
# y_real 和 y_expect 是同样长度的数组.
return ((y_real - y_expect) ** 2).mean()
class NeuralNetworkDemo:
'''
一个神经网络包括:
- 2个输入
- 1个有2 个神经元 (h1, h2)的隐藏层
- 一个神经元的输出层 (o1)
'''
def __init__(self):
# 随机初始化权重值
self.w1 = np.random.normal()
self.w2 = np.random.normal()
self.w3 = np.random.normal()
self.w4 = np.random.normal()
self.w5 = np.random.normal()
self.w6 = np.random.normal()
# 随机初始化偏置值
self.b1 = np.random.normal()
self.b2 = np.random.normal()
self.b3 = np.random.normal()
def feedforward(self, x):
# x包含x[0]、x[1],计算前馈:神经元的输入向前传递获得输出的过程
h1 = sigmoid(self.w1 * x[0] + self.w2 * x[1] + self.b1)
h2 = sigmoid(self.w3 * x[0] + self.w4 * x[1] + self.b2)
o1 = sigmoid(self.w5 * h1 + self.w6 * h2 + self.b3)
return o1
# 训练过程:
# 1、从数据集中选择一个样本;
# 2、计算损失函数对所有权重和偏置的偏导数;
# 3、使用更新公式更新每个权重和偏置;
# 4、回到第1步。
def train(self, data, all_y_reals):
'''
- data is a (n x 2) numpy array, n = # of samples in the dataset.
- all_y_reals is a numpy array with n elements.
Elements in all_y_reals correspond to those in data.
'''
learn_rate = 0.1


# 循环遍历整个数据集的次数
iterations = 1000
for iteration in range(iterations):
for x, y_real in zip(data, all_y_reals):
# 计算h1的前馈
sum_h1 = self.w1 * x[0] + self.w2 * x[1] + self.b1
h1 = sigmoid(sum_h1)
# 计算h2的前馈
sum_h2 = self.w3 * x[0] + self.w4 * x[1] + self.b2
h2 = sigmoid(sum_h2)
# 计算o1的前馈
sum_o1 = self.w5 * h1 + self.w6 * h2 + self.b3
o1 = sigmoid(sum_o1)
y_expect = o1
# --- 计算部分偏导数
# --- d_L_d_w1 表示 "偏导数 L / 偏导数 w1"
d_L_d_ypred = -2 * (y_real - y_expect)
# 神经元 o1
d_ypred_d_w5 = h1 * deriv_sigmoid(sum_o1)
d_ypred_d_w6 = h2 * deriv_sigmoid(sum_o1)
d_ypred_d_b3 = deriv_sigmoid(sum_o1)
d_ypred_d_h1 = self.w5 * deriv_sigmoid(sum_o1)
d_ypred_d_h2 = self.w6 * deriv_sigmoid(sum_o1)
# 神经元 h1
d_h1_d_w1 = x[0] * deriv_sigmoid(sum_h1)
d_h1_d_w2 = x[1] * deriv_sigmoid(sum_h1)
d_h1_d_b1 = deriv_sigmoid(sum_h1)
# 神经元 h2
d_h2_d_w3 = x[0] * deriv_sigmoid(sum_h2)
d_h2_d_w4 = x[1] * deriv_sigmoid(sum_h2)
d_h2_d_b2 = deriv_sigmoid(sum_h2)

# --- 更新权重值和偏置值
# 神经元 h1
self.w1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w1
self.w2 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_w2
self.b1 -= learn_rate * d_L_d_ypred * d_ypred_d_h1 * d_h1_d_b1
# 神经元 h2
self.w3 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w3
self.w4 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_w4
self.b2 -= learn_rate * d_L_d_ypred * d_ypred_d_h2 * d_h2_d_b2
# 神经元 o1
self.w5 -= learn_rate * d_L_d_ypred * d_ypred_d_w5
self.w6 -= learn_rate * d_L_d_ypred * d_ypred_d_w6
self.b3 -= learn_rate * d_L_d_ypred * d_ypred_d_b3
# 每一次循环计算总的损失 --- Calculate total loss at the end of each iteration
if iteration % 10 == 0:
y_expects = np.apply_along_axis(self.feedforward, 1, data)
loss = mse_loss(all_y_reals, y_expects)
print("Iteration %d Mes loss: %.3f" % (iteration, loss))
# 定义数据集
data = np.array([
[-2, -1],
[25, 6],
[17, 4],
[-15, -6],
])
all_y_reals = np.array([
1,
0,
0,
1,
])
# 训练神经网络
network = NeuralNetworkDemo()
network.train(data, all_y_reals)

  运行结果:

Iteration 0 Mes loss: 0.335
Iteration 10 Mes loss: 0.205
Iteration 20 Mes loss: 0.128
Iteration 30 Mes loss: 0.087
Iteration 40 Mes loss: 0.063
Iteration 50 Mes loss: 0.049
Iteration 60 Mes loss: 0.039
Iteration 70 Mes loss: 0.032
Iteration 80 Mes loss: 0.028
Iteration 90 Mes loss: 0.024
Iteration 100 Mes loss: 0.021
Iteration 110 Mes loss: 0.019
Iteration 120 Mes loss: 0.017
Iteration 130 Mes loss: 0.015
Iteration 140 Mes loss: 0.014
Iteration 150 Mes loss: 0.013
Iteration 160 Mes loss: 0.012
Iteration 170 Mes loss: 0.011
Iteration 180 Mes loss: 0.010
Iteration 190 Mes loss: 0.010
Iteration 200 Mes loss: 0.009
Iteration 210 Mes loss: 0.009
Iteration 220 Mes loss: 0.008
Iteration 230 Mes loss: 0.008
Iteration 240 Mes loss: 0.007
Iteration 250 Mes loss: 0.007
Iteration 260 Mes loss: 0.007
Iteration 270 Mes loss: 0.006
Iteration 280 Mes loss: 0.006
Iteration 290 Mes loss: 0.006
Iteration 300 Mes loss: 0.006
Iteration 310 Mes loss: 0.005
Iteration 320 Mes loss: 0.005
Iteration 330 Mes loss: 0.005
Iteration 340 Mes loss: 0.005
Iteration 350 Mes loss: 0.005
Iteration 360 Mes loss: 0.005
Iteration 370 Mes loss: 0.004
Iteration 380 Mes loss: 0.004
Iteration 390 Mes loss: 0.004
Iteration 400 Mes loss: 0.004
Iteration 410 Mes loss: 0.004
Iteration 420 Mes loss: 0.004
Iteration 430 Mes loss: 0.004
Iteration 440 Mes loss: 0.004
Iteration 450 Mes loss: 0.004
Iteration 460 Mes loss: 0.003
Iteration 470 Mes loss: 0.003
Iteration 480 Mes loss: 0.003
Iteration 490 Mes loss: 0.003
Iteration 500 Mes loss: 0.003
Iteration 510 Mes loss: 0.003
Iteration 520 Mes loss: 0.003
Iteration 530 Mes loss: 0.003
Iteration 540 Mes loss: 0.003
Iteration 550 Mes loss: 0.003
Iteration 560 Mes loss: 0.003
Iteration 570 Mes loss: 0.003
Iteration 580 Mes loss: 0.003
Iteration 590 Mes loss: 0.003
Iteration 600 Mes loss: 0.003
Iteration 610 Mes loss: 0.003
Iteration 620 Mes loss: 0.002
Iteration 630 Mes loss: 0.002
Iteration 640 Mes loss: 0.002
Iteration 650 Mes loss: 0.002
Iteration 660 Mes loss: 0.002
Iteration 670 Mes loss: 0.002
Iteration 680 Mes loss: 0.002
Iteration 690 Mes loss: 0.002
Iteration 700 Mes loss: 0.002
Iteration 710 Mes loss: 0.002
Iteration 720 Mes loss: 0.002
Iteration 730 Mes loss: 0.002
Iteration 740 Mes loss: 0.002
Iteration 750 Mes loss: 0.002
Iteration 760 Mes loss: 0.002
Iteration 770 Mes loss: 0.002
Iteration 780 Mes loss: 0.002
Iteration 790 Mes loss: 0.002
Iteration 800 Mes loss: 0.002
Iteration 810 Mes loss: 0.002
Iteration 820 Mes loss: 0.002
Iteration 830 Mes loss: 0.002
Iteration 840 Mes loss: 0.002
Iteration 850 Mes loss: 0.002
Iteration 860 Mes loss: 0.002
Iteration 870 Mes loss: 0.002
Iteration 880 Mes loss: 0.002
Iteration 890 Mes loss: 0.002
Iteration 900 Mes loss: 0.002
Iteration 910 Mes loss: 0.002
Iteration 920 Mes loss: 0.002
Iteration 930 Mes loss: 0.002
Iteration 940 Mes loss: 0.002
Iteration 950 Mes loss: 0.002
Iteration 960 Mes loss: 0.002
Iteration 970 Mes loss: 0.002
Iteration 980 Mes loss: 0.002
Iteration 990 Mes loss: 0.001

可以看出随着迭代的不断进行,损失越来越小。

接下来可以使用自定义输入来预测性别了

import numpy as np
def sigmoid(x):
..........

def deriv_sigmoid(x):
..........
def mse_loss(y_real, y_expect):
..........
class NeuralNetworkDemo:
..........

def testPredict(self):
you = np.array([-10, -10]) # 体重90,身高160。对于x1=90-100=-10,x2=160-170=-1017 print("your output is ", network.feedforward(you))
# 定义数据集
..........
# 训练神经网络
..........
network.testPredict()

取一个体重90,身高160的女生来测试,输出结果为:

your output is  0.9651728303105593

不断执行多少次,其输出结果都接近于1,所以可以预测这是女生。

六、结尾

关于神经网络还有其它延伸及应用较广泛的类型(CNN、RNN、DBN、GAN等),有兴趣可以来探讨。

· 14 分钟阅读

在之前的文章《持续集成与持续交付之间的联系和区别》中,我们提到了持续集成与持续交付的基本概念以及两者之间的联系和区别。而本文将更进一步,旨在为大家详细介绍如何通过Choerodon 猪齿鱼的CD流水线功能来帮助项目团队实现持续交付。

什么是持续交付

在进行功能介绍之前,我们先来回顾下持续交付的概念。持续交付是指在持续集成的基础上,将集成后的代码持续不断地部署到开发、测试或预生产环境进行测试与验证的能力。也就是说,持续交付是对非生产环境的每一次变更进行交付,而最终选择部署到生产环境的将会是一项完整的功能或一组功能,又或者是一项完整的应用或服务。

上图很好的展示了持续交付的整个流程,在通过持续集成之后,便可以持续地将代码部署至开发或测试环境。最后,待整体功能与需求测试验收完成,就可以将其手动部署至生产环境之中。当然,还需要注意的是:

持续交付并不意味着每一次变化都要尽快部署到生产环境。而是意味着每一次变化都是随时可以部署的。 —— Carl Caum(Caum,2013)

为什么要进行持续交付

众所周知,持续交付是DevOps实践中重要的一环,但持续交付能为团队带来哪些好处呢?

  • 快速发布。能够快速响应业务需求,并更快地实现软件价值;
  • 持续交付倡导的频繁部署以及自动部署,是持续测试的前提,进而提高软件质量;
  • 高质量的软件发布标准。整个交付过程标准化、可重复、可靠;
  • 整个交付过程可视化,方便团队人员了解项目进度;
  • 更先进的团队协作方式。从需求分析、产品的用户体验到交互 设计、开发、测试、运维等角色密切协作,相比于传统的瀑布式软件团队,更少浪费。

如何通过Choerodon实现持续交付?

Choerodon平台通过CD流水线的形式将开发模块与部署模块进行串联,用户只需在流水线中预设对应的部署任务或人工卡点任务,便能将目标应用服务集成后的代码自动部署到开发环境、测试环境、预生产环境以及生产环境(流转至生产环境阶段需要通过设置审批人员)。然后再设置人工卡点任务,即可通过邮件与站内信的方式及时通知到产品负责人或测试人员对新的部署进行验收与测试。

创建持续交付流水线

首先在“部署-应用部署-流水线”菜单页面,点击创建流水线,此时出现下图的流水线创建页面。项目人员可在此按需求定义多个流水线阶段,同时也可在各阶段中定义多个任务。

流水线基础设置

在创建CD流水线时,需自定义该条流水线的名称,并设置该条流水线的触发方式为自动触发或是人工触发;

  • 自动触发:满足所有触发条件时,该流水线才会自动执行。若选择自动触发,则该流水线中阶段一的任务一只能为部署类型的任务来作为触发器。
  • 手动触发:需要手动点击执行,才能触发流水线。若选择手动触发,则需要为该流水线选择触发人员(可多选),只有被选中的人员才有权限执行该流水线。若流水线中含有部署任务,则要求触发人员必须拥有流水线中所有部署任务对应的环境权限。

添加与设置阶段

  • 添加阶段;点击阶段之间的添加按钮,即可成功添加一个阶段;此外还支持修改该阶段的名称,设置阶段之间的流转方式,若选择手动流转,需要为此设置审核人员(可多选,且默认为其中一个人员审核通过则该任务通过,第一个审核人员点击中止则该任务中止)。
  • 任务设置;每个阶段下,需要选择设置对应阶段中任务的执行方式。分别是:任务串行与任务并行,其中任务串行是指阶段中的所有任务从上至下依次执行;并行是指阶段中所有任务同时执行,但阶段中任务并行时,此阶段中便不能添加人工卡点的任务。
  • 只有当一个阶段中的所有任务均执行成功后,才能进入下一阶段。

添加部署任务

为了实现部署流程的可重复性、可靠性以及可伸缩性,持续交付流水线中支持了自动部署的任务类型;但需要在其中配置应用服务、触发版本类型、环境以及实例部署的相关信息,具体步骤如下:

  • 选择任务类型为部署后,需填写任务名称,并在项目下选择一个已存在版本的应用服务;
  • 输入或选择服务版本类型(此处可以选择默认给出的版本类型或手动输入自定义的版本类型。若不填写此栏,则默认自动部署该应用服务的所有版本);
  • 选择环境;只可选择运行中的环境;
  • 选择部署模式(部署模式有新建实例和替换实例两种);
  • 选择部署配置, 此处会根据您选的应用服务与环境自动匹配所有关联的部署配置,您可根据给出的配置信息进行选择。若所选应用服务与环境暂无对应的部署配置,则需要在部署配置页面创建一个对应的部署配置。

若流水线中仅存在这一个部署任务,那么当开发人员提交代码,跑完CI,生成了满足条件的应用服务版本后(生成的版本名称中须包含在任务中定义的版本类型),该条流水线便会被触发。当流水线中存在多个部署任务,第一次触发流水线时,需要满足其中所有流水线的触发条件(生成满足条件的服务版本)。

添加人工卡点任务

人工卡点任务用于为阶段中的部署任务添加控制与审核人员,指定的审核人员审核通过后,流水线才能继续执行。若审核未不通过,整条流水线便会在此任务节点终止。添加人工卡点任务的步骤如下:

  • 选择人工卡点任务类型后,需要填写任务名称,并选择审核人员(可多选);
  • 若选择了多个审核人员,还需选择审核模式,其中包括:会签和或签。(会签是指所选的审核人员全部审核通过后才算通过,其中有一人选择终止,则此任务终止;或签是指所选的审核人员中,一人审核通过后此任务便通过,一人选择终止则此任务终止,以其中第一个审核人员的审核结果为准)。

人工卡点任务创建成功后,当流水线执行到此类型的任务时,会默认通过邮件与站内信的方式告知审核人员。在测试与验收了对应的部署之后,审核人员便可将此任务审核通过,使得流水线继续执行。

查看持续交付流水线记录

CD流水线的每一次执行,都会产生一条执行记录,每条记录里还包含了所有阶段与任务的执行详情。

在“应用部署-部署”菜单页面,项目人员能在列表中查看到流水线部署记录的编号、对应的流水线名称、触发方式、执行者、运行时间以及运行结果;目前,运行结果存在以下几种情况:

运行结果含义
成功流水线中所有任务执行成功
失败流水线中有任务执行失败
执行中流水线中有任务正在运行
待审核流水线正停留在人工审核的节点,包括人工卡点与阶段间的人工审核
已终止人工审核时,点击终止任务,最后流水线为已终止状态
已删除原流水线已被删除,但是执行记录依然保存在此页面

在记录列表中,对不同状态的流水线部署可以执行相应的操作。对于执行失败的流水线,项目所有者可以重新执行流水线中的所有任务;若流水线状态为待审核,则需指定的审核人员审核后才能继续执行;而对于执行中状态的流程,项目所有者可以对其进行强制失败的操作。

点击某条记录的编号,便能查看到该条记录的详情,在此详情页面中,会展示出对应流水线的执行详情。其中包括了流水线的触发方式、触发人员、阶段详情以及任务详情。

总结

总的来说,持续交付是持续不断地将应用服务部署到交付流水线各种环境中的能力。而与持续交付相关的持续集成、持续部署、持续测试、持续反馈以及他们共同作用带来的持续改善,都是DevOps实践落地过程中不可或缺的一部分。

关于猪齿鱼

Choerodon 猪齿鱼作为全场景效能平台,是基于Kubernetes,Istio,knative,Gitlab,Spring Cloud来实现本地和云端环境的集成,实现企业多云/混合云应用环境的一致性。平台通过提供精益敏捷、持续交付、容器环境、微服务、DevOps等能力来帮助组织团队来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。

更加详细的内容,请参阅Release Notes官网

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。

· 9 分钟阅读

Trivy是一种适用于CI的简单而全面的容器漏洞扫描程序。软件漏洞是指软件或操作系统中存在的故障、缺陷或弱点。Trivy检测操作系统包(Alpine、RHEL、CentOS等)和应用程序依赖(Bundler、Composer、npm、yarn等)的漏洞。Trivy很容易使用,只要安装二进制文件,就可以扫描了。扫描只需指定容器的镜像名称。与其他镜像扫描工具相比,例如Clair,Anchore Engine,Quay相比,Trivy在准确性、方便性和对CI的支持等方面都有着明显的优势。

推荐在CI中使用它,在推送到container registry之前,您可以轻松地扫描本地容器镜像,Trivy具备如下的特征:

  1. 检测面很全,能检测全面的漏洞,操作系统软件包(Alpine、Red Hat Universal Base Image、Red Hat Enterprise Linux、CentOS、Oracle Linux、Debian、Ubuntu、Amazon Linux、openSUSE Leap、SUSE Enterprise Linux、Photon OS 和Distrioless)、应用程序依赖项(Bundler、Composer、Pipenv、Poetry、npm、yarn和Cargo);
  2. 使用简单,仅仅只需要指定镜像名称;
  3. 扫描快且无状态,第一次扫描将在10秒内完成(取决于您的网络)。随后的扫描将在一秒钟内完成。与其他扫描器在第一次运行时需要很长时间(大约10分钟)来获取漏洞信息,并鼓励您维护持久的漏洞数据库不同,Trivy是无状态的,不需要维护或准备;
  4. 易于安装,安装方式:
    • apt-get install
    • yum install
    • brew install无需安装数据库、库等先决条件(例外情况是需要安装rpm以扫描基于RHEL/CentOS的图像)。

Trivy的安装

这里安装Trivy的环境是Centos7,安装的版本是0.4.4,安装的命令如下:

rpm -ivh https://github.com/aquasecurity/trivy/releases/download/v0.4.4/trivy_0.4.4_Linux-64bit.rpm

安装结果如下图所示即安装成功:

Trivy的简单使用

下面介绍一些Trivy的简单使用的命令和一些测试的结果。主要从几个方面来测试Trivy的性能指标:

  • 镜像大小对Trivy扫描速度的影响;
  • 扫描的镜像大小和网络流量使用情况的关系;
  • 扫描的结果是否容易解析;

镜像大小对Trivy扫描速度的影响

  • 当镜像位于本地,大小90MB左右时候的扫描:

命令:trivy registry.cn-hangzhou.aliyuncs.com/choerodon-tools/javabase:0.5.0

结果:

时间:第一次扫描会DownLoad DB,大概花十分钟以内(14M,看网速),国外的主机10s以内,第二次扫描十秒钟以内完成。

  • 当镜像位于本地,大小408MB左右时候的扫描:

trivy registry.cn-hangzhou.aliyuncs.com/choerodon-tools/mysql:5.7.17

结果:

时间:10秒左右。

  • 当扫描的镜像位于线上,大小为316M左右时候的扫描:

结果:

时间:20s左右

结论:本地扫描镜像的大小对扫描速度影响不大,线上扫描与本地扫描的方式对扫描的速度影响大不。

扫描的镜像大小和网络流量使用情况的关系

  • 线上扫描之前网络流量使用情况:

扫描镜像大小:316M左右

  • 扫描之后服务器的磁盘,网络流量使用情况:

结论:接收到的网络流量等于线上镜像的大小,镜像被下载放在服务器磁盘的某处(目前本服务器未装docker)。

注:再次全量扫描相同的镜像,接收流量和磁盘使用占比均不再增加。

扫描的结果是否容易解析

  • 使用json输出扫描的结果:

扫描镜像,openjdk:15-ea-jdk-buster

大小:316M左右

时间:10s左右

返回结果:标准的josn格式的文件

  • 其他更多使用命令如下,结果可自行测试:

    • 按严重性筛选漏洞:

      $ trivy --severity HIGH,CRITICAL ruby:2.3.0

    • 按类型筛选漏洞:

      $ trivy --vuln-type os ruby:2.3.0

    • 跳过漏洞数据库的跟新:Trivy在开始运行时总是更新其漏洞数据库。这通常很快,因为这是一个差异更新。但是,如果您甚至想跳过这一步,请使用--skip update选项。

      $ trivy --skip-update python:3.4-alpine3.9

    • 仅下载漏洞数据库:您还可以要求Trivy简单地检索漏洞数据库。这对于初始化连续集成系统中的工作人员非常有用。在第一次运行中,--only update选项将被忽略。

      $ trivy --download-db-only

      $ trivy --download-db-only --only-update alpine

    • 忽略未修复的漏洞:默认情况下,Trivy还会检测未修补/未修复的漏洞。这意味着即使更新了所有包,也无法修复这些漏洞。如果要忽略它们,请使用--ignore unfixed选项。

      $ trivy --ignore-unfixed ruby:2.3.0

    • 指定退出代码:默认情况下,即使检测到漏洞,Trivy也会以代码0退出。如果要使用非零退出代码退出,请使用--exit code选项。此选项对CI/CD很有用。在下面的示例中,仅当发现关键漏洞时,测试才会失败。

      $ trivy --exit-code 1 python:3.4-alpine3.9

      $ trivy --exit-code 0 --severity MEDIUM,HIGH ruby:2.3.0

      $ trivy --exit-code 1 --severity CRITICAL ruby:2.3.0

    • 忽略指定的漏洞:

      $ cat .trivyignore

      CVE-2018-14618

      CVE-2019-1543

      $ trivy python:3.4-alpine3.9

    • 指定缓存目录:

      $ trivy --cache-dir /tmp/trivy/ python:3.4-alpine3.9

    • 清除镜像缓存:--clear cache选项删除镜像缓存。如果更新具有相同tag的图像(例如使用最新tag时),此选项非常有用。

      $ trivy --clear-cache

    • --reset选项删除所有缓存和数据库。在此之后,需要很长时间才能在本地重建漏洞数据库。

      $ trivy --reset

    • 使用轻量级数据库:

      $ trivy --light alpine:3.10

轻量级数据库不包含诸如描述和引用之类的漏洞详细信息。因此,数据库的大小更小,下载速度更快。

当您不需要漏洞详细信息时,此选项非常有用,并且适用于CI/CD。

要查找其他信息,可以在NVD网站上搜索漏洞详细信息。(https://nvd.nist.gov/vuln/search网站

不推荐使用的选项:

--only-update,--refresh并且--auto-refresh被废弃了,因为他们现在是不必要的。这些选项将在下一版本中删除。

将Trivy集成进CI

Trivy有对CI友好的特点,并且官方也以这种方式使用它,想要集成CI只需要一段简单的Yml配置文件即可,如果发现漏洞,测试将失败。如果不希望测试失败,请指定--exit code 0。由于在自动化场景(如CI/CD)中,您只对最终结果感兴趣,而不是对完整的报告感兴趣,因此请使用--light标志对此场景进行优化,以获得快速的结果。

集成GitLab CI的Yml配置可以参考:https://github.com/aquasecurity/trivy#gitlab-ci

使用注意点

  • 国内拉取漏洞数据库慢。
  • 同一台服务器,多个镜像扫描的时候不可并行执行。
  • 可以使用--light使用轻量级数据库来优化执行扫描的效率。

参考资料:https://github.com/aquasecurity/trivy

· 10 分钟阅读

SAFe框架为企业解决多团队开发提供了多层级的指导,包括团队(team)层、项目群(program)层、价值流(value stream)层以及投资组合(portfolio)层。Choerodon猪齿鱼就是应用了SAFe框架概念进行的大规模敏捷实践。本文将为你介绍SAFe框架项目群层的基本术语及其在Choerodon猪齿鱼上的对应功能。

术语功能说明
项目群敏捷项目群SAFe的核心是项目群层,在这一层里敏捷团队、主要的利益相关者以及其他资源,致力于完成一个重要的、进行中的解决方案使命,他们组成了一个项目群结构,被称为“敏捷发布火车(ART)”
Agile Release Train(ART)ART设置敏捷发布火车,一个长期存在的、自管理和自组织的由多个敏捷团队组成的大团队,他们共同计划、承诺和执行,并且交付价值。ART是跨职能的团队组织,具备定义、实施、测试和部署新系统功能所需要的全部软件、硬件、固件和其他所有的能力。ART可以持续运行,其目标是交付持续的产品开发流。
Agile Teams子项目敏捷团队是由5-11个人组成的跨职能团队,他们在短时间内定义,构建,测试并交付价值增量。
RTE火车发布工程师是一名仆人领导,也是火车的敏捷教练。RTE通过项目群使用各种机制(如项目群看板,检查与适应研讨会,ART同步会和PI规划等),有助于优化价值流。
史诗(epic)史诗公司的关键战略略举措,可以是重大的业务方向,也可以是重大的技术演讲。企业通过对Epic的发现、定义、投资、管理和落地达成,使得企业的战略投资主题得以落地,并获得相应的市场地位和回报。通常和公司的经营、竞争力、市场环境紧密相关。
特性特性是满足利益相关者需求的服务。每个特性均包括收益假设和接受标准。它用于描述满足用户需求的大型系统行为,并在特性和利益矩阵中以简单的语言进行表达。它可以通过项目群看板进行开发和管理。通过使用WSJF方法,来维护和优先处理被批准的项目群待办事项列表中的条目。
使能使能是非功能性需求,是一项技术举措,用来促成和支持业务举措的开发实现,使能可用于支持即将到来的业务功能特性所需的任何活动。
项目群看板项目群看板一种可视化和管理特性状态流的方法,通过连续的交付管道管理特性从构思到分析、实现和发布的过程。
WSJFWSJF加权最短作业优先, WSJF通过计算延迟成本和工作规模(持续时间的代理),说明了ART待办事项如何通过加权最短作业优先(WSJF)重新确定优先级。在PI边界使用此算法根据当前业务背景、价值、时间、发展情况、风险和工作注意事项不断更新工作的优先级。它也可以快速地、自动地忽略沉没成本(付出且不可回收的成本),这是精益经济学的重要原则。延迟成本除以持续时间来计算WSJF,优先选择在最短时间内交付最大价值(或CoD)的特性用于实施。
Program Backlog路线图-待办列表待办事项包括即将到来的功能特性(项目群待办事项),这些特性旨在满足用户需求,并为ART提供业务收益。它还包含了构建架构跑道所必需的使能和特性。
PI planningPI规划Pl计划是敏捷发布火车中重要且有节奏的同步点,它是常规的面对面事件,有着标准化的流程,包括业务背景和愿景的展示,紧随其后的还有为即将到来的Pl制订计划的团队活动。PI计划由发布火车工程师组织,参与会议的成员需要尽可能包括敏捷发布火车的所有成员。
Program Increment(PI)PI项目群增量,PI提供了一个更大、更具有战略意义的固定时间盒,用于进行计划、执行以及检视和调整。
Innovation and Planning (IP) IterationIP创新与计划迭代在每一个Pl中为团队提供规律的、有节奏的时间,提供为满足Pl目标实现所需要的缓冲时间,并且为创新和探索、持续学习、探究新的技术栈、PI规划以及检查和适应提供专用时间。
路线图路线图由一系列计划的PI组成,并标注了里程碑和发布的一个长期视图。路线图上的每个元素都是计划在特定的PI中完成的功能,特性(甚至是史诗)。PI路线图也可能反映在此期间发生的固定日期和里程碑。
项目群公告板项目群公告板公告板展示了特性的交付期间、特性和团队之间依赖关系,方便ART快速消除障碍。

总结

SAFe可以帮助企业在可持续的最短前置时间内处理开发和交付企业级软件和系统中的各种重大挑战。SAFe中的核心是项目群层,它围绕着ART开展工作,ART包括了从概念设计到部署过程中所需要的所有角色。每个ART都通过协调敏捷团队来实现共同的使命和愿景,使用同一个项目群待办事项列表,它产生有价值的、可测试的系统解决方案。ART在架构师和敏捷教练的指导下,使用固定的PI计划和执行时间周期。

如何在Choerodon中进行SAFe大规模敏捷实践,请参考大规模敏捷实践指南(一):如何开启大规模敏捷之旅

关于猪齿鱼

Choerodon 猪齿鱼作为全场景效能平台,是基于Kubernetes,Istio,knative,Gitlab,Spring Cloud来实现本地和云端环境的集成,实现企业多云/混合云应用环境的一致性。平台通过提供精益敏捷、持续交付、容器环境、微服务、DevOps等能力来帮助组织团队来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。

更加详细的内容,请参阅Release Notes官网

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。

· 12 分钟阅读

原文作者:Jeremy Bird

原文地址:https://uxplanet.org/lean-product-strategy-balancing-value-and-minimums-aae06e754f68

译者:付新圆&柴晓燕

“ MVP”或“最小化可行产品”是技术中使用最多,却最难理解的概念之一。该术语由弗兰克·罗宾逊(Frank Robinson)于2001年提出,如今这个词有很多种解释,但大部分失去了最初的含义。大家现在似乎都只专注于“最小化”,但忘记了产品的“可行”或“有价值”的部分。

现在大多数技术公司都把MVP定义为一个“可用的最小功能集合”。但是什么是可用的最小功能集合呢?这样做的目的是什么呢?

弗兰克·罗宾逊(Frank Robinson)清楚地定义了他所说的“ MVP”。

MVP是为您的公司和客户量身定做的产品。它有足够大的规模,能让公司和客户满意的接受,并可以正常销售;但又不至于太大,以至于膨胀到充满风险的程度。

—弗兰克·罗宾逊(Frank Robinson), 2001

换句话说,MVP是一种解决问题的方法,它可以从三个方面提高用户体验:有效性、效率和满意度。

为了最小化风险并且得到最大的投资回报,MVP是精益的。换句话说,MVP是每次迭代都要交付一个可用的最小功能集合,这个集合的功能可以满足用户的基本需求,虽不完善但至少可用。Jeff Gothelf和Josh Seidon在他们的书《精益UX》中这样写道:

每个设计都是一个被提议的业务解决方案——一个假设。通过客户反馈,逐步修正产品设计和解决方案,MVP就可以来验证产品是否满足客户的需求。

—Jeff Gothelf和Josh Seidon,“精益UX”

大多数公司的问题在于,他们对MVP的理解还只停留在“最小的代价”部分。只是急于做出一个版本,用来告诉公司上层项目取得了良好的进展。还有一些公司他们的问题是,完全忽略了客户的体验和反馈。假设你正在开发一个产品,客户有Frank Robinson、Jeff Gothelf、Josh Seidon,但是他们从来都没有使用和反馈过产品,那就不会有MVP了。

MVP首先着眼于基本的客户需求,快速构建一个可满足客户需要的初步产品原型。做出最小可用产品,通过销售数字、应用商店评级等形式获得客户反馈后,逐步调整产品战略,尽快达成短期目标。即使它没有任何效益,但满足用户的基本需求,虽不完善但至少可用。为了避免消耗过多精力和费用得到一个有效的、精益的MVP,基于客户需求进行开发很重要。

滑板&汽车车架

关于MVP这个概念,我最喜欢的总结之一是Henrik Kniberg撰写的《赛车vs滑板》(以下是我的团队成员对原版的改编):

图片来源:Bethany Larsen和Jason Baddley

在这个假设的示例中,我们正在改善步行通勤用户的交通状况。请注意,我们不是从产品功能角度出发达到客户需求,而是把重点放在改善步行交通状况的结果上。从A到达B滑板的速度比步行快;踏板车吸引了更多人,并且更易于使用;自行车速度很快;摩托车比自行车还要快;汽车更安全,并且可以容纳更多人。通过以上分析,我们可以迅速地将产品价值传递到用户手中,通过客户的反馈和时间的推移还能不断增加这种价值。

实现一辆汽车传统的产品设计思路是一步一步,从车轮、车轱辘、外壳、动力装置、内部装饰一个流程一个流程做起,同样需要很多时间来完成。但是在上面的方法中,我们在完成的同时还能为客户提供了更高的价值(因此也提高了效率、效果和满意度)。

这里还要强调的是,从自行车到摩托车是需要一个过程的,无论是制造自行车还是摩托车,都不是一蹴而就的。通过用户的反馈,你可以用MVP的思想不断的改进、迭代,从而推动获得更好的产品,这就是敏捷和产品进步的意义所在。即使大家都懂得这个道理,但还是有很多开发人员和领导会因为用户的反馈而感到困惑。值得再次重复的是:没有什么工作是一蹴而就的,如果想要产品进步,那么它的改善一定离不开你的客户,只有不断的改进、迭代、收集反馈,才能得到更优质的产品。

如果不是因为从自行车中得到用户的反馈,也许我们永远不会制造出摩托车。如果我们制造了摩托车后,得到的客户反馈是摩托车已经满足了所有需求,那么我们就不用去造汽车了,防止浪费时间和资源。

MVP和怎么去做MVP

但另一方面,如果我们要做的项目是城市交通呢?在这种情况下,滑板是否还是MVP吗?当然不是。但是我看到的许多公司都会犯一个错误,他们只做到了“最小的代价”部分,却忘记了产品的有效性,效率和客户的满意度。

为什么会这样?我观察到,当公司只专注于根据产出衡量成功时,他们就无法预测结果,就会发生这种情况。在Scrum世界中,大多数开发团队都是在完成所有事务、提高迭代速度和准确估计时间点的基础上进行产出评估的。当你沿着这个链走下去的时候,就会进入发布时间表会比实际改善用户体验更重要的误区。

快速行动虽然很重要(这就是精益用户体验的意义所在),但是我们应该牢记快速行动的方向:提高效率,有效性和客户满意度。

引导公司专注于成果

那么,在持续产出的敏捷世界中,我们如何将重点转移到结果上呢?寻找一种让自己的敏捷团队以及利益相关者能够定期与用户沟通的方法。这种沟通方法可以是远程的,也可以是不定期式的。一开始需要一些技巧的,甚至需要对潜在客户(而不是实际用户)进行不定期式研究,如果你这样做并找到一种简单的方法汇总结果,与团队其他成员分享学习经验,就会发现你的团队会有很大的转变。

我过去不得不使用了这种方法。我带领的是一支完全不愿意分配时间并拒绝与用户联系的团队,但通过这种方法,我们的用户研究作为竞争优势为公司赢得了数百万美元的交易。

最后一点,不要认为这种变化会在一夜之间发生的,这是一个过程。就像在用户界面中一样,在这个过程中收集用户需求,采访用户(团队成员,利益相关者,业务主管)体验。了解为什么他们觉得没有时间了,然后想办法让他们知道得到想要的东西的方式。(例如,更快的更新版本)。

当人们认为您试图说服他们改变优先顺序时,他们很难被说服。如果你发现什么对他们来说是重要的,并向他们展示用户研究将如何帮助他们实现他们想要的,事情就会变得容易得多。

另一个有帮助的想法是制定在各个阶段都有成功标准的路线图。你的“MVP”步骤是什么?如何快速地做到这一点,如何进行学习和调整呢?下一步可以看到什么?以这种方式设计组织中的流程更改,能够最大限度地减少挫败感,并随着您的前进而看到递增的结果

总结

随着社会越来越发达,用户的需求越来越高会给开发者很大的压力。但是,当我们将“最小的努力”和短时间的精力集中在用户反馈和学习上,以提高用户对产品的有效性,效率和满意度时,我们会对开发的产品更有信心,用户会有更好的体验感和购买欲。如果用户还是不满意,我们就迅速进行调整,以减少失败的损失。

· 8 分钟阅读

通过之前的文章《Choerodon猪齿鱼实践之应用生命周期管理》,我们已经基本了解了Choerodon平台中应用服务的特性和微服务架构的特点。在此基础上,本文将为大家介绍Choerodon平台中导入应用服务的功能。

导入应用服务功能的背景

由于Choerodon平台采用的是微服务架构,因此其中的应用会被分解为更小、完全独立的服务组件,这使得它们拥有更高的敏捷性、可伸缩性和可用性。基于这些特点,我们就可以将各个项目中开发得到的应用服务组件化,并达到复用应用服务的效果,以此来避免重复造轮子的情况。

对于同组织内不同项目之间的应用服务共享,我们在之前的文章中已经进行了详细地介绍(具体步骤请参考:Choerodon猪齿鱼实践之应用服务共享);但是如果想将应用服务共享至其他组织中的某个项目,或者将之前在其他平台中开发的应用服务迁移到Choerodon平台之中,我们要怎样实现呢?这就涉及到下面要介绍的“导入应用服务”的功能。

导入应用服务功能介绍

导入应用服务目前有3个来源,分别是:共享应用(组织内其他项目共享至本项目下的应用服务)、GitHubGitLab。目的是从这些来源中导入已有的应用服务及其对应的代码仓库,并支持在已有应用服务的基础上进行开发,以此来避免重复造轮子的情况。

共享应用

若同组织其他项目下存在符合需求的应用服务,只需通过共享规则的方式将此应用服务共享至本项目即可;而在本项目中导入目标应用服务后,便能在原有代码库的基础上进行二次开发或部署。(注意:选择添加应用服务后,会默认选择该应用服务共享出来的最新版本;若直接导入,此时便会将该服务版本对应的代码库与commit导入到项目之中;此处版本可自主切换)

从GitHub导入

若目标应用服务的代码已经被上传至GitHub之中,此时只需在导入应用服务中选择“从GitHub导入”,再输入GitHub的HTTP地址,便能将应用服务的仓库克隆至本项目下进行二次开发。Choerodon平台目前支持从GitHub公库导入应用服务,且不能导入空库。

此外,Choerodon还在GitHub上预置了多个常用的应用服务模板供各个项目团队选择。只需在“从GitHub导入”的选项中,选择导入来源为“系统预设模板”即可。其中的应用服务模板是由同类型应用服务的代码库整理而成的,引用了相应的应用服务模板后,即可在gitlab中快速地创建初始代码库。其中包括:

  • 微服务-MicroService;
  • web前端-MicroServiceFront;
  • Java库-JavaLib;
  • Java库-SpringBoot;
  • Go库-GoTemplate;
  • 自动化测试-Mocha-ChoerodonMochaTemplate;

在这些模板中,至少都包含了 Dockerfile 文件、CI 文件以及 Chart 目录文件。

其中Dockerfile是由一系列命令和参数构成的脚本,这些命令应用于基础镜像并最终创建一个新的镜像,主要用于控制应用容器化的进程。其次是CI文件,模板中的CI文件主要用于设置在提交代码后,自动集成时要经历的所有阶段。而其中的Chart目录文件则用于将平台中的容器打包,统一置于K8S平台进行管理。

从GitLab导入

若目标应用服务的代码已经被上传至GitLab之中,此时只需在导入应用服务中选择“从GitLab导入”,再输入GitLab的HTTP地址(若为私库,还需输入私有Token)。便能将应用服务的代码仓库克隆至本项目下进行二次开发。Choerodon平台目前支持从GitLab的公库和私库导入应用服务,且同样不能导入空库。

应用服务导入成功之后,系统会默认在本项目对应的 gitlab group 中创建一个 project 作为此应用服务的初始代码库,而后再通过相应的页面功能实现对此应用服务的管理。同时,可以在“代码管理”模块,按照规范的开发流程对导入的应用服务进行分支管理、合并请求管理、版本管理、CI管理、标记管理以及代码质量的监测。

总结

导入应用服务功能使得Choerodon平台中的应用服务更加灵活,不仅仅支持组织内各项目之间应用服务的共享与复用,还可通过GitHub与GitLab导入的方式实现跨组织和跨平台的复用已有的应用服务,充分地发挥微服务架构的敏捷性与可伸缩性。

关于猪齿鱼

Choerodon 猪齿鱼作为全场景效能平台,是基于Kubernetes,Istio,knative,Gitlab,Spring Cloud来实现本地和云端环境的集成,实现企业多云/混合云应用环境的一致性。平台通过提供精益敏捷、持续交付、容器环境、微服务、DevOps等能力来帮助组织团队来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。

更加详细的内容,请参阅Release Notes官网

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。

· 7 分钟阅读

为了解决与多个团队合作时的效率低下的问题,通常有人建议引入大规模敏捷框架。此类框架最著名的示例就是规模化敏捷框架(SAFe)。在计划增量级别,SAFe提出了Scrum作为创建产品增量的方法之一。因此,改编版的Scrum通常是SAFe的一部分。在《Choerodon大规模敏捷|大规模敏捷框架SAFe》中您可以了解什么是大规模敏捷框架SAFe。

本文将为你介绍如何用SAFe开启大规模敏捷之旅,主要内容包括:

  • 设置ART节奏;
  • 组建ART中的敏捷团队;
  • 组织敏捷团队
  • 准备项目群待办事项列表

对于Choerodon来说,规模化敏捷是实现多团队研发协作不可或缺的一部分,Choerodon实现规模化敏捷就是以SAFe框架为理论基础,目前Choerodon仅实现了项目群层-团队层部分。

  • 项目群层:由产品经理(Product Manager)负责把等待安排的计划(Backlog)进行排序,并且把史诗(Epic)拆分成具体的新功能(Feature),同时和业务负责人一起设计出项目的愿景和路线。
  • 团队层:由产品负责人(Product Owner)和团队成员根据上面的定义细化出用户故事(User Story)和验收标准,开发团队可以从候选的用户故事里面优先选择可以提前开始的内容,其余的留到需求池里面等待后续的选择。

设置ART节奏;

启动ART必须确定迭代节奏,它是由迭代长度(默认2周)和PI长度(默认包含两个迭代,以及IP迭代)组成。

PI开启后,子项目会自动按照这里的默认节奏创建冲刺。

product是由多个团队集成的,因此各个团队在相同的节奏下使用相同的迭代持续时间是十分重要的。否则,团队可能按照迭代执行,但是很难集成,而且会以步调最慢的团队为准结束工作,延迟对集成时问题的发现。最终团队可以按迭代执行,但是系统无法按迭代交付。

为了解决这一问题,SAFe提供两个同步的PDCA环,团队的PDCA循环是同步的,所有的团队在同一时间开始迭代,也在同一时间结束迭代。团队的循环包含在PI的循环里面。

那么疑问来了,如果没有节奏和同步,会怎样呢?

  • product变得混乱,缺乏可预测性;
  • 集成延迟,导致延期交付;
  • 单个团队可能是敏捷过程,但是系统没有按照迭代交付;
  • 无法让合适的人都参与会议。

Choerodon-设置ART节奏:

组建ART中的敏捷团队;

敏捷团队是跨职能的,拥有创建产品增量所需的所有团队技能,没有敏捷团队,就不可能有火车,他们为ART乃至整个企业提供动力。ART负责提供更大的解决方案价值。火车上的所有团队都与其他团队合作,为PI目标和路线图做出贡献 ,并参加ART活动。此外,他们还负责构建持续交付管道和DevOps功能。

组织敏捷团队

  • 把为ART提供动力的敏捷团队都整合到ART中,并且确定每个团队的主要负责人,可以是产品负责人,也可以是Scrum Master。
  • 组织ART的管理团队,包括发布火车工程师,系统架构师,产品负责人等。

准备项目群待办事项列表

准备好我们的人员和团队,我们就需要定义我们要做什么,做成什么样,做到什么程度。也就是我们的敏捷团队最终要构建什么。

“构建什么”是由项目群待办事项列表控制的,它包含一系列待开发的特性、非功能性需求。他是由ART的利益相关者,包括发布火车工程师、架构师、业务负责人、产品负责人、客户,共同定制出一个待办事项列表。

总结

经过上面的准备,大规模敏捷的前期准备工作已经就绪,就可以正式步入大规模敏捷了,以提高团队的生产力,增加价值交付。

关于猪齿鱼

Choerodon 猪齿鱼作为全场景效能平台,是基于Kubernetes,Istio,knative,Gitlab,Spring Cloud来实现本地和云端环境的集成,实现企业多云/混合云应用环境的一致性。平台通过提供精益敏捷、持续交付、容器环境、微服务、DevOps等能力来帮助组织团队来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。

更加详细的内容,请参阅Release Notes官网

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。

· 6 分钟阅读

背景

Serverless Kubernetes 让您无需管理和维护集群与服务器,即可快速创建 Kuberentes 容器应用,并且根据应用实际使用的 CPU 和内存资源量进行按需付费。使用 Serverless Kubernetes,您可以专注于设计和构建应用程序,而不是管理运行应用程序的基础设施。充分结合了虚拟化资源带来的安全性、弹性和 Kubernetes 生态。

由此定位,这种集群是很适合用来跑 CI 流程场景的。CI 触发时创建对应资源,CI 结束后销毁对应的资源。目前 Serverless Kubernetes 或多或少有一些不友好的体验,比如:

  • 不能自定义 CoreDNS/Kube-DNS 配置。
  • 不能使用 docker-in-docker 方式构建镜像。

需求资源

  • NAS 存储
  • Serverless Kubernetes 集群

部署

注册 Runner

获取 registration token

在项目的 settings/ci_cd 页面,或者管理员的 /admin/runners 页面都可以找到 tokentoken 是 Runner 注册的凭证。如果是从项目获取的 token,那么这个 Runner 属于此项目,可以通过配置允许其他项目也可以使用。如果是从管理员页面获取的 token ,那么这个 Runner,所有项目可见,都可以使用,即共享 Runner。

下面以共享的 Runner 为例:

需要获取图中模糊处理的两个参数:

  • URL: https://gitlab.example.choerodon.io
  • Registration token: XXXXXXXXXXXXXXXXXXX

注册 Runner

  • 运行 Runner,进入容器

    docker run -it --rm --entrypoint=bash dockerhub.azk8s.cn/gitlab/gitlab-runner:alpine-v11.8.0
  • 进行注册

    gitlab-runner register
  • 注册成功后查看生成的token

    cat /etc/gitlab-runner/config.toml

执行完上述操作记录 [[runners]] 域下的 token

  • token: **********************

部署 Runner

  • 创建缓存挂载PV/PVC配置文件:cache.yaml (若不需要缓存挂载,请跳过此步骤) PS: 若需挂载多个目录,请按实际情况创建PV、PVC

    apiVersion: v1
    kind: PersistentVolume
    metadata:
    name: runner-cache-pv
    spec:
    accessModes:
    - ReadWriteMany
    capacity:
    storage: 100Gi
    mountOptions:
    - nolock,tcp,noresvport
    - vers=3
    nfs:
    path: /cache
    server: abcde.cn-shanghai.nas.example.choerodon.io
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
    name: runner-cache-pvc
    spec:
    accessModes:
    - ReadWriteMany
    resources:
    requests:
    storage: 100Gi
    volumeName: runner-cache-pv
  • 在触发 CI 流程时,Runner 会在 Kubenertes 集群中创建对应的 Pod 执行任务,故需要集群权限,复制 kubeconfig 文件中的信息,将对应值粘贴到对应字段创建 Secret,这里不再累述。最后 Runner 引用到这个 Secret 用以认证,编写文件serverless-runner-kubeconfig.yaml

    apiVersion: v1
    kind: Secret
    metadata:
    name: serverless-runner-kubeconfig
    type: kubernetes.io/tls
    data:
    ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUq......
    tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR......
    tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQp......
  • 编写 Runner 配置文件:serverless-runner-config.yaml

    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: serverless-runner-config
    data:
    config.toml: |
    # 可同时并发执行任务的数量
    concurrent = 10
    check_interval = 0
    [[runners]]
    name = "runner"
    url = "https://gitlab.example.choerodon.io"
    # [[runners]] 域下的 token
    token = "**********************"
    executor = "kubernetes"
    # 日志大小限制
    output_limit = 51200
    # 常用固定环境变量
    environment = ["CHOERODON_URL=http://api.example.choerodon.io",
    "SONAR_URL=http://sonarqube.example.choerodon.io"]
    [runners.kubernetes]
    # kubeconfig 文件中的 apiserver 访问地址
    host = "https://abcde.serverless-1.kubernetes.cn-shanghai.example.choerodon.io:6443"
    # 对应上面创建的 Secret
    cert_file = "/etc/gitlab-runner/tls.crt"
    key_file = "/etc/gitlab-runner/tls.crt"
    ca_file = "/etc/gitlab-runner/ca.crt"
    namespace = "default"
    pull_policy = "always"
    # 由于 Serverless Kubernetes 使用有规格约束,故在 Pod 资源申请与限制应按相应规格进行设置,本例为 4c8g
    cpu_limit = "3750m"
    cpu_request = "3750m"
    memory_limit = "7680Mi"
    memory_request = "7680Mi"
    helper_cpu_limit = "250m"
    helper_cpu_request = "250m"
    helper_memory_limit = "512Mi"
    helper_memory_request = "512Mi"
    helper_image = "dockerhub.azk8s.cn/gitlab/gitlab-runner-helper:x86_64-4745a6f3"
    [runners.kubernetes.pod_annotations]
    # 未购买 snat 可挂载 eip 使得 CI Job 可以访问公网资源,例如阿里云使用以下 annotation
    # "k8s.aliyun.com/eci-with-eip" = "true"
    [runners.kubernetes.volumes]
    # 挂载缓存目录(可选)
    [[runners.kubernetes.volumes.pvc]]
    name = "runner-cache-pvc"
    mount_path = "/cache"
    readonly = false
    # [[runners.kubernetes.volumes.pvc]]
    # name = "runner-maven-pvc"
    # mount_path = "/root/.m2"
    # readonly = false
  • 编写 Runner manifest 文件:serverless-runner.yaml

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: serverless-runner
    spec:
    selector:
    matchLabels:
    app: gitlab-runner
    template:
    metadata:
    labels:
    app: gitlab-runner
    spec:
    containers:
    - image: dockerhub.azk8s.cn/gitlab/gitlab-runner:alpine-v11.8.0
    imagePullPolicy: IfNotPresent
    name: runner-gitlab
    volumeMounts:
    - mountPath: /etc/gitlab-runner
    name: config
    volumes:
    - name: config
    projected:
    defaultMode: 420
    sources:
    - secret:
    items:
    - key: ca.crt
    path: ca.crt
    - key: tls.crt
    path: tls.crt
    - key: tls.key
    path: tls.key
    name: serverless-runner-kubeconfig
    - configMap:
    items:
    - key: config.toml
    path: config.toml
    name: serverless-runner-config
  • 应用配置

    • 创建PV/PVC (可选)
      kubectl apply -f cache.yaml
    • 应用 Runner
      kubectl apply -f serverless-runner-kubeconfig.yaml -f serverless-runner-config.yaml -f serverless-runner.yaml

不能自定义CoreDNS/Kube-DNS配置

自定义CoreDNS/Kube-DNS配置是为了让集群内部应用不通过公网访问同一VPC下的其他应用直接通过内网即可,比如Gitlab,Harbor等。

例如阿里云官方提供的解决方案是使用privateZone,其他云厂商也有类似的云产品。

不能使用 docker-in-docker 方式构建镜像

使用kaniko进行镜像构建,这里我们将官方镜像中的可执行文件复制到自己的CI镜像中即可使用,Dockerfile如下。

FROM gcr.azk8s.cn/kaniko-project/executor:v0.18.0 AS kaniko
FROM registry.cn-shanghai.aliyuncs.com/c7n/cibase:0.9.1
COPY --from=kaniko /kaniko/executor /usr/bin/kaniko

这是有build好可直接使用的镜像:

registry.cn-shanghai.aliyuncs.com/c7n/cibase:0.10.0
  • 待友好解决的问题:执行kaniko后会出现Deleting filesystem...操作,导致执行完kaniko后不能再执行其他命令,在CI中可以再分出一个stage来规避这个问题。

· 8 分钟阅读

Choerodon 是一个全场景效能平台,通过使用Kubernetes来部署和升级应用,而在集群中部署应用时,像忘记配置资源请求或忘记配置限制这样简单的事情就可能破坏自动伸缩,甚至导致工作负载耗尽资源。因此,保证集群的稳定运行十分必要。

基于这个目的,Choerodon团队通过借鉴 Polaris 健康检查的实现原理,并结合自身业务需求,在 Agent 组件中实现了一套自己的健康检查规则,既能从集群的维度查看其中可能影响稳定性、可靠性、可伸缩性和安全性的配置问题,也支持检测某个环境某个实例中具体的配置问题,从而达到监控集群监控环境健康状态的目的。

本文旨在为大家介绍Choerodon v0.21版本中的健康检查功能。

集群健康检查

执行健康检查

在Choerodon平台中,选择一个存在关联环境且状态为“运行中”的集群,进入“集群管理-健康检查”的页面后,点击页面中的“扫描”按钮,即可执行该集群的健康检查。

在此过程中,Polaris支持的检测类型有:Health Checks、Images、Networking、Resources、Security;检测完毕之后,会将这些分类中所有的配置项分为 passed、warning 以及error 三个状态,分别代表检测通过、警告和错误的状态。

若想详细了解各分类中所包含的默认配置项,请参考Polaris - Kubernetes最佳实践之配置校验

查看健康检查结果

健康检查执行成功后,便能从“集群概览”和“环境详情”两个维度查看存在配置项的问题(即状态为warning和error的配置项)。界面上的健康分值,是根据passed状态配置项的占比计算得来的。需要注意的是,各类详情下,只会展示出warning与 error状态的配置项。

通过检测得出的结果(尤其是存在error状态配置项的分类),集群管理员可以及时的捕捉到集群中存在的问题与隐患,并快速地进行修复。

健康分值= passed配置项数量/(passed配置项数量+1/2warning配置项数量+error配置项数量)

  • 集群概览

“集群概览”维度支持查看集群中各个分类的配置项问题;主要分为了: 健康检查、镜像检查、网络配置、资源分配以及安全这五类,并可以分别查看各个分类下有问题的配置项(warning与 error状态的配置项)及其所属的环境。

  • 环境详情

“环境详情”维度则支持查看集群中所有环境(包括Choerodon平台环境与非Choerodon平台环境)中存在问题的配置项(warning与 error状态的配置项)。

环境健康检查

执行健康检查

通过以上内容,我们知道了集群管理员如何在集群中执行健康检查操作,以此来监控集群的健康状态。那在环境层,环境管理员(运维人员)又如何对环境的健康状态进行监控的呢?针对这个问题,我们在“部署-资源-实例视图-环境层”中,也新增了“健康检查”的功能,用来检测环境下所有实例的配置项问题。 其中包括了对Deployments、StatefulSets、DaemonSets 、Jobs、CronJobs以及ReplicationControllers的检测。(注意:在进行扫描之前,请确保环境中已有实例资源,并且该环境为“运行中”状态)

查看健康检查结果

健康检查执行成功之后,会将这些分类中所有的配置项分为 passed、warning 以及 error 三个状态,分别代表检测通过、警告和错误的状态。并在各个实例下方,展示出有问题的配置项(warning与 error状态的配置项)。此外,对于含有“error”状态配置项的实例和对象,会在其名称后面标出警示标志。

界面上健康分值的计算方式,和上文中集群的一致。环境管理员通过有问题的配置项便能及时的定位与解决问题,从而避免造成更大的损失。

总结

健康检查功能是集群与环境监控模块中重要的一环,能够帮助运维人员实时地检测出集群与环境中可能影响稳定性、可靠性、可伸缩性和安全性的配置问题,以此来保证集群与环境的稳定运行。

关于猪齿鱼

Choerodon 猪齿鱼作为全场景效能平台,是基于Kubernetes,Istio,knative,Gitlab,Spring Cloud来实现本地和云端环境的集成,实现企业多云/混合云应用环境的一致性。平台通过提供精益敏捷、持续交付、容器环境、微服务、DevOps等能力来帮助组织团队来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。

Choerodon 猪齿鱼v0.21安装/升级地址如下。

更加详细的内容,请参阅Release Notes官网

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。

· 10 分钟阅读

Prometheus 是 Kubernetes 中默认的监控方案,它专注于告警和收集存储最近的监控指标。但在一定的集群规模下,Prometheus 也暴露出一些问题。例如:如何以经济可靠的方式存储 PB 级别的历史数据,并且不牺牲查询时间?如何通过单一的查询接口访问到不同 Prometheus 服务器上的所有指标数据?能否以某种方式合并采集到的重复数据?针对以上的这些问题, Thanos 提供了高可用的的解决方案,并且它有着不受限制的数据存储能力。

Thanos

Thanos 基于 Prometheus。当我们以不同方式使用 Thanos 时,或多或少都会用到 Prometheus 功能,但是 Prometheus 始终是指标收集和使用本地数据进行预警功能的基础。

Thanos 使用 Prometheus 存储格式,把历史数据以相对高性价比的方式保存在对象存储里,同时兼有较快的查询速度。此外,它还能对你所有的 Prometheus 提供全局查询视图。

依据 KISS 原则和 Unix 哲学,Thanos 划分如下特定功能的组件。

  • 边车组件(Sidecar):连接Prometheus,并把Prometheus暴露给查询网关(Querier/Query),以供实时查询,并且可以上传Prometheus数据给云存储,以供长期保存;
  • 查询网关(Querier/Query):实现了Prometheus API,与汇集底层组件(如边车组件Sidecar,或是存储网关Store Gateway)的数据;
  • 存储网关(Store Gateway):将云存储中的数据内容暴露出来;
  • 压缩器(Compactor):将云存储中的数据进行压缩和下采样;
  • 接收器(Receiver):从Prometheus’ remote-write WAL(Prometheus远程预写式日志)获取数据,暴露出去或者上传到云存储;
  • 规则组件(Ruler):针对数据进行评估和报警;

组件之间的关系如图:

部署

在 K8S 集群中部署 Prometheus 最简单的方法是用 helm 安装 prometheus-operator。更多关于 Prometheus-operator 的内容请阅读《Prometheus-operator 介绍和配置解析 》。Prometheus-Operator 提供了高可用的支持,Thanos 边车组件(Sidecar)的注入,以及监控服务器、监控 Kubernetes 基础组件,可以监控应用所需的预制报警。

Choerodon 提供的 Prometheus-opertor 在社区版的基础上添加多集群监控的仪表盘。

本篇文章部署架构图如下所示:

对象存储

目前 thanos 支持大部分云厂商的对象存储服务,具体使用请参考thanos 对象存储。本文使用 minio 代替 S3 对象存储。为了方便将 minio 安装在 Observability 集群。

编写 minio 参数配置文件minio.yaml:

mode: distributed
accessKey: "AKIAIOSFODNN7EXAMPLE"
secretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
persistence:
enabled: true
storageClass: nfs-provisioner
ingress:
enabled: true
path: /
hosts:
- minio.example.choerodon.io

执行安装命令:

helm install c7n/minio \
-f minio.yaml \
--version 5.0.4 \
--name minio \
--namespace monitoring

登录 minio 创建一个 thanos 桶。

在每个集群中都创建一个存储secret。

  • 配置文件 thanos-storage-minio.yaml

    type: s3
    config:
    bucket: thanos
    endpoint: minio.example.choerodon.io
    access_key: AKIAIOSFODNN7EXAMPLE
    secret_key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
    insecure: true
    signature_version2: false
  • 创建存储的 secret:

    kubectl -n monitoring create secret generic thanos-objectstorage --from-file=thanos.yaml=thanos-storage-minio.yaml

Observability 集群安装 Promethues-operator

在 Observability 集群中安装的 Promethues-operator 需要安装 grafana 并且修改默认的 datasource 为 Thanos 的 Query 组件。Observability-prometheus-operator.yaml 配置文件如下:

grafana:
persistence:
enabled: true
storageClassName: nfs-provisioner
ingress:
enabled: true
hosts:
- grafana.example.choerodon.io
additionalDataSources:
- name: Prometheus
type: prometheus
url: http://thanos-querier:9090/
access: proxy
isDefault: true
sidecar:
datasources:
defaultDatasourceEnabled: false
prometheus:
retention: 12h
prometheusSpec:
externalLabels:
cluster: observe # 添加 cluster 标签区分集群
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: nfs-provisioner
resources:
requests:
storage: 10Gi
thanos:
baseImage: quay.io/thanos/thanos
version: v0.10.1
objectStorageConfig:
key: thanos.yaml
name: thanos-objectstorage

安装 prometheus-operator 集群

helm install c7n/prometheus-operator \
-f Observability-prometheus-operator.yaml \
--name prometheus-operator \
--version 8.5.8 \
--namespace monitoring

A\B 集群安装 Promethues-operator

A\B 集群中就只需要安装 prometheus 相关的组件,grafana、alertmanager 等组件不再需要安装,配置文件proemtheus-operator.yaml 如下:

alertmanager:
enabled: false

grafana:
enabled: false

prometheus:
retention: 12h
prometheusSpec:
externalLabels:
cluster: a-cluster # 添加 cluster 标签区分集群
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: nfs-provisioner
resources:
requests:
storage: 10Gi
thanos:
baseImage: quay.io/thanos/thanos
version: v0.10.1
objectStorageConfig:
key: thanos.yaml
name: thanos-objectstorage

安装 prometheus-operator 集群

helm install c7n/prometheus-operator \
-f prometheus-operator.yaml \
--name prometheus-operator \
--version 8.5.8 \
--namespace monitoring

为 Thanos SideCar 创建子域名分别指向集群A/B

thanos-a.example.choerodon.io
thanos-b.example.choerodon.io

以 A 集群为例创建 ingress 规则

apiVersion: v1
kind: Service
metadata:
labels:
app: prometheus
name: thanos-sidecar-a
spec:
ports:
- port: 10901
protocol: TCP
targetPort: grpc
name: grpc
nodePort: 30901
selector:
statefulset.kubernetes.io/pod-name: prometheus-prometheus-operator-prometheus-0
type: NodePort
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "GRPC"
labels:
app: prometheus
name: thanos-sidecar-0
spec:
rules:
- host: thanos-a.example.choerodon.io
http:
paths:
- backend:
serviceName: thanos-sidecar-a
servicePort: grpc

Observability 集群安装 thanos

使用 kube-thanos 安装 Thanos。

安装需要的软件工具:

$ yum install -y golang
$ go get github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb
$ go get github.com/brancz/gojsontoyaml
$ go get github.com/google/go-jsonnet/cmd/jsonnet

使用 jsonnet-bundler 安装 kube-thanos

$ mkdir my-kube-thanos; cd my-kube-thanos
$ jb init # Creates the initial/empty `jsonnetfile.json`
# Install the kube-thanos dependency
$ jb install github.com/thanos-io/kube-thanos/jsonnet/kube-thanos@master # Creates `vendor/` & `jsonnetfile.lock.json`, and fills in `jsonnetfile.json`

更新 kube-thanos 依赖

$ jb update

创建 example.jsonnet

local k = import 'ksonnet/ksonnet.beta.4/k.libsonnet';
local sts = k.apps.v1.statefulSet;
local deployment = k.apps.v1.deployment;
local t = (import 'kube-thanos/thanos.libsonnet');

local commonConfig = {
config+:: {
local cfg = self,
namespace: 'monitoring',
version: 'v0.10.1',
image: 'quay.io/thanos/thanos:' + cfg.version,
objectStorageConfig: {
name: 'thanos-objectstorage',
key: 'thanos.yaml',
},
volumeClaimTemplate: {
spec: {
accessModes: ['ReadWriteOnce'],
storageClassName: ''
resources: {
requests: {
storage: '10Gi',
},
},
},
},
},
};

local s = t.store + t.store.withVolumeClaimTemplate + t.store.withServiceMonitor + commonConfig + {
config+:: {
name: 'thanos-store',
replicas: 1,
},
};

local q = t.query + t.query.withServiceMonitor + commonConfig + {
config+:: {
name: 'thanos-query',
replicas: 1,
stores: [
'dnssrv+_grpc._tcp.%s.%s.svc.cluster.local' % [service.metadata.name, service.metadata.namespace]
for service in [s.service]
],
replicaLabels: ['prometheus_replica', 'rule_replica'],
},
};


{ ['thanos-store-' + name]: s[name] for name in std.objectFields(s) } +
{ ['thanos-query-' + name]: q[name] for name in std.objectFields(q) }

创建 build.sh

#!/usr/bin/env bash

# This script uses arg $1 (name of *.jsonnet file to use) to generate the manifests/*.yaml files.

set -e
set -x
# only exit with zero if all commands of the pipeline exit successfully
set -o pipefail

# Make sure to start with a clean 'manifests' dir
rm -rf manifests
mkdir manifests

# optional, but we would like to generate yaml, not json
jsonnet -J vendor -m manifests "${1-example.jsonnet}" | xargs -I{} sh -c 'cat {} | gojsontoyaml > {}.yaml; rm -f {}' -- {}

# The following script generates all components, mostly used for testing

rm -rf examples/all/manifests
mkdir -p examples/all/manifests

jsonnet -J vendor -m examples/all/manifests "${1-all.jsonnet}" | xargs -I{} sh -c 'cat {} | gojsontoyaml > {}.yaml; rm -f {}' -- {}

执行以下命令创建 K8S 资源文件

$ ./build.sh example.jsonnet

对于生成的资源文件有两处需要修改

$ vim manifests/thanos-store-statefulSet.yaml
------------------------------------------------------
spec:
containers:
- args:
- store
- --data-dir=/var/thanos/store
- --grpc-address=0.0.0.0:10901
- --http-address=0.0.0.0:10902
- --objstore.config=$(OBJSTORE_CONFIG)
# - --experimental.enable-index-header # 注释掉这多余的一行
env:

$ vim manifests/thanos-query-deployment.yaml
------------------------------------------------------
containers:
- args:
- query
- --grpc-address=0.0.0.0:10901
- --http-address=0.0.0.0:9090
- --query.replica-label=prometheus_replica
- --query.replica-label=rule_replica
- --store=dnssrv+_grpc._tcp.thanos-store.monitoring.svc.cluster.local
# 添加本集群和 A/B 集群的 Store API
- --store=dnssrv+_grpc._tcp.prometheus-operated.monitoring.svc.cluster.local
- --store=dns+thanos-a.example.choerodon.io:30901
- --store=dns+thanos-b.example.choerodon.io:30901

创建 Thanos

$ kubectl create -f manifests/

通过端口转发查看 Thanos Query 是否正常

$ kubectl port-forward svc/thanos-query 9090:9090 -n monitoring

现在访问 http://grafana.example.choerodon.io 就可以查看多集群的监控信息。

总结

通过以上步骤就完成了使用 Thanos 的 Prometheus 多集群监控的安装,该方案具有长期存储的功能,并且可以查看跨集群的监控信息。


关于猪齿鱼

Choerodon 猪齿鱼作为全场景效能平台,是基于Kubernetes,Istio,knative,Gitlab,Spring Cloud来实现本地和云端环境的集成,实现企业多云/混合云应用环境的一致性。平台通过提供精益敏捷、持续交付、容器环境、微服务、DevOps等能力来帮助组织团队来完成软件的生命周期管理,从而更快、更频繁地交付更稳定的软件。

Choerodon 猪齿鱼v0.21已经发布,欢迎大家前来安装/升级。

更加详细的内容,请参阅Release Notes官网

大家也可以通过以下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献:

欢迎加入Choerodon猪齿鱼社区,共同为企业数字化服务打造一个开放的生态平台。