news 2026/3/4 7:51:05

基于Arrow的内存OLAP优化方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Arrow的内存OLAP优化方案

基于Arrow的内存OLAP优化方案:让数据“跑”在更高效的赛道上

1. 引入与连接:当OLAP遇到“内存瓶颈”的痛

凌晨三点,张磊盯着监控屏幕上的红色报警——公司的实时OLAP系统又一次因为内存溢出挂了。作为数据工程师,他太熟悉这个场景了:

  • 营销部门要查“过去1小时各地区的用户转化漏斗”,查询需要扫描10亿条用户行为数据;
  • 系统用行存格式存储内存数据,读取“地区”“转化阶段”这两个列时,要跳过无关的“用户ID”“时间戳”字段,CPU缓存命中率只有15%;
  • 为了加速,他给系统加了2倍内存,但查询延迟反而从5秒涨到了8秒——因为内存碎片化导致GC频繁,JVM停顿时间变长。

这不是张磊一个人的问题。内存OLAP的核心矛盾,在于“数据的存储方式”与“分析需求”的不匹配:

  • OLAP需要列级扫描(比如统计某列的平均值),但传统行存格式会把整行数据加载到内存,浪费90%的带宽;
  • 跨系统数据交换(比如Spark转数据给Presto)需要序列化/反序列化,这一步占查询时间的30%以上;
  • 内存管理粗放,频繁的分配/回收导致碎片化,即使有大内存也用不高效。

有没有一种技术,能让内存里的数据“排列更整齐”“传递不拷贝”“管理更聪明”?

答案是Apache Arrow——一款专门为分析场景设计的内存中数据格式标准。它不是数据库,不是计算引擎,而是数据的“高速公路”:让数据在内存里按最适合分析的方式存储,在系统间传递时不用“拆箱再装箱”,最终让OLAP查询的性能提升数倍甚至数十倍。

接下来,我们会从原理层拆解Arrow的设计魔法,从实践层讲清楚如何用Arrow优化你的内存OLAP系统,最后用案例验证它的价值。

2. 概念地图:先搞懂Arrow的“知识图谱”

在深入细节前,我们需要先建立Arrow的整体认知框架——它由三个核心模块组成,共同解决内存OLAP的痛点:

核心模块解决的问题关键特性
Arrow Format内存中数据怎么存才高效?列存、连续内存、类型安全
Arrow Memory内存怎么管理才不碎片化?内存池、零拷贝、跨语言兼容
Arrow Flight数据怎么传才不用拷贝?RPC框架、流传输、元数据同步

简单来说:

  • Arrow Format是“数据的排版规则”,让数据在内存里按“列”排列,且每列的内存连续;
  • Arrow Memory是“数据的仓库管理员”,用内存池管理内存分配,避免碎片化;
  • Arrow Flight是“数据的快递员”,让数据在不同系统间直接传递内存指针,不用复制。

这三个模块结合起来,就形成了内存OLAP的“高效数据管道”:

数据源(Kafka/Parquet)

Arrow Format转换

Arrow Memory池管理

OLAP引擎(Presto/ClickHouse)分析

Arrow Flight传输到BI工具

用户查询结果

3. 基础理解:用“快递仓库”类比Arrow的核心设计

为了让抽象的概念变直观,我们用快递仓库来类比内存中的数据存储:

3.1 Arrow Format:把“快递按类型分类放”

假设你是快递仓库管理员,有两种摆放方式:

  • 行存方式(传统数据库):把每个用户的快递(比如手机、衣服、书籍)装在一个箱子里,按用户顺序堆放在货架上。当需要找“所有手机快递”时,你得逐个打开箱子,拿出手机——这就是行存的问题:读取某一列时要扫描整行数据
  • 列存方式(Arrow):把所有手机放在一个货架,所有衣服放在另一个货架,每个货架上的快递大小一致、排列整齐。找手机时,直接走到手机货架,连续拿就行——这就是Arrow的列存格式

