docker常用命令操作方法

继续docker的学习之旅,今天练习一些常用的命令:

一、镜像相关

1.1 列出本机所有镜像

后面的操作,都以ubuntu做为练习的目标。

另外:如果某些镜像文件不想要了,可以用下面的命令删除

1.2 删除镜像

docker rmi 镜像Id(即:1.1图中的IMAGE ID)

有时候删除会失败,比如:有一个容器正在使用该镜像文件。这时可以加参数-f 强制删除,如果不清楚每个命令可以加哪些参数,可以用

docker 命令 --help

查看帮助,比如:

 bin docker rmi --help
Usage: docker rmi [OPTIONS] IMAGE [IMAGE...]
Remove one or more images
 -f, --force=false Force removal of the image
 --help=false   Print usage
 --no-prune=false  Do not delete untagged parents

二、容器相关

2.1 最基本的启动

docker run -it ubuntu

参数-it的含义,可以用docker run --help查看,就不展开了

2.2 启动后执行命令

docker run -it ubuntu echo 'hello world'

2.3 启动时指定容器名称

docker run -it --name 'myubuntu' ubuntu

容器名称是一个很有意思的东东,后面马上会讲到。上面的命令运行完以后,先用exit退出,以便后面学习其它命令。

2.4 查看最近运行过的所有容器

docker ps -a  

  

从图上可以看出,如果启动时未显示指定容器名称,docker会自动生成一个好玩的名称,命令的风格大致是:什么样的_谁谁,比如图中的insane_lamarr,字面的意思为"疯狂的拉马尔",从这些细节可以感受到,docker的创造者们都是一帮很爱玩的家伙。

除了容器名称,还有二列非常重要:CONTAINER ID及STATUS,其中STATUS中以Up开头的,表示容器正在运行(注:容器是否处于运行状态,排除人为docker stop的因素外,很大程序上是由docker run 最后的命令参数决定的,如果启动时不指定任何命令参数,默认执行/bin/bash,如果指定了类似echo "hello world"之类瞬间就执行完的命令,run起来,马上就会转为关闭,因为命令已经执行完了),而CONTAINER ID在很多场景中都会用到(比如:删除容器)

另外,对于同一个镜像(比如ubuntu),默认不指定容器名称的话,每次容器启动docker都会生成一个唯一的名称,这个有点象OOP编程,镜像相当于Class类定义,是一个只读的模板,而容器则是类的运行实例,java中每次new出来的实例,其hashcode必然不同,所以每次启动的docker容器,名称也不一样,只不过与OOP不同的是,OOP中实例消亡了,所有关联的信息全清掉了,而docker容器就算停止掉,docker仍会记住其最后的运行状态。

可以做一个小试验,刚才我们已经创建了一个名为myubuntu的容器:

docker run -it --name 'myubuntu' ubuntu

这一行命令再次运行的话,就会报错:

Error response from daemon: Conflict. The name "myubuntu" is already in use by container d1c261ad0b1e. You have to remove (or rename) that container to be able to reuse that name.

大意是容器名称mybutun已经被另一个容器(ID为d1c261ad0b1e)占用了,要么把原来的容器删除,要么换个名字。

这其中的设计思想,可以仔细琢磨一二,想想也十分合理:类比一下,我们写代码时,同一个类new出多个实例,每个实例都会有自己不同的应用场景,比如:同样是一个Order实例,可以用在订单创建的业务场景中,也可以用在订单查询的返回结果中...,docker的镜像也是如此,同样一个ubuntu镜像文件,有人用它创建容器是为了安装nginx当成web server,有人用它创建容器是为了学习hadoop...,为了能以一种友好的方式来区分,所以名字不能冲突,然后,同一个名字的容器,今天安装了软件A,玩事儿后将它关闭,明天可能会继续在这个容器上折腾其它事情,所以每次容器停止,不可能象OOP中的实例一样,彻底丢弃实例的所有信息,否则明天就没办法接着玩了。

2.5 停止运行中的容器

docker stop 容器名称

2.6 删除容器

docker rm 容器ID

如果容器处于运行状态,上面的操作会失败,可以加-f参数强制删除  

2.7 在已运行的容器中,直接执行命令

docker exec 容器名称 命令

例如:  

