关于后端:Java扩展Nginx之七共享内存

欢送拜访我的GitHub

这里分类和汇总了欣宸的全副原创(含配套源码):https://github.com/zq2599/blog_demos

本篇概览

  • 作为《Java扩大Nginx》系列的第七篇,咱们来理解一个实用工具<font color=”blue”>共享内存</font>,正式开始之前先来看一个问题
  • 在一台电脑上,nginx开启了多个worker,如下图,如果此时咱们用了nginx-clojure,就相当于有了四个jvm过程,彼此互相独立,对于同一个url的屡次申请,可能被那四个jvm中的任何一个解决:
  • 当初有个需要:统计某个url被拜访的总次数,该怎么做呢?在java内存中用全局变量必定不行,因为有四个jvm过程都在响应申请,你存到哪个下面都不行
  • 聪慧的您应该想到了redis,的确,用redis能够解决此类问题,但如果不波及多个服务器,而只是单机的nginx,还能够思考nginx-clojure提供的另一个简略计划:共享内存,如下图,一台电脑上,不同过程操作同一块内存区域,拜访总数放入这个内存区域即可:
  • 相比redis,共享内存的益处也是不言而喻的:
  • redis是额定部署的服务,共享内存不须要额定部署服务
  • redis申请走网络,共享内存不必走网络
  • 所以,单机版nginx如果遇到多个worker的数据同步问题,能够思考共享内存计划,这也是咱们明天实战的次要内容:在应用nginx-clojure进行java开发时,用共享内存在多个worker之间同步数据
  • 本文由以下内容组成:
  • 先在java内存中保留计数,放在多worker环境中运行,验证计数不准的问题的确存在
  • 用nginx-clojure提供的Shared Map解决问题

    用堆内存保留计数

  • 写一个content handler,代码如下,用UUID来表明worker身份,用<font color=”blue”>requestCount</font>记录申请总数,每解决一次申请就加一:

    package com.bolingcavalry.sharedmap;
    
    import nginx.clojure.java.ArrayMap;
    import nginx.clojure.java.NginxJavaRingHandler;
    import java.io.IOException;
    import java.util.Map;
    import java.util.UUID;
    import static nginx.clojure.MiniConstants.CONTENT_TYPE;
    import static nginx.clojure.MiniConstants.NGX_HTTP_OK;
    
    public class HeapSaveCounter implements NginxJavaRingHandler {
    
      /**
       * 通过UUID来表明以后jvm过程的身份
       */
      private String tag = UUID.randomUUID().toString();
    
      private int requestCount = 1;
    
      @Override
      public Object[] invoke(Map<String, Object> map) throws IOException {
    
          String body = "From "
                      + tag
                      + ", total request count [ "
                      + requestCount++
                      + "]";
    
          return new Object[] {
                  NGX_HTTP_OK, //http status 200
                  ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map
                  body
          };
      }
    }
  • 批改nginx.conf的<font color=”blue”>worker_processes</font>配置,改为auto,则依据电脑CPU核数主动设置worker数量:

    worker_processes  auto;
  • nginx减少一个location配置,服务类是方才写的HeapSaveCounter:

    location /heapbasedcounter {
      content_handler_type 'java';
      content_handler_name 'com.bolingcavalry.sharedmap.HeapSaveCounter';
    }
  • 编译构建部署,再启动nginx,先看jvm过程有几个,如下可见,除了jps本身之外有8个jvm过程,等于电脑的CPU核数,和设置的worker_processes是合乎的:

    (base) willdeMBP:~ will$ jps
    4944
    4945
    4946
    4947
    4948
    4949
  • Jps
    4943

  • 先用Safari浏览器拜访<font color=”blue”>/heapbasedcounter</font>,第一次收到的响应如下图,总数是1:
  • 刷新页面,UUID不变,总数变成2,这意味着两次申请到了同一个worker的JVM上:
  • 改用Chrome浏览器,拜访同样的地址,如下图,这次UUID变了,证实申请是另一个worker的jvm解决的,总数变成了1:
  • 至此,问题失去证实:多个worker的时候,用jvm的类的成员变量保留的计数只是各worker的状况,不是整个nginx的总数
  • 接下来看如何用共享内存解决此类问题

    对于共享内存

  • nginx-clojure提供的共享内存有两种:Tiny Map和Hash Map,它们都是key&value类型的存储,键和值均能够是这四种类型:int,long,String, byte array
  • Tiny Map和Hash Map的区别,用下表来比照展现,可见次要是量化的限度以及应用内存的多少:
个性 Tiny Map Hash Map
键数量 2^31=2.14Billions 64位零碎:2^63
32位零碎:2^31
应用内存下限 64位零碎:4G
32位零碎:2G
受限于操作系统
单个键的大小 16M 受限于操作系统
单个值的大小 64位零碎:4G
32位零碎:2G
受限于操作系统
entry对象本身所用内存 24 byte 64位零碎:40 byte
32位零碎:28 byte
  • 您能够基于上述区别来选自应用Tiny Map和Hash Map,就本文的实战而言,应用Tiny Map就够用了
  • 接下来进入实战

应用共享内存

  • 应用共享内存一共分为两步,如下图,先配置再应用:
  • 当初nginx.conf中减少一个http配置项<font color=”blue”>shared_map</font>,指定了共享内存的名称是<font color=”red”>uri_access_counters</font>:

    # 减少一个共享内存的初始化调配,类型tiny,空间1M,键数量8K
    shared_map uri_access_counters  tinymap?space=1m&entries=8096;
  • 而后写一个新的content handler,该handler在收到申请时,会在共享内存中更新申请次数,总的代码如下,有几处要重点留神的中央,稍后会提到:

    package com.bolingcavalry.sharedmap;
    
    import nginx.clojure.java.ArrayMap;
    import nginx.clojure.java.NginxJavaRingHandler;
    import nginx.clojure.util.NginxSharedHashMap;
    import java.io.IOException;
    import java.util.Map;
    import java.util.UUID;
    import static nginx.clojure.MiniConstants.CONTENT_TYPE;
    import static nginx.clojure.MiniConstants.NGX_HTTP_OK;
    
    public class SharedMapSaveCounter implements NginxJavaRingHandler {
    
      /**
       * 通过UUID来表明以后jvm过程的身份
       */
      private String tag = UUID.randomUUID().toString();
    
      private NginxSharedHashMap smap = NginxSharedHashMap.build("uri_access_counters");
    
      @Override
      public Object[] invoke(Map<String, Object> map) throws IOException {
          String uri = (String)map.get("uri");
    
          // 尝试在共享内存中新建key,并将其值初始化为1,
          // 如果初始化胜利,返回值就是0,
          // 如果返回值不是0,示意共享内存中该key曾经存在
          int rlt = smap.putIntIfAbsent(uri, 1);
    
          // 如果rlt不等于0,示意这个key在调用putIntIfAbsent之前曾经在共享内存中存在了,
          // 此时要做的就是加一,
          // 如果relt等于0,就把rlt改成1,示意拜访总数曾经等于1了
          if (0==rlt) {
              rlt++;
          } else {
              // 原子性加一,这样并发的时候也会程序执行
              rlt = smap.atomicAddInt(uri, 1);
              rlt++;
          }
    
          // 返回的body内容,要体现出JVM的身份,以及share map中的计数
          String body = "From "
                  + tag
                  + ", total request count [ "
                  + rlt
                  + "]";
    
          return new Object[] {
                  NGX_HTTP_OK, //http status 200
                  ArrayMap.create(CONTENT_TYPE, "text/plain"), //headers map
                  body
          };
      }
    }
  • 上述代码曾经增加了具体正文,置信您一眼就看懂了,我这里挑几个重点阐明一下:
  • 写上述代码时要牢一件事:这段代码可能运行在高并发场景,既同一时刻,不同过程不同线程都在执行这段代码
  • NginxSharedHashMap类是ConcurrentMap的子类,所以是线程平安的,咱们更多思考应该留神跨过程读写时的同步问题,例如接下来要提到的第三和第四点,都是多个过程同时执行此段代码时要思考的同步问题
  • <font color=”blue”>putIntIfAbsent</font>和redis的setnx相似,能够当做跨过程的分布式锁来应用,只有指定的key不存在的时候才会设置胜利,此时返回0,如果返回值不等于0,示意共享内存中曾经存在此key了
  • <font color=”blue”>atomicAddInt</font>确保了原子性,多过程并发的时候,用此办法累加能够确保计算精确(如果咱们本人写代码,先读取,再累加,再写入,就会遇到并发的笼罩问题)
  • 对于那个atomicAddInt办法,咱们回顾一下java的AtomicInteger类,其incrementAndGet办法在多线程同时调用的场景,也能计算精确,那是因为外面用了CAS来确保的,那么nginx-clojure这里呢?我很好奇的去探寻了一下该办法的实现,这是一段C代码,最初没看到CAS无关的循环,只看到一段最简略的累加,如下图:
  • 很显著,上图的代码,在多过程同时执行时,是会呈现数据笼罩的问题的,如此只有两种可能性了,第一种:即使是多个worker存在,执行底层共享内存操作的过程也只有一个
  • 第二种:欣宸的C语言程度不行,基本没看懂JVM调用C的逻辑,自我感觉这种可能性很大:如果C语言程度能够,欣宸就用C去做nginx扩大了,没必要来钻研nginx-clojure呀!(如果您看懂了此段代码的调用逻辑,还望您指导欣宸一二,谢谢啦)
  • 编码实现,在nginx.conf上配置一个location,用SharedMapSaveCounter作为content handler:

    location /sharedmapbasedcounter {
      content_handler_type 'java';
       content_handler_name 'com.bolingcavalry.sharedmap.SharedMapSaveCounter';
    }
  • 编译构建部署,重启nginx
  • 先用Safari浏览器拜访<font color=”blue”>/sharedmapbasedcounter</font>,第一次收到的响应如下图,总数是1:
  • 刷新页面,UUID发生变化,证实这次申请到了另一个worker,总数也变成2,这意味着共享内存失效了,不同过程应用同一个变量来计算数据:
  • 改用Chrome浏览器,拜访同样的地址,如下图,UUID再次变动,证实申请是第三个worker的jvm解决的,然而拜访次数始终正确:
  • 实战实现,后面的代码中只用了两个API操作共享内存,学到的知识点无限,接下来做一些适当的延长学习

    一点延长

  • 方才曾提到NginxSharedHashMap是ConcurrentMap的子类,那些罕用的put和get办法,在ConcurrentMap中是在操作以后过程的堆内存,如果NginxSharedHashMap间接应用父类的这些办法,岂不是与共享内存无关了?
  • 带着这个疑难,去看NginxSharedHashMap的源码,如下图,水落石出:get、put这些罕用办法,都被重写了,红框中的nget和nputNumber都是native办法,都是在操作共享内存:
  • 至此,nginx-clojure的共享内存学习实现,高并发场景下跨进程同步数据又多了个轻量级计划,至于用它还是用redis,置信聪慧的您心中已有定论

    源码下载

  • 《Java扩大Nginx》的残缺源码可在GitHub下载到,地址和链接信息如下表所示(https://github.com/zq2599/blog_demos):
名称 链接 备注
我的项目主页 https://github.com/zq2599/blog_demos 该我的项目在GitHub上的主页
git仓库地址(https) https://github.com/zq2599/blog_demos.git 该我的项目源码的仓库地址,https协定
git仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该我的项目源码的仓库地址,ssh协定
  • 这个git我的项目中有多个文件夹,本篇的源码在<font color=”blue”>nginx-clojure-tutorials</font>文件夹下的<font color=”red”>shared-map-demo</font>子工程中,如下图红框所示:
  • 本篇波及到nginx.conf的批改,残缺的参考在此:https://raw.githubusercontent.com/zq2599/blog_demos/master/ng…

    欢送关注思否:程序员欣宸

学习路上,你不孤独,欣宸原创一路相伴…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理