这个需求不是很常用,但需要用时,在网上不是很好搜索到,故在此记录。
例如,要表示“每月的第二个星期六的12:34:00”,是这样写:
0 34 12 ? * SAT#2
注意,这个不是标准的形式,不是所有所有程序都接受这样的格式。一般程序都会有校验功能,可以检查下次执行的时间来进行确认。(第一是”?”表示不指定不一定支持,第二是”SAT”不一定支持,第三是”#2″更不一定支持)
这个需求不是很常用,但需要用时,在网上不是很好搜索到,故在此记录。
例如,要表示“每月的第二个星期六的12:34:00”,是这样写:
0 34 12 ? * SAT#2
注意,这个不是标准的形式,不是所有所有程序都接受这样的格式。一般程序都会有校验功能,可以检查下次执行的时间来进行确认。(第一是”?”表示不指定不一定支持,第二是”SAT”不一定支持,第三是”#2″更不一定支持)
有一个项目升级到 Spring Boot 3.0,同事踩坑填坑花了点时间。遇到最大的一个坑就是 swagger 了,我顺便记录一下。
可以去它的官网看,SpringFox 已经有两三年没有更新了,属于被遗忘的项目。我去年也遇到 SpringFox 的问题,当时我用 Spring Boot 2.6,已经出现了不兼容报错的情况,所以退回了 2.5。今年是 Spring Boot 3.0,SpringFox 直接就别用了。
SpringDoc 可以自动化生成 API 文档,它支持:(注意,本文所指均是 SpringDoc v2 版本)
把原来的io.springfox
、swagger 2
相关的依赖都删除,然后添加 SpringDoc 的依赖:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.1.0</version> </dependency>
把原来 swagger 2 的注解替换成 swagger 3 的,springdoc-openapi-starter-webmvc-ui
中已经包含了 swagger 3 的依赖。
@Api
→ @Tag
@ApiIgnore
→ @Parameter(hidden = true)
或 @Operation(hidden = true)
或 @Hidden
@ApiImplicitParam
→ @Parameter
@ApiImplicitParams
→ @Parameters
@ApiModel
→ @Schema
@ApiModelProperty(hidden = true)
→ @Schema(accessMode = READ_ONLY)
@ApiModelProperty
→ @Schema
@ApiOperation(value = "foo", notes = "bar")
→ @Operation(summary = "foo", description = "bar")
@ApiParam
→ @Parameter
@ApiResponse(code = 404, message = "foo")
→ @ApiResponse(responseCode = "404", description = "foo")
之后 swagger 就可以在 http://server:port/context-path/swagger-ui.html
访问了。
创建一个 controller:
@RestController @RequestMapping("/test") public class TestController { @GetMapping("/greet") @CrossOrigin @Operation(summary = "say hello", description = "这里写描述") public String greet(@RequestParam(required = false) String name) { return "hello " + name; } }
访问 swagger 大概是这样:
具体的接口:
上面只展示了最简单的使用,高级的用法和具体的配置在这里先不展开,需要时自己看官网文档。
毕业后,好多年没有参加考试了。这次让我重温了看书学习赶 deadline 的感觉。没有刷题,纯靠看书理解。
做一个有资金从业资格的码农。
2022 匆匆过去了,这一年受疫情的影响相当大,无论是年初还是年尾,但都大步跨过了。
2023,稳扎稳打,继续努力。
一个 Pod 创建之后,Service 马上就能选择它,并且请求也有可能转发到这个 Pod。然而,Pod 的启动很可能是需要时间的,在启动、加载、预热的过程中,如果有请求转发进来,这个请求很可能会失败。
K8S 处理这个问题的办法是引入了就绪探针(Readiness Probe),readiness 这个词的词根不是“read”,而是“ready”,就是说这个 Pod ready 了没有。如果没 ready,请求就不转发给它;ready 之后,就可以转发给它。
综合运用 Liveness Probe 和 Readiness Probe,可以让服务的自愈、启动、重启、升级更得心应手。
就绪探针怎么判断 Pod 是否就绪?和存活探针(Liveness Probe)一样,常用的有 httpGet 和 exec 两种方式。
顾名思义,K8S 通过发送 HTTP GET 请求,判断 Pod 是否就绪。若该请求收到 HTTP 2xx~3xx 状态码均判断为成功。下面是例子:
apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 3 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: nginx:latest name: container-0 readinessProbe: httpGet: path: /health port: 8080 scheme: HTTP # 还可以是HTTPS initialDelaySeconds: 60 # 容器启动后要等待多少秒后探针才开始,默认是0秒,最小值是0 timeoutSeconds: 10 # 探测超时的阈值。默认值是1秒。最小值是1 periodSeconds: 30 # 每次执行探测的时间间隔(单位是秒)。默认是10秒。最小值 1 successThreshold: 1 # 视为成功的最小连续成功次数。默认值是1。存活和启动探测的该值必须是1。最小值是1 failureThreshold: 3 # 视为失败的最小连续失败次数。默认值是3。最小值是1
还可以通过 linux 命令的方式来判断,若 error code = 0,则表明命令正常。
apiVersion: v1 kind: Pod metadata: labels: test: liveness name: liveness-exec spec: containers: - name: liveness image: registry.k8s.io/busybox args: - /bin/sh - -c - touch /tmp/healthy; sleep 30; rm -f /tmp/healthy; sleep 600 readinessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5
上面这个例子中,每 5s 会cat /tmp/healthy
一次,30s 后该文件被删除,cat /tmp/healthy
就会失败。
服务在生产环境跑,出了点问题,需要 debug 级别的日志;但是平时生产环境谁用 debug,都是用 info 级别。
按一般的流程,改日志级别需要修改文件、重新打包发版,重启是免不了了,但重启可能导致问题不能复现。
现在才知道,日志级别是可以在运行过程中动态修改的。记录下来。
这个功能有赖于 Spring Boot Actuator。诶,又是你,之前项目中已经有引入的,用来做服务的监控。
如果没有引入,需要在pom.xml
中添加
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
然后找一下配置项management.endpoints.web.exposure.include
是否存在,如果没有就添加
management.endpoints.web.exposure.include=info,health,loggers
如果有的话,就在原有值里加上loggers
即可。
开启了 loggers endpoint,应用启动后就可以访问以下3个接口:
首先访问第一个接口(http://localhost:8080/actuator/loggers)看一看:
找到loggers
部分,我这里返回的是
"loggers": { "ROOT": { "configuredLevel": "INFO", "effectiveLevel": "INFO" } }
表示根节点的日志级别是INFO
。(访问http://localhost:8080/actuator/loggers/ROOT也可以看到)
那么要怎么修改呢,调用对应的POST
接口即可:
curl -X POST http://localhost:8080/actuator/loggers/ROOT -H "Content-Type: application/json" -d "{\"configuredLevel\":\"DEBUG\",\"effectiveLevel\":\"DEBUG\"}"
再次查看GET
接口,可以看到已经变为DEBUG
级别。
有关原理和其他细节,可以查看以下的参考。
首先说明,这个运行其他 Python 版本并不是影响 jupyter notebook 的 Python 版本,而是在 notebook 中通过!python xxxxx.py
执行我上传的脚本所使用的 Python。
撰写本文时,Colab 的 Python 是 3.7。
我的项目中用到了一些 3.8、3.9 的新语法,所以没法在 Colab 上面跑,但我也不想把这些语法改成旧的,所以只能在 Colab 上升级了。
经过一番摸索,发现最靠谱的方法是在 apt 中安装新版 Python,然后全程使用 venv,而不用系统的 Python。系统的 Python 总是遇到 pip 安装库时的各种奇怪问题。
所以,在 notebook 中的命令如下:
!sudo apt-get update -y !sudo apt-get install python3.9 python3.9-distutils python3.9-venv !python3.9 --version !python3.9 -m venv venv
之后运行 pip 安装第三方库、运行 python xxxx.py 时,都使用如下方式:
!venv/bin/pip install xxx_library !venv/bin/python xxx.py
有一个后端的 Java/Kotlin 项目需要与同事的模块通信,协议是 protobuf。
这块我想用 Kotlin 来写,当然 Java 也不是不行。没想到踩了些坑,故以本文作记录。
protobuf 官方是有 Kotlin 的教程的,但是并不全面,这是出现各种坑的根源。
另外,我原以为 protobuf 对 Kotlin 的专门支持是多年前就有的(指的是支持 Kotlin 的各种方便写法),然而我后来才发现是2021年才有,见 Announcing Kotlin support for protocol buffers。
首先找同事得到 .proto 文件,然后下载 protoc,解压后得到一个可执行文件。
然后按照官方教程,在命令行中执行
protoc --java_out=$DST_DIR --kotlin_out=$DST_DIR xxxxxx.proto
注意--java_out
和--kotlin_out
都是需要的,当前版本的 protoc 对 Kotlin 的支持是在原来 Java 生成的基础上多了 Kotlin 的增强。
然后就得到了一些 .java .kt 文件。
把生成的文件放进项目中,可以发现编译是不能通过的,因为缺少 protobuf 相关库。我的项目是 Maven 的,所以在 pom.xml 中加上
<!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-kotlin --> <dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-kotlin</artifactId> <version>3.19.4</version> <!-- version 要与之前 protoc 的版本对应 --> </dependency>
这个 pom 的 artifactId (protobuf-kotlin) 是关键!!我看到的文档写的都是protobuf-java
,就会导致生成的 .kt 里面的
@kotlin.OptIn(com.google.protobuf.kotlin.OnlyForUseByGeneratedProtoCode::class)
报找不到的错误,必须使用protobuf-kotlin
。
这里参照 Announcing Kotlin support for protocol buffers 举的例子,用了 Kotlin 之后,写法更简洁了。假设原来 Java 的是这样写:
DiceSeries series = DiceSeries.newBuilder() .addRoll(DiceRoll.newBuilder() .setValue(5)) .addRoll(DiceRoll.newBuilder() .setValue(20) .setNickname("critical hit")) .build()
而 Kotlin 可以改成:
val series = diceSeries { rolls = listOf( diceRoll { value = 5 }, diceRoll { value = 20 nickname = "critical hit" } ) }
至于如何序列化和反序列化,继续用刚才的例子,序列化是这样的:
series.toByteArray()
反序列化:
DiceSeries.parseFrom(message)
我想在一个 suspend fun 里面 launch 一个协程,应该怎么做?
我用了GlobalScope.launch {}
,但是 IDE 给我标黄了,不建议我这样写,那应该怎么写呢?
带着这个问题,我搜索并阅读了相关参考中的解答,记录为本文。
CoroutineScope
首先是一个接口,这个接口要求有一个CoroutineContext
属性,相当于CoroutineScope
给这个属性包了一层。具体来说,CoroutineScope
是这样定义的:
public interface CoroutineScope { /** * The context of this scope. * Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope. * Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages. * * By convention, should contain an instance of a [job][Job] to enforce structured concurrency. */ public val coroutineContext: CoroutineContext }
然后,CoroutineScope
同样的名字,还是一个函数,我们代码中调用的就是这个函数。这个CoroutineScope
函数会创建一个给定context
的CoroutineScope
。通过下面的代码我们还可以发现,该函数返回的 scope 的context
里面肯定会带有一个Job
,如果传入的context
没有Job
,函数会给你附送一个。
/** * Creates a [CoroutineScope] that wraps the given coroutine [context]. * * If the given [context] does not contain a [Job] element, then a default `Job()` is created. * This way, cancellation or failure of any child coroutine in this scope cancels all the other children, * just like inside [coroutineScope] block. */ @Suppress("FunctionName") public fun CoroutineScope(context: CoroutineContext): CoroutineScope = ContextScope(if (context[Job] != null) context else context + Job() ) internal class ContextScope(context: CoroutineContext) : CoroutineScope { override val coroutineContext: CoroutineContext = context // CoroutineScope is used intentionally for user-friendly representation override fun toString(): String = "CoroutineScope(coroutineContext=$coroutineContext)" }
上面提到的context
,它包括了一系列的参数,用来决定该协程将如何执行。这些参数主要有:
最重要的两个参数当然是CoroutineDispatcher
和Job
。
Dispatcher 主要有自带的Dispatchers.Default
、Dispatchers.IO
、Dispatchers.Main
(Android)。
CPU密集型的任务用Dispatchers.Default
;而网络、文件的 IO 就用Dispatchers.IO
,大家都懂的。详情可看官方文档。
而 Job 则代表了创建出来的协程,launch()
或aysnc()
会返回一个Job
的实例。可以调用Job
实例的isActive
、isCancelled
获取协程的状态,也可以调用它的cancel()
等方法手动取消这个协程。
而GlobalScope
是什么呢?可以看到,它继承了CoroutineScope
,并且它的context
是固定的EmptyCoroutineContext
。
@DelicateCoroutinesApi public object GlobalScope : CoroutineScope { /** * Returns [EmptyCoroutineContext]. */ override val coroutineContext: CoroutineContext get() = EmptyCoroutineContext }
那么回到最开始的问题,GlobalScope.launch
与CoroutineScope.launch
的区别是什么?
GlobalScope.launch {}
会在顶层创建一个协程,跑在 Dispatchers.Default 所指定的线程中;GlobalScope.launch(Dispatchers.IO) {}
会在顶层创建一个协程,跑在 Dispatchers.IO 对应的 IO 线程中;CoroutineScope(Dispatchers.IO).launch {}
和第 2 个是一样的,也是在顶层创建,只是语法的区别;launch {}
会沿用当前的 context,不在顶层,但本文的背景为“在一个 suspend fun 中创建协程”,在没有 scope 的情况下,是不能直接用launch {}
的;CoroutineScope(currentCoroutineContext()).launch {}
沿用当前的 context,且不在顶层;我在 GitHub 上搜了一下,挺少人这样写的上面提到的“在顶层”的意思是,不受 structured concurrency 的影响,即不会被父协程的 cancel() 取消,也不会被其他协程抛出的异常导致自己也被退出。
对于一个在顶层被创建的协程,不用的时候记得 cancel(),否则它会在后台一直跑,直到里面的程序结束,这也是 IDE 把 GlobalScope 标黄的原因。
// 最后附上我测试的代码,test()中有3种写法,在顶层创建的协程不会受RuntimeException的影响 import kotlinx.coroutines.* import kotlin.concurrent.thread fun main() { thread(isDaemon = true) { runBlocking { test() delay(1000L) throw RuntimeException() } } Thread.sleep(5000L) } suspend fun test() { // launch { // 不可以 CoroutineScope(currentCoroutineContext()).launch(Dispatchers.IO) { // CoroutineScope(Dispatchers.Default).launch { // GlobalScope.launch { while (true) { println("${Thread.currentThread().name}, ${currentCoroutineContext()} inside") delay(1000L) } } }