Jimmy's Harbor


  • 首页

  • 标签

  • 分类

  • 归档

  • 关于

如何打造优雅的Thrift RPC封装库 - Part2

发表于 2017-10-30 | 分类于 rpc

上一篇我们介绍了如何一步步打造一个包含基础功能,支持服务注册、服务发现的Thrift RPC封装库,本文叙接上文,主要围绕以下几个方面来介绍:

  • 负载均衡
  • 连接池管理
  • 客户端代理封装V2
  • 统一监控

负载均衡

常用的负载均衡算法参见常用负载均衡算法介绍
所谓负载均衡,说白了就是根据下游多台机器的负载情况来动态调整流量分配。一个好的负载均衡算法要考虑到以下几种情况:

  • 服务器异构,不同服务器处理能力不同
  • 某一台或多台服务异常

代码示例结构如下:
[负载均衡代码结构]

我们定义负载均衡算法接口ILoadBalancer如下:

1
2
3
4
5
6
7
8
9
10
public interface ILoadBalancer {
/**
* choose one server instance
*
* @param all all available server instances
* @param blacklist server instance can't be connected during one request
* @return
*/
ServerInstance getServerInstance(List<ServerInstance> all, List<ServerInstance> blacklist);
}

其他几个类(RoundRobinLoadBalancer/CoHashLoadBalancer/PredictiveLoadBalancer)都是具体实现,分别代表round-robin算法、一致性hash算法以及根据下游负载以及处理能力动态调整算法。

连接池管理

目前应用的最普遍的连接池管理工具包是apache common pool,我们就选用最新的v2版本来作为我们的连接池管理工具。
具体maven依赖如下:

1
2
3
4
5
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>

连接池管理代码结构如下:
[连接池管理代码结构]
其中定义了两个接口IConnectionPool和IConnectionPoolFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface IConnectionPool<K, V> {
V borrowObject(K key);
void returnObject(K key, V value);
void discardObject(K key, V value);
void shutdown();
}
public interface IConnectionPoolFactory<K, V> {
/**
* get a connection pool based on provided ifaceClazz
*
* @param ifaceClazz (server class definition)
* @return
*/
IConnectionPool<K,V> getConnectionPool(Class<?> ifaceClazz);
}

ConnectionPoolConfig类提供了连接池的参数设置,这些参数都是Commons pool中指定的,我们选取了部分重要的参数开放给用户设定:
[连接池重要参数]

ConnectionKey类是common pool连接池管理中每个连接对应的key,它主要包含三个字段:
[连接池管理key对象]

