前言

一个 AI 方向的朋友因为标数据集发了篇 SCI 论文,看着他标了两个多月的数据集这么辛苦,就想着人工智能都能站在围棋巅峰了,难道不能动动小手为自己标数据吗?查了一下还真有一些能够满足此需求的框架,比如 cvatdoccanolabel studio 等,经过简单的对比后发现还是 label studio 最好用。本文首先介绍了 label studio 的安装过程;然后使用 MMDetection 作为后端人脸检测标记框架,并通过 label studio ml 将 MMDetection 模型封装成 label studio 后端服务,实现数据集的自动标记[1];最后参考 label studio ml 示例,为自己的 MMDetection 人脸标记模型设计了一种迭代训练方法,使之能够不断随着标记数据的增加而跟进训练,最终实现了模型自动标记数据集、数据集更新迭代训练模型的闭环。

依赖安装

本项目涉及的源码已开源在 label-studio-demo 中,所使用的软件版本如下,其中 MMDetection 的版本及配置参考 MMDetection 使用示例:从入门到出门

软件 版本
label-studio 1.6.0
label-studio-ml 1.0.8
label-studio-tools 0.0.1

本文最终项目目录结构如下:

LabelStudio
├── backend // 后端功能
│ ├── examples // label studio ml 官方示例(非必须)
│ ├── mmdetection // mmdetection 人脸检测模型
│ ├── model // label studio ml 生成的后端服务 (自动生成)
│ ├── workdir // 模型训练时工作目录
│ | ├── fcos_common_base.pth // 后端模型基础权重文件
│ | └── latest.pth // 后端模型最新权重文件
│ └── runbackend.bat // 生成并启动后端服务的脚本文件
├── dataset // 实验所用数据集(非必须)
├── label_studio.sqlite3 // label studio 数据库文件
├── media
│ ├── export
│ └── upload // 上传的待标记数据集
└── run.bat // 启动 label studio 的脚本文件(非必须)

label studio 安装启动

label-studio 是一个开源的多媒体数据标注工具(用来提供基本标注功能的GUI),并且可以很方便的将标注结果导出为多种常见的数据格式。其安装方法主要有以下几种:

  1. Docker
docker pull heartexlabs/label-studio:latest
  1. pip
pip install label-studio

建议是通过 pip 安装,其配置更清晰方便。环境安装完成后在任意位置打开命令行,使用以下命令启动 label studio :

label-studio --data-dir LabelStudio -p 80

其中 --data-dir 用于指定工作目录, -p 用来指定运行端口,运行成功后会当前目录会生成 LabelStudio 目录:



并弹出浏览器打开 label studio 工作界面,创建用户后即可登录使用:

label studio ml 安装

label studio ml 是 label studio 的后端配置,其主要提供了一种能够快速将AI模型封装为 label studio 可使用的预标记服务(提供模型预测服务)。其安装方法有以下几种:

  1. GitHub 安装
git clone https://github.com/heartexlabs/label-studio-ml-backend
cd label-studio-ml-backend
pip install -U -e .
  1. pip 安装:
pip install label-studio-ml

仍然建议通过 pip 安装,GitHub 安装可能会有依赖问题。安装完成后使用 label-studio-ml -h 命令检查是否安装成功。

前端配置

在 label studio 前端主页中选择创建项目:

  1. 项目基本信息

  2. 导入数据

    直接将图片选中拖入数据框即可。

  3. 选择标记模板

    label studio 内置了很多常见的深度学习标记模板,本示例是人脸识别,所以选择 Object Detection with Bounding Boxes 模板,确定后将模板内自带的 Airplane 、 Car 标签删除,然后添加自定义的标签 face (标签的类别数量可以比后端支持的类别多,也可以更少,但是同类别的标签名必须一致)。

此时我们已经可以通过 label studio 进行普通的图片标记工作,如果要使用其提供的辅助预标记功能,则需要进行后续配置。

后端配置

选取后端模型

MMDetection 使用示例:从入门到出门 中,我们已经完成了基于 celeba100 数据集的人脸检测模型的训练,本文将直接使用其中训练的结果模型。