docker exec myubuntu apg-get update

2.8 附加到已经运行的容器

docker attach 容器名称

注:该命令运行后,mac上屏幕没任何输出,还以为卡死了,这是假象,直接继续输入命令,比如pwd之类的就能看到结果了.

attach这个命令不太好用,进入终端后,没办法退出而不停止容器,要退出只能输入exit,但这样就将容器停止了,另外一个缺点是,如果多个容器同时attach到相同的容器,在一个窗口中操作的结果,会同步显示到所有窗口。

建议用下面的命令代替:

docker exec -it 容器名称 sh

当然进入容器还有其它一些办法,比如网络端口22映射本机某个端口,容器里启动ssh服务,然后ssh连接进入,或者用nsenter结合进程id进入,但个人觉得这些方法操作都太复杂,远不如上面这行命令简单

2.9 保存对容器所做的修改

在容器上做了一堆操作后,比如在ubuntu的基础上安装了一些软件、部署了一些应用之类,希望分发到其它机器,最简单的办法就是把容器重新生成一个新镜像,然后其它人直接docker pull你的新镜像就可以了。

docker commit -a 作者名字 -m 提交原因 -p 容器ID 镜像名称:版本号

比如:

docker commit -a 'yjmyzz' -m 'test commit' -p d1c261ad0b1e yjmyzz/ubuntu:V2

提交完成后,可以

docker images 查看

从图中可以看出,在ubuntu原来的基础上,生成一个名为yjmyzz/ubuntu的新镜像,然后用新镜像创建容器试试看

docker run -it --name 'myubuntu2' yjmyzz/ubuntu:V2 

三、卷(volumn)相关

我们平时在使用电脑的过程中,会经常通过usb插入一些外部存储设备,比如:u盘之类,插好后,就能象常规硬盘目录一样访问外部存储设备。卷(volume)的意思其实跟这个差不多,可以把host机上的某个目录"插入"到容器中,然后容器中就能直接访问host机上的文件了,即使容器删除掉,卷里的数据仍然可能持久保存。

3.1 创建卷

docker run -it -v /Users/yjmyzz/docker_volumn:/opt/webapp --name myubuntu ubuntu /bin/bash

这个命令略长,但并不复杂,跟前面提到的启动容器相比,只是多了一个-v /Users/yjmyzz/docker_volumn:/opt/webapp的部分,意思就是将本机/Users/yjmyzz/docker_volumn这个目录映射到容器中的/opt/webapp,启动成功后,保持当前窗口不退出,可以再新开一个terminal容器,进入容器验证一下

可以尝试在host本机修改下/Users/yjmyzz/docker_volumn/index.html这个文件,然后在容器中cat看下内容,应该马上就能看到最新的内容。

三个大坑:

其一:

-v 参数可以只写前面第一部分,-v /Users/yjmyzz/docker_volumn 这样启动也不会报错,但是这样做的效果,在最新版本的docker(1.9.1)上,只会把本机目录挂到容器中,容器中看不到本机的任何文件,所以一定要记得写:后的部分

其二:

权限问题,mac机上如果从网上down(非apple store官方)了一个文件到本机,该文件甚至保存文件的目录权限,都会被设置成特殊权限@,见下面的截图:

这本来是mac 10.5以后做的一项安全改进,有此标识的程序,在首次执行时会提示

但是有这类特殊权限的目录或文件,被挂到容器中后,docker容器内根本看不到,也就是无权读取。处理办法:

ll -l@ -a

先用这个显示特殊权限的详细信息:

然后用xattr -r -d 详细信息 * 去掉这些特殊权限(参考下图),然后再重新挂到容器中,就能正常使用了

其三:

mac上挂载的本机目录,必须是在~/(即:当前用户的目录)下,类似/opt/www这样的目录,就算给它所有权限,挂到容器中后,也只能看到目录,读不到任何文件,centOS上没这问题。

此外,还可以用命令

docker inspect myubuntu

查看此时容器的所有状态,会看到一段长长的json输出,类似下面这样:

[
{
 "Id": "21d15713166ae83b022eea8806bd466da9917422e487e874cc098a0f1329dd48",
 "Created": "2016-01-28T02:23:43.91086474Z",
 "Path": "/bin/bash",
 "Args": [],
 "State": {
  "Status": "running",
  "Running": true,
  "Paused": false,
  "Restarting": false,
  "OOMKilled": false,
  "Dead": false,
  "Pid": 1843,
  "ExitCode": 0,
  "Error": "",
  "StartedAt": "2016-01-28T02:26:09.414485616Z",
  "FinishedAt": "2016-01-28T02:25:43.868883111Z"
 },
 "Image": "8693db7e8a0084b8aacba184cfc4ff9891924ed2270c6dec6a9d99bdcff0d1aa",
 "ResolvConfPath": "/mnt/sda1/var/lib/docker/containers/21d15713166ae83b022eea8806bd466da9917422e487e874cc098a0f1329dd48/resolv.conf",
 "HostnamePath": "/mnt/sda1/var/lib/docker/containers/21d15713166ae83b022eea8806bd466da9917422e487e874cc098a0f1329dd48/hostname",
 "HostsPath": "/mnt/sda1/var/lib/docker/containers/21d15713166ae83b022eea8806bd466da9917422e487e874cc098a0f1329dd48/hosts",
 "LogPath": "/mnt/sda1/var/lib/docker/containers/21d15713166ae83b022eea8806bd466da9917422e487e874cc098a0f1329dd48/21d15713166ae83b022eea8806bd466da9917422e487e874cc098a0f1329dd48-json.log",
 "Name": "/myubuntu",
 "RestartCount": 0,
 "Driver": "aufs",
 "ExecDriver": "native-0.2",
 "MountLabel": "",
 "ProcessLabel": "",
 "AppArmorProfile": "",
 "ExecIDs": null,
 "HostConfig": {
  "Binds": [
   "/Users/yjmyzz/docker_volumn:/opt/webapp"
  ],
  "ContainerIDFile": "",
  "LxcConf": [],
  "Memory": 0,
  "MemoryReservation": 0,
  "MemorySwap": 0,
  "KernelMemory": 0,
  "CpuShares": 0,
  "CpuPeriod": 0,
  "CpusetCpus": "",
  "CpusetMems": "",
  "CpuQuota": 0,
  "BlkioWeight": 0,
  "OomKillDisable": false,
  "MemorySwappiness": -1,
  "Privileged": false,
  "PortBindings": {},
  "Links": null,
  "PublishAllPorts": false,
  "Dns": [],
  "DnsOptions": [],
  "DnsSearch": [],
  "ExtraHosts": null,
  "VolumesFrom": null,
  "Devices": [],
  "NetworkMode": "default",
  "IpcMode": "",
  "PidMode": "",
  "UTSMode": "",
  "CapAdd": null,
  "CapDrop": null,
  "GroupAdd": null,
  "RestartPolicy": {
   "Name": "no",
   "MaximumRetryCount": 0
  },
  "SecurityOpt": null,
  "ReadonlyRootfs": false,
  "Ulimits": null,
  "LogConfig": {
   "Type": "json-file",
   "Config": {}
  },
  "CgroupParent": "",
  "ConsoleSize": [
   0,
   0
  ],
  "VolumeDriver": ""
 },
 "GraphDriver": {
  "Name": "aufs",
  "Data": null
 },
 "Mounts": [
  {
   "Source": "/Users/yjmyzz/docker_volumn",
   "Destination": "/opt/webapp",
   "Mode": "",
   "RW": true
  }
 ],
 "Config": {
  "Hostname": "21d15713166a",
  "Domainname": "",
  "User": "",
  "AttachStdin": true,
  "AttachStdout": true,
  "AttachStderr": true,
  "Tty": true,
  "OpenStdin": true,
  "StdinOnce": true,
  "Env": null,
  "Cmd": [
   "/bin/bash"
  ],
  "Image": "ubuntu",
  "Volumes": null,
  "WorkingDir": "",
  "Entrypoint": null,
  "OnBuild": null,
  "Labels": {},
  "StopSignal": "SIGTERM"
 },
 "NetworkSettings": {
  "Bridge": "",
  "SandboxID": "893c76e283a75e3eebb474bf1b5bce901a37778de3514b526312134fcc858d2c",
  "HairpinMode": false,
  "LinkLocalIPv6Address": "",
  "LinkLocalIPv6PrefixLen": 0,
  "Ports": {},
  "SandboxKey": "/var/run/docker/netns/893c76e283a7",
  "SecondaryIPAddresses": null,
  "SecondaryIPv6Addresses": null,
  "EndpointID": "a7fee41964177719fbd149df820bf66dbd976ebe7cea0b68497ae2fe4c06efc5",
  "Gateway": "172.17.0.1",
  "GlobalIPv6Address": "",
  "GlobalIPv6PrefixLen": 0,
  "IPAddress": "172.17.0.2",
  "IPPrefixLen": 16,
  "IPv6Gateway": "",
  "MacAddress": "02:42:ac:11:00:02",
  "Networks": {
   "bridge": {
    "EndpointID": "a7fee41964177719fbd149df820bf66dbd976ebe7cea0b68497ae2fe4c06efc5",
    "Gateway": "172.17.0.1",
    "IPAddress": "172.17.0.2",
    "IPPrefixLen": 16,
    "IPv6Gateway": "",
    "GlobalIPv6Address": "",
    "GlobalIPv6PrefixLen": 0,
    "MacAddress": "02:42:ac:11:00:02"
   }
  }
 }
}
]

