Guava Cache 原理分析与最佳实践-程序员宅基地

技术标签: cache  guava  # 《深入理解Java虚拟机》  

前言

        在大部分互联网架构中 Cache 已经成为了必可不少的一环。常用的方案有大家熟知的 NoSQL 数据库(Redis、Memcached),也有大量的进程内缓存比如 EhCache 、Guava Cache、Caffeine 等。

        本讲主要针对本地 Cache 的老大哥 Guava Cache 进行介绍和分析,会选取本地缓存和分布式缓存(NoSQL)的优秀框架比较他们各自的优缺点、应用场景、项目中的最佳实践以及原理分析。


一、Guava Cache介绍

        Guava Cache 是 google 开源的一款本地缓存工具库,它的设计灵感来源于ConcurrentHashMap,使用多个 segments 方式的细粒度锁,在保证线程安全的同时,支持高并发场景需求,同时支持多种类型的缓存清理策略,包括基于容量的清理、基于时间的清理、基于引用的清理等。

        传统的JVM 缓存,是堆缓存,其实就是创建一些全局容器,比如:List、Set、Map等。这些容器用来做数据存储还可以,却不能按照一定的规则淘汰数据,如 LRU,LFU,FIFO 等,也没有清除数据时的回调通知,而且多线程不安全。虽然针对高并发可以使用CurrentHashMap,但是过期处理和数据刷新都需要手动完成。

        相比较而言,同样是基于 JVM 缓存的 Guava Cache 就显得优势明显,且很有必要:

1. 缓存过期和淘汰机制

        在Guava Cache中可以设置Key的过期时间,包括访问过期和创建过期Guava Cache在缓存容量达到指定大小时,采用LRU的方式,将不常使用的键值从Cache中删除;

2. 并发处理能力

        Guava Cache类似CurrentHashMap,是线程安全的。它提供了设置并发级别的API,使得缓存支持并发的写入和读取;

        像ConcurrentHashMap结构类似,GuavaCache也使用Segment做分区,采用分离锁机制,分离锁能够减小锁力度,提升并发能力分离锁是分拆锁定,把一个集合看分成若干partition, 每个partiton一把锁。ConcurrentHashMap就是分了16个区域,这16个区域之间是可以并发的。

3. 更新锁定

        一般情况下,在缓存中查询某个key,如果不存在,则查源数据,并回填缓存。在高并发下会出现,多次查源并重复回填缓存,可能会造成源的宕机(DB),性能下降 Guava Cache 可以在 CacheLoader 的load方法中加以控制,对同一个key,只让一个请求去读源并回填缓存,其他请求阻塞等待。

4. 集成数据源

        一般我们在业务中操作缓存,都会操作缓存和数据源两部分。Guava Cache 的 get 可以集成数据源,在从缓存中读取不到时,可以从数据源中读取数据并回填缓存。

5. 监控缓存加载/命中情况

        既然是缓存服务,那统计数据的功能自然也是少不了。


二、基本用法

Guava Cache 的 maven 依赖:

<dependency>
		<groupId>com.google.guava</groupId>
		<artifactId>guava</artifactId>
		<version>21.0</version>
</dependency>

2.1 构建Cache对象

        Guava Cache 通过简单好用的 Client 可以快速构造出符合需求的 Cache 对象,不需要过多复杂的配置,大多数情况就像构造一个 POJO 一样的简单。

        这里介绍两种构造 Cache 对象的方式:CacheLoader 和 Callable

2.1.1 CacheLoader

        构造 LoadingCache 的关键在于实现 load 方法,也就是在需要访问的缓存项不存在的时候 Cache 会自动调用 load 方法将数据加载到 Cache 中。这里你肯定会想假如有多个线程过来访问这个不存在的缓存项怎么办,也就是缓存的并发问题如何怎么处理是否需要人工介入,这些在下文中也会介绍到。

除了实现 load 方法之外还可以配置缓存相关的一些性质,比如:过期加载策略、刷新策略 。

