• 有效清理 docker 所占用的空间

    docker 用久了,难免会占用很多空间,有时候不知道是怎么占用的,就几十几百G了。

    要怎么找到是什么东西占用的?运行

    docker system df

    会返回像

    TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
    Images          59        8         83.21GB   77.55GB (93%)
    Containers      9         8         2.344GB   0B (0%)
    Local Volumes   1         1         0B        0B
    Build Cache     10        0         6.38GB    5.47GB

    这样的结果。我们就可以发现是 image 和 build cache 在疯狂占用空间。

    对于 images,可以通过以下办法来删除:

    1. 删除悬空镜像 (未被任何镜像引用的中间层)

      docker image prune

      2. 删除所有未被容器使用的镜像 (包括未被标记和未被容器引用的镜像)

        docker image prune -a

        而 build cache,可以这样清理:

        docker builder prune

        后记:最近很少更新博文,我感觉最大的原因是大语言模型的普及。很多类似的技术问题,问 LLM 都能有比较好的答案,不用去阅读一些低质量的文章踩坑了。

      1. FastAPI CPU 密集型任务的处理办法

        FastAPI 经常号称是非常快的框架,因为它大量使用 async。对于一些 IO 密集型的任务,是有一些坑要处理的,不然整个 async 就被堵住了。这个处理还是比较简单的,按官网文档的说法,

        如果你的应用程序不需要与其他任何东西通信而等待其响应,请使用async def
        如果你不清楚,使用def就好

        那如果是 CPU 密集型的任务呢?这是工作中同事遇到的问题。

        def的办法就不行了——会把线程池堵住。

        下面 4 种写法,有 2 种都可以解决这个问题,都是进程池的方案。

        from concurrent.futures import ProcessPoolExecutor
        import asyncio
        import time
        
        from fastapi import FastAPI  
        import uvicorn
        
        
        app = FastAPI()  
        
        def fib(n: int) -> int:
            if n == 1 or n == 2:
                return 1
            return fib(n-1) + fib(n-2)
        
        def some_cpu_work() -> int:
            return fib(36)  # 大概1s
        
        
        # 通用配置:20个并发,同时请求
        
        @app.get("/test1")  
        async def test1():
            """ async定义+直接计算    排队 27s  CPU占用1核心 """
            data = some_cpu_work()
            print(data)
            return data  
        
        @app.get("/test2")  
        def test2():
            """ 普通定义+直接计算      自带线程池 依然27s  CPU占用1核心 """
            data = some_cpu_work()
            print(data)
            return data  
        
        #################################### 开一个进程池
        process_pool_executor = ProcessPoolExecutor(max_workers=4)
        
        @app.get("/test3")
        def test3():
            """ 普通定义+进程池   8s CPU占用4核心   """
            task = process_pool_executor.submit(some_cpu_work)
            data = task.result()
            print(data)
            return data
        
        @app.get("/test4")
        async def test4():
            """ async定义+交给进程池  8s CPU占用4核心  """
            data = await asyncio.get_running_loop().run_in_executor(process_pool_executor, some_cpu_work)
            print(data)
            return data
        
        if __name__ == "__main__":
            uvicorn.run(app, host="127.0.0.1", port=8000)

        具体来说,20 个请求同时请求,这 20 个请求在 4 种写法的结果如下:(其中第2种堵住了自带线程池的结果最出乎意料)

        阅读更多…
      2. Logback 过滤特定字符串的日志

        有一个第三方的包,在运行时会打出大量我觉得没有用的日志,假设它都以abcdef开头,要怎么办呢?一般的 log 过滤,都是按等级来设置的,例如不记录 debug、info 级别的 log。如果涉及到字符串的匹配,要怎么做?

        做法是在logback.xml<appender>里面,加上<filter>

        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <encoder>
                <pattern>%d{yy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{32} - %msg%n</pattern>
                <charset>UTF-8</charset>
            </encoder>
        
            <filter class="com.xxxxxxx.MyThresholdFilter"/>  <!-- 加在这里 -->
        </appender>

        然后另外再创建这个MyThresholdFilter.java

        public class MyThresholdFilter extends ThresholdFilter {
            @Override
            public FilterReply decide(ILoggingEvent event) {
                if (event.getMessage().startsWith("abcdef")) {
                    return FilterReply.DENY;
                }
                return FilterReply.NEUTRAL;
            }
        }
      3. 解决 Docker PHP “Failed opening required ‘xxx.php'” 的问题

        这个问题是我最近迁移本网站时遇到的。

        本网站是 Docker + PHP-FPM + nginx,迁移后,Wordpress 的 .php 访问报错:Failed opening required /xxx/wp-config.php

        很明显就是容器里没有权限。那么为什么会没权限呢?

        很久没搞这个的部署,忘得差不多了。经过重温与找资料,大概是这样解决的,不一定是完美的方案:

        原因:

        • Docker 的 PHP 运行的时候,默认是用www-data用户(并且uid=33),属于www-data用户组(且gid=33)
        • 某些系统自带了该用户与用户组(例如我网站迁移前所在的机器),某些没有自带(例如现机器)

        解决:

        1. 检查宿主机的用户、用户组是否有www-data,且 33 号的 uid/gid 是否被占用:
          # cat /etc/group
          # cat /etc/passwd
        2. 发现用户组 gid=33 被名为“tape”的用户组占用了(我的机器如此),似乎没有什么机会用到这个用户组,所以我把它的 id 改成 34:
          # groupmod -g 34 tape
        3. 创建 gid=33 的www-data用户组:
          # groupadd -g 33 www-data
        4. 创建 uid=33 的www-data用户,不创建home目录(-M),设置默认shell(-s)为nologin:
          # useradd -M -u 33 -g www-data -s /usr/sbin/nologin www-data

          至于为什么要这样创建,是为了尽可能模拟自带的www-data用户,在自带的机器上,passwd文件是这样的:
          # cat /etc/passwd | grep www-data
          www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

          而执行上述 useradd 后,passwd 是这样的,已经很像了:
          www-data:x:33:33::/home/www-data:/usr/sbin/nologin
        5. 最后,在宿主机,把缺少权限的整个目录所有权,赋予给www-data
          # chown -R www-data:www-data some_directory
      4. 网站搬到了腾讯云

        阿里云用了几年,现在迁移到腾讯云。

        新的服务器,内存大了一倍。希望在腾讯云用得稳定、顺心。

        顺便附个广告:【腾讯云】年度爆款2核2G4M云服务器118元/年,新老用户同享

      5. 用 cron 表达式表示“每月的第几个星期几”

        这个需求不是很常用,但需要用时,在网上不是很好搜索到,故在此记录。

        例如,要表示“每月的第二个星期六的12:34:00”,是这样写:

        0 34 12 ? * SAT#2

        注意,这个不是标准的形式,不是所有所有程序都接受这样的格式。一般程序都会有校验功能,可以检查下次执行的时间来进行确认。(第一是”?”表示不指定不一定支持,第二是”SAT”不一定支持,第三是”#2″更不一定支持)

      6. Spring Boot 3.x swagger 迁移 SpringFox 到 SpringDoc

        背景

        有一个项目升级到 Spring Boot 3.0,同事踩坑填坑花了点时间。遇到最大的一个坑就是 swagger 了,我顺便记录一下。

        SpringFox

        可以去它的官网看,SpringFox 已经有两三年没有更新了,属于被遗忘的项目。我去年也遇到 SpringFox 的问题,当时我用 Spring Boot 2.6,已经出现了不兼容报错的情况,所以退回了 2.5。今年是 Spring Boot 3.0,SpringFox 直接就别用了。

        SpringDoc

        SpringDoc 可以自动化生成 API 文档,它支持:(注意,本文所指均是 SpringDoc v2 版本)

        • OpenAPI 3
        • Spring Boot v3 (Java 17 & Jakarta EE 9)
        • JSR-303, specifically for @NotNull, @Min, @Max, and @Size.
        • Swagger-ui
        • OAuth 2
        • GraalVM native images

        从 SpringFox 迁移

        引入依赖

        把原来的io.springfoxswagger 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 大概是这样:

        具体的接口:

        结语

        上面只展示了最简单的使用,高级的用法和具体的配置在这里先不展开,需要时自己看官网文档。

        参考链接

      7. 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
      8. 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)