使用Nginx实现负载均衡

关于负载均衡,实际上一直有没有用过。但是公司一直有在用,只是有专门的部门去做配置,开发人员基本接触不到。接触到也没什么意义,因为是F5的硬件负载均衡。而且这种硬件价格高,估计大公司用的都少。

硬件上的接触不到,那就看下软件的吧。比较有名的就是用Nginx实现负载均衡,据说国内很多大型的互联网公司都在用。

Nginx

关于Nginx,网上的介绍也很多。大家可以看官网(英文)和Wikipedia的介绍,下面是从Wiki上截取的:

Nginx(发音同engine x)是一款由俄罗斯程序员Igor Sysoev所开发轻量级的网页服务器、–服务器以及电子邮件(IMAP/POP3)–服务器。起初是供俄国大型的门户网站及搜索引擎Rambler(俄语:Рамблер)使用。此软件BSD-like协议下发行,可以在UNIX、GNU/Linux、BSD、Mac OS X、Solaris,以及Microsoft Windows等操作系统中运行。

安装启动很简单,Linux(Ubuntu为例):

apt-get install nginx

nginx -s signal

nginx -s stop

Windows:

http://nginx.org/download/nginx-1.9.2.zip

start nginx.exe

nginx.exe -s stop

回到负载均衡上,Nginx目前支持三种方式:

round-robin — requests to the application servers are distributed in a round-robin fashion, (请求在几个节点间是依次循环分发)
least-connected — next request is assigned to the server with the least number of active connections, (请求被分发到持有最少连接的节点)
ip-hash — a hash-function is used to determine what server should be selected for the next request (based on the client’s IP address). (对请求的IP做哈希运算,然后判断分发到哪个节点)

1. 默认的配置很简单,只需要在nginx.conf中的http部分添加如下配置:

http {
    upstream demo {
        server localhost:8080;
        server localhost:8081;
        server localhost:8082;
    }

    server {
        listen 80;
        server_name localhost;
        location / {
            proxy_pass http://demo;
        }
    }
}

比如,我本地demo集群下有三个服务器作为节点,对应的端口分别是8080、8081和8082。Nginx监听localhost上的80端口,对所有“/”下的请求转发demo集群下的节点上。默认的方式是round-robin。

Nginx可以为HTTP, HTTPS, FastCGI, uwsgi, SCGI, and memcached提供负载均衡。

如果是为HTTPS提供负载均衡,只需要切换到HTTPS协议和相应的端口。memcached是一个开源的缓存系统,通过Nginx的负载均衡,可以搭建分布式的缓存系统。这是只需要把proxy_pass换成memcached_pass并指向对应的集群或者服务器。

2. Least connected负载均衡

round-robin有个缺点,比如有些请求完成的时间比较长,循环分配的方式会导致某个或某些节点的负载比较大,达不到均衡的目的。

Least connected就解决了这种问题,nginx会根据节点的活动连接数把请求分发到活动连接数少的节点上。

upstream demo {
        least_conn;
        server localhost:8080;
        server localhost:8081;
        server localhost:8082;
    }

这里不得不提的就是以上两种方式下,同一个会话的不同请求可能会被分发到不同的节点上,这样就会出现session的持久化问题。

3. ip-hash负载均衡

客户端的ip会作为哈希key去判断请求应该被分发的哪个节点上,这就意味着同一会话(ip不变)的不同请求都是被分发到同一个节点上。

upstream demo {
        ip_hash;
        server localhost:8080;
        server localhost:8081;
        server localhost:8082;
    }

4. 加权的负载均衡

上面的三种方式里,我们假设的是所有的节点性能相同,都会被同等的对待。如果服务器的性能不均,使用以上三种方式便达不到均衡的目的。这时我们可以在配置集群的时候为不同的服务器加上权重,以达到均衡的目的。

加权的方式,可以和以上三种结合使用。

upstream demo {
        server localhost:8080 weight=3;
        server localhost:8081;
        server localhost:8082;
    }