private static final LoadingCache<String, String> CACHE = CacheBuilder
    .newBuilder()
    // 最大容量为 100 超过容量有对应的淘汰机制,下文详述
    .maximumSize(100)
    // 缓存项写入后多久过期,下文详述
    .expireAfterWrite(60 * 5, TimeUnit.SECONDS)
    // 缓存写入后多久自动刷新一次,下文详述
    .refreshAfterWrite(60, TimeUnit.SECONDS)
    // 创建一个 CacheLoader,load 表示缓存不存在的时候加载到缓存并返回
    .build(new CacheLoader<String, String>() {
    // 加载缓存数据的方法
        @Override
        public String load(String key) {
            return "cache [" + key + "]";
        }
});

public void getTest() throws Exception {
    CACHE.get("KEY_25487");
}

2.1.2 Callable

        除了在构造 Cache 对象的时候指定 load 方法来加载缓存外,我们亦可以在获取缓存项时指定载入缓存的方法,并且可以根据使用场景在不同的位置采用不同的加载方式。

        比如,在某些位置可以通过二级缓存加载不存在的缓存项,而有些位置则可以直接从 DB 加载缓存项。

// 注意返回值是 Cache
private static final Cache<String, String> SIMPLE_CACHE = CacheBuilder
    .newBuilder()
    .build();

public void getTest1() throws Exception {
    String key = "KEY_25487";
    // get 缓存项的时候指定 callable 加载缓存项
    SIMPLE_CACHE.get(key, () -> "cache [" + key + "]");
}

2.1.3 CacheLoader和Callable的区别

        CacheLoader 和 Callable 都实现了一种逻辑:先取缓存,如果取不到相关key对应的内容,再执行 load 或者 call 方法中自己实现的逻辑来获取相关数据并缓存起来,不同点是:

  • CacheLoader 是按 key 统一加载,所有取不到的统一执行一种 load 逻辑;
  • Callable 方法允许在 get 的时候指定 key,传入一个Callable实例并实现加载逻辑。

2.2 CacheBuilder参数介绍

CacheBuilder是一个用于构建Cache的类,是建造者模式的一个例子,主要的方法有:

  1. maximumSize(long maximumSize):设置缓存存储的所有元素的最大个数。(慎重设置)
  2. maximumWeight(long maximumWeight):设置缓存存储的所有元素的最大权重。
  3. expireAfterAccess(long duration, TimeUnit unit):设置元素在最后一次访问多久后过期。
  4. expireAfterWrite(long duration, TimeUnit unit):设置元素在写入缓存后多久过期。
  5. concurrencyLevel(int concurrencyLevel):设置并发水平,即允许多少线程无冲突的访问Cache,默认值是4,该值越大,LocalCache中的segment数组也会越大,访问效率越高,当然空间占用也大一些。
  6. removalListener(RemovalListener<? super K1, ? super V1> listener):设置元素删除通知器,在任意元素无论何种原因被删除时会调用该通知器。
  7. setKeyStrength(Strength strength):设置元素的key是强引用,还是弱引用,默认强引用,并且该属性也指定了EntryFactory使用是强引用还是弱引用。
  8. setValueStrength(Strength strength):设置元素的value是强引用,还是弱引用,默认强引用。

2.3 get和put

Cache的存放数据的方法只有一种,和map一样:put(K,V);拿取的方法有三种,区别如下:

  1. get(K):使用这个方法要么返回已经缓存的值,要么使用CacheLoader向缓存原子地加载新值。由于CacheLoader可能抛出异常,LoadingCache.get(K)也声明为抛出ExecutionException异常。如果你定义的CacheLoader没有声明任何检查型异常,则可以通过 getUnchecked(K) 查找缓存;但必须注意,一旦CacheLoader声明了检查型异常,就不可以调用getUnchecked(K)。
  2. getIfPresent(key):从现有的缓存中获取,如果缓存中有key,则返回value,如果没有则返回null,不加载load()方法;
  3. getAll(Iterable<? extends K>):用来执行批量查询。默认情况下,对每个不在缓存中的键,getAll方法会单独调用CacheLoader.load来加载缓存项。可以通过重写 load()方法来提高加载缓存的效率;

