在前面的文章之中我们介绍了基于Kubernetes及Istio如何一步一步把Service Mesh微服务架构玩起来!在该文章中,我们演示了一个非常贴近实战的案例,这里回顾下该案例的结构,如下图所示:
该案例所演示的就是我们日常使用微服务架构开发时,服务间最普遍的通信场景。在Spring Cloud微服务体系中,服务间可以通过Fegin+Ribbon组合的方式,实现服务间负载均衡方式的Http接口调用;但在Service Mesh架构中,服务发现及负载均衡等治理逻辑已经由SideCar代理,如果还希望延续Spring Cloud场景下服务间接口调用的代码体验,一般可以通过改写Feign组件,去掉其中关于服务治理的逻辑,只保留简单的接口声明式调用逻辑来实现。
上述案例中“micro-api->micro-order”之间的服务通信调用,就是基于该方式实现的(可参考之前的文章)。但在微服务架构中除了采用Http协议通信外,对于某些对性能有着更高要求的系统来说,采用通信效率更高的RPC协议往往是更合适的选择!
在基于Spring Cloud框架的微服务体系中,服务之间也可以通过RPC协议通信,但由于服务治理的需要,也需要一套类似于Fegin+Ribbon组合的SDK支持。例如gRPC框架就有针对Spring Boot框架的“grpc-client-spring-boot-starter”依赖支持!该项目是一个 gRPC 的 Spring Boot 模块,可以在 Spring Boot 中内嵌一个 gRPC Server 对外提供服务,并支持 Spring Cloud 的服务发现、注册、链路跟踪等等。
那么在Service Mesh微服务体系下,服务间基于gRPC框架的通信应该怎么实现呢?接下来,我将以案例中"micro-order->micro-pay"之间的服务调用为例,演示在Service Mesh微服务架构下实现服务间的gRPC通信调用,并将案例中Http+gRPC服务间通信的完整场景串起来!
gRPC概述
在演示Service Mesh微服务架构下的gRPC通信场景之前,我们先简单介绍下RPC协议及gRPC框架的基本知识。
RPC(Remote Procedure Call),又称远程过程调用,是一种通过掩藏底层网络通信复杂性,从而屏蔽远程和本地调用区别的通信方式。相比于Http协议,RPC协议属于一种自定义的TCP协议,从而在实现时避免了一些Http协议信息的臃肿问题,实现了更高效率的通信。
在主流实现RPC协议的框架中,比较著名的有Dubbo、Thrift及gRPC等。因为目前主流的容器发布平台Kubernetes,以及Service Mesh开源平台Istio都是通过gRPC协议来实现内部组件之间的交互,所以在Service Mesh微服务架构中,服务间通信采用gRPC协议,从某种角度上说会更具有原生优势。况且在此之前,gRPC框架已经在分布式、多语言服务场景中得到了大量应用,因此可以预测在Service Mesh微服务架构场景下,基于gRPC框架的微服务通信方式会逐步成为主流。
gRPC是Google发布的基于HTTP/2.0传输层协议承载的高性能开源软件框架,提供了支持多种编程语言的、对网络设备进行配置和纳管的方法。由于是开源框架,通信的双方可以进行二次开发,所以客户端和服务器端之间的通信会更加专注于业务层面的内容,减少了对由gRPC框架实现的底层通信的关注。
接下来的内容就具体演示在Service Mesh微服务架构下,实现微服务“micro-order->micro-pay”的gRPC通信调用!
构建gRPC服务端程序(micro-pay)
首先从gRPC服务端的角度,在微服务micro-pay项目中集成gRPC-Java,并实现一个gRPC服务端程序。具体如下:
1、构建Spring Boot基本工程(micro-pay/micro-pay-client)
使用Spring Boot框架构建基本的Maven工程,为了工程代码的复用,这里单独抽象一个micro-pay-client工程,并定义micro-pay微服务gRPC服务接口的protobuf文件(*/proto/paycore.proto),代码如下:
syntax = "proto3";
package com.wudimanong.pay.client;
option java_multiple_files = true;
option java_package = "com.wudimanong.micro.pay.proto";
service PayService {
//定义支付rpc方法
rpc doPay (PayRequest) returns (PayResponse);
}
message PayRequest {
string orderId = 1;
int32 amount=2;
}
message PayResponse {
int32 status = 1;
}
如上所示,创建了一个基于protobuf协议的支付接口定义文件,其中定义了支付服务PayService及其中的doPay支付rpc方法,并定义了其请求和返回参数对象,具体的语法遵循“proto3”协议。
为了能够正常编译和生成protobuf文件所定义服务接口的代码,需要在项目pom.xml文件中引入jar包依赖及Maven编译插件配置,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
....
<dependencies>
....
<!--gRPC通信类库(截止目前的最新版本)-->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-all</artifactId>
<version>1.36.1</version>
</dependency>
</dependencies>
<build>
<!--引入gRpc框架proto文件编译生产插件-->
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.12.0:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.36.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
这是单独关于gRPC接口proto文件定义的工程,定义后编译工程,maven就会根据前面定义的paycore.proto文件生成gRPC服务端/客户端相关代码。
完成后,继续构建micro-pay微服务的spring boot工程代码,并在其pom.xml文件中引入上述gRPC协议文件定义的依赖,例如:
<!--引入支付服务gRPC ProtoBuf定义依赖-->
<dependency>
<groupId>com.wudimanong</groupId>
<artifactId>micro-pay-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在micro-pay-client工程中所引入的gRPC相关的依赖及插件配置会自动继承至micro-pay工程!
2、编写gRPC支付服务代码
在micro-pay代码工程中创建一个PayCoreProvider接口代码,用于表示支付gRPC服务的入口(类似于Controller),其代码如下:
package com.wudimanong.micro.pay.provider;
import com.wudimanong.micro.pay.proto.PayRequest;
import com.wudimanong.micro.pay.proto.PayResponse;
import com.wudimanong.micro.pay.proto.PayServiceGrpc;
import io.grpc.stub.StreamObserver;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class PayCoreProvider extends PayServiceGrpc.PayServiceImplBase {
/**
* 实现ProtoBuf中定义的服务方法
*
* @param request
* @param responseStreamObserver
*/
@Override
public void doPay(PayRequest request, StreamObserver<PayResponse> responseStreamObserver) {
//逻辑处理(简单模拟打印日志)
log.info("处理gRPC支付处理请求,orderId->{};payAmount{}", request.getOrderId(), request.getAmount());
//构建返回对象(构建处理状态)
PayResponse response = PayResponse.newBuilder().setStatus(2).build();
//设置数据响应
responseStreamObserver.onNext(response);
responseStreamObserver.onCompleted();
}
}
上述代码所引入的一些依赖代码如PayServiceGrpc等,就是前面定义paycore.proto文件所生成的桩文件代码!由于只是简单测试,这里仅仅打印了下日志就返回了,如果涉及复杂业务还是可以按照MVC分层架构思想进行代码拆分!
3、编写gRPC与Spring Boot框架集成配置代码
在Spring Cloud微服务中集成gRPC可以通过前面提到的“grpc-client-spring-boot-starter”来实现,但目前还没有现成的支持Service Mesh架构下的集成SDK,所以这里通过手工配置定义的方式实现集成。先创建一个配置类,代码如下:
package com.wudimanong.micro.pay.config;
import com.wudimanong.micro.pay.provider.PayCoreProvider;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class GrpcServerConfiguration {
@Autowired
PayCoreProvider service;
/**
* 注入配置文件中的端口信息
*/
@Value("${grpc.server-port}")
private int port;
private Server server;
public void start() throws IOException {
// 构建服务端
log.info("Starting gRPC on port {}.", port);
server = ServerBuilder.forPort(port).addService(service).build().start();
log.info("gRPC server started, listening on {}.", port);
// 添加服务端关闭的逻辑
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down gRPC server.");
GrpcServerConfiguration.this.stop();
log.info("gRPC server shut down successfully.");
}));
}
private void stop() {
if (server != null) {
// 关闭服务端
server.shutdown();
}
}
public void block() throws InterruptedException {
if (server != null) {
// 服务端启动后直到应用关闭都处于阻塞状态,方便接收请求
server.awaitTermination();
}
}
}
如上所示,在该配置代码中,通过gRPC-Java依赖所提供的Server对象构建了gRPC服务端启动、停止、阻塞的方法,并在启动时将前面定义的服务端类通过“.addService()”方法进行了加入(可考虑封装更优雅的方式)!
为了让该配置类与Spring Boot集成,再定义一个集成类,代码如下:
package com.wudimanong.micro.pay.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
public class GrpcCommandLineRunner implements CommandLineRunner {
@Autowired
GrpcServerConfiguration configuration;
@Override
public void run(String... args) throws Exception {
configuration.start();
configuration.block();
}
}
上述代码会在Spring Boot应用启动时自动加载,其中的逻辑就是启动gRPC服务,并阻塞等待连接!
接下来在配置文件中定义服务所开启的gRPC端口,配置如下:
spring:
application:
name: micro-pay
server:
port: 9092
#定义gRPC服务开放的端口
grpc:
server-port: 18888
该配置所定义的参数在前面的服务配置类中引用,表示gRPC服务开启的端口,这里定义的是18888!
到这里gRPC服务端工程代码就构建完成了,从整体上看就是Spring Boot+gRPC的集成与整合,这其中没有引入Spring Boot定制的gRPC集成SDK,目的在于避免其中所涉及的客户端服务治理逻辑(与前面Http调用不直接引入Open Feign一样)。
构建gRPC客户端程序(micro-order)
接下来我们改造micro-order微服务,使其成为调用micro-pay微服务的gRPC客户端程序!
1、引入gRPC客户端依赖包
引入前面定义micro-pay gRPC服务时构建的micro-pay-client protobuf工程依赖,代码如下:
<!--引入支付服务gRPC ProtoBuf定义依赖-->
<dependency>
<groupId>com.wudimanong</groupId>
<artifactId>micro-pay-client</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2、业务逻辑中实现gRPC服务调用
接下来在micro-order逻辑中调用gRPC支付服务,代码示例如下:
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
/**
* 引入gRPC客户端配置依赖
*/
@Autowired
GrpcClientConfiguration gRpcClent;
@Override
public CreateOrderBO create(CreateOrderDTO createOrderDTO) {
log.info("现在开始处理下单请求.....");
//生成订单号
String orderId = String.valueOf(new Random(100).nextInt(100000) + System.currentTimeMillis());
//构建支付请求(gRPC调用)
PayRequest payRequest = PayRequest.newBuilder().setOrderId(orderId).setAmount(createOrderDTO.getAmount())
.build();
//使用stub发送请求到服务端
PayResponse payResponse = gRpcClent.getStub().doPay(payRequest);
log.info("pay gRpc response->" + payResponse.toString());
return CreateOrderBO.builder().orderId(orderId).status(payResponse.getStatus()).build();
}
}
如上所示,该业务逻辑在接收micro-api通过Http调用的请求后,会在逻辑实现过程中通过gRPC协议访问支付服务,其中涉及的接口定义代码,由protobuf文件所定义!
3、gRPC客户端配置
上述逻辑是通过定义“GrpcClientConfiguration”gRPC客户端配置类来实现gRPC服务调用的,该配置类代码如下:
@Slf4j
@Component
public class GrpcClientConfiguration {
/**
* 支付gRPC Server的地址
*/
@Value("${server-host}")
private String host;
/**
* 支付gRPC Server的端口
*/
@Value("${server-port}")
private int port;
private ManagedChannel channel;
/**
* 支付服务stub对象
*/
private PayServiceGrpc.PayServiceBlockingStub stub;
public void start() {
//开启channel
channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
//通过channel获取到服务端的stub
stub = PayServiceGrpc.newBlockingStub(channel);
log.info("gRPC client started, server address: {}:{}", host, port);
}
public void shutdown() throws InterruptedException {
//调用shutdown方法后等待1秒关闭channel
channel.shutdown().awaitTermination(1, TimeUnit.SECONDS);
log.info("gRPC client shut down successfully.");
}
public PayServiceGrpc.PayServiceBlockingStub getStub() {
return this.stub;
}
}
如上所示配置代码,通过依服务配置文件指定的gRPC服务端地址+端口,实现对gRPC客户端的配置,其中主要包括启动和停止方法,并在启动的过程中初始化gRPC服务客户端的桩代码的实例(可考虑更优雅地实现)。
在该配置类中所依赖的gRPC服务端地址+端口配置,依赖于服务配置文件的定义,代码如下:
spring:
application:
name: micro-order
server:
port: 9091
#支付微服务Grpc服务地址、端口配置
server-host: ${grpc_server_host}
server-port: ${grpc_server_port}
如果是本地测试可以直接指定grpc_server_host及端口的值,但在Service Mesh微服务架构中,直接在应用的配置文件中指定其他微服务的地址及端口可能并不是很灵活,这个配置信息将在发布Kubernetes集群时,通过Kubernetes发布文件注入!
为了让gRPC客户端配置与Spring Boot集成,这里也需要定义一个Spring Boot加载类,代码如下:
@Component
@Slf4j
public class GrpcClientCommandLineRunner implements CommandLineRunner {
@Autowired
GrpcClientConfiguration configuration;
@Override
public void run(String... args) throws Exception {
//开启gRPC客户端
configuration.start();
//添加客户端关闭的逻辑
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
configuration.shutdown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
}
}
该代码将在Spring Boot应用自动时自动加载!到这里micro-order gRPC客户端配置就完成了!
将部署服务至Service Mesh架构环境
前面基于“micro-order->micro-pay”微服务间的gRPC调用场景,分别将两个微服务改造成了gRPC服务端/客户端。但此时从代码上是很难看出来它们二者之间应该怎么实现调用!而这也恰恰就印证了Service Mesh架构的优势,服务的发现、及负载均衡调用之类的服务治理逻辑,已经完全不用微服务自己管了!
在Istio中,它们是基于Kubernetes的Service发现机制+Istio-proxy(SideCar代理)来实现的。而具体的操作就是通过微服务Kubernetes服务发布文件的定义,接下来分别定义micro-order及micro-pay的Kubernetes发布文件。
先看下作为gRPC服务端的micro-pay的发布文件(micro-pay.yaml),代码如下:
apiVersion: v1
kind: Service
metadata:
name: micro-pay
labels:
app: micro-pay
service: micro-pay
spec:
type: ClusterIP
ports:
- name: http
#容器暴露端口
port: 19092
#目标应用端口
targetPort: 9092
#设置gRPC端口
- name: grpc
port: 18888
targetPort: 18888
selector:
app: micro-pay
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-pay-v1
labels:
app: micro-pay
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: micro-pay
version: v1
template:
metadata:
labels:
app: micro-pay
version: v1
spec:
containers:
- name: micro-pay
image: 10.211.55.2:8080/micro-service/micro-pay:1.0-SNAPSHOT
imagePullPolicy: Always
tty: true
ports:
- name: http
protocol: TCP
containerPort: 19092
#指定服务gRPC端口
- name: grpc
protocol: TCP
containerPort: 18888
如上所示k8s发布文件,主要是定义了Service服务访问资源及Deployment容器编排资源,这两种资源都是Kubernetes的资源类型,在容器编排资源和服务资源中分别定义了gRPC的访问端口,通过这种设置,后续gRPC客户端通过Service资源访问服务时,就能够进行端口映射了!
而其他配置则是基本的Kubernetes发布部署逻辑,其中涉及的镜像,需要在发布之前,通过构建的方式对项目进行Docker镜像打包并上传私有镜像仓库(如果有疑问,可以参考本号之前的文章)。
接下来继续看看作为gRPC客户端的micro-order微服务的k8s发布文件(micro-order.yaml),代码如下:
apiVersion: v1
kind: Service
metadata:
name: micro-order
labels:
app: micro-order
service: micro-order
spec:
type: ClusterIP
ports:
- name: http
#此处设置80端口的原因在于改造的Mock FeignClient代码默认是基于80端口进行服务调用
port: 80
targetPort: 9091
selector:
app: micro-order
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: micro-order-v1
labels:
app: micro-order
version: v1
spec:
replicas: 2
selector:
matchLabels:
app: micro-order
version: v1
template:
metadata:
labels:
app: micro-order
version: v1
spec:
containers:
- name: micro-order
image: 10.211.55.2:8080/micro-service/micro-order:1.0-SNAPSHOT
imagePullPolicy: Always
tty: true
ports:
- name: http
protocol: TCP
containerPort: 19091
#环境参数设置(设置微服务返回gRPC服务端的地址+端口)
env:
- name: GRPC_SERVER_HOST
value: micro-pay
- name: GRPC_SERVER_PORT
value: "18888"
在该发布文件中,需要说明的主要就是通过容器env环境参数的设置,指定了之前gRPC客户端服务配置中所依赖的参数变量“GRPC_SERVER_HOST及GRPC_SERVER_PORT”,其中服务地址就是micro-pay微服务在Kubernetes中Service资源定义的名称,端口则是gRPC服务端所开启的端口。
这样在gRPC客户端在Kubernetes集群中根据Service名称发起微服务调用时,Kubernetes集群自身的服务发现逻辑就能自动将请求映射到相应的Pod资源了!这其实就是Service Mesh微服务架构服务发现的基本逻辑!
接下来将微服务进行发布,这里假设你已经部署了一套Kubernetes集群并安装了基于Istio的Service Mesh微服务架构环境,最终的部署效果如下所示:
root@kubernetes:/opt/istio/istio-1.8.4# kubectl get pods
NAME READY STATUS RESTARTS AGE
micro-api-6455654996-9lsxr 2/2 Running 2 43m
micro-order-v1-744d469d84-rnqq8 2/2 Running 0 6m28s
micro-order-v1-744d469d84-vsn5m 2/2 Running 0 6m28s
micro-pay-v1-7fd5dd4768-txq9d 2/2 Running 0 43s
micro-pay-v1-7fd5dd4768-wqw6b 2/2 Running 0 43s
如上所示,可以看到案例所涉及的微服务都被部署了,并且对应的SideCar代理(istio-proxy)也被正常启动了!为了演示负载均衡效果,这里micro-order及micro-pay都分别被部署了两个副本!
微服务多副本负载均衡调用演示
如果环境都没啥问题,此时可以通过调用Istio Gateway来访问micro-api服务,然后micro-api服务会通过Http的方式访问micro-order服务,之后micro-order服务通过gRPC协议调用micro-pay服务。
通过curl命令访问Istio Gateway网关服务,效果如下:
curl -H "Content-Type:application/json" -H "Data_Type:msg" -X POST --data '{"businessId": "202012102", "amount": 100, "channel": 2}' http://10.211.55.12:30844/api/order/create
如果正常返回响应结果,则说明上述调用链路走通了!此时分别通过观察服务的业务日志和istio-proxy代理日志来加以观测!
其中micro-pay两个实例(PodA~PodB)业务日志信息:
//支付微服务接口访问日志(POD-A)
root@kubernetes:~# kubectl logs micro-pay-v1-7fd5dd4768-txq9d micro-pay
....
2021-04-01 14:46:15.818 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : Starting gRPC on port 18888.
2021-04-01 14:46:18.859 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : gRPC server started, listening on 18888.
2021-04-01 15:07:36.709 INFO 1 --- [ault-executor-0] c.w.micro.pay.provider.PayCoreProvider : 处理gRPC支付处理请求,orderId->1617289656289;payAmount100
//支付微服务接口访问日志(POD-B)
root@kubernetes:~# kubectl logs micro-pay-v1-7fd5dd4768-wqw6b micro-pay
...
2021-04-01 15:34:59.673 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : Starting gRPC on port 18888.
2021-04-01 15:35:06.175 INFO 1 --- [ main] c.w.m.p.config.GrpcServerConfiguration : gRPC server started, listening on 18888.
2021-04-01 15:40:22.019 INFO 1 --- [ault-executor-0] c.w.micro.pay.provider.PayCoreProvider : 处理gRPC支付处理请求,orderId->1617291624127;payAmount100
2021-04-01 15:44:31.630 INFO 1 --- [ault-executor-2] c.w.micro.pay.provider.PayCoreProvider : 处理gRPC支付处理请求,orderId->1617291867537;payAmount100
可以看到,多次访问接口,基于gRPC的微服务调用也实现了负载均衡调用!接下来分别看下这两个微服务的istio-proxy(SideCar代理)的日志,具体如下:
--istio-proxy代理日志(POD-A)
root@kubernetes:~# kubectl logs micro-pay-v1-7fd5dd4768-txq9d istio-proxy
...
2021-04-01T15:34:48.009972Z info Envoy proxy is ready
[2021-04-01T15:40:26.240Z] "POST /com.wudimanong.pay.client.PayService/doPay HTTP/2" 200 - "-" 22 7 498 477 "-" "grpc-java-netty/1.36.1" "8eb318e5-ac09-922d-9ca7-603a5c14bdd5" "micro-pay:18888" "127.0.0.1:18888" inbound|18888|| 127.0.0.1:57506 10.32.0.10:18888 10.32.0.12:36844 outbound_.18888_._.micro-pay.default.svc.cluster.local default
2021-04-01T15:45:18.377555Z info xdsproxy disconnected
...
[2021-04-01T15:45:34.885Z] "POST /com.wudimanong.pay.client.PayService/doPay HTTP/2" 200 - "-" 22 7 1200 171 "-" "grpc-java-netty/1.36.1" "c08d540e-db46-9228-b381-0808ac08377e" "micro-pay:18888" "127.0.0.1:18888" inbound|18888|| 127.0.0.1:33218 10.32.0.10:18888 10.32.0.2:42646 outbound_.18888_._.micro-pay.default.svc.cluster.local default
...
2021-04-01T15:52:49.825955Z info xdsproxy connecting to upstream XDS server: istiod.istio-system.svc:15012
如上所示,可以看到istio-proxy代理日志中显示了通过post方式转发gRPC服务的情况,而且可以看出gRRPC是采用Http/2实现的!
后记
本文通过实战案例,演示了在Service Mesh微服务架构下,服务间通过gRPC协议实现通信调用的场景!
欢迎大家关注我的公众号【风平浪静如码】,海量Java相关文章,学习资料都会在里面更新,整理的资料也会放在里面。
觉得写的还不错的就点个赞,加个关注呗!点关注,不迷路,持续更新!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。