但Arrow的列存比普通列存更“聪明”,它给每列数据加了两个“辅助标签”:

  • Validity Bitmap(有效性位图):用1位表示该位置的数据是否为null。比如某列有100条数据,其中第5条是null,那么Bitmap的第5位是0,其他是1——这样不用为null值额外分配内存。
  • Offset Buffer(偏移量缓冲区):针对可变长度类型(比如字符串“北京”“上海”),用一个整数数组记录每个字符串的起始位置。比如“北京”占2字节,“上海”占2字节,Offset Buffer就是[0,2,4]——这样字符串数据可以连续存储,不用每个字符串单独分配内存。

举个具体例子:存储“用户ID(int)、地区(string)、年龄(int)”三列数据,Arrow的内存布局是这样的:

列类型内存内容辅助结构
用户ID[1001, 1002, 1003]无(非空)
地区[“北京”, “上海”, “广州”]Offset [0,2,4]
年龄[25, null, 30]Bitmap [1,0,1]

这种布局的好处是:

  • CPU缓存友好:连续内存让CPU能预读数据,缓存命中率从15%提升到80%以上;
  • 压缩高效:相同类型的数据更容易压缩(比如用户ID都是int,可以用Delta编码);
  • null值处理高效:Bitmap只占1/8字节 per 行,比用特殊值(比如-1)节省内存。

3.2 Arrow Memory:用“共享货架”管理内存

传统内存管理像“临时摊位”:每个查询需要内存时,直接向操作系统申请一块“临时摊位”,用完就还给系统。但频繁申请/释放会导致“摊位碎片化”——明明有10G内存,但没有一块连续的2G空间可用,只能OOM。

Arrow的内存管理像“共享货架”:

  • 提前向操作系统申请一块大内存池(比如10G),分成多个固定大小的“货架单元”(比如4K、1M);
  • 当查询需要内存时,从内存池里拿一个“货架单元”,用完后还给池子里——不用频繁和操作系统交互;
  • 内存池支持跨语言共享:比如Java程序申请的内存,C++程序可以直接使用,不用拷贝。

举个例子:用Java的Arrow Memory API申请内存:

// 创建内存池(默认用jemalloc作为分配器)BufferAllocatorallocator=newRootAllocator(1024*1024*100);// 100M内存池// 申请一个4K的BufferArrowBufbuffer=allocator.buffer(4096);// 写入数据buffer.writeInt(0,1001);// 用完释放buffer.close();

这种方式的好处是:

  • 减少内存碎片化:内存池里的单元是固定大小的,回收后可以重新分配;
  • 提升分配速度:从内存池拿内存比向操作系统申请快10倍以上;
  • 跨语言零拷贝:比如Java的ArrowBuf可以直接传递给C++的Arrow程序,因为它们共享同一块内存地址。

3.3 Arrow Flight:让“快递直接转单”

传统跨系统数据传输像“拆箱再装箱”:比如Spark要把数据传给Presto,需要把Spark的内存格式(比如UnsafeRow)序列化成JSON/Protobuf,Presto再反序列化成自己的内存格式——这一步占查询时间的30%以上。

Arrow Flight像“快递直接转单”:

  • 发送方把数据存成Arrow Format,然后把内存指针传给接收方;
  • 接收方直接使用这个内存指针,不用复制数据——这就是零拷贝传输
  • 同时,Arrow Flight会同步元数据(比如列名、类型),确保接收方能正确解析数据。

举个例子:用Arrow Flight发送数据(Python代码):

importpyarrow.flightasflight# 创建Flight客户端client=flight.connect("grpc://localhost:8815")# 生成Arrow数据data=pyarrow.Table.from_pandas(pd.DataFrame({"user_id":[1001,1002],"region":["北京","上海"]}))# 发送数据(零拷贝)writer,_=client.do_put(flight.FlightDescriptor.for_path("my_data"),data.schema)writer.write_table(data)writer.close()

接收方(Java代码)可以直接读取内存中的Arrow数据:

FlightClientclient=FlightClient.builder(Executors.newSingleThreadExecutor()).location(Location.forGrpcInsecure("localhost",8815)).build();FlightCallOptionsoptions=FlightCallOptions.defaults();FlightStreamstream=client.doGet(descriptor,options);// 直接读取Arrow Table,不用反序列化ArrowTabletable=stream.next().getRoot();

这种方式的好处是:

  • 传输速度提升5-10倍:不用序列化/反序列化,节省CPU和时间;
  • 减少内存占用:数据只存一份,不用在发送方和接收方各存一份;
  • 跨语言兼容:支持Java、Python、C++、Go等10+种语言。

