Tallate

该吃吃该喝喝 啥事别往心里搁

为什么要使用 Nginx

Nginx 优点

  1. 轻量级,采用 C 进行编写,同样的 web 服务,会占用更少的内存及资源
  2. 抗并发,nginx 以 epoll 和 kqueue 作为开发模型,处理请求是异步非阻塞的,负载能力比 apache 高很多,而 apache 则是阻塞型的。在高并发下 nginx 能保持低资源低消耗高性能 ,而 apache 在 PHP 处理慢或者前端压力很大的情况下,很容易出现进程数飙升,从而拒绝服务的现象。
  3. nginx 处理静态文件好,静态处理性能比 apache 高三倍以上
  4. nginx 的设计高度模块化,编写模块相对简单
  5. nginx 配置简洁,正则配置让很多事情变得简单,而且改完配置能使用 -t 测试配置有没有问题,apache 配置复杂 ,重启的时候发现配置出错了,会很崩溃
  6. nginx 作为负载均衡服务器,支持 7 层负载均衡
  7. nginx 本身就是一个反向代理服务器,而且可以作为非常优秀的邮件代理服务器
  8. 启动特别容易, 并且几乎可以做到 7*24 不间断运行,即使运行数个月也不需要重新启动,还能够不间断服务的情况下进行软件版本的升级
  9. 社区活跃,各种高性能模块出品迅速

Nginx 优点(说出原因)

  1. Nginx 在核心代码都使用了与操作系统无关的代码实现,在与操作系统相关的系统调用上则分别针对各个操作系统都有独立实现,这最终造就了 Nginx 的可移植性。
  2. 非阻塞、高并发连接:处理 2-3 万并发连接数,官方监测能支持 5 万并发
  3. 内存消耗小:开启 10 个 nginx 才占 150M 内存,Nginx 采取了分阶段资源分配技术
    nginx 处理静态文件好,耗费内存少
  4. 内置的健康检查功能:如果有一个服务器宕机,会做一个健康检查,再发送的请求就不会发送到宕机的服务器了。重新将请求提交到其他的节点上。
    节省宽带:支持 GZIP 压缩,可以添加浏览器本地缓存
    稳定性高:宕机的概率非常小
  5. master/worker 结构:一个 master 进程,生成一个或者多个 worker 进程
    接收用户请求是异步的:浏览器将请求发送到 nginx 服务器,它先将用户请求全部接收下来,再一次性发送给后端 web 服务器,极大减轻了 web 服务器的压力
    一边接收 web 服务器的返回数据,一边发送给浏览器客户端
    网络依赖性比较低,只要 ping 通就可以负载均衡
    可以有多台 nginx 服务器
  6. 事件驱动:通信机制采用 epoll 模型

Apache 优点

  1. apache 的 rewrite 比 nginx 强大,在 rewrite 频繁的情况下,用 apache
  2. apache 发展到现在,模块超多,基本想到的都可以找到
  3. apache 更为成熟,少 bug ,nginx 的 bug 相对较多
  4. apache 超稳定
  5. apache 对 PHP 支持比较简单,nginx 需要配合其他后端用
  6. apache 在处理动态请求有优势,nginx 在这方面是鸡肋,一般动态请求要 apache 去做,nginx 适合静态和反向。
  7. apache 仍然是目前的主流,拥有丰富的特性,成熟的技术和开发社区

Nginx 和 Apache 区别总结

两者最核心的区别在于 apache 是同步多进程模型,一个连接对应一个进程,而 nginx 是异步的,多个连接(万级别)可以对应一个进程
一般来说,需要性能的 web 服务,用 nginx 。如果不需要性能只求稳定,更考虑 apache ,后者的各种功能模块实现得比前者,例如 ssl 的模块就比前者好,可配置项多。epoll(freebsd 上是 kqueue ) 网络 IO 模型是 nginx 处理性能高的根本理由,但并不是所有的情况下都是 epoll 大获全胜的,如果本身提供静态服务的就只有寥寥几个文件,apache 的 select 模型或许比 epoll 更高性能。当然,这只是根据网络 IO 模型的原理作的一个假设,真正的应用还是需要实测了再说的。
更为通用的方案是,前端 nginx 抗并发,后端 apache 集群,配合起来会更好。

接入层演进

通过研究接入层的发展历程,我们可以一窥 Nginx 在互联网架构中的地位。

接入层结构

接入层结构
可以看到,每一个下游都有多个上游调用,只需要做到,每一个上游都均匀访问每一个下游,就能实现整体的均匀分摊。

  1. 客户端层->反向代理层
    DNS 轮询
    DNS-server 对于一个域名配置了多个解析 ip,每次 DNS 解析请求来访问 DNS-server,会轮询返回这些 ip,保证每个 ip 的解析概率是相同的。这些 ip 就是 nginx 的外网 ip,以做到每台 nginx 的请求分配也是均衡的。
  2. 反向代理层->站点层
    反向代理层到站点层的负载均衡,是通过“nginx”实现的。
    修改 nginx.conf,可以实现多种均衡策略:
    2.1 请求轮询:和 DNS 轮询类似,请求依次路由到各个 web-server;
    2.2 最少连接路由:哪个 web-server 的连接少,路由到哪个 web-server;
    2.3 ip 哈希:按照访问用户的 ip 哈希值来路由 web-server,只要用户的 ip 分布是均匀的,请求理论上也是均匀的,ip 哈希均衡方法可以做到,同一个用户的请求固定落到同一台 web-server 上,此策略适合有状态服务,例如 session;

    session 不推荐放到站点层,后期扩展会有问题,更好的方案是放到数据层。

  3. 站点层->服务层
    站点层到服务层的负载均衡,是通过“服务连接池”实现的。
    上游连接池会建立与下游服务多个连接,每次请求会“随机”选取连接来访问下游服务。除了负载均衡,服务连接池还能够实现故障转移、超时处理、限流限速、ID 串行化等诸多功能。
  4. 服务层->数据层
    在数据量很大的情况下,由于数据层(db/cache)涉及数据的水平切分,所以数据层的负载均衡更为复杂一些,它分为“数据的均衡”,与“请求的均衡”。
    数据的均衡是指:水平切分后的每个服务(db/cache),数据量是均匀的。
    请求的均衡是指:水平切分后的每个服务(db/cache),请求量是均匀的。

接入层演进

  1. 单机架构
    客户端用 DNS 解析出来的 IP 就是 web 服务器的地址。
    缺点:
    • 单点;
    • 扩展性差。
  2. DNS 轮询
    在 DNS 服务器上多配几个 IP,由域名服务器的解析策略实现负载均衡。
    缺点:
    • 非高可用,因为一个 web 服务器挂掉后 DNS 服务器仍然会将请求解析到该服务器对应的 IP 上;
    • 扩容非实时,DNS 服务器有一个配置生效的延时时间;
    • 暴露太多外网 IP。
  3. 反向代理 Nginx
    DNS 解析到 Nginx 的 IP,然后由 Nginx 将请求负载均衡到 web 服务器。
    缺点:基本解决了上一个架构存在的问题,且可以利用 Nginx 的探活机制实现 web 服务器的高可用,但是此时 Nginx 也会成为一个单点。
  4. keepalived
    两台 Nginx 组成集群,分别部署上 keepalived,设置成相同的虚 IP,保证 Nginx 的高可用。当一台 Nginx 挂了,keepalived 能够探测到并将流量迁移到另一台 Nginx 上,整个过程对调用方透明。
    缺点:
    • 资源利用率低;
    • 扩容不方便,如果吞吐量超过 Nginx 性能上线,要加机器配置起来比较麻烦。
  5. lvs/f5
    DNS 解析出来的 IP 是 lvs 的地址。由 lvs 反向代理 Nginx 服务器,lvs 的机器上部署 keepalived+VIP 实现高可用;
    f5 的性能比 lvs 更高,但是成本也会更高。
  6. DNS 轮询
    水平扩展才是解决性能问题的根本方案,能够通过加机器扩充性能的方案才具备最好的扩展性。
    可扩展高可用接入层架构
    • 通过 DNS 轮询来线性扩展入口 lvs 层的性能;
    • 通过 keepalived 保证高可用;
    • 通过 lvs 来扩展多个 Nginx;
    • 通过 Nginx 实现对业务服务器的七层负载均衡。

使用 Nginx

手动安装 Nginx

  1. 配置
    创建目录/var/temp/nginx
    这个目录保存临时文件,在安装配置中指定:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ./configure \
    --prefix=/usr/local/nginx \
    --pid-path=/var/run/nginx/nginx.pid \
    --lock-path=/var/lock/nginx.lock \
    --error-log-path=/var/log/nginx/error.log \
    --http-log-path=/var/log/nginx/access.log \
    --with-http_gzip_static_module \
    --http-client-body-temp-path=/var/temp/nginx/client \
    --http-proxy-temp-path=/var/temp/nginx/proxy \
    --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \
    --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \
    --http-scgi-temp-path=/var/temp/nginx/scgi
  2. 安装
    1
    2
    make
    make install
  3. 配置环境变量
    /etc/profile中编辑,这样就可以直接使用 nginx 命令启动了
    1
    export PATH=$PATH:/usr/local/nginx/sbin
  4. 启动
    1
    2
    3
    nginx
    # 指定配置文件
    nginx -c /usr/local/nginx/conf/nginx.conf
    如果不指定-c,nginx 在启动时默认加载 conf/nginx.conf 文件,此文件的地址也可以在编译安装 nginx 时指定./configure 的参数(–conf-path= 指向配置文件(nginx.conf))
  5. 停止
    1
    2
    nginx -s stop # 相当于先查出nginx进程id再kill
    nginx -s quit # 建议使用,这种方法是等nginx进程的任务处理完毕后再停止
  6. 重启
    1
    2
    nginx -s quit
    nginx
    要想在修改配置文件 nginx.conf 后生效:
    1
    nginx -s reload

开机自启

  1. 创建/etc/init.d/nginx
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    #!/bin/bash
    # nginx Startup script for the Nginx HTTP Server
    # it is v.0.0.2 version.
    # chkconfig: - 85 15
    # description: Nginx is a high-performance web and proxy server.
    # It has a lot of features, but it's not for everyone.
    # processname: nginx
    # pidfile: /var/run/nginx.pid
    # config: /usr/local/nginx/conf/nginx.conf
    nginxd=/usr/local/nginx/sbin/nginx
    nginx_config=/usr/local/nginx/conf/nginx.conf
    nginx_pid=/var/run/nginx.pid
    RETVAL=0
    prog="nginx"
    # Source function library.
    . /etc/rc.d/init.d/functions
    # Source networking configuration.
    . /etc/sysconfig/network
    # Check that networking is up.
    [ ${NETWORKING} = "no" ] && exit 0
    [ -x $nginxd ] || exit 0
    # Start nginx daemons functions.
    start() {
    if [ -e $nginx_pid ];then
    echo "nginx already running...."
    exit 1
    fi
    echo -n $"Starting $prog: "
    daemon $nginxd -c ${nginx_config}
    RETVAL=$?
    echo
    [ $RETVAL = 0 ] && touch /var/lock/subsys/nginx
    return $RETVAL
    }
    # Stop nginx daemons functions.
    stop() {
    echo -n $"Stopping $prog: "
    killproc $nginxd
    RETVAL=$?
    echo
    [ $RETVAL = 0 ] && rm -f /var/lock/subsys/nginx /var/run/nginx.pid
    }
    # reload nginx service functions.
    reload() {
    echo -n $"Reloading $prog: "
    #kill -HUP `cat ${nginx_pid}`
    killproc $nginxd -HUP
    RETVAL=$?
    echo
    }
    # See how we were called.
    case "$1" in
    start)
    start
    ;;
    stop)
    stop
    ;;
    reload)
    reload
    ;;
    restart)
    stop
    start
    ;;
    status)
    status $prog
    RETVAL=$?
    ;;
    *)
    echo $"Usage: $prog {start|stop|restart|reload|status|help}"
    exit 1
    esac
    exit $RETVAL
  2. 设置文件访问权限
    1
    chmod a+x /etc/init.d/nginx
  3. 设置开机加载
    /etc/rc.local中加入一行
    1
    /etc/init.d/nginx start

