麒麟v10系统部署 Redis 7.4.7(支持单机/主从/哨兵模式)

麒麟v10系统部署 Redis 7.4.7(支持单机/主从/哨兵模式)

📋 概述

本安装脚本用于在 Kylin Linux Advanced Server V10 (ARM64) 系统上部署 Redis 7.4.7,支持三种部署模式:
单机模式 (Standalone):独立运行的 Redis 实例
主从模式 (Replication):一主一从的高可用架构
哨兵模式 (Sentinel):主从架构 + 自动故障转移

脚本放到了末尾,自行验证。

🖥️ 环境信息

  • 操作系统: Kylin Linux Advanced Server V10 (Halberd)
  • 架构: aarch64 (ARM64)
  • 内核版本: 4.19.90-89.23.v2401.ky10.aarch64
  • Redis 版本: 7.4.7
  • Jemalloc 版本: 5.3.0(源码编译)

🎯 部署模式选择

特性 单机模式 主从模式 哨兵模式
适用场景 开发、测试 生产环境 生产环境(推荐)
服务器数量 1 台 2 台(1 主 1 从) 2 台 Redis(三台也行自行修改SLAVE_IP) + 3 台哨兵
数据冗余 ❌ 无 ✅ 主从同步 ✅ 主从同步
读写分离 ❌ 不支持 ✅ 支持 ✅ 支持
自动故障转移 ❌ 不支持 ❌ 需手动切换 ✅ 自动切换
配置复杂度 简单 中等 较复杂
IP 要求 无限制 需配置主从 IP 需配置主从 + 哨兵 IP

📍 节点信息示例

主从模式

  • 主节点: 10.202.17.12
  • 从节点: 10.202.17.13

哨兵模式

  • 主节点: 10.202.17.12
  • 从节点: 10.202.17.13,10.202.17.14(在SLAVE_IP这里切换ip就行)
  • 哨兵节点: 10.202.17.12, 10.202.17.13, 10.202.17.14(可与 Redis 同机部署)

✨ 功能特性

多种部署模式
– 单机模式:适用于开发、测试或小规模生产环境
– 主从模式:适用于需要高可用的生产环境
– 哨兵模式:适用于需要自动故障转移的生产环境

智能检测
– 自动检测系统环境和架构
– 根据配置自动识别部署模式
– 主从/哨兵模式下自动检测节点角色(主/从/哨兵)
– 检查运行用户是否存在
– 避免重复添加系统配置

源码编译
– jemalloc 5.3.0 源码编译安装
– Redis 使用 jemalloc 内存分配器
ARM64 架构

灵活配置
– 支持单机、主从、哨兵三种部署模式
– 支持完全自定义安装路径
– 通过 config.sh 集中管理配置
– 主从/哨兵模式下自动识别节点角色
– 哨兵可与 Redis 同机部署或独立部署

系统优化
– 内核参数优化
– 禁用透明大页
– 资源限制配置
– Systemd 服务管理

安全加固
– 密码认证
– 用户权限隔离
– 安全的服务停止方式(TERM 信号)

持久化
– RDB 快照
– AOF 日志
– 自动保存策略

📁 文件清单

redis_install/
├── redis-7.4.7.tar.gz           # Redis 源码包(必需)
├── jemalloc-5.3.0.tar.bz2       # jemalloc 源码包(必需)
├── install_redis.sh              # 主安装脚本(必需)
├── config.sh                     # 配置文件(推荐)
├── config.sh.example             # 配置文件示例(参考)
└── README.md                     # 说明文档

redis版本下载地址:https://download.redis.io/releases/

jemalloc-5.3.0下载地址:https://github.com/jemalloc/jemalloc/releases/download/5.3.0/jemalloc-5.3.0.tar.bz2

🚀 快速开始

1. 前置要求

必须满足的条件
– ✅ Root 权限
– ✅ 运行用户已存在(默认:gzapps)
– ✅ 所有源码包已上传
– ✅ 主从模式下:当前主机 IP 必须为主节点或从节点 IP

创建运行用户(如果不存在):

useradd -r -s /sbin/nologin gzapps

2. 修改配置

方法一:使用示例配置文件(推荐)

# 复制示例配置文件
cp config.sh.example config.sh

# 编辑配置
vim config.sh

方法二:直接编辑现有配置

vim config.sh

重要配置项

2.1 选择部署模式

# ============================================================================
# 部署模式配置
# ============================================================================
# 可选值: standalone (单机) / replication (主从) / sentinel (哨兵)
export DEPLOY_MODE="standalone"

2.2 基础配置(所有模式都需要)

# 安装目录(根据实际情况修改)
export INSTALL_DIR="/AppHome/redis"        # Redis 安装目录
export CONFIG_DIR="/AppHome/redis/conf"    # 配置文件目录
export DATA_DIR="/AppHome/redis/data"      # 数据目录
export LOG_DIR="/AppHome/redis/logs"       # 日志目录

# 运行用户(必须已存在)
export REDIS_USER="gzapps"

# Redis 密码(必须修改)
export REDIS_PASSWORD="1q2w3e4rAa!@#"

# 性能配置
export REDIS_MAXMEMORY="4gb"

2.3 主从模式额外配置(仅在 DEPLOY_MODE=replication 或 sentinel 时需要)

# 主从节点 IP
export MASTER_IP="10.202.17.12"
export SLAVE_IP="10.202.17.13"
#export SLAVE_IP="10.202.17.14"

2.4 哨兵模式额外配置(仅在 DEPLOY_MODE=sentinel 时需要)

# 哨兵配置
export SENTINEL_PORT=26379                                      # 哨兵端口
export SENTINEL_QUORUM=2                                        # 投票数(建议:哨兵总数/2+1)
export SENTINEL_IPS="10.202.17.12 10.202.17.13 10.202.17.14"  # 哨兵节点 IP(空格分隔)
export MASTER_NAME="mymaster"                                   # 主节点名称

