资讯详情

dind(docker in docker)镜像-使用Docker学习Docker

文章目录

  • 使用Docker学习Docker
    • 一、构建演示环境
    • 二、关联基础
        • docker image 文件系统
        • 什么是image layer
        • Dockerfile VOLUME(数据卷) 指令
          • Volume命令的使用
        • 什么是container-diff
          • 使用
    • 三、演示时间
      • 探索 docker create 命令
      • 探索 docker start 命令
      • 探索 docker stop 命令
      • 探索 docker exec 命令
    • 四、总结

使用Docker学习Docker

Learning Docker with Docker - Toying With DinD For Fun And Profit

你有没有注意到一半的容器管理命令看起来像过程管理操作,另一半看起来像文件管理操作? 在这里插入图片描述

这里有一个小练习来加深你对容器的理解… 通过玩它们,我们的目标是证明容器不仅仅是 Linux 这个过程,它也是 Linux文件!

这个想法很简单——用一台安装了 Docker 守护进程的 Linux 机器在上面运行一系列常见的命令,如 Docker create | start | exec | … 密切关注机器的文件系统,希望能有一些有趣的发现。

???

一、构建演示环境

首先,简要介绍临时演示环境。基本上,任何操作 Docker 的干净 Linux 所有的机器都可以。但是因为我们想跟踪主机文件系统 created/deleted/modified我们应该在每个文件中 docker <command> 之后对它进行快照,以便之后对比这些快照。

听起来真的很像 Docker-in-Docker 一个很好的用例。如果实验Docker 保护过程本身在容器中运行,我们可以随时使用标准 docker commit <CONTAINER> 命令将容器的文件系统转储到 image layer。由于容器镜像可以很容易地比较,我们可以使用类似的镜像 container-diff 计算两个比较快照的文件差异的工具。

但是官方的 docker:dind容器镜像 不能使用,因为它包含一个特定的 VOLUME 指令。该指令有充分的理由——将 /var/lib/docker文件夹移出容器(slow and expensive)联合文件系统。然而,正如你马上会看到的,这个文件夹将是我们实验中最热门的位置之一。因此,我们需要确保将/var/lib/docker不需要提交到快照镜像,volumes。

$ git clone https://github.com/docker-library/docker.git $ cd docker/20.10/dind $ sed -i '/VOLUME/d' ./Dockerfile $ docker build -t my-dind:origin . 

另一个重要要求是保持环境足够小和可控。为此,我们需要使用一个超小的测试容器镜像,包括我们的实验Docker 守护进程(guinea-pig Docker daemon)实验所需的内容。一个从头开始的镜像,里面有一个简单的镜像 Go 二进制文件听起来不错。但是,由于涉及临时容器的运行,外构建,因为它涉及临时容器的运行,因此可能会破坏临时环境。

There are many ways to build and distribute images, but for our experiment, I ended up with probably the simplest possible setup: 建造和发布镜像的方法有很多,但对于我们的实验,最终可能是最简单的设置: 以下是如何运行的 Docker 的Linux上述设置在计算机上运行:

# 1. Prepare config to make Docker trust the local registry: $ cat > daemon.json <<EOF { "insecure-registries" : ["my-registry:5000"] } EOF   # 2. 放入上述配置 # the host's /etc/docker/daemon.json   # 3. Restart the host's Docker daemon: $ sudo systemctl restart docker.service   # 4. Create a user-defined network to allow the DinD container # access the Registry container by its hostname: $ docker network create skynet   # 5. Run an (insecure) local registry: $ docker run --detach \     --network skynet \     --publish 5000:5000 \     --name my-registry \     registry:2   # 6. Make the local registry addressible from the host system: $ REGISTRY_IP=$(docker inspect \ -f '{ 
         {range.NetworkSettings.Networks}}{ 
         {.IPAddress}}{ 
         {end}}' \ my-registry)
$ echo "$REGISTRY_IP my-registry" | sudo tee --append /etc/hosts


# 7. Run the guinea-pig Docker daemon using the patched DinD image:
$ docker run --detach --privileged \
    --network skynet \
    --volume `pwd`/daemon.json:/etc/docker/daemon.json \
    --name my-dind \
    my-dind:origin

# 8. Commit the initial state of the guinea-pig Docker system
# as the starting point for further comparisons:
$ docker commit my-dind my-dind:just-started

# [Optional] See what changes have been made to my-dind
# container's filesystem during the container's startup:
$ container-diff diff --type=file \
  daemon://my-dind:origin \
  daemon://my-dind:just-started

现在,准备测试应用程序——一个简单的 HTTP 程序和一个sleep的cli工具:

# syntax=docker/dockerfile:1.4

# ---=== This part is just for building ===---
FROM golang:1.18 as builder

WORKDIR /

COPY <<EOF server.go package main import ( "fmt" "log" "net/http" ) func main() { log.Println("Starting HTTP server...") http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { log.Println("Incoming request") fmt.Fprintf(w, "hello\n") }) http.ListenAndServe(":8090", nil) } EOF

COPY <<EOF sleep.go package main import ( "log" "time" ) func main() { for { log.Println("Zzz...") time.Sleep(1 * time.Second) } } EOF

RUN CGO_ENABLED=0 go build -o server server.go
RUN CGO_ENABLED=0 go build -o sleep sleep.go


# ---=== This is the actual testing image, quite minimalistic ===---
FROM scratch

COPY --from=builder /server /server
COPY --from=builder /sleep /sleep

CMD ["/server"]

Last but not least:

docker buildx build -t my-registry:5000/my-app .
docker push my-registry:5000/my-app

二、关联基础

docker image 文件系统

一图看尽 Docker 容器文件系统 参考URL: https://zhuanlan.zhihu.com/p/362132467

docker 容器启动就是一个文件系统的启动。在docker中,每一层镜像都具备一些文件。

从基本的看起,一个典型的Linux文件系统由bootfs和rootfs两部分组成,bootfs(boot file system)主要包含bootloader和kernel,bootloader主要用于引导加载kernel,当kernel被加载到内存中后bootfs会被umount掉。rootfs (root file system)包含的就是典型Linux系统中的/dev,/proc,/bin,/etc等标准目录和文件。不同的linux发行版(如ubuntu和CentOS )在rootfs这一层会有所区别,体现发行版本的差异性。

但Docker在bootfs自检完毕之后并不会把rootfs的read-only改为read-write,而是利用union mount(UnionFS的一种挂载机制)将image中的其他的layer加载到之前的read-only的rootfs层之上,每一层layer都是rootfs的结构,并且是read-only的。所以,我们是无法修改一个已有镜像里面的layer的!只有当我们创建一个容器,也就是将Docker镜像进行实例化,系统会分配一层空的read-write的rootfs,用于保存我们做的修改。一层layer所保存的修改是增量式的,就像git一样。

综上,image其实就是一个文件系统,它与宿主机的内核一起为程序提供一个虚拟的linux环境。在启动docker container时,依据image,docker会为container构建出一个虚拟的linux环境。

Docker目前支持五种镜像层次的存储driver:aufs、device mapper、btrfs、vfs、overlay。

其中最常用的就是aufs了,但随着,overlay的地位变得更重目前docker默认的存储类型就是overlay2,docker版本是1.8,如下 docker默认的存储目录是/var/lib/docker,我们只关心image和overlay2,image:主要存放镜像中layer层的元数据和overlay2:各层的具体信息。

什么是image layer

Dockerfile由多条指令构成,Dockerfile中的每一条指令都会对应于Docker镜像中的一层。

如下Dockerfile

FROM ubuntu:14.04
ADD run.sh /
VOLUME /data
CMD ["./run.sh"]

通过docker build以上Dockerfile的时候,会在Ubuntu:14.04镜像基础上,添加三层独立的镜像,依次对应于三条不同的命令。镜像示意图如下: Dockerfile中命令与镜像层一一对应,那么是否意味着docker build完毕之后,镜像的总大小=每一层镜像的大小总和呢?答案是肯定的。依然以上图为例:如果ubuntu:14.04镜像的大小为200MB,而run.sh的大小为5MB,那么以上三层镜像从上到下,每层大小依次为0、0以及5MB,那么最终构建出的镜像大小的确为0+0+5+200=205MB。

Dockerfile VOLUME(数据卷) 指令

有状态容器都有数据持久化需求。Docker 采用 AFUS 分层文件系统时,

想要了解Docker Volume,首先我们需要知道Docker的文件系统是如何工作的。Docker镜像是由多个文件系统(只读层)叠加而成。当我们启动一个容器的时候,Docker会加载只读镜像层并在其上(译者注:镜像栈顶部)添加一个读写层。如果运行中的容器修改了现有的一个已经存在的文件,那该文件将会从读写层下面的只读层复制到读写层,该文件的只读版本仍然存在,只是已经被读写层中该文件的副本所隐藏。当删除Docker容器,并通过该镜像重新启动时,之前的更改将会丢失。在Docker中,只读层及在顶部的读写层的组合被称为Union File System(联合文件系统)。

