GATK HaplotypeCaller 内存与并行配置的性能观察记录
本文最后更新于 54 天前,如有失效请评论区留言。

实验背景

在进行 GATK HaplotypeCaller 变异检测时,对不同的内存配置和并行任务数进行了性能测试。本次实验基于一批全基因组测序数据的处理,旨在找到最优的资源分配策略。

gatk-4.6.2.0

实验配置

-------------------- A Bench.sh Script By Teddysun -------------------
 Version            : v2025-05-08
 Usage              : wget -qO- bench.sh | bash
----------------------------------------------------------------------
 CPU Model          : Intel(R) Core(TM) i9-14900KF
 CPU Cores          : 32 @ 800.000 MHz
 CPU Cache          : 36864 KB
 AES-NI             : ✓ Enabled
 VM-x/AMD-V         : ✓ Enabled
 Total Disk         : 3.6 TB (2.0 TB Used)
 Total Mem          : 125.6 GB (8.5 GB Used)
 Total Swap         : 8.0 GB (688.9 MB Used)
 System uptime      : 9 days, 21 hour 3 min
 Load average       : 2.01, 1.50, 1.26
 OS                 : Ubuntu 24.04.3 LTS
 Arch               : x86_64 (64 Bit)
 Kernel             : 6.14.0-36-generic
 TCP CC             : cubic
 Virtualization     : Dedicated
 IPv4/IPv6          : ✗ Offline / ✗ Offline
 Region             : No ISP detected
----------------------------------------------------------------------
 I/O Speed(1st run) : 2.6 GB/s
 I/O Speed(2nd run) : 2.9 GB/s
 I/O Speed(3rd run) : 2.9 GB/s
 I/O Speed(average) : 2867.2 MB/s
----------------------------------------------------------------------

初始配置

  • 参数设置: -Xmx20g -Xms8g (最大堆内存20GB,初始堆内存8GB)
  • 并行任务数: 20个
  • CPU核心: 每任务分配4个核心(由 --native-pair-hmm-threads 4 控制)
  • 任务资源占用: 每个任务实际内存占用约5%-6%
  • 完成时间: 约26-28小时完成整批任务

后续配置(单个任务重跑)

  • 参数设置: -Xmx40g -Xms20g (最大堆内存40GB,初始堆内存20GB)
  • 并行任务数: 仅1个(重跑失败的任务)
  • CPU核心: 保持4个核心
  • 任务资源占用: 实际内存占用约10%-12%
  • 完成时间: 约6小时完成单个任务

性能对比分析

时间效率对比

配置每任务平均时间总处理时间效率提升
20g/8g × 20并行(已实际运行)~27小时/任务~27小时/20任务基准
40g/20g × 1任务(已实际运行)6小时/任务6小时/任务单任务约4.5倍
40g/20g × 10任务(推测)6小时/任务6小时/10任务总体约2倍

实际效率会增加一倍,因为相当于12个小时能跑完20个(单任务内存使用量加倍,并行任务数减半,但速度快了约4倍。即1/2*4=2)。

内存使用效率

  • 低内存配置: 20GB最大内存,实际使用(5%-6% * 总128G)
  • 高内存配置: 40GB最大内存,实际使用(10%-12% * 总128G)

关键发现

  1. 内存增加显著提升速度: 内存加倍(20g/8g →40g/20g)使单任务运行时间从约27小时减少到6小时
  2. 内存使用率较低: 即使在40g/20g配置下,实际内存使用率也仅10%-12% * 总128G(12g左右),表明HaplotypeCaller有进一步优化空间
  3. 并行效率瓶颈: 20个并行任务时,系统可能存在I/O竞争或CPU调度开销

推论与假设

基于观察数据,可以提出以下假设:

假设1:适度增加内存、减少并行数可能更高效

如果采用以下配置:

  • 参数设置: -Xmx40g -Xms20g
  • 并行任务数: 10个(而非20个)
  • 预期效果:
  • 每个任务运行时间可能从27小时减少到6小时左右(非线性的,考虑到cpu和内存占用率都较高时,可能出现性能下降)
  • 总处理时间可能从减半(例如200个样品,原本20g/8g × 20并行两周,40g/20g × 10任务可能只需要一周)