哨兵部署说明
– 哨兵可以与 Redis 主从节点同机部署(如示例中的 10.202.17.12 和 10.202.17.13)
– 也可以部署在独立节点上(如示例中的 10.202.17.14)
– 建议至少部署 3 个哨兵节点以保证高可用
– SENTINEL_QUORUM 建议设置为哨兵总数的一半加一

3. 执行安装

3.1 单机模式安装

cd redis_install
chmod +x *.sh

# 确保 config.sh 中 DEPLOY_MODE="standalone"
./install_redis.sh

3.3 哨兵模式安装

在主节点 (10.202.17.12) 上

cd redis_install
chmod +x *.sh

# 确保 config.sh 中 DEPLOY_MODE="sentinel"
# 确保当前 IP 在 SENTINEL_IPS 列表中(如果要部署哨兵)
./install_redis.sh

在从节点 (10.202.17.13) 上

cd redis_install
chmod +x *.sh

# 使用相同的 config.sh 配置
./install_redis.sh

在哨兵节点 (10.202.17.14) 上(如果是独立哨兵节点):

cd redis_install
chmod +x *.sh

# 使用相同的 config.sh 配置
# 注意:独立哨兵节点的 IP 不在 MASTER_IP 和 SLAVE_IP 中
# 但必须在 SENTINEL_IPS 列表中
./install_redis.sh

说明
– 哨兵可以与 Redis 主从节点同机部署
– 脚本会自动判断当前节点是否需要部署哨兵服务
– 建议至少部署 3 个哨兵节点

4. 验证安装

4.1 单机模式验证

# 检查服务状态
systemctl status redis

# 测试连接
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# ping

# 查看 Redis 信息
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info server

4.2 主从模式验证

# 检查服务状态
systemctl status redis

# 测试连接
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# ping

# 查看主从状态
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

4.3 哨兵模式验证

# 检查 Redis 服务状态
systemctl status redis

# 测试 Redis 连接
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# ping

# 查看主从状态
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

# 检查哨兵服务状态(在哨兵节点上)
systemctl status redis-sentinel

# 查看哨兵监控的主节点信息
/AppHome/redis/bin/redis-cli -p 26379 sentinel masters

# 查看哨兵监控的从节点信息
/AppHome/redis/bin/redis-cli -p 26379 sentinel slaves mymaster

# 查看所有哨兵节点
/AppHome/redis/bin/redis-cli -p 26379 sentinel sentinels mymaster

📂 安装后的目录结构

/AppHome/redis/
├── bin/                          # 可执行文件
│   ├── redis-server              # Redis 服务器
│   ├── redis-cli                 # Redis 客户端
│   ├── redis-benchmark           # 性能测试工具
│   ├── redis-check-rdb           # RDB 检查工具
│   └── redis-check-aof           # AOF 检查工具
├── conf/
│   └── redis.conf                # Redis 配置文件
├── data/
│   ├── dump.rdb                  # RDB 持久化文件
aof            # AOF 持久化文件
│   └── redis_6379.pid            # PID 文件
└── logs/
    └── redis.log                 # Redis 日志文件

/usr/local/lib/
└── libjemalloc.so.*              # jemalloc 库文件

/etc/systemd/system/
└── redis.service                 # Systemd 服务文件

⚙️ 配置说明

Redis 核心配置

配置项 说明
bind 0.0.0.0 监听所有网卡
port 6379 监听端口
daemonize yes 后台运行
requirepass 自定义 Redis 密码
maxmemory 4gb 最大内存限制
maxmemory-policy allkeys-lru LRU 淘汰策略
hz 100 高频率定时任务
appendonly yes 启用 AOF
save 900 1 / 300 10 / 60 10000 RDB 保存策略

系统优化参数

脚本会自动配置(避免重复添加):

# 内核参数
vm.overcommit_memory = 1          # 允许内存过量分配
net.core.somaxconn = 65535        # 增加连接队列

# 禁用透明大页
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 资源限制
gzapps soft nofile 65535
gzapps hard nofile 65535
gzapps soft nproc 65535
gzapps hard nproc 65535

Systemd 服务配置

[Service]
Type=forking                      # 后台进程模式
User=gzapps                       # 运行用户
PIDFile=/AppHome/redis/data/redis_6379.pid
ExecStart=/AppHome/redis/bin/redis-server /AppHome/redis/conf/redis.conf
ExecStop=/bin/kill -s TERM $MAINPID  # 优雅停止
TimeoutStartSec=30s               # 启动超时
Restart=always                    # 自动重启
LimitNOFILE=65535                 # 文件描述符限制

🔧 服务管理

基本操作

# 启动服务
systemctl start redis

# 停止服务
systemctl stop redis

# 重启服务
systemctl restart redis

# 查看状态
systemctl status redis

# 查看日志
journalctl -u redis -f

# 开机自启
systemctl enable redis

# 禁用自启
systemctl disable redis

Redis 命令

# 连接 Redis
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@#

# 查看信息
redis-cli -a 1q2w3e4rAa!@# info
redis-cli -a 1q2w3e4rAa!@# info replication
redis-cli -a 1q2w3e4rAa!@# info memory

# 查看配置
redis-cli -a 1q2w3e4rAa!@# config get "*"

# 查看慢查询
redis-cli -a 1q2w3e4rAa!@# slowlog get 10

# 查看客户端连接
redis-cli -a 1q2w3e4rAa!@# client list

🔍 主从复制验证(仅主从模式)

在主节点验证

/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

期望输出

Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
# Replication
role:master
connected_slaves:1
slave0:ip=10.202.17.13,port=6379,state=online,offset=1632,lag=0
master_failover_state:no-failover
master_replid:f6f7770caf8dc4d40f47a30824ec181caa3c6676
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1632
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:1631

