JD的JIMDB教程

                                                                                                JimDB                                                                                                                                                                

简介

JIMDB是Jingdong In Memory DataBase(京东内存数据库)的缩写,是对原生redis(2.8)的集群化方案;它据有如下的特性:

1、支持无缝自动伸缩

  •  能够根据业务对集群的访问情况自动将集群调整到一个合适的规模

 2、自动故障检测、恢复

  • 支持分片级别的并行故障恢复,大幅提升故障恢复速度
3、保证数据的高可靠性
  • 支持跨机房热备
  • 具有持久化机制
4、丰富的读策略配置
  • 支持机房就近访问
  • 支持不同客户端读不同的从组
  • 支持多种读策略
5、防止集群间相互干扰
  • 支持物理分区
  • 提供实例级别的限速功能
6、丰富的自动化运维支持
  • server自升级
  • 自扩容
  • 故障自恢复
7、支持多种复制方案
  • 全量复制

寻根问源:什么是JIMDB?

  据现场介绍,JIMDB从缓存发展而来, 目前服务于京东的几乎所有的业务系统,包括很多重要的业务系统,例如, 前台的商品详情页, 交易平台, 广告,搜索, 即时通讯…… , 后台的订单履约, 库存管理, 派送和物流……。

  JIMDB发展历程主要有三个阶段

  阶段1、JIMDB 1.0

  主要采用官方Redis作为单节点服务; 客户端一致性Hash + Presharding技术; 管理,监控和报警。

  阶段2、JIMDB 2.0

  功能包括:故障检测和自动切换;平滑纵向扩容和平滑横向扩容;基于内存+SSD的两级存储结构和自主研发存储引擎。

  阶段3、JIMDB 3.0

  包括了:自助接入和自动部署;容器化;全自动弹性调度。

  JIMDB功能特性

  1、支持大容量缓存

  将缓存数据分摊到多个分片(每个分片上具有相同的构成,比如:都是一主一从两个节点)上,从而可以创建出大容量的缓存。

  2、数据的高可用性

  支持异步复制和同步复制,目前可以达到等同于Mysql级别的数据可用性。

  3、支持多种I/O策略

  针对读操作可分为“主优先”、“从优先”、“随意挑选”等方式;不同的I/O策略,对数据一致性的影响也不同,应用可以根据自身对数据一致性的需求,选择不同的I/O策略。

  4、哨兵服务和故障自动切换

  通过选举算法实现的哨兵服务能够自动判断实例的不存活状态,通知 Failover服务进行主从自动切换, 切换时间在秒级,以保证缓存服务的7 *24小时不间断运行。

  5、支持动态扩容

  可通过多种途径实现动态扩容:

  第一种形式,通过在单个节点上预留内存,然后需要扩容时直接使用预留内存的方法达到扩容的目的。

  第二种形式,通过数据迁移来实现扩容。(平滑纵向扩容)

  第三种形式,通过增加分片数来实现扩容。(平滑横向扩容)




IMDB技术的应用体现在:

  1、数据查询和维护

  提供类似于redis命令窗口的web控制台,禁止危险命令,严格控制写命令和一些运维相关命令,适当放开查询命令。

  将缓存数据分摊到多个分片(每个分片上具有相同的构成,比如:都是一主一从两个节点)上,从而可以创建出大容量的缓存。

     2、监控与报警

  Pains(痛点)

  例如:网络不佳的情况下可能发生误判; Redis单线程执行,在进行长任务时可能发生误判。

  Solution(解决方案)

  比如:在机房中不同区域部署多个Failure Detector;多个Failure Detector之间采用分布式选举算法,判断Redis实例的死活;连接健康度不佳时, 验证端口是否通畅。

  3、迁移与扩容

  Scaling Up – 纵向扩容

  首先,在内存不够, 需要增加内存时首先考虑的是纵向扩容,即增加每个分片的主、从节点的内存。

  其次,纵向扩容时如果Redis实例所在计算机物理内存不够,就需要进行数据迁移。

  再就是,数据迁移的同时,服务不能暂停

  Scaling Out – 横向扩容

  首先,单一分片的内存是不能无限扩容(纵向)的, 太大了会影响复制的效率;其次,在纵向扩容无法进行的情况下(单一分片内存已经很大,或者流量压力很大),就需要进行横向扩容,即增加集群的分片数。再有,横向扩容的同时,服务不能暂停。

解读JIMDB 京东分布式缓存与高速KV存储

