Eisen's Blog

© 2024. All rights reserved.

对目前校验与异常处理的总结

2020 November-22

目前的问题

很久之前记录的一篇文章在用 Spring MVC 构建 RESTful API 时进行验证和异常处理,讨论了基本的 rest 的校验思路:

  1. 区分 FormObjectDomain Entity 尽量以 user-case 的场景为每一个 input 做独立的校验;
  2. 基本结构校验(Bean Validation)和业务校验(Service Business Logic)分别处理;
  3. 基本结构校验在 MVC 层,通过 ControllerAdviceBindingResult 对其校验内容做转换暴露给客户端;

在最近几年的项目里,基本还是依照这个思路来的。但是有以下几个痛点没有解决:

  1. 业务逻辑和基本校验的逻辑很多时候边界很模糊,对于客户端来说,其实没什么区别,强行区分显得不是那么优雅;
  2. 随着业务逻辑的越发复杂,大量的校验逻辑肯定是会放在 application 层的,那么关注点分离的需求愈发明显,将校验逻辑和正常流程区分才能让代码变得更清晰易懂;
  3. Exception 的类别越来越多,报错信息五花八门,需要对其进行统一的梳理,并整理出一套规则,方便后续的维护开发套用;

目前的尝试

为了解决以上问题,曾经做过以下的改进:

对于第一条,并没有做处理,依然让业务逻辑和基本校验做了分离,尤其是对于那种需要触碰数据库的业务(比如查重)还是把它当作是业务逻辑的,仅仅是说通过 BindingResult 返回一致的报错结果了。这部分还是很希望能够把一些更复杂的校验也放到 Bean Validation 中去的。

对于第二条,首先在 application 层也提供了一个 ValidationResult 它实现了 org.springframework.validation.Errors 可是说是在 applicationBindingResult 的同类了,这样子就可以在 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 层类似的逻辑:

  1. 有一个和 BindingResult 等价的 ValidationResult 用于收集校验结果;
  2. 当校验不通过时,抛出异常 InvalidRequestException(validationResult);

这个方法很好的将业务逻辑异常拆出来了,还是很能解决问题的,也没太多副作用。硬要说的话就是 ValidationResult 有点难看,InvalidRequestException 有点像是 web 层漏到 application 层了,应该用个更好的类替代。

对于第三点,整理出了一个类 ErrorCode 用来安放 ErrorCodeErrorMessage

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 异常处理和校验的一些内容,后面应该会做以下事情:

  1. 梳理目前 spring exception 的方式
  2. 提出适合当前项目的最佳实践
  3. 对目前的校验做改进,提出校验方面的最佳实践

将 MySQL 通过 bitpoke/mysql-operator 部署到 k8s 内部

2020 October-19

目前 openbayes 的几乎所有组件都部署在 k8s 内部,但 mysql 作为核心的数据存储节点对其要求都蛮高的,通常来说是需要独立部署的,对于目前的业务场景,其要求主要包含以下几点:

  1. 需要持久化存储,一旦数据丢失问题非常严重
  2. 对性能有要求,不然会拖垮依赖它的一切服务
  3. 需要一些额外的备份机制,可以快速的从一个备份做恢复
  4. 需要对应的监控体系
  5. mysql 需要可以比较容易的通过各种客户端访问,方便不同的角色对数据做分析或者做 debug
  6. 在规模比较大的时候可能会做读写分离

之所以希望将 mysql 部署到 k8s 内主要还是希望达到以下目的:

  1. 减少外部依赖,支持更广泛部署场景;目前对于一些环境是使用了云服务商所提供的数据库(aws / ucloud),然而并不是所有的情况都能这么做。
  2. 统一部署模式,降低部署门槛;对于无法使用云服务商的数据库的场景,通常需要独立在某一台机器上安装 mysql 但这个部署模式与 k8s 是分离的,相当于多了一部分手工部署的工作量,而且手动部署也很难满足以上的几点要求的,自动化越少,部署门槛就会越高。

openbayes 本身对数据库性能的要求没有那么高,用户的大量数据是以分布式存储的形式保存的,mysql 的负载通常不高,这也让 mysql 部署在 k8s 里成为了可能。

下面介绍 bitpoke/mysql-operator 如何满足上述要求。

基本介绍