在从节点验证

/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# info replication

期望输出

Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
# Replication
role:slave
master_host:10.202.17.12
master_port:6379
master_link_status:up
master_last_io_seconds_ago:7
master_sync_in_progress:0
slave_read_repl_offset:1660
slave_repl_offset:1660
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:f6f7770caf8dc4d40f47a30824ec181caa3c6676
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1660
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:804
repl_backlog_histlen:857

测试数据同步

主节点写入

redis-cli -a 1q2w3e4rAa!@# set test_key "hello from master"

从节点读取

redis-cli -portRedis@2026 get test_key
# 输出: "hello from master"

🐛 故障排查

1. 服务启动超时

症状systemctl start redis 超时

排查步骤

# 1. 查看 systemd 日志
journalctl -u redis -n 50

# 2. 查看 Redis 日志
tail -100 /AppHome/redis/logs/redis.log

# 3. 检查 PID 文件
ls -la /AppHome/redis/data/redis_6379.pid

# 4. 手动启动测试
/AppHome/redis/bin/redis-server /AppHome/redis/conf/redis.conf

2. 主从同步失败

症状:从节点 master_link_status:down

排查步骤

# 1. 检查网络连通性
ping 10.202.17.12
telnet 10.202.17.12 6379

# 2. 检查防火墙
firewall-cmd --list-all

# 3. 检查主节点日志
tail -f /AppHome/redis/logs/redis.log

# 4. 检查密码配置
grep "requirepass\|masterauth" /AppHome/redis/conf/redis.conf

解决方案

# 开放防火墙端口
fird-port=6379/tcp
firewall-cmd --reload

redis-server /AppHome/redis/conf/redis.conf --test-memory 1
   ```

3. **检查资源**
   ```bash
   top
   free -h
   df -h
   ```

4. **检查网络**
   ```bash
   netstat -tlnp | grep 6379
   ss -tlnp | grep 6379
   ```

---


## 🔄 哨兵模式故障转移测试(仅哨兵模式)

### 测试自动故障转移

**1. 模拟主节点故障**

在主节点上停止 Redis 服务:
```bash
systemctl stop redis

2. 观察哨兵日志

在任意哨兵节点上查看日志:

tail -f /AppHome/redis/logs/sentinel.log

你会看到类似以下日志:

+sdown master mymaster 10.202.17.12 6379
+odown master mymaster 10.202.17.12 6379 #quorum 2/2
+failover-triggered master mymaster 10.202.17.12 6379
+failover-state-select-slave master mymaster 10.202.17.12 6379
+selected-slave slave 10.202.17.13:6379 10.202.17.13 6379 @ mymaster 10.202.17.12 6379
+failover-state-send-slaveof-noone slave 10.202.17.13:6379 10.202.17.13 6379 @ mymaster 10.202.17.12 6379
+failover-end master mymaster 10.202.17.12 6379
+switch-master mymaster 10.202.17.12 6379 10.202.17.13 6379

3. 验证新主节点

查看哨兵监控的主节点信息:

/AppHome/redis/bin/redis-cli -p 26379 sentinel get-master-addr-by-name mymaster

应该返回新的主节点 IP(原从节点):

1) "10.202.17.13"
2) "6379"

4. 恢复原主节点

启动原主节点的 Redis 服务:

systemctl start redis

原主节点会自动变成从节点,同步新主节点的数据。

验证故障转移结果

# 在新主节点上查看角色
/AppHome/redis/bin/redis-cli -h 10.202.17.13 -a 1q2w3e4rAa!@# info replication | grep role
# 输出: role:master

# 在原主节点上查看角色
/AppHome/redis/bin/redis-cli -h 10.202.17.12 -a 1q2w3e4rAa!@# info replication | grep role
# 输出: role:slave

💾 备份与恢复

自动备份脚本

创建 /usr/local/bin/redis_backup.sh

#!/bin/bash
BACKUP_DIR="/backup/redis/$(date +%Y%m%d)"
mkdir -p $BACKUP_DIR

# 触发 RDB 快照
/AppHome/redis/bin/redis-cli -a 1q2w3e4rAa!@# bgsave

# 等待快照完成
sleep 10

# 备份文件
cp /AppHome/redis/data/dump.rdb $BACKUP_DIR/
cp /AppHome/redis/data/appendonly.aof $BACKUP_DIR/
cp /AppHome/redis/conf/redis.conf $BACKUP_DIR/

# 保留最近 7 天
find /backup/redis/ -type d -mtime +7 -exec rm -rf {} \;

添加到 crontab

# 每天凌晨 2 点备份
0 2 * * * /usr/local/bin/redis_backup.sh

📊 性能测试

基础性能测试

/AppHome/redis/bin/redis-benchmark \
  -h 127.0.0.1 \
  -p 6379 \
  -a 1q2w3e4rAa!@# \
  -c 50 \
  -n 10000

内存不足

症状:Redis 拒绝写入

排查步骤

# 查看内存使用
redis-cli -a 1q2w3e4rAa!@# info memory | grep used_memory_human
redis-cli -a 1q2w3e4rAa!@# info memory | grep maxmemory_human

解决方案

# 临时调整(重启后失效)
redis-cli -a 1q2w3e4rAa!@# config set maxmemory 8gb

脚本如下

config.sh

#!/bin/bash

################################################################################
# Redis 安装配置文件
# 
# 使用方法:
# 1. 修改下面的配置参数
# 2. 将此文件和 install_redis.sh 放在同一目录
# 3. 运行 install_redis.sh 时会自动加载此配置
################################################################################

# ============================================================================
# 部署模式配置
# ============================================================================
# 可选值: 
#   - standalone   : 单机模式
#   - replication  : 主从模式
#   - sentinel     : 主从+哨兵模式
export DEPLOY_MODE="standalone"