通过 Docker 运行 Nginx

手动进行 Nginx 配置十分繁琐,可以使用 Docker 来简化部署流程:

1
docker run -d -p 80:80 nginx

Nginx 原理 - 进程

Nginx 代码的模块化结构

Nginx 的代码是由一个核心和一系列的模块组成的。
核心主要用于提供 WebServer 的基本功能,以及 Web 和 Mail 反向代理的功能;还用于启用网络协议,创建必要的运行时环境以及确保不同的模块之间平滑地进行交互。
不过,大多跟协议相关的功能和应用特有的功能都是由 nginx 的模块实现的。
换句话说, 每一个功能或操作都由一个模块来实现
这些功能模块大致可以分为事件模块、阶段性处理器、输出过滤器、变量处理器、协议、upstream 和负载均衡几个类别,这些共同组成了 nginx 的 http 功能。
事件模块主要用于提供 OS 独立的(不同操作系统的事件机制有所不同)事件通知机制如 kqueue 或 epoll 等。
协议模块则负责实现 nginx 通过 http、tls/ssl、smtp、pop3 以及 imap 与对应的客户端建立会话。
在 Nginx 内部,进程间的通信是通过模块的 pipelinechain 实现的。
换句话说,每一个功能或操作都由一个模块来实现。例如:压缩、通过 FastCGI 或 uwsgi 协议与 upstream 服务器通信、以及与 memcached 建立会话等。

进程结构

一个 Nginx 服务器实例由一个 master 进程和多个 worker 进程组成。
进程结构
master进程主要用来管理 worker 进程,还有一些对整个服务器的初始化和日志记录等工作。
管理 worker 的过程:接收来自外界的信号,向各 worker 进程发送 信号 ,监控 worker 进程的运行状态,当 worker 进程退出后(异常情况下),会自动重新启动(fork)新的 worker 进程。

master 主要功能

  • 读取并验证配置信息;
  • 创建、绑定及关闭套接字;
  • 启动、终止 worker 进程及维护 worker 进程的个数;
  • 无须中止服务而重新配置工作;
  • 控制非中断式程序升级,启用新的二进制程序并在需要时回滚至老版本;
  • 重新打开日志文件;
  • 编译嵌入式 perl 脚本

对请求的实际处理由 worker 负责,且每个请求只能由一个 worker 负责(一对一)。在启动时,创建一组初始的监听套接字,HTTP 请求和响应之时,worker 连续接收、读取和写入套接字。

worker 主要功能

  • 接收、传入并处理来自客户端的连接;
  • 提供反向代理及过滤功能;
  • nginx 任何能完成的其它任务

Nginx 的启动

nginx 启动后,在 unix 系统中会以 daemon 的方式在后台运行,后台进程包含一个 master 进程和多个 worker 进程(你可以理解为工人和管理员)。

Nginx 处理连接过程

nginx 不会为每个连接派生进程或线程,而是由 worker 进程通过监听共享套接字接受新请求,并且使用高效的 循环 来处理数千个连接。
Nginx 不使用仲裁器或分发器来分发连接,这个工作由操作系统内核机制完成。 监听套接字 在启动时就完成初始化,worker 进程通过这些套接字接受、读取请求和输出响应。

一次请求过程大概执行过程为:

  1. nginx 在启动时,会解析配置文件,得到需要监听的端口与 ip 地址,然后在 nginx 的 master 进程里面先初始化好这个监控的 socket,再进行 listen(listenfd);
  2. 由 master 进程 fork 出多个 worker 进程;
  3. 此时客户端可以向 nginx 发起连接了,客户端会与 nginx 进行三次握手(TCP),与 nginx 建立好一个连接;
  4. 所有 worker 进程的 listenfd 会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有 worker 进程会在注册 listenfd 读事件前抢 accept_mutex,抢到互斥锁的那个进程注册 listenfd 读事件,然后在读事件里调用 accept 接受该连接
  5. 当一个 worker 进程在 accept 这个连接之后,然后创建 nginx 对连接的封装,即 ngx_connection_t 结构体,就开始读取请求、解析请求、处理请求( 异步非阻塞 ),主要是根据事件调用相应的事件处理模块,如 http 模块与客户端进行数据的交换
  6. 产生数据后,再返回给客户端,最后才断开连接,或者由客户端主动关闭连接。

进程间通信

在 Nginx 内部,进程间的通信是通过模块的 pipelinechain 实现的,其原理是信号机制,master 对 worker 进程采用信号进行控制。

事件驱动

所谓事件驱动架构,简单来说,就是由一些事件发生源来产生事件,由一个或多个事件收集器(epolld 等)来收集、分发事件,然后许多事件处理器会注册自己感兴趣的事件,同时会“消费”这些事件。nginx 不会使用进程或线程作为事件消费者,只能是某个模块,当前进程调用模块。
传统 web 服务器(如 Apache)的所谓事件局限在 TCP 连接建立、关闭上,其他读写都不再是事件驱动,这时会退化成按序执行每个操作的批处理模式,这样每个请求在连接建立后都将始终占用系统资源,直到连接关闭才会释放资源。大大浪费了内存、cpu 等资源。并且把一个进程或线程作为事件消费者。 传统 Web 服务器每个事件消费者独占一个进程资源,相对来说,Nginx 只是被事件分发者进程短期调用而已。
nginx 采用多 worker 的方式来处理请求,每个 worker 里面只有一个主线程,那能够处理的并发数很有限,多少个 worker 就能处理多少个并发,那么何来的高并发呢?
其实,Nginx 是采用了异步非阻塞的 IO 模型来处理请求的(epoll),异步的概念是和同步相对的,也就是不同事件之间不是同时发生的。非阻塞的概念是和阻塞对应的,阻塞是事件按顺序执行,每一事件都要等待上一事件的完成,而非阻塞是如果事件没有准备好,这个事件可以直接返回,过一段时间再进行处理询问,这期间可以做其他事情。
请求的多阶段异步处理只能基于事件驱动框架实现,就是把一个请求的处理过程按照事件的触发方式分为多个阶段,每个阶段都可以有事件收集、分发器(epoll 等)来触发。比如一个 http 请求可以分为七个阶段。
每种事件都有一个事件队列,按触发的先后顺序处理。

惊群现象

惊群是多个子进程在同一时刻监听同一个端口引起的;
Nginx 解决方法:同一个时刻只能有唯一一个 worker 子进程监听 web 端口,此时新连接事件只能唤醒唯一正在监听端口的 worker 子进程。这可以通过锁或互斥量实现。

为什么不使用多线程

  • Apache: 创建多个进程或线程,而每个进程或线程都会为其分配 cpu 和内存(线程要比进程小的多,所以 worker 支持比 perfork 高的并发),并发过大会榨干服务器资源。
  • Nginx: 采用单线程来异步非阻塞处理请求(管理员可以配置 Nginx 主进程的工作进程的数量)(epoll),不会为每个请求分配 cpu 和内存资源,节省了大量资源,同时也减少了大量的 CPU 的上下文切换。所以才使得 Nginx 支持更高的并发。

模块

模块命名

ngx_http_[module-name]_[main|srv|loc]_conf_t
前缀表示模块名,后面表示模块运行在哪一层

模块化结构

Nginx由内核和一系列模块组成,内核提供web服务的基本功能,如启用网络协议,创建运行环境,接收和分配客户端请求,处理模块之间的交互。Nginx的各种功能和操作都由模块来实现。
Nginx的模块从结构上分为核心模块、基础模块和第三方模块。

  • 核心模块: HTTP模块、EVENT模块和MAIL模块
  • 基础模块: HTTP Access模块、HTTP FastCGI模块、HTTP Proxy模块和HTTP Rewrite模块
  • 第三方模块: HTTP Upstream Request Hash模块、Notice模块和HTTP Access Key模块及用户自己开发的模块

这样的设计使Nginx方便开发和扩展,也正因此才使得Nginx功能如此强大。Nginx的模块默认编译进nginx中,如果需要增加或删除模块,需要重新编译Nginx,这一点不如Apache的动态加载模块方便。如果有需要动态加载模块,可以使用由淘宝网发起的web服务器Tengine,在nginx的基础上增加了很多高级特性,完全兼容Nginx,已被国内很多网站采用。

模块大致结构如下图所示。
Nginx模块结构
Nginx模块,简单地讲,就是:在特定地方调用的函数。

模块执行过程

nginx的配置 指令作用域 分为以下几种:main,server,location
main作用域的范围为整个配置文件,而server是指某个具体的服务器(通过端口号来区分),而location就是指要访问这个server的哪个location。

Nginx 本身做的工作实际很少,当它接到一个 HTTP 请求时,它仅仅是通过查找配置文件将此次请求映射到一个 locationblock,而此 location 中所配置的各个指令则会启动不同的模块去完成工作。
通常一个 location 中的指令会涉及一个 handler 模块和多个 filter 模块(当然,多个 location 可以复用同一个模块)。handler 模块负责处理请求,完成响应内容的生成,而 filter 模块对响应内容进行处理。
模块处理请求的大致过程如下图所示。
Nginx模块执行流程

http index模块(ngx_http_index_module)

定义将要被作为默认页的文件。 文件的名字可以包含变量。 文件以配置中指定的顺序被 nginx 检查。 列表中的最后一个元素可以是一个带有绝对路径的文件。

1
2
3
4
location / {
root /home/ftpuser/www;
index index.html index.$haha.html index.htm;
}

需要注意的是,index 文件会引发内部重定向,请求可能会被其它 location 处理。
比如下面的配置,请求”/“实际上将会在第二个location中作为”/index.html”被处理:

1
2
3
4
5
6
location = / {
index index.html;
}
location / {
...
}

http log模块(ngx_http_log_module)

1
2
3
4
log_format  gzip '$remote_addr-$remote_user[$time_local]'
:'$request$status $bytes_sent'
:'" $ http _ referer" "$http_user_agent" "$gzip_ratio"';
access_log /spool/logs/nginx-access.log gzip buffer=32k;

指令 access_log 指派路径、格式和缓存大小:

1
2
3
4
# 格式
access_log path [format [buffer=size | off ]
# 默认
access_log log/access.log combined

其中参数 “off” 将清除当前级别的所有 access_log 指令。如果未指定格式,则使用预置的 “combined” 格式。缓存不能大于能写入磁盘的文件的最大值(在 FreeBSD 3.0-6.0 ,缓存大小无此限制)。
指令 log_format 指定日志格式:

1
2
3
4
# 格式
log_format name format [format ...]
# 默认
log_format combined "..."

Access模块(ngx_http_access_module)

此模块提供了一个简易的基于主机的访问控制。
ngx_http_access_module 模块让我们可以对特定 IP 客户端进行控制。 规则检查按照第一次匹配的顺序,此模块对网络地址有放行和禁止的权利。

1
2
3
4
5
6
7
# 仅允许网段 10.1.1.0/16 和 192.168.1.0/24 中除 192.168.1.1 之外的 ip 访问
location / {
: deny 192.168.1.1;
: allow 192.168.1.0/24;
: allow 10.1.1.0/16;
: deny all;
}
  1. 放行语法
    1
    allow [ address | CIDR | all ]
    作用域: http, server, location, limit_except
    allow 描述的网络地址有权直接访问
  2. 禁止语法
    1
    deny [ address | CIDR | all ]
    作用域: http, server, location, limit_except
    deny 描述的网络地址拒绝访问

Rewrite模块(ngx_http_rewrite_module)

执行 URL 重定向,允许你去掉带有恶意的 URL,包含多个参数(修改).利用正则的匹配,分组和引用,达到目的 配置范例:该模块允许使用正则表达式改变 URL,并且根据变量来转向以及选择配置

  1. if语法
    1
    if (condition) { ... }
    作用域: server, location
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    if ($http_user_agent ~ MSIE) {
    : rewrite ^(.*)$ /msie/$1 break;
    }
    if ($http_cookie ~* "id=([^;] +)(?:;|$)" ) {
    : set $id $1;
    }
    if ($request_method = POST ) {
    : return 405;
    }
    if (!-f $request_filename) {
    : break;
    : proxy_pass http://127.0.0.1;
    }
    if ($slow) {
    : limit_rate 10k;
    }
    if ($invalid_referer) {
    : return 403;
    }
  2. return语法
    这个指令根据规则的执行情况,返回一个状态值给客户端。可使用值包括:204,400,402-406,408,410,411,413,416 及 500-504。也可以发送非标准的 444 代码-未发送任何头信息下结束连接。
    1
    return cod
    作用域: server, location, if
  3. rewrite语法
    这个指令根据正则表达式或者待替换的字符串来更改 URL。指令根据配置文件中的先后顺序执行生效。
    1
    rewrite regex replacement flag
    flag可以有以下取值:
    • last :表示完成 rewrite
    • break:本规则匹配完成后,终止匹配,不再匹配后面的规则
    • redirect:返回 302 临时重定向,地址栏会显示跳转后的地址
    • permanent:返回 301 永久重定向,地址栏会显示跳转后的地址
      作用域: server, location, if

Proxy模块(ngx_http_proxy_module)

此模块能代理请求到其它服务器.也就是说允许你把客户端的 HTTP 请求转到后端服务器(这部分的指令非常多,但不是全部都会被用到,详细指令列表可以上官网查看,这里是比较常见的指令简介)

1
2
3
4
5
6
7
8
# 强制一些被忽略的头传递到客户端
proxy_pass_header Server;
# 允许改写出现在 HTTP 头却被后端服务器触发重定向的 URL,对响应本身不做任何处理
proxy_redirect off;
# 允许你重新定义代理 header 值再转到后端服务器,目标服务器可以看到客户端的原始主机名
proxy_set_header Host $http_host;
# 目标服务器可以看到客户端的真实 ip,而不是转发服务器的 ip
proxy_set_header X-Real-IP $remote_addr;

upstream模块(ngx_http_upstream_module)

该指令将来自客户端的一个请求分到多个上行服务器上,即我们常说的负载均衡。
默认情况下采用轮询策略,每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
可以使用weight来指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。

1
2
3
4
5
6
7
8
9
10
upstream backend {
server backend1.example.com weight=5;
server backend2.example.com:8080;
server unix:/tmp/backend3;
}
server {
location / {
proxy_pass http://backend;
}
}
  1. upstream指令
    这个指令描述了一个服务器的集合,该集合可被用于 proxy_pass 和 fastcgi_pass 指令中,作为一个单独的实体。这些服务器可以是监听在不同的端口,另外,并发使用同时监听 TCP 端口和 Unix 套接字的服务器是可能的。 这些服务器能被分配不同的权重。如果没有指定,则都为 1 ,即默认的策略为轮询。
    1
    2
    3
    4
    5
    upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;
    }
    可以通过weight参数指定每个上行服务器的权重,weight和访问比率成正比,用于后端服务器性能不均的情况。
    1
    2
    3
    4
    upstream test{
    server localhost:8080 weight=9;
    server localhost:8081 weight=1;
    }
  2. ip_hash指令
    请求基于客户端的 IP 地址在服务器间进行分发。 IPv4 地址的前三个字节或者 IPv6 的整个地址,会被用来作为一个散列 key。 这种方法可以确保从同一个客户端过来的请求,会被传给同一台服务器。除了当服务器被认为不可用的时候,这些客户端的请求会被传给其他服务器,而且很有可能也是同一台服务器。
    如果其中一个服务器想暂时移除,应该加上 down 参数。这样可以保留当前客户端 IP 地址散列分布。
    作用域:upstream
    1
    2
    3
    4
    5
    6
    7
    upstream backend {
    ip_hash;
    server backend1.example.com;
    server backend2.example.com;
    server backend3.example.com down;
    server backend4.example.com;
    }
  3. fair(第三方)
    按后端服务器的响应时间来分配请求,响应时间短的优先分配。
    1
    2
    3
    4
    5
    upstream backend { 
    fair;
    server localhost:8080;
    server localhost:8081;
    }
  4. url_hash(第三方)
    按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。 在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法。
    1
    2
    3
    4
    5
    6
    upstream backend {
    hash $request_uri;
    hash_method crc32;
    server localhost:8080;
    server localhost:8081;
    }
  5. server指令
    定义服务器的地址 address 和其他参数 parameters。 地址可以是域名或者 IP 地址,端口是可选的,或者是指定“unix:”前缀的 UNIX 域套接字的路径。如果没有指定端口,就使用 80 端口。 如果一个域名解析到多个 IP,本质上是定义了多个 server。
    作用域:upstream
    1
    2
    3
    4
    5
    upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server unix:/tmp/backend3;
    }

nginx配置文件优化

配置文件

默认配置文件位置在/usr/local/nginx/conf/nginx.conf,在configue时决定,也可以在运行nginx时指定配置文件

1
nginx nginx.conf

顶层配置

顶层配置即nginx.conf中前面、暴露在外面的那几项

1
2
3
#user  nobody;
worker_processes 1;
#pid logs/nginx.pid;

worker_processes 定义了 nginx 在为你的网站提供服务时,worker 进程的数量。
这个优化值受到包括 CPU 内核数、存储数据的磁盘数、负载值在内的许多因素的影响。如果不确定的话,将其设置为可用的 CPU 内核的数量是一个不错的选择(设置为“auto”,将会尝试自动检测可用的值)。
另外本机的CPU核心信息可以使用下面命令查看

1
cat /proc/cpuinfo | grep processor

events模块

events模块包括了 nginx 中处理链接的全部设置

1
2
3
4
events {
worker_connections 1024;
# multi_accept on;
}

worker_connections 设置了一个 worker 进程可以同时打开的链接数。
multi_accept 的作用是告诉 nginx 在收到新链接的请求通知时,尽可能接受链接。最好开着

http模块

当外部有 http 请求时,nginx 的 http 模块才是处理这个请求的核心。

  1. Basic Settings
    sendfile 指向 sendfile()函数。sendfile() 在磁盘和 TCP 端口(或者任意两个文件描述符)之间复制数据。
    在 sendfile 出现之前,为了传输这样的数据,需要在用户空间上分配一块数据缓存,使用 read() 从源文件读取数据到缓存,然后使用 write() 将缓存写入到网络。
    sendfile() 直接从磁盘上读取数据到操作系统缓冲。由于这个操作是在内核中完成的,sendfile() 比 read() 和 write() 联合使用要更加有效率。
    tcp_nopush 配置 nginx 在一个数据包中发送全部的头文件,而不是一个一个发送。
    tcp_nodelay 配置 nginx 不要缓存数据,应该快速的发送小数据——这仅仅应该用于频繁发送小的碎片信息而无需立刻获取响应的、需要实时传递数据的应用中。
    keepalive_timeout 指定了与客户端的 keep-alive 链接的超时时间。服务器会在这个时间后关闭链接。我们可以降低这个值,以避免让 worker 过长时间的忙碌。
  2. Logging Settings
    access_log 确定了 nginx 是否保存访问日志。将这个设置为关闭可以降低磁盘 IO 而提升速度。
    error_log 设置 nginx 应当记录错误日志。
  3. Gzip Settings
    gzip 设置 nginx gzip 压缩发送的数据。这会减少需要发送的数据的数量。
    gzip_disable 为指定的客户端禁用 gzip 功能。
    gzip_proxied 允许或禁止基于请求、响应的压缩。设置为 any,就可以 gzip 所有的请求。
    gzip_comp_level 设置了数据压缩的等级。等级可以是 1-9 的任意一个值,9 表示最慢但是最高比例的压缩。
    gzip_types 设置进行 gzip 的类型。有下面这些,不过还可以添加更多。

mail模块

略…

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
user nginxuser;
worker_processes 4;
pid logs/nginx.pid;

events {
worker_connections 1024;
multi_accept on;
}

http {

##
# Basic Settings
##


sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
# server_tokens off;


# server_names_hash_bucket_size 64;
# server_name_in_redirect off;


include /etc/nginx/mime.types;
default_type application/octet-stream;


##
# Logging Settings
##


access_log off;
error_log /var/log/nginx/error.log;


##
# Gzip Settings
##


gzip on;
gzip_disable "msie6";


gzip_vary on;
gzip_proxied any;
gzip_comp_level 9;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;


##
# nginx-naxsi config
##
# Uncomment it if you installed nginx-naxsi
##


#include /etc/nginx/naxsi_core.rules;


##
# nginx-passenger config
##
# Uncomment it if you installed nginx-passenger
##


#passenger_root /usr;
#passenger_ruby /usr/bin/ruby;


##
# Virtual Host Configs
##


include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}



# mail {
# # See sample authentication script at:
# # http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript
#
# # auth_http localhost/auth.php;
# # pop3_capabilities "TOP" "USER";
# # imap_capabilities "IMAP4rev1" "UIDPLUS";
#
# server {
# listen localhost:110;
# protocol pop3;
# proxy on;
# }
#
# server {
# listen localhost:143;
# protocol imap;
# proxy on;
# }
#}

优化 - Linux 最大连接数

查看 Linux 连接数

默认配置下,Linux 只支持有限的连接数。
Linux 的线程其实是一个进程,所以 java 的也是,具体来说,叫做“light weight process(LWP)”—轻量级进程。
LWP 与其它进程共享所有(或大部分)逻辑地址空间和系统资源,一个进程可以创建多个 LWP,这样它们共享大部分资源;LWP 有它自己的进程标识符,并和其他进程有着父子关系;。LWP 由内核管理并像普通进程一样被调度
使用以下命令可以看到某个用户使用了多少进程资源

1
ps -eLf | grep xjjbot(uid)  | wc -l

使用下面命令可以查看具体每个进程开启了多少线程

1
ps -o nlwp,pid,lwp,args -u xjjbot(uid)  | sort -n

根据 linux 一切都是文件的规则,首先想到的,是修改 ulimit 的参数,然而也不是,因为它已经足够大了。交叉回想一下 elasticsearch,在安装的时候,需要配置一个叫做 nproc 的东西,问题大概就出在这,是进程资源不够用啦。
相关的配置文件:

1
/etc/security/limits.conf

