• dockerd 里面使用 lz4 解压缩测试小结

    线上 dockerd 版本: Docker version 19.03.12, build 48a66213fe

    实验使用的 dockerd 版本:v20.10.9

    目的

    为了加速容器的启动,docker pull 做为其中的一环,调研一下如何加速 docker pull。

    局域网环境问题下,docker pull 里面的解压的时间占了大头。Lz4 的解压速度比当前 Docker 默认的 gzip 要快不少,我们就在 Docker 里面实际测下看效果。

  • Harbor v2.2.0 中 Sweep 引发的两个问题

    在说这些问题之前,先说一下 Harbor 里面触发镜像复制任务(其他任务类似)的大概流程。

    1. Put Manifest 接口已经是 Docker Push 的最后一步了,里面会生成 Event,并做 Publish。至于需要 Publish 到哪里去,是https://github.com/goharbor/harbor/blob/release-2.4.0/src/pkg/notifier/notifier.go 里面注册的。

    2. controller/replication/controller:Start 里面创建一个 Execution (注意,这里会创建 Sweep Goroutine)

    3. 开一个 Goroutine ,创建 Tasks (一个 Execution 可能对应多个 Tasks)

    4. Task 里面提交 Job,POST /api/v1/jobs

    5. Core 组件里面收到这个请求,调用 LauchJob,会任务 Enqueue 到 Redis 里面注册的。

    以上都是 Core 里面做的。

    接下来 Jobservice 组件收到这个 Job,开始处理。并在任务成功或者失败时,回调一下配置在 Job 里面的 StatusHook。

  • 我们的 Habror 灾备架构

    介绍一下背景,我们使用 harbor 做镜像仓库服务,后端存储使用 Ceph。

    现在 1 需要做两机房的灾备, 2 Ceph 还没有两机房的集群能力。

  • 记一个 Harbor 中的小问题 -- get-manifest header-content-type 变化

    起因

    一个同事 使用 Ruby 调用 harbor GET /v2/<name>/manifests/<reference> 接口,开始的时候没有问题。

    后来,因为我们 harbor 架构的问题,对 harbor 代码做了一个小的改造。导致同事那边 Ruby 拿到的结果不认为是 Json,而是一个 String,需要再次 Json 解析一次。

    同事看起来,认为是 header 里面的 content-type 变了导致的。需要我们查明一下原因。

  • Docker Pull Image 404

    切换 DR 之后,有些镜像 Pull 失败。想对比一下两边的全量的 Tag,看一下有哪些缺失。结果发现全量 Tag 列表是一样的。

    然后就找了一下 Pull 404 的镜像。走 Registry:5000/v2/tag/list 的确返回列表里面有这个 Tag。那为啥docker pull 会报404呢?第一反应是哪里有缓存吧,翻看了一下代码,还真没有缓存。

    直接 curl /v2/{repo}manifest/{tag} ,如果走 Harbor,是404;走 Registry 就是200。看来问题出在 Harbor 这里。

    抓包看了一下,Harbor 居然把 Tag 转成 sha256 再去请求,到了 Registry 的请求是 /v2/{repo}manifest/{sha256},不太明白为啥要这样搞。

    这块代码是2年前的,可能和当时的 Registry 有关系。不过现在的确带来了副作用。

  • Docker Registry Implement

    对照https://docs.docker.com/registry/spec/api/,使用 Flask 做一下实现。但发现有两处和文档不太符合的地方。

    1. Chunked Upload

    文档中说,需要返回格式如下:

    202 Accepted
    Location: /v2/<name>/blobs/uploads/<uuid>
    Range: bytes=0-<offset>
    Content-Length: 0
    Docker-Upload-UUID: <uuid>
    

    这里显示 Range 的格式与Range - HTTP | MDN 中说的一致。

    但实际上,前面不能加 bytes=。否则 docker cli 会报错。

    上面的文档是说 Request 里面的 Range,但 Docker 文档这里用到的其实是 Response,不知道是不是这个区别,感觉前面加单位是合理的。

    2. Completed Upload

    传输完成一个 Layer之后,需要 Put 确认完成。 Server 应该返回如下信息。

    201 Created
    Location: /v2/<name>/blobs/<digest>
    Content-Length: 0
    Docker-Content-Digest: <digest>
    

    文档中说 The Location header will contain the registry URL to access the accepted layer file

    但实际上,docker cli 不会通过这个返回的 Location 来确认 Layer。而是通过 HEAD /v2/liujia/test/blobs/sha256:c74f8866df097496217c9f15efe8f8d3db05d19d678a02d01cc7eaed520bb136 HTTP/1.1 来确认的。就是说,不管Location 返回什么,都是通过之前的 digest 来做 HEAD 请求确认 Layer 信息的。

    3. Get Manifest

    返回的时候,需要解析 manifest 文件里面的 mediaType 作为 content-type 返回。

    代码

    模拟实现 registry 的 python+flask 代码如下。

    # -*- coding: utf-8 -*-
    
    from flask import Flask
    from flask import request
    from flask import make_response
    
    from uuid import uuid1
    import os
    import json
    from hashlib import sha256
    
    
    app = Flask(__name__)
    logger = app.logger
    
    registry_data_root = "./data/"
    
    
    @app.route("/v2/")
    def v2():
        return ""
    
    
    @app.route("/v2/<path:reponame>/manifests/<string:reference>", methods=["HEAD", "GET"])
    def get_manifest(reponame, reference):
        path = os.path.join(
            registry_data_root, "manifests", reponame, *reference.split(":")
        )
        resp = make_response()
        if not os.path.exists(path):
            resp.status = 404
            return resp
    
        resp.status = 200
        if request.method == "GET":
            data = open(path, "rb").read()
            resp.set_data(data)
            sha256_rst = sha256(data).hexdigest()
            resp.headers["Docker-Content-Digest"] = f"sha256:{sha256_rst}"
            resp.headers["Content-Length"] = str(len(data))
            content_type = json.loads(data)["mediaType"]
            resp.headers["Content-Type"] = content_type
        logger.info(resp.headers)
        return resp
    
    
    @app.route("/v2/<path:reponame>/blobs/<string:digest>", methods=["HEAD", "GET"])
    def get_layer(reponame, digest):
        logger.info(request.headers)
    
        path = os.path.join(registry_data_root, "blobs", reponame)
        dst = os.path.join(path, digest)
        resp = make_response()
        if not os.path.exists(dst):
            resp.status = 404
            return resp
    
        resp.status = 200
        if request.method == "GET":
            data = open(dst, "rb").read()
            resp.set_data(data)
            sha256_rst = sha256(data).hexdigest()
            resp.headers["Docker-Content-Digest"] = f"sha256:{sha256_rst}"
            resp.headers["Content-Length"] = str(len(data))
    
        logger.info(resp.headers)
        return resp
    
    
    @app.route("/v2/<path:reponame>/blobs/uploads/", methods=["POST"])
    def upload(reponame):
        logger.info(reponame)
        logger.info(request.headers)
        uuid = str(uuid1())
        resp = make_response()
        resp.headers["Location"] = f"/v2/{reponame}/blobs/uploads/{uuid}"
        resp.headers["Range"] = "bytes=0-0"
        resp.headers["Content-Length"] = "0"
        resp.headers["Docker-Upload-UUID"] = uuid
        resp.status = 202
        return resp
    
    
    @app.route(
        "/v2/<path:reponame>/blobs/uploads/<string:uuid>",
        methods=["PATCH"],
    )
    def patch_upload(reponame, uuid):
        """
        Chunked Upload
        """
        logger.info(request.url)
        logger.info(reponame)
        logger.info(uuid)
        logger.info(request.headers)
    
        r = [e for e in request.headers if e[0].upper() == "RANGE"]
        if r:
            start = int(r[0].split("-")[0])
        else:
            start = 0
    
        path = os.path.join(registry_data_root, "_upload", reponame)
        os.makedirs(path, exist_ok=True)
        f = open(os.path.join(path, uuid), "ab")
        f.seek(start, os.SEEK_SET)
        data = request.stream.read()
        f.write(data)
    
        resp = make_response()
        resp.headers["Location"] = f"/v2/{reponame}/blobs/uploads/{uuid}"
        resp.headers["Range"] = f"0-{len(data)}"
        resp.headers["Content-Length"] = "0"
        resp.headers["Docker-Upload-UUID"] = uuid
        logger.info(f"{resp.headers=}")
        resp.status = 202
        return resp
    
    
    @app.route(
        "/v2/<path:reponame>/blobs/uploads/<string:uuid>",
        methods=["PUT"],
    )
    def put_upload(reponame, uuid):
        """
        Completed Upload
        """
        logger.info(request.url)
        logger.info(reponame)
        logger.info(uuid)
        logger.info(request.headers)
        digest = request.args["digest"]
    
        dst_path = os.path.join(registry_data_root, "blobs", reponame)
        os.makedirs(dst_path, exist_ok=True)
        dst = os.path.join(dst_path, digest)
        src = os.path.join(registry_data_root, "_upload", reponame, uuid)
        logger.info(f"{src=} {dst=}")
        os.rename(src, dst)
    
        resp = make_response()
        resp.headers["Location"] = f"/v2/{reponame}/blobs/{digest}"
        logger.info(resp.headers)
        resp.status = 201
        return resp
    
    
    @app.route(
        "/v2/<path:reponame>/manifests/<string:reference>",
        methods=["PUT"],
    )
    def put_manifest(reponame, reference):
        """
        Completed Upload
        """
        logger.info(request.url)
        logger.info(reponame)
        logger.info(request.headers)
    
        path = os.path.join(registry_data_root, "manifests", reponame)
        os.makedirs(path, exist_ok=True)
        data = request.stream.read()
        with open(os.path.join(path, reference), "wb") as f:
            f.write(data)
    
        sha256_rst = sha256(data).hexdigest()
        path = os.path.join(path, "sha256")
        os.makedirs(path, exist_ok=True)
        with open(os.path.join(path, sha256_rst), "wb") as f:
            f.write(data)
    
        resp = make_response()
        resp.headers["Docker-Content-Digest"] = f"sha256:{sha256_rst}"
        resp.headers["Location"] = f"/v2/{reponame}/manifests/{reference}"
        logger.info(resp.headers)
        resp.status = 201
        return resp
    
  • Harbor DR 方案的一些尝试记录

    现状

    我们使用 Harbor v2.2.0 做 Docker 的镜像仓库。后端使用 Swift+Ceph。

    因为后端 Ceph 不能做两 IDC 的高可用方案。所以就搞了两套 Harbor,IDC A 和 IDC B 各一个。使用 Harbor 自己带的 Replication 功能做镜像的复制。

    这种方案有个问题:应用层面的数据不一致,用户的信息,项目的配置,包括 ID,Webhook 配置,用户权限等,都不能同步。

  • harbor push image 时偶尔出错的定位

    harbor 后端使用 Ceph 做存在,前端加 Swift 一层(应该是因为 registry 不直接支持 Ceph吧,这块还不是特别了解。)

    最近一段时间发现偶尔有 push image 出错。看 registry.log 是因为 PutContent 失败。 查 Ceph Access 日志,是 401 报错,接着一个 400 报错。

  • logrotate 笔记一二

    1

    logrotate 里面有如下配置:

    prerotate
       sh -c "[[ ! $1 =~ mongodb.log$ ]]"
    endscript
    

    运行时有如下报错:

    error: error running non-shared prerotate script for XXX

    这个其实不是错误,是说 prerotate 脚本 return code 不是0,也就是说会跳过这个日志文件的处理。

    2

    如果有两个 Pattern 有重合,比如说 /var/log/*.log, 和 /var/log/redis.log , Logrotate 不会处理两次,前面那个配置生效。后面的配置会被完全忽略掉。这里要小心。

    补充说明一下,如果一个文件在两个 pattern 里面都匹配到,第二个配置被完全忽略掉,而不仅仅是这个文件被忽略。

    可以使用这个规则来过滤一些日志。 比如说 /var/log/redis.log 不想处理,可以加一个 prerotate 过滤掉

    prerotate
       sh -c "[[ ! $1 =~ mongodb.log$ ]]"
    endscript
    

    更新

    这样会有问题的!后面的 /var/log*.log 配置项完全失败了。

    大概翻看了一下 logrotate 的代码,逻辑应该是这样的:

    /var/log/*.log 称为一个 entry,每一个 entry,要遍历符合的所有日志,然后和前面的所有 entry 的所有日志对比,如果有一样的,就报 duplicate log entry 这个错误,并完全忽略这个 entry,也就是说,这个 entry 里面的所有日志都会跳过。

    我这个需求怎么办?

    我有这样一个需求,对 /var/lib/*.log 里面的日志做 rotate,但是

    1. mongodb.log 不需要做
    2. redis.log 不使用copytruncate

    想来想去,只能是“半手工”的来实现,如下:

    /var/lib/*.log {
        copytruncate
        prerotate
           sh -c "[[ $1 =~ redis.log$ ]]" && mv $1 $1-`date +%Y%m%d-%s` && exit 1
           sh -c "[[ ! ($1 =~ mongodb.log$ || $1 =~ redis.log$) ]]"
        endscript
    }
    
  • docker registry 更换后端存储后 Pull/Push 失败

    使用 harbor v1.10 做 docker hub。

    在一次测试过程,换了后端的 Swift 存储配置。 然后 Push 一个之前存在的 Image 时,显示 layers 都存在了,直接跳过,最后显示 Push 成功。但这显然是不可能的,因为后面的存储都是新的,不可能 layers 已经存在。 而且 Pull 的时候果然失败了。