纵向扩容

  横向扩容

  Pains(痛点)

  首先,纵向扩容并不增加分片数,简单修改JIMDB实例运行时参数可提高该实例可用内存上限,但在机器内存吃紧时,若要提高该分片内存上限,需要将该实例平滑迁移至一台内存资源更加充沛的机器。

  其次,流量打满或者出现热点时, 需要加分片分散压力。 机器内存不够时, 有时也需要加分片

  再次,Pre-sharding的方式, 在不影响服务的情况下增加分片有难度。

  还有,可以通过定制开发引入bucket来进行横向扩容, 但线上还有2.4, 2.6, 2.8等既有版本,也有加分片的需求。

  再有,避免主从数据不一致。

  最后,服务不能暂停,平滑不影响业务。

  Solution(解决方案)

  首先,通过Filtered replication, 实现某个结点的分裂(split)

  其次,开发一个支持Hold和Split的Proxy, 并通过一个流程控制器来协调客户端,服务结点,Proxy, 等相关各方。

  横向扩容

  对于水平扩容,则是依赖于bucket来解决的。每个JIMDB实例内部都含有若干个buckets,和上述第一类扩容相似,水平扩容也是通过对数据进行平滑迁移来实现的,但迁移的粒度不再是整个实例,而是针对集群中的这些buckets。扩容前后如下图所示:

解读JIMDB 京东分布式缓存与高速KV存储

  4、冷热数据及二级存储

  Pains

  比如,Redis完全依赖于内存,往往内存不够使用;Redis启动时需要把全部数据加载到内存,在数据量大时启动速度慢; 规划总是赶不上业务发展, 内存总量不断被突破, 不断陷入扩容, 再扩容…的梦魇。

  Solution

  引入RAM + SSD两级存储,在内存中存储热点数据, 冷数据被自动交换到磁盘,解决了内存不足的问题;启动时并不把所有数据加载入内存,而是在运行时根据需要加载,解决了启动速度慢的问题; 因为引入了二级存储, 存储容量通常比较大, 所以不需要频繁的扩容了。

  京东私有云技术体系包括:

  1、存储

  JFS:京东文件系统,提供统一的文件/对象/块存储服务

  JIMDB:京东分布式缓存与高速KV存储,兼容Redis协议

  2、中间件

  SAF:服务框架,SOA之基石

  JMQ:消息队列,The Datacenter Pipes!

  3、弹性计算

  JDOS:软件定义数据中心 = 软件定义计算单元(VM & Container)+ 软件定义网络(自研SDN)+ 软件定义存储(JFS & JIMDB)。

  CAP: 弹性计算云 = 软件定义数据中心 + 自动化容器集群调度。

 



适用场景

适合于所有需要加速的场景

系统架构

系统的基础架构图如下:

 

1.4.5相比1.4.4在API上没有变化,其Changelog为:

  • 支持jimUrl形式配置
  • 支持自动扩容
  • 通过com.jd.jim.cli.compat.ReloadableJimClientFactoryBean兼容configId+token配置形式


其中,超时时间、连接池以及读写分离的情形可以通过点击以下界面按钮自行设置:



公共测试环境:

1.4.5客户端除支持configId和token的配置方式外,还支持jimUrl的简化配置方式,在测试机上配置hosts:192.168.150.61 cfs.jim.jd.local,以下测试环境的jimUrl可任选其一来用:

 

1

jim://1803528818953446384/1

2

jim://1803528671997086613/2

3jim://2809907038999011671/4
4jim://2809907022807591111/5

  如果不喜欢上面这种方式,可继续按照老的configId+token的方式使用以前提供的测试环境(公用configId和token现有8个)。

自已可控的专有测试环境:

第一步:    jim://2870414099454224775/3

第二步:   配置host

192.168.150.61 cfs.jim.jd.local

<redis实例所在的IP> redis.local

第三步:部署redis实例:
  1.  最新版本(055e848cb62e1f4dac31a542d0e67688  jimdb-3.2.24.tgz )
    执行步骤如下:

    mkdir -p /export/Data/redis_data/6379
    mkdir -p /export/Logs/redis/
    mkdir -p /export/servers/
    tar -zxvf jimdb-3.2.24.tgz -C  /export/servers
    nohup /export/servers/jimdb-3.2.24/src/redis-server /export/servers/jimdb-3.2.24/conf/redis_6379.conf &

  2. 在部署服务器通过redis-cli 连接redis实例
    /export/servers/jimdb-3.2.24/src/redis-cli  -h <redis实例所在的IP>  -p   <redis实例端口> 