ThriftConnectionKeyedPooledObjectFactory 继承自common pool的BaseKeyedPooledObjectFactory类,key是ConnectionKey,返回的value是TServiceClient,这个类主要提供具体创建连接的方法实现,具体如下:

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
public class ThriftConnectionKeyedPooledObjectFactory extends BaseKeyedPooledObjectFactory<ConnectionKey, TServiceClient> {
public TServiceClient create(ConnectionKey key) throws Exception {
//create client
try {
TSocket socket = new TSocket(key.getServerInstance().getIp(), key.getServerInstance().getPort(), key.getTimeout());
TTransport transport = new TFramedTransport(socket);
transport.open();
TProtocol protocol = new TBinaryProtocol(transport);
Constructor<?> cons = key.getiClientClass().getConstructor(TProtocol.class);
TServiceClient res = (TServiceClient) cons.newInstance(protocol);
return res;
} catch (TTransportException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public PooledObject<TServiceClient> wrap(TServiceClient value) {
if (value == null)
return null;
return new DefaultPooledObject<TServiceClient>(value);
}
}

ThriftConnectionPool是真正的Thrift连接池实现类,它依赖commons pool的对象池GenericKeyedObjectPool来管理连接,提供创建连接、归还连接、删除连接以及关闭连接池的功能。

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
public class ThriftConnectionPool implements IConnectionPool<ConnectionKey, TServiceClient> {
private static Logger logger = LoggerFactory.getLogger(ThriftConnectionPool.class);
private GenericKeyedObjectPool<ConnectionKey, TServiceClient> connectionPool;
private ConnectionPoolConfig config;
public ThriftConnectionPool(ConnectionPoolConfig config) {
this.connectionPool = new GenericKeyedObjectPool<ConnectionKey, TServiceClient>(new ThriftConnectionKeyedPooledObjectFactory());
this.config = config;
this.connectionPool.setMaxTotal(config.getMaxTotal());
this.connectionPool.setMaxTotalPerKey(config.getMaxTotalPerKey());
this.connectionPool.setMaxIdlePerKey(config.getMaxIdlePerKey());
this.connectionPool.setBlockWhenExhausted(config.isBlockWhenExhausted());
this.connectionPool.setMaxWaitMillis(config.getMaxWaitTimeInMillis());
this.connectionPool.setTestOnBorrow(config.isTestOnBorrow());
this.connectionPool.setTestOnReturn(true);
this.connectionPool.setTimeBetweenEvictionRunsMillis(config.getTimeBetweenEvictionRunsMillis());
}
public TServiceClient borrowObject(ConnectionKey key) {
TServiceClient client = null;
try {
client = this.connectionPool.borrowObject(key);
} catch (Exception e) {
logger.error("borrow client with key={} failed, Exception: {}", key, e);
return null;
}
return client;
}
public void returnObject(ConnectionKey key, TServiceClient client) {
this.connectionPool.returnObject(key, client);
}
public void discardObject(ConnectionKey key, TServiceClient value) {
try {
this.connectionPool.invalidateObject(key, value);
} catch (Exception e) {
logger.warn("discard client failed! key={}, Exception={}", key, e);
}
}
public void shutdown() {
this.connectionPool.close();
}
}

DefaultConnectionPoolFactory和PerServiceConnectionPoolFactory是连接池工厂的具体实现,分别代表的是统一连接池管理和分服务不同连接池管理,后者主要是为了减少不同服务依赖之间的互相影响。

客户端代理封装V2

好了,前面分别介绍了负载均衡和连接池管理,那么我们是如何将这些功能串起来的呢?

我们先来回忆下第一篇中我们介绍的最简单封装,那种情况下我们是直接创建好了服务的Client对象直接返还给使用者,使用者直接使用这个Client来发起调用,逻辑很简单,也很好理解;但是当我们加入连接池的时候情况就不一样了,有了连接池,我们不仅要创建好Client,这个Client还应该是可以重用的,那么这种情况下如果我们只是简单提供给用户返还Client对象的方法就很容易出问题,你永远不要去猜测用户的使用习惯,最好的方法就是我们提供封装库来统一管理连接的创建/借用/归还/关闭。

那么我们该怎么做呢?

很显然,前一篇介绍的方法在这里很难再继续走下去,我们需要换个思路,如果我们来管理连接,那么就意味着用户是不需要关心具体调用使用的是哪个Client,只要我们保证在用户发起调用的时候能正确找到一个可用的连接,发起调用,调用成功后归还连接、调用失败关闭连接就好了。这里Java的代理机制就派上用场了,我们可以创建一个代理类来代理Client类,所有细节都封装在这个代理类中(具体Java代理的机制我们这里就不展开细说了,不熟悉的同学可以去baidu/google一下)。我们返还给客户端的实际上是这个代理类,因为这个代理类实现了跟Client对象完全一样的接口,对使用者来说是感受不到差别的。

我们看下代码结构:
[客户端代理V2代码结构]

ClientProxyConfigFactory和ClientProxyV1是第一篇中我们介绍的封装,后者这里只是改了个名字。
ServiceProxy还是第一篇中介绍的封装,这里没有做任何改动(后续改进会单独讲)。

我们重点看一下ThriftClientConfig、ThriftClientFactory以及ThriftClientProxy三个类:

  • ThriftClientConfig是客户端配置类,可以用它指定要使用的LoadBalancer、ConnectionPoolFactory、ServiceDiscovery、timeout等配置;
  • ThriftClientProxy是一个实现了InvocationHandler的代理类,他主要负责拦截用户发起的调用,根据要调用的服务找到一个可用的连接,然后通过反射发起调用,同时负责连接生命周期的维护;
  • ThriftClientFactory是提供给用户使用的接口。

具体看下ThriftClientProxy类的实现:

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
class ThriftClientProxy implements InvocationHandler {
private Class<?> ifaceClass;
private Class<?> iclientClass;
private ThriftClientConfig config;
private IConnectionPool<ConnectionKey, TServiceClient> connectionPool;
private ServiceDiscovery serviceDiscovery;
public ThriftClientProxy(Class<?> ifaceClass, ThriftClientConfig config) {
this.ifaceClass = ifaceClass;
this.config = config;
this.iclientClass = getIClientClass(ifaceClass);
this.connectionPool = config.getConnectionPoolFactory().getConnectionPool(ifaceClass);
serviceDiscovery = config.getServiceDiscovery();
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
IConfig properties = config.getConfigProvider().getConfig(ifaceClass);
List<ServerInstance> invalidInstances = new ArrayList<ServerInstance>();
while (true) {
List<ServerInstance> serverInstances = serviceDiscovery.discoverService(ifaceClass);
String loadbalancer = properties.getProperty(Constants.RPC_LOADBALANCER, null);
ILoadBalancer loadBalancer = getLoadBalancer(loadbalancer);
ServerInstance instance = loadBalancer.getServerInstance(serverInstances, invalidInstances);
if (instance == null) {
throw new RuntimeException("Can't find a valid server instance!");
}
ConnectionKey connectionKey = new ConnectionKey(iclientClass, instance, config.getTimeout());
TServiceClient client = this.connectionPool.borrowObject(connectionKey);
if (client == null) {
if (config.isFailFast()) {
throw new RuntimeException("Can't borrow client from server: " + instance);
} else {
invalidInstances.add(instance);
continue;
}
}
boolean exceptionOccurred = false;
try {
return method.invoke(client, args);
} catch (Exception e) {
exceptionOccurred = true;
this.connectionPool.discardObject(connectionKey, client);
if (config.isFailFast()) {
throw new RuntimeException(e);
} else {
invalidInstances.add(instance);
continue;
}
} finally {
if (!exceptionOccurred) {
this.connectionPool.returnObject(connectionKey, client);
}
}
}
}
}

所有重要的逻辑都是invoke方法中,它支持两种模式:retry-until-succeed以及fail-fast。前者是当调用失败的时候会将当前的服务节点标记为不可用,然后重新选取一个节点发起调用直到成功为止;后者是一旦有失败立即终止,更适合于latency比较敏感的服务,比如广告。

我们再看下具体逻辑,首先通过serverdiscovery找到可用的所有节点,然后从中根据指定的Loadbalancer算法选取一个节点,接下来从连接池中借用/创建一个该节点对应的连接,并通过反射直接发起调用,调用成功则归还连接并返回结果;如果失败则关闭当前连接,并根据指定的模式来决定是否重试。

我们可以看到,这种模式非常灵活,所有服务发现、节点选取、连接池管理的逻辑都隐藏在代理类中,用户使用的时候只需要简单的调用ThriftClientFactory获取连接代理即可,ThriftClientFactory实现如下:

1
2
3
4
5
6
7
8
9
public class ThriftClientFactory {
public static <T> T getClient(Class<T> ifaceClazz, ThriftClientConfig config) {
ThriftClientProxy proxy = new ThriftClientProxy(ifaceClazz, config);
T clientProxy = (T) Proxy.newProxyInstance(ifaceClazz.getClassLoader(),
new Class<?>[]{ifaceClazz, TServiceClient.class}, proxy);
return clientProxy;
}
}

下面是客户端使用的一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void V2Demo() throws TException {
ThriftClientConfig clientConfig = new ThriftClientConfig(true);
clientConfig.setConfigProvider(new ZookeeperConfigProvider());
Calculator.Iface ifaceClient = ThriftClientFactory.getClient(Calculator.Iface.class, clientConfig);
if (ifaceClient != null) {
ifaceClient.ping();
int result = ifaceClient.add(100, 50);
System.out.println("100 + 50 = " + result);
ifaceClient.ping();
ifaceClient.ping();
ifaceClient.ping();
result = ifaceClient.add(100, 80);
System.out.println("100 + 80 = " + result);
} else {
System.out.println("create client failed!");
}
ifaceClient.zip();
}

统一监控

其实有了前面的代理,统一监控就非常容易实现了,我们直接在代理类中添加监控就好,可以细分到service和method级别,添加qps、latency、超时等指标的监控,这里就不再具体展示了。

小结

本文从如何更好的进行负载均衡和连接池管理出发,引出对前一篇基本封装的改进完善,通过引入java代理机制,使得实现更优美,功能更灵活,也更容易支持客户端封装的统一监控。下一篇我们会介绍在V2基础上如何支持熔断、降级和限流,敬请期待。

如何打造优雅的Thrift RPC封装库 - Part 1

发表于 2017-10-22 | 分类于 rpc

本系列文章我们重点对Thrift RPC封装库进行介绍,计划分成几部分来讲:

* 一个简易版本的通用Thrift RPC库封装,支持服务注册、服务发现。
* Thrift RPC熔断、降级、限流
* Thrift RPC异步化(swift)

这篇主要讲第一部分。

Thrift RPC简介

Thrift 是Facebook 开源的提供跨语言支持的服务框架,一般在业务开发中会被用于以下两种场景:

  • 结构化对象序列化,这个主要得益于Thrift提供的完善的序列化/反序列化机制,可以很好的应用于结构化数据存储(结合Kafka、Scribe、Hive等)
  • RPC服务框架,Thrift RPC相比Restful API的好处主要是结构化+bianry data传输效率更高,缺点是表达性不如Restful API直白,一般用于企业内部服务之间的通信。

具体介绍Thrift的概念和文档已经很多了,这里就不详细展开了,感兴趣的同学可以移步Thrift 官网了解详情。

Thrift RPC调用

首先,我们先看一个正常Thrift RPC调用的例子。我们首先定义一个thrift 服务描述文件Calculator.thrift

1
2
3
4
5
6
7
namespace java tutorial
service Calculator {
void ping(),
i32 add(1:i32 num1, 2:i32 num2),
oneway void zip()
}

通过thrift命令

1
thrift --gen <language> <Thrift filename>

项目中引用生成的java代码 (推荐使用maven thrift plugin插件),启动server代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Calculator.Processor processor = new Calculator.Processor(new CalculatorServiceImpl());
try {
TServerTransport serverTransport = new TServerSocket(9090);
TServer server = new TSimpleServer(processor, serverTransport);
// Use this for a multithreaded server
// TServer server = new TThreadPoolServer(new TThreadPoolServer.Args(serverTransport).processor(processor));
System.out.println("Starting the simple server...");
server.serve();
} catch (Exception e) {
e.printStackTrace();
}

Server thrift提供了多种类型,这里demo只用了最简单的TSimpleServer。
我们看下启动过程:

  • 首先,使用生成代码中的Processor类,构造使用的参数是我们提供的服务实现CalculatorServiceImpl对象。
  • 创建TServerTransport,服务绑定的端口是9090
  • 使用Processor和serverTransport创建TSimpleServer,并通过server.serve启动服务。

接下来我们看下client 调用 server端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
TTransport transport;
transport = new TSocket("localhost", 9090);
transport.open();
TProtocol protocol = new TBinaryProtocol(transport);
Calculator.Client client = new Calculator.Client(protocol);
client.ping();
int result = client.add(100, 20);
System.out.println(result);
} catch (TTransportException e) {
e.printStackTrace();
} catch (TException e) {
e.printStackTrace();
}

可以看到,客户端指定传输协议,通过socket连接到本机9090端口;接下来指定编码协议是使用二进制编码TBinaryProtocol;最后构造Calculator Client对象,并通过创建的Client对象调用远程服务接口进行操作。

为什么需要封装通用的Thrift RPC库

刚才我们描述了一个最简单的thrift服务启动和客户端调用流程。如果我们只有这么一个服务,这种方式可以work得很happy。而实际上往往没有这么简单,每次都需要这么一段代码来创建一个客户端,并且如果下游服务有多台机器,我还要考虑如何管理链接、如何进行负载均衡,诸如此类事情如果让每个服务定义方/使用方自己去做,无疑代价是巨大的,后续的维护升级都不是一件容易的事情,这也是为什么通常我们都需要封装一个通用的Thrift RPC库的最直接的原因之一。

那么我们该如何来定义通用的Thrift RPC库呢?

最简单的Thrift RPC封装

我们再来回顾下刚才的代码示例,首先为了通用,我们需要约束序列化的机制(比如都使用Bianry),统一了序列化之后,我们先看下服务端,在服务端我们要做的其实很简单,只需要框架能够根据提供的服务名(这里比如Calculator.Iface)能创建对应的TProcessor就可以。这个可以通过反射来完成,因为Thrift自动生成的stub代码编译后,Processor class文件都是以$Processor来命名的。示例代码如下:

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
static <T> TServer getServer(Class<T> clazz, T serviceImplInstance, int port) {
//get Processor class name
String simpleName = clazz.getName();
String name = simpleName;
if (simpleName.endsWith("$Iface")) {
name = simpleName.substring(0, simpleName.indexOf("$Iface"));
}
name = name + "$Processor";
//verify processpr class exist
Class<?> processorClass;
try {
processorClass = Class.forName(name);
} catch (ClassNotFoundException e) {
System.out.println("Class not found: " + name);
return null;
}
//create server
try {
TServerTransport serverTransport = new TServerSocket(port);
Constructor<?> constructor = processorClass.getConstructor(clazz);
TProcessor tProcessor = (TProcessor) constructor.newInstance(serviceImplInstance);
TServer server = new TSimpleServer(tProcessor, serverTransport);
return server;
} catch (TTransportException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

启动服务代码如下:

1
2
3
4
5
6
7
TServer server = getServer(Calculator.Iface.class, new CalculatorServiceImpl(), 9090);
if (server != null) {
System.out.println("Starting the simple server...");
server.serve();
} else {
System.out.println("Get server failed!");
}

客户端测也类似,我们可以看到,客户端需要构建Client对象,而所有服务的Client对象实际上都实现了TServerClient和对应的Iface接口:

1
public static class Client implements TServiceClient, Iface {

Client的构造函数也是固定的使用TProtocol,所以基于这些信息,我们可以通过反射创建出TServerClient,然后返回给客户端的时候转为对应的Iface类型,这样客户端就可以通过简单得提供Iface class来获取相应的客户端操作对象,并进行后续的调用处理。代码示例如下:

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
static <T> T getClient(Class<T> clazz, String host, int port) {
//get client class name
String simpleName = clazz.getName();
String name = simpleName;
if(simpleName.endsWith("$Iface")) {
name = simpleName.substring(0, simpleName.indexOf("$Iface"));
}
name = name + "$Client";
//verify client class exist
Class<?> clientClass;
try {
clientClass = Class.forName(name);
} catch (ClassNotFoundException e) {
System.out.println("Class not found: " + name);
return null;
}
//create client
try {
TTransport transport;
transport = new TSocket(host, port);
transport.open();
TProtocol protocol = new TBinaryProtocol(transport);
Constructor<?> cons = clientClass.getConstructor(TProtocol.class);
T res = (T) cons.newInstance(protocol);
return res;
} catch (TTransportException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

使用方式如下:

1
2
3
4
5
6
7
8
Calculator.Iface ifaceClient = getClient(Calculator.Iface.class, "localhost", 9090);
if (ifaceClient != null) {
ifaceClient.ping();
int result = ifaceClient.add(100, 20);
System.out.println("100 + 20 = " + result);
} else {
System.out.println("create client failed!");
}

至此我们已经可以很简单的构造一个通用服务启动代理和客户端代理来进行RPC调用了。

升级版Thrift RPC代理 – 服务注册、服务发现

前面我们已经介绍了最简单的服务和客户端封装,我们可以看到调用服务的地址都是固定的某台机器,实际线上使用过程中,下游服务往往都是会部署多台,为了简化服务提供方和服务使用方的工作,我们需要在Thrift RPC框架中支持服务注册和服务发现,下面我们就分别介绍一下我们该如何在刚才封装的基础上来支持服务注册和服务发现。

服务注册

目前通用的服务注册中心有Zookeeper、Etcd、Eureka、Consul等,这里我们选用Zookeeper作为demo(其他流程类似,感兴趣可以自行实现)。
服务注册的目的是在服务器启动的时候能够将自己注册到Zookeeper上,在服务关闭的时候将自己从zk的注册列表中移除。我们约定服务在zk注册的形式如下:

1
/registry/<service full name>/Nodes/<ip>:<port>

我们定义服务注册接口ServiceRegistry如下:

1
2
3
4
public interface ServerRegistry {
void register() throws Exception;
void unregister() throws Exception;
}

基于CuratorFramework实现的ZookeeperServeRegistry如下:

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
public class ZookeeperServerRegistry implements ServerRegistry {
private CuratorFramework client;
private String zkPathPattern = "/registry/%s/Nodes/%s:%d";
private String zkPath;
public ZookeeperServerRegistry(String zkhost, int zkPort, int serverPort, String serverName) {
client = CuratorFrameworkFactory.newClient(
zkhost + ":" + zkPort,
new RetryNTimes(10, 5000)
);
client.start();
System.out.println("zk client start successfully!");
String ip = Utils.getIp();
zkPath = String.format(zkPathPattern, serverName, ip, serverPort);
}
public void register() throws Exception {
Stat stat = null;
try {
stat = client.checkExists().creatingParentContainersIfNeeded().forPath(zkPath);
} catch (Exception e) {
e.printStackTrace();
}
if(stat == null) {
try {
client.create().creatingParentsIfNeeded().forPath(zkPath);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
public void unregister() throws Exception {
Stat stat = null;
try {
stat = client.checkExists().creatingParentContainersIfNeeded().forPath(zkPath);
} catch (Exception e) {
e.printStackTrace();
}
if (stat != null) {
try {
client.delete().deletingChildrenIfNeeded().forPath(zkPath);
} catch (Exception e) {
e.printStackTrace();
throw e;
}
}
}
}

下面我们看下服务启动通用注册流程:

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
static <T> void startServer(Class<T> ifaceClass, T serverImpleInstance, int serverPort) throws Exception {
final TServer server = getServer(ifaceClass, serverImpleInstance, serverPort);
if (server != null) {
System.out.println("Starting the simple server...");
Thread thread = new Thread(new Runnable() {
public void run() {
server.serve();
}
});
thread.start();
final ZookeeperServerRegistry registry = new ZookeeperServerRegistry("127.0.0.1", 2181, 9090, getServerName(ifaceClass));
registry.register();
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
public void run() {
try {
registry.unregister();
} catch (Exception e) {
e.printStackTrace();
}
}
}));
} else {
System.out.println("Get server failed!");
}
}

至此我们完成了基于Zookeeper的服务注册。

服务发现

服务注册完成后,所有服务的使用者就不再需要自己去在代码里维护一个服务节点列表,直接从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
public interface ServiceDiscovery {
public List<ServerInstance> discoverService(Class<?> ifaceClazz);
}
public class ServerInstance {
private String ip;
private int port;
public ServerInstance(String ip, int port) {
this.ip = ip;
this.port = port;
}
public String getIp() {
return ip;
}
public int getPort() {
return port;
}
@Override
public String toString() {
return "ServerInstance{" +
"ip='" + ip + '\'' +
", port=" + port +
'}';
}
}

同样基于CuratorFramework实现ZookeeperServiceDiscovery:

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
public class ZookeeperServiceDiscovery implements ServiceDiscovery {
private CuratorFramework client;
private String zkPathPattern = "/registry/%s/Nodes";
private String zkPath;
private List<ServerInstance> serverInstances;
public ZookeeperServiceDiscovery(String zkhost, int zkPort, Class<?> ifaceClass) {
client = CuratorFrameworkFactory.newClient(
zkhost + ":" + zkPort,
new RetryNTimes(10, 5000)
);
client.start();
String serverName = Utils.getServerName(ifaceClass);
zkPath = String.format(zkPathPattern, serverName);
}
public List<ServerInstance> discoverService(Class<?> ifaceClazz) {
if (serverInstances != null) {
return serverInstances;
}
try {
List<String> items = client.getChildren().usingWatcher(new CuratorWatcher() {
public void process(WatchedEvent watchedEvent) throws Exception {
if (watchedEvent.getType() == Watcher.Event.EventType.NodeChildrenChanged) {
update();
} else if (watchedEvent.getType() == Watcher.Event.EventType.NodeDeleted) {
serverInstances = null;
}
}
}).forPath(zkPath);
serverInstances = parseServer(items);
} catch (Exception e) {
e.printStackTrace();
}
return serverInstances;
}
private void update() {
try {
List<String> items = client.getChildren().forPath(zkPath);
serverInstances = parseServer(items);
} catch (Exception e) {
e.printStackTrace();
}
}
private List<ServerInstance> parseServer(List<String> items) {
if (items == null) {
return null;
}
List<ServerInstance> servers = new ArrayList<ServerInstance>();
for(String item: items) {
String[] parts = StringUtils.split(item, ":");
servers.add(new ServerInstance(parts[0], Integer.valueOf(parts[1])));
}
return servers;
}
}

客户端的封装更改如下:

1
2
3
4
5
6
7
8
9
10
static <T> T getClient(Class<T> clazz) {
ZookeeperServiceDiscovery serviceDiscovery = new ZookeeperServiceDiscovery("127.0.0.1", 2181, clazz);
List<ServerInstance> serverInstances = serviceDiscovery.discoverService(clazz);
if (serverInstances == null || serverInstances.isEmpty()) {
throw new RuntimeException("No server instance found!");
}
//here choose one endpoint, for demo we just take the first one
ServerInstance instance = serverInstances.get(0);
return getClient(clazz, instance.getIp(), instance.getPort());
}

这样我们就可以通过简单的指定服务定义的Iface类来获取到对应服务的客户端实例进行操作。

简单总结

本文主要介绍的是最简易版本的Thrift RPC通用封装,支持基于Zookeeper的服务注册、服务发现,还远远称不上是一个完备的RPC封装库,架子已经搭好,接下来的一篇会介绍如何对RPC封装进行通用的负载均衡、监控、熔断、降级、限流等功能的完善,敬请期待。

GRPC系列-(1): GRPC的历史以及设计理念

发表于 2016-09-04

最近听说GRPC 1.0发布了,这是google开源的一个rpc框架,本着“google出品,必属精品”的原则,决定来学习一下这个框架,看看它跟现有的一些框架相比有什么优缺点,第一篇先简单介绍一个grpc框架的由来以及设计理念。

GRPC 介绍

GRPC是由google开源的一个高性能、跨语言的RPC框架,面向移动和HTTP/2设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
GRPC 基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

GRPC 动机

十多年来,google一直使用一个叫做Stubby的通用RPC基础框架,用它来连接数据中心内和跨数据中心运行的大量微服务。google内部系统早就接受了如今越来越流行的微服务架构,拥有一个统一的、跨平台的RPC的基础框架,使得服务的首次发行在效率、安全性、可靠性和行为分析上得到全面提升,这是支撑这一时期谷歌快速增长的关键。
Stubby有许多非常棒的特性,然而,它没有基于任何标准,而且与google内部的基础框架耦合得太紧密以至于被认为不适合公开发布。随着SPDY、HTTP/2和QUIC的到来,许多类似特性在公共标准中出现,并提供了Stubby不支持的其它功能。很明显,是时候利用这些标准来重写Stubby,并将其适用性扩展到移动、物联网和云场景。而这就是GRPC在google诞生的动机。

GRPC 设计理念

按照google自己官方说法,grpc的设计理念主要包含以下几方面:

  • 服务而非对象、消息而非引用 —— 促进微服务的系统间粗粒度消息交互设计理念,同时避免分布式对象的陷阱和分布式计算的谬误。
  • 普遍并且简单 —— 该基础框架应该在任何流行的开发平台上适用,并且易于被个人在自己的平台上构建。它在CPU和内存有限的设备上也应该切实可行。
  • 免费并且开源 —— 所有人可免费使用基本特性。以友好的许可协议开源方式发布所有交付件。
  • 互通性 —— 该数据传输协议(Wire Protocol)必须遵循普通互联网基础框架。
  • 通用并且高性能 —— 该框架应该适用于绝大多数用例场景,相比针对特定用例的框架,该框架只会牺牲一点性能。
  • 分层 —— 该框架的关键是必须能够独立演进。对数据传输格式(Wire Format)的修改不应该影响应用层。
  • 负载无关的 —— 不同的服务需要使用不同的消息类型和编码,例如protocol buffers、JSON、XML和Thrift,协议上和实现上必须满足这样的诉求。类似地,对负载压缩的诉求也因应用场景和负载类型不同而不同,协议上应该支持可插拔的压缩机制。
  • 流 —— 存储系统依赖于流和流控来传递大数据集。像语音转文本或股票代码等其它服务,依靠流表达时间相关的消息序列。
  • 阻塞式和非阻塞式 —— 支持异步和同步处理在客户端和服务端间交互的消息序列。这是在某些平台上缩放和处理流的关键。
  • 取消和超时 —— 有的操作可能会用时很长,客户端运行正常时,可以通过取消操作让服务端回收资源。当任务因果链被追踪时,取消可以级联。客户端可能会被告知调用超时,此时服务就可以根据客户端的需求来调整自己的行为。
  • Lameducking —— 服务端必须支持优雅关闭,优雅关闭时拒绝新请求,但继续处理正在运行中的请求。
  • 流控 —— 在客户端和服务端之间,计算能力和网络容量往往是不平衡的。流控可以更好的缓冲管理,以及保护系统免受来自异常活跃对端的拒绝服务(DOS)攻击。
  • 可插拔 —— 数据传输协议(Wire Protocol)只是功能完备API基础框架的一部分。大型分布式系统需要安全、健康检查、负载均衡和故障恢复、监控、跟踪、日志等。实现上应该提供扩展点,以允许插入这些特性和默认实现。
  • API扩展 —— 可能的话,在服务间协作的扩展应该最好使用接口扩展,而不是协议扩展。这种类型的扩展可以包括健康检查、服务内省、负载监测和负载均衡分配。
  • 元数据交换 —— 常见的横切关注点,如认证或跟踪,依赖数据交换,但这不是服务公共接口中的一部分。部署依赖于他们将这些特性以不同速度演进到服务暴露的个别API的能力。
  • 标准化状态码 —— 客户端通常以有限的方式响应API调用返回的错误。应该限制状态代码名字空间,使得这些错误处理决定更清晰。如果需要更丰富的特定域的状态,可以使用元数据交换机制来提供。

grpc的理念是很赞的!很多都是目前的rpc框架所不能满足的(比如thrift不支持异步调用),接下来会参考源代码提供的示例具体看下grpc的使用情况。

Google guice学习(1)

发表于 2016-08-21 | 分类于 java

最近在研究DRUID,druid整个设计采用了Google的guice依赖注入框架,所以要想学习druid源代码,首先要弄明白guice框架是怎么回事。

什么是GUICE

Guice是Google开发的一个轻量级,基于Java5(主要运用泛型与注释特性)的依赖注入框架(IOC)。Guice非常小而且快。Guice是类型安全的,它能够对构造函数,属性,方法(包含任意个参数的任意方法,而不仅仅是setter方法)进行注入。

guice还具有一些可选的特性比如:自定义scopes,传递依赖,静态属性注入,与Spring集成和AOP联盟方法注入等。一部分人认为,Guice可以完全替代spring, 因为对于DI组件框架来说, 性能是很重要的, guice比spring快十倍左右, 另外, 也是最重要的一点, 使用spring很容易写成service locator的风格, 而用guice, 你会很自然的形成DI风格.甚至说,guice简单超轻量级的DI框架效率是spring的1.6倍,Spring使用XML使用将类与类之间的关系隔离到xml中,由容器负责注入被调用的对象,而guice将类与类之间的关系隔离到Module中,声明何处需要注入,由容器根据Module里的描述,注入被调用的对象,使用Annotation使用支持自定义Annotation标注,对于相同的接口定义的对象引用,为它们标注上不同的自定义Annotation注释,就可以达到同一个类里边的同一个接口的引用,注射给不同的实现,在Module里用标注做区分,灵活性大大增加

guice跟spring对比

  • 使用XML
    Spring 使用将类与类之间的关系隔离到xml中,由容器负责注入被调用的对象,因此叫做依赖注入。
    Guice 不使用xml,将类与类之间的关系隔离到Module中,声名何处需要注入,由容器根据Module里的描述,注入被调用的对象。
  • 使用Annotation
    Guice 使用,支持自定义Annotation标注
  • 运行效率
    Spring 装载spring配置文件时,需解析xml,效率低,getBean效率也不高,不过使用环境不会涉及到getBean,只有生产环境的时候会用到getBean,在装载spring应用程序的时候,已经完成全部的注射,所以这个低效率的问题不是问题。
    Guice 使用Annotation,cglib, 效率高与spring最明显的一个区别,spring是在装载spring配置文件的时候把该注入的地方都注入完,而Guice呢,则是在使用的时候去注射,运行效率和灵活性高。
  • 类耦合度
    Spring 耦合度低,强调类非侵入,以外部化的方式处理依赖关系,类里边是很干净的,在配置文件里做文章,对类的依赖性极低。
    Guice 高,代码级的标注,DI标记@inject侵入代码中,耦合到了类层面上来,何止侵入,简直侵略,代码耦合了过多guice的东西,大大背离了依赖注入的初衷,对于代码的可维护性,可读性均不利
  • 类编写时
    Spring 需要编写xml,配置Bean,配置注入。
    Guice 只需声明为@inject,等着被注入,最后在统一的Module里声明注入方式。
  • 仅支持IoC
    Spring 否,spring目前已经涉猎很多部分。
    Guice 是,目前仅仅是个DI容器。
  • 是否易于代码重构
    Spring 统一的xml配置入口,更改容易。
    Guice 配置工作是在Module里进行,和spring异曲同功
  • 支持多种注入方式
    Spring 构造器,setter方法。
    Guice Field,构造器,setter方法.
  • 灵活性
    Guice 1.如果同一个接口定义的引用需要注入不同的实现,就要编写不同的Module,烦琐 2.动态注入,如果你想注射的一个实现,你还未知呢,怎么办呢,spring是没办法,事先在配置文件里写死的,而Guice就可以做到,就是说我想注射的这个对象我还不知道注射给谁呢,是在运行时才能得到的的这个接口的实现,所以这就大大提高了依赖注射的灵活性,动态注射。
    与现有框架集成度
    Spring 1.高,众多现有优秀的框架(如struts1.x等)均提供了spring的集成入口,而且spring已经不仅仅是依赖注入,包括众多方面。2. Spring也提供了对Hibernate等的集成,可大大简化开发难度。3.提供对于orm,rmi,webservice等等接口众多,体系庞大。
    Guice 1.可以与现有框架集成,不过仅仅依靠一个效率稍高的DI,就想取代spring的地位,有点难度。
  • 配置复杂度
    Spring 在xml中定位类与类之间的关系,难度低。
    Guice 代码级定位类与类之间的关系,难度稍高。

Linux常用命令之strace

发表于 2015-11-26

Linux常用命令之top

发表于 2015-11-14

top是linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,.该命令可以按CPU使用.内存使用和执行时间对任务进行排序,而且该命令的很多特性都可以通过交互式命令或者在个人定制文件中进行设定
下面分几个部分介绍一下这个命令:

  1. 命令相关参数介绍。
  2. 常用命令示例。

命令相关参数介绍

在命令行执行top命令显示如下:
[top命令示例]

统计信息区前五行是系统整体的统计信息。

第一行是任务队列信息,其内容如下:

top - 15:55:44 up 58 days, 21:49, 1 user, load average: 0.00, 0.05, 0.07

其中15:55:44表示当前时间;up 58 days, 21:49 表示系统已经连续运行了58天21小时49分钟; 1 user 表示当前登录的用户数; load average: 0.00, 0.05, 0.07 表示系统负载,也即任务队列的平均长度,三个数值分别代表过去1分钟、5分钟以及15分钟的系统平均负载,如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了。

第二行是任务(进程)信息,其内容为:

Tasks: 197 total, 1 running, 196 sleeping, 0 stopped, 0 zombie

这一行表示系统现在共有197个进程,其中处于运行中的有1个,196个在休眠(sleep),stopped状态的有0个,zombie(僵尸)状态的有0个

第三行是CPU信息:

Cpu(s): 9.6%us, 0.7%sy, 0.0%ni, 89.6%id, 0.0%wa, 0.0%hi, 0.1%si, 0.0%st

9.6%us 表示用户空间占用CPU的百分比为9.6%;0.7%sy 表示内核空间占用CPU的百分比为0.7%;0.0%ni 表示改变过优先级的进程占用CPU的百分比为0;89.6%id 表示空闲CPU的百分比为89.6%; 0.0%wa 表示IO等待占用CPU的百分比为0;0.0%hi 表示硬中断(Hardware Interrupts)占用CPU的百分比为0;0.1%si 表示软中断(Software Interrupts)占用CPU的百分比为0.1%;0.0%st 表示虚拟机占用CPU的百分比为0。

第四行是内存信息:

Mem: 32850724k total, 32569108k used, 281616k free, 491520k buffers

32850724k total 表示总物理内存为32G;32569108k used 表示已经使用的物理内存大小为将近32G;281616k free 表示当前空闲的物理内存大小为270多M;491520k buffers 表示用作内核缓存的内存大小约为480M;

第五行是swap交换分区信息:

Swap: 12582904k total, 0k used, 12582904k free, 20604232k cached

12582904k total 表示交换区总量为12G;0k used 表示当前已经使用的交换区大小为0;12582904k free 表示空闲的交换区总量为12G;20604232k cached 表示缓冲的交换区总量,内存中的内容被换出到交换区,而后又被换入到内存,但使用过的交换区尚未被覆盖,该数值即为这些内容已存在于内存中的交换区的大小,相应的内存再次被换出时可不必再对交换区写入。

这里要说明一下,第四行中使用中的内存总量(used)指的是现在系统内核控制的内存数,空闲内存总量(free)是内核还未纳入其管控范围的数量。纳入内核管理的内存不见得都在使用中,还包括过去使用过的现在可以被重复利用的内存,内核并不把这些可被重新使用的内存交还到free中去,因此在linux上free内存会越来越少,但不用为此担心。
如果出于习惯去计算可用内存数,这里有个近似的计算公式:第四行的free + 第四行的buffers + 第五行的cached,按这个公式此台服务器的可用内存:281616k+491520k+20604232k = 20GB左右。
对于内存监控,在top里我们要时刻监控第五行swap交换分区的used,如果这个数值在不断的变化,说明内核在不断进行内存和swap的数据交换,这是真正的内存不够用了。

5行之后列出的是详细的进程统计信息,top命令提供的统计信息包含:

序号 列名 含义
a PID 进程id
b PPID 父进程ID
c RUSER Real user name
d UID 进程所有者的用户ID
e USER 进程所有者的用户名
f GROUP 进程所有者的组名
g TTY 启动进程的终端名,不是从终端启动的进程显示为?
h PR 进程优先级
i NI 进程nice值。负值表示高优先级,正值表示低优先级,值越小代表优先级越高。
j P 最后使用的CPU,仅在多CPU环境下有意义
k %CPU 上次更新到现在的CPU时间占用百分比
l TIME 进程使用的CPU时间总计,单位秒
m TIME+ 进程使用的CPU时间总计,单位1/100秒
n %MEM 进程使用的物理内存百分比
o VIRT 进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
p SWAP 进程使用的虚拟内存中,被换出的大小,单位kb。
q RES 进程使用的、未被换出的物理内存大小,单位kb。RES=CODE+DATA
r CODE 可执行代码占用的物理内存大小,单位kb
s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb
t SHR 共享内存大小,单位kb
u nFLT 页面错误次数
v nDRT 最后一次写入到现在,被修改过的页面数。
w S 进程状态(D=不可中断的睡眠状态,R=运行,S=睡眠,T=跟踪/停止,Z=僵尸进程)
x COMMAND 进程启动的命令行
y WCHAN 若该进程在睡眠,则显示睡眠中的系统函数名
z Flags 任务标志,参考 sched.h

示例中目前列出来的主要有:

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

  • 如果想更改显示出来的内容, 可以按 f 键进入选择显示的内容视图。按 f 键之后会显示列的列表,按 a-z 即可显示或隐藏对应的列,当前显示的列会用*号以及大写序号标识,没有显示的用小写标识,选择完成后按回车键确定。示例如下:
[top命令示例]
  • 如果想要更改显示的列的顺序,可以按 o 键进入调整显示列顺序的视图。在该视图中可以按小写的 a-z 可以将相应的列向右移动,而大写的 A-Z 可以将相应的列向左移动。最后按回车键确定。 示例如下:
[top命令示例]
  • 如果想要更改排序的列(默认是%CPU), 可以按大写的 F 或 O 键,然后按 a-z 可以将进程按照相应的列进行排序。而大写的 R 键可以将当前的排序倒转。示例如下:
[top命令示例]

top交互命令

top交互命令总结如下:

交互命令 说明
h或? 显示帮助画面,给出一些简短的命令总结说明
Z 修改Summary/Messages or Prompts/Column Heads/Task Information的显示颜色
z 分颜色显示on/off
B 打开或者关闭Bold显示模式(summary信息以及正在运行的Task)
b 只有当x或者y正在使用的时候有效,取消/显示高亮
x 高亮当前正在排序的列(on/off)
y 高亮当前正在运行的Task (on/off)
l 显示/隐藏load信息
t 显示/隐藏task/cpu的信息
m 显示/隐藏mem的信息
1 显示/隐藏每个CPU的详细信息
I 打开/关闭Irix/Solaris模式
f 增加/减少显示的列
o 更改列显示的顺序
F/O 更改排序的列
> 向右更改排序的列
< 向左更改排序的列
R 顺序/逆序显示
H 显示具体的线程信息
c 显示/隐藏程序启动命令
i 显示/隐藏idle的Task(进程)
S 打开/关闭cumulative time
u 输入某个用户名可以只显示属于该用户的进程
n/# 设置最多显示的进程的数目
k 输入进程ID杀掉该进程
r 输入进程ID重新设置该进程的nice值
d/s 设置刷新时间
W 将更改后的配置保存到本地~/.toprc文件中
q 退出
[top命令示例]

常用命令示例

多核CPU监控(按1)

[top命令示例]

高亮当前正在运行的进程(按y,如果没有高亮,接着按b)

[top命令示例]

高亮当前用于排序的列(按x,如果没有高亮,接着按b)

[top命令示例]

通过”shift + >”或”shift + <”可以向右或左改变排序列

[top命令示例]

显示进程启动命令(按c)

[top命令示例]

只显示某个进程(top -p )

[top命令示例]

Android应用程序签名

发表于 2015-11-01

最近工作中需要封装一个客户端的sdk包,写了一个简单的示例程序,准备安装到手机的时候提示程序没有签名,安装失败,特查了一下android包的签名方式,在此记录一下,以备后用。

签名的意义

为了保证每个应用程序开发商合法ID,防止部分开放商可能通过使用相同的Package Name来混淆替换已经安装的程序,我们需要对我们发布的APK文件进行唯一签名,保证我们每次发布的版本的一致性(如自动更新不会因为版本不一致而无法安装)。

APK签名步骤

  1. 生成keystore
  2. 用jarsigner签名apk
  3. zipalign对齐优化apk文件(可选)
    下面分别介绍一下这三部分。

生成keystore

这一步需要使用keytool这个工具,这个工具是跟随JDK一起安装的,所以只要安装完java设置好路径后一般都是可以直接在命令行使用的。打开命令行,执行下面命令:

keytool -genkey -alias abc.keystore -keyalg RSA -validity 20000 -keystore abc.keystore

  • -genkey 产生密钥
  • -alias abc.keystore 别名 abc.keystore (abc只是示例所用的名字,可以根据需要自己重新命名的)
  • -keyalg RSA 使用RSA算法对签名加密
  • -validity 20000 有效期限20000天
  • -keystore abc.keystore (一般要跟alias保持一致)
    执行完这个命令后会要求输入一堆信息,示例如下: 执行成功的话会在当前目录下看到生成的abc.keystore文件

用jarsigner签名apk

jarsigner这个tool也是跟随JDK一起安装的,命令行可以直接使用。命令如下:
< jarsigner -verbose -keystore abc.keystore -signedjar demo_signed.apk demo.apk abc.keystore

  • -verbose 输出签名的详细信息
  • -keystore abc.keystore 密钥库位置
  • -signedjar demo_signed.apk demo.apk abc.keystore 正式签名,三个参数中依次为签名后产生的文件demo_signed,要签名的文件demo.apk和密钥库abc.keystore.
    提示输入密码的时候记得输入生成keystore文件时设置的密码

zipalign优化生成的apk文件

未签名的apk不能使用,也不能优化。签名之后的apk谷歌推荐使用zipalign.exe(位于下载的android-sdk包中)工具对其优化,命令如下:

zipalign -v 4 demo_signed.apk final.apk

zipalign能够使apk文件中未压缩的数据在4个字节边界上对齐(4个字节是一个性能很好的值),这样android系统就可以使用mmap()(请自行查阅这个函数的用途)函数读取文件,可以在读取资源上获得较高的性能,在4个字节边界上对齐的意思就是,一般来说,是指编译器吧4个字节作为一个单位来进行读取的结果,这样的话,CPU能够对变量进行高效、快速的访问(较之前不对齐)。对齐的根源是android系统中的Davlik虚拟机使用自己专有的格式DEX,DEX的结构是紧凑的,为了让运行时的性能更好,可以进一步用”对齐”进一步优化,但是大小一般会有所增加。

经过以上三个步骤,最终生成的final.apk文件就可以通过adb install成功安装到手机上了。

签名对APP的影响

你不可能只做一个APP,你可能有一个宏伟的战略工程,想要在生活,服务,游戏,系统各个领域都想插足的话,你不可能只做一个APP,谷歌建议你把你所有的APP都使用同一个签名证书。
使用你自己的同一个签名证书,就没有人能够覆盖你的应用程序,即使包名相同,所以影响有:

  1. App升级。 使用相同签名的升级软件可以正常覆盖老版本的软件,否则系统比较发现新版本的签名证书和老版本的签名证书不一致,不会允许新版本安装成功的。
  2. App模块化。android系统允许具有相同的App运行在同一个进程中,如果运行在同一个进程中,则他们相当于同一个App,但是你可以单独对他们升级更新,这是一种App级别的模块化思路。
  3. 允许代码和数据共享。android中提供了一个基于签名的Permission标签。通过允许的设置,我们可以实现对不同App之间的访问和共享,如下:

    AndroidManifest.xml:<permission android:protectionLevel=”normal” />

其中protectionLevel标签有4种值:normal(缺省值),dangerous, signature,signatureOrSystem。简单来说,normal是低风险的,所有的App不能访问和共享此App。dangerous是高风险的,所有的App都能访问和共享此App。signature是指具有相同签名的App可以访问和共享此App。signatureOrSystem是指系统image中App和具有相同签名的App可以访问和共享此App,谷歌建议不要使用这个选项,因为签名就足够了,一般这个许可会被用在在一个image中需要共享一些特定的功能的情况下。

最后,请一定要记得保管好你的签名证书的密码。

常用负载均衡算法

发表于 2015-10-30

最近调查一个线上问题,发现问题跟负载均衡算法有关系,特意了解了目前常用的几种算法,总结如下:

  1. 随机:负载均衡方法随机的把负载分配到各个可用的服务器上,通过随机数生成算法选取一个服务器,然后把连接发送给它。虽然许多均衡产品都支持该算法,但是它的有效性一直受到质疑,除非把服务器的可运行时间看的很重。

  2. 轮询:轮询算法按顺序把每个新的连接请求分配给下一个服务器,最终把所有请求平分给所有的服务器。轮询算法在大多数情况下都工作的不错,但是如果负载均衡的设备在处理速度、连接速度和内存等方面不是完全均等,那么效果会更好。

  3. 加权轮询:该算法中,每个机器接受的连接数量是按权重比例分配的。这是对普通轮询算法的改进,比如你可以设定:第三台机器的处理能力是第一台机器的两倍,那么负载均衡器会把两倍的连接数量分配给第3台机器。

  4. 动态轮询:类似于加权轮询,但是,权重值基于对各个服务器的持续监控,并且不断更新。这是一个动态负载均衡算法,基于服务器的实时性能分析分配连接,比如每个节点的当前连接数或者节点的最快响应时间等。

  5. 最快算法:最快算法基于所有服务器中的最快响应时间分配连接。该算法在服务器跨不同网络的环境中特别有用。

  6. 最少连接:系统把新连接分配给当前连接数目最少的服务器。该算法在各个服务器运算能力基本相似的环境中非常有效。

  7. 观察算法:该算法同时利用最小连接算法和最快算法来实施负载均衡。服务器根据当前的连接数和响应时间得到一个分数,分数较高代表性能较好,会得到更多的连接。

  8. 预判算法:该算法使用观察算法来计算分数,但是预判算法会分析分数的变化趋势来判断某台服务器的性能正在改善还是降低。具有改善趋势的服务器会得到更多的连接。该算法适用于大多数环境。

references: introduction to load balancing

GC学习资料收集

发表于 2015-10-25

学习资料收集

  1. 垃圾收集器介绍
  2. GC学习笔记
  3. 一次CMS GC问题排查过程(理解原理+读懂GC日志)
  4. 深入理解Major GC, Full GC, CMS
  5. 了解 CMS 垃圾回收日志
  6. 成为Java GC专家(3)—如何优化Java垃圾回收机制
  7. JVM参数设置以及分析
  8. 触发Full GC执行的情况
  9. 一次非常诡异的CMS GC频繁问题的排查
  10. java内存区域理解-初步了解
  11. jstack命令使用
  12. jstat命令使用
  13. jhat命令使用
  14. 关于施用full gc频繁的分析及解决
  15. JVM GC算法CMS详解

ImageDemo

发表于 2015-10-25

测试一下插入本地图片
[serial young gc]

12
wxpjimmy

wxpjimmy

Be a better man!

11 日志
2 分类
16 标签
GitHub E-Mail
© 2017 wxpjimmy
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.3