为了能够保存(持久化)数据以及共享容器间的数据,Docker提出了Volume的概念。简单来说,Volume就是目录或者文件,它可以绕过默认的联合文件系统,而以正常的文件或者目录的形式存在于宿主机上。

Volume:即数据卷。

  • Docker Volume命令能让容器从宿主主机中读取文件,或从容器中持久化数据到宿主主机内,让容器与容器产生的数据分离开来,一个容器可以挂载多个不同的目录。
  • Volume的生命周期是独立于容器的生命周期之外的,即使容器删除了,volume(数据卷)也会被保留下来,Docker也不会因为这个volume(数据卷)没有被容器使用而回收。
  • 在容器中,添加或修改这个文件夹里的文件也不会影响容器的联合文件系统。

作用:创建一个匿名数据卷挂载点 格式:

VOLUME ["/data"] 

详解:运行容器时可以从本地主机或其他容器挂载数据卷,一般用来存放数据库和需要保持的数据等

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化

Volume命令的使用

(1)创建数据卷 命令:docker volume create 自定义名称

root@cka-k8s-master:~# docker volume create myVolume
myVolume
root@cka-k8s-master:~#

每创建一个Volume,Docker默认会在宿主机的/var/lib/docker/volumes/目录下创建一个子目录,默认情况下目录名是一串UUID。 如果指定了名称,则目录名是Volume名称(例如上面的myVolume)。Volume里的数据都存储在这个子目录的_data目录下。 之后我们可以把这个数据卷挂载到一个新的容器中,例如Nginx容器。

(2)查看本地数据卷列表 命令:docker volume ls

root@cka-k8s-master:~# docker volume ls DRIVER VOLUME NAME local myVolume root@cka-k8s-master:~#

(3)打印myVolume数据卷的详细信息 命令:docker volume inspect 一个或多个Volume名称

root@cka-k8s-master:~# docker volume inspect myVolume
[
    { 
        
        "CreatedAt": "2022-04-27T10:21:53Z",
        "Driver": "local",
        "Labels": { 
        },
        "Mountpoint": "/data/docker/volumes/myVolume/_data",
        "Name": "myVolume",
        "Options": { 
        },
        "Scope": "local"
    }
]
root@cka-k8s-master:~#
  • 每创建一个Volume,Docker默认会在宿主机的/var/lib/docker/volumes/目录下创建一个子目录,默认情况下目录名是一串UUID。

  • 如果指定了名称,则目录名是Volume名称(例如上面的myVolume)。Volume里的数据都存储在这个子目录的_data目录下。

具名挂载和匿名挂载 (1)匿名挂载 匿名挂载格式:-v /容器内路径或者-v /宿主机路径:/容器内路径 (2)具名挂载   指令:docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx nginx

注意:这里 -v juming-nginx: 代表直接给定名字,但是没有指定路径   如图,查看数据卷一栏增加了刚刚添加的数据卷。

什么是container-diff

官方github: https://github.com/GoogleContainerTools/container-diff

container-diff 是 Google 开源的一个分析和比较容器镜像的工具,可用来分析 Docker 镜像之间的差异。

container-diff 可通过几个不同的标准(角度)来检查镜像,包括:

  • Docker 镜像历史
  • 镜像文件系统
  • Apt 包管理器
  • pip 包管理器
  • npm 包管理器
使用

分析单个Docker镜像

container-diff analyze <image-name>

对比两个Docker镜像

container-diff diff <image1-name> <image2-name>

如果不指定type,默认分析/对比的是镜像大小,即–type=size 可以通过指定type,分析/对比特定维度

container-diff analyze <image-name> --type=<type-name>
container-diff diff <image1-name> <image2-name> --type=<type-name>

type类型支持如下:

  • history:镜像构建历史
  • file:镜像文件
  • size:镜像大小
  • rpm:rpm包管理器
  • pip:pip包管理器
  • apt:apt包管理器
  • node:node包管理器

通过设置--type=file和--filename=/path/file,可以比较比较两个docker镜像中某目录或文件的区别,例如:

container-diff diff nginx:v1 nginx:v2 --type=file --filename=/etc/  

复制代码通过设置-j,可以使用json格式输出结果。 通过设置-w ,可以将结果输入到文件。

三、演示时间

For the experiment itself, I’ll use two terminals simultaneously: 对于实验本身,我将同时使用两个终端:

如下,我们进入到 my-dind 容器中,使用docker ps,在容器中我们看到其他容器- -!这就是容器中的容器!

# Terminal 1 - DinD
$ docker exec -it my-dind sh
$ docker ps
  <empty>