weight=3作用是保证每5个请求中的3条会被分发到8080端口的节点上。

健康检查

Nginx中提供了通信内(被动)的检查,如果来自某个节点响应失败,Nginx便会把该检点标识成failed,并在一段时间内避免把请求分发到该节点。

max_fails检查表示该节点响应连续失败(非超时)的次数,默认情况下值是1。0表示不会对该节点做健康检查。fail_timeout表示该节点被认定fail并保持多久,一旦时间超过,Nginx会试着将请求分发到该节点。如果成功,改节点便会被标识成有效。

upstream demo {
        server localhost:8080;
        server localhost:8081 fails_timeout=5s;
        server localhost:8082 max_fail=3;
    }

更多

上面只是Nginx负载均衡中设置的冰山一角,比如proxy_next_upstream, backup, down, and keepalive。

关于SLF4J

Spring的功能越来越强大,同时也越来越臃肿。比如想快速搭建一个基于Spring的项目,解决依赖问题非常耗时。Spring的项目模板的出现就解决了这个问题,通过这个描述文件,可以快速的找到你所需要的模板。

第一次认识SLF4J就是在这些项目模板里,它的全称是Simple Logging Facade for Java。从字面上可以看出它只是一个Facade,不提供具体的日志解决方案,只服务于各个日志系统。简单说有了它,我们就可以随意的更换日志系统(如java.util.logging、logback、log4j)。比如在开发的时候使用logback,部署的时候可以切换到log4j;如果关闭所有的log,切换到NOP就可以了。只需要更改依赖,提供日志配置文件,免去了修改代码的麻烦。

首先看如何使用:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}

SLF4J封装了使用起来和其他日志系统一样简单。上面提到过SLF4J不提供具体的日志解决方案,所以使用的时候除了要引用SLF4J包,还要引用具体的日志解决方案包(log4j、logging–JDK提供、logback),还有所对应的binding包(slf4j-log4j、slf4j-jdk14、logback-classic)。

以log4j为例,我们看SLF4J的实现方式。

SLF4J类在初始化的时候会尝试从ClassLoader中org/slf4j/impl/StaticLoggerBinder.class。这个类比较特殊,每个binding包里都有。不同binding包里的StaticLoggerBinder类会去初始化一个相应的实例,如slf4j-log4j里:

/**
 * 截取的部分代码
 */
private StaticLoggerBinder() {
    loggerFactory = new Log4jLoggerFactory();
}

而Log4jLoggerAdapter实现了SLF4J的Logger接口,使用了Adapter模式对Log4j的Logger进行了封装并暴露了Logger的接口,Log4jLoggerFactory持有了Log4jLoggerAdapter的实例。

/**
 * 截取的部分代码
 */
public class Log4jLoggerFactory implements ILoggerFactory {
	public Logger getLogger(String name) {
	    Logger slf4jLogger = null;
	    // protect against concurrent access of loggerMap
	    synchronized (this) {
	        slf4jLogger = (Logger) loggerMap.get(name);
	      if (slf4jLogger == null) {
	        org.apache.log4j.Logger log4jLogger;
	        if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) {
	           log4jLogger = LogManager.getRootLogger();
	        } else {
	          log4jLogger = LogManager.getLogger(name);
	        }
	        slf4jLogger = new Log4jLoggerAdapter(log4jLogger);
	        loggerMap.put(name, slf4jLogger);
	      }
	    }
	    return slf4jLogger;
	  }
}

具体的Log解决方案就不做剖析了。

支付宝服务窗平台开发

服务窗平台是为支付宝用户提供服务的平台,平台开发接口是提供服务的基础。开发者在服务窗平台中创建服务窗,并且申请接口权限之后,可以通过阅读该文档来完成服务窗的开发工作。

