ES与Redis实现千万级数据的范围查询性能比较,远程 http调用耗时也能降低到0ms

原创 吴就业 125 0 2019-11-04

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/2defb4194fee4512875aa80122deadb0

作者:吴就业
链接:https://wujiuye.com/article/2defb4194fee4512875aa80122deadb0
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

如何使用redis实现IP库的范围查询可以看上篇《基于Redis实现范围查询的IP库缓存设计方案》

本篇内容:

ES与Redis实现IP库的范围查询谁性能更强

由于ip库的每条记录存储的是一段ip范围内的国家、城市和运营商信息,要想查询一个ip的信息,就需要用到像SQL那样的支持大于等于小于等于的查询。

在上一篇文章中,我介绍了如何使用Redis实现范围查询,也是一个很好的Sorted Set使用场景。在此,我再总结一下,使用Redis实现ip库缓存支持范围查询的设计方案,详细设计可以看下上篇文章。

在模拟200个线程并发执行的场景下,平均耗时20ms,因为是本地测的,可能受电脑配置差的影响,而且redis也是单节点,这个数值只能做参考。

但redis的执行命令单线程特性,一个查询耗时长会影响到其它业务的接口的执行耗时,并发越高越严重。

如果是几十万的数据是没有什么问题的,可以轻松应对,但,对于一个有一千两百多万条记录的ip库,且无法简单的用key-value方式存储。最重要的是用于高并发场景,对单次查询的耗时要求非常高。

因此,我想出了用ES替代Redis的第二个方案。

使用ES的实现方案就非常简单,直接创建索引将ip库数据写入ES即可,然后就是使用ES的搜索功能实现快速范围查询。

使用ES的方案,本地测试平均耗时也是二十多毫秒,但毕竟只是本地做的简单测试,并没有说服力,所以需要经过实际的线上考验,才能对比得出结论。

最后是实现两个方式共存,并将ip库的数据查询功能做为单独的服务,通过restful对外提供查询接口。目的是ip的缓存实现策略更换不影响正常的业务,也是让其它项目或微服务能够共用这个ip库。由于只是提供一个简单的查询接口,所以我使用netty+webflux实现。

为每个实现方案部署一个节点,通过负载均衡,将一半请求打到选用redis实现方案的节点,一半请求打到选用es实现方案的节点,最后统计两者在实际线上高并发场景下性能比较,选择平均耗时最短的方案。下面是测试结果比较。

图片

[图为Redis实现方案的查询耗时日记]

图片

[图为Es实现方案的查询耗时日记]

在实际的线上测试结果中,使用redis方案的查询平均耗时是2ms,而es高达10ms,结果很明显,还是使用redis的性能更高。

图片

远程 http调用耗时也能降低到0ms

我提倡的是将IP库作为一个工具服务,提供api给其它服务调用。这样可以封装为一个工具类,在公共组件下。但是,面对高并发的服务,远程调用的耗时又将会是高并发服务的痛点。面对追求高并发低时延的需求,又不得不重新考虑。

如果必须要坚持将IP库查询作为一个独立的服务,那么当前可选的方案只有以下两种:

1、将IP库使用dubbo提供远程调用,注册到注册中心。

2、netty+webflux提供 restful接口,不需要注册中心,仅提供url。

对于方案一,IP库我并不想将它跟业务捆绑在一起,原本只是一个提供ip位置信息查询的服务,如果使用dubbo将会和业务偶尔,因为调用者必须要依赖一个只有一个接口的jar包。而像管理后台和定时任务这些服务如果用到ip库,又不得不再提供restful接口,这就需要dubbo跟restful共存。

dubbo的rpc远程调用相比http性能更高,如果使用方案二,就可能会增加原本高并发服务的单次处理请求的耗时,导致QPS下降。那么,是否能在高并发服务上消除http远程查询 ip信息的耗时呢。

我结合项目中业务需求,想到的方案就是异步http请求。在一次请求中,如果并不是接收到请求就立即需要知道来源ip的位置信息的时候,就可以使用异步的方式,如图。

图片

从图中可以看出,在处理业务逻辑1和2的时候,还不需要知道request的ip所在国家和城市等位置信息。对于这种场景我们就可以考虑使用异步方案,在接收到请求的时候,发送一个异步请求获取位置,当处理到业务逻辑3时,再获取异步请求的响应结果,如果此时异步请求还未完成,再进行阻塞等待,改进后的流程如下。

图片

在项目中我使用的是OkHttp框架实现http请求,而OkHttp就天然支持异步调用。其实同步也是在异步的基础上实现的,同步无非就是发送请求后就立即阻塞当前线程,直到接收到响应后再继续当前线程。不管是dubbo、okhttp,只要是远程调用的,都是异步调用,这句话百分百没有毛病。

来看下我的实现方案。描述ip国家信息的类IP2LocationBean

1、在业务层提供一个包装类AsyncWraper。包装类实现的功能跟代理差不多。

public class AsyncWraper<T> {
    public interface RefProxy<T> {
        T get();
    }
    
    private RefProxy<T> ref;

    public AsyncWraper(RefProxy<T> ref) {
        this.ref = ref;
    }

    public T getRef() {
        // 调用引用对象的get方法获取结果,
        // 对于异步调用,如果请求未得到响应,此时才会阻塞等待结果
        return ref.get();
    }
}