90~97行的Mounts节点描述了当前容器挂载的"卷"信息。

最后指出一点:目前docker仅支持在run(创建)容器时使用-v创建卷,对于一个已经start的容器,如果想动态添加卷,是十分困难的。虽然国外有牛人,实现了在容器启动后动态添加卷,但过程十分曲折,而且并不能能用,有兴趣的可以参考下面的文章

http://jpetazzo.github.io/2015/01/13/docker-mount-dynamic-volumes/

3.2 列出所有卷

docker volume ls

3.3 删除卷

docker volume rm 卷名称

注:删除一个容器时,默认不会删除容器关联的卷,所以随着时间的推移,host上可能会存在大量的"僵尸"卷,占用硬盘空间。建议每次docker rm 容器时,加上参数-v,这样删除容器时会一并将对应的卷删除,但是这样也会有一个副作用,如果多个容器同时关联到同一个卷,可能会影响到其它容器。所以在使用卷的时候要规划清楚,最好一个容器只对应一个卷。

tips:如果要批量删除所有卷,一个一个rm显然太麻烦了,可以用下面的方式快速搞定

a) 进入docker虚拟机defaut

docker-machine ssh default

b) 查看volume所在的目录

c)切换到sudo模式

sudo -i

d) 进入volume所在根目录

cd /var/lib/docker/volumes/ 

  

上图的ls命令已经说明,所谓的数据卷,其实就是一个个目录,再次印证了linux里的一句名言『一切皆是文件』,剩下的事情,地球人都知道了,邪恶的

rm -rf *

,最后还要重启虚拟机,退回到mac主机

docker-machine restart default 

3.4 数据卷容器

如果多个容器之间希望共享一份数据,除了上面的方式外,docker还允许定义一个专用的容器,这个容器啥也不干,只用来放数据,这种容器称为『数据卷容器』

示例:

docker run -it -v /Users/yjmyzz/docker_volumn:/sites --name site_files kitematic/hello-world-nginx echo 'only for nginx web files'

上面的命令跟之前创建卷的完全一样,现在我们有了一个名为site_files的数据卷容器,注意:创建数据卷容器时,最后的命令通常都是些打酱油的echo之类,反正只是一个存数据的容器,不用执行其它命令,甚至它本身都不需要处于启动状态。

然后,其它容器创建时,就可以使用它了:

docker run -d --volumes-from site_files --name nginx1 kitematic/hello-world-nginx sh ./start.sh

注意上面的--volumes-from site_files 这个就是使用数据卷容器的关键,其它跟之前的完全相同,多个容器可以挂同一个数据卷容器,一个容器也可以挂多个数据卷容器。

四、网络相关

4.1 端口映射

-p IP:host_port:container_port

上面的参数表示将本机IP上的hostport映射到容器的container_port,示例:  

docker run -it -v /Users/yjmyzz/Documents/Kitematic/hello-world-nginx/website_files:/website_files -p 0.0.0.0:10080:80 --name my-nginx kitematic/hello-world-nginx sh /start.sh