假设2:资源竞争是关键限制因素

当20个任务并行时:

  • I/O竞争: 大量BAM/GVCF文件同时读写
  • CPU竞争: 虽然每任务4核心,但80个核心的总需求可能导致调度开销
  • 内存碎片: JVM垃圾回收可能更频繁

重要注意事项

⚠️ 单个任务的性能不代表并行性能

关键提示: 单个任务在6小时内完成是在系统资源非常充足的情况下实现的:

  • 没有其他并行任务竞争I/O
  • CPU核心完全专用于该任务
  • 内存分配无竞争
  • 临时文件I/O无干扰

当并行10个相同配置的任务时:

  1. I/O瓶颈: 10个任务同时读写BAM/GVCF文件会显著降低I/O性能
  2. CPU调度开销: 40个核心(10×4)的调度比4个核心复杂(实际是10×2,因为这一步每个任务最多使用不到3个线程,单任务在1.5-2.5线程)
  3. 内存竞争: 虽然每个任务分配40GB,但实际使用约12GB,总需求约12GB,仍在系统容量内
  4. 网络/存储延迟: 并行任务会增加共享存储的访问延迟

⚠️ 非线性的扩展性

GATK HaplotypeCaller的性能扩展不是线性的:

  • 从1个任务到10个任务,总时间不会减少到1/10
  • 从20GB内存到40GB内存,速度提升可能不是2倍
  • 最佳配置需要在资源利用和并行效率间平衡

推荐配置策略

基于本次实验,建议以下配置策略:

小规模集群(<128GB内存)

# 中等并行度,中等内存
-Xmx30g -Xms12g
并行任务数:8-12个
每任务线程:4个 

大规模集群(>256GB内存)

# 较高内存,适度并行
-Xmx40g -Xms20g
并行任务数:10-15个
每任务线程:4-6个

关键优化建议

  1. 监控实际使用: 使用 tophtopps 监控实际内存使用
  2. I/O优化: 将临时目录 (--tmp-dir) 设置为高性能存储(如SSD)
  3. 分批处理: 如果样本数超过50,考虑分批次运行
  4. 参数调优: 根据参考基因组大小调整 --max-reads-per-alignment-start

后续实验建议

为了更准确评估性能,建议进行控制实验:

  1. 固定样本测试: 使用相同5个样本,测试不同配置
  2. 资源监控: 记录CPU、内存、I/O使用率
  3. 缩放性测试: 测试1、5、10、15个并行任务的效率
  4. 内存阶梯测试: 测试20GB、30GB、40GB、50GB内存配置

结论

本次实验表明,GATK HaplotypeCaller 对内存配置敏感,适当增加内存可以显著减少运行时间。然而,单个任务的优化效果不能简单线性推广到并行环境

在资源充足的情况下,单任务-Xmx40g -Xms20g 配置显著优于 -Xmx20g -Xms8g。但并行多个任务时,需要综合考虑系统总资源、I/O瓶颈和调度开销。

最佳实践: 根据具体硬件配置和样本数量,通过小规模测试确定最优的内存和并行配置,再进行大规模处理。

最后附上两个代码

#!/bin/bash

# 设置目录和文件
input_bam_dir="/home/vensin/workspace/snpcalling_wild/4.picard_dedup/markdup"  # Picard去重后的BAM文件
output_dir="/home/data/5.gatk_haplotypecaller"
reference_genome="/home/vensin/workspace/snpcalling_wild/0.genome/SFZ.A.onlychr.fa"
gatk_path="/home/vensin/software/gatk-4.6.2.0/gatk" 

# 创建输出目录
mkdir -p "$output_dir/gvcf" "$output_dir/tmp" "$output_dir/logs" "$output_dir/metrics"

# 设置日志文件
log_file="$output_dir/haplotypecaller_processing.log"
echo "GATK HaplotypeCaller processing started at $(date)" > "$log_file"

