此处我说的HTTP服务主要指如访问京东网站时我们看到的热门搜索、用户登录、实时价格、实时库存、服务支持、广告语等这种非Web页面,而是在Web页面中异步加载的相关数据。这些服务有个特点即访问量巨大、逻辑比较单一;但是如实时库存逻辑其实是非常复杂的。在京东这些服务每天有几亿十几亿的访问量,比如实时库存服务曾经在没有任何IP限流、DDos防御的情况被刷到600多万/分钟的访问量,而且能轻松应对。支撑如此大的访问量就需要考虑设计良好的架构,并很容易实现水平扩展。

架构

此处介绍下我曾使用过Nginx+JavaEE的架构。

1、单DB架构

早期架构可能就是Nginx直接upstream请求到后端Tomcat,扩容时基本是增加新的Tomcat实例,然后通过Nginx负载均衡upstream过去。此时数据库还不是瓶颈。当访问量到一定级别,数据库的压力就上来了,此处单纯的靠单个数据库可能扛不住了,此时可以通过数据库的读写分离或加缓存来实现。

2、DB+Cache/数据库读写分离架构

此时就通过使用如数据库读写分离或者Redis这种缓存来支撑更大的访问量。使用缓存这种架构会遇到的问题诸如缓存与数据库数据不同步造成数据不一致(一般设置过期时间),或者如Redis挂了,此时会直接命中数据库导致数据库压力过大;可以考虑Redis的主从或者一致性Hash 算法做分片的Redis集群;使用缓存这种架­构要求应用对数据的一致性要求不是很高;比如像下订单这种要落地的数据不适合用Redis存储,但是订单的读取可以使用缓存。

3、Nginx+Lua+Local Redis+Mysql集群架构

首先Nginx通过Lua读取本机Redis缓存,如果不命中才回源到后端Tomcat集群;后端Tomcat集群再读取Mysql数据库。Redis都是安装到和Nginx同一台服务器,Nginx直接读本机可以减少网络延时。Redis通过主从方式同步数据,Redis主从一般采用树的方式实现:

在叶子节点可以做AOF持久化,保证在主Redis挂时能进行恢复;此处假设对Redis很依赖的话,可以考虑多主Redis架构,而不是单主,来防止单主挂了时数据的不一致和击穿到后端Tomcat集群。这种架构的缺点就是要求Redis实例数据量较小,如果单机内存不足以存储这么多数据,当然也可以通过如尾号为1的在A服务器,尾号为2的在B服务器这种方式实现;缺点也很明显,运维复杂、扩展性差。

4、Nginx+Lua+ Redis集群+Mysql集群架构

和之前架构不同的点是此时我们使用一致性Hash算法实现Redis集群而不是读本机Redis,保证其中一台挂了,只有很少的数据会丢失,防止击穿到数据库。Redis集群分片可以使用Twemproxy;如果 Tomcat实例很多的话,此时就要考虑Redis和Mysql链接数问题,因为大部分Redis/Mysql客户端都是通过连接池实现,此时的链接数会成为瓶颈。一般方法是通过中间件来减少链接数。

Twemproxy与Redis之间通过单链接交互,并Twemproxy实现分片逻辑;这样我们可以水平扩展更多的Twemproxy来增加链接数。

此时的问题就是Twemproxy实例众多,应用维护配置困难;此时就需要在之上做负载均衡,比如通过LVS/HAProxy实现VIP(虚拟IP),可以做到切换对应用透明、故障自动转移;还可以通过实现内网DNS来做其负载均衡。

本文没有涉及Nginx之上是如何架构的,对于Nginx、Redis、Mysql等的负载均衡、资源的CDN化不是本文关注的点,有兴趣可以参考

很早的Taobao CDN架构

Nginx/LVS/HAProxy负载均衡软件的优缺点详解

实现

接下来我们来搭建一下第四种架构。

以获取如京东商品页广告词为例,如下图、

假设京东有10亿商品,那么广告词极限情况是10亿;所以在设计时就要考虑:

1、数据量,数据更新是否频繁且更新量是否很大;

2、是K-V还是关系,是否需要批量获取,是否需要按照规则查询。

而对于本例,广告词更新量不会很大,每分钟可能在几万左右;而且是K-V的,其实适合使用关系存储;因为广告词是商家维护,因此后台查询需要知道这些商品是哪个商家的;而对于前台是不关心商家的,是KV存储,所以前台显示的可以放进如Redis中。 即存在两种设计:

1、所有数据存储到Mysql,然后热点数据加载到Redis;

2、关系存储到Mysql,而数据存储到如SSDB这种持久化KV存储中。