服务窗平台开发接口向开发者提供包括自定义菜单、消息交互、获取用户基础信息等能力。当用户发消息给服务窗或者与服务窗发生其他交互时,支付宝网关会使用HTTP请求将消息推送给开发者网关,开发者网关也可以通过异步调用的方式给用户发送消息。

更多平台和接口的介绍请看这里

最近有作支付宝服务窗平台的研究,遇到的问题是开发者模式顺利激活之后,为进行测试添加菜单的时候报40002的错误,错误明细是“无效签名”。

初步判断是签名的方法有问题,平台采用了RSA签名加密的机制,要求对请求中所有的数据编码和签名。 浏览了下文档,并未找到具体的编码和签名的实现方式。这时就只能求助源代码了,所幸的是支付宝提供了完整的SDK源代码。

通过查看源代码发现,发送消息时的数据参数有三类:

  • 请求的主体
  • 必须的参数,包括API的方法名、API版本号、APP的ID、签名方式、终端的类型、终端的相关信息、时间戳,还有最重要的是一个就是三类参数(主体、必须和可选)中所有参数以参数名排序后的签名
  • 可选的参数,包括格式、访问令牌、SDK版本、产品码

以添加菜单为例:

  • 请求的主体就是JSON格式的菜单数据,参数名是“biz_content”
{"button":[{"actionParam":"ZFB_HFCZ","actionType":"out","name":"话费充值"},{"name":"查询","subButton":[{"actionParam":"ZFB_YECX","actionType":"out","name":"余额查询"},{"actionParam":"ZFB_LLCX","actionType":"out","name":"流量查询"},{"actionParam":"ZFB_HFCX","actionType":"out","name":"话费查询"}]},{"actionParam":"http://m.alipay.com","actionType":"link","name":"最新优惠"}]}
  • 必须的参数(终端相关的信息可为空):
app_id=2014072300007148
method=alipay.mobile.public.menu.add
charset=GBK
sign_type=RSA
version=1.0
sign=/*所有参数的签名*/
terminal_type=
terminal_info=
  • 可选参数(此处未用到):
format=json
auth_toke=/**/用于OAuth2.0的授权验证
alipay_sdk=alipay-sdk-java-dynamicVersionNo
prod_code=