后端服务实现

引入后端模型

在根目录下创建 backend 目录,并将 MMDetection 使用示例:从入门到出门 中的整个项目文件复制其中,此时项目目录为:

.
├── backend
│ └── mmdetection // 复制的 mmdetection 文件夹
│ ├── checkpoints
│ ├── completion.json
│ ├── configs
│ ├── conf.yaml
│ ├── detect.py
│ ├── label_studio_backend.py // 需要自己实现的后端模型
│ ├── mmdet
│ ├── model
│ ├── test.py
│ ├── tools
│ └── train.py
├── dataset
├── export
├── label-studio-ml-backend
├── label_studio.sqlite3
├── media
└── run.bat

创建后端模型

label studio 的后端模型有自己固定的写法,只要继承 label_studio_ml.model.LabelStudioMLBase 类并实现其中的接口都可以作为 label studio 的后端服务。在 mmdetection 文件夹下创建 label_studio_backend.py 文件,然后在文件中引入通用配置:

ROOT = os.path.join(os.path.dirname(__file__))
print('=> ROOT = ', ROOT)
# label-studio 启动的前端服务地址
os.environ['HOSTNAME'] = 'http://localhost:80'
# label-studio 中对应用户的 API_KEY
os.environ['API_KEY'] = '37edbb42f1b3a73376548ea6c4bc7b3805d63453'
HOSTNAME = get_env('HOSTNAME')
API_KEY = get_env('API_KEY') print('=> LABEL STUDIO HOSTNAME = ', HOSTNAME)
if not API_KEY:
print('=> WARNING! API_KEY is not set') with open(os.path.join(ROOT, "conf.yaml"), errors='ignore') as f:
conf = yaml.safe_load(f)

这里的 API_KEY 可以在前端的 Account & Settings 中找到。



然后在 label_studio_backend.py 中创建自己预标记模型的类,使其继承 label_studio_ml.model.LabelStudioMLBase 并实现关键方法,不同方法对应不同功能,后面会陆续实现:

class MyModel(LabelStudioMLBase):
def __init__(self, **kwargs):
pass
def predict(self, tasks, **kwargs):
pass
def fit(self, completions, batch_size=32, num_epochs=5, **kwargs):
pass
def gen_train_data(self, project_id):
pass

完成其中的 __init__ 方法,以实现模型初始化功能(必须):

    def __init__(self, **kwargs):
super(MyModel, self).__init__(**kwargs)
# 按 mmdetection 的方式加载模型及权重
if self.train_output:
self.detector = init_detector(conf['config_file'], self.train_output['model_path'], device=conf['device'])
else:
self.detector = init_detector(conf['config_file'], conf['checkpoint_file'], device=conf['device'])
# 获取后端模型标签列表
self.CLASSES = self.detector.CLASSES
# 前端配置的标签列表
self.labels_in_config = set(self.labels_in_config)
# 一些项目相关常量
self.from_name, self.to_name, self.value, self.labels_in_config = get_single_tag_keys(self.parsed_label_config, 'RectangleLabels', 'Image') # 前端获取任务属性

完成其中的 predict 方法,以实现预标记模型的标记功能(必须):

    def predict(self, tasks, **kwargs):