在使用云服务商的数据库的时候我就在想,如果能有一套 k8s 的 operator 能够支持快速部署 / 数据库配置 / 周期性备份 / prometheus 指标暴露就好了,在做了简单的搜索后还真的发现了这么个东西 bitpoke/mysql-operator ,满足了说所提及的这一切:

  1. 内置了 mysql 部署配置,简单修改配置可以实现将 mysql 的存储放置在 hostPath 或者指定的 storageClass 解决了持久化存储的问题
  2. 既然可以指定具体部署的存储,那么也能指定 mysql 部署的节点,性能的问题基本得到解决
  3. 内置 extraBackup 支持手动或者 cronjob 周期性备份数据库到指定的对象存储
  4. 部署起来的 mysql 自带 exporter 可以直接和 prometheus 对接,然后把数据通过 grafana 展示,监控 / 告警也就有了
  5. 通过配置额外的 nodePort 类型的 Service 可以将 mysql 服务暴露出来,外部访问的问题就解决了
  6. 这个 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 需要修改的部分主要就是两部分:

  1. 镜像位置(image sidecarImage orchestrator.image),国内部署速度不太行,建议自行拉到访问比较好的国内节点
  2. 存储,默认 persistence.enabled: false 可以按照自己的情况做修改,这里只支持 storageClass 的方式

部署好之后才是第一步,即成功部署了 operator 本身,下面就是具体部署一个 mysql 了,在 https://github.com/bitpoke/mysql-operator/tree/master/examples 有一个例子,可以看到 mysql 被定义为了一个叫做 MysqlCluster 的 CRD。主要需要修改的部分有以下:

  1. secretNamehttps://github.com/bitpoke/mysql-operator/blob/master/examples/example-cluster-secret.yaml 指初始化的一些数据,如 root 密码,数据库名称,用户名,用户密码
  2. image / mysqlVersion mysql 的镜像,同样推荐修改为国内的镜像,具体版本也依照实际情况
  3. backupSchedule 如果设置则是需要周期性备份,数据会按照该配置定期备份到指定的对象存储中,当然 backupSecretName 也需要配置正确才能使用
  4. mysqlConf 对应 mysql.cnf 中的字段,依据自己需求配置
  5. volumeSpec 数据持久化方式,和上文中 operator 的类似,但是更灵活,支持 hostPath
  6. initFileExtraSQL 感觉这个 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

这些步骤我都测试过了,确认可以走的通的。以及这个备份的功能已经非常体贴了:

  1. 支持手动备份通过 cron 控制
  2. 支持保存最近的 N 个版本
  3. 恢复只需要在初始 mysql 时填写 s3 路径即可

在备份到 s3 不成功可以看看具体的报错信息,它具体备份采用的是 rclone 这个工具。不成功基本就是两个方向:

  1. s3 设置有问题,上传直接挂了
  2. 你所使用的对象存储可能不是 rclone 会完全支持的,这种情况比较少见,但是我确实踩到了,具体来讲就是 ucloud 之前缺乏某些操作的支持,但是目前已经支持了呢

监控

2020 10 19 18 34 53

如上图所示,这是我直接将 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

独立 io

在使用的过程中遇到一个特殊的情况,mysql 如果和其他的服务共用一个 storageClass 可能会出现 io 抢占的情况,导致 mysql 的延迟非常巨大。目前 k8s 还没有一个很好的办法解决这个问题。唯一想到的就是为 mysql 分配一套单独的 storageClass(比如 local storage path 的方案)。


youtube-dl 的一些实用技巧

2020 October-09

众所周知,在国内 youtube 是不能使用的,但 youtube 目前已经成为了一个视频 host 的存在了,其内容是非常丰富的,丰富到各种各样的学习资源也都放在这里。对个人来说,通过一些手段勉强还能看到一些内容,但如果想要投屏、或者分享,这就需要做一些搬运的工作了。目前来看 youtude-dl 是最强的 youtube 资源下载工具,没有之一。这里结合场景记录一下最近使用它的一些技巧,方便后续查阅。

目前来看,bilibili 的性质非常像 youtube 如果需要将一些资源投屏或者分享给其他人,比较简单的办法就是将 youtube 资源直接搬运到 bilibili。这种场景通常有以下需求:

  1. 需要下载最好的画质
  2. 需要考虑下载声音和视频,当然有些时候可能只下载声音就够了
  3. 需要下载字幕

下面以一个 fastai 的搬运为例,来看看怎么用 youtube-dl 实现上述功能。

2020 10 09 23 37 08

这个是最新的 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 上传也没什么问题。但是有两个小缺点:

  1. mkv 格式相对于 mp4 的兼容性会略差,也就是有些地方不一定可以播放,并且 bilbili 是更推荐上传 mp4 格式
  2. 没有下载相应的字幕,算是个生肉

下一步为了解决以上两个问题,可以做下述的变化。

修改输出格式

首先是更换输出文件格式:

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)

可以看到存在三种类型:

  1. audio only
  2. video only
  3. video + audio

这个视频很显然其 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