# ============================================================================
# Redis 版本配置
# ============================================================================
export REDIS_VERSION="7.4.7"

# ============================================================================
# 安装目录配置
# ============================================================================
export INSTALL_DIR="/AppHome/redis"        # Redis 安装目录
export CONFIG_DIR="/AppHome/redis/conf"    # 配置文件目录(redis.conf 存放位置)
export DATA_DIR="/AppHome/redis/data"      # 数据存储目录
export LOG_DIR="/AppHome/redis/logs"       # 日志目录

# ============================================================================
# Redis 服务配置
# ============================================================================
export REDIS_PORT=6379                     # Redis 监听端口
export REDIS_USER="gzapps"                 # Redis 运行用户
export REDIS_PASSWORD="YourStrongPassword123"  # Redis 密码

# ============================================================================
# 主从节点 IP 配置(仅在 DEPLOY_MODE=replication 或 sentinel 时生效)
# ============================================================================
export MASTER_IP="10.202.17.12"            # 主节点 IP
export SLAVE_IP="10.202.17.13"             # 从节点 IP

# ============================================================================
# 哨兵配置(仅在 DEPLOY_MODE=sentinel 时生效)
# ============================================================================
export SENTINEL_PORT=26379                 # 哨兵监听端口
export SENTINEL_QUORUM=2                   # 哨兵判定主节点下线所需的投票数
export SENTINEL_IPS="10.202.17.12 10.202.17.13 10.202.17.14"  # 哨兵节点 IP 列表(空格分隔)
export MASTER_NAME="mymaster"              # 主节点名称(哨兵监控用)

# ============================================================================
# 性能配置
# ============================================================================
export REDIS_MAXMEMORY="4gb"               # 最大内存限制(根据服务器实际内存调整)

# ============================================================================
# 持久化配置
# ============================================================================
export ENABLE_AOF="yes"                    # 是否启用 AOF 持久化

install_redis.sh

#!/bin/bash

################################################################################
# Redis 7.4.7 安装脚本(支持单机/主从/哨兵模式)
# 适用系统: Kylin Linux Advanced Server V10 (ARM64)
# 作者: glj
# 日期: 2025-02-10
# 
# 部署模式:
#   - standalone: 单机模式
#   - replication: 主从模式
#   - sentinel: 主从+哨兵模式
################################################################################

set -e  # 遇到错误立即退出

# ============================================================================
# 加载外部配置文件(如果存在)
# ============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/config.sh" ]]; then
    source "$SCRIPT_DIR/config.sh"
    echo "[INFO] 已加载外部配置文件: config.sh"
fi

# ============================================================================
# 全局变量配置(可被 config.sh 覆盖)
# ============================================================================
DEPLOY_MODE="${DEPLOY_MODE:-standalone}"         # 部署模式:standalone、replication 或 sentinel
REDIS_VERSION="${REDIS_VERSION:-7.4.7}"
REDIS_TAR="redis-${REDIS_VERSION}.tar.gz"
REDIS_DIR="redis-${REDIS_VERSION}"
INSTALL_DIR="${INSTALL_DIR:-/usr/local/redis}"
CONFIG_DIR="${CONFIG_DIR:-/etc/redis}"           # 配置文件目录(可自定义)
DATA_DIR="${DATA_DIR:-/data/redis}"
LOG_DIR="${LOG_DIR:-/var/log/redis}"
REDIS_PORT="${REDIS_PORT:-6379}"
REDIS_USER="${REDIS_USER:-redis}"
REDIS_PASSWORD="${REDIS_PASSWORD:-YourStrongPassword123}"  # 请修改为实际密码
MAXMEMORY="${REDIS_MAXMEMORY:-2gb}"

# 主从配置(仅在 replication 或 sentinel 模式下使用)
MASTER_IP="${MASTER_IP:-10.202.17.12}"
SLAVE_IP="${SLAVE_IP:-10.202.17.13}"

# 哨兵配置(仅在 sentinel 模式下使用)
SENTINEL_PORT="${SENTINEL_PORT:-26379}"
SENTINEL_QUORUM="${SENTINEL_QUORUM:-2}"
SENTINEL_IPS="${SENTINEL_IPS:-10.202.17.12 10.202.17.13 10.202.17.14}"
MASTER_NAME="${MASTER_NAME:-mymaster}"

# 节点角色和当前 IP(由 check_node_role 函数设置)
CURRENT_IP=""
NODE_ROLE=""
IS_SENTINEL_NODE=false  # 当前节点是否为哨兵节点

# 配置文件完整路径
REDIS_CONF="${CONFIG_DIR}/redis.conf"
SENTINEL_CONF="${CONFIG_DIR}/sentinel.conf"

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# ============================================================================
# 工具函数
# ============================================================================