# 获取待标记图片
images = [get_local_path(task['data'][self.value], hostname=HOSTNAME, access_token=API_KEY) for task in tasks]
for image_path in images:
w, h = get_image_size(image_path)
# 推理演示图像
img = mmcv.imread(image_path)
# 以 mmdetection 的方法进行推理
result = inference_detector(self.detector, img)
# 手动获取标记框位置
bboxes = np.vstack(result)
# 手动获取推理结果标签
labels = [np.full(bbox.shape[0], i, dtype=np.int32) for i, bbox in enumerate(result)]
labels = np.concatenate(labels)
# 推理分数 FCOS算法结果会多出来两个分数极低的检测框,需要将其过滤掉
scores = bboxes[:, -1]
score_thr = 0.3
inds = scores > score_thr
bboxes = bboxes[inds, :]
labels = labels[inds]
results = [] # results需要放在list中再返回
for id, bbox in enumerate(bboxes):
label = self.CLASSES[labels[id]]
if label not in self.labels_in_config:
print(label + ' label not found in project config.')
continue
results.append({
'id': str(id), # 必须为 str,否则前端不显示
'from_name': self.from_name,
'to_name': self.to_name,
'type': 'rectanglelabels',
'value': {
'rectanglelabels': [label],
'x': bbox[0] / w * 100, # xy 为左上角坐标点
'y': bbox[1] / h * 100,
'width': (bbox[2] - bbox[0]) / w * 100, # width,height 为宽高
'height': (bbox[3] - bbox[1]) / h * 100
},
'score': float(bbox[4] * 100)
})
avgs = bboxes[:, -1]
results = [{'result': results, 'score': np.average(avgs) * 100}]
return results

完成其中的 gen_train_data 方法,以获取标记完成的数据用来训练(非必须,其实 label studio 自带此类方法,但在实践过程中有各种问题,所以自己写了一遍):

    def gen_train_data(self, project_id):
import zipfile
import glob
download_url = f'{HOSTNAME.rstrip("/")}/api/projects/{project_id}/export?export_type=COCO&download_all_tasks=false&download_resources=true'
response = requests.get(download_url, headers={'Authorization': f'Token {API_KEY}'})
zip_path = os.path.join(conf['workdir'], "train.zip")
train_path = os.path.join(conf['workdir'], "train") with open(zip_path, 'wb') as file:
file.write(response.content) # 通过二进制写文件的方式保存获取的内容
file.flush()
f = zipfile.ZipFile(zip_path) # 创建压缩包对象
f.extractall(train_path) # 压缩包解压缩
f.close()
os.remove(zip_path)
if not os.path.exists(os.path.join(train_path, "images", str(project_id))):
os.makedirs(os.path.join(train_path, "images", str(project_id)))
for img in glob.glob(os.path.join(train_path, "images", "*.jpg")):
basename = os.path.basename(img)
shutil.move(img, os.path.join(train_path, "images", str(project_id), basename))
return True

完成其中的 fit 方法,以实现预标记模型的自训练功能(非必须):

    def fit(self, completions, num_epochs=5, **kwargs):
if completions: # 使用方法1获取 project_id
image_urls, image_labels = [], []
for completion in completions:
project_id = completion['project']
u = completion['data'][self.value]
image_urls.append(get_local_path(u, hostname=HOSTNAME, access_token=API_KEY))
image_labels.append(completion['annotations'][0]['result'][0]['value'])
elif kwargs.get('data'): # 使用方法2获取 project_id
project_id = kwargs['data']['project']['id']
if not self.parsed_label_config:
self.load_config(kwargs['data']['project']['label_config'])
if self.gen_train_data(project_id):
# 使用 mmdetection 的方法训练模型
from tools.mytrain import MyDict, train
args = MyDict()
args.config = conf['config_file']
data_root = os.path.join(conf['workdir'], "train")
args.cfg_options = {}
args.cfg_options['data_root'] = data_root
args.cfg_options['runner'] = dict(type='EpochBasedRunner', max_epochs=num_epochs)
args.cfg_options['data'] = dict(
train=dict(img_prefix=data_root, ann_file=data_root + '/result.json'),
val=dict(img_prefix=data_root, ann_file=data_root + '/result.json'),
test=dict(img_prefix=data_root, ann_file=data_root + '/result.json'),
)
args.cfg_options['load_from'] = conf['checkpoint_file']
args.work_dir = os.path.join(data_root, "work_dir")
train(args)
checkpoint_name = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) + ".pth"
shutil.copy(os.path.join(args.work_dir, "latest.pth"), os.path.join(conf['workdir'], checkpoint_name))
print("model train complete!")
# 权重文件保存至运行环境,将在下次运行 init 初始化时加载
return {'model_path': os.path.join(conf['workdir'], checkpoint_name)}
else:
raise "gen_train_data error"