2.4 自动加载

        Cache的get方法有两个参数,第一个参数是要从Cache中获取记录的key,第二个记录是一个Callable对象。

        当缓存中已经存在key对应的记录时,get方法直接返回key对应的记录。如果缓存中不包含key对应的记录,Guava会启动一个线程执行Callable对象中的call方法,call方法的返回值会作为key对应的值被存储到缓存中,并且被get方法返回。

        Guava可以保证当有多个线程同时访问Cache中的一个key时,如果key对应的记录不存在,Guava只会启动一个线程执行get方法中Callable参数对应的任务加载数据存到缓存。当加载完数据后,任何线程中的get方法都会获取到key对应的值。

String value = cache.get("key", new Callable<String>() {
    public String call() throws Exception {
        // 在这里写从DB获取数据的方法并返回
        // 模拟加载时间
        Thread.sleep(1000);                 
        return "auto load by Callable";
    }
});

2.5 缓存回收策略

不管是磁盘也好,内存也罢,我们的空间都不是无限的。所以,我们必须决定:什么时候某个缓存项就不值得保留了。

Guava Cache提供了三种基本的缓存回收方式:基于容量回收、定时回收和基于引用回收。

2.5.1 基于容量的回收

如果要规定缓存项的数目不超过固定值,只需使用CacheBuilder.maximumSize(long)。缓存将尝试回收最近没有使用或总体上很少使用的缓存项。

警告在缓存项的数目达到限定值之前,缓存就可能进行回收操作,通常来说,这种情况发生在缓存项的数目逼近限定值时

Cache<String,String> cache = CacheBuilder.newBuilder()
        .maximumSize(2)
        .build();
        
cache.put("key1","value1");
cache.put("key2","value2");
System.out.println("第一个值:" + cache.getIfPresent("key1"));
System.out.println("第二个值:" + cache.getIfPresent("key2"));

cache.put("key3","value3");
System.out.println("第一个值:" + cache.getIfPresent("key1"));
System.out.println("第二个值:" + cache.getIfPresent("key2"));
System.out.println("第三个值:" + cache.getIfPresent("key3"));

结果:
第一个值:value1
第二个值:value2
第一个值:null
第二个值:value2
第三个值:value3

2.5.2 定时回收