生产环境:

 生产环境的jimUrl(或configId、token),在垂直运维处理完你的集群申请后,系统会发送邮件通知。


客户端使用配置:建议直接demo下载

(JimDB client 1.4.5 demo下载 兼容老的配置风格)

 

 

通过maven引入jar包
 
<dependency>
    <groupId>com.jd.jim.cli</groupId>
    <artifactId>jim-cli-jedis</artifactId>
    <version>1.4.5-SNAPSHOT</version>
</dependency>
<dependency>
    <groupId>com.jd.jim.cli</groupId>
    <artifactId>jim-cli-api</artifactId>
    <version>1.4.5-SNAPSHOT</version>
</dependency>

 

 

 

 

 

 

 

jimUrl风格配置形式

 

 

Spring环境下配置使用:

注意:在系统退出时,要确保销毁spring容器上下文

project/src/main/resources/spring-conf.xml
<bean id="jimClient" class="com.jd.jim.cli.ReloadableJimClientFactoryBean">
    <property name="jimUrl" value="jim://1803528671997086613/2" />
</bean>
java代码:
@Resource(name = "jimClient")
private Cluster jimClient;
 
public String getByKey(String key){
    return jimClient.get(key);
}

 

 

非Spring环境配置使用:
注意:在系统退出时,调用ReloadableJimClientFactory的clear()方法。
package com.jd.jim.cli.demo;

import com.jd.jim.cli.Cluster;
import com.jd.jim.cli.ReloadableJimClientFactory;


public class JimClientDemo {
    public void close() {
        JimClientFactory.close();
    }

    public String getByKey(String key) {
        return JimClientFactory.getJimClient().get(key);
    }

    private static class JimClientFactory {
        private static final ReloadableJimClientFactory clientFactory;                                                                  
        private static final Cluster CLIENT_INSTANCE;

        static {
            clientFactory = new ReloadableJimClientFactory();
            clientFactory.setJimUrl("jim://1803528671997086613/2");

            CLIENT_INSTANCE = clientFactory.getClient();
        }

        public static Cluster getJimClient() {
            return CLIENT_INSTANCE;
        }

        public static void close() {
            if (clientFactory != null) {
                clientFactory.clear();
            }
        }
    }
}

 

 

Spring Cache注解方式:

spring-conf.xml

<bean id="configClient" class="com.jd.jim.cli.config.client.ConfigLongPollingClientFactoryBean">
    <property name="serviceEndpoint" value="http://cfs.jim.jd.local"></property>
</bean>

<bean id="jimClient" class="com.jd.jim.cli.ReloadableJimClientFactoryBean">
    <property name="configClient" ref="configClient"></property><!-- configured to your needs -->
    <property name="jimUrl" value="jim://1803528818953446384/1" />
</bean>

<bean id="cacheManager" class="com.jd.jim.cli.springcache.JimCacheManager">
    <property name="caches">
        <list>
            <bean class="com.jd.jim.cli.springcache.JimStringCache">
                <!-- key前缀 -->
                <property name="keyPrefix" value="cachetest_" />
                <!-- 客户端对象 -->
                <property name="jimClient" ref="jimClient" />
                <!-- TTL时间(以'秒'为单位) -->
                <property name="entryTimeout" value="60" />
                <!-- 值的序列化器 -->
                <property name="valueSerializer">
                    <bean class="com.jd.jim.cli.serializer.DefaultStringSerializer" />
                </property>
            </bean>
        </list>
    </property>
</bean>

<cache:annotation-driven cache-manager="cacheManager" />
 
 

java代码:

@Cacheable(value = "cachetest_", key = "#key")
public String getMessage(String key) {
    System.out.println("miss, get from db...");
    return "msg";
}

@CachePut(value = "cachetest_", key = "#key")
public String setMessage(String key, String message) {
    return message;
}

@Caching(evict = { @CacheEvict(value = "cachetest_", key = "#key") })
public boolean delete(String key) {
    return true;
}

 

 

 

 

 

 

 

 

 

 
1.4.5新旧机房相同配置方式说明:
兼容旧的配置方式

(访问新旧机房配置方式都相同)

相同的com.jd.jim.cli.compat.ReloadableJimClientFactoryBean,相同的工厂com.jd.jim.cli.compat.ReloadableJimClientFactory,相同的配置项configId、token,相同的api。

 