上述完整代码如下:

import os
import yaml
import time
import shutil
import requests
import numpy as np
from label_studio_ml.model import LabelStudioMLBase
from label_studio_ml.utils import get_image_size, get_single_tag_keys
from label_studio_tools.core.utils.io import get_local_path
from label_studio_ml.utils import get_env from mmdet.apis import init_detector, inference_detector
import mmcv ROOT = os.path.join(os.path.dirname(__file__))
print('=> ROOT = ', ROOT)
os.environ['HOSTNAME'] = 'http://localhost:80'
os.environ['API_KEY'] = '37edbb42f1b3a73376548ea6c4bc7b3805d63453'
HOSTNAME = get_env('HOSTNAME')
API_KEY = get_env('API_KEY') print('=> LABEL STUDIO HOSTNAME = ', HOSTNAME)
if not API_KEY:
print('=> WARNING! API_KEY is not set') with open(os.path.join(ROOT, "conf.yaml"), errors='ignore') as f:
conf = yaml.safe_load(f) class MyModel(LabelStudioMLBase): def __init__(self, **kwargs):
super(MyModel, self).__init__(**kwargs)
# 按 mmdetection 的方式加载模型及权重
if self.train_output:
self.detector = init_detector(conf['config_file'], self.train_output['model_path'], device=conf['device'])
else:
self.detector = init_detector(conf['config_file'], conf['checkpoint_file'], device=conf['device'])
# 获取后端模型标签列表
self.CLASSES = self.detector.CLASSES
# 前端配置的标签列表
self.labels_in_config = set(self.labels_in_config)
# 一些项目相关常量
self.from_name, self.to_name, self.value, self.labels_in_config = get_single_tag_keys(self.parsed_label_config, 'RectangleLabels', 'Image') # 前端获取任务属性 def predict(self, tasks, **kwargs):
# 获取待标记图片
images = [get_local_path(task['data'][self.value], hostname=HOSTNAME, access_token=API_KEY) for task in tasks]
for image_path in images:
w, h = get_image_size(image_path)
# 推理演示图像
img = mmcv.imread(image_path)
# 以 mmdetection 的方法进行推理
result = inference_detector(self.detector, img)
# 手动获取标记框位置
bboxes = np.vstack(result)
# 手动获取推理结果标签
labels = [np.full(bbox.shape[0], i, dtype=np.int32) for i, bbox in enumerate(result)]
labels = np.concatenate(labels)
# 推理分数 FCOS算法结果会多出来两个分数极低的检测框,需要将其过滤掉
scores = bboxes[:, -1]
score_thr = 0.3
inds = scores > score_thr
bboxes = bboxes[inds, :]
labels = labels[inds]
results = [] # results需要放在list中再返回
for id, bbox in enumerate(bboxes):
label = self.CLASSES[labels[id]]
if label not in self.labels_in_config:
print(label + ' label not found in project config.')
continue
results.append({
'id': str(id), # 必须为 str,否则前端不显示
'from_name': self.from_name,
'to_name': self.to_name,
'type': 'rectanglelabels',
'value': {
'rectanglelabels': [label],
'x': bbox[0] / w * 100, # xy 为左上角坐标点
'y': bbox[1] / h * 100,
'width': (bbox[2] - bbox[0]) / w * 100, # width,height 为宽高
'height': (bbox[3] - bbox[1]) / h * 100
},
'score': float(bbox[4] * 100)
})
avgs = bboxes[:, -1]
results = [{'result': results, 'score': np.average(avgs) * 100}]
return results def fit(self, completions, num_epochs=5, **kwargs):
if completions: # 使用方法1获取 project_id
image_urls, image_labels = [], []
for completion in completions:
project_id = completion['project']
u = completion['data'][self.value]
image_urls.append(get_local_path(u, hostname=HOSTNAME, access_token=API_KEY))
image_labels.append(completion['annotations'][0]['result'][0]['value'])
elif kwargs.get('data'): # 使用方法2获取 project_id
project_id = kwargs['data']['project']['id']
if not self.parsed_label_config:
self.load_config(kwargs['data']['project']['label_config'])
if self.gen_train_data(project_id):
# 使用 mmdetection 的方法训练模型
from tools.mytrain import MyDict, train
args = MyDict()
args.config = conf['config_file']
data_root = os.path.join(conf['workdir'], "train")
args.cfg_options = {}
args.cfg_options['data_root'] = data_root
args.cfg_options['runner'] = dict(type='EpochBasedRunner', max_epochs=num_epochs)
args.cfg_options['data'] = dict(
train=dict(img_prefix=data_root, ann_file=data_root + '/result.json'),
val=dict(img_prefix=data_root, ann_file=data_root + '/result.json'),
test=dict(img_prefix=data_root, ann_file=data_root + '/result.json'),
)
args.cfg_options['load_from'] = conf['checkpoint_file']
args.work_dir = os.path.join(data_root, "work_dir")
train(args)
checkpoint_name = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time())) + ".pth"
shutil.copy(os.path.join(args.work_dir, "latest.pth"), os.path.join(conf['workdir'], checkpoint_name))
print("model train complete!")
# 权重文件保存至运行环境,将在下次运行 init 初始化时加载
return {'model_path': os.path.join(conf['workdir'], checkpoint_name)}
else:
raise "gen_train_data error" def gen_train_data(self, project_id):
import zipfile
import glob
download_url = f'{HOSTNAME.rstrip("/")}/api/projects/{project_id}/export?export_type=COCO&download_all_tasks=false&download_resources=true'
response = requests.get(download_url, headers={'Authorization': f'Token {API_KEY}'})
zip_path = os.path.join(conf['workdir'], "train.zip")
train_path = os.path.join(conf['workdir'], "train") with open(zip_path, 'wb') as file:
file.write(response.content) # 通过二进制写文件的方式保存获取的内容
file.flush()
f = zipfile.ZipFile(zip_path) # 创建压缩包对象
f.extractall(train_path) # 压缩包解压缩
f.close()
os.remove(zip_path)
if not os.path.exists(os.path.join(train_path, "images", str(project_id))):
os.makedirs(os.path.join(train_path, "images", str(project_id)))
for img in glob.glob(os.path.join(train_path, "images", "*.jpg")):
basename = os.path.basename(img)
shutil.move(img, os.path.join(train_path, "images", str(project_id), basename))
return True