CacheBuilder提供两种定时回收的方法:

  • expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。
  • expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写操作(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。

定时回收在周期性地在写操作中执行,偶尔在读操作中执行。

2.5.3 基于引用的回收

通过weakKeys和weakValues方法指定Cache只保存对缓存记录key和value的弱引用。这样当没有其他强引用指向key和value时,key和value对象就会被垃圾回收器回收。

  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
  • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值。
Cache<String,Object> cache = CacheBuilder.newBuilder()
            .maximumSize(2)
            .weakValues()
            .build();

2.6 主动清除

可以调用Cache的 invalidate 或 invalidateAll 方法显示删除Cache中的记录。

  • invalidate方法:一次只能删除Cache中一个记录,接收的参数是要删除记录的key。
  • invalidateAll方法:可以批量删除Cache中的记录,当没有传任何参数时,invalidateAll方法将清除Cache中的全部记录。invalidateAll也可以接收一个Iterable类型的参数,参数中包含要删除记录的所有key值。
Cache<String,String> cache = CacheBuilder.newBuilder().build();
Object value = new Object();
cache.put("key1","value1");
cache.put("key2","value2");
cache.put("key3","value3");

// 1.清除指定的key
cache.invalidate("key1");

// 2.批量清除list中全部key对应的记录
List<String> list = new ArrayList<String>();
list.add("key1");
list.add("key2");
cache.invalidateAll(list);

2.7 移除动作监听器

可以为Cache对象添加一个移除监听器,这样当有记录被删除时可以感知到这个事件。

RemovalListener<String, String> listener = new RemovalListener<String, String>() {
    public void onRemoval(RemovalNotification<String, String> notification) {
        System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
    }
};
Cache<String,String> cache = CacheBuilder.newBuilder()
        .maximumSize(3)
        .removalListener(listener)
        .build();

2.8 统计信息

可以对Cache的命中率、加载数据时间等信息进行统计。在构建Cache对象时,可以通过CacheBuilder的recordStats方法开启统计信息的开关。开关开启后Cache会自动对缓存的各种操作进行统计,调用Cache的stats方法可以查看统计后的信息。

Cache<String,String> cache = CacheBuilder.newBuilder()
            .maximumSize(3)
            .recordStats() //开启统计信息开关
            .build();
System.out.println(cache.stats()); //获取统计信息

三、缓存项加载机制

        如果某个缓存过期了或者缓存项不存在于缓存中,而恰巧此此时有大量请求过来请求这个缓存项,如果没有保护机制就会导致大量的线程同时请求数据源加载数据并生成缓存项,就算某一个线程率先获取到数据生成了缓存项,其他的线程还是继续请求 DB 而不会走到缓存,这就是所谓的 “缓存击穿”

        看到上面这个图或许你已经有方法解这个问题了,如果多个线程过来如果我们只让一个线程去加载数据生成缓存项,其他线程等待然后读取生成好的缓存项岂不是就完美解决。那么恭喜你在这个问题上,和 Google 工程师的思路是一致的。不过采用这个方案,解决了缓存击穿,却又会引入线程阻塞的新问题。

        其实, Guava Cache 在 load 的时候做了并发控制,在多个线程请求一个不存在或者过期的缓存项时保证只有一个线程进入 load 方法,其他线程等待直到缓存项被生成,这样就避免了大量的线程击穿缓存直达 DB 。

        不过,试想下如果有上万 QPS 同时过来会有大量的线程阻塞导致线程无法释放,甚至会出现线程池满的尴尬场景,这也是说为什么这个方案解了 “缓存击穿” 问题但又没完全解

        上述机制其实就是 expireAfterWrite / expireAfterAccess 来控制的,如果你配置了过期策略,对应的缓存项在过期后被访问就会走上述流程来加载缓存项。


四、缓存项刷新机制

缓存项的刷新和加载看起来是相似的,都是让缓存数据处于最新的状态。区别在于:

  1. 缓存项加载是一个被动的过程,而缓存刷新是一个主动触发动作。如果缓存项不存在或者过期只有下次 get 的时候才会触发新值加载。而缓存刷新则更加主动替换缓存中的老值。
  2. 另外一个很重要点的在于,缓存刷新的项目一定是存在缓存中的,他是对老值的替换而非是对 NULL 值的替换。

        由于缓存项刷新的前提是该缓存项存在于缓存中,那么缓存的刷新就不用像缓存加载的流程一样让其他线程等待而是允许一个线程去数据源获取数据,其他线程都先返回老值直到异步线程生成了新缓存项

        这个方案完美解决了上述遇到的 “缓存击穿” 问题,不过他的前提是已经生成缓存项了。在实际生产情况下我们可以做 缓存预热 ,提前生成缓存项,避免流量洪峰造成的线程堆积。

        这套机制在 Guava Cache 中是通过 refreshAfterWrite 实现的,在配置刷新策略后,对应的缓存项会按照设定的时间定时刷新,避免线程阻塞的同时保证缓存项处于最新状态。

        但他也不是完美的,比如他的限制是缓存项已经生成,并且如果恰巧你运气不好,大量的缓存项同时需要刷新或者过期, 就会有大量的线程请求 DB,这就是常说的 “缓存血崩”


五、缓存项异步刷新机制

上面说到缓存项大面积失效或者刷新会导致雪崩,那么就只能限制访问 DB 的数量了,位置有三个地方:

  1. 源头:因为加载缓存的线程就是前台请求线程,所以如果控制请求线程数量的确是减少大面积失效对 DB的请求,那这样一来就不存在高并发请求,就算不用缓存都可以。
  2. 中间层缓冲:因为请求线程和访问 DB 的线程是同一个,假如在中间加一层缓冲,通过一个后台线程池去异步刷新缓存所有请求线程直接返回老值,这样对于 DB 的访问的流量就可以被后台线程池的池大小控住。
  3. 底层:直接控 DB 连接池的池大小,这样访问 DB 的连接数自然就少了,但是如果大量请求到连接池发现获取不到连接程序一样会出现连接池满的问题,会有大量连接被拒绝的异常。

        所以,比较合适的方式是通过添加一个异步线程池异步刷新数据,在 Guava Cache 中实现方案是重写 Cache Loader 的 reload 方法

private static final LoadingCache<String, String> ASYNC_CACHE = CacheBuilder.newBuilder()
    .build(
        CacheLoader.asyncReloading(new CacheLoader<String, String>() {
    
        @Override
        public String load(String key) {
            return key;
        }

        @Override
        public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
            return super.reload(key, oldValue);
        }
    }, new ThreadPoolExecutor(5, Integer.MAX_VALUE,
    60L, TimeUnit.SECONDS,
    new SynchronousQueue<>()))
);