基本数据结构:商品ID、广告词、所属商家、开始时间、结束时间、是否有效。

后台逻辑

1、商家登录后台;

2、按照商家分页查询商家数据,此处要按照商品关键词或商品类目查询的话,需要走商品系统的搜索子系统,如通过Solr或elasticsearch实现搜索子系统;

3、进行广告词的增删改查;

4、增删改时可以直接更新Redis缓存或者只删除Redis缓存(第一次前台查询时写入缓存);

前台逻辑

1、首先Nginx通过Lua查询Redis缓存;

2、查询不到的话回源到Tomcat,Tomcat读取数据库查询到数据,然后把最新的数据异步写入Redis(一般设置过期时间,如5分钟);此处设计时要考虑假设Tomcat读取Mysql的极限值是多少,然后设计降级开关,如假设每秒回源达到100,则直接不查询Mysql而返回空的广告词来防止Tomcat应用雪崩。

为了简单,我们不进行后台的设计实现,只做前端的设计实现,此时数据结构我们简化为[商品ID、广告词]。另外有朋友可能看到了,可以直接把Tomcat部分干掉,通过Lua直接读取Mysql进行回源实现。为了完整性此处我们还是做回源到Tomcat的设计,因为如果逻辑比较复杂的话或一些限制(比如使用Java特有协议的RPC)还是通过Java去实现更方便一些。

项目搭建

项目部署目录结构。

  1. /usr/chapter6
  2. redis_6660.conf
  3. redis_6661.conf
  4. nginx_chapter6.conf
  5. nutcracker.yml
  6. nutcracker.init
  7. webapp
  8. WEB-INF
  9. lib
  10. classes
  11. web.xml

Redis+Twemproxy配置

此处根据实际情况来决定Redis大小,此处我们已两个Redis实例(6660、6661),在Twemproxy上通过一致性Hash做分片逻辑。

安装

之前已经介绍过Redis和Twemproxy的安装了。

Redis配置redis_6660.conf和redis_6661.conf

  1. #分别为6660 6661
  2. port 6660
  3. #进程ID 分别改为redis_6660.pid redis_6661.pid
  4. pidfile "/var/run/redis_6660.pid"
  5. #设置内存大小,根据实际情况设置,此处测试仅设置20mb
  6. maxmemory 20mb
  7. #内存不足时,按照过期时间进行LRU删除
  8. maxmemory-policy volatile-lru
  9. #Redis的过期算法不是精确的而是通过采样来算的,默认采样为3个,此处我们改成10
  10. maxmemory-samples 10
  11. #不进行RDB持久化
  12. save “”
  13. #不进行AOF持久化
  14. appendonly no

将如上配置放到redis_6660.conf和redis_6661.conf配置文件最后即可,后边的配置会覆盖前边的。

Twemproxy配置nutcracker.yml

  1. server1:
  2. listen: 127.0.0.1:1111
  3. hash: fnv1a_64
  4. distribution: ketama
  5. redis: true
  6. timeout: 1000
  7. servers:
  8. - 127.0.0.1:6660:1 server1
  9. - 127.0.0.1:6661:1 server2

复制nutcracker.init到/usr/chapter6下,并修改配置文件为/usr/chapter6/nutcracker.yml。

启动

  1. nohup /usr/servers/redis-2.8.19/src/redis-server  /usr/chapter6/redis_6660.conf &
  2. nohup /usr/servers/redis-2.8.19/src/redis-server  /usr/chapter6/redis_6661.conf &
  3. /usr/chapter6/nutcracker.init start
  4. ps -aux | grep -e redis  -e nutcracker

Mysql+Atlas配置

Atlas类似于Twemproxy,是Qihoo 360基于Mysql Proxy开发的一个Mysql中间件,据称每天承载读写请求数达几十亿,可以实现分表、分库(sharding版本)、读写分离、数据库连接池等功能,缺点是没有实现跨库分表功能,需要在客户端使用分库逻辑,目前Atlas不活跃。另一个选择是使用如阿里的TDDL,它是在客户端完成之前说的功能。到底选择是在客户端还是在中间件根据实际情况选择。

此处我们不做Mysql的主从复制(读写分离),只做分库分表实现。

Mysql初始化