Spring环境下配置使用:
<bean id="objectSerializer" class="com.jd.jim.cli.demo.CustomObjectSerializer">
    <property name="enableCompress" value="false"></property><!-- 关闭byte[]类型的value压缩,如果无特殊需求不要这个bean也行 -->
</bean>

<!-- 黄村 -->
<bean id="jimClientHC" class="com.jd.jim.cli.compat.ReloadableJimClientFactoryBean">
    <!-- <property name="objectSerializer" ref="objectSerializer" /> -->
    <property name="configId" value="/redis/cluster/19" />
    <property name="token" value="1417632068813" />
</bean>

<!-- 廊坊 -->
<bean id="jimClientLF" class="com.jd.jim.cli.compat.ReloadableJimClientFactoryBean">
    <!-- <property name="objectSerializer" ref="objectSerializer" /> -->
    <property name="configId" value="jim://1803528671997086613/2" />
    <property name="token" value="jim://1803528671997086613/2" />
</bean>  
 

 

 

非Spring环境配置使用:
package com.jd.jim.cli.demo;

import com.jd.jim.cli.Cluster;
import com.jd.jim.cli.compat.ReloadableJimClientFactory;


public class JimClientDemo {
    public void close() {
        JimClientFactory.close();
    }

    public String getByKey(String key) {
        return JimClientFactory.getJimClient().get(key);
    }

    private static class JimClientFactory {
        private static final ReloadableJimClientFactory clientFactory;
        private static final Cluster CLIENT_INSTANCE;

        static {
            clientFactory = new ReloadableJimClientFactory();
            clientFactory.setConfigId("jim://1803528671997086613/2");                                                                  
            clientFactory.setToken("jim://1803528671997086613/2");
            CLIENT_INSTANCE = clientFactory.getClient();
        }

        public static Cluster getJimClient() {
            return CLIENT_INSTANCE;
        }

        public static void close() {
            if (clientFactory != null) {
                clientFactory.clear();
            }
        }
    }
}
 

                                                                   Jimdb S功能特性                                   

   

                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         

   
                      
   
  •                        
  •                    
            
                       
                          

Jimdb S是一个完全兼容redis协议的大容量高速NoSQL数据库,存储大容量数据的同时提供高性能和低延迟, 用SSD持久化数据。使用Jimdb S以后可以保存全量数据,把缓存+数据库的两层架构用一层架构取代,提供更低的延迟,同时简化应用逻辑,不需要再关心缓存更新和失效的问题,避免数据不一致的风险。

 

主要特性:

1) 可靠性:
Jimdb S可以保证即使操作系统崩溃或断电的情况下,最多丢失30毫秒内写入的数据。

如果配置了主从复制,可以用同步命令的方式保证数据写入多个副本后才返回成功,保证成功写入的数据即使发生意外也不会丢失。

 

2)可扩展性:

Jimdb S支持平滑的迁移和扩展,可以通过增加分片承担更大的访问压力和更大的数据容量,理论上单集群可以支持上百TB的数据。

 

3)单实例读性能:
Jimdb S的读可以分为两种情况:

A,数据在内存命中;

B,数据磁盘文件中,需要硬盘IO。

A的性能和原生redis是相同的,ops可以达到十万以上,延迟低于1ms。B 的延迟基本在2ms - 20ms之间,大多数情况下小于10ms,并发越多,延迟会越高,总的ops在3万左右。

所以实际应用场景的读性能取决于读请求中属于A, B 两种情况的比例。热点数据越集中,性能越好。

 

4)单实例写性能:
Jimdb S的写性能在value 1KB的情况下tps可以达到3万左右,延迟小于1ms。读写混合时,读IO(前面提到的B情况下)和写IO之和在3万左右。

 

5)范围查询功能:
Jimdb S提供一个范围查询命令,可以以字典序查询指定范围内的key,格式如下:

JRANGE [{start} ({end} LIMIT <offset> <limit>

其中’[’代表开区间,’(‘代表闭区间,LIMIT是可选项,后面跟两个参数。

 

JRANGE [key001 (key005

会返回 key001, key002, key003, key004;

JRANGE [key001 (key005 LIMIT 1 2

会返回 key002, key003。

 

6)主从复制:
Jimdb S的复制采用与redis兼容的rdb模式,这种方式兼容性最好,缺点是速度比较慢,100G的数据可能需要几十分钟才能复制完成。

 

7)启动时间:

redis如果配置了持久化,启动时会把所有数据从硬盘载入内存,如果数据量大,会需要几十秒到几分钟的时间,而Jimdb S启动时先从索引文件中载入索引,通常索引文件远小于数据文件,因而加快了启动速度,如果索引没有保存才会从数据文件中读取数据创建索引。

 

8)数据统计:

Jimdb S除了支持redis已有的统计数据之外,还添加了网络流量统计和延迟统计,在客户端遇到高延迟的问题时,更容易定位问题的原因。

 

注意事项:

1) Jimdb S目前不支持大数据结构,最大限制为1024,如果元素个数超过这个值,性能会有下降。建议将这部分数据分离出来单独存储。

2) Jimdb S单实例的数据大小最好不要超过100GB,太大的实例会导致主从复制的时间太长。

3) Jimdb S会将全量索引保存在内存中,如果数据的value都很小,则需要比较多的内存。

 

设计和实现:

 

1)写入逻辑:

当Jimdb S在内存里完成写操作时,不会立即执行写cycledb操作,而是把修改过的key放入dirtyKeys map里,在每30毫秒执行的时间任务里,把脏数据写入cycledb。

在前台执行写入cycledb时,会在内存中更新索引和元数据,然后再触发一次磁盘文件的顺序写,性能很高。数据会在30毫秒的周期内持久化在硬盘上,即使系统崩溃,最多丢失30毫秒的数据。

redis的数据结构类型有5种,string/list/hash/set/zset,在Jimdb S里,这5种类型在写入cycledb时会统一用序列化rdb的方式生成一个string,默认使用redis自带的LZF的压缩算法进行压缩,LZF是一种高速的压缩算法,即使数据是已经压缩过的,再压缩一遍开销也是很低的。

 

2)内存硬盘数据交换逻辑:

当Jimdb S接收到一个请求时,首先需要算出这个命令里涉及到的key有哪些,在执行这个命令之前,我们需要保证这些key都已经载入内存,Jimdb S会在内存里查找这些key,如果这些key在内存里都有,那就会立即执行这个命令,如果有的key在内存找不到,就需要查询cycledb,并把cycledb里的数据载入内存,这个函数名loadKeyValues。

如果我们在主线程执行读cycledb的命令,那么主线程很可能会阻塞几毫秒,因为redis是单线程模型,这会严重影响性能,所以读取cycledb的操作需要在后台线程完成。而直接把后台读取cycledb得到的数据载入内存会带来一个问题,就是数据不一致,有可能导致旧数据覆盖新数据。Jimdb S的解决方案是, 第一次执行loadKeyValues的时候,遇到需要读取cycledb的时候,创建后台任务读取cycledb, 这个后台任务的作用实际上并不是读取,而是预热,读到数据后并不把结果载入内存,执行完成后,前台主线程会重新执行一次loadKeyValues,这次直接在前台读取cycledb并载入内存,因为这个key刚刚被读取过一次,所以一定会被操作系统缓存,第二次读不会有硬盘IO,所以不会阻塞,操作瞬间完成。

当Jimdb S的内存达到配置的maxmemory时,会用随机的lru算法淘汰内存里较久没有访问过的key,保证内存使用量在maxmemory之内,因为数据在硬盘上全量保存,所以即使key被淘汰,数据也不会丢失。

 

3)主从复制逻辑:

原生redis在复制时需要fork一个后台进程dump RDB文件,如果没有设置overcommit_memory,有可能会fork失败。而Jimdb S完全不需要fork后台进程,所有操作都在一个进程内完成,通过后台线程生成数据快照。

当Jimdb S接收到从发来的复制请求时,会启动后台线程遍历cycledb里的数据,保存在一个rdb文件里,这个过程消耗的时间跟数据文件中的数据量有关,数据量越大耗时会越久,完成之后,Jimdb S会把这个rdb文件发送给从,传输的速度会受到网卡和硬盘读写速度的限制。当Jimdb S作为从接收到rdb文件后会读取并载入内存,并持续地把新写入的脏数据写入cycledb,这个过程也会消耗很长时间。

由于一个Jimdb S实例会保存几十G到上百G的数据,主从全量复制的代价很高,如果一块硬盘上多个实例同时复制,很可能导致硬盘写满,或者占用大量网卡带宽,所以jimdb加入了全量复制锁save-lock,在同一台主机上,同一时间只有一个实例会保存快照,如果多个实例同时接受到从发来的全量同步请求,只有获取文件锁的那个实例才会执行复制操作,其他的实例会定期重试。

 

未来的开发计划:

1) 智能动态的调整内部参数配置,减少需要人工调整的配置项,预防配置失误的风险。

2) 支持多种存储引擎。

 

           

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值