# 检查输入BAM文件数量
echo "=== 检查输入BAM文件 ===" >> "$log_file"
bam_count=$(find "$input_bam_dir" -name '*.markdup.bam' | wc -l)
echo "找到 $bam_count 个标记重复的BAM文件" >> "$log_file"

if [ "$bam_count" -eq 0 ]; then
    echo "错误: 未找到标记重复的BAM文件" | tee -a "$log_file"
    exit 1
fi

# 显示前几个样本
echo "BAM文件示例:" >> "$log_file"
find "$input_bam_dir" -name '*.markdup.bam' | head -5 | xargs -n 1 basename >> "$log_file"

# 检查参考基因组和相关文件
echo "=== 检查参考基因组和相关文件 ===" >> "$log_file"
if [ ! -f "$reference_genome" ]; then
    echo "错误: 参考基因组文件不存在: $reference_genome" | tee -a "$log_file"
    exit 1
fi

if [ ! -f "$reference_genome.fai" ]; then
    echo "创建FASTA索引..." >> "$log_file"
    samtools faidx "$reference_genome" >> "$log_file" 2>&1
fi

dict_file="${reference_genome%.*}.dict"
if [ ! -f "$dict_file" ]; then
    echo "创建序列字典..." >> "$log_file"
    $gatk_path CreateSequenceDictionary -R "$reference_genome" -O "$dict_file" >> "$log_file" 2>&1
fi

# 创建HaplotypeCaller处理脚本
HC_SCRIPT="$output_dir/haplotypecaller_process_sample.sh"

cat > "$HC_SCRIPT" << 'EOF'
#!/bin/bash
# HaplotypeCaller样本处理脚本

input_bam=$1
output_dir=$2
reference_genome=$3
gatk_path=$4

base_name=$(basename "$input_bam" .markdup.bam)

# 样本特定的日志
sample_log="$output_dir/logs/${base_name}.log"

echo "开始HaplotypeCaller处理样本: $base_name" > "$sample_log"
echo "  输入BAM: $(basename "$input_bam")" >> "$sample_log"

# 创建样本特定的临时目录
sample_tmp_dir="$output_dir/tmp/$base_name"
mkdir -p "$sample_tmp_dir"

# 输出文件
gvcf_output="$output_dir/gvcf/${base_name}.g.vcf.gz"

# 执行 GATK HaplotypeCaller
echo "  运行HaplotypeCaller..." >> "$sample_log"
$gatk_path --java-options "-Xmx20g -Xms8g" HaplotypeCaller \
    -R "$reference_genome" \
    -I "$input_bam" \
    -O "$gvcf_output" \
    -ERC GVCF \
    --tmp-dir "$sample_tmp_dir" \
    --native-pair-hmm-threads 4 \
    --sample-ploidy 2 >> "$sample_log"

hc_exit_code=$?

if [ $hc_exit_code -eq 0 ]; then
    echo "  HaplotypeCaller 成功: $base_name" >> "$sample_log"
    
    # 验证输出的GVCF文件
    if [ -f "$gvcf_output" ]; then
        echo "  GVCF文件生成成功: $(basename "$gvcf_output")" >> "$sample_log"
        echo "$base_name: 成功" >> "$output_dir/haplotypecaller_processing.log"
        
        # 可选:收集一些基本统计
        echo "  收集GVCF统计..." >> "$sample_log"
        $gatk_path --java-options "-Xmx6g" CountVariants \
            -V "$gvcf_output" 2>> "$sample_log" | head -5 >> "$sample_log"
    else
        echo "  GVCF文件未生成: $base_name" >> "$sample_log"
        echo "$base_name: GVCF文件缺失" >> "$output_dir/haplotypecaller_processing.log"
        hc_exit_code=1
    fi
else
    echo "  HaplotypeCaller 失败 (退出码: $hc_exit_code): $base_name" >> "$sample_log"
    echo "$base_name: 失败" >> "$output_dir/haplotypecaller_processing.log"
fi

# 清理临时文件
rm -rf "$sample_tmp_dir"

echo "  处理完成: $base_name" >> "$sample_log"
exit $hc_exit_code
EOF

chmod +x "$HC_SCRIPT"