2、在接收请求时,异步调用获取ip位置信息

OkHttpUtils.Future<IP2LocationUtils.IP2LocationBean> future 
    = IP2LocationUtils.asyncLocationBy(platform, request.getIp());
bean.setLocationBeanAsyncWraper(new AsyncWraper<>(() -> {
     try {
           return future.get();
     } catch (Exception e) {
           return null;
     }
}));

使用包装类的好处是,我们无需关心,业务什么时候是第一次需要获取位置信息,我们在接收请求的时候就将请求的参数解析为一个Bean,所以在其它地方需要获取到请求参数的,都必须是调用该bean的getXxx方法,那么就可以将描述ip位置信息的IP2LocationBean类型的字段使用包装类包装,通过setLocationBeanAsyncWraper方法赋值给bean。

3、在需要获取ip位置信息的地方调用

bean.getLocationBeanAsyncWraper.getRef().getCountryCode();

当调用getRef时,实际上是调用future的get方法获取结果。这就是提供包装类的目的。

4、实现异步请求

/**
     * 异步获取结果,在业务之前发送请求,
     * 在业务需要获取请求结果时再调用get
     */
    public static class Future<T> {

        private Class<T> tClass;
        private volatile T response;
        private volatile Exception exception;

        public Future(Class<T> tClass) {
            this.tClass = tClass;
        }

        void setResponse(OkHttpUtils.Response response) {
            try {
                if (response.code == 200) {
                    try {
                        this.response = JSON.parseObject(response.getBody(), tClass);
                    } catch (Exception e) {
                        this.exception = e;
                    }
                } else {
                    exception = new Exception("response code:" + response.code + ", msg:" + response.getBody());
                }
            } finally {
                this.notifyAll();
            }
        }

        void setException(Exception exception) {
            try {
                this.exception = exception;
            } finally {
                this.notifyAll();
            }
        }

        public T get() throws Exception {
            while (response == null || exception == null) {
                this.wait();
            }
            if (exception != null) {
                throw exception;
            }
            return this.response;
        }

    }

    /**
     * 异步get
     *
     * @param url
     * @return Future:在需要获取结果时,调用getResponse获取返回结果,
     * 如果request过程中出错则会在该方法抛出异常
     */
    public static <T> Future<T> asyncSendRequest(String url, Class<T> tClass) {
        Request.Builder builder = new Request.Builder()
                .url(url)
                .get();
        Future<T> future = new Future<>(tClass);
        getHttpClient().newCall(builder.build()).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                future.setException(e);
            }

            @Override
            public void onResponse(Call call, okhttp3.Response okHttpResponse) throws IOException {
                OkHttpUtils.Response response = new OkHttpUtils.Response();
                response.setCode(okHttpResponse.code());
                if (okHttpResponse.isSuccessful()) {
                    response.setBody(okHttpResponse.body().string());
                }
                future.setResponse(response);
            }
        });
        return future;
    }

在将请求放入OkHttp的异步队列之后,就返回一个Future。通过向OkHttp注册回调函数可以拿到请求的响应结果,然后将再结果写入到Future中,外部通过调用Future的get方法获取请求的响应结果。

如果请求未接收到响应结果,异步回调接口就不会被调用,那么Future的get就会堵塞调用者线程,直接接收到请求结果,由回调方法调用Future的setResponse方法将阻塞的业务线程唤醒。

假设ip信息查询接口的平均耗时是2ms,而业务服务的处理一次请求耗时是10ms,由于业务的耗时远远大于ip查询的耗时,故可以达到将远程http调用的ip信息查询耗时降为0ms。

Future使用volatile实现可见性,而wait只会阻塞调用者线程,由于一次请求是在一个线程内完成的,每个请求都是持有独立的一个Future,所以,完美避开加锁操作。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

源码分析Dubbo负载均衡策略的权重如何动态修改

dubbo随机负载均衡的权重很少会用到吗?之前我想给随机负载均衡策略配置权重,各种搜索都找不到答案,包括翻阅官方文档。而且我们项目中用的还是最新的Nacos注册中心,非常无奈,最后只能在源码中寻找答案。

深入理解Dubbo源码,Dubbo与Spring的整合之注解方式分析

dubbo通过BeanFactoryPostProcessor与BeanPostProcessor分别完成ServiceBean的注册与被@Reference注释的属性的依赖注入,通过BeanPostProcessor完成配置文件与相关配置类bean的属性绑定。

Fastjson与Jackson性能问题

可以给出的结论是,jackson在解析大json场景下性能是秒杀fastjson的,不急,我们有图有真相。

基于Redis实现范围查询的IP库缓存设计方案

IP信息库是按区间存储的,拿到一个ip得要知道它所属的范围才能知道它对应哪条记录。本篇介绍如何使用Redis的Sorted Set数据结构实现支持范围查找的IP库缓存方案。

网络编程中,关于Keep-Alive与Idle你了解多少?

有朋友问我,关于使用Netty开发长连接应用,为什么要加一个Keep Alive为true的配置。你是否也有这样的疑惑呢?

我所经历的一次Dubbo服务雪崩,压力山大

服务雪崩,听到这个词就能想到问题的严重性。是的,整个项目,整条业务线都挂了,从该业务线延伸出来的下游业务线也跟着凉了。