六、LocalCache 源码分析

        先整体看下 Cache 的类结构,下面的这些子类表示了不同的创建方式本质还都是 LocalCache。

        核心代码都在 LocalCache 这个文件中,并且通过这个继承关系可以看出 Guava Cache 的本质就是 ConcurrentMap。

在看源码之前先理一下流程,先理清思路,源码太多就不一一粘贴了。这里核心理一下 Get 的流程,put 阶段比较简单就不做分析了。


总结

        Guava Cache 没有额外的线程去做数据清理和刷新的,基本都是通过 Get 方法来触发这些动作,减少了设计的复杂性和降低了系统开销。

        回顾下 Get 的流程以及在每个阶段做的事情,返回的值。首先判断缓存是否过期然后判断是否需要刷新,如果过期了就调用 loading 去同步加载数据(其他线程阻塞),如果是仅仅需要刷新调用 reloading 异步加载(其他线程返回老值)。

        注意,如果 refreshTime > expireTime 意味着永远走不到缓存刷新逻辑,缓存刷新是为了在缓存有效期内尽量保证缓存数据一致性,所以,在配置刷新策略和过期策略时一定保证 refreshTime < expireTime 。

最后关于 Guava Cache 的使用建议 (最佳实践) :

  1. 如果刷新时间配置的较短一定要重载 reload 异步加载数据的方法,传入一个自定义线程池保护 DB;
  2. 失效时间一定要大于刷新时间;
  3. 如果是常驻内存的一些少量数据,失效时间可以配置的较长,刷新时间配置短一点 (根据业务对缓存失效容忍度)。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_44259720/article/details/125890496

智能推荐

windows下配置IIS以及优化配置_windows iis-程序员宅基地

文章浏览阅读2.7k次。IIS配置及优化_windows iis

Python爬虫入门教程 80-100 Python 玩转NewSpaper爬虫框架_python 爬虫框架 newspaper-程序员宅基地

文章浏览阅读1.9w次,点赞3次,收藏6次。虽然newspaper框架存在不完美的地方,但是依然需要认识它_python 爬虫框架 newspaper

MP4学习(一)MP4格式分析_vc mvhd box-程序员宅基地

文章浏览阅读8.1k次,点赞4次,收藏15次。MP4格式分析mp4文件由一系列的box(也有人把它叫做atom)构成,每个box包含box头部和box体。box体可以包含普通的数据,也可以包含其他的box,如果box中包含了另一个box,这种box称为container box。box由head和body构造,head中指明了box的大小和类型,如果size为1,则表示这个box的大小为large size,真正的si_vc mvhd box

生产者与消费者 模型理解,与简单设计-程序员宅基地

文章浏览阅读571次。题外话:从中秋请假到现在,接近20天的假期,让我彻底耍晕了,现在来写代码,发现始终没进入状态。以后建议大家没特殊事情,还是别请这么久啦,分开耍,有利于放松,也不至于耍得忘乎所以。我是一直想做互联网 并发 大数据方面的工作,有兴趣才有动力嘛,企业开发感觉活力不强,太多重复的劳动,还是喜欢研究 解决问题,有挑战的东东。线程这块,生产者与消费者模型一直是很经典的东东,这里我用自己的理解分享一下,有误..._现场总线生产者与消费者模型分析

