构建理想容器镜像——以CSI为例

简介: 本文围绕阿里云CSI(Container Storage Interface)镜像构建的实际案例,探讨了一系列优化容器镜像的最佳实践。

欢迎扫描文末二维码,关注「阿里云开发者」公众号,了解更多技术干货,关于阿里的技术创新均呈现于此。

何为理想镜像


当前容器服务在组件中心已对外提供了数十个组件,每个组件都有一个或多个镜像,我们团队可谓每个人都需要日常和容器镜像打交道。然而,我们却还尚没有如何构建容器镜像的最佳实践方案,各个组件的构建方法也是五花八门。容器镜像作为我们最终交付的构建产物,只要功能能跑起来,我们就满足了吗?作为一个专业的容器服务团队,显然不应是这样的,我们还有很多非功能性的需求。


1.镜像大小:在满足功能需求的前提下,镜像的存储空间占用应当尽量减小。这可以加速镜像使用的各个环节,从镜像的构建,数十个Region间的同步,并最终分发到数万用户的无数节点上,都能因此提速。CSI尤其如此,在新加入的节点上,挂载有相关存储卷的Pod必须等CSI的Pod拉起后才能拉起,因此CSI的镜像大小将直接影响到客户弹性的效率。


2.精简无关软件:这是减小镜像大小的重要方式之一。同时,这也能提升镜像扫描工具(如安全漏洞CVE)的信噪比。这也是Google提出distroless基础镜像[1]的初衷。


3.SBOM收集:SBOM中结构化地记录了容器镜像中所使用的所有软件包,并能构建索引。当新的CVE曝光时,SBOM能帮我们快速从海量的镜像历史版本中找到受其影响的版本,并对应处置。


4.可重现构建:想必部分同学听说了近期也有字节员工在checkpoint中下毒,干扰训练过程的新闻。可重现构建意味着整个从源码到最终制品(容器镜像)的过程应该是完全确定性的。任何人都可以较容易地独立地复现整个构建过程,并得到完全一致的制品,从而验证构建流程没有被入侵。详情可见[2]。Go语言的编译器也在近期达成了完全可重现构建的目标。使用Go语言编写的应用也受益于此,较容易达成可重现的结果。


5.构建速度:在调试的过程中,可能会需要频繁地多次构建镜像。镜像构建速度快对开发体验的帮助是很大的。这主要可以通过两方面来提高:


a.交叉编译:Buildkit自带通过qemu模拟其他指令集来编译软件的功能,但效率是非常低的。若能使用工具链的交叉编译功能,例如,在amd64的机器上直接执行amd64原生的编译器,但调整参数使其输出arm64的二进制,可大大提升构建速度。

b.充分利用缓存:每次编译的内容通常和上次构建都不会相差太多。若能只构建变化过的内容则可大幅提升构建速度。Go的工具链在这方面提供了非常便利的支持。


他山之石

那么,针对上述目标,其他国际大厂是怎么做的呢?本文对Google和AWS开源CSI镜像构建方案进行了调查。


Google CSI

GCE PD的CSI插件[3]为例,它在Dockerfile中,从gcr.io/distroless/base-debian12基础镜像开始,逐个复制CSI的Golang二进制及其依赖的其他二进制和所有动态库。虽然他基本达成了精简无关软件的目标,但个人认为,这个镜像构建的流程有几个明显的缺点:

  • 在手工维护的源文件中写入所有传递依赖的列表实在不是啥高明之举。虽然可以依赖CI以检查是否有误,但还是需要多轮迭代以修复错误。
  • 每个二进制分别拷贝到不同镜像层中。虽然在更新时,更多层意味着更多复用的可能性,但过多的层可能导致拉取时网络请求数量过多,挂载时overlayfs的层数过多,这都有潜在的性能影响。以当前最新版registry.k8s.io/cloud-provider-gcp/gcp-compute-persistent-disk-csi-driver:v1.15.1为例,拉取时需要下载多达45个blob。


skopeo copy --override-os linux docker://registry.k8s.io/cloud-provider-gcp/gcp-compute-persistent-disk-csi-driver:v1.15.1 oci:gcp
ls gcp/blobs/sha256 | wc -l


  • SBOM不友好,单独复制二进制文件会导致SBOM扫描软件无法获知它的来源和版本。


syft oci-dir:gcp


AWS CSI