以上都在SDK中得以实现,用SDK提供的接口和类,添加Menu的代码就是


		//AlipayMobilePublicMenuAddRequest类中已经封装了API方法名:alipay.mobile.public.menu.add
		AlipayMobilePublicMenuAddRequest addMenuRequest = new AlipayMobilePublicMenuAddRequest();
		//请求的主体
		addMenuRequest.setBizContent("{\"button\":[{\"actionParam\":\"ZFB_HFCZ\",\"actionType\":\"out\",\"name\":\"话费充值\"},{\"name\":\"查询\",\"subButton\":[{\"actionParam\":\"ZFB_YECX\",\"actionType\":\"out\",\"name\":\"余额查询\"},{\"actionParam\":\"ZFB_LLCX\",\"actionType\":\"out\",\"name\":\"流量查询\"},{\"actionParam\":\"ZFB_HFCX\",\"actionType\":\"out\",\"name\":\"话费查询\"}]},{\"actionParam\":\"http://m.alipay.com\",\"actionType\":\"link\",\"name\":\"最新优惠\"}]}");
		//API版本号
		addMenuRequest.setApiVersion("1.0");
		//支付宝服务窗网关
		String serverUrl = "https://openapi.alipay.com/gateway.do";
		//APP ID (测试号)
		String appId = "2014072300007148";
		//开发者私钥(测试)
		String privateKey = "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMiAec6fsssguUoRN3oEVEnQaqBLZjeafXAxCbKH3MTJaXPmnXOtqFFqFtcB8J9KqyFI1+o6YBDNIdFWMKqOwDDWPKqtdo90oGav3QMikjGYjIpe/gYYCQ/In/oVMVj326GmKrSpp0P+5LNCx59ajRpO8//rnOLd6h/tNxnfahanAgMBAAECgYEAusouMFfJGsIWvLEDbPIhkE7RNxpnVP/hQqb8sM0v2EkHrAk5wG4VNBvQwWe2QsAuY6jYNgdCPgTNL5fLaOnqkyy8IobrddtT/t3vDX96NNjHP4xfhnMbpGjkKZuljWKduK2FAh83eegrSH48TuWS87LjeZNHhr5x4C0KHeBTYekCQQD5cyrFuKua6GNG0dTj5gA67R9jcmtcDWgSsuIXS0lzUeGxZC4y/y/76l6S7jBYuGkz/x2mJaZ/b3MxxcGQ01YNAkEAzcRGLTXgTMg33UOR13oqXiV9cQbraHR/aPmS8kZxkJNYows3K3umNVjLhFGusstmLIY2pIpPNUOho1YYatPGgwJBANq8vnj64p/Hv6ZOQZxGB1WksK2Hm9TwfJ5I9jDu982Ds6DV9B0L4IvKjHvTGdnye234+4rB4SpGFIFEo+PXLdECQBiOPMW2cT8YgboxDx2E4bt8g9zSM5Oym2Xeqs+o4nKbcu96LipNRkeFgjwXN1708QuNNMYsD0nO+WIxqxZMkZsCQHtS+Jj/LCnQZgLKxXZAllxqSTlBln2YnBgk6HqHLp8Eknx2rUXhoxE1vD9tNmom6PiaZlQyukrQkp5GOMWDMkU=";
		//编码集
		String charset = "GBK";
		AlipayClient alipayClient = new DefaultAlipayClient(serverUrl, appId, privateKey, null, charset);

		try {
			//发送请求
			//发送时会添加其他必要参数和对所有参数进行排序、签名和编码
			AlipayMobilePublicMenuAddResponse addMenuResponse = alipayClient.execute(addMenuRequest);
			if(addMenuResponse != null){
				System.out.println(addMenuResponse.getCode());
				System.out.println(addMenuResponse.getMsg());
			}
		} catch (AlipayApiException e) {
			e.printStackTrace();
		}
运行结果(请使用正确的APP ID和开发者密钥):
200
成功

从Google搜索结果中移除信息

为什么说起这个呢?这还得从毕业那年说起。毕业的时候初出茅庐,到处找工作:招聘网站、技术论坛。其中就有过一次在CSDN上贴过自己的简历想找人看下简历,当时忘记隐去个人的一些信息,之后也忘记去删除帖子了。现在是只要在Google中搜索自己的手机号码,结果中的第一条就是那篇简历。本人的名字,出生年月,邮箱和毕业院校等都在上面。如今信息安全也都是大家谈论的话题,也应尽量避免信息的泄漏。所以就想到要去删除这些信息。

这就想到了Google了,正好Google也有这个功能。

如果您想从 Google 搜索结果中移除照片、个人资料链接或网页,您通常需要与相关网站的所有者(即网站站长)联系并让他们移除这些信息。

如果您需要从搜索结果中移除敏感的个人信息(例如您的银行帐号或手写签名图片),则可使用此页面请求我们移除此类信息。请参阅我们的移除政策,详细了解 Google 会移除哪些信息。

既然如此,只能先找网站所有者了。便联系了论坛的版主和客服(有QQ的),验证了信息之后很顺利的删除了帖子。

之后便是去Google,在输入框中输入要删除的信息。Google会先检测页面是否存在,然后根据提示请求删除。

最后就是正待Google的审核了,且不知道失效如何。

后记:14 Mar, Google搜索结果中的CSDN源已经被删除了。但是发现CSDN的帖子被另一个网站的爬虫爬过去,果断联系网站工作人员,几分钟后就答复删除了。然后又在Google上提交了一次,至此Google上已经搜不到任何关于这个手机号码的信息了。

我猜想,任何网站都有义务去保护个人信息不被泄漏的。如果大家发现类似情况,可以做下参考。