ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Alluxio v2.9.4实战:部署、挂载S3/HDFS与缓存调优全解析

Alluxio v2.9.4实战:部署、挂载S3/HDFS与缓存调优全解析 简介Alluxio分布式存储系统 v2.9.4 是一套基于内存的分布式存储中间件面向Hadoop、Spark等大数据生态旨在屏蔽底层存储系统差异并加速数据访问。该版本提供灵活的文件API类似于java.io.File并兼容Hadoop HDFS的文件系统接口使MapReduce和Spark可直接用其替代HDFS从而在大数据作业中获得更低的延迟和更高的吞吐。资源包共2000个文件压缩包大小约16.3MB内容以Java源码为主辅以TypeScript、Markdown、XML、Shell等类型分别对应核心逻辑、前端界面、说明文档和部署脚本。已有171人学习。通过阅读源码可以系统理解可插拔底层存储机制的实现了解如何对接S3、HDFS、Ceph、OSS等多种底层存储同时也能掌握内存与SSD/HDD层级存储的自动化管理策略以及文件API和HDFS兼容层的设计细节对从事大数据平台建设、分布式存储开发或技术选型的人员具有直接参考价值。1. 为什么数据湖里要放一个Alluxio它不是存数据的是搬数据的做数据平台的人基本都遇到过这种场景Spark 跑批任务每次都直接打远程的 S3 或 HDFS底层存储的吞吐一上去网络带宽和磁盘 IO 立刻见顶任务从 20 分钟拖到 2 小时。你加再多计算节点也没用瓶颈根本不在 CPU。Alluxio 这个分布式存储系统干的事就是在计算框架和底层存储之间插一层本地化缓存层让热数据留在离计算最近的地方。v2.9.4 是这个项目里比较稳的一个长期版本我这边线上和测试环境现在都钉在这个版本上。换句话说Alluxio 不是一个传统意义上的“存数据”的系统你往它里面写的数据最终要落到 UFS底层存储系统上它更像一个带内存缓存和统一命名空间的数据编排层。适合谁适合那些被底层存储拖累、又不想把数据全量拷贝多份的人。这篇东西我会按照从零部署到挂载底层存储、再到读写调优的完整路径来写涉及的关键配置参数和踩过的坑也会一并放出来照着走能少翻几次车。2. 部署 Alluxio v2.9.4 集群从下载到三节点跑通的完整步骤2.1 下载与环境检查JDK 和内存预设是第一步先说明一点这里讲的是独立集群部署方式不依赖 K8s因为绝大多数第一次接触 Alluxio 的人都是在已有的大数据集群上补这一层独立部署最容易暴露问题。我在这个阶段吃过最大的亏是没检查 JDK 版本拉起来之后 Master 进程反复挂掉日志里全是UnsupportedClassVersionError。v2.9.x 要求 JDK 8 或 JDK 11这里建议直接用 JDK 11后面跑 Spark 或者 Flink 作业兼容性更好。另外Alluxio 的 Master 和 Worker 都是 Java 进程Master 默认 JVM 堆是 1 到 8GBv2.9 里ALLUXIO_MASTER_JAVA_OPTS默认给了 8GB 上限Worker 默认堆是 4GB。你先用free -g看一下节点内存。如果机器只有 8GB就别硬上Master 一个进程 4GBWorker 再吃 2GBOS 本身还要占一部分内存不够会导致进程被 OOM Killer 干掉而且这种死亡方式不会在 Alluxio 日志里留明显异常。下载建议直接去官网选2.9.4版本对应的 tar 包比如alluxio-2.9.4-bin.tar.gz。这里我给你一套最小化部署的命令# 每台机器都要执行解压到统一目录 tar -zxvf alluxio-2.9.4-bin.tar.gz -C /opt/ mv /opt/alluxio-2.9.4 /opt/alluxio # 创建软链方便后续版本升级切换 ln -s /opt/alluxio-2.9.4 /opt/alluxio # 配置环境变量写入 /etc/profile.d/alluxio.shsource 后生效 export ALLUXIO_HOME/opt/alluxio export PATH$ALLUXIO_HOME/bin:$PATH这段逻辑不复杂但有一个细节容易漏掉软链和ALLUXIO_HOME环境变量最好一开始就做对不要图省事直接用解压目录。等以后升级到 2.9.x 的新补丁版本你只需要把软链切换到新目录旧目录可以直接保留做回退。环境检查的另外一步是看系统文件句柄和网络参数。Alluxio 的 Worker 和底层存储之间会建立大量 TCP 连接ulimit -n默认 1024 在高并发下不够用。这个可以直接在/etc/security/limits.conf里配硬性上限# 追加到 /etc/security/limits.conf然后重新登录生效 * soft nofile 65535 * hard nofile 65535网络方面需要确认节点间 19998Master RPC、19999Master Web UI、29998Worker RPC、29999Worker Web UI这几个端口是互通的。很多集群配了防火墙规则外部访问 Master UI 没问题但 Worker 之间互相注册失败最后看起来是“某个 Worker 一直不在线”实际原因只是防火墙把 29998 断了。2.2 修改 conf 目录下的配置文件alluxio-site.properties 是最核心的一步Alluxio v2.9.4 的配置体系是分层的alluxio-site.properties是用户级配置优先级最高alluxio-default.properties是系统默认值不要改这个文件否则升级的时候会被覆盖而且排查问题时你根本分不清哪些是默认行为、哪些是自己的改动。我的建议是在$ALLUXIO_HOME/conf/目录下用cp alluxio-site.properties.template alluxio-site.properties生成一份自己的配置。这个文件是全文最关键的配置文件Master 和 Worker 的绝大多数行为都受它控制。# 在 conf 目录下创建/修改 alluxio-site.properties以下配置基于三节点集群master 2 worker # Master 节点主机名在 Master 所在机器上必须配置 alluxio.master.hostnamemaster-node # 底层存储地址这里先挂一个 HDFS 路径后文会讲 S3 alluxio.master.mount.table.root.ufshdfs://nameservice/alluxio # Worker 的缓存存储目录默认是内存盘生产环境可以加一个 SSD 作为第二层 alluxio.worker.tieredstore.level0.dirs.path/mnt/ramdisk alluxio.worker.tieredstore.level0.dirs.mediumRAM alluxio.worker.tieredstore.level0.dirs.quota8GB # 每个 Worker 最多可用的缓存总容量这里配置为节点总内存的一半以上 alluxio.worker.memory.size8GB # RPC 和 Web 端口确认和防火墙规则一致 alluxio.master.rpc.port19998 alluxio.master.web.port19999 alluxio.worker.rpc.port29998 alluxio.worker.web.port29999 # 元数据备份相关Journal 存放在本地 alluxio.master.journal.typeEMBEDDED alluxio.master.journal.folder/data/alluxio/journal配置参数说明alluxio.master.hostname是 Master 注册给 Worker 用的地址其他机器通过这个地址连 Masteralluxio.master.mount.table.root.ufs决定了你在 Alluxio 根目录下看到的数据最终落在哪里这里相当于一个默认的底层挂载点alluxio.worker.memory.size这个参数决定 Worker 能用来做缓存的最大上限它和机器物理内存直接相关——注意不要分配超过节点物理内存的 70%否则系统 Swap 会被打爆。alluxio.master.journal.typeEMBEDDED是 v2.9.x 里推荐的单机或小集群模式把 journal 写到本地目录。但这种模式在三节点上存在风险如果 Master 挂掉journal 也丢了。生产环境如果要 HA应该把 journal 放到多副本的底层存储上或者直接搭 3 个 Master 节点的 RAFT 模式这个后文会展开。2.3 格式化与快速启动journal 和 worker 的启动顺序有讲究配置写完之后不要直接上生产环境。先在最小范围把它跑通。第一步是格式化这个动作要谨慎——它会把 journal 目录和 Worker 的缓存目录全部清空相当于初始化一个全新的 Alluxio 集群。# 在 Master 节点上执行这一步会创建 journal 并清空 worker 的缓存目录 bin/alluxio format # 启动 Master 进程 bin/alluxio-start.sh master # 在每台 Worker 节点上执行不需要在 master 上重复跑 bin/alluxio-start.sh worker # 等一下确认所有进程都在 bin/alluxio fsadmin report summaryformat命令的逻辑要交代清楚它检查alluxio.master.journal.folder和 worker 的 tiered store 目录如果存在旧数据会直接清空或要求你确认。所以这个命令只能在首次部署或者确定要丢弃所有元数据的时候执行。一旦集群跑起来有真实业务数据在走绝对不要随意format否则元数据全没了底层存储里孤儿文件找不回来。启动顺序方面Master 必须第一个起来因为 Worker 启动后会立刻尝试向 Master 注册注册失败会不断重试默认重试次数很多日志看着像卡住其实没坏。我一般会等 Master 日志里出现Alluxio Master started字样再去批量拉起 workers。用bin/alluxio-start.sh workers这个命令可以跳过手动逐个登录它借助配置好的conf/workers文件实现 SSH 批量操作前提是 Master 到各 Worker 的 SSH 免密已经建好。到这里一个最小集群就已经在线了。用浏览器打开http://master-node:19999能看到 Master 的 Web UI上面有当前注册的 Worker 数量和总缓存容量。如果这里显示 0 worker基本就是 2.1 节说的端口问题或 hostname 不可达问题先查本机/etc/hosts有没有三台机器的完整映射这个最简单也最容易被忽略。3. 挂载底层存储把 S3 和 HDFS 接到 Alluxio 上的关键参数3.1 挂载 S3路径挂载与凭据配置的正确姿势根路径默认挂载是写在alluxio.master.mount.table.root.ufs里的但实际业务中没人只用一个底层存储。Alluxio 支持在根路径下动态挂载多个 UFS比如把 HDFS 挂到/hdfs把 S3 挂到/s3这样一来同一套 Spark 作业可以通过alluxio://master:19998/s3/...访问两套完全不同底层协议的数据。先在alluxio-site.properties里把 S3A 需要的 Hadoop 依赖和访问凭证配好v2.9.4 用的 S3A 客户端是 Hadoop 提供的所以这两项配置本质上最后会进core-site.xml的内容但 Alluxio 允许直接写在 properties 里# 追加到 conf/alluxio-site.properties alluxio.underfs.s3a.endpointhttp://s3-cn-north-1.amazonaws.com.cn alluxio.underfs.s3a.access.key你的AK alluxio.underfs.s3a.secret.key你的SK alluxio.underfs.s3a.regioncn-north-1配好之后重启 Master 和 Worker这里的重启是必须的因为alluxio-site.properties只在进程启动时读取一次不会热加载然后执行挂载命令bin/alluxio fs mkdir /s3 bin/alluxio fs mount --option s3a.accessKey你的AK --option s3a.secretKey你的SK /s3 s3a://my-bucket/prefix bin/alluxio fs mount挂载命令的最后一个输出应该是/s3 - s3a://my-bucket/prefix这一行证明挂载成功。--option参数可以把某一次挂载特有的凭据单独传进去避免把 AK/SK 写死在公共配置里。但要注意fs mount命令在 v2.9.4 上如果你不加--option它会读alluxio.underfs.s3a.*前缀的全局配置所以两种方式至少要有一种能取到有效凭据。S3 挂载最容易翻车的地方在 endpoint。私有化的 S3 兼容服务比如 MinIO或者国内云厂商的对象存储endpoint 如果写错或者写成公网默认地址大概率表现为第一次ls成功第二次读写超时。这是因为 S3A 客户端解析区域后自动转发到默认域名和你的 endpoint 不一致导致网络黑洞。解决办法是显式设置alluxio.underfs.s3a.endpoint为内网服务地址同时把alluxio.underfs.s3a.path.style.accesstrue打开让请求用路径方式访问而不是虚拟主机方式。3.2 挂载 HDFS版本兼容是最大变量如果你的底层存储是 HDFS版本兼容问题是最大的坑。Alluxio 底层用hadoop-ufs模块去跟 HDFS 通信它默认编译时用的 Hadoop 版本是某一个大版本比如 3.3.x。如果你的集群是 Hadoop 2.7.x直接挂载可能会在启动 Master 时抛NoSuchMethodError或ClassNotFoundException。v2.9.4 的解决方式是在alluxio-site.properties中显式声明 Hadoop 版本# 如果你的 HDFS 是 3.3.x 或 2.10.x这里自己改 alluxio.underfs.hdfs.version3.3.0 alluxio.underfs.hdfs.configuration/etc/hadoop/conf/core-site.xml:/etc/hadoop/conf/hdfs-site.xmlalluxio.underfs.hdfs.configuration这个参数非常实用。它允许你把现有 Hadoop 集群的 XML 配置直接引过来这样 NameNode 地址、DFS client failover 配置、kerberos 认证这些细节你就不用重抄到 Alluxio 文件里少走一堆弯路。挂载命令如下bin/alluxio fs mkdir /hdfs bin/alluxio fs mount /hdfs hdfs://mycluster/data需要注意当 Alluxio 挂载 HDFS 后它的目录名空间逻辑和 HDFS 原目录不是一一对应的同步复制。你在 Alluxio 里rm -rf /hdfs/foo默认情况下会直接删掉底层 HDFS 里的对应目录。想防止这种误删要开启alluxio.underfs.object.directory.service.concurrent相关的保护或使用alluxio fs rm -R前仔细确认。这一点后文避坑部分会重点再讲。3.3 通配符挂载与只读挂载两种实际工作中常见的挂载需求除了静态挂载单个路径Alluxio 还支持URI中的通配符。对多租户的团队来说经常有“把每个业务组的桶挂到对应命名空间路径下”的需求。假设你有多个桶bucket-app1、bucket-app2、bucket-app3如果一个个手写mount命令以后加租户还得再操作一次。而 Alluxio 允许用alluxio fs mount /app app-s3://bucket-app*这类方式做前缀挂载。但通配符挂载有一个使用限制你只能在挂载时指定一层通配符而且需要确保底层存储列表是可枚举的。建议先写个小脚本循环生成挂载命令而不是直接用通配符——挂载失败时排查哪一层失效也比较麻烦。我的习惯是for bucket in $(aws s3 ls | awk {print $3} | grep bucket-app); do alluxio fs mkdir /app/$bucket alluxio fs mount /app/$bucket s3a://$bucket/ done只读挂载则适合那些“底层数据由其他平台维护Alluxio 侧只读”的场景。在挂载时用--readonly参数# 只读挂载避免误操作写坏底层生产数据 bin/alluxio fs mount --readonly /hdfs/prod hdfs://nameservice/data/prod在alluxio.master.mount.table.root.ufs根挂载上也支持类似的alluxio.master.mount.table.root.readonlytrue如果整个 Alluxio 只是做计算加速层底层数据的增删都走原有通道我建议直接把根挂载也设成只读这会省掉后面很多数据安全方面的沟通成本。4. 第一次读写数据缓存命中率、块大小和一致性从哪里看4.1 用 Alluxio 命令行完成上传、加载与缓存验证挂载完底层存储接下来要验证数据能不能正常读和写。常用命令其实不多alluxio fs ls、alluxio fs copyFromLocal、alluxio fs load、alluxio fs free这四招基本覆盖所有日常需求。# 写一个测试文件到 Alluxio并让它落到底层 HDFS echo hello alluxio /tmp/test.txt bin/alluxio fs copyFromLocal /tmp/test.txt /hdfs/test.txt # 查看当前文件状态和所在缓存层 bin/alluxio fs stat /hdfs/test.txt # 主动把底层已有的大文件加载到缓存 bin/alluxio fs load /hdfs/bigdata/file.parquet # 释放缓存不删底层数据 bin/alluxio fs free /hdfs/bigdata/file.parquetstat命令的输出里有一个Persistence State字段PERSISTED表示数据已同步到底层NOT_PERSISTED表示数据只存在于 Alluxio 缓存内、还没有写回底层存储。这个字段很关键。比如你直接用alluxio fs copyFromLocal写入文件时数据是先进 Worker 缓存再异步持久化到 UFS 的。刚写完几秒内去 stat有时候会看到NOT_PERSISTED这是正常现象异步持久化还没完成别慌。但如果文件长时间保持不变大概率说明底层存储写失败或者凭证过期了。此时去 Worker 日志里搜UnderFileSystem相关的错误通常能看到AccessDeniedException或者Connection refused。很多人在这个点上折腾半天最后发现只是 S3 的 AK/SK 轮换了没有同步更新到 Alluxio 的配置里。4.2 缓存命中率怎么看仪表盘、页面和日志三个来源缓存命中率是判断 Alluxio 到底有没有价值的核心指标。如果命中率只有 10%说明你的工作负载基本全是热数据一次性访问Alluxio 没有起到缓存作用该调策略而不是继续加容量。v2.9.4 里看命中率有两条路# 通过 fsadmin 拿到 Master 的指标快照 bin/alluxio fsadmin report metrics输出里关注Cluster.BytesReadAlluxio和Cluster.BytesReadUfsAlluxio这两个计数器。命中率 BytesReadAlluxio / (BytesReadAlluxio BytesReadUfsAlluxio)。同时Master Web UI 的 Metrics 页面也有一个实时的图表显示从 Alluxio 缓存读取和从 UFS 读取的字节比例还有缓存清除率。如果页面上的 Cache Hit Rate 长期低于 50%检查读作业的访问局部性而不是继续调参数。另一个我常用的验证方式是alluxio fs ls的输出中会标记CACHE或NOT_CACHED。比如 load 之后文件前面会出现一个小写字母P和C。C表示当前在 worker 的缓存中P表示已在 UFS 持久化。这些命令输出本身不够系统化但对临时验证单文件很直观。4.3 块大小与并发参数调好这两个才能扛住计算高峰Alluxio 内部像 HDFS 一样把大文件切成一个个 block默认的块大小是 64MBalluxio.user.block.size.bytes.default。对于生产环境我习惯调到 128MB和 HDFS 的 block 大小对齐。块太大小文件会浪费缓存空间块太小Master 的元数据膨胀读写请求数量也增加。# 如果不生成新文件只是读改这两个参数就够了 alluxio.user.block.size.bytes.default128MB alluxio.user.ufs.block.read.concurrency32alluxio.user.ufs.block.read.concurrency控制一个块最多被多少个 reader 并发从 UFS 读取。如果底层是 SSD可以调到 64如果是 S3建议 16 以内因为 S3 单连接的带宽有限并发太高反而造成连接池耗尽。另外和它配合的还有alluxio.worker.network.reader.buffer.size读写吞吐瓶颈会从网络转到这里。这组参数本身不涉及代码改动但调完必须重启 Master。注意重启 Master 不会主动清空 worker 的缓存数据所以生产环境重启时不用太担心缓存丢失。唯一要做的是在业务低峰期执行因为重启的几十秒内新的作业拿不到锁可能会直接报连接失败。5. 避坑Alluxio v2.9.4 常见的五个故障与对应解法5.1 日志里出现 No space left on device根本没有配置错是 tmpfs 满了现象Worker 持续写入缓存时日志报No space left on device但用df -h看磁盘还有大量空闲。原因Worker 的默认缓存目录是/mnt/ramdisk它被挂载成 tmpfs大小由alluxio.worker.memory.size指定。ramdisk 的天花板就是内存你在df -h里看到的是磁盘根分区不知道/mnt/ramdisk已经 100% 占满。解决执行df -h /mnt/ramdisk确认容量。要把缓存目录放到数据盘上就在alluxio-site.properties里改 tiered store 配置比如把level0.dirs.path/data/alluxio/cache同时mediumSSD容量改成磁盘实际大小。另外可以打开alluxio.worker.space.quota用于限定某个 worker 的磁盘缓存上限避免一个 worker 把整块缓存盘写爆。5.2 Master 启动失败journal 目录损坏导致的连锁问题现象bin/alluxio-start.sh master执行后进程存在但客户端连不上或者启动时直接抛Journal is already locked、Failed to acquire lock。原因上一轮 Master 异常退出后journal 目录里的锁文件没释放或者多个 Master 进程同时指向同一个 journal 目录比如你用 systemd 不小心启了两个实例。解决先jps确认所有 Master 进程都杀掉再删掉 journal 目录下的.lock文件最后重新启动。不要轻易用format否则元数据全清。如果 journal 目录所在磁盘写满了也会表现为同样的启动失败先df -h查看。这个坑我遇过一次表面看是锁冲突实际是/data分区 100% 导致 journal 无法落盘。5.3 Worker 频繁断连堆内存不能只给默认值现象Master Web UI 里显示 Worker 注册后几分钟就变成LOST状态然后重新注册间歇性发生。原因Worker 的默认 JVM 堆是 4GB但如果你缓存的数据量很大Worker 进行 block 元数据管理时堆内存不够触发频繁 GCMaster 判定 worker 心跳超时。日志里搜WorkerClient或timeout能看到heartbeat timeout字样。解决把 Worker 堆内存调大alluxio-worker进程的 JVM 参数在conf/alluxio-env.sh里设置。如果你是 64GB 内存的机器且缓存容量分配了 48GB建议至少给 worker 进程分配 6~8GB 堆。# 在 conf/alluxio-env.sh 中修改 ALLUXIO_WORKER_JAVA_OPTS-Xmx8g -Xms8g堆内存设得和缓存容量越接近需要调优的 GC 参数就越多。简单起见堆给 8GB 起步不要小于缓存容量的 1/8。5.4 误删底层数据Alluxio 的 delete 是透传的现象执行alluxio fs rm /hdfs/data/important结果底层 HDFS 里的该目录也被删了。原因Alluxio 对 UFS 的操作默认是“透传”的。它让用户以 Alluxio 命名空间视角操作但删除动作会直接作用到 UFS 对应路径不会先询问也没有回收站机制v2.9.4 没有内置 trash。解决没有银弹只能靠权限和习惯。在挂载时对生产目录使用--readonly同时限制用户的rm权限。对必须开放写权限的目录跟底层存储团队约定好定期快照或者在 Alluxio 之上再包一层网关。我的个人习惯是任何rm -R操作之前先执行alluxio fs ls确认路径和预期一致再看一眼挂载表确认它落在哪个 UFS 里。5.5 大量小文件场景下 Master 内存暴涨现象业务侧在 Alluxio 上跑日志清洗任务每小时生成数万个几 KB 的小文件运行两周后 Master 进程占满堆内存。原因Alluxio Master 的元数据全在内存中。每个文件块都有对应的 inode 和 block 元数据一百万个小文件就可能吃掉 Master 2~4GB 堆。相比 HDFS这一点没有本质区别但因为 Alluxio 还额外维护 UFS 状态映射压力更明显。解决写入路径上对小文件做合并下游读作业用小文件合并工具汇总后才落到 Alluxio。如果已存在大量小文件可以调大alluxio.user.block.size.bytes.default但这只对新文件生效。另一个实用方案是把小文件所在的目录用alluxio fs load --partial选择性加载避免一次性把海量小文件的元数据全部拉进 Master。6. 进阶把元数据放内存还不够试试这些配置与验证把集群跑稳之后值得做一轮定向优化。我比较推荐的三个方向分层存储、HDFS 联邦模式的 UFS 隔离、以及冷热数据迁移策略。分层存储不只是 SSDRAM 两级v2.9.4 支持给不同路径设置medium类型比如内存盘给高优先级的报表查询SSD 给普通 ETL。用alluxio fs setTier的交互可以动态调整文件的存储层不需要重启。验证优化效果时不要只盯着冒烟测试跑通就结束了。我在生产上用的标准是挑一个 2GB 以上的 Hive 表连续跑三遍同一个 Spark 查询第一遍冷读打底第二遍看查询时长是否下降 30% 以上第三遍看fsadmin report metrics的UFS bytes read有没有明显少于第一遍。如果三次都是相同的读取路径和耗时说明缓存根本没生效回到第 4 章的命中率指标排查。另一个值得配置的是 Alluxio 的和谐停机和恢复策略。在有大作业跑着的时候不要去重启 Worker先bin/alluxio-stop.sh worker停机再等作业释放文件句柄。我养成的习惯是每周在低峰期执行一次bin/alluxio fsadmin report summary记录关键数据然后对照 Web UI 的 Metrics 曲线主动发现 worker 堆内存增长是否过快、是否达到 80% 以上。与其等异常告警不如提前处理。如果你想让运维更省心还可以把 Master 的 metrics 通过alluxio.master.metrics.sink.prometheustrue配置直接暴露给 Prometheus 抓取配好 Grafana 看板后缓存命中率趋势、worker 容量水位这些信息一目了然。设置方式是修改alluxio-site.properties再重启 Master不会影响现有 worker 缓存。最后说一句我的个人体会Alluxio 的坑十个里有八个不是代码问题而是配置和环境问题。JDK 版本、权限、网络白名单、底层存储的 endpoint每一样都值得在部署前先做一轮自检清单。把这些基础打牢v2.9.4 的稳定程度才会真正体现出来。希望这次的实战拆解对你有帮助。本文还有配套的精品资源点击获取
返回列表