AWS EBS的CSI插件[4]为例,这个仓库的Dockerfile中仅有复制Golang的二进制这一个步骤,其基础镜像的构建是在另一个专门构建镜像的项目[5]中。这个项目很复杂,他承担了AWS很多组件的基础镜像的构建。这个仓库总体是基于Buildkit和Dockerfile的,它提供了一个统一的接口,各个组件仅需声明自己直接依赖的二进制,相关脚本就会自动从AWS的AL2 Linux发行版中选择所需的rpm软件包,并通过yum --installroot 命令直接安装到最终镜像中。由于是使用包管理安装的,相关元数据也能得以保留,并可被用于SBOM扫描。但传统包管理的依赖管理粒度还是稍大,导致安装完成后还需要一些自定义的清理操作(例如删除systemd相关软件),略微增加了复杂性。但总体来说,它的构建结果还是非常理想的。


distroless基础镜像

那么作为Kubernetes社区几乎所有社区发布的镜像的基础镜像,distroless本身又是怎么构建的呢?它使用的是Google的构建工具bazel,甚至在构建时并不依赖任何容器技术,而是直接生成组成镜像的各个tar包,并自行生成容器镜像的元数据。例如,对于libc和OpenSSL,它基于debian的软件包,对其deb包中的文件和控制文件进行一些变换,最终得到容器镜像层使用的软件包。这么做确实感觉很理想,在利用现有软件包的同时,也可对容器内的内容精确控制,包括可以保留deb包中的元数据以供SBOM扫描;同时各个层之间相互独立,可重现性强。但咱们内部之前从来没有使用bazel构建的项目,专门为了容器镜像搞一个估计也不值得,所以仅仅是参考一下他们的思路。


我的方案

那么,说了这么多,我打算把阿里云的CSI镜像构建改造成啥样呢?除了上述理想镜像的特性,我们基于容器服务chorus的buildkit相关基础设施,构建流程也应该是基于Dockerfile的;此外,CSI是开源软件,我也希望我们CSI的构建流程能和其他大厂一样,保持开源并可被任意人审计和复现。欢迎大家Review我的PR build: rewrite dockerfile to distroless #1098[6]


1. 切换为distroless基础镜像

之前我们是基于alinux3的基础镜像,然后直接使用dnf安装我们所需的软件包。这么做有几个弊端:

  • alinux3镜像更新频次较低,我们需要在镜像里运行dnf upgrade以获取安全修复。这导致了最终镜像大小的不可预期,取决于镜像发布后有多少更新可用。
  • alinux3基础镜像中本身就有很多咱们用不到的软件包。由于分层结构,就算手动删除也无法降低镜像的大小。

为此,我决定直接切换到受到社区广泛欢迎的distroless镜像。同时debain系交叉编译C的软件也更加方便,如下文所述。


2. 自动收集依赖,deb包元数据

CSI需要执行很多操作系统相关的操作,传统上这些操作都是直接使用命令完成的,相关命令并没有使用Go语言重写。因此,CSI仍然需要使用很多C语言编写的二进制。这些脚本我使用了和Google CSI类似的方法,在一个更完整的debian镜像中安装,但只手动指定直接依赖,使用脚本收集所有传递依赖,类似AWS CSI。以下附上我使用的脚本,该脚本将所有所需文件收集到/staging-node目录中:


#!/bin/bash
set -e

# This directory is distroless specific, and recognized by syft
mkdir -p /staging-node/var/lib/dpkg/status.d

DEPS=(
    /etc/mke2fs.conf /sbin/{fsck,mkfs,mount,umount}.{ext{2,3,4},xfs,nfs}
    /usr/bin/{mount,umount,lspci,mkdir,chmod,grep,tail,nsenter}
    /usr/sbin/{fsck,mkfs,sfdisk,losetup,blockdev}
    /sbin/dumpe2fs /sbin/resize2fs
    /usr/sbin/xfs_io /usr/sbin/xfs_growfs
)