# Terminal 2 - host system
$ docker ps
CONTAINER ID  IMAGE           COMMAND                 ...  STATUS         NAMES
d07b66a353b0  my-dind:origin  "dockerd-entrypoint.…"  ...  Up 25 minutes  my-dind
9a6addf796f6  registry:2      "/entrypoint.sh /etc…"  ...  Up 26 minutes  my-registry

探索 docker create 命令

第一个实验——使用 DinD Docker 实例创建一个 my-app 容器,然后看看会创建哪些文件:

使用两个终端进行实验-DinD 在左边,主机在右边。

# Terminal 1 (DinD)

# Create container:
$ docker create --name my-app my-registry:5000/my-app

# List existing containers:
$ docker ps -a
CONTAINER ID  IMAGE                    COMMAND    ... STATUS   NAMES
2c23a0da2b19  my-registry:5000/my-app  "/server"      Created  my-app

# List running processes:
$ ps auxf
PID   USER     TIME  COMMAND
    1 root      0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.
   61 root      0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsv
   70 root      0:21 containerd --config /var/run/docker/containerd/containerd.toml --log-level
  176 root      0:00 sh
  232 root      0:00 ps auxf

发现 # 1: docker create 创建了一个容器,但是没有创建任何新的进程!

# Terminal 2 (Host)

# Snapshot the DinD container filesystem:
$ docker commit my-dind my-dind:cont-created

# Compare the current state with the previous one:
$ container-diff diff --type=file \
  daemon://my-dind:just-started \
  daemon://my-dind:cont-created

-----File-----

These entries have been added to my-dind:just-started:  # <-- looks like a bug in container-diff output
FILE
...
# This group of files seems important
/var/lib/docker/buildkit/cache.db
/var/lib/docker/buildkit/containerdmeta.db
/var/lib/docker/buildkit/content
/var/lib/docker/buildkit/content/ingest
/var/lib/docker/buildkit/executor
/var/lib/docker/buildkit/metadata_v2.db
/var/lib/docker/buildkit/snapshots.db

# So... This is where containers live on disk!
/var/lib/docker/containers/<CONTAINER-ID>
/var/lib/docker/containers/<CONTAINER-ID>/checkpoints
/var/lib/docker/containers/<CONTAINER-ID>/config.v2.json
/var/lib/docker/containers/<CONTAINER-ID>/hostconfig.json

# And this is where images live
/var/lib/docker/image
...
# Look ma', our `app` image files!
/var/lib/docker/vfs/dir/<LAYER-ID1>
/var/lib/docker/vfs/dir/<LAYER-ID1>/server
/var/lib/docker/vfs/dir/<LAYER-ID2>
/var/lib/docker/vfs/dir/<LAYER-ID2>/server
/var/lib/docker/vfs/dir/<LAYER-ID2>/sleep
...

These entries have been deleted from my-dind:just-started: None

These entries have been changed between my-dind:just-started and my-dind:cont-created:
FILE
/certs/...

发现 # 2: /var/lib/docker/containers/<container-id> ——这是我们的容器在磁盘存储的文件。

发现 # 3: docker create 似乎与 runc create 非常不同——到目前为止还没有创建任何运行时绑定!

Finding #4: Container logs aren’t a thing at this stage yet. The container-diff output above is abridged, but if you’re performing this exercise while reading the article, try searching for files containing the log word in their name - you won’t find anything.

发现 # 4: 在此阶段,容器日志还找不到。上面的container-diff输出是删减的,但是如果您在阅读文章时执行此练习,请尝试搜索包含其名称的日志单词的文件 - 您找不到任何东西。

探索 docker start 命令

是时候启动 my-app 容器了:

# Terminal 1 (DinD)

$ docker start my-app

$ docker ps -a
CONTAINER ID  IMAGE                    COMMAND    ...  STATUS         NAMES
435edb948b83  my-registry:5000/my-app  "/server"       Up 21 seconds  my-app

$ ps auxf
PID   USER     TIME  COMMAND
    1 root      0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem
   60 root      0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem
   69 root      0:07 containerd --config /var/run/docker/containerd/containerd.toml --log-level info
  210 root      0:00 sh
  265 root      0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace moby -id 435edb948b8360ffcbae5452fd6fc0451b5c17daf6940f63db6795c099958357 -address /var/run/docker/containerd/containerd.sock
👉284 root      0:00 /server
  320 root      0:00 ps auxf

那么,已经创建了哪些文件?

# Terminal 2 (Host)

$ docker commit my-dind my-dind:cont-started

# Compare the current state with the previous one:
$ container-diff diff --type=file \
  daemon://my-dind:cont-created \
  daemon://my-dind:cont-started

