• K8S 之就绪探针(Readiness Probe)

    一个 Pod 创建之后,Service 马上就能选择它,并且请求也有可能转发到这个 Pod。然而,Pod 的启动很可能是需要时间的,在启动、加载、预热的过程中,如果有请求转发进来,这个请求很可能会失败。

    K8S 处理这个问题的办法是引入了就绪探针(Readiness Probe),readiness 这个词的词根不是“read”,而是“ready”,就是说这个 Pod ready 了没有。如果没 ready,请求就不转发给它;ready 之后,就可以转发给它。

    综合运用 Liveness Probe 和 Readiness Probe,可以让服务的自愈、启动、重启、升级更得心应手。

    就绪探针怎么判断 Pod 是否就绪?和存活探针(Liveness Probe)一样,常用的有 httpGet 和 exec 两种方式。

    httpGet

    顾名思义,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

    exec

    还可以通过 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就会失败。

    相关参考

    1. K8S官方文档 除了 httpGet 和 exec,还有基于 TCP 端口和 gRPC 的探测方式
    2. https://support.huaweicloud.com/devg-cci/cci_05_0026.html
  • Spring Boot 动态修改日志级别

    背景

    服务在生产环境跑,出了点问题,需要 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个接口:

    • GET /actuator/loggers 返回当前应用全部的日志级别信息
    • GET /actuator/loggers/{name} 查看{name}的日志级别
    • POST /actuator/loggers/{name} 修改{name}的日志级别

    首先访问第一个接口(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级别。

    有关原理和其他细节,可以查看以下的参考。

    相关参考

    1. Spring Boot 系列(4):日志动态配置详解 – 掘金 (juejin.cn)
  • Google Colab 运行其他 Python 版本

    首先说明,这个运行其他 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
  • Kotlin 使用 protobuf 的正确方法(Maven)

    背景

    有一个后端的 Java/Kotlin 项目需要与同事的模块通信,协议是 protobuf。

    这块我想用 Kotlin 来写,当然 Java 也不是不行。没想到踩了些坑,故以本文作记录。

    文档

    protobuf 官方是有 Kotlin 的教程的,但是并不全面,这是出现各种坑的根源。

    另外,我原以为 protobuf 对 Kotlin 的专门支持是多年前就有的(指的是支持 Kotlin 的各种方便写法),然而我后来才发现是2021年才有,见 Announcing Kotlin support for protocol buffers

    方法

    1. 从 .proto 生成 .java .kt

    首先找同事得到 .proto 文件,然后下载 protoc,解压后得到一个可执行文件。

    然后按照官方教程,在命令行中执行

    protoc --java_out=$DST_DIR --kotlin_out=$DST_DIR xxxxxx.proto

    注意--java_out--kotlin_out都是需要的,当前版本的 protoc 对 Kotlin 的支持是在原来 Java 生成的基础上多了 Kotlin 的增强。

    然后就得到了一些 .java .kt 文件。

    2. Maven

    把生成的文件放进项目中,可以发现编译是不能通过的,因为缺少 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

    3. 使用

    这里参照 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)
  • GlobalScope.launch 与 CoroutineScope.launch 的区别?

    背景

    我想在一个 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函数会创建一个给定contextCoroutineScope。通过下面的代码我们还可以发现,该函数返回的 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 — 控制协程的生命周期
    • CoroutineName — 协程的名称
    • CoroutineExceptionHandler — 处理未被捕获的异常

    最重要的两个参数当然是CoroutineDispatcherJob

    Dispatcher 主要有自带的Dispatchers.DefaultDispatchers.IODispatchers.Main(Android)。

    CPU密集型的任务用Dispatchers.Default;而网络、文件的 IO 就用Dispatchers.IO,大家都懂的。详情可看官方文档

    而 Job 则代表了创建出来的协程,launch()aysnc()会返回一个Job的实例。可以调用Job实例的isActiveisCancelled获取协程的状态,也可以调用它的cancel()等方法手动取消这个协程。

    GlobalScope是什么呢?可以看到,它继承了CoroutineScope,并且它的context是固定的EmptyCoroutineContext

    @DelicateCoroutinesApi
    public object GlobalScope : CoroutineScope {
        /**
         * Returns [EmptyCoroutineContext].
         */
        override val coroutineContext: CoroutineContext
            get() = EmptyCoroutineContext
    }

    那么回到最开始的问题,GlobalScope.launchCoroutineScope.launch的区别是什么?

    1. GlobalScope.launch {}会在顶层创建一个协程,跑在 Dispatchers.Default 所指定的线程中;
    2. GlobalScope.launch(Dispatchers.IO) {}会在顶层创建一个协程,跑在 Dispatchers.IO 对应的 IO 线程中;
    3. CoroutineScope(Dispatchers.IO).launch {}和第 2 个是一样的,也是在顶层创建,只是语法的区别;
    4. launch {}会沿用当前的 context,不在顶层,但本文的背景为“在一个 suspend fun 中创建协程”,在没有 scope 的情况下,是不能直接用launch {}的;
    5. 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)
            }
        }
    }
  • 2022 年了

    2021 年匆匆过去了。没有很大的成就,也没有太大的曲折。

    总觉得,2022 会是相当重要的一年,从各种角度来说都是如此。

    在第一天做规划,新的一年,继续努力!

  • 使用 OpenConnect 代替 Cisco AnyConnect,避免路由表被锁定

    前言

    最近做一个项目,需要通过 VPN 连结到对方的服务器,对方用的是 Cisco AnyConnect。

    我们装上给的 AnyConnect 客户端,可以连上。

    问题

    后来,我们的系统跑不起来,发现了问题:我们的系统需要连接我们内网的服务 X,而这个服务 X 的 IP 地址被 AnyConnect 的路由表包含在其中了。查看路由表可以发现这个情况,把 VPN 一断开,服务 X 又能访问了,一连上马上又不行了。我们连 VPN 只需要访问对方的仅仅一个 IP,但是这 VPN 把一堆内网网段都路由了。

    我直接删了对应的路由表,再刷新路由表一看,那个条目怎么还在??

    后来找了好久,总算找到了元凶,就是 AnyConnect 本身。它就是会故意监视路由表,一旦发现被篡改就给你改回去。

    解决

    在刚才的链接中,有人就提出了办法:不用 AnyConnect,改用开源的 OpenConnect。安装很简单,Linux/Mac OS 都有现成的包可以通过命令行安装,Windows 也有对应的 GUI 版本。

    安装完成后,使用以下命令连接 VPN:

    # echo password | openconnect --background -u vpnuser \
    --servercert pin-sha256:xxxxxxxxxxxxxxxxxxxx \
    vpn.your.com

    连接建立后,可以查看路由表,发现路由表确实又被加了许多。

    但是没关系,sudo ip del xxxxxxxxxx,直接就能删掉。这样问题就解决了。

  • CTP Java API Linux/Windows x64 编译(SWIG 封装 C++ 动态库),并解决中文乱码问题

    前言

    网上有不少教程讲到 CTP API 的编译,但是我按照多数教程照着做都不太成功,只有一份是成功了。在此把过程记录下来,希望可以帮到更多的人。

    (2022年4月更新:补充 Windows 下的支持,写在文末)

    Linux

    1. 下载 CTP API

    在上期的官网下载,然后解压到某一个工作目录。我们只需要 Linux x64 的,工作目录的文件如下:

    error.dtd
    error.xml
    ThostFtdcMdApi.h
    ThostFtdcTraderApi.h
    ThostFtdcUserApiDataType.h
    ThostFtdcUserApiStruct.h
    thostmduserapi_se.so
    thosttraderapi_se.so
    阅读更多…
  • 禁用 Hibernate 的代理(proxy),获取其真实对象 / Disable Hibernate Proxy and Retrieve Real Entity Object

    Java 的各种框架还是很复杂的,不看文档或者不踩坑,都不知道原来框架还“偷偷地”做了一些别的事情——可能这就是一部分面试题的出处吧😂

    踩坑

    假设有一个 Model 叫做 Person,存到数据库里,框架用的是 Hibernate。代码中用 personDao.findById() 来从数据库中获取一个 Person 实例。项目中大概就是这么写的。

    问题来了:要把这个 Person 实例序列化,之后再反序列化。序列化的时候没发现什么问题,反序列化的时候报错了,告诉我这不是一个 Person,而是一个 Person$HibernateProxy$9cjcxRNr

    啥情况,代码明明写的是 Person person = personDao.findById(123); 怎么出来的不是 Person 而是 Person 后面加了一长串东西?

    解惑

    经过搜索,得知 Hibernate 使用 Proxy 实现延迟加载(Lazy Loading)功能。延迟加载,这个我懂,真正用到时才去查询。

    找到的资料还告诉我,被偷偷换成了 proxy 之后,还可能有别的坑:直接访问成员变量可能访问不到、用 IDE 断点调试时的行为和实际运行的行为不一致、instanceOf 的行为可能与预期不一致等。

    解决

    第一个办法,若不确定它是不是 proxy,可以强行让 Hibernate 给我们转回真正的实例,是就转,不是就原样返回:

    Object unproxiedEntity = Hibernate.unproxy(proxy); // 自己再做强制类型转换

    第二个办法,通过注解,不让 Hibernate 给这个 Model / Entity Class 做延迟加载:

    @Proxy(lazy = false)

    相关参考