09 | 切片集群:数据增多了,是该加内存还是加实例?

210 阅读7分钟

1.基本概念

  • 实现方案

    • 用 Redis 保存 5000 万个键值对,每个键值对大约是 512B

    • 键值对所占的内存空间大约是 25GB(5000 万 *512B\

    • 选择一台 32GB 内存的云主机来部署 Redis\

    • 采用 RDB 对数据做持久化,以确保 Redis 实例故障后,还能从 RDB 恢复数据\

  • 问题:上述栗子Redis 的响应有时会非常慢

  • 排查:使用 INFO 命令查看 Redis 的 latest_fork_usec 指标值(表示最近一次 fork 的耗时),快到秒级别了

  • 分析:在使用 RDB 进行持久化时,Redis 会 fork 子进程来完成 ,fork 操作的用时和 Redis 的数据量是正相关的(25GB) ,而 fork 在执行时会阻塞主线程

  • 解决:将数据分片,利用横向扩展降低单机fork压力

//info # Server 基础信息 redis_version:3.2.0 redis_git_sha1:00000000 redis_git_dirty:0 redis_build_id:8c57f577cb1c6bc6 redis_mode:standalone os:Linux 3.10.0-514.16.1.el7.x86_64 x86_64 arch_bits:64 multiplexing_api:epoll gcc_version:4.8.5 process_id:455 run_id:69669d1db7966e2dab3457b81d97cf83962dca69 //redis实例id tcp_port:6379 uptime_in_seconds:4309158 uptime_in_days:49 hz:10 lru_clock:8444669 executable:/home/xiaoju/redis/./bin/redis-server config_file:/home/xiaoju/redis/redis.conf  # Clients 客户端信息 connected_clients:220 client_longest_output_list:0 client_biggest_input_buf:0 blocked_clients:0  # Memory 内存信息 used_memory:7217712 used_memory_human:6.88M //内存大小 used_memory_rss:13815808 used_memory_rss_human:13.18M used_memory_peak:15046936 used_memory_peak_human:14.35M total_system_memory:270227243008 total_system_memory_human:251.67G used_memory_lua:37888 used_memory_lua_human:37.00K maxmemory:0 maxmemory_human:0B maxmemory_policy:noeviction mem_fragmentation_ratio:1.91 mem_allocator:jemalloc-4.0.3  # Persistence 持久化信息 loading:0 rdb_changes_since_last_save:0 rdb_bgsave_in_progress:0 rdb_last_save_time:1635834385 rdb_last_bgsave_status:ok rdb_last_bgsave_time_sec:0 rdb_current_bgsave_time_sec:-1 aof_enabled:0 aof_rewrite_in_progress:0 aof_rewrite_scheduled:0 aof_last_rewrite_time_sec:-1 aof_current_rewrite_time_sec:-1 aof_last_bgrewrite_status:ok aof_last_write_status:ok  # Stats 状态信息 total_connections_received:451 total_commands_processed:10095 instantaneous_ops_per_sec:0 total_net_input_bytes:828525 total_net_output_bytes:248488 instantaneous_input_kbps:0.00 instantaneous_output_kbps:0.00 rejected_connections:0 sync_full:0 sync_partial_ok:0 sync_partial_err:0 expired_keys:2396 evicted_keys:0 keyspace_hits:2395 keyspace_misses:138 pubsub_channels:0 pubsub_patterns:0 latest_fork_usec:527 //fork执行时间 migrate_cached_sockets:0  # Replication 主从节点信息 role:master connected_slaves:0 master_repl_offset:0 repl_backlog_active:0 repl_backlog_size:1048576 repl_backlog_first_byte_offset:0 repl_backlog_histlen:0  # CPU cpu信息 used_cpu_sys:1239.72 used_cpu_user:1185.27 used_cpu_sys_children:20.26 used_cpu_user_children:73.27  # Cluster 集群信息 cluster_enabled:0  # Keyspace db0:keys=3733,expires=0,avg_ttl=0

2.如何保存更多数据?

  • 如何保存更多数据

  • 纵向扩展

    • 大内存云主机
    • 优点:实施起来简单直接
    • 缺点:随着数据量增加,fork时间增加+纵向扩展会受到硬件和成本的限制+扩展不是呈现线性增长
  • 横向扩展

    • 切片集群,也叫分片集群
    • 启动多个Redis实例组成一个集群,按照一定的规则把收到数据划分成多份,每份用一个实例保存
    • 优点:仍可以保存多份数据+fork时间缩短+可扩展性更好
    • 缺点:引入复杂度(分布式管理等等)
  • 分布式需要解决的两大问题

    • 数据切片后,在多个实例之间如何分布?
    • 客户端怎么确定想要访问的数据在哪个实例上?\

\

3.数据切片和实例的对应分布关系

  • 切片集群是一种保存大量数据的通用机制,这个机制可以有不同的实现方案\

    • Redis 3.0 之前,官方并没有针对切片集群提供具体的方案(codis等替代方案)\

    • 从 3.0 开始,官方提供了一个名为 Redis Cluster 的方案,用于实现切片集群\

  • 实现

    • Redis Cluster 方案采用哈希槽(Hash Slot,接下来我会直接称之为 Slot),来处理数据和实例之间的映射关系\

    • 在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中\

    • 一个redis实例管理一部分slot

  • 查找过程

    • 根据键值对的 key,按照CRC16 算法计算一个 16 bit 的值\

    • 用这个 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽\

    • 补充:集群的创建过程

      • 自动版

        • 部署 Redis Cluster 方案时,可以使用 cluster create 命令创建集群

        • Redis 会自动把这些槽平均分布在集群实例上\

        • 每个实例上的槽个数为 16384/N 个\

      • 手动版

        • 可以使用 cluster meet 命令手动建立实例间的连接,形成集群,再使用 cluster addslots 命令,指定每个实例上的哈希槽个数\

        • 使用 cluster addslots 命令手动分配哈希槽\

        • 在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作

\

redis-cli -h 172.16.19.3p 6379 cluster addslots 0,1 redis-cli -h 172.16.19.4p 6379 cluster addslots 2,3 redis-cli -h 172.16.19.5p 6379 cluster addslots 4

4.客户端如何定位数据?

\

  • 要进一步定位到实例,还需要知道哈希槽分布在哪个实例上\

    • 客户端和集群实例建立连接后,实例就会把哈希槽的分配信息发给客户端\

    • 但是每个实例只知道自己被分配了哪些哈希槽,是不知道其他实例拥有的哈希槽信息的

    • Redis 实例会把自己的哈希槽信息发给和它相连接的其它实例\

    • 客户端收到哈希槽信息后,会把哈希槽信息缓存在本地。当客户端请求键值对时,会先计算键所对应的哈希槽,然后就可以给相应的实例发送请求了。\

  • 但是hash槽可能发生变化

    • 在集群中,实例有新增或删除,Redis 需要重新分配哈希槽\

    • 为了负载均衡,Redis 需要把哈希槽在所有实例上重新分布一遍\

  • 由于hash槽位置的变化导致与客户端的缓存不一致

    • 迁移完成的情况

      • Redis Cluster 方案提供了一种重定向机制\

      • 当客户端把一个键值对的操作请求发给一个实例时,如果这个实例上并没有这个键值对映射的哈希槽\

      • 这个实例就会给客户端返回下面的 MOVED 命令响应结果,这个结果中就包含了新实例的访问地址\

      • 客户端还会更新缓存

    • 迁移中的情况

      • 在这种迁移部分完成的情况下,客户端就会收到一条 ASK 报错信息

      • 客户端请求的键值对所在的哈希槽 13320,在 172.16.19.5 这个实例上,但是这个哈希槽正在迁移

      • 客户端需要先给 172.16.19.5 这个实例发送一个 ASKING 命令

      • 让这个实例允许执行客户端接下来发送的命令。然后,客户端再向这个实例发送 GET 命令,以读取数据\

      • 不会更改本地缓存,只会让请求新的实例槽

GET hello:key (error) MOVED 13320 172.16.19.5:6379

\

5.总结

  • 切片集群在保存大量数据方面的优势,以及基于哈希槽的数据分布机制和客户端定位键值对的方法

    • 大部分情况由客户端缓存完成(槽->实例)
  • 虽然增加内存这种纵向扩展的方法简单直接,但是会造成数据库的内存过大,导致性能变慢\

  • Redis 切片集群提供了横向扩展的模式,也就是使用多个实例,并给每个实例配置一定数量的哈希槽,数据可以通过键的哈希值映射到哈希槽,再通过哈希槽分散保存到不同的实例上\

  • 集群的实例增减,或者是为了实现负载均衡而进行的数据重新分布,会导致哈希槽和实例的映射关系发生变化,客户端发送请求时,会收到命令执行报错信息。了解了 MOVED 和 ASK 命令,你就不会为这类报错而头疼了\

  • redis3.0之前的方案

    • 基于客户端的ShardedJedis
    • 基于代理的Codis、Twemproxy
  • 可以根据这些方案的特点,选择合适的方案实现切片集群,以应对业务需求了\

\