-----File-----

These entries have been added to my-dind:cont-created:  # <-- rather in cont-started...
FILE
...

# Here it goes - the OCI runtime bundle!
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/address
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/config.json
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/init.pid
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/log.json
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/options.json
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/rootfs
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/runtime
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/work
...

发现 # 5: OCI runtime bundle 是在容器启动时创建的,而不是在容器创建时创建的。

发现 # 6: bundle 是在临时文件系统上创建的! 我现在很好奇——它总是这样吗?

发现 #7: Container logs finally appeared, and it’s just a plain file on disk (depends on the log driver, though): 最终出现了容器日志,它只是磁盘上的一个普通文件(不过取决于日志驱动程序):

📦 # Terminal 1 (DinD)

cat /var/lib/docker/containers/<CONTAINER-ID>/<CONTAINER-ID>-json.log
{ 
        "log":"2022/04/26 05:21:59 Starting HTTP server...\n","stream":"stderr","time":"2022-04-26T05:21:59.3588249Z"}

探索 docker stop 命令

那么,当您停止容器时会发生什么? 它的文件会被删除吗?

# Terminal 1 (DinD)

$ docker stop my-app

$ docker ps -a
CONTAINER ID  IMAGE                    COMMAND    ...  STATUS                    NAMES
435edb948b83  my-registry:5000/my-app  "/server"       Exited (2) 2 seconds ago  my-app

$ ps auxf
PID   USER     TIME  COMMAND
    1 root      0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem
   60 root      0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem
   69 root      0:10 containerd --config /var/run/docker/containerd/containerd.toml --log-level info
  210 root      0:00 sh
  372 root      0:00 ps auxf

发现 # 8: 停止容器会删除 OCI runtime bundle,但不会删除容器在/var/lib/docker/<container-id>的状态(除非 --rm 在创建步骤中被使用)。所以,重新开始是可能的!

探索 docker exec 命令

让我们重复这个实验,但是这一次试着捕捉当你执行到一个运行的容器中时会发生什么:

# Terminal 1 (DinD)

$ docker start my-app

# ...jump to the second terminal for a second

# Terminal 2 (Host)

$ docker commit my-dind my-dind:cont-restarted

# ...back to the DinD terminal

# Terminal 1 (DinD)
$ docker exec -it my-app /sleep
2022/04/21 19:19:52 Zzz...
2022/04/21 19:19:53 Zzz...
2022/04/21 19:19:54 Zzz...
2022/04/21 19:19:55 Zzz...
2022/04/21 19:19:56 Zzz...

# Terminal 3 (also DinD)
$ ps auxf
PID   USER     TIME  COMMAND
    1 root      0:00 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem
   60 root      0:00 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2376 --tlsverify --tlscacert /certs/server/ca.pem --tlscert /certs/server/cert.pem --tlskey /certs/server/key.pem
   69 root      0:07 containerd --config /var/run/docker/containerd/containerd.toml --log-level info
  210 root      0:00 sh
  265 root      0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace moby -id 435edb948b8360ffcbae5452fd6fc0451b5c17daf6940f63db6795c099958357 -address /var/run/docker/containerd/containerd.sock
👉284 root      0:00 /server
  320 root      0:00 ps auxf
👉363 root      0:00 /sleep

但是在实际执行期间究竟发生了什么?

# Terminal 2 (Host)

$ docker commit my-dind my-dind:cont-execed

$ container-diff diff --type=file \
  daemon://my-dind:cont-restarted \
  daemon://my-dind:cont-execed

-----File-----

These entries have been added to my-dind:cont-restarted:
FILE
/run/docker/containerd/daemon/io.containerd.runtime.v2.task/moby/<CONTAINER-ID>/<SOME-OTHER-ID>.pid

These entries have been deleted from my-dind:cont-restarted: None
These entries have been changed between my-dind:cont-restarted and my-dind:cont-execed: None

好吧,这是令人惊讶的!我期待着为 exec 会话创建另一个临时(和匿名)容器,因为 exec 不是真正的 OCI 运行时命令,而只是由 Docker 和类似的容器管理器实现的一个方便的高级操作,它重用 OCI 运行时 start 命令。但是实际的实现似乎非常轻量级,并且使用与主容器相同的 OCI runtime bundle。

发现 # 9: docker exec 几乎没有文件系统足迹!

四、总结

好吧,我希望这是一个有趣的练习至少对我来说,——容器与文件和进程一样重要。

标签: 35100d07no光电传感器35100d07pc光电传感器2565d07nc光电传感器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台