很久之前记录的一篇文章在用 Spring MVC 构建 RESTful API 时进行验证和异常处理,讨论了基本的 rest 的校验思路:
FormObject
和 Domain Entity
尽量以 user-case 的场景为每一个 input 做独立的校验;ControllerAdvice
和 BindingResult
对其校验内容做转换暴露给客户端;在最近几年的项目里,基本还是依照这个思路来的。但是有以下几个痛点没有解决:
application
层的,那么关注点分离的需求愈发明显,将校验逻辑和正常流程区分才能让代码变得更清晰易懂;Exception
的类别越来越多,报错信息五花八门,需要对其进行统一的梳理,并整理出一套规则,方便后续的维护开发套用;为了解决以上问题,曾经做过以下的改进:
对于第一条,并没有做处理,依然让业务逻辑和基本校验做了分离,尤其是对于那种需要触碰数据库的业务(比如查重)还是把它当作是业务逻辑的,仅仅是说通过 BindingResult
返回一致的报错结果了。这部分还是很希望能够把一些更复杂的校验也放到 Bean Validation 中去的。
对于第二条,首先在 application
层也提供了一个 ValidationResult
它实现了 org.springframework.validation.Errors
可是说是在 application
层 BindingResult
的同类了,这样子就可以在 application
层返回和 web
层一样的报错结果了。然后提出了一个 Validator
的概念,专门用来做校验工作。举一个例子,下面是一个创建 AutoML
的方法,可以看到第一步就是做校验:
public AutoML createAutoML(CreateModelCommand command) {
autoMLValidator.validate(command);
...
}
而 AutoMLValidator
内容如下:
public class AutoMLValidator {
...
void validate(CreateModelCommand command) {
ValidationResult result = new ValidationResult();
modelTemplateValidator.validate(command, result);
autoMLNameValidator.validate(command, result);
resourceValidator.validate(command, result);
if (result.hasErrors()) {
throw new InvalidRequestException(result);
}
autoMLDataBindingsValidator.validate(command, result);
if (result.hasErrors()) {
throw new InvalidRequestException(result);
}
parallelRunningCountValidator.validate(command, result);
ClusterResource clusterResource = getResource(command.getResource());
resourceRelatedValidator.validateJobLimitationAndComputationAndStorage(
command.getOwner(), clusterResource);
if (result.hasErrors()) {
throw new InvalidRequestException(result);
}
}
...
}
可以看到,这里面使用了 web
层类似的逻辑:
BindingResult
等价的 ValidationResult
用于收集校验结果;InvalidRequestException(validationResult)
;这个方法很好的将业务逻辑异常拆出来了,还是很能解决问题的,也没太多副作用。硬要说的话就是 ValidationResult
有点难看,InvalidRequestException
有点像是 web 层漏到 application 层了,应该用个更好的类替代。
对于第三点,整理出了一个类 ErrorCode
用来安放 ErrorCode
与 ErrorMessage
:
public enum ErrorCode {
TRANSFER_REQUEST_TARGET_USER_EXISTED("资源已经有目标用户,请先将之前请求撤销"),
RESOURCE_NAME_NOTMATCH(" 资源名称不匹配"),
RESOURCE_TRANSFER_TO_SELF(" 不许与资源拥有者将资源传递给自己"),
RESET_PASSWORD_MISMATCH("无效的验证码"),
CODE_MISMATCH("无效的验证码"),
...
private String message;
ErrorCode(String message) {
this.message = message;
}
}
在具体的报错的时候将 ad-hoc 的报错替换成这样的 KV 对即可。很显然这个写法有点像一个低配版本的国际化方案,显得有点拙劣,干嘛不直接换成 i18n 的 messages 呢?并且这个报错的格式也没有做统一,依然显得有点混乱。
为了进一步的解决以上依然存在的问题,最近也开始重新看了 spring / springmvc / springboot 异常处理和校验的一些内容,后面应该会做以下事情:
目前 openbayes 的几乎所有组件都部署在 k8s 内部,但 mysql 作为核心的数据存储节点对其要求都蛮高的,通常来说是需要独立部署的,对于目前的业务场景,其要求主要包含以下几点:
之所以希望将 mysql 部署到 k8s 内主要还是希望达到以下目的:
openbayes 本身对数据库性能的要求没有那么高,用户的大量数据是以分布式存储的形式保存的,mysql 的负载通常不高,这也让 mysql 部署在 k8s 里成为了可能。
下面介绍 bitpoke/mysql-operator 如何满足上述要求。
在使用云服务商的数据库的时候我就在想,如果能有一套 k8s 的 operator 能够支持快速部署 / 数据库配置 / 周期性备份 / prometheus 指标暴露就好了,在做了简单的搜索后还真的发现了这么个东西 bitpoke/mysql-operator ,满足了说所提及的这一切:
https://github.com/bitpoke/mysql-operator/blob/master/deploy/charts/mysql-operator/values.yaml 这是 helm charts 的 values.yaml 把这个文件下载到本地,按照具体环境做一定修改后执行以下命令即可部署 operator 了:
# 这里用的是 helm3
helm repo add bitpoke https://helm-charts.bitpoke.io
helm install mysql-operator bitpoke/mysql-operator \
-f values.yaml \
-n infra --create-namespace
其中 values.yaml
需要修改的部分主要就是两部分:
persistence.enabled: false
可以按照自己的情况做修改,这里只支持 storageClass
的方式部署好之后才是第一步,即成功部署了 operator
本身,下面就是具体部署一个 mysql
了,在 https://github.com/bitpoke/mysql-operator/tree/master/examples 有一个例子,可以看到 mysql 被定义为了一个叫做 MysqlCluster
的 CRD。主要需要修改的部分有以下:
secretName
见 https://github.com/bitpoke/mysql-operator/blob/master/examples/example-cluster-secret.yaml 指初始化的一些数据,如 root 密码,数据库名称,用户名,用户密码image
/ mysqlVersion
mysql 的镜像,同样推荐修改为国内的镜像,具体版本也依照实际情况backupSchedule
如果设置则是需要周期性备份,数据会按照该配置定期备份到指定的对象存储中,当然 backupSecretName
也需要配置正确才能使用mysqlConf
对应 mysql.cnf 中的字段,依据自己需求配置volumeSpec
数据持久化方式,和上文中 operator
的类似,但是更灵活,支持 hostPathinitFileExtraSQL
感觉这个 MysqlCluster 是希望用户每个数据库建立一个独立的资源,但是 openbayes 这里有一些附属数据库如果分开放置感觉有点没必要,所以这里就采用这个机制同时初始化了其他的数据库 initFileExtraSQL:
- "CREATE DATABASE IF NOT EXISTS `<otherdb>`"
- "DROP USER IF EXISTS <otheruser>@'%'"
- "CREATE USER <otheruser>@'%' IDENTIFIED BY '<PASSOWRD>'"
- "GRANT ALL PRIVILEGES ON <otherdb>.* TO <otheruser>@'%'"
- "FLUSH PRIVILEGES"
注意这里有个奇怪的写法是需要先去 DROP USER
... 至于为啥我并不知道,我只知道不这么做就是会报错...
如上文所述,这个 MysqlCluster
支持自动的备份,当然也支持主动的备份,具体的文档在Cluster Backups and Recovery。
既然支持备份也支持恢复,具体的文档在也在Cluster Backups and Recovery。
这些步骤我都测试过了,确认可以走的通的。以及这个备份的功能已经非常体贴了:
在备份到 s3 不成功可以看看具体的报错信息,它具体备份采用的是 rclone 这个工具。不成功基本就是两个方向:
如上图所示,这是我直接将 https://grafana.com/grafana/dashboards/7362 这个仪表盘导入所看到的效果。
增加一个额外的 NodePort 即可:
apiVersion: v1
kind: Service
metadata:
name: local-openbayes-mysql-nodeport-master
spec:
ports:
- name: mysql
port: 3306
protocol: TCP
targetPort: 3306
nodePort: 30016
selector:
app.kubernetes.io/managed-by: mysql.bitpoke.org
app.kubernetes.io/name: mysql
mysql.bitpoke.org/cluster: <local-openbayes>
role: master
type: NodePort
在使用的过程中遇到一个特殊的情况,mysql 如果和其他的服务共用一个 storageClass 可能会出现 io 抢占的情况,导致 mysql 的延迟非常巨大。目前 k8s 还没有一个很好的办法解决这个问题。唯一想到的就是为 mysql 分配一套单独的 storageClass(比如 local storage path 的方案)。
众所周知,在国内 youtube 是不能使用的,但 youtube 目前已经成为了一个视频 host 的存在了,其内容是非常丰富的,丰富到各种各样的学习资源也都放在这里。对个人来说,通过一些手段勉强还能看到一些内容,但如果想要投屏、或者分享,这就需要做一些搬运的工作了。目前来看 youtude-dl 是最强的 youtube 资源下载工具,没有之一。这里结合场景记录一下最近使用它的一些技巧,方便后续查阅。
目前来看,bilibili 的性质非常像 youtube 如果需要将一些资源投屏或者分享给其他人,比较简单的办法就是将 youtube 资源直接搬运到 bilibili。这种场景通常有以下需求:
下面以一个 fastai 的搬运为例,来看看怎么用 youtube-dl 实现上述功能。
这个是最新的 fastai 的视频讲解的第一节,其 url 为 https://www.youtube.com/watch?v=_QUEXsHfsA0&t=4s 。
如果有最简单的命令如下:
youtube-dl https://www.youtube.com/watch\?v\=_QUEXsHfsA0\&t\=4s
其生成日志如下:
[youtube] _QUEXsHfsA0: Downloading webpage
WARNING: Requested formats are incompatible for merge and will be merged into mkv.
[download] Destination: Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.f137.mp4
[download] 100% of 178.27MiB in 02:17
[download] Destination: Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.f251.webm
[download] 100% of 75.84MiB in 01:16
[ffmpeg] Merging formats into "Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.mkv"
Deleting original file Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.f137.mp4 (pass -k to keep)
Deleting original file Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.f251.webm (pass -k to keep)
首先这样子生成的 .mkv
格式文件找个播放器就可以用了。自己拷贝到自己的设备上也可以看了,并且其默认下载的就是最优画质,这个甚至 bilibili 上传也没什么问题。但是有两个小缺点:
下一步为了解决以上两个问题,可以做下述的变化。
首先是更换输出文件格式:
youtube-dl https://www.youtube.com/watch\?v\=_QUEXsHfsA0\&t\=4s --merge-output-format mp4
这样子输出的就是 .mp4
格式了。
在下载字幕之前首先需要确认下该视频到底支持哪些语言的字幕:
youtube-dl https://www.youtube.com/watch\?v\=_QUEXsHfsA0\&t\=4s --list-subs
会展示所有支持的字幕以及其格式,包括机器生成字幕以及人工提交字幕等内容:
[youtube] _QUEXsHfsA0: Downloading webpage
[youtube] _QUEXsHfsA0: Looking for automatic captions
Available automatic captions for _QUEXsHfsA0:
Language formats
my vtt, ttml, srv3, srv2, srv1
ca vtt, ttml, srv3, srv2, srv1
ceb vtt, ttml, srv3, srv2, srv1
zh-Hans vtt, ttml, srv3, srv2, srv1
zh-Hant vtt, ttml, srv3, srv2, srv1
co vtt, ttml, srv3, srv2, srv1
...
Available subtitles for _QUEXsHfsA0:
Language formats
ar vtt, ttml, srv3, srv2, srv1
bg vtt, ttml, srv3, srv2, srv1
zh-Hans vtt, ttml, srv3, srv2, srv1
en vtt, ttml, srv3, srv2, srv1
可以看到人工提交的字幕有 zh-Hans
不过其提供的字幕格式(vtt, ttml, srv3, srv2, srv1)很不常见的样子。在 bilibili 是需要提交 srt
格式的字幕。这里 youtube-dl
也给考虑到了,其 --convert-subs
参数支持 srt ass vtt lrc
这些字幕格式的转换:
youtube-dl https://www.youtube.com/watch\?v\=_QUEXsHfsA0\&t\=4s --merge-output-format mp4 --write-sub --sub-lang zh-Hans --sub-format vtt --convert-subs srt
这样子对应的字幕以及格式转换也都搞定了:
[youtube] _QUEXsHfsA0: Downloading webpage
[info] Writing video subtitles to: Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.zh-Hans.vtt
[download] Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.mp4 has already been downloaded and merged
[ffmpeg] Converting subtitles
Deleting original file Lesson 1 - Deep Learning for Coders (2020)-_QUEXsHfsA0.zh-Hans.vtt (pass -k to keep)
如果需要自由选择下载视频的大小,可以按照以下方式操作:
首先通过参数罗列支持的格式:
youtube-dl https://www.youtube.com/watch\?v\=_QUEXsHfsA0\&t\=4s --list-formats
输出如下所示:
[youtube] _QUEXsHfsA0: Downloading webpage
[info] Available formats for _QUEXsHfsA0:
format code extension resolution note
249 webm audio only tiny 54k , opus @ 50k (48000Hz), 27.19MiB
250 webm audio only tiny 87k , opus @ 70k (48000Hz), 40.85MiB
140 m4a audio only tiny 134k , m4a_dash container, mp4a.40.2@128k (44100Hz), 76.42MiB
251 webm audio only tiny 164k , opus @160k (48000Hz), 75.84MiB
160 mp4 256x144 144p 71k , avc1.4d400c, 30fps, video only, 8.53MiB
278 webm 256x144 144p 87k , webm container, vp9, 30fps, video only, 19.13MiB
242 webm 426x240 240p 171k , vp9, 30fps, video only, 29.85MiB
133 mp4 426x240 240p 220k , avc1.4d4015, 30fps, video only, 15.86MiB
243 webm 640x360 360p 406k , vp9, 30fps, video only, 58.88MiB
134 mp4 640x360 360p 584k , avc1.4d401e, 30fps, video only, 28.04MiB
244 webm 854x480 480p 766k , vp9, 30fps, video only, 96.62MiB
135 mp4 854x480 480p 1157k , avc1.4d401f, 30fps, video only, 42.30MiB
247 webm 1280x720 720p 1622k , vp9, 30fps, video only, 180.75MiB
136 mp4 1280x720 720p 2102k , avc1.4d401f, 30fps, video only, 60.57MiB
248 webm 1920x1080 1080p 3132k , vp9, 30fps, video only, 324.32MiB
137 mp4 1920x1080 1080p 4356k , avc1.640028, 30fps, video only, 178.27MiB
22 mp4 1280x720 720p 231k , avc1.64001F, 30fps, mp4a.40.2@192k (44100Hz)
18 mp4 640x360 360p 297k , avc1.42001E, 30fps, mp4a.40.2@ 96k (44100Hz), 175.59MiB (best)
可以看到存在三种类型:
这个视频很显然其 video + audio 的格式 22 和 18 都不是最优解。默认是选取了 251 和 137(最好声音 + 最好画质)做了合并(也就是 --merge-output-format
的作用)。如果需要其他组合可以通过命令 -f <video>+<audio>
形式:
youtube-dl https://www.youtube.com/watch\?v\=_QUEXsHfsA0\&t\=4s -f 137+140 --merge-output-format mp4 --write-sub --sub-lang zh-Hans --sub-format vtt --convert-subs srt