declare -A FILE_PACKAGES
for line in $(dpkg-query --show --showformat 'PKG_NAME:${Package}\n${db-fsys:Files}'); do
    if [[ "$line" = PKG_NAME:* ]]; then
        pkg=${line#PKG_NAME:}
    else
        FILE_PACKAGES[$line]=$pkg
    fi
done
echo "indexed ${#FILE_PACKAGES[@]} files"

MTIME=0
gather_dep() {
    local source target t pkg copyright
    # resolve all but last component of symlink
    source=$(realpath "$(dirname "$1")")/$(basename "$1")

    # find the package that contains the source
    pkg=${FILE_PACKAGES[$source]}
    if [ -z "$pkg" ] && [[ "$source" = /usr/* ]]; then
        # retry without /usr prefix
        # use source path matching dpkg for SBOM to work, because /lib is not linked to /usr/lib in distroless
        source="${source#/usr}"
        pkg=${FILE_PACKAGES[$source]}
    fi
    if [ -z "$pkg" ]; then
        echo "failed to find package for $source"
        return 1
    fi
    if [ -e "/base$source" ]; then
        echo "$source already exist in base"
        return 0
    fi

    [ -e "/staging-node$source" ] && return 0
    if [ -h "$source" ]; then
        target=$(realpath "$source")
        echo "gathering link $source => $target"
        gather_dep "$target"
    fi
    echo "gathering dep $pkg: $source"
    t=$(stat -c '%Y' "$source")
    [ "$t" -gt "$MTIME" ] && MTIME=$t
    cp -dp --parents "$source" /staging-node

    # deb package metadata is useful for SBOM
    [ -e "/staging-node/var/lib/dpkg/status.d/$pkg" ] && return 0
    echo "installing deb package $pkg metadata"
    dpkg-query --status "$pkg" > "/staging-node/var/lib/dpkg/status.d/$pkg"
    dpkg-query --control-show "$pkg" md5sums > "/staging-node/var/lib/dpkg/status.d/$pkg.md5sums"
    copyright="/usr/share/doc/$pkg/copyright"
    if [ -e "$copyright" ]; then
        echo "installing deb package $pkg copyright"
        cp -dp --parents "$copyright" /staging-node
    fi
}
for f in "${DEPS[@]}"; do
    if ! [ -e "$f" ]; then
        echo "$f does not exist"
        continue
    fi
    gather_dep "$f"
done

mapfile -t LIBS < <(ldd /staging-node/{usr/,}{bin,sbin}/* 2>/dev/null | grep -Po '(?<= => )[^ ]+' | sort -u)
for f in "${LIBS[@]}"; do
    gather_dep "$f"
done
echo "latest mtime is $(date --date "@$MTIME" --iso-8601=seconds)"
find /staging-node -type d -exec touch --date="@$MTIME" {} +
touch --date="@$MTIME" /staging-node/var/lib/dpkg/status.d/*


这其中有几个坑点:


  • debain镜像中,/lib是到/usr/lib目录的符号链接,bin,sbin目录也是同理。但distroless中并不是这样,他们都是相互独立的目录。那么,划归一下如何呢:
  • 所有文件都拷贝到/usr下的目录里,反正他们都在默认path里。确实能用,但由于deb的元数据中可能记录的是/bin里的路径,导致SBOM无法正确将文件和软件包关联起来。
  • 那么,在distroless镜像里,也创建相关符号链接如何呢?确实能修复SBOM的问题,能关联上了。但distroless的基础镜像里/lib和/usr/lib里都是有文件的,如果在其中一个地方创建符号链接,那么这个链接在overlayfs挂载后就会隐藏那个目录下原来的文件,运行时相关库就加载不了。


我的解决方法是:复制前在deb的数据库中搜索这个文件,并只使用deb记录的标准化的路径执行所有后续操作。


  • 使用deb-query命令收集deb的相关元数据到/var/lib/dpkg/status.d目录,每个包使用不同的文件。这个目录看起来是distroless特有的,debian默认是将所有包的数据都写入/var/lib/dpkg/status这一个文件里。但这样就做不到不同软件分别独立在不同镜像层里了。神奇的是,syft这个SBOM扫描的工具还特意支持了这个目录。(这就是受社区影响力啊)
  • 识别依赖的时候,我并没有像AWS那样,使用软件包声明的依赖,而是使用ldd命令直接查找所有依赖的动态库。如此可以真正最小化依赖,绝无一个多余的文件,也同时节省了事后清理的步骤。诚然,这样有漏掉使用dlopen之类的方法动态依赖的库,或者其他配置/数据文件的风险,但大多二进制都已经有Google的长期验证,并且CSI现在也有较为完善的回归测试,风险可控。
  • 为了可重现构建考虑,不能将构建当时的时间保留在文件的inode上。为此,我在复制文件时保留了原有的时间戳,同时,将所有动态创建的目录和文件的修改时间设置为所有deb包中的文件修改时间的最大值。


3. deb包下载版本的可重现

通常我们在Dockerfile中安装依赖时,都直接apt-get update && apt-get install xxx 这样的话,apt会获取构建当时最新版本的软件包。为了实现可重现构建,可以依赖[7]提供的服务,获取当时的版本,以确保未来构建时也可以安装完全相同的软件包。debian镜像中的debian.sources文件中的注释已写明了构建镜像时使用的snapshot。可以直接复用这个。


echo 'Acquire::Check-Valid-Until false;' > /etc/apt/apt.conf.d/snapshot && \
sed -i '/^URIs:/d; s|^# \(http://snapshot.debian.org/\)|URIs: \1|' /etc/apt/sources.list.d/debian.sources && \
apt-get update


4. C语言交叉编译

blkid这个程序新版本有集成一些我们想要的优化,但Debian最新版本还没有集成,于是我们选择了自己编译并打包到镜像里。之前使用Alinux3的时候,我们使用的是qemu指令集模拟编译的,编译效率比较低。但得益于Debian打包的交叉编译器,及其Multiarch设计,交叉编译C的程序变得更简单了。(blkid甚至没啥二进制依赖,不需要安装其他架构的包)

以下这段Dockerfile可以安装任意构建/目标架构的组合的gcc编译器,交叉编译或非交叉编译通用!


FROM build-0 as build-util-linux-amd64
ENV HOST=x86_64-linux-gnu

FROM build-0 as build-util-linux-arm64
ENV HOST=aarch64-linux-gnu

FROM build-util-linux-$TARGETARCH as build-util-linux
ARG BUILDARCH
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-cache-$BUILDARCH \
    --mount=type=cache,target=/var/lib/apt,sharing=locked,id=apt-lib-$BUILDARCH <<EOF
#!/bin/bash
apt-get update && apt-get install -y gcc-${HOST//_/-}
EOF


以下也附上我们编译blkid使用的脚本,注意我们只给./configure加了一个--host=$HOST参数就完成了交叉编译的支持!


ADD --link --checksum=sha256:59e676aa53ccb44b6c39f0ffe01a8fa274891c91bef1474752fad92461def24f \
    https://www.kernel.org/pub/linux/utils/util-linux/v2.40/util-linux-2.40.1.tar.xz /src.tar.xz
RUN mkdir -p /src && tar -C /src --strip-components=1 -xf /src.tar.xz

RUN <<EOF
set -e
cd /src
SOURCE_DATE_EPOCH=$(stat -c %Y /src.tar.xz)
export SOURCE_DATE_EPOCH
echo "util-linux released at $(date --date "@$SOURCE_DATE_EPOCH" --iso-8601=seconds)"
./configure --disable-all-programs --enable-blkid --enable-libblkid --prefix=/usr/local \
    --disable-nls --disable-bash-completion --disable-asciidoc --disable-dependency-tracking --disable-static --host=$HOST
make -j
make install-strip DESTDIR=/out
cd /out/usr/local && rm -r include share lib/pkgconfig
EOF


5. 缓存友好的Go编译

go build 命令已经封装了编译器的实际调用,它也已经集成了构建缓存功能,所以我们如果直接在本地构建,只改一点代码的话,是非常快的。然而容器镜像构建时,通常都是重新构建,并不会用到上次构建的缓存。这时候就要用到RUN --mount=type=cache 这个buildkit相比docker build的新功能了,它可以将之前构建时产生的数据带到下一次构建。对于Go语言的构建来说,这是我实践出来的好用的方法:


FROM --platform=$BUILDPLATFORM golang:1.22.3 as build
WORKDIR /go/src/github.com/kubernetes-sigs/alibaba-cloud-csi-driver
ARG TARGETARCH
ARG TARGETOS
RUN --mount=type=bind,target=. \
    --mount=type=cache,target=/root/.cache/go-build \
    GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 \
    go build -o /out/plugin.csi.alibabacloud.com


只要挂载了/root/.cache/go-build目录即可自动复用缓存,设置相关环境变量即可交叉编译,非常简单。


6. 缓存友好的apt包安装

在镜像构建中,apt软件包的安装有时会占据大量时间。尤其是在国内,访问部分源的速度非常感人。为此,十分有必要尽量避免重复的下载操作:


FROM debian:bookworm-20241016-slim as debian

ARG TARGETARCH
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-cache-$TARGETARCH \
    --mount=type=cache,target=/var/lib/apt,sharing=locked,id=apt-lib-$TARGETARCH \
    rm -f /etc/apt/apt.conf.d/docker-clean && \
    echo 'Acquire::Check-Valid-Until false;' > /etc/apt/apt.conf.d/snapshot && \
    sed -i '/^URIs:/d; s|^# \(http://snapshot.debian.org/\)|URIs: \1|' /etc/apt/sources.list.d/debian.sources && \
    apt-get update && \
    apt-get install -y nfs-common e2fsprogs xfsprogs pciutils fdisk


这就是我安装软件包的方法,无论是软件包列表,还是实际deb文件,均无需重复下载。


7. Controller和Node共用镜像层

CSI同时运行于中心侧和节点侧,分别负责不同部分的工作,但它们使用的Go二进制是一样的。然而,节点侧需要额外的二进制以执行和操作系统相关的操作,但中心侧却因为安全需求,最好不要带上它们。之前的方案是维护多份dockerfile文件。那我这次也顺便将它们合二为一,节点侧直接在中心侧的镜像的基础上,再加几层来存放这些额外的二进制。这样不但减少了重复代码,降低维护成本,由于层复用,也能节省构建,存储和拉取相关镜像的成本,特别是当两者运行于同一个节点上时。


优化效果

那么,让我们来看看上述优化的效果吧。


镜像大小

最直观的比较就是镜像大小了,让我们将当前CSI发布的最新版本与优化后的版本的主镜像进行对比:


镜像

下载大小

解压后大小

registry-cn-hangzhou.ack.aliyuncs.com/acs/csi-plugin:v1.31.1-e749bf2-aliyun

240M

581MB

registry-cn-hangzhou.ack.aliyuncs.com/acs/csi-plugin:v1.31.2-0b2ccf6-aliyun (最新发布版本)

125M

412MB

registry-cn-hangzhou.ack.aliyuncs.com/test-public/csi-plugin:v1.5.0-187-gbac52000 (优化后)

46M

142MB


可见,镜像大小有非常明显的下降,下载大小仅有最新版的37%。同时也验证了之前所说,使用alinux基础镜像会导致镜像大小不可预期,例如最近两个版本的下载大小相差了约1倍。


注:测试镜像均为arm64架构,因为是在arm架构的Macbook上测试的。arm64的镜像通常比amd64的略微小一点。

测试方法:

下载大小:


skopeo copy --override-os linux docker://$IMAGE oci-archive:csi.tar
ls -lh csi.tar

解压后大小:


podman pull $IMAGE
podman images $IMAGE


SBOM扫描

可以验证,所有deb包均能被正常识别。除了在镜像中自己编译的blkid外,其余所有文件均能正常和deb包关联。添加参数--select-catalogers "+sbom-cataloger" 后,syft也能读取到我手动在镜像中写入到blkid到SBOM。


构建速度

最常见的情况下,修改单行Go源代码重新构建镜像大概仅需要8秒,其中go build 约4秒。体验可以说是很不错了。

修改依赖的apt包时,经过验证也仅需要下载新增依赖的包。BTW,可以修改buildkit的配置,提升一些缓存的大小,可以减少需要重新编译/下载的次数。


结语

愿本文能助你向客户交付更加理想的容器镜像。提升用户体验,避免安全问题,体现专业水平,获取用户信任。


参考链接:

[1]https://github.com/GoogleContainerTools/distroless[2]https://reproducible-builds.org/[3]https://github.com/kubernetes-sigs/gcp-compute-persistent-disk-csi-driver/blob/master/Dockerfile[4]https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/Dockerfile[5]https://github.com/aws/eks-distro-build-tooling/blob/main/eks-distro-base/Dockerfile.minimal-base-csi-ebs[6]https://github.com/kubernetes-sigs/alibaba-cloud-csi-driver/pull/1098

[7]https://snapshot.debian.org/




来源  |  阿里云开发者公众号

作者  |  元昌




相关文章
|
10天前
|
存储 人工智能 弹性计算
阿里云弹性计算_加速计算专场精华概览 | 2024云栖大会回顾
2024年9月19-21日,2024云栖大会在杭州云栖小镇举行,阿里云智能集团资深技术专家、异构计算产品技术负责人王超等多位产品、技术专家,共同带来了题为《AI Infra的前沿技术与应用实践》的专场session。本次专场重点介绍了阿里云AI Infra 产品架构与技术能力,及用户如何使用阿里云灵骏产品进行AI大模型开发、训练和应用。围绕当下大模型训练和推理的技术难点,专家们分享了如何在阿里云上实现稳定、高效、经济的大模型训练,并通过多个客户案例展示了云上大模型训练的显著优势。
|
14天前
|
存储 人工智能 调度
阿里云吴结生:高性能计算持续创新,响应数据+AI时代的多元化负载需求
在数字化转型的大潮中,每家公司都在积极探索如何利用数据驱动业务增长,而AI技术的快速发展更是加速了这一进程。
|
5天前
|
并行计算 前端开发 物联网
全网首发!真·从0到1!万字长文带你入门Qwen2.5-Coder——介绍、体验、本地部署及简单微调
2024年11月12日,阿里云通义大模型团队正式开源通义千问代码模型全系列,包括6款Qwen2.5-Coder模型,每个规模包含Base和Instruct两个版本。其中32B尺寸的旗舰代码模型在多项基准评测中取得开源最佳成绩,成为全球最强开源代码模型,多项关键能力超越GPT-4o。Qwen2.5-Coder具备强大、多样和实用等优点,通过持续训练,结合源代码、文本代码混合数据及合成数据,显著提升了代码生成、推理和修复等核心任务的性能。此外,该模型还支持多种编程语言,并在人类偏好对齐方面表现出色。本文为周周的奇妙编程原创,阿里云社区首发,未经同意不得转载。
|
10天前
|
人工智能 运维 双11
2024阿里云双十一云资源购买指南(纯客观,无广)
2024年双十一,阿里云推出多项重磅优惠,特别针对新迁入云的企业和初创公司提供丰厚补贴。其中,36元一年的轻量应用服务器、1.95元/小时的16核60GB A10卡以及1元购域名等产品尤为值得关注。这些产品不仅价格亲民,还提供了丰富的功能和服务,非常适合个人开发者、学生及中小企业快速上手和部署应用。
|
5天前
|
人工智能 自然语言处理 前端开发
用通义灵码,从 0 开始打造一个完整APP,无需编程经验就可以完成
通义灵码携手科技博主@玺哥超carry 打造全网第一个完整的、面向普通人的自然语言编程教程。完全使用 AI,再配合简单易懂的方法,只要你会打字,就能真正做出一个完整的应用。本教程完全免费,而且为大家准备了 100 个降噪蓝牙耳机,送给前 100 个完成的粉丝。获奖的方式非常简单,只要你跟着教程完成第一课的内容就能获得。
|
21天前
|
自然语言处理 数据可视化 前端开发
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
合合信息的智能文档处理“百宝箱”涵盖文档解析、向量化模型、测评工具等,解决了复杂文档解析、大模型问答幻觉、文档解析效果评估、知识库搭建、多语言文档翻译等问题。通过可视化解析工具 TextIn ParseX、向量化模型 acge-embedding 和文档解析测评工具 markdown_tester,百宝箱提升了文档处理的效率和精确度,适用于多种文档格式和语言环境,助力企业实现高效的信息管理和业务支持。
3945 4
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
|
10天前
|
算法 安全 网络安全
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
2024阿里云11.11金秋云创季活动火热进行中,活动月期间(2024年11月01日至11月30日)通过折扣、叠加优惠券等多种方式,阿里云WoSign SSL证书实现优惠价格新低,DV SSL证书220元/年起,助力中小企业轻松实现HTTPS加密,保障数据传输安全。
530 3
阿里云SSL证书双11精选,WoSign SSL国产证书优惠
|
9天前
|
数据采集 人工智能 API
Qwen2.5-Coder深夜开源炸场,Prompt编程的时代来了!
通义千问团队开源「强大」、「多样」、「实用」的 Qwen2.5-Coder 全系列,致力于持续推动 Open Code LLMs 的发展。
|
16天前
|
安全 数据建模 网络安全
2024阿里云双11,WoSign SSL证书优惠券使用攻略
2024阿里云“11.11金秋云创季”活动主会场,阿里云用户通过完成个人或企业实名认证,可以领取不同额度的满减优惠券,叠加折扣优惠。用户购买WoSign SSL证书,如何叠加才能更加优惠呢?
995 3
|
14天前
|
机器学习/深度学习 存储 人工智能
白话文讲解大模型| Attention is all you need
本文档旨在详细阐述当前主流的大模型技术架构如Transformer架构。我们将从技术概述、架构介绍到具体模型实现等多个角度进行讲解。通过本文档,我们期望为读者提供一个全面的理解,帮助大家掌握大模型的工作原理,增强与客户沟通的技术基础。本文档适合对大模型感兴趣的人员阅读。
447 18
白话文讲解大模型| Attention is all you need