# 日志输出函数
log_info() {
    echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log_warn() {
    echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') - $1"
}

# 检查命令是否存在
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# 检查是否为 root 用户
check_root() {
    if [[ $EUID -ne 0 ]]; then
        log_error "此脚本必须以 root 用户运行"
        exit 1
    fi
}

# 检查当前主机角色(根据部署模式)
check_node_role() {
    log_info "检查部署模式: $DEPLOY_MODE"

    # 验证部署模式配置
    if [[ "$DEPLOY_MODE" != "standalone" && "$DEPLOY_MODE" != "replication" && "$DEPLOY_MODE" != "sentinel" ]]; then
        log_error "无效的部署模式: $DEPLOY_MODE"
        log_error "有效值: standalone (单机) / replication (主从) / sentinel (哨兵)"
        exit 1
    fi

    # 获取当前主机 IP
    CURRENT_IP=$(hostname -I | awk '{print $1}')
    log_info "当前主机 IP: $CURRENT_IP"

    # 单机模式:直接设置为 standalone
    if [[ "$DEPLOY_MODE" == "standalone" ]]; then
        NODE_ROLE="standalone"
        log_info "部署模式: 单机模式 (Standalone)"
        return 0
    fi

    # 主从模式或哨兵模式:检查当前主机角色
    if [[ "$DEPLOY_MODE" == "replication" || "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "部署模式: ${DEPLOY_MODE} 模式"
        log_info "检查当前主机角色..."

        if [[ "$CURRENT_IP" == "$MASTER_IP" ]]; then
            NODE_ROLE="master"
            log_info "当前节点为主节点 (Master)"
        elif [[ "$CURRENT_IP" == "$SLAVE_IP" ]]; then
            NODE_ROLE="slave"
            log_info "当前节点为从节点 (Slave)"
        else
            log_error "当前 IP ($CURRENT_IP) 不在配置的主从节点中"
            log_error "配置的主节点: $MASTER_IP"
            log_error "配置的从节点: $SLAVE_IP"
            log_error "请检查 config.sh 中的 MASTER_IP 和 SLAVE_IP 配置"
            exit 1
        fi
    fi

    # 哨兵模式:额外检查是否为哨兵节点
    if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "检查是否为哨兵节点..."

        # 将哨兵 IP 列表转换为数组
        local sentinel_ips_array=($SENTINEL_IPS)

        # 检查当前 IP 是否在哨兵列表中
        for sentinel_ip in "${sentinel_ips_array[@]}"; do
            if [[ "$CURRENT_IP" == "$sentinel_ip" ]]; then
                IS_SENTINEL_NODE=true
                log_info "当前节点同时为哨兵节点 (Sentinel)"
                break
            fi
        done

        if [[ "$IS_SENTINEL_NODE" == false ]]; then
            log_warn "当前节点不在哨兵列表中,将只部署 Redis 服务"
        fi
    fi
}

# 检查系统信息
check_system() {
    log_info "检查系统信息..."

    # 检查操作系统
    if [[ -f /etc/os-release ]]; then
        . /etc/os-release
        log_info "操作系统: $PRETTY_NAME"
    fi

    # 检查架构
    ARCH=$(uname -m)
    log_info "系统架构: $ARCH"

    # 检查内核版本
    KERNEL=$(uname -r)
    log_info "内核版本: $KERNEL"
}

# ============================================================================
# 依赖检查与安装
# ============================================================================

# 检查并安装编译依赖
install_dependencies() {
    log_info "检查并安装编译依赖..."

    local packages=(
        "gcc"
        "make"
        "tcl"
        "wget"
        "tar"
        "bzip2"
    )

    local to_install=()

    # 检查哪些包需要安装
    for pkg in "${packages[@]}"; do
        if ! rpm -q "$pkg" &>/dev/null; then
            to_install+=("$pkg")
        fi
    done

    # 如果有需要安装的包
    if [[ ${#to_install[@]} -gt 0 ]]; then
        log_info "需要安装的包: ${to_install[*]}"
        log_info "正在安装依赖包(请稍候...)"
        yum install -y "${to_install[@]}" > /dev/null 2>&1
        if [[ $? -eq 0 ]]; then
            log_info "依赖包安装完成"
        else
            log_warn "部分依赖包安装失败,继续执行..."
        fi
    else
        log_info "所有依赖包已安装"
    fi
}

# 编译安装 jemalloc
install_jemalloc() {
    log_info "开始编译安装 jemalloc..."

    local JEMALLOC_VERSION="5.3.0"
    local JEMALLOC_TAR="jemalloc-${JEMALLOC_VERSION}.tar.bz2"
    local JEMALLOC_DIR="jemalloc-${JEMALLOC_VERSION}"
    local BUILD_LOG="/tmp/jemalloc_build.log"

    # 检查是否已安装
    if ldconfig -p | grep -q libjemalloc; then
        log_info "jemalloc 已安装,跳过编译"
        return 0
    fi

    # 检查源码包是否存在
    if [[ ! -f "$JEMALLOC_TAR" ]]; then
        log_error "jemalloc 源码包 $JEMALLOC_TAR 不存在"
        log_error "请将 jemalloc-5.3.0.tar.bz2 放到当前目录"
        return 1
    fi

    log_info "使用本地 jemalloc 源码包: $JEMALLOC_TAR"

    # 解压
    log_info "解压 jemalloc..."
    tar -xjf "$JEMALLOC_TAR" 2>&1 | tee "$BUILD_LOG" > /dev/null
    cd "$JEMALLOC_DIR"

    # 生成配置文件
    log_info "生成配置文件..."
    ./autogen.sh >> "$BUILD_LOG" 2>&1 || {
        log_warn "autogen.sh 失败,尝试直接 configure"
    }

    # 配置
    log_info "配置 jemalloc..."
    ./configure --prefix=/usr/local >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "配置失败,查看日志: $BUILD_LOG"
        cd ..
        return 1
    fi

    # 编译
    log_info "编译 jemalloc(使用 $(nproc) 核心,请稍候...)"
    make -j$(nproc) >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "编译失败,查看日志: $BUILD_LOG"
        cd ..
        return 1
    fi

    # 安装
    log_info "安装 jemalloc..."
    make install >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "安装失败,查看日志: $BUILD_LOG"
        cd ..
        return 1
    fi

    # 配置库路径
    log_info "配置库路径..."
    echo "/usr/local/lib" > /etc/ld.so.conf.d/jemalloc.conf
    ldconfig

    # 返回上级目录
    cd ..

    # 验证安装
    if ldconfig -p | grep -q libjemalloc; then
        log_info "jemalloc 安装成功"
        # 清理临时文件
        rm -rf "$JEMALLOC_DIR"
        rm -f "$BUILD_LOG"
        return 0
    else
        log_error "jemalloc 安装失败,查看日志: $BUILD_LOG"
        return 1
    fi
}

# ============================================================================
# Redis 编译安装
# ============================================================================

# 编译 Redis
compile_redis() {
    log_info "开始编译 Redis ${REDIS_VERSION}..."

    local BUILD_LOG="/tmp/redis_build.log"

    # 检查源码包是否存在
    if [[ ! -f "$REDIS_TAR" ]]; then
        log_error "Redis 源码包 $REDIS_TAR 不存在"
        exit 1
    fi

    # 解压源码
    log_info "解压 Redis 源码..."
    tar -zxf "$REDIS_TAR" 2>&1 | tee "$BUILD_LOG" > /dev/null
    cd "$REDIS_DIR"

    # 使用 jemalloc 编译(Redis 官方推荐方式)
    log_info "使用 jemalloc 编译 Redis(使用 $(nproc) 核心,请稍候...)"
    make MALLOC=jemalloc -j$(nproc) >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "编译失败,查看日志: $BUILD_LOG"
        cd ..
        exit 1
    fi

    # 安装
    log_info "安装 Redis 到 $INSTALL_DIR..."
    make PREFIX="$INSTALL_DIR" install >> "$BUILD_LOG" 2>&1
    if [[ $? -ne 0 ]]; then
        log_error "安装失败,查看日志: $BUILD_LOG"
        cd ..
        exit 1
    fi

    cd ..
    log_info "Redis 编译安装完成"
    rm -f "$BUILD_LOG"
}

# ============================================================================
# 系统配置
# ============================================================================

# 检查或创建 Redis 用户
check_redis_user() {
    log_info "检查 Redis 用户: $REDIS_USER"

    if ! id "$REDIS_USER" &>/dev/null; then
        log_error "用户 $REDIS_USER 不存在"
        log_error "请先创建用户,或修改 config.sh 中的 REDIS_USER 配置"
        log_error "创建用户命令: useradd $REDIS_USER"
        exit 1
    else
        log_info "用户 $REDIS_USER 已存在"
    fi
}

# 创建目录结构
create_directories() {
    log_info "创建目录结构..."

    # 创建所有必要的目录
    mkdir -p "$INSTALL_DIR"
    mkdir -p "$DATA_DIR"
    mkdir -p "$LOG_DIR"
    mkdir -p "$CONFIG_DIR"

    # 统一设置所有目录的所有者为 Redis 用户
    log_info "设置目录权限..."
    chown -R "$REDIS_USER:$REDIS_USER" "$INSTALL_DIR"
    chown -R "$REDIS_USER:$REDIS_USER" "$DATA_DIR"
    chown -R "$REDIS_USER:$REDIS_USER" "$LOG_DIR"
    chown -R "$REDIS_USER:$REDIS_USER" "$CONFIG_DIR"

    log_info "目录创建完成"
}

# 配置系统参数
configure_system() {
    log_info "配置系统参数..."

    # 配置内核参数(避免重复添加)
    if ! grep -q "vm.overcommit_memory" /etc/sysctl.conf; then
        log_info "添加内核参数到 /etc/sysctl.conf..."
        cat >> /etc/sysctl.conf <<EOF

# Redis 优化参数
vm.overcommit_memory = 1
net.core.somaxconn = 65535
EOF
        sysctl -p
    else
        log_info "内核参数已存在,跳过添加"
    fi

    # 禁用透明大页
    if [[ -f /sys/kernel/mm/transparent_hugepage/enabled ]]; then
        echo never > /sys/kernel/mm/transparent_hugepage/enabled
        echo never > /sys/kernel/mm/transparent_hugepage/defrag
        log_info "已禁用透明大页"
    fi

    # 配置 limits(避免重复添加)
    if ! grep -q "^$REDIS_USER.*nofile" /etc/security/limits.conf; then
        log_info "添加资源限制到 /etc/security/limits.conf..."
        cat >> /etc/security/limits.conf <<EOF

# Redis 资源限制
$REDIS_USER soft nofile 65535
$REDIS_USER hard nofile 65535
$REDIS_USER soft nproc 65535
$REDIS_USER hard nproc 65535
EOF
    else
        log_info "资源限制已存在,跳过添加"
    fi

    log_info "系统参数配置完成"
}

# ============================================================================
# Redis 配置
# ============================================================================

# 生成 Redis 配置文件
generate_redis_config() {
    log_info "生成 Redis 配置文件..."

    # 使用之前检查的节点角色
    local role="$NODE_ROLE"
    local mode_desc=""

    # 根据角色设置描述
    case "$role" in
        standalone)
            mode_desc="单机模式 (Standalone)"
            ;;
        master)
            if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
                mode_desc="哨兵模式 - 主节点 (Sentinel Master)"
            else
                mode_desc="主从模式 - 主节点 (Master)"
            fi
            ;;
        slave)
            if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
                mode_desc="哨兵模式 - 从节点 (Sentinel Slave)"
            else
                mode_desc="主从模式 - 从节点 (Slave)"
            fi
            ;;
    esac

    log_info "生成配置: ${mode_desc}"

    # 生成配置文件
    cat > "$REDIS_CONF" <<EOF
# Redis ${REDIS_VERSION} 配置文件
# 部署模式: ${mode_desc}
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')

# ============================================================================
# 网络配置
# ============================================================================
bind 0.0.0.0
protected-mode yes
port ${REDIS_PORT}
tcp-backlog 511
timeout 0
tcp-keepalive 300

# ============================================================================
# 通用配置
# ============================================================================
daemonize yes
pidfile ${DATA_DIR}/redis_${REDIS_PORT}.pid
loglevel notice
logfile ${LOG_DIR}/redis.log
databases 16
hz 100

# ============================================================================
# 持久化配置
# ============================================================================
# RDB 配置
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir ${DATA_DIR}

# AOF 配置
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes

# ============================================================================
# 安全配置
# ============================================================================
requirepass ${REDIS_PASSWORD}

# ============================================================================
# 内存管理
# ============================================================================
maxmemory ${MAXMEMORY}
maxmemory-policy allkeys-lru
maxmemory-samples 5

# ============================================================================
# 慢查询日志
# ============================================================================
slowlog-log-slower-than 10000
slowlog-max-len 128

# ============================================================================
# 客户端配置
# ============================================================================
maxclients 10000

EOF

    # 如果是从节点,添加主从复制配置
    if [[ "$role" == "slave" ]]; then
        log_info "添加主从复制配置..."
        cat >> "$REDIS_CONF" <<EOF
# ============================================================================
# 主从复制配置
# ============================================================================
replicaof ${MASTER_IP} ${REDIS_PORT}
masterauth ${REDIS_PASSWORD}
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100

EOF
    fi

    # 设置配置文件权限
    chown "$REDIS_USER:$REDIS_USER" "$REDIS_CONF"
    chmod 640 "$REDIS_CONF"

    log_info "Redis 配置文件生成完成: $REDIS_CONF"
}

# 生成哨兵配置文件
generate_sentinel_config() {
    log_info "生成 Sentinel 配置文件..."

    # 生成哨兵配置
    cat > "$SENTINEL_CONF" <<EOF
# Redis Sentinel ${REDIS_VERSION} 配置文件
# 生成时间: $(date '+%Y-%m-%d %H:%M:%S')

# ============================================================================
# 网络配置
# ============================================================================
bind 0.0.0.0
port ${SENTINEL_PORT}
daemonize yes
pidfile ${DATA_DIR}/redis-sentinel.pid
logfile ${LOG_DIR}/sentinel.log

# ============================================================================
# 监控配置
# ============================================================================
# 监控主节点:sentinel monitor <master-name> <ip> <port> <quorum>
# quorum: 判定主节点下线所需的哨兵投票数
sentinel monitor ${MASTER_NAME} ${MASTER_IP} ${REDIS_PORT} ${SENTINEL_QUORUM}

# 主节点密码
sentinel auth-pass ${MASTER_NAME} ${REDIS_PASSWORD}

# ============================================================================
# 故障转移配置
# ============================================================================
# 主节点无响应超时时间(毫秒)
sentinel down-after-milliseconds ${MASTER_NAME} 5000

# 故障转移超时时间(毫秒)
sentinel failover-timeout ${MASTER_NAME} 15000

# 同时进行同步的从节点数量
sentinel parallel-syncs ${MASTER_NAME} 1

EOF

    # 设置配置文件权限
    chown "$REDIS_USER:$REDIS_USER" "$SENTINEL_CONF"
    chmod 640 "$SENTINEL_CONF"

    log_info "Sentinel 配置文件生成完成: $SENTINEL_CONF"
}

# ============================================================================
# Systemd 服务配置
# ============================================================================

# 创建 systemd 服务文件
create_systemd_service() {
    log_info "创建 systemd 服务..."

    # 创建 Redis 服务
    cat > /etc/systemd/system/redis.service <<EOF
[Unit]
Description=Redis In-Memory Data Store
After=network.target

[Service]
Type=forking
User=${REDIS_USER}
Group=${REDIS_USER}
PIDFile=${DATA_DIR}/redis_${REDIS_PORT}.pid
ExecStart=${INSTALL_DIR}/bin/redis-server ${REDIS_CONF}
ExecStop=/bin/kill -s TERM \$MAINPID
TimeoutStartSec=30s
TimeoutStopSec=30s
Restart=always
RestartSec=5s
LimitNOFILE=65535

# 安全加固
PrivateTmp=yes
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
EOF

    log_info "Redis 服务创建完成"

    # 如果是哨兵模式且当前节点是哨兵节点,创建哨兵服务
    if [[ "$DEPLOY_MODE" == "sentinel" && "$IS_SENTINEL_NODE" == true ]]; then
        log_info "创建 Sentinel 服务..."

        cat > /etc/systemd/system/redis-sentinel.service <<EOF
[Unit]
Description=Redis Sentinel
After=network.target redis.service
Requires=redis.service

[Service]
Type=forking
User=${REDIS_USER}
Group=${REDIS_USER}
PIDFile=${DATA_DIR}/redis-sentinel.pid
ExecStart=${INSTALL_DIR}/bin/redis-sentinel ${SENTINEL_CONF}
ExecStop=/bin/kill -s TERM \$MAINPID
TimeoutStartSec=30s
TimeoutStopSec=30s
Restart=always
RestartSec=5s
LimitNOFILE=65535

# 安全加固
PrivateTmp=yes
NoNewPrivileges=true

[Install]
WantedBy=multi-user.target
EOF

        log_info "Sentinel 服务创建完成"
    fi

    # 重载 systemd
    systemctl daemon-reload

    log_info "Systemd 服务配置完成"
}

# ============================================================================
# 启动与验证
# ============================================================================

# 启动 Redis 服务
start_redis() {
    log_info "启动 Redis 服务..."

    systemctl enable redis
    systemctl start redis

    # 等待服务启动
    sleep 3

    # 检查服务状态
    if systemctl is-active --quiet redis; then
        log_info "Redis 服务启动成功"
    else
        log_error "Redis 服务启动失败"
        systemctl status redis
        exit 1
    fi

    # 如果是哨兵模式且当前节点是哨兵节点,启动哨兵服务
    if [[ "$DEPLOY_MODE" == "sentinel" && "$IS_SENTINEL_NODE" == true ]]; then
        log_info "启动 Sentinel 服务..."

        systemctl enable redis-sentinel
        systemctl start redis-sentinel

        # 等待服务启动
        sleep 3

        # 检查服务状态
        if systemctl is-active --quiet redis-sentinel; then
            log_info "Sentinel 服务启动成功"
        else
            log_error "Sentinel 服务启动失败"
            systemctl status redis-sentinel
            exit 1
        fi
    fi
}

# 验证 Redis 安装
verify_redis() {
    log_info "验证 Redis 安装..."

    # 检查 Redis 版本
    local version=$("$INSTALL_DIR/bin/redis-server" --version | awk '{print $3}' | cut -d'=' -f2)
    log_info "Redis 版本: $version"

    # 检查 jemalloc(通过 Redis INFO 命令)
    local allocator=$("$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning \
        INFO memory 2>/dev/null | grep -E 'mem_allocator' | awk -F: '{print $2}' | tr -d '\r')

    if [[ "$allocator" =~ jemalloc ]]; then
        log_info "Redis 正在使用 jemalloc 内存分配器: $allocator"
    else
        log_warn "Redis 未使用 jemalloc,当前内存分配器: ${allocator:-未知}"
    fi

    # 测试连接(隐藏密码警告)
    log_info "测试 Redis 连接..."
    if "$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning ping 2>/dev/null | grep -q PONG; then
        log_info "Redis 连接测试成功"
    else
        log_error "Redis 连接测试失败"
        exit 1
    fi

    # 根据部署模式检查状态
    if [[ "$DEPLOY_MODE" == "replication" ]]; then
        log_info "检查主从复制状态..."
        "$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning info replication 2>/dev/null
    elif [[ "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "检查主从复制状态..."
        "$INSTALL_DIR/bin/redis-cli" -a "$REDIS_PASSWORD" --no-auth-warning info replication 2>/dev/null

        # 如果当前节点是哨兵节点,检查哨兵状态
        if [[ "$IS_SENTINEL_NODE" == true ]]; then
            log_info "检查 Sentinel 状态..."
            "$INSTALL_DIR/bin/redis-cli" -p "$SENTINEL_PORT" --no-auth-warning sentinel masters 2>/dev/null
        fi
    else
        log_info "单机模式部署,跳过主从状态检查"
    fi
}

# ============================================================================
# 清理函数
# ============================================================================

cleanup() {
    log_info "清理临时文件..."

    # 清理 Redis 临时目录
    if [[ -d "$REDIS_DIR" ]]; then
        rm -rf "$REDIS_DIR"
        log_info "已清理 Redis 临时文件"
    fi

    # 清理 jemalloc 临时目录
    if [[ -d "jemalloc-5.3.0" ]]; then
        rm -rf "jemalloc-5.3.0"
        log_info "已清理 jemalloc 临时文件"
    fi
}

# ============================================================================
# 主函数
# ============================================================================

main() {
    log_info "=========================================="
    log_info "Redis ${REDIS_VERSION} 安装脚本"
    log_info "=========================================="

    # 前置检查
    check_root
    check_node_role      # 检查部署模式和节点角色
    check_redis_user     # 检查 Redis 用户是否存在
    check_system

    # 安装依赖
    install_dependencies

    # 编译安装 jemalloc
    install_jemalloc

    # 编译安装 Redis
    compile_redis

    # 系统配置
    create_directories
    configure_system

    # Redis 配置
    generate_redis_config

    # 哨兵模式:如果当前节点是哨兵节点,生成哨兵配置
    if [[ "$DEPLOY_MODE" == "sentinel" && "$IS_SENTINEL_NODE" == true ]]; then
        generate_sentinel_config
    fi

    create_systemd_service

    # 启动服务
    start_redis

    # 验证安装
    verify_redis

    # 清理
    # cleanup

    # 输出安装信息
    log_info "=========================================="
    log_info "Redis 安装完成!"
    log_info "=========================================="
    log_info "部署模式: $DEPLOY_MODE"
    log_info "节点角色: $NODE_ROLE"
    log_info "安装目录: $INSTALL_DIR"
    log_info "配置文件: $REDIS_CONF"
    log_info "数据目录: $DATA_DIR"
    log_info "日志目录: $LOG_DIR"
    log_info "服务管理: systemctl {start|stop|restart|status} redis"
    log_info "连接命令: $INSTALL_DIR/bin/redis-cli -a $REDIS_PASSWORD"

    # 主从模式额外提示
    if [[ "$DEPLOY_MODE" == "replication" ]]; then
        log_info "----------------------------------------"
        log_info "主从复制信息:"
        log_info "  主节点: $MASTER_IP"
        log_info "  从节点: $SLAVE_IP"
        log_info "  查看复制状态: $INSTALL_DIR/bin/redis-cli -a $REDIS_PASSWORD info replication"
    fi

    # 哨兵模式额外提示
    if [[ "$DEPLOY_MODE" == "sentinel" ]]; then
        log_info "----------------------------------------"
        log_info "哨兵模式信息:"
        log_info "  主节点: $MASTER_IP"
        log_info "  从节点: $SLAVE_IP"
        log_info "  哨兵节点: $SENTINEL_IPS"
        log_info "  哨兵端口: $SENTINEL_PORT"
        log_info "  主节点名称: $MASTER_NAME"
        log_info "  查看复制状态: $INSTALL_DIR/bin/redis-cli -a $REDIS_PASSWORD info replication"

        if [[ "$IS_SENTINEL_NODE" == true ]]; then
            log_info "  哨兵配置: $SENTINEL_CONF"
            log_info "  哨兵服务: systemctl {start|stop|restart|status} redis-sentinel"
            log_info "  查看哨兵状态: $INSTALL_DIR/bin/redis-cli -p $SENTINEL_PORT sentinel masters"
        fi
    fi

    log_info "=========================================="
}

# 执行主函数
main "$@"

暂无评论

发送评论 编辑评论


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