zoukankan      html  css  js  c++  java
  • Jenkins与Docker的自动化CI/CD实战(一)

    做这个项目之前回顾一下docker

    一 、docker介绍

    1.什么是docker

    Docker属于Linux容器的一种封装,提供简单易用的容器使用接口。
    Docker将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。
    有了Docker,就不用担心环境问题。 总体来说,Docker的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

    2.docker的概念

    Docker是开发人员和系统管理员使用容器开发、部署和运行应用程序的平台。使用Linux容器来部署应用程序称为集装箱化。使用docker轻松部署应用程序。

    集装箱化的优点:

    • 灵活:即使是复杂的应用程序也可封装。
    • 轻量级:容器利用并共享主机内核。
    • 便携式:您可以在本地构建,部署到云上并在任何地方运行。
    • 可扩展性:您可以增加和自动分发容器副本。
    • 可堆叠:您可以垂直堆叠服务并及时并及时堆叠服务。               

    3.容器和虚拟机

    虚拟机(virtual machine)就是带环境安装的一种解决方案。它可以在一种操作系统里面运行另一种操作系统,比如在Windows系统里面运行Linux系统。应用程序对此毫无感知,
    因为虚拟机看上去跟真丝系统一模一样,而对于底层系统来说,虚拟机就是一个普通文件,不需要了就删掉,对其它部分毫无影响。
    Linux容器不是模拟一个完整的操作系统,而是对进程进行隔离。或者说,在正常进程的外面套了一个保护层。对于容器里面的进程来说,它接触到的各种资源都是虚拟的,从而实现与底层系统的隔离。
    由于容器是进程级别的,相比虚拟机又很多优势。
    优缺点 虚拟机 linux容器(LXC)
    资源 资源占用多 资源占用少
    启动速度 启动慢 启动快
    复杂程度 冗余步骤多 体积小
    安全性 安全--防止交互 有些不安全

    4.基本概念

       4.1docker镜像

    对于linux而言,会挂载root文件系统为其提供用户空间支持。而Docker镜像,相当于一个root文件系统。除了提供容器运行时所需的程序、库、资源、配置等文件外,还包括了一些为运行时准备的一些配置参数(,匿名卷,环境变量,用户等)

    分层存储

      docker设计时,充分利用Union FS的技术,将其设计为分层存储的架构。所以镜像是一个虚拟概念,其实实际是由多层文件系统联合组成。分层存储的特征还使得镜像的复用,定制变得更为容易,甚至可以用之前构建好的镜像作为基础层。

     

    统一文件系统(Union file system)能够将不同层整合成一个文件系统,为这些曾提供了一个统一的视角,这样就隐藏了多层的存在,用户视角只能看到一个文件系统。

       4.2 Docker容器

      镜像和容器的关系,就想是面向对象程序设计中类和实例一样,镜像是静态的定义,容器是镜像运行的实体。

      容器存储层的生存周期和容器一样,容器小王时,容器存储层也随之消亡。因此,任何保存于容器存储层的信息都会随容器删除而丢失

    5.仓库

       Docker Registry

    镜像构建完成后,可以很容易的在当前宿主机上运行,但是,如果需要在其它服务器上使用这个镜像,我们就需要一个集中的存储、分发镜像的服务,Docker Registry就是这样的服务。
    一个Docker Registry中可以包含多个仓库(Repository);每个仓库可以包含多个标签(tag);每个标签对应一个镜像。
    通常,一个仓库会包含一个软件不同版本的镜像,而标签就常用于对应该软件的各个版本。我们可以通过<仓库名>:<标签>的格式来指定具体是这个软件那个版本的镜像。如果不给出标签,将以laest作为默认标签。
    以ubuntu镜像为例,ubuntu是仓库的名字,其包含有不同的版本标签,如,14.04,16.04。我们可以通过ubuntu:14.04或者ubuntu:16.04来具体指定所需要哪个版本的镜像。如果忽略了标签,比如ubuntu,那将视为ubuntu:latest。
    仓库名经常以两段式路径形式出现,比如jwilder/nginx-proxy,前者意味着Docker Registry多用户环境下的用户名,后者则往往是对应的软件名。但这并非绝对,取决于所使用的具体Docker Registry的软件或服务。

         公开服务

      最常用的Registry公开服务是官方的Docker Hub,这也是磨人的Registry,并拥有大量的高质量的官方镜像。比如:Google Container Registry,Kubernetes的镜像使用就是这个服务。

          私有仓库

     Docker提供了Docker registry镜像,可以作为私有仓库服务

    二、Docker的使用场景

    1、Web应用服务

    Web应用服务是使用最广泛的一类服务,典型的架构是前端一个Tomcat + Java服务,后端mysql数据库。前端的Java web服务器是最适合使用Docker容器的,先将Java运行环境、web服务器直接打包成一个通用的基础docker镜像,之后再将自定义应用代码或编译程序包加入到该基础镜像中就能产生一个新的应用镜像,最后通过docker服务立马就能以容器的形式启动web应用服务。因为web应用程序一般是无状态的,随着业务访问量增减,用同样的镜像新建、销毁容器即可轻松实现伸缩(前面还需配上DNS域名或者负载均衡的服务),例如下图所示。

     2.持续集成和持续部署

    互联网行业倡导敏捷开发,使用docker容器云平台,就能实现从代码编写完成推送到git/svn后,自动触发后端平台将代码下载、编译并构建成测试docker镜像,再替换测试环境容器服务,自动在Jenkins中运行单元/集成测试,最后测试通过后,马上就能自动将新版本镜像更新到线上,完成服务升级。整个过程全自动化,一气呵成,最大程度地简化了运维成本,而且保证线上、线下环境完全一致,而且线上服务版本与git/svn发布分支也实现统一。

    三、docker组件

    docker client

    docker daemon

    docker image

    docker Registry

    docker Container

    (1)docker client,docker daemon

    Docker 是一个客户端、服务端(C/S)架构的程序。Docker客户端只需向Docker服务器或守护进程发出请求,服务器或守护进程将完成所有工作并返回结果。Docker守护进程有时也称为Docker引擎。Docker提供了一个命令行工具docker以及一整套RESTful API来与守护进程交互。用户可以在同一台宿主机上运行Docker守护进程和客户端,也可以从本地的Docker客户端连接到运行在另一台宿主机上的远程Docker守护进程。

    (2)docker image

    (3)docker Registry

    (4)docker Container

    四 docker安装

    4.1 安装

    (1)环境要求

    安装docker ce ,需要centos7维护版本。

    (2)卸载旧版本

     sudo yum remove docker 
                      docker-client 
                      docker-client-latest 
                      docker-common 
                      docker-latest 
                      docker-latest-logrotate 
                      docker-logrotate 
                      docker-engine

    (3)使用存储库安装

    Docker 要求CentOS系统的内核版本高于3.10

    ①查看内核版本

    uname -r
    升级内核

    ②确保yum包更新到最新

    yum update -y
    reboot

    ③安装所需的软件包。yum-utils 提供了 yum-config-manager ,并且 device mapper 存储驱动程序需要 device-mapper-persistent-data 和 lvm2。

    yum install -y yum-utils 
      device-mapper-persistent-data 
      lvm2

    ④以下命令来设置稳定的仓库。

    yum-config-manager 
        --add-repo 
        https://download.docker.com/linux/centos/docker-ce.repo

    ⑤要安装特定版本的 Docker Engine-Community,请在存储库中列出可用版本,然后选择并安装:

    yum list docker-ce --showduplicates | sort -r

    ⑥安装

    yum install docker-ce
    yum install <FQPN>  安装指定版本

    ⑦启动并设为自启

    systemctl start docker
    systemctl enable docker

    docker version

    4.2 docker基本操作

    配置镜像加速器:

    针对docker客户端版本大于1.10.0用户

    mkdir /etc/docker
    sudo tee /etc/docker/daemon.json <<-'EOF' 
    { 
    "registry-mirrors": ["https://sopn42m9.mirror.aliyuncs.com"] 
    } 
    EOF 
    sudo systemctl daemon-reload 
    sudo systemctl restart docker

    4.2.1 运行一个WEB应用

    docker pull training/webapp  #拉去镜像
    docker run -d -P training/webapp python app.py

    参数说明:

    -d  :容器在后台运行

    -P :将容器内部使用的网络端口映射到我们使用的主机上

    -p: 8088:8080  前面为宿主机的端口,后面为容器的端口

    docker run -d -p 5000:5000 training/webapp python app.py

    查看容内部标准输出

    docker logs -f  "container id"

     4.2.2 docker运行(创建)容器命令

    docker run tomcat
    docker run -it 镜像名
    docker run -it --name 别名 镜像名
     
    参数说明:

    -i : 表示创建要给交互式容器

    -t:表示运行容器的同时创建一个伪终端,一般与 -i 一起使用

    –-name : 自定义容器别名

    4.2.3 docker查看容器命令

    [root@localhost ~]# docker ps
    CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
    971de2af3135        registry            "/entrypoint.sh /etc…"   23 hours ago        Up About a minute   0.0.0.0:5000->5000/tcp   registry

    CONTAINER ID : 容器唯一id

    IMAGE : 对应镜像名

    COMMAND:启动方式

    CREATED:创建时间

    STATUS:当前状态

    PORTS:占用端口

    NAMES:容器名称(别名)

    参数:

    -l(小写的L) : 默认的查看只会查看正在运行中的容器信息,而ps -l 会显示最近运行的一条容器信息

    -a : 显示所有运行过的镜像信息

    -q :表示只显示对应的容器id 信息

    4.2.4 docker启动、重启,停止容器命令

    [root@localhost ~]# docker ps -a
    CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS                       PORTS                     NAMES
    db10f9ea1854        192.168.253.158:5000/zpzc   "/usr/local/tomcat/b…"   17 hours ago        Exited (255) 6 minutes ago   0.0.0.0:28888->8080/tcp   zpzc
    
    [root@localhost ~]# docker start db10f9ea1854
    db10f9ea1854
    [root@localhost ~]# docker ps -a
    CONTAINER ID        IMAGE                       COMMAND                  CREATED             STATUS                      PORTS                     NAMES
    db10f9ea1854        192.168.253.158:5000/zpzc   "/usr/local/tomcat/b…"   17 hours ago        Up 2 seconds 
    0.0.0.0:28888->8080/tcp zpzc

    进入终端(可以使用docker attach 或 docker exec等命令进入

    [root@localhost ~]# docker restart db10f9ea1854

    [root@localhost ~]# docker stop db10f9ea1854

    docker kill 容器id

    这种方式比较粗暴,但是速度快,直接结束线程,好比电脑直接长按关机

    4.2.5 docker删除容器命令

    docker rm 容器id
    #删除一个未运行的容器对象,支持多条删除,但是删除的容器id还未停止,则会删除异常
    
    docker rm -f 容器id
    #-f : 强制删除,包括正在运行的容器也可以强制删除,支持多条删除
    
    docker rm -f $(docker ps -aq) 
    #组合命令,删除全部的容器信息

    4.2.6 docker进入容器命令

    docker attach 容器id
    #attach:可以再次进入该容器的伪终端控制台
    
    docker exec -it 容器id
    #exec:该命令,可以实现不需要进入容器终端进行交互,可以在宿主机与容器进行交互,
    
    -it : 表明执行一个交互式的伪终端方式进入到容器中

    4.2.7docker查看容器内运行的线程

    docker top 容器id

    #docker查看容器的内部细节

    docker inspect 容器id

    docker cp 容器id:容器文件路径 宿主机路径

    cp:通过cp命令,将a57faaef1751容器中的 tmp目录下的yum.log文件,复制到宿主机的当前目录下,命名为aa.log

    4.3Docker镜像使用

    当运行容器时,使用的镜像如果在本地不存在,docker会自动从docker镜像仓库中下载,默认是从Docker Hub公共镜像资源下载

    (1)列出镜像列表

    [root@localhost ~]# docker images
    REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
    192.168.253.158:5000/zpzc         latest              4ffe43c11d34        19 hours ago        397MB
    192.168.253.158:5000/tomcat-8.5   latest              d4e99a0e37c0        23 hours ago        295MB
    <none>                            <none>              8d8ac51442d6        23 hours ago        285MB
    registry                          latest              2d4f4b5309b1        11 days ago         26.2MB
    centos                            7                   b5b4d78bc90c        7 weeks ago         203MB

    各项说明:

    REPOSITORY:表示镜像的仓库源
    TAG:镜像的标签,同一个仓库源可以有多个TAG,代表这个仓库源的不同版本。例如:ubuntu运行镜像时,ubuntu:15.10,如果不指定版本,则默认使用latest镜像
    IMAGE ID:镜像ID
    CREATED:镜像创建时间
    SIZE:镜像大小

    (2)查找镜像

    [root@localhost ~]# docker search httpd
    NAME                                    DESCRIPTION                                     STARS               OFFICIAL            AUTOMATED
    httpd                                   The Apache HTTP Server Project                  3076                [OK]
    centos/httpd-24-centos7                 Platform for running Apache httpd 2.4 or bui…   33               
    centos/httpd                                                                            29                                      [OK]
    arm32v7/httpd                           The Apache HTTP Server Project                  9                
    polinux/httpd-php                       Apache with PHP in Docker (Supervisor, CentO…   4       

    (3)创建镜像

    当我们从docker镜像仓库中下载的奖项不能满足我们的需求时,我们可以通过以下两种方式对镜像进行更改。

    1.从已经创建的容器中更新镜像,并且提交这个镜像

    2.使用Dockerfile指令创建一个新的镜像

    五、docker镜像操作 

    5.1. 更新镜像

    在运行容器内使用apt-get update命令进行更新

    [root@localhost ~]# docker pull httpd
    [root@localhost ~]# docker run -t -i httpd /bin/bash
    root@472ce8701afd:/usr/local/apache2# apt-get update
    exit

    容器id为472ce8701afd,通过docker commit来提交容器副本。

    [root@localhost ~]# docker commit -m="has update" -a="lanist" 472ce8701afd lansit/httpd:v2.0

    参数说明:

    -m :提交描述信息

    -a :指定镜像作者

    lansit/httpd:v2.0:指定要创建的目标镜像名

    sha256:5816d65e2fa22d6a617fc2913f1bb46cee019dfbe77c37c356c61d1e9b9bd97c

    [root@localhost ~]# docker images
    REPOSITORY   TAG    IMAGE       ID CREATED     SIZE
    lansit/httpd v2.0 5816d65e2fa2  4 minutes ago  183MB

    5.2.构建镜像

    使用docker build,需要创建一个Dockerfile文件,其中包含一组指令来告诉Docker如何构建我们的镜像

    dockerfile

    5.2.1dockerfile是用来构建Docker镜像的构建文件,是由一系列命令和参数构成的脚本

    每条保留字指令都必须为大写字母且后面要跟随至少一个参数

    指令按照从上到下,顺序执行

    每条指令都会创建一个新的镜像层,并对镜像进行提交

    5.2.2dcoker执行dockerfile的大致流程

    ①docker从基础镜像运行一个容器

    ②执行一条指令并对容器做出修改

    ③执行类似docker commit的操作提交一个新的镜像层

    ④docker再基于刚提交的镜像运行一个新容器

    ⑤执行dockerfile中的下一条指令直到所有指令都执行完成

    5.2.3 dockerfile文件说明

    FROM:指定基础镜像,必须为第一个命令

    格式:
      FROM <image>
      FROM <image>:<tag>
      FROM <image>@<digest>
    示例:
      FROM mysql:5.6
    注:
      tag或digest是可选的,如果不使用这两个值时,会使用latest版本的基础镜像

    MAINTAINER: 维护者信息

    格式:
        MAINTAINER <name>
    示例:
        MAINTAINER Jasper Xu
        MAINTAINER sorex@163.com
        MAINTAINER Jasper Xu <sorex@163.com>

    RUN:构建镜像时执行的命令

    RUN用于在镜像容器中执行命令,其有以下两种命令执行方式:
    shell执行
    格式:
        RUN <command>
    exec执行
    格式:
        RUN ["executable", "param1", "param2"]
    示例:
        RUN ["executable", "param1", "param2"]
        RUN apk update
        RUN ["/etc/execfile", "arg1", "arg1"]
    注:
      RUN指令创建的中间镜像会被缓存,并会在下次构建中使用。如果不想使用这些缓存镜像,可以在构建时指定--no-cache参数,如:docker build --no-cache
    复制代码

    ADD:将本地文件添加到容器中,tar类型文件会自动解压(网络压缩资源不会被解压),可以访问网络资源,类似wget

    复制代码
    格式:
        ADD <src>... <dest>
        ADD ["<src>",... "<dest>"] 用于支持包含空格的路径
    示例:
        ADD hom* /mydir/          # 添加所有以"hom"开头的文件
        ADD hom?.txt /mydir/      # ? 替代一个单字符,例如:"home.txt"
        ADD test relativeDir/     # 添加 "test" 到 `WORKDIR`/relativeDir/
        ADD test /absoluteDir/    # 添加 "test" 到 /absoluteDir/
    复制代码

    COPY:功能类似ADD,但是是不会自动解压文件,也不能访问网络资源

    CMD:构建容器后调用,也就是在容器启动时才进行调用。

    复制代码
    格式:
        CMD ["executable","param1","param2"] (执行可执行文件,优先)
        CMD ["param1","param2"] (设置了ENTRYPOINT,则直接调用ENTRYPOINT添加参数)
        CMD command param1 param2 (执行shell内部命令)
    示例:
        CMD echo "This is a test." | wc -
        CMD ["/usr/bin/wc","--help"]
    注:
      CMD不同于RUN,CMD用于指定在容器启动时所要执行的命令,而RUN用于指定镜像构建时所要执行的命令。
    复制代码

    ENTRYPOINT:配置容器,使其可执行化。配合CMD可省去"application",只使用参数。

    复制代码
    格式:
        ENTRYPOINT ["executable", "param1", "param2"] (可执行文件, 优先)
        ENTRYPOINT command param1 param2 (shell内部命令)
    示例:
        FROM ubuntu
        ENTRYPOINT ["top", "-b"]
        CMD ["-c"]
    注:
       ENTRYPOINT与CMD非常类似,不同的是通过docker run执行的命令不会覆盖ENTRYPOINT,而docker run命令中指定的任何参数,都会被当做参数再次传递给ENTRYPOINT。Dockerfile中只允许有一个ENTRYPOINT命令,多指定时会覆盖前面的设置,而只执行最后的ENTRYPOINT指令。
    复制代码

    LABEL:用于为镜像添加元数据

    格式:
        LABEL <key>=<value> <key>=<value> <key>=<value> ...
    示例:
      LABEL version="1.0" description="这是一个Web服务器" by="IT笔录"
    注:
      使用LABEL指定元数据时,一条LABEL指定可以指定一或多条元数据,指定多条元数据时不同元数据之间通过空格分隔。推荐将所有的元数据通过一条LABEL指令指定,以免生成过多的中间镜像。

    ENV:设置环境变量

    复制代码
    格式:
        ENV <key> <value>  #<key>之后的所有内容均会被视为其<value>的组成部分,因此,一次只能设置一个变量
        ENV <key>=<value> ...  #可以设置多个变量,每个变量为一个"<key>=<value>"的键值对,如果<key>中包含空格,可以使用来进行转义,也可以通过""来进行标示;另外,反斜线也可以用于续行
    示例:
        ENV myName John Doe
        ENV myDog Rex The Dog
        ENV myCat=fluffy
    复制代码

    EXPOSE:指定于外界交互的端口

    复制代码
    格式:
        EXPOSE <port> [<port>...]
    示例:
        EXPOSE 80 443
        EXPOSE 8080
    EXPOSE 11211/tcp 11211/udp
    注:
      EXPOSE并不会让容器的端口访问到主机。要使其可访问,需要在docker run运行容器时通过-p来发布这些端口,或通过-P参数来发布EXPOSE导出的所有端口
    复制代码

    VOLUME:用于指定持久化目录

    复制代码
    格式:
        VOLUME ["/path/to/dir"]
    示例:
        VOLUME ["/data"]
        VOLUME ["/var/www", "/var/log/apache2", "/etc/apache2"
    注:
      一个卷可以存在于一个或多个容器的指定目录,该目录可以绕过联合文件系统,并具有以下功能:
    1 卷可以容器间共享和重用
    2 容器并不一定要和其它容器共享卷
    3 修改卷后会立即生效
    4 对卷的修改不会对镜像产生影响
    5 卷会一直存在,直到没有任何容器在使用它
    复制代码

    WORKDIR:工作目录,类似于cd命令

    复制代码
    :
        WORKDIR /path/to/workdir
    示例:
        WORKDIR /a  (这时工作目录为/a)
        WORKDIR b  (这时工作目录为/a/b)
        WORKDIR c  (这时工作目录为/a/b/c)
    注:
      通过WORKDIR设置工作目录后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT、ADD、COPY等命令都会在该目录下执行。在使用docker run运行容器时,可以通过-w参数覆盖构建时所设置的工作目录。
    复制代码

    USER:指定运行容器时的用户名或 UID,后续的 RUN 也会使用指定用户。使用USER指定用户时,可以使用用户名、UID或GID,或是两者的组合。当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户

    复制代码

     格式:
      USER user
      USER user:group
      USER uid
      USER uid:gid
      USER user:gid
      USER uid:group

     示例:
      USER www

     注:

      使用USER指定用户后,Dockerfile中其后的命令RUN、CMD、ENTRYPOINT都将使用该用户。镜像构建完成后,通过docker run运行容器时,可以通过-u参数来覆盖所指定的用户。

    复制代码

    ARG:用于指定传递给构建运行时的变量

    格式:
        ARG <name>[=<default value>]
    示例:
        ARG site
        ARG build_user=www

    ONBUILD:用于设置镜像触发器

    格式:
      ONBUILD [INSTRUCTION]
    示例:
      ONBUILD ADD . /app/src
      ONBUILD RUN /usr/local/bin/python-build --dir /app/src
    注:
      当所构建的镜像被用做其它镜像的基础镜像,该镜像中的触发器将会被钥触发

    5.3自定义tomcat

    方法一:

    此方法的jdk环境,在宿主机上下载,配置好,在以下ENV JAVA_HOME /usr/local/jdk使用

    FROM centos:7
    MAINTAINER lanist
    
    ENV VERSION=8.5.56
    ENV JAVA_HOME /usr/local/jdk
    
    RUN yum install wget -y
    
    RUN wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-8/v8.5.56/bin/apache-tomcat-8.5.56.tar.gz && 
        tar zxf apache-tomcat-${VERSION}.tar.gz && 
        mv apache-tomcat-${VERSION} /usr/local/tomcat && 
        rm -rf apache-tomcat-${VERSION}.tar.gz /usr/local/tomcat/webapps/*
    
    EXPOSE 8080
    CMD ["catalina.sh", "run"]

    构建一个新镜像

    docker build -t 192.168.253.158:5000/tomcat-8.5

    -t: 指定要生成的目标镜像名

    运行----tomcat运行需要jdk环境,所以在宿主机上安装jdk,写好环境变量,运行时,映射

    docker run -d --name lanist/tomcat8.5  -p 2888:8080 -v /usr/local/jdk:/usr/local/jdk 192.168.253.158:5000/tomcat-8.5

    方法二:

    1.新建一个目录,将jdk和tomcat的压缩包copy进来

    2.编写dockerfile

    5.4 docker容器连接

    首先,查看一个名叫docker0的网络设备:

    docker守护进程就是通过docker0为docker容器提供网络连接的各种服务。docker0实质是linux的续集网桥

    可以设置IP,相当于一个隐藏虚拟网卡

    在宿主机上查看网桥设备

    [root@localhost ~]# yum install bridge-utils -y
    [root@localhost ~]#brctl show
    bridge name     bridge id               STP enabled     interfaces
    docker0         8000.024224c297ea       no              veth988e2f5

    可以对docker0进行修改,使之成为我们希望的网段

    ifconfig docker0 IP netmask NATMASK

    5.4.1Docker容器的互联

    docker会为每个相连的容器分配一个虚拟子网。但尽管容器都连在同一个虚拟网桥上,如果没有额外配置,容器还是不能通信

  • 相关阅读:
    如何缓解考前紧张和焦虑
    **浅谈差分【复习】**
    杂题训练之十一
    浅谈dfs/Tarjan找环【复习】
    杂题训练之十一
    杂题训练之十
    动态规划训练之二十
    浅谈欧拉函数【复习】
    数论训练之五
    浅谈杨辉三角【复习】
  • 原文地址:https://www.cnblogs.com/lanist/p/13209678.html
Copyright © 2011-2022 走看看