这个命令更长了,结合了之前所有学习到的参数,注意多出的问题-p 0.0.0.0:10080:80,表示将本机10080端口映射到容器80端口 

注:如果把-p换成大写的-P,系统会随机映射到本机一个空闲的端口号 

4.2 指定hostname

默认创建容器时,hostname是一个唯一的随机字符串,很难记,可以在docker run -h hostname名称来指定,这个就不演示了

4.3 容器间的网络连接

假如有二个容器mysql, appserver,通常appserver中要访问数据库,所以需要appserver能直接访问mysql,下面演示了如何实现:

a) 先创建mysql容器

docker run -it -h mysql --name mysql ubuntu /bin/bash

b) 再创建appserver容器  

docker run -it -h appserver --name appserver --link mysql:mysqlserver ubuntu /bin/bash

注意其中的--link mysql:mysqlserver,冒号前的为容器名称,冒号后的为容器别名,启动后appserver中就能直接ping通mysql容器了,见下图:

注:这个连接是单向的,即appserver可以ping通mysql容器,但反过来不行。而且最新版的docker在ps时,Name列也不再象之前网上说的那个显示成A/B这种格式,要查看一个容器是否有连接,最直接的方式还是docker inspect 容器名称

  

总结

以上所述是小编给大家介绍的docker常用命令操作方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • Docker 常用命令整理并介绍

    什么是Docker? Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的.可移植的.自给自足的容器.开发者在笔记本上编译测试通过的容器可以批量地在生产环境中部署,包括VMs(虚拟机).bare metal.OpenStack 集群和其他的基础应用平台. Docker通常用于如下场景: web应用的自动化打包和发布: 自动化测试和持续集成.发布: 在服务型环境中部署和调整数据库或其他的后台应用: 从头编译或者扩展现有的OpenShift或Cloud Foundry平台来搭建自己的P

  • docker测试时候命令无法补全的解决方法

    发现问题 在输入docker swarm 然后tab键不能像这样进行提示 和补全 tab 键也无法补全nginx容器名,下面是运行截图 解决方法: yum install -y bash-completion 退出 后重进就正常了 总结 以上就是这篇文章的全部内容了,希望本文的内容对同样遇到这个问题的朋友们能有所帮助,如果有疑问大家可以留言交流.

  • 常用的Docker基本命令及用法汇总

    查看docker安装信息 docker info 获取docker镜像 docker pull azraelrabbit/monupw 创建docker容器 docker run --name monojexus -d -p 32022:22 -p 32080:80 -v /mydata:/data azraelrabbit/monupw 以后台进程的方式运行docker容器 sample_job=$(docker run -d monojexus /bin/sh -c "while true;

  • Docker中镜像构建文件Dockerfile与相关命令的详细介绍

    前言 使用docker build命令或使用Docker Hub的自动构建功能构建Docker镜像时,都需要一个Dockerfile文件.Dockerfile文件是一个由一系列构建指令组成的文本文件,docker build命令会根据这些构建指令完成Docker镜像的构建.本文将会介绍Dockerfile文件,及其中使用的构建指令. 1. Dockerfile文件使用 docker build命令会根据Dockerfile文件及上下文构建新Docker镜像.构建上下文是指Dockerfile所在

  • Docker常用的清除容器镜像命令小结

    前言 Docker 是一个非常有趣的项目.它自己宣称可以减轻部署服务器的难度,当然我相信里面有炒作的成分.但是实际使用后,我觉得 Docker 的表现还是可圈可点的.这篇文章主要总结了Docker清除容器镜像常用的命令,下面来一起看看吧. 杀死所有running状态的容器 docker kill $(docker ps -q) 删除所有已经停止的容器 docker rm $(docker ps -a -q) 删除所有\'untagged/dangling\' ()状态的镜像 docker rmi

  • Docker 打包python的命令详解

    最近用Python写了一段爬虫程序,为了隔离其运行环境,易于分发,把项目打包成Docker镜像 Dockerfile FROM python:2.7.12-alpine ADD ./src /job CMD ["python", "/job/main.py"] 构建命令 $ docker build -t job . 运行 $ docker run -d --name job job 比较简单 以上所述是小编给大家介绍的Docker 打包python的命令详解,希望

  • 详解修改docker时区及docker常用命令

    前几天遇到这样一个业务场景,数据库运行在docker 中,docker 的市区是utc 所以就跟北京时间相差8个小时.但是又不能重新运行一个容器,只能保证数据库运行状态,并把宿主机的时区复制给docker 容器.很苦恼, 首先我先把宿主机的时区改成啦CST 北京时间.然后把宿主机的时区复制给docker 容器.命令如下 docker cp /etc/localtime:[容器ID或者NAME]/etc/localtime 当然也可以进入容器进行修改时区(不过我的容器修改的时候总是报/etc/lo

  • Docker run 命令的使用方法详解

    注意,本文基于最新的Docker 1.4文档翻译. Docker会在隔离的容器中运行进程.当运行 docker run命令时,Docker会启动一个进程,并为这个进程分配其独占的文件系统.网络资源和以此进程为根进程的进程组.在容器启动时,镜像可能已经定义了要运行的二进制文件.暴露的网络端口等,但是用户可以通过docker run命令重新定义(译者注:docker run可以控制一个容器运行时的行为,它可以覆盖docker build在构建镜像时的一些默认配置),这也是为什么run命令相比于其它命

  • 详解Docker创建Mysql容器并通过命令行连接到容器

    拉取网易蜂巢的mysql-server:5.6 docker pull hub.c.163.com/nce2/mysql:5.6 创建mysql5.6容器 1master+3个slave docker run --name mysql-master -d -P hub.c.163.com/nce2/mysql:5.6 docker run --name mysql-slave1 -d -P hub.c.163.com/nce2/mysql:5.6 docker run --name mysql-

  • Docker 基础之Dockerfile命令详解

    Dockerfile 是一个文本格式的配置文件,用户可以使用 Dockerfile 快速创建自定义的镜像.我们会先介绍 Dockerfile 的基本结构及其支持的众多指令,并具体讲解通过执行指令来编写定制镜像的 Dockerfile. 基本结构 Dockerfile 由一行行命令语句组成,并且支持已 # 开头的注释行.一般而言,Dockerfile 的内容分为四个部分:基础镜像信息.维护者信息.镜像操作指令和容器启动时执行指令.例如: # This dockerfile uses the Ubu

  • 详解Docker学习之用commit命令创建镜像

    构建镜像的两种方法: 使用docker commit 命令 使用docker build命令和Dockerfile文件 Dockerfile更抢到.灵活,推荐使用. 一般来说不是真的"创建"新镜像,而是基于一个已有的基础镜像,比如Ubuntu.Fedora等,构建新的镜像而已. 用commit创建镜像 创建Docker账号 共享和发布镜像时构建镜像中重要的环节,可以将镜像推送到Docker Hub或资金的私有Registry中.首先到https://hub.docker.com/acc

  • Docker 修改文件是否需要重启(命令详解)

    Docker 修改文件是否需要重启 Docker 修改文件是否需要重启,在做项目的时候,经常会修改文件,对于命令不是多了解,用了也是试试的态度,这里就整理下该如何使用,提高工作效率. 需要修改一期项目中的代码了,之前每次都是用"试一下"的方式得出要不要启动,现在总结下哪些情况需要启动什么 1.首先再熟悉下每条命令的作用 docker-compose up -d -将会在后台启动并运行所有的容器 docker-compose stop -停止一个已经运行的容器,但不删除它,可通过dock

  • Docker基础命令详解

    docker基本概念 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的 Linux 机器上. Docker是一个重新定义了程序开发测试.交付和部署过程的开放平台,Docker则可以称为构建一次,到处运行,这就是docker提出的"Build once,Run anywhere" 创建镜像 创建镜像的方法有三种: 基于已有的容器创建 基于本地模板导入 基于dockerfile 基于已有的容器创建 主要使用docker

  • Docker 清理命令集锦

    杀死所有正在运行的容器 复制代码 代码如下: docker kill $(docker ps -a -q) 删除所有已经停止的容器 复制代码 代码如下: docker rm $(docker ps -a -q) 删除所有未打 dangling 标签的镜像 复制代码 代码如下: docker rmi $(docker images -q -f dangling=true) 删除所有镜像 复制代码 代码如下: docker rmi $(docker images -q) 为这些命令创建别名 复制代码

随机推荐