启动后端服务

以下命令为 window 脚本,皆在 backend 根目录下执行。

  1. 根据后端模型生成服务代码
label-studio-ml init model --script mmdetection/label_studio_backend.py --force

label-studio-ml init 命令提供了一种根据后端模型自动生成后端服务代码的功能, model 为输出目录, --script 指定后端模型路径, --force 表示覆盖生成。该命令执行成功后会在 backend 目录下生成 model 目录。

2. 复制 mmdetection 依赖文件

由于 label-studio-ml 生成的后端服务代码只包含基本的 label_studio_backend.py 中的内容,而我们所用的 mmdetection 框架的执行需要大量额外的依赖,所以需要手动将这些依赖复制到生成的 model 目录中。使用以下命令完成自动复制依赖:

md .\model\mmdet
md .\model\model
md .\model\configs
md .\model\checkpoints
md .\model\tools
md .\model\workdir
xcopy .\mmdetection\mmdet .\model\mmdet /S /Y /Q
xcopy .\mmdetection\model .\model\model /S /Y /Q
xcopy .\mmdetection\configs .\model\configs /S /Y /Q
xcopy .\mmdetection\checkpoints .\model\checkpoints /S /Y /Q
xcopy .\mmdetection\tools .\model\tools /S /Y /Q
copy .\mmdetection\conf.yaml .\model\conf.yaml
  1. 启动后端服务
label-studio-ml start model --host 0.0.0.0 -p 8888

启动成功后效果如下:

前端自动标注

前面我们已经能够从 label studio 前端正常手动标注图片,要想实现自动标注,则需要在前端引入后端服务。在我们创建的项目中依次选择 Settings ->