# 处理所有样本 - 使用 GNU Parallel 进行并行处理
echo "=== 开始GATK HaplotypeCaller处理 ===" >> "$log_file"

# 检查是否安装了 GNU Parallel
if command -v parallel >/dev/null 2>&1; then
    echo "使用 GNU Parallel 并行处理 ..." >> "$log_file"
    
    # 创建任务列表文件
    task_file="$output_dir/task_list.txt"
    find "$input_bam_dir" -name '*.markdup.bam' | sort > "$task_file"
    
    # 使用 parallel 并行处理
    cat "$task_file" | parallel -j 20 --joblog "$output_dir/parallel_jobs.log" \
        "$HC_SCRIPT {} $output_dir $reference_genome $gatk_path"
    
    # 计算成功数量
    successful_jobs=$(grep -c ": 成功" "$log_file" 2>/dev/null || echo 0)
else
    echo "GNU Parallel 未安装,使用串行处理..." >> "$log_file"
    
    # 串行处理
    success_count=0
    processed_count=0
    while IFS= read -r input_bam; do
        if [ -n "$input_bam" ]; then
            ((processed_count++))
            echo "处理样本 $processed_count/$bam_count: $(basename "$input_bam" .markdup.bam)" | tee -a "$log_file"
            if "$HC_SCRIPT" "$input_bam" "$output_dir" "$reference_genome" "$gatk_path"; then
                ((success_count++))
            fi
        fi
    done < <(find "$input_bam_dir" -name '*.markdup.bam' | sort)
    
    successful_jobs=$success_count
fi

# 清理临时脚本
rm -f "$HC_SCRIPT"

# 统计结果
successful_jobs=${successful_jobs:-0}
bam_count=${bam_count:-0}
failed_count=$((bam_count - successful_jobs))

echo "处理完成: $successful_jobs/$bam_count 个样本成功处理" >> "$log_file"

# 显示处理摘要
echo "=== HaplotypeCaller处理摘要 ===" >> "$log_file"
echo "成功: $successful_jobs" >> "$log_file"
echo "失败: $failed_count" >> "$log_file"

