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

6次阅读

共计 6095 个字符,预计需要花费 16 分钟才能阅读完成。

欢送拜访我的 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…

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

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

正文完
 0