Java Extension Nginx Seven:共有メモリ

一緒に書く習慣を身につけましょう!「ナゲッツデイリーニュープラン・4月アップデートチャレンジ」に参加して20日目です。クリックしてイベントの詳細をご覧ください

私のGitHubへようこそ

Xinchenのすべてのオリジナル作品(サポートするソースコードを含む)は、ここに分類され、要約されています:github.com/zq2599/blog…

この記事の概要

  • 「JavaExtendsNginx」シリーズの7番目の記事として、ユーティリティ共有メモリについて学び、正式な開始前に質問を見てみましょう。
  • 次の図に示すように、コンピューター上でnginxは複数のワーカーを起動します。この時点でnginx-clojureを使用すると、互いに独立した4つのjvmプロセスを持つことに相当します。同じURLに対する複数のリクエストの場合、これらの4つのjvmのいずれかによって処理される可能性があります。

ここに画像の説明を挿入

  • ここで要件があります:URLへの訪問の総数をカウントする方法は?要求に応答する4つのjvmプロセスがあり、それらのいずれにも保存できないため、Javaメモリでグローバル変数を使用することは絶対に不可能です。
  • 賢い場合は、redisを検討する必要があります。実際、redisはそのような問題を解決できますが、複数のサーバーが関与せず、単一マシンのnginxのみである場合は、nginx-clojureが提供する別の簡単なソリューションを検討することもできます。次の図に示すように、コンピューターでは、異なるプロセスが同じメモリ領域を操作し、アクセスの総数をこのメモリ領域に入れることができます。

ここに画像の説明を挿入

  • redisと比較すると、共有メモリの利点も明らかです。
  1. Redisは追加でデプロイされたサービスであり、共有メモリは追加のデプロイメントサービスを必要としません
  2. Redisリクエストはネットワークを通過します。共有メモリはネットワークを通過する必要はありません
  • したがって、スタンドアロンバージョンのnginxで複数のワーカーのデータ同期の問題が発生した場合は、共有メモリソリューションを検討できます。これは、今日の実際の戦闘の主なコンテンツでもあります。Java開発にnginx-clojureを使用する場合は、共有を使用してください。複数のワーカー間のメモリ同期データ

  • この記事は次の内容で構成されています。

  1. まず、カウントをJavaメモリに保存し、マルチワーカー環境で実行して、カウントが不正確であるという問題が存在することを確認します。
  2. nginx-clojureによって提供される共有マップの問題を解決します

ヒープメモリでカウントを保存

  • 写一个content handler,代码如下,用UUID来表明worker身份,用requestCount记录请求总数,每处理一次请求就加一:
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的worker_processes配置,改为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
4950
4968 Jps
4943
复制代码
  • 先用Safari浏览器访问/heapbasedcounter,第一次收到的响应如下图,总数是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配置项shared_map,指定了共享内存的名称是uri_access_counters:
# 增加一个共享内存的初始化分配,类型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
        };
    }
}
复制代码
  • 上述代码已经添加了详细注释,相信您一眼就看懂了,我这里挑几个重点说明一下:
  1. 写上述代码时要牢一件事:这段代码可能运行在高并发场景,既同一时刻,不同进程不同线程都在执行这段代码
  2. NginxSharedHashMap类是ConcurrentMap的子类,所以是线程安全的,我们更多考虑应该注意跨进程读写时的同步问题,例如接下来要提到的第三和第四点,都是多个进程同时执行此段代码时要考虑的同步问题
  3. putIntIfAbsent和redis的setnx类似,可以当做跨进程的分布式锁来使用,只有指定的key不存在的时候才会设置成功,此时返回0,如果返回值不等于0,表示共享内存中已经存在此key了
  4. atomicAddInt确保了原子性,多进程并发的时候,用此方法累加可以确保计算准确(如果我们自己写代码,先读取,再累加,再写入,就会遇到并发的覆盖问题)
  5. 关于那个atomicAddInt方法,咱们回忆一下java的AtomicInteger类,其incrementAndGet方法在多线程同时调用的场景,也能计算准确,那是因为里面用了CAS来确保的,那么nginx-clojure这里呢?我很好奇的去探寻了一下该方法的实现,这是一段C代码,最后没看到CAS有关的循环,只看到一段最简单的累加,如下图:

ここに画像の説明を挿入 6. 很明显,上图的代码,在多进程同时执行时,是会出现数据覆盖的问题的,如此只有两种可能性了,第一种:即便是多个worker存在,执行底层共享内存操作的进程也只有一个 7. 第二种:欣宸的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浏览器访问/sharedmapbasedcounter,第一次收到的响应如下图,总数是1:

ここに画像の説明を挿入

  • 刷新页面,UUID发生变化,证明这次请求到了另一个worker,总数也变成2,这意味着共享内存生效了,不同进程使用同一个变量来计算数据:

ここに画像の説明を挿入

  • 改用Chrome浏览器,访问同样的地址,如下图,UUID再次变化,证明请求是第三个worker的jvm处理的,但是访问次数始终正确:

ここに画像の説明を挿入

  • 实战完成,前面的代码中只用了两个API操作共享内存,学到的知识点有限,接下来做一些适当的延伸学习

一点延伸

  • 刚才曾提到NginxSharedHashMap是ConcurrentMap的子类,那些常用的put和get方法,在ConcurrentMap中是在操作当前进程的堆内存,如果NginxSharedHashMap直接使用父类的这些方法,岂不是与共享内存无关了?
  • 带着这个疑问,去看NginxSharedHashMap的源码,如下图,真相大白:get、put这些常用方法,都被重写了,红框中的nget和nputNumber都是native方法,都是在操作共享内存:

ここに画像の説明を挿入

  • 至此,nginx-clojure的共享内存学习完成,高并发场景下跨进程同步数据又多了个轻量级方案,至于用它还是用redis,相信聪明的您心中已有定论

源码下载

名前 リンク 述べる
プロジェクトホームページ github.com/zq2599/blog… GitHubのプロジェクトのホームページ
gitリポジトリアドレス(https) github.com/zq2599/blog… プロジェクトのソースコードのウェアハウスアドレス、httpsプロトコル
gitリポジトリアドレス(ssh) [email protected]:zq2599 / blog_demos.git プロジェクトのソースコードのウェアハウスアドレス、sshプロトコル
  • このgitプロジェクトには複数のフォルダーがあります。この記事のソースコードは、次の図の赤いボックスに示すように、nginx-clojure-tutorialsフォルダーの下のshared-map-demoサブプロジェクトにあります。

ここに画像の説明を挿入

ナゲッツへようこそ:プログラマーXin Chen

学習の道では、あなたは一人ではありません、XinchenOriginalはずっとあなたに同行します...

おすすめ

転載: juejin.im/post/7088471292977872927