# 生成GVCF文件统计汇总
echo "=== GVCF文件统计汇总 ===" >> "$log_file"
for gvcf_file in "$output_dir/gvcf"/*.g.vcf.gz; do
    if [ -f "$gvcf_file" ]; then
        sample_name=$(basename "$gvcf_file" .g.vcf.gz)
        echo "样本: $sample_name" >> "$log_file"
        
        # 检查文件完整性
        if $gatk_path ValidateVariants -V "$gvcf_file" 2>/dev/null; then
            echo "  文件验证: 通过" >> "$log_file"
            
            # 获取变体数量统计
            variant_count=$($gatk_path --java-options "-Xmx6g" CountVariants -V "$gvcf_file" 2>/dev/null | awk '/CountVariants/{getline; print $1}')
            if [ -n "$variant_count" ]; then
                echo "  变体数量: $variant_count" >> "$log_file"
            fi
        else
            echo "  文件验证: 失败" >> "$log_file"
        fi
        echo "---" >> "$log_file"
    fi
done

echo "GATK HaplotypeCaller processing completed at $(date)" >> "$log_file"

# 在终端显示最终结果
echo "GATK HaplotypeCaller处理完成!"
echo "成功处理: $successful_jobs/$bam_count 个样本"
echo "详细日志: $log_file"
echo "GVCF文件: $output_dir/gvcf/"
echo "各样本日志: $output_dir/logs/"
#!/bin/bash

# 设置目录和文件
input_bam_dir="/home/data/XNF_1"  # Picard去重后的BAM文件
output_dir="/home/data/5.gatk_haplotypecaller_XNF_1"
reference_genome="/home/vensin/workspace/snpcalling_wild/0.genome/SFZ.A.onlychr.fa"
gatk_path="/home/vensin/software/gatk-4.6.2.0/gatk" 

# 创建输出目录
mkdir -p "$output_dir/gvcf" "$output_dir/tmp" "$output_dir/logs" "$output_dir/metrics"

# 设置日志文件
log_file="$output_dir/haplotypecaller_processing.log"
echo "GATK HaplotypeCaller processing started at $(date)" > "$log_file"

# 检查输入BAM文件数量
echo "=== 检查输入BAM文件 ===" >> "$log_file"
bam_count=$(find "$input_bam_dir" -name '*.markdup.bam' | wc -l)
echo "找到 $bam_count 个标记重复的BAM文件" >> "$log_file"

if [ "$bam_count" -eq 0 ]; then
    echo "错误: 未找到标记重复的BAM文件" | tee -a "$log_file"
    exit 1
fi

# 显示前几个样本
echo "BAM文件示例:" >> "$log_file"
find "$input_bam_dir" -name '*.markdup.bam' | head -5 | xargs -n 1 basename >> "$log_file"

# 检查参考基因组和相关文件
echo "=== 检查参考基因组和相关文件 ===" >> "$log_file"
if [ ! -f "$reference_genome" ]; then
    echo "错误: 参考基因组文件不存在: $reference_genome" | tee -a "$log_file"
    exit 1
fi

if [ ! -f "$reference_genome.fai" ]; then
    echo "创建FASTA索引..." >> "$log_file"
    samtools faidx "$reference_genome" >> "$log_file" 2>&1
fi

dict_file="${reference_genome%.*}.dict"
if [ ! -f "$dict_file" ]; then
    echo "创建序列字典..." >> "$log_file"
    $gatk_path CreateSequenceDictionary -R "$reference_genome" -O "$dict_file" >> "$log_file" 2>&1
fi

# 创建HaplotypeCaller处理脚本
HC_SCRIPT="$output_dir/haplotypecaller_process_sample.sh"

cat > "$HC_SCRIPT" << 'EOF'
#!/bin/bash
# HaplotypeCaller样本处理脚本

input_bam=$1
output_dir=$2
reference_genome=$3
gatk_path=$4

base_name=$(basename "$input_bam" .markdup.bam)

# 样本特定的日志
sample_log="$output_dir/logs/${base_name}.log"

echo "开始HaplotypeCaller处理样本: $base_name" > "$sample_log"
echo "  输入BAM: $(basename "$input_bam")" >> "$sample_log"

# 创建样本特定的临时目录
sample_tmp_dir="$output_dir/tmp/$base_name"
mkdir -p "$sample_tmp_dir"

# 输出文件
gvcf_output="$output_dir/gvcf/${base_name}.g.vcf.gz"

# 执行 GATK HaplotypeCaller
echo "  运行HaplotypeCaller..." >> "$sample_log"
$gatk_path --java-options "-Xmx40g -Xms20g" HaplotypeCaller \
    -R "$reference_genome" \
    -I "$input_bam" \
    -O "$gvcf_output" \
    -ERC GVCF \
    --tmp-dir "$sample_tmp_dir" \
    --native-pair-hmm-threads 8 \
    --sample-ploidy 2 >> "$sample_log"

hc_exit_code=$?

if [ $hc_exit_code -eq 0 ]; then
    echo "  HaplotypeCaller 成功: $base_name" >> "$sample_log"
    
    # 验证输出的GVCF文件
    if [ -f "$gvcf_output" ]; then
        echo "  GVCF文件生成成功: $(basename "$gvcf_output")" >> "$sample_log"
        echo "$base_name: 成功" >> "$output_dir/haplotypecaller_processing.log"
        
        # 可选:收集一些基本统计
        echo "  收集GVCF统计..." >> "$sample_log"
        $gatk_path --java-options "-Xmx6g" CountVariants \
            -V "$gvcf_output" 2>> "$sample_log" | head -5 >> "$sample_log"
    else
        echo "  GVCF文件未生成: $base_name" >> "$sample_log"
        echo "$base_name: GVCF文件缺失" >> "$output_dir/haplotypecaller_processing.log"
        hc_exit_code=1
    fi
else
    echo "  HaplotypeCaller 失败 (退出码: $hc_exit_code): $base_name" >> "$sample_log"
    echo "$base_name: 失败" >> "$output_dir/haplotypecaller_processing.log"
fi

# 清理临时文件
rm -rf "$sample_tmp_dir"

echo "  处理完成: $base_name" >> "$sample_log"
exit $hc_exit_code
EOF

chmod +x "$HC_SCRIPT"

# 处理所有样本 - 使用 GNU Parallel 进行并行处理
echo "=== 开始GATK HaplotypeCaller处理 ===" >> "$log_file"

# 检查是否安装了 GNU Parallel
if command -v parallel >/dev/null 2>&1; then
    echo "使用 GNU Parallel 并行处理 ..." >> "$log_file"
    
    # 创建任务列表文件
    task_file="$output_dir/task_list.txt"
    find "$input_bam_dir" -name '*.markdup.bam' | sort > "$task_file"
    
    # 使用 parallel 并行处理
    cat "$task_file" | parallel -j 15 --joblog "$output_dir/parallel_jobs.log" \
        "$HC_SCRIPT {} $output_dir $reference_genome $gatk_path"
    
    # 计算成功数量
    successful_jobs=$(grep -c ": 成功" "$log_file" 2>/dev/null || echo 0)
else
    echo "GNU Parallel 未安装,使用串行处理..." >> "$log_file"
    
    # 串行处理
    success_count=0
    processed_count=0
    while IFS= read -r input_bam; do
        if [ -n "$input_bam" ]; then
            ((processed_count++))
            echo "处理样本 $processed_count/$bam_count: $(basename "$input_bam" .markdup.bam)" | tee -a "$log_file"
            if "$HC_SCRIPT" "$input_bam" "$output_dir" "$reference_genome" "$gatk_path"; then
                ((success_count++))
            fi
        fi
    done < <(find "$input_bam_dir" -name '*.markdup.bam' | sort)
    
    successful_jobs=$success_count
fi

# 清理临时脚本
rm -f "$HC_SCRIPT"

# 统计结果
successful_jobs=${successful_jobs:-0}
bam_count=${bam_count:-0}
failed_count=$((bam_count - successful_jobs))

echo "处理完成: $successful_jobs/$bam_count 个样本成功处理" >> "$log_file"

# 显示处理摘要
echo "=== HaplotypeCaller处理摘要 ===" >> "$log_file"
echo "成功: $successful_jobs" >> "$log_file"
echo "失败: $failed_count" >> "$log_file"

# 生成GVCF文件统计汇总
echo "=== GVCF文件统计汇总 ===" >> "$log_file"
for gvcf_file in "$output_dir/gvcf"/*.g.vcf.gz; do
    if [ -f "$gvcf_file" ]; then
        sample_name=$(basename "$gvcf_file" .g.vcf.gz)
        echo "样本: $sample_name" >> "$log_file"
        
        # 检查文件完整性
        if $gatk_path ValidateVariants -V "$gvcf_file" 2>/dev/null; then
            echo "  文件验证: 通过" >> "$log_file"
            
            # 获取变体数量统计
            variant_count=$($gatk_path --java-options "-Xmx6g" CountVariants -V "$gvcf_file" 2>/dev/null | awk '/CountVariants/{getline; print $1}')
            if [ -n "$variant_count" ]; then
                echo "  变体数量: $variant_count" >> "$log_file"
            fi
        else
            echo "  文件验证: 失败" >> "$log_file"
        fi
        echo "---" >> "$log_file"
    fi
done

echo "GATK HaplotypeCaller processing completed at $(date)" >> "$log_file"

# 在终端显示最终结果
echo "GATK HaplotypeCaller处理完成!"
echo "成功处理: $successful_jobs/$bam_count 个样本"
echo "详细日志: $log_file"
echo "GVCF文件: $output_dir/gvcf/"
echo "各样本日志: $output_dir/logs/"

记录时间:2025年12月8日
数据规模:平均约22×全基因组重测序数据
硬件环境:高性能计算集群
记录目的:供后续分析参考,优化GATK流程资源配置

版权声明:除特殊说明,博客文章均为Vensin原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。 如有需要,请至学习地图系统学习本博客的教程。 | 博客订阅:RSS | 广告招租:留言板 | 博客 |
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇
# # #