在不同的内核版本上,也有一些小差异。比如:/etc/security/limits.d/*
下的文件,会在某些时候覆盖 limits.conf 的配置。所以配置不生效的情况下,记得检查一下。
鉴于以上原因,可以将 limits.d 中的配置全部注释掉,统一在 limits.conf 中配置。
以下是原始配置

1
2
*          soft    nproc     4096
root soft nproc unlimited

将 4096 改为大点的数字,或者直接改成 unlimited 就可以了。

单机支持 100 万连接是可行的,但带宽问题会成为显著的瓶颈。启用压缩的二进制协议会节省部分带宽,但开发难度增加。

操作系统优化

更改进程最大文件句柄数

1
ulimit -n 1048576

复制代码修改单个进程可分配的最大文件数

1
echo 2097152 > /proc/sys/fs/nr_open

复制代码修改/etc/security/limits.conf 文件

1
2
3
4
*   soft nofile  1048576
* hard nofile 1048576
* soft nproc unlimited
root soft nproc unlimited

复制代码记得清理掉/etc/security/limits.d/*下的配置

网络优化

打开/etc/sysctl.conf,添加配置然后执行,使用 sysctl 生效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#单个进程可分配的最大文件数
fs.nr_open=2097152

#系统最大文件句柄数
fs.file-max = 1048576

#backlog 设置
net.core.somaxconn=32768
net.ipv4.tcp_max_syn_backlog=16384
net.core.netdev_max_backlog=16384

#可用知名端口范围配置
net.ipv4.ip_local_port_range='1000 65535'

#TCP Socket 读写 Buffer 设置
net.core.rmem_default=262144
net.core.wmem_default=262144
net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.core.optmem_max=16777216
net.ipv4.tcp_rmem='1024 4096 16777216'
net.ipv4.tcp_wmem='1024 4096 16777216'

#TCP 连接追踪设置
net.nf_conntrack_max=1000000
net.netfilter.nf_conntrack_max=1000000
net.netfilter.nf_conntrack_tcp_timeout_time_wait=30

#TIME-WAIT Socket 最大数量、回收与重用设置
net.ipv4.tcp_max_tw_buckets=1048576

# FIN-WAIT-2 Socket 超时设置
net.ipv4.tcp_fin_timeout = 15

参考

  1. Linux Web 运维(Nginx)实战
  2. Nginx 开发从入门到精通
  3. nginx documentation

config

nginx.conf配置文件详解 http://www.ha97.com/5194.html
更多配置技巧 https://www.nginx.com/resources/wiki/start/

Nginx 原理

  1. Nginx 内部有使用多线程吗?
  2. 如果这篇文章说不清 epoll 的本质,那就过来掐死我吧! (1)

模块

  1. log
    Module ngx_http_log_module
    Module ngx_stream_log_module
    nginx 日志格式及自定义日志配置
  2. proxy
    Module ngx_http_proxy_module
  3. TCP / UDP
    How nginx processes a TCP/UDP session

Tengine

  1. Documentation

这一篇是对在公司内缓存代码应用 Redis-Lua 的一个总结,经过 benchmark 测试,这种方式效率更高,且理论上有更低的可能性。
顺便,一开始先描述一下Redis中的事务的原理,因为Redis-Lua本身是事务的一个替代品,这二者一般放在一起讨论。

阅读全文 »

Java5

语法、集合框架等

  • 泛型
  • 枚举
  • 装箱拆箱
  • 变长参数
  • 注解
  • foreach 循环
  • 静态导入
  • 格式化
  • 线程框架/数据结构
  • Arrays 工具类/StringBuilder/instrument

泛型

不同于 C++中的泛型,Java 的泛型会在编译后被清除,这种机制被称为泛型擦除。

java 的类型推断基本都在编译期完成

优点:可以免去大量的显式类型转换;
缺点:由于泛型擦除的存在,在很多场合下容易引起误会:

  • 比如向 List类型的表里添加一个 String 类型对象就不会通过,因为在编译期间还需要进行类型检查。
  • 在继承重写方法时,若父类中被重写的方法中含有泛型,因为泛型擦除理应变成重载,但是 Java 编译器会在编译后的字节码中添加桥方法(已经被类型擦除)、桥方法再调用重写的方法来解决;
  • 泛型类型参数不能使用基本类型,因为基本类型不是 Object 的子类;
  • 其他一些注意事项…

枚举

枚举可以使用 enum 声明,在 switch 中可以作为 case 后的标签。可以使用 EnumMap 来保存枚举到其他类型的映射或使用 EnumSet 保存枚举值的集合。
优点

  • 相对使用 int 或 String 当作枚举对象来说,Java 编译器本身提供了对 enum 的类型检查,可以更安全地使用;
  • 可以用于声明单例对象。

装箱拆箱

基本类型可以自动转换成对应的包装类型,比如 boolean 会被包装为 Boolean。
优点

  • 方便。

缺点

  • 注意拆箱时不能对 null 拆箱,不然会报空指针。

变长参数

可以传入任意多个相同类型的参数。
优点

  • 提供了更多灵活性,比如编写输出方法时可以格式化多个参数。

缺点

  • 需要注意 null 值的传入。

注解

注解需要和反射配合使用,JDK 提供了一些具有特定语义的注解:
@Inherited:是否对类的子类继承的方法等起作用;
@Target:作用目标;
@Rentation:表示 annotation 是否保留在编译过的 class 文件中还是在运行时可读。

for/in 循环

优点

  • 方便

缺点

  • 获取不到元素所在 index;
  • 无法在遍历的时候删除元素;

静态 import

可以直接使用一个类中的静态方法。
缺点

  • 如果有同名的容易引起混淆;

Formatter

提供对日期、数字等的格式化支持

线程框架/数据结构

  1. 在线程中可以设置 UncaughtExceptionHandler,当抛出异常后可以执行指定的逻辑;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    public class ThreadingTest extends Thread {
    private int[] numbers;
    public ThreadingTest(int[] numbers) {
    setName("Simple Thread");
    setUncaughtExceptionHandler(
    new SimpleThreadExceptionHandler());
    this.numbers = numbers;
    }
    public void run() {
    int index = numbers.length;
    boolean finished = false;
    while (!finished) {
    index--;
    finished = true;
    for (int i = 0; i < index; i++) {
    // Create error condition
    if (numbers[i + 1] < 0) {
    throw new IllegalArgumentException(
    "Cannot pass negative numbers into this thread!");
    }
    if (numbers[i] > numbers[i + 1]) {
    // swap
    int temp = numbers[i];
    numbers[i] = numbers[i + 1];
    numbers[i + 1] = temp;
    finished = false;
    }
    }
    }
    }
    public static void main(String[] args) {
    int[] numbers = new int[]{2, -1, 56, 4, 7};
    ThreadingTest threadingTest = new ThreadingTest(numbers);
    threadingTest.start();
    }
    }
    class SimpleThreadExceptionHandler implements
    Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
    System.err.printf("%s: %s at line %d of %s%n",
    t.getName(),
    e.toString(),
    e.getStackTrace()[0].getLineNumber(),
    e.getStackTrace()[0].getFileName());
    }
    }
  2. 引入 Queue、BlockingQueue、ConcurrentMap 数据结构;
  3. 引入 JUC 线程池;
    • 每次提交任务时,如果线程数还没达到 coreSize 就创建新线程并绑定该任务。 所以第 coreSize 次提交任务后线程总数必达到 coreSize,不会重用之前的空闲线程。
    • 线程数达到 coreSize 后,新增的任务就放到工作队列里,而线程池里的线程则努力的使用 take()从工作队列里拉活来干。
    • 如果队列是个有界队列,又如果线程池里的线程不能及时将任务取走,工作队列可能会满掉,插入任务就会失败,此时线程池就会紧急的再创建新的临时线程来补救。
    • 临时线程使用 poll(keepAliveTime,timeUnit)来从工作队列拉活,如果时候到了仍然两手空空没拉到活,表明它太闲了,就会被解雇掉。
    • 如果 core 线程数+临时线程数 >maxSize,则不能再创建新的临时线程了,转头执行 RejectExecutionHanlder。默认的 AbortPolicy 抛 RejectedExecutionException 异常,其他选择包括静默放弃当前任务(Discard),放弃工作队列里最老的任务(DisacardOldest),或由主线程来直接执行(CallerRuns),或你自己发挥想象力写的一个。
  4. Arrays
    提供数组相关的一些工具类。
    1
    2
    3
    4
    5
    Arrays.sort(myArray);
    Arrays.toString(myArray)
    Arrays.binarySearch(myArray, 98)
    Arrays.deepToString(ticTacToe)
    Arrays.deepEquals(ticTacToe, ticTacToe3)

Override 支持协变

返回类型可以是父类中相应类型或其子类。

JVM

CDS(Class Data Sharing)

JRE installer 能将一些系统 jar 文件加载到一种私有内部表示方式,然后转储到一个文件内,称为“shared archive”,下次启动应用的时候可以直接使用这个包内的类数据,这样可以减少部分启动时间。

自动检测服务器级机器

如果机器至少有 2 CPUs 和至少 2GB 物理内存,use the Java HotSpot Server Virtual Machine (server VM) instead of the Java HotSpot Client Virtual Machine (client VM).,The aim is to improve performance even if no one configures the VM to reflect the application it’s running. In general, the server VM starts up more slowly than the client VM, but over time runs more quickly.

垃圾收集器自适应

服务器类机器默认垃圾回收器改为并行垃圾回收器。
可以指定性能目标,并行收集器可以自动调整堆的大小,比如:

1
2
3
4
-XX:GCTimeLimit=time-limit :花费在GC上的时间上限,默认是98,当超过上限时,会抛出OutOfMemory(HeapSpace)的异常
-XX:GCHeapFreeLimit=space-limit :Heap空闲空间的最低比例下限,默认是2,当超过下限时,会抛出OutOfMemory(HeapSpace)的异常
-XX:MaxGCPauseMillis=nnn :最长的GC暂停时间,如果时间过长,会相应调整空间的大小(单位是毫秒)
-XX:GCTimeRatio=nnn :最大的GC占总可用时间的比例,如果时间过长,会相应调整空间的大小(花费在GC上的时间比例不超过1 / (1 + nnn))

线程优先级

Thread 类中给出了三个线程优先级常量:

1
2
3
java.lang.Thread.MIN_PRIORITY = 1
java.lang.Thread.NORM_PRIORITY = 5
java.lang.Thread.MAX_PRIORITY = 10

默认情况下线程优先级为 java.lang.Thread.NORM_PRIORITY,我们可以自定义设置在[1..10]内。
JVM(Java HotSpot)将 Java 线程关联到唯一的一个 native thread。

网络编程(Socket)

InetAddress

IP 地址是在网络层封装上的,确定 Internet 上的一个唯一的地址,端口号是由传输层封装上的,标志主机上的一个服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class InetAddressTest {
public static void main(String args[]){
InetAddressTest.printAddress();
}
static void printAddress(){
try{
InetAddress address = InetAddress.getLocalHost();
System.out.println(address);//输出:机器名/IP地址
address = InetAddress.getByName("www.baidu.com");
System.out.println(address);//输出:域名/IP地址
InetAddress[] addresses = InetAddress.getAllByName("www.baidu.com");
for(InetAddress a : addresses){
System.out.println(a);
}
}
catch(UnknownHostException e){
e.printStackTrace();
}
}
}

URL 和 UrlConnection

UrlConnection 可以从一个 URL 中打开流,可以方便地进行 Http 数据的收发。
内部是使用 Socket 进行连接的。

1
2
3
4
5
6
7
8
9
// 获取链接属性
URL url = new URL("http://java.sun.com:80/docs/books/tutorial/index.html#DOWN");//#后面的DOWN是位置标识符,在获得网页后,浏览器将直接跳到网页的DOWN处读取
String protocal = url.getProtocol();
String host = url.getHost();
String file = url.getFile();
int port = url.getPort();
String ref = url.getRef();//获得#后面的
System.out.println(protocal + ", " + host + ", " + file + ", "
+ port + ", " + ref);

访问链接

1
2
3
4
5
6
7
8
9
10
// 访问链接读取数据
URL url = new URL("http://www.cnblogs.com/mengdd/archive/2013/03/09/2951877.html");
BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream()));

String line;
while((line = reader.readLine()) != null){
System.out.println(line);
}
reader.close();

下面是对 URLConnection 的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class URLTest {
public static void main(String args[]){
URLTest c = new URLTest();
c.createURL();
c.printURLParam();
c.readURL();
c.printURL();
}
void createURL(){
try{
URL url = new URL("http://localhost:8080/");
}
catch(MalformedURLException e){
e.printStackTrace();
}
}
void printURLParam(){
try{
URL url = new URL("http://java.sun.com:80/docs/books/tutorial/index.html#DOWN");//#后面的DOWN是位置标识符,在获得网页后,浏览器将直接跳到网页的DOWN处读取
String protocal = url.getProtocol();
String host = url.getHost();
String file = url.getFile();
int port = url.getPort();
String ref = url.getRef();//获得#后面的
System.out.println(protocal + ", " + host + ", " + file + ", "
+ port + ", " + ref);
}
catch(MalformedURLException e){
e.printStackTrace();
}
}
void readURL(){//将网页内容拷贝到本地
try{
URL url = new URL("http://www.baidu.com");
URLConnection conn = url.openConnection();
InputStream is = conn.getInputStream();
//或者直接is = url.openStream();
OutputStream os = new FileOutputStream("e:\\baidu.txt");
byte[] buffer = new byte[2048];
int length = 0;
while((length = is.read(buffer, 0, buffer.length)) != -1){
os.write(buffer, 0, length);
}
is.close();
os.close();
}
catch(MalformedURLException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
}
void printURL(){//读取网页内容到控制台
try{
URL url = new URL("http://www.baidu.com");
BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream()));
String line;
while((line = reader.readLine()) != null){
System.out.println(line);
}
reader.close();
}
catch(MalformedURLException e){
e.printStackTrace();
}
catch(IOException e){
e.printStackTrace();
}
}
}

TCP 和 UDP

它们都是位于传输层的协议,为应用进程提供服务,根据不同的应用场景,会使用不同的协议。
TCP 是基于连接的、面向流的协议,提供可靠通信,因此每次通信必须先建立连接,建立连接后可以分多次进行传输任务,并且保证数据的正确性。
UDP 是基于无连接的、面向数据报的协议,提供不可靠通信,每次通信只需要发送一次数据报,可以分多次发送,但不保证能否到达、到达的顺序。

Socket 是 TCP 的应用编程接口,DatagramSocket 是 UDP 的应用编程接口,他们之间没有继承关系(都实现 Closeable 接口)。
Socket 使用时需要先指定目标主机地址和端口号,然后打开 io 流进行操作
1.服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 监听8080端口
ServerSocket server = new ServerSocket(8080);
// 等待请求
Socket socket = server.accept();
// 进行通信
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line = reader.readLine();
System.out.println(line);
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println(line);
writer.flush(); // 不要忘了这个
// 关闭资源
writer.close();
reader.close();
socket.close();
server.close();

2.客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 监听8080端口
Socket socket = new Socket("127.0.0.1", 8080);
// 开始通信
PrintWriter writer = new PrintWriter(socket.getOutputStream());
writer.println("hello");
writer.flush();
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
String line = reader.readLine();
System.out.println(line);
// 关闭资源
writer.close();
reader.close();
socket.close();

SocketChannel 和 ServerSocketChannel

缓冲(Buffer):相当于货物
管道(Channel):相当于配货车,支持同时装多件货物。
选择器(Selector):是 SelectableChannel 的多路复用器。用于监控 SelectableChannel 的 IO 状况。相当于中转站的分拣员。

使用 DatagramSocket 进行 UDP 通信

下面的代码使用 DatagramSocket 实现 UDP 通信

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建服务器socket,指定端口
DatagramSocket socket = new DatagramSocket(7000);
// 接收信息保存到一个缓冲区,DatagramPacket(bytes, len)
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, 1024);
socket.receive(packet);
System.out.println(new String(buffer, 0, packet.getLength()));
// 响应信息,接收时需要指定地址和端口
String str = "Welcome!";
DatagramPacket packet1 = new DatagramPacket(str.getBytes(),
str.length(), packet.getAddress(), packet.getPort());
socket.send(packet1);
// 关闭资源
socket.close();

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
DatagramSocket socket = new DatagramSocket();
// 发送数据包
String str = "Hello World";
DatagramPacket packet = new DatagramPacket(str.getBytes(),
str.length(), InetAddress.getByName("localhost"), 7000);
socket.send(packet);
// 接收响应
byte[] buffer = new byte[1024];
DatagramPacket packet1 = new DatagramPacket(buffer, 100);
socket.receive(packet1);
System.out.println(new String(buffer, 0, packet1.getLength()));
// 关闭资源
socket.close();

下面的代码使用 NIO 实现数据报协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 发送
public void send() throws IOException{
// 打开数据报通道
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
ByteBuffer buf = ByteBuffer.allocate(1024);
Scanner scan = new Scanner(System.in);
while(scan.hasNext()){
String str = scan.next();
buf.put((new Date().toString() + ":\n" + str).getBytes());
buf.flip();
dc.send(buf, new InetSocketAddress("127.0.0.1", 9898));
buf.clear();
}
dc.close();
}
// 接收
public void receive() throws IOException{
// 传送数据报通道
DatagramChannel dc = DatagramChannel.open();
dc.configureBlocking(false);
dc.bind(new InetSocketAddress(9898));
Selector selector = Selector.open();
dc.register(selector, SelectionKey.OP_READ);
while(selector.select() > 0){
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey sk = it.next();
if(sk.isReadable()){
ByteBuffer buf = ByteBuffer.allocate(1024);
dc.receive(buf);
buf.flip();
System.out.println(new String(buf.array(), 0, buf.limit()));
buf.clear();
}
}
it.remove();
}
}

使用 NIO 实现简易 HttpServer

主要思路很简单:
(1) 服务器打开后首先为 Selector 注册一个 OP_ACCEPT 的 key,这样 select 时就能接收客户端请求了;
(2) 每接收一个请求后即为该 key 创建一个线程,处理该 key 的操作,操作包括 accept 和 read,对于前者,只需为该 key 的 selector 再注册一个 OP_READ 用于准备接下来的读请求;
(3) 读取时先读入一个 Buffer,首先解析请求头部分,直到遇到一个空行结束,因为这里只考虑 GET 请求,所以不必继续解析请求体了;
(4) 返回时,首先构建响应头,同样使用一个空行结束,然后构建响应体,写回客户端,结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
public class HttpServer {
public static void main(String[] args) throws IOException {
// 监听8080端口
ServerSocketChannel server = ServerSocketChannel.open();
server.socket().bind(new InetSocketAddress(8080));
// 设置为非阻塞模式
server.configureBlocking(false);
// 为server注册选择器
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
// 创建处理器
while(true) {
// 等待请求,每次阻塞3s,若超过3s线程继续运行,
// select(0)或select()表示一直阻塞
if(selector.select(3000) == 0) {
continue;
}
// 获取所有待处理的选择键
Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
while(keyIter.hasNext()) {
SelectionKey key = keyIter.next();
// 启动新线程以处理SelectionKey
new Thread(new HttpHandler(key)).run();
// 处理完毕后,移除当前key
keyIter.remove();
}
}
}

private static class HttpHandler implements Runnable {
private int bufferSize = 1024;
private String localCharset = "UTF-8";
private SelectionKey key;

public HttpHandler(SelectionKey key) {
this.key = key;
}
// 定义操作
private void handleAccept() throws IOException {
// 接受请求后,注册OP_READ选择键以等待下一次请求
SocketChannel clientChannel = ((ServerSocketChannel)key.channel()).accept();
clientChannel.configureBlocking(false);
// !!!请求报文被限制在1024个字节内
clientChannel.register(key.selector(),
SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
private void handleRead() throws IOException {
// 获取
SocketChannel sc = (SocketChannel) key.channel();
// 获取Buffer并重置
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
// 读取,并判断内容是否为空,若是则关闭并退出
if(sc.read(buffer) == -1) {
sc.close();
return;
}
// 接收请求数据
buffer.flip();
String receivedString = Charset.forName(localCharset).newDecoder().
decode(buffer).toString();

// 打印请求报文头
String[] requestMessage = receivedString.split("\r\n");
for(String s: requestMessage) {
System.out.println(s);
// 遇到空行说明报文头已经打印完
if(s.isEmpty()) {
break;
}
}
// 控制台打印首行信息
String[] firstLine = requestMessage[0].split(" ");
System.out.println();
System.out.println("Method:\t" + firstLine[0]);
System.out.println("url:\t" + firstLine[1]);
System.out.println("HTTP Version:\t" + firstLine[2]);
System.out.println();
// 返回客户端(!!!考虑对不同的Url和不同的请求方法进行不同的处理和响应)
StringBuilder sendString = new StringBuilder();
sendString.append("HTTP/1.1 200 OK\r\n"); // 响应报文首行
sendString.append("Content-Type:text/html;charset=" + // !!!如果要传输流数据必须修改Content-Type
localCharset + "\r\n");
sendString.append("\r\n"); // 报文结束后加一空行
// 响应体
sendString.append("<html><head><title>显示报文</title></head><body>");
sendString.append("接收到请求报文是: <br/>");
for(String s: requestMessage) {
sendString.append(s + "<br/>");
}
sendString.append("</body></html>");
// 使用缓冲区写入channel
buffer = ByteBuffer.wrap(sendString.toString().getBytes(localCharset));
sc.write(buffer);
// 关闭资源
sc.close();
}

public void run() {
try {
// 根据请求类型进行转发
if(key.isAcceptable()) {
handleAccept();
}
if(key.isReadable()) {
handleRead();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

使用 NIO-Selector 实现简易聊天室

客户端

声明数据结构

1
2
3
4
// 管道、选择器、字符集
private SocketChannel sc = null;
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");

创建线程类用于从服务端获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
private class ClientThread extends Thread {
public void run() {
try {
// 遍历所有选择键
while(selector.select() > 0) {
for(SelectionKey sk : selector.selectedKeys()) {
// 删除正在处理的
selector.selectedKeys().remove(sk);
// 如果该键对应的通道中有可读的数据
if(sk.isReadable()) {
// 使用缓冲区读取管道内的数据
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
// 打印
System.out.println("聊天信息" + content);
sk.interestOps(SelectionKey.OP_READ);
}
}
}
} catch(IOException e) {
e.printStackTrace();
}
}
}

初始化

1
2
3
4
5
6
7
// 初始化SocketChannel
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999);
sc = SocketChannel.open(isa);
sc.configureBlocking(false);
// 注册选择器
selector = Selector.open();
sc.register(selector, SelectionKey.OP_READ);

创建线程从服务端拉取数据,及不断从键盘读入发送到服务端

1
2
3
4
5
6
7
8
// 启动线程不断从服务端拉取
new ClientThread().start();
// 读取键盘输入到通道
Scanner reader = new Scanner(System.in);
while(reader.hasNextLine()) {
String line = reader.nextLine();
sc.write(charset.encode(line));
}

服务端

声明

1
2
3
// 选择器、字符集
private Selector selector = null;
private Charset charset = Charset.forName("UTF-8");

初始化

1
2
3
4
5
6
7
8
// 打开管道
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 9999);
server.socket().bind(isa);
server.configureBlocking(false);
// 打开选择器
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);

接受连接,读取及发送数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 依次处理选择器上的选择键
while(selector.select() > 0) {
for(SelectionKey sk : selector.selectedKeys()) {
selector.selectedKeys().remove(sk);
// 连接请求
if(sk.isAcceptable()) {
SocketChannel sc = server.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
sk.interestOps(SelectionKey.OP_ACCEPT);
}
// 存在可读取数据
if(sk.isReadable()) {
// 使用缓冲区读取
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buff = ByteBuffer.allocate(1024);
String content = "";
try {
while(sc.read(buff) > 0) {
buff.flip();
content += charset.decode(buff);
}
System.out.println("=======" + content);
// 将管道设置为准备下一次读取
sk.interestOps(SelectionKey.OP_READ);
} catch(IOException e) {
// 如果该sk对应的管道出现异常,表明管道的客户端出现异常,
// 所以从选择器中取消sk
e.printStackTrace();
sk.cancel();
if(sk.channel() != null) {
sk.channel().close();
}
}
// 说明聊天信息不为空
if(content.length() > 0) {
// 将聊天信息输入每个选择键对应的管道中
for(SelectionKey key : selector.keys()) {
Channel targetChannel = key.channel();
if(targetChannel instanceof SocketChannel) {
SocketChannel dest = (SocketChannel) targetChannel;
dest.write(charset.encode(content));
}
}
}
}
}
}

QA

  1. 为什么 Socket 可以通过流来“持续地”读写,而 DatagramSocket 却只能一个一个数据报发哩?
    这是由 TCP 和 UDP 的协议决定的,TCP 是面向流的协议,而 UDP 是面向数据报的协议。
  2. 可以用 TCP 客户端连接 UDP 服务器吗(或者反过来)?
    不能,实验过确实不行,但是我还是心存疑惑,我猜测是因为接收方可以判断数据包的协议类型来确定是否接收。
  3. socket 是怎么实现”全双工”的?

参考

  1. Java Code Examples
  2. Java 应用中的日志
  3. 错误处理的推荐实践
  4. Java 开发中 10 个最为微妙的最佳编程实践
  5. 《Effective Java》
  6. 阿里巴巴编码规范(Java)
  7. Java 泛型中的 PECS 原则

JNI

  1. Program Library HOWTO (how to create and use program libraries on Linux)
  2. Java and C/C++: JNI Guide

Java5

  1. Java5 的新特性
  2. New Features and Enhancements J2SE 5.0
  3. java 泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题

网络

  1. HttpClient 使用详解
  2. 基于 JavaMail 的 Java 邮件发送:简单邮件发送
  3. qq 邮箱服务器地址

工程

  1. IntelliJ IDEA 使用教程(2019 图文版) – 从入门到上瘾

公司内接手的老项目近段时间遇到了内存瓶颈,新发版 GC 日志刷个不停,且集中在 YGC,显然有不自然的内存分配,看来内存优化是绕不过了。
在 Java 技术栈内,对内存的分析优化主要集中在堆中,往往需要先使用一些内存分析工具导出堆的一份快照,然后查看是哪些对象在浪费空间,它们可能是非常大的、非常短命的。
除了 JMM,对宿主机的内存管理原理也是有必要掌握的,这样能从底层的角度来进行解释 JMM 的原理,在针对 JMM 进行调参的时候也能更有把握(和运维撕的时候也更不容易被忽悠),鉴于现在的生产环境绝大多数都是 Linux,因此我也会对 Linux 的虚拟内存管理机制作一个简单分析。

阅读全文 »

本地缓存

由线上缓存 bug 引起的对本地缓存的思考

LoadingCache 是 Guava 提供的一个本地缓存组件,但是我对它是又爱又恨,一方面因为 LoadingCache 比较完善,免去很多应用层缓存的细节问题(如何写出 GC 友好的缓存?),而另一方面如果对 LoadingCache 了解不够深入又容易出现奇奇怪怪的问题。下面就来描述一下之前碰到过的两个 LoadingCache 的坑,首先给出最简单的缓存配置:

1
2
3
4
5
6
7
8
9
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.expireAfterAccess(60 * 1000, MILLISECONDS) // 1
.maximumSize(500)
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String id) throws Exception {
return query(id); // 2
}
});
  1. 在构建 LoadingCache 的时候可以配置缓存的过期策略,LoadingCache 其实有三种常用的过期策略:
    • expireAfterAccess:在最后一次访问后空闲一段时间才过期;
    • expireAfterWrite:在最后一次写后空闲一段时间才过期;
    • refreshAfterWrite:同样是写后空闲一段时间过期,和上一个的区别是不会阻塞过期时到达的请求,因为刷新一般需要请求远程服务来获取数据,会有比较长的延迟,refreshAfterWrite 会先返回旧数据,而 expireAfterWrite 会先阻塞这些请求。
      如果是为了吞吐量起见,一般使用 refreshAfterWrite 更多,如果是为了保证同步性,则是使用 expireAfterWrite 更多。
      因为我们使用缓存的场景是“读多写少的场景”,读端是提供给用户的,而写端由甲方客户控制,当它们更新了某个 id 的数据后,希望能够马上展示到用户眼前,换句话说,缓存应当能够被马上刷新,但是前面的配置中使用的是 expireAfterAccess,因为用户的访问非常频繁,所以缓存一直不能过期,上线的数据不能及时地生效,导致甲方爸爸非常生气。
  2. 缓存更新的时候一般会从远程服务或数据库查询数据,这里没有考虑返回值为空的情况,为了保险起见,一般都是需要进行空值校验的,而且如果这里返回了空值,LoadingCache 会直接抛出异常。

所以更合理的配置方式应该是下面这样的:

1
2
3
4
5
6
7
8
9
10
LoadingCache<String, Object> cache = CacheBuilder.newBuilder()
.refreshAfterWrite(60 * 1000, MILLISECONDS) // 1
.maximumSize(500)
.build(new CacheLoader<String, Object>() {
@Override
public Object load(String id) throws Exception {
Object res = query(id); // 2
return res == null ? DEFAULT_OBJ : res;
}
});

所以,当我们在使用缓存时,一般是事先考虑:

  • dataSource:当没有命中时,从哪里获取数据?一般请求下会调用其他服务的远程接口,也可以直接从数据库、缓存中间件查询,但是要注意数据隔离性,比如权限服务就不适合直接到缓存或数据库中查询订单数据,因为这样不利于后期扩容,而且入口越多越不安全;
  • expire:定义淘汰策略,比如有一段时间没有访问就淘汰,比如容量限制,当超出容量的时候如何淘汰一般有 LRU(Least Recently Used)、LFU(Least Frequently Used),可以使用 weigher 设置每个 key 的权重;

如果是一些要求不高的内部信息管理系统,这些属性不需要太关注,但是如果是对并发量有一定要求的系统,对自己所使用的工具知根知底是最低的要求。

本地缓存是什么

本地缓存的英文是 Local Cache,Cache、Buffer、Pool 是经常出现但又容易混淆的一组概念,它们都能存取数据,但是有本质上的区别:

  • Cache 的主要功能是“将东西放到更容易拿到的地方”、从而加快速度,具有随机存取的功能,一般为了不导致内存溢出会设置数据的过期回收策略,比如计算机体系结构中的 L1、L2、L3 缓存;
  • Buffer 是为了缓冲、减少对脆弱系统的冲击,具有顺序访问的特点,比如每个 TCP Socket 都有的接收发送缓存区;
  • Pool 是为了缓存资源,它和 Cache 的主要区别是 Pool 中缓存的对象往往是同构而没有特殊价值的数据,比如连接池中存储的数据库连接,所以 Pool 不需要随机存取功能、随取随用即可。

本地缓存的优点

  • 节省了了内⽹带宽。
  • 响应时延会更低。

本地缓存的缺点

⽆法保证⼀致性,解决办法是:

  1. 单节点通知其他节点,但是会导致同⼀服务的多个节点相互耦合;
  2. 利用 mq 通知其他节点,但系统会变得更复杂;
  3. 使用 timer 定时从后端拉取更新内存缓存,但在更新数据后、访问其他节点会得到脏数据,直到其他节点 timer 拉取数据。

本地缓存如何保证一致性

现在基本没有对外应用会是单机部署的,本地缓存是将数据保存到实例本身的内存中,所以一致性问题就是:我们怎么保证同一时间从每台实例上获取到的数据都是相同的?

  • 集群广播:当数据源变更时,发消息通知所有实例
    优点:实现一致性
    缺点:不适合更新特别频繁的场景,可能产生消息堆积。
  • 定时拉取:每台实例定时从数据源拉取数据更新本地缓存
    优点:实现简单,使用Guava就可以实现;
    缺点:无法控制每台实例同时去拉取,可能有的拉到了,有的还没有到定时拉取的时间。
  • zk同步:将机器注册到zk,所有机器都注册一个watcher,当有数据变更时通知所有实例。
    优点:类似消息同步的方式实现一致性。
    缺点:引入zk提升复杂度,zk并不保证高可用(CP)。

什么时候需要本地缓存

分层架构设计,有⼀条准则:站点层、服务层要做到⽆数据⽆状态,这样才能任意的加节点⽔平扩展,数据和状态尽量存储到后端的数据存储服务,例如数据库服务或者缓存服务。
可以看到,站点与服务的进程内缓存,实际上违背了分层架构设计的⽆状态准则,故一般情况下并不推荐使用
在分布式缓存存在的情况下,一般本地缓存都是不必要的,一方面本地缓存会占用大量的堆空间,容易引起频繁的 GC;另一方面,因为是在局域网内,所以访问分布式缓存的网络开销不会太大。

那么,什么时候可以使⽤进程内缓存?以下情况,可以考虑使用进程内缓存,并且应该注意对过期策略、并发安全等的定义。

  1. 只读数据,可以考虑在进程启动时加载到内存。

    当然此时也可以把数据加载到 redis / memcache 等缓存中间件,进程外缓存同样能解决这个问题。

  2. 性能敏感、极其⾼并发的、如果透传对后端压力极大的场景,可以考虑使用进程内缓存。例如,首页列表、秒杀业务,并发量极高,需要站点层挡住流量,可以使⽤内存缓存。
  3. 一定程度上允许数据不一致的业务。
    例如,有一些计数场景,运营场景,⻚面对数据⼀致性要求较低,可以考虑使⽤进程内⻚面缓存。

避免过早优化

后端开发基本都是完美主义者(粗心导致留下 Bug 可是会被产品、测试鄙视的),但是完美主义也有一个缺点——容易过早优化。
比如,项目早期使用者不多、订单只有 10W~100W 的量级,但是开发刚上来在对行业知识、产品使用场景都没有深刻理解的情况下,直接决定对 id 散列来进行分表,这个对我们来说当然是无可厚非的,但是随着业务扩大、订单量增加到 100W~1000W,发现线上数据库中近期的订单被使用得更多(即热数据)、而老订单一般不被问津,所以原来那种新老混杂的分表就不合适了,但是现在再重构成按时间分表的方式就费事了。因此,更好的方式是刚开始仅用单表存就足够了,之后时刻关注线上使用的反馈,即时地进行优化。

Java 引用

Java 中除了基本类型外所有对象都是通过引用来使用的,引用分为强引用、软引用、弱引用和虚引用。
对于一般的缓存场景来说,软引用是更好的选择,因为软引用可以避免内存用完而 GC 又回收不了内存进而导致的服务宕机,又不会像弱引用那样每次 GC 都会被回收掉、连带导致缓存被击穿。

应用 - 失败次数统计

一般调用 RPC 接口都会有重试逻辑,最简单的重试可以用一个局部变量记录失败次数:

1
2
3
4
5
6
7
8
9
10
int failedCount = 0;
while(failedCount < 3) {
try {
rpcService.hello();
break;
} catch (Exception e) {
logger.warn("调用失败 " + failedCount + " 次", e);
failedCount++;
}
}

如果不是需要实时响应的功能,可以用一个队列缓存请求,然后用一个线程轮询,因为失败后需要重新丢进队列中等待,这时就不能单纯使用局部变量来保存失败次数了,可以使用一个 Cache<string, AtomicInteger>软引用缓存失败调用记录,成功后再使其失效。这种方式能控制失败重试次数,而且当内存不足时,缓存数据可以被 GC 回收以腾出一些空间。
以 Guava 中的 LoadingCache 为例:

1
2
3
4
5
6
7
8
9
10
private LoadingCache<String, AtomicInteger> failedCache = 
CacheBuilder.newBuilder()
.softValues()
.maximumSize(10000)
.build(new CacheLoader<String, AtomicInteger>() {
@Override
public AtomicInteger load(String id) throws Exception {
return new AtomicInteger(0);
}
});
  • 当失败时,调用 failedCache.getUnchecked(id).incrementAndGet()增加失败次数。
  • 当成功时,调用 failedCache.invalidate(id)使缓存失效。

GC 友好的缓存

diff(在不等的情况下才 put 或直接修改已有对象,提高内存利用率,不然每次刷新缓存都要放到年轻代。不能用分离链接法实现,因为老的会引用年轻的(每次 put 到头部不就可以了?),导致年轻代不能转移到老年代)

如何避免 OOM(弱引用)

缓存淘汰机制
过期策略

如何实现一个本地缓存 - ConcurrentHashMap

  1. 线程安全的 Map
  2. 回收机制
    如 LRU
  3. 软引用

分布式缓存

本地缓存和分布式缓存

缓存一般分为本地缓存和分布式缓存两种。本地缓存指的是将数据存储在本机内存中,操作缓存数据的速度很快,但是缺点也很明显:第一,缓存数据的数量与大小受限于本地内存;第二,如果有多台应用服务器,可能所有应用服务器都要维护一份缓存,这样就占用了很多的内存。
分布式缓存正好解决了这两个问题。首先,数据存储在了另外的机器上,理论上由于可以不断添加缓存机器,所以缓存的数据的数量是无限的;其次,缓存集中设置在远程的缓存服务器上,应用服务器不需要耗费空间来维护缓存。但是,分布式缓存也是有缺点的,比如由于是远程操作,所以操作缓存数据的速度相较于本地缓存慢很多。
当前用得最多的本地缓存是 GoogleGuavache,用得最多的分布式缓存是 Memcached 和 Redis

缓存穿透

概念及场景:缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能 DB 就挂掉了,要是有人利用不存在的 key 频繁攻击我们的应用,这就是漏洞。
解决方案:有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
总而言之,当通过一个 key 去数据库查询出来的数据结果为 null,缓存系统就不会缓存该数据,每次该 key 查询都会经过数据库层,造成没有必要的 DB 开销。这种情况下,我们可以将该 key 缓存至缓存系统中,value 为一个特殊值(^^,&&…)。

缓存雪崩

概念及场景

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到 DB,DB 瞬时压力过重雪崩。
key 缓存过期失效而新缓存未到期间,该 key 的查询所有请求都会去查询数据,造成 DB 压力上升,产生不必要的 DB 开销

解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
解决方案总结:

  1. 加锁排队重建,使请求可以串行化,而不用全部的请求都去查询数据库
  2. 假设 key 的过期时间是 A,创建一个 key_sign,它的过期时间比 A 小,查询 key 的时候检查 key_sign 是否已经过期,如果过期则加锁后台起一个线程异步去更新 key 的值,而实际的缓存没有过期(如果实际缓存已经过期,需要加锁排队重建),但是会浪费双份缓存
  3. 在原有的 value 中存一个过期值 B,B 比 A 小,取值的时候根据 B 判断 value 是否过期,如果过期,解决方案同上
  4. 牺牲用户体验,当发现缓存中没有对应的数据直接返回失败,并且把需要的数据放入一个分布式队列,后台通过异步线程更新队列中需要更新的缓存

缓存污染

概念和场景

一些非正常操作(比如导出 excel 文件、运营偶发性访问)而导致内存中出现很多冷数据

解决方案

选取合适的缓存算法(LUR-N 算法)。

缓存首次上线

概念及场景

缓存首次上线,如果网站的访问量很大,所有的请求都经过数据库(如果访问量比较少,可以由用户访问自行缓存)

解决方案

缓存预热,在系统上线之前,所有的缓存都预先加载完毕(增加一个刷新缓存程序,上线后手动刷新或发布时自动调用刷用)

缓存击穿

概念及场景:对于一些设置了过期时间的 key,如果这些 key 可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一 key 缓存,前者则是很多 key。缓存在某个时间点过期的时候,恰好在这个时间点对这个 Key 有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端 DB 压垮。

解决方案

1.使用互斥锁(mutex key)
业界比较常用的做法,是使用 mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去 load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如 Redis 的 SETNX 或者 Memcache 的 ADD)去 set 一个 mutex key,当操作返回成功时,再进行 load db 的操作并回设缓存;否则,就重试整个 get 缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在 redis2.6.1 之前版本未实现 setnx 的过期时间,所以这里给出两种版本代码参考:
//2.6.1 前单机版本锁
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, “1”)) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息 50 毫秒后重试
Thread.sleep(50);
get(key);
}
}
}
最新版本代码:
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置 3min 的超时,防止 del 操作失败的时候,下次缓存过期一直不能 load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经 load db 并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
memcache 代码:
if (memcache.get(key) == null) {
// 3 min timeout to avoid mutex holder crash
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
}

除了使用Redis加锁,zk也是常见的分布式锁实现方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
try {
value = redis.get(key);
if (Objects.isNull(value)) {
long start = System.currentTimeMillis();

InterProcessLock lock = ZKSpringFactory.get().opsForLock(key.toString());
try {
// 获取zk分布式锁
acquire(lock, key);
if (Objects.isNull(value = redis.get(key))) {
// 利用客户端传入的callback回源,一般是查数据库
value = callback.call();
redis.setnx(key,value.toString(),expireTime,timeUnit);
return value;
}
} catch (Throwable throwable) {
logger.error("加载redisson key异常,key={}", key, throwable);
// TODO 报警
throw UnsafeUtil.throwException(throwable);
} finally {
try {
lock.release();
M.zk_lock_time.timer().get().update(System.currentTimeMillis() - start, TimeUnit.MILLISECONDS);
} catch (Throwable throwable) {
logger.error("释放lock异常", throwable);
// TODO 报警
try {
lock.release();
} catch (Exception e) {
logger.error("释放lock异常", throwable);
}
}
}
}
} catch (Exception ex) {
logger.error("redisson 获取值异常,key:"+key,ex);
}

private static void acquire(InterProcessLock lock, String key) throws Exception {
if (!lock.acquire(LOCK_EXPIRE_SECOND, TimeUnit.SECONDS)) {
logger.error("获取lock超时,key={}", key);
// 报警
throw new BizException("网络繁忙,请您稍后再试");
}
}

2.”提前”使用互斥锁(mutex key):
在 value 内部设置 1 个超时值(timeout1), timeout1 比实际的 memcache timeout(timeout2)小。当从 cache 读取到 timeout1 发现它已经过期时候,马上延长 timeout1 并重新设置到 cache。然后再从数据库加载数据并设置到 cache 中。伪代码如下:
v = memcache.get(key);
if (v == null) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
value = db.get(key);
memcache.set(key, value);
memcache.delete(key_mutex);
} else {
sleep(50);
retry();
}
} else {
if (v.timeout <= now()) {
if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
// extend the timeout for other threads
v.timeout += 3 * 60 * 1000;
memcache.set(key, v, KEY_TIMEOUT * 2);

        // load the latest value from dbplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplainplain
        v = db.get(key);    
        v.timeout = KEY_TIMEOUT;    
        memcache.set(key, value, KEY_TIMEOUT * 2);    
        memcache.delete(key_mutex);    
    } else {    
        sleep(50);    
        retry();    
    }    
}    

}

3.”永远不过期”:
这里的“永远不过期”包含两层意思:
(1) 从 redis 上看,确实没有设置过期时间,这就保证了,不会出现热点 key 过期问题,也就是“物理”不过期。
(2) 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在 key 对应的 value 里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 异步更新后台异常执行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = “mutex:” + key;
if (redis.setnx(keyMutex, “1”)) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}

  1. 资源保护:
    采用 netflix 的 hystrix,可以做资源的隔离保护主线程池,如果把这个应用到缓存的构建也未尝不可。
解决方案 优点 缺点
简单分布式互斥锁(mutex key) 1. 思路简单;2. 保证一致性 1. 代码复杂度增大;2. 存在死锁的风险;3. 存在线程池阻塞的风险
“提前”使用互斥锁 1. 保证一致性 同上
不过期(本文) 1. 异步构建缓存,不会阻塞线程池 1. 不保证一致性;2. 代码复杂度增大(每个 value 都要维护一个 timekey);3. 占用一定的内存空间(每个 value 都要维护一个 timekey)。
资源隔离组件 hystrix(本文) 1. hystrix 技术成熟,有效保证后端;2. hystrix 监控强大。 1. 部分访问存在降级策略。

设计总结

针对业务系统,永远都是具体情况具体分析,没有最好,只有最合适。
最后,对于缓存系统常见的缓存满了和数据丢失问题,需要根据具体业务分析,通常我们采用 LRU 策略处理溢出,Redis 的 RDB 和 AOF 持久化策略来保证一定情况下的数据安全。

  1. 缓存失效策略
    添加 key 的时候要设置一个过期时间,采用惰性删除和定时删除相结合的策略删除过期键
  2. 多级缓存
    线程级->内存级->进程级->文件(静态资源)->分布式(redis)->Db 结果.
  3. 二级缓存
    二级缓存更多的解决是,缓存穿透与程序的健壮性,当集中式缓存出现问题的时候,我们的应用能够继续运行;一些热点数据做成内存缓存,这些数据是在上线之前是已知的(比如说秒杀,大促商品),通过配置定时任务定时刷新内存缓存,完成和分布式缓存的数据置换;更加自动化的方案,可以根据上游自动发现热点数据,广播消息替换现在集群中内存缓存的数据(但在整个集群中广播,成本比较高,并且二级缓存的管理的成本也很大);

实现一个简单的多级缓存

多级数据来源

  • 本地缓存
    在并发量不大的系统内,本地缓存的意义不大,反而增加维护的困难。但在高并发系统中,本地缓存可以大大节约带宽。但是要注意本地缓存不是银弹,它会引起多个副本间数据的不一致,还会占据大量的内存,所以不适合保存特别大的数据,而且需要严格考虑刷新机制。
  • 缓存 / 搜索服务器
    TODO:
  • 数据库服务器
    TODO:
  • 同机房的其他业务服务器
    TODO:
  • 不同机房的其他业务服务器
    TODO:

多级缓存组件的主要执行流程
详细描述 TODO:

缓存时机

  • 5 分钟法则
    5 分钟法则即:如果一个数据的访问周期在 5 分钟以内则存放在内存中,否则应该存放在硬盘中。
    引申到缓存中,可以表述为:如果一个数据访问吞吐率大于 1 次 / 5 分钟,就可以考虑放到缓存中。
  • 局部性原理
    局部性原理原指 CPU 访问存储器时,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中。
    反过来说,如果访问存在热点,就完全可以把这些热点数据放到缓存里。

算法 - FIFO

算法 - LRU

算法 - LFU

回收策略

  • 空间
  • 容量
    条⽬数
  • 时间
    存活期活太久淘汰
    空闲期太久没访问淘汰

缓存监控

命中率 = 缓存读取次数 / (缓存读取次数 + 慢速设备读取次数)

过期时间

本地缓存过期时间比分布式缓存小至少一半,以防止本地缓存太久造成多实例数据不一致。

  • 不过期缓存
    场景:长尾访问的数据、访问频率⾼、缓存空间⾜够
    使⽤Cache-Aside 模式
    不要放事务⾥,因为⽹络抖动可能导致写缓存响应时间慢,阻塞数据库事务。但是同样存在事务成功但缓存失败⽆法回滚的情况。解决办法是使用 canal 实现缓存同步。
    若对⼀致性要求不高且数据量不⼤可改成定期全量同步
  • 过期缓存
    场景:热点数据、来⾃自其他系统的数据、空间有限、访问频率低

SoR(Source-of-Resource)

数据的来源,一般称为记录系统或数据源
回源即回到源头获取数据,Cache 没有命中时,需要从 SoR 获取数据,即回源。

大 Value

如果有⼤Value 最好切换到多线程实现的缓存如 MC,或者拆成多个小 Value 由客户端聚合。

热点缓存

频繁访问的热点数据,如果每次都要从缓存服务器获取,可能导致缓存服务器负载过高、或者带宽过⾼。
解决办法是加缓存服务器,或者加本地缓存。

缓存更新

原⼦更新:

  • 版本号
  • 如果是 redis,因为单线程机制本身就是⽀持原⼦更新的
  • 使用 canal 订阅数据库 binlog 将更新请求按规则路由到多个队列,每个队列进⾏单线程的更新
  • 加分布式锁

异步写

写本地缓存后异步更新分布式缓存,尽快返回用户请求,最好不要同步写分布式缓存。

维度化与增量缓存

场景:一个商品包含多个属性,其中部分属性如上下架这种可能频繁更新的,最好做维度化并增量更新。

缓存策略 - 分区读

读缓存时划分分区异步批量读:

  • 分区可以防⽌出现慢查询;
  • 异步可以把各批 key 并⾏化。

缓存策略 - nullobj 防缓存击穿

当 db 中本身就没有该数据时,会产生每次请求都击穿的现象,解决办法是引入一个 nullobj
db 不不存在时写一个 nullobj 到缓存,下次读到 null 对象 就不去 db 读了。

缓存策略 - Cache-aside 模式

Cache-Aside 即业务代码围绕着 Cache 写,是由业务代码直接维护缓存。

什么时候使用

  • 当 Cache不提供原⽣的 Read-Through 和 Write-Through 操作的时候
  • 资源的需求是不可预测的时候。Cache-Aside 模式令应用可以根据需求来加载数据,对于应⽤需求什么数据,不需要提前做出假设。

Read 模式

先从缓存获取数据,如果没有命中,则回源到 SoR 并将源数据放入缓存供下次读取使用。

Write 模式

类似 Write-Through 策略。

  1. 先将数据写入 SoR,写入成功后立即将数据同步写入缓存;
  2. 或先将数据写入 SoR,写入成功后将缓存数据过期,下次读取时再加载缓存。

读优化 - 一致性哈希

读可以⽤⼀致性哈希减少并发。

写优化 - 使用 Canal 订阅更新

更新可以⽤Canal 订阅 binlog。

缓存数据的⽣存时间

很多 Cache 实现了过期的策略的,这些过期的策略可以实现数据的更新,将旧数据失效化,同时也令⼀定时间没有访问的数据失效。
为了让 Cache-Aside 模式能够⽣效,开发者必须确保过期策略能够正确匹配应用所访问的数据。同时,注意不能让过期时间太短,因为太短的过期时间会令应⽤频繁地从数据仓库中获取数据来添加到 Cache 之中。当然,也不要配置超时的时间太⻓,过⻓的超时时间会让缓存的数据冗余。Cache 的性能是跟其相关的数据的读取周期等信息⾼度相关的。

去除数据

绝⼤多数的缓存跟数据仓库⽐起来,容量是很有限的,所以,如果可以的话,Cache 会移除数据。
多数的 Cache 会采用 LRU 的策略来移除缓存中的数据,当然,移除的策略也是可以⾃定义的。配置全局的过期属性和缓存的其他属性,可以确保 Cache 消耗的内存资源是高效的。当然,通常不会只配置⼀个全局的过期策略。比如,某些特别昂贵、访问特别频繁而又不常更新的数据,完全可以延长其过期时间。

一致性

实现 Cache-Aside 模式并不能保证 Cache 和数据仓库之间的数据⼀致性。因为数据仓库中的数据可能在任何时候被其他程序所修改,⽽这个修改不会及时的反映到 Cache 上,直到下一次 Cache 被刷新为止。如果数据仓库中数据频繁由⾮Cahce 程序更新的话,这种一致性问题会变得更加明显。

本地(内存)缓存

Cache 也是可以做到应⽤本身里⾯的。Cache-Aside 模式在⼀些应⽤频繁访问相同的数据的时候尤其有效。然⽽,本地 Cache 都是应⽤私有 的,是属于每个应用中独有的额外的拷⻉。所以这个数据可能很快在不同的应⽤中就不一致了,所以刷新的频率最好更快些以保证⼀致性。在 有些情况下可以使⽤共享的缓存,有的时候也可以使⽤本地 Cache,具体使⽤哪⼀种就需要根据实际的场景来判断了。

缓存策略 - Cache-as-SoR 模式

由 Cache 委托给 SoR 进⾏真实的读写

缓存策略 - Read-Through

读 miss 则由 cache 回源到 SoR(需要防⽌dog-pile effect 即 miss 时只允许⼀个请求回源而不是所有请求都回源)。

缓存策略 - Write-Through

由 cache 组件负责写缓存和 SoR(一般是先 SoR 再缓存)

缓存策略 - Write-Behind

异步(队列+线程池)写 SoR

缓存策略 - Copy-Pattern

Copy-On-Read
Copy-On-Write
本地缓存的是引⽤,被擅⾃修改可能引起不可预测的问题

参考

所有参考文献的归档,集中管理,方便随时查阅。

多级缓存架构

  1. 一篇文章让你明白你多级缓存的分层架构
  2. 一个牛逼的多级缓存实现方案
  3. 日访问量百亿级的微博如何做缓存架构设计
  4. 分布式内存缓存系统设计
  5. 那些年我们一起追过的缓存写法(一)

内存池

  1. 设计模式之争:新分配内存还是内存池?(含评测)

本地缓存

不得不承认本地缓存不比分布式缓存更简单,当我们讨论分布式缓存时,更多的是在讨论如何节省带宽、如何平滑扩容,但在本地缓存的范畴内,我们更多的需要关注所使用语言的内存管理机制、甚至需要向下探索到硬件层面(其实分布式缓存的基础一般也是本地缓存)。

  1. 146. LRU Cache
    460. LFU Cache
    LeetCode 上有几道缓存相关的问题,可以拿来作热身。
  2. Java 内存模型
    Java 中的垃圾回收技术已经比较完善了,开发人员能做的除了给出合理的配置外,就是要做到对自己使用的垃圾回收技术知根知底、能写出 GC 友好的代码。
    The Java Memory Model - William Pugh
    JSR 133 (Java Memory Model) FAQ
    Java 内存模型 FAQ
    上面这篇的中文翻译,适合我这种英语渣对照阅读。
    GC(GC 友好编程)
    Doug Lea’s Home Page
    Doug Lea 并发编程文章全部译文
  3. NonBlocking HashTable
    HashTable 是实现缓存的常用数据结构,而在操作时进行普通的加锁又非常影响性能,所以一般会做一些 NonBlocking 的优化。除了 JUC 的 ConcurrentHashMap,还有其他的一些相似实现。
    stephenc/high-scale-lib
    JCTools/JCTools
  4. guava - cache
    github - google/guava - Caches
  5. ehcache
    github - ehcache3
    玩转 EhCache 之最简单的缓存框架
  6. J2Cache
    J2Cache 是一个国产的本地缓存框架,同时也提供了二级缓存、多机同步等特性。
    红薯 / J2Cache
  7. Netty 中的对象池
    netty/netty - Reference counted objects
    Netty 源码 Recycler 对象池全面解析
    netty 源码分析 4 - Recycler 对象池的设计
    Netty 为了提高性能,IO 时直接使用非堆内存来缓存收发的内容(Buf 对象),在非堆内存中 GC 效率会比 JVM 的堆内存效率低(只能通过 FullGC 回收或 CMS GC),所以 Netty 内部维护了一个对象池(Recycler),使用引用计数法来回收不用的对象到对象池中,而不是直接回收,减少了 GC 的频率。
  8. C / C++ 内存模型
    C / C++ 只保证最基本的内存管理(malloc / free),因为其贴近操作系统的特性,很多框架都会封装一套自己的内存管理库(包括 memcached、MySQL、Cocos2d-x 等),甚至是 GC。
    《C 语言接口与实现》 - 第 2、4、5、6 章
    dlmalloc - Doug Lea
    内存管理(Memory) - 许式伟
    C++ Memory Management Innovation: GC Allocator
    《STL 源码剖析》 - 第 2 章
    《深入探索 C++对象模型》
  9. bangerlee/mempool
  10. 应用层内存管理
    Memory Management Reference
    一个神奇的网站,内存管理相关的概念、综述、深入参考文献基本都能在这里找到,而且偏应用层,讲解方式友好、适合扫盲。
  11. 操作系统层内存管理
    The Unix and Internet Fundamentals HOWTO
    非常精炼地解释了 Unix 系统和网络的基本原理。
  12. 硬件层内存管理

分布式缓存

  1. 分布式缓存设计及解决方案(后端)
    大型分布式网站架构
    浅谈缓存(一)
    那些年我们一起追过的缓存写法(一)
    缓存穿透、并发和失效,来自一线架构师的解决方案
  2. 《分布式缓存——原理、架构及 Go 语言实现》
  3. 荐书:《深入分布式缓存》
  4. Redis 教程及手册
    《Redis 设计与实现》
    《Redis 深度历险:核心原理与应用实践》
    Redis 命令参考
    Redis Command Reference
    Redis Documentation
  5. Redis 及客户端源码
    github - antirez/redis
    github - redisson/redisson
    github - xetorthio/jedis
    github - lettuce-io/lettuce-core
  6. Redis - 数据结构
    Redis 为何这么快
    Redis strings vs Redis hashes to represent JSON: efficiency?
    如果业务里需要使用到对象的单个域则使用 hash 类型保存 JSON,否则使用 string。
  7. Redis - Sentinel
    Sentinel 是 Redis 提供的一种高可用集群方案,它能实现自动的故障转移。
    Redis Sentinel Documentation
    Sentinel Clients
    Jepsen: Redis
    Reply to Aphyr attack to Sentinel
    Asynchronous replication with failover
  8. Redis - Cluster
    Redis Cluster 不是使用一致性 hash、而是利用哈希槽的模式来分配数据,相对来说更简单,但是数据迁移的时候成本也会更大。
    Redis cluster tutorial
    Redis Cluster Specification
  9. 在 Spring 架构后台中使用 Redis
    spring-framework-reference - 8. Cache Abstraction
    Spring Data Redis
    Spring Boot Reference Guide
  10. Redis 最佳实践
    阿里云 Redis 开发规范
    你所不知道的 Redis 热点问题以及如何发现热点
    史上最全 50 道 Redis 面试题(含答案),以后面试再也不怕问 Redis 了
  11. Redis 应用
    一文看透 Redis 分布式锁进化史(解读 + 缺陷分析)
    How to do distributed locking
    刷新 Redis 缓存时需要加分布式锁,保证只有一个线程能够写入缓存。
    INCR key (分布式限流器的例子)
    布隆过滤器实战【防止缓存击穿】
    NOSQL 数据建模技术
  12. github - memcached
    memcached 是经常和 Redis 一并提起的一个分布式缓存中间件,了解其原理能对分布式缓存的实现模式有更好的认识。

Web 缓存

Web 缓存指的是在服务器和客户端之间的缓存,不同于我们上边提到的都是服务器(本地缓存)及服务器之后的缓存(分布式缓存)。
Web 缓存其实不仅仅指浏览器内的缓存,在剖析从客户端发送请求到服务器接收为止的一系列链路之后,可以发现 Web 缓存主要包括浏览器缓存、代理服务器缓存、网关缓存。

  1. Web 开发人员需知的 Web 缓存知识

测试

测试是发现问题的手段,最好的解决问题方式是避免问题。

  1. 《Java 并发编程实战》 - 第三部分(并发安全及性能)
  2. Redis 有多快?
  3. 《构建高性能 Web 站点》 - 第 3 章
  4. 《Java 程序性能优化》
  5. 《Web 性能权威指南》
  6. 《OptimizingLinux(R)PerformanceAHands-OnGuidetoLinux(R)PerformanceTools》
  7. 《性能之巅》

运维

  1. 《The Art of Capacity Planning》
  2. 探寻 Redis 内存诡异增长的元凶

其他

  1. How To Ask Questions The Smart Way
  2. Software Release Practice HOWTO
  3. github - hyperoslo/Cache
    这个项目同样在缓存上做文章,不过是用 swift 写的。
  4. Learn X in Y minutes - Where X=Lua
  5. 《设计模式》 - Factory, Builder, Proxy, Chain of Responsibility, Command, Iterator, template method
  6. 《Spring 源码深度解析》 - 第 5、6、7 章
  7. 使用 AI 生成图标

在一次编译 jvm 的过程中,尝试将没用版本的 gcc、g++卸载,但是没想到 apt 非常智能地把无线网卡等一系列东西都给删掉了,启动后连网都上不了了,重装系统又觉得麻烦,结果开始了苦逼的填坑之旅。

阅读全文 »
0%