为了测试我们此处分两个表。

  1. CREATE DATABASE chapter6 DEFAULT CHARACTER SET utf8;
  2. use chapter6;
  3. CREATE TABLE  chapter6.ad_0(
  4. sku_id BIGINT,
  5. content VARCHAR(4000)
  6. ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
  7. CREATE TABLE  chapter6.ad_1
  8. sku_id BIGINT,
  9. content VARCHAR(4000)
  10. ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

Atlas安装

  1. cd /usr/servers/
  2. wget https://github.com/Qihoo360/Atlas/archive/2.2.1.tar.gz -O Atlas-2.2.1.tar.gz
  3. tar -xvf Atlas-2.2.1.tar.gz
  4. cd Atlas-2.2.1/
  5. #Atlas依赖mysql_config,如果没有可以通过如下方式安装
  6. apt-get install libmysqlclient-dev
  7. #安装Lua依赖
  8. wget http://www.lua.org/ftp/lua-5.1.5.tar.gz
  9. tar -xvf lua-5.1.5.tar.gz
  10. cd lua-5.1.5/
  11. make linux && make install
  12. #安装glib依赖
  13. apt-get install libglib2.0-dev
  14. #安装libevent依赖
  15. apt-get install libevent
  16. #安装flex依赖
  17. apt-get install flex
  18. #安装jemalloc依赖
  19. apt-get install libjemalloc-dev
  20. #安装OpenSSL依赖
  21. apt-get install openssl
  22. apt-get install libssl-dev
  23. apt-get install libssl0.9.8
  24. ./configure --with-mysql=/usr/bin/mysql_config
  25. ./bootstrap.sh
  26. make && make install

Atlas配置

  1. vim /usr/local/mysql-proxy/conf/chapter6.cnf
  1. [mysql-proxy]
  2. #Atlas代理的主库,多个之间逗号分隔
  3. proxy-backend-addresses = 127.0.0.1:3306
  4. #Atlas代理的从库,多个之间逗号分隔,格式ip:port@weight,权重默认1
  5. #proxy-read-only-backend-addresses = 127.0.0.1:3306,127.0.0.1:3306
  6. #用户名/密码,密码使用/usr/servers/Atlas-2.2.1/script/encrypt 123456加密
  7. pwds = root:/iZxz+0GRoA=
  8. #后端进程运行
  9. daemon = true
  10. #开启monitor进程,当worker进程挂了自动重启
  11. keepalive = true
  12. #工作线程数,对Atlas的性能有很大影响,可根据情况适当设置
  13. event-threads = 64
  14. #日志级别
  15. log-level = message
  16. #日志存放的路径
  17. log-path = /usr/chapter6/
  18. #实例名称,用于同一台机器上多个Atlas实例间的区分
  19. instance = test
  20. #监听的ip和port
  21. proxy-address = 0.0.0.0:1112
  22. #监听的管理接口的ip和port
  23. admin-address = 0.0.0.0:1113
  24. #管理接口的用户名
  25. admin-username = admin
  26. #管理接口的密码
  27. admin-password = 123456
  28. #分表逻辑
  29. tables = chapter6.ad.sku_id.2
  30. #默认字符集
  31. charset = utf8

因为本例没有做读写分离,所以读库proxy-read-only-backend-addresses没有配置。分表逻辑即:数据库名.表名.分表键.表的个数,分表的表名格式是table_N,N从0开始。

Atlas启动/重启/停止

  1. /usr/local/mysql-proxy/bin/mysql-proxyd chapter6 start
  2. /usr/local/mysql-proxy/bin/mysql-proxyd chapter6 restart
  3. /usr/local/mysql-proxy/bin/mysql-proxyd chapter6 stop

如上命令会自动到/usr/local/mysql-proxy/conf目录下查找chapter6.cnf配置文件。

Atlas管理

通过如下命令进入管理接口

  1. mysql -h127.0.0.1 -P1113  -uadmin -p123456

通过执行SELECT * FROM help查看帮助。还可以通过一些SQL进行服务器的动态添加/移除。

Atlas客户端

通过如下命令进入客户端接口

  1. mysql -h127.0.0.1 -P1112  -uroot -p123456
  1. use chapter6;
  2. insert into ad values(1 '测试1);
  3. insert into ad values(2, '测试2');
  4. insert into ad values(3 '测试3);
  5. select * from ad where sku_id=1;
  6. select * from ad where sku_id=2;
  7. #通过如下sql可以看到实际的分表结果
  8. select * from ad_0;
  9. select * from ad_1;

此时无法执行select * from ad,需要使用如“select * from ad where sku_id=1”这种SQL进行查询;即需要带上sku_id且必须是相等比较;如果是范围或模糊是不可以的;如果想全部查询,只能挨着遍历所有表进行查询。即在客户端做查询-聚合。

此处实际的分表逻辑是按照商家进行分表,而不是按照商品编号,因为我们后台查询时是按照商家维度的,此处是为了测试才使用商品编号的。

到此基本的Atlas就介绍完了,更多内容请参考如下资料:

Mysql主从复制

http://369369.blog.51cto.com/319630/790921/

Mysql中间件介绍

http://www.guokr.com/blog/475765/

Atlas使用

http://www.0550go.com/database/mysql/mysql-atlas.html

Atlas文档

https://github.com/Qihoo360/Atlas/blob/master/README_ZH.md

Java+Tomcat安装

Java安装

  1. cd /usr/servers/
  2. #首先到如下网站下载JDK
  3. #http://www.oracle.com/technetwork/cn/java/javase/downloads/jdk7-downloads-1880260.html
  4. #本文下载的是 jdk-7u75-linux-x64.tar.gz。
  5. tar -xvf jdk-7u75-linux-x64.tar.gz
  6. vim ~/.bashrc
  7. 在文件最后添加如下环境变量
  8. export JAVA_HOME=/usr/servers/jdk1.7.0_75/
  9. export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$PATH
  10. export CLASSPATH=$CLASSPATH:.:$JAVA_HOME/lib:$JAVA_HOME/jre/lib
  11. #使环境变量生效
  12. source ~/.bashrc

Tomcat安装

  1. cd /usr/servers/
  2. wget http://ftp.cuhk.edu.hk/pub/packages/apache.org/tomcat/tomcat-7/v7.0.59/bin/apache-tomcat-7.0.59.tar.gz
  3. tar -xvf apache-tomcat-7.0.59.tar.gz
  4. cd apache-tomcat-7.0.59/
  5. #启动
  6. /usr/servers/apache-tomcat-7.0.59/bin/startup.sh
  7. #停止
  8. /usr/servers/apache-tomcat-7.0.59/bin/shutdown.sh
  9. #删除tomcat默认的webapp
  10. rm -r apache-tomcat-7.0.59/webapps/*
  11. #通过Catalina目录发布web应用
  12. cd apache-tomcat-7.0.59/conf/Catalina/localhost/
  13. vim ROOT.xml

ROOT.xml

  1. <!-- 访问路径是根,web应用所属目录为/usr/chapter6/webapp -->
  2. <Context path="" docBase="/usr/chapter6/webapp"></Context>
  1. #创建一个静态文件随便添加点内容
  2. vim /usr/chapter6/webapp/index.html
  3. #启动
  4. /usr/servers/apache-tomcat-7.0.59/bin/startup.sh

访问如http://192.168.1.2:8080/index.html能处理内容说明配置成功。

  1. #变更目录结构
  2. cd /usr/servers/
  3. mv apache-tomcat-7.0.59 tomcat-server1
  4. #此处我们创建两个tomcat实例
  5. cp –r tomcat-server1 tomcat-server2
  6. vim tomcat-server2/conf/server.xml
  1. #如下端口进行变更
  2. 8080--->8090
  3. 8005--->8006

启动两个Tomcat

  1. /usr/servers/tomcat-server1/bin/startup.sh
  2. /usr/servers/tomcat-server2/bin/startup.sh

分别访问,如果能正常访问说明配置正常。

http://192.168.1.2:8080/index.html

http://192.168.1.2:8090/index.html

如上步骤使我们在一个服务器上能启动两个tomcat实例,这样的好处是我们可以做本机的Tomcat负载均衡,假设一个tomcat重启时另一个是可以工作的,从而不至于不给用户返回响应。

Java+Tomcat逻辑开发

搭建项目

我们使用Maven搭建Web项目,Maven知识请自行学习。

项目依赖

本文将最小化依赖,即仅依赖我们需要的servlet、mysql、druid、jedis。

  1. <dependencies>
  2. <dependency>
  3. <groupId>javax.servlet</groupId>
  4. <artifactId>javax.servlet-api</artifactId>
  5. <version>3.0.1</version>
  6. <scope>provided</scope>
  7. </dependency>
  8. <dependency>
  9. <groupId>mysql</groupId>
  10. <artifactId>mysql-connector-java</artifactId>
  11. <version>5.1.27</version>
  12. </dependency>
  13. <dependency>
  14. <groupId>com.alibaba</groupId>
  15. <artifactId>druid</artifactId>
  16. <version>1.0.5</version>
  17. </dependency>
  18. <dependency>
  19. <groupId>redis.clients</groupId>
  20. <artifactId>jedis</artifactId>
  21. <version>2.5.2</version>
  22. </dependency>
  23. </dependencies>

核心代码

com.github.zhangkaitao.chapter6.servlet.AdServlet

  1. public class AdServlet extends HttpServlet {
  2. @Override
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  4. String idStr = req.getParameter("id");
  5. Long id = Long.valueOf(idStr);
  6. //1、读取Mysql获取数据
  7. String content = null;
  8. try {
  9. content = queryDB(id);
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
  13. return;
  14. }
  15. if(content != null) {
  16. //2.1、如果获取到,异步写Redis
  17. asyncSetToRedis(idStr, content);
  18. //2.2、如果获取到,把响应内容返回
  19. resp.setCharacterEncoding("UTF-8");
  20. resp.getWriter().write(content);
  21. } else {
  22. //2.3、如果获取不到,返回404状态码
  23. resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
  24. }
  25. }
  26. private DruidDataSource datasource = null;
  27. private JedisPool jedisPool = null;
  28. {
  29. datasource = new DruidDataSource();
  30. datasource.setUrl("jdbc:mysql://127.0.0.1:1112/chapter6?useUnicode=true&characterEncoding=utf-8&autoReconnect=true");
  31. datasource.setUsername("root");
  32. datasource.setPassword("123456");
  33. datasource.setMaxActive(100);
  34. GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
  35. poolConfig.setMaxTotal(100);
  36. jedisPool = new JedisPool(poolConfig, "127.0.0.1", 1111);
  37. }
  38. private String queryDB(Long id) throws Exception {
  39. Connection conn = null;
  40. try {
  41. conn = datasource.getConnection();
  42. String sql = "select content from ad where sku_id = ?";
  43. PreparedStatement psst = conn.prepareStatement(sql);
  44. psst.setLong(1, id);
  45. ResultSet rs = psst.executeQuery();
  46. String content = null;
  47. if(rs.next()) {
  48. content = rs.getString("content");
  49. }
  50. rs.close();
  51. psst.close();
  52. return content;
  53. } catch (Exception e) {
  54. throw e;
  55. } finally {
  56. if(conn != null) {
  57. conn.close();
  58. }
  59. }
  60. }
  61. private ExecutorService executorService = Executors.newFixedThreadPool(10);
  62. private void asyncSetToRedis(final String id, final String content) {
  63. executorService.submit(new Runnable() {
  64. @Override
  65. public void run() {
  66. Jedis jedis = null;
  67. try {
  68. jedis = jedisPool.getResource();
  69. jedis.setex(id, 5 * 60, content);//5分钟
  70. } catch (Exception e) {
  71. e.printStackTrace();
  72. jedisPool.returnBrokenResource(jedis);
  73. } finally {
  74. jedisPool.returnResource(jedis);
  75. }
  76. }
  77. });
  78. }
  79. }

整个逻辑比较简单,此处更新缓存一般使用异步方式去更新,这样不会阻塞主线程;另外此处可以考虑走Servlet异步化来提示吞吐量。

web.xml配置

  1. <servlet>
  2. <servlet-name>adServlet</servlet-name>
  3. <servlet-class>com.github.zhangkaitao.chapter6.servlet.AdServlet</servlet-class>
  4. </servlet>
  5. <servlet-mapping>
  6. <servlet-name>adServlet</servlet-name>
  7. <url-pattern>/ad</url-pattern>
  8. </servlet-mapping>

打WAR包

  1. cd D:\workspace\chapter6
  2. mvn clean package

此处使用maven命令打包,比如本例将得到chapter6.war,然后将其上传到服务器的/usr/chapter6/webapp,然后通过unzip chapter6.war解压。

测试

启动Tomcat实例,分别访问如下地址将看到广告内容:

  1. http://192.168.1.2:8080/ad?id=1
  2. http://192.168.1.2:8090/ad?id=1

nginx配置

vim /usr/chapter6/nginx_chapter6.conf

  1. upstream backend {
  2. server 127.0.0.1:8080 max_fails=5 fail_timeout=10s weight=1 backup=false;
  3. server 127.0.0.1:8090 max_fails=5 fail_timeout=10s weight=1 backup=false;
  4. check interval=3000 rise=1 fall=2 timeout=5000 type=tcp default_down=false;
  5. keepalive 100;
  6. }
  7. server {
  8. listen       80;
  9. server_name  _;
  10. location ~ /backend/(.*) {
  11. keepalive_timeout   30s;
  12. keepalive_requests  100;
  13. rewrite /backend(/.*) $1 break;
  14. #之后该服务将只有内部使用,ngx.location.capture
  15. proxy_pass_request_headers off;
  16. #more_clear_input_headers Accept-Encoding;
  17. proxy_next_upstream error timeout;
  18. proxy_pass http://backend;
  19. }
  20. }

upstream配置:http://nginx.org/cn/docs/http/ngx_http_upstream_module.html

server:指定上游到的服务器, weight:权重,权重可以认为负载均衡的比例;  fail_timeout+max_fails:在指定时间内失败多少次认为服务器不可用,通过proxy_next_upstream来判断是否失败。

check:ngx_http_upstream_check_module模块,上游服务器的健康检查,interval:发送心跳包的时间间隔,rise:连续成功rise次数则认为服务器up,fall:连续失败fall次则认为服务器down,timeout:上游服务器请求超时时间,type:心跳检测类型(比如此处使用tcp)更多配置请参考https://github.com/yaoweibin/nginx_upstream_check_modulehttp://tengine.taobao.org/document_cn/http_upstream_check_cn.html

keepalive:用来支持upstream server http keepalive特性(需要上游服务器支持,比如tomcat)。默认的负载均衡算法是round-robin,还可以根据ip、url等做hash来做负载均衡。更多资料请参考官方文档。

tomcat keepalive配置: http://tomcat.apache.org/tomcat-7.0-doc/config/http.html

maxKeepAliveRequests:默认100;

keepAliveTimeout:默认等于connectionTimeout,默认60秒;

location proxy配置:http://nginx.org/cn/docs/http/ngx_http_proxy_module.html

rewrite:将当前请求的url重写,如我们请求时是/backend/ad,则重写后是/ad。

proxy_pass:将整个请求转发到上游服务器。

proxy_next_upstream:什么情况认为当前upstream server失败,需要next upstream,默认是连接失败/超时,负载均衡参数。

proxy_pass_request_headers:之前已经介绍过了,两个原因:1、假设上游服务器不需要请求头则没必要传输请求头;2、ngx.location.capture时防止gzip乱码(也可以使用more_clear_input_headers配置)。

keepalive:keepalive_timeout:keepalive超时设置,keepalive_requests:长连接数量。此处的keepalive(别人访问该location时的长连接)和upstream keepalive(nginx与上游服务器的长连接)是不一样的;此处注意,如果您的服务是面向客户的,而且是单个动态内容就没必要使用长连接了。

vim /usr/servers/nginx/conf/nginx.conf

  1. include /usr/chapter6/nginx_chapter6.conf;
  2. #为了方便测试,注释掉example.conf
  3. #include /usr/example/example.conf;

重启nginx

/usr/servers/nginx/sbin/nginx -s reload

访问如192.168.1.2/backend/ad?id=1即看到结果。可以kill掉一个tomcat,可以看到服务还是正常的。

vim /usr/chapter6/nginx_chapter6.conf

  1. location ~ /backend/(.*) {
  2. internal;
  3. keepalive_timeout   30s;
  4. keepalive_requests  1000;
  5. #支持keep-alive
  6. proxy_http_version 1.1;
  7. proxy_set_header Connection "";
  8. rewrite /backend(/.*) $1 break;
  9. proxy_pass_request_headers off;
  10. #more_clear_input_headers Accept-Encoding;
  11. proxy_next_upstream error timeout;
  12. proxy_pass http://backend;
  13. }

加上internal,表示只有内部使用该服务。

Nginx+Lua逻辑开发

核心代码

/usr/chapter6/ad.lua

  1. local redis = require("resty.redis")
  2. local cjson = require("cjson")
  3. local cjson_encode = cjson.encode
  4. local ngx_log = ngx.log
  5. local ngx_ERR = ngx.ERR
  6. local ngx_exit = ngx.exit
  7. local ngx_print = ngx.print
  8. local ngx_re_match = ngx.re.match
  9. local ngx_var = ngx.var
  10. local function close_redis(red)
  11. if not red then
  12. return
  13. end
  14. --释放连接(连接池实现)
  15. local pool_max_idle_time = 10000 --毫秒
  16. local pool_size = 100 --连接池大小
  17. local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
  18. if not ok then
  19. ngx_log(ngx_ERR, "set redis keepalive error : ", err)
  20. end
  21. end
  22. local function read_redis(id)
  23. local red = redis:new()
  24. red:set_timeout(1000)
  25. local ip = "127.0.0.1"
  26. local port = 1111
  27. local ok, err = red:connect(ip, port)
  28. if not ok then
  29. ngx_log(ngx_ERR, "connect to redis error : ", err)
  30. return close_redis(red)
  31. end
  32. local resp, err = red:get(id)
  33. if not resp then
  34. ngx_log(ngx_ERR, "get redis content error : ", err)
  35. return close_redis(red)
  36. end
  37. --得到的数据为空处理
  38. if resp == ngx.null then
  39. resp = nil
  40. end
  41. close_redis(red)
  42. return resp
  43. end
  44. local function read_http(id)
  45. local resp = ngx.location.capture("/backend/ad", {
  46. method = ngx.HTTP_GET,
  47. args = {id = id}
  48. })
  49. if not resp then
  50. ngx_log(ngx_ERR, "request error :", err)
  51. return
  52. end
  53. if resp.status ~= 200 then
  54. ngx_log(ngx_ERR, "request error, status :", resp.status)
  55. return
  56. end
  57. return resp.body
  58. end
  59. --获取id
  60. local id = ngx_var.id
  61. --从redis获取
  62. local content = read_redis(id)
  63. --如果redis没有,回源到tomcat
  64. if not content then
  65. ngx_log(ngx_ERR, "redis not found content, back to http, id : ", id)
  66. content = read_http(id)
  67. end
  68. --如果还没有返回404
  69. if not content then
  70. ngx_log(ngx_ERR, "http not found content, id : ", id)
  71. return ngx_exit(404)
  72. end
  73. --输出内容
  74. ngx.print("show_ad(")
  75. ngx_print(cjson_encode({content = content}))
  76. ngx.print(")")

将可能经常用的变量做成局部变量,如local ngx_print = ngx.print;使用jsonp方式输出,此处我们可以将请求url限定为/ad/id方式,这样的好处是1、可以尽可能早的识别无效请求;2、可以走nginx缓存/CDN缓存,缓存的key就是URL,而不带任何参数,防止那些通过加随机数穿透缓存;3、jsonp使用固定的回调函数show_ad(),或者限定几个固定的回调来减少缓存的版本。

vim /usr/chapter6/nginx_chapter6.conf

  1. location ~ ^/ad/(\d+)$ {
  2. default_type 'text/html';
  3. charset utf-8;
  4. lua_code_cache on;
  5. set $id $1;
  6. content_by_lua_file /usr/chapter6/ad.lua;
  7. }

重启nginx

  1. /usr/servers/nginx/sbin/nginx -s reload

访问如http://192.168.1.2/ad/1即可得到结果。而且注意观察日志,第一次访问时不命中Redis,回源到Tomcat;第二次请求时就会命中Redis了。

第一次访问时将看到/usr/servers/nginx/logs/error.log输出类似如下的内容,而第二次请求相同的url不再有如下内容:

  1. redis not found content, back to http, id : 2

到此整个架构就介绍完了,此处可以直接不使用Tomcat,而是Lua直连Mysql做回源处理;另外本文只是介绍了大体架构,还有更多业务及运维上的细节需要在实际应用中根据自己的场景自己摸索。后续如使用LVS/HAProxy做负载均衡、使用CDN等可以查找资料学习。

第六章 (Nginx+Lua)Web开发实战HTTP服务的更多相关文章

  1. 第十六章:Python の Web开发基础(三) jQuery与Ajax

    本課主題 jQuery 介绍 Ajax 介绍 jQuery 介绍 选择器 jQuery 的选择器分不同的种类,主要目的是用来查找目标的 HTML 标签,方便对目标标签进行操作,比如找到 <li& ...

  2. Nginx+Lua(OpenResty)开发高性能Web应用

    使用Nginx+Lua(OpenResty)开发高性能Web应用 博客分类: 跟我学Nginx+Lua开发 架构 ngx_luaopenresty 在互联网公司,Nginx可以说是标配组件,但是主要场 ...

  3. 《Python Web开发实战》|百度网盘免费下载|Python Web开发

    <Python Web开发实战>|百度网盘免费下载|Python Web开发 提取码:rnz4 内容简介 这本书涵盖了Web开发的方方面面,可以分为如下部分: 1. 使用最新的Flask ...

  4. 安装Nginx+Lua+OpenResty开发环境配置全过程实例

    安装Nginx+Lua+OpenResty开发环境配置全过程实例 OpenResty由Nginx核心加很多第三方模块组成,默认集成了Lua开发环境,使得Nginx可以作为一个Web Server使用. ...

  5. HTML5移动Web开发实战 PDF扫描版​

    <HTML5移动Web开发实战>提供了应对这一挑战的解决方案.通过阅读本书,你将了解如何有效地利用最新的HTML5的那些针对移动网站的功能,横跨多个移动平台.全书共分10章,从移动Web. ...

  6. 《Java Web开发实战》——Java工程师必备干货教材

    一年一度毕业季,又到了简历.offer漫天飞,失望与希望并存的时节.在IT行业,高校毕业生求职时,面临的第一道门槛就是技能与经验的考验,但学校往往更注重学生的理论知识,忽略了对学生实践能力的培养,因而 ...

  7. 《Java web 开发实战经典》读书笔记

    去年年末,也就是大四上学期快要结束的时候,当时保研的事情确定了下来,终于有了一些空闲的时间可以学点实用的技术. 之前做数据库课程设计的时候,也接触过java web的知识,当时做了一个卖二手书籍的网站 ...

  8. Laravel 教程 - Web 开发实战入门 ( Laravel 5.5 )购买链接

      Laravel 教程 - Web 开发实战入门 ( Laravel 5.5 )购买链接: 推荐给你高品质的实战课程 https://laravel-china.org/courses?rf=158 ...

  9. 进击的Python【第十六章】:Web前端基础之jQuery

    进击的Python[第十六章]:Web前端基础之jQuery 一.什么是 jQuery ? jQuery是一个JavaScript函数库. jQuery是一个轻量级的"写的少,做的多&quo ...

  10. 用Nginx+Lua(OpenResty)开发高性能Web应用

    在互联网公司,Nginx可以说是标配组件,但是主要场景还是负载均衡.反向代理.代理缓存.限流等场景:而把Nginx作为一个Web容器使用的还不是那么广泛.Nginx的高性能是大家公认的,而Nginx开 ...

随机推荐

  1. 如何在HarmonyOS Next中编译React-Native包

    一.创作背景 鸿蒙既出,万众瞩目.作为国内操作系统自力更生的代表,它承载着十四亿中国人民的强烈期望,系国家安全和国运于一身.就算抛开爱国情怀不谈,作为一名软件开发人员,偌大的就业市场,海量的翻身机会就 ...

  2. .NET 创建动态方法方案及 Natasha V9

    前言 本篇文章前面客观评估了 .NET 创建动态方方案多个方面的优劣,后半部分是 Natasha V9 的新版特性. .NET 中创建动态方法的方案 创建动态方法的不同选择 以下陈列了几种创建动态方法 ...

  3. 100 款支持 .NET 多版本的强大 WPF 控件库

    前言 推荐一款集成了超过100款控件的流行 XAML 控件库,同时提供了一系列常用的 .NET 帮助类-CookPopularUI.它可以简化开发流程,让我们能够更加专注于核心业务逻辑的实现. 让我们 ...

  4. 符合ASTM标准的雨流计数法及其不同的改进方法

    随着研究的深入,人们发现采用时间序列计算载荷谱太麻烦了,处理的工作量太大,我们不需要将每个时刻点的载荷都做运算,疲劳计算只需要提供幅值.均值和循环次数,鉴于此发展出了很多不同的计数方法,雨流法是最常见 ...

  5. Apache APISIX 和 Kong 的选型对比

    从 API 网关核心功能点来看,两者均已覆盖: 功能 Apache APISIX Kong 动态上游 支持 支持 动态路由 支持 支持 健康检查和熔断器 支持 支持 动态SSL证书 支持 支持 七层和 ...

  6. 如何使用建造者模式(Builder Pattern)创建不可变类

    本文由 ImportNew - 唐小娟 翻译自 Journaldev.如需转载本文,请先参见文章末尾处的转载要求. ImportNew注:如果你也对Java技术翻译分享感兴趣,欢迎加入我们的 Java ...

  7. pycharm集成Jupyter Notebook

    1. Jupyter Notebook Jupyter项目是一个非盈利的开源项目,源于 2014 年的 ipython 项目,支持运行 40 多种编程语言.Jupyter Notebook 的本质是一 ...

  8. PHP之项目环境变量设置

    需求 在PHP开发中为了区分线上生产环境还是本地开发环境, 如果我们能通过判断$_SERVER['RUNTIME_ENVIROMENT']为 'DEV'还是'PRO'来区分该多好, 可惜的是$_SER ...

  9. 新型大语言模型的预训练与后训练范式,苹果的AFM基础语言模型

    前言:大型语言模型(LLMs)的发展历程可以说是非常长,从早期的GPT模型一路走到了今天这些复杂的.公开权重的大型语言模型.最初,LLM的训练过程只关注预训练,但后来逐步扩展到了包括预训练和后训练在内 ...

  10. 【C++】关于 Visual Studio 的使用技巧(保姆级教程)

    目录 fliter 视图 输出文件位置设置 查看预处理结果 将目标文件转换为可读的汇编 自定义程序入口 调试时查看变量在内存中的具体值 查看代码的反汇编 fliter 视图 visual studio ...