MyBatis-Plus使用queryWrapper解决字符串中含数字的排序问题_mybatis字符串数字排序-程序员宅基地

文章浏览阅读7.4k次,点赞6次,收藏9次。今天遇到了Oracle数据库字符串(含数字)排序问题,这里记录的是如何用MyBatis-Plus的queryWrapper条件构造器来解决的方法。造成的原因:数据库字段为varchar类型(若为Number类型则无此问题)数据库字符串排序是按照顺序一位一位比较的,按照ascII码值比较。如:2比1大,所以12会排在2前面解决办法:先按字符串长度排序,再按字段排序关键代码(queryWrapper条件构造器实现形式)//利用数据库length函数获取字符串长度(这里的code是我数据库中的_mybatis字符串数字排序

kube operator部署kubernetes集群_kubeop-程序员宅基地

文章浏览阅读1.3k次。kube operator简介kube operator是一个kubernetes集群部署及多集群管理工具,提供web ui支持在离线环境部署多个kubernetes集群。KubeOperator 是一个开源项目,通过 Web UI 在 VMware、OpenStack 和物理机上规划、部署和运营生产级别的 Kubernetes 集群。支持内网离线环境、支持 GPU、内置应用商店,已通过 CNCF 的 Kubernetes 软件一致性认证。官网:https://kubeoperator.io/离线包_kubeop

随便推点

CentOS 7 安装最新版Docker教程_centos7安装最新版dockers-程序员宅基地

文章浏览阅读781次。docker安装官方文档:Install Docker Engine on CentOS2、安装提供了工具3、通过添加docker repository如果出现上面的错误提示,可通阿里源进行添加4、安装docker4.1、直接安装最新版本这步完成后可直接跳至启动docker4.2、或者安装指定版本按版本号倒序列出可安装版本列表安装指定版本例如安装20.10.9版本5、启动docker通过进行启动设置docker服务开机启动6、测试7、卸载docker下的_centos7安装最新版dockers

敏感字识别算法基于JDK8 lambada表达式_敏感文本识别算法-程序员宅基地

文章浏览阅读429次。package aaa.bbb.demo;import java.util.ArrayList;import java.util.List;public class RecognitionDemo { public static void main(String[] args) { String str1="SB哈NM哈哈哈WBDhdsada"; String str_敏感文本识别算法

华为鸿蒙系统(Huawei HarmonyOS)

华为鸿蒙系统Huawei HarmonyOS

JS读取粘贴板内容-程序员宅基地

文章浏览阅读4.9k次。1.1 监听onpaste事件1.1.1 定义和用法npaste 事件在用户向元素中粘贴文本时触发。注意:虽然使用的 HTML 元素都支持 onpaste 事件,但实际上并非支持所有元素,例如 <p> 元素, 除非设置了 contenteditable 为 "true" (查看下文的更多实例)。提示:onpaste 事件通常用于 type="text" 的 ..._js 获取粘贴板内容 移动端

Win10系统下软件UI显示不完整解决方案_用苹果设计的ui在win显示不明显-程序员宅基地

文章浏览阅读4.7k次,点赞2次,收藏4次。在最初升级win10的时候就想到了这些问题,例如和各种软件的不兼容性。当然,事实上win10并没有想象的那么糟,作为一个windows user 来说,win10的确是很高大上的,无论是颜值或者是体验,都让人爱不释手。下面我就说一下最近和win10的那些事之一:在win10系统下安装了IBM Cognos Transform后,Transform的一些设计界面不能完全显示,而后我们就是要想办法_用苹果设计的ui在win显示不明显

HTTP方式在线访问Hadoop HDFS上的文件解决方案

为了通过HTTP方式在线访问HDFS上的文件,您可以利用WebHDFS REST API或者HttpFS Gateway这两种机制实现。1:httpfs是cloudera公司提供的一个hadoop hdfs的一个http接口,通过WebHDFS REST API 可以对hdfs进行读写等访问2:与WebHDFS的区别是不需要客户端可以访问hadoop集群的每一个节点,通过httpfs可以访问放置在防火墙后面的hadoop集群3:httpfs是一个Web应用,部署在内嵌的tomcat中。