texec);
}
```
这里需要解释一下,上述实际分成了两大类:
* exec 无返回值的rpc调用
* call 有返回值的调用
这里使用了Java 8的函数式编程进行抽象。如果不太熟悉的朋友,可以自行查阅相关资料。
在函数式编程的帮助下,我们可以将每一个rpc调用都分为同步和异步两种,异步的调用会返回一个Future。
再来看一下AbstractThriftClient:
```java
/**
* @(#)AbstractThriftClient.java, Aug 01, 2017.
*
* Copyright 2017 fenbi.com. All rights reserved.
* FENBI.COM PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package com.coder4.lmsia.thrift.client;
import com.coder4.lmsia.thrift.client.func.ThriftCallFunc;
import com.coder4.lmsia.thrift.client.func.ThriftExecFunc;
import org.apache.thrift.TServiceClient;
import org.apache.thrift.TServiceClientFactory;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.apache.thrift.protocol.TProtocol;
import org.apache.thrift.transport.TTransport;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author coder4
*/
public abstract class AbstractThriftClient implements ThriftClient {
protected static final int THRIFT_CLIENT_DEFAULT_TIMEOUT = 5000;
protected static final int THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE = 1024 * 1024 * 16;
private Class> thriftClass;
private static final TBinaryProtocol.Factory protocolFactory = new TBinaryProtocol.Factory();
private TServiceClientFactory clientFactory;
// For async call
private ExecutorService threadPool;
public void init() {
try {
clientFactory = getThriftClientFactoryClass().newInstance();
} catch (Exception e) {
throw new RuntimeException();
}
if (!check()) {
throw new RuntimeException("Client config failed check!");
}
threadPool = new ThreadPoolExecutor(
10, 100, 0,
TimeUnit.MICROSECONDS, new LinkedBlockingDeque<>());
}
protected boolean check() {
if (thriftClass == null) {
return false;
}
return true;
}
@Override
public Future asyncCall(ThriftCallFunc tcall) {
return threadPool.submit(() -> this.call(tcall));
}
@Override
public Future> asyncExec(ThriftExecFunc texec) {
return threadPool.submit(() -> this.exec(texec));
}
protected TCLIENT createClient(TTransport transport) throws Exception {
// Step 1: get TProtocol
TProtocol protocol = protocolFactory.getProtocol(transport);
// Step 2: get client
return clientFactory.getClient(protocol);
}
private Class> getThriftClientFactoryClass() {
Class clientClazz = getThriftClientClass();
if (clientClazz == null) {
return null;
}
for (Class> clazz : clientClazz.getDeclaredClasses()) {
if (TServiceClientFactory.class.isAssignableFrom(clazz)) {
return (Class>) clazz;
}
}
return null;
}
private Class getThriftClientClass() {
for (Class> clazz : thriftClass.getDeclaredClasses()) {
if (TServiceClient.class.isAssignableFrom(clazz)) {
return (Class) clazz;
}
}
return null;
}
public void setThriftClass(Class> thriftClass) {
this.thriftClass = thriftClass;
}
}
```
上述抽象的Thrift客户端实现了如下功能:
1. 客户端线程池,这里主要是为异步调用准备的,与之前构造的服务端的线程池是完全不同的。
* asyncCall和asyncExec使用了线程池来完成异步调用
1. thriftClass 存储了Thrift的桩代码了类,不同业务生成的ThriftClass不一样,所以这里存储了class。
1. createClient提供了共用函数,传入一个transport,即可构造生成一个Thrift Client,特别注意的是,这里设定的通信协议为TBinaryProtocol,必须与服务端保持一致,否则无法成功通信。
由于call和exec与连接实现较为相关,因此并未在这一层中实现,最后我们来看一下EasyThriftClient:
```java
package com.coder4.lmsia.thrift.client;
import com.coder4.lmsia.thrift.client.func.ThriftCallFunc;
import com.coder4.lmsia.thrift.client.func.ThriftExecFunc;
import org.apache.thrift.TServiceClient;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
/**
* @author coder4
*/
public class EasyThriftClient extends AbstractThriftClient {
private static final int EASY_THRIFT_BUFFER_SIZE = 1024 * 16;
protected String thriftServerHost;
protected int thriftServerPort;
@Override
protected boolean check() {
if (thriftServerHost == null || thriftServerHost.isEmpty()) {
return false;
}
if (thriftServerPort <= 0) {
return false;
}
return super.check();
}
private TTransport borrowTransport() throws Exception {
TSocket socket = new TSocket(thriftServerHost, thriftServerPort, THRIFT_CLIENT_DEFAULT_TIMEOUT);
TTransport transport = new TFramedTransport(
socket, THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE);
transport.open();
return transport;
}
private void returnTransport(TTransport transport) {
if (transport != null && transport.isOpen()) {
transport.close();
}
}
private void returnBrokenTransport(TTransport transport) {
if (transport != null && transport.isOpen()) {
transport.close();
}
}
@Override
public TRET call(ThriftCallFunc tcall) {
// Step 1: get TTransport
TTransport tpt = null;
try {
tpt = borrowTransport();
} catch (Exception e) {
throw new RuntimeException(e);
}
// Step 2: get client & call
try {
TCLIENT tcli = createClient(tpt);
TRET ret = tcall.call(tcli);
returnTransport(tpt);
return ret;
} catch (Exception e) {
returnBrokenTransport(tpt);
throw new RuntimeException(e);
}
}
@Override
public void exec(ThriftExecFunc texec) {
// Step 1: get TTransport
TTransport tpt = null;
try {
tpt = borrowTransport();
} catch (Exception e) {
throw new RuntimeException(e);
}
// Step 2: get client & exec
try {
TCLIENT tcli = createClient(tpt);
texec.exec(tcli);
returnTransport(tpt);
} catch (Exception e) {
returnBrokenTransport(tpt);
throw new RuntimeException(e);
}
}
public String getThriftServerHost() {
return thriftServerHost;
}
public void setThriftServerHost(String thriftServerHost) {
this.thriftServerHost = thriftServerHost;
}
public int getThriftServerPort() {
return thriftServerPort;
}
public void setThriftServerPort(int thriftServerPort) {
this.thriftServerPort = thriftServerPort;
}
```
简单解释下上述代码
1. 需要外部传入RPC服务器的主机名和端口 thriftServerHost和thriftServerPort
1. borrowTransport完成Transport(Thrift中类似Socket的抽象) 的构造,注意这里要使用TFramedTransport,与之前服务端的构造保持一致。
1. returnTransport关闭Transport
1. returnBrokenTransport关闭出异常的Transport
1. call和exec 在拿到Transport后,使用函数式编程的方式,完成rpc调用,如果有异常则关闭连接。
最后我们来看一下对应的Builder,EasyThriftClientBuilder:
```java
package com.coder4.lmsia.thrift.client.builder;
import com.coder4.lmsia.thrift.client.EasyThriftClient;
import org.apache.thrift.TServiceClient;
/**
* @author coder4
*/
public class EasyThriftClientBuilder {
private final EasyThriftClient client = new EasyThriftClient<>();
protected EasyThriftClient build() {
client.init();
return client;
}
protected EasyThriftClientBuilder setHost(String host) {
client.setThriftServerHost(host);
return this;
}
protected EasyThriftClientBuilder setPort(int port) {
client.setThriftServerPort(port);
return this;
}
protected EasyThriftClientBuilder setThriftClass(Class> thriftClass) {
client.setThriftClass(thriftClass);
return this;
}
}
```
Builder的代码比较简单,就是以链式调用的方式,通过主机和端口,方便地构造一个EasyThriftClient。
看了EasyThriftClient后下面我们来看一下如何集成到项目中。
在[Gradle子项目划分与微服务的代码结构](sb-gradle-structure.md)一节中,我们已经提到,将每个微服务的RPC客户端放在xx-client子工程中,现在我们再来回顾下lmsia-abc-client的目录结构。
```shell
├── build.gradle
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── coder4
│ │ └── lmsia
│ │ └── abc
│ │ └── client
│ │ ├── configuration
│ │ │ └── LmsiaAbcThriftClientConfiguration.java
│ │ ├── LmsiaAbcEasyThriftClientBuilder.java
│ │ └── LmsiaK8ServiceThriftClientBuilder.java
│ └── resources
│ └── META-INF
│ └── spring.factories
└── test
```
我们简单介绍一下:
1. LmsiaAbcThriftClientConfiguration: 客户端自动配置,当激活时,自动生成lmsia-abc对应的RPC服务的客户端。引用者直接@Autowired一下,就可以使用了。
1. LmsiaAbcEasyThriftClientBuilder: EasyThriftClient构造器,主要是自动配置需要。
1. spring.factories: 与服务端的自动配置类似,需要在这个文件中指定自动配置的类路径,才能让Spring Boot自动扫描到自动配置。
1. 其他K8ServiceThriftClient相关的部分,我们将在下一小节进行介绍。
LmsiaAbcEasyThriftClientBuilder文件:
```java
package com.coder4.lmsia.abc.client;
import com.coder4.lmsia.abc.thrift.LmsiaAbcThrift;
import com.coder4.lmsia.abc.thrift.LmsiaAbcThrift.Client;
import com.coder4.lmsia.thrift.client.ThriftClient;
import com.coder4.lmsia.thrift.client.builder.EasyThriftClientBuilder;
/**
* @author coder4
*/
public class LmsiaAbcEasyThriftClientBuilder extends EasyThriftClientBuilder {
public LmsiaAbcEasyThriftClientBuilder(String host, int port) {
setThriftClass(LmsiaAbcThrift.class);
setHost(host);
setPort(port);
}
public static ThriftClient buildClient(String host, int port) {
return new LmsiaAbcEasyThriftClientBuilder(host, port).build();
}
}
```
上述Builder完成了实际的参数填充,主要有:
1. ThriftClient的桩代码类设置(LmsiaAbcThrift.class)
1. 设置主机名和端口
LmsiaAbcClientConfiguration文件:
```java
package com.coder4.lmsia.abc.client.configuration;
import com.coder4.lmsia.abc.client.LmsiaAbcEasyThriftClientBuilder;
import com.coder4.lmsia.abc.client.LmsiaK8ServiceClientBuilder;
import com.coder4.lmsia.abc.thrift.LmsiaAbcThrift;
import com.coder4.lmsia.abc.thrift.LmsiaAbcThrift.Client;
import com.coder4.lmsia.thrift.client.K8ServiceKey;
import com.coder4.lmsia.thrift.client.ThriftClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class LmsiaAbcThriftClientConfiguration {
private Logger LOG = LoggerFactory.getLogger(getClass());
@Bean(name = "lmsiaAbcThriftClient")
@ConditionalOnMissingBean(name = "lmsiaAbcThriftClient")
@ConditionalOnProperty(name = {"lmsiaAbcThriftServer.host", "lmsiaAbcThriftServer.port"})
public ThriftClient easyThriftClient(
@Value("${lmsiaAbcThriftServer.host}") String host,
@Value("${lmsiaAbcThriftServer.port}") int port
) {
LOG.info("######## LmsiaAbcClientConfiguration ########");
LOG.info("easyClient host = {}, port = {}", host, port);
return LmsiaAbcEasyThriftClientBuilder.buildClient(host, port);
}
}
```
如上所示,满足两个条件时,会自动构造LmsiaAbcEasyThriftClient:
1. 还没有生成其他的LmsiaAbcEasyThriftClient(ConditionalOnMissingBean)
2. 配置中指定了lmsiaAbcThriftServer.host和lmsiaAbcThriftServer.port
根据我们前面的介绍,大家应该能理解,虽然有自动配置,但上述配置是一种很糟糕的方式。试想一下,如果我们的服务依赖了5个其他RPC服务,那么岂不是要分别配置5组IP和端口?此外,这种方式也无法支持节点的负载均衡。
如何解决这个问题呢?我们将在K8ServiceThriftClient中解决。
本小节的最后,我们看一下spring.factories:
```shell
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.coder4.lmsia.abc.client.configuration.LmsiaAbcThriftClientConfiguration
```
和之前lmsia-abc-server子工程中的文件类似,这里设置了自动配置的详细类路径,方便Spring Boot的自动扫描。
## K8ServiceThriftClient
在对EasyThriftClient的介绍中,我们发现了一个问题,需要单独配置IP和端口,不支持服务自动发现。
此外,在这个客户端的实现中,默认每次都要建立新的连接。而对于后端服务而言,RPC的服务端和客户端多数都是在内网环境中,连接情况比较稳定,可以通过连接池的方式减少连接握手开销,从而提升RPC服务的性能。如果你对连接池的原理还不太熟悉,可以参考[百科连接池](https://baike.baidu.com/item/%E8%BF%9E%E6%8E%A5%E6%B1%A0)
为此,我们本将介绍K8ServiceThriftClient,它很好的解决了上述问题。
首先,我们使用commons-pool2来构建了TTransport层的连接池。
TTransportPoolFactory:
```java
package com.coder4.lmsia.thrift.client.pool;
import com.coder4.lmsia.thrift.client.K8ServiceKey;
import org.apache.commons.pool2.BaseKeyedPooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.thrift.transport.TFramedTransport;
import org.apache.thrift.transport.TSocket;
import org.apache.thrift.transport.TTransport;
/**
* @author coder4
*/
public class TTransportPoolFactory extends BaseKeyedPooledObjectFactory {
protected static final int THRIFT_CLIENT_DEFAULT_TIMEOUT = 5000;
protected static final int THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE = 1024 * 1024 * 16;
@Override
public TTransport create(K8ServiceKey key) throws Exception {
if (key != null) {
String host = key.getK8ServiceHost();
int port = key.getK8ServicePort();
TSocket socket = new TSocket(host, port, THRIFT_CLIENT_DEFAULT_TIMEOUT);
TTransport transport = new TFramedTransport(
socket, THRIFT_CLIENT_DEFAULT_MAX_FRAME_SIZE);
transport.open();
return transport;
} else {
return null;
}
}
@Override
public PooledObject wrap(TTransport transport) {
return new DefaultPooledObject<>(transport);
}
@Override
public void destroyObject(K8ServiceKey key, PooledObject obj) throws Exception {
obj.getObject().close();
}
@Override
public boolean validateObject(K8ServiceKey key, PooledObject obj) {
return obj.getObject().isOpen();
}
}
```
上述代码主要完成以下功能:
1. 连接超时配置(5秒)
1. create, 生成新连接(TTransport),这里与之前的EasyThriftClient非常类似,不再赘述
1. 验证连接是否有效,通过TTransport的isOpen判断。
TTransportPool:
```java
package com.coder4.lmsia.thrift.client.pool;
import com.coder4.lmsia.thrift.client.K8ServiceKey;
import org.apache.commons.pool2.impl.GenericKeyedObjectPool;
import org.apache.thrift.transport.TTransport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author coder4
*/
public class TTransportPool extends GenericKeyedObjectPool {
private Logger LOG = LoggerFactory.getLogger(getClass());
private static int MAX_CONN = 1024;
private static int MIN_IDLE_CONN = 8;
private static int MAX_IDLE_CONN = 32;
public TTransportPool(TTransportPoolFactory factory) {
super(factory);
setTimeBetweenEvictionRunsMillis(45 * 1000);
setNumTestsPerEvictionRun(5);
setMaxWaitMillis(30 * 1000);
setMaxTotal(MAX_CONN);
setMaxTotalPerKey(MAX_CONN);
setMinIdlePerKey(MIN_IDLE_CONN);
setMaxTotalPerKey(MAX_IDLE_CONN);
setTestOnCreate(true);
setTestOnBorrow(true);
setTestWhileIdle(true);
}
@Override
public TTransportPoolFactory getFactory() {
return (TTransportPoolFactory) super.getFactory();
}
public void returnBrokenObject(K8ServiceKey key, TTransport transport) {
try {
invalidateObject(key, transport);
} catch (Exception e) {
LOG.warn("return broken key " + key);
e.printStackTrace();
}
}
}
```
上述代码主要是完成连接池的配置,比较直观:
1. 设置最大连接数1024
1. 设置最大空闲数32,最小空闲数8,每间隔45秒尝试更改维护连接池中的连接数量。
1. 当每次"创建"、从池子中"借用"、"空闲"时,检查连接是否有效。
下面我们来看一下如何在K8ServiceThriftClient中使用:
```java
package com.coder4.lmsia.thrift.client;
import com.coder4.lmsia.thrift.client.func.ThriftCallFunc;
import com.coder4.lmsia.thrift.client.func.ThriftExecFunc;
import com.coder4.lmsia.thrift.client.pool.TTransportPool;
import com.coder4.lmsia.thrift.client.pool.TTransportPoolFactory;
import org.apache.thrift.TServiceClient;
import org.apache.thrift.transport.TTransport;
public class K8ServiceThriftClient
extends AbstractThriftClient {
private K8ServiceKey k8ServiceKey;
private TTransportPool connPool;
@Override
public void init() {
super.init();
// check
if (k8ServiceKey == null) {
throw new RuntimeException("invalid k8ServiceName or k8Serviceport");
}
// init pool
connPool = new TTransportPool(new TTransportPoolFactory());
}
@Override
public TRET call(ThriftCallFunc tcall) {
// Step 1: get TTransport
TTransport tpt = null;
K8ServiceKey key = getConnBorrowKey();
try {
tpt = connPool.borrowObject(key);
} catch (Exception e) {
throw new RuntimeException(e);
}
// Step 2: get client & call
try {
TCLIENT tcli = createClient(tpt);
TRET ret = tcall.call(tcli);
returnTransport(key, tpt);
return ret;
} catch (Exception e) {
returnBrokenTransport(key, tpt);
throw new RuntimeException(e);
}
}
@Override
public void exec(ThriftExecFunc texec) {
// Step 1: get TTransport
TTransport tpt = null;
K8ServiceKey key = getConnBorrowKey();
try {
// borrow transport
tpt = connPool.borrowObject(key);
} catch (Exception e) {
throw new RuntimeException(e);
}
// Step 2: get client & exec
try {
TCLIENT tcli = createClient(tpt);
texec.exec(tcli);
returnTransport(key, tpt);
} catch (Exception e) {
returnBrokenTransport(key, tpt);
throw new RuntimeException(e);
}
}
private K8ServiceKey getConnBorrowKey() {
return k8ServiceKey;
}
private void returnTransport(K8ServiceKey key, TTransport transport) {
connPool.returnObject(key, transport);
}
private void returnBrokenTransport(K8ServiceKey key, TTransport transport) {
connPool.returnBrokenObject(key, transport);
}
public K8ServiceKey getK8ServiceKey() {
return k8ServiceKey;
}
public void setK8ServiceKey(K8ServiceKey k8ServiceKey) {
this.k8ServiceKey = k8ServiceKey;
}
}
```
上述大部分代码和EasyThriftClient非常接近,有差异的部分主要是与连接的"借用"、"归还"相关的:
1. 在call和exec中,借用连接
* getConnBorrowKey先构造一个key,包含了主机名和端口。这里的主机名是[微服务的自动发现](./ms-discovery/msd.md)中提到的Kubernetes服务,如果你对相关原理不太熟悉,可以自行回顾对应章节。
* 从connPool中借用一个连接(TTransport)
* 剩余发起rpc调用的步骤就和EasyThriftClient相同了,不再赘述。
1. 当rpc调用结束后
* 正常结束,调用connPool.returnObject将TTransport归还到连接池中。
* 非正常结束,调用connPool.returnBrokenTransport,让连接池销毁这个连接,以防后续借用到这个可能出错的TTransport。
类似的,我们也配套了对应的Builder:
```java
package com.coder4.lmsia.thrift.client.builder;
import com.coder4.lmsia.thrift.client.EasyThriftClient;
import org.apache.thrift.TServiceClient;
/**
* @author coder4
*/
public class EasyThriftClientBuilder {
private final EasyThriftClient client = new EasyThriftClient<>();
protected EasyThriftClient build() {
client.init();
return client;
}
protected EasyThriftClientBuilder setHost(String host) {
client.setThriftServerHost(host);
return this;
}
protected EasyThriftClientBuilder setPort(int port) {
client.setThriftServerPort(port);
return this;
}
protected EasyThriftClientBuilder setThriftClass(Class> thriftClass) {
client.setThriftClass(thriftClass);
return this;
}
}
```
上述Builder主要是设置所需的两个参数,Host和Port,看起来和EasyThriftClient并没有什么不同?
别着急,我们继续看一下lmsia-abc-client中的集成:
```java
package com.coder4.lmsia.abc.client;
import com.coder4.lmsia.abc.thrift.LmsiaAbcThrift;
import com.coder4.lmsia.abc.thrift.LmsiaAbcThrift.Client;
import com.coder4.lmsia.thrift.client.K8ServiceKey;
import com.coder4.lmsia.thrift.client.ThriftClient;
import com.coder4.lmsia.thrift.client.builder.K8ServiceThriftClientBuilder;
/**
* @author coder4
*/
public class LmsiaK8ServiceThriftClientBuilder extends K8ServiceThriftClientBuilder {
public LmsiaK8ServiceThriftClientBuilder(K8ServiceKey k8ServiceKey) {
setThriftClass(LmsiaAbcThrift.class);
setK8ServiceKey(k8ServiceKey);
}
public static ThriftClient buildClient(K8ServiceKey k8ServiceKey) {
return new LmsiaK8ServiceThriftClientBuilder(k8ServiceKey).build();
}
}
```
在集成的时候,我们需要传入一个key,可以手动制定,也可以自动配置
我们看一下完整的自动配置代码,LmsiaAbcThriftClientConfiguration:
```java
public class LmsiaAbcThriftClientConfiguration {
@Bean(name = "lmsiaAbcThriftClient")
@ConditionalOnMissingBean(name = "lmsiaAbcThriftClient")
@ConditionalOnProperty(name = {"lmsiaAbcThriftServer.host", "lmsiaAbcThriftServer.port"})
public ThriftClient easyThriftClient(
@Value("${lmsiaAbcThriftServer.host}") String host,
@Value("${lmsiaAbcThriftServer.port}") int port
) {
LOG.info("######## LmsiaAbcThriftClientConfiguration ########");
LOG.info("easyThriftClient host = {}, port = {}", host, port);
return LmsiaAbcEasyThriftClientBuilder.buildClient(host, port);
}
@Bean(name = "lmsiaAbcThriftClient")
@ConditionalOnMissingBean(name = "lmsiaAbcThriftClient")
public ThriftClient k8ServiceThriftClient() {
LOG.info("######## LmsiaAbcThriftClientConfiguration ########");
K8ServiceKey k8ServiceKey = new K8ServiceKey(K8_SERVICE_NAME, K8_SERVICE_PORT);
LOG.info("k8ServiceThriftClient key:" + k8ServiceKey);
return LmsiaK8ServiceThriftClientBuilder.buildClient(k8ServiceKey);
}
//...
}
```
对比easyThriftClient和k8ServiceThriftClient不难发现,K8ServiceThriftClient的参数,是通过常量直接写死的。也就是我们在[微服务的自动发现与负载均衡](../ms-discovery/msd.md)中提到的,约定好服务的命名规则。
看下常量定义:
```java
public class LmsiaAbcConstant {
// ......
public static final String PROJECT_NAME = "lmsia-abc";
public static final String K8_SERVICE_NAME = PROJECT_NAME + "-server";
public static final int K8_SERVICE_PORT = 3000;
// ......
}
```
这样以来,一旦确定了项目名,那么Kubernetes中的服务名字也确定了。因此,k8ServiceThriftClient自动配置会被自动激活,即只要引用了lmsia-abc-client这个包,就会自动配置好一个RPC客户端,是不是非常方便?
我们来看一下具体的使用例子:
```java
import com.coder4.lmsia.thrift.client.ThriftClient;
public class LmsiaAbctProxy {
@Autowired
private ThriftClient client;
public String hello() {
return client.call(cli -> cli.sayHi());
}
```
至此,我们已经完成了在Spring Boo中集成Thrift RPC的服务端、客户端的工作。
* 服务端,我们通过ThriftServerConfiguration、ThriftProcessorConfiguration自动配置了Thrift RPC服务端。
* 客户端,通过Kubernetes的服务功能,自动配置了带服务发现功能的Thrift RPC客户端K8ServiceThriftClient。该客户端同时内置了连接池,用于节省连接开销。
================================================
FILE: legacy/toolchain/README.md
================================================
# 研发工具链
子曰: "工欲善其事必先利其器"。
本书的开篇已经指出,微服务的架构对研发人员提出了更高的要求。
幸运的是,通过不断完善、改进研发工具链,可以为研发人员提供更高效、更便捷的开发环境。
本书反复强调"微服务"、"研发工具链"、"运维工具链"三者是一个整体,如果只重视微服务的开发技术,而不重视工具链的建设,微服务的架构便无从谈起。
本章将对微服务架构下,常见的研发工具进行介绍。
大致又可分为两部分:
* 研发环境构建: 主要包括内部帐号管理、代码版本管理、Java依赖管理,这些基础研发环境。
* 高效研发构建: 主要通过小工具、代码模板、开源项目的引入,降低微服务开发难度,提升开发效率。
现在,让我们开始研发工具链的构建之旅吧!
================================================
FILE: legacy/toolchain/bom.md
================================================
# BOM 减少版本冲突
在应用了Gradle构建工具,以及Maven仓库来管理版本依赖后,程序的构建、依赖问题已经得到了基本的解决。
但随着项目的不断发展,一个微服务的依赖可能会越来越多,出现版本冲突的问题。
举个版本冲突的例子:项目依赖的A的0.9版本,同时依赖了项目B,项目B又依赖了项目A的1.0版本。此时,项目会选择A的0.9还是1.0版本呢?
事实上,按照Maven的依赖规则,会选用最小的版本0.9。如果0.9和1.0是API兼容的,那么问题不大。如果1.0的API发生了"break change",那么很遗憾,项目B中的代码会包错,更离谱的是,只有运行时才会发生问题。这类问题经常难以诊断,因此,我们应当尽量减少版本冲突的问题。
BOM(Bill Of Materials)就是为了解决这个问题而生的,它定义了一组依赖管理的项目并约定了对应的版本。其他项目可以直接引用BOM而不用设定对应版本,BOM会自动把缺失的版本补全。
在本书的微服务架构下,我们强烈建议定义公共库的BOM,以减少版本冲突的问题。
应用BOM需要两个步骤:
1. 新建一个BOM的Maven项目
1. 在项目中引用该BOM项目
新建一个BOM项目非常简单,只需要一个xml文件
```xml
4.0.0
com.coder4.lmsia
pom-parent
0.0.4
pom
UTF-8
1.8
org.springframework.boot
spring-boot-dependencies
1.5.7.RELEASE
pom
import
com.google.guava
guava
23.0
com.coder4.lmsia
redis
0.0.4
com.coder4.lmsia
cache
0.0.5
com.coder4.lmsia
rabbitmq
0.0.2
com.coder4.lmsia
thrift-server
0.0.5
com.coder4.lmsia
database
0.0.1
com.h2database
h2
1.4.196
nexus_coder4
http://192.168.99.100:8081/nexus/content/repositories/releases/
nexus_coder4
http://192.168.99.100:8081/nexus/content/repositories/snapshots/
```
解释下上面的代码:
1. 这是一个pom,应用了若干包,并指定了他们的版本
1. 底部指定了maven仓库的发布地址(如果你有多个不同的maven repo权限才需要设定)
然后看一下在gradle项目中如何引用bom:
build.gradle:
```
plugins {
id "io.spring.dependency-management" version "1.0.3.RELEASE"
}
apply plugin: 'java'
apply plugin: 'application'
repositories {
maven {
credentials {
username "$mavenUser"
password "$mavenPass"
}
url 'http://maven.coder4.com/nexus/content/groups/public'
}
}
// import bom
dependencyManagement {
imports {
mavenBom 'com.coder4.sbmvt:pom-parent:0.0.1'
}
}
dependencies {
// use bom version
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'com.coder4.lmsia:redis'
// Use JUnit test framework
testCompile 'junit:junit:4.12'
}
// Define the main class for the application
mainClassName = 'App'
```
如上所示,我们通过dependencyManagement插件引入了bom项目,而指定项目时只有group和project、没有版本,版本会自动使用bom中统一定义的。
对于微服务架构,我们可以将使用的数据库、RPC、消息队列、工具类等共用库的版本都放入BOM,以统一依赖的版本。
================================================
FILE: legacy/toolchain/gerrit.md
================================================
# gerrit 代码的版本管理与审查
## 为什么选用git作为版本管理系统
在实际工作中,绝大多数的项目都使用了代码的版本管理系统。在应用版本管理系统后,可以代码许多好处,相信大家有有所体会:
* 团队合作: 应用版本管理系统后,每个团队成员都可以对每个文件进行修改,而不用担心出现不一致、改动丢失、甚至冲突的情况,版本管理系统会负责这些事情。
* 改动可见: 项目开发往往不是一蹴而就,而是划分为许多个小步骤。我们可以将每个改动作为一次提交,版本管理系统可以展示出两个提交之间的差异,项目的开发进展一目了然。
* 轻松回滚: 如果我们不小心搞出了一个bug,或者某个设计思路出现了较大错误,可以轻松的回滚到某个之前的版本,这也是版本管理系统为我们提供的便利功能。
在版本系统的选型上,我们选用了git,相比于svn,它具有诸多优点:
* 分布式、协作方便: git的设计就是分布式版本管理系统,更适用于多人协作。而svn设计理念就是中央式管理,中规中矩但不利于团队协作。
* 速度更快: 在文件模式上,git基于"指针式"设计,比svn更快。在微服务架构下,创建新服务新项目更加频繁,git的速度优势会更加明显。
* 分支切换: git的分支设计非常轻量级,完全可以在本地完成,而svn则需要完全拉取分支的所有文件,如果你使用svn管理过多分支的大项目,一定对此深有感触。
* 操作更丰富:git提供了丰富的操作手段,当你使用熟练后,会比使用svn的效率更高。
当然,git也有一个最大的缺点:学习曲线较为陡峭。对于新手而言,svn简单看看文档就能上手,git可能需要几天才能掌握基本操作。
但是,面对git带给我们的种种好处,还是值得仔细学习一下的,篇幅所限,我们不会讨论git的用法。
如果你想仔细学习,推荐阅读[廖雪锋的Git教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)。
## 为什么代码需要代码审查
如果是一个人做的开源项目,有版本管理系统就足够了。
但对于团队开发,除了版本管理外,一般还应有代码审查(code review)。代码审查的优势如下:
* 相互检查、提升质量: 在开发过程中,我们自己写出的bug,往往是看不出来的,换个人却很容易发现,就是所谓的"当局者迷,旁观者清"。通过相互检查代码,可以有效提升软件质量。
* 让新成员快速提高: 我们希望新加入的团队成员,可以快速学习、快速成长。阅读项目固然是一个很好的方式,但一个项目往往太大,难以下手,代码审查的粒度是一次提交,更小、更适合新手学习。
* 边开发边讨论: 在方案设计阶段,我们可能有了大致的方案,但在开发过程中,往往会暴露出更多的问题。代码审查为这些问题的讨论提供了一个合适的契机,大家可以在代码审核的同时进行讨论。
在系统选型方面,我们选用了较为成熟的gerrit作为代码审查系统。
需要指出的是gerrit同时内置了git服务器的功能,因此我们使用gerrit同时作为版本管理和代码审查系统。
## gerrit系统的基本配置
与之前的LDAP类似,我们也将gerrit部署在Kubernetes上。
首先保证物理机上Volume挂载点的创建
```shell
minikube ssh
$sudo mkdir /data/gerrit
$sudo chown -R 999:999 /data/gerrit/
```
接着我们看一下deployment文件。
gerrit-deployment.yaml
```shell
piVersion: apps/v1
kind: Deployment
metadata:
name: gerrit-deployment
spec:
selector:
matchLabels:
app: gerrit
replicas: 1
template:
metadata:
labels:
app: gerrit
spec:
restartPolicy: Always
nodeSelector:
kubernetes.io/hostname: minikube
containers:
- name: gerrit-ct
image: openfrontier/gerrit:2.15.1
ports:
- containerPort: 8080
hostPort: 80
- containerPort: 29418
hostPort: 29418
volumeMounts:
- mountPath: "/var/gerrit/review_site"
name: volume
env:
- name: GITWEB_TYPE
value: gitiles
- name: AUTH_TYPE
value: LDAP
- name: LDAP_SERVER
value: ldap://192.168.99.100
- name: LDAP_ACCOUNTBASE
value: "dc=coder4,dc=com"
- name: LDAP_ACCOUNTPATTERN
value: "(cn=${username})"
- name: LDAP_ACCOUNTSSHUSERNAME
value: "${cn}"
- name: LDAP_ACCOUNTFULLNAME
value: "${sn}"
- name: LDAP_USERNAME
value: "cn=guest,dc=coder4,dc=com"
- name: LDAP_PASSWORD
value: "guest123"
- name: WEBURL
value: "http://192.168.99.100"
volumes:
- name: volume
hostPath:
path: /data/gerrit/
```
虽然文件很长,但并不复杂,我们简单解读下:
* Docker镜像为openfrontier/gerrit:2.15.1
* 端口映射8080到物理机的的80端口上
* 挂载点/var/gerrit/review_site
* 使用LDAP作为帐号接入,具体配置在之前LDAP一节已经见识过了,这里不再赘述。
* WEB跳转URL定义为 http://物理机IP
下面启动一下:
```shell
kubectl apply -f ./gerrit-deployment.yaml
```
启动成功后,我们访问gerrit,然后点击右上角的"Sign In"即可登录。这里的帐号,填写之前创建的一个LDAP内部帐号。需要特别说明的是,第一个登录的用户,会被gerrit认为是超级管理员,所以请慎重选择。

如果一切顺利的话,就会登录成功了。至此,我们已经完成了gerrit服务器的基本配置。
## gerrit常用插件
gerrit系统的基本功能比较简单,需要配合插件才能发挥出更大优势
在此,我们先安装两个系统内置的插件:
* commit msg长度检查
* 项目下载url生成器
安装插件是通过ssh命令完成的,一次,首先要将ssh密钥的公钥上传到gerrit上。
如果你还没有ssh密钥,可以使用sshkeygen生成,这里不做详细展开。
点击右上角的姓名 -> Settings -> SSH Public Keys,粘贴后点击"Add"。
然后添加插件:
```shell
ssh -p 29418 lihy@192.168.99.100 gerrit plugin install 'jar:file:/var/gerrit/review_site/bin/gerrit.war!/WEB-INF/plugins/download-commands.jar'
ssh -p 29418 lihy@192.168.99.100 gerrit plugin install 'jar:file:/var/gerrit/review_site/bin/gerrit.war!/WEB-INF/plugins/commit-message-length-validator.jar'
```
## gerrit项目的权限控制
gerrit默认的权限配置是对所有人(包括注册用户和匿名用户)开放所有项目。
这样的设置可能过为宽松,可以自行更改。
使用管理员帐号登录,然后进入Projects -> All Projects,点击底部的顶部的"Access",点击Edit。然后找到 Reference: refs/* -> Read,修改为 -> Block Anonymous Users,修改完成后点击"Save for change"。

我们可以登出当前用户,再次访问gerrit主页,可以发现,在未登录状态,无法找到任何review和项目了。
## 第一个gerrit代码review
下面我们尝试用gerrit完成一个完整的流程:从新建项目、提交、到审核代码。
我们尝试新建一个项目:Projects -> Create New Project:
* 项目名为lmsia-xyz
* 继承自All-Projects
然后点击"Create Project"
创建完成后,我们就可以将代码克隆到本地进行开发了。
选择:Projects -> List 找到lmsia-xyz并点击,在顶部,可以找到Clone工具栏,选择右侧的ssh,底下会出现一行命令:
```shell
git clone ssh://lihy@192.168.99.100:29418/lmsia-xyz
```
我们在本地执行这行命令,即可成功得克隆代码
```shell
git clone ssh://lihy@192.168.99.100:29418/lmsia-xyz
Cloning into 'lmsia-xyz'...
remote: Counting objects: 2, done
remote: Finding sources: 100% (2/2)
remote: Total 2 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (2/2), done.
Checking connectivity... done.
```
如果报权限错误,一般是ssh密钥配置的不对,请检查gerrit个人资料中的key是否为本地设置的公钥。
配置修改后,可以自行通过这条命令测试
```shell
ssh -p 29418 lihy@192.168.99.100
**** Welcome to Gerrit Code Review ****
Hi 李赫元, you have successfully connected over SSH.
Unfortunately, interactive shells are disabled.
To clone a hosted Git repository, use:
git clone ssh://lihy@192.168.99.100:29418/REPOSITORY_NAME.git
Connection to 192.168.99.100 closed.
```
下面我们新建一个文件:
```shell
touch README.md
```
添加并提交:
```shell
git add .
git commit -m "ADD: README.md"
```
至此,我们已经完成了代码的提交,当然这只是提交到本地git仓库中。
我们还需要推送到gerrit仓库中供别人审核。
在可以推送到gerrit之前,还需要进行2个配置:
1. (每台机器配置一次)若你的操作系统用户名和gerrit用户名一致,需要配置ssh选项。
1. (每个项目配置一次)配置项目的gerrit远程仓库
1. (每个项目配置一次)配置项目推送到gerrit后默认的代码审核人
首先是ssh配置,以我的环境为例,我的操作系统用户名是coder4,而gerrit用户名是lihy,于是在~/.ssh/config中添加如下配置:
```shell
Host 192.168.99.100
User lihy
IdentityFile ~/.ssh/id_rsa
Hostname 192.168.99.100
Port 29418
```
这个配置并不复杂,就是告诉操作系统,当连接192.168.99.100这个host时,默认用户改为lihy而不是系统默认的coder4
而上面每个项目需要执行一次的2和3稍微,这个操作稍微复杂一些,所以我将它合并成了一个脚本,方便大家调用。
```shell
#!/usr/bin/env bash
GERRIT_HOST="192.168.99.100"
EMAIL_POSTFIX="coder4.com"
set -e
function join { local IFS="$1"; shift; echo "$*"; }
if [ -z "$1" ]; then
echo "Usage: $0 reviewer[,reviewer ...]"
exit 1
fi
set -u
if [ -z `git remote | grep origin` ]; then
echo "Remote origin not found, please clone this repository correctly or add origin remote by 'git remote add'."
exit 1
fi
scp -p -P 29418 $GERRIT_HOST:hooks/commit-msg .git/hooks/
cat > .git/hooks/pre-commit << EOF
##!/bin/sh
if git-rev-parse --verify HEAD >/dev/null 2>&1 ; then
against=HEAD
else
# Initial commit: diff against an empty tree object
against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
fi
# Find files with trailing whitespace
for FILE in \`exec git diff-index --check --cached \$against -- | sed '/^[+-]/d' | sed -E 's/:[0-9]+:.*//' | uniq\` ; do
# Fix them!
sed -i '' -E 's/[[:space:]]*$//' "\$FILE"
git add "\$FILE"
done
EOF
chmod a+x .git/hooks/pre-commit
originURL=`git remote -v | grep fetch | perl -nle'print $& if m{(?<=origin\t)\S*}'`
(git remote remove review >& /dev/null || exit 0)
git remote add review $originURL
IFS=',' read -a reviewers <<< "$1"
sed -i '/\+refs\/heads\/\*:refs\/remotes\/review\/\*/d' .git/config
for i in "${!reviewers[@]}"; do
reviewers[$i]="--reviewer=${reviewers[$i]}@$EMAIL_POSTFIX"
done
echo -e "\tpush = HEAD:refs/for/master" >> .git/config
echo -e "\treceivepack = git receive-pack `join " " ${reviewers[@]}`" >> .git/config
```
如上的脚本做了3件事情:
* 从gerrit上下载commit-msg的钩子,这是gerrit生成Change-ID所必须的。
* 配置远程review仓库
* 配置推送后默认的代码审核人
执行一下,默认自己和张三审核:
```shell
initGerrit.sh lihy,zhangsan
```
上述配置完成后,就可以推送你的第一个code review了:
```shell
git push review
Counting objects: 3, done.
Writing objects: 100% (3/3), 244 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: Processing changes: refs: 1, done
remote: ERROR: [127a929] missing Change-Id in commit message footer
remote:
remote: Hint: To automatically insert Change-Id, install the hook:
remote: gitdir=$(git rev-parse --git-dir); scp -p -P 29418 lihy@192.168.99.100:hooks/commit-msg ${gitdir}/hooks/
remote: And then amend the commit:
remote: git commit --amend
remote:
To ssh://lihy@192.168.99.100:29418/lmsia-xyz
! [remote rejected] HEAD -> refs/for/master ([127a929] missing Change-Id in commit message footer)
error: failed to push some refs to 'ssh://lihy@192.168.99.100:29418/lmsia-xyz'
```
然而我们发现还是执行失败,这是因为,我们先执行了commit后执行了initGerrit,导致commit时候没有Change-ID。
我们可以按照提示补救一下:
```shell
git commit --amend
```
再次执行推送,成功:
```shell
git push review
Counting objects: 3, done.
Writing objects: 100% (3/3), 285 bytes | 0 bytes/s, done.
Total 3 (delta 0), reused 0 (delta 0)
remote: Processing changes: new: 1, done
remote:
remote: New Changes:
remote: http://192.168.99.100/#/c/lmsia-xyz/+/21 ADD: README.md
remote:
To ssh://lihy@192.168.99.100:29418/lmsia-xyz
* [new branch] HEAD -> refs/for/master
```
我们到gerrit上看一眼,发现已经有了这个推送:

我们点击进去,自行+2,然后点击Submit,如下两图所示。


此时,代码就被成功合并进master分支了。
我们的gerrit默认配置了gitweb,即可以通过网页的方式查看项目的完整源码: Plugsin -> gitiles,界面如下图所示:

通过选择不同项目,可以查看不同分支的完整代码。
至此,我们完成了Gerrit服务的搭建,并通过完整的例子演示了项目的创建、克隆、开发、提交、审核流程。
Gerrit还有很多强大的功能,例如Web上可以创建分支、Rebase代码等等,如果你想探索这些高级用法,可以参考[官方文档](https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.2/index.html)。
================================================
FILE: legacy/toolchain/kanboard.md
================================================
# Kanboard Scrum看板
================================================
FILE: legacy/toolchain/ldap.md
================================================
# LDAP 内部账号管理系统
## LDAP及其必要性
对于任何一个研发团队,一套内部通用的帐号管理系统都是必不可少的。请注意我的用词:"内部通用"。
公司内部可能有各种系统:
* 行政层面的OA系统、邮件系统、会议室预订系统。
* 研发团队内部又可能有代码管理、项目进度管理、Bug追踪、依赖管理、Wiki等等。
如果没有内部通用帐号,那么每来一个新员工,就需要到上述所有系统中,分别注册一次。想象一下,这是多么让人头疼的事情!
因此,我们建议团队一定要拥有一套"内部通用"的帐号管理系统。
在这里,我们选用了LDAP(Lightweight Directory Access Protocol)。是一个开放的,中立的,工业标准的应用协议,通过IP协议提供访问控制和维护分布式信息的目录信息。
在技术型团队中,LDAP可以当作内部帐号管理系统来使用。此外,LDAP可以很轻松地与其他系统对接,我们后面即将构建的代码管理、版本管理,都将通过LDAP帐号接入。
## OpenLDAP服务的初步配置
能提供LDAP服务的开源项目有很多,我们选用了较为成熟的开源服务器OpenLDAP。
虽然OpenLDAP并不是微服务,但我们依然放到Kubernetes集群部署,主要原因是:
* 方便运维: 如果不用Docker,就需要手动的安装、配置。一旦物理服务器发生故障,需要迁移服务时,就需要重新执行这些操作。运维起来非常麻烦。
* 方便备份与恢复: 对于这类帐号系统,可用性倒要求并不高(偶尔挂掉1个小时,能接受),但是对数据安全性,特别是备份有较高要求。使用Docker后,我们只需要将产生的数据挂载到Volume上,然后定期备份Volume即可。
来看一下部署文件openldap-deployment.yaml:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: openldap-deployment
spec:
selector:
matchLabels:
app: openldap
replicas: 1
template:
metadata:
labels:
app: openldap
spec:
restartPolicy: OnFailure
nodeSelector:
kubernetes.io/hostname: minikube
containers:
- name: openldap-ct
image: osixia/openldap:1.1.9
ports:
- containerPort: 389
hostPort: 389
- containerPort: 636
hostPort: 636
volumeMounts:
- mountPath: "/etc/ldap/slapd.d"
name: volume
subPath: conf
- mountPath: "/var/lib/ldap"
name: volume
subPath: data
env:
- name: LDAP_TLS
value: "false"
- name: LDAP_DOMAIN
value: "coder4.com"
- name: LDAP_ADMIN_PASSWORD
value: "admin123"
- name: LDAP_READONLY_USER
value: "true"
- name: LDAP_READONLY_USER_USERNAME
value: "guest"
- name: LDAP_READONLY_USER_PASSWORD
value: "guest123"
volumes:
- name: volume
hostPath:
path: /data/openldap/
```
这是一个很长的文件,我们来逐条解释下:
* restartPolicy: 虽然这是一个内部服务,但我们还是希望它能稳定提供服务。如果万一服务挂掉,希望能自动重启。因此我们设置自动重启策略为OnFailure。
* nodeSelector: 我们强制选择了主机名。即这个Pod只能启动在minikube这台hostname的主机上,为什么呢?因为我们的OpenLDAP服务使用了本地Volume(hostVolume),如果不固定机器,允许Pod在任意物理机启动的话,对应Volume并不会自动迁移,导致之前的账户信息"丢失"。因此,对于需要使用Volume的服务,要么选择一种可自动迁移的Volume,要么就需要绑定到一台物理机上。如果你想选用自动迁移的Volume,可以参考[官方Volumes文档](https://kubernetes.io/docs/concepts/storage/volumes/)。
* ports: 我们直接对集群外暴露了389和636两个端口。在实际生产中,我建议选择一台独立的物理机部署所有的内部服务(ldap、maven、git等)。为什么这样搞呢?如果物理机是固定的,我们可以给它分配一个固定的办公网IP,甚至固定的办公网DNS域名,然后简单地通过暴露端口的方式,就可以对全部办公网提供服务了。
* volumeMounts & volumes: 定义了两个volume挂载点,分别挂载到容器的/etc/ldap/slapd.d(配置)和/var/lib/ldap(数据)目录上。对应的物理机挂载目录在/data/openldap/conf和/data/openldap/data上。
* env: 通过环境变量完成了一些初始化的设定,具体如下。
* 不用加密协议[^1]
* 设置域为coder4.com,可以根据你的需求自行更改。
* 创建系统管理员帐号,密码是admin123,这是一个超级管理员,对应用户名是admin(无法更改)
* 创建系统只读帐号,用户名和密码是guest/guest123。这主要是用于其他服务与OpenLDAP服务的通信,只能读取、验证信息,不能做任何更改。
在部署前,我们先要保证物理机上的挂载点存在。
```shell
minikube ssh
$ cd /data
$ sudo mkdir openldap
```
然后部署OpenLDAP服务:
```shell
kubectl apply -f ./openldap-deployment.yaml
```
查看下状态,启动成功了:
```shell
kubectl get pods
NAME READY STATUS RESTARTS AGE
openldap-deployment-7d6b7875f-hxqxf 1/1 Running 0 14m
```
获取集群的IP:
```shell
minikube ip
192.168.99.100
```
验证下,端口已经成功暴露给了集群外:
```shell
telnet 192.168.99.100 389
Trying 192.168.99.100...
Connected to 192.168.99.100.
Escape character is '^]'.
^]
```
操作ldap集群,需要安装一些工具,以Ubuntu为例:
```shell
sudo apt-get install ldap-utils
```
有了工具后,两个系统帐号已经创建成功:
```shell
ldapwhoami -h 192.168.99.100 -p 389 -D "cn=admin,dc=coder4,dc=com" -w admin123
dn:cn=admin,dc=coder4,dc=com
ldapwhoami -h 192.168.99.100 -p 389 -D "cn=guest,dc=coder4,dc=com" -w guest123
dn:cn=guest,dc=coder4,dc=com
```
至此,我们已经完成了OpenLDAP的基础配置,并且成功创建了两个系统帐号。
## 创建内部用户
在刚才的配置中,我们创建了两个系统帐号,但在实际工作中,团队成员一般不会使用系统帐号。
对于一个团队成员,它的帐号至少需要有如下属性:
* 用户名, 一般是纯英文、拼音缩写
* 中文姓名,这个不解释了
* 密码,最好不是明文,而是加密存储
* 邮箱,公司内部的电子邮箱地址
大公司的内部,会细分为多个团队,此时还应当将用户划分到相应的属组。由于篇幅所限,我们在此不讨论属组的问题。
在密码加密方面,我们采用ssha,它需要命令slappasswd,你可以在任何安装了openldap的机器上找到它:
```shell
slappasswd -h {ssha} -s pass123
{SSHA}yG3DQj7iol10+fzWoeBAgoZ+D+h9uQre
```
上述即生成了一个ssha加密过的密码pass123。
我们前面已经提到,LDAP是一个"目录式"的权限管理服务。其本身规则非常复杂到可以单独写一本书:-)
本书不会对其规则进行过多讲解,这里先提供了一个简单的模板,供大家学习。
./users.ldif
```shell
version: 1
# users org
dn: ou=users,dc=coder4,dc=com
objectClass: top
objectClass: organizationalUnit
ou: users
# group org
dn: ou=groups,dc=coder4,dc=com
objectClass: top
objectClass: organizationalUnit
ou: groups
# define users here
dn: cn=lihy,ou=users,dc=coder4,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: lihy
sn:: 5p2O6LWr5YWD
mail: lihy@coder4.com
userPassword: {SSHA}yG3DQj7iol10+fzWoeBAgoZ+D+h9uQre
dn: cn=zhangsan,ou=users,dc=coder4,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
cn: zhangsan
sn:: 5byg5LiJ
mail: zhangsan@coder4.com
userPassword: {SSHA}yG3DQj7iol10+fzWoeBAgoZ+D+h9uQre
# should also modify here if insert new user
dn: cn=Users,ou=groups,dc=coder4,dc=com
objectClass: top
objectClass: groupOfUniqueNames
cn: Users
uniqueMember: cn=lihy,ou=users,dc=coder4,dc=com
uniqueMember: cn=zhangsan,ou=users,dc=coder4,dc=com
# define admin here
dn: cn=Admin,ou=groups,dc=coder4,dc=com
objectClass: top
objectClass: groupOfUniqueNames
cn: Admin
uniqueMember: cn=lihy,ou=users,dc=coder4,dc=com
```
简单解释下:
* 我们创建了2个组users和groups,前者存放用户,后者表示用户的属组。
* 定义两个用户lihy和zhangsan,他们的密码用前面提到的SSLA加密
* 将两个用户加入Users组内
* 将lihy用户加入管理员组内
我们来应用这个模板:
```shell
ldapadd -c -h 192.168.99.100 -p 389 -w admin123 -D "cn=admin,dc=coder4,dc=com" -f ./users.ldif
```
如上,需要用admin帐号,-c选项是忽略所有错误,继续执行。
验证一下新增的内部用户:
```shell
ldapwhoami -h 192.168.99.100 -p 389 -D "cn=lihy,ou=users,dc=coder4,dc=com" -w pass123
dn:cn=lihy,ou=users,dc=coder4,dc=com
```
添加新用户,需要操作三个步骤:
1. 在user.idlf中增加用户的定义
1. 在user.idlf对应属组中添加
1. 执行ldapadd命令
## LDAP系统管理脚本
不用我说大家也明白,上述步骤真的是非常繁琐,而且容易出错。
面对这种情况,大家可以选用第三方的工具来管理LDAP帐号,例如phpLDAPadmin,但是这需要额外维护一套系统,不免有些笨重。
为了降低维护成本,我提供了几个简单的小脚本,以满足日常的管理工作。
添加帐号,ldap_add.sh
```shell
#!/bin/bash
# const
LDAP_SERVER_IP="192.168.99.100"
LDAP_SERVER_PORT="389"
LDAP_ADMIN_USER="cn=admin,dc=coder4,dc=com"
LDAP_ADMIN_PASS="admin123"
if [ x"$#" != x"3" ];then
echo "Usage: $0 "
exit -1
fi
# param
USERNAME="$1"
PASSWORD="$2"
ENCRYPT_PASSWORD=$(slappasswd -h {ssha} -s "$PASSWORD")
REALNAME="$3"
REALNAME_BASE64=$(echo -n $REALNAME | base64)
# add count & group
cat < "
exit -1
fi
# param
USERNAME="$1"
PASSWORD="$2"
ENCRYPT_PASSWORD=$(slappasswd -h {ssha} -s "$PASSWORD")
# modify
cat <"
exit -1
fi
# param
USERNAME="$1"
# delete user
ldapdelete -c -h $LDAP_SERVER_IP -p $LDAP_SERVER_PORT -w $LDAP_ADMIN_PASS -D $LDAP_ADMIN_USER "cn=$USERNAME,ou=users,dc=coder4,dc=com"
```
尝试删除一下:
```shell
./ldap_delete.sh zhangsan
```
然后验证下,确实无法登录了
```shell
ldapwhoami -h 192.168.99.100 -p 389 -D "cn=zhangsan,ou=users,dc=coder4,dc=com" -w pass123
ldap_bind: Invalid credentials (49)
```
至此,我们完成了LDAP服务的构建,并可以通过简单的脚本完成帐号的添删改操作。
[^1]: 如果你十分看中帐号服务对外通信的安全性,建议还是开启,具体可以参考[docker-openldap](https://github.com/osixia/docker-openldap))
================================================
FILE: legacy/toolchain/nexus.md
================================================
# Nexus 私有maven仓库
依赖管理是技术栈的重要一环,几乎所有的现代编程语言都拥有自己的依赖管理系统。
如果你在很久以前就从事了Java开发,或者参与过一些"不太正规"的项目,一定经历有过"jar包随便拷、jar包满天飞"的经历。在这种情况下,每次上线发布、升级jar包都是非常痛苦的事情。
大概从2003年开始,构建工具逐渐走入Java开发者的视野,Maven是构建工具中应用最为广泛的工具之一,它在提供构建功能的同时,也自带了强大的依赖管理功能。采用maven后,我们只需要定义xml就可以自动下载依赖的jar包,而不需要"手动将jar包拷来拷去"。
近几年来,作为一种更高效的构建工具 - Gradle - 逐渐崛起,在一些开发领域(如Android),Gradle已经完全取代了Maven成为事实上的构建标准。
尽管从构建工具的角度而言,Maven的地位有所下降,但它在Java依赖管理子领域的地位却不容撼动。Gradle默认也是直接采取Maven的依赖管理框架(只不过换为更简便的描述语言)。
在[架构概览](architecture/README.md)一章中,我们已经说明:在选型上,我们使用Gradle作为构建工具,但依然采用Maven来管理依赖。
在Maven的依赖管理方面,我们将使用Nexus搭建私有Maven服务器。为什么要搭建私服呢?
这和为什么搭建私有Git服务器却不用GitHub公开仓库是一个道理:没有公司愿意将自己的代码暴露给全世界:-)
## Nexus仓库的基本配置
与前两节类似,我们首先在Kubernetes上部署Nexus服务。
创建之前,先在物理机上创建Volume挂载点:
```shell
minikube ssh
$sudo mkdir /data/nexus
$sudo chmod -R 777 /data/nexus/
```
这里因为nexus需要有一个文件锁,默认权限是不够的,我们给了777权限,如果你觉得过于宽松,可以自行更改Kubernetes的启动用户,并设定相应权限。
看一下部署文件, nexus-deployment.yaml:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nexus-deployment
spec:
selector:
matchLabels:
app: nexus
replicas: 1
template:
metadata:
labels:
app: nexus
spec:
restartPolicy: Always
nodeSelector:
kubernetes.io/hostname: minikube
containers:
- name: nexus-ct
image: sonatype/nexus:2.14.8
ports:
- containerPort: 8081
hostPort: 8081
volumeMounts:
- mountPath: "/sonatype-work"
name: volume
volumes:
- name: volume
hostPath:
path: /data/nexus/
```
部署一下:
```shell
kubectl apply -f ./nexus-deployment.yaml
```
一切顺利的话,稍等一会访问"http://192.168.99.100:8081/nexus",部署成功,如下图所示:

## Nexus接入LDAP帐号
服务虽然好了,但还没有接入LDAP帐号系统,Nexus中接入LDAP帐号较为繁琐,请耐心操作完。
首先,设置一下LDAP的连接配置。
1. 使用默认管理员帐号登录,用户名admin,密码admin123
1. 点击左侧菜单"Security" -> "LDAP Configuration"
1. 设置LDAP配置如下
1. Protocol: ldap
1. Hostname: 192.168.99.100
1. SearchBase: dc=coder4,dc=com
1. Authentication Method: Simple Authentication
1. Username: cn=guest,dc=coder4,dc=com
1. Password: guest123
1. Base DN: ou=users
1. Object Class: inetOrgPerson
1. User ID Attribute: cn
1. Real Name Attribute: sn
1. E-Mail Attribute: mail
1. Group Element Mapping: 不选中
上述配置稍显繁琐,请耐心完成。都配置完成后,点击"Save"。
此外,点击底部的"Check User Mapping",如果一切配置正确,可以展示所有的列表,如下图所示。

第二步,下面我们来更改默认认证方式为LDAP。
点击左侧菜单"Administration" -> "Server",进行如下配置:
1. 在Security Settings中,将右侧的"OSS LDAP Authentication Realm"加入到左边,并将其拖动到最顶部。
1. 取消勾选"Anoymous Access"。

配置可以参考上图,设置好后,点击"Save"。
最后,我们需要对所有用户配置权限,注意,每次LDAP新接入用户后,都要执行下述操作。
点击左侧菜单"Security" -> "Users" ,执行下述操作:
1. 点击"All Configured Users"旁边的小箭头,选择"LDAP"
1. 点击"Refresh",此时就能拿到所有LDAP中的用户了。
1. 选中一个要操作的用户,例如"lihy",选择底部的"Config",然后"Role Management"。
1. 一般用户给"Nexus Deployment Role"就可以了,管理员可以给"Nexus Administrator Role"。
1. 设置好后点击"Save"

一个配置好的结果如上图所示。
至此,我们已经成功接入了LDAP,试着用配置好的用户登录下,发现可以登录成功。
## 配置Nexus中央仓库的缓存
Maven依赖仓库也是分布式,我们最长用的,是"Maven Central"这个中央仓库。
我们建议将中央仓库的索引缓存到Nexus私服上,这大约需要20GB的空间。
使用管理员帐号登录后,点击左侧菜单"View/Repositories":
1. 选择"Repositories"
1. 在右侧选择"Central"这个仓库
1. 底部"Configuration"配置
1. Remote Storage Location: http://maven.aliyun.com/nexus/content/repositories/central/ (这里我们使用了阿里云的国内镜像以加快速度)
1. Download Remote Indexes: True
1. 最后点击底部的"Save"

缓存的时间比较长,在我的虚拟机上,花费了20分钟。进度可以在这里查看,左侧菜单"Administration" -> "Logging" 选择Log, 可以看到目前还在缓存:
```shell
2018-05-28 08:40:19,792+0000 INFO [pxpool-1-thread-1] admin org.sonatype.nexus.index.DefaultIndexerManager - Trying to get remote index for repository "Central" [id=central]
```
等缓存成功后,在本地仓库的"Browse Index中",应当能看到与中央仓库一样的目录结构,如下图所示:

至此,我们成功架设了基于Nexus的Maven私有仓库,集成了LDAP登录,并缓存了Maven中央仓库。
## 如何在Gradle中应用私有仓库
在配置了私有仓库后,我们还需要在微服务项目中启用这个私有仓库。
这大致需要2步
1. 配置maven私有仓库用户名和密码
1. build.gradle中配置
下面我们分别看一下
## 配置Maven私有仓库用户名和米按摩
```shell
vim ~/.m2/settings.xml
# 新增如下内容
nexus_coder4
lihy
pass
nexus_coder4
central
http://192.168.99.100:8081/content/groups/public
nexus_coder4
central
http://192.168.99.100:8081/content/groups/public
true
true
central
http://192.168.99.100:8081/content/groups/public
true
true
nexus_coder4
```
如上,我们新增了私有仓库的地址、用户配置,如果你觉得在文件中直接"裸写"密码不安全,可以参考[maven密码加密方法](https://maven.apache.org/guides/mini/guide-encryption.html#How_to_encrypt_server_passwords)。
下面,我们在build.gradle中配置:
```build.gradle
buildscript {
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
maven { url 'https://jitpack.io' }
}
dependencies {
// version just for plugin, not important
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.6.RELEASE")
}
}
subprojects {
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'maven'
apply plugin: 'org.springframework.boot'
sourceCompatibility = 1.8
targetCompatibility = 1.8
group = 'com.coder4.lmsia'
version = '0.0.1'
repositories {
maven {
credentials {
username "$mavenUser"
password "$mavenPass"
}
url 'http://192.168.99.100:8081/nexus/content/groups/public'
}
mavenLocal()
}
// maven deploy config start
configurations {
deployerJars
}
uploadArchives {
repositories.mavenDeployer {
configuration = configurations.deployerJars
repository(url: "http://192.168.99.100:8081/nexus/content/repositories/releases/") {
authentication(userName: "$mavenUser", password: "$mavenPass")
}
snapshotRepository(url: "http://192.168.99.100:8081/nexus/content/repositories/snapshots/") {
authentication(userName: "$mavenUser", password: "$mavenPass")
}
}
}
// maven deploy config end
}
```
如上,build.gradle主要进行如下配置:
* 子项目的仓库,采用私有仓库
* 子项目发布包时,也发布到私有仓库上
至此,我们成功地将maven私有仓库应用到了gradle的微服务上。
================================================
FILE: legacy/toolchain/spring-boot-scripts.md
================================================
# 懒人脚本
================================================
FILE: legacy/toolchain/spring-boot-template.md
================================================
# Spring Boot 项目模板
================================================
FILE: legacy/toolchain/stress-test.md
================================================
# 打压工具
当业务刚刚起步的时候,微服务的稳定性是我们的首要保障目标,即服务能否稳定运行而不会挂掉。
随着业务逐渐发展,用户数据量不断增大,并发的请求数也会不断加大。慢慢地,性能问题也会逐渐暴露出来。
典型的性能问题有:
1. 服务响应变慢
1. 并发请求过多,导致数据库连接打满,无法访问数据库
1. 流量过大,带宽被打满
想要解决性能问题,先要能客观的评价性能,例如:在100个并发用户的前提下,我们的服务每秒能处理多少请求?
要评估这类性能问题,就需要做一些性能压测。
性能打压工具可以大致分为两类:
1. 在UI界面配置完成打压,如JMeter、Tsung等
1. 需要写代码完成打压,如Gatling、Locust等
对于JMeter等工具,虽然上手简单,但是可定制程度较低,一些复杂的规则和参数配置起来很繁琐,一般由测试人员做简单打压时使用。
而Locust等工具,虽然需要写代码,但了都提供了简化的API,编写起来非常简单,而且可以适应复杂的业务需求。
综上所述,我们选用代码类打压工具。
Gatling性能很好,但只支持Scala语言;Locust是Python语言开发的,可以支持多种编程语言。在本书中,我们将分别介绍这两款打压工具。
## 暴露服务端口
在介绍打压工具前,我们先要对服务进行一些变更,让服务能够从集群外访问到。
在[Spring Boot整合REST服务](spring-boot-1/sb-rest.md)章节中,我们配置了基于Kubernetes的REST服务,并设置了虚拟IP、虚拟IP的8080端口负责多结点的负载均衡。但是,虚拟IP默认只在集群内部生效。
当我们需要将Kubernetes服务暴露给集群外时,一般有如下选择:
* 为Service添加NodePort
* 为Service添加ClusterIP
* 增加Nginx反向代理,并为Nginx添加上述外部暴露的端口
在这里,我们采用第一种方式,如果你想了解其他方式,可以参考[Publising Service](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types)
看一下更新的service描述文件
```yaml
apiVersion: v1
kind: Service
metadata:
name: lmsia-abc-server-service
spec:
selector:
app: lmsia-abc-server
type: NodePort
ports:
- name: http
protocol: TCP
port: 8080
nodePort: 30888
- name: rpc
protocol: TCP
port: 3000
nodePort: 30999
```
与之前的文件相比,上述yaml描述主要是增加了NodePort的定义和描述。
* http端口对外暴露的是30888
* rpc端口对外暴露的是30999
我们应用下配置变更:
```shell
kubectl apply -f lmsia-abc-server-service-node-port.yaml
```
然后尝试访问,可以成功访问:
```shell
curl http://192.168.99.100:30888/lmsia-abc/api/
Hello, REST
```
## Locust打压工具
在你的开发机上安装
```shell
pip install locustio
```
下面我们来看打压脚本hello.py:
```python
from locust import HttpLocust, TaskSet, task
import resource
resource.setrlimit(resource.RLIMIT_NOFILE, (999999, 999999))
print resource.getrlimit(resource.RLIMIT_NOFILE)
class TestSet(TaskSet):
@task(1)
def hello(self):
self.client.get("/lmsia-abc/api/")
class WebsiteUser(HttpLocust):
task_set = TestSet
min_wait = 5000
max_wait = 9000
```
上面的代码非常简单,就是访问地址"/lmsia-abc/api/"。
下面来运行打压工具
```
locust -f hello.py --host=http://192.168.99.100:30888
```
启动后,访问localhost:8089,会进入如下界面:

上面是设置最终多少并发,下面是设置用户增长的速度(每秒新增多少)。
我们这里分别设置1000和200,然后点击开始。
之后会进入打压进度页面,如下图所示:

点击"Charts",可以看到随着用户数变化,响应时间、QPS的变化曲线,如下图:

打压结束后,点击"STOP"即可。
除了单击打压外,Locust还支持分布式打压,即可以启动若干个节点共同完成打压作业,具体可以参考官方文档[Running Locust distributed](https://docs.locust.io/en/stable/running-locust-distributed.html)。
## Gatling打压工具
首先,到[官网](https://gatling.io/download/)下载最新版的gatling:
```shell
https://repo1.maven.org/maven2/io/gatling/highcharts/gatling-charts-highcharts-bundle/2.3.1/gatling-charts-highcharts-bundle-2.3.1-bundle.zip
```
然后解压缩到本地路径:
```shell
unzip gatling-charts-highcharts-bundle-2.3.1.zip
mv gatling-charts-highcharts-bundle-2.3.1 gatling
```
然后看一下打压脚本:
```scala
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class HelloSimulation extends Simulation {
val httpConf = http
.baseURL("http://192.168.99.100:30888")
val scn = scenario("HelloSimulation").during(30) {
exec(http("hello_1")
.get("/lmsia-abc/api/"))
}
setUp(
scn.inject(atOnceUsers(2000))
).protocols(httpConf)
}
```
如上,Gating的打压脚本稍微复杂一些:
* 服务根地址192.168.99.100:30888
* 访问的get请求"/lmsia-abc/api/"
* 并发2000个用户
执行一下打压:
```shell
gatling.sh -sf . -s HelloSimulation
```
结果中可以直接看到各项统计结果:
```shell
---- Global Information --------------------------------------------------------
> request count 353505 (OK=353505 KO=0 )
> min response time 0 (OK=0 KO=- )
> max response time 1644 (OK=1644 KO=- )
> mean response time 170 (OK=170 KO=- )
> std deviation 122 (OK=122 KO=- )
> response time 50th percentile 143 (OK=143 KO=- )
> response time 75th percentile 219 (OK=219 KO=- )
> response time 95th percentile 408 (OK=408 KO=- )
> response time 99th percentile 606 (OK=606 KO=- )
> mean requests/sec 11047.031 (OK=11047.031 KO=- )
---- Response Time Distribution ------------------------------------------------
> t < 800 ms 353348 (100%)
> 800 ms < t < 1200 ms 148 ( 0%)
> t > 1200 ms 9 ( 0%)
> failed 0 ( 0%)
================================================================================
Reports generated in 2s.
```
Gating的打压工具功能更为强大,具体可以参考官方教程[Gatling UserGuides](https://gatling.io/docs/2.3/)
================================================
FILE: src/README.md
================================================
# 从0到1实战微服务架构(第2版)
## 地址汇总
* [Github项目 求Star:-)](https://github.com/liheyuan/hands-on-microservices)
* [在线阅读](https://coder4.com/homs_online/)
## 第2版前言
自从本书发布了后,技术圈发生了许多变化:
* Spring Boot 2.X 稳定版发布
* Kubernetes下的包管理项目“Helm”,正式加入CNCF基金会
* 阿里巴巴开源了Nacos服务发现项目
* ......
3年后的2021年,我正式开启了本书2.0版的写作计划。
由于技术更新迭代频繁,这是一次完全的重写,不是修订。
由于gitbook项目已不再维护,我改用[mdBook](https://github.com/rust-lang/mdBook)做为渲染工具,[MarkText](https://github.com/marktext/marktext)做为写作工具。
写作水平有限,还请各位多提宝贵意见。
## 第1版前言
微服务是继SOA后,最流行的服务架构风格之一。
按照微服务对系统进行拆分后,每个服务的业务逻辑都更加简单、清晰。服务之间是松耦合的,模块之间的边界也更加清晰。
微服务有效降低了软件项目的业务复杂程度,为小团队独立开发、持续交付和部署打下了良好的基础。
遗憾的是,微服务并不是银弹。与传统的单一架构相比,微服务架构对团队的组织架构、技术水平、运维能力等方面,都提出了更高的要求。如果没有掌握得当的方法而生搬硬套,微服务架构只会会适得其反--降低项目的开发效率,这是本书的创作初衷之一。
在国内外的技术社区中,比较推崇现有开源方案,如"Spring Cloud全家桶"或者阿里开源的"Dubbo"。
上述框架通常已经实现了服务发现、配置、负载均衡、限流熔断,等微服务架构所必须的的核心功能。
使用开源框架省却了造轮子的过程,但也降低了我们学习、思考的动力。
为什么需要服务发现,又如何实现它呢?配置中心呢....思考和设计的过程充满了挑战,也是提升自身架构能力的一种手段。这是本书的创作初衷之二。
已有的微服务资料过于重视微服务的开发,忽略了微服务赖以生存的生态系统:工具链、自动化运维。可以说,离开了这两点的支持,微服务架构将难以落地。完善这两方面的思考和实战,是本书的创作初衷之三。
为此,我撰写了这本《从0到1实战微服务架构》。让我们"暂时忘掉"已有的、成熟的开源解决方案。尝试亲自动手,实现微服务架构的各个模块。
我们会从微服务开发、工具链、运维这三个角度,阐述微服务架构的实战方案。
如果本书帮助了你,欢迎在在[github](https://github.com/liheyuan/hands-on-microservices)加Star,但严禁用于商业用途!(参见本页底部版权声明)
由于能力水平所限,本书难免存在各种错误,恳请各位进行指正(Issue or PR),谢谢!
## 读者基础
由于篇幅、精力所限,本书无法写成一本”零起点”教程。我假设读者具有至少2年的服务端工作经验,并且了解以下技术或原理:
* Git
* Maven & Gradle
* Docker & Kubernetes
* Java
* Spring / Spring Boot
* 数据库: 如MySQL
* 消息队列: 如RabbitMQ
* 缓存系统: 如Memcached
* 内存数据库: 如Redis
本书可以供架构师、项目经理、高级服务端程序员参考、学习。
动手实战是本书的核心内容,因此本书所涉及的全部代码,都托管到了我的[Github上](https://github.com/liheyuan)(以lmsia-开头的项目)。
这些代码以研讨为主要目的,也可以直接应用于生产,但本人不对其稳定性负责。
## 版权
本书虽然在github上公开写作,但版权归本人[Coder4](https://coder4.com)所有。
依照 [署名-非商业性使用-相同方式共享](https://creativecommons.org/licenses/by-nc-sa/2.5/cn/) ,任何人可以在保留署名的情况下转载。但严禁用于商业用途。
This is a book powered by [mdBook](https://github.com/rust-lang/mdBook).
================================================
FILE: src/SUMMARY.md
================================================
# [从0到1实战微服务架构](./README.md)
- [前言](./README.md)
- [微服务概述](./ch01-architecture/micro-service-intro.md)
- [微服务研发工具链](./ch01-architecture/rd-ops-toolchain.md)
- [持续集成、持续部署、持续交付](./ch01-architecture/continuous-x.md)
- [一种微服务的分层架构](./ch01-architecture/ms-architecture.md)
- [一种微服务分层架构的技术栈选型](./ch01-architecture/ms-tech-stack.md)
- [微服务开发上篇](./ch02-ms-dev1/README.md)
- [Gradle构建工具配置](./ch02-ms-dev1/gradle.md)
- [Sprint Boot项目与Gradle的集成](./ch02-ms-dev1/spring-boot.md)
- [Spring Boot集成SQL数据库1](./ch02-ms-dev1/database1.md)
- [Spring Boot集成SQL数据库2](./ch02-ms-dev1/database2.md)
- [Spring Boot集成gRPC框架](./ch02-ms-dev1/rpc.md)
- [Spring Boot集成Redis](./ch02-ms-dev1/redis.md)
- [微服务开发中篇](./ch03-ms-dev2/README.md)
- [Nacos注册中心:注册篇](./ch03-ms-dev2/registry1.md)
- [Nacos注册中心:发现篇](./ch03-ms-dev2/registry2.md)
- [Spring Boot集成配置中心](./ch03-ms-dev2/config.md)
- [Spring Boot集成熔断、限流、降级](./ch03-ms-dev2/circuit-breaker-and-limiter.md)
- [Spring Boot集成消息队列](./ch03-ms-dev2/mq.md)
- [微服务开发下篇](./ch04-ms-dev3/README.md)
- [基于ELKFK打造日志平台](./ch04-ms-dev3/elkfk.md)
- [基于SkyWalking的链路追踪系统](./ch04-ms-dev3/skywalking.md)
- [基于MicroMeter实现自定义应用监控指标](./ch04-ms-dev3/micrometer.md)
- [基于VictoriaMetrics + Grafana的监控系统](./ch04-ms-dev3/victorialmetrics.md)
- [容器与编排系统](./ch05-k8s/README.md)
- [从集装箱到容器](./ch05-k8s/container.md)
- [快速入门Kubernetes](./ch05-k8s/k8s-101.md)
- [搭建Kubernetes集群](./ch05-k8s/k8s-cluster.md)
- [搭建Kubernetes高可用集群](./ch05-k8s/k8s-ha-cluster.md)
- [通过ingress暴露内部服务](./ch05-k8s/k8s-ingress.md)
- [持续交付流水线](./ch06-cd/README.md)
- [Jenkins搭建入门](./ch06-cd/jenkins.md)
- [Jenkins定制Agent](./ch06-cd/jenkins-custom.md)
- [Jenkins实现Kubernetes部署流水](./ch06-cd/jenkins-k8s.md)
- [Jenkins优化Kubernetes部署流水线](./ch06-cd/jenkins-k8s-optimize.md)
- [工具链](./ch07-tools/README.md)
- [基于LDAP的内网统一认证](./ch06-cd/jenkins.md)
- [JFrog Artifactory搭建Maven私有仓库](./ch07-tools/ldap.md)
- [使用Registry2搭建Docker私有仓库](./ch07-tools/registry2.md)
================================================
FILE: src/ch01-architecture/README.md
================================================
# 第1章 微服务架构概述
当我们讨论服务端的架构时,“微服务”已经成为了最热门的关键字。
如果没有接触过"微服务",那么你的心里一定存在很多号?
不要急,我们将从三个基本问题谈起:
- 什么是“微”服务?
- 为什么需要微服务?
- 微服务是“银弹”么?
接着,我们将沿着:
- 研发工具链
- 微服务架构
两个线索展开,最后将讨论技术选型。
让我们开始“微”服务的探索之旅吧:-)
================================================
FILE: src/ch01-architecture/continuous-x.md
================================================
# 持续集成、持续部署、持续交付
标题里的三个“持续”在前几年特别火热,属于技术热词(BuzzWord)。
持续交付(Continuous Delivery)由马丁·福勒(Martin Fowler)于2006年提出。
是的,你没看错,又是马丁·福勒,那位提出微服务的大神。
歪个楼,介绍一些马丁·福勒的代表作:
- 《重构:改善既有代码的设计》
- 《企业应用架构模式》
- 《敏捷软件开发宣言》(联合)
- “微服务”、“持续部署” ....
以上任何一条单独拿出来,都足以封神。
言归正传,我们在一本“微服务”的书中讨论持续交付,仅仅因为它是由大神提出的么?
当然不是,我们将在本文的末尾再讨论这个问题。
[这篇](https://www.mindtheproduct.com/what-the-hell-are-ci-cd-and-devops-a-cheatsheet-for-the-rest-of-us/)文章很好的阐述了三个概念的联系与区别,我们展开讨论。
## 持续集成
小王每次向gitlab提交一个代码,就会触发一次项目的自动构建、运行单元测试,这就是持续集成(Continuous Integration)。如下图所示:

假设小王在提交中引入了一个Bug,借助CI流程(中的集成 or 单元测试),我们就能在第一时间发现,并尽早修复问题。
管理学大师戴明指出:“问题发现的越早,修复的成本越低”。通过持续集成,我们可以尽早发现问题,从而降低(修复问题带来的)返工成本。
## 持续部署
持续部署(Continuous Deployment)指的是:在持续集成(成功)的基础上,自动将服务部署到"类似于线上"的环境中,如下图所示:

为什么要部署到"类似于线上环境"呢?因为代码只在"集成阶段"通过了一部分"单元测试",假设单元测试覆盖不全,甚至还需要人工测试,那就可能将隐含的Bug发布到线上,造成生产事故。
图中画的"TEST"(测试环境)、"STAGING"(预发环境),都是这类"类似线上环境"。当新版本通过最终确认后,再手动(MANUAL)部署到线上。
## 持续交付
持续交付(Continuous Delivery)在持续部署的基础上,更近了一步:成功发布到"类似生产环境"后,会继续自动发布到线上,如下图所示:

显然,这种"自动发布"需要极强的自信和勇气。这可能源于充分的单元测试,清晰的架构,以及对业务能力的自信。
实际上业界只有极少数公司"从容地"实现了上述意义上的"持续交付"。
其余宣称实现了"持续交付"的公司,或者混淆了持续部署的概念,或者对技术故障存在较大容忍度。 (先发布再灰度,难道不是一种容忍?)
这并不是高级黑,如果你认真做过一段时间软件开发,应该能明白“即使100%的单测覆盖率,也不能自动检查出尚未发现的Bug”,更何况绝大多数项目根本无法达到100%单元覆盖率。
我们回到本文开头的问题:为什么要在一本“微服务”的书中,讨论持续部署?
还记得微[服务概述](./micro-service-intro.md)一节中,微服务的缺点么?可靠性陷阱、运维复杂度升高。
- 借助持续集成,能够尽早发现缺陷,提升微服务架构下的可靠性。
- 应用持续部署,可以上线效率,降低运维难度。
由此可见,持续集成、持续部署,能够切实解决微服务中存在的问题。我们将在本书的后续章节,打造自己的持续集成系统,敬请期待。
================================================
FILE: src/ch01-architecture/micro-service-intro.md
================================================
# 微服务概述
## 什么是“微”服务?
如果你仔细观察,会发现我在上一行的标题中,将“微”打了个引号。
如果我们暂时去掉这个''微"字理解,微服务就是我们熟知的“服务端” 或者 “后端”。
现在让我们把微字加回来:-)
"微服务"(Microservices)由马丁·福勒(Martin Fowler)提出的一种架构理念,[原文]([Microservices](https://martinfowler.com/articles/microservices.html))发表于2014年。
> 微服务是一种架构模式或者说是一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。
我们抓三个关键点来理解:
- 单一应用划分为一组更小的服务:将一个较大的、复杂的应用,拆分为多个小的服务。你可能会问:“这样不会增加复杂度么”?是的,会增加。但这种拆分也会带来明显的优点,我们后面会提到。
- 独立的进程:每个微服务独立运行在自己的进程中,互不干扰。虽然这里并没有限制进程的部署方式,但可以想见,经过"划分"后的微服务,势必会产生众多进程。微服务是拆分而来的,他们之间势必存在逻辑的耦合。由此,会产生新的问题"微服务间的通信"。
- 相互协调、配合:微服务的进程间需要通信、交互。从理论而言,所有IPC(Inter-Process Communacation,进程间通信)的方式都可以完成这个过程。但微服务的进程众多,很难完整地部署在同一台机器上,这势必产生跨主机的网络通信。所以,在微服务中,多采用RPC(Reomote Procedure Call,远程过程调用)的方式来完成通信。

上图展示了单体服务 和 微服务的区别。
## 为什么需要微服务?
在前文中,我们挖了一个坑:'微服务的划分会导致复杂度上升',为什么还要使用一项有缺陷的技术呢?
我们先讲第一个故事。
小张入职了一家互联网创业公司,一开始只有3个后端程序员,每天的工作是:和产品经理讨(si)论(bi)需求、写代(b)码(ug),改Bug,工作紧张但规律。服务端的上线窗口是周五下午:合并分支、代码Review、推送线上,一气呵成,不仅能准点下班,还能去吃个火锅。
过了两个月,行业赶上了风口,公司的业务快速发展,后端团队也快速膨胀到20人。然而,麻烦也接踵而至:大家修改的是同一个仓库下的服务端代码,"解冲突"成为了家常便饭,还发生了几次"一个小修改,破坏了其他业务主流程“的严重线上事故。
为了改善这种情况,老板招聘了2位QA(质量保证,测试)人员,由他们负责测试工作。然而,一个很小的改动都需要对整个后端服务的case进行全量回归测试。一个功能的开发需要1天,测试却耗费1周,迫于老板的压力,研发同学只能安慰自己:XX功能简单,不需要测试了,直接上线。
最终,周五成为了"噩梦日":周四晚上要提前开一个Excel表、统计好第二天要上线的需求,并按优先级排定顺序。周五全员提前1小时来公司,开始逐个逐个需求的"合并代码"”、"解冲突",“上线”、"观察" 、“回滚”、“修改代码”......
上线结束的收工时间从6点变成了9点,又逐渐拖到了11点,最后索性全员加班、通宵上线。技术团队的每一位同学,都感到身心俱疲。
听完这个故事,你是否有"似曾相识"的感觉?
科普一下,上述故事中的服务一般称作“单体服务” 或者 “巨石服务”(Monoliths)。
接下来,是第二个故事。
由于工作强度大、线上故障频发、团队士气低落,老板请来了老刘担任技术经理。
第一周:老刘带领团队将复杂、臃肿的"巨型服务"拆分成了“用户”、“订单”、“服务”三个微服务。
第二周:老刘将团队进行了上述类似的拆分,也分成了三个小组。
第三周:事情有了微妙的变化。分组后,合并代码引发的冲突减少了。开发业务时,多数的改动都封闭在单独的微服务内,改动造成的影响范围减少了,测试周期缩短了。
......
三个月后的一个周五的下午,(得益于提高的交付质量,以及微服务的独立并行上线),团队提前2小时完成了上线,距离上一次故障通报已经过去了两个月。
研发讨论群里,小张发了一条消息:“今天居然可以正点下班了,老刘真厉害!”
老刘回复:这是大家的努力的结果,真正“厉害”的应该是“微服务”。
听完这两个故事,我们来总结下微服务架构的两个优点:)
- 逻辑清晰:一个微服务只负责一项(或少数几项)很明确的业务,逻辑更加简介清晰,易于理解。
- 独立自治:每个微服务由一个小组负责。减少了跨团队的代码冲突,同时降低了改动的影响范围,提高了研发效率。
在故事之外,微服务架构还具有以下的优点:
- 伸缩性强:相对于庞大的巨石服务,微服务更加独立,可以针对不同的性能需求,有选择的对不同微服务进行伸缩。举个栗子:明天有大促,产品预测:注册功能提升10倍,其他功能无波动。针对巨石服务,我们只能整体扩容10倍;微服务架构下,我们只需要10倍扩容用户微服务。
- 技术异构性:每个微服务内可以使用不同的技术栈,甚至不同的开发语言。只要微服务之间使用统一的通信方式即可。
微服务架构有很多优势,那团队抓紧上马微服务吧?
## 微服务是“银弹”么?
直接泼一盆冷水:
> There is no Silver Bullet. -- 《人月神话》
微服务不是“银弹”,它存在以下缺点:
- 复杂度升高:在巨石服务中,所有修改都集中在同一个项目内;在微服务架构下,复杂功能的开发,需要同步修改多个微服务,复杂度骤然升高。
- 性能损耗:原本在巨石服务中的方法调用,演变为微服务之间的跨进程、网络通信。性能会受到较大影响。
- 可靠性陷阱:假设每个服务的可靠性都是99%,一个巨石服务,可靠性是99%、三个微服务的可靠性会下降到99% x 99% x 99% = 97%。
- 运维难度加大:巨石服务被拆分成N个微服务,部署的数量翻倍的增长。此外,多组微服务的运行,也会增大运维、监控的难度。
有意思的是:"拆分"带来了优点,也引入了缺点。
> 夫尺有所短,寸有所长,物有所不足,智有所不明。 -- 《楚辞.屈原.卜居》
微服务架构也是如此,它的优缺点并存。
## 微服务适用什么场景?
什么场景适用微服务,什么场景不适用呢?
这篇文章[《When to use and not use microservices》]([Best of 2020: When To Use - and Not To Use - Microservices - Container Journal](https://containerjournal.com/topics/container-ecosystems/when-to-use-and-not-to-use-microservices/))给出了一些建议:
适用微服务架构的场景:
- 希望巨石服务能适应“可扩展性”、“敏捷性”、“可管理性”,提升交付速度时
- 需要为(使用陈旧技术开发的)的老系统,迭代新功能时
- 有一些相对独立的模块可以跨业务复用时:如登录、检索、身份验证等。
- 构建需要快速交付、创新度高、敏捷的应用 / 服务
不适用微服务架构的场景:
- 业务简单,无需处理复杂问题
- 团队规模太小,尚无法负担微服务拆分带来的复杂度提升
- 为了微服务而微服务
最后,引用马丁·福勒(Martin Fowler)论文的结尾做结束本节的讨论。
> 我们怀着谨慎、乐观的态度写了这篇文章。到目前为止,我们已经看到:微服务风格是一条非常值得探索的路。我们不能肯定地说,我们将在哪里结束,但软件开发的挑战之一是,你只能基于目前能拿到手的、不完善的信息作出决定。
================================================
FILE: src/ch01-architecture/ms-arch.plantuml
================================================
@startuml
title "微服务架构实现"
package "聚合接入层" as l5 {
[聚合服务1] as n51
[聚合服务2] as n52
[聚合服务3] as n53
[PaddingPadding] as n54
hide n54
[n51] -[hidden]right-> [n52]
[n52] -[hidden]right-> [n53]
[n53] -[hidden]right-> [n54]
}
package "业务服务层" as l4 {
[微服务1 ] as n41
[微服务2] as n42
[微服务3] as n43
[微服务4] as n44
[Pa] as n45
hide n45
[n41] -[hidden]right-> [n42]
[n42] -[hidden]right-> [n43]
[n43] -[hidden]right-> [n44]
[n44] -[hidden]right-> [n45]
}
package "微服务设施层" as l3 {
[开发框架] as n31
[RPC] as n32
[服务注册发现] as n33
[配置中心] as n34
[ ] as n35
hide n35
[熔断/限流] as n36
[数据库] as n37
[NoSQL] as n38
[消息队列] as n39
[链路追踪] as n3a
[监控] as n3b
[报警] as n3c
[日志] as n3d
[n31] -[hidden]right-> [n32]
[n32] -[hidden]right-> [n33]
[n33] -[hidden]right-> [n34]
[n34] -[hidden]right-> [n35]
[n31] -[hidden]down-> [n36]
[n36] -[hidden]right-> [n37]
[n37] -[hidden]right-> [n38]
[n38] -[hidden]right-> [n39]
[n36] -[hidden]down-> [n3a]
[n3a] -[hidden]right-> [n3b]
[n3b] -[hidden]right-> [n3c]
[n3c] -[hidden]right-> [n3d]
}
package "运维平台层" as l2 {
[CI/CD系统] as n21
[部署版本系统] as n22
[容器调度系统] as n23
[PaddingP] as n24
hide n24
n21 -[hidden]right-> n22
n22 -[hidden]right-> n23
n23 -[hidden]right-> n24
}
package "基础设施" as l1 {
[计 算 资 源] as n11
[存 储 资 源] as n12
[网 络 资 源] as n13
[PaddingPPddi] as n14
hide n14
n11 -[hidden]right-> n12
n12 -[hidden]right-> n13
n13 -[hidden]right-> n14
}
n51 -[hidden]down-> n41
n41 -[hidden]down-> n31
n3a -[hidden]down-> n21
n21 -[hidden]down-> n11
@enduml
================================================
FILE: src/ch01-architecture/ms-architecture.md
================================================
# 一种微服务的分层架构
在上一小节,我们讨论了微服务架构“的的特征、优缺点等话题。
你可能对微服务有了一个模糊的概念,依然感觉不够清晰。
这种感受能够理解。因为,微服务的理论只是提供了一种“架构风格”的建议,并不包含具体的实施方案。
下图展示了一种微服务的分层架构:

让我们自底向上、逐层分解:
1. 基础设施层
基础设施层涵盖了服务端运行时,所需要的物理资源。包括:计算资源、存储资源、网络资源等。
针对小型公司,可以直接选用云计算平台的资源(如阿里云、AWS等);中大型公司出于成本、审计等因素,会自建机房或混合云。
计算资源:CPU、GPU、内存等。除了CPU的核数、内存容量,配比等常见问题,还需要考虑计算资源的弹性伸缩能力,即如何应对“平台大促”等场景带来的流量提升。
存储资源:不仅要考虑磁盘容量,还要考虑磁盘性能([IOPS]([IOPS - 维基百科,自由的百科全书](https://zh.wikipedia.org/wiki/IOPS)))。举个例子:服务端日志主要是顺序写,异步处理 + 大容量机械磁盘就能满足要求;对MySQL等数据库场景,涉及大量随机读,使用SSD可以显著提升性能。
网络资源:外网带宽(峰值)、内网带宽、负载均衡、VPC等。内外网带宽问题较为常见,我们不再讨论。负载均衡:当业务流量规模升高后,接入层的传统软负载解决方案(Nginx、LVS)会显得力不从心。硬件负载均衡(F5)可以提供更高的性能,但做为专用计算的商业产品,其价格在百万以上。这几年,随着Kernel By Pass技术的兴起,基于X86通用硬件 + Linux的的软负载均衡也取得一定的性能突破,感兴趣的话,可以参考这篇[文章]([从Maglev到Vortex,揭秘100G+线速负载均衡的设计与实现-InfoQ](https://www.infoq.cn/article/Maglev-Vortex/))。
基础设施层的技能栈主要是:运维、网络建设,我们不在本书中做更多讨论。
2. 运维平台层
运维平台层是“[持续交付](continuous-x.md)”的重要载体,包括:
持续部署系统:构建从代码仓库、持续集成、持续部署的全链路系统、最终实现持续交付。
部署的版本管理系统:管理部署镜像粒度的版本,以支持滚动发布、回滚等部署功能。
容器、容器管理调度平台:容器是一种操作系统级的轻量虚拟化技术。在部署系统中,不仅需要容器技术、还需要容器调度管理系统。这两项技术我们会在后续章节展开讨论。
3. 微服务设施层
本层为微服务的开发和运行提供公用的设施基础。
在这里我们只做基本介绍,在后续章节会详细展开。
开发框架:微服务的开发需要一些基础的编程框架,可以自己从零搭建,也可以基于成熟开源框架完善。
RPC:微服务内部使用RPC(Remote Procedure Call)完成通信。
服务注册与发现:微服务A调用服务B且后者有3个实例,如何感知这3个实例的IP、端口,以及A要调用哪个实例呢?这就是服务的注册与发现问题,是微服务的核心问题之一。
配置中心:微服务的数量、实例众多,逐一修改配置文件的传统模式,既不经济又容易出错。配置中心是一个中央(但不一定是单机)配置系统,负责配置管理、修改等工作。
熔断:当微服务调链路上,服务不可用或响应时间太长时,触发熔断,快速提前返回。举个例子:家里有用电设备路时电流过大,空气开关会直接跳闸,防止造成进一步的破坏。
限流:为了保护服务不被流量击垮,而提前限制流量。举个例子:经过测算,故宫接待能力是每日1万人。那么当天超过1万后,就触发限流,不让更多游客入园。
数据库:传统的SQL数据库用于业务数据落盘,NoSQL数据库则用于缓存或高性能存取。
消息队列:将业务流量“削峰填谷”,对应对突发流量。
中间件:中间件是介于 服务端 与 数据库、消息队列等设施的中间。中间件帮助 业务服务更简单地使用这些基础设施。
近几年,“可观测性”成为了新的技术热词。这个舶来于控制理论的词,在软件系统中指的是:可以帮团队有效调试系统的工具或解决方案。以这个视角看,下述部分都是可观测性的一部分:
日志:如何在众多的微服务实例中,快速定位到某一种出错日志?日志平台实现了微服务实例中的日志收集、存储、检索、分析。
监控系统:通过采集多种指标,实时反馈系统运行状态,保证服务的平稳运行。举个生活中的例子:汽车驾驶位的仪表盘。
报警系统:当监控系统发现异常时,及时将报警发送出来。
链路追踪:当服务A->B->C调用链上发生超时,如何快速定位哪个环节发生了故障?链路追踪解决了分布式、复杂调用链路中的采集、追踪,分析工作。
4. 业务服务层
借助“基础设施“、”运维平台“、”微服务设施“的帮助,我们可以更高效、稳健的应用微服务,实现业务目标。关于微服务的拆分、建模理论,可以参考“领域驱动设计”的相关内容,本书不做讨论。
5. 聚合接入层
在“[微服务概述](micro-service-intro.md)”一节中,我们曾提到微服务的缺点之一:拆分导致的复杂度升高。在当前主流的前后端分离架构中,用户对这一拆分基本无感知。复杂度被转嫁到 前端 / 客户端 中:原本只需要调用一个接口,现在要分别调用N个微服务。还需要考虑时序关系、错误处理等。聚合接入层就是为解决这个问题而生的,他聚合多个微服务的调用,只保留必要字段,为前端 / 客户端提供了统一、清晰的服务接口。聚合接入层可以由服务端实现,有时还会加入部分熔断、限流等逻辑,组合成为微服务网关。聚合接入也可以由前端实现,有时也被称作BFF(Backend For Frontend)。
在剖析微服务的各层架构之后,不难发现:微服务的架构下,需要多个团队,多层系统、多纬度的支持。这也印证了在“[微服务概述](micro-service-intro.md)”一节中的观点:应用微服务架构,需要较高成本。
因此,尽量选用成熟、易维护的技术,从而尽可能降低成本,显得尤为重要。我们将在下一节展开讨论技术选型。
================================================
FILE: src/ch01-architecture/ms-tech-stack.md
================================================
# 一种微服务分层架构的技术栈选型
我们在[工具链](./rd-ops-toolchain.md)、[一种微服务的分层架构](./ms-architecture.md) 两小节中讨论了技术栈的需求。
在本节中,我们将具体讨论技术栈的选型。
你可能注意到,上一节的标题是“一种微服务的分层架构”,而这一节的标题是“一种微服务分层架构的技术栈选型”。
加上“一种”这个词是有意而为之,请不要怀疑我的语文水平:-)
"一种"强调的是:
- 微服务只是一种架构风格,他可以有N种不同的实现,上一节只介绍了其中一种。
- 每一种微服务架构的实现,也可以对应N种不同的技术栈选型。
那么,在这N^2种架构 + 技术栈的组合种,哪一种才是最好的?
不急着回答,我们先来看下这个:
> php is the best language for web programming.
这是PHP官方手册的原文,更多人更熟悉前5个单词,“PHP是全世界最好的语言”。
但加上后3个单词“for web programming”后,就变成了“PHP是web领域最好的语言”。
而我的观点(哪个架构更优) 与 PHP社区(关于语言优劣)的观点,是一致的:没有最好的语言(技术),只有最适合具体场景的。
因此,我们只会针对各项场景,列出技术选型,而不会打“为什么A比B好的”口水战。
## 容器管理平台的技术选型
微服务架构下会对服务进行拆分,产生大量的服务实例。
容器化技术,可以实现环境隔离、快速部署,是微服务架构的基石。
Docker凭借“快速”、“可移植性”等特性""一战成名",是单机或小规模应用部署的最佳选择
然而,在复杂的分布式部署场景中,"扩容"、"编排"、"故障恢复"等成为了"刚需",“容器管理平台”应运而生。在这个赛道上,曾经出现过三个主流产品:
- swarm: Docker公司于2014年末推出的容器集群技术方案。尽管swarm是Docker公司的“亲儿子”、手握大量社区资源,但很快被Kubernetes超越。
- Kubernetes: 简称k8s,支持自动部署,扩展和管理容器化应用程序的开源系统。k8s借鉴了Google的Borg管理系统,自问世以来发展迅猛,当前已经成为了容器管理的事实标准。
- Marathon: 构建在[Apache Mesos]([Apache Mesos](http://mesos.apache.org/))集群上的一套容器集群管理软件。由于Mesos的部署存在门槛,Marathon项目的关注度并不高,社区也并不活跃。其上一个发布版本依然停留在2019年,已经近2年没有更新。
因此,我们"毫无争议"地选择k8s作为微服务架构下的容器管理平台。
除了容器管理平台,我们还需要镜像仓库存储应用的容器镜像,我们将使用Docker搭建私有镜像仓库。
## 微服务设施层的技术选型
设施层涉及较多的技术需求,技术选型如下:
| 需求 | 选型 | 版本 |
| ---------------- | ------------------------- | -------------- |
| 开发语言 | Java | 8 |
| 开发框架 | Spring Boot | 2.5.4 |
| RPC | gRPC | 1.14.x |
| 服务注册 / 发现 / 配置中心 | Nacos | 2.x |
| 熔断 / 限流 | Resilience4j | 1.7.1 |
| SQL数据库 | MySQL | 8.0.X |
| 内存数据库 | Redis | 6.2 |
| 消息队列 | RocketMQ | 4.9.1 |
| 日志 | Kafka + ELK | 2.13 + 7.14.X |
| 监控 / 告警 | VictoriaMetrics + Grafana | 1.64.1 + 8.1.X |
| 链路追踪 | SkyWalking | 8.7.0 |
开发语言:我们选择了Java做为开发语言。与新近崛起的Go、Rust等语言相比,Java不是最完美的语言,但它依然拥有较高的开发、运行效率,最充足的人才供给。版本方面我们选择Java 8(最后一个免费的Java版本)。
开发框架:在Java开发领域,Spring生态的渗透率已超过60% ([出处]([Spring dominates the Java ecosystem with 60% using it for their main applications | Snyk](https://snyk.io/blog/spring-dominates-the-java-ecosystem-with-60-using-it-for-their-main-applications/)))。顺应这一趋势,我们选择Spring 生态内的Spring Boot做为主要开发框架。Spring Boot提供的注解配置、嵌入式容器、starter等特性,可以极大简化Java应用的开发。
RPC框架:我们选择开源的gRPC做为RPC框架,它使用Protocl Buffer序列化,HTTP 2传输协议,具有更灵活的通信模式和较高的传输效率。
服务注册、发现、配置中心:[Nacos]([什么是 Nacos](https://nacos.io/zh-cn/docs/what-is-nacos.html))是阿里巴巴开源的服务管理项目,同时具备服务注册、发现、配置中心。Nacos原生支持Spring Boot、k8s等融合方向。经过几年的发展,Nacos已经较为成熟,支撑了阿里巴巴、中国移动等数十家大型公司的线上系统。
熔断、限流:本书不会探讨Service Mesh等平台级别的流量控制方案。我们主要讨论服务进程级别的熔断、限流方案。老牌项目Hystrix停更后,我们选择开源的Resilience4j做为熔断、限流的Java库解决方案。
数据库:做为开源数据库的佼佼者,MySQL常年稳居市场份额的前三名。我们选择其较新的稳定版8.0.X。
内存数据库:做为SQL数据库的补充,内存数据库的应用场景是:吞吐量更大、延迟更低。高性能的Redis是最佳选择。根据[官方评测](https://redis.io/topics/benchmarks),Redis 6.x在开启pipeline模式的前提下,可以提供高达55万RPS。
消息队列:Apache RocketMQ是阿里巴巴的开源的分布式消息队列,具有极低的延迟和较高的吞吐量。相比于老牌的Kafka,Rocket MQ更适用于消息队列的场景。我们选用其最新稳定版4.9.1。
日志:ELK是经典的日志日志方案。在此基础上,我们前置增加了Kafka,利用其强大的写能力,构建起缓冲队列,以应对海量日志的突发写入。
监控 / 告警:纵观DevOps领域,Prometheus + Grafana已经成为了监控领域的事实标准。然而,Prometheus并不支持原生的集群部署,其在大规模应用下很容易出现瓶颈。[VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)是一款可以嵌入Prometheus的分布式时序存储引擎。起初VictoriaMetrics只想做一个引擎,在近几个版本社区加大了对vmagent的开发投入。vmagent是一款轻量级的代理,兼容Prometheus协议,可以直接替代Prometheus完成大部分工作。在本书中,我们直接选择VictoriaMetrics + Grafana做为兼容告警的默认技术栈。
链路追踪:[SkyWalking](https://skywalking.apache.org/)是由国人主导的一款开源APM(application performance management)。在小米、滴滴等公司都有应用。我们选择其最新的稳定版本。
看了上面的文字,你可能有点困惑:“只是简单罗列选型结果,并没有具体分析过程“?
技术选型是一个非常大的话题,每一个点单独拎出来,都能洋洋洒洒的写一章出来,但是我觉得必要性不大,原因在于:
- 技术演进的速度非常快,今天适合的明天就有可能被淘汰(看看Docker)
- 每个公司面临的具体场景情况都是不同的,很难穷尽、更无法全部都满足
因此,我只是在自己可见的技术水平内,选择了相对靠谱的方案,解决了一部分“选择障碍的问题”,如果你有更优秀的选择,也欢迎提Issue交流、讨论。
================================================
FILE: src/ch01-architecture/rd-ops-toolchain.md
================================================
## 微服务研发工具链
> 子曰:“工欲善其事,必先利其器。居是邦也,事其大夫之贤者,友其士之仁者。”
>
> -- 《论语》
普通话版:工匠想要做好工作,先要把工具打磨锋利。
程序员版:软件工程师要想写好代码,需要一把机械键盘,并定期清洗轴以维持最佳手感。
对于程序员而言,除了键盘等硬件,还有一系列软件。我们这里将这些软件称为工具链。
## 小王的一天
下面,让我们跟随小张 - 是的,就是在风口创业公司的那位 - 看看在微服务架构下,研发工具链包含了哪些内容。
| 时间 | 工作 | 工具需求 | 备注 |
| ----- | ------------------------------ | ------------------------------ | ---------------------- |
| 09:01 | 打开浏览器,登录公司内网 | 使用同一个账号,登录公司所有的内部系统。 | 暂不讨论“操作系统”、“浏览器”等通用软件。 |
| 09:03 | 打开代码审核平台,查看Review | 代码版本控制、代码托管,代码审核 | |
| 10:23 | 老张让我升级下xx的包,加了新接口 | 版本依赖管理系统 | 我们将开发语言暂时限定为Java |
| 11:56 | 修改了一部分逻辑,午饭前抓紧提交上去,看能否跑通所有Case | 持续集成(Continuous integration)系统 | 暂不讨论“IDE”等通用软件。 |
| 15:20 | 功能开发完毕,上线! | 持续交付(Continuous delivery)系统 | |
| 16:03 | X功能重构,拆分到两个微服务中 | 微服务开发辅助工具 | |
## 研发工具链
小张的公司还处于创业阶段,出于节省成本的考虑,我们尽量选择开(mian)源(fei)的解决方案:
1. 内部帐号统一管理:在企业的内部,存在许多内部系统。出于安全性、管理性的考虑,需要统一的帐号管理系统。这里我们选用[OpenLDAP](https://www.openldap.org/):一款的开源的帐号管理服务,它实现了广泛使用的“轻量级目录管理协议”(LDAP v3),可以轻松对接各类系统的帐号管理功能。
2. 代码管理:团队协作的软件开发模式,需要版本控制系统。我们选用了[Git](https://git-scm.com/)做为代码的版本控制系统。在代码的托管、审核方面,[Gerrit](https://www.gerritcodereview.com/)和[GitLab](https://about.gitlab.com/install/?version=ce)都是成熟的开源解决方案。Gitlab上手容易,生态链更加成熟;Gerrit有一定上手门槛,在代码Review方面更加优秀。关于两者的讨论,可以参考这篇[帖子](https://www.reddit.com/r/git/comments/8ekeem/do_you_use_gerrit_software/)。经过多方面的综合考虑,我们选择了GitLab。
3. 版本依赖系统:在Java开发中,[Maven](https://maven.apache.org/)是依赖管理的事实标准。同时在企业开发中,不希望将私有包发布到公开仓库中,我们选用[Nexus Repository OSS](https://www.sonatype.com/products/repository-oss)搭建私有的Maven仓库。
4. 持续集成、持续交付,持续部署是三个既相近又重要的概念,我们将在下一小节展开讨论。
5. 微服务辅助开发工具:在微服务架构下,新增微服务、升级pom版本,接口变更等操作会频繁发生。需要开发一些辅助工具,提升研发效率。我们会在后面展开讨论。
针对上述选择的工具,我们会在后续章节详细介绍。
## 微服务辅助开发工具
结合微服务的开发特点,我们需要这样一些辅助工具:
- 自动创建新的微服务:包括从模板项目生成微服务代码、自动创建git项目、部署项目
- RPC桩文件生成:在RPC的(IDL)接口文件变更后,需要重新生成桩文件,这个步骤较为繁琐,需要工具辅助完成。
- pom版本自动升级:微服务之间的版本依赖,更新会更加频繁,我们需要一个工具,自动修改pom版本
这里我们只初步讨论一下需求,具体的实现会在后续章节展开。
================================================
FILE: src/ch02-ms-dev1/README.md
================================================
# 微服务开发上篇:开发框架及其与RPC、数据库、Redis的集成
从这一章开始,我们正式进入微服务开发篇,共分上、中、下三篇。
本章我们将讨论开发框架,框架与RPC、数据库、Redis的集成。
2001年,我刚开始编程时,接触的第一个语言是"ASP"(没有.net),它通过脚本注解的方式,实现动态功能(存取数据库等),有点类似于PHP。在那个没有开发框架的年代,我们依然可以实现功能。但是这里只是“功能上的满足”,确无法做到“工程上的最优”,例如:
- HTML与脚本混编,无论是页面样式修改,还是逻辑修改都很麻烦(视图、逻辑混合)
- 有不少功能重复的代码,无法复用(如创建数据库连接)
- 页面之间的内部依赖难以处理(往往只能通过url / session参数传递)
开发框架的出现,解决了上述部分问题,以Spring为例:
- Spring MVC实现的分层架构,将页面、视图、逻辑层强制分离
- Spring JPA组件可以创建数据库模板,减少重复代码
- 通过IoC容器,可以清晰地分离逻辑、处理依赖
- ....
当然,引入开发框架会带来额外的学习成本。Spring Boot借鉴了ROR框架中“约定优于配置”的设计理念,进行了大量的改造,实现了框架的“开箱可用”,有效降低了学习成本。
本章会使用一个微服务为例,介绍Gradle + Spring Boot的基础集成。在此基础上,我们会介绍几个与框架紧密相关的内容:RPC框架、数据库、Redis的集成。
================================================
FILE: src/ch02-ms-dev1/database1.md
================================================
# Spring Boot集成SQL数据库1
从银行的交易数据到打车订单,衣食住行,都离不开数据库的存储。
在接下来的两个小节中,我们将通过3种不同的技术,在Spring Boot中集成MySQL数据库。
- JDBC
- MyBatis
- JPA (Hibernate)
本节的前半部分,我们将通过Docker快速搭建MySQL的环境,随后介绍JDBC的集成方式。
## 搭建MySQL实验环境
本书的重点是讨论微服务实战,我们直接使用Docker的方式,快速搭建实验环境。
如果你想部署在生产环境,请参考官方[部署文档](https://dev.mysql.com/doc/mysql-installation-excerpt/8.0/en/linux-installation.html)。
首先,请确认已经成功安装了Docker:
```shell
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
```
若尚未安装Docker,可以参考[官方文档]([Install Docker Engine | Docker Documentation](https://docs.docker.com/engine/install/))。
MySQL的Docker运行脚本如下:
```bash
#!/bin/bash
NAME="mysql"
PUID="1000"
PGID="1000"
VOLUME="$HOME/docker_data/mysql"
MYSQL_ROOT_PASS="123456"
mkdir -p $VOLUME
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
--hostname $NAME \
--name $NAME \
--volume "$VOLUME":/var/lib/mysql \
--env MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASS \
--env PUID=$PUID \
--env PGID=$PGID \
-p 3306:3306 \
--detach \
--restart always \
mysql:8.0
```
如脚本所述:
- 使用官方的8.0镜像启动Docker
- 退出后自动重启
- 暴露3306端口到本机
- 设置Volume盘到~/docker_data/mysql路径下
- root密码123456(请务必更改为安全密码)
执行后的效果:
```bash
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
feb2838197a6 mysql:8.0 "docker-entrypoint.s…" 46 hours ago Up 7 hours 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp mysql
```
启动成功后,我们尝试连接数据库,新建库并授权给用户:
```bash
mysql -h 127.0.0.1 -u root -p
> CREATE DATABASE homs_demo;
> CREATE USER 'HomsDemo'@'%' identified by '123456';
> GRANT ALL PRIVILEGES ON homs_demo.* TO 'HomsDemo'@'%';
```
尝试用新用户登录:
```bash
mysql -h 127.0.0.1 -u HomsDemo -p homs_demo
```
若能成功登录,我们创建本书实验所需的表:
```sql
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
这里我们创建了表users,有两个列:id和name。
温馨提示:我们使用utf8mb4字符集,如果用utf8是会有坑,可以参考[这篇文章]([掘金](https://adamhooper.medium.com/in-mysql-never-use-utf8-use-utf8mb4-11761243e434))。强烈推荐你对所有的数据表,都设置为utf8mb4。
## Spring Boot 集成 JDBC操作MySQL
我们先通过集成jdbc的方式操作MySQL数据库。
首先在server项目的build.gradle中添加依赖
```groovy
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'mysql:mysql-connector-java:8.0.20'
```
上述依赖中:
- spring-boot-starter-jdbc是集成jdbc的starter依赖包
- mysql-connector-java是集成MySQL的驱动
接着,我们配置下数据源:
```yaml
spring.datasource:
url: jdbc:mysql://127.0.0.1:3306/homs_demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: HomsDemo
password: 123456
hikari:
minimumIdle: 10
maximumPoolSize: 100
```
上述配置分为两部分:
- spring.datasource.url / username / password定义了MySQL的访问链接
- hikari是数据库连接池的配置。
Hikari是Spring Boot 2默认的链接池,[官方性能评测优秀](https://github.com/brettwooldridge/HikariCP-benchmark)。这里我们配置了minimumIdle(最小连接数)和maximumPoolSize(最大连接数)两个选项。更多配置参数可以参考[官方文档]([GitHub - brettwooldridge/HikariCP: 光 HikariCP・A solid, high-performance, JDBC connection pool at last.](https://github.com/brettwooldridge/HikariCP#gear-configuration-knobs-baby))。
经过上述的组合配置后,对应DataSource对应的Configuration会自动激活,并注册一系列的关联Bean。
下面让我们使用它访问MySQL数据库:
```java
@Repository
public class UserRepository1Impl implements UserRepository {
@Autowired
protected NamedParameterJdbcTemplate db;
private static RowMapper ROW_MAPPER = new BeanPropertyRowMapper<>(User.class);
@Override
public Optional create(User user) {
String sql = "INSERT INTO `users`(`name`) VALUES(:name)";
SqlParameterSource param = new MapSqlParameterSource("name", user.getName());
KeyHolder holder = new GeneratedKeyHolder();
if (db.update(sql, param, holder) > 0) {
return Optional.ofNullable(holder.getKey().longValue());
} else {
return Optional.empty();
}
}
@Override
public Optional getUser(long id) {
String sql = "SELECT * FROM `users` WHERE `id` = :id";
SqlParameterSource param = new MapSqlParameterSource("id", id);
try {
return Optional.ofNullable(db.queryForObject(sql, param, ROW_MAPPER));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
@Override
public Optional getUserByName(String name) {
String sql = "SELECT * FROM `users` WHERE `name` = :name";
SqlParameterSource param = new MapSqlParameterSource("name", name);
try {
return Optional.ofNullable(db.queryForObject(sql, param, ROW_MAPPER));
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
}
```
在上面的代码中,我们自动装配了"NamedParameterJdbcTemplate",然后用它访问MySQL数据库:
- 读请求使用db.query,配合RowMapper做类型转化
- 写请求使用db.update,配合KeyHolder获取自增主键
使用JDBC访问MySQL的方式,优点和缺点是完全一样的:使用显示的SQL语句操作数据库。
优点:直接、方便代码Review和性能检查
缺点:SQL编写过程繁琐、易错,特别是对于CRUD请求,效率较低
================================================
FILE: src/ch02-ms-dev1/database2.md
================================================
# Spring Boot集成SQL数据库2
## Spring Boot 集成 MyBatis操作MySQL
MyBatis是一款半自动的ORM框架。由于某国内大厂的广泛使用,MyBatis在国内非常火热(在国外其热度不如Hibernate)。
首先还是集成依赖:
```groovy
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
implementation 'mysql:mysql-connector-java:8.0.20'
```
套路与jdbc类似,但starter并不是官方的了,而是mybatis自己做的starter,感兴趣的可以来[这里](https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter/2.2.0)看下具体组成(会有惊喜)。
接下来是yaml配置环节:
```yaml
spring.datasource:
url: jdbc:mysql://127.0.0.1:3306/homs_demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: HomsDemo
password: 123456
hikari:
minimumIdle: 10
maximumPoolSize: 100
# mybatis extra
mybatis:
configuration:
map-underscore-to-camel-case: true
type-aliases-package: com.coder4.homs.demo.server.mybatis.dataobject
```
不难发现,数据库链接的定义复用了jdbc的那一套,MyBatis的定义分3行,如下:
- configuration:开启驼峰规则转化
- type-aliases-package:mapper文件存放的包名
更多MyBatis的配置选项可以参考[这里]([mybatis-spring-boot-autoconfigure – Introduction](https://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/))
接着,我们定义Mapper,在MyBatis中,Mapper相当于前面手写的Repository,定义如下:
```java
package com.coder4.homs.demo.server.mybatis.mapper;
import com.coder4.homs.demo.server.mybatis.dataobject.UserDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
*
* Mapper 接口
*
*
* @author author
* @since 2021-09-09
*/
@Repository
@Mapper
public interface UserMapper {
@Insert("INSERT INTO users(name) VALUES(#{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
long create(UserDO user);
@Select("SELECT * FROM users WHERE id = #{id}")
UserDO getUser(@Param("id") Long id);
@Select("SELECT * FROM users WHERE name = #{name}")
UserDO getUserByName(@Param("name") String name);
}
```
你可能会奇怪:这不是接口(interface)么,并没有实现?
是的,通过定义@Repository和@Mapper,MyBatis会通过运行时的切面注入,帮我们自动实现,具体执行的SQL和映射,会读取@Select、@Options等注解中的配置。
经过上述介绍,你可以发现:
MyBatis可以直接通过注解的方式快速访问数据库,(相对于JDBC的)精简了大量无用代码。
同时,MyBatis依然需要指定运行的SQL语句,这与JDBC的方式是一致的。虽然有些繁琐,但可以保证性能可控。
如果你在网上搜索"MyBatis Spring集成",会找到大量xml配置的用法。
在一些老项目中,xml是标准的集成方式。在这种配置方式下,配置繁琐、代码量大,即使借助"MyBatisX"等插件,也依然较为复杂。
因此,除非你要维护遗留的老项目代码,我都建议你使用(本文中)注解式集成MyBatis。
## Spring Boot集成 JPA 操作MySQL
JPA的全称是Java Persistence API,即持久化访问规范API。
Spring也提供了集成JPA的方案,称为 Spring Data JPA,其底层是通过Hibernate的JPA来实现的。
首先集成依赖:
```groovy
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'mysql:mysql-connector-java:8.0.20'
```
与前面类似,不再重复介绍。
接着是配置:
```yaml
# jdbc demo
spring.datasource:
url: jdbc:mysql://127.0.0.1:3306/homs_demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: HomsDemo
password: 123456
hikari:
minimumIdle: 10
maximumPoolSize: 100
# jpa demo
spring.jpa:
database-platform: org.hibernate.dialect.MySQL8Dialect
hibernate.ddl-auto: validate
```
在MySQL连接上,我们依然复用了Spring DataSource的配置。
jpa侧的配置为:
- database-platform:设置使用MySQL8语法
- hibernate.ddl-auto:只校验表,不回主动更新数据表的结构
接着,我们来定义实体(Entity):
```java
@Entity
@Data
@Table(name = "users")
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
// @Column(name = "name")
private String name;
public User toUser() {
User user = new User();
user.setId(id);
user.setName(name);
return user;
}
}
```
这里我们将UserEntity与表"users"做了关联。
接下来是Repository:
```java
@Repository
public interface UserJPARepository extends CrudRepository {
Collection findByName(String name);
}
```
我们继承了CrudRepository,他会自动生成针对UserEntity的CRUD操作。
此外,我们还定义了1个额外函数:
- findByName,通过隐士语法规则,让JPA自动帮我们生成对应SQL
从直观感受上,JPA比MyBatis更加“高级” -- 一些简单的SQL都不用写了。
但天下真的有免费的馅饼么?我们先卖个关子。
## JMJ应该选哪个
经过这两节的介绍,你已经掌握了JDBC、MyBatis、JPA三种操作数据库的方式。
在实战中,究竟要选哪个呢?
从易用性的角度来评估,我们可以得出结论:JPA > MyBatis > JDBC
那么从性能的角度来看呢?
我们使用wrk做了(get-by-id接口的)简单压测,结论如下:
| | 读QPS |
| ------- | ---- |
| JDBC | 457 |
| MyBatis | 445 |
| JPA | 114 |
这里,你会惊讶的发现:
- JDBC和MyBatis的性能差别不大,在5%以内
- JPA(Hibernate)的性能,居然只有其余两种方式的1/3
如此差的性能,真的让人百思不得其解,我尝试打印了SQL和执行耗时,并没有发现什么异常。
更进一步的,我们尝试用指定SQL的方式,替换了自动生成的接口,如下
```java
@Repository
public interface UserJPARepository extends CrudRepository {
@Query(value = "SELECT * FROM users WHERE id = :id", nativeQuery = true)
Optional findByIdFast(@Param("id") long id);
}
```
这次的压测结果是:447,性能基本和JDBC持平了。但是这种NativeSQL的用法并没有使用自动生成SQL的功能,没有发挥Hibernate本来的功效。
所以,我们认为,锅在于Hibernate自动生成SQL的逻辑耗时过大。
当然,Hibernate也不是一无是处,针对多层关联,建模复杂的场景,使用Entity做映射,会更加方便。
让我们回到前面的问题上:JMJ应该选哪个?
- 如果对性能有极致要求,建议JDBC或者MyBatis。
- 如果建模场景复杂,嵌套密集,且对性能要求不高,可以选用Hibernate。
================================================
FILE: src/ch02-ms-dev1/gradle.md
================================================
# Gradle构建工具配置
构建工具解决了依赖管理、打包流程、项目结构工程化等问题,是现代软件开发中的必备工具。
Gradle是一款Java开发语言的构建工具,兼容POM以来,使用Groovy作为描述语言,构建速度快、可拓展性强,是大量项目的首选。
在本节中,我们将介绍Gradle的基本用法与配置。
## Gradle的下载与安装
我们使用稳定版7.2,你可以在[官网](https://gradle.org/releases/)下载二进制版本。
解压缩后,需要将二进制目录加入你的PATH路径:
```shell
export PATH=$PATH:HOME/soft/gradle/bin/
```
然后执行gradle,查看是否安装成功
```shell
gradle -v
------------------------------------------------------------
Gradle 7.2
------------------------------------------------------------
Build time: 2021-08-17 09:59:03 UTC
Revision: a773786b58bb28710e3dc96c4d1a7063628952ad
Kotlin: 1.5.21
Groovy: 3.0.8
Ant: Apache Ant(TM) version 1.10.9 compiled on September 27 2020
JVM: 1.8.0_291 (Oracle Corporation 25.291-b10)
OS: Mac OS X 10.16 x86_64
```
## 修改Gradle的Maven仓库镜像
gradle的依赖使用了Maven的仓库。由于众所周知的原因,这些仓库在国内的速度并不稳定,我们需要将仓库切换成国内镜像。
修改~/.gradle/init.gradle文件如下:
```
// project
allprojects{
repositories {
mavenLocal()
maven { url 'https://maven.aliyun.com/repository/public/' }
maven { url 'https://maven.aliyun.com/repository/jcenter/' }
maven { url 'https://maven.aliyun.com/repository/google/' }
maven { url 'https://maven.aliyun.com/repository/gradle-plugin/' }
maven { url 'https://jitpack.io/' }
}
}
// plugin
settingsEvaluated { settings ->
settings.pluginManagement {
// Clear repositories collection
repositories.clear()
// Add my Artifactory mirror
repositories {
mavenLocal()
maven {
url "https://maven.aliyun.com/repository/gradle-plugin/"
}
}
}
}
```
解释下文件配置:
- 上半部分:将maven中央仓库、jcenter仓库都修改为国内镜像(阿里云),并增加了jitpack仓库(后续章节会使用)。
- 下半部分:将gradle插件仓库修改为国内镜像,这部分是必须的,不要忘记。
我们可以通过一个简单的脚本,检查配置是否生效
验证脚本build.gradle
```groovy
task listrepos {
doLast {
println "Repositories:"
project.repositories.each { println "Name: " + it.name + "; url: " + it.url }
}
}
```
执行验证:
```
gradle listrepos
Repositories:
Name: MavenLocal; url: file:/Users/coder4/.m2/repository/
Name: maven; url: https://maven.aliyun.com/repository/public/
Name: maven2; url: https://maven.aliyun.com/repository/jcenter/
Name: maven3; url: https://maven.aliyun.com/repository/google/
Name: maven4; url: https://maven.aliyun.com/repository/gradle-plugin/
Name: maven5; url: https://jitpack.io/
IntelliJ
```
## gradle-wrapper生成
gradle-wrapper是用于执行gradle的脚本 + 精简版的gradle二进制文件。
既然已经有了gradle,为什么还要单独弄一个wrapper出来么?
- 方便没有安装gradle的环境执行构建(例如打包机)
- 支持多版本gradle的快速切换(实现nvm的效果)
初始化gradle项目时,执行如下命令:
```shell
gradle init
```
gradle会生成如下wrapper相关文件:
```shell
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
```
建议将上述文件一并加入git仓库中,以防出现版本兼容问题。
## IntelliJ IDEA中配置Gradle
IntelliJ IDEA是一款功能强大的IDE,是许多Java程序员的首选。
IDEA默认支持Gradle,请确保配置正确:

上方的Gradle配置文件默认路径,请维持默认配置,使用家目录下默认的。
下方的Gradle版本,推荐使用默认选项(gradle-wrapper.properties),即使用项目路径下gradle-wrapper.properties指定的版本。
经过上述配置,我们已经搭建了Gradle的构建环境。在下一节,我们会在此基础上集成Spring Boot框架。
================================================
FILE: src/ch02-ms-dev1/redis.md
================================================
# Spring Boot集成Redis内存数据库
常规的业务数据,一般选择存储在SQL数据库中。
传统的SQL数据库基于磁盘存储,可以正常的流量需求。然而,在高并发应用场景中容易被拖垮,导致系统崩溃。
针对这种情况,我们可以通过增加缓存、使用NoSQL数据库等方式进行优化。
Redis是一款开源的内存NoSQL数据库,其稳定性高、[性能强悍]([How fast is Redis? – Redis](https://redis.io/topics/benchmarks)),是KV细分领域的[市场占有率冠军](https://db-engines.com/en/ranking/key-value+store)。
本节将介绍Redis与Spring Boot的集成方式。
## Redis环境准备
与前文类似,我们使用Docker快速部署Redis服务器。
```bash
#!/bin/bash
NAME="redis"
PUID="1000"
PGID="1000"
VOLUME="$HOME/docker_data/redis"
mkdir -p $VOLUME
docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
docker run \
--hostname $NAME \
--name $NAME \
--volume "$VOLUME":/data \
-p 6379:6379 \
--detach \
--restart always \
redis:6 \
redis-server --appendonly yes --requirepass redisdemo
```
在上述脚本中:
- 使用了最新的redis 6镜像
- 开启"appendonly"的持久化方式
- 启用密码"redisdemo"
- 端口暴露为6379
我们尝试连接一下:
```bash
redis-cli -h 127.0.0.1 -a redisdemo
```
成功!(如果你没有redis-cli的可执行文件,可以到[官网下载](https://redis.io/download))
## Redis的缓存使用
Spring提供了内置的Cache框架,可以通过@Cache注解,轻松实现redis Cache的功能。
首先引入依赖:
```groovy
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-json'
implementation 'org.apache.commons:commons-pool2:2.11.0'
```
上述依赖的作用分别为:
- redis客户端:Spring Boot 2使用的是[lettuce](http://github.com/lettuce-io/lettuce-core)
- json依赖:我们要使用jackson做json的序列化 / 反序列化
- commons-pool2线程池,这里其实是data-redis没处理好,需要额外加入,按理说应该集成在starter里的
接着我们在application.yaml中定义数据源:
```yaml
# redis demo
spring:
redis:
host: 127.0.0.1
port: 6379
password: "redisdemo"
lettuce:
pool:
max-active: 50
min-idle: 5
```
接着我们需要设置自定义的Configuration:
```java
package com.coder4.homs.demo.server.configuration;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
/**
* @author coder4
*/
@Configuration
@EnableCaching
public class RedisCacheCustomConfiguration extends CachingConfigurerSupport {
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
// sb.append(target.getClass().getName());
sb.append(target.getClass().getSimpleName());
sb.append(":");
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
sb.append(":");
}
sb.deleteCharAt(sb.length() - 1);
return sb.toString();
};
}
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
Jackson2JsonRedisSerializer