Machine Learning -> Add model ,然后输入后端地址 http://10.100.143.125:8888/ 点击保存(此地址为命令行打印地址,而非 http://127.0.0.1:8888/ ):



此时我们从前端项目中打开待标记图片,前端会自动请求后端对其进行标记(调用后端的 predict 方法),等待片刻后即可看见预标记结果,我们只需要大致核对无误后点击 submit 即可:



如果觉得每次打开图片都需要等待片刻才会收到后端预测结果比较费时,可以在 Settings -> Machine Learning 设置中选择打开 Retrieve predictions when loading a task automatically ,此后前端会在我们每次打开项目时自动对所有任务进行自动预测,基本能够做到无等待:

后端自动训练

现在所有的图片都已经有了与标注信息,我们先检查所有图片,检查并改进所有标注信息然后点击 submit 提交:



在 Settings -> Machine Learning 中点击后端服务的 Start Training 按钮,即可调用后端模型使用已标记信息进行训练:



该操作会调用后端模型的 fit 方法对模型进行训练,可以在后端命令行界面看见训练过程,训练完成后的所有新数据集都会使用新的模型进行预测:



也可以 Settings -> Machine Learning 中允许模型自动训练,但训练频率过高会影响程序效率。

部分常见问题

Q: 一种访问权限不允许的方式做了一个访问套接字的尝试。

A: label-studio-ml start 启动时指定端口 -p 8888

Q: Can't connect to ML backend http://127.0.0.1:8888/, health check failed. Make sure it is up and your firewall is properly configured.

A: label-studio-ml start 启动后会打印一个监听地址,label studio 前端添加该地址而非 http://127.0.0.1:8888/

Q: FileNotFoundError: Can't resolve url, neither hostname or project_dir passed: /data/upload/1/db8f065a-000001.jpg

A: 接口返回的是项目的相对地址,无法通过该地址直接读取到图片原件,需要配合 get_local_path 函数使用。

Q: UnicodeEncodeError: 'gbk' codec can't encode character '\xa0' in position 2: illegal multibyte sequence

A: 修改 C:\Users\Fantasy.conda\envs\labelstudio\lib\json_init_.py#line 179 为:

    for chunk in iterable:
fp.write(chunk.replace(u'\xa0', u''))

参考


  1. Cai Yichao. label_studio自动预标注功能. CSDN. [2022-01-19]

label studio 结合 MMDetection 实现数据集自动标记、模型迭代训练的闭环的更多相关文章

  1. Tapdata Cloud 2.1.4 来啦:数据连接又上新,PolarDB MySQL、轻流开始接入,可自动标记不支持的字段类型

      需求持续更新,优化一刻不停--Tapdata Cloud 2.1.4 来啦!   最新发布的版本中,在新增数据连接之余,默认标记不支持同步的字段类型,避免因此影响任务的正常运行. 更新速览 ① 数 ...

  2. Windows下mnist数据集caffemodel分类模型训练及测试

    1. MNIST数据集介绍 MNIST是一个手写数字数据库,样本收集的是美国中学生手写样本,比较符合实际情况,大体上样本是这样的: MNIST数据库有以下特性: 包含了60000个训练样本集和1000 ...

  3. 关于Spring @RequestBody 自动映射模型原理

    关于Spring @RequestBody 自动映射模型 2016年10月18日 22:17:12 稻子丶 阅读数:5049   在很多时候,Spring的注解为我们提供了很多方便,但只知道其用法,不 ...

  4. thinkphp模型中的获取器和修改器(根据字段名自动调用模型中的方法)

    thinkphp模型中的获取器和修改器(根据字段名自动调用模型中的方法) 一.总结 记得看下面 1.获取器的作用是在获取数据的字段值后自动进行处理 2.修改器的作用是可以在数据赋值的时候自动进行转换处 ...

  5. 吴裕雄 python 机器学习——超大规模数据集降维IncrementalPCA模型

    # -*- coding: utf-8 -*- import numpy as np import matplotlib.pyplot as plt from sklearn import datas ...

  6. Visual Studio Code使用typings拓展自动补全功能

    转自:http://blog.csdn.net/liyijun4114/article/details/51658087 参考来源: 官方介绍: https://code.visualstudio.c ...

  7. 在 Visual Studio 等编辑器/IDE中自动切换输入法,不需要手动的有没有?

    使用Visual Studio写代码,经常遇到的一个问题就是切换中文输入法麻烦,输入完注释//,要切换到中文,输入完引号,要输入中文,然后还需要切换回来,有没有? 有时候中文输入法忽然失效有没有?明明 ...

  8. Android studio之更改快捷键及自动导包

    更改AS中的代码提示快捷键,AS做的也挺智能的,在Keymap中可以选择使用eclipse的快捷键设置,但是虽然设置了,对有些快捷键还是不能使用,那么就需要我们手动去修改了. 在代码提示AS默认的快捷 ...

  9. Android Studio添加原生库并自动构建

    [时间:2017-09] [状态:Open] [关键词:Android,Android Studio,gradle,native,c,c++,cmake,原生开发,ndk-build] 0 引言 最近 ...

  10. 在visual studio code和visual studio中编写TypeScript文件自动生成JavaScript文件

    注:此处的自动生成都为保存ts文件时自动生成js文件 VS CODE 只需要在TypeScript的终端控制台中输入如下命令即可,并注意需要将其中的*换成对应的文件名,此处的*似乎不能作为通用匹配. ...