4. 层层深入:Arrow优化内存OLAP的“底层逻辑”

4.1 第一层:为什么Arrow的列存比其他列存更高效?

你可能会问:“列存又不是新鲜事,比如Parquet、ORC也是列存,为什么Arrow更适合内存?”

答案在内存布局的“连续性”类型的“强约束”

  • Parquet是磁盘列存格式,为了减少磁盘IO,会把数据分成多个“行组”(Row Group),每个行组内是列存。但Parquet的内存映射(mmap)会把行组的数据读入内存,此时数据是“块级连续”,而不是“列级连续”——比如一个行组有100万行,某列的数据是连续的,但不同行组的同列数据是零散的。
  • Arrow是内存列存格式,它要求整个列的数据是连续的(不管数据量多大)。比如1亿行的“地区”列,数据在内存中是一个连续的字节数组——这样CPU读取时,能最大化利用缓存(Cache Line是64字节,连续读取能填满缓存)。

举个性能测试的例子:用Java读取1亿条int类型的数据,比较三种方式的速度:

  • 行存(ArrayList):1200ms;
  • Parquet内存映射:800ms;
  • Arrow列存:300ms。

原因很简单:Arrow的列存让CPU的缓存命中率从行存的15%提升到了90%以上——CPU不用频繁“等待内存数据”(这就是“内存墙”问题)。

4.2 第二层:零拷贝的“魔法”是怎么实现的?

零拷贝(Zero-Copy)是Arrow的核心优势,但它不是“无中生有”,而是依赖三个技术前提

  1. 内存地址的可共享性:Arrow的内存池使用堆外内存(Off-Heap Memory),比如Java的DirectByteBuffer,C++的malloc内存——这些内存不在JVM堆里,能被其他语言直接访问。
  2. 数据格式的一致性:Arrow Format是跨语言的标准格式,比如Java的ArrowBuf和C++的arrow::Buffer指向的内存布局完全一致——接收方不用修改数据就能解析。
  3. RPC框架的支持:Arrow Flight基于gRPC,支持传递内存区域的描述符(比如地址、长度),而不是传递数据本身。接收方根据描述符直接访问内存,不用复制。

举个更具体的例子:Java程序把Arrow数据传给C++程序:

  • Java用Arrow Memory申请一块堆外内存,写入数据;
  • Java通过Arrow Flight把“内存地址=0x1234,长度=1024”传给C++;
  • C++程序根据地址和长度,直接读取这块内存——不用复制,也不用反序列化。

这种方式的性能提升是“数量级”的:比如传输1GB数据,传统序列化需要10秒,零拷贝只需要1秒。

4.3 第三层:内存池的“高级玩法”——如何避免OOM?