随机推荐

  1. Openstack Neutron : LBaaS v2

    目录 - LBaaS v2 - 负载均衡概念 - 服务器池 Pool - 监听器 Listener - L7 转发策略 l7 policy - 负载均衡算法 Algorithms - 健康监测 Mon ...

  2. .NET WebAPI 自定义 NullableConverter 解决请求入参 “”空字符触发转换异常问题

    最近在项目中启用了Nullable 可为空的类型,这个特性确实很好用,在 WebAPI 的入参上可以直接采用 ? 来标记一个字段是否允许为空,但是使用过程中遇到了如下一个问题,比如创建部门接口 我们定 ...

  3. 14. 第十三篇 二进制安装kube-proxy

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247484231&idx=1&sn=9e722bee ...

  4. centos7.5升级系统内核版本

    1.yum update curl nss 2.yum install wget 3.rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.or ...

  5. Debian+Wine For Termux,兼容Windows on arm的安卓手机子系统!

    如果已经安装了termux,先删掉. 安装方法 下载安装我提供的termux 链接: https://pan.baidu.com/s/13hbp6igps18V2RJcOxgQIg 提取码: 1irn ...

  6. spring boot集成redis基础入门

    redis 支持持久化数据,不仅支持key-value类型的数据,还拥有list,set,zset,hash等数据结构的存储. 可以进行master-slave模式的数据备份 更多redis相关文档请 ...

  7. 通过netty把百度地图API获取的地理位置从Android端发送到Java服务器端

    本篇记录我在实现时的思考过程,写给之后可能遇到困难的我自己也给到需要帮助的人. 写的比较浅显,见谅. 在写项目代码的时候,需要把Android端的位置信息传输到服务器端,通过Netty达到连续传输的效 ...

  8. [Err] 1052 - Column ‘roleId‘ in where clause is ambiguous

    1.先看错误的sql语句: select a.authName from roles as r,authority as a,role_ah as ra where ra.roleId=r.roleI ...

  9. 微服务组件--限流框架Spring Cloud Hystrix分析

    Hystrix的介绍 [1]Hystrix是springCloud的组件之一,Hystrix 可以让我们在分布式系统中对服务间的调用进行控制加入一些调用延迟或者依赖故障的容错机制. [2]Hystri ...

  10. 亚马逊云 RDB数据故障转移(多可用区)

    RDB关系数据库(Relational Database,RDB) 创建名为VPC for RDS的vpc 两个可用区,两组公内网 创建安全组 创建RDS数据库实例用的数据库子网组 创建RDS数据库实 ...