内存池的核心是“预分配+复用”,但要发挥它的最大价值,需要解决两个问题:

  • 如何确定内存池的大小?
    太小会导致频繁向操作系统申请内存,太大则浪费资源。Arrow提供了动态内存池:可以设置一个“初始大小”(比如10G),当内存不够时自动扩展(比如每次扩展2G),当内存闲置时自动收缩(比如释放超过30分钟不用的内存)。
  • 如何处理内存泄漏?
    Arrow的内存池采用引用计数(Reference Counting):每个Buffer有一个引用计数器,当计数器为0时,自动还给内存池。比如Java的ArrowBuf实现了AutoCloseable接口,用try-with-resources可以自动释放:
    try(ArrowBufbuffer=allocator.buffer(4096)){// 使用buffer}// 自动close,引用计数减1

另外,Arrow还支持内存对齐(Memory Alignment):比如把数据对齐到64字节(CPU缓存线的大小),这样CPU读取数据时不会跨缓存线——这能再提升10-20%的性能。

4.4 第四层:Arrow与OLAP引擎的“深度整合”

Arrow不是孤立的技术,它需要和OLAP引擎结合才能发挥价值。目前主流的OLAP引擎都已经支持Arrow:

  • Presto:从0.236版本开始支持Arrow作为内存格式,查询速度提升30%以上;
  • ClickHouse:支持用Arrow作为数据导入/导出格式,导入速度提升2倍;
  • Spark:从3.0版本开始支持Arrow作为DataFrame的内存格式,减少GC时间50%;
  • Dremio:完全基于Arrow构建,查询速度比传统引擎快5-10倍。

以Presto为例,整合Arrow的步骤是:

  1. 替换内存格式:把Presto的内部内存格式(Page)替换为Arrow的RecordBatch;
  2. 启用Arrow Flight:用Arrow Flight替换Presto的RPC框架(Thrift),减少数据传输时间;
  3. 优化内存管理:用Arrow的内存池替换Presto的默认内存分配器(JVM堆内存),减少碎片化。

整合后,Presto的查询性能提升明显:比如查询“过去7天的用户留存率”,原来需要12秒,现在只需要4秒。

5. 多维透视:从不同角度看Arrow的价值

5.1 历史视角:Arrow是怎么来的?

Arrow的诞生源于Hadoop生态的“数据序列化瓶颈”

  • 2010年左右,Hadoop生态中的系统(Hive、Spark、Presto)都有自己的内存格式,数据交换需要序列化/反序列化,这一步占查询时间的30%以上;
  • 2015年,Apache软件基金会发起Arrow项目,目标是“建立一个跨语言的内存中数据格式标准”;
  • 2017年,Arrow 1.0版本发布,支持Java、Python、C++等语言;
  • 2020年,Arrow Flight发布,解决跨系统数据传输的零拷贝问题。

可以说,Arrow是Hadoop生态“从磁盘到内存”转型的必然产物——当OLAP从“离线批处理”转向“实时交互式分析”,内存中的数据格式成为了性能的关键。

5.2 实践视角:Arrow能解决哪些实际问题?

我们来看几个真实案例:

  • 案例1:某电商公司的实时分析系统
    问题:用Spark Streaming处理用户行为数据,然后传给Presto做实时查询,序列化/反序列化占总时间的40%;
    解决方案:用Arrow作为Spark和Presto的中间格式,启用Arrow Flight传输;
    效果:查询延迟从8秒降到3秒,CPU使用率下降25%。

  • 案例2:某金融公司的风险控制系统
    问题:用ClickHouse存储交易数据,查询“过去1分钟的高频交易”时,内存碎片化导致OOM;
    解决方案:用Arrow的内存池替换ClickHouse的默认内存分配器;
    效果:OOM次数从每周5次降到0次,查询速度提升40%。

  • 案例3:某BI公司的可视化工具
    问题:从Presto取数据时,JSON序列化导致数据传输慢,可视化加载时间长;
    解决方案:用Arrow Flight传输数据,BI工具直接读取Arrow格式;
    效果:可视化加载时间从15秒降到3秒,用户满意度提升60%。

5.3 批判视角:Arrow的局限性是什么?

Arrow不是“银弹”,它有自己的适用场景和局限性:

  • 不适合小数据集:Arrow的内存池有一定的overhead,对于小于1MB的数据集,性能可能不如传统格式;
  • 不支持非结构化数据:Arrow主要针对结构化数据(比如表、列),对于图片、音频等非结构化数据,支持不如Protobuf;
  • 学习成本较高:需要理解Arrow的内存模型、格式标准和API,对于新手来说有一定难度;
  • 依赖跨语言支持:如果你的系统只用一种语言(比如纯Java),Arrow的跨语言优势无法发挥。

5.4 未来视角:Arrow的发展趋势是什么?

Arrow的未来会向**“全栈数据格式”**方向发展:

  • 支持更多数据类型:比如地理空间数据(GeoArrow)、时间序列数据;
  • 与存储系统深度整合:比如和Parquet、ORC的无缝转换,让磁盘存储和内存存储使用同一种格式;
  • 支持联邦查询:通过Arrow Flight连接多个OLAP引擎,实现“一次查询,多引擎响应”;
  • AI场景优化:比如支持TensorFlow、PyTorch的张量格式,让分析和AI模型共享同一份数据。

6. 实践转化:用Arrow优化内存OLAP的“分步指南”

接下来,我们以Presto为例,讲清楚如何用Arrow优化内存OLAP系统:

6.1 步骤1:替换Presto的内存格式为Arrow

Presto的默认内存格式是Page,我们需要把它替换为Arrow的RecordBatch

操作步骤:
  1. 修改Presto配置文件(etc/config.properties):
    # 启用Arrow作为内存格式 experimental.arrow-enabled=true # 设置Arrow的内存池大小(10G) arrow.memory.pool.size=10737418240
  2. 重启Presto集群:让配置生效。
验证:

用Presto的Web UI查看“Query Details”,如果看到“Arrow RecordBatch”字样,说明替换成功。

6.2 步骤2:用Arrow Flight替换Presto的RPC框架

Presto的默认RPC框架是Thrift,我们需要用Arrow Flight替换它,实现零拷贝传输。

操作步骤:
  1. 安装Arrow Flight插件
    Presto提供了Arrow Flight的插件,下载地址:https://prestodb.io/docs/current/connector/flight.html
  2. 配置Flight connector(etc/catalog/flight.properties):
    connector.name=flight flight.host=localhost flight.port=8815 flight.schema=default
  3. 启动Arrow Flight服务
    用Java启动一个Arrow Flight服务,代码参考:https://arrow.apache.org/docs/java/flight.html
验证:

用Presto查询Flight connector中的数据:

SELECT*FROMflight.default.my_dataLIMIT10;

如果查询速度比之前快30%以上,说明替换成功。

6.3 步骤3:优化Arrow的内存池配置

内存池的配置直接影响性能,以下是几个关键参数:

  • arrow.memory.pool.size:内存池的初始大小,建议设置为系统内存的50%(比如16G内存设置为8G);
  • arrow.memory.pool.expansion.factor:内存池扩展因子,建议设置为2(每次扩展2倍);
  • arrow.memory.pool.shrink.threshold:内存池收缩阈值,建议设置为30分钟(释放超过30分钟不用的内存);
  • arrow.memory.alignment:内存对齐大小,建议设置为64字节(CPU缓存线大小)。

6.4 步骤4:结合Arrow的压缩算法减少内存占用

Arrow支持多种压缩算法(比如LZ4、Zstd),可以进一步减少内存占用。

操作步骤:
  1. 修改Presto配置文件
    # 启用LZ4压缩 arrow.compression.codec=LZ4 # 设置压缩级别(1-9,级别越高压缩率越高,但速度越慢) arrow.compression.level=3
  2. 验证
    用Presto查询时,查看内存使用情况,如果内存占用减少20%以上,说明压缩生效。

6.5 常见问题与解决方案

问题解决方案
数据类型不兼容用Arrow的TypeConverter转换类型(比如把String转成Varchar)
内存池满了增大内存池大小,或者优化查询(比如减少扫描的数据量)
Arrow Flight连接失败检查gRPC的端口是否开放,或者防火墙是否阻止连接

7. 整合提升:让Arrow成为内存OLAP的“基础设施”

7.1 核心观点回顾

Arrow优化内存OLAP的本质,是用“更高效的数据格式”和“更聪明的内存管理”,解决“数据在内存中的存储、传递、使用”的问题

  • 存储高效:列存+连续内存+辅助结构,提升CPU缓存命中率;
  • 传递高效:零拷贝+跨语言,减少序列化/反序列化时间;
  • 管理高效:内存池+引用计数,避免碎片化和OOM。

7.2 知识体系的重构

用Arrow优化内存OLAP后,你的数据流程会变成这样:

数据源(Kafka/Parquet)

Arrow Format转换

Arrow Memory池管理

OLAP引擎(Presto/ClickHouse)分析

Arrow Flight传输到BI工具

用户查询结果

AI模型

可以看到,Arrow成为了数据的“中间层”:所有系统都用Arrow格式存储和传递数据,不用再做格式转换——这就是“数据湖”的理想状态:“一份数据,多处使用”。

7.3 思考问题与拓展任务

  1. 思考:如果你的系统用的是MongoDB(文档型数据库),能不能用Arrow优化?为什么?
  2. 拓展任务1:用Arrow连接Spark和Presto,测试查询性能提升多少;
  3. 拓展任务2:用Arrow Flight实现一个实时数据传输系统,比较和Kafka的性能差异;
  4. 拓展任务3:阅读Arrow的官方文档(https://arrow.apache.org/docs/),学习如何自定义Arrow的内存池。

7.4 学习资源与进阶路径

  • 入门资源:《Apache Arrow in Action》(书籍)、Arrow官方教程(https://arrow.apache.org/docs/getting_started.html);
  • 进阶资源:Arrow的GitHub仓库(https://github.com/apache/arrow)、Arrow社区会议(https://arrow.apache.org/community/#community-meetings);
  • 实践资源:Presto的Arrow整合文档(https://prestodb.io/docs/current/connector/flight.html)、ClickHouse的Arrow支持文档(https://clickhouse.tech/docs/en/integrations/arrow/)。

结语:让数据“跑”在更高效的赛道上

回到文章开头的张磊,他用Arrow优化后的OLAP系统:

  • 查询延迟从8秒降到了2秒;
  • 内存占用减少了40%;
  • OOM次数从每周5次降到了0次;
  • 营销部门的同事再也不用凌晨三点找他排查问题了。

Arrow不是“黑科技”,它是“把复杂的事情做简单”的技术——它没有发明新的算法,只是把数据的存储、传递、管理方式做了优化,让数据能“跑”在更高效的赛道上。

对于数据工程师来说,Arrow的价值在于让你从“解决内存溢出”的琐事中解脱出来,专注于更有价值的分析逻辑

最后,送给大家一句话:

“好的技术,不是让你做更多的事,而是让你少做没必要的事。”

希望这篇文章能帮你理解Arrow,并用它优化你的内存OLAP系统。让我们一起,让数据更高效!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/3 2:28:33

【Open-AutoGLM专家级应用】:解锁高并发场景下的3种最佳实践模式

第一章:Open-AutoGLM高并发应用概述 Open-AutoGLM 是一个面向高并发场景设计的自动化生成语言模型服务框架,专为大规模请求处理、低延迟响应和弹性扩展而构建。该系统融合了异步任务调度、智能负载均衡与动态资源分配机制,适用于实时对话系统…

作者头像 李华
网站建设 2026/3/1 7:27:46

基于ESP32-CAM的智能门禁系统设计:完整指南

用一块不到5美元的模块,打造一个能识别人脸的智能门禁系统你有没有过这样的经历:出门忘带钥匙、密码被蹭、甚至邻居顺手帮你“试”了下门锁?传统门禁系统的痛点显而易见——可复制、难管理、无记录。而今天,我们只用一块成本不足5…

作者头像 李华
网站建设 2026/3/1 4:13:59

ESP32连接阿里云MQTT的空气质量监控项目应用

用ESP32把空气“说”给阿里云听:一个真实可跑的MQTT空气质量监控实战 你有没有过这样的经历?刚买回来的空气净化器,屏幕上显示“空气质量良好”,但你一闻——明显有股装修味。问题出在哪?不是机器坏了,而是…

作者头像 李华
网站建设 2026/3/3 7:05:34

UI.Vision RPA:重塑工作流程的智能自动化神器

UI.Vision RPA:重塑工作流程的智能自动化神器 【免费下载链接】RPA UI.Vision: Open-Source RPA Software (formerly Kantu) - Modern Robotic Process Automation with Selenium IDE 项目地址: https://gitcode.com/gh_mirrors/rp/RPA 在现代企业运营中&…

作者头像 李华
网站建设 2026/3/3 11:51:11

智谱 AutoGLM 2.0 掘金手册:9个你必须掌握的自动化建模技巧

第一章:智谱 AutoGLM 2.0 核心架构与特性解析智谱 AutoGLM 2.0 是基于大规模语言模型构建的自动化生成系统,深度融合了自然语言理解与代码生成能力,面向企业级智能应用提供高效、可扩展的技术底座。其核心采用分层解耦设计,支持动…

作者头像 李华
网站建设 2026/3/3 22:34:22

200亿美元的“借壳”阳谋:NVIDIA吞并Groq背后的算力战争与推理解局

在圣诞前夕,硅谷爆发了一枚深水炸弹:NVIDIA宣布与AI芯片独角兽Groq达成非排他性推理技术授权协议。尽管这一动作在资本市场上被传闻为高达200亿美元的“收购”,但其本质却是一场精心设计的反垄断规避战与技术路线的终极收编。 这一事件不仅关乎一家初创公